WebSocket 协议是基于 TCP 的一种网络协议,它实现了浏览器与服务器全双工(Full-duplex)通信——允许服务器主动发送信息给客户端。
以前,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每 1 秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断地向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
在这种情况下,HTML 5 定义了 WebSocket 协议,能更好得节省服务器资源和带宽,并且能够更实时地进行通讯。WebSocket 协议在 2008 年诞生,2011 年成为国际标准,现在主流的浏览器都已经支持。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
优点
WebSocket 在握手之后便直接基于 TCP 进行消息通信,但 WebSocket 只是 TCP 上面非常轻的一层,它仅仅将 TCP 的字节流转换成消息流(文本或二进制),至于怎么解析这些消息的内容完全依赖于应用本身。
因此为了协助 Client 与 Server 进行消息格式的协商,WebSocket 在握手的时候保留了一个子协议字段。
STOMP 即 Simple(or Streaming)Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理(Broker)进行交互。STOMP 协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到了广泛的应用。
STOMP 协议并不是为 Websocket 设计的,它是属于消息队列的一种协议,它和 Amqp、Jms 平级。只不过由于它的简单性恰巧可以用于定义 Websocket 的消息体格式。可以这么理解,Websocket 结合 Stomp 子协议段,来让客户端和服务器在通信上定义的消息内容达成一致。
STOMP 协议分为客户端和服务端,具体如下。
STOMP 服务端被设计为客户端可以向其发送消息的一组目标地址。STOMP 协议并没有规定目标地址的格式,它由使用协议的应用自己来定义。例如,/topic/a、/queue/a、queue-a 对于 STOMP 协议来说都是正确的。应用可以自己规定不同的格式以此来表明不同格式代表的含义。比如应用自己可以定义以 /topic 打头的为发布订阅模式,消息会被所有消费者客户端收到,以 /user 开头的为点对点模式,只会被一个消费者客户端收到。
对于 STOMP 协议来说,客户端会扮演下列两种角色的任意一种:
实际上,WebSocket 结合 STOMP 相当于构建了一个消息分发队列,客户端可以在上述两个角色间转换,订阅机制保证了一个客户端消息可以通过服务器广播到多个其他客户端,作为生产者,又可以通过服务器来发送点对点消息。
COMMAND
header1:value1
header2:value2
Body^@
其中,^@ 表示行结束符。
一个 STOMP 帧由三部分组成:命令、Header(头信息)、Body(消息体)。
来看一个实际的帧例子:
SEND
destination:/broker/roomId/1
content-length:57
{“type":"OUT","content":"ossxxxxx-wq-yyyyyyyy"}
第 1 行:表明此帧为 SEND 帧,是 COMMAND 字段。
第 2 行:Header 字段,消息要发送的目的地址,是相对地址。
第 3 行:Header 字段,消息体字符长度。
第 4 行:空行,间隔 Header 与 Body。
第 5 行:消息体,为自定义的 JSON 结构。
更多 STOMP 协议细节,可以参考 STOMP 官网。
Websocket 使用 ws 或 wss 的统一资源标示符,类似于 HTTPS,其中 wss 表示在 TLS 之上的 Websocket。例如:
ws://example.com/wsapi
wss://secure.example.com/
Websocket 使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,Websocket 协议使用 80 端口;运行在 TLS 之上时,默认使用 443 端口。
下面是一个页面使用 Websocket 的示例:
var ws = new WebSocket("ws://localhost:8080");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
Spring Boot 提供了 Websocket 组件 spring-boot-starter-websocket,用来支持在 Spring Boot 环境下对 Websocket 的使用。
Websocket 双向通讯的特性非常适合开发在线聊天室,这里以在线多人聊天室为示例,演示 Spring Boot Websocket 的使用。
首先我们梳理一下聊天室都有什么功能:
利用前端框架 Bootstrap 渲染页面,使用 HTML 搭建页面结构,完整页面内容如下(index.html):
chat room websocket
聊天室
最上面使用 textarea 画一个对话框,用来显示聊天室的内容;中间部分添加用户加入聊天室和离开聊天室的按钮,按钮上面是输入用户名的入口;页面最下面添加发送消息的入口,页面显示效果如下:
接下来在页面添加 WebSocket 通讯代码:
这段代码的功能主要是监听三个按钮的点击事件,当用户登录、离开、发送消息是调用对应的 WebSocket 事件,将信息传送给服务端。用户登录时创建了 WebSocket 对象,页面会监控 WebSocket 事件,将后端服务和前端通讯室将对应的信息展示在页面。
引入依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
主要添加 Web 和 Websocket 组件。
启动类需要添加 @EnableWebSocket 开启 WebSocket 功能。
@EnableWebSocket
@SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
在创建服务端消息接收功能之前,我们先创建一个 WebSocketUtils 工具类,用来存储聊天室在线的用户信息,以及发送消息的功能。首先定义一个全局变量 ONLINE_USER_SESSIONS 用来存储在线用户,使用 ConcurrentHashMap 提升高并发时效率。
public static final Map ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
封装消息发送方法,在发送之前首先判单用户是否存在再进行发送:
public class WebSocketUtils {
private static final Logger logger = LoggerFactory.getLogger(WebSocketUtils.class);
/**
* 存储websocket session
*/
public static final Map ONLINE_USER_SESSIONS = new ConcurrentHashMap();
public static void sendMessage(Session session, String message) {
if (session == null) {
return;
}
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null) {
return;
}
try {
basic.sendText(message);
} catch (IOException e) {
logger.error("sendMessage IOException ",e);
}
}
public static void sendMessageAll(String message) {
ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message));
}
}
聊天室的消息是所有在线用户可见,因此每次消息的触发实际上是遍历所有在线用户,给每个在线用户发送消息。
其中,ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message)) 是 JDK 1.8 forEach 的简洁写法。
这样我们在创建 ChatRoomServerEndpoint 类的时候就可以直接将工具类的方法和全局变量导入:
import static com.neo.utils.WebSocketUtils.ONLINE_USER_SESSIONS;
import static com.neo.utils.WebSocketUtils.sendMessageAll;
接收类上需要添加 @ServerEndpoint(“url”) 代表监听此地址的 WebSocket 信息。
import static com.rede.course01.utils.WebSocketUtils.ONLINE_USER_SESSIONS;
import static com.rede.course01.utils.WebSocketUtils.sendMessageAll;
@RestController
@ServerEndpoint("/chat-room/{username}")
public class ChatRoomServerEndpoint {
private static final Logger logger = LoggerFactory.getLogger(ChatRoomServerEndpoint.class);
@OnOpen
public void openSession(@PathParam("username") String username, Session session) {
ONLINE_USER_SESSIONS.put(username, session);
String message = "欢迎用户[" + username + "] 来到聊天室!";
logger.info("用户登录:"+message);
sendMessageAll(message);
}
@OnMessage
public void onMessage(@PathParam("username") String username, String message) {
logger.info("发送消息:"+message);
sendMessageAll("用户[" + username + "] : " + message);
}
@OnClose
public void onClose(@PathParam("username") String username, Session session) {
//当前的Session 移除
ONLINE_USER_SESSIONS.remove(username);
//并且通知其他人当前用户已经离开聊天室了
sendMessageAll("用户[" + username + "] 已经离开聊天室了!");
try {
session.close();
} catch (IOException e) {
logger.error("onClose error",e);
}
}
@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
logger.error("onError excepiton",e);
}
logger.info("Throwable msg "+throwable.getMessage());
}
}
到此我们服务端内容就开发完毕了。
启动 spring-boot-websocket 项目,在浏览器中输入地址 http://localhost:8080/ 打开两个页面进行测试。
在第一个页面中以用户“小王”登录聊天室,第二个页面以“小张”登录聊天室。
大家在两个页面模式小王和小张对话,可以看到两个页面的展示效果,页面都可实时无刷新展示最新聊天内容,页面最终展示效果如下:
总结:
这节课首先介绍了 WebSocket,以及 WebSocket 的相关特性和优点,Spring Boot 提供了 WebSocket 对应的组件包,因此很容易让我们集成在项目中。利用 WebSocket 可以双向通讯的特点做了一个简易版的聊天室,来验证 WebSocket 相关特性,通过示例实践发现 WebSocket 双向通讯机制非常高效简洁,特别适合在服务端和客户端通讯较多的场景下使用,相比以前的轮询方式更加优雅易用。