是时候了解Java Socket底层实现了

在Java中,提供了一系列Socket API,可以轻松建立两个主机之间的连接、读取数据,那底层到底怎么实现,很少人去关心。这其实最终还是通过调用操作系统提供得Socket接口完成(TCP/IP是由操作系统来实现)。

在这里不讨论TCP的三次握手四次挥手等,只讨论一下操作系统提供的接口,以及这些接口的使用,还有Java Socket底层是如何做的。

首先了解一下操作系统为我们提供的Socket编程接口。

拿Windows举例,提供了socket、bind、listen、accept、connect、send、recv等函数,如果了解过Socket编程的小伙伴应该一眼能看出这些函数是干什么得,如connect进行连接,send发送数据,recv接收数据。(鄙人有幸四年前研究过一些,后来转Java就在没怎么深入,但是现在看起来还倍感亲切)。。。

(并且在Linux中也同样有这一系列函数)

然后我们试着用这些函数去创建一个简单得通信程序,为了简单、易懂,将使用VB进行编程。(或者说,重新回忆一下当初的入门语言),并且,懂其他语言的人也能轻松把VB语言翻译成自己拿手的。

首先需要声明用到得一些函数,如下,这是非常痛苦的一点。

Private Type SOCKADDR
    sin_family As Integer
    sin_port As Integer
    sin_addr As Long
    sin_zero As String * 8
End Type


Private Declare Function socket Lib "ws2_32.dll" (ByVal af As Long, ByVal lType As Long, ByVal protocol As Long) As Long
Private Declare Function bind Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As SOCKADDR, ByVal namelen As Long) As Long
Private Declare Function listen Lib "ws2_32.dll" (ByVal s As Long, ByVal backlog As Long) As Long
Private Declare Function recv Lib "ws2_32.dll" (ByVal s As Long, ByVal buf As String, ByVal lLen As Long, ByVal flags As Long) As Long
Private Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As SOCKADDR, ByRef addrlen As Long) As Long
Private Declare Function send Lib "ws2_32.dll" (ByVal s As Long, ByVal buf As String, ByVal lLen As Long, ByVal flags As Long) As Long
Private Declare Function closesocket Lib "ws2_32.dll" (ByVal s As Long) As Long
Private Declare Function connect Lib "ws2_32.dll" (ByVal s As Long, ByRef name As SOCKADDR, ByVal namelen As Long) As Long

Private Const WS2API_DECNET_MAX As Long = 10

Private Const sockaddr_size = 16
Private Const WSA_DESCRIPTIONLEN = 256
Private Const WSA_DescriptionSize = WSA_DESCRIPTIONLEN + 1
Private Const WSA_SYS_STATUS_LEN = 128
Private Const WSA_SysStatusSize = WSA_SYS_STATUS_LEN + 1
Private Declare Function WSAGetLastError Lib "ws2_32.dll" () As Long
Private Type WSAData
    wVersion As Integer
    wHighVersion As Integer
    szDescription As String * WSA_DescriptionSize
    szSystemStatus As String * WSA_SysStatusSize
    iMaxSockets As Integer
    iMaxUdpDg As Integer
    lpVendorInfo As Long
End Type
Private Declare Function WSAStartup Lib "ws2_32.dll" (ByVal wVersionRequired As Integer, ByRef lpWsAdata As WSAData) As Long
Private Declare Function WSACleanup Lib "ws2_32.dll" () As Long


Private Const AF_INET As Long = 2
Private Const SOCK_STREAM As Long = 1
Private Const IPPROTO_TCP As Long = 6
Private Declare Function inet_addr Lib "ws2_32.dll" (ByVal cp As String) As Long
Private Declare Function htons Lib "ws2_32.dll" (ByVal hostshort As Integer) As Integer

Private Const SOMAXCONN As Long = &H7FFFFFFF
Private Const SOCKET_ERROR As Long = -1

Private Const AF_INET6 As Long = 23

接下来我们创建一个服务端Socket,接受客户端请求,并回显一段字符。在Java中服务端得编写流程应该很清楚把1.创建ServerSocket,2.调用ServerSocket得bind()进行绑定,3.不断accept()等待并返回客户端Socket。

用Windows Api实现大概也是这个过程。先上一张简单的流程图。
是时候了解Java Socket底层实现了

0.WSAStartup

