即时通讯

iOS中WebSocket的使用

2022-08-01  本文已影响0人  MambaYong

简介

HTTP协议是无状态的协议,采用的是请求/应答的模式,所以只能是客户端发送请求,服务器响应请求,服务器是无法给客户端主动推送消息的,而有时候客户端需要在服务器数据更新的时候及时的进行更新界面或者其他的逻辑处理,以前的方案是客户端通过轮询不断的发送HTTP请求到服务器来拿到服务器最新的数据,非常的麻烦。

WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

WebSocket

WebSocket在建立连接之前也是需要经过握手的,而且当初WebScoket为了兼容性,在握手的时候使用HTTP请求来完成握手,客户端发送HTTP请求,其中头部headers信息会包含如下信息:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

服务器收到了连接请求后响应如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

WebSocket Data

Websocket数据是以Frame流的形式进行传输,其格式如下:

掩码的目的是为了避免被网络代理服务器误认为是HTTP请求,从而招致代理服务器被恶意脚本攻击,采用掩码对客户端的数据进行掩码操作后,中间人代理就无法预测其数据流量,无法进行缓存,所以在WebSocket中客户端到服务器的数据是一定要经过掩码处理的。

实际应用

项目主要演示的是一个问答的App,客户端新建一个question后会通过WebSocketquestion传给服务器,服务器收到这些question后会存到数据库,当通过网页回答了此问题后,服务器会通过WebSocket主动告知客户端对应的question的回答状态,客户端会同步更新question的状态。

服务端搭建

服务端这里采用了SwiftVapor框架来搭建服务端程序,这里只贴出服务端程序的主要代码实现,我们的重点是iOS客户端的实现,在实际开发中服务端的实现也是后台开发人员所需要完成的,而且后台采用的技术栈也不是固定的。

Websocket的链接

func connect(_ ws: WebSocket) {
      let uuid = UUID()
      self.lock.withLockVoid {
        self.sockets[uuid] = ws
      }
      ws.onBinary { [weak self] ws, buffer in
        guard let self = self,
          let data = buffer.getData(
            at: buffer.readerIndex, length: buffer.readableBytes) else {
              return
        }
        self.onData(ws, data)
      }
      ws.onText { [weak self] ws, text in
        guard let self = self,
          let data = text.data(using: .utf8) else {
            return
        }
        self.onData(ws, data)
      }
      self.send(message: QnAHandshake(id: uuid), to: .socket(ws))
  }
  func onData(_ ws: WebSocket, _ data: Data) {
      let decoder = JSONDecoder()
      do {
        let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
        switch sinData.type {
        case .newQuestion:
          let newQuestionData = try decoder.decode(NewQuestionMessage.self,from: data)
          self.onNewQuestion(ws, sinData.id, newQuestionData)
        default:
          break
        }
      } catch {
        logger.report(error: error)
      }
  }
}
    func onNewQuestion(_ ws: WebSocket, _ id: UUID, _ message: NewQuestionMessage) {
        let q = Question(content: message.content, askedFrom: id)
        self.db.withConnection {
            q.save(on: $0)
        }.whenComplete { res in
            let success: Bool
            let message: String
            switch res {
            case .failure(let err):
                self.logger.report(error: err)
                success = false
                message = "Something went wrong creating the question."
            case .success:
                self.logger.info("Got a new question!")
                success = true
                message = "Question created. We will answer it as soon as possible :]"
            }
            try? self.send(message: NewQuestionResponse(
                success: success,
                message: message,
                id: q.requireID(),
                answered: q.answered,
                content: q.content,
                createdAt: q.createdAt
            ), to: .socket(ws))
        }
    }
  func send<T: Codable>(message: T, to sendOption: WebSocketSendOption) {
      logger.info("Sending \(T.self) to \(sendOption)")
      do {
        let sockets: [WebSocket] = self.lock.withLock {
          switch sendOption {
          case .id(let id):
            return [self.sockets[id]].compactMap { $0 }
          case .socket(let socket):
            return [socket]
          case .all:
            return self.sockets.values.map { $0 }
          case .ids(let ids):
            return self.sockets.filter { key, _ in ids.contains(key) }.map { $1 }
          }
        }
        let encoder = JSONEncoder()
        let data = try encoder.encode(message)
        sockets.forEach {
          $0.send(raw: data, opcode: .binary)
        }
      } catch {
        logger.report(error: error)
      }

  }
   func answer(req: Request) throws -> EventLoopFuture<Response> {
      guard let questionId = req.parameters.get("questionId"),
        let questionUid = UUID(questionId) else {
          throw Abort(.badRequest)
      }
      return Question.find(questionUid, on: req.db)
                     .unwrap(or: Abort(.notFound))
                     .flatMap { question in
        question.answered = true
        return question.save(on: req.db).flatMapThrowing {
          try self.wsController.send(message:
            QuestionAnsweredMessage(questionId: question.requireID()),
            to: .id(question.askedFrom))
          return req.redirect(to: "/")
        }
      }
    }

