WebSocket协议

Posted 小辉python

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebSocket协议相关的知识,希望对你有一定的参考价值。

a.http是一个协议。
- 数据格式
- 一次请求和响应之后断开连接(短连接、无状态)

b. 服务端可以向客户端主动推送消息吗?不可以

c. 服务端只能做出响应。

d. 为了伪造服务端向客户端主动推送消息的效果,我们使用:轮询和长轮询。

轮询的,就用一个定时器,2秒不断的发送请求。

长轮询--没有数据的时候会hand住;在hand住期间有数据就马上返回,这样不断的循环发送请求。

#用长轮询做   在线投票(实时跟新)
#微信通信也是用的这个,只要是 实时 和 在线 都是基于长轮询的;
#长轮询就是 没有信息(投票)的时候就hand住,最多hand住20秒;然后在发送请求,不断的循环,
#只要在hand住的时候有数据,就会马上把数据返回给用户。
#相比 轮询   连接少了,没有延迟性。

#投选谁最帅
User_list={
	\'1\':{"name":\'小辉\',\'count\':1},
	\'2\':{"name":\'小红\',\'count\':1},
	\'3\':{"name":\'小黑\',\'count\':1},
}

Queue_list={
	#\'uuid\': Queue()    给每一个用户创建一个队列,这样就没数据就可以hand住,有数据就马上显示了。
}