在调用API进行套接字编程前,必须调用WSAStartup函数对Winsock服务的初始化,否则后续API调用都会失败。
参数一是指定加载的winsock版本号,高字节是次要版本,低字节是主版本,可以通过MAKEWORD(l,h)来指定。但是VB中没有这个,需要自己写一个。

Private Function MakeWord(ByVal bLow As Byte, ByVal bHigh As Byte) As Integer
    MakeWord = bLow + bHigh * 256
End Function

lpWSAData:指向LPWSADATA结构的指针,该参数返回最终加载动态库的相关信息。
是时候了解Java Socket底层实现了

1.创建Socket

创建需要使用socket函数,它有三个参数,分别是:地址族或者协议族、socket类型、传输协议。
地址族:也就是IP地址类型,常用的有两种,AF_INET(IPv4)和AF_INET6(IPv6)。
socket类型:有SOCK_STREAM流格式套接字(面相连接)和SOCK_DGRAM数据报套接字(无连接),
传输协议:常用得有IPPROTO_TCP(TCP传输)和IPPROTO_UDP(UDP传输),如果为0,则根据上面设置的socket类型自动选择。
是时候了解Java Socket底层实现了
所以,创建一个面向连接的Socket代码如下,他的返回值也称之为套接字描述符。

Dim lpWsAdata As WSAData
WSAStartup(MakeWord(4, 4), lpWsAdata)
hSocket = socket(AF_INET, SOCK_STREAM, 0)

2.绑定

绑定也需要三个参数,分别为套接字描述符、sockaddr、sockaddr大小。第一个参数就不说了,是socket函数的返回值,第二个sockaddr是一个结构体,要绑定的信息在里面,赋值得时候需要用到htons函数和inet_addr进行转换(或者其他函数),如果端口直接写,则会失败。第三个参数可以通过len(vb)、sizeof(c)函数来获取。
是时候了解Java Socket底层实现了

 Dim lSocketAddress As SOCKADDR, hBind As Long
 lSocketAddress.sin_family = AF_INET
 lSocketAddress.sin_port = htons(2002)
 lSocketAddress.sin_addr = inet_addr("127.0.0.1")
 hBind = bind(hSocket, lSocketAddress, Len(lSocketAddress))

3.监听

listen函数让一个套接字处于监听连接请求的状态,调用它后,可以通过netstat 命令查看状态。如果不调用,后续accept会发生错误而导致直接返回。
是时候了解Java Socket底层实现了
参数有两个,分别代表套接字描述符,还是socket得返回值。第二个表示连接请求队列的最大长度,如果不断有新的请求进来,它们会按照先后顺序依次排队,直到这个队列满了,可以设置为SOMAXCONN,由系统来决定请求队列长度。

在通俗的说,假如服务端的队列此时大小是10,如果有10个人向服务端发起请求,而服务端暂时都没有调用accept。这时候在有其他的客户请求则会抛出异常,直到服务端调用accept从这个列队中取出一个,给后续腾出空间。
是时候了解Java Socket底层实现了
代码为:

 listen(hSocket, SOMAXCONN)

这个再Java中可能不需要手动调用,但是当我们调用serverSocket.bind()绑定时候,他紧接着就会调用listen方法,如果不指定backlog,则默认是50。
是时候了解Java Socket底层实现了

4.同意请求并返回数据

在java中accept()方法是阻塞的,直到有连接过来,同样accept函数也是阻塞式的,也就是在队列中没有连接时线程阻塞。accept也有三个参数,分别是socket描述符、第二个也就是存放连接请求的客户端地址,同样也是sockaddr结构体,第三个则是第二个参数大小。返回值是请求者的socket描述符。

send函数用来向socket中发送数据。参数分别是socket描述符、要发送数据得缓冲区、要发送数据的大小,最后一个一般为0。

  Dim lpAddR As SOCKADDR, hClientSocket As Long
  hClientSocket = accept(hSocket, lpAddR, LenB(lpAddR))
  
  Dim mBufData As String
  mBufData = "Hello Window Socket" + vbCrLf
  send hClientSocket, mBufData, LenB(mBufData), 0

上面得服务端基本就完成了,下面是客户端。
客户端的逻辑主要使用connect连接,并使用recv 进行接收数据,connect和服务端bind参数一样,就不说了,recv 的参数分别是socket描述符、接收数据存放得缓冲区、缓存区大小、最后一个一般也为0,recv成功时,返回值是接收数据的长度。

Dim lpWsAdata As WSAData

WSAStartup(MakeWord(4, 4), lpWsAdata)
Dim hSocket As Long
hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
If hSocket = 0 Then
    MsgBox WSAGetLastError
Else
   Dim lSocketAddress As SOCKADDR, mSocketConnectResult As Long
    lSocketAddress.sin_family = AF_INET
    lSocketAddress.sin_port = htons(2002)
    lSocketAddress.sin_addr = inet_addr("127.0.0.1")
    mSocketConnectResult = connect(hSocket, lSocketAddress, LenB(lSocketAddress))
    If mSocketConnectResult = 0 Then
        Dim sBuff As String * 255
        recv hSocket, sBuff, Len(sBuff), 0
        MsgBox sBuff
    Else
        MsgBox "连接错误"
    End If
End If

总体各个函数得参数都比较简单,下面运行一下。
服务端启动后再启动客户端,客户端会弹出接收到服务端发送的数据。
是时候了解Java Socket底层实现了
如果要把上述客户端转换为Java代码,也很简单。

  public static void main(String[] args) {
      try {
          Socket socket = new Socket("127.0.0.1", 2002);
          BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
          System.out.println("数据:"+bufferedReader.readLine());
          bufferedReader.close();
      } catch (IOException e) {
          e.printStackTrace();
      }
  }

当运行之后,效果如下
是时候了解Java Socket底层实现了

Java Socket 分析

当我们分析java底层到底调用什么方法时,往往发现都是些native方法,所以,我们需要一个openjdk源码,可以到http://hg.openjdk.java.net/下载。

在此之前,非常有必要清楚这张继承结构图。
是时候了解Java Socket底层实现了
从Java new一个Socket开始分析,看看底层做了哪些。

在空构造方法中直接调用了setImpl(),其中factory 只有在调用setSocketImplFactory后才会被赋值,所以先不管它,最重要要看SocksSocketImpl中做了哪些工作。

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

SocksSocketImpl构造方法中什么都没做,但是,这是表面得,别忘了我们类得初始化顺序,所以,我们需要看他得父类做了什么。

  SocksSocketImpl() {
      // Nothing needed
  }

在父类PlainSocketImpl中得静态代码快中判断了java运行环境版本和preferIPv4Stack,在官网对preferIPv4Stack的解释是:如果操作系统上有IPv6可用,则默认情况下,基础本机套接字将是一个IPv6套接字,该套接字使应用程序可以连接到IPv4和IPv6主机并接受来自它们的连接。但是,如果应用程序宁愿使用仅IPv4套接字,则可以将此属性设置为true。这意味着应用程序将无法与仅IPv6主机进行通信。他得默认值是false。
是时候了解Java Socket底层实现了
由静态代码块可得处useDualStackImpl值,我这里是true,则PlainSocketImpl中的impl实现类是DualStackPlainSocketImpl。

 PlainSocketImpl() {
     if (useDualStackImpl) {
         impl = new DualStackPlainSocketImpl(exclusiveBind);
     } else {
         impl = new TwoStacksPlainSocketImpl(exclusiveBind);
     }
 }

此时空构造方法大概就结束了,这似乎没做什么,当然,连接得逻辑在socket.connect()中。

省去前面的一些判断逻辑直接看重点。

