NIO 源碼分析(02-1) BIO 源碼分析

NIO 源碼分析(02-1) BIO 源碼分析java

Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)linux

1、BIO 最簡使用姿式

(1) JDK BIO 啓動服務典型場景編程

// 1. 綁定端口
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress((InetAddress) null, PROT), BACKLOG);

while (true) {
    // 2. 獲取客戶端請求的Socket,沒有請求就阻塞
    Socket socket = serverSocket.accept();
    // 3. 開啓一個線程執行客戶端的任務
    new Thread(new ServerHandler(socket)).start();
}

// 綁定端口,開啓服務
public void bind(SocketAddress endpoint, int backlog) throws IOException {
    getImpl().bind(epoint.getAddress(), epoint.getPort());
    getImpl().listen(backlog);
}

ok,代碼已經完成!!!下面咱們和 Linux 下的網絡編程進行對比。windows

(2) Linux BIO 啓動服務典型場景數組

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listenfd, BACKLOG);

socklen_t cliaddr_len = sizeof(client_addr);
int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);

對比 Linux 上網絡編程,咱們會發現 JDK Socket 的編程邏輯是如出一轍的。實時上也是這樣,JDK 網絡編程也沒有作不少事,主要仍是調用了 Linux 相關的函數。惟一的不一樣是 Linux 是面向過程程序,socket 函數返回的是一個句柄,bind 和 listen 都是對這個句柄的操做;而 JDK 是面向對象編程,new ServerSocket() 返回了一個對象,咱們能夠調用這個 serverSocket 對象的各類方法。安全

Linux NIO 系列(02) 阻塞式 IO 網絡編程網絡

2、ServerSocket 源碼分析

JDK 爲咱們提供了 ServerSocket 類做爲服務端套接字的實現,經過它可讓主機監聽某個端口而接收其餘端的請求,處理完後還能夠對請求端作出響應。它的內部真正實現是經過 SocketImpl 類來實現的,它提供了工廠模式,因此若是本身想要其餘的實現也能夠經過工廠模式來改變的。默認的實現類是 SocksSocketImpl,ServerSocket 和 Socket 只是一個門面模式。jvm

2.1 相關類圖

前面說到 ServerSocket 類真正的實現是經過 SocketImpl 類,因而能夠看到它使用了 SocketImpl 類,但因爲 windows 和 unix-like 系統有差別,而 windows 不一樣的版本也須要作不一樣的處理,因此兩類系統的類不盡相同。socket

SocketImpl類圖

說明:

  1. SocketImpl 類實現了 SocketOptions 接口,接着還派生出了一系列的子類,其中 AbstractPlainSocketImpl 是原始套接字的實現的一些抽象,而 PlainSocketImpl 類是一個代理類。

  2. windows 下 PlainSocketImpl 代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 兩種不一樣實現。存在兩種實現的緣由是一個用於處理 Windows Vista 如下的版本,另外一個用於處理 Windows Vista 及以上的版本。

  3. unix-like 不存在版本的問題,因此它直接由 PlainSocketImpl 類實現。

  4. 這兩類操做系統都還存在一個 SocksSocketImpl 類,它其實主要是實現了防火牆安全會話轉換協議,包括 SOCKS V4 和 V5 。

根據上面能夠看到其實對於不一樣系統就是須要作差別處理,基本都是大同小異,下面涉及到套接字實現均以 Windows Vista 及以上的版本爲例進行分析,即 DualStackPlainSocketImpl。

2.2 主要屬性

private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl;
private boolean oldImpl = false;
  • created 表示是否已經建立了 SocketImpl 對象,ServerSocket 須要依賴該對象實現套接字操做。
  • bound 是否已綁定地址和端口。
  • closed 是否已經關閉套接字。
  • closeLock 關閉套接字時用的鎖。
  • impl 真正的套接字實現對象。
  • oldImpl 是否是使用舊的實現。

