赞
踩
目录
一:简介
二:对比:
Http:
WebSocket:
三:socket实现步骤
服务端:
客户端:
四:简单实现,实现连接
服务端:
浏览器:
五:数据接收规则
数据帧格式:
实现规则解码:
实现循环获取数据
六:数据发送规则(需要发送二进制包struct模块)
实现发送数据
七:tornado实现websocket聊天室
tornado服务端
前端模板
消息插件
实现效果
游客二
推文:WebSocket 是什么原理?为什么可以实现持久连接?
推文:WebSocket:5分钟从入门到精通(很好)
WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
1. 服务端开启socket,监听IP和端口 3. 允许连接 * 5. 服务端接收到特殊值【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】 * 6. 加密后的值发送给客户端
2. 客户端发起连接请求(IP和端口) * 4. 客户端生成一个xxx,【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服务端发送一段特殊值 * 7. 客户端接收到加密的值
# coding:utf8 # __author: Administrator # date: 2018/6/29 0029 # /usr/bin/env python import socket,base64,hashlib def get_headers(data): '''将请求头转换为字典''' header_dict = {} data = str(data,encoding="utf-8") header,body = data.split("\r\n\r\n",1) header_list = header.split("\r\n") for i in range(0,len(header_list)): if i == 0: if len(header_list[0].split(" ")) == 3: header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ") else: k,v=header_list[i].split(":",1) header_dict[k]=v.strip() return header_dict sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sock.bind(("127.0.0.1",8080)) sock.listen(5) #等待用户连接 conn,addr = sock.accept() print("conn from ",conn,addr) #获取握手消息,magic string ,sha1加密 #发送给客户端 #握手消息 data = conn.recv(8096) headers = get_headers(data) # 对请求头中的sec-websocket-key进行加密 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://%s%s\r\n\r\n" magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url']) # 响应【握手】信息 conn.send(bytes(response_str, encoding='utf-8'))
请求头
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> </body> </html> <script> ws =new WebSocket("ws://127.0.0.1:8080"); ws.onopen = function (ev) { //若是连接成功,onopen函数会执行 console.log(22222) } </script>
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 | #Payload len(第二个字节的前七位,最大127)决定头部的长度 |I|S|S|S| (4) |A| (7) | (16/64) | #若是小于126:Extended payload length扩展头部长度为0字节,后面全部为主体数据 |N|V|V|V| |S| | (if payload len==126/127) | #若是等于126:Extended payload length扩展头部长度为2字节,后面全部为主体数据 | |1|2|3| |K| | | #若是等于127:Extended payload length扩展头部长度为8字节,后面全部为主体数据 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | #注意:主体数据中的前四位为mask掩码,用于后面的消息的解码,解码方式为循环异或操作 + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | #数据过长,需要分部发送,这时需要FIN和opcode +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
View Code
FIN:1个比特。
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。
RSV1, RSV2, RSV3:各占1个比特。
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。
Opcode: 4个比特。
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下: %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。 %x1:表示这是一个文本帧(frame) %x2:表示这是一个二进制帧(frame) %x3-7:保留的操作代码,用于后续定义的非控制帧。 %x8:表示连接断开。 %x9:表示这是一个ping操作。 %xA:表示这是一个pong操作。 %xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1个比特。
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。 如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。 如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。 掩码的算法、用途在下一小节讲解。
Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。
假设数Payload length === x,如果 x为0~126:数据的长度为x字节。 x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。 x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。 此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。
Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。 备注:载荷数据的长度,不包括mask key的长度。
Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。 扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。 应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。
def get_data(info): #info是我们连接后,接受的数据 payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() #这里我们使用字节将数据全部收集,再去字符串编码,这样不会导致中文乱码 for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') return body
服务端代码
客户端代码
注意:使用控制台完成发送,而不是刷新页面,会报错,因为我们关闭了连接,试图将关闭信号字节编码出错。这里我们需要利用mask(第二字节中,1表示连接,0断开)
def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\x81" #接收的第一字节,一般都是x81不变 length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True
服务端
前端onmessage 当数据接收会触发
import tornado.ioloop import tornado.web import tornado.websocket import datetime class MainHandler(tornado.web.RequestHandler): def get(self): self.render("s1.html") def post(self, *args, **kwargs): pass users = set() class ChatHandler(tornado.websocket.WebSocketHandler): def open(self, *args, **kwargs): '''客户端连接''' print("connect....") print(self.request) users.add(self) def on_message(self, message): '''有消息到达''' now = datetime.datetime.now() content = self.render_string("recv_msg.html",date=now.strftime("%Y-%m-%d %H:%M:%S"),msg=message) for client in users: if client == self: continue client.write_message(content) def on_close(self): '''客户端主动关闭连接''' users.remove(self) st ={ "template_path": "template",#模板路径配置 "static_path":'static', } #路由映射 匹配执行,否则404 application = tornado.web.Application([ ("/index",MainHandler), ("/wschat",ChatHandler), ],**st) if __name__=="__main__": application.listen(8080) #io多路复用 tornado.ioloop.IOLoop.instance().start()
s1.html
recv_msg.html
作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。