一文搞懂四種 WebSocket 使用方式,建議收藏!!!
作者:和耳朵
來源:SegmentFault 思否社區(qū)

在這里,我想讓大家思考一下我在思維導圖中列舉的第四種做 WebScoket 支持的方案可能是什么?不知道大家能不能猜對,后文將會給出答案。
Github https://github.com/rookie-ricardo/spring-boot-learning-demo Gitee https://gitee.com/he-erduo/spring-boot-learning-demo
WS簡介
在 Web 開發(fā)領域,我們最常用的協(xié)議是 HTTP,HTTP 協(xié)議和 WS 協(xié)議都是基于 TCP 所做的封裝,但是 HTTP 協(xié)議從一開始便被設計成請求 -> 響應的模式,所以在很長一段時間內 HTTP 都是只能從客戶端發(fā)向服務端,并不具備從服務端主動推送消息的功能,這也導致在瀏覽器端想要做到服務器主動推送的效果只能用一些輪詢和長輪詢的方案來做,但因為它們并不是真正的全雙工,所以在消耗資源多的同時,實時性也沒理想中那么好。
既然市場有需求,那肯定也會有對應的新技術出現(xiàn),WebSocket 就是這樣的背景下被開發(fā)與制定出來的,并且它作為 HTML5 規(guī)范的一部分,得到了所有主流瀏覽器的支持,同時它還兼容了 HTTP 協(xié)議,默認使用 HTTP 的80端口和443端口,同時使用 HTTP header 進行協(xié)議升級。
和 HTTP 相比,WS 至少有以下幾個優(yōu)點:
使用的資源更少:因為它的頭更小。 實時性更強:服務端可以通過連接主動向客戶端推送消息。 有狀態(tài):開啟鏈接之后可以不用每次都攜帶狀態(tài)信息。
像握手過程我就不說了,因為它復用了 HTTP 頭只需要在維基百科(阮一峰的文章講的也很明白)上面看一下就明白了,像協(xié)議幀的話無非就是:標識符、操作符、數(shù)據(jù)、數(shù)據(jù)長度這些協(xié)議通用幀,基本都沒有深入了解的必要,我認為一般只需要關心 WS 的操作符就可以了。
WS 的操作符代表了 WS 的消息類型,它的消息類型主要有如下六種:
文本消息 二進制消息 分片消息(分片消息代表此消息是一個某個消息中的一部分,想想大文件分片) 連接關閉消息 PING 消息 PONG 消息(PING的回復就是PONG)
J2EE方式
這套代碼中定義了一套適用于 WS 開發(fā)的注解和相關支持,我們可以利用它和 Tomcat 進行WS 開發(fā),由于現(xiàn)在更多的都是使用 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 服務器監(jiān)聽的 URL。 @OnOpen :這個注解修飾的方法會在 WS 連接開始時執(zhí)行。 @OnClose :這個注解修飾的方法則會在 WS 關閉時執(zhí)行。 @OnMessage :這個注解則是修飾消息接受的方法,并且由于消息有文本和二進制兩種方式,所以此方法參數(shù)上可以使用 String 或者二進制數(shù)組的方式,就像下面這樣:
@OnMessage
public void onMessage(Session session, String message) throws IOException {
}
@OnMessage
public void onMessage(Session session, byte[] message) throws IOException {
}
細心的小伙伴們可能發(fā)現(xiàn)了,示例中的 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 連接發(fā)生錯誤");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
System.out.println("WS 關閉連接");
}
// 支持分片消息
@Override
public boolean supportsPartialMessages() {
return false;
}
}
上面這個例子很好的展示了 WebSocketHandler 接口中的五個函數(shù),通過名字我們就應該知道它具有什么功能了:
afterConnectionEstablished:連接成功后調用。 handleMessage:處理發(fā)送來的消息。 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 開發(fā)來說我是比較推薦 Spring 方式的。
最后上個簡陋的 WS 效果示例圖,前端方面直接使用 HTML5 的 WebScoket 標準庫,具體可以查看我的倉庫代碼:

SocoketlO方式
Socket.IO 主要使用WebSocket協(xié)議。但是如果需要的話,Socket.io可以回退到幾種其它方法,例如Adobe Flash Sockets,JSONP拉取,或是傳統(tǒng)的AJAX拉取,并且在同時提供完全相同的接口。
不過我要先給大家提個醒,不再建議使用它了,不是因為它很久沒更新了,而是因為它支持的 Socket-Client 版本太老了,截止到 2022-04-29 日,SocketIO 已經(jīng)更新到 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服務器時執(zhí)行此事件
* @param client
*/
@OnConnect
public void onConnect(SocketIOClient client) {
System.out.println("SocketIoHandle 收到連接:" + client.getSessionId());
}
/**
* 客戶端斷開socket服務器時執(zhí)行此事件
* @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已關閉");
});
//發(fā)生了錯誤事件
socket.on('connect_error', () => {
console.log("websocket發(fā)生了錯誤");
})
}
}
第四種方式?
注意:以下內容如果沒有 Netty 基礎可能一臉蒙的進,一臉蒙的出,不過還是建議大家看看,Netty 其實很簡單。
第一步需要先引入一個 Netty 開發(fā)包,我這里為了方便一般都是 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)
//設置保持活動連接狀態(tài)
.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啟動成功");
//對關閉通道進行監(jiān)聽
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 處理業(yè)務邏輯-----------");
String responseStr = "{\"status\":200, \"content\":\"收到\"}";
ctx.channel().writeAndFlush(new TextWebSocketFrame(responseStr));
System.out.println("-----------WsTextInBoundHandle 數(shù)據(jù)回復完畢-----------");
}
@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 連接邏輯中發(fā)生了異常");
cause.printStackTrace();
ctx.close();
}
}

一圖勝千言,我想不用多說大家也都知道具體的類是處理什么消息了把,在上文的示例中我們是一定了一個文本 WS 消息的處理類,如果你想處理其他數(shù)據(jù)類型的消息,可以將泛型中的 TextWebSocketFrame 換成其他 WebSocketFrame 類就可以了。
至于為什么沒有連接成功后的處理,這個是和 Netty 的相關機制有關,可以在 channelActive 方法中處理,大家有興趣的可以了解一下 Netty。
最后上個簡陋的 WS 效果示例圖,前端方面直接使用 HTML5 的 WebScoket 標準庫,具體可以查看我的倉庫代碼:

總結
在上文中,我總共介紹了四種在 Java 中使用 WS 的方式,從我個人使用意向來說我感覺應該是這樣的:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,當然了,如果你的業(yè)務存在瀏覽器兼容性問題,其實只有一種選擇: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 也點點贊,大家的點贊與評論都是我更新的不懈動力,下期見。

評論
圖片
表情
