Java NIO 實現網絡通信

Java NIO 的相關資料很多,對 channel,buffer,selector 如何相關概念也有詳細的闡述。但是,不親自寫代碼調試一遍,對這些概念的理解仍然是一知半解。

即使代碼跑起來,也不見得有多懂這些概念,因爲只是膚淺的嘗試,但膚淺的嘗試勝過於紙上談兵,至少邁出了第一步,後續深入,可能要等到真的有實際應用時,纔會深入研究。先貼示例代碼。

一個典型的服務端:

Server端代碼

Java

public class MyServer { private Selector selector; private ServerSocketChannel serverChannel; public void start() throws Exception { int port = 9527; // 創建選擇器 selector = Selector.open(); // 打開監聽通道 serverChannel = ServerSocketChannel.open(); // 如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 serverChannel.configureBlocking(false);// 開啓非阻塞模式 // 綁定端口 backlog設爲1024 serverChannel.socket().bind(new InetSocketAddress(port), 1024); // 監聽客戶端連接請求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服務器已啓動,端口號:" + port); while (true) { // 無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); // 阻塞,只有當至少一個註冊的事件發生的時候纔會繼續. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); handleInput(key); } } } private void handleInput(SelectionKey key) throws Exception { if (key.isValid()) { // 處理新接入的請求消息 if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // 通過ServerSocketChannel的accept創建SocketChannel實例 // 完成該操作意味着完成TCP三次握手,TCP物理鏈路正式建立 SocketChannel sc = ssc.accept(); // 設置爲非阻塞的 sc.configureBlocking(false); // 註冊爲讀 sc.register(selector, SelectionKey.OP_READ); } // 讀消息 if (key.isReadable()) { SocketChannel sc = (SocketChannel) key.channel(); // 創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); // 讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); // 讀取到字節,對字節進行編解碼 if (readBytes > 0) { // 將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); // 根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; // 將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String input = new String(bytes, "UTF-8"); System.out.println("服務器收到消息:" + input); // 發送應答消息 doWrite(sc, LocalTime.now().toString()); } } else if (key.isWritable()) { ByteBuffer sendbuffer = ByteBuffer.allocate(1024); sendbuffer.clear(); SocketChannel sc = (SocketChannel) key.channel(); sc.write(sendbuffer); } } } // 異步發送應答消息 private void doWrite(SocketChannel channel, String response) throws IOException { // 將消息編碼爲字節數組 byte[] bytes = response.getBytes(); // 根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); // 將字節數組複製到緩衝區 writeBuffer.put(bytes); // flip操作 writeBuffer.flip(); // 發送緩衝區的字節數組 channel.write(writeBuffer); } }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990

public class MyServer {  private Selector selector; private ServerSocketChannel serverChannel;  public void start() throws Exception { int port = 9527; // 創建選擇器 selector = Selector.open(); // 打開監聽通道 serverChannel = ServerSocketChannel.open(); // 如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 serverChannel.configureBlocking(false);// 開啓非阻塞模式 // 綁定端口 backlog設爲1024 serverChannel.socket().bind(new InetSocketAddress(port), 1024); // 監聽客戶端連接請求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服務器已啓動,端口號:" + port); while (true) { // 無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); // 阻塞,只有當至少一個註冊的事件發生的時候纔會繼續. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); handleInput(key); } } }  private void handleInput(SelectionKey key) throws Exception { if (key.isValid()) { // 處理新接入的請求消息 if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // 通過ServerSocketChannel的accept創建SocketChannel實例 // 完成該操作意味着完成TCP三次握手,TCP物理鏈路正式建立 SocketChannel sc = ssc.accept(); // 設置爲非阻塞的 sc.configureBlocking(false); // 註冊爲讀 sc.register(selector, SelectionKey.OP_READ); } // 讀消息 if (key.isReadable()) { SocketChannel sc = (SocketChannel) key.channel(); // 創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); // 讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); // 讀取到字節,對字節進行編解碼 if (readBytes > 0) { // 將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); // 根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; // 將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String input = new String(bytes, "UTF-8"); System.out.println("服務器收到消息:" + input); // 發送應答消息 doWrite(sc, LocalTime.now().toString()); }  } else if (key.isWritable()) { ByteBuffer sendbuffer = ByteBuffer.allocate(1024); sendbuffer.clear(); SocketChannel sc = (SocketChannel) key.channel(); sc.write(sendbuffer); } } }  // 異步發送應答消息 private void doWrite(SocketChannel channel, String response) throws IOException { // 將消息編碼爲字節數組 byte[] bytes = response.getBytes(); // 根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); // 將字節數組複製到緩衝區 writeBuffer.put(bytes); // flip操作 writeBuffer.flip(); // 發送緩衝區的字節數組 channel.write(writeBuffer); }}

啓動Server端

Java

public class Main { public static void main(String[] args) throws Exception { MyServer myserver = new MyServer(); myserver.start(); } }

12345678

public class Main {  public static void main(String[] args) throws Exception { MyServer myserver = new MyServer(); myserver.start(); } }

關鍵代碼:

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

把 channel 註冊到 selector 上,如此,一個 selector 可以管理多個 channel。

selector.select(1000);

該方法功能就是阻塞直到該選擇器中的通道所關注的事件就緒,最多阻塞 1000 毫秒,使得程序可以繼續往下運行。

while(true){

while (it.hasNext()) {}

}

這樣的循環結構,使得服務器不斷的輪詢是否有請求事件發生,如果有發生,則會獲得這個請求中的 channel,並且往這個 channel 中讀寫數據。

handleInput 方法中,就是處理對應的請求。

客戶端:

Client端代碼

Java

import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class MyClient { private Selector selector; private SocketChannel socketChannel; public void start() throws Exception { int port = 9527; String host = "127.0.0.1"; // 創建選擇器 selector = Selector.open(); // 打開監聽通道 socketChannel = SocketChannel.open(); // 如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 socketChannel.configureBlocking(false);// 開啓非阻塞模式 socketChannel.connect(new InetSocketAddress(host, port)); // 等待100毫秒直到連接上服務器 while (!socketChannel.finishConnect()) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } socketChannel.register(selector, SelectionKey.OP_CONNECT); while (true) { try { // 無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); // 阻塞,只有當至少一個註冊的事件發生的時候纔會繼續. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } } private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { SocketChannel sc = (SocketChannel) key.channel(); // 讀消息 if (key.isReadable()) { // 創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); // 讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); // 讀取到字節,對字節進行編解碼 if (readBytes > 0) { // 將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); // 根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; // 將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String result = new String(bytes, "UTF-8"); System.out.println("客戶端收到消息:" + result); } } } } // 異步發送消息 private void doWrite(SocketChannel channel, String request) throws IOException { // 將消息編碼爲字節數組 byte[] bytes = request.getBytes(); // 根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); // 將字節數組複製到緩衝區 writeBuffer.put(bytes); // flip操作 writeBuffer.flip(); // 發送緩衝區的字節數組 channel.write(writeBuffer); } public void sendMsg(String msg) throws Exception { socketChannel.register(selector, SelectionKey.OP_READ); doWrite(socketChannel, msg); } }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110

import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.ClosedChannelException;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.Set; public class MyClient { private Selector selector; private SocketChannel socketChannel;  public void start() throws Exception { int port = 9527; String host = "127.0.0.1"; // 創建選擇器 selector = Selector.open(); // 打開監聽通道 socketChannel = SocketChannel.open(); // 如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 socketChannel.configureBlocking(false);// 開啓非阻塞模式  socketChannel.connect(new InetSocketAddress(host, port)); // 等待100毫秒直到連接上服務器 while (!socketChannel.finishConnect()) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } socketChannel.register(selector, SelectionKey.OP_CONNECT);  while (true) { try { // 無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); // 阻塞,只有當至少一個註冊的事件發生的時候纔會繼續. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } }  }  private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { SocketChannel sc = (SocketChannel) key.channel(); // 讀消息 if (key.isReadable()) { // 創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); // 讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); // 讀取到字節,對字節進行編解碼 if (readBytes > 0) { // 將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); // 根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; // 將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String result = new String(bytes, "UTF-8"); System.out.println("客戶端收到消息:" + result); } } } }  // 異步發送消息 private void doWrite(SocketChannel channel, String request) throws IOException { // 將消息編碼爲字節數組 byte[] bytes = request.getBytes(); // 根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); // 將字節數組複製到緩衝區 writeBuffer.put(bytes); // flip操作 writeBuffer.flip(); // 發送緩衝區的字節數組 channel.write(writeBuffer); }  public void sendMsg(String msg) throws Exception { socketChannel.register(selector, SelectionKey.OP_READ); doWrite(socketChannel, msg); }}

啓動Server端Client

Java

public class Main { public static void main(String[] args) throws Exception { final MyClient myclient = new MyClient(); FutureTask<Integer> Task2 = new FutureTask<>(() -> { myclient.start(); return 0; });// 用FutureTask包裹 Thread Thread2 = new Thread(Task2);// 用Thread包裹 Thread2.start(); Thread.sleep(1000); myclient.sendMsg("time"); } }

1234567891011121314151617

public class Main {  public static void main(String[] args) throws Exception { final MyClient myclient = new MyClient(); FutureTask<Integer> Task2 = new FutureTask<>(() -> { myclient.start(); return 0; });// 用FutureTask包裹 Thread Thread2 = new Thread(Task2);// 用Thread包裹 Thread2.start(); Thread.sleep(1000); myclient.sendMsg("time"); } }

客戶端代碼和服務器端代碼,原理是類似的。

上述例子中,先啓動服務器端代碼,然後啓動客戶端代碼,就能跑起來。例子中,客戶端發送任意字符到服務器端,服務器返回當前時間給客戶端。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章