WebSocket 双端实践(iOS/ Golang)
上一篇:《今天我们来聊一聊WebSocket》
主要介绍了WebSocket的原理、应用场景等等。
本篇将介绍WebSocket的双端实战(
Client
、Server
)。
分为两部分:
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的
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后台保活
奇舞周刊