WebSocket


WebSocket

?
1 2 3 4 5 6 7 8 9 10 import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5) # 等待用户连接 conn, address = sock.accept() ... ... ...

启动Socket服务器后,等待用户【连接】,然后进行收发数据。

2. 客户端连接

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import socket   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5) # 获取客户端socket对象 conn, address = sock.accept() # 获取客户端的【握手】信息 data = conn.recv(1024) ... ... ... conn.send('响应【握手】信息')

请求和响应的【握手】信息需要遵循规则:

  • 从请求【握手】信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端

注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

请求【握手】信息为:

+ ?
1 2 3 4 5 6 7 8 9 10 11 12 GET /chatsocket HTTP/1.1 Host: 127.0.0.1:8002 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://localhost:63342 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits ... ...

提取Sec-WebSocket-Key值并加密:

+ ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import socket import base64 import hashlib   def get_headers(data):     """     将请求头格式化成字典     :param data:     :return:     """     header_dict = {}     data = str(data, encoding='utf-8')       for i in data.split('\r\n'):         print(i)     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[i].split(' ')) == 3:                 header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')         else:             k, v = header_list[i].split(':', 1)             header_dict[k] = v.strip()     return header_dict     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5)   conn, address = sock.accept() data = conn.recv(1024) 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')) ... ... ...

4.客户端和服务端收发数据

a. 基于Python socket实现的WebSocket服务端:

+ ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #!/usr/bin/env python # -*- coding:utf-8 -*- import socket import base64 import hashlib     def get_headers(data):     """     将请求头格式化成字典     :param data:     :return:     """     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[i].split(' ')) == 3:                 header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')         else:             k, v = header_list[i].split(':', 1)             header_dict[k] = v.strip()     return header_dict     def send_msg(conn, msg_bytes):     """     WebSocket服务端向客户端发送消息     :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()     :param msg_bytes: 向客户端发送的字节     :return:     """     import struct       token = b"\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     def run():     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)     sock.bind(('127.0.0.1', 8003))     sock.listen(5)       conn, address = sock.accept()     data = conn.recv(1024)     headers = get_headers(data)     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"       value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'     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:         try:             info = conn.recv(8096)         except Exception as e:             info = None         if not info:             break         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')         send_msg(conn,body.encode('utf-8'))       sock.close()   if __name__ == '__main__':     run()

b. 利用JavaScript类库实现客户端

+ ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 DOCTYPE html> <html> <head lang="en">     <meta charset="UTF-8">     <title>title> head> <body>     <div>         <input type="text" id="txt"/>         <input type="button" id="btn" value="提交" onclick="sendMsg();"/>         <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>     div>     <div id="content">div>   <script type="text/javascript">     var socket = new WebSocket("ws://127.0.0.1:8003/chatsocket");       socket.onopen = function () {         /* 与服务器端连接成功后,自动执行 */           var newTag = document.createElement('div');         newTag.innerHTML = "【连接成功】";         document.getElementById('content').appendChild(newTag);     };       socket.onmessage = function (event) {         /* 服务器端向客户端发送数据时,自动执行 */         var response = event.data;         var newTag = document.createElement('div');         newTag.innerHTML = response;         document.getElementById('content').appendChild(newTag);     };       socket.onclose = function (event) {         /* 服务器端主动断开连接时,自动执行 */         var newTag = document.createElement('div');         newTag.innerHTML = "【关闭连接】";         document.getElementById('content').appendChild(newTag);     };       function sendMsg() {         var txt = document.getElementById('txt');         socket.send(txt.value);         txt.value = "";     }     function closeConn() {         socket.close();         var newTag = document.createElement('div');         newTag.innerHTML = "【关闭连接】";         document.getElementById('content').appendChild(newTag);     }   script> body> html>

6. 基于Tornado框架实现Web聊天室

Tornado是一个支持WebSocket的优秀框架,其内部原理正如1~5步骤描述,当然Tornado内部封装功能更加完整。

以下是基于Tornado实现的聊天室示例:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import json
import tornado.ioloop
import tornado.web
import tornado.websocket


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('index.html')


class ChatHandler(tornado.websocket.WebSocketHandler):
    # 用户存储当前聊天室用户
    waiters = set()
    # 用于存储历时消息
    messages = []

    def open(self):
        """
        客户端连接成功时,自动执行
        :return: 
        """
        ChatHandler.waiters.add(self)
        uid = str(uuid.uuid4())
        self.write_message(uid)

        for msg in ChatHandler.messages:
            content = self.render_string('message.html', **msg)
            self.write_message(content)

    def on_message(self, message):
        """
        客户端连发送消息时,自动执行
        :param message: 
        :return: 
        """
        msg = json.loads(message)
        ChatHandler.messages.append(message)

        for client in ChatHandler.waiters:
            content = client.render_string('message.html', **msg)
            client.write_message(content)

    def on_close(self):
        """
        客户端关闭连接时,,自动执行
        :return: 
        """
        ChatHandler.waiters.remove(self)


def run():
    settings = {
        'template_path': 'templates',
        'static_path': 'static',
    }
    application = tornado.web.Application([
        (r"/", IndexHandler),
        (r"/chat", ChatHandler),
    ], **settings)
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
    run()
app.py
DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python聊天室title>
head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
    div>
    <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">

    div>

    <script src="/static/jquery-2.1.4.min.js">script>
    <script type="text/javascript">
        $(function () {
            wsUpdater.start();
        });

        var wsUpdater = {
            socket: null,
            uid: null,
            start: function() {
                var url = "ws://127.0.0.1:8888/chat";
                wsUpdater.socket = new WebSocket(url);
                wsUpdater.socket.onmessage = function(event) {
                    console.log(event);
                    if(wsUpdater.uid){
                        wsUpdater.showMessage(event.data);
                    }else{
                        wsUpdater.uid = event.data;
                    }
                }
            },
            showMessage: function(content) {
                $('#container').append(content);
            }
        };

        function sendMsg() {
            var msg = {
                uid: wsUpdater.uid,
                message: $("#txt").val()
            };
            wsUpdater.socket.send(JSON.stringify(msg));
        }

script>

body>
html>
index.html

示例源码下载

参考文献:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers