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

1.通信本质

1.1 握手

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', 8002))
sock.listen(5)

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


# 获取客户端的【握手】信息
data = conn.recv(1024)
print("内容", data)
1
2
3
<script type="text/javascript">
var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
</script>

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

  • 从请求【握手】信息中提取 Sec-WebSocket-Key

    1
    2
    3
    4
    5
    6
    7
    8
    GET /chatsocket HTTP/1.1
    Host: 127.0.0.1:8002
    Upgrade: websocket
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    ...
    ...
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密

  • 将加密结果响应给客户端

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

提取Sec-WebSocket-Key值并 进行hmac1加密,再进行base64加密,返回给浏览器,完成握手。

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
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'))

1.2 收发数据

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

1.接收数据【解包】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
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
22
23
24
25
26
27
The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.

The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.

The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.

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:

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.
Read the next 16 bits and interpret those as an unsigned integer. You're done.
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];
}



Now you can figure out what DECODED means depending on your application.

2.发送数据【封包】

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 <= 0xFFFF:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)

msg = token + msg_bytes
conn.send(msg)
return True

1.3 案例

1.服务端

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()

2.客户端

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>

2.网页案例

image-20240411181614418

2.1 后端API

1
pip install tornado
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
import asyncio
from tornado.web import RequestHandler, Application
from tornado.websocket import WebSocketHandler


class MainHandler(RequestHandler):
def get(self):
self.render("index.html")


class ChatSocketHandler(WebSocketHandler):
waiters = set()

def open(self):
self.waiters.add(self)

def on_close(self):
self.waiters.remove(self)

def on_message(self, message):
print("接收:", message)
for waiter in self.waiters:
# waiter.write_message("xxx")
waiter.write_message(message)


async def main():
handlers = [
(r"/", MainHandler),
(r"/chat", ChatSocketHandler)
]
settings = dict(
cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
template_path="templates",
static_path="static",
xsrf_cookies=True,
)
app = Application(handlers, **settings)
app.listen(address="0.0.0.0",port=8888)
await asyncio.Event().wait()


if __name__ == "__main__":
asyncio.run(main())

2.2 网页

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
<!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="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script type="text/javascript">
$(function () {
wsUpdater.start();
});

var wsUpdater = {
socket: null,
start: function () {
var url = "ws://127.0.0.1:8888/chat";
wsUpdater.socket = new WebSocket(url);
wsUpdater.socket.onmessage = function (event) {
wsUpdater.showMessage(event.data);
}
},
showMessage: function (content) {
var tag = $("<div>").text(content)
$('#container').append(tag);
}
};

function sendMsg() {
wsUpdater.socket.send($("#txt").val());
}

</script>

</body>
</html>

3.APP案例

image-20240411184055256

3.1 后端API

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
import asyncio
from tornado.web import RequestHandler, Application
from tornado.websocket import WebSocketHandler


class ChatSocketHandler(WebSocketHandler):
waiters = set()

def open(self):
print("来了")
self.waiters.add(self)

def on_close(self):
self.waiters.remove(self)

def on_message(self, message):
print("接收:", message)
for waiter in self.waiters:
# waiter.write_message("xxx")
waiter.write_message(message)


async def main():
handlers = [
(r"/chat", ChatSocketHandler)
]
settings = dict(
template_path="templates",
static_path="static",
)
app = Application(handlers, **settings)
app.listen(address="0.0.0.0", port=8888)
await asyncio.Event().wait()


if __name__ == "__main__":
asyncio.run(main())

3.2 APP开发

1
2
3
4
5
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'

//implementation "com.squareup.okhttp3:okhttp:3.4.2"
implementation "com.squareup.okhttp3:okhttp:3.8.1"
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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nb.ws01">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Ws01">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--禁用掉明文流量请求的检查-->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送消息" />

</LinearLayout>
</LinearLayout>
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
package com.nb.ws01;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;

public class MainActivity extends AppCompatActivity {
private Button btn1;
private WebSocket mWebSocket;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initWebSocket();

btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSend();
}
});


}


private void initWebSocket() {
class WsListener extends WebSocketListener {

@Override
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
super.onOpen(webSocket, response);
Log.e("消息", "连接成功!");
}

@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
super.onMessage(webSocket, text);
Log.e("消息", "客户端收到消息:" + text);
}

@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
super.onClosed(webSocket, code, reason);
}

}

