咱們先來看一段傳統IO的代碼java
public class OioServer { public static void main(String[] args) throws IOException { //這裏能夠直接寫成ServerSocket server = new ServerSocket(10101); ServerSocket server = new ServerSocket(); server.bind(new InetSocketAddress(10101)); System.out.println("服務器啓動"); while (true) { //此處會阻塞 Socket socket = server.accept(); System.out.println("來了一個新客戶端"); handler(socket); } } public static void handler(Socket socket) { try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while (true) { int read = inputStream.read(bytes); if (read != -1) { System.out.println(new String(bytes,0,read)); }else { break; } } } catch (IOException e) { e.printStackTrace(); }finally { try { System.out.println("socket關閉"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用telnet鏈接web
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.shell
咱們會看到OioServer的運行狀況緩存
服務器啓動
來了一個新客戶端服務器
可是當咱們又使用一個telnet鏈接進來的時候,OioServer的運行狀況沒變,說明一個服務端只能接收一個客戶端點鏈接,緣由在於Socket socket = server.accept();發生了堵塞,如今咱們將其改寫成多線程websocket
public class OioServerThread { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(10101); ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); System.out.println("服務器啓動"); while (true) { Socket socket = server.accept(); System.out.println("來了一個新客戶端"); service.execute(() -> handler(socket)); } } public static void handler(Socket socket) { try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while (true) { int read = inputStream.read(bytes); if (read != -1) { System.out.println(new String(bytes,0,read)); }else { break; } } } catch (IOException e) { e.printStackTrace(); }finally { try { System.out.println("socket關閉"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
運行可知,當咱們啓動了多個telnet進行鏈接的時候,它是能夠一塊兒鏈接進來的多線程
服務器啓動
來了一個新客戶端
來了一個新客戶端併發
可是這裏有一個問題,咱們線程池的可用線程是有限的,不可能無限提供線程來接收大量客戶端的鏈接,早晚它會無響應被堵塞的。socket
咱們如今來看一下NIO,NIO實際上是使用傳統IO的特性建立一個channel(通道),經過該通道來註冊事件SelectionKey測試
SelectionKey有四種事件
這裏 注意,下面兩種,SelectionKey.OP_READ ,SelectionKey.OP_WRITE ,
1.當向通道中註冊SelectionKey.OP_READ事件後,若是客戶端有向緩存中write數據,下次輪詢時,則會 isReadable()=true;
2.當向通道中註冊SelectionKey.OP_WRITE事件後,這時你會發現當前輪詢線程中isWritable()一直爲ture,若是不設置爲其餘事件
public class NIOServer { // 通道管理器 private Selector selector; /** * 得到一個ServerSocket通道,並對該通道作一些初始化的工做 * * @param port * 綁定的端口號 * @throws IOException */ public void initServer(int port) throws IOException { // 得到一個ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 設置通道爲非阻塞 serverChannel.configureBlocking(false); // 將該通道對應的ServerSocket綁定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 得到一個通道管理器 this.selector = Selector.open(); // 將通道管理器和該通道綁定,併爲該通道註冊SelectionKey.OP_ACCEPT事件,註冊該事件後, // 當該事件到達時,selector.select()會返回,若是該事件沒到達selector.select()會一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 採用輪詢的方式監聽selector上是否有須要處理的事件,若是有,則進行處理 * * @throws IOException */ public void listen() throws IOException { System.out.println("服務端啓動成功!"); // 輪詢訪問selector while (true) { // 當註冊的事件到達時,方法返回;不然,該方法會一直阻塞 selector.select(); // 得到selector中選中的項的迭代器,選中的項爲註冊的事件 Iterator<?> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 刪除已選的key,以防重複處理 ite.remove(); handler(key); } } } /** * 處理請求 * * @param key * @throws IOException */ public void handler(SelectionKey key) throws IOException { // 客戶端請求鏈接事件 if (key.isAcceptable()) { handlerAccept(key); // 得到了可讀的事件 } else if (key.isReadable()) { handelerRead(key); } } /** * 處理鏈接請求 * * @param key * @throws IOException */ public void handlerAccept(SelectionKey key) throws IOException { ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 得到和客戶端鏈接的通道 SocketChannel channel = server.accept(); // 設置成非阻塞 channel.configureBlocking(false); // 在這裏能夠給客戶端發送信息哦 System.out.println("新的客戶端鏈接"); // 在和客戶端鏈接成功以後,爲了能夠接收到客戶端的信息,須要給通道設置讀的權限。 channel.register(this.selector, SelectionKey.OP_READ); } /** * 處理讀的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) throws IOException { // 服務器可讀取消息:獲得事件發生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服務端收到信息:" + msg); //回寫數據 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 將消息回送給客戶端 }else{ System.out.println("客戶端關閉"); key.cancel(); } } /** * 啓動服務端測試 * * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(10101); server.listen(); } }
NIO與傳統IO最大的不一樣
使用telnet測試,NIO是確定支持多個客戶端同時操做的,但很重要的一點是NIO是單線程的,傳統IO和NIO的邏輯以下
傳統IO
NIO
至於NIO如何多線程,能夠參考NIO如何多線程操做 ,這其實也是Netty的原理。
分別用兩個telnet鏈接
admindeMacBook-Pro:IOServer admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
dsfds
好的
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的
服務端顯示以下
服務端啓動成功!
新的客戶端鏈接
服務端收到信息:dsfds
新的客戶端鏈接
服務端收到信息:22222
當咱們退出其中一個的時候
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的^]
telnet> quit
Connection closed.
服務端顯示以下
服務端啓動成功!
新的客戶端鏈接
服務端收到信息:dsfds
新的客戶端鏈接
服務端收到信息:22222
客戶端關閉
若是咱們使用telnet鏈接進去之後,直接關閉shell,則服務端會拋出異常
服務端啓動成功!
新的客戶端鏈接
服務端收到信息:
Exception in thread "main" java.io.IOException: Connection reset by peer
at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:197)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at com.guanjian.websocket.io.NIOServer.handelerRead(NIOServer.java:111)
at com.guanjian.websocket.io.NIOServer.handler(NIOServer.java:77)
at com.guanjian.websocket.io.NIOServer.listen(NIOServer.java:59)
at com.guanjian.websocket.io.NIOServer.main(NIOServer.java:134)
說明在讀取Buffer緩衝區的時候,拋出了異常,因此咱們應該在讀取的時候捕獲異常,而不是拋出異常
/** * 處理讀的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) { // 服務器可讀取消息:獲得事件發生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); try { int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服務端收到信息:" + msg); //回寫數據 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 將消息回送給客戶端 }else{ System.out.println("客戶端關閉"); key.cancel(); } } catch (IOException e) { e.printStackTrace(); } }
咱們如今來證實NIO是單線程的,將以上代碼修改一下
/** * 處理讀的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) { // 服務器可讀取消息:獲得事件發生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); try { int read = channel.read(buffer); Thread.sleep(60000); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服務端收到信息:" + msg); //回寫數據 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 將消息回送給客戶端 }else{ System.out.println("客戶端關閉"); key.cancel(); } } catch (Exception e) { e.printStackTrace(); } }
咱們讓他發送消息的時候睡一分鐘。啓動服務端,鏈接第一個telnet進來,併發幾個字符
此時咱們連進第二個telnet,會發現服務端沒反應,須要等到一分鐘以後,第一個telnet纔會收到"好的",而服務端纔會顯示"新的客戶端鏈接"。
說明服務端在處理髮送字符的時候被阻塞,NIO爲單線程。