• 是时候了解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实现大概也是这个过程。先上一张简单的流程图。

    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结构的指针,该参数返回最终加载动态库的相关信息。

    1.创建Socket

    创建需要使用socket函数,它有三个参数,分别是:地址族或者协议族、socket类型、传输协议。
    地址族:也就是IP地址类型,常用的有两种,AF_INET(IPv4)和AF_INET6(IPv6)。
    socket类型:有SOCK_STREAM流格式套接字(面相连接)和SOCK_DGRAM数据报套接字(无连接),
    传输协议:常用得有IPPROTO_TCP(TCP传输)和IPPROTO_UDP(UDP传输),如果为0,则根据上面设置的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)函数来获取。
    在这里插入图片描述

     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会发生错误而导致直接返回。
    在这里插入图片描述
    参数有两个,分别代表套接字描述符,还是socket得返回值。第二个表示连接请求队列的最大长度,如果不断有新的请求进来,它们会按照先后顺序依次排队,直到这个队列满了,可以设置为SOMAXCONN,由系统来决定请求队列长度。

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

     listen(hSocket, SOMAXCONN)
    

    这个再Java中可能不需要手动调用,但是当我们调用serverSocket.bind()绑定时候,他紧接着就会调用listen方法,如果不指定backlog,则默认是50。
    在这里插入图片描述

    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代码,也很简单。

      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底层到底调用什么方法时,往往发现都是些native方法,所以,我们需要一个openjdk源码,可以到http://hg.openjdk.java.net/下载。

    在此之前,非常有必要清楚这张继承结构图。
    在这里插入图片描述
    从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。
    在这里插入图片描述
    由静态代码块可得处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,
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    2月5号:linux运维shell
    linux命令go on
    2月6日:linux下命令与查看方式
    AHU_OJ 08
    AHU_OJ 57
    javascript学习笔记2
    javascipt学习笔记1
    javascript学习笔记4
    javascript学习笔记3
    未能加载文件或程序集“”或它的某一个依赖项。系统找不到指定的文件
  • 原文地址:https://www.cnblogs.com/HouXinLin/p/12560034.html
Copyright © 2020-2023  润新知