WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
传统的HTTP协议是一个请求-响应协议,浏览器不主动请求,服务器是没法主动发数据给浏览器的。
传统服务器推送方式
Ajax 轮询
浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。
缺点
- 实时性不够
- 频繁的请求会给服务器带来极大的压力。
服务器反推
本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。暂时地解决了实时性问题。
缺点
- 以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。
- 一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接。 长期占用连接,丧失了无状态高并发的特点。
WebSocket协议
WebSocket并不是全新的协议,而是利用了HTTP协议来建立TCP连接。
请求
WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求。
格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
WebSocket请求和普通的HTTP请求有几点不同:
- GET请求的地址不是类似/path/,而是以ws://开头的地址;
- 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
- Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
- Sec-WebSocket-Version指定了WebSocket的协议版本。
响应
服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议
WebSocket、HTTP 与 TCP 区别
HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。 所以连接和断开,都要遵循 TCP 协议中的三次握手和四次握手 ,只是在连接之后发送的内容不同,或者是断开的时间不同。
对于 WebSocket
来说,它必须依赖 HTTP 协议进行一次握手
,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
Socket 与 WebScoket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。
WebSocket 则不同,它是一个完整的 应用层协议,包含一套标准的 API。
从使用上来说,WebSocket 更易用,而 Socket 更灵活。
HTML5 与 WebSocket
WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。
注意事项
长连接应用必须加心跳,否则连接可能由于长时间未通讯被路由节点强行断开。
消息堆积
心跳作用主要有两个
1、客户端定时给服务端发送点数据,防止连接由于长时间没有通讯而被某些节点的防火墙关闭导致连接断开的情况。
2、服务端可以通过心跳来判断客户端是否在线,如果客户端在规定时间内没有发来任何数据,就认为客户端下线。这样可以检测到客户端由于极端情况(断电、断网等)下线的事件。
心跳间隔建议值:
建议客户端发送心跳间隔小于60秒,比如55秒。
HTML5 WebSocket
WebSocket 属性
Socket.readyState
只读属性 readyState 表示连接状态,可以是以下值:
0 - 表示连接尚未建立。
1 - 表示连接已建立,可以进行通信。
2 - 表示连接正在进行关闭。
3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
WebSocket 事件
假定我们使用了以上代码创建了 Socket 对象:
Socket.onopen 连接建立时触发
Socket.onmessage 客户端接收服务端数据时触发
Socket.onerror 通信发生错误时触发
Socket.onclose 连接关闭时触发
WebSocket 方法
假定我们使用了以上代码创建了 Socket 对象:
Socket.send() 使用连接发送数据
Socket.close() 关闭连接
参考:
WebSocket
注意是事项
注意:长连接应用必须加心跳,否则连接可能由于长时间未通讯被路由节点强行断开。
心跳作用主要有两个:
1、客户端定时给服务端发送点数据,防止连接由于长时间没有通讯而被某些节点的防火墙关闭导致连接断开的情况。
2、服务端可以通过心跳来判断客户端是否在线,如果客户端在规定时间内没有发来任何数据,就认为客户端下线。这样可以检测到客户端由于极端情况(断电、断网等)下线的事件。
心跳间隔建议值:
建议客户端发送心跳间隔小于60秒,比如55秒。
代码演示
服务端代码:
这里采用php方式来进行演示,其他语言也是类似,这里不在叙述。
<?php
use Workerman\Worker;
require_once __DIR__ . '/../../Workerman/Autoloader.php';
use \Workerman\Lib\Timer;
// 创建一个Worker监听2000端口,使用websocket协议通讯
$ws_worker = new Worker("websocket://0.0.0.0:2000");
// 进程数设置为1,采用单进程
$ws_worker->count = 1;
// 保存uid到connection的映射(uid是用户id或者客户端唯一标识)
$ws_worker->uidConnections = [];
// 设置心跳时间 0 代表服务器主动保活
define('HEARTBEAT_TIME', 10);
// 进程启动后设置一个每秒运行一次的定时器
$ws_worker->onWorkerStart = function ($worker) {
// 这里使用一个定时器,间隔时间为1s
Timer::add(12, function () use ($worker) {
$time_now = time();
foreach ($worker->connections as $connection) {
// 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
if (HEARTBEAT_TIME > 0 && $time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
$connection->close("Network connection timeout!");
}
// 如果心跳间隔设置为0的话,可以服务端主动发送ping
if (HEARTBEAT_TIME == 0) {
$connection->send('ping');
}
}
});
};
$ws_worker->onMessage = function ($connection, $data) {
global $ws_worker;
// 假设消息格式为
// uid:message 时是对 uid 发送 message
// uid 为 all 时是全局广播
list($recv_uid, $message) = explode(':', $data);
$ws_worker->uidConnections[$recv_uid] = $connection;
// 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
$connection->lastMessageTime = time();
// 全局广播
if ($recv_uid == 'all') {
broadcast($message);
} // 给特定uid发送
else {
// 可以向执行的uid发送消息
sendMessageByUid($recv_uid, $message);
}
};
/**
* 直接将消息发送给用户推送数据
* @param $message
*/
function broadcast($message)
{
global $ws_worker;
foreach ($ws_worker->uidConnections as $connection) {
$connection->send($message);
}
}
/**
* 针对uid推送数据
* @param $uid
* @param $message
*/
function sendMessageByUid($uid, $message)
{
global $ws_worker;
// 这里可以自定义自己的逻辑业务
if (isset($ws_worker->uidConnections[$uid]) && $uid) {
$ws_worker->uidConnections[$uid]->send($message);
}
}
// 运行worker
Worker::runAll();
客户端代码:
这个采用html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket示例</title>
<script type="text/javascript">
function WebSocketTest() {
if ("WebSocket" in window) {
// 打开一个 web socket
var ws = new WebSocket("ws://localhost:2000");
ws.onopen = function () {
ws.send("100:haha");
console.log("数据发送中...");
};
ws.onmessage = function (e) {
var received_msg = e.data;
console.log("数据已接收:" + received_msg)
};
ws.onclose = function () {
// 关闭 websocket
console.log("连接已关闭...")
};
}
else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">打开连接</a>
</div>
</body>
</html>
效果