一、Websocket原理(握手、解密、加密)

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

  本文将使用Python编写Socket服务端,一步一步分析请求过程!!!

1、启动服务端

1
2
3
4
5
6
7
8
9
10
11
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', 9527))
sock.listen(5)
# 获取客户端socket对象,并等待用户连接
conn, address = sock.accept()
......
......
......

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

2、客户端连接

1
2
3
4
<script type="text/javascript">
    var socket = new WebSocket("ws://127.0.0.1:9527/xxoo");
    ......
</script>

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

3、建立连接【握手】

1
2
3
4
5
6
7
8
9
10
11
12
13
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', 9527))
sock.listen(5)
# 获取客户端socket对象,并等待用户连接
conn, address = sock.accept()
# 获取客户端的【握手】信息,data即握手信息
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
13
14
15
b'GET /xxoo HTTP/1.1\r\n
Host: 127.0.0.1:9527\r\n
Connection: Upgrade\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36\r\n
Upgrade: websocket\r\n
Origin: http://localhost:63342\r\n
Sec-WebSocket-Version: 13\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: session=ba01367c-59b9-41d4-81ba-30b70db282c6\r\n
Sec-WebSocket-Key: jLSLU57WxRJACRQxlN47Tw==\r\n
Sec-WebSocket-Extensions: permessage-deflate;
......'

  提取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
import socket
import base64
import hashlib
 
 
def get_headers(data):
    """
    将请求头格式化成字典
    :param data: 请求头
    :return: 请求头信息字典
    """
    header_dict = {}
    header_str = data.decode("utf8")
    for i in header_str.split("\r\n"):
        if str(i).startswith("Sec-WebSocket-Key"):
            header_dict["Sec-WebSocket-Key"] = i.split(":")[1].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', 9527))
sock.listen(5)
# 获取客户端socket对象,并等待用户连接
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://127.0.0.1:9527\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'))
# 响应【握手】信息
conn.send(response_str.encode("utf-8"))
......
......

  至此,客户端与服务端完成握手过程。

4、客户端与服务端收发数据

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

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

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
# b'\x81\x86#\xa47\x93\xc7\x19\x97v\x86\x19'     你好
 
hashstr = b'\x81\x83\xceH\xb6\x85\xffz\x85'
# b'\x81  \x86#   \xa47\x93\xc7\x19    \x97v\x86\x19'
 
# 将第二个字节 \x86# 也就是第9-16位 与 127 进行位运算
payload_len = hashstr[1] & 127
 
if payload_len == 127:
    extend_payload_len = hashstr[2:10]
    mask = hashstr[10:14]
    decoded = hashstr[14:]
# 当位运算结果等于127时,则第3-10个字节为数据长度
# 第11-14字节为mask ,即解密所需字符串
# 则数据为第15字节至结尾
 
if payload_len == 126:
    extend_payload_len = hashstr[2:4]
    mask = hashstr[4:8]
    decoded = hashstr[8:]
# 当位运算结果等于126时,则第3-4个字节为数据长度
# 第5-8字节为mask ,即解密所需字符串
# 则数据为第9字节至结尾
 
if payload_len <= 125:
    extend_payload_len = None
    mask = hashstr[2:6]
    decoded = hashstr[6:]
# 当位运算结果小于等于125时,则这个数字就是数据的长度
# 第3-6字节为mask ,即解密所需字符串
# 则数据为第7字节至结尾
 
str_byte = bytearray()
 
for i in range(len(decoded)):   # 循环数据长度
    byte = decoded[i] ^ mask[i % 4# ^ 是或运算
    str_byte.append(byte)
 
print(str_byte.decode("utf8"))

  解包详细过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 == 126:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)
 
    msg = token + msg_bytes
    conn.send(msg)
    return True

二、基于Python实现简单示例

1、基于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
# !/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import base64
import hashlib
 
 
def get_headers(data):
    """
    将请求头格式化成字典
    :param data: 请求头
    :return: 请求头信息字典
    """
    header_dict = {}
    header_str = data.decode("utf8")
    for i in header_str.split("\r\n"):
        if str(i).startswith("Sec-WebSocket-Key"):
            header_dict["Sec-WebSocket-Key"] = i.split(":")[1].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 == 126:
        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', 9527))
    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://127.0.0.1:9527\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'))
    # 响应【握手】信息
    conn.send(response_str.encode("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()

2、利用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:9527/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>

参考博客(一):http://www.runoob.com/html/html5-websocket.html

参考博客(二):https://www.cnblogs.com/fuqiang88/p/5956363.html

参考博客(三):https://baijiahao.baidu.com/s?id=1617829980923673614&wfr=spider&for=pc

WebsSocket的更多相关文章

  1. webssocket简介-服务器可以主动传送数据给浏览器

    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并 ...

  2. 7.1.1.关闭WebSocket连接

    7.1.定义 7.1.1.关闭WebSocket连接 为_关闭WebSocket连接_,端点需关闭底层TCP连接.端点应该使用一个方法完全地关闭TCP连接,以及TLS会话,如果合适,丢弃任何可能已经接 ...

  3. 开源的物联网技术平台(Thingsboard)

    1   总体说明 1.1   产品概述 1.1.1 Thingsboard作用 1.置备并控制设备. 2.采集设备数据并进行数据可视化. 3.分析设备数据,触发告警. 4.将数据传输到另一个系统. 5 ...

随机推荐

  1. 原生JS发送Ajax请求、JSONP

    一.JS原生Ajax Ajax=异步Javascript+XML: ajax是一种数据请求的方式,不需要刷新整个页面.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新. ajax的核心 ...

  2. input 更改 pleaseholder 的字体样式

    /* WebKit browsers */input::-webkit-input-placeholder {    color: red;    font-size: 14px;}/* Mozill ...

  3. npm设置成淘宝镜像

    1.淘宝 npm 网址 https://npm.taobao.org/ 2.修改 2.1 通过命令配置 2.1.1. 命令 npm config set registry https://regist ...

  4. Spring boot dubbo+zookeeper 搭建------基于gradle项目的消费端与服务端分离实战

    1. Dubbo简介 Dubbo是Alibaba开源的分布式框架,是RPC模式的一种成熟的框架,优点是可以与Spring无缝集成,应用到我们的后台程序中.具体介绍可以查看Dubbo官网. 2. Why ...

  5. 11 - Vue模板语法

    Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据. 所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML ...

  6. 用jackson的@JsonProperty注解属性名,会多出一个字段

    遇见了这个情况,我的字段定义是xVal,yVal,用的lombok的@Data注解.然后查询到了下面这偏文章,https://bbs.csdn.net/topics/392305619,里面的回答是图 ...

  7. Codeforces Round #595 (Div. 3) A,B,C,D

    A题:n个学生,分成任意组,要求每组中任意两名学生的技能值相差不等于1,问最小分组. #include<bits/stdc++.h> using namespace std; #defin ...

  8. GO111MODULE的设置(及GOPROXY)

    环境:win7  go1.13.1 早听说GO111MODULE大名,今天才测试成功,步骤如下: 因为我的Go version >= 1.13,直接用go env -w 设置(注意大小写) go ...

  9. CF812C Sagheer and Nubian Market 二分+贪心

    模拟赛给他们出T1好了~ code: #include <bits/stdc++.h> #define ll long long #define N 100006 #define setI ...

  10. Visual Studio Code:使用技巧汇总

    造冰箱的大熊猫@cnblogs 2019/8/9(最后更新2019/8/11) 试用了下微软出的Visual Studio Code,哎呀,有点小惊喜 将VSC的快捷键小结一下,不定期更新 功能 快捷 ...