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

2024-04-15 12:18

本文主要是介绍你真的理解java BIO/NIO的accept()方法了么?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近发现很多资料,包括官方文档针对JDK的ServerSocket类的accept()方法介绍都是错误或者模糊不清的,这篇文章希望能从更底层去挖掘accept()方法到底是起什么作用,理解用户写的服务器程序代码和操作系统内核究竟是如何完美配合的来共同完成一些基本的网络功能,从而为更好的学习Java的网络编程打下坚实的基础。

本文假定您已经理解了TCP连接建立过程、操作系统内核空间、用户空间、系统调用、动态链接库、JDK native方法的一些基本概念,如果不清楚,可以先搞清楚相关概念,不然本文会理解有些困难。

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

1. FD(File descriptor): 

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

2. Socket:

Linux中一切皆文件,socket也属于一种文件,所以每个socket也有一个FD。在内核中,socket有专门对应的结构体来保存相关信息。比如IP、端口、其他协议相关信息,读缓冲区、写缓冲区等。Socket的类型有很多种,比如在socket()的man page中,我们可以看到如下几种:

      SOCK_STREAMProvides sequenced, reliable, two-way, connection-basedbyte streams.  An out-of-band data transmission mechanismmay be supported.SOCK_DGRAMSupports datagrams (connectionless, unreliable messages ofa fixed maximum length).SOCK_SEQPACKETProvides a sequenced, reliable, two-way connection-baseddata transmission path for datagrams of fixed maximumlength; a consumer is required to read an entire packetwith each input system call.SOCK_RAWProvides raw network protocol access.SOCK_RDMProvides a reliable datagram layer that does not guaranteeordering.SOCK_PACKETObsolete and should not be used in new programs; seepacket(7).

其中对于我们常用于TCP连接的scoket,对应的类型就是SOCK_STREAM,他是面向连接,有序、可靠、双向的。而我们今天要介绍的就是TCP对应的SOCK_STREAM类型的socket,他又可以细分成两种:

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

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

也就是说客户端的连接请求(SYN,ACK)消息都会最终发到listening socket上,而TCP连接建立好之后,后续的客户端应用层发送的数据是会发给established scoket的。

有了上边基本概念之后,我蛮再来介绍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)NAMEaccept - accept a connection on a socketSYNOPSIS#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);DESCRIPTIONThe accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts thefirst 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 thelistening state.  The original socket sockfd is unaffected by this call.

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

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

	//Create socketsocket_desc = socket(AF_INET , SOCK_STREAM , 0);if (socket_desc == -1){printf("Could not create socket");}puts("Socket created");//Prepare the sockaddr_in structureserver.sin_family = AF_INET;server.sin_addr.s_addr = INADDR_ANY;server.sin_port = htons( 8888 );//Bindif( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0){//print the error messageperror("bind failed. Error");return 1;}puts("bind done");//Listenlisten(socket_desc , 3);//Accept and incoming connectionputs("Waiting for incoming connections...");c = sizeof(struct sockaddr_in);//accept connection from an incoming clientclient_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”字样了,表示这个established socket已经有归属了。

[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写的一个Server Socket demo。对于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 nametcp        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最后一列会从 - 变成17116/java,表示他又归属了。执行三次accept(),Recv-Q就是0了。这表示accept()确实从listening socket的receive queue中接收数据了,只不过接收到不是客户端发来的用于建立TCP连接的SYN,ACK包数据或者其他客户端发来的业务数据,而是一个FD,这个FD指向了其中一个操作系统已经为我们建立好的established socket。也就是说客户端与操作系统建立TCP连接的三次握手过程,不直接受用户应用程序accept()代码直接控制。那受间接控制么?答案是肯定的。比如服务端迟迟不执行accept(),或者执行accept()的频率完全没有客户端发送的连接请求快,这很有可能就导致Recv-Q满了,然后客户端再发SYN连接请求,就会直接被丢弃或者服务端发一个reset消息给客户端通知他重置连接。这个可以通过以下内核参数控制:

 /proc/sys/net/ipv4/tcp_abort_on_overflow

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

总结:

从上边的实验和分析,我们可以的出结论,当服务端程序建立listening socket之后,即使没有正在执行accept()方法,而是去干一些别的事情,这时也不会直接影响操作系统内核接收客户端建立连接请求并建立连接。accept()方法也不会直接控制连接的建立,他只是从listening socket的receive queue中读取一个已经建立好的established socket的FD,并返回给accept()方法的调用程序。这也是为什么java的accept()方法会返回一个Socket对象,因为该Socket对象里边就保存了accept()系统调用获取到的established socket的FD。

这篇关于你真的理解java BIO/NIO的accept()方法了么?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/905794

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Nginx设置连接超时并进行测试的方法步骤

《Nginx设置连接超时并进行测试的方法步骤》在高并发场景下,如果客户端与服务器的连接长时间未响应,会占用大量的系统资源,影响其他正常请求的处理效率,为了解决这个问题,可以通过设置Nginx的连接... 目录设置连接超时目的操作步骤测试连接超时测试方法:总结:设置连接超时目的设置客户端与服务器之间的连接

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程