下面咱們看一下 ServerSocket 的主要方法。

2.3 構造函數

有五類構造函數,能夠什麼參數都不傳,也能夠傳入 SocketImpl、端口、backlog 和地址等。主要看一下最後一個構造函數,setImpl 方法用於設置實現對象,而後檢查端口大小是否正確,檢查 backlog 小於 0 就讓它等於 50,最後進行端口和地址綁定操做。

public ServerSocket() throws IOException {
    setImpl();
}

總結: 在 new ServerSocket() 時會經過 setImpl 方法建立一個 SocketImpl 的實現類,以 window 下 DualStackPlainSocketImpl 爲例。時序圖以下:

ServerSocket建立時序圖

2.3.1 setImpl 方法

設置套接字實現對象,這裏提供了工廠模式能夠方便的對接其餘的實現,而默認是沒有工廠對象的,因此模式的實現爲 SocksSocketImpl 對象。

private void setImpl() {
    if (factory != null) {
        impl = factory.createSocketImpl();
        checkOldImpl();
    } else {
        impl = new SocksSocketImpl();
    }
    if (impl != null)
        impl.setServerSocket(this);
}

總結: 從上面的方法能夠看出構造方法只是建立了 SocksSocketImpl 對象,這些都只是 JDK 層面的東西,並沒直接建立網絡鏈接,bind 方法則會直接建立網絡鏈接。

2.4 bind 方法

該方法用於將套接字綁定到指定的地址和端口上,若是 SocketAddress 爲空,即表明地址和端口都不指定,此時系統會將套接字綁定到全部有效的本地地址,且動態生成一個端口。邏輯以下:

// 綁定端口,開啓服務
public void bind(SocketAddress endpoint, int backlog) throws IOException {
    // 省略...
    if (endpoint == null)
        endpoint = new InetSocketAddress(0);
    if (backlog < 1)
          backlog = 50;
    getImpl().bind(epoint.getAddress(), epoint.getPort());
    getImpl().listen(backlog);
}

總結: ServerSocket.bind 方法調用 impl.bind 和 impl.listen 建立網絡鏈接,下面咱們就重點分析這兩個方法都作了些什麼事。

注意: 地址是否爲空,爲空則建立一個 InetSocketAddress,默認是全部有效的本地地址,對應的爲
0.0.0.0,而端口默認爲0,由操做系統動態生成,backlog 若是小於 1 則設爲 50。

ServerSocket bind

2.4.1 socketCreate 方法

先看一下 createImpl 方法。

// ServerSocket
void createImpl() throws SocketException {
    if (impl == null)
        setImpl();
    try {
        impl.create(true);
        created = true;
    } catch (IOException e) {
        throw new SocketException(e.getMessage());
    }
}

總結: createImpl 將建立 socket 網絡套接字的任務直接委任給了對應的 impl 實現類,在這裏也就是 DualStackPlainSocketImpl。

// AbstractPlainSocketImpl
protected synchronized void create(boolean stream) throws IOException {
    this.stream = stream;
    if (!stream) {  // UDP
        ResourceManager.beforeUdpCreate();
        // only create the fd after we know we will be able to create the socket
        fd = new FileDescriptor();
        try {
            socketCreate(false);
        } catch (IOException ioe) {
            ResourceManager.afterUdpClose();
            fd = null;
            throw ioe;
        }
    } else {        // TCP
        fd = new FileDescriptor();
        socketCreate(true);
    }
    if (socket != null)
        socket.setCreated();
    if (serverSocket != null)
        serverSocket.setCreated();
}

總結: 在 AbstractPlainSocketImpl 抽象類中 create 方法對 UDP 和 TCP 協議分別作了處理,建立 socket 套接字的代碼就一句 socketCreate(true),由具體的實現類完成。下面咱們再看一下 DualStackPlainSocketImpl 是如何進行網絡鏈接的。