public void connect(SocketAddress endpoint, int timeout) throws IOException {
	.....
    if (!created)
        createImpl(true);
    if (!oldImpl)
        impl.connect(epoint, timeout);
    else if (timeout == 0) {
        if (epoint.isUnresolved())
            impl.connect(addr.getHostName(), port);
        else
            impl.connect(addr, port);
    } else
        throw new UnsupportedOperationException("SocketImpl.connect(addr, timeout)"
    connected = true;
}

上面主要通过 impl.connect(addr.getHostName(), port);这句去连接,impl是哪个类的实例在构造方法中已经初始化了,是SocksSocketImpl类,但是我们不能忽略createImpl(true);这句,他用来创建一个Socket,这其中关键点在于 impl.create(stream),但是SocksSocketImpl没有重写create方法,所以我们要到他父类里面找。

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

他的父类PlainSocketImpl中又调用了 impl.create,而在上面我们已经知道impl实现类是谁了,以及如何决定。可DualStackPlainSocketImpl中也没有重写create方法,还需要往上走。

 protected synchronized void create(boolean stream) throws IOException {
     impl.create(stream);
     // set fd to delegate's fd to be compatible with older releases
     this.fd = impl.fd;
 }

所以到了AbstractPlainSocketImpl,他默认是流式的方式,关键点还是socketCreate,一个抽象方法,又必须交给之类实现

 protected synchronized void create(boolean stream) throws IOException {
     this.stream = stream;
     if (!stream) {
     	..........
     } else {
         fd = new FileDescriptor();
         socketCreate(true);
     }
	    ..........
 }
 abstract void socketCreate(boolean isServer) throws IOException;

于是我们要回到DualStackPlainSocketImpl中查看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中做了什么。它在DualStackPlainSocketImpl.c中实现。
从这里我们发现了几个关键点,AF_INET6、SOCK_STREAM、SOCK_DGRAM,这非常像我们开头创建socket的时候指定的参数。但是他调用了NET_Socket,所以,我们还要继续看NET_Socket方法又干了什么。

JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
  (JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
    int fd, rv, opt=0;
    //创建Socket
    fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
   。。。此处省略一些
    return fd;
}

我们通过跟踪,发现最终实现在net_util_md.c中,天哪,这不就是开头说的socket函数吗?并且他的参数和我们创建的方式一样。

socket已经算是底层了,在底层就是操作系统对socket的实现。

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

这不行啊,光看到了socket函数还不够,connect、listen等呢?

慢慢来,connect直接在DualStackPlainSocketImpl.c就能看到,也是由DualStackPlainSocketImpl中的socketConnect方法中调用,第一个参数就是socket的描述符。底层调用NET_InetAddressToSockaddr把InetAddress转换成SOCKETADDRESS,但是SOCKETADDRESS可不是windows提供的,是它自己定义的,这个结构中就包含了我们熟悉的sockaddr。

typedef union {
    struct sockaddr     sa;
    struct sockaddr_in  sa4;
    struct sockaddr_in6 sa6;
} SOCKETADDRESS;
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_connect0
  (JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port) {
    SOCKETADDRESS sa;
    int rv, sa_len = 0;
    if (NET_InetAddressToSockaddr(env, iaObj, port, &sa,
                                  &sa_len, JNI_TRUE) != 0) {
      return -1;
    }
    rv = connect(fd, &sa.sa, sa_len);
    if (rv == SOCKET_ERROR) {
        int err = WSAGetLastError();
        if (err == WSAEWOULDBLOCK) {
            return java_net_DualStackPlainSocketImpl_WOULDBLOCK;
        } else if (err == WSAEADDRNOTAVAIL) {
            JNU_ThrowByName(env, JNU_JAVANETPKG "ConnectException",
                "connect: Address is invalid on local machine, or port is not valid o
        } else {
            NET_ThrowNew(env, err, "connect");
        }
        return -1;  // return value not important.
    }
    return rv;
}

再看一下listen,同样在看到DualStackPlainSocketImpl.c中,又是熟悉的身影,熟悉的参数,但是listen0只有服务端Socket才会调用,也就是ServerSocket。

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

recv函数在SocketInputStream.c中。所以说,socket.getInputStream()返回值就是SocketInputStream。

这里面做了非常多的工作,但是我们还是要看recv。还是熟悉的身影,熟悉的参数,熟悉的0。

JNIEXPORT jint JNICALL
Java_java_net_SocketInputStream_socketRead0(JNIEnv *env, jobject this,
                                            jobject fdObj, jbyteArray data,
                                            jint off, jint len, jint timeout)
{
	。。。。。
 		nread = recv(fd, bufP, len, 0);
 	。。。。。
}

剩下的就不说了,如果调用Socket的有参构造方法,流程还是差不多的。这个就需要自己debug跟踪代码。

以上是对于Window,Linux下可能不同,下面是Linux的继承结构图,主要实现在PlainSocketImpl的一系列本地方法中。对应的c文件也就是PlainSocketImpl.c,
是时候了解Java Socket底层实现了
是时候了解Java Socket底层实现了

上一篇:给 DataGrid 的删除信息添加提示


下一篇:如何限制甘特图中的时标宽度?