上面的服务端实现要熟悉大致的实现逻辑即可,不同的后台语言实现的逻辑都是一样的。

iOS客户端的实现

struct ContentView: View {

    @State var newQuestion: String = ""
    @ObservedObject var keyboard: Keyboard = .init()
    @ObservedObject var socket: WebSocketController = .init()
    
    var body: some View {
      VStack(spacing: 8) {
        Text("Your asked questions:")
        Divider()
          List(socket.questions.map { $1 }.sorted(), id: \.id) { q in
            VStack(alignment: .leading) {
              Text(q.content)
              Text("Status: \(q.answered ? "Answered" : "Unanswered")")
                .foregroundColor(q.answered ? .green : .red)
            }
          }

        Divider()
        TextField("Ask a new question", text: $newQuestion, onCommit: {
          guard !self.newQuestion.isEmpty else { return }
          self.socket.addQuestion(self.newQuestion)
          self.newQuestion = ""
        })
          .textFieldStyle(RoundedBorderTextFieldStyle())
          .padding(.horizontal)
          .edgesIgnoringSafeArea(keyboard.height > 0 ? .bottom : [])
      }
      .padding(.vertical)
      .alert(item: $socket.alertWrapper) { $0.alert }
    }

}
  func connect() {
      self.session = URLSession(configuration: .default)
      self.socket = session.webSocketTask(with:
        URL(string: "ws://localhost:8080/socket")!)
      self.listen()
      self.socket.resume()

  }
func listen() {
      self.socket.receive { [weak self] (result) in
        guard let self = self else { return }
        switch result {
        case .failure(let error):
          print(error)
          let alert = Alert(
              title: Text("Unable to connect to server!"),
              dismissButton: .default(Text("Retry")) {
                self.alert = nil
                self.socket.cancel(with: .goingAway, reason: nil)
                self.connect()
              }
          )
          self.alert = alert
          return
        case .success(let message):
          switch message {
          case .data(let data):
            self.handle(data)
          case .string(let str):
            guard let data = str.data(using: .utf8) else { return }
            self.handle(data)
          @unknown default:
            break
          }
        }
        self.listen()
      }

  }
  func addQuestion(_ content: String) {
      guard let id = self.id else { return }
      let message = NewQuestionMessage(id: id, content: content)
      do {
        let data = try encoder.encode(message)
        self.socket.send(.data(data)) { (err) in
          if err != nil {
            print(err.debugDescription)
          }
        }
      } catch {
        print(error)
      }

  }

上文我们提到过WebSocket在发送消息时当数据量较大时需要进行分片发送,同时客户端发送给服务器的数据必须利用masking-key进行掩码处理,同时发送时需要设置opcode等,这些都被URLSessionWebSocketTask在背后默默处理了。

  func handle(_ data: Data) {
      do {
        let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
        switch sinData.type {
        case .handshake:
          print("Shook the hand")
          let message = try decoder.decode(QnAHandshake.self, from: data)
          self.id = message.id
        case .questionResponse:
          try self.handleQuestionResponse(data)
        case .questionAnswer:
          try self.handleQuestionAnswer(data)
        default:
          break
        }
      } catch {
        print(error)
      }

  }
  func handleQuestionAnswer(_ data: Data) throws {
      let response = try decoder.decode(QuestionAnsweredMessage.self, from: data)
      DispatchQueue.main.async {
        guard let question = self.questions[response.questionId] else { return }
        question.answered = true
        self.questions[response.questionId] = question
      }

  }
  func handleQuestionResponse(_ data: Data) throws {
      let response = try decoder.decode(NewQuestionResponse.self, from: data)
      DispatchQueue.main.async {
        if response.success, let id = response.id {
          self.questions[id] = response
          let alert = Alert(title: Text("New question received!"),
                            message: Text(response.message),
                            dismissButton: .default(Text("OK")) { self.alert = nil })
          self.alert = alert
        } else {
          let alert = Alert(title: Text("Something went wrong!"),
                            message: Text(response.message),
                            dismissButton: .default(Text("OK")) { self.alert = nil })
          self.alert = alert
        }
      }

  }

总结

本文主要介绍了WebSocket的一些概念,同时从前后端的角度进行了实际的项目演示,iOS客户端在实现的过程中采用了原生的URLSessionWebSocketTask进行了使用,当然实际开发中我们也可以使用第三方来实现 。

上一篇下一篇

猜你喜欢

热点阅读