转载

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

用于指定通信的协议类型,它的返回值为socket descriptor

函数定义为 int socket(int family,int type,int protocol),在 sys/socket.h中定义。

  • family:指定协议族,比如 AF_INET表示IPv4协议,AF_INET6表示IPv6协议
  • type:表明套接字类型,比如 SCOK_STREAM 表示字节流套接字,SCOK_DGRAM表示数据报套接字
  • protocol:表示某个协议类型的常量值,一般为0,表示对所有family和type的用系统默认值。IPROTO_TCP表示TCP协议,IPROTO_UDP表示UDP协议

connect

客户端用来建立与TCP服务器的连接,它的调用将激发TCP的三路握手,即会使当前套接字从CLOSED状态转移到SYN_SENT状态,若成功再转移到ESTABLISHED状态。只有连接建立或者出错才会返回。

connect失败则该套接字不可再用,必须关闭,想要重连接必须再调用socket

connect在那些情况下会出错?

  1. 客户端没有收到SYN的响应,返回ETIMEDOUT错误。

对于4.4BSD内核发送SYN,没有响应再等6s发送,无响应等24s,如果总共等了75s仍然没有就返回ETIMEDOUT错误

  1. 客户端收到SYN响应为RST,返回ECONNREFUESED错误。

这是种硬错误。收到RST可能是:没有服务器监听连接的端口;TCP想取消连接;TCP收到一个根本不存在的连接上的分节

  1. 路由器引发了‘destination unreachable’ ICMP错误。

这是种软错误

bind

将本地协议地址赋予一个套接字。

本地协议地址:比如 IPv4或IPv6地址与端口的组合

调用bind的端口和地址可以都指定或者都不指定,或者只指定一个。如果端口号不指定,内核会在bind被调用时选择一个临时的端口。

函数定义为 int bind(int sockfd,const struct *myaddr,socklen_t addrlen);第一个参数就是就是socket返回的套接字描述符,第二个参数是指向特定于协议的地址结构的指针,第三个是该地址结构的长度。由于地址结构是个常量,所以如果是内核指定端口,无法返回,所以要获取内核指定的临时端口,必须调用 getsockname 返回协议地址

listen

做两件事

  1. 指示内核应该接受指向此套接字的连接请求,对应TCP状态转移为套接字从CLOSED状态变成LISTEN状态
  2. 规定内核应该为相应套接字排队的最大连接个数

socket创建的套接字默认是用来主动发起请求的,即用来调用connect函数,listen则是将这个套接字变成被动套接字,用来接收请求

内核维护的监听套接字队列

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

backlog的同一个取值根据操作系统不同,实际的数目会有差别

  1. 未完成队列:由某个客户端发出的SYN包到达了服务器,而服务器正在等待完成相应的TCP三次握手的过程;
  2. 已完成的队列:每个已完成TCP三次握手的客户端对应的其中一项

三次握手正常完成的这项会从未完成连接对列移到已完成队列的队尾。当进程调用accept时,已完成队列的头部将返回给进程,如果已完成队列为空,进程将被投入睡眠,睡眠针对的是默认的阻塞模式,直到TCP在该队列中放入一项才唤醒。

当客户SYN到达时,如果队列是满的,TCP会忽略这个包,使得客户端会重传

accept

用于从已完成连接队列队头返回下一个已完成连接。如果accept成功,返回值是有内核自动生成的一个全新的描述符,代表与客户端建立的TCP连接。

一个服务器通常只创建一个监听套接字,他在这个服务的声明周期内一直存在。但是会为每个客户端的连接建立一个以连接套接字,对客户端的服务完成时,就关闭这个连接套接字

accept生成新的描述符处理已连接的请求过程

首先处于监听状态的服务器监听客户端发来的连接请求

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

第二步accept返回结果,连接被内核接受,新的套接字(connfd)创建

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

第三步并发服务器会调用fork,此时listenfd和connfd在父进程和子进程之间共享

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

最后父进程关闭已连接套接字,子进程关闭监听套接字,由子进程处理与客户端的连接,父进程则继续监听下一个客户端连接请求

一文告诉你java NIO底层用到的那些connect、bind、listen、accept、close

父进程中调用fork之前所打开的所有描述符在fork返回之后与子进程共享。

并发服务器

并发服务器的存在是不希望一个服务一个客户端过长时间,而导致整个服务器被单个客户端长期占用,Unix中编写并发服务器最简单的办法就是 fork一个子进程来服务每个客户,一般实现如下:

for(;;){
  connfd=Accept(listenfd,..)
// fork调用一次会返回两次。在子进程中返回值一次,返回值为0;在调用进程,即父进程,中返回一次,返回值为新建的子进程的进程ID;
  if((pid=Fork())==0){
      Close(listenfd); //子进程不监听,直接关闭
      doSomething(connfd); //处理客户端请求
      Close(connfd); //处理客户端请求完毕,关闭连接
      exit(0);
  }
  Close(connfd) //由子进程处理,父进程就可以断开连接
}
复制代码

父进程中关闭了新建立的连接,为什么子进程还能处理连接请求?

每个文件或套接字都有一个引用计数。在文件表中维护,它表示的是当前打开着的引用该文件或者套接字的描述符的个数。socket返回后与listenfd关联的文件表项的引用计数值为1,accept返回的connfd也是如此。fork之后,两个文件描述符在父子进程之间共享,因此引用计数均变成2,这样当父进程关闭connfd的时候,只是引用计数从2变成了1,而真正的资源清理和释放只有在变为0才发生。

close

用来关闭套接字,如果文件的引用计数此时恰好为0,就会发送FIN包,终止TCP连接。

如果想直接终止可以用shutdown

原文  https://juejin.im/post/5b5f06b76fb9a04fa259efa3
正文到此结束
Loading...