JVM的ServerSocket是怎么实现的(下)

概况

JDK 为我们提供了 ServerSocket 类作为服务端套接字的实现,通过它可以让主机监听某个端口而接收其他端的请求,处理完后还可以对请求端做出响应。它的内部真正实现是通过 SocketImpl 类来实现的,它提供了工厂模式,所以如果自己想要其他的实现也可以通过工厂模式来改变的。

继承结构

--java.lang.Object
  --java.net.ServerSocket
复制代码

相关类图

前面说到 ServerSocket 类真正的实现是通过 SocketImpl 类,于是可以看到它使用了 SocketImpl 类,但由于 windows 和 unix-like 系统有差异,而 windows 不同的版本也需要做不同的处理,所以两类系统的类不尽相同。

下图是 windows 的类图关系, SocketImpl 类实现了 SocketOptions 接口,接着还派生出了一系列的子类,其中 AbstractPlainSocketImpl 是原始套接字的实现的一些抽象,而 PlainSocketImpl 类是一个代理类,由它代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 两种不同实现。存在两种实现的原因是一个用于处理 Windows Vista 以下的版本,另一个用于处理 Windows Vista 及以上的版本。

比起 windows 的实现,unix-like 的实现则不会这么繁琐,它不存在版本的问题,所以它直接由 PlainSocketImpl 类实现,此外,可以看到两类操作系统都还存在一个 SocksSocketImpl 类,它其实主要是实现了防火墙安全会话转换协议,包括 SOCKS V4 和 V5 。

根据上面可以看到其实对于不同系统就是需要做差异处理,基本都是大同小异,下面涉及到套接字实现均以 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方法逻辑为,

  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本地方法如下,逻辑为,

  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内核设计剖析》

欢迎关注:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值