websocket

WebSocket、HTTP 与 TCP

HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。我们可以把这些高级协议理解成对 TCP 的封装。
既然大家都使用 TCP 协议,那么大家的连接和断开,都要遵循 TCP 协议中的三次握手和四次挥手 ,只是在连接之后发送的内容不同,或者是断开的时间不同.
我们知道http是基于tcp实现的,浏览器可以看做是客户端socket,这个socket在开发的时候遵循http规范,数据在发送并接收成功的时候就断开socket了,那么如果我们的socket一直连接不断开,并且发送数据的时候都把数据进行加密,这样是不是也是可以的呢?这种想法就可以引出websocket了,值得一提的是,websocket的兼容性不是所有版本的浏览器都支持(取决于是否能够new WebSocket), 但是随着技术的发展,浏览器对websocket的支持肯定会越来越好。
对于 WebSocket 来说,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
Websocket和socket不同,Websocket工作在应用层,而socket是基于门面模式的一种封装,可以让程序员简便地写网络通信程序

flask实现websocket

安装gevent-websocket模块

from flask import Flask,render_template,request
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json

app = Flask(__name__)

USERS = {
    '1':{'name':'jack','count':0},
    '2':{'name':'tuple','count':0},
    '3':{'name':'lily','count':100},
}

@app.route('/index')
def index():
    return render_template('index.html',users=USERS)

# http://127.0.0.1:5000/message  既可以接受http,也可以接受websocket
WEBSOCKET_LIST = []
@app.route('/message')
def message():
    ws = request.environ.get('wsgi.websocket')
    if not ws:
        return '您使用的是Http协议'
    WEBSOCKET_LIST.append(ws)
    while True:
        # 当关闭webscoekt的时候cid 得到None
        cid = ws.receive()
        if not cid:
            WEBSOCKET_LIST.remove(ws)
            ws.close()
            break
        old_count = USERS[cid]['count']
        new_count = old_count + 1
        USERS[cid]['count'] = new_count
        for client in WEBSOCKET_LIST:
            client.send(json.dumps({'cid':cid,'count':new_count}))
    return ''

if __name__ == '__main__':
    # 当请求是websocket的时候,那么就用WebSocketHandler进行处理
    http_server = WSGIServer(('0.0.0.0', 5000), app, handler_class=WebSocketHandler)
    http_server.serve_forever()

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>投票系统</h1>
    <ul>
        {% for k,v in users.items() %}
            <li onclick="vote({{k}})" id="id_{{k}}">{{v.name}}<span>{{v.count}}</span></li>
        {% endfor %}
    </ul>

    <script src="{{ url_for('static',filename='jquery-3.3.1.min.js')}}"></script>
    <script>
        var ws = new WebSocket('ws://127.0.0.1:5000/message');
        // 事件监听绑定回调函数,一旦有消息发送过来就会调用函数
        ws.onmessage = function (event) {
            /* 服务器端向客户端发送数据时,自动执行 */
            // {'cid':cid,'count':new}
            var response = JSON.parse(event.data);
            $('#id_'+response.cid).find('span').text(response.count);

        };

        function vote(cid) {
            ws.send(cid)
        }
    </script>
</body>
</html>

webscoket 原理

websocket的握手需要借助http, new WebSocket的时候,服务端接收到的的请求头信息有有三个比较特殊:

Sec-WebSocket-Key:FUH2Nige4Npq/InFS0OoJQ==
Upgrade:websocket
Connection: Upgrade

表示这次请求是用于升级http为websocket,并把一个随机字符串发送过去,此时服务端应该拿到这个字符串进行加密然后发送过去,浏览器对这个字符串也加密,比较加密后的字符串和服务端发送过来的字符串是否一致,一致就表名握手成功,此时response header为

Connection:Upgrade
Sec-WebSocket-Accept:+lZCKceYfJsmNj6q0GYPa9r9LXE=
Upgrade:websocket

状态码为:101 Switching Protocols,表示协议切换成功,此时,后面的通信就和http没有半毛钱关系了。

自定义实现websocket服务端

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)


import base64
import hashlib
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)

while 1:
    # 1. 等待用户连接
    conn, address = sock.accept()

    # 2. 接收验证消息
    msg = conn.recv(8096)
    msg_dict = get_headers(msg)

    # 3. 对数据加密, 其中魔法字符串'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 是固定的值
    value = msg_dict['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())

    # 4. 将加密后的结果返回给浏览器,发送的时候是用http通信的,回消息的时候也要封装成http格式
    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://127.0.0.1:8002\r\n\r\n"
    response = response_tpl %(ac.decode('utf-8'),)
    conn.send(response.encode('utf-8'))

    # 5. 接受浏览器发送过来的加密数据,并进行解密,发送数据也需要进行加密,加密和解密规则可以参考官方
    while True:

        info = conn.recv(8096)
        # 10111111 去掉第一位,和 01111111 相与就行
        payload_len = info[1] & 127
        if payload_len == 127:
            # 如果payload_len是127,那么需要往后面取8个字节作为头信息,头部总共10个字节
            extend_payload_len = info[2:10]
            mask = info[10:14]
            decoded = info[14:]
        elif payload_len == 126:
            # 如果payload_len是126,那么需要往后面取2个字节作为头信息,头部总共4个字节
            extend_payload_len = info[2:4]
            mask = info[4:8]
            decoded = info[8:]
        else:
            # 如果payload_len小于126,那么头部总共2个字节
            extend_payload_len = None
            mask = info[2:6]
            decoded = info[6:]

        bytes_list = bytearray()
        # decoded 是真实的数据
        for i in range(len(decoded)):
            chunk = decoded[i] ^ mask[i % 4]
            bytes_list.append(chunk)
        body = str(bytes_list, encoding='utf-8')
        print('浏览器发送过来的数据',body)
        #
        # import  time
        # time.sleep(2)
        send_msg(conn,body.encode('utf8'))

posted @ 2018-09-02 15:50  龙云飞谷  阅读(229)  评论(0编辑  收藏  举报