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运行情况
三、 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..)操作的逆操作
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生成是否错误
2. 与websocket client 通信
笔者为了简单,就做了回显,即将收到的报文内容自动返回给client
2.1 解析client报文
如上文所述,webscoket client 报文由四部分组成
固定head, 报文长度L, 掩码M, 报文内容C
解析步骤:
根据报文的第二个字节L确定报文长度所占的字节(1字节=8bit)数B
L < 126, B = 1
L == 126, B = 2
L == 127, B = 4
掩码M长度为四字节,紧跟在字节长度之后,使用python字符串分片即可获得
- 报文内容C为掩码M后面至报文结束的内容
对报文内容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
六、 结束语
窘境,思路已经十分的清楚,可是代码上的欠缺导致功能不能实现,还望请读者耐下心来,一点一点的做,笔者也是花了好长时间才做出来的。实在做不出来,可以放下,过几天接着做,相信自己,总会做出来的。