专栏首页云前端WebSocket 简介及应用实例

WebSocket 简介及应用实例

HTML5 的出现,标志着后 Flash 时代各种现代浏览器的集体爆发,也是谨防 Adobe 一家独大的各家厂商们,历经多年各自为战,想换个活法儿并终于达成一定共识后,所积kao累bei的技术的一次集中释放 -- 正所谓 “H5 是个筐,什么都可以往里装”。

其中引人瞩目并被广泛支持的一项,就是此次要谈论的 WebSocket 了。本文将尝试说明它被用来解决什么问题,以及与久经沙场的“传统” Socket 又有什么异同等基础问题。

I. 定义及由来

望文而生义,面对 WebSocket 这个名称,web 无需做太多解释,傻傻分不清楚的 socket 看着也是相当的面熟;甭管有没有联系,先来了解一下也无妨:

(1.1) 传统的 Socket API

Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API

如图,英文单词 "socket" 的字面原义是 “孔” 或 “插座”。

作为一个技术用语时,socket 通常取后一种意思,像一个多孔插座。用于描述一个通信链路两端的 IP 地址和端口等,可以用来实现不同设备之间的通信。SocketTCP Socket都是通用的叫法,中文一般习惯性的译作“套接字”、“TCP套接字” 等。

...至于为嘛把“插座儿”翻译成“套接字”,好奇的程序猿并不在少数,科考文章在文章底部参考链接中可以找到。

可以将服务端主机想象成一个布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供固定电话信号,有的则提供有线电视节目。客户端软件将插头接入不同编号的插座,就可以得到不同的服务

Socket API 所处的楼层

OSI 模型作为一种概念模型,由国际标准化组织(ISO)提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。我们熟悉的 HTTP、FTP 等协议都工作在最顶端的应用层(Application Layer)。

TCP/IP 协议族(Protocol Suite)将软件通信过程抽象化为四个抽象层,常被视为是简化的七层OSI模型。当多个层次的协议共同工作时,类似数据结构中的堆栈,因此又被称为 TCP/IP 协议栈(Protocol Stack)

单说 TCP 的话,指的是面向连接的一种传输控制协议。TCP 连接之后,客户端和服务器可以互相发送和接收消息,在客户端或者服务器没有主动断开之前,连接一直存在,故称为长连接。

Socket 其实并不是一个标准的协议,而是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,工作位置基本在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。

在设计模式中,Socket其实就是一个门面(facade)模式,它把复杂的 TCP/IP 协议族隐藏在 Socket API 后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议

最早的一套 Socket API 是采用 C 语言实现的,也就成为了 Socket 的事实标准。

常见的 Socket API 实现

一些语言的实现

传统的后端编程语言基本都有 Socket API 的封装;而在 HTML5 出现之前,要想用纯前端技术实现 TCP Socket 的客户端,也基本只有 Java Applet (java.net.Socketjava.net.DatagramSocketjava.net.MulticastSocket) 、Flash (flash.net.Socketflash.net.XMLSocket) 或 Silverlight(System.Net.Sockets) 等可以选择。

下面以 PHP 的 服务器/客户端 实现为例,演示一个最基础的例子:

<?php
//server.phpset_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.phperror_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";
?>

(1.2) HTML5 带来的 WebSocket 协议

WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端,是一种区别于 HTTP 的全新双向数据流协议

web + socket ?

简单的说,传统的 TCP Socket 是一套相对标准化的 API,而出现时间不久的 WebSocket 是一种网络协议 -- 两码事。

WebSocket 底层是基于 TCP 协议的,所以早期草案中叫做 TCPConnection,最后之所以改名,其实是借用了传统 Socket 沟通 TCP 网络两端的意思而已。

   Socket 之于 WebSocket
   就像
   Java 和 JavaScript
   雷锋 和 雷峰塔
   张三 和 张三丰
   周杰 和 周杰伦
   北大 和 北大青鸟
   印度 和 印度尼西亚
   一样
   基本上没啥关系...
   
           -- 鲁迅《我没说过的话》            

要解决的问题

HTTP 的工作方式

在基于 请求/响应 模式的 HTTP/HTTPS 下,如果是对实时性要求较高的场景,客户端就需要不停的询问服务端有无可用的数据,这在各方面都是笨拙而不划算的。

WebSocket 的工作方式

而在 WebSocket 的全双工(允许数据在两个方向上同时传输)方式下,客户端只要静静地听招呼即可,有可用数据时服务端会自动通知它。

与 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 的缺点在于:

  • 不支持 CORS
  • 单向通道,只能服务器向浏览器端发送
  • 浏览器兼容性稍差

WebSocket 的用武之地

大部分传统的方式既浪费带宽(HTTP HEAD 是比较大的),又消耗服务器 CPU 占用(没有信息也要接受请求);而 WebSocket 则会大幅降低上述的消耗,更适用于以下场景:

  • 实时性要求高的应用
  • 聊天室
  • IoT (物联网 - internet of things)
  • 在线多人游戏

兼容性也令人满意,非要说何时不适用的话,大概就是少数必须兼容老旧浏览器,或者对实时要求明显不高的情况下了。

HTTP 的扩展

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=

其中第一个关键点是 UpdateConnection 请求响应头,表示请求和确认切换到 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();
};

II. 一个多用户交互的 WebSocket 实例

这里随便设想一个用户场景,比如我们要做一个在线纸牌游戏,肯定就是一个多人进入同一个房间的形式,并且每个人的动作能广播给其他人。

下面用 WebSocket 做一个最基础的验证原型,让每个玩家知道其他人的进入、离开、出牌、悔牌,甚至是耍赖换牌等:

(2.1) 服务器端的实现

我们用 nodejs+expressjs 搭建基础服务器,并用 https://github.com/websockets/ws 封装的库实现 WebSocket 协议的服务器端逻辑:

// server.jsvar 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.jsconst { 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, '玩家胡牌了');
} );

(2.2) 客户端的实现

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

(2.3) 运行效果

玩家 0 加入:

玩家 1 加入:

玩家 1 出牌:

玩家 1 胡牌并退出:

III. 总结

  • 传统的 TCP Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API
  • WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端
  • WebSockets 是 HTML 5 规范的一个组成部分,是一种区别于 HTTP 的全新双向数据流协议
  • 全双工通信的 WebSockets 有效改善了之前 长轮询 等方式的弊端
  • WebSockets 适用于实时性要求高的应用、聊天室、多人游戏等

IV. 参考资料

  • 为什么要翻译成套接字:https://www.bbsmax.com/A/kvJ3rDV9zg/
  • https://blog.zengrong.net/post/2199.html
  • http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html
  • https://coderanch.com/t/204527/java/Successful-Applet-Socket-Connection
  • https://www.zhihu.com/question/20215561
  • https://www.jianshu.com/p/42260a2575f8
  • https://www.jianshu.com/p/59b5594ffbb0
  • http://enterprisewebbook.com/ch8_websockets.html
  • https://medium.com/@dominik.t/what-are-web-sockets-what-about-rest-apis-b9c15fd72aac
  • https://medium.com/platform-engineer/web-api-design-35df8167460
  • https://www.websocket.org/quantum.html
  • https://blog.gtwang.org/web-development/websocket-protocol/
  • https://medium.com/kifi-engineering/websockets-vs-regular-sockets-b3b8e7ea0708
  • http://shouce.jb51.net/actionscript3.0/flash/net/Socket.html
  • https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/XMLSocket.html
  • https://github.com/theturtle32/AS3WebSocket
  • https://baike.baidu.com/item/socket/281150
  • https://baike.baidu.com/item/TCP%2FIP协议族
  • https://www.cnblogs.com/xuehaoyue/p/6639029.html
  • https://kotlintc.com/articles/4925
  • https://blog.csdn.net/future_todo/article/details/50097363
  • https://developer.mozilla.org/en-US/docs/Web/API/WebSocketsAPI/WritingWebSocket_servers
  • http://lamb-mei.com/462/websocket-%E7%A5%9E%E5%A5%87%E5%AD%97%E4%B8%B2-258eafa5-e914-47da-95ca-c5ab0dc85b11/
  • https://kaazing.com/html5-websocket-security-is-strong/
  • https://www.asni.cn/2152

(end)

本文分享自微信公众号 - 云前端(fewelife),作者:lua

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • jQuery.i18n.properties 实现 Web 前端的国际化

    国际化英文单词为:Internationalization,又称 i18n,“i”为单词的第一个字母,“18”为“i”和“n”之间单词的个数,而“n”代表这个单...

    江米小枣
  • 初探在WSL中设置vim前端开发环境

    在之前的 《在win10的WSL中设置前端开发环境》 一文中,介绍了 win10 结合其 linux 子系统(WSL)的前端开发设置,使用的是 VSCode 开...

    江米小枣
  • 前端开发中的几种资源重定向方法

    嗯,但是为嘛需要这么做呢...... 这道题面试中也真有很多稀里糊涂做完类似的项目,却答不上来的小伙伴,那你自个儿寻思去吧~

    江米小枣
  • 打造云端毕业盛宴 腾讯乐享助力东华大学2020毕业典礼

    6月30日,东华大学借助腾讯乐享平台,为2020届毕业生举办了一场特殊的毕业典礼。受疫情影响,很多学子无法返校,东华大学选择通过云端举行本次毕业典礼,并借助腾...

    鹅老师
  • 只要130 行代码即可生成二维样本,心动了吗?

    AI 研习社按:此前我们曾编译了一篇英文教程,详细介绍了如何基于 PyTorch 平台用 50 行代码实现 GAN(生成对抗网络),详情参见:在 PyTorch...

    AI研习社
  • 小程序案例丨【明星在线】互联网+明星经济,实现资源高效对接

    明星经纪、IP经济已经成为娱乐向产品买卖主流收益,通过明星影响力、流量数据、衍生产品等维度形成to C经济效益圈,大批商业主已然意识到大浪淘金的年代里,想凝汇“...

    齿轮易创说互联网
  • 在线浏览器,在线PS,在线AI,在线编程 ...

    现在越来越习惯使用在线工具,拿来即用,用完即走。只要有网,在哪都可以用。比如我们推出的在线绘图 (http://www.ehbio.com/ImageGP)已经...

    生信宝典
  • 小程序问世 微信的一手好牌被打烂?

    T客汇官网:tikehui.com 撰文 | 人称T客 ? 昨晚小程序如约而至,被朋友圈刷必不可,有人惊呼狼来了,小程序这是要干掉所有 App 的节奏,但也有人...

    人称T客
  • 腾讯再次开源三项技术,提升企业运维效率

    (2019年4月11日,深圳)在腾讯内部,工程师文化依然是主流,鼓励用代码、用技术说话——“talk is cheap, show me the code。”而...

    腾讯开源
  • 腾讯再次开源三项技术,提升企业运维效率

    (2019年4月11日,深圳)在腾讯内部,工程师文化依然是主流,鼓励用代码、用技术说话——“talk is cheap, show me the code。”而...

    腾讯技术工程官方号

扫码关注云+社区

领取腾讯云代金券