前言
刚完成一个需求,用到了WebSocket。很好,又是一个我不会的东西。写完总该记录一下,复盘有利于加深新知识的理解。
起源
众所周知,HTTP虽然使用的是TCP协议,但信息流动的主导只有客户端,总是需要客户端向服务端发送一个Request,服务端才会返回一个Response。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。
解决办法之一就是采用轮询(客户端利用Ajax等不停地ping服务器),无论是对客户端还是服务器都是一种资源浪费。
WebSocket就是为了给如上情景提供一种良好的解决方案。
简介
WebSocket是一种网络通信协议,RFC6455定义了它的通信标准。
WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。它在2008年诞生,2011年成为国际标准,目前几乎所有浏览器都已经支持了。它最大的特点在于,服务器可以主动向客户端推送消息,客户端也可以主动向服务器端发送消息,是真正的平等对话。它的其他特点如下:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
使用
客户端
这是一个JavaScript
的代码,在需要使用的HTML
页面上引入这一JS文件,并给响应按钮设置事件即可。
要是你还是不太理解,可以参考我的这篇博客,是采用两种方式(原生WebSocket或SockJS)实现的多房间聊天室。
有关WebSocket的前端API及使用也可以参考这个官方文档
1 |
|
好,现在对如上代码进行介绍。
1 | var ws = new WebSocket("ws://localhost:8080/chat"); |
这是使用原生WebSocket建立一条Endpoint为/chat
的连接,也就是说,一旦new WebSocket(url)浏览器就会开始尝试建立一条Websocket连接。
1 | ws.onopen = function(evt) { |
ws.onopen
的值是一个函数,这个函数指定了当WebSocket连接成功后执行的操作,参数evt是一个Event。
同理,ws.onclose
指定连接关闭后的回调函数,ws.onerror
指定报错时的回调函数, ws.onmessage
指定收到服务器数据后的回调函数。需要注意的是,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象),可以通过如下方式进行区分操作
1 | ws.onmessage = function(event){ |
如果接收到的是对象,可以用如下方式进行解析:
假设服务器端传来的对象是这个:
1 |
|
那么可以这样解析
1
2
3
4ws.onmessage = function (event) {
var stu = JSON.parse(evt.data); //获取到stu对象
console.log("name = "+stu.name+" age = "+stu.age); //使用获取到的数据
}
除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。
1 | // 收到的是 blob 数据 |
除了以上几个API,还有ws.send()
和ws.close()
,介绍如下:
实例对象的send()方法用于向服务器发送数据。
发送文本的例子
1 | ws.send("hello word"); |
发送JSON对象的例子
1 | ws.send(JSON.stringify( |
发送Blob对象的例子
1 | var file = document.querySelector('input[type="file"]').files[0]; |
发送ArrayBuffer对象的例子
1 | // Sending canvas ImageData as ArrayBuffer |
ws.close()
方法用于显式的关闭连接。
服务器端
这里用的是Spring boot,所以需要在maven里添加这个依赖spring-boot-starter-websocket
。业务代码如下:
首先需要一个Config类来对@ServerEndpoint进行处理
1 |
|
然后写一个ServerEndpoint类用于处理通讯事件
1 |
|
用@ServerEndpoint标记一个类,表示这个类是一个WebSocket服务器。并给这个注解添加属性value=“url”表示Endpoint的路径。前端 new WebSocket(url)中的url就与此对应。比如@ServerEndpoint( value = "/chat")
,那么前端想要与此建立连接就是var websocket = new WebSocket("ws://localhost:8080/chat")
。
再用@Component注解标记,表示将该类交由Spring Boot处理,并生成实例。OnMessage
等的使用时机也和前端一致。当我们在服务器端需要给前端发送消息时,代码如下
1 |
|
⚠️当我们需要给前端传一个对象时,直接使用sendObject()方法是会报错的,如javax.websocket.EncodeException: No encoder specified for object of class [这里是你sendObject里传的对象的全名]
。报错的原因是我们没有指定编码器。解决方案是自定义一个编码器如下:
1 | /* |
然后在@ServerEndpoint注解中添加encoders
1 |
ok,问题解决
⚠️⚠️⚠️无论双方在哪发送消息,对方接收到消息都是调用onmessage方法。
比如客户端在onopen的回调中使用websocket .send
或者在别的地方调用websocket.send
,服务器端都是在@OnMessage里处理消息的。反过来也一样,服务端在@OnOpen里调用了sendText,客户端也是在onMessage里处理。