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

Posted 远洋号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 及以上的版本。

image

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

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 需要依赖该对象实现套接字操作。

  • closed 是否已经关闭套接字。

  • closeLock 关闭套接字时用的锁。

  • impl 真正的套接字实现对象。

  • oldImpl 是不是使用旧的实现。

主要方法

构造函数

ServerSocket(SocketImpl impl) {
       this.impl = impl;
       impl.setServerSocket(this);
   }
public ServerSocket() throws IOException {
       setImpl();
   }
public ServerSocket(int port) throws IOException {
       this(port, 50, null);
   }
public ServerSocket(int port, int backlog) throws IOException {
       this(port, backlog, 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;
       }
   }    

setImpl方法

设置套接字实现对象,这里提供了工厂模式可以方便的对接其他的实现,而默认是没有工厂对象的,所以模式的实现为 SocksSocketImpl 对象。

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

createImpl方法

该方法用于创建套接字实现对象,如果实现对象为空则先调用setImpl方法设置一下,接着调用套接字实现对象的create方法创建套接字。

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

create方法干了些啥?它的实现逻辑在 AbstractPlainSocketImpl 类中,这里会传入一个 boolean 类型的 stream 变量,这里其实用来标识是 udp 还是 tcp 协议,stream 即是流,tcp是基于连接的,自然存在流的抽象。而 udp 是非连接的非流的。

两类连接是通过 boolean 类型来标识的,true 为 tcp,false 为 udp,再通过 socketCreate 方法传入到本地实现中,在此之前两者都会创建 FileDescriptor 对象作为套接字的引用,FileDescriptor 为文件描述符,可以用来描述文件、套接字和资源等。另外,udp 协议时还会通过 ResourceManager.beforeUdpCreate()来统计虚拟机 udp 套接字数量,超过指定最大值则会抛出异常,默认值为25。最后将套接字的 created 标识设为 true,对应 Java 中抽象的客户端套接字 Socket 对象和服务端套接字 ServerSocket 对象。

protected synchronized void create(boolean stream) throws IOException {
       this.stream = stream;
       if (!stream) {
           ResourceManager.beforeUdpCreate();
           fd = new FileDescriptor();
           try {
               socketCreate(false);
           } catch (IOException ioe) {
               ResourceManager.afterUdpClose();
               fd = null;
               throw ioe;
           }
       } else {
           fd = new FileDescriptor();
           socketCreate(true);
       }
       if (socket != null)
           socket.setCreated();
       if (serverSocket != null)
           serverSocket.setCreated();
   }

往下看上面调用的socketCreate方法的逻辑,判断文件描述符不能为空,再调用本地socket0方法,最后将得到的句柄关联到文件描述符对象上。

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);
   }
static native int socket0(boolean stream, boolean v6Only) throws IOException;

接着看本地方法socket0的实现,逻辑为:

  1. 通过调用NET_Socket函数创建套接字句柄,其中通过 Winsock 库的 socket函数创建句柄,并且通过SetHandleInformation函数设置句柄的继承标志。这里可以看到根据 stream 标识对应的类别为SOCK_STREAMSOCK_DGRAM。如果句柄是无效的则抛出 create 异常。

  2. 然后通过setsockopt函数设置套接字的选项值,如果发生错误则抛出 create 异常。

  3. 最后再次通过SetHandleInformation设置句柄的继承标志,返回句柄。

JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
 (JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/)
{
   int fd, rv, opt=0;
   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;
}
int NET_Socket (int domain, int type, int protocol) {
   SOCKET sock;
   sock = socket (domain, type, protocol);
   if (sock != INVALID_SOCKET) {
       SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
   }
   return (int)sock;
}

bind方法

  1. 判断是否已关闭,关闭则抛SocketException("Socket is closed")

  2. 判断是否已绑定,绑定则抛SocketException("Already bound")

  3. 判断对象是否为 InetSocketAddress 类型,不是则抛IllegalArgumentException("Unsupported address type")

  4. backlog 如果小于1则设为50。

  5. 通过安全管理器检查端口。

  6. 通过套接字实现对象调用bindlisten方法。

  7. bound 标识设为 true。

