WebSocket 雙端實踐(iOS/ Golang)

級別:★★☆☆☆
標籤:「WebSocket」「Starscream」「Golang」
做者: 647
審校: 沐靈洛php


上一篇:《今天咱們來聊一聊WebSocket》
主要介紹了WebSocket的原理、應用場景等等。
git

本篇將介紹WebSocket的雙端實戰(ClientServer)。
分爲兩部分:
1.Client:使用Starscream(swift)完成客戶端長鏈需求。
2.Server:使用Golang完成服務端長鏈需求。github

1、使用Starscream(swift)完成客戶端長鏈需求

首先附上Starscream:GitHub地址web

第一步:將Starsream導入到項目。

打開Podfile,加上:swift

pod 'Starscream', '~> 4.0.0'
複製代碼

接着pod installvim

第二步:實現WebSocket能力。

  • 導入頭文件,import Starscreamruby

  • 初始化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(斷開)。websocket

// Mark - Actions
    // 鏈接
    @objc func connetButtonClicked() {
        socketManager?.connect()
    }
    // 通訊
    @objc func sendButtonClicked() {
        socketManager?.write(string: "some message.")
    }
    // 斷開
    @objc func closeButtonCliked() {
        socketManager?.disconnect()
    }
複製代碼

第三步:實現WebSocket回調方法(接收服務端消息)

遵照並實現WebSocketDelegateapp

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就算完成了。

  • 客戶端成功,日誌截圖:

image

Demo源碼


2、使用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接口,便可調用長鏈服務。

  • 服務端,成功日誌截圖:

image

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後臺保活
奇舞週刊

相關文章
相關標籤/搜索