关于Websocket的报文分析,个人理解及实践

博客建起来的第二天,总想着要写些什么东西。刚好最近在公司做的项目有关于websocket的一些功能。遂借着这次机会,把自己知道的东西好好的梳理一下。

1、Websocket 概述

1.1 websocket 是为何诞生的?

作为一名做web开发的工程师,我们总会碰到,总会出现一些需求。比如:

  • 用户购买完商品后。需要对用户商品的物流信息做及时推送
  • 炒股软件中股价的实时变动
  • 游戏或这体育赛事的事件变动

这些功能中,信息变动是的时间是不固定的,它们随时都有可能变动。在传统的基于http协议开发流程中,我们知道,http协议是无状态的,服务端并不会保存客户端的状态。它总是被动接受请求,然后做出响应。于是给出的解决方案一般都是,基于前端页面的ajax轮循请求。通过定时请求,保证了页面数据的“自动”更新的功能。

但是就算如此数据也还是没有达到“即时”通知的效果,数据更新的过程中,最差的延迟情况就是定时请求刷新的时间长度。

那有人会说:我们可以设置1秒请求一次,这样就保证的减少了数据的延迟时间。但这样服务器端的压力也就大了许多。并且每次请求到要建立连接,都要做tcp三次握手(当然可以通过添加请求头 Connection: keep-alive的方式优化)。这样就无端浪费了很多计算资源。

于是有人就想:为什么不能有一种协议,请求建立后保持连接,让服务器主动发送消息。于是websocket就就出现了

1.2 什么是websocket?

以下参照百度百科的介绍:

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket允许服务端主动向客户端推送数据。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

1.2.1、报文分析

Websocket的请求是基于http协议升级而来的。我们来看看二者的请求报文

HTTP请求:

1
2
3
4
5
6
7
8
9
10
# 请求
GET http://example.com/api HTTP/1.1
Host: htp.example.com # 请求接受的主机名
Connection: Keep-Alive
Accept-Encoding: gzip, deflate, br # 通知服务器可以接受的编码格式
Accept-Language: zh-CN,zh;q=0.9 # 通知服务器可以接受的语言
Origin: http://example.com
Host: test.example.com
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36

Websocket请求:

1
2
3
4
5
6
GET ws://example.com/ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: pPGMRQbgnTPFfT18zSoEqg==
Sec-WebSocket-Version: 13

通过对比我们发现 websocket 请求多了以下几个字段

1
2
3
4
5
Connection: Upgrade                         #我要升级协议
Upgrade: websocket #升级成 websocket 协议
Sec-WebSocket-Version: 13 #协议版本为13号
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: pPGMRQbgnTPFfT18zSoEqg==

注意:

  • websocket 必须是get请求且协议版本不小于1.1
  • 必须包括”Sec-WebSocket-Key”头域,其值采用base64编码的随机16字节长的字符序列

那么基于这个请求报文,响应报文如下:

1
2
3
4
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Sec-WebSocket-Accept: 7R4MOvm39xX694unFsImAPKLCaU=
Upgrade: websocket

Sec-WebSocket-Accept 是必须包括的字段,该字段是由请求报文中Sec-WebSocket-Key的值拼接上 Websocket协议中特殊字符串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11”后,做sha-1加密在base64编码后的一串字符

就此Websocket连接建立完毕。后续连接的保持则是需要”心跳包”机制来作为验证的。

1.2.2、心跳机制

websocket连接完成后,服务器也需要检测对端的连接状态,中途是否断开是否无响应。此时为了检验两端的响应状态,需要在规定时间范围内向对端发送数据,证明双方连接正常。若有一端未在规定时间内接到数据,则判断连接异常断开连接

心跳机制只需要双方定时给对端发送数据集合完成验证,后面将会给出事例代码

2、Websocket 功能实现

由于我是写php的,所以后台websocket功能由swoole来实现

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
<?php
/**
* server.php
* swoole 实现websocket,监听本机9500端口
*/
class Server
{
function __construct()
{
$server = new swoole_websocket_server("127.0.0.1", 9500);
// 设置心跳检测时间
$server->set([
"heartbeat_check_interval" => 5,
"heartbeat_idle_time" => 10
]);

$server->on('open', [$this, "open"]);
$server->on('message', [$this, "message"]);
$server->on('close', [$this, "close"]);

$server->start();
}

// 连接开启时打印连接fd
public function open($server, $req)
{
echo "connection open: {$req->fd}\n";
}

// 接收并发送消息
public function message($server, $frame)
{
echo "received message: {$frame->data}\n";
$msg = "you send message is:{$frame->data}" . PHP_EOL;
$server->push($frame->fd, $msg);
}

// 连接关闭时打印连接fd
public function close($server, $fd)
{
echo "connection close: {$fd}\n";
}
}

$server = new Server();

使用 php-cli 运行此脚本开启服务,监听9500端口

sudo php server.php

由于服务端设置了心跳机制,所以客户端在保持连接时得定时向服务器发送”心跳包”,表示连接正常.下面是客户端代码:

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
/**
* client.html
* 客户端由js实现
*/
var ws = new WebSocket("ws://127.0.0.1:9500")
// 事件回调
ws.onopen = function (e) {
console.log("connention was opened")
ws.send("hello");
}

ws.onmessage = function (e) {
console.log(e)
}

ws.onclose = function (e) {
console.log(e)
}

ws.error = function (e) {
console.log(e)
}

var time = 0
// 计时
start = setInterval(function () {
console.log(++time)
},1000)

// 心跳
heart = setInterval(() => {
ws.send("ping")
}, 5000);

打开chart.html在控制台即可看见相关情况

3、总结

以上用swoole简单的实现了一个websocket服务器.配合上消息队列如:rabbitmq, kafka也可以达到推送最新消息的功能.Websocket服务不仅可用于开发聊天室,还能用于开发物联网应用等.

本次后台事例使用了swoole实现,后续会陆续出自己关于swoole的一些理解