实时交互协议与实现简介
2 建立连接
建立连接的方式,维持连接的方式很多,这里我们简单介绍在需要长时间连接时的几个。
上帝与世界有一个古老而漫长的连接,
2.1 全双工协议简介
websocket使用tcp做为4层通信的协议,因此有tcp协议支持的系统都可以使用此协议,只要符合BSD socket准则
BSD 套接字(BSD sockets)是一种应用程序接口(API),用于网络套接字( socket)与Unix域套接字,包括了一个用C语言写成的应用程序开发库,主要用于实现进程间通讯,在计算机网络通讯方面被广泛使用。
Berkeley套接字(也作BSD套接字应用程序接口)刚开始是4.2BSD Unix操作系统(于1983发布)的一套应用程序接口。然而,由于AT&T的专利保护着UNIX,所以只有在1989年伯克利大学才能自由地发布自己的操作系统和网络库
创建一个socket连接需要三个参数,domain,type,protocol, 当不符合这三个参数时,函数返回-1
如果成功,函数返回一个代表新分配的描述符的整数。
该websocket协议包含基本5个事件帧,数据帧,二进制,文本,和控制帧 保活请求,保活响应,关闭,分别为 1,2,8,9,10
Text = 1
Binary = 2
Close = 8
PingM = 9
Pong = 10
不同的框架对其实现并不相同,有些提供明显的针对协议的方法,
js默认包含使用 addEventListener() 或将一个事件监听器赋值给本接口的 oneventname 属性,来监听下面的事件。 比如事件有如下几类
-
close
当一个 WebSocket 连接被关闭时触发。 也可以通过 onclose 属性来设置。
-
error
当一个 WebSocket 连接因错误而关闭时触发,例如无法发送数据时。 也可以通过 onerror 属性来设置。
-
message
当通过 WebSocket 收到数据时触发。 也可以通过 onmessage 属性来设置。
-
open
当一个 WebSocket 连接成功时触发。 也可以通过 onopen 属性来设置。
比如某著名框架tornado 对应的框架方法可能为
onInit() { ... } onClose() { ... } onMessage() { ... } onOpen() { ... }
而一些简单的框架可能只有 Read,Write,Close 三个函数。
因此使用不同的框架有不同的事情需要做。 如前端js
WebSocket.close([code[, reason]])
关闭当前链接。
WebSocket.send(data)
对要传输的数据进行排队。
TCP帧格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
2.2 服务端处理连接和消息
WebSocket 服务器是一个长链接的TCP 程序,监听服务器上任何遵循特定协议的端口,就这样。
服务器对websocket握手的响应,响应头包括上一节提到的内容:
Writer.Header().Set("Cache-Control", "no-cache")
Writer.Header().Set("Connection", "keep-alive")
当服务器收到握手请求时,它应该发回一个特殊的响应,表明协议将从 HTTP 变为 WebSocket。
看起来像这样(记住每个请求头以 \r\n结尾,并在最后一个之后放置一个额外的 \r\n):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPasdwaaQ9kYGzzhZRbK+xOo=
创建自定义服务器的任务往往听起来很困难,然而,在您选择的平台上实现一个简单的 WebSocket 服务器是很容易的。
它通常包括以下步骤:
-
Create
调用socket函数创建套接字,应当使用的参数参见例程。
-
Bind
调用bind函数把套接字绑定到一个监听端口上。注意bind函数需要接受一个sockaddr_in结构体作为参数,因此在调用bind函数之前, 程序要先声明一个 sockaddr_in结构体,用memset函数将其清零,然后将其中的sin_family设置为AF_INET,接下来,程序需要设置其sin_port成员变量,即监听端口。 需要说明的是,sin_port中的端口号需要以网络字节序存储,因此需要调用htons函数对端口号进行转换(函数名是"host to network short"的缩写)。
-
Listen
调用listen函数,使该套接字成为一个处在监听状态的套接字。
-
Accept
接下来,服务器可以通过accept函数接受客户端的连接请求。 若没有收到连接请求,accept函数将不会返回并阻塞程序的执行。接收到连接请求后,accept函数会为该连接返回一个套接字描述符。 accept函数可以被多次调用来接受不同客户端的连接请求,而且之前的连接仍处于监听状态——直到其被关闭为止。
-
Write
现在,服务器可以通过对send,recv或者对write,read等函数的调用来同客户端进行通信。
-
Close
对于一个不再需要的套接字,可以使用close函数关闭它
以下为实际实现,提供一个接口处理函数,用以推送ws信息
func HttpWsfunc(ctx *gin.Context, stream *ServiceMsg) {
writer := ctx.Writer
reader := ctx.Request
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{ctx.GetHeader("Sec-WebSocket-Protocol")},
}
conn, err := upgrader.Upgrade(writer, reader, nil)
if err != nil {
log.Println("upgrade err:", err)
}
defer conn.Close()
ServiceConn(conn, stream)
}
在服务连接处理函数中,执行三个动作,
处理推送消息 服务器-> 客户端,
go func() {
for {
if msg, ok := <-stream.Message; ok {
conn.WriteMessage(websocket.TextMessage, []byte(msg))
} else {
continue
}
}
// return false
}()
读取消息 客户端 -> 服务器,
go func() {
for {
if t, msg, err := conn.ReadMessage(); err != nil {
strmsg := string(msg)
fmt.Printf("conn readmessage: type:%v, msg:%v, err:%v\n", t, strmsg, err)
if strmsg == "close" {
logger.Infof("read msg:%v from conn:%#v\n", strmsg, conn)
conn.Close()
}
break
}
}
}()
拉取消息 队列服务或数据消息 -> 服务
go func() {
SubService(stream)
}()
客户端或服务器端都可以通过发送一个带有指定控制序列的控制帧以开始关闭连接握手。
对端收到这个控制帧会回复一个关闭帧,关闭发起端关闭连接。任何在关闭连接后接收到的数据都会被丢弃。
2.3 客户端建立连接和解读消息
客户端协议握手请求头必须包含必要的信息,并且如果上层协议为HTTP,则必须使用GET
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGgdawNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
现成的如客户端 aurelia,它是一个js 的web框架,移动端,桌面端也可以使用。 它让使用者可以专注在业务逻辑,不需要在框架层面改动,简单而强大。
解读消息的步骤,
1 读取 9-15(包括) 位并将其解析为无符号整型。如果长度小于等于 125,那么就是长度;你就完成了。如果是 126,到第二步。如果是 127,到步骤 3。
2 读取下面的 16 位,并将其解释为无符号整型。你就完成了。
3 读取接下来的 64 位,并将其解释为无符号整型 (最重要的位必须为 0)。
取和解密数据,如果设置了掩码位 (对于客户机到服务器的消息应该是这样),则读取接下来的 4 个字节 (32 位);这是掩蔽键。
一旦有效负载长度和掩蔽键被解码,您就可以继续从套接字读取字节数。让我们调用已编码的数据和密钥掩码。
要获得解码,可以通过编码的八位元 (字节,即文本数据的字符) 和 XOR 八位元 (i 模 4) 掩码的第四个八位元进行循环。
例如以下伪代码中 (恰好是有效的 JavaScript):
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
我们只需要使用浏览器已支持的对象
<script>
let addr = 'ws://localhost:8085/message';
let socket = new WebSocket(addr);
连接启用监听 connection opened
socket.addEventListener('open', function (event) {
document.getElementById("connect").innerText = "connected to server:" + addr
});
监听消息
socket.addEventListener('message', function (event) {
var body = document.getElementById("content")
var list = document.createElement("p")
list.innerHTML = event.data
body.appendChild(list)
});
也可以复制一份消息,模拟其他接口的使用
socket.onmessage = (msg) => {
var suff = "new message from "
var body = document.getElementById("msg")
var list = document.createElement("p")
list.innerHTML = suff + addr + " : " + msg.data
body.appendChild(list)
}
现在启动服务,查看其动物园门票实时效果吧。
3 小结
协议原文
https://datatracker.ietf.org/doc/rfc6455/?include_text=1
希望您的服务器遵守某些子协议,那么很自然地,您需要服务器上的额外代码。假设子协议是 JSON。在这个子协议中,所有数据都以 JSON 的形式传递。
如果客户端请求这个协议,而服务器想要使用它,服务器将需要一个 JSON 解析器。实际上,这是库的一部分,但是服务器需要传递数据。
为了避免命名冲突,可以把子协议名称加上域名字符串。
如果您正在构建一个自定义聊天应用程序,该应用程序使用的是ChatInc.独有的专有格式,
那么您可以使用这个:Sec-WebSocket-Protocol: ChatInc.com.
但是这不是必需的,它只是一个可选的约定,您可以使用任何字符串。
下一节,我们讨论它。
- 点赞
- 收藏
- 关注作者
评论(0)