java nio中,爲何客戶端一方正常關閉了Socket,而服務端的isReadable()還老是返回true?

我這篇文章想講的是編程時如何正確關閉tcp鏈接。
首先給出一個網絡上絕大部分的java nio代碼示例:
服務端:
1首先實例化一個多路I/O複用器Selector
2而後實例化一個ServerSocketChannel
3ServerSocketChannel註冊爲非阻塞(channel.configureBlocking(false);)
4ServerSocketChannel註冊到Selector,並監聽鏈接事件(serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);)
5Selector開始輪詢,若是監聽到了isAcceptable()事件,就創建一個鏈接,若是監聽到了isReadable()事件,就讀數據。
6處理完或者在處理每一個事件以前將SelectionKey移除出Selector.selectedKeys()
代碼:java

package qiuqi.main;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NioServer {
    public static void main(String[] args) throws IOException {
        startServer();
    }

    static void startServer() throws IOException {

        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(999));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                iterator.remove();

                if (sk.isAcceptable()) {
                    SocketChannel channel = serverSocketChannel.accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);

                } else if (sk.isReadable()) {
                    System.out.println("讀事件!!!");
                    SocketChannel channel = (SocketChannel) sk.channel();
                    try {
                        ByteBuffer byteBuffer = ByteBuffer.allocate(200);
                        //這裏只讀數據,未做任何處理
                        channel.read(byteBuffer);
          
                    } catch (IOException e) {
                        //手動關閉channel
                        System.out.println(e.getMessage());
                        sk.cancel();
                        if (channel != null)
                            channel.close();
                    }
                }


            }
        }
    }
}

還有說明一下,爲何在if (sk.isReadable()){}這個裏面加上異常捕捉,由於可能讀數據的時候客戶端忽然斷掉,若是不捕捉這個異常,將會致使整個程序結束。
而客戶端若是使用NIO編程,那麼和服務端很像,然鵝,咱們並不須要使用NIO編程,由於這裏我想講的問題和NIO或是普通IO無關,在我想講的問題上,他倆是同樣的,那麼我就用普通socket編程來說解,由於這個好寫:)。編程

直接給代碼以下:網絡

package qiuqi.main;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

public class TraditionalSocketClient {

    public static void main(String[] args) throws IOException {

        startClient();
    }
    static void startClient() throws IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress(999));
        socket.getOutputStream().write(new byte[100]);
        //要注意這個close方法,這是正常關閉socket的方法
        //也是致使這個錯誤的根源
        socket.close();
    }
}

咱們運行客戶端和服務端的代碼,輸出的結果是:
讀事件!!!
讀事件!!!
讀事件!!!
讀事件!!!
讀事件!!!
讀事件!!!
....
讀事件!!!
讀事件!!!
無限個讀事件!!!
why???
客戶端正常關閉,而後顯然客戶端不可能再給服務端發送任何數據了,服務端怎麼可能還有讀響應呢?
咱們如今把客戶端代碼的最後一行socket.close();這個去掉,再運行一次!輸出結果是:
讀事件!!!
讀事件!!!
遠程主機強迫關閉了一個現有的鏈接。socket

而後。。。就正常了(固然代碼裏會有異常提示的),這裏的正常指的是不會輸出多餘的讀事件!!!了。
這又是怎麼回事?
咱們知道若是去掉socket.close();那麼客戶端是非正常關閉,服務端這邊會引起IOException。
引起完IOExpection以後,咱們的程序在catch{}語句塊中手動關閉了channel。tcp

既然非正常關閉會引起異常,那麼正常關閉呢?什麼都不引起?可是這樣服務端怎麼知道客戶端已經關閉了呢?
顯然服務端會收到客戶端的關閉信號(可讀數據),而網絡上絕大多數代碼並無根據這個關閉信號來結束channel。
那麼關閉信號是什麼?.net

channel.read(byteBuffer);

這個語句是有返回值的,大多數狀況是返回一個大於等於0的值,表示將多少數據讀入byteBuffer緩衝區。
然鵝,當客戶端正常斷開鏈接的時候,它就會返回-1。雖然這個斷開鏈接信號也是可讀數據(會使得isReadable()爲true),可是
這個信號沒法被讀入byteBuffer,也就是說一旦返回-1,那麼不管再繼續讀多少次都是-1,而且會引起可讀事件isReadable()。
所以,這樣寫問題就能獲得解決,下面的代碼在try語句塊裏。code

SocketChannel channel = (SocketChannel) sk.channel();
            try {
                ByteBuffer byteBuffer = ByteBuffer.allocate(200);
                int num;
                //這裏只讀數據,未做任何處理
                num = channel.read(byteBuffer);
                if(num == -1)
                    throw new IOException("讀完成");

            } catch (IOException e) {
                System.out.println(e.getMessage());
                sk.cancel();
                if (channel != null)
                    channel.close();
            }

這裏我根據返回值-1來拋出異常,使得下面的catch語句塊捕捉並關閉鏈接,也能夠不拋出異常,直接在try{}裏處理。
還要注意一點的是,假如說bytebuffer已經滿了,也就是channel.read(byteBuffer)返回0,那麼即便客戶端正常關閉,也沒法收到-1。所以當bytebuffer滿的時候須要及時清空,或者一開始就開一個大一點的bytebuffer。server

相關文章
相關標籤/搜索