JVM的ServerSocket是怎麼實現的(下)

概況

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

繼承結構

--java.lang.Object
  --java.net.ServerSocket
複製代碼

相關類圖

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

下圖是 windows 的類圖關係, SocketImpl 類實現了 SocketOptions 接口,接着還派生出了一系列的子類,其中 AbstractPlainSocketImpl 是原始套接字的實現的一些抽象,而 PlainSocketImpl 類是一個代理類,由它代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 兩種不一樣實現。存在兩種實現的緣由是一個用於處理 Windows Vista 如下的版本,另外一個用於處理 Windows Vista 及以上的版本。 安全

image

比起 windows 的實現,unix-like 的實現則不會這麼繁瑣,它不存在版本的問題,因此它直接由 PlainSocketImpl 類實現,此外,能夠看到兩類操做系統都還存在一個 SocksSocketImpl 類,它其實主要是實現了防火牆安全會話轉換協議,包括 SOCKS V4 和 V5 。bash

image

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

類定義

public class ServerSocket implements java.io.Closeable
複製代碼

ServerSocket 類的聲明很簡單,實現了 Closeable 接口,該接口只有一個close方法。併發

主要屬性

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 是否是使用舊的實現。

主要方法

close方法

該方法用於關閉套接字,邏輯以下,機器學習

  1. 加鎖。
  2. 判斷若是已關閉則返回。
  3. 若是已建立則調用套接字實現對象的close方法。
  4. 將已關閉標識設爲 true。
public void close() throws IOException {
        synchronized(closeLock) {
            if (isClosed())
                return;
            if (created)
                impl.close();
            closed = true;
        }
    }
複製代碼

套接字實現對象的close方法邏輯爲,socket

  1. 加鎖。
  2. 判斷文件描述符是否爲空。
  3. 是否爲 UDP 協議,若是是的話經過ResourceManager.afterUdpClose()操做將 UDP 套接字計數器減一,前面說過 Java 是有控制 UDP 套接字數量的。
  4. 判斷是否有線程在使用文件描述符,fdUseCount 用於記錄多少線程在使用文件描述符,爲0時則沒有線程使用,此時判斷 closePending 是否爲 true,它表示是否已經在關閉了,若是已經在關閉則直接返回,沒有的話則將 closePending 設爲 true,標明該套接字已經在關閉了。接着再調socketPreClosesocketClose兩個方法完成關閉操做,而且將文件描述符設爲 null,最後返回。
  5. 若是有線程在使用該文件描述符,則將 closePending 設爲 true,fdUseCount 減一,再調用socketPreClose方法。
protected void close() throws IOException {
        synchronized(fdLock) {
            if (fd != null) {
                if (!stream) {
                    ResourceManager.afterUdpClose();
                }
                if (fdUseCount == 0) {
                    if (closePending) {
                        return;
                    }
                    closePending = true;
                    try {
                        socketPreClose();
                    } finally {
                        socketClose();
                    }
                    fd = null;
                    return;
                } else {
                    if (!closePending) {
                        closePending = true;
                        fdUseCount--;
                        socketPreClose();
                    }
                }
            }
        }
    }
複製代碼

socketPreClose方法調用了socketClose0方法,它的邏輯很簡單,判斷文件描述符爲空則拋出SocketException("Socket closed")異常,判斷文件描述符無效則直接返回,接着獲取本地文件描述符,經過調用close0本地方法完成關閉操做。分佈式

private void socketPreClose() throws IOException {
        socketClose0(true);
    }
    
void socketClose0(boolean useDeferredClose/*unused*/) throws IOException {
        if (fd == null)
            throw new SocketException("Socket closed");

        if (!fd.valid())
            return;

        final int nativefd = fdAccess.get(fd);
        fdAccess.set(fd, -1);
        close0(nativefd);
    }
複製代碼

close0本地方法以下,邏輯爲,ide

  1. 經過 Winsock 庫的getsockopt函數獲取 SO_LINGER 選項的值。
  2. 若是 linger 結構體的 l_onoff 爲0,則調用 Winsock 庫的WSASendDisconnect來啓動關閉鏈接操做,達到優雅關閉。
  3. 最後調用 Winsock 庫的closesocket函數進行關閉操做,這裏額外說下 SO_LINGER 選項,若是 linger 結構體的第一個元素爲0,此時表示關閉操做當即返回,操做系統接管套接字而且保證將全部數據發送給對端。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_close0
  (JNIEnv *env, jclass clazz, jint fd) {
     NET_SocketClose(fd);
}

JNIEXPORT int JNICALL
NET_SocketClose(int fd) {
    struct linger l = {0, 0};
    int ret = 0;
    int len = sizeof (l);
    if (getsockopt(fd, SOL_SOCKET, SO_LINGER, (char *)&l, &len) == 0) {
        if (l.l_onoff == 0) {
            WSASendDisconnect(fd, NULL);
        }
    }
    ret = closesocket (fd);
    return ret;
}
複製代碼

setOption方法

該方法用於設置套接字的選項,它經過套接字實現對象的setOption方法來設置,

public <T> ServerSocket setOption(SocketOption<T> name, T value)
        throws IOException
    {
        getImpl().setOption(name, value);
        return this;
    }
複製代碼

套接字實現對象的setOption方法實現以下,對不一樣的選項的合法性判斷,只有SO_KEEPALIVE SO_SNDBUF SO_RCVBUF SO_REUSEADDR SO_REUSEPORT SO_LINGER IP_TOS TCP_NODELAY 這些選項屬於 Java 支持的選項,而其餘選項則拋出不支持異常。最後會再調另一個setOption方法,其中選項參數值由 SocketOptions 接口定義。

protected <T> void setOption(SocketOption<T> name, T value) throws IOException {
        if (name == StandardSocketOptions.SO_KEEPALIVE &&
                (getSocket() != null)) {
            setOption(SocketOptions.SO_KEEPALIVE, value);
        } else if (name == StandardSocketOptions.SO_SNDBUF &&
                (getSocket() != null)) {
            setOption(SocketOptions.SO_SNDBUF, value);
        } else if (name == StandardSocketOptions.SO_RCVBUF) {
            setOption(SocketOptions.SO_RCVBUF, value);
        } else if (name == StandardSocketOptions.SO_REUSEADDR) {
            setOption(SocketOptions.SO_REUSEADDR, value);
        } else if (name == StandardSocketOptions.SO_REUSEPORT &&
            supportedOptions().contains(name)) {
            setOption(SocketOptions.SO_REUSEPORT, value);
        } else if (name == StandardSocketOptions.SO_LINGER &&
                (getSocket() != null)) {
            setOption(SocketOptions.SO_LINGER, value);
        } else if (name == StandardSocketOptions.IP_TOS) {
            setOption(SocketOptions.IP_TOS, value);
        } else if (name == StandardSocketOptions.TCP_NODELAY &&
                (getSocket() != null)) {
            setOption(SocketOptions.TCP_NODELAY, value);
        } else {
            throw new UnsupportedOperationException("unsupported option");
        }
    }
複製代碼

setOption方法邏輯以下,

  1. 判斷是否正在關閉,是的話拋SocketException("Socket Closed")異常。
  2. 若是是 SO_LINGER 則判斷該選項的值得合法性,而且若是是布爾類型則認爲關閉,由於要打開就必須設置一個整型數字。
  3. 若是是 SO_TIMEOUT 判斷其合法性並將其轉換爲整型。
  4. 其餘選項也作相似處理。
  5. 最後調socketSetOption方法。
public void setOption(int opt, Object val) throws SocketException {
        if (isClosedOrPending()) {
            throw new SocketException("Socket Closed");
        }
        boolean on = true;
        switch (opt) {
        case SO_LINGER:
            if (val == null || (!(val instanceof Integer) && !(val instanceof Boolean)))
                throw new SocketException("Bad parameter for option");
            if (val instanceof Boolean) {
                on = false;
            }
            break;
        case SO_TIMEOUT:
            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);
    }
複製代碼

繼續看socketSetOption方法,邏輯以下,

  1. 獲取本地的文件描述符。
  2. 若是是SO_TIMEOUT選項則直接返回,由於SO_TIMEOUT選項屬於 Java 層本身定義出來的,並不須要傳遞到操做系統中,因此只要在 Java 層進行維護便可。
  3. 若是是SO_REUSEPORT選項則直接拋出UnsupportedOperationException("unsupported option")異常,由於 windows 並無該選項。
  4. 若是是其餘的選項則將其值轉換成對應的類型,最後調用setIntOption本地方法。
void socketSetOption(int opt, boolean on, Object value)
        throws SocketException {
        int nativefd = checkAndReturnNativeFD();

        if (opt == SO_TIMEOUT) {  
            return;
        }
        if (opt == SO_REUSEPORT) {
            throw new UnsupportedOperationException("unsupported option");
        }

        int optionValue = 0;

        switch(opt) {
            case SO_REUSEADDR :
                if (exclusiveBind) {
                    isReuseAddress = on;
                    return;
                }
            case TCP_NODELAY :
            case SO_OOBINLINE :
            case SO_KEEPALIVE :
                optionValue = on ? 1 : 0;
                break;
            case SO_SNDBUF :
            case SO_RCVBUF :
            case IP_TOS :
                optionValue = ((Integer)value).intValue();
                break;
            case SO_LINGER :
                if (on) {
                    optionValue =  ((Integer)value).intValue();
                } else {
                    optionValue = -1;
                }
                break;
            default :/* shouldn't get here */ throw new SocketException("Option not supported"); } setIntOption(nativefd, opt, optionValue); } 複製代碼

setIntOption方法的邏輯主要是組裝好 Winsock 庫接口須要的數據結構,根據 Java 層對應的選項映射成本地對應的選項,接着經過NET_SetSockOpt函數設置該選項的值。

JNIEXPORT void JNICALL
Java_java_net_DualStackPlainSocketImpl_setIntOption
  (JNIEnv *env, jclass clazz, jint fd, jint cmd, jint value)
{
    int level = 0, opt = 0;
    struct linger linger = {0, 0};
    char *parg;
    int arglen;

    if (NET_MapSocketOption(cmd, &level, &opt) < 0) {
        JNU_ThrowByName(env, "java/net/SocketException", "Invalid option");
        return;
    }

    if (opt == java_net_SocketOptions_SO_LINGER) {
        parg = (char *)&linger;
        arglen = sizeof(linger);
        if (value >= 0) {
            linger.l_onoff = 1;
            linger.l_linger = (unsigned short)value;
        } else {
            linger.l_onoff = 0;
            linger.l_linger = 0;
        }
    } else {
        parg = (char *)&value;
        arglen = sizeof(value);
    }

    if (NET_SetSockOpt(fd, level, opt, parg, arglen) < 0) {
        NET_ThrowNew(env, WSAGetLastError(), "setsockopt");
    }
}
複製代碼

NET_SetSockOpt函數核心邏輯是調用 Winsock 庫的setsockopt函數對選項進行設置,另外,對於一些選項會作額外處理,好比當SO_REUSEADDR選項時,會先查詢操做系統的SO_EXCLUSIVEADDRUSE選項的值是否爲1,便是否開啓了獨佔地址功能,若是開啓了則不用進一步調用setsockopt函數而直接返回。

JNIEXPORT int JNICALL
NET_SetSockOpt(int s, int level, int optname, const void *optval,
               int optlen)
{
    int rv = 0;
    int parg = 0;
    int plen = sizeof(parg);

    if (level == IPPROTO_IP && optname == IP_TOS) {
        int *tos = (int *)optval;
        *tos &= (IPTOS_TOS_MASK | IPTOS_PREC_MASK);
    }

    if (optname == SO_REUSEADDR) {
        rv = NET_GetSockOpt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&parg, &plen);
        if (rv == 0 && parg == 1) {
            return rv;
        }
    }

    rv = setsockopt(s, level, optname, optval, optlen);

    if (rv == SOCKET_ERROR) {
        ...
    }

    return rv;
}
複製代碼