public void bind(SocketAddress endpoint) throws IOException {
       bind(endpoint, 50);
   }
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;
       }
   }

套接字实现对象的bind方法会间接调用socketBind方法,逻辑如下:

  1. 获取本地文件描述符 nativefd。

  2. 调用bind0本地方法。

  3. 如果端口为0还会调用localPort0本地方法获取本地端口赋值给套接字实现对象的 localport 属性上,目的是获取操作系统动态生成的端口。

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;
   }
static native void bind0(int fd, InetAddress localAddress, int localport, boolean exclBind)
static native int localPort0(int fd) throws IOException
;

bind0本地方法逻辑如下,

  1. 通过NET_InetAddressToSockaddr函数将 Java 层的 InetAddress 对象的属性值填充到 SOCKETADDRESS 联合体中,对应的都是 Winsock 库的结构体,目的即是为了填充好它们。

typedef union {
   struct sockaddr     sa;
   struct sockaddr_in  sa4;
   struct sockaddr_in6 sa6;
} SOCKETADDRESS;
  1. NET_WinBind函数的逻辑是先根据 exclBind 标识看是否需要独占端口,如果需要则通过 Winsock 库的setsockopt函数设置SO_EXCLUSIVEADDRUSE选型,在 Java 层中决定独不独占端口可以通过sun.net.useExclusiveBind参数来配置,默认情况下是独占的。接着,通过操作系统的bind函数完成绑定操作。

  2. 如果绑定失败则抛异常。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0
 (JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port,
  jboolean exclBind)

{
   SOCKETADDRESS sa;
   int rv, sa_len = 0;
   if (NET_InetAddressToSockaddr(env, iaObj, port, &sa,
                                 &sa_len, JNI_TRUE) != 0) {
     return;
   }
   rv = NET_WinBind(fd, &sa, sa_len, exclBind);
   if (rv == SOCKET_ERROR)
       NET_ThrowNew(env, WSAGetLastError(), "NET_Bind");
}
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_localPort0
 (JNIEnv *env, jclass clazz, jint fd)
{
   SOCKETADDRESS sa;
   int len = sizeof(sa);
   if (getsockname(fd, &sa.sa, &len) == SOCKET_ERROR) {
       if (WSAGetLastError() == WSAENOTSOCK) {
           JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                   "Socket closed");
       } else {
           NET_ThrowNew(env, WSAGetLastError(), "getsockname failed");
       }
       return -1;
   }
   return (int) ntohs((u_short)GET_PORT(&sa));
}

套接字实现对象的listen方法会间接调用socketListen方法,逻辑比较简单,获取本地的文件描述符然后调用listen0本地方法。可以看到本地方法很简单,仅仅是调用了 Winsock 库的listen函数来完成监听操作。

void socketListen(int backlog) throws IOException {
       int nativefd = checkAndReturnNativeFD();
       listen0(nativefd, backlog);
   }
static native void listen0(int fd, int backlog) throws IOException;
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
 (JNIEnv *env, jclass clazz, jint fd, jint backlog)
{
   if (listen(fd, backlog) == SOCKET_ERROR) {
       NET_ThrowNew(env, WSAGetLastError(), "listen failed");
   }
}

accept方法

该方法用于接收套接字连接,套接字开启监听后会阻塞等待套接字连接,一旦有连接可接收了则通过该方法进行接收操作。逻辑为,

  1. 判断套接字是否已经关闭。

  2. 判断套接字是否已经绑定。

  3. 创建 Socket 对象,并调用implAccept方法,

  4. 返回 Socket 对象。

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

implAccept方法逻辑为,

  1. 传入的 Socket 对象里面的套接字实现如果为空,则通过setImpl方法设置套接字实现,如果非空就执行reset操作。

  2. 调用套接字实现对象的accept方法完成接收操作,做这一步是因为我们的 Socket 对象里面的 SocketImpl 对象还差操作系统底层的套接字对应的文件描述符。

  3. 调用安全管理器检查权限。

  4. 得到完整的 SocketImpl 对象,赋值给 Socket 对象,并且调用postAccept方法将 Socket 对象设置为已创建、已连接、已绑定。

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

套接字实现对象的accept方法主要调用如下的socketAccept方法,逻辑为,

  1. 获取操作系统的文件描述符。

  2. SocketImpl 对象为空则抛出NullPointerException("socket is null")

  3. 如果 timeout 小于等于0则直接调用本地accept0方法,一直阻塞。

  4. 反之,如果 timeout 大于0,即设置了超时,那么会先调用configureBlocking本地方法,该方法用于将指定套接字设置为非阻塞模式。接着调用waitForNewConnection本地方法,如果在超时时间内能获取到新的套接字,则调用accept0方法获取新套接字的句柄,获取成功后再次调用configureBlocking本地方法将新套接字设置为阻塞模式。最后,如果非阻塞模式失败了,则将原来的套接字设置会紫塞模式,这里使用了 finally,所以能保证就算发生异常也能被执行。

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) {
           newfd = accept0(nativefd, isaa);
       } else {
           configureBlocking(nativefd, false);
           try {
               waitForNewConnection(nativefd, timeout);
               newfd = accept0(nativefd, isaa);
               if (newfd != -1) {
                   configureBlocking(newfd, true);
               }
           } finally {
               configureBlocking(nativefd, true);
           }
       }
       fdAccess.set(s.fd, newfd);
       InetSocketAddress isa = isaa[0];
       s.port = isa.getPort();
       s.address = isa.getAddress();
       s.localport = localport;
   }

configureBlocking本地方法逻辑很简单,如下,核心就是通过调用 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");
   }
}

waitForNewConnection本地方法逻辑如下,核心是通过 Winsock 库的select函数来实现超时的功能,它会等待 timeout 时间看指定的文件描述符是否有活动,超时了的话则会返回0,此时向 Java 层抛出 SocketTimeoutException 异常。而如果返回了-1则表示套接字已经关闭了,抛出 SocketException 异常。如果返回-2则抛出 InterruptedIOException。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForNewConnection
 (JNIEnv *env, jclass clazz, jint fd, jint timeout)
{
   int rv;
   rv = NET_Timeout(fd, timeout);
   if (rv == 0) {
       JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                       "Accept timed out");
   } else if (rv == -1) {
       JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "socket closed");
   } else if (rv == -2) {
       JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                       "operation interrupted");
   }
}
JNIEXPORT int JNICALL
NET_Timeout(int fd, long timeout)
{
   int ret;
   fd_set tbl;
   struct timeval t;
   t.tv_sec = timeout / 1000;
   t.tv_usec = (timeout % 1000) * 1000;
   FD_ZERO(&tbl);
   FD_SET(fd, &tbl);
   ret = select (fd + 1, &tbl, 0, 0, &t);
   return ret;
}

accept0本地方法实现逻辑为,

  1. 通过C语言的memset函数将 SOCKETADDRESS 联合体对应的结构体内的值设置为0。

  2. 判断接收的套接字描述符是否无效,分别可能抛 InterruptedIOException 或 SocketException 异常。

  3. 通过SetHandleInformation函数设置句柄的继承标志。

  4. NET_SockaddrToInetAddress函数用于将得到的套接字转换成 Java 层的 InetAddress 对象。

  5. 将生成的 InetAddress 对象用于生成 Java 层的 InetSocketAddress 对象。

  6. 赋值给 Java 层的 InetSocketAddress 数组对象。

  7. 返回新接收的套接字的文件描述符。

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, &sa.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;
   }
   SetHandleInformation((HANDLE)(UINT_PTR)newfd, HANDLE_FLAG_INHERIT, 0);
   ia = NET_SockaddrToInetAddress(env, &sa, &port);
   isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port);
   (*env)->SetObjectArrayElement(env, isaa, 0, isa);
   return newfd;
}


-------------推荐阅读------------







------------------广告时间----------------

知识星球:远洋号


以上是关于JVM的ServerSocket是怎么实现的(上)的主要内容,如果未能解决你的问题,请参考以下文章

java实现聊天室是怎么做到的?

serversocket 类如何在同一个端口上服务多个客户端连接?

ServerSocket卡在accept的时候怎么正确关闭

了解 JVM和JVM内存结构(JVM运行时数据区)

java通过ServerSocket与Socket实现通信

java通过ServerSocket与Socket实现通信