When the previous company was doing the IM messaging system, it always used WebSocket as the basic component for sending and receiving messages. Today, I will talk to you about the four common postures of using WebSocket in Java. If you need it in the future or now The use of WebSoocket can be a reference.
The above mind map has listed three ways to use WebSocket. The following will explain their characteristics one by one. Different methods have different characteristics. We will not list them first.
Here, I want you to think about what the fourth solution I listed in the mind map to support WebScoket might be? I don’t know if you can guess right, but the answer will be given later.
The code of this article: the spring-websocket module in the following warehouse, after pulling the whole warehouse, you can compile this module separately in the IDEA Maven toolbar.
- Github
Introduction to WS
Before the official start, I feel it is necessary to briefly introduce the WebSocket protocol. Before introducing anything, it is necessary to know why we need it?
In the field of web development, our most commonly used protocol is HTTP. Both HTTP protocol and WS protocol are encapsulated based on TCP, but HTTP protocol has been designed as a request->response mode from the very beginning, so for a long time Internal HTTP can only be sent from the client to the server, and does not have the function of actively pushing messages from the server, which also leads to the effect of active server push on the browser side. The polling scheme is used to do it, but because they are not true full-duplex, they consume a lot of resources and the real-time performance is not as good as ideal.
Since there is a demand in the market, there will definitely be corresponding new technologies. WebSocket was developed and formulated in this context, and as part of the HTML5 specification, it is supported by all major browsers, and it is also compatible with The HTTP protocol is used, and the port 80 and 443 of HTTP are used by default, and the HTTP header is used for protocol upgrade.
Compared with HTTP, WS has at least the following advantages:
- Less resources used: because its header is smaller.
- More real-time: The server can actively push messages to the client through the connection.
- Stateful: After opening the link, you don't need to carry the state information every time.
In addition to these advantages, I think that for WS, our developers should at least understand the meaning of its handshake process and protocol frames, just like when learning TCP, we need to understand the meaning of each byte frame of the TCP header.
I won't talk about the handshake process, because it reuses the HTTP header. You only need to read it on Wikipedia (Yifeng Ruan's article is also very clear) to understand. For example, the protocol frame is nothing more than: identifier, operation There is basically no need for in-depth understanding of the general frames of the protocols such as symbols, data, and data lengths. I think generally you only need to care about the operators of WS.
The operator of WS represents the message type of WS, and its message types mainly include the following six:
- text message
- binary message
- Fragmented message (a fragmented message means that the message is part of a certain message, think large file fragmentation)
- connection closed message
- PING message
- PONG message (PING's reply is PONG)
Now that we know that WS mainly has the above six operations, a normal WS framework should be able to easily handle the above types of messages, so the next part is the core content of this article, to see if the following WS frameworks can It is very convenient to handle these kinds of WS messages.
J2EE way
Let’s start with J2EE. Generally, I call the extension of JavaWeb in the javax package as J2EE. I don’t think it is necessary to investigate whether this definition is completely correct. It is just a personal habit. The package name prefix of this code is called: javax.websocket
.
This set of code defines a set of annotations and related support suitable for WS development. We can use it and Tomcat for WS development. Since most of the embedded containers of SpringBoot are now used, this time we will follow the SpringBoot embedded container to demonstrate.
The first is to introduce the dependency of SpringBoot - Web
, because this dependency introduces the embedded container Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
The next step is to define a class as a WS server. This step is also very simple. You only need to add @ServerEndpoint
annotation to this class. There are three commonly used parameters in this annotation: WS path, sequence Processing class, deserialization processing class.
@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;
}
Next, let's look at a specific example of a WS server 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("连接关闭");
}
}
In the above code, we focus on WS-related annotations, mainly the following four:
-
@ServerEndpoint
: Just like RequestMapping, put a URL monitored by the WS server. -
@OnOpen
: The method decorated with this annotation will be executed when the WS connection starts. -
@OnClose
: The method modified by this annotation will be executed when WS is closed. @OnMessage
: This annotation is the method of modifying the message acceptance, and since the message has two modes of text and binary, String or binary array can be used as the parameter of this method, as follows:@OnMessage public void onMessage(Session session, String message) throws IOException { } @OnMessage public void onMessage(Session session, byte[] message) throws IOException { }
In addition to the above, the commonly used functions are one fragmentation message, Ping message and Pong message. For these three functions, I did not find the relevant usage, but only saw a PongMessage interface in the interface list of the source code. Readers and friends who know it can point it out in the comment area.
Careful friends may have found that there is another WebSocketServer class in the example@Component 注解
, this is because we are using an inline container, and the inline container needs to be managed and initialized by Spring, so we need to give The WebSocketServer class adds such an annotation, so there is also such a configuration in the code:@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
Tips : You can skip the above steps when you don't use an inline container.
Finally, the last simple WS effect example diagram, the front-end side directly uses the HTML5 WebScoket standard library, you can check my warehouse code for details:Spring way
The second part is about the Spring method. As the big brother in the Java development world, Spring encapsulates almost everything that can be encapsulated. For WS development, Spring also provides a set of related support, and I think it is easier to use than J2EE in terms of use. .
The first step to use it is to introduce theSpringBoot - WS
dependency, which also implicitly depends on the SpringBoot - Web package:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
The second step is to prepare a Handle for handling WS requests. Spring provides an interface for this - WebSocketHandler. We can customize the logic by implementing this interface and rewriting its interface methods. Let's look at an example:
@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; } }
The above example is a good example of the five functions in the WebSocketHandler interface. We should know what it has by name:
- afterConnectionEstablished : Called after the connection is successful.
- handleMessage : handle the sent message.
- handleTransportError: Called when there is an error in the WS connection.
- afterConnectionClosed : Called after the connection is closed.
- supportsPartialMessages : Whether to support fragmented messages.
The above methods can focus on the handleMessage method. There is a WebSocketMessage
parameter in the handleMessage method, which is also an interface. We generally do not use this interface directly but use its implementation class. It has the following Several implementation classes:
- BinaryMessage : binary message body
- TextMessage : text message body
- PingMessage: Ping message body
- PongMessage: Pong message body
But since handleMessage
this method parameter is WebSocketMessage
, so we may need to judge which subclass of the current message is in actual use, for example:
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);
}
}
But it’s not a problem to always write like this. In order to avoid these repetitive codes, Spring defines a AbstractWebSocketHandler
for us, which has encapsulated these repetitive tasks. We can directly inherit this class and then rewrite what we want Types of messages handled:
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 {
}
The above part is all about the operation of Handle. After we have Handle, we need to bind it to a certain URL, or listen to a certain URL, so the following configuration is necessary:
@Configuration
@EnableWebSocket
public class SpringSocketConfig implements WebSocketConfigurer {
@Autowired
private SpringSocketHandle springSocketHandle;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");
}
}
Here I registered my custom Handle to "/spring-ws"
and set up the cross-domain, and also marked the @EnableWebSocket
annotation on the entire configuration class to enable WS monitoring.
Spring’s method is the same as the above. I don’t know if you feel that the WS package provided by Spring is more convenient and more comprehensive than J2EE. At least I can know the usage of all common functions just by looking at the WebSocketHandler interface, so for WS For development, I prefer the Spring approach.
Finally, the last simple WS effect example diagram, the front-end side directly uses the HTML5 WebScoket standard library, you can check my warehouse code for details:
SocketIO method
The SocketIO method is a bit different from the above two, because SocketIO was born for the sake of compatibility. Front-end readers should be more familiar with it because it is a JS library. Let's take a look at Wikipedia. definition:
Socket.IO is a JavaScript library for real-time web applications. It enables real-time two-way communication between server and client. It has two parts: a client-side library that runs in the browser, and a server-side library for Node.js, both with almost the same API.
Socket.IO mainly uses the WebSocket protocol. But if needed, Socket.io can fall back to several other methods, such as Adobe Flash Sockets, JSONP pull, or traditional AJAX pull, and provide the exact same interface at the same time.
So I think it is more for compatibility, because the native WS after HTML5 should be enough, but it is a front-end library, so there is no official support for the Java language. Fortunately, the folk gods have been based on Netty Developed a Java library that can interface with it: netty-socketio
.
But I want to warn you first, it is no longer recommended to use it, not because it has not been updated for a long time, but because the Socket-Client version it supports is too old, as of 2022-04-29, SocketIO has It has been updated to 4.X, but NettySocketIO only supports the Socket-Client version of 2.X.
Having said that, it's time to teach you how to use it. The first step is to introduce the latest dependencies:
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.19</version>
</dependency>
The second step is to configure a WS service:
@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());
}
}
In the above configuration, you can see that some Web server parameters are set, such as port number and listening path, and the service is started. After the service is started, the log will print such a log:
[ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 8001
This means that the startup is successful, and the next step is to do some processing on the WS message:
@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());
}
}
I believe that for the above code, the first two methods are easy to understand, but the third method is difficult to understand if you have not touched SocketIO. Why @OnEvent( value = "onMsg")
This value is customized, This involves the mechanism of sending messages in SocketIO. Sending a message through SocketIO is to send a message to an event, so the third method here is to monitor all messages sent to the onMsg
event, and listen to all the messages sent to the event. Then I sent another message to the client, this time the event sent is: chatMsg
, the client also needs to listen to this event to receive this message.
Finally, let's go to a simple rendering:
Since the front-end code is no longer the standard HTML5 connection method, I will briefly paste the relevant code here. For more details, please refer to my code repository:
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发生了错误");
})
}
}
Fourth way?
The fourth way is actually Netty. Netty, as a well-known development component in the Java world, also encapsulates all common protocols, so we can easily use WebSocket directly in Netty. Next, we can see how Netty works WS server for development.
Note: If you don't have Netty foundation, the following content may be blinded in and out, but it is recommended that you take a look. Netty is actually very simple.
The first step is to introduce a Netty development kit. I usually use All In here for convenience:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.75.Final</version>
</dependency>
In the second step, you need to start a Netty container. There are many configurations, but the most important ones are:
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();
}
}
}
In the above code, we mainly care about the port number and the rewritten ChannelInitializer
. We define five filters (Netty uses the chain of responsibility mode), and the first three are commonly used filters for HTTP requests (after all The WS handshake uses HTTP headers, so HTTP support should also be configured), the fourth is the support of WS, it will intercept the /ws
path, the most critical is the fifth filter, which is our specific The business logic processing class is basically the same as the Handle in the Spring department. Let's take a look at the code:
@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();
}
}
I won't say any of the methods here, just look at the name and you'll know it. The main thing is to look at the generic type of this class: TextWebSocketFrame , obviously this is a WS text message class, we follow its definition to find it Inheriting WebSocketFrame , then let's look at its subclasses:
A picture is worth a thousand words, I think it goes without saying that everyone knows what messages a specific class handles. In the above example, we must have a text WS message processing class. If you want to process other data types message, you can replace TextWebSocketFrame in generics with other WebSocketFrame classes .
As for why there is no processing after the connection is successful, this is related to the related mechanism of Netty, which can be processed in the channelActive method. If you are interested, you can learn about Netty.
Finally, the last simple WS effect example diagram, the front-end side directly uses the HTML5 WebScoket standard library, you can check my warehouse code for details:
Summarize
Five thousand words eloquently, don't forget to praise when you have gained something.
In the above, I have introduced a total of four ways to use WS in Java. From my personal use intention, I feel that it should be like this: Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式
Of course, if your business has a browser Compatibility issues, in fact, there is only one choice: SocketIO.
Finally, I estimate that some readers will go to the specific code to see the code, so I will briefly talk about the code structure:
├─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
The code structure is as shown above. The application code is divided into three folders, and the specific sample codes of the three methods are respectively placed. There are also three HTML files in the templates folder under the resource folder, which are the HTML pages corresponding to the three samples. I have preset the link address and port inside, just pull it down and compile this module separately and run it.
I didn't put Netty's code in it because I felt that there was very little content in Netty. The code in the example in the article can be used directly. If I write Netty later, I will open a Netty module to put Netty-related code.
Well, that's all for today's content. I hope that if it is helpful to you, you can give me a like for the article, and GitHub also likes it. Everyone's likes and comments are the unremitting motivation for my update, see you in the next issue.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。