【Netty】NIO 网络编程 聊天室案例
文章目錄
- 一、 NIO 聊天室需求
- 二、 NIO 聊天室 服務(wù)器端 代碼分析
- 三、 NIO 聊天室 客戶端 代碼分析
- 四、 NIO 聊天室 服務(wù)器端 完整代碼
- 五、 NIO 聊天室 客戶端 完整代碼
- 六、 NIO 聊天室 運(yùn)行
一、 NIO 聊天室需求
1 . NIO 聊天室需求 :
① 服務(wù)器 客戶端 通信 : 服務(wù)器 與 客戶端 實(shí)現(xiàn) 雙向通信 ; 服務(wù)器可以寫(xiě)出數(shù)據(jù)到客戶端 , 也能讀取客戶端的數(shù)據(jù) ; 客戶端可以寫(xiě)出數(shù)據(jù)到服務(wù)器端 , 也可以讀取服務(wù)器端的數(shù)據(jù) ;
② 多人聊天 : 一個(gè)服務(wù)器 與 多個(gè)客戶端 進(jìn)行數(shù)據(jù)交互 , 同時(shí)還要實(shí)現(xiàn)將某一個(gè)客戶端的數(shù)據(jù)轉(zhuǎn)發(fā)給其它客戶端 ;
③ 用戶狀態(tài)監(jiān)測(cè) : 服務(wù)器可以檢測(cè)用戶的 上線 , 離線 狀態(tài) ;
2 . 數(shù)據(jù)傳輸細(xì)節(jié) :
① 上線監(jiān)聽(tīng) : 當(dāng)有客戶端連接時(shí) , 服務(wù)器檢測(cè)到用戶上線 , 服務(wù)器將該用戶上線狀態(tài)通知給其它客戶端 ;
② 下線監(jiān)聽(tīng) : 如果有客戶端離線 , 服務(wù)器檢測(cè)到連接斷開(kāi) , 服務(wù)器將該用戶離線的狀態(tài)通知給聊天室的其它客戶端 ;
③ 聊天信息轉(zhuǎn)發(fā) : 客戶端發(fā)送消息時(shí) , 服務(wù)器端接收到該數(shù)據(jù) , 并轉(zhuǎn)發(fā)給聊天室的其它用戶客戶端 ;
二、 NIO 聊天室 服務(wù)器端 代碼分析
服務(wù)器端的連接管理流程 : 創(chuàng)建 服務(wù)器套接字通道 ( ServerSocketChannel ) , 將該通道注冊(cè)給 選擇器 ( Selector ) , 選擇器開(kāi)啟監(jiān)聽(tīng) , 監(jiān)聽(tīng)到客戶端連接 , 就創(chuàng)建一個(gè) 套接字通道 ( SocketChannel ) , 注冊(cè)給選擇器 ;
服務(wù)器端的消息轉(zhuǎn)發(fā)流程 : 服務(wù)器端收到客戶端發(fā)送的消息 , 將該消息轉(zhuǎn)發(fā)給除該客戶端外的其它客戶端 , 從選擇器中可以獲取到所有的 通道 , 注意 屏蔽 服務(wù)器套接字通道 和 發(fā)送本消息的客戶端對(duì)應(yīng)的通道 ;
服務(wù)器連接監(jiān)聽(tīng) : 當(dāng)客戶端與服務(wù)器連接成功 , 即觸發(fā)注冊(cè)給 選擇器 ( Selector ) 的 服務(wù)器套接字通道 ( ServerSocketChannel ) 的 SelectionKey.OP_ACCEPT 事件 , 表示有客戶端連接服務(wù)器成功 , 用戶上線 ;
服務(wù)器斷開(kāi)連接監(jiān)聽(tīng) : 當(dāng)服務(wù)器端與客戶端讀寫(xiě)數(shù)據(jù)出現(xiàn)異常時(shí) , 說(shuō)明該客戶端離線 , 在異常處理代碼中可以判定某個(gè)客戶端離線 ;
1 . 服務(wù)器套接字通道 : 調(diào)用 open 靜態(tài)方法創(chuàng)建服務(wù)器套接字通道 , 并綁定 8888 端口 , 設(shè)置非阻塞網(wǎng)絡(luò)通信模式 ;
// 創(chuàng)建并配置 服務(wù)器套接字通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(PORT)); serverSocketChannel.configureBlocking(false);2 . 服務(wù)器端選擇器 : 調(diào)用 open 靜態(tài)方法獲取 選擇器 , 注冊(cè)之前創(chuàng)建的 服務(wù)器套接字通道 ;
// 獲取選擇器, 并注冊(cè) 服務(wù)器套接字通道 ServerSocketChannel selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);3 . 監(jiān)聽(tīng)事件 : 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù) ; 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 ;
// 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù) // 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 // 下面開(kāi)始遍歷上述 selectedKeys 集合 try {int eventTriggerCount = selector.select(); } catch (IOException e) {e.printStackTrace(); }4 . 處理客戶端連接事件 : 接受客戶端連接 , 獲取 網(wǎng)絡(luò)套接字通道 ( SocketChannel ) , 并注冊(cè)給 選擇器 ( Selector ) , 監(jiān)聽(tīng) SelectionKey.OP_READ 數(shù)據(jù)讀取事件 ;
// 客戶端連接服務(wù)器, 服務(wù)器端需要執(zhí)行 accept 操作 if (key.isAcceptable()) {//創(chuàng)建通道 : 為該客戶端創(chuàng)建一個(gè)對(duì)應(yīng)的 SocketChannel 通道//不等待 : 當(dāng)前已經(jīng)知道有客戶端連接服務(wù)器, 因此不需要阻塞等待//非阻塞方法 : ServerSocketChannel 的 accept() 是非阻塞的方法SocketChannel socketChannel = null;try {socketChannel = serverSocketChannel.accept();//如果 ServerSocketChannel 是非阻塞的, 這里的 SocketChannel 也要設(shè)置成非阻塞的//否則會(huì)報(bào) java.nio.channels.IllegalBlockingModeException 異常socketChannel.configureBlocking(false);//注冊(cè)通道 : 將 SocketChannel 通道注冊(cè)給 選擇器 ( Selector )//關(guān)注事件 : 關(guān)注事件時(shí)讀取事件, 服務(wù)器端從該通道讀取數(shù)據(jù)//關(guān)聯(lián)緩沖區(qū) :socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));System.out.println(String.format("用戶 %s 進(jìn)入聊天室", socketChannel.getRemoteAddress()));} catch (IOException e) {e.printStackTrace();} }5 . 處理客戶端消息轉(zhuǎn)發(fā)事件 :
① 讀取客戶端上傳的數(shù)據(jù) : 通過(guò) SelectionKey 獲取 通道 和 緩沖區(qū) , 使用 套接字通道 ( SocketChannel ) 讀取 緩沖區(qū) ( ByteBuffer ) 中的數(shù)據(jù) , 然后記錄顯示該數(shù)據(jù) ;
// 獲取 通道 ( Channel ) : 通過(guò) SelectionKey 獲取 SocketChannel socketChannel = (SocketChannel) key.channel(); // 獲取 緩沖區(qū) ( Buffer ) : 獲取到 通道 ( Channel ) 關(guān)聯(lián)的 緩沖區(qū) ( Buffer ) ByteBuffer byteBuffer = (ByteBuffer) key.attachment(); String remoteAddress = null; String message = null; try {// 讀取客戶端傳輸?shù)臄?shù)據(jù)int readCount = socketChannel.read(byteBuffer);byte[] messageBytes = new byte[readCount];byteBuffer.flip();byteBuffer.get(messageBytes);// 處理讀取的消息message = new String(messageBytes);//重置以便下次使用byteBuffer.flip();remoteAddress = socketChannel.getRemoteAddress().toString();System.out.println(String.format("%s : %s", remoteAddress, message)); } catch (IOException e) {//e.printStackTrace();// 如果此處出現(xiàn)異常, 說(shuō)明該客戶端離線了, 服務(wù)器提示, 取消選擇器上的注冊(cè)信息, 關(guān)閉通道try {System.out.println( String.format("%s 用戶離線 !", socketChannel.getRemoteAddress()) );key.cancel();socketChannel.close();//繼續(xù)下一次循環(huán)continue;} catch (IOException ex) {ex.printStackTrace();} }② 轉(zhuǎn)發(fā)給其它客戶端 : 從 選擇器 ( Selector ) 的 keys 集合 中獲取所有注冊(cè)的通道 , 然后除 ServerSocketChannel 和 發(fā)送本信息的 客戶端對(duì)應(yīng)的 SocketChannel 通道 之外 , 其它所有的通道都轉(zhuǎn)發(fā)一份聊天信息 ;
// 向其它客戶端轉(zhuǎn)發(fā)消息, 發(fā)送消息的客戶端自己就不用再發(fā)送該消息了 // 遍歷所有注冊(cè)到 選擇器 Selector 的 SocketChannel Set<SelectionKey> selectionKeys = selector.keys(); for (SelectionKey selectionKey : selectionKeys) {// 獲取客戶端對(duì)應(yīng)的 套接字通道// 這里不能強(qiáng)轉(zhuǎn)成 SocketChannel, 因?yàn)檫@里可能存在 ServerSocketChannelChannel channel = selectionKey.channel();// 將自己排除在外, 注意這里是地址對(duì)比, 就是這兩個(gè)類不能是同一個(gè)地址的類// 這個(gè)類的類型必須是 SocketChannel, 排除之前注冊(cè)的 ServerSocketChannel 干擾if (socketChannel != channel && channel instanceof SocketChannel) {// 將通道轉(zhuǎn)為 SocketChannel, 之后將字符串發(fā)送到客戶端SocketChannel clientSocketChannel = (SocketChannel) channel;// 寫(xiě)出字符串到其它客戶端try {clientSocketChannel.write(ByteBuffer.wrap( ( remoteAddress + " : " + message ).getBytes()));} catch (IOException e) {//e.printStackTrace();// 如果此處出現(xiàn)異常, 說(shuō)明該客戶端離線了, 服務(wù)器提示, 取消選擇器上的注冊(cè)信息, 關(guān)閉通道try {System.out.println( String.format("%s 用戶離線 !", clientSocketChannel.getRemoteAddress()) );selectionKey.cancel();clientSocketChannel.close();} catch (IOException ex) {ex.printStackTrace();}}} }三、 NIO 聊天室 客戶端 代碼分析
客戶端的連接與數(shù)據(jù)接收 : 客戶端的工作是連接服務(wù)器 , 得到與服務(wù)器通信的 套接字通道 ( SocketChannel ) , 注冊(cè)該通道到 選擇器 ( Selector ) , 監(jiān)聽(tīng) SelectionKey.OP_READ 讀取數(shù)據(jù)事件 , 接收到數(shù)據(jù)后顯示即可 ;
1 . 連接服務(wù)器 : 連接服務(wù)器 , 并設(shè)置網(wǎng)絡(luò)通信非阻塞模式 ;
// 創(chuàng)建并配置 服務(wù)器套接字通道 ServerSocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_ADDRESS, PORT)); socketChannel.configureBlocking(false);2 . 獲取選擇器并注冊(cè)通道 : 獲取 選擇器 ( Selector ) , 并將 套接字通道 ( SocketChannel ) 注冊(cè)給該選擇器 ;
// 獲取選擇器, 并注冊(cè) 服務(wù)器套接字通道 ServerSocketChannel selector = Selector.open(); //注冊(cè)通道 : 將 SocketChannel 通道注冊(cè)給 選擇器 ( Selector ) //關(guān)注事件 : 關(guān)注事件時(shí)讀取事件, 服務(wù)器端從該通道讀取數(shù)據(jù) //關(guān)聯(lián)緩沖區(qū) : socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));3 . 監(jiān)聽(tīng)服務(wù)器端下發(fā)的消息 : 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù) ; 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 ;
// 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù) // 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 // 下面開(kāi)始遍歷上述 selectedKeys 集合 try {int eventTriggerCount = selector.select(); } catch (IOException e) {e.printStackTrace(); }4 . 處理服務(wù)器端發(fā)送的數(shù)據(jù) : 如果監(jiān)聽(tīng)到服務(wù)器下發(fā)數(shù)據(jù) , 開(kāi)始遍歷當(dāng)前觸發(fā)事件的通道 , 調(diào)用該通道讀取數(shù)據(jù)到緩沖區(qū) , 之后顯示該數(shù)據(jù) ;
// 處理事件集合 : // 獲取當(dāng)前發(fā)生的事件的 SelectionKey 集合, 通過(guò) SelectionKey 可以獲取對(duì)應(yīng)的 通道 Set<SelectionKey> keys = selector.selectedKeys(); // 使用迭代器迭代, 涉及到刪除操作 Iterator<SelectionKey> keyIterator = keys.iterator(); while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 根據(jù) SelectionKey 的事件類型, 處理對(duì)應(yīng)通道的業(yè)務(wù)邏輯// 客戶端寫(xiě)出數(shù)據(jù)到服務(wù)器端, 服務(wù)器端需要讀取數(shù)據(jù)if (key.isReadable()) {// 獲取 通道 ( Channel ) : 通過(guò) SelectionKey 獲取SocketChannel socketChannel = (SocketChannel) key.channel();// 獲取 緩沖區(qū) ( Buffer ) : 獲取到 通道 ( Channel ) 關(guān)聯(lián)的 緩沖區(qū) ( Buffer )ByteBuffer byteBuffer = (ByteBuffer) key.attachment();String message = null;try {// 讀取客戶端傳輸?shù)臄?shù)據(jù)int readCount = socketChannel.read(byteBuffer);byte[] messageBytes = new byte[readCount];byteBuffer.flip();byteBuffer.get(messageBytes);// 處理讀取的消息message = new String(messageBytes);byteBuffer.flip();System.out.println(String.format(message));} catch (IOException e) {//e.printStackTrace();// 客戶端連接斷開(kāi)key.cancel();try {socketChannel.close();} catch (IOException ex) {ex.printStackTrace();}}// try}// if (key.isReadable())四、 NIO 聊天室 服務(wù)器端 完整代碼
package kim.hsl.nio.chat;import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;/*** 聊天室服務(wù)器端** @author hsl* @date 2020-05-29 17:24*/ public class Server {/*** 服務(wù)器監(jiān)聽(tīng)的端口號(hào)*/public static final int PORT = 8888;/*** 監(jiān)聽(tīng) ServerSocketChannel 通道和各個(gè)客戶端對(duì)應(yīng)的 SocketChannel 通道*/private Selector selector;/*** 服務(wù)器端的套接字通道, 相當(dāng)于 BIO 中的 ServerSocket*/private ServerSocketChannel serverSocketChannel;/*** 初始化服務(wù)器相關(guān)操作*/public Server() {initServerSocketChannelAndSelector();}/*** 初始化 服務(wù)器套接字通道 和*/private void initServerSocketChannelAndSelector() {try {// 創(chuàng)建并配置 服務(wù)器套接字通道 ServerSocketChannelserverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(PORT));serverSocketChannel.configureBlocking(false);// 獲取選擇器, 并注冊(cè) 服務(wù)器套接字通道 ServerSocketChannelselector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {e.printStackTrace();}}/*** Selector 開(kāi)始執(zhí)行 監(jiān)聽(tīng)工作*/private void selectorStartSelectOperation() {System.out.println("服務(wù)器端啟動(dòng)監(jiān)聽(tīng) :");while (true) {// 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù)// 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中// 下面開(kāi)始遍歷上述 selectedKeys 集合try {int eventTriggerCount = selector.select();} catch (IOException e) {e.printStackTrace();}// 當(dāng)前狀態(tài)說(shuō)明 :// 如果能執(zhí)行到該位置, 說(shuō)明 selector.select() 方法返回值大于 0// 當(dāng)前有 1 個(gè)或多個(gè)事件觸發(fā), 下面就是處理事件的邏輯// 處理事件集合 :// 獲取當(dāng)前發(fā)生的事件的 SelectionKey 集合, 通過(guò) SelectionKey 可以獲取對(duì)應(yīng)的 通道Set<SelectionKey> keys = selector.selectedKeys();// 使用迭代器迭代, 涉及到刪除操作Iterator<SelectionKey> keyIterator = keys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 根據(jù) SelectionKey 的事件類型, 處理對(duì)應(yīng)通道的業(yè)務(wù)邏輯// 客戶端連接服務(wù)器, 服務(wù)器端需要執(zhí)行 accept 操作if (key.isAcceptable()) {//創(chuàng)建通道 : 為該客戶端創(chuàng)建一個(gè)對(duì)應(yīng)的 SocketChannel 通道//不等待 : 當(dāng)前已經(jīng)知道有客戶端連接服務(wù)器, 因此不需要阻塞等待//非阻塞方法 : ServerSocketChannel 的 accept() 是非阻塞的方法SocketChannel socketChannel = null;try {socketChannel = serverSocketChannel.accept();//如果 ServerSocketChannel 是非阻塞的, 這里的 SocketChannel 也要設(shè)置成非阻塞的//否則會(huì)報(bào) java.nio.channels.IllegalBlockingModeException 異常socketChannel.configureBlocking(false);//注冊(cè)通道 : 將 SocketChannel 通道注冊(cè)給 選擇器 ( Selector )//關(guān)注事件 : 關(guān)注事件時(shí)讀取事件, 服務(wù)器端從該通道讀取數(shù)據(jù)//關(guān)聯(lián)緩沖區(qū) :socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));System.out.println(String.format("用戶 %s 進(jìn)入聊天室", socketChannel.getRemoteAddress()));} catch (IOException e) {e.printStackTrace();}}// 客戶端寫(xiě)出數(shù)據(jù)到服務(wù)器端, 服務(wù)器端需要讀取數(shù)據(jù)if (key.isReadable()) {// 獲取 通道 ( Channel ) : 通過(guò) SelectionKey 獲取SocketChannel socketChannel = (SocketChannel) key.channel();// 獲取 緩沖區(qū) ( Buffer ) : 獲取到 通道 ( Channel ) 關(guān)聯(lián)的 緩沖區(qū) ( Buffer )ByteBuffer byteBuffer = (ByteBuffer) key.attachment();String remoteAddress = null;String message = null;try {// 讀取客戶端傳輸?shù)臄?shù)據(jù)int readCount = socketChannel.read(byteBuffer);byte[] messageBytes = new byte[readCount];byteBuffer.flip();byteBuffer.get(messageBytes);// 處理讀取的消息message = new String(messageBytes);//重置以便下次使用byteBuffer.flip();remoteAddress = socketChannel.getRemoteAddress().toString();System.out.println(String.format("%s : %s", remoteAddress, message));} catch (IOException e) {//e.printStackTrace();// 如果此處出現(xiàn)異常, 說(shuō)明該客戶端離線了, 服務(wù)器提示, 取消選擇器上的注冊(cè)信息, 關(guān)閉通道try {System.out.println( String.format("%s 用戶離線 !", socketChannel.getRemoteAddress()) );key.cancel();socketChannel.close();//繼續(xù)下一次循環(huán)continue;} catch (IOException ex) {ex.printStackTrace();}}// 向其它客戶端轉(zhuǎn)發(fā)消息, 發(fā)送消息的客戶端自己就不用再發(fā)送該消息了// 遍歷所有注冊(cè)到 選擇器 Selector 的 SocketChannelSet<SelectionKey> selectionKeys = selector.keys();for (SelectionKey selectionKey : selectionKeys) {// 獲取客戶端對(duì)應(yīng)的 套接字通道// 這里不能強(qiáng)轉(zhuǎn)成 SocketChannel, 因?yàn)檫@里可能存在 ServerSocketChannelChannel channel = selectionKey.channel();// 將自己排除在外, 注意這里是地址對(duì)比, 就是這兩個(gè)類不能是同一個(gè)地址的類// 這個(gè)類的類型必須是 SocketChannel, 排除之前注冊(cè)的 ServerSocketChannel 干擾if (socketChannel != channel && channel instanceof SocketChannel) {// 將通道轉(zhuǎn)為 SocketChannel, 之后將字符串發(fā)送到客戶端SocketChannel clientSocketChannel = (SocketChannel) channel;// 寫(xiě)出字符串到其它客戶端try {clientSocketChannel.write(ByteBuffer.wrap( ( remoteAddress + " : " + message ).getBytes()));} catch (IOException e) {//e.printStackTrace();// 如果此處出現(xiàn)異常, 說(shuō)明該客戶端離線了, 服務(wù)器提示, 取消選擇器上的注冊(cè)信息, 關(guān)閉通道try {System.out.println( String.format("%s 用戶離線 !", clientSocketChannel.getRemoteAddress()) );selectionKey.cancel();clientSocketChannel.close();} catch (IOException ex) {ex.printStackTrace();}}}}}// 處理完畢后, 當(dāng)前的 SelectionKey 已經(jīng)處理完畢// 從 Set 集合中移除該 SelectionKey// 防止重復(fù)處理keyIterator.remove();}}}public static void main(String[] args) {Server server = new Server();server.selectorStartSelectOperation();} }
五、 NIO 聊天室 客戶端 完整代碼
package kim.hsl.nio.chat;import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Scanner; import java.util.Set;public class Client {/*** 服務(wù)器地址*/public final static String SERVER_ADDRESS = "127.0.0.1";/*** 服務(wù)器監(jiān)聽(tīng)的端口號(hào)*/public static final int PORT = 8888;/*** 監(jiān)聽(tīng) SocketChannel 通道的 選擇器*/private Selector selector;/*** 服務(wù)器端的套接字通道, 相當(dāng)于 BIO 中的 ServerSocket*/private SocketChannel socketChannel;public Client() {initClientSocketChannelAndSelector();}/*** 初始化 服務(wù)器套接字通道 和*/private void initClientSocketChannelAndSelector() {try {// 創(chuàng)建并配置 服務(wù)器套接字通道 ServerSocketChannelsocketChannel = SocketChannel.open(new InetSocketAddress(SERVER_ADDRESS, PORT));socketChannel.configureBlocking(false);// 獲取選擇器, 并注冊(cè) 服務(wù)器套接字通道 ServerSocketChannelselector = Selector.open();//注冊(cè)通道 : 將 SocketChannel 通道注冊(cè)給 選擇器 ( Selector )//關(guān)注事件 : 關(guān)注事件時(shí)讀取事件, 服務(wù)器端從該通道讀取數(shù)據(jù)//關(guān)聯(lián)緩沖區(qū) :socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));} catch (IOException e) {e.printStackTrace();}}/*** 向服務(wù)器端發(fā)送消息* @param message*/public void sendMessageToServer(String message){try {socketChannel.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}public void readMessageFromServer(){// 阻塞監(jiān)聽(tīng), 如果有事件觸發(fā), 返回觸發(fā)的事件個(gè)數(shù)// 被觸發(fā)的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中// 下面開(kāi)始遍歷上述 selectedKeys 集合try {int eventTriggerCount = selector.select();} catch (IOException e) {e.printStackTrace();}// 當(dāng)前狀態(tài)說(shuō)明 :// 如果能執(zhí)行到該位置, 說(shuō)明 selector.select() 方法返回值大于 0// 當(dāng)前有 1 個(gè)或多個(gè)事件觸發(fā), 下面就是處理事件的邏輯// 處理事件集合 :// 獲取當(dāng)前發(fā)生的事件的 SelectionKey 集合, 通過(guò) SelectionKey 可以獲取對(duì)應(yīng)的 通道Set<SelectionKey> keys = selector.selectedKeys();// 使用迭代器迭代, 涉及到刪除操作Iterator<SelectionKey> keyIterator = keys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 根據(jù) SelectionKey 的事件類型, 處理對(duì)應(yīng)通道的業(yè)務(wù)邏輯// 客戶端寫(xiě)出數(shù)據(jù)到服務(wù)器端, 服務(wù)器端需要讀取數(shù)據(jù)if (key.isReadable()) {// 獲取 通道 ( Channel ) : 通過(guò) SelectionKey 獲取SocketChannel socketChannel = (SocketChannel) key.channel();// 獲取 緩沖區(qū) ( Buffer ) : 獲取到 通道 ( Channel ) 關(guān)聯(lián)的 緩沖區(qū) ( Buffer )ByteBuffer byteBuffer = (ByteBuffer) key.attachment();String message = null;try {// 讀取客戶端傳輸?shù)臄?shù)據(jù)int readCount = socketChannel.read(byteBuffer);byte[] messageBytes = new byte[readCount];byteBuffer.flip();byteBuffer.get(messageBytes);// 處理讀取的消息message = new String(messageBytes);byteBuffer.flip();System.out.println(String.format(message));} catch (IOException e) {//e.printStackTrace();// 客戶端連接斷開(kāi)key.cancel();try {socketChannel.close();} catch (IOException ex) {ex.printStackTrace();}}// try}// if (key.isReadable())// 處理完畢后, 當(dāng)前的 SelectionKey 已經(jīng)處理完畢// 從 Set 集合中移除該 SelectionKey// 防止重復(fù)處理keyIterator.remove();}}public static void main(String[] args) {Client client = new Client();// 接收服務(wù)器端數(shù)據(jù)線程new Thread(new Runnable() {@Overridepublic void run() {while (true){//不停地從服務(wù)器端讀取數(shù)據(jù)client.readMessageFromServer();}}}).start();Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()){String message = scanner.nextLine();client.sendMessageToServer(message);}} }
六、 NIO 聊天室 運(yùn)行
按照如下步驟演示聊天室功能 ( 一定要按照順序執(zhí)行 ) ;
1 . 服務(wù)器啟動(dòng) : 首先
2 . 設(shè)置客戶端多個(gè) : 點(diǎn)擊下圖綠框中的下拉菜單 , 選擇 Edit Configuration , 彈出如下對(duì)話框 , 配置 Client 應(yīng)用 , 勾選 Allow parallel run 選項(xiàng) , 之后 Client 程序就可以運(yùn)行多個(gè) , 否則只能運(yùn)行一個(gè) ;
3 . 客戶端 111 連接 : 運(yùn)行客戶端程序即可 ;
4 . 客戶端 222 連接 : 運(yùn)行客戶端程序即可 ;
5 . 客戶端 333 連接 : 運(yùn)行客戶端程序即可 ;
6 . 客戶端 222 發(fā)送消息 :
服務(wù)器顯示 : 客戶端 222 將消息發(fā)送給服務(wù)器 , 服務(wù)器顯示該消息 ;
客戶端 111 顯示 : 這是由服務(wù)器轉(zhuǎn)發(fā)的客戶端 222 發(fā)送的消息 , 客戶端 333 也收到該消息 ;
7 . 客戶端 222 退出 : 終止客戶端 222 程序 , 服務(wù)器端檢測(cè)到異常 , 就可以將客戶端 222 退出 ;
總結(jié)
以上是生活随笔為你收集整理的【Netty】NIO 网络编程 聊天室案例的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【Android】Handler 机制
- 下一篇: 【Netty】mmap 和 sendFi