SocketOptions 接口在 Java 層定義瞭如下的選項,並非每一個選項名都在 Winsock 庫中有選項與之對應,但能對應上的選項的值都相同。

public interface SocketOptions {
    @Native public static final int TCP_NODELAY = 0x0001;
    @Native public static final int SO_BINDADDR = 0x000F;
    @Native public static final int SO_REUSEADDR = 0x04;
    @Native public static final int SO_REUSEPORT = 0x0E;
    @Native public static final int SO_BROADCAST = 0x0020;
    @Native public static final int IP_MULTICAST_IF = 0x10;
    @Native public static final int IP_MULTICAST_IF2 = 0x1f;
    @Native public static final int IP_TOS = 0x3;
    @Native public static final int SO_LINGER = 0x0080;
    @Native public static final int SO_TIMEOUT = 0x1006;
    @Native public static final int SO_SNDBUF = 0x1001;
    @Native public static final int SO_RCVBUF = 0x1002;
    @Native public static final int SO_KEEPALIVE = 0x0008;
    @Native public static final int SO_OOBINLINE = 0x1003;
}
複製代碼

Winsock 庫的相關的大部分選項的定義以下,好比TCP_NODELAY選項在 Java 層和 C/C++ 層的值是相同的。其餘選項也相似,在 Java 層能找到對應的選項則在本地也能找到與之對應的選項。

#define SO_DEBUG 0x0001 /* turn on debugging info recording */
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
#define SO_REUSEADDR 0x0004 /* allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_DONTROUTE 0x0010 /* just use interface addresses */
#define SO_BROADCAST 0x0020 /* permit sending of broadcast msgs */
#define SO_USELOOPBACK 0x0040 /* bypass hardware when possible */
#define SO_LINGER 0x0080 /* linger on close if data present */
#define SO_OOBINLINE 0x0100 /* leave received OOB data in line */
#define SO_SNDBUF 0x1001 /* send buffer size */
#define SO_RCVBUF 0x1002 /* receive buffer size */
#define SO_SNDLOWAT 0x1003 /* send low-water mark */
#define SO_RCVLOWAT 0x1004 /* receive low-water mark */
#define SO_SNDTIMEO 0x1005 /* send timeout */
#define SO_RCVTIMEO 0x1006 /* receive timeout */
#define SO_ERROR 0x1007 /* get error status and clear */
#define SO_TYPE 0x1008 /* get socket type */
#define SO_BSP_STATE 0x1009 /* get socket 5-tuple state*/
#define SO_GROUP_ID 0x2001 /* ID of a socket group*/
#define SO_GROUP_PRIORITY 0x2002 /* the relative priority within a group*/
#define SO_MAX_MSG_SIZE 0x2003 /* maximum message size*/
#define SO_CONDITIONAL_ACCEPT 0x3002 /* enable true conditional accept: connection is not ack-ed to the other side until conditional function returns CF_ACCEPT*/
#define SO_PAUSE_ACCEPT 0x3003 /* pause accepting new connections*/
#define SO_COMPARTMENT_ID 0x3004 /* get/set the compartment for a socket*/
#define WSK_SO_BASE 0x4000 /* */
#define TCP_NODELAY 0x0001 /* Options to use with [gs]etsockopt at the IPPROTO_TCP level.*/
複製代碼

setSoTimeout方法

該方法主要用於設置 ServerSocket 的 accept方法,也就是接收套接字鏈接的等待超時時間,與之對應的爲 SO_TIMEOUT 選項,它的單位是毫秒,一旦達到該超時時間則會拋出 SocketTimeoutException 異常,但該 ServerSocket 對象仍然是有效,也就是說若是捕獲到以上拋出的異常的話仍是能夠繼續使用它的。

