一文搞懂四种 WebSocket 使用方式,建议收藏!!!
作者:和耳朵
来源:SegmentFault 思否社区
在这里,我想让大家思考一下我在思维导图中列举的第四种做 WebScoket 支持的方案可能是什么?不知道大家能不能猜对,后文将会给出答案。
Github https://github.com/rookie-ricardo/spring-boot-learning-demo Gitee https://gitee.com/he-erduo/spring-boot-learning-demo
WS简介
在 Web 开发领域,我们最常用的协议是 HTTP,HTTP 协议和 WS 协议都是基于 TCP 所做的封装,但是 HTTP 协议从一开始便被设计成请求 -> 响应的模式,所以在很长一段时间内 HTTP 都是只能从客户端发向服务端,并不具备从服务端主动推送消息的功能,这也导致在浏览器端想要做到服务器主动推送的效果只能用一些轮询和长轮询的方案来做,但因为它们并不是真正的全双工,所以在消耗资源多的同时,实时性也没理想中那么好。
既然市场有需求,那肯定也会有对应的新技术出现,WebSocket 就是这样的背景下被开发与制定出来的,并且它作为 HTML5 规范的一部分,得到了所有主流浏览器的支持,同时它还兼容了 HTTP 协议,默认使用 HTTP 的80端口和443端口,同时使用 HTTP header 进行协议升级。
和 HTTP 相比,WS 至少有以下几个优点:
使用的资源更少:因为它的头更小。 实时性更强:服务端可以通过连接主动向客户端推送消息。 有状态:开启链接之后可以不用每次都携带状态信息。
像握手过程我就不说了,因为它复用了 HTTP 头只需要在维基百科(阮一峰的文章讲的也很明白)上面看一下就明白了,像协议帧的话无非就是:标识符、操作符、数据、数据长度这些协议通用帧,基本都没有深入了解的必要,我认为一般只需要关心 WS 的操作符就可以了。
WS 的操作符代表了 WS 的消息类型,它的消息类型主要有如下六种:
文本消息 二进制消息 分片消息(分片消息代表此消息是一个某个消息中的一部分,想想大文件分片) 连接关闭消息 PING 消息 PONG 消息(PING的回复就是PONG)
J2EE方式
这套代码中定义了一套适用于 WS 开发的注解和相关支持,我们可以利用它和 Tomcat 进行WS 开发,由于现在更多的都是使用 SpringBoot 的内嵌容器了,所以这次我们就来按照 SpringBoot 内嵌容器的方式来演示。
首先是引入 SpringBoot - Web 的依赖,因为这个依赖中引入了内嵌式容器 Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
@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;
}
@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("连接关闭");
}
}
@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 {
}
细心的小伙伴们可能发现了,示例中的 WebSocketServer 类还有一个 @Component 注解,这是由于我们使用的是内嵌容器,而内嵌容器需要被 Spring 管理并初始化,所以需要给 WebSocketServer 类加上这么一个注解,所以代码中还需要有这么一个配置:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
Spring方式
使用它的第一步我们先引入 SpringBoot - WS 依赖,这个依赖包也会隐式依赖 SpringBoot - Web 包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@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:是否支持分片消息。
BinaryMessage:二进制消息体 TextMessage:文本消息体 PingMessage:Ping 消息体 PongMessage:Pong 消息体
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);
}
}
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 {
}
@Configuration
@EnableWebSocket
public class SpringSocketConfig implements WebSocketConfigurer {
@Autowired
private SpringSocketHandle springSocketHandle;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");
}
}
Spring 的方式也就以上这些内容了,不知道大家是否感觉 Spring 所提供的 WS 封装要比 J2EE 的更方便也更全面一些,起码我只要看 WebSocketHandler 接口就能知道所有常用功能的用法,所以对于 WS 开发来说我是比较推荐 Spring 方式的。
最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
SocoketlO方式
Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。
不过我要先给大家提个醒,不再建议使用它了,不是因为它很久没更新了,而是因为它支持的 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>
@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());
}
}
[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());
}
}
最后再上一个简陋的效果图:
function changeSocketStatus() {
let element = document.getElementById("socketStatus");
if (socketStatus) {
element.textContent = "关闭WebSocket";
const socketUrl="ws://127.0.0.1:8001";
socket = io.connect(socketUrl, {
transports: ['websocket'],
path: "/socketio-ws"
});
//打开事件
socket.on('connect', () => {
console.log("websocket已打开");
});
//获得消息事件
socket.on('chatMsg', (msg) => {
const serverMsg = "收到服务端信息:" + msg;
pushContent(serverMsg, 2);
});
//关闭事件
socket.on('disconnect', () => {
console.log("websocket已关闭");
});
//发生了错误事件
socket.on('connect_error', () => {
console.log("websocket发生了错误");
})
}
}
第四种方式?
注意:以下内容如果没有 Netty 基础可能一脸蒙的进,一脸蒙的出,不过还是建议大家看看,Netty 其实很简单。
第一步需要先引入一个 Netty 开发包,我这里为了方便一般都是 All In:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
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();
}
}
}
@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();
}
}
一图胜千言,我想不用多说大家也都知道具体的类是处理什么消息了把,在上文的示例中我们是一定了一个文本 WS 消息的处理类,如果你想处理其他数据类型的消息,可以将泛型中的 TextWebSocketFrame 换成其他 WebSocketFrame 类就可以了。
至于为什么没有连接成功后的处理,这个是和 Netty 的相关机制有关,可以在 channelActive 方法中处理,大家有兴趣的可以了解一下 Netty。
最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
总结
在上文中,我总共介绍了四种在 Java 中使用 WS 的方式,从我个人使用意向来说我感觉应该是这样的:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种选择:SocketIO。
最后,我估计某些读者会去具体拉代码看代码,所以我简单说一下代码结构:
├─java
│ └─com
│ └─example
│ └─springwebsocket
│ │ SpringWebsocketApplication.java
│ │ TestController.java
│ │
│ ├─j2ee
│ │ WebSocketConfig.java
│ │ WebSocketServer.java
│ │
│ ├─socketio
│ │ SocketIoConfig.java
│ │ SocketIoHandle.java
│ │
│ └─spring
│ SpringSocketConfig.java
│ SpringSocketHandle.java
│
└─resources
└─templates
J2eeIndex.html
SocketIoIndex.html
SpringIndex.html
我没有往里面放 Netty 的代码,是因为感觉 Netty 部分内容很少,文章示例中的代码直接复制就能用,后面如果写 Netty 的话会再开一个 Netty 模块用来放 Netty 相关的代码。
好了,今天的内容就到这了,希望对大家有帮助的话可以帮我文章点点赞,GitHub 也点点赞,大家的点赞与评论都是我更新的不懈动力,下期见。
评论