0%

WebSocket简要入门

前言

刚完成一个需求,用到了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
2
3
4
5
6
7
8
9
10
11
12
13
14

var ws = new WebSocket("ws://localhost:8080/chat");

ws.onopen = function(evt) {
console.log("Connection open ...");
};

ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
};

ws.onclose = function(evt) {
console.log("Connection closed.");
};

好,现在对如上代码进行介绍。

1
var ws = new WebSocket("ws://localhost:8080/chat");

这是使用原生WebSocket建立一条Endpoint为/chat的连接,也就是说,一旦new WebSocket(url)浏览器就会开始尝试建立一条Websocket连接。

1
2
3
ws.onopen = function(evt) { 
console.log("Connection open ...");
};

ws.onopen的值是一个函数,这个函数指定了当WebSocket连接成功后执行的操作,参数evt是一个Event。
同理,ws.onclose指定连接关闭后的回调函数,ws.onerror指定报错时的回调函数, ws.onmessage指定收到服务器数据后的回调函数。需要注意的是,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象),可以通过如下方式进行区分操作

1
2
3
4
5
6
7
8
9
10
ws.onmessage = function(event){
if(typeof event.data === String) {
console.log("Received data string");
}

if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}

如果接收到的是对象,可以用如下方式进行解析:
假设服务器端传来的对象是这个:

1
2
3
4
5
@Data
public class student{
string name;
int age;
}

那么可以这样解析

1
2
3
4
ws.onmessage = function (event) {
var stu = JSON.parse(evt.data); //获取到stu对象
console.log("name = "+stu.name+" age = "+stu.age); //使用获取到的数据
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。

1
2
3
4
5
6
7
8
9
10
11
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};

除了以上几个API,还有ws.send()ws.close(),介绍如下:
实例对象的send()方法用于向服务器发送数据。
发送文本的例子

1
ws.send("hello word");

发送JSON对象的例子

1
2
3
4
5
ws.send(JSON.stringify(
{
"name": $("#name").val(),
"age":$("#age").val()
}))

发送Blob对象的例子

1
2
var file = document.querySelector('input[type="file"]').files[0];
ws.send(file);

发送ArrayBuffer对象的例子

1
2
3
4
5
6
7
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
ws.send(binary.buffer);

ws.close()方法用于显式的关闭连接。

服务器端

这里用的是Spring boot,所以需要在maven里添加这个依赖spring-boot-starter-websocket。业务代码如下:
首先需要一个Config类来对@ServerEndpoint进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebSocketConfig {

/**
* 注入一个bean对象,自动注册使用了@ServiceEndpoint的类
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}

}

然后写一个ServerEndpoint类用于处理通讯事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ServerEndpoint(value = "/chat")
@Component
public void WebSocketServer(){

@OnOpen
public void connect(Session session){
//TODO 这里处理连接成功后的事件
}

@OnClose
public void close(Session session){
//TODO 这里处理连接关闭后的事件
}

@OnMessage
public void receiveMsg(Session session,String msg){
//TODO 这里处理接收到前端传来消息时的逻辑
}

@OnError
public void error(Session session){
//TODO 这里在报错时调用
}
}

用@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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@OnOpen
public void connect(Session session){
String msg = "ok, I got you !"
session.getBasicRemote().sendText(msg);
}

@OnMessage(Session session,String msg){
/*
*如果前端发送的是一个JSON对象,这里也会把它解析成String类来接收.
*当我们需要使用解析后的对象,可以用诸如jackson等工具将其反序列化成Object类。
*/
JsonMapper jsonMapper = new JsonMapper();
Student stu = jsonMapper.readValue(msg,Student.class);
/*
* 如果想给前端传一个对象,可以使用session.getBasicRemote().sendObject方法
*/
session.getBasicRemote.sendObject(stu);
}

⚠️当我们需要给前端传一个对象时,直接使用sendObject()方法是会报错的,如javax.websocket.EncodeException: No encoder specified for object of class [这里是你sendObject里传的对象的全名]。报错的原因是我们没有指定编码器。解决方案是自定义一个编码器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* Text<ResponseMessage>里的ResponseMessage是我自己写的一个消息类
* 如果你写了一个名叫Student的类,需要通过sendObject()方法发送,那么这里就是Text<Student>
*/
public class ServerEncoder implements Encoder.Text<ResponseMessage> {

@Override
public void destroy() {
// TODO Auto-generated method stub
// 这里不重要
}
@Override
public void init(EndpointConfig arg0) {
// TODO Auto-generated method stub
// 这里也不重要

}
/*
* encode()方法里的参数和Text<T>里的T一致,如果你是Student,这里就是encode(Student student)
*/
@Override
public String encode(ResponseMessage responseMessage) throws EncodeException {
try {
/*
* 这里是重点,只需要返回Object序列化后的json字符串就行
* 你也可以使用gosn,fastJson来序列化。
*/
JsonMapper jsonMapper = new JsonMapper();
return jsonMapper.writeValueAsString(responseMessage);

} catch ( JsonProcessingException e) {
e.printStackTrace();
return null;
}
}

然后在@ServerEndpoint注解中添加encoders

1
@ServerEndpoint(value = "/chat/{room}",encoders = { ServerEncoder.class })

ok,问题解决
⚠️⚠️⚠️无论双方在哪发送消息,对方接收到消息都是调用onmessage方法。
比如客户端在onopen的回调中使用websocket .send或者在别的地方调用websocket.send,服务器端都是在@OnMessage里处理消息的。反过来也一样,服务端在@OnOpen里调用了sendText,客户端也是在onMessage里处理。

-------------------本文结束 感谢阅读-------------------