python---websocket的使用

目录

一:简介
二:对比:
  Http:
  WebSocket:
三:socket实现步骤
  服务端:
  客户端:
四:简单实现,实现连接
  服务端:
  浏览器:
五:数据接收规则
  数据帧格式:
  实现规则解码:
   实现循环获取数据
六:数据发送规则(需要发送二进制包struct模块)
  实现发送数据
七:tornado实现websocket聊天室
   tornado服务端
  前端模板
  消息插件
  实现效果
  游客二

一:简介

推文: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,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断开)

六:数据发送规则(需要发送二进制包struct模块)

复制代码

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 当数据接收会触发

七:tornado实现websocket聊天室

 tornado服务端

复制代码

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

实现效果

游客一:

游客二

 

作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值