一、什么是WebSocket
WebSocket
是HTML5
开始提供的一种在单个TCP
连接上进行全双工通讯的协议。WebSocket
使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。- 在
WebSocket Api
中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
二、WebSocket
的作用
主要用于客户端和服务端的双向通讯,一般的使用场景有社交聊天、多人游戏、体育实况更新、弹幕等高实时性应用。
三、WebSocket
出现的背景是什么
- 在
WebSocket
出现以前,最常用的技术就是轮询获取服务器资源,轮询分普通轮询和长轮询- 普通轮询就是通过客户端不断的请求服务端来获取数据,这种方式效率低下,每次都需要建立连接,释放连接,且只有在服务端有需要的数据的时候,请求才是真正有效的请求,其他大部分的请求都是试探性的不必要的请求。这样就造成了资源的浪费,并且对实时性要求较高的应用很不友好。
客户端:有没有消息 服务端:没有 客户端:有没有消息 服务端:没有 客户端:有没有消息 服务端:没有 客户端:有没有消息 服务端:有,消息是... 客户端:有没有消息 服务端:没有
- 长轮询是通过客户端请求,服务端阻塞住请求,当有数据的时候就返回给客户端,这种方式虽然实时性较好,但是却占用了服务端的资源,因为是在服务端进行阻塞,所以对并发并不友好,只能用于小企业内部少量使用。
客户端:有消息你通知我啊 服务端:好的,有消息我通知你 服务端:有消息了,消息是...
- 普通轮询就是通过客户端不断的请求服务端来获取数据,这种方式效率低下,每次都需要建立连接,释放连接,且只有在服务端有需要的数据的时候,请求才是真正有效的请求,其他大部分的请求都是试探性的不必要的请求。这样就造成了资源的浪费,并且对实时性要求较高的应用很不友好。
- 轮询最主的问题还是服务端无法推送消息给客户端,只能由客户端请求服务端来获取数据,为了解决这些问题,就出现了
WebSocket
协议。 - 伴随着
HTML5
推出的WebSocket
,真正实现了Web的实时通信,使B/S
模式具备了C/S
模式的实时通信能力。
上图可以看出轮询是Http
协议是一次请求对应一次响应,每次请求都需要由客户端发起,而WebSocket
的通信只需要客户端与服务器通过Http
协议建立握手,双方便可以平等地、无差别地相互传送信息了,直至任意一方主动断开连接结束。
四、Swoole WebSocket
Swoole
从1.7.9
增加了内置的WebSocket
服务器支持,通过几行PHP代码就可以写出一个异步非阻塞多进程的WebSocket
服务器。WebSocket
除了接收Swoole\Server和Swoole\Http\Server
基类的回调函数外,额外增加了3个回调函数设置。其中:onMessage
回调函数为必选,onOpen
和onHandShake
回调函数为可选function onMessage(swoole_websocket_server $server, swoole_websocket_frame $frame)
- 当服务器收到来自客户端的数据帧时会回调此函数。
$frame
是swoole_websocket_frame
对象,包含了客户端发来的数据帧信息$frame->fd
,客户端的socket id
,使用$server->push
推送数据时需要用到$frame->data
,数据内容,可以是文本内容也可以是二进制数据,可以通过opcode
的值来判断$frame->opcode
,WebSocket
的OpCode
类型,可以参考WebSocket
协议标准文档$frame->finish
, 表示数据帧是否完整,一个WebSocket
请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)
$data
如果是文本类型,编码格式必然是UTF-8
,这是WebSocket
协议规定的onMessage
回调必须被设置,未设置服务器将无法启动- 客户端发送的
ping
帧不会触发onMessage
,底层会自动回复pong
包
function onOpen(swoole_websocket_server $svr, swoole_http_request $req)
- 当
WebSocket
客户端与服务器建立连接并完成握手后会回调此函数。 onOpen
事件回调是可选的$req
是一个Http
请求对象,包含了客户端发来的握手请求信息onOpen
事件函数中可以调用push
向客户端发送数据或者调用close
关闭连接
- 当
function onHandShake(swoole_http_request $request, swoole_http_response $response)
WebSocket
建立连接后进行握手。WebSocket
服务器已经内置了handshake
,如果用户希望自己进行握手处理,可以设置onHandShake
事件回调函数。onHandShake
事件回调是可选的- 设置
onHandShake
回调函数后不会再触发onOpen
事件,需要应用代码自行处理 onHandShake中
必须调用response->status
设置状态码为101
并调用end
响应, 否则会握手失败.- 内置的握手协议为
Sec-WebSocket-Version
:13
,低版本浏览器需要自行实现握手 - 示例代码
$server->on('handshake', function (\swoole_http_request $request, \swoole_http_response $response) { // print_r( $request->header ); // if (如果不满足我某些自定义的需求条件,那么返回end输出,返回false,握手失败) { // $response->end(); // return false; // } // websocket握手连接算法验证 $secWebSocketKey = $request->header['sec-websocket-key']; $patten = '#^[+/0-9A-Za-z]{21}[AQgw]==$#'; if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) { $response->end(); return false; } echo $request->header['sec-websocket-key']; $key = base64_encode(sha1( $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true )); $headers = [ 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Accept' => $key, 'Sec-WebSocket-Version' => '13', ]; if (isset($request->header['sec-websocket-protocol'])) { $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol']; } foreach ($headers as $key => $val) { $response->header($key, $val); } $response->status(101); $response->end(); });
WebSocket
常用方法function WebSocket\Server->push(int $fd, $data, int $opcode = 1, bool $finish = true)
WebSocket\Server->push
在1.7.11
以上版本可用- 向
websocket
客户端连接推送数据,长度最大不得超过2M。 - 传参分两种模式
- 模式一
$fd
客户端连接的ID
,如果指定的$fd
对应的TCP
连接并非websocket
客户端,将会发送失败$data
要发送的数据内容$opcode
,指定发送数据内容的格式,默认为文本。发送二进制内容$opcode
参数需要设置为WEBSOCKET_OPCODE_BINARY
- 发送成功返回
true
,发送失败返回false
- 模式二
需要
4.2.0
及以上版本$data
也就是第一个参数, 可以传入一个swoole_websocket_frame
对象, 支持发送各种帧类型
- 向
function WebSocket\Server->exist(int $fd)
4.3.0
以后, 此API
仅用于判断连接是否存在, 请使用isEstablished
判断是否为WebSocket
连接- 判断
WebSocket
客户端是否存在,并且状态为Active
状态。 - 连接存在,并且已完成
WebSocket
握手,返回true
- 连接不存在或尚未完成握手,返回
false
- 判断
function WebSocket\Server::pack(string $data, int $opcode = 1, bool $finish = true, bool $mask = false)
- 打包
WebSocket
消息。 $data
:消息内容$opcode
:WebSocket
的opcode
指令类型,1
表示文本,2
表示二进制数据,9
表示心跳ping
$finish
:帧是否完成$mask
:是否设置掩码- 返回打包好的
WebSocket
数据包,可通过Socket
发送给对端
- 打包
function WebSocket\Server::unpack(string $data)
- 解析
WebSocket
数据帧。 - 解析失败返回
false
,解析成功返回Swoole\WebSocket\Frame
对象
- 解析
function WebSocket\Server->disconnect(int $fd, int $code = 1000, string $reason = "")
在
4.0.3
以上版本可用- 主动向
WebSocket
客户端发送关闭帧并关闭该连接 $fd
客户端连接的ID
,如果指定的$fd
对应的TCP
连接并非WebSocket
客户端,将会发送失败$code
关闭连接的状态码,根据RFC6455
,对于应用程序关闭连接状态码,取值范围为1000或4000-4999之间$reason
关闭连接的原因,utf-8
格式字符串,字节长度不超过125
- 发送成功返回
true
,发送失败或状态码非法时返回false
- 主动向
function WebSocket\Server->isEstablished(int $fd);
- 检查连接是否为有效的
WebSocket
客户端连接。此函数与exist
方法不同,exist
方法仅判断是否为TCP
连接,无法判断是否为已完成握手的WebSocket
客户端。
- 检查连接是否为有效的
WebSocket\CloseFrame
- 之前介绍了一个
Frame
对象,除此之外还有一个CloseFrame
对象,对比Frame
对象多了两个属性code
和reason
- 如果服务端需要接收
close frame
, 需要通过$server->set
开启open_websocket_close_frame
参数- 启用
WebSocket
协议中关闭帧(opcode
为0x08
的帧)在onMessage
回调中接收,默认为false
。 - 开启后,可在
WebSocketServer
中的onMessage
回调中接收到客户端或服务端发送的关闭帧,开发者可自行对其进行处理。
- 启用
- 之前介绍了一个
五、浅析建立连接
- 目前主流浏览器都支持
WebSocket
协议,此处使用的是谷歌浏览器,首先创建一个WebSocket
对象,传入服务端的ip和监听的WebSocket
端口,WebSocket
的统一资源标志符为ws
,如果是加密版本则为wss
。WebSocket
使用和HTTP
相同的TCP
端口,可以绕过大多数防火墙的限制。默认情况下,WebSocket
协议使用80
端口;运行在TLS
之上时,默认使用443
端口。- js中
WebSocket
有如下的事件:onopen、onmessage、onerror、onclose
,分别在连接、收到客户端消息、通信发生错误和关闭连接时候触发- js中
WebSocket
有如下的方法:send、close
,分别是发送消息和主动关闭连接
Websocket首先通过http协议进行握手的
在请求头中除了Http
带有的请求信息还会带有如下的信息
request headers | value | explain |
---|---|---|
Connection | Upgrade | 表示客户端希望连接升级。 |
Upgrade | websocket | 表示希望升级到WebSocket 协议。 |
Sec-WebSocket-Key | VI8UvTbArkqKYJ5xSEsoyA== | 浏览器生成的Base64 编码的16 位随机字符 |
Sec-WebSocket-Version | 13 | 表示支持的WebSocket 版本 |
服务端响应(状态代码101表示协议切换,到此完成协议升级,后续的数据交互都按照新的协议来)
response headers | value | explain |
---|---|---|
Connection | Upgrade | 表示连接升级。 |
Upgrade | websocket | 表示使用的是WebSocket 协议。 |
Sec-Websocket-Accept | LwwMZdLAE7yn4D1HpimICz99NlU= | 由Sec-WebSocket-Key 跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,通过sha1 加密并转换成base64 生成 |
Sec-WebSocket-Version | 13 | 表示支持的WebSocket 版本 |
六、代码演示
1.客户端代码
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>websocket</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
if ("WebSocket" in window)
{
//创建webSocket对象
var ws = new WebSocket("ws://192.168.2.163:3190");
//连接建立时触发
ws.onopen = function()
{
console.log('建立连接');
}
//客户端接收服务端数据时触发
ws.onmessage = function (evt)
{
var received_msg = evt.data;
console.log('收到消:'+received_msg);
}
//连接关闭时触发
ws.onclose = function(evt)
{
console.log("连接已关闭...");
}
//连接关闭时触发
ws.onerror = function()
{
console.log("连接发生错误...");
}
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
function sendData() {
var msg = $("#msg").val();
ws.send(msg);
}
</script>
</head>
<body>
<input type="text" placeholder="请输入要发送的文字" id="msg">
<button onclick="sendData()">点击发送</button>
</body>
</html>
2.服务端代码
<?php
//创建WebSocket服务
$server = new Swoole\WebSocket\Server("0.0.0.0", 3190);
//启用websocket协议中关闭帧
$server->set(array("open_websocket_close_frame" => true));
//连接建立时触发
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
foreach ($server->connections as $connection) {
$server->push($connection, "用户{$request->fd}进入聊天");
}
});
//收到客户端数据时触发
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
if ($frame->opcode == 0x08) {
foreach ($server->connections as $connection) {
$server->push($connection, "用户{$frame->fd}退出聊天");
}
return;
}
foreach ($server->connections as $connection) {
if ($connection == $frame->fd) {
$server->push($frame->fd, "您说:{$frame->data}");
}else{
$server->push($connection, "用户{$frame->fd}说:{$frame->data}");
}
}
});
//关闭连接是触发
$server->on('close', function ($ser, $fd) {
echo "客户端{$fd} 关闭\n";
});
$server->start();