級別:★★☆☆☆
標籤:「WebSocket」「Starscream」「Golang」
做者: 647
審校: 沐靈洛php
上一篇:《今天咱們來聊一聊WebSocket》
主要介紹了WebSocket的原理、應用場景等等。git
本篇將介紹WebSocket的雙端實戰(
Client
、Server
)。
分爲兩部分:
1.Client:使用Starscream(swift)
完成客戶端長鏈需求。
2.Server:使用Golang
完成服務端長鏈需求。github
首先附上Starscream:GitHub地址web
Starsream
導入到項目。打開Podfile
,加上:swift
pod 'Starscream', '~> 4.0.0'
複製代碼
接着pod install
。vim
導入頭文件,import Starscream
ruby
初始化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()
}
複製代碼
遵照並實現WebSocketDelegate
。app
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
就算完成了。
僅僅有客戶端也沒法驗證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
接口,便可調用長鏈服務。
相關參考連接:
《微信,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後臺保活
奇舞週刊