// DualStackPlainSocketImpl
void socketCreate(boolean stream) throws IOException {
    if (fd == null)
        throw new SocketException("Socket closed");
    int newfd = socket0(stream, false /*v6 Only*/);
    fdAccess.set(fd, newfd);
}

總結: socket0 是一個 native 方法,也就是上面 Linux 的 Socket 函數完成的。而後將返回的 Socket 句柄設置到 fd 對象中(FileDescriptor 是 JDK 對句柄的抽象)。

補充1:socket0 在 JVM 中的實現
// windows/native/java/net/DualStackPlainSocketImpl.c
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
  (JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
    int fd, rv, opt=0;

    // 最關鍵的一句代碼,怎麼樣,是否是和 Linux 網絡編程的 socket 函數如出一轍的
    fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
    if (fd == INVALID_SOCKET) {
        NET_ThrowNew(env, WSAGetLastError(), "create");
        return -1;
    }

    rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt));
    if (rv == SOCKET_ERROR) {
        NET_ThrowNew(env, WSAGetLastError(), "create");
    }

    SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE);
    return fd;
}

2.4.2 socketBind 方法

// AbstractPlainSocketImpl
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();
}

總結: 和 create 方法同樣 AbstractPlainSocketImpl 也把綁定端口 socketBind 交給子類 DualStackPlainSocketImpl 實現。

// DualStackPlainSocketImpl
void socketBind(InetAddress address, int port) throws IOException {
    int nativefd = checkAndReturnNativeFD();
    if (address == null)
        throw new NullPointerException("inet address argument is null.");

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

    this.address = address;
}

總結: bind0 也是一個 native 方法,下面看一下 Winidow 上的實現。

補充2:bind0 在 JVM 中的實現
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0
  (JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port,
   jboolean exclBind) {
    SOCKETADDRESS sa;
    int rv;
    int sa_len = sizeof(sa);

    if (NET_InetAddressToSockaddr(env, iaObj, port, (struct sockaddr *)&sa,
                                 &sa_len, JNI_TRUE) != 0) {
      return;
    }
    // 最關鍵的一句代碼,綁定端口
    rv = NET_WinBind(fd, (struct sockaddr *)&sa, sa_len, exclBind);

    if (rv == SOCKET_ERROR)
        NET_ThrowNew(env, WSAGetLastError(), "JVM_Bind");
}

2.4.3 socketListen 方法

// DualStackPlainSocketImpl
void socketListen(int backlog) throws IOException {
    int nativefd = checkAndReturnNativeFD();
    listen0(nativefd, backlog);
}
補充3:listen0 在 JVM 中的實現
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
  (JNIEnv *env, jclass clazz, jint fd, jint backlog) {
    // 關鍵的一句代碼 listen 啓動服務
    if (listen(fd, backlog) == SOCKET_ERROR) {
        NET_ThrowNew(env, WSAGetLastError(), "listen failed");
    }
}

2.5 accept 方法

accept方法

該方法用於接收套接字鏈接,套接字開啓監聽後會阻塞等待套接字鏈接,一旦有鏈接可接收了則經過該方法進行接收操做。

2.5.1 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;
}

總結: accept 作了三件事:一是判斷套接字是否已經關閉。 二是判斷套接字是否已經綁定。三是建立 Socket 對象,並調用 implAccept 接收鏈接。

2.5.2 ServerSocket.implAccept

protected final void implAccept(Socket s) throws IOException {
    // 1. 建立一個空的 Socket 對象,用於接收 Socket 鏈接
    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();

        // 2. 最關鍵的一步,接收請求
        getImpl().accept(si);

        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;
    }
    // 3. 調用 Socket.postAccept 通知鏈接已經完成
    s.impl = si;
    s.postAccept();
}

