HTML5 的出现,标志着后 Flash 时代各类现代浏览器的集体爆发,也是谨防 Adobe 一家独大的各家厂商们,历经多年各自为战,想换个活法儿并终于达成必定共识后,所积kao累bei的技术的一次集中释放 -- 正所谓 “H5 是个筐,什么均可以往里装”。javascript
其中引人瞩目并被普遍支持的一项,就是这次要谈论的 WebSocket 了。本文将尝试说明它被用来解决什么问题,以及与久经沙场的“传统” Socket 又有什么异同等基础问题。php
望文而生义,面对 WebSocket 这个名称,web
无需作太多解释,傻傻分不清楚的 socket
看着也是至关的面熟;甭管有没有联系,先来了解一下也无妨:html
Socket 每每指的是 TCP/IP 网络环境中的两个链接端,以及为方便此类开发所设计的一组编程 API前端
如图,英文单词 "socket" 的字面原义是 “孔” 或 “插座”。html5
做为一个技术用语时,socket
一般取后一种意思,像一个多孔插座。用于描述一个通讯链路两端的 IP 地址和端口等,能够用来实现不一样设备之间的通讯。Socket
、TCP Socket
都是通用的叫法,中文通常习惯性的译做**“套接字”、“TCP套接字”** 等。java
...至于为嘛把“插座儿”翻译成“套接字”,好奇的程序猿并不在少数,科考文章在文章底部参考连接中能够找到。node
能够将服务端主机想象成一个布满各类插座的房间,每一个插座有一个编号,有的插座提供 220 伏交流电,有的提供固定电话信号,有的则提供有线电视节目。客户端软件将插头接入不一样编号的插座,就能够获得不一样的服务git
OSI 模型做为一种概念模型,由国际标准化组织(ISO)提出,一个试图使各类计算机在世界范围内互连为网络的标准框架。咱们熟悉的 HTTP、FTP 等协议都工做在最顶端的应用层(Application Layer)。github
而 **TCP/IP 协议族(Protocol Suite)**将软件通讯过程抽象化为四个抽象层,常被视为是简化的七层OSI模型。当多个层次的协议共同工做时,相似数据结构中的堆栈,所以又被称为 TCP/IP 协议栈(Protocol Stack)。web
单说 TCP 的话,指的是面向链接的一种传输控制协议。TCP 链接以后,客户端和服务器能够互相发送和接收消息,在客户端或者服务器没有主动断开以前,链接一直存在,故称为长链接。
Socket 其实并非一个标准的协议,而是应用层与 TCP/IP 协议族通讯的中间软件抽象层,它是一组接口,工做位置基本在 OSI 模型会话层(第5层),是为了方便你们直接使用更底层协议(通常是 TCP 或 UDP )而存在的一个抽象层。
在设计模式中,Socket其实就是一个门面(facade)模式,它把复杂的 TCP/IP 协议族隐藏在 Socket API 后面,对用户来讲,一组简单的接口就是所有,让 Socket 去组织数据,以符合指定的协议
最先的一套 Socket API 是采用 C 语言实现的,也就成为了 Socket 的事实标准。
传统的后端编程语言基本都有 Socket API 的封装;而在 HTML5 出现以前,要想用纯前端技术实现 TCP Socket 的客户端,也基本只有 Java Applet (java.net.Socket
或 java.net.DatagramSocket
或 java.net.MulticastSocket
) 、Flash (flash.net.Socket
或 flash.net.XMLSocket
) 或 Silverlight(System.Net.Sockets
) 等能够选择。
下面以 PHP 的 服务器/客户端 实现为例,演示一个最基础的例子:
<?php
//server.php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 1999;
// 建立一个Socket
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
// 绑定Socket地址和端口
$ret = socket_bind($sock,$ip,$port);
// 开始监听连接
$ret = socket_listen($sock,4);
$count = 0; //最多接受几回请求后就退出
do {
// 另外一个Socket来处理通讯
if (($msgsock = socket_accept($sock)) >= 0) {
// 发到客户端
$msg ="server: HELLO CLIENTS!\n";
if (socket_write($msgsock, $msg, strlen($msg))) {
echo "发送成功!\n";
}
// 得到客户端的输入
$buf = socket_read($msgsock,8192);
$talkback = "接受成功!内容为:$buf\n";
echo $talkback;
if(++$count >= 5){
break;
};
}
// 关闭sockets
socket_close($msgsock);
} while (true);
socket_close($sock);
echo "TCP 链接关闭OK\n";
?>
复制代码
<?php
//client.php
error_reporting(E_ALL);
set_time_limit(0);
$port = 1999;
$ip = "127.0.0.1";
// 建立Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 绑定Socket地址和端口
$result = socket_connect($socket, $ip, $port);
if ($result >= 0) echo "TCP 链接OK\n";
$in = "client: HELLO SERVER!\r\n";
if(socket_write($socket, $in, strlen($in))) {
echo "发送成功!\n";
}
$out = '';
while($out = socket_read($socket, 8192)) {
echo "接受成功!内容为:",$out;
}
socket_close($socket);
echo "TCP 链接关闭OK\n";
?>
复制代码
WebSockets
为 C/S 两端提供了实时交互通讯的能力,容许服务器主动发送信息给客户端,是一种区别于 HTTP 的全新双向数据流协议
简单的说,传统的 TCP Socket 是一套相对标准化的 API,而出现时间不久的 WebSocket 是一种网络协议 -- 两码事。
WebSocket 底层是基于 TCP 协议的,因此早期草案中叫作 TCPConnection,最后之因此更名,实际上是借用了传统 Socket 沟通 TCP 网络两端的意思而已。
Socket 之于 WebSocket
就像
Java 和 JavaScript
雷锋 和 雷峰塔
张三 和 张三丰
周杰 和 周杰伦
北大 和 北大青鸟
印度 和 印度尼西亚
同样
基本上没啥关系...
-- 鲁迅《我没说过的话》
复制代码
在基于 请求/响应 模式的 HTTP/HTTPS 下,若是是对实时性要求较高的场景,客户端就须要不停的询问服务端有无可用的数据,这在各方面都是笨拙而不划算的。
而在 WebSocket 的全双工(容许数据在两个方向上同时传输)方式下,客户端只要静静地听招呼便可,有可用数据时服务端会自动通知它。
实际上,每当谈到实时双向通讯的问题时,咱们天然会想起历年来一些基于 HTTP 技术的尝试;也正是基于这些以前工做中的实践和困扰,WebSocket 才应运而生。让咱们大概回顾一下相关的方案及其缺陷:
轮询 (Polling)
借助于 setInterval() 等方式,客户端不断的发送请求并获得响应。这种作法比较简单,能够在必定程度上解决问题。不过对于轮询的时间间隔须要进行仔细考虑。轮询的间隔过长,会致使用户不能及时接收到更新的数据;轮询的间隔太短,会致使查询请求过多,增长服务器端的负担。
长轮询 (Long Polling)
这是对轮询的一种改进。客户端发出请求后,服务器端用 while(true) 等方式阻塞住请求,直到有可用数据才发送响应数据,而客户端收到响应后再发送下一个请求。
这种方式又被成为 “Hanging GET”、“反向 Ajax” 或 “Comet” 等,虽然看上去很像服务器推送,但仍然是基于 HTTP 的一种慢响应;且在数据更新频繁的状况下,其效率并不优于通常的轮询。
HTTP 流 (Streaming)
使用 HTTP 1.1 且响应头中包含 Transfer-Encoding: chunked
的状况下,服务器发送给客户端的数据能够分红多个部分,保持打开(while-true, sleep等),并周期性 flush() 分块传输。
客户端只发送一个HTTP链接,在 xhr.readyState==3 状态下,用 xhr.responseText.substring 获取每次的数据。
可是数据响应可能会因 代理服务器 或 防火墙 等中间人形成延迟,因此可能还要额外探测这种状况以切换到长轮询方式。
SSE (Server-Sent Events)
SSE 规范也是 HTML 5 规范的一个组成部分。服务器端响应的内容类型是text/event-stream
,在浏览器端使用 EventSource
对象处理返回的数据。
比之于 WebSocket,SSE 的缺点在于:
大部分传统的方式既浪费带宽(HTTP HEAD 是比较大的),又消耗服务器 CPU 占用(没有信息也要接受请求);而 WebSocket 则会大幅下降上述的消耗,更适用于如下场景:
兼容性也使人满意,非要说什么时候不适用的话,大概就是少数必须兼容老旧浏览器,或者对实时要求明显不高的状况下了。
WebSocket 链接的 URL 使用 ws://
或 wss://
等开头,其加密、cookie 等策略和对应的 HTTP/HTTPS 基本相同。
HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的,能够把这些高级协议理解成对 TCP 的封装。
在 HTTP 下,客户端不发请求的话,服务器永远没法发送数据给客户端。
而对于 WebSocket 来讲,它必须依赖 HTTP 协议进行一次握手,以兼容浏览器的规范;能够将之视为 HTTP 的一种补充和升级。
在进行第一次 HTTP 请求以后,后续的就所有采用 TCP 通道进行双向通信了。因此,HTTP 和 WebSocket 虽都基于 TCP 协议,倒是彻底不一样的两种通信方式。
来看个典型的 WebSocket 握手请求:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
复制代码
服务器响应则是:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
复制代码
其中第一个关键点是 Update
和 Connection
请求响应头,表示请求和确认切换到 WebSocket 协议;
13
是一个固定的版本号;
而请求头 Sec-WebSocket-Key
为浏览器随机生成的 Base64 编码,将之附加上一个固定为 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
的“魔法字符串”,再计算SHA-1摘要并进行BASE-64编码,就是响应中的 Sec-WebSocket-Accept
了。 ╮(╯▽╰)╭
在浏览器中能够直接调用 WebSocket
对象,其定义以下:
enum BinaryType { "blob", "arraybuffer" };
[Constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []), Exposed=(Window,Worker)]
interface WebSocket : EventTarget {
readonly attribute USVString url;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long long bufferedAmount;
// networking
attribute EventHandler onopen;
attribute EventHandler onerror;
attribute EventHandler onclose;
readonly attribute DOMString extensions;
readonly attribute DOMString protocol;
void close([Clamp] optional unsigned short code, optional USVString reason);
// messaging
attribute EventHandler onmessage;
attribute BinaryType binaryType;
void send(USVString data);
void send(Blob data);
void send(ArrayBuffer data);
void send(ArrayBufferView data);
};
复制代码
使用起来大概是这样的:
var ws = new WebSocket('ws://www.xxx.com/some.php');
ws.send('xxx'); //每次只能发送字符串
ws.onmessage = function(event) {
var data = event.data;
};
ws.onerror = function() {
ws.close();
};
复制代码
这里随便设想一个用户场景,好比咱们要作一个在线纸牌游戏,确定就是一个多人进入同一个房间的形式,而且每一个人的动做能广播给其余人。
下面用 WebSocket 作一个最基础的验证原型,让每一个玩家知道其余人的进入、离开、出牌、悔牌,甚至是耍赖换牌等:
咱们用 nodejs+expressjs 搭建基础服务器,并用 https://github.com/websockets/ws 封装的库实现 WebSocket 协议的服务器端逻辑:
// server.js
var express = require('express')
var ws = require('./ws')
var app = express()
app.get('/', function (req, res) {
res.sendFile(__dirname + '/ws.html');
})
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
})
复制代码
// ws.js
const { Server, OPEN } = require('ws');
const clients = []; //array of websocket clients
const cardsArr = []; //array of {cardId, count, title, ...}
let _lock = false;
const wss = new Server({port: 40510})
wss.on('connection', function (ws) {
const _cid = clients.push(ws) - 1;
ws.on('message', function (json) {
const {
act,
cid,
data
} = JSON.parse(json);
switch (act) {
case 'client:join':
_onCustomerJoin(ws, _cid);
break;
case 'client:leave':
_onCustomerLeave(cid);
break;
case 'client:add': //增长牌
_onAddCard(cid, data);
break;
case 'client:update': //修改牌
_onUpdateCard(cid, data);
break;
case 'client:remove': //删除牌
_onRemoveCard(cid, data);
break;
case 'client:win': //下单
_onWin(cid);
break;
default:
console.log('received: %s', act, cid)
break;
}
});
});
function _ensureLock(func) {
return function() {
if (_lock) return;
_lock = true;
const rtn = func.apply(null, arguments);
_lock = false;
return rtn;
};
}
function _findCard(cardId) {
const cidx = cardsArr.map(Card=>Card.cardId).indexOf(cardId);
return cidx;
}
const _broadcast = (excludeId, msg, data=null)=>{
clients.forEach( (client, cidx)=>{
if (cidx === excludeId) return;
if (client && client.readyState === OPEN) {
client.send(JSON.stringify({
act: 'server:broadcast',
msg: msg,
data: data
}));
}
} );
};
const _onCustomerJoin = (ws, cid)=>{
ws.send(JSON.stringify({
act: 'server:regist',
data: {
cid: cid
}
}));
_broadcast(cid, '玩家加入:', {cid: cid});
};
const _onCustomerLeave = (cid)=>{
clients[cid].terminate();
clients.splice(cid, 1);
_broadcast(cid, '玩家退出:', {cid: cid});
};
const _onAddCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d !== -1) {
cardsArr.splice(d, 1);
}
cardsArr.push(data);
_broadcast(-1, '玩家添加了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onUpdateCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d === -1) return;
cardsArr[d] = data;
_broadcast(-1, '玩家更改了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onRemoveCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d === -1) return;
cardsArr.splice(d, 1);
_broadcast(-1, '玩家删除了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onWin = _ensureLock( (cid)=>{
//do sth. here
_broadcast(cid, '玩家胡牌了');
} );
复制代码
<h1></h1>
<div></div>
<button onclick="_add()">出牌</button>
<button onclick="_update()">换牌</button>
<button onclick="_remove()">悔牌</button>
<button onclick="_win()">胡牌</button>
<button onclick="_leave()">离开</button>
<script> let cid = null; const ws = new WebSocket('ws://localhost:40510'); ws.onopen = function () { console.log('websocket is connected ...'); _send({ act: 'client:join' }); }; ws.onmessage = function (ev) { const { act, msg, data } = JSON.parse(ev.data); switch(act) { case 'server:regist': cid = data.cid; console.log(`regist: cid is ${cid}`); document.querySelector('h1').innerHTML = 'I AM: ' + cid; break; case 'server:broadcast': console.log('从服务器端接收的广播:', msg, data); if (data && data.cardsArr) { document.querySelector('div').innerHTML = JSON.stringify( data.cardsArr, null, 4 ); } break; default: console.log(ev); break; } } function _send(json) { ws.send(JSON.stringify(json)); } function _add() { _send({ act: 'client:add', cid: cid, data: { cardId: 111, count: 1, title: '红桃A' } }) } function _update() { _send({ act: 'client:update', cid: cid, data: { cardId: 111, count: 2, title: '黑桃9' } }) } function _remove() { _send({ act: 'client:remove', cid: cid, data: { cardId: 111 } }) } function _win() { _send({ act: 'client:win', cid: cid }) } function _leave() { _send({ act: 'client:leave', cid: cid }) } </script>
复制代码
玩家 0 加入:
玩家 1 加入:
玩家 1 出牌:
玩家 1 胡牌并退出:
长按二维码或搜索 fewelife 关注咱们哦