一:简介
推文:WebSocket 是什么原理?为什么可以实现持久连接?
推文:WebSocket:5分钟从入门到精通(很好)
WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
二:对比:
Http:
socket实现,单工通道(浏览器只发起,服务端只做响应),短连接,请求响应
WebSocket:
socket实现,双工通道,请求响应,推送。socket创建连接,不断开
三: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. 客户端接收到加密的值
注意:这个魔数是固定的 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
四:简单实现,实现连接
服务端:
# coding:utf8 # __author: Administrator # date: 2018/6/29 0029 # /usr/bin/env python import socket,base64,hashlibdef 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_dictsock = 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'))
''' b' GET / HTTP/1.1\r\n Host: 127.0.0.1:8080\r\n Connection: Upgrade\r\n Pragma: no-cache\r\n Cache-Control: no-cache\r\n Upgrade: websocket\r\n Origin: http://localhost:63342\r\n Sec-WebSocket-Version: 13\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.8\r\n Sec-WebSocket-Key: +uL/aiakjNABjEoMzAqm6Q==\r\n Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n' '''
浏览器:
<!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 ... | +---------------------------------------------------------------+
The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions. The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later. Decoding Payload LengthTo read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps: Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3. Read the next 16 bits and interpret those as an unsigned integer. You're done. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done. Reading and Unmasking the DataIf the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) {DECODED[i] = ENCODED[i] ^ MASK[i % 4]; }Now you can figure out what DECODED means depending on your application.
数据帧格式:
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] & 127if 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 = Nonemask = 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
实现循环获取数据
import socket,base64,hashlibdef 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_dictdef get_data(info):payload_len = info[1] & 127if 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 = Nonemask = 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 bodysock = 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'))#可以进行通信 while True:data = conn.recv(8096)data = get_data(data)print(data)
<!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);ws.send("你好");} </script>
注意:使用控制台完成发送,而不是刷新页面,会报错,因为我们关闭了连接,试图将关闭信号字节编码出错。这里我们需要利用mask(第二字节中,1表示连接,0断开)
六:数据发送规则(需要发送二进制包struct模块)
def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息:param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept():param msg_bytes: 向客户端发送的字节:return: """ import structtoken = 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_bytesconn.send(msg)return True
实现发送数据
# coding:utf8 # __author: Administrator # date: 2018/6/29 0029 # /usr/bin/env python import socket,base64,hashlibdef 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_dictdef get_data(info):payload_len = info[1] & 127if 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 = Nonemask = 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 bodydef send_msg(conn, msg_bytes):""" WebSocket服务端向客户端发送消息:param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept():param msg_bytes: 向客户端发送的字节:return:""" import structtoken = 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_bytesconn.send(msg)return Truesock = 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() #获取握手消息,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'))#可以进行通信 while True:data = conn.recv(8096)data = get_data(data)print(data)send_msg(conn,bytes(data+"geah",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);ws.send("你好");}ws.onmessage = function (ev) {console.log(ev);} </script>
七:tornado实现websocket聊天室
tornado服务端
import tornado.ioloop import tornado.web import tornado.websocket import datetimeclass MainHandler(tornado.web.RequestHandler):def get(self):self.render("s1.html")def post(self, *args, **kwargs):passusers = 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:continueclient.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()
前端模板
<!DOCTYPE html> <html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link href="/static/css/bootstrap.min.css" rel="stylesheet"><link href="/static/css/nifty.min.css" rel="stylesheet"><link href="/static/css/demo/nifty-demo-icons.min.css" rel="stylesheet"><link href="/static/css/demo/nifty-demo.min.css" rel="stylesheet"><link href="/static/plugins/pace/pace.min.css" rel="stylesheet"><script src="/static/js/jquery-2.2.4.min.js"></script><script src="/static/plugins/pace/pace.min.js"></script><script src="/static/js/bootstrap.min.js"></script><script src="/static/js/nifty.min.js"></script><script src="/static/js/demo/nifty-demo.min.js"></script><script src="/static/plugins/flot-charts/jquery.flot.min.js"></script><script src="/static/plugins/flot-charts/jquery.flot.resize.min.js"></script><script src="/static/plugins/gauge-js/gauge.min.js"></script><script src="/static/plugins/skycons/skycons.min.js"></script><script src="/static/plugins/easy-pie-chart/jquery.easypiechart.min.js"></script><script src="/static/js/demo/widgets.js"></script> </head> <body> <div id="container" class="effect aside-bright mainnav-sm aside-right aside-in"><div class="boxed"><div id="content-container"><div class="row"><div class="col-md-8 col-lg-8 col-sm-8"><!--Chat widget--><!--===================================================--><div class="panel" style="height: 640px"><!--Heading--><div class="panel-heading"><h3 class="panel-title">Chat</h3></div><!--Widget body--><div style="height:510px;padding-top:0px;" class="widget-body"><div class="nano"><div class="nano-content pad-all"><ul class="list-unstyled media-block"></ul></div></div><!--Widget footer--><div class="panel-footer" style="height: 90px;"><div class="row"><div class="col-xs-9"><input type="text" placeholder="Enter your text" class="form-control chat-input"></div><div class="col-xs-3"><button class="btn btn-primary btn-block" οnclick="sendMsg(this);" type="submit">Send</button></div></div></div></div></div><!--===================================================--><!--Chat widget--></div><div class="col-md-4 col-lg-4 col-sm-4"><aside id="aside-container"><div id="aside"><div class="nano has-scrollbar"><div class="nano-content" tabindex="0" style="right: -17px;"><!--Nav tabs--><!--================================--><ul class="nav nav-tabs nav-justified"><li class="active"><a href="#demo-asd-tab-1" data-toggle="tab"><i class="demo-pli-speech-bubble-7"></i></a></li></ul><!--================================--><!--End nav tabs--><!-- Tabs Content --><!--================================--><div class="tab-content"><div class="tab-pane fade in active" id="demo-asd-tab-1"><p class="pad-hor text-semibold text-main"><span class="pull-right badge badge-success">0</span> Friends</p></div></div></div><div class="nano-pane" style="display: none;"><div class="nano-slider" style="height: 4059px; transform: translate(0px, 0px);"></div></div></div></div></aside></div></div></div></div> </div> </body> </html><script>ws = new WebSocket("ws://127.0.0.1:8080/wschat");function sendMsg(ths) {var dt = new Date()var now_time = dt.toLocaleString();var msg = $(ths).parents(".row").find(".chat-input").val();$(ths).parents(".row").find(".chat-input").empty();var li = '<li class="mar-btm"><div class="media-right"><img src="" class="img-circle img-sm" alt="Profile Picture"></div>';li += '<div class="media-body pad-hor speech-right"><div class="speech"><a href="#" class="media-heading">游客</a>';li += '<p>'+msg+'</p>';li += '<p class="speech-time">';li += '<i class="demo-pli-clock icon-fw"></i>'+now_time;li += '</p></div></div></li>';$(ths).parents(".widget-body").find(".list-unstyled").append(li);$(ths).parents(".panel-footer").find(".chat-input").val("");ws.send(msg);}ws.onmessage=function (ev) {$(".list-unstyled").append(ev.data);} </script>
消息插件
<li class="mar-btm"><div class="media-left"><img src="img/profile-photos/1.png" class="img-circle img-sm" alt="Profile Picture"></div><div class="media-body pad-hor"><div class="speech"><a href="#" class="media-heading">游客</a><p>{{msg}}</p><p class="speech-time"><i class="demo-pli-clock icon-fw"></i>{{date}}</p></div></div></li>
实现效果
游客一:
游客二