你真的理解java BIO/NIO的accept()方法了么?

介绍JDK的ServerSocket类的accept()方法之前,先介绍Linux操作系统的两个概念:

1. FD(File descriptor): 

文件描述符。在Linux操作系统,一切接文件,比如硬件,内存,socket,等等都是文件。每个文件在打开时,都对应一个FD,就是一个正整数。操作系统维护一个FD table,这个表有两个重要的列,一个是FD,另一个就是该文件对应的inode指针,而inode里边包含了大量的文件信息, 比如文件在磁盘的位置,当前文件读取/写入的指针,大小,最后修改时间等等。这样当应用程序提供FD给操作系统,操作系统就可以查找这张表定位对应的文件。

2. Socket:

套接字也是文件,所以每个socket也有一个FD。socket可以归为两类:

第一类是处于listening(监听状态)的socket,比如通过代码建立serverSocket,并绑定端口后,在底层操作系统就会为你建立一个socket,专门用来接收客户端发过来的连接请求。

第二类socket就是处于established(连接建立)状态的socket。当客户端与服务器连接建立成功之后,操作系统内核就会为你新建立一额新的established状态的socket。这种socket,有(目的ip, 目的port, 源ip, 源port)这样一个四元组。而第一类listening状态socket,里边只有一个(目的ip,目的port),并没有源ip+port的信息。

有了上边基本概念之后,我蛮再来介绍JDK的ServerSocket类的accept()方法

服务器程序执行accept()方法,会调用accept()系统调用,从listening socket的receive queue接收数据。JDK对accept()方法解释如下:

    * Listens for a connection to be made to this socket and accepts
     * it. The method blocks until a connection is made.
     *
     * <p>A new Socket {@code s} is created and, if there
     * is a security manager,
     * the security manager's {@code checkAccept} method is called
     * with {@code s.getInetAddress().getHostAddress()} and
     * {@code s.getPort()}
     * as its arguments to ensure the operation is allowed.
     * This could result in a SecurityException.

Linux的man page对Accept()系统调用的解释如下:

ACCEPT(2)                  Linux Programmer’s Manual                 ACCEPT(2)

NAME
       accept - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

DESCRIPTION
       The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts the
       first connection request on the queue of pending connections for the listening socket, sockfd, creates a  new  con-
       nected  socket, and returns a new file descriptor referring to that socket.  The newly created socket is not in the
       listening state.  The original socket sockfd is unaffected by this call.

这两段解释好像都认为客户端连接请求过来,连接是否能够建立,取决于accept方法是否在服务端被调用。但实际情况不是这样的。

比如如下c语言用来建立服务端监听程序代码,服务端在调用socket()、bind()、listen()方法后,在调用accept()方法之前操作系统内核就已经为该服务程序建立listening socket了,并且能够接收客户端连接请求,连接建立之后,也能新建一个socket。

	//Create socket
	socket_desc = socket(AF_INET , SOCK_STREAM , 0);
	if (socket_desc == -1)
	{
		printf("Could not create socket");
	}
	puts("Socket created");
	
	//Prepare the sockaddr_in structure
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons( 8888 );
	
	//Bind
	if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
	{
		//print the error message
		perror("bind failed. Error");
		return 1;
	}
	puts("bind done");
	
	//Listen
	listen(socket_desc , 3);
	
	//Accept and incoming connection
	puts("Waiting for incoming connections...");
	c = sizeof(struct sockaddr_in);
	
	//accept connection from an incoming client
	client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);

这个可以通过如下验证,让服务端程序在执行accept()方法之前打一个断点挺住,然后客户端用telnet工具telnet服务端ip + 端口,我这里用的是9000端口,用netstat -anlp 有如下输出,我们会发现已经有一个established的socket了,但是该socket的最后一个字段是 '-',就表示该socket操作系统还不知道属于哪个进程。当这时执行accept()方法,就会发现,该socket最后一列就有“17116/java”字样了。我这里用的是java写的一个Server Socket demo。

[root@test ~]# netstat -anlp |grep 9000
tcp        0      0 0.0.0.0:9000                0.0.0.0:*                   LISTEN      17116/java
tcp        2      0 10.11.13.145:9000         10.12.29.101:56459         ESTABLISHED -

对于java也是一样的,最简单的代码如下,这里看不到socket()、bind()、listen()方法,实际上他们都被封装到了ServerSocket()的构造方法里了,当执行ServerSocket serverSocket = new ServerSocket(9000); 时,这些方法都会被调用。课题通过查看jdk源码很容易看到。

	public static void main(String[] args) throws IOException {
		
		ServerSocket serverSocket = new ServerSocket(9000);
		while (true) {
			System.out.println("Wait for connection ...");
			Socket clientSocket = serverSocket.accept();

总结:

 也就是说在执行socket()、bind()、listen()方法之后,accept()方法之前,操作系统已经可以接收客户端连接请求,建立新的连接。accept()方法不会直接控制客户端连接请求是否能建立成功。这证明了Linux 的man page和jdk的帮助文档写的是不准确的。那accept()方法到底是做什么用的呢?

我们可以做另外一个实验,还是上边的java程序,让在accept();方法上打断点,当程序执行到accept()方法时挺住,这时accept()方法还没有执行。然后我们同时打开三个telnet,来连接这个服务的9000端口,我们会看到如下输出, 我们可以看到listening socket的Recv-Q的大小是3,而操作系统为我们建的三个established socket,都还没有归属(最后一列表示该socket所属的进程,这里显示的是 - )。

[root@test ~]# netstat -anlp |grep 9000
Proto Recv-Q Send-Q Local Address               Foreign Address             State       PID/Program name

tcp        3      0 0.0.0.0:9000                0.0.0.0:*                   LISTEN      17116/java
tcp        0      0 10.11.13.145:9000         10.12.29.101:56459         ESTABLISHED -
tcp        0      0 10.11.13.145:9000         10.12.29.101:56460         ESTABLISHED -
tcp        0      0 10.11.13.145:9000         10.12.29.101:56461         ESTABLISHED -

这时我们让程序执行一遍accept()方法,这时listening socket()的Recv-Q就编程了2,其中一个established socket后边会加上PID。执行三次accept(),Recv-Q就是0了。这表示accept()确实从listening socket的receive queue中接收数据了,只不过接收到不是客户端发来的用于建立TCP连接的SYN,ACK包数据或者其他客户端发来的业务数据,而是一个FD,这个FD指向了其中一个新建立的established socket。也就是说客户端与操作系统建立TCP连接的三次握手过程,不直接受用户应用程序代码控制。那首间接控制么?比如服务端迟迟不执行accept(),或者执行accept()的频率完全没有客户端发送的连接请求块,这很有可能就导致Recv-Q满了,然后客户端再发请求,就会直接被丢弃或者服务端发一个reset消息给客户端。这个可以通过以下内核参数控制:

 /proc/sys/net/ipv4/tcp_abort_on_overflow

值0表示服务端再listening socket的Recv-Q满之后,会直接丢弃客户端的SYN连接请求。值1表示,服务端会发一个Reset消息给客户端,通知客户端重置连接。

上一篇:SAP HUM 内向交货单凭证流和Relationship Browser


下一篇:threading模块