美文网首页QiShare文章汇总
WebSocket 双端实践(iOS/ Golang)

WebSocket 双端实践(iOS/ Golang)

作者: QiShare | 来源:发表于2020-02-13 16:37 被阅读0次

    级别:★★☆☆☆
    标签:「WebSocket」「Starscream」「Golang」
    作者: 647
    审校: 沐灵洛


    上一篇:《今天我们来聊一聊WebSocket》
    主要介绍了WebSocket的原理、应用场景等等。

    本篇将介绍WebSocket的双端实战(ClientServer)。
    分为两部分:
    1.Client:使用Starscream(swift)完成客户端长链需求。
    2.Server:使用Golang完成服务端长链需求。

    一、使用Starscream(swift)完成客户端长链需求

    首先附上Starscream:GitHub地址

    第一步:将Starsream导入到项目。

    打开Podfile,加上:

    pod 'Starscream', '~> 4.0.0'
    

    接着pod install

    第二步:实现WebSocket能力。

    • 导入头文件,import Starscream

    • 初始化WebSocket,把一些请求头包装一下(与服务端对好)

    private func initWebSocket() {
        // 包装请求头
        var request = URLRequest(url: URL(string: "ws://127.0.0.1:8000/chat")!)
        request.timeoutInterval = 5 // Sets the timeout for the connection
        request.setValue("some message", forHTTPHeaderField: "Qi-WebSocket-Header")
        request.setValue("some message", forHTTPHeaderField: "Qi-WebSocket-Protocol")
        request.setValue("0.0.1", forHTTPHeaderField: "Qi-WebSocket-Version")
        request.setValue("some message", forHTTPHeaderField: "Qi-WebSocket-Protocol-2")
        socketManager = WebSocket(request: request)
        socketManager?.delegate = self
    }
    

    同时,我用三个Button的点击事件,分别模拟了connect(连接)、write(通信)、disconnect(断开)。

        // Mark - Actions
        // 连接
        @objc func connetButtonClicked() {
            socketManager?.connect()
        }
        // 通信
        @objc func sendButtonClicked() {
            socketManager?.write(string: "some message.")
        }
        // 断开
        @objc func closeButtonCliked() {
            socketManager?.disconnect()
        }
    

    第三步:实现WebSocket回调方法(接收服务端消息)

    遵守并实现WebSocketDelegate

    extension ViewController: WebSocketDelegate {
        // 通信(与服务端协商好)
        func didReceive(event: WebSocketEvent, client: WebSocket) {
            switch event {
            case .connected(let headers):
                isConnected = true
                print("websocket is connected: \(headers)")
            case .disconnected(let reason, let code):
                isConnected = false
                print("websocket is disconnected: \(reason) with code: \(code)")
            case .text(let string):
                print("Received text: \(string)")
            case .binary(let data):
                print("Received data: \(data.count)")
            case .ping(_):
                break
            case .pong(_):
                break
            case .viablityChanged(_):
                break
            case .reconnectSuggested(_):
                break
            case .cancelled:
                isConnected = false
            case .error(let error):
                isConnected = false
                // ...处理异常错误
                print("Received data: \(String(describing: error))")
            }
        }
    }
    

    分别对应的是:

    public enum WebSocketEvent {
        case connected([String: String])  //!< 连接成功
        case disconnected(String, UInt16) //!< 连接断开
        case text(String)                 //!< string通信
        case binary(Data)                 //!< data通信
        case pong(Data?)                  //!< 处理pong包(保活)
        case ping(Data?)                  //!< 处理ping包(保活)
        case error(Error?)                //!< 错误
        case viablityChanged(Bool)        //!< 可行性改变
        case reconnectSuggested(Bool)     //!< 重新连接
        case cancelled                    //!< 已取消
    }
    

    这样一个简单的客户端WebSocket demo就算完成了。

    • 客户端成功,日志截图:

    Demo源码


    二、使用Golang完成简单服务端长链需求

    仅仅有客户端也无法验证WebSocket的能力。
    因此,接下来我们用Golang简单做一个本地的服务端WebSocket服务。

    PS:最近,正好在学习Golang,参考了一些大神的作品。

    直接上代码了:

    package main
    
    import (
        "crypto/sha1"
        "encoding/base64"
        "errors"
        "io"
        "log"
        "net"
        "strings"
    )
    
    func main() {
        ln, err := net.Listen("tcp", ":8000")
        if err != nil {
            log.Panic(err)
        }
        for {
            log.Println("wss")
            conn, err := ln.Accept()
            if err != nil {
                log.Println("Accept err:", err)
            }
            for {
                handleConnection(conn)
            }
        }
    }
    
    func handleConnection(conn net.Conn) {
        content := make([]byte, 1024)
        _, err := conn.Read(content)
        log.Println(string(content))
        if err != nil {
            log.Println(err)
        }
        isHttp := false
        // 先暂时这么判断
        if string(content[0:3]) == "GET" {
            isHttp = true
        }
        log.Println("isHttp:", isHttp)
        if isHttp {
            headers := parseHandshake(string(content))
            log.Println("headers", headers)
            secWebsocketKey := headers["Sec-WebSocket-Key"]
            // NOTE:这里省略其他的验证
            guid := "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
            // 计算Sec-WebSocket-Accept
            h := sha1.New()
            log.Println("accept raw:", secWebsocketKey+guid)
            io.WriteString(h, secWebsocketKey+guid)
            accept := make([]byte, 28)
            base64.StdEncoding.Encode(accept, h.Sum(nil))
            log.Println(string(accept))
            response := "HTTP/1.1 101 Switching Protocols\r\n"
            response = response + "Sec-WebSocket-Accept: " + string(accept) + "\r\n"
            response = response + "Connection: Upgrade\r\n"
            response = response + "Upgrade: websocket\r\n\r\n"
            log.Println("response:", response)
            if lenth, err := conn.Write([]byte(response)); err != nil {
                log.Println(err)
            } else {
                log.Println("send len:", lenth)
            }
            wssocket := NewWsSocket(conn)
            for {
                data, err := wssocket.ReadIframe()
                if err != nil {
                    log.Println("readIframe err:", err)
                }
                log.Println("read data:", string(data))
                err = wssocket.SendIframe([]byte("good"))
                if err != nil {
                    log.Println("sendIframe err:", err)
                }
                log.Println("send data")
            }
        } else {
            log.Println(string(content))
            // 直接读取
        }
    }
    
    type WsSocket struct {
        MaskingKey []byte
        Conn       net.Conn
    }
    
    func NewWsSocket(conn net.Conn) *WsSocket {
        return &WsSocket{Conn: conn}
    }
    
    func (this *WsSocket) SendIframe(data []byte) error {
        // 这里只处理data长度<125的
        if len(data) >= 125 {
            return errors.New("send iframe data error")
        }
        lenth := len(data)
        maskedData := make([]byte, lenth)
        for i := 0; i < lenth; i++ {
            if this.MaskingKey != nil {
                maskedData[i] = data[i] ^ this.MaskingKey[i%4]
            } else {
                maskedData[i] = data[i]
            }
        }
        this.Conn.Write([]byte{0x81})
        var payLenByte byte
        if this.MaskingKey != nil && len(this.MaskingKey) != 4 {
            payLenByte = byte(0x80) | byte(lenth)
            this.Conn.Write([]byte{payLenByte})
            this.Conn.Write(this.MaskingKey)
        } else {
            payLenByte = byte(0x00) | byte(lenth)
            this.Conn.Write([]byte{payLenByte})
        }
        this.Conn.Write(data)
        return nil
    }
    
    func (this *WsSocket) ReadIframe() (data []byte, err error) {
        err = nil
        //第一个字节:FIN + RSV1-3 + OPCODE
        opcodeByte := make([]byte, 1)
        this.Conn.Read(opcodeByte)
        FIN := opcodeByte[0] >> 7
        RSV1 := opcodeByte[0] >> 6 & 1
        RSV2 := opcodeByte[0] >> 5 & 1
        RSV3 := opcodeByte[0] >> 4 & 1
        OPCODE := opcodeByte[0] & 15
        log.Println(RSV1, RSV2, RSV3, OPCODE)
    
        payloadLenByte := make([]byte, 1)
        this.Conn.Read(payloadLenByte)
        payloadLen := int(payloadLenByte[0] & 0x7F)
        mask := payloadLenByte[0] >> 7
        if payloadLen == 127 {
            extendedByte := make([]byte, 8)
            this.Conn.Read(extendedByte)
        }
        maskingByte := make([]byte, 4)
        if mask == 1 {
            this.Conn.Read(maskingByte)
            this.MaskingKey = maskingByte
        }
    
        payloadDataByte := make([]byte, payloadLen)
        this.Conn.Read(payloadDataByte)
        log.Println("data:", payloadDataByte)
        dataByte := make([]byte, payloadLen)
        for i := 0; i < payloadLen; i++ {
            if mask == 1 {
                dataByte[i] = payloadDataByte[i] ^ maskingByte[i%4]
            } else {
                dataByte[i] = payloadDataByte[i]
            }
        }
        if FIN == 1 {
            data = dataByte
            return
        }
        nextData, err := this.ReadIframe()
        if err != nil {
            return
        }
        data = append(data, nextData...)
        return
    }
    
    func parseHandshake(content string) map[string]string {
        headers := make(map[string]string, 10)
        lines := strings.Split(content, "\r\n")
        for _, line := range lines {
            if len(line) >= 0 {
                words := strings.Split(line, ":")
                if len(words) == 2 {
                    headers[strings.Trim(words[0], " ")] = strings.Trim(words[1], " ")
                }
            }
        }
        return headers
    }
    

    完成后,在本地执行:

    go run WebSocket_demo.go
    

    即可开启本地服务。

    这时候访问ws://127.0.0.1:8000/chat接口,即可调用长链服务。

    • 服务端,成功日志截图:

    Demo源码


    相关参考链接:
    《微信,QQ这类IM app怎么做——谈谈Websocket》(冰霜大佬)
    《WebSocket的实现原理》


    小编微信:可加并拉入《QiShare技术交流群》。

    关注我们的途径有:
    QiShare(简书)
    QiShare(掘金)
    QiShare(知乎)
    QiShare(GitHub)
    QiShare(CocoaChina)
    QiShare(StackOverflow)
    QiShare(微信公众号)

    推荐文章:
    今天我们来聊一聊WebSocket(iOS/Golang)
    用 Swift 进行贝塞尔曲线绘制
    Swift 5.1 (11) - 方法
    Swift 5.1 (10) - 属性
    iOS App后台保活
    奇舞周刊

    相关文章

      网友评论

        本文标题:WebSocket 双端实践(iOS/ Golang)

        本文链接:https://www.haomeiwen.com/subject/jellfhtx.html