歪脖斯考特初探

banner:https://www.pixiv.net/artworks/98483067

转眼就快到期末周了,勤奋的群友都在偷偷卷这个事实使我这个真正摆烂的人有些焦虑。有的群友已经考完了好多门试,有的群友已经做完了好多门的课设,还有的群友已经获得了《We Were Here Forever》的好多成就和速通了好多gal,可以上显然和天天翘课的我没有一点关系,令人感叹。某天我像往常一样八点醒来时,( 发现老师早已在钉钉群里布置了个课设:

可一个都不会的选题让我犯了难。这时我又想起了万能的群友:

既然群友能在半夜花不到40分钟就从不会python到搞懂了用python写ws服务器,那本人也来斗胆试试罢。至於为什么标题是歪脖司考特,这当然也是因为上述的群友——AKA lot[1]大佬,亲口说它的读音是“WebScott”,并且认为此协议是server与物联网设备通信的极佳解决方案。虽然我对其读音和使用场景略有疑议,但专业的人做专业的事,还是顺从大佬好了。

既然是计网课设,那么最好研究一下稍微底层的东西。因此就不得不读一下像Farex的游戏库一样又臭又长的RFC了。A Request for Comments (RFC) is a publication in a series——维基百科是这么描述的。书上还特意提了一句RFC是“请求评论”[2],感觉怪怪的。等我一看RFC 6455才发现非常致命:rfc6455有足足70页,而大家熟知的FTP协议[3]只有68页,新一点的HTTP/2[4]也不过95页。这种时候我就不得不羡慕海圣六级680的含金量了。RFC 6455 目前處於 Proposed Standard 阶段,是建议标准。

看了一下上图表格的范例,发现大多数的固定选题都是利用python进行仿真。这可能比较简单,但并不好玩。仿真和实现还是有点区别的。同样的道理,随便找个包调用一下来实现我已经把规则都忘了的多人飞行棋看上去好像重点在前端,总觉得差点意思。本篇博文我们就试着来实现一下服务器端的websocket。

握手

客户端

1.3. Opening Handshake
  _This section is non-normative._
  The opening handshake is intended to be compatible with HTTP-based server-side software and intermediaries, so that a single port can be used by both HTTP clients talking to that server and WebSocket clients talking to that server. To this end, the WebSocket client’s handshake is an HTTP Upgrade request:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

我们可以看到客户端侧的握手请求与HTTP协议是兼容的,以便进行端口复用。

那么我们先搭建一个前端页面来进行测试[5]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html>

<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>

<body>
<div>
<input type="text" id="txt" />
<input type="button" id="btn" value="提交" onclick="sendMsg();" />
<input type="button" id="open" value="打开连接" onclick="openConn();" />
<input type="button" id="close" value="关闭连接" onclick="closeConn();" />
</div>
<div id="content"></div>

<script type="text/javascript">
var socket;
function openConn() {
socket = new WebSocket("ws://127.0.0.1:8003/chat");

socket.onopen = function () {
/* 与服务器端连接成功后,自动执行 */

var newTag = document.createElement('div');
newTag.innerHTML = "【连接成功】";
document.getElementById('content').appendChild(newTag);
};

socket.onmessage = function (event) {
/* 服务器端向客户端发送数据时,自动执行 */
var response = event.data;
var newTag = document.createElement('div');
newTag.innerHTML = response;
document.getElementById('content').appendChild(newTag);
};

socket.onclose = function (event) {
/* 服务器端主动断开连接时,自动执行 */
socket = undefined;
var newTag = document.createElement('div');
newTag.innerHTML = "【关闭连接,onclose回调】";
document.getElementById('content').appendChild(newTag);
};
}

function sendMsg() {
var txt = document.getElementById('txt');
socket.send(txt.value);
txt.value = "";
}

function closeConn() {
socket.close();
var newTag = document.createElement('div');
newTag.innerHTML = "【关闭连接】";
document.getElementById('content').appendChild(newTag);
}

</script>
</body>

</html>

后端想要实现,就离不开socket。书上又说了[6],同一个名词socket可以表示多种不同的意思,这里既用到了TCP连接的端点,又用到了socket API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ADDRESS = ('127.0.0.1', 8003)
BUFSIZE = 4096


def main():

# 创建TCP Socket,AF_INET是一类地址族(address family),用于IPv4协议;SOCK_STREAM是一类Socket类型,提供面向流的TCP传输
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.bind(ADDRESS)
sock.listen()

conn, address = sock.accept() # 阻塞方式等待连接
print('connected by:', address)
data = b''
while True:
_ = conn.recv(BUFSIZE)
if not _:
break
data += _
print(data.decode())

然后前端点击打开连接,浏览器就会自动发送一个握手请求,数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /chat HTTP/1.1
Host: 127.0.0.1:8003
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en-US;q=0.7,en;q=0.6
Sec-WebSocket-Key: f03NpU0iebD9rbIRGj8NPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

可以看到,除了常规的请求头外,还有一些ws独有的:

  • Connection: Upgrade # 设置 Connection 头的值为 “Upgrade” 来指示这是一个升级请求
  • Upgrade: websocket # Upgrade 头指定升级为ws协议
  • Sec-WebSocket-Key # base64后的随机数,后续server端需要用这个值来生成Sec-WebSocket-Accept
  • Sec-WebSocket-Protocol # 子协议,可选
  • Sec-WebSocket-Version # 必须为13
  • Sec-WebSocket-Extensions # 可选,指定客户端支持的扩展

服务器端

当服务器收到握手请求时,它应该发回一个特殊的响应,表明协议将从HTTP变为WebSocket。看起来像这样:

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept怎样计算呢, 把客户发送的 Sec-WebSocket-Key 和 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” ( it’s a “magic string” )连接起来,把结果用SHA-1编码,再用base64编码一次,就可以了。

1
2
3
4
def get_sec_websocket_accept(key):
value = key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
return ac.decode('utf-8')

那么我们就可以对握手请求进行响应了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@classmethod
def check_headers_and_return_ac(cls, data: bytes):
header_dict = {}
header = data.decode('utf-8')
header_list = header.split('\r\n')

assert header_list[0] == 'GET /chat HTTP/1.1'
for line in header_list[1:]:
key, value = line.split(': ')
header_dict[key] = value

assert header_dict['Upgrade'] == 'websocket'
assert header_dict['Connection'] == 'Upgrade'
assert header_dict['Sec-WebSocket-Key']
assert header_dict['Sec-WebSocket-Version'] == '13'
return cls.get_sec_websocket_accept(header_dict['Sec-WebSocket-Key'])

def accept_connection(self):
headers = b''
while True:
_ = self.conn.recv(1)
if not _:
raise socket.error(socket.EBADF, 'Bad file descriptor')
headers += _
if headers.endswith(b'\r\n\r\n'):
break
headers = headers[:-4]
ac = self.check_headers_and_return_ac(headers)
ac_headers = "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade:websocket\r\n" \
"Connection:Upgrade\r\n" \
f"Sec-WebSocket-Accept:{ac}\r\n\r\n"
self.conn.sendall(ac_headers.encode("utf8"))

至此,握手阶段结束。

数据帧

结构如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ... |
+---------------------------------------------------------------+

读帧

在处理数据时,我们先来考虑一个问题:如何从socket中读取长度为bufsize(字节)的数据?像我肯定直接来一个socket.recv(bufsize),追求的就是一个简单粗暴。但我知道,像海圣和海南队长这种深耕量化交易浸心web3豪取NFT的大佬心思会更细腻些:

1
2
3
4
5
6
7
8
9
10
def _read_strict(self, bufsize):
remaining = bufsize
_bytes = b""
while remaining:
_buffer = self.conn.recv(remaining)
if not _buffer:
raise socket.error(socket.EBADF, 'Bad file descriptor')
_bytes += _buffer
remaining = bufsize - len(_bytes)
return _bytes

上面的代码好处就比较多了——既防止了读取数据的长度比bufsize小(这是完全有可能的),也对套接字忽然的关闭抛出了异常。

接下来我们就可以根据RFC 5.2节来看一下Frame:

  1. FIN: 1 bit
    Indicates that this is the final fragment in a message. The first fragment MAY also be the final fragment.

  2. RSV1, RSV2, RSV3: 1 bit each
    无扩展协商时,应始终为0。

  3. Opcode: 4 bits
    操作代码,决定如何解析数据。如果操作代码未知,那么接收端应该断开连接。

    • %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
    • %x1:表示这是一个文本帧。
    • %x2:表示这是一个二进制帧。
    • %x3-7:保留的操作代码,用于后续定义的非控制帧。
    • %x8:表示连接断开。
    • %x9:表示这是一个ping操作。
    • %xA:表示这是一个pong操作。
    • %xB-F:保留的操作代码,用于后续定义的控制帧。
  4. Mask: 1 bit
    下文详述。

  5. Payload length: 7 bits, 7+16 bits, or 7+64 bits
    The length of the “Payload data”. 以字节为单位。如果0-125,这是负载长度。如果126,之后的两字节解释为一个16位的无符号整数是负载长度。如果127,之后八字节解释为的一个64位的无符号整数是负载长度。字节排列方式为(the most significant bit MUST be 0),既大端序。
    我在这补充一下我贫瘠的计算机基础以便理解,像网络工程大佬和土木计算机跨界大佬就不要嘲笑我了。7比特能表示的无符号整数的最大值是2^7-1=127,这种情况对应了上面7+64 bits。而1个byte(字节)的size是8个bit,所以上述也可以表示为7 bits, 7 bits + 2 bytes, or 7 bits + 8 bytes。值得注意的是,理论上一个帧的最大数据载荷是(2^64-1)÷8÷1024÷1024÷1024=2147483648(GB),但实际上,过大的数据应该分片传输。

  6. Masking-key: 0 or 4 bytes
    下文详述。

  7. Payload data: Payload length
    Cuz no extension has been negotiatedPayload data的长度就是第五点的Payload length

然后就可以根据从字节流中获取各字段了,只要相应的位与1进行与即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
header_bytes = self._read_strict(2)
b1 = header_bytes[0]
fin = b1 >> 7 & 1
opcode = b1 & 0b1111
b2 = header_bytes[1]
mask = b2 >> 7 & 1
length = b2 & 0b1111111

length_data = ""
if length == 126:
length_data = self._read_strict(2)
length = struct.unpack("!H", length_data)[0]
elif length == 127:
length_data = self._read_strict(8)
length = struct.unpack("!Q", length_data)[0]

西南战争中西乡盛隆的战败标志着日本士族时代的结束,两军的的伤亡几乎相同。战争后,在被歌颂为“西国无双”的萨摩地区武士几乎绝迹,武士们同欧洲中世纪的骑士一样被历史的车轮无情地轧碎。线列兵与燧发枪将骑士们的冲锋击碎,国民军与野战炮将精于武道的武士沉寂。而讽刺的是,西乡盛隆本人在死后成为了日本军国派与统制派的精神图腾,追随西乡失败的旧武士们却掀起了一次次民权运动,极力维持着明治三杰逝去后崩坏的脆弱的民主制度。

举个比方,打个例子,经过我一个一个掰着手指头数了三遍,以上不知出处的文章共有203字,经utf8编码后有203*3=609个字节,所以length=126,length_data = b'\x02a'。用我们小学二年级就学过的进制相关知识,最终的length可以表示为length_data[0] * (16^2) + length_data[1] = 609。而如果一开始的length==127,这么硬算出来的length就是 length_data[0] * (16^14) + length_data[1] * (16^12) + length_data[2] * (16^10) + length_data[3] * (16^8) + length_data[4] * (16^6) + length_data[5] * (16^4) + length_data[6] * (16^2) + length_data[7] 。这显然略微有点麻烦,好在python的标准库struct[7]内建了unpack()可以很方便地来转换。

接下来看看Mask:所有从客户端发给服务端的数据都需要带有这个值。服务器接收时要用其来解码数据。算法如下:

1
2
3
4
5
Octet i of the transformed data ("transformed-octet-i") is the XOR of 
octet i of the original data ("original-octet-i") with octet at index
i modulo 4 of the masking key ("masking-key-octet-j"):
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

1
2
3
4
5
6
7
def unmask(cls, mask: bytes, data: bytes):
mask = array.array("B", mask)
original_data = array.array("B", data)
transformed_data = array.array("B", data)
for i in range(len(data)):
transformed_data[i] = original_data[i] ^ mask[i % 4]
return transformed_data.tobytes()

总的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def read_frame(self):
header_bytes = self._read_strict(2)
b1 = header_bytes[0]
fin = b1 >> 7 & 1
opcode = b1 & 0b1111
b2 = header_bytes[1]
mask = b2 >> 7 & 1
length = b2 & 0b1111111

length_data = ""
if length == 126:
length_data = self._read_strict(2)
length = struct.unpack("!H", length_data)[0]
elif length == 0x7f:
length_data = self._read_strict(8)
length = struct.unpack("!Q", length_data)[0]
mask_key = ""
if mask:
mask_key = self._read_strict(4)
data = self._read_strict(length)
if mask:
data = self.unmask(mask_key, data)
return fin, opcode, data

写帧

过程与读帧类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def _write_frame(self, fin: bool, opcode: int, data):
if fin:
finbit = 0x80
else:
finbit = 0
frame = struct.pack("!B", finbit | opcode)

if isinstance(data, str):
data = data.encode('utf-8')

length = len(data)
if length < 126:
frame += struct.pack("!B", length)
elif length <= 0xFFFF:
frame += struct.pack("!BH", 126, length)
else:
frame += struct.pack("!BQ", 127, length)

frame += data
try:
self.conn.send(frame)
except socket.error:
self.write_close()

处理控制帧

目前所定义的控制帧的opcode代码有0x8 (Close), 0x9 (Ping), and 0xA (Pong)。

关闭

应用发送关闭帧后,不应继续发送数据帧。关闭帧可以有内容(body)。如果有内容,内容开头的两个字节是关闭状态码(status code)。如果终端首次收到关闭帧,则应尽快发送一个关闭帧。在都收到和发送关闭帧后,服务端认为ws连接已关闭,并且必须关闭TCP连接;客户端应等待服务端关闭连接。

1
2
3
4
5
6
7
8
9
def write_close(self, code: int = 0, reason: bytes = b''):
if code == 0:
code = 1000 # rfc6455#section-7.4 "normal closure" status code

close_data = struct.pack('>H', code)
close_data += reason
self._write_frame(True, self.OPCODE_CLOSE, close_data)
self.connected = False
self.conn.close()

乒乓

一句话,收到乒,就发送一个一样的乓。

总的代码

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import array
import base64
import hashlib
import socket
import struct


class WebSocket:
ADDRESS = ('127.0.0.1', 8003)
OPCODE_CONTINUATION = 0x0
OPCODE_TEXT = 0x1
OPCODE_BINARY = 0x2
OPCODE_CLOSE = 0x8
OPCODE_PING = 0x9
OPCODE_PONG = 0xa

def __init__(self):
# 创建TCP Socket,AF_INET是一类地址族(address family),用于IPv4协议;SOCK_STREAM是一类Socket类型,提供面向流的TCP传输
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

self.sock.bind(self.ADDRESS)
self.sock.listen()

self.conn, self.address = self.sock.accept() # 阻塞方式等待连接
print('connected by:', self.address)

self.connected = False
self.run()

@classmethod
def get_sec_websocket_accept(cls, key: str):
value = key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
return ac.decode('utf-8')

@classmethod
def check_headers_and_return_ac(cls, data: bytes):
header_dict = {}
header = data.decode('utf-8')
header_list = header.split('\r\n')

assert header_list[0] == 'GET /chat HTTP/1.1'
for line in header_list[1:]:
key, value = line.split(': ')
header_dict[key] = value

assert header_dict['Upgrade'] == 'websocket'
assert 'Upgrade' in header_dict['Connection']
assert header_dict['Sec-WebSocket-Key']
assert header_dict['Sec-WebSocket-Version'] == '13'
return cls.get_sec_websocket_accept(header_dict['Sec-WebSocket-Key'])

def accept_connection(self):
headers = b''
while True:
_ = self.conn.recv(1)
if not _:
raise socket.error(socket.EBADF, 'Bad file descriptor')
headers += _
if headers.endswith(b'\r\n\r\n'):
break
headers = headers[:-4]
ac = self.check_headers_and_return_ac(headers)
ac_headers = "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade:websocket\r\n" \
"Connection:Upgrade\r\n" \
f"Sec-WebSocket-Accept:{ac}\r\n\r\n"
self.conn.sendall(ac_headers.encode("utf8"))

@classmethod
def unmask(cls, mask: bytes, data: bytes):
mask = array.array("B", mask)
original_data = array.array("B", data)
transformed_data = array.array("B", data)
for i in range(len(data)):
transformed_data[i] = original_data[i] ^ mask[i % 4]
return transformed_data.tobytes()

def run(self):
if not self.connected:
self.accept_connection()
self.connected = True
while self.connected:
opcode, data = self.read_data()
if opcode == self.OPCODE_TEXT:
self._write_frame(True, self.OPCODE_TEXT, data)
elif opcode == self.OPCODE_BINARY:
self._write_frame(True, self.OPCODE_BINARY, data)

def read_data(self):
fin, opcode, data = self.read_frame()
if opcode == self.OPCODE_TEXT:
while fin != 1:
fin, _, _data = self.read_frame()
data += _data
return (opcode, data)
elif opcode == self.OPCODE_BINARY:
return (opcode, data)
elif opcode == self.OPCODE_CLOSE:
close_code, close_reason = 0, b''
if len(data) >= 2:
close_code = struct.unpack('>H', data[:2])[0]
if len(data) > 2:
close_reason = data[2:]
self.write_close(close_code, close_reason)
return (opcode, None)
elif opcode == self.OPCODE_PING:
self._write_frame(True, self.OPCODE_PONG, data)
return (opcode, data)

def read_frame(self):
header_bytes = self._read_strict(2)
b1 = header_bytes[0]
fin = b1 >> 7 & 1
opcode = b1 & 0b1111
b2 = header_bytes[1]
mask = b2 >> 7 & 1
length = b2 & 0b1111111

length_data = ""
if length == 126:
length_data = self._read_strict(2)
length = struct.unpack("!H", length_data)[0]
elif length == 0x7f:
length_data = self._read_strict(8)
length = struct.unpack("!Q", length_data)[0]
mask_key = ""
if mask:
mask_key = self._read_strict(4)
data = self._read_strict(length)
if mask:
data = self.unmask(mask_key, data)
return fin, opcode, data

def _write_frame(self, fin: bool, opcode: int, data):
if fin:
finbit = 0x80
else:
finbit = 0
frame = struct.pack("!B", finbit | opcode)

if isinstance(data, str):
data = data.encode('utf-8')

length = len(data)
if length < 126:
frame += struct.pack("!B", length)
elif length <= 0xFFFF:
frame += struct.pack("!BH", 126, length)
else:
frame += struct.pack("!BQ", 127, length)

frame += data
try:
self.conn.send(frame)
except socket.error:
self.write_close()

def write_close(self, code: int = 0, reason: bytes = b''):
if code == 0:
code = 1000 # rfc6455#section-7.4 "normal closure" status code

close_data = struct.pack('>H', code)
close_data += reason
self._write_frame(True, self.OPCODE_CLOSE, close_data)
self.connected = False
self.conn.close()

def _read_strict(self, bufsize):
remaining = bufsize
_bytes = b""
while remaining:
_buffer = self.conn.recv(remaining)
if not _buffer:
raise socket.error(socket.EBADF, 'Bad file descriptor')
_bytes += _buffer
remaining = bufsize - len(_bytes)
return _bytes


if __name__ == '__main__':
ws = WebSocket()

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<!DOCTYPE html>
<html>

<head lang="zh">
<meta charset="UTF-8">
<title></title>
</head>

<body>
<div>
<input type="text" id="txt" />
<input type="button" id="btn" value="提交" onclick="sendMsg()" />
<input type="button" id="open" value="打开连接" onclick="openConn()" />
<input type="button" id="close" value="关闭连接" onclick="closeConn()" />
<input type="button" id="start_record" value="开始录音" onclick="start_record()" />
<audio></audio>
</div>
<div id="content"></div>

<script type="text/javascript">
let socket, stream, recorder;
var sourceBuffer;
var mediaSource = new MediaSource();
var audio = document.querySelector('audio');
audio.controls = true;
audio.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);

