WebSocket 双端实践(iOS/ Golang)

_647_· 2020-02-13

上一篇:《今天我们来聊一聊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的实现原理》