@ac.route(\'/user/list\',methods=[\'GET\'])
def user():
	user_uuid = str(uuid.uuid4())
	Queue_list[user_uuid] = queue.Queue()

	session[\'current_uid\'] = user_uuid

	return render_template(\'user.html\',user=User_list)


@ac.route(\'/vote\',methods=[\'POST\'])
def vote():
	uid = request.form.get(\'uid\')
	User_list[uid]["count"] +=1
	#只要是用户投票后就马上的返回;
	for q in Queue_list.values():
		q.put(User_list)
	return \'投票成功\'


@ac.route(\'/getvote\', methods=[\'GET\'])
def getvote():
	#获取票数的时候,如果没有人投就hand住,直到有人投了。
	uid = session[\'current_uid\']
	q = Queue_list[uid]
	ret = {\'status\': True, \'data\': None}
	try:
		user = q.get(timeout=20)
		ret[\'data\'] = user
	#没有数据的话,就报以下错误。
	except queue.Empty:
		ret[\'status\'] = False

	return jsonify(ret)
<script>

    getVote();

    $(\'#vote\').on(\'click\', \'.btn\', function () {
        var uid = $(this).attr(\'uid\');
        $.ajax({
            url: \'http://127.0.0.1:5000/api/vote\',
            method: \'POST\',
            data: {\'uid\': uid},
            dataType: \'json\',
            success: function (arg) {
                console.log(arg);

            }

        });

    });
    /*
        获取投票信息
     */
    function getVote() {
        $.ajax({
            url: \'http://127.0.0.1:5000/api/getvote\',
            method: \'GET\',
            dataType: \'json\',
            success: function (arg) {
                //先清空列表
                if (arg.status) {
                    $(\'#vote\').empty();
                    $.each(arg.data, function (k, v) {
                        var li = document.createElement(\'li\');
                        li.setAttribute(\'class\', \'btn\');
                        li.setAttribute(\'uid\', k);
                        li.innerText = \'编号\' + k + \': \' + v.name + \'(\' + v.count + \')\';
                        $(\'#vote\').append(li)
                    });
                }
                //没有投票记录,就再发送请求,没有就hand住 这样类似递归,不断的接收实时数据。
                getVote();
            }
        });

    }

    //做轮询的,就用一个定时器,2秒不断的发送请求。
    // setInterval(getVote,1000)
</script>

  

因此推出了新的一种协议websocket协议用来解决Http协议的不足。

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

1. 什么是websocket?是一套基于http的协议,协议规定了:
- 连接时需要握手
- 发送数据进行加密
- 连接之后不断开

2. websocket的意义?
服务端可以真正的做到向 客户端 发送消息。

3. websocket的兼容性是他的缺点。

4. 应用场景?
实时响应页面时,可以使用websocket。服务端向客户端发送消息

基于flask的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\':\'钢弹\',\'count\':0},
    \'2\':{\'name\':\'铁锤\',\'count\':0},
    \'3\':{\'name\':\'贝贝\',\'count\':100},
}


# http://127.0.0.1:5000/index
@app.route(\'/index\')
def index():
    return render_template(\'index.html\',users=USERS)

# http://127.0.0.1:5000/message
WEBSOCKET_LIST = []
@app.route(\'/message\')
def message():
    ws = request.environ.get(\'wsgi.websocket\')
    if not ws:
        print(\'http\')
        return \'您使用的是Http协议\'
    WEBSOCKET_LIST.append(ws)
    while True:
        cid = ws.receive()
        if not cid:
            WEBSOCKET_LIST.remove(ws)
            ws.close()
            break
        old = USERS[cid][\'count\']
        new = old + 1
        USERS[cid][\'count\'] = new
        for client in WEBSOCKET_LIST:
            client.send(json.dumps({\'cid\':cid,\'count\':new}))



if __name__ == \'__main__\':
    http_server = WSGIServer((\'0.0.0.0\', 5000), app, handler_class=WebSocketHandler)
    http_server.serve_forever()
 <script src="{{ url_for(\'static\',filename=\'jquery-3.3.1.min.js\')}}"></script>
    <script>
        var ws = new WebSocket(\'ws://192.168.13.253: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>

 

通过socket代码来展现出websocket原理:

  1.服务端运行,等待客户端连接

import socket
# 1. 服务端运行,等待客户端连接
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((\'127.0.0.1\', 8000))
sk.listen(5)
conn, addr = sk.accept()

  2.web来连接,服务端同意。当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!web立即向服务端发送一个“握手信息” data;

<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
    ...
</script>

 解析握手信息,并将请求头格式化成字典

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

# 2.来连接,服务端同意。web立即发送一个“握手信息” data;
data = conn.recv(1024)
headers = get_headers(data)

 

# \'\'\'
# “握手信息” data
# GET /xxxx HTTP/1.1\\r\\n
# Host: 127.0.0.1:8002\\r\\n
# Connection: Upgrade\\r\\n
# Pragma: no-cache\\r\\n
# Cache-Control: no-cache\\r\\n
# Upgrade: websocket\\r\\n
# Origin: http://localhost:63342\\r\\nSec-WebSocket-Version: 13\\r\\n
# User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36\\r\\n
# Accept-Encoding: gzip, deflate, br\\r\\n
# Accept-Language: zh-CN,zh;q=0.9\\r\\n
# Cookie: csrftoken=ojyruuaF3Tk0OToIrXy1sRSdSk3SeDgd6Ti3jocEXAuEExaMtxjhJglpenj6Iq8F\\r\\n
# Sec-WebSocket-Key: 4NZY2fTOr691upgWe2yq7w==\\r\\n ########  这里 ########
# Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\\r\\n\\r\\n
# \'\'\'

 

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

  • 从请求【握手】信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端
# 3.服务端接收握手信息后需要对数据进行加密,给客户端返回
# Sec-WebSocket-Key: aMBECUXBSDW6YHJwl/eeyg==\\r\\n;magic_string = \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\'
# 加密方式: value = aMBECUXBSDW6YHJwl/eeyg== + magic_string

value = headers[\'Sec-WebSocket-Key\'] + \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\'
ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest())

 4.服务端接收握手信息后需要对数据进行加密,给客户端返回  

# 给客户端返回,返回的响应也要遵守websocket协议
# "Sec-WebSocket-Accept: 放入加密后的结果(是字节形式的)\\r\\n"
response_tpl = \\
	"HTTP/1.1 101 Switching Protocols\\r\\n" \\
	"Upgrade:websocket\\r\\n" \\
	"Connection: Upgrade\\r\\n" \\
	"Sec-WebSocket-Accept: {}\\r\\n" \\
	"WebSocket-Location: ws://127.0.0.1:8002\\r\\n\\r\\n".format(ac.decode(\'utf-8\'))
conn.send(bytes(response_tpl, encoding=\'utf-8\'))

返回响应后没有报错就表示已经连接成功了,双方可以进行互相通信:

客户端和服务端收发数据

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

第一步:获取客户端发送的数据【解包】

 读取第二个字节的后7位 :info[1]

127:10,4,数据
126:4,4,数据
<=125: 2,4,数据

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)

 

Decoding Payload Length

To read the payload data, you must know when to stop reading. That\'s why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:

  1. Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it\'s 125 or less, then that\'s the length; you\'re done. If it\'s 126, go to step 2. If it\'s 127, go to step 3.
  2. Read the next 16 bits and interpret those as an unsigned integer. You\'re done.
  3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You\'re done.

Reading and Unmasking the Data

If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let\'s call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):

 

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

 

第二步:向客户端发送数据【封包】

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

 

基于python scoket实现的webscoket服务端:

# -*- coding: utf-8 -*-
# @Author: 曾辉
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


# 1. 服务端运行,等待客户端连接
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((\'127.0.0.1\', 8000))
sk.listen(5)
conn, addr = sk.accept()

# 2.来连接,服务端同意。web立即发送一个“握手信息” data;
data = conn.recv(1024)
headers = get_headers(data)

# \'\'\'
# “握手信息” data
# GET /xxxx HTTP/1.1\\r\\n
# Host: 127.0.0.1:8002\\r\\n
# Connection: Upgrade\\r\\n
# Pragma: no-cache\\r\\n
# Cache-Control: no-cache\\r\\n
# Upgrade: websocket\\r\\n
# Origin: http://localhost:63342\\r\\nSec-WebSocket-Version: 13\\r\\n
# User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36\\r\\n
# Accept-Encoding: gzip, deflate, br\\r\\n
# Accept-Language: zh-CN,zh;q=0.9\\r\\n
# Cookie: csrftoken=ojyruuaF3Tk0OToIrXy1sRSdSk3SeDgd6Ti3jocEXAuEExaMtxjhJglpenj6Iq8F\\r\\n
# Sec-WebSocket-Key: 4NZY2fTOr691upgWe2yq7w==\\r\\n ########  这里 ########
# Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\\r\\n\\r\\n
# \'\'\'
# 3.服务端接收握手信息后需要对数据进行加密,给客户端返回
# Sec-WebSocket-Key: aMBECUXBSDW6YHJwl/eeyg==\\r\\n;magic_string = \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\'
# 加密方式: value = aMBECUXBSDW6YHJwl/eeyg== + magic_string

value = headers[\'Sec-WebSocket-Key\'] + \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\'
ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest())

# 给客户端返回,返回的响应也要遵守websocket协议
# "Sec-WebSocket-Accept: 放入加密后的结果(是字节形式的)\\r\\n"
response_tpl = \\
	"HTTP/1.1 101 Switching Protocols\\r\\n" \\
	"Upgrade:websocket\\r\\n" \\
	"Connection: Upgrade\\r\\n" \\
	"Sec-WebSocket-Accept: {}\\r\\n" \\
	"WebSocket-Location: ws://127.0.0.1:8002\\r\\n\\r\\n".format(ac.decode(\'utf-8\'))
conn.send(bytes(response_tpl, encoding=\'utf-8\'))

# 返回响应后没有报错就表示已经连接成功了

# 给客户端发送消息
send_msg(conn, b\'123123123\')
while True:
	# 服务端接收消息
	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)

Javascript实现的客户端

<!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>

  

 

客户端接收发消息

<script>
    var ws = new WebSocket("ws://127.0.0.1:8000/xxx");
    ws.onmessage = function (event) {
          /* 服务器端向客户端发送数据时,自动执行 */
        console.log(event.data)
    //event.data是发送过来的数据
    };
  ws.send(123)//是发送消息 </script>   


以上是关于WebSocket协议的主要内容,如果未能解决你的问题,请参考以下文章

Websocket入门

动手实践,即时通讯WebSocket的代码实现

C#-WebSocket协议通讯_Net5

Watson语音到文本 - 无法构造'WebSocket':URL包含片段标识符

NodeJS - 使用协议 HTTPS 建立连接 WebSocket

WebSocket服务端和客户端代码示例