应用场景
WebSocket常用在那些场景 常见的应用场景如下:
在线聊天:实现实时的文本聊天功能。
直播和视频会议:实现实时的音频和视频传输。
游戏:实现实时的游戏状态同步和控制。
实时监控和控制:实现实时的硬件监控和控制。
数据可视化:实现实时的数据可视化和分析。
在线协作:实现实时的文档协作和编辑。
智能家居:实现实时的智能家居控制。
推送消息:实现实时的推送消息功能
J2EE(J2EE 是使用 Java 技术开发企业级应用的工业标准\javaee是一系列的规范及实现\ 核心是EJB\servlet\jdbc\jsp)
WS 的操作符代表了 WS 的消息类型,它的消息类型主要有如下六种:
- 文本消息
- 二进制消息
- 分片消息(分片消息代表此消息是一个某个消息中的一部分,想想大文件分片)
- 连接关闭消息
- PING 消息
- PONG 消息(PING的回复就是PONG)
一.J2EE 方式则是指 Tomcat 为 WS 所做的支持,这套代码的包名前缀叫做:javax.websocket。
引入 SpringBoot - Web 的依赖,因为这个依赖中引入了内嵌式容器 Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
二. 将一个类定义为 WS 服务器, 需要为这个类加上@ServerEndpoint注解 ,在这个注解中比较常用的有三个参数:WS路径、序列化处理类、反序列化处理类。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ServerEndpoint {
String value();
String[] subprotocols() default {};
Class<? extends Decoder>[] decoders() default {};
Class<? extends Encoder>[] encoders() default {};
Class<? extends Configurator> configurator() default Configurator.class;
}
具体的一个 WS 服务器类示例:
@Component
@ServerEndpoint("/j2ee-ws/{msg}")
public class WebSocketServer {
//建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "msg") String msg){
System.out.println("WebSocketServer 收到连接: " + session.getId() + ", 当前消息:" + msg);
}
//收到客户端信息
@OnMessage
public void onMessage(Session session, String message) throws IOException {
message = "WebSocketServer 收到连接:" + session.getId() + ",已收到消息:" + message;
System.out.println(message);
session.getBasicRemote().sendText(message);
}
//连接关闭
@OnClose
public void onclose(Session session){
System.out.println("连接关闭");
}
}
在以上代码中, 着重关心 WS 相关的注解,主要有以下四个:
@ServerEndpoint: 这里就像 RequestMapping 一样,放入一个 WS 服务器监听的 URL。@OnOpen:这个注解修饰的方法会在 WS 连接开始时执行。@OnClose:这个注解修饰的方法则会在 WS 关闭时执行。@OnMessage:这个注解则是修饰消息接受的方法,并且由于消息有文本和二进制两种方式,所以此方法参数上可以使用 String 或者二进制数组的方式,就像下面这样:
@OnMessage
public void onMessage(Session session, String message) throws IOException {
}
@OnMessage
public void onMessage(Session session, byte[] message) throws IOException {
}
除了以上这几个以外,常用的功能方面还差一个分片消息、Ping 消息 和 Pong 消息, 示例中的 WebSocketServer 类还有一个 @Component 注解,这是由于我们使用的是内嵌容器,而内嵌容器需要被 Spring 管理并初始化,所以需要给 WebSocketServer 类加上这么一个注解,所以代码中还需要有这么一个配置:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
Tips:在不使用内嵌容器的时候可以不做以上步骤。
Spring 方式
Spring 作为 Java 开发界的老大哥,几乎封装了一切可以封装的, 比 J2EE 的更易用。
一、 先引入 SpringBoot - WS 依赖,这个依赖包也会隐式依赖 SpringBoot - Web 包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
二、准备一个用来处理 WS 请求的 Handle了,Spring 为此提供了一个接口 WebSocketHandler, 通过实现此接口重写其接口方法的方式自定义逻辑 例子:
@Component
public class SpringSocketHandle implements WebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("SpringSocketHandle, 收到新的连接: " + session.getId());
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String msg = "SpringSocketHandle, 连接:" + session.getId() + ",已收到消息。";
System.out.println(msg);
session.sendMessage(new TextMessage(msg));
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println("WS 连接发生错误");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
System.out.println("WS 关闭连接");
}
// 支持分片消息
@Override
public boolean supportsPartialMessages() {
return false;
}
}
WebSocketHandler 接口中的五个函数, 它具有什么功能:
- afterConnectionEstablished:连接成功后调用。
- handleMessage:处理发送来的消息。
- handleTransportError: WS 连接出错时调用。
- afterConnectionClosed:连接关闭后调用。
- supportsPartialMessages:是否支持分片消息。
重点可以来看一下 handleMessage 方法,handleMessage 方法中有一个 WebSocketMessage 参数,这也是一个接口,一般不直接使用这个接口而是使用它的实现类,它有以下几个实现类:
- BinaryMessage:二进制消息体
- TextMessage:文本消息体
- PingMessage: Ping ****消息体
- PongMessage: Pong ****消息体
但是由于 handleMessage 这个方法参数是WebSocketMessage, 实际使用中需要判断一下当前来的消息具体是它的哪个子类,比如:
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
** if (message instanceof TextMessage) {**
this.handleTextMessage(session, (TextMessage)message);
** } else if (message instanceof BinaryMessage) {**
this.handleBinaryMessage(session, (BinaryMessage)message);
}
}
为了避免这些重复性代码,Spring 给我们定义了一个 AbstractWebSocketHandler,它已经封装了这些重复劳动,直接继承AbstractWebSocketHandler这个类 重写要处理的消息类型:
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
}
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
}
复制代码
上面都是对于 Handle 的操作,有了 Handle 之后还需要将它绑定在某个 URL 上【监听某个 URL】,需要以下配置:
@Configuration
@EnableWebSocket
public class SpringSocketConfig implements WebSocketConfigurer {
@Autowired
private SpringSocketHandle springSocketHandle;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");
}
}
这里我把我的自定义 Handle 注册到 "/spring-ws" 上面并设置了一下跨域,在整个配置类上还要打上@EnableWebSocket 注解,用于开启 WS 监听。
Spring 所提供的 WS 封装要比 J2EE 的更方便也更全面一些, 只要看 WebSocketHandler 接口就能知道所有常用功能的用法,所以对于 WS 开发来说 比较推荐 Spring 方式的。
SocketIO 方式
Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库,两者有着几乎一样的API。
Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。
所以我觉得使用它更多是因为兼容性,因为 HTML5 之后原生的 WS 应该也够用了,然而它是一个前端库,所以 Java 语言这块并没有官方支持,好在民间大神已经以 Netty 为基础开发了能与它对接的 Java 库: netty-socketio。
不过我要先给大家提个醒,不再建议使用它了,不是因为它很久没更新了,而是因为它支持的 Socket-Client 版本太老了,截止到 2022-04-29 日,SocketIO 已经更新到 4.X 了,但是 NettySocketIO 还只支持 2.X 的 Socket-Client 版本。
第一步还是引入最新的依赖:
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.19</version>
</dependency>
复制代码
第二步就是配置一个 WS 服务:
@Configuration
public class SocketIoConfig {
@Bean
public SocketIOServer socketIOServer() {
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setHostname("127.0.0.1");
config.setPort(8001);
config.setContext("/socketio-ws");
SocketIOServer server = new SocketIOServer(config);
server.start();
return server;
}
@Bean
public SpringAnnotationScanner springAnnotationScanner() {
return new SpringAnnotationScanner(socketIOServer());
}
}
大家在上文的配置中,可以看到设置了一些 Web 服务器参数,比如:端口号和监听的 path,并将这个服务启动起来,服务启动之后日志上会打印这样一句日志:
[ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 8001
这就代表启动成功了,接下来就是要对 WS 消息做一些处理了:
@Component
public class SocketIoHandle {
/**
* 客户端连上socket服务器时执行此事件
* @param client
*/
@OnConnect
public void onConnect(SocketIOClient client) {
System.out.println("SocketIoHandle 收到连接:" + client.getSessionId());
}
/**
* 客户端断开socket服务器时执行此事件
# * @param client
*/
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
System.out.println("当前链接关闭:" + client.getSessionId());
}
@OnEvent( value = "onMsg")
public void onMessage(SocketIOClient client, AckRequest request, Object data) {
System.out.println("SocketIoHandle 收到消息:" + data);
request.isAckRequested();
client.sendEvent("chatMsg", "我是 NettySocketIO 后端服务,已收到连接:" + client.getSessionId());
}
}
第三个方法:为什么@OnEvent( value = "onMsg")里面这个值是自定义的,这就涉及到 SocketIO 里面发消息的机制了,通过 SocketIO 发消息是要发给某个事件的,所以这里的第三个方法就是监听 发给onMsg事件的所有消息,监听到之后我又给客户端发了一条消息,这次发给的事件是:chatMsg,客户端也需要监听此事件才能接收到这条消息。
Netty
Netty 作为 Java 界大名鼎鼎的开发组件,对于常见协议也全部进行了封装,所以我们可以直接在 Netty 中去很方便的使用 WebSocket
一个 Netty 开发包,我这里为了方便一般都是 All In:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
第二步:启动一个 Netty 容器了,配置比较关键:
public class WebSocketNettServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(boss, work)
.channel(NioServerSocketChannel.class)
//设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.localAddress(8080)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
// HTTP 请求解码和响应编码
.addLast(new HttpServerCodec())
// HTTP 压缩支持
.addLast(new HttpContentCompressor())
// HTTP 对象聚合完整对象
.addLast(new HttpObjectAggregator(65536))
// WebSocket支持
.addLast(new WebSocketServerProtocolHandler("/ws"))
.addLast(WsTextInBoundHandle.INSTANCE);
}
});
//绑定端口号,启动服务端
ChannelFuture channelFuture = bootstrap.bind().sync();
System.out.println("WebSocketNettServer启动成功");
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully().syncUninterruptibly();
work.shutdownGracefully().syncUninterruptibly();
}
}
}
以上代码 主要关心端口号和重写的 ChannelInitializer 就行了,里面定义了五个过滤器(Netty 使用责任链模式),前面三个都是 HTTP 请求的常用过滤器(毕竟 WS 握手是使用 HTTP 头的所以也要配置 HTTP 支持),第四个则是 WS 的支持,它会拦截 /ws 路径,
最关键的就是第五个了过滤器它是我们具体的业务逻辑处理类,效果基本和 Spring 中的 Handle 差不多, 代码:
@ChannelHandler.Sharable
public class WsTextInBoundHandle extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private WsTextInBoundHandle() {
super();
System.out.println("初始化 WsTextInBoundHandle");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("WsTextInBoundHandle 收到了连接");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String str = "WsTextInBoundHandle 收到了一条消息, 内容为:" + msg.text();
System.out.println(str);
System.out.println("-----------WsTextInBoundHandle 处理业务逻辑-----------");
String responseStr = "{"status":200, "content":"收到"}";
ctx.channel().writeAndFlush(new TextWebSocketFrame(responseStr));
System.out.println("-----------WsTextInBoundHandle 数据回复完毕-----------");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("WsTextInBoundHandle 消息收到完毕");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("WsTextInBoundHandle 连接逻辑中发生了异常");
cause.printStackTrace();
ctx.close();
}
}
主要是看一下这个类的泛型:TextWebSocketFrame,很明显这是一个 WS 文本消息的类,我们顺着它的定义去看发现它继承了 WebSocketFrame,接着我们去看它的子类:
一图胜千言,示例中我们是一定了一个文本 WS 消息的处理类,如果你想处理其他数据类型的消息,可以将泛型中的 TextWebSocketFrame 换成其他 WebSocketFrame 类就可以了 。
至于为什么没有连接成功后的处理,这个是和 Netty 的相关机制有关,可以在 channelActive 方法中处理
总结:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种选择:SocketIO。