const now = () => {
const date = new Date();
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`;
}

function sourceOpen(e) {
sourceBuffer = mediaSource.addSourceBuffer('audio/webm; codecs=opus');
}

const openConn = () => {
socket = new WebSocket("ws://127.0.0.1:8003/chat");

socket.onopen = function () {
/* 与服务器端连接成功后,自动执行 */
audio.play();
const newTag = document.createElement('div');
newTag.innerHTML = "【连接成功】";
document.getElementById('content').appendChild(newTag);
};

socket.onmessage = async (event) => {
/* 服务器端向客户端发送数据时,自动执行 */
if (typeof event.data === 'string') {
const response = `收到文本: ${event.data} ${now()} `;
const newTag = document.createElement('div');
newTag.innerHTML = response;
document.getElementById('content').appendChild(newTag);
} else {
const _buffer = await event.data.arrayBuffer()
sourceBuffer.appendBuffer(_buffer);
}
};

socket.onclose = function (event) {
/* 服务器端主动断开连接时,自动执行 */
socket = undefined;
const newTag = document.createElement('div');
newTag.innerHTML = "【关闭连接,onclose回调】";
document.getElementById('content').appendChild(newTag);
};
}

const sendMsg = () => {
const txt = document.getElementById('txt');
socket.send(txt.value);
const msg = `发送文本: ${txt.value} ${now()} `;
const newTag = document.createElement('div');
newTag.innerHTML = msg;
document.getElementById('content').appendChild(newTag);
txt.value = "";
}

const closeConn = () => {
socket.close();
if (recorder && recorder.state !== 'inactive') {
recorder.stop();
}
const newTag = document.createElement('div');
newTag.innerHTML = "【主动关闭连接】";
document.getElementById('content').appendChild(newTag);
}

const start_record = async () => {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recorder = new MediaRecorder(stream);
recorder.ondataavailable = async (e) => {
if (typeof socket === 'undefined') {
return;
}
// 理论上可以直接用`socket.send(e.data)`,
// 但是谷歌浏览器实现有bug,详见https://bugs.chromium.org/p/chromium/issues/detail?id=962857
const filereader = new FileReader();
filereader.onload = () => {
socket.send(filereader.result)
};
filereader.readAsArrayBuffer(e.data);
}

recorder.start(100);
console.log('started');

}

const stop_record = () => {
recorder.stop();
}

</script>
</body>

</html>

  1. 注意开头的字母是小写的L,同样是大佬所言,博主注 ↩︎

  2. 谢希仁,《计算机网络(第8版)》,以下简称《计网》,P8 ↩︎

  3. RFC 959, STD9 ↩︎

  4. RFC 7540 ↩︎

  5. 前端代码来自 https://blog.csdn.net/u012324798/article/details/103549249 ,有修改 ↩︎

  6. 《计网》 P220 ↩︎

  7. https://docs.python.org/zh-cn/3/library/struct.html ↩︎