Netty 核心模塊組件
一、Bootstrap、ServerBootstrap
1、Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程序,串聯各個組件Netty中
Bootstrap
類是客戶端
程序的啓動引導類ServerBootstrap
是服務端
啓動引導類
2、常見的方法
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),該方法用於服務器端,用來設置兩個 EventLoop
public B group(EventLoopGroup group) ,該方法用於客戶端,用來設置一個 EventLoop
public B channel(Class<? extends C> channelClass),該方法用來設置一個服務器端的通道實現
public < T > B option(ChannelOption option, T value),用來給 ServerChannel 添加配置
public < T > ServerBootstrap childOption(ChannelOption childOption, T value),用來給接收到的通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler),該方法用來設置業務處理類(自定義的handler)針對WorkerGroup(幹活的類)
public ServerBootstrap handler(ChannelHandler handler)針對BoosGroup(專門負責接收客戶端請求的類)
public ChannelFuture bind(int inetPort) ,該方法用於服務器端,用來設置佔用的端口號
public ChannelFuture connect(String inetHost, int inetPort) ,該方法用於客戶端,用來連接服務器端
二、Future、ChannelFuture
Netty 中所有的 IO 操作都是異步
的,不能立刻得知消息是否被正確處理。
但是可以過一會等它執行完成或者直接註冊一個監聽,具體的實現就是通過 Future 和 ChannelFutures,他們可以註冊一個監聽,當操作執行成功或失敗時監聽會自動觸發註冊的監聽事件。
常見的方法:
- Channel channel(),返回當前正在進行 IO 操作的通道
- ChannelFuture sync(),等待異步操作執行完畢
三、Channel
1、Netty 網絡通信的組件,能夠用於執行網絡 I/O 操作
2、通過 Channel 可獲得當前網絡連接的通道的狀態
3、通過 Channel 可獲得 網絡連接的配置參數
(例如接收緩衝區大小)
4、Channel 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味着任何 I/O 調用都將立即返回,並且不保證在調用結束時所請求的 I/O 操作已完成
5、調用立即返回一個 ChannelFuture 實例,通過註冊監聽器
到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調通知調用方
6、支持關聯 I/O 操作與對應的處理程序
7、不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,常用的 Channel 類型
- NioSocketChannel,異步的客戶端 TCP Socket 連接。
- NioServerSocketChannel,異步的服務器端 TCP Socket 連接。
- NioDatagramChannel,異步的 UDP 連接。
- NioSctpChannel,異步的客戶端 Sctp 連接。
- NioSctpServerChannel,異步的 Sctp 服務器端連接,這些通道涵蓋了 UDP 和 TCP 網絡 IO 以及文件 IO。
四、Selector
1、Netty 基於 Selector 對象實現 I/O 多路複用
,通過 Selector 一個線程可以監聽多個連接的 Channel 事件
。
2、當向一個 Selector 中註冊 Channel 後,Selector 內部的機制就可以自動不斷地查詢(Select) 這些註冊的Channel 是否有已就緒的 I/O 事件
(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel
五、ChannelHandler 及其實現類
1、ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
2、ChannelHandler 本身並沒有提供很多方法,因爲這個接口有許多的方法需要實現,方便使用期間,可以繼承它的子類
3、ChannelHandler 及其實現類一覽圖
4、我們經常需要自定義一個 Handler 類去繼承 ChannelInboundHandlerAdapter
5、然後通過重寫相應方法實現業務邏輯,我們接下來看看一般都需要重寫哪些方法
六、Pipeline 和 ChannelPipeline
1、ChannelPipeline 是一個重點
1、ChannelPipeline 是一個 Handler 的集合
2、它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當於一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是 保存 ChannelHandler 的 List,用於處理或攔截Channel 的入站事件和出站操作)
3、ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel中各個的 ChannelHandler 如何相互交互
4、在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關係如下
- channel能拿到他對應的channelPipeline
- channelPipeline也可以獲取到對應的channel
- channelPipeline中包含一個個的ChannelHandlerContext的雙向鏈表
- 每個ChannelHandlerContext(保存 Channel 相關的所有上下文信息)裏面包含對應具體的channelHandler
2、常用方法
1、ChannelPipeline addFirst(ChannelHandler… handlers) 把一個業務處理類(handler)添加到鏈中的第一個位置
2、ChannelPipeline addLast(ChannelHandler… handlers) 把一個業務處理類(handler)添加到鏈中的最後一個位置
七、ChannelHandlerContext
1、保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象
2、即 ChannelHandlerContext 中 包 含 一 個 具 體 的 事 件 處 理 器 ChannelHandler , 同 時ChannelHandlerContext 中也綁定了對應的 pipeline 和 Channel 的信息,方便對 ChannelHandler 進行調用.
3、常用方法
八、ChannelOption
1、Netty 在創建 Channel 實例後,一般都需要設置 ChannelOption 參數。
2、ChannelOption 參數
如下:
九、EventLoopGroup 和其實現類 NioEventLoopGroup
1、EventLoopGroup 是一組 EventLoop(就是對應線程)
的抽象,Netty 爲了更好的利用多核 CPU 資源,一般會有多個 EventLoop同時工作,每個 EventLoop 維護着一個 Selector 實例。
2、EventLoopGroup 提供 next 接口,可以從組裏面按照一定規則獲取其中一個 EventLoop 來處理任務。
3、在 Netty服務器端編程中 ,我們一般 都 需 要 提 供 兩 個 EventLoopGroup , 例如 :
- BossEventLoopGroup
- WorkerEventLoopGroup
4、通常一個服務端口即一個 ServerSocketChannel 對應一個 Selector 和一個 EventLoop 線程。
5、服務端中,BossEventLoop 負責接收客戶端的連接並將 SocketChannel 交給 WorkerEventLoopGroup 來進行 IO 處理,如下圖所示
6、常用方法:
public NioEventLoopGroup(),構造方法
public Future<?> shutdownGracefully(),斷開連接,關閉線程
十、 Unpooled 類
1、Netty 提供一個專門用來操作緩衝區
(即 Netty 的數據容器)的工具類
2、內部維護了對應的readerIndex
和writerIndex
3、相比NIO的ByteBuffer,Netty 提供的ByteBuf不用考慮flip反轉去操作讀寫
4、常用方法如下所示
5、舉例說明 Unpooled 獲取 Netty 的數據容器 ByteBuf 的基本使用
6、案例一
package com.sun.netty.components; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; /** * @Author: sunguoqiang * @Description: TODO * @DateTime: 2023/1/4 17:14 **/ public class NettyByteBuf01 { public static void main(String[] args) { /** * 創建一個 ByteBuf * 說明: * 1. 創建 對象,該對象包含一個數組 arr , 是一個 byte[10] * 2. 在 netty 的 buffer 中,不需要使用 flip 進行反轉 , 底層維護了 readerindex 和 writerIndex * 3. 通過 readerindex 和 writerIndex 和 capacity, 將 buffer 分成三個區域 * 1、0---readerindex 已經讀取的區域、 * 2、readerindex---writerIndex,可讀的區域、 * 3、writerIndex---capacity, 可寫的區域 */ ByteBuf buffer = Unpooled.buffer(10); for (int i = 0; i < 10; i++) { buffer.writeByte(i); } System.out.println("capacity=" + buffer.capacity());//10 // 輸出 for (int i = 0; i < buffer.capacity(); i++) { // 此方式readerindex不會改變 System.out.println(buffer.getByte(i)); } for (int i = 0; i < buffer.capacity(); i++) { // 此方式readerindex會改變 System.out.println(buffer.readByte()); } System.out.println("執行完畢"); } }
7、案例二
package com.sun.netty.components; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.nio.charset.Charset; /** * @Author: sunguoqiang * @Description: TODO * @DateTime: 2023/1/4 17:14 **/ public class NettyByteBuf02 { public static void main(String[] args) { // 創建 ByteBuf ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8")); // 使用相關的方法 if (byteBuf.hasArray()) { // true byte[] content = byteBuf.array(); // 將 content 轉成字符串 System.out.println(new String(content, Charset.forName("utf-8"))); // 輸出ByteBuf對象 System.out.println("byteBuf=" + byteBuf); // 輸出byteBuf偏移量 System.out.println(byteBuf.arrayOffset()); // 0 // byteBuf讀取索引位置 System.out.println(byteBuf.readerIndex()); // 0 // byteBuf寫入索引位置 System.out.println(byteBuf.writerIndex()); // 12 // byteBuf容量 System.out.println(byteBuf.capacity()); // 36 // byteBuf可讀取的第一個字節 System.out.println(byteBuf.readByte()); // 104 // byteBuf指定索引位置的字節 System.out.println(byteBuf.getByte(0)); // 104 // byteBuf可讀取字節數 int len = byteBuf.readableBytes(); // 12 System.out.println("len=" + len); //使用 for 取出各個字節 for (int i = 0; i < len; i++) { System.out.println((char) byteBuf.getByte(i)); } // 按照某個範圍讀取 System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8"))); System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8"))); } } }
十一、Netty 應用實例-羣聊系統
1、實例要求
1、編寫一個 Netty 羣聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
2、實現多人羣聊
3、服務器端:可以監測用戶上線,離線,並實現消息轉發功能
4、客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(有服務器轉發得到)
5、目的:進一步理解 Netty 非阻塞網絡編程機制
2、服務端
GroupChatServer 服務端
package com.sun.netty.groupChat; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; /** * @Author: sunguoqiang * @Description: 羣聊系統服務端 * @DateTime: 2023/1/9 14:25 **/ public class GroupChatServer { // 監聽端口 private final Integer port; public GroupChatServer(Integer port) { this.port = port; } public void run() throws InterruptedException { // 創建兩個線程組 NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); NioEventLoopGroup workerGroup = new NioEventLoopGroup(8); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // 向pipeline加入處理器 ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("decoder", new StringDecoder()); // 如果不加這個編碼解碼器的 無法直接傳輸字符串 pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast("myHandler", new GroupChatServerHandler()); } }); System.out.println("服務器啓動完成,綁定端口:" + port); ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); // 監聽關閉時間 channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { GroupChatServer groupChatServer = new GroupChatServer(8989); groupChatServer.run(); } }
GroupChatServerHandler 服務端處理器
package com.sun.netty.groupChat; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.GlobalEventExecutor; import java.text.SimpleDateFormat; import java.util.Date; /** * @Author: sunguoqiang * @Description: 自定義服務器處理器 * @DateTime: 2023/1/9 14:26 **/ public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> { //定義管理每個客戶端的channel組 //GlobalEventExecutor.INSTANCE 全局的事件執行器,單例的 private static final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); // 當連接建立會第一個執行該方法,【客戶端連接事件】 @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // 當客戶端連接,第一時間將每個客戶端的channel加入到channelGroup統一管理 Channel channel = ctx.channel(); // 給當前channelGroup管理的所有channel的客戶端都發送消息 channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"進入聊天室..."); channelGroup.add(channel); } // channel活躍時觸發,監測上線 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); System.out.println(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"上線..."); // channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"上線..."); } // 當某個channel處於活動狀態,就會觸發,用於發送某某上線【客戶端上線活動狀態事件】 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"離線..."); } // 當某個channel離開狀態,就會觸發,用於發送某某下線【客戶端離開非活動狀態事件】 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"離開聊天室..."); } // 客戶端發送消息會觸發【讀取客戶端數據事件】 @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception { // 獲取當前發送消息的用戶 Channel channel = channelHandlerContext.channel(); // 排除發送者自己,不給他轉發消息 for (Channel channel1 : channelGroup) { if(channel.equals(channel1)){ channel1.writeAndFlush(sdf.format(new Date()) + "[自己發送]:"+msg); }else{ channel1.writeAndFlush(sdf.format(new Date()) + "[客戶端-"+channel.remoteAddress()+"]:"+msg); } } } // 當發生異常會觸發【異常觸發事件】 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { Channel channel = ctx.channel(); channel.close(); } }
3、客戶端
GroupChatClient 客戶端
package com.sun.netty.groupChat; import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import java.util.Scanner; /** * @Author: sunguoqiang * @Description: 羣聊系統客戶端 * @DateTime: 2023/1/9 14:59 **/ public class GroupChatClient { private final String ip; private final Integer port; public GroupChatClient(String ip, int port) { this.ip = ip; this.port = port; } public void run() throws InterruptedException { NioEventLoopGroup clientGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(clientGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast("myHandler", new GroupChatClientHandler()); } }); ChannelFuture channelFuture = bootstrap.connect(ip, port); // 監聽連接是否成功 channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if (channelFuture.isSuccess()) { System.out.println("連接成功..."); } else { System.out.println("連接失敗..."); } } }); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String nextLine = scanner.nextLine(); channelFuture.channel().writeAndFlush(nextLine); } channelFuture.channel().close().sync(); } finally { } } public static void main(String[] args) throws InterruptedException { GroupChatClient groupChatClient = new GroupChatClient("127.0.0.1", 8989); groupChatClient.run(); } }
GroupChatClientHandler 客戶端處理器
package com.sun.netty.groupChat; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; /** * @Author: sunguoqiang * @Description: 聊天室客戶端處理器 * @DateTime: 2023/1/9 14:59 **/ public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> { // 讀取服務端發來的消息【讀寫消息事件】 @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception { Channel channel = channelHandlerContext.channel(); System.out.println(msg); } }
十二、Netty 心跳檢測機制案例
1、實例要求
- 編寫一個 Netty 心跳檢測機制案例, 當服務器超過 3 秒沒有讀時,就提示讀空閒
- 當服務器超過 5 秒沒有寫操作時,就提示寫空閒
- 實現當服務器超過 7 秒沒有讀或者寫操作時,就提示讀寫空閒
2、服務端
package com.sun.netty.heartbeat; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.timeout.IdleStateHandler; import java.util.concurrent.TimeUnit; /** * @Author: sunguoqiang * @Description: TODO * @DateTime: 2023/1/10 11:29 **/ public class MyHeartBeatServer { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); /** * 加入netty提供的IdleStateHandler * IdleStateHandler;空閒狀態處理器 * 構造參數↓ * long readerIdleTime : 表示多長時間沒讀操作了,服務端沒有接受到客戶端的讀操作;(觸發後,服務端就會發送心跳檢測包給客戶端,檢測是否還是連接狀態) * long writerIdleTime : 表示有多長時間沒有寫操作了,服務端沒有接收到客戶端的寫操作 * long allIdleTime : 表示多長時間既沒有讀操作也沒有寫操作。 * TimeUnit unit : 時間單位 */ pipeline.addLast(new IdleStateHandler(3, 5, 8, TimeUnit.SECONDS)); /** * 加入一個對空閒狀態檢測進一步處理的handler * 時間到了就會觸發對應的事件(IdleStateEvent),我們再寫一個自定義的handler來監聽這個時間就可以做出對應的操作 * 當事件觸發之後,就會傳遞給對應的channel管道的下一個handler去處理 * 通過調用(觸發)下一個handler的 userEventTiggerd()方法,該方法去處理 */ pipeline.addLast(new MyHeartBeatServerHandler()); } }); System.out.println("server start ok ..."); ChannelFuture channelFuture = serverBootstrap.bind(7878).sync(); channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
package com.sun.netty.heartbeat; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleStateEvent; /** * @Author: sunguoqiang * @Description: 心跳檢測服務端處理器,因爲不需要接受數據,所以就直接繼承 ChannelInboundHandlerAdapter * @DateTime: 2023/1/10 11:35 **/ public class MyHeartBeatServerHandler extends ChannelInboundHandlerAdapter { /** * 當觸發心跳事件後,會觸發該方法 * @param ctx 上下文 * @param evt 事件 * @throws Exception 異常 */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) evt; switch (event.state()) { case ALL_IDLE: System.out.println("【讀寫空閒】事件"); break; case READER_IDLE: System.out.println("【讀空閒】事件"); break; case WRITER_IDLE: System.out.println("【寫空閒】事件"); } System.out.println(ctx.channel().remoteAddress() + "發生事件:" + event.state()); // ctx.channel().close(); } } }
3、客戶端
package com.sun.netty.heartbeat; import com.sun.netty.groupChat.GroupChatClientHandler; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import java.util.Scanner; /** * @Author: sunguoqiang * @Description: 羣聊系統客戶端 * @DateTime: 2023/1/9 14:59 **/ public class MyHeartBeatClient { private final String ip; private final Integer port; public MyHeartBeatClient(String ip, int port) { this.ip = ip; this.port = port; } public void run() throws InterruptedException { NioEventLoopGroup clientGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(clientGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast("myHandler", new GroupChatClientHandler()); } }); ChannelFuture channelFuture = bootstrap.connect(ip, port); // 監聽連接是否成功 channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if (channelFuture.isSuccess()) { System.out.println("連接成功..."); } else { System.out.println("連接失敗..."); } } }); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String nextLine = scanner.nextLine(); channelFuture.channel().writeAndFlush(nextLine); } channelFuture.channel().close().sync(); } finally { } } public static void main(String[] args) throws InterruptedException { MyHeartBeatClient myHeartBeatClient = new MyHeartBeatClient("127.0.0.1", 7878); myHeartBeatClient.run(); } }
十三、Netty 通過 WebSocket 編程實現服務器和客戶端長連接
1、實例要求
1、Http 協議是無狀態的, 瀏覽器和服務器間的請求響應一次,下一次會重新創建連接.
2、要求:實現基於 webSocket 的長連接的全雙工的交互
3、改變 Http 協議多次請求的約束,實現長連接了, 服務器可以發送消息給瀏覽器
4、客戶端瀏覽器和服務器端會相互感知,比如服務器關閉了,瀏覽器會感知,同樣瀏覽器關閉了,服務器會感知
5、運行界面
2、代碼
WebSocketServerFrameHandler自定義處理業務邏輯的處理器package com.sun.netty.websocket; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.time.LocalDateTime; /** * @Title: WebSocketServerFrameHandler * @Author sunguoqiang * @Package com.sun.netty.websocket * @Date 2023/1/14 11:22 * @description: * 自定義處理業務邏輯的處理器 * * 對於websocket是通過【幀frame】的形式傳遞的(數據鏈路層的傳輸單元是幀) * * TextWebSocketFrame:表示一個文本幀,就是傳輸過程中的數據 */ public class WebSocketServerFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("服務端收到消息:" + msg.text()); //回覆瀏覽器 ctx.channel().writeAndFlush(new TextWebSocketFrame("服務器時間:" + LocalDateTime.now() + ":" + msg.text())); } //客戶端斷開連接時會觸發該事件 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { //id.asLongText表示獲取這channel唯一的值 System.out.println("handlerRemoved被調用:" + ctx.channel().id().asLongText()); } //客戶端連接時會觸發該事件 @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { //id.asLongText表示獲取這channel唯一的值 System.out.println("handlerAdded被調用:" + ctx.channel().id().asLongText()); //id.asShortText表示獲取這channel的值,這個不是唯一的。有可能重複 System.out.println("handlerAdded被調用:" + ctx.channel().id().asShortText()); } //當客戶端發生異常時會觸發該事件 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("異常發生:" + cause.getMessage()); ctx.channel().close(); } }Websocket雙工TCP長連接—服務端package com.sun.netty.websocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; 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.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.stream.ChunkedWriteHandler; /** * @Author: sunguoqiang * @Description: Websocket雙工TCP長連接 * @DateTime: 2023/1/12 9:59 **/ public class WebsocketServer { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup boosGroup = new NioEventLoopGroup(); NioEventLoopGroup workGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(boosGroup, workGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO))//netty自帶的日誌處理器 .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //基於http協議,使用http的編碼和解碼器 pipeline.addLast(new HttpServerCodec()); //是以 塊方式 寫,添加ChunkedWriteHandler處理器 pipeline.addLast(new ChunkedWriteHandler()); /** * 說明: * 1、因爲http的數據在傳輸過程中是【分段】的,HttpObjectAggregator可以將多個分段聚合起來 * 2、這就是當瀏覽器發送大量數據時,就會發送多次http請求 */ pipeline.addLast(new HttpObjectAggregator(8192)); /** * 說明: * 1、對於websocket是通過【幀frame】的形式傳遞的(數據鏈路層的傳輸單元是幀) * 2、可以看到WebSocketFrame 下面有6個子類 * 3、瀏覽器發送文件、請求時,ws://localhost:7000/websocket 表示請求的uri * 4、WebSocketServerProtocolHandler核心功能:將http協議升級爲ws協議(websocket長連接協議) * 5、爲什麼http能夠升級爲ws協議呢?是通過狀態碼101 */ pipeline.addLast(new WebSocketServerProtocolHandler("/websocket")); //自定義處理業務邏輯的處理器 pipeline.addLast(new WebSocketServerFrameHandler()); } }); System.out.println("服務端綁定端口7000.....啓動成功"); //啓動服務端 ChannelFuture cf = serverBootstrap.bind(7000).sync(); cf.channel().closeFuture().sync(); } finally { boosGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }前端的簡單頁面-websocket.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <script> var socket; //判斷當前瀏覽器是否支持websocket編程 if (window.WebSocket) { socket = new WebSocket("ws://localhost:7000/websocket"); //相當於服務器中的channelRead0事件【客戶端讀取服務端數據事件】,msg爲服務端發來的數據 socket.onmessage = function (resp) { var rt = document.getElementById('responseText'); rt.value = rt.value + "\n" + resp.data } //相當於客戶端與服務端連接開啓的事件 socket.onopen = function (resp) { var rt = document.getElementById('responseText'); rt.value = "連接已開啓"; } //相當於客戶端與服務端連接關閉的事件 socket.onclose = function (resp) { var rt = document.getElementById('responseText'); rt.value = rt.value + "\n" + "連接已關閉"; } } else { alert("當前瀏覽器不支持websocket編程"); } //發送消息給服務端的方法 function send(msg) { //判斷websocket是否創建好了 if (!window.socket) { return } //判斷當前狀態是否已經連接開啓 if (socket.readyState == WebSocket.OPEN) { //通過socket發送消息 socket.send(msg); } else { alert("連接未開啓"); } } </script> <body> <form onsubmit="return false"> <textarea name="message" style="height: 300px;width: 300px"></textarea> <input type="button" value="發送" onclick="send(this.form.message.value)"> <textarea id="responseText" style="height: 300px;width: 300px"></textarea> <input type="button" value="清空" onclick="document.getElementById('responseText').value=''"> </form> </body> </html>測試效果: