場景
Netty中實現多客戶端連接與通信-以實現聊天室群聊功能為例(附代碼下載):
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108623306
上面講了使用使用Socket搭建多客戶端的連接與通信。
那么如果在Netty中使用WebSocket進行長連接通信要怎么實現。
WebSocket
現在,很多網站為了實現推送技術,所用的技術都是 Ajax 輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP請求,然后由服務器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。
HTML5 定義的 WebSocket 協議,能更好的節省服務器資源和帶寬,並且能夠更實時地進行通訊。
WebSocket是一種在單個TCP連接上進行全雙工通信的協議。
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務器端就可以通過 TCP 連接直接交換數據。
當你獲取 Web Socket 連接后,你可以通過 send() 方法來向服務器發送數據,並通過 onmessage 事件來接收服務器返回的數據。
WebSocket 屬性
以下是 WebSocket 對象的屬性。假定我們使用了以上代碼創建了 Socket 對象:
屬性 | 描述 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示連接狀態,可以是以下值:
|
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,但是還沒有發出的 UTF-8 文本字節數。 |
WebSocket 事件
以下是 WebSocket 對象的相關事件。假定我們使用了以上代碼創建了 Socket 對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時觸發 |
message | Socket.onmessage | 客戶端接收服務端數據時觸發 |
error | Socket.onerror | 通信發生錯誤時觸發 |
close | Socket.onclose | 連接關閉時觸發 |
WebSocket 方法
以下是 WebSocket 對象的相關方法。假定我們使用了以上代碼創建了 Socket 對象:
方法 | 描述 |
---|---|
Socket.send() | 使用連接發送數據 |
Socket.close() | 關閉連接 |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
關注公眾號
霸道的程序猿
獲取編程相關電子書、教程推送與免費下載。
實現
在IDEA中搭建好Netty的項目並引入環境可以參照如下:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108592418
在此基礎上,在src下新建包com.badao.NettyWebSocket
然后新建服務端類WebSocketServer
package com.badao.NettyWebSocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; public class WebSocketServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new WebSocketInitializer()); //綁定端口 ChannelFuture channelFuture = serverBootstrap.bind(70).sync(); channelFuture.channel().closeFuture().sync(); }finally { //關閉事件組 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
服務端的搭建在上面已經講解,這里又添加了Netty自帶的日志處理器LoggingHandler
然后又添加了自定義的初始化器WebSocketInitializer
所以新建類WebSocketInitializer
package com.badao.NettyWebSocket; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; public class WebSocketInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new HttpObjectAggregator(8192)); pipeline.addLast(new WebSocketServerProtocolHandler("/badao")); pipeline.addLast(new WebSocketHandler()); } }
因為Netty也是基於Http的所以這里需要添加HttpServerCodec處理器等。
要想實現WebSocket功能,主要是添加了WebSocketServerProtocolHandler這個WebSocket服務端協議處理器。注意這里的參數需要添加一個WebSocket的路徑,這里是/badao
最后添加自定義的處理器WebSocketHandler 用來做具體的處理
所以新建類WebSocketHandler
package com.badao.NettyWebSocket; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.time.LocalDateTime; public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("收到消息:"+msg.text()); ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服務端在"+ LocalDateTime.now()+"發送消息(公眾號:霸道的程序猿)")); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerAdded:"+ctx.channel().id().asLongText()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerRemoved:"+ctx.channel().id().asLongText()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("異常發生"); ctx.close(); } }
使其繼承SimpleChannelInboundHandler注意此時的泛型類型為TextWebSocketFrame
然后重寫channelRead0方法用來對收到數據時進行處理
@Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("收到消息:"+msg.text()); ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服務端在"+ LocalDateTime.now()+"發送消息(公眾號:霸道的程序猿)")); }
在服務端將收到的消息進行輸出並給客戶端發送數據。
然后重寫handlerAdded方法,在客戶端與服務端建立連接時進行操作
@Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerAdded:"+ctx.channel().id().asLongText()); }
這里通過通道的id方法的asLongText方法獲取連接的唯一標志。
然后在服務端輸出。
同理重寫handlerRemoved方法,在斷掉連接時將連接的唯一標志進行輸出。
@Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerRemoved:"+ctx.channel().id().asLongText()); }
最后重寫出現異常時的處理方法,在出現異常時將連接關閉。
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("異常發生"); ctx.close(); }
至此WebSocket服務端搭建完成,然后客戶端通過JS就能實現。
在項目目錄下src下新建webapp目錄,在此目錄下新建badao.html
修改html的代碼為
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>公眾號:霸道的程序猿</title> </head> <body> <script type="text/javascript"> var socket; if(window.WebSocket) { socket = new WebSocket("ws://localhost:70/badao") socket.onmessage=function (ev) { var ta = document.getElementById("responseText"); ta.value = ta.value+"\n"+ev.data; } socket.onopen = function (ev) { var ta = document.getElementById("responseText"); ta.value = "連接開啟"; } socket.onclose = function (ev) { var ta = document.getElementById("responseText"); ta.value = ta.value+"\n連接關閉"; } } else{ alert("當前瀏覽器不支持WebSocket") } function send(message) { if(!window.WebSocket) { return; }else { if(socket.readyState = WebSocket.OPEN) { socket.send(message); } else { alert("連接尚未開啟"); } } } </script> <form> <textarea name="message" style="width: 400px;height: 200px"> </textarea> <input type="button" value="發送數據" onclick="send(this.form.message.value)"> <h3>服務端輸出:</h3> <textarea id="responseText" style="width: 400px;height: 200px"> </textarea> </form> </body> </html>
在js中通過windows.WebSocket判斷是否支持WebSocket
如果支持則
socket = new WebSocket("ws://localhost:70/badao")
建立連接,url的寫法前面的ws://是固定的類似http://
后面的是跟的ip:端口號/上面配置的WebSocket路徑
然后就是以這個WebSocket對象為中心進行連接和數據的顯示。
下面的回調方法onopen會在建立連接成功后回調,onclose會在斷掉連接后回調,
onmessage會在收到服務端發送的數據時回調並通過ev.data獲取數據。
客戶端向服務端發送數據時調用的是socket的send方法,通過
if(socket.readyState = WebSocket.OPEN)
判斷連接已經成功建立。
實現長連接通信
運行WebSocketServer的main方法,然后在badao.html上右擊選擇運行
建立連接成功后會在服務端輸出連接的id,在客戶端會顯示連接開啟。
此時如果刷新瀏覽器,服務端會輸出一次斷開連接和建立連接
再將服務端停掉,客戶端會輸出連接關閉
再重新啟動服務端,在上面的輸入框輸入內容並點擊發送數據
服務端會收到消息並輸出,並向客戶端發送一個消息。
繼續發送也是如此
示例代碼下載
https://download.csdn.net/download/BADAO_LIUMANG_QIZHI/12853829