1、WebSocket是什么
WebSocket是一种在单个TCP连接上进行全双工通信的协议,其目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,使得服务器可以主动发送消息给浏览器。在HTML5中包含了WebSocket API规范。
WebSocket 协议在2008年诞生,2011年成为国际标准。目前所有浏览器都已经支持。
根据安全与否,与HTTP/HTTPS类似,WebSocket有ws/wss两种协议。
(需要注意的是,目前浏览器对WebSocket未做同源策略限制,因此采用WebSocket的应用需要注意防范跨站请求伪造)
2、Web双向通信方案
所谓双向通信,关键在于服务端”主动“能发信息给客户端。
传统的HTTP协议是一个请求-响应协议,是无状态的:请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。即服务端处于被动状态,只有在收到客户端请求后才能响应发数据给客户端,且请求与响应一一对应。
在WebSocket之前,在Web要实现类似双向通信功能,只能通过 ajax poll (轮询)或 long poll (长轮询) 等。
1、ajax poll (轮询):客户端每隔几秒发送请求询问服务端是否有新消息,服务器接收到请求后马上返回并关闭连接。
优点:后台实现简单。
缺点:实时性不够;TCP建立和关闭操作浪费时间和带宽,频繁请求造成大访问压力,且有很多是无用请求,浪费带宽和服务器资源。
实例:小型应用
2、long poll (长轮询):本质也是轮询,不同的是客户端发起请求后,服务端若没有新消息则hold阻塞,直到有消息才返回并关闭连接。
优点:在无消息的情况下不会频繁请求,耗费资源小。
缺点:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大浪费服务器资源;HTTP长时间没传数据,该连接可能被网关关闭,不可控,故需要发”心跳“。
实例:WebQQ、Hi网页版、Facebook IM
3、HTTP长连接:一个TCP连接可以发送多次HTTP请求,而不是传统那样每个请求都重新建立一个连接。
在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
优点:消息即时到达,不发无用请求;管理起来也相对方便。
缺点:服务器维护一个长连接会增加开销,当客户端越来越多的时候,server压力大。
4、Flash Socket:在页面中内嵌入一个使用了Socket类的 Flash 程序,JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。
优点:实现真正的即时通信,而不是伪即时。
缺点:客户端必须安装Flash插件,移动端支持不好,IOS系统中没有flash的存在;非HTTP协议,无法自动穿越防火墙。
实例:网络互动游戏。
3、WebSocket的特点
WebSocket的出现可以取代轮询和长连接,客户端不用定期轮询(网关问题仍存在,故WebSocket内部也定期发送”心跳“),其特点有:
1、建立在 TCP 协议之上,服务器端的实现比较容易。(基于TCP,所以可以支持全双工通信)
2、与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。3、数据格式比较轻量,性能开销小,通信高效。4、可以发送文本,也可以发送二进制数据。(通常用JSON,方便处理)5、没有同源限制,客户端可以与任意服务器通信。相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
4、WebSocket协议原理
可以视为两个阶段,先借助HTTP进行握手,握手完成后就建立了连接以后直接双向通信。
1、握手阶段利用HTTP协议发送一次握手请求,协商升级到WebSocket协议(101 Switching Protocols)。握手请求是一个标准HTTP请求,格式如下:
请求格式示例:GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==Sec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13Origin: http://example.com响应格式示例:HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=Sec-WebSocket-Protocol: chat
2、握完手后与HTTP协议无关了,客户端和服务端直接建立了连接,可以直接双向主动发送数据
WebSocket 是独立创建在TCP上的协议,HTTP协议中的那些概念都和WebSocket 没有关联,唯一关联的是使用HTTP协议的 101 状态码进行协议切换。
5、WebSocket实践(SocketIO)
关于WebSocket的API,前端有WebSocket API已经成为HTML5标准的一部分,后端有很多框架,Java中也有很多。这里以SocketIO为例.
5.1、SocketIO
SocketIO是基于WebSocket实现的一个跨平台的实时通信库,基于engine.io实现。engine.io 使用了 WebSocket 和 XMLHttprequest或JSONP封装了一套自己的 Socket 协议(暂时叫 EIO Socket),在低版本浏览器里面使用长轮询替代 WebSocket。一个完整的 EIO Socket 包括多个 XHR 和 WebSocket 连接。
SocketIO不仅支持WebSocket,为了兼容有些浏览器不支持WebSocket的问题还提供了降级功能:
Websocket
Adobe® Flash® SocketAJAX long pollingAJAX multipart streamingForever IframeJSONP Polling这些降级功能对用户来说是透明的,SocketIO会根据浏览器支持情况进行自动选择。
此外,SocketIO还提供了命名空间、自动重连等功能。
关于SocketIO的库很多:
5.2、SocketIO示例
Java服务端:
依赖:(netty-socketio、socket-io-client)
io.socket socket.io-client 1.0.0 com.corundumstudio.socketio netty-socketio 1.7.12
Java服务端代码示例:
public class SocketServer { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static SocketIOServer server = initServer(); /** * 初始化服务端 * * @return */ private static SocketIOServer initServer() { Configuration config = new Configuration(); config.setHostname("localhost"); config.setPort(9090); config.setContext("/wsapi");// 前端连接时通过指定path与此对应 config.setAuthorizationListener(new AuthorizationListener() { // 授权 @Override public boolean isAuthorized(HandshakeData data) { String token = data.getSingleHeader("X-Authorization");// cannot // get,always // null return true; } }); server = new SocketIOServer(config); return server; } /** * 启动服务端 */ public void startServer() { // 添加连接监听 server.addConnectListener(new ConnectListener() { @Override public void onConnect(SocketIOClient socketIOClient) { String acid = socketIOClient.getHandshakeData().getSingleUrlParam("acid");// 前端连接时带上的参数 String clientId = socketIOClient.getSessionId().toString(); logger.info("server 服务端启动成功"); } }); // 添加断开连接监听 server.addDisconnectListener(new DisconnectListener() { @Override public void onDisconnect(SocketIOClient socketIOClient) { logger.info("server 服务端断开连接"); } }); // 添加事件监听 server.addEventListener("join", String.class, new DataListener() { @Override public void onData(SocketIOClient socketIOClient, String str, AckRequest ackRequest) throws Exception { logger.info("收到客户端加入消息:" + str); server.getBroadcastOperations().sendEvent("joinSuccess", "join success"); } }); // 启动服务端 server.start(); } /** * 停止服务端 */ public void stopServer() { server.stop(); }}
前端代码示例:
socketio-client
socket.io 提供了三种默认的事件(客户端和服务器都有):connect 、message 、disconnect 。当与对方建立连接后自动触发 connect 事件,当收到对方发来的数据后触发 message 事件(通常为 socket.send() 触发),当对方关闭连接后触发 disconnect 事件。
此外,socket.io 还支持自定义事件,毕竟以上三种事件应用范围有限,正是通过这些自定义的事件才实现了丰富多彩的通信。最后,需要注意的是,在服务器端区分以下三种情况:socket.emit() :向建立该连接的客户端广播
socket.broadcast.emit() :向除去建立该连接的客户端的所有客户端广播io.sockets.emit() :向所有客户端广播,等同于上面两个的和
其他相关:
如何带认证参数:官方文档中说通过extraHeaders不过只在polling模式下生效故不可取,可以在连接url中作为参数传输不过url可以直接看到故不安全。
设置context path:如代码中所示,前端通过连接时的path参数设置,不设置则默认为 /socket.io
更多启动选项设置参考:
如:transportOptions (Object): hash of options, indexed by transport name, overriding the common options for the given transport
6、相关资料