總結: ServerSocket.implAccept 調用邏輯以下:

  1. 傳入的 Socket 對象裏面的套接字實現若是爲空,則經過 setImpl 方法設置套接字實現,若是非空就執行 reset 操做。

  2. 調用套接字實現對象的 accept 方法完成接收操做,作這一步是由於咱們的 Socket 對象裏面的 SocketImpl 對象還差操做系統底層的套接字對應的文件描述符。

  3. 獲得完整的 SocketImpl 對象,賦值給 Socket 對象,而且調用 postAccept 方法將 Socket 對象設置爲已建立、已鏈接、已綁定。

2.5.3 AbstractPlainSocketImpl.accept

// AbstractPlainSocketImpl
protected void accept(SocketImpl s) throws IOException {
    acquireFD();        // fdUseCount++
    try {
        socketAccept(s);// 接收請求  
    } finally {
        releaseFD();    // fdUseCount--
    }
}

總結: accept 直接委託給 socketAccept 方法。

2.5.4 DualStackPlainSocketImpl.socketAccept

void socketAccept(SocketImpl s) throws IOException {
    // 1. 獲取操做系統的文件描述符。 
    int nativefd = checkAndReturnNativeFD();
    if (s == null)
        throw new NullPointerException("socket is null");

    int newfd = -1;
    InetSocketAddress[] isaa = new InetSocketAddress[1];
    // 2. timeout <= 0 則表示阻塞式接收鏈接
    if (timeout <= 0) {
        newfd = accept0(nativefd, isaa);
    // 3. 若是 timeout 大於0,即設置了超時,那麼會先非阻塞式接收鏈接
    } else {
        // 3.1 serverSocket 設置成非阻塞模式
        configureBlocking(nativefd, false);
        try {
            // 3.2 本地方法,阻塞 timeout 時長用於獲取新的 socket
            waitForNewConnection(nativefd, timeout);
            // 3.3 由於如今是非阻塞的,無論有沒有鏈接都會立刻返回
            newfd = accept0(nativefd, isaa);
            if (newfd != -1) {
                // 3.4 新創建的 Socket 設置爲阻塞模式
                configureBlocking(newfd, true);
            }
        } finally {
            // 3.5 serverSocket 恢復爲阻塞模式
            configureBlocking(nativefd, true);
        }
    }
    
    // 4. 將獲取到的新文件描述符賦給 SocketImpl 對象,
    //    同時也將遠程端口、遠程地址、本地端口等都賦給它相關變量。
    fdAccess.set(s.fd, newfd);
    InetSocketAddress isa = isaa[0];
    s.port = isa.getPort();
    s.address = isa.getAddress();
    s.localport = localport;
}

總結: socketAccept 調用本地方法 accept0 接收鏈接。具體邏輯以下:

  1. 獲取操做系統的文件描述符。
  2. SocketImpl 對象爲空則拋出 NullPointerException("socket is null")。
  3. 若是 timeout 小於等於 0 則直接調用本地 accept0 方法,一直阻塞。
  4. 反之,若是 timeout 大於0,即設置了超時,那麼會先調用 configureBlocking 本地方法,該方法用於將指定套接字設置爲非阻塞模式。接着調用waitForNewConnection 本地方法,若是在超時時間內能獲取到新的套接字,則調用 accept0 方法獲取新套接字的句柄,獲取成功後再次調用 configureBlocking 本地方法將新套接字設置爲阻塞模式。最後,若是非阻塞模式失敗了,則將原來的套接字設置會紫塞模式,這裏使用了 finally,因此能保證就算髮生異常也能被執行。
  5. 最後將獲取到的新文件描述符賦給 SocketImpl 對象,同時也將遠程端口、遠程地址、本地端口等都賦給它相關變量。
補充4:configureBlocking 在 JVM 中的實現

