WebSocket

一、介绍

WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信

全双工指的是数据可以同时在两个方向上传输

WebSocket为应用层协议,其定义在TCP/IP协议栈之上。WebSocket连接服务器的URI以"ws"或者"wss"开头。ws开头的默认TCP端口为80,wss开头的默认端口为443

二、为什么用websocket

因为 HTTP 协议有一个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的

 

websocket没有同源限制,客户端可以与任意服务器通信

 

三、流程

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

2、当客户端向服务端发送连接请求时,不仅会发送连接请求还会发送【握手】信息,并等待服务端响应,至此连接才创建成功

2.1、

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 进行hashlib.sha1加密,再进行base64加密
  • 将加密结果响应给客户端

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

 

 总的来说就是:客户端建立连接请求并发送随机字符串,Sec-WebSocket-Key就是随机字符串,服务端对这个随机字符串进行加密,加密的方法就是随机字符串加上魔法字符串,然后进行sha1和base64加密,把这个加密好的字符串返回给客户端,客户端校验成功后就代表连接成功,并且执行onopen方法

 

 

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

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'))
...
...
...
加密随机字符串过程

 

3、建立好连接后浏览器和服务端就可以相互收发数据了

客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现

 (1)浏览器向服务端发送数据,服务端解包过程

 

 先获取到数据的第2个字节和127做与运算,得到的就是前7位,前7为若是小于等于125的话则代表请求头就是前2个字节,后四个字节是mask_key,最后是真实数据;若是等于126则代表请求头就是前4个字节,后四个字节仍是mask_key,最后是真实数据;若是等于127则代表前10个字节是请求头,后4个字节是mask_key,最后是真实的数据,以此来得到最后保存真实数据的内容,注意此时得到的真实数据还要和mask_key 做运算得到的结果才是浏览器发送的最原始数据

 

info = conn.recv(8096)

    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')
    print(body)

基于Python实现解包过程(未实现长内容)
基于Python实现解包过程(未实现长内容)

 

(2)服务端向浏览器发送数据:服务端封包过程

也是会加请求头

浏览器就会调用onmessage方法执行

 

(3)服务端主动断开连接

触发浏览器的onclose方法执行

 

 

 

 

 

 

举例:基于websocket的实时投票系统

服务端:

from flask import Flask,render_template,request,session,redirect,jsonify
import uuid
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json


app = Flask(__name__)
app.secret_key = 'xfsdfqw'

USERS = {
    '1':{'name':'王旭','count':0},
    '2':{'name':'放景洪','count':0},
    '3':{'name':'六五','count':0},
}


@app.before_request
def before_request():
    if request.path == '/login':
        return None
    user_info = session.get('user_info')
    if user_info:
        return None
    return redirect('/login')


@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == "GET":
        return render_template('login.html')
    else:
        uid = str(uuid.uuid4())
        session['user_info'] = {'id':uid,'name':request.form.get('user')}
        return redirect('/index')


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

WS_DICT = {

}

@app.route('/message')
def message():
    if request.environ.get('wsgi.websocket'):
        ws = request.environ['wsgi.websocket']
        # 1. 刚连接成功
        uid = session.get('user_info').get('id')
        WS_DICT[uid] = ws

        from geventwebsocket.websocket import WebSocket
        while True:
            # 2. 等待用户发送消息,并接受
            message = ws.receive()
            # 关闭:message=None
            if not message:
                del WS_DICT[uid]
                break

            old = USERS[message]['count']
            new = old + 1
            USERS[message]['count'] = new

            data = {'user':message,'count':new}

            for k,v in WS_DICT.items():
                # 3. 向客户端推送消息
                v.send(json.dumps(data))

    return "Connected!"

if __name__ == '__main__':
    http_server = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler)
    http_server.serve_forever()

 

浏览器:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>投票系统</h1>
    <a onclick="closeConn();">关闭连接</a>
    <a onclick="createConn();">创建连接</a>
    <ul>
        {% for k,v in users.items() %}
            <li id="user_{{k}}"  ondblclick="vote('{{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 socket = null;

        function socketInit() {
            socket.onopen = function () {
            /* 与服务器端连接成功后,自动执行 */
        };

            socket.onmessage = function (event) {
                /* 服务器端向客户端发送数据时,自动执行 */
                var response = JSON.parse(event.data); // {'user':1,'count':new}
                var nid = '#user_' + response.user;
                $(nid).find('span').text(response.count)
            };

            socket.onclose = function (event) {
                /* 服务器端主动断开连接时,自动执行 */
            };

        }

        /*
        我要投票
        id:帅哥id
         */
        function vote(id) {

            socket.send(id);
        }

        function closeConn() {
            socket.close()
        }
        function createConn() {
            socket = new WebSocket("ws://127.0.0.1:5000/message");
            socketInit();
        }
    </script>
</body>
</html>

 

posted @ 2018-04-08 15:44  九二零  阅读(582)  评论(0编辑  收藏  举报