Java Socket NIO
服務端:
public class NIOServer {private static final String HOST = "localhost";private static final int PORT = 10086;public static void main(String[] args) {ServerSocketChannel serverSocketChannel = null;ServerSocket serverSocket = null;Selector selector = null;try {serverSocketChannel = ServerSocketChannel.open();//工廠方法創建ServerSocketChannelserverSocket = serverSocketChannel.socket(); //獲取channel對應的ServerSocketserverSocket.bind(new InetSocketAddress(HOST, PORT)); //綁定地址serverSocketChannel.configureBlocking(false); //設置ServerSocketChannel非阻塞模式selector = Selector.open();//工廠方法創建SelectorserverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//通道注冊選擇器,接受連接就緒狀態。while (true) {//循環檢查if (selector.select() == 0) {//阻塞檢查,當有就緒狀態發生,返回鍵集合continue;}// if (selector.select(2000) == 0) {// //在等待信道準備的同時,也可以異步地執行其他任務, 這里打印*// System.out.print("*");// continue;// }Iterator<SelectionKey> it = selector.selectedKeys().iterator(); //獲取就緒鍵遍歷對象。while (it.hasNext()) {SelectionKey selectionKey = it.next();//處理就緒狀態if (selectionKey.isAcceptable()) {ServerSocketChannel schannel = (ServerSocketChannel) selectionKey.channel();//只負責監聽,阻塞,管理,不發送、接收數據SocketChannel socketChannel = schannel.accept();//就緒后的操作,剛到達的socket句柄if (null == socketChannel) {continue;}socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ); //告知選擇器關心的通道,準備好讀數據} else if (selectionKey.isReadable()) {SocketChannel socketChannel = (SocketChannel) selectionKey.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(4 * 1024);System.out.println(socketChannel.getRemoteAddress());int read = socketChannel.read(byteBuffer);if (read == -1) {//如果不關閉會一直產生isReadable這個消息 selectionKey.cancel();socketChannel.close();} else {StringBuilder result = new StringBuilder();while (read > 0) {//確保讀完 byteBuffer.flip();result.append(new String(byteBuffer.array()));byteBuffer.clear();//每次清空 對應上面flip()read = socketChannel.read(byteBuffer);}System.out.println("server receive: " + result.toString());socketChannel.register(selector, SelectionKey.OP_WRITE);}} else if (selectionKey.isWritable()) {SocketChannel socketChannel = (SocketChannel) selectionKey.channel();String sendStr = "server send data: " + Math.random();ByteBuffer send = ByteBuffer.wrap(sendStr.getBytes());while (send.hasRemaining()) {socketChannel.write(send);}socketChannel.register(selector, SelectionKey.OP_READ);System.out.println(sendStr);}it.remove();}}} catch (IOException e) {e.printStackTrace();}} }客戶端:
public class NIOClient {public static void main(String[] args) throws Exception {SocketChannel clntChan = SocketChannel.open();clntChan.configureBlocking(false);if (!clntChan.connect(new InetSocketAddress("localhost", 10086))) {//不斷地輪詢連接狀態,直到完成連接while (!clntChan.finishConnect()) {//在等待連接的時間里,可以執行其他任務,以充分發揮非阻塞IO的異步特性//這里為了演示該方法的使用,只是一直打印"."System.out.print(".");}}//為了與后面打印的"."區別開來,這里輸出換行符System.out.print("\n");//分別實例化用來讀寫的緩沖區ByteBuffer writeBuf = ByteBuffer.wrap("send send send".getBytes());ByteBuffer readBuf = ByteBuffer.allocate("send".getBytes().length - 1);while (writeBuf.hasRemaining()) {//如果用來向通道中寫數據的緩沖區中還有剩余的字節,則繼續將數據寫入信道 clntChan.write(writeBuf);}Thread.sleep(10000);StringBuffer stringBuffer = new StringBuffer();//如果read()接收到-1,表明服務端關閉,拋出異常while ((clntChan.read(readBuf)) > 0) {readBuf.flip();stringBuffer.append(new String(readBuf.array(), 0, readBuf.limit()));readBuf.clear();}//打印出接收到的數據System.out.println("Client Received: " + stringBuffer.toString());//關閉信道 clntChan.close();} }注意當客戶端斷開socket的時候需要處理,不然服務端對應的channel會一直處于readable的狀態,會造成死循環。
當客戶端調用:clntChan.close();
這一句是正常關閉代碼,它會傳給服務端關閉的指令(也就是數據)。
而服務端的socketChannel.read(byteBuffer)=-1則代表收到這個關閉指令(數據),可以依據-1來關閉服務端的channel,不過仍有可能有問題,可能是客戶端調用socketChannel.close()的時候,服務端這邊的bytebuffer已經滿了,那么num只能返回0而不能返回-1。具體該怎么寫你應該根據你自己的業務來處理。
?
當socketChannel為阻塞方式時(默認就是阻塞方式)read函數,不會返回0,阻塞方式的socketChannel,若沒有數據可讀,或者緩沖區滿了,就會阻塞,直到滿足讀的條件,所以一般阻塞方式的read是比較簡單的,不過阻塞方式的socketChannel的問題也是顯而易見的。這里我結合基于NIO 寫ftp服務器調試過程中碰到的問題,總結一下非阻塞場景下的read碰到的問題。注意:這里的場景都是基于客戶端以阻塞socket的方式發送數據。
1、read什么時候返回-1
read返回-1說明客戶端的數據發送完畢,并且主動的close socket。所以在這種場景下,(服務器程序)你需要關閉socketChannel并且取消key,最好是退出當前函數。注意,這個時候服務端要是繼續使用該socketChannel進行讀操作的話,就會拋出“遠程主機強迫關閉一個現有的連接”的IO異常。
2、read什么時候返回0
其實read返回0有3種情況,一是某一時刻socketChannel中當前(注意是當前)沒有數據可以讀,這時會返回0,其次是bytebuffer的position等于limit了,即bytebuffer的remaining等于0,這個時候也會返回0,最后一種情況就是客戶端的數據發送完畢了(注意看后面的程序里有這樣子的代碼),這個時候客戶端想獲取服務端的反饋調用了recv函數,若服務端繼續read,這個時候就會返回0。
-------------------------------------------------------------------------------------------------
實際寫代碼過程中觀察發現,如果客戶端發送數據后不關閉channel,同時服務端收到數據后反倒再次發給客戶端,那么此時客戶端read方法永遠返回0.
?
SelectionKey.OP_WRITE
當時有一個連接進來后,如果注冊了SelectionKey.OP_WRITE消息,selectionKey.isWritable()會一直返回true知道寫緩沖區寫滿。所以這點就值注冊了SelectionKey.OP_READ.
OP_WRITE事件的就緒條件并不是發生在調用channel的write方法之后,而是在當底層緩沖區有空閑空間的情況下。因為寫緩沖區在絕大部分時候都是有空閑空間的,所以如果你注冊了寫事件,這會使得寫事件一直處于就就緒,選擇處理現場就會一直占用著CPU資源。所以,只有當你確實有數據要寫時再注冊寫操作,并在寫完以后馬上取消注冊。其實,在大部分情況下,我們直接調用channel的write方法寫數據就好了,沒必要都用OP_WRITE事件。那么OP_WRITE事件主要是在什么情況下使用的了?
其實OP_WRITE事件主要是在發送緩沖區空間滿的情況下使用的。如: while (buffer.hasRemaining()) {int len = socketChannel.write(buffer); if (len == 0) {selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);selector.wakeup();break;} } 當buffer還有數據,但緩沖區已經滿的情況下,socketChannel.write(buffer)會返回已經寫出去的字節數,此時為0。那么這個時候我們就需要注冊OP_WRITE事件,這樣當緩沖區又有空閑空間的時候就會觸發OP_WRITE事件,這是我們就可以繼續將沒寫完的數據繼續寫出了。
而且在寫完后,一定要記得將OP_WRITE事件注銷:
selectionKey.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
注意,這里在修改了interest之后調用了wakeup();方法是為了喚醒被堵塞的selector方法,這樣當while中判斷selector返回的是0時,會再次調用selector.select()。而selectionKey的interest是在每次selector.select()操作的時候注冊到系統進行監聽的,所以在selector.select()調用之后修改的interest需要在下一次selector.select()調用才會生效。
nio的select()的時候,只要數據通道允許寫,每次select()返回的OP_WRITE都是true。所以在nio的寫數據里面,我們在每次需要寫數據之前把數據放到緩沖區,并且注冊OP_WRITE,對selector進行wakeup(),這樣這一輪select()發現有OP_WRITE之后,將緩沖區數據寫入channel,清空緩沖區,并且反注冊OP_WRITE,寫數據完成。這里面需要注意的是,每個SocketChannel只對應一個SelectionKey,也就是說,在上述的注冊和反注冊OP_WRITE的時候,不是通過channel.register()和key.cancel()做到的,而是通過key.interestOps()做到的。
public void write(MessageSession session, ByteBuffer buffer) throws ClosedChannelException {SelectionKey key = session.key();if((key.interestOps() & SelectionKey.OP_WRITE) == 0) {key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);}try {writebuf.put(buffer);} catch(Exception e) {System.out.println("want put:"+buffer.remaining()+", left:"+writebuf.remaining());e.printStackTrace();}selector.wakeup(); } while(true) {selector.select();.....if(key.isWritable()) {MessageSession session = (MessageSession)key.attachment();//System.out.println("Select a write");synchronized(session) {writebuf.flip();SocketChannel channel = (SocketChannel)key.channel();int count = channel.write(writebuf);//System.out.println("write "+count+" bytes"); writebuf.clear();key.interestOps(SelectionKey.OP_READ);}}......}要點一:不推薦直接寫channel,而是通過緩存和attachment傳入要寫的數據,改變interestOps()來寫數據;
要點二:每個channel只對應一個SelectionKey,所以,只能改變interestOps(),不能register()和cancel()。
參考:https://www.cnblogs.com/burgeen/p/3618059.html
轉載于:https://www.cnblogs.com/grasp/p/10607271.html
總結
以上是生活随笔為你收集整理的Java Socket NIO的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NodeJs(一)
- 下一篇: 用vb.net实现拖放功能