public synchronized void setSoTimeout(int timeout) throws SocketException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        getImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
    }
複製代碼

套接字實現對象的setOption方法上面有詳細的講解,注意看到SO_TIMEOUT 的狀況,判斷設置的值必須爲整型,且將其轉換成整型並賦給 timeout 變量,由於SO_TIMEOUT選項屬於 Java 層本身定義出來的,並不須要傳遞到操做系統中,因此只要在 Java 層進行維護便可。最後調用的socketSetOption方法也是直接返回並不作什麼操做。

setReuseAddress方法

該方法能夠容許屢次綁定同個地址端口,它的做用主要是某個地址端口關閉後會有一段時間處於 TIME_WAIT 狀態,該狀態下可能不在容許套接字綁定該端口,必需要等到徹底關閉才容許再次綁定,經過設置該方法可讓其重複綁定。另外,該方法實際與sun.net.useExclusiveBind系統參數有緊密聯繫,默認狀況下該參數值爲 true,因此操做系統默認是使用了排他綁定的,這種狀況下,調用setReuseAddress方法不會真正去改變操做系統。

public void setReuseAddress(boolean on) throws SocketException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        getImpl().setOption(SocketOptions.SO_REUSEADDR, Boolean.valueOf(on));
    }
複製代碼

setReuseAddress方法調用套接字實現對象的setOption方法,該方法前面有詳細的講解,其中能夠看到case SO_REUSEADDR時將其值轉換成 boolean 值而後調用socketSetOption方法。

socketSetOption方法中,當case SO_REUSEADDR時能夠看到 exclusiveBind 爲 true 時則直接設置完標識就返回了,不會繼續作其餘操做,而這裏的 exclusiveBind 默認爲 true,能夠經過sun.net.useExclusiveBind參數來改變。

若是sun.net.useExclusiveBind參數設置爲 false,則會調用setIntOption本地方法,該函數會間接調用NET_SetSockOpt函數,主要邏輯是先判斷是否是已經設置了SO_EXCLUSIVEADDRUSE選項,若是設置了則無需再作操做了,直接返回。不然經過 Winsock 庫的setsockopt函數來設置SO_REUSEADDR選項。

toString方法

返回 ServerSocket 對象字符串,若是還沒綁定則返回ServerSocket[unbound],若是綁定了則根據安全管理器爲不爲空分別獲取回送地址或IP地址,最後返回形如ServerSocket[addr=xxx,localport=xxx]的字符串。

public String toString() {
        if (!isBound())
            return "ServerSocket[unbound]";
        InetAddress in;
        if (System.getSecurityManager() != null)
            in = InetAddress.getLoopbackAddress();
        else
            in = impl.getInetAddress();
        return "ServerSocket[addr=" + in +
                ",localport=" + impl.getLocalPort()  + "]";
    }
複製代碼

setReceiveBufferSize方法

該方法用於設置接收的緩衝區大小,設置後做爲 ServerSocket 接收到的套接字的接收緩衝區的默認值,默認值爲64K,在 ServerSocket 綁定以前設置才能生效。該方法主要邏輯是先判斷大小必須大於0且套接字不處於關閉狀態,而後調用套接字實現對象的setOption方法。

public synchronized void setReceiveBufferSize (int size) throws SocketException {
        if (!(size > 0)) {
            throw new IllegalArgumentException("negative receive size");
        }
        if (isClosed())
            throw new SocketException("Socket is closed");
        getImpl().setOption(SocketOptions.SO_RCVBUF, size);
    }
複製代碼

setOption方法前面有詳細講解,這裏再也不贅述。

-------------推薦閱讀------------

個人2017文章彙總——機器學習篇

個人2017文章彙總——Java及中間件

個人2017文章彙總——深度學習篇

個人2017文章彙總——JDK源碼篇

個人2017文章彙總——天然語言處理篇

個人2017文章彙總——Java併發篇

------------------廣告時間----------------

知識星球:遠洋號

公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。

爲何寫《Tomcat內核設計剖析》

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索