WebSocket 双端实践(iOS/ Golang)

级别:★★☆☆☆

标签:「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 就算完成了。

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

二、使用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 {
        return errors.New("send iframe data error")
    }
    lenth := len(data)
    maskedData := make([]byte, lenth)
    for i := 0; i > 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 = 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 接口,即可调用长链服务。

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

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

关注我们的途径有:

QiShare(简书)

QiShare(掘金)

QiShare(知乎)

QiShare(GitHub)

QiShare(CocoaChina)

QiShare(StackOverflow)

QiShare(微信公众号)