本地方法邏輯很簡單,以下,核心就是經過調用 Winsock 庫的 ioctlsocket 函數來設置套接字爲阻塞仍是非阻塞,根據 blocking 標識。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_configureBlocking
  (JNIEnv *env, jclass clazz, jint fd, jboolean blocking) {
    u_long arg;
    int result;

    if (blocking == JNI_TRUE) {
        arg = SET_BLOCKING;    // 0
    } else {
        arg = SET_NONBLOCKING;   // 1
    }

    result = ioctlsocket(fd, FIONBIO, &arg);
    if (result == SOCKET_ERROR) {
        NET_ThrowNew(env, WSAGetLastError(), "configureBlocking");
    }
}
補充5:waitForNewConnection 在 JVM 中的實現

經過 Winsock 庫的 select 函數來實現超時的功能,它會等待 timeout 時間看指定的文件描述符是否有活動,超時了的話則會返回 0,此時向 Java 層拋出 SocketTimeoutException 異常。而若是返回了 -1 則表示套接字已經關閉了,拋出 SocketException 異常。若是返回-2則拋出 InterruptedIOException。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForConnect
  (JNIEnv *env, jclass clazz, jint fd, jint timeout) {
    int rv, retry;
    int optlen = sizeof(rv);
    fd_set wr, ex;
    struct timeval t;

    FD_ZERO(&wr);
    FD_ZERO(&ex);
    FD_SET(fd, &wr);
    FD_SET(fd, &ex);
    t.tv_sec = timeout / 1000;
    t.tv_usec = (timeout % 1000) * 1000;

    rv = select(fd+1, 0, &wr, &ex, &t);

    if (rv == 0) {
        JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                        "connect timed out");
        shutdown( fd, SD_BOTH );
        return;
    }

    if (!FD_ISSET(fd, &ex)) {
        return;         /* connection established */
    }

    for (retry=0; retry<3; retry++) {
        NET_GetSockOpt(fd, SOL_SOCKET, SO_ERROR,
                       (char*)&rv, &optlen);
        if (rv) {
            break;
        }
        Sleep(0);
    }

    if (rv == 0) {
        JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                        "Unable to establish connection");
    } else {
        NET_ThrowNew(env, rv, "connect");
    }
}
補充6:accept0 在 JVM 中的實現
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_accept0
  (JNIEnv *env, jclass clazz, jint fd, jobjectArray isaa) {
    int newfd, port=0;
    jobject isa;
    jobject ia;
    SOCKETADDRESS sa;
    int len = sizeof(sa);

    memset((char *)&sa, 0, len);
    newfd = accept(fd, (struct sockaddr *)&sa, &len);

    if (newfd == INVALID_SOCKET) {
        if (WSAGetLastError() == -2) {
            JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                            "operation interrupted");
        } else {
            JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                            "socket closed");
        }
        return -1;
    }

    ia = NET_SockaddrToInetAddress(env, (struct sockaddr *)&sa, &port);
    isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port);
    (*env)->SetObjectArrayElement(env, isaa, 0, isa);

    return newfd;
}

總結: 本地方法 accept0 實現邏輯:

  1. 經過 C 語言的 memset 函數將 SOCKETADDRESS 聯合體對應的結構體內的值設置爲 0。
  2. 經過 Winsock 庫的 accept 函數獲取套接字地址。
  3. 判斷接收的套接字描述符是否無效,分別可能拋 InterruptedIOException 或 SocketException 異常。
  4. 經過 SetHandleInformation 函數設置句柄的繼承標誌。
  5. NET_SockaddrToInetAddress 函數用於將獲得的套接字轉換成 Java 層的 InetAddress 對象。
  6. 將生成的 InetAddress 對象用於生成 Java 層的 InetSocketAddress 對象。
  7. 賦值給 Java 層的 InetSocketAddress 數組對象。
  8. 返回新接收的套接字的文件描述符。

2.6 總結

能夠看到 ServerSocket 的核心方法都是 native 方法,是由 JVM 調用 linux 的內核函數完成的。想要對 Socket 網絡編程有更詳細的瞭解就必須進一步瞭解 Linux 網絡編程

參考:

  1. JVM的ServerSocket是怎麼實現的(上)

天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索