BIO到NIO原始碼的一些事兒之BIO

知秋z發表於2019-01-02

此篇文章會詳細解讀由BIO到NIO的逐步演進的心靈路程,為Reactor-Netty 庫的講解鋪平道路。

關於Java程式設計方法論-Reactor與Webflux的視訊分享,已經完成了Rxjava 與 Reactor,b站地址如下:

Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…

Reactor原始碼解讀與分享:www.bilibili.com/video/av353…

引入

我們通過一個BIO的Demo來展示其用法:

//服務端
public class BIOServer {
    public void initBIOServer(int port)
    {
        ServerSocket serverSocket = null;//服務端Socket
        Socket socket = null;//客戶端socket
        BufferedReader reader = null;
        String inputContent;
        int count = 0;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println(stringNowTime() + ": serverSocket started");
            while(true)
            {
                socket = serverSocket.accept();
                System.out.println(stringNowTime() + ": id為" + socket.hashCode()+ "的Clientsocket connected");
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                while ((inputContent = reader.readLine()) != null) {
                    System.out.println("收到id為" + socket.hashCode() + "  "+inputContent);
                    count++;
                }
                System.out.println("id為" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"讀取結束");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                reader.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public String stringNowTime()
    {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return format.format(new Date());
    }

    public static void main(String[] args) {
        BIOServer server = new BIOServer();
        server.initBIOServer(8888);

    }
}
// 客戶端
public class BIOClient {

    public void initBIOClient(String host, int port) {
        BufferedReader reader = null;
        BufferedWriter writer = null;
        Socket socket = null;
        String inputContent;
        int count = 0;
        try {
            reader = new BufferedReader(new InputStreamReader(System.in));
            socket = new Socket(host, port);
            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            System.out.println("clientSocket started: " + stringNowTime());
            while (((inputContent = reader.readLine()) != null) && count < 2) {
                inputContent = stringNowTime() + ": 第" + count + "條訊息: " + inputContent + "\n";
                writer.write(inputContent);//將訊息傳送給服務端
                writer.flush();
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
                reader.close();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String stringNowTime() {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return format.format(new Date());
    }

    public static void main(String[] args) {
        BIOClient client = new BIOClient();
        client.initBIOClient("127.0.0.1", 8888);
    }

}

複製程式碼

通過上面的例子,我們可以知道,無論是服務端還是客戶端,我們關注的幾個操作有基於服務端的serverSocket = new ServerSocket(port) serverSocket.accept(),基於客戶端的Socket socket = new Socket(host, port); 以及兩者都有的讀取與寫入Socket資料的方式,即通過流來進行讀寫,這個讀寫不免通過一箇中間位元組陣列buffer來進行。

ServerSocket中bind解讀

於是,我們通過原始碼來看這些相應的邏輯。我們先來看ServerSocket.java這個類的相關程式碼。 我們檢視ServerSocket.java的構造器可以知道,其最後依然會呼叫它的bind方法:

//java.net.ServerSocket#ServerSocket(int)
public ServerSocket(int port) throws IOException {
    this(port, 50, null);
}
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
    setImpl();
    if (port < 0 || port > 0xFFFF)
        throw new IllegalArgumentException(
                    "Port value out of range: " + port);
    if (backlog < 1)
        backlog = 50;
    try {
        bind(new InetSocketAddress(bindAddr, port), backlog);
    } catch(SecurityException e) {
        close();
        throw e;
    } catch(IOException e) {
        close();
        throw e;
    }
}
複製程式碼

按照我們的Demo和上面的原始碼可知,這裡傳入的引數endpoint並不會為null,同時,屬於InetSocketAddress型別,backlog大小為50,於是,我們應該關注的主要程式碼邏輯也就是getImpl().bind(epoint.getAddress(), epoint.getPort());:

public void bind(SocketAddress endpoint, int backlog) throws IOException {
    if (isClosed())
        throw new SocketException("Socket is closed");
    if (!oldImpl && isBound())
        throw new SocketException("Already bound");
    if (endpoint == null)
        endpoint = new InetSocketAddress(0);
    if (!(endpoint instanceof InetSocketAddress))
        throw new IllegalArgumentException("Unsupported address type");
    InetSocketAddress epoint = (InetSocketAddress) endpoint;
    if (epoint.isUnresolved())
        throw new SocketException("Unresolved address");
    if (backlog < 1)
        backlog = 50;
    try {
        SecurityManager security = System.getSecurityManager();
        if (security != null)
            security.checkListen(epoint.getPort());
        // 我們應該關注的主要邏輯
        getImpl().bind(epoint.getAddress(), epoint.getPort());
        getImpl().listen(backlog);
        bound = true;
    } catch(SecurityException e) {
        bound = false;
        throw e;
    } catch(IOException e) {
        bound = false;
        throw e;
    }
}
複製程式碼

這裡getImpl(),由上面構造器的實現中,我們有看到setImpl();,可知,其factory預設為null,所以,這裡我們關注的是SocksSocketImpl這個類,建立其物件,並將當前ServerSocket物件設定其中,這個設定的原始碼請在SocksSocketImpl的父類java.net.SocketImpl中檢視。 那麼getImpl也就明瞭了,其實就是我們Socket的底層實現對應的實體類了,因為不同的作業系統核心是不同的,他們對於Socket的實現當然會各有不同,我們這點要注意下,這裡針對的是win下面的系統。

/**
* The factory for all server sockets.
*/
private static SocketImplFactory factory = null;
private void setImpl() {
    if (factory != null) {
        impl = factory.createSocketImpl();
        checkOldImpl();
    } else {
        // No need to do a checkOldImpl() here, we know it's an up to date
        // SocketImpl!
        impl = new SocksSocketImpl();
    }
    if (impl != null)
        impl.setServerSocket(this);
}
/**
* Get the {@code SocketImpl} attached to this socket, creating
* it if necessary.
*
* @return  the {@code SocketImpl} attached to that ServerSocket.
* @throws SocketException if creation fails.
* @since 1.4
*/
SocketImpl getImpl() throws SocketException {
    if (!created)
        createImpl();
    return impl;
}
/**
* Creates the socket implementation.
*
* @throws IOException if creation fails
* @since 1.4
*/
void createImpl() throws SocketException {
    if (impl == null)
        setImpl();
    try {
        impl.create(true);
        created = true;
    } catch (IOException e) {
        throw new SocketException(e.getMessage());
    }
}

複製程式碼

我們再看SocksSocketImpl的bind方法實現,然後得到其最後無非是呼叫本地方法bind0

//java.net.AbstractPlainSocketImpl#bind
/**
* Binds the socket to the specified address of the specified local port.
* @param address the address
* @param lport the port
*/
protected synchronized void bind(InetAddress address, int lport)
    throws IOException
{
    synchronized (fdLock) {
        if (!closePending && (socket == null || !socket.isBound())) {
            NetHooks.beforeTcpBind(fd, address, lport);
        }
    }
    socketBind(address, lport);
    if (socket != null)
        socket.setBound();
    if (serverSocket != null)
        serverSocket.setBound();
}

//java.net.PlainSocketImpl#socketBind
@Override
void socketBind(InetAddress address, int port) throws IOException {
    int nativefd = checkAndReturnNativeFD();

    if (address == null)
        throw new NullPointerException("inet address argument is null.");

    if (preferIPv4Stack && !(address instanceof Inet4Address))
        throw new SocketException("Protocol family not supported");

    bind0(nativefd, address, port, useExclusiveBind);
    if (port == 0) {
        localport = localPort0(nativefd);
    } else {
        localport = port;
    }

    this.address = address;
}
//java.net.PlainSocketImpl#bind0
static native void bind0(int fd, InetAddress localAddress, int localport,
                             boolean exclBind)
        throws IOException;
複製程式碼

這裡,我們還要了解的是,使用了多執行緒只是能夠實現對"業務邏輯處理"的多執行緒,但是對於資料包文的接收還是需要一個一個來的,也就是我們上面Demo中見到的accept以及read方法阻塞問題,多執行緒是根本解決不了的,那麼首先我們來看看accept為什麼會造成阻塞,accept方法的作用是詢問作業系統是否有新的Socket套接字資訊從埠XXX處傳送過來,注意這裡詢問的是作業系統,也就是說Socket套接字IO模式的支援是基於作業系統的,如果作業系統沒有發現有套接字從指定埠XXX連線進來,那麼作業系統就會等待,這樣accept方法就會阻塞,他的內部實現使用的是作業系統級別的同步IO。

ServerSocket中accept解讀

於是,我們來分析下ServerSocket.accept方法的原始碼過程:

public Socket accept() throws IOException {
    if (isClosed())
        throw new SocketException("Socket is closed");
    if (!isBound())
        throw new SocketException("Socket is not bound yet");
    Socket s = new Socket((SocketImpl) null);
    implAccept(s);
    return s;
}
複製程式碼

首先進行的是一些判斷,接著建立了一個Socket物件(為什麼這裡要建立一個Socket物件,後面會講到),執行了implAccept方法,來看看implAccept方法:

/**
* Subclasses of ServerSocket use this method to override accept()
* to return their own subclass of socket.  So a FooServerSocket
* will typically hand this method an <i>empty</i> FooSocket.  On
* return from implAccept the FooSocket will be connected to a client.
*
* @param s the Socket
* @throws java.nio.channels.IllegalBlockingModeException
*         if this socket has an associated channel,
*         and the channel is in non-blocking mode
* @throws IOException if an I/O error occurs when waiting
* for a connection.
* @since   1.1
* @revised 1.4
* @spec JSR-51
*/
protected final void implAccept(Socket s) throws IOException {
SocketImpl si = null;
try {
    if (s.impl == null)
        s.setImpl();
    else {
        s.impl.reset();
    }
    si = s.impl;
    s.impl = null;
    si.address = new InetAddress();
    si.fd = new FileDescriptor();
    getImpl().accept(si);  // <1>
    SocketCleanable.register(si.fd);   // raw fd has been set

    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccept(si.getInetAddress().getHostAddress(),
                                si.getPort());
    }
} catch (IOException e) {
    if (si != null)
        si.reset();
    s.impl = si;
    throw e;
} catch (SecurityException e) {
    if (si != null)
        si.reset();
    s.impl = si;
    throw e;
}
s.impl = si;
s.postAccept();
}
複製程式碼

上面執行了<1>處getImpl的accept方法之後,我們在AbstractPlainSocketImpl找到accept方法:

//java.net.AbstractPlainSocketImpl#accept
/**
* Accepts connections.
* @param s the connection
*/
protected void accept(SocketImpl s) throws IOException {
acquireFD();
try {
    socketAccept(s);
} finally {
    releaseFD();
}
}
複製程式碼

可以看到他呼叫了socketAccept方法,因為每個作業系統的Socket地實現都不同,所以這裡Windows下就執行了我們PlainSocketImpl裡面的socketAccept方法:

// java.net.PlainSocketImpl#socketAccept
@Override
void socketAccept(SocketImpl s) throws IOException {
    int nativefd = checkAndReturnNativeFD();

    if (s == null)
        throw new NullPointerException("socket is null");

    int newfd = -1;
    InetSocketAddress[] isaa = new InetSocketAddress[1];
    if (timeout <= 0) {  //<1>
        newfd = accept0(nativefd, isaa); // <2>
    } else {
        configureBlocking(nativefd, false);
        try {
            waitForNewConnection(nativefd, timeout);
            newfd = accept0(nativefd, isaa);  // <3>
            if (newfd != -1) {
                configureBlocking(newfd, true);
            }
        } finally {
            configureBlocking(nativefd, true);
        }
    } // <4>
    /* Update (SocketImpl)s' fd */
    fdAccess.set(s.fd, newfd);
    /* Update socketImpls remote port, address and localport */
    InetSocketAddress isa = isaa[0];
    s.port = isa.getPort();
    s.address = isa.getAddress();
    s.localport = localport;
    if (preferIPv4Stack && !(s.address instanceof Inet4Address))
        throw new SocketException("Protocol family not supported");
}
//java.net.PlainSocketImpl#accept0
 static native int accept0(int fd, InetSocketAddress[] isaa) throws IOException;
複製程式碼

這裡<1>到<4>之間是我們關注的程式碼,<2>和<3>執行了accept0方法,這個是native方法,具體來說就是與作業系統互動來實現監聽指定埠上是否有客戶端接入,正是因為accept0在沒有客戶端接入的時候會一直處於阻塞狀態,所以造成了我們程式級別的accept方法阻塞,當然對於程式級別的阻塞,我們是可以避免的,也就是我們可以將accept方法修改成非阻塞式,但是對於accept0造成的阻塞我們暫時是沒法改變的,作業系統級別的阻塞其實就是我們通常所說的同步非同步中的同步了。 前面說到我們可以在程式級別改變accept的阻塞,具體怎麼實現?其實就是通過我們上面socketAccept方法中判斷timeout的值來實現,在第<1>處判斷timeout的值如果小於等於0,那麼直接執行accept0方法,這時候將一直處於阻塞狀態,但是如果我們設定了timeout的話,即timeout值大於0的話,則程式會在等到我們設定的時間後返回,注意這裡的newfd如果等於-1的話,表示這次accept沒有發現有資料從底層返回;那麼到底timeout的值是在哪設定?我們可以通過ServerSocket的setSoTimeout方法進行設定,來看看這個方法:

/**
* Enable/disable {@link SocketOptions#SO_TIMEOUT SO_TIMEOUT} with the
* specified timeout, in milliseconds.  With this option set to a non-zero
* timeout, a call to accept() for this ServerSocket
* will block for only this amount of time.  If the timeout expires,
* a <B>java.net.SocketTimeoutException</B> is raised, though the
* ServerSocket is still valid.  The option <B>must</B> be enabled
* prior to entering the blocking operation to have effect.  The
* timeout must be {@code > 0}.
* A timeout of zero is interpreted as an infinite timeout.
* @param timeout the specified timeout, in milliseconds
* @exception SocketException if there is an error in
* the underlying protocol, such as a TCP error.
* @since   1.1
* @see #getSoTimeout()
*/
public synchronized void setSoTimeout(int timeout) throws SocketException {
if (isClosed())
    throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
}
複製程式碼

其執行了getImpl的setOption方法,並且設定了timeout時間,這裡,我們從AbstractPlainSocketImpl中檢視:

//java.net.AbstractPlainSocketImpl#setOption
public void setOption(int opt, Object val) throws SocketException {
    if (isClosedOrPending()) {
        throw new SocketException("Socket Closed");
    }
    boolean on = true;
    switch (opt) {
        /* check type safety b4 going native.  These should never
            * fail, since only java.Socket* has access to
            * PlainSocketImpl.setOption().
            */
    case SO_LINGER:
        if (val == null || (!(val instanceof Integer) && !(val instanceof Boolean)))
            throw new SocketException("Bad parameter for option");
        if (val instanceof Boolean) {
            /* true only if disabling - enabling should be Integer */
            on = false;
        }
        break;
    case SO_TIMEOUT: //<1>
        if (val == null || (!(val instanceof Integer)))
            throw new SocketException("Bad parameter for SO_TIMEOUT");
        int tmp = ((Integer) val).intValue();
        if (tmp < 0)
            throw new IllegalArgumentException("timeout < 0");
        timeout = tmp;
        break;
    case IP_TOS:
            if (val == null || !(val instanceof Integer)) {
                throw new SocketException("bad argument for IP_TOS");
            }
            trafficClass = ((Integer)val).intValue();
            break;
    case SO_BINDADDR:
        throw new SocketException("Cannot re-bind socket");
    case TCP_NODELAY:
        if (val == null || !(val instanceof Boolean))
            throw new SocketException("bad parameter for TCP_NODELAY");
        on = ((Boolean)val).booleanValue();
        break;
    case SO_SNDBUF:
    case SO_RCVBUF:
        if (val == null || !(val instanceof Integer) ||
            !(((Integer)val).intValue() > 0)) {
            throw new SocketException("bad parameter for SO_SNDBUF " +
                                        "or SO_RCVBUF");
        }
        break;
    case SO_KEEPALIVE:
        if (val == null || !(val instanceof Boolean))
            throw new SocketException("bad parameter for SO_KEEPALIVE");
        on = ((Boolean)val).booleanValue();
        break;
    case SO_OOBINLINE:
        if (val == null || !(val instanceof Boolean))
            throw new SocketException("bad parameter for SO_OOBINLINE");
        on = ((Boolean)val).booleanValue();
        break;
    case SO_REUSEADDR:
        if (val == null || !(val instanceof Boolean))
            throw new SocketException("bad parameter for SO_REUSEADDR");
        on = ((Boolean)val).booleanValue();
        break;
    case SO_REUSEPORT:
        if (val == null || !(val instanceof Boolean))
            throw new SocketException("bad parameter for SO_REUSEPORT");
        if (!supportedOptions().contains(StandardSocketOptions.SO_REUSEPORT))
            throw new UnsupportedOperationException("unsupported option");
        on = ((Boolean)val).booleanValue();
        break;
    default:
        throw new SocketException("unrecognized TCP option: " + opt);
    }
    socketSetOption(opt, on, val);
}
複製程式碼

這個方法比較長,我們僅看與timeout有關的程式碼,即<1>處的程式碼。其實這裡僅僅就是將我們setOption裡面傳入的timeout值設定到了AbstractPlainSocketImpl的全域性變數timeout裡而已。

這樣,我們就可以在程式級別將accept方法設定成為非阻塞式的了,但是read方法現在還是阻塞式的,即後面我們還需要改造read方法,同樣將它在程式級別上變成非阻塞式。

通過Demo改造來進行accept的非阻塞實現

在正式改造前,我們有必要來解釋下Socket下同步/非同步和阻塞/非阻塞:

同步/非同步是屬於作業系統級別的,指的是作業系統在收到程式請求的IO之後,如果IO資源沒有準備好的話,該如何響應程式的問題,同步的話就是不響應,直到IO資源準備好;而非同步的話則會返回給程式一個標誌,這個標誌用於當IO資源準備好後通過事件機制傳送的內容應該發到什麼地方。

阻塞/非阻塞是屬於程式級別的,指的是程式在請求作業系統進行IO操作時,如果IO資源沒有準備好的話,程式該怎麼處理的問題,阻塞的話就是程式什麼都不做,一直等到IO資源準備好,非阻塞的話程式則繼續執行,但是會時不時的去檢視下IO到底準備好沒有呢;

我們通常見到的BIO是同步阻塞式的,同步的話說明作業系統底層是一直等待IO資源準備直到ok的,阻塞的話是程式本身也在一直等待IO資源準備直到ok,具體來講程式級別的阻塞就是accept和read造成的,我們可以通過改造將其變成非阻塞式,但是作業系統層次的阻塞我們沒法改變。

我們的NIO是同步非阻塞式的,其實它的非阻塞實現原理和我們上面的講解差不多的,就是為了改善accept和read方法帶來的阻塞現象,所以引入了ChannelBuffer的概念。 好了,我們對我們的Demo進行改進,解決accept帶來的阻塞問題(為多個客戶端連線做的非同步處理,這裡就不多解釋了,讀者可自行思考,實在不行可到本人相關視訊中找到對應解讀):

public class BIOProNotB {

    public void initBIOServer(int port) {
        ServerSocket serverSocket = null;//服務端Socket
        Socket socket = null;//客戶端socket
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ClientSocketThread thread = null;
        try {
            serverSocket = new ServerSocket(port);
            serverSocket.setSoTimeout(1000);
            System.out.println(stringNowTime() + ": serverSocket started");
            while (true) {
                try {
                    socket = serverSocket.accept();
                } catch (SocketTimeoutException e) {
                    //執行到這裡表示本次accept是沒有收到任何資料的,服務端的主執行緒在這裡可以做一些其他事情
                    System.out.println("now time is: " + stringNowTime());
                    continue;
                }
                System.out.println(stringNowTime() + ": id為" + socket.hashCode() + "的Clientsocket connected");
                thread = new ClientSocketThread(socket);
                threadPool.execute(thread);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String stringNowTime() {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        return format.format(new Date());
    }

    class ClientSocketThread extends Thread {
        public Socket socket;

        public ClientSocketThread(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            BufferedReader reader = null;
            String inputContent;
            int count = 0;
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                while ((inputContent = reader.readLine()) != null) {
                    System.out.println("收到id為" + socket.hashCode() + "  " + inputContent);
                    count++;
                }
                System.out.println("id為" + socket.hashCode() + "的Clientsocket " + stringNowTime() + "讀取結束");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    reader.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        BIOProNotB server = new BIOProNotB();
        server.initBIOServer(8888);
    }


}
複製程式碼

為我們的ServerSocket設定了timeout時間,這樣的話呼叫accept方法的時候每隔1s他就會被喚醒一次,而不再是一直在那裡,只有有客戶端接入才會返回資訊;我們執行一下看看結果:

2019-01-02 17:28:43:362: serverSocket started
now time is: 2019-01-02 17:28:44:363
now time is: 2019-01-02 17:28:45:363
now time is: 2019-01-02 17:28:46:363
now time is: 2019-01-02 17:28:47:363
now time is: 2019-01-02 17:28:48:363
now time is: 2019-01-02 17:28:49:363
now time is: 2019-01-02 17:28:50:363
now time is: 2019-01-02 17:28:51:364
now time is: 2019-01-02 17:28:52:365
now time is: 2019-01-02 17:28:53:365
now time is: 2019-01-02 17:28:54:365
now time is: 2019-01-02 17:28:55:365
now time is: 2019-01-02 17:28:56:365 // <1>
2019-01-02 17:28:56:911: id為1308927845的Clientsocket connected
now time is: 2019-01-02 17:28:57:913 // <2>
now time is: 2019-01-02 17:28:58:913
複製程式碼

可以看到,我們剛開始並沒有客戶端接入的時候,是會執行System.out.println("now time is: " + stringNowTime());的輸出,還有一點需要注意的就是,仔細看看上面的輸出結果的標記<1>與<2>,你會發現<2>處時間值不是17:28:57:365,原因就在於如果accept正常返回值的話,是不會執行catch語句部分的。

通過Demo改造來進行read的非阻塞實現

這樣的話,我們就把accept部分改造成了非阻塞式了,那麼read部分可以改造麼?當然可以,改造方法和accept很類似,我們在read的時候,會呼叫 java.net.AbstractPlainSocketImpl#getInputStream:

/**
* Gets an InputStream for this socket.
*/
protected synchronized InputStream getInputStream() throws IOException {
synchronized (fdLock) {
    if (isClosedOrPending())
        throw new IOException("Socket Closed");
    if (shut_rd)
        throw new IOException("Socket input is shutdown");
    if (socketInputStream == null)
        socketInputStream = new SocketInputStream(this);
}
return socketInputStream;
}
複製程式碼

這裡面建立了一個SocketInputStream物件,會將當前AbstractPlainSocketImpl物件傳進去,於是,在讀資料的時候,我們會呼叫如下方法:

public int read(byte b[], int off, int length) throws IOException {
    return read(b, off, length, impl.getTimeout());
}

int read(byte b[], int off, int length, int timeout) throws IOException {
    int n;

    // EOF already encountered
    if (eof) {
        return -1;
    }

    // connection reset
    if (impl.isConnectionReset()) {
        throw new SocketException("Connection reset");
    }

    // bounds check
    if (length <= 0 || off < 0 || length > b.length - off) {
        if (length == 0) {
            return 0;
        }
        throw new ArrayIndexOutOfBoundsException("length == " + length
                + " off == " + off + " buffer length == " + b.length);
    }

    // acquire file descriptor and do the read
    FileDescriptor fd = impl.acquireFD();
    try {
        n = socketRead(fd, b, off, length, timeout);
        if (n > 0) {
            return n;
        }
    } catch (ConnectionResetException rstExc) {
        impl.setConnectionReset();
    } finally {
        impl.releaseFD();
    }

    /*
        * If we get here we are at EOF, the socket has been closed,
        * or the connection has been reset.
        */
    if (impl.isClosedOrPending()) {
        throw new SocketException("Socket closed");
    }
    if (impl.isConnectionReset()) {
        throw new SocketException("Connection reset");
    }
    eof = true;
    return -1;
}
private int socketRead(FileDescriptor fd,
                           byte b[], int off, int len,
                           int timeout)
        throws IOException {
        return socketRead0(fd, b, off, len, timeout);
}
複製程式碼

這裡,我們看到了socketRead同樣設定了timeout,而且這個timeout就是我們建立這個SocketInputStream物件時傳入的AbstractPlainSocketImpl物件來控制的,所以,我們只需要設定serverSocket.setSoTimeout(1000)即可。 我們再次修改服務端程式碼(程式碼總共兩次設定,第一次是設定的是ServerSocket級別的,第二次設定的客戶端連線返回的那個Socket,兩者不一樣):

public class BIOProNotBR {

    public void initBIOServer(int port) {
        ServerSocket serverSocket = null;//服務端Socket
        Socket socket = null;//客戶端socket
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ClientSocketThread thread = null;
        try {
            serverSocket = new ServerSocket(port);
            serverSocket.setSoTimeout(1000);
            System.out.println(stringNowTime() + ": serverSocket started");
            while (true) {
                try {
                    socket = serverSocket.accept();
                } catch (SocketTimeoutException e) {
                    //執行到這裡表示本次accept是沒有收到任何資料的,服務端的主執行緒在這裡可以做一些其他事情
                    System.out.println("now time is: " + stringNowTime());
                    continue;
                }
                System.out.println(stringNowTime() + ": id為" + socket.hashCode() + "的Clientsocket connected");
                thread = new ClientSocketThread(socket);
                threadPool.execute(thread);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String stringNowTime() {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        return format.format(new Date());
    }

    class ClientSocketThread extends Thread {
        public Socket socket;

        public ClientSocketThread(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            BufferedReader reader = null;
            String inputContent;
            int count = 0;
            try {
                socket.setSoTimeout(1000);
            } catch (SocketException e1) {
                e1.printStackTrace();
            }
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                while (true) {
                    try {
                        while ((inputContent = reader.readLine()) != null) {
                            System.out.println("收到id為" + socket.hashCode() + "  " + inputContent);
                            count++;
                        }
                    } catch (Exception e) {
                        //執行到這裡表示read方法沒有獲取到任何資料,執行緒可以執行一些其他的操作
                        System.out.println("Not read data: " + stringNowTime());
                        continue;
                    }
                    //執行到這裡表示讀取到了資料,我們可以在這裡進行回覆客戶端的工作
                    System.out.println("id為" + socket.hashCode() + "的Clientsocket " + stringNowTime() + "讀取結束");
                    sleep(1000);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                try {
                    reader.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        BIOProNotBR server = new BIOProNotBR();
        server.initBIOServer(8888);
    }


}
複製程式碼

執行如下:

2019-01-02 17:59:03:713: serverSocket started
now time is: 2019-01-02 17:59:04:714
now time is: 2019-01-02 17:59:05:714
now time is: 2019-01-02 17:59:06:714
2019-01-02 17:59:06:932: id為1810132623的Clientsocket connected
now time is: 2019-01-02 17:59:07:934
Not read data: 2019-01-02 17:59:07:935
now time is: 2019-01-02 17:59:08:934
Not read data: 2019-01-02 17:59:08:935
now time is: 2019-01-02 17:59:09:935
Not read data: 2019-01-02 17:59:09:936
收到id為1810132623  2019-01-02 17:59:09: 第0條訊息: ccc // <1>
now time is: 2019-01-02 17:59:10:935
Not read data: 2019-01-02 17:59:10:981 // <2>
收到id為1810132623  2019-01-02 17:59:11: 第1條訊息: bbb
now time is: 2019-01-02 17:59:11:935
Not read data: 2019-01-02 17:59:12:470
now time is: 2019-01-02 17:59:12:935
id為1810132623的Clientsocket 2019-01-02 17:59:13:191讀取結束
now time is: 2019-01-02 17:59:13:935
id為1810132623的Clientsocket 2019-01-02 17:59:14:192讀取結束
複製程式碼

其中,Not read data輸出部分解決了我們的read阻塞問題,每隔1s會去喚醒我們的read操作,如果在1s內沒有讀到資料的話就會執行System.out.println("Not read data: " + stringNowTime()),在這裡我們就可以進行一些其他操作了,避免了阻塞中當前執行緒的現象,當我們有資料傳送之後,就有了<1>處的輸出了,因為read得到輸出,所以不再執行catch語句部分,因此你會發現<2>處輸出時間是和<1>處的時間相差1s而不是和之前的17:59:09:936相差一秒;

這樣的話,我們就解決了accept以及read帶來的阻塞問題了,同時在服務端為每一個客戶端都建立了一個執行緒來處理各自的業務邏輯,這點其實基本上已經解決了阻塞問題了,我們可以理解成是最初版的NIO,但是,為每個客戶端都建立一個執行緒這點確實讓人頭疼的,特別是客戶端多了的話,很浪費伺服器資源,再加上執行緒之間的切換開銷,更是雪上加霜,即使你引入了執行緒池技術來控制執行緒的個數,但是當客戶端多起來的時候會導致執行緒池的BlockingQueue佇列越來越大,那麼,這時候的NIO就可以為我們解決這個問題,它並不會為每個客戶端都建立一個執行緒,在服務端只有一個執行緒,會為每個客戶端建立一個通道。

對accept()一些程式碼注意點的思考

accept()本地方法,我們可以來試著看一看Linux這塊的相關解讀:

#include <sys/types.h>

#include <sys/socket.h>

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
複製程式碼

accept()系統呼叫主要用在基於連線的套接字型別,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所監聽套接字的等待連線佇列中第一個連線請求,建立一個新的套接字,並返回指向該套接字的檔案描述符。新建立的套接字不在監聽狀態,原來所監聽的套接字也不受該系統呼叫的影響。

備註:新建立的套接字準備傳送send()和接收資料recv()。

引數:

sockfd, 利用系統呼叫socket()建立的套接字描述符,通過bind()繫結到一個本地地址(一般為伺服器的套接字),並且通過listen()一直在監聽連線;

addr, 指向struct sockaddr的指標,該結構用通訊層伺服器對等套接字的地址(一般為客戶端地址)填寫,返回地址addr的確切格式由套接字的地址類別(比如TCP或UDP)決定;若addr為NULL,沒有有效地址填寫,這種情況下,addrlen也不使用,應該置為NULL;

備註:addr是個指向區域性資料結構sockaddr_in的指標,這就是要求接入的資訊本地的套接字(地址和指標)。

addrlen, 一個值結果引數,呼叫函式必須初始化為包含addr所指向結構大小的數值,函式返回時包含對等地址(一般為伺服器地址)的實際數值;

備註:addrlen是個區域性整形變數,設定為sizeof(struct sockaddr_in)。

如果佇列中沒有等待的連線,套接字也沒有被標記為Non-blocking,accept()會阻塞呼叫函式直到連線出現;如果套接字被標記為Non-blocking,佇列中也沒有等待的連線,accept()返回錯誤EAGAIN或EWOULDBLOCK。

備註:一般來說,實現時accept()為阻塞函式,當監聽socket呼叫accept()時,它先到自己的receive_buf中檢視是否有連線資料包;若有,把資料拷貝出來,刪掉接收到的資料包,建立新的socket與客戶發來的地址建立連線;若沒有,就阻塞等待;

為了在套接字中有到來的連線時得到通知,可以使用select()poll()。當嘗試建立新連線時,系統傳送一個可讀事件,然後呼叫accept()為該連線獲取套接字。另一種方法是,當套接字中有連線到來時設定套接字傳送SIGIO訊號。

返回值 成功時,返回非負整數,該整數是接收到套接字的描述符;出錯時,返回-1,相應地設定全域性變數errno。

所以,我們在我們的Java部分的原始碼裡(java.net.ServerSocket#accept)會new 一個Socket出來,方便連線後拿到的新Socket的檔案描述符的資訊給設定到我們new出來的這個Socket上來,這點在java.net.PlainSocketImpl#socketAccept中看到的尤為明顯,讀者可以回顧相關原始碼。

參考 :linux.die.net/man/2/accep…

相關文章