new Thread() {
@Override
public void run() {
try {
OkHttpClient mClient = new OkHttpClient.Builder()
.pingInterval(5, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder().url("ws://192.168.1.19:8888/chat").build();
mWebSocket = mClient.newWebSocket(request, new WsListener());

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}

private void doSend(){
mWebSocket.send("hello");
}

}

4.案例:Ws01

4.1 Charles抓包

image-20240411184726821

4.2 模拟请求

1
pip install websocket-client
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
from websocket import WebSocketApp


def on_open(ws, message):
print("on_open")


def on_message(ws, message):
print("on_message", message)


def on_error(ws, message):
pass


def on_close(ws, close_status_code, message):
pass


def run():
ws = WebSocketApp(
url="ws://192.168.1.19:8888/chat",
header={},
cookie="",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.run_forever()


if __name__ == '__main__':
run()

获取数据+发送数据:

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
import threading

from websocket import WebSocketApp


def on_open(ws, message):
print("on_open")


def on_message(ws, message):
print("on_message", message)


def on_error(ws, message):
pass


def on_close(ws, close_status_code, message):
pass


def do_send(ws):
while True:
txt = input("请输入:")
ws.send(txt)

def run():
ws = WebSocketApp(
url="ws://192.168.1.19:8888/chat",
header={},
cookie="",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
t = threading.Thread(target=do_send, args=(ws,))
t.start()

ws.run_forever()


if __name__ == '__main__':
run()

5.案例:京东

版本:v12.6.0

5.1 抓包

image-20240411194438132

5.2 模拟请求(失败)

请求失败,需逆向token

image-20240411194811431

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
from websocket import WebSocketApp


def on_open(ws, message):
print("on_open")


def on_message(ws, message):
print("on_message", message)


def on_error(ws, message):
print("on_error", message)


def on_close(ws, close_status_code, message):
print("on_close",close_status_code, message)


def run():
ws = WebSocketApp(
url="wss://live-ws4.jd.com/?token=amQubWFsbF_muLjlrqJfNTg0NzAwMjkzMTlfMTcxMjgzNTg5MTU5OThoSVJHYg==",
header={},
cookie="",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.run_forever()


if __name__ == '__main__':
run()

错误分析:

1
2
3
4
5
6
7
8
9
from websocket import create_connection

ws = create_connection(
"wss://live-ws4.jd.com/?token=amQubWFsbF_muLjlrqJfNDI5MTk4NzMxMTJfMTcxMjgyNDMzNjY0M1AxcnkzRw=="
)
# ws.send("Hello, World")
result = ws.recv()
print("接收:", result)
ws.close()

image-20240411194926256

image-20240411195035027

6. 笔记

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
抓包-WebSocket

1.关于概念
- http协议、https协议(证书+非对称密钥+对称密钥)、WebSocket
- TCP协议
socket客户端 socket服务端
----
---->
<----
断开

WebSocket存在意义?
- 给浏览器用
- 默认支持:http协议、https协议(请求 -> 响应),无法支持服务端向客户端主动推送消息
- WebSocket协议
浏览器 服务端
---->
<----

- 关于WebSocket常用场景:
- web聊天室
- 直播弹幕

注意:一般情况下WebSocket都是应用在浏览器(网页端)

2.WebSocket协议原理

2.1 握手
GET /xxoo 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
User-Agent: MozillaTML, like Gecko) 37.36\r\n
Upgrade: websocket\r\n
Origin: http://localhost:63342\r\n
Sec-WebSocket-Version: 13\r\n
Accept-Encoding: gzip, deflate, br, zstd\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Sec-WebSocket-Key: PgYOLph6kqlXtQAU3QvuxQ==\r\n 【重要】
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n'

2.2 收发数据(“加密”算法公开)

3.案例:开发聊天室

3.1 后端
pip install tornado

3.2 前端
var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");

4.案例:Ws01(安卓程序)

4.1 后端API(支持ws)

4.2 安卓APP(ok3)
ws://192.168.1.19:8888/chat

5.逆向
- 浏览器抓包
- 安卓应用转包(charles)
- 基于Python发送ws请求
pip install websocket-client

6.案例:京东直播(v12.6.0)

6.1 抓包
wss://live-ws4.jd.com/?token=amQubWFsbF_muLjlrqJfNDE3MDY5MzMxNzNfMTcxMjg0NDIxNTIzNEtuRmNsNA==
wss://live-ws4.jd.com/?token=amQubWFsbF_muLjlrqJfNDIwMTI2OTE0OTFfMTcxMjg0NDI0NTgwOXpYcEdFag==

6.2 请求模拟
on_error --> scheme https is invalid

原因:京东的token是某种算法,不可重用。

总结:
- 【了解】websocket原理(握手+加密)
- 【了解】案例
- 抓包
- websocket-client发送请求
- 在APP端WebSocket少,直接用TCP请求

__END__