Python 3.6.0 实现 websocket server

Python 3.6.0 实现 websocket server

网上的好多教程都是基于Python2.X的,虽然差不多,但是对于我们这些刚刚听说过webSocket的小白来说,微小的差异也会让我们debug半天,所以以此博客做我实现的记录,仅供后来者参考

需要用到的知识:

  • python模块:socket, struct,hashlib, threading

  • JavaScript websocket简单使用

  • chrome开发者工具(对于websocket的报错更加详细,利于debug)

一、 webSocket协议

1. sebsocket client向服务器发送握手请求

格式如下:

GET / HTTP/1.1\r\n

/省略不相关信息/

Sec-WebSocket-Key: G4cZeCrg+0Znd6MLvVJSTg==\r\n

Connection: keep-alive, Upgrade\r\n

Upgrade: websocket\r\n\r\n

Sec-WebSocket-Key对应的键值由websocket client 随机生成;Connection: keep-alive, Upgrade表示网络协议升级(upgrade);Upgrade: websocket表示将协议升级为websocket连接协议

2. sebsocket server向client 返回基于Sec-WebSocket-Key的Sec-WebSocket-Accept

Magic_string = 258EAFA5-E914-47DA-95CA-C5AB0DC85B11(固定)

combined_string = Sec-WebSocket-Key + Magic_string

对 combined_String 取sha1数字摘要,然后进行base64编码,得到Sec-WebSocket-Accept_str

返回格式

HTTP/1.1 101 Web Socket Protocol Handshake

/省略不相关信息/

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: Sec-WebSocket-Accept_str

3.websocket握手部分完成,现在可以进行双全工通信

3.1 client部分由javaScript完成,后面会进行详细的介绍
3.2 server端发送websocekt报文

向客户端发送的websocket报文分为3部分:

  • 固定部分 ‘\x81’

  • 报文内容长度

    • 小于127, 填充8bit表示内容长度
    • 小于2^16-1, 填充第一个8bit为126的十六进制表示,后面16bit表示内容长度
    • 小于2^64-1, 填充第一个8bit为127的十六进制表示,后面64bit表示内容长度
  • 报文内容

将三部分有序组装即可使用socket.send()发送给哭护短

3.3 server端解析websocekt报文

客户端发送至server的websocket报文分为四部分:

  • 固定部分 ‘\x81’

  • 报文内容长度(同上文”报文内容长度”)

  • 掩码mask

    • mask由四字节组成
  • 报文内容content

获得掩码mask和content,注意报文内容长度不同会影响mask和content在websocket报文中的起始位置

对content进行按字节循环与处理(python描述):

result = ""
i = 0
for d in content:
    result += d ^ chr(d ^ ord(mask[i%4]))
    i += 1

得到result即为client发送到server的数据

二、 构建websocket客户端

为了能够使大家先体验一把websocket的乐趣,同时也可以为后面server构建过程中能够有debug参照,首先实现基于JavaScript的websocket的客户端

1.JavaScript事件驱动模型

简单理解就是,无阻塞,当发生A事件时,自动调用B函数,处理A事件。在js中,实现这一机制的就是回调函数的使用。样例:

var ws = new websocket("ws://127.0.0.1:8124");
ws.on("error", function(e){
    console.log(e.message);
}):

样例第一行表示创建websocket对象

样例第二行至第四行,error为关键字,function(e){…}即为回调函数。表示,当ws发生错误时,调用function(e){…}对错误进行处理

2. 创建websocket对象
var ws = new websocket("ws://127.0.0.1:8124");

解释url字段

  • ws 表示使用websocket协议,与http/https相似

  • url,即表示目的地址 目的端口

3. 完整代码
<!DOCTYPE html>
<html>
<head>
<title>websocket</title>
</head>
<body>
<script type="text/javascript">
var ws;
function startWS() {
    console.log('start once again');
    // ws = new WebSocket("ws://127.0.0.1:8124");
    ws = new WebSocket("ws://echo.websocket.org");
    ws.onopen =  function (msg) {
        console.log('webSocket opened');
    };
    ws.onmessage = function (message) {
        console.log('receive message : ' + message.data);
    };
    ws.onerror = function (error) {
        console.log('error :' + error.name + error.number);
    };

    ws.onclose =  function () {
        console.log('webSocket closed');
    };
}
function sendMessage () {
    console.log("sending a message");
    ws.send("websocket from js");
}
</script>

<button onclick="startWS()">createWebsocket</button><br>
<button onclick="sendMessage()">sendMessage</button>
</body>
</html>

将上述代码保存问xxx.html文件,即可使用浏览器打开。可在浏览器“开发者工具”->控制台console中进行查看client运行情况
websocket client调试结果

三、 python模块解析

1. struct模块
  • struct.pack(fmt, value1, value2)

fmt为由特定字符组成的字符串,函数功能为,将python数据类型value1,value2转化为C数据类型

fmt字符类型:

Format  C Type          Python type     Standard size
x       pad byte        no value         
c       char            bytes of length 1 
b       signed char     integer         1   
B       unsigned char   integer         1   
?       _Bool           bool            1   
h       short           integer         2   
H       unsigned short  integer         2   
i       int             integer         4   
I       unsigned int    integer         4   
l       long            integer         4   
L       unsigned long   integer         4   
q       long long       integer         8   
Q       unsigned long long  integer     8   
n       ssize_t         integer             
N       size_t          integer             
e       (7)             float           2   
f       float           float           4   
d       double          float           8   
s       char[]          bytes            
p       char[]          bytes            
P       void *          integer         (6)
  • struct.unpack(fmt, buffer)

即为struct.pack(fmt, value..)操作的逆操作

详见python3.6.0 struct官方文档

2. 其他知识
  • python分片

  • 字符串:替换,子字符串,查找

  • str <=> bytes

四、 python websocet server实现

1. 创建主线程,用于实现接受websocket建立请求
if __name__ == "__main__":
    serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    host = ("127.0.0.1", 8124)
    serverSocket.bind(host)
    serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serverSocket.listen(5)
    print("server running")
    while True:
        print("getting connection")
        clientSocket, addressInfo = serverSocket.accept()
        print("get connected")
        receivedData = str(clientSocket.recv(2048))
        # print(receivedData)
        entities = receivedData.split("\\r\\n")
        Sec_WebSocket_Key = entities[11].split(":")[1].strip() + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
        print("key ", Sec_WebSocket_Key)
        response_key = base64.b64encode(hashlib.sha1(bytes(Sec_WebSocket_Key, encoding="utf8")).digest())
        response_key_str = str(response_key)
        response_key_str = response_key_str[2:30]
        # print(response_key_str)
        response_key_entity = "Sec-WebSocket-Accept: " + response_key_str +"\r\n"
        clientSocket.send(bytes("HTTP/1.1 101 Web Socket Protocol Handshake\r\n", encoding="utf8"))
        clientSocket.send(bytes("Upgrade: websocket\r\n", encoding="utf8"))
        clientSocket.send(bytes(response_key_entity, encoding="utf8"))
        clientSocket.send(bytes("Connection: Upgrade\r\n\r\n", encoding="utf8"))
        print("send the hand shake data")
  • 强调多次调用clientSocket.send():因为socket.send()认为”\r\n”即为结束标记,所以对于websocket报文中要求的换行”\r\n”,我们要多次调用clientSocket.send()方法将报文一行一行的发送出去,这也是与python2.x中构建websocket server中很重要的一点,笔者在此处踩坑

  • 下文代码是笔者根据浏览器发送的handshake请求获得Sec_WebSocket_Key的方法,可能在不同的环境中会有差异,调试是可全部打印出websocket请求报文,即*取消注释 “print(receivedData)”*

    Sec_WebSocket_Key = entities[11].split(“:”)[1].strip()

  • 如何验证自己生成的Sec_WebSocket_Accept是正确的。上文提到构建websocket client。可打开“开发者工具”->“网络network”,然后点击”createWebsocket”按钮,得到浏览器发送的报文与回复报文,可以找到一对正确的(Sec_WebSocket_Key, Sec_WebSocket_Accpet)。使用自己的Sec_WebSocket_Accept生成代码将Sec_WebSocket_Key加密,得到结果与正确Sec_WebSocket_Accept相比较,即可确认自己的Sec_WebSocket_Accept生成是否错误
    (key,accpet)键值对

2. 与websocket client 通信

笔者为了简单,就做了回显,即将收到的报文内容自动返回给client

2.1 解析client报文

如上文所述,webscoket client 报文由四部分组成

固定head, 报文长度L,  掩码M, 报文内容C

解析步骤:

  1. 根据报文的第二个字节L确定报文长度所占的字节(1字节=8bit)数B

    • L < 126, B = 1

    • L == 126, B = 2

    • L == 127, B = 4

  2. 掩码M长度为四字节,紧跟在字节长度之后,使用python字符串分片即可获得

  3. 报文内容C为掩码M后面至报文结束的内容
  4. 对报文内容C和掩码M进行按字节循环与操作(见上文)

    #解析报文部分     
    def parse_data(self, data):
        v = data[1] & 0x7f
        if v == 0x7e:
            p = 4
        elif v == 0x7f:
            p = 10
        else:
            p = 2
        mask = data[p: p+4]
        data = data[p+4:]
        print(data)
        i = 0
        raw_str = ""
        for d in data:
            raw_str += chr(d ^ mask[i%4])
            i += 1
        return raw_str
    
2.2 向client发送websocket报文

如上文所述,webscoket server 报文由三部分组成

固定head, 报文长度L, 报文内容C
  • 报文长度小于126时,L占一个字节,L = hex(报文长度)

  • 报文长度小于2^16-1时,L占两个字节,L = hex(126,报文长度)

  • 报文长度小于2^64-1时,L占九个字节,L = hex(126,报文长度)

    #发送websocket server报文部分
    def sendMessage(self, message):
        msgLen = len(message)
        backMsgList = []
        backMsgList.append(struct.pack('B', 129))
    
        if msgLen <= 125:
            backMsgList.append(struct.pack('b', msgLen))
        elif msgLen <=65535:
            backMsgList.append(struct.pack('b', 126))
            backMsgList.append(struct.pack('>h', msgLen))
        elif msgLen <= (2^64-1):
            backMsgList.append(struct.pack('b', 127))
            backMsgList.append(struct.pack('>h', msgLen))
        else :
            print("the message is too long to send in a time")
            return
        message_byte = bytes()
        print(type(backMsgList[0]))
        for c in backMsgList:
            # if type(c) != bytes:
            # print(bytes(c, encoding="utf8"))
            message_byte += c
        message_byte += bytes(message, encoding="utf8")
        #print("message_str : ", str(message_byte))
        # print("message_byte : ", bytes(message_str, encoding="utf8"))
        # print(message_str[0], message_str[4:])
        # self.connection.send(bytes("0x810x010x63", encoding="utf8"))
        self.connection.send(message_byte)
    

五、 资源链接

python websocket server + javascript websocket client

六、 结束语

窘境,思路已经十分的清楚,可是代码上的欠缺导致功能不能实现,还望请读者耐下心来,一点一点的做,笔者也是花了好长时间才做出来的。实在做不出来,可以放下,过几天接着做,相信自己,总会做出来的。

  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值