banner:https://www.pixiv.net/artworks/98483067
序
转眼就快到期末周了,勤奋的群友都在偷偷卷这个事实使我这个真正摆烂的人有些焦虑。有的群友已经考完了好多门试,有的群友已经做完了好多门的课设,还有的群友已经获得了《We Were Here Forever》的好多成就和速通了好多gal,可以上显然和天天翘课的我没有一点关系,令人感叹。某天我像往常一样八点醒来时,(
发现老师早已在钉钉群里布置了个课设:
可一个都不会的选题让我犯了难。这时我又想起了万能的群友:
既然群友能在半夜花不到40分钟就从不会python到搞懂了用python写ws服务器,那本人也来斗胆试试罢。至於为什么标题是歪脖司考特,这当然也是因为上述的群友——AKA lot大佬,亲口说它的读音是“WebScott”,并且认为此协议是server与物联网设备通信的极佳解决方案。虽然我对其读音和使用场景略有疑议,但专业的人做专业的事,还是顺从大佬好了。
既然是计网课设,那么最好研究一下稍微底层的东西。因此就不得不读一下像Farex的游戏库 一样又臭又长的RFC了。A Request for Comments (RFC) is a publication in a series——维基百科是这么描述的。书上还特意提了一句RFC是“请求评论”,感觉怪怪的。等我一看RFC 6455 才发现非常致命:rfc6455有足足70页,而大家熟知的FTP协议只有68页,新一点的HTTP/2也不过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协议是兼容的,以便进行端口复用。
那么我们先搭建一个前端页面来进行测试:
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。书上又说了,同一个名词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 (): 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:
FIN: 1 bit
Indicates that this is the final fragment in a message. The first fragment MAY also be the final fragment.
RSV1, RSV2, RSV3: 1 bit each
无扩展协商时,应始终为0。
Opcode: 4 bits
操作代码,决定如何解析数据。如果操作代码未知,那么接收端应该断开连接。
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧。
%x2:表示这是一个二进制帧。
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1 bit
下文详述。
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)
,但实际上,过大的数据应该分片传输。
Masking-key: 0 or 4 bytes
下文详述。
Payload data: Payload length
Cuz no extension has been negotiated
,Payload 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内建了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 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 arrayimport base64import hashlibimport socketimport structclass 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 ): 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 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 ; } 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 >