本文主要参考的原文:Know your TCP system call sequences
socket api可以参考我的博客:socket API 介绍
TCP DEMO:tcp demo
注意:本文主要是翻译文章,因此会存在翻译难懂或者翻译有问题的地方,请自行阅读原文更正
通俗的说,用户空间就是运行着用户编写的应用程序的虚拟内存空间。在32位的操作系统中,每个进程都有 4GB 独立的虚拟内存空间,而 0 ~ 3GB 的虚拟内存空间就是用户空间 。
内核空间就是运行着操作系统代码的虚拟内存空间,而 3GB ~ 4GB 的虚拟内存空间就是内核空间。
在Linux中(当然windows下也有用户态和内核态的区分,不过一般说用户态和内核态针对的事Linux系统而已),为了更好地保护内核空间,将程序运行空间分为内核空间和用户空间(也就是常说的内核态和用户态),它们分别运行在不同的级别上,逻辑上相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,他们只能在用户空间访问用户数据,调用用户空间的函数。
但是,在有些情况下,用户空间的进程需要的进程需要获得一定的系统服务(调用内核空间的程序),这时操作系统就必须调用系统为用户提供的“特殊接口”-系统调用规定用户进程进入内核空间的具体位置。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后在返回内核空间。这就涉及到了”系统调用”。
系统调用是操作系统提供给用户程序调用的一组“特殊”编程接口,用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。通常,我们可以将这组“特殊”的接口称之为内核API.
系统调用按照功能逻辑可以分为:进程控制,进程间通信,文件系统控制,系统控制,存储管理,网络管理,socket控制,用户管理等。
通常,在日常的软件开发中,系统调用不直接与程序员进行交互,它仅仅是一个软中断机制向内核提交请求以获得内核服务的接口。实际使用中程序员调用的通常是用户编程接口-API,然后在由用户编程接口API去通知内核完成相应的系统调用。
例如,获取进程号的API函数getpid()对应getpid系统调用。但并不是所有的函数都对应一个系统调用,有时,一个API函数会需要几个系统调用来共同完成函数的功能,甚至有一些API函数不需要相应的系统调用(因此它所完成的不是内核提同的服务)。
在Linux中用户编程接口(API)遵循了在UNIX中最流行的应用编程界面标准-POSIX标准。
这里博客的主要内容不是介绍linux系统的API,而主要介绍linux系统中涉及到网络API(也是基于POSIX标准的网络API),如果对socket API不熟悉的同学可以参考我的博客
为了方便后续的说明,这里先对涉及到的一些概念进行说明。
服务器与客户端的通信是通过五元组来区分的,五元组的元素通常是指源IP地址,源端口,目的IP地址,目的端口和传输层协议(TCP/UDP).五元组能够区分不同会话,并且对应的会话是唯一的。通过五元组我们就可以实现一次找到一次通信的来源和去处。
TCB是什么东西,关于TCB的资料不太多,TCB在整个TCP生命周期具体有什么作用,我也不是很清楚,先来看看TCB结构的定义。
struct tcb {
short tcb_state; /* TCP state TCP状态(11种,比如LISTEN状态) */
short tcb_ostate; /* output state */
short tcb_type; /* TCP type (SERVER, CLIENT) */
int tcb_mutex; /* tcb mutual exclusion */
short tcb_code; /* TCP code for next packet */
short tcb_flags; /* various TCB state flags */
short tcb_error; /* return error for user side */
//五元组信息
IPaddr tcb_rip; /* remote IP address */
u_short tcb_rport; /* remote TCP port */
IPaddr tcb_lip; /* local IP address */
u_short tcb_lport; /* local TCP port */
struct netif *tcb_pni; /* pointer to our interface */
tcpseq tcb_suna; /* send unacked */
tcpseq tcb_snext; /* send next */
tcpseq tcb_slast; /* sequence of FIN, if TCBF_SNDFIN */
u_long tcb_swindow; /* send window size (octets) */
tcpseq tcb_lwseq; /* sequence of last window update */
tcpseq tcb_lwack; /* ack seq of last window update */
u_int tcb_cwnd; /* congestion window size (octets) */
u_int tcb_ssthresh; /* slow start threshold (octets) */
u_int tcb_smss; /* send max segment size (octets) */
tcpseq tcb_iss; /* initial send sequence */
int tcb_srt; /* smoothed Round Trip Time */
int tcb_rtde; /* Round Trip deviation estimator */
int tcb_persist; /* persist timeout value */
int tcb_keep; /* keepalive timeout value */
int tcb_rexmt; /* retransmit timeout value */
int tcb_rexmtcount; /* number of rexmts sent */
tcpseq tcb_rnext; /* receive next */
tcpseq tcb_rupseq; /* receive urgent pointer */
tcpseq tcb_supseq; /* send urgent pointer */
int tcb_lqsize; /* listen queue size (SERVERs) */
int tcb_listenq; /* listen queue port (SERVERs) */
struct tcb *tcb_pptcb; /* pointer to parent TCB (for ACCEPT) */
int tcb_ocsem; /* open/close semaphore */
int tcb_dvnum; /* TCP slave pseudo device number */
int tcb_ssema; /* send semaphore */
u_char *tcb_sndbuf; /* send buffer */
u_int tcb_sbstart; /* start of valid data */
u_int tcb_sbcount; /* data character count */
u_int tcb_sbsize; /* send buffer size (bytes) */
int tcb_rsema; /* receive semaphore */
u_char *tcb_rcvbuf; /* receive buffer (circular) */
u_int tcb_rbstart; /* start of valid data */
u_int tcb_rbcount; /* data character count 接收数据长度 */
u_int tcb_rbsize; /* receive buffer size (bytes) 接收缓冲区大小 */
u_int tcb_rmss; /* receive max segment size */
tcpseq tcb_cwin; /* seq of currently advertised window 记录了当前窗口可接收的最大报文段序号 */
int tcb_rsegq; /* segment fragment queue */
tcpseq tcb_finseq; /* FIN sequence number, or 0 */
tcpseq tcb_pushseq; /* PUSH sequence number, or 0 */
};
TCB(Transmission Control Block,传输控制块),基本上网上对于这个说法如下:socket包含两个成分,一个是IP地址,一个是端口号。同一个设备可以对应一个IP端口,但不同的“水管”用不同的端口号区分开来,于是同一个设备发送给其他不同设备的信息就不会产生混乱。在同一时刻,设备可能会产生多种数据需要分发给不同的设备,为了确保数据能够正确分发,TCP用一种叫做TCB,也叫传输控制块的数据结构把发给不同设备的数据封装起来,我们可以把该结构看做是信封。一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作。
如果相对TCB控制块结构的每个参数都需要了解,那么可能需要参考更专业的书籍以及对TCP有更深入的学习才可以,不过可以tcb_state字段知道TCB具有整个TCP过程的整个生命周期,具体的来说就是从socket创建到socket的关闭整个过程,因此TCB的创建是在socket函数进行创建的,结束于close函数。随着时间的推移,TCB结构的各个参数也会发生变化。
在Linux系统中,一切皆文件,Socket也不例外,对socket的操作实际上也是对某种特殊文件的读写操作。只不过操作的文件在服务器和客户端各自拥有一个。
在介绍socket系统调用之前,先介绍一些内核API。
**fget_light()和fput_light():**轻量级的文件查找入口。多任务对同一个文件进行操作,所以需要对文件做引用计数。fget_light在当前进程的struct files_struct中根据所谓的用户空间文件描述符fd来获取文件描述符。另外,根据当前fs_struct是否被多各进程共享来判断是否需要对文件描述符进行加锁,并将加锁结果存到一个int中返回, fput_light则根据该结果来判断是否需要对文件描述符解锁。
fget_light()/fput_light是fget/fput的变形,不用考虑多进程共享同一个文件表而导致的竞争避免锁。
fget/fput:指在文件表的引用计数+1/-1
sockfd_lookup_light:根据fd找到相应的socket object(内核真正操作的对象)。
so_xxx: 内核相关socket操作接口。socket object操作协议栈的api入口。
in_pcballoc():分配内核内存,内存名字叫Internet protocol control block。
in_pcbbind():绑定IN_PCB到指定的地址,如果不指定地址,那么会寻找一个可用的端口进行绑定
in_pcblookup():指定的端口是否可用。
sbappend():追加数据到发送缓冲区。
so->so_proto->pr_usrreq: socket object操作协议栈的函数
tcp_ursreq():是tcp 协议栈操作的入口函数,支持以下操作类型:PRU_ATTACH,PRU_BIND,PRU_LISTEN, PRU_ACCEPT, PRU_CONNECT, PRU_SHUTDOWN,PRU_ABORT, PRU_DETACH,PRU_SEND,PRU_SENDOOB,PRU_RCVD,PRU_RCVOOB
tcp_newtcpcb():TCP control block被分配,socket描述符指向的正是这个TCP control block。
tcp_attach().
tcp_xxx: tcp_close(), tcp_disconect(),tcp_drop()
pr_xxx: 一套socket层和协议栈通信的接口,包括pr_usrreq(),pr_input(),pr_output(),pr_ctlinput(),pr_ctloutput()。
上图显示了 TCP 系统调用在物理链路上发出之前进行传播的各个层。,我们一些列的API操作都只是在用户态(Process)进行,套接字层(Socket layer)接收进行的任何TCP系统调用,验证 TCP 应用程序传递的参数的正确性。这是一个独立于协议的层,因为尚未将协议连接到调用中。
套接字层下面是协议层(Protocol layer),该层包含协议的实际实现(本例中为 TCP)。当套接字层对协议层进行调用时,将确保对两个层之间共享的数据结构具有独占访问权限。这样做是为了避免任何数据结构损坏。
各种网络设备驱动程序在接口层(Interface layer)运行,该层从物理链路接收数据,并向物理链路传输数据。
每个套接字都有一个套接字队列,每个接口都有一个用于数据通信的接口队列。然而,整个协议层只有一个协议队列,称为IP输入队列。接口层通过这个IP输入队列向协议层输入数据。协议层使用各自的接口队列向接口输出数据。
在客户端没有连接到服务器之前,服务器需要做一些初始化工作,以便能够监听客户端的连接,主要包括创建socket,将socket绑定到固定的端口和地址(可选,可以绑定到本机的任意地址),最后监听客户端的连接,涉及到的用户编程接口API包括socket,bind,listen.
在我们调用socket 编程接口API之后,是在内核创建了一个socket对象,并返回对象的引用fd,通过这个fd我们可以操作这个socket。
socket在系统调用的表现形式如下:
socket (struct proc *p, struct socket_args *uap, int retval)
struct sock_args
{
int domain,
int type,
int protocol;
};
p是一个指针,指向进行套接字调用的进程的proc结构。
uap是指向socket_args结构的指针,该结构包含在socket系统调用中传递给进程的参数。
retval是系统调用的返回值。
socket系统调用通过分配一个新的描述符来创建一个新的socket。新描述符将返回给调用进程。任何后续的系统调用都用创建的套接字标识。socket系统调用还将协议分配给创建的套接字描述符。
domain、type和protocol参数值指定要分配给所创建套接字的族、类型和协议(即我们调用socket API传递的参数)。下图显示了socket系统调用的过程。
一旦从进程中检索到参数,socket函数就会调用socreate函数。socreate函数根据进程指定的参数查找指向协议切换protsw结构的指针。然后,socreate函数分配一个新的套接字结构。然后进行协议特定的调用 pr_usrreq,进而切换到与套接字描述符关联的相应协议特定的请求。pr_usrreq 函数的原型为:
int pr_usrreq(struct socket ∗so , int req, struct mbuf ∗m0 , ∗m1 , ∗m2);
在 pr_usrreq 函数中:
so是指向套接字结构的指针。
req的功能是标识请求。本例中为 PRU_ATTACH。
m0、m1和m2是指向mbuf结构的指针。值因请求而异。
pr_usrreq功能为大约16个请求提供服务。
tcp_usrreq()函数调用了tcp_attach( ),以处理PRU_ATTACH请求。为了分配Internet协议控制块(Internet protocol control block),需要调用in_pcballoc()。在in_pcballoc()中,调用了内核的内存分配器函数,该函数将内存分配给Internet控制块。完成所有必要的Internet控制块结构指针初始化之后,该控制返回到tcp_attach()。
分配新的TCP控制块(TCB),并调用tcp_newtcpcb()进行初始化。它还初始化所有TCP定时器变量,控制返回给tcp_attach()。套接字状态初始化为CLOSED。,tcp_usrreq函数返回时,套接字描述符将指向套接字的tcp控制块(TCB)。
Internet控制块是双向的循环链表,其指针指向套接字结构,同时套接字结构的so_pcb字段指向Internet控制块结构。Internet控制块还具有指向TCP控制块的指针。
bind的系统调用如下,其中的uap参数即是我们通过bind API传递给内核层的参数。
bind (struct proc ∗p, struct bind_args ∗uap, int ∗retval)
struct bind_args
{ int s;
caddr_t name;
int namelen;
};
s是套接字描述符。
name是指向包含网络传输地址的缓冲区的指针。
namelen是缓冲区的大小。
bind系统调用将本地网络传输地址与套接字相关联。对于客户端进程,不需要强制bind调用。当客户端进程发出connect系统调用时,内核负责执行隐式绑定。服务器进程在接受连接或开始与客户端通信之前,通常需要发出显式bind请求。
bind 调用将进程指定的本地地址复制到 mbuf,并调用 sobind,后者则根据请求使用 PRU_BIND 调用 tcp_usrreq()。tcp_usrreq() 中的切换实例调用 in_pcbbind(),后者将本地地址和端口号绑定到套接字。in_pcbbind 函数首先执行一些完整性检查,以确保不绑定套接字两次,并且保证其绑定了一个地址(存在多网卡的情况)。in_pcbbind 负责隐式和显式绑定。
如果调用in_pcbbind()中的第二个参数(指向sockaddr_in结构的指针)非空,则会发生显式绑定。否则会发生隐式绑定。在显式绑定的情况下,将对绑定的IP地址执行检查,并相应地设置套接字选项。
bind系统调用的流程如下:
如果指定的本地端口为非零值,则如果绑定位于保留端口上(例如,根据Berkley约定,端口号<1024),则会检查超级用户权限。然后调用in_pcblookup()来查找具有所述本地IP地址和本地端口号的控制块。in_pcblookup()验证本地地址和端口对是否尚未使用。如果in_pcbbind()中的第二个参数为NULL或本地端口为零,则通过检查找到临时端口(例如,根据Berkley约定,端口号>1024且<5000)。然后调用in_pcblookup()来验证找到的端口是否未使用。
listen (struct proc ∗p, struct listen_args ∗uap, int ∗retval)
struct listen_args
{ int s;
int backlog;
};
在listen系统调用中
s是套接字描述符。
backlog是套接字上连接数的队列限制。
listen 调用指示协议,服务器进程准备接受套接字上任何新传入的连接。存在一个可以排列的连接数限制,在该连接数之后,忽略任何进一步的连接请求。
关于backlog参数后面介绍完三次握手之后还会继续说明在不同系统中,这个参数的作用。
listen 系统调用使用套接字描述符和 listen 调用中指定的backlog 值调用 solisten。solisten 仅使用 PRU_LISTEN 作为请求调用 tcp_usrreq 函数。在 tcp_usrreq() 函数的switch语句中,PRU_LISTEN 的实例检查套接字是否绑定到端口。如果端口为零,则调用 in_pcbbind(),将套接字绑定到一个端口
如果端口上已经有侦听套接字,则该套接字的状态将更改为LISTEN。通常,所有服务器进程都会侦听一个已知的端口号。调用in_pcbbind为服务器进程执行隐式绑定是非常罕见的。listen系统调用流程如下。
在TCP握手的过程中,编程接口包括connect和accept接口,前者用户客户端连接到服务器,后者用于服务器接受客户端的连接,在具体介绍系统调用之前,先来讲一下三次握手,关于三次握手网上也有很多资料。我这里也是参考了大量的博客,个人觉得比较好的来讲解。
当服务器端创建好socket,并调用了bind和listen之后,服务器就处于监听状态,随时监听客户端的连接,握手过程如下图所示。
当客户端调用connect函数之后,服务器和客户端之间就开启了握手,在服务器端会维护2个连接队列,一个是半连接队列(又称为SYN 队列),另一个是全连接队列(又称为全连接队列)。第一次握手的时候,客户端会携带客户端的信息(地址和端口)以及服务器的信息(要连接的服务器地址和端口等)以及会发送一个同步序号(SYN)给服务器,这个序号告诉服务器需要使用这个序号+1来同我进行同步,服务器接着会创建一个socket(这个socket信息不完整,不能进行通信,不过该socket具有TCB控制块信息,能够存放TCP状态信息),并将这个socket信息放入到半连接队列,此时客户端的TCP状态为SYN_SET,服务器端的TCP状态为SYN_RECV,服务器随即给客户端发送一个ACK(作为对客户端请求的回应,序号值ACK=SYN+1,注意这个SYN为客户端发给服务器的SYN序号),并且服务器自身也会发送一个SYN序号给服务器,客户端收到ACK和SYN后,比对ACK是否自己发送的SYN+1,如果是,则代表这是对我连接的回应,此时客户端的状态为ESTABLISHED,这时候告知客户端可以接收到服务器的信息,然而由于在网络环境比较复杂的情况,客户端可能会连续发送多次请求。如果只设计成两次握手的情况,服务端只能一直接收请求,然后返回请求信息,也不知道客户端是否请求成功。这些过期请求的话就会造成网络连接的混乱,因此这时候客户端还需要发送第三次握手通知服务器,此时服务器端的TCP状态也处于ESTABLISHED。这样服务器与客户端就建立起了连接,三次握手完成的时候,服务器刚刚创建的socket信息就是一个完整的socket信息(能够和客户端进行通信),并且将该socket信息从半连接队列中移除,并加入到全连接队列中,这时候accept就会从全队列中取出一个socket信息,这个socket信息负责和客户端进行通信。
connect (struct proc ∗p, struct connect_args ∗uap, int ∗retval);
struct connect_args
{
int s;
caddr_t name;
int namelen;
};
s是套接字描述符。
name是指向服务器端IP/端口地址对的缓冲区的指针。
namelen是缓冲区的长度。
connect系统调用通常由客户端进程调用,以连接到服务器进程。如果客户端进程在启动连接之前没有显式发出bind系统调用,则本地套接字上的隐式绑定由堆栈负责。
connect系统调用将外部地址(连接请求需要发送到的地址)从进程复制到内核,并调用soconnect()。从soconnect()返回时,connect()函数会发出一个睡眠,直到协议层将其唤醒,这表明连接已建立(对于阻塞的fd而言,当三次握手的第二次握手完成时,connect就被唤醒)或套接字上出现了一些错误。soconnect()检查套接字的有效状态,并调用pr_usrreq(),请求时使PRU_CONNECT。
tcp_usrreq()函数中的switch case检查本地端口与套接字的绑定。如果套接字尚未绑定,则调用in_pcbbind(),执行隐式绑定。然后调用in_pcbconnect(),它获取到目的地的路由,找到必须输出数据包的接口,并验证connect()指定的外部套接字对(IP地址和端口号)是否唯一。然后,它用外部IP地址和端口号更新其Internet控制块,并返回到PRU_CONNECT case语句。
tcp_usrreq()调用soisconnecting(),它将客户端主机上套接字的状态设置为SYN_SENT。调用函数tcp_output,将SYN数据包输出到网络上。控制返回到connect()函数,该函数将一直休眠,直到协议层被唤醒——这表明连接现在已建立,或者套接字上出现了错误。
accept(struct proc ∗p, struct accept_args ∗uap, int ∗retval);
struct accept_args
{
int s;
caddr_t name;
int ∗anamelen;
};
在accpet系统调用中:
s是套接字描述符。
name是一个缓冲区(OUT参数),它包含外部主机的网络传输地址。
anamelen是名称缓冲区的大小。
accept系统调用是等待传入连接的阻塞调用。处理连接请求后,accept将返回一个新的套接字描述符。此新套接字已连接到客户端,而其他套接字仍处于侦听状态以接受进一步的连接。
accept系统调用过程如下:
accept调用首先验证参数,并等待连接请求到达。在此之前,该功能会在while循环中阻塞。一旦新连接到达,协议层就会唤醒服务器进程。Accept然后检查阻塞时可能发生的任何套接字错误。如果有任何套接字错误,该函数将返回,并通过从队列中拾取新连接并调用soaccept进一步进行操作。在soaccept()中调用tcp_usrreq()函数,请求为PRU_ACCEPT。tcp_usrreq函数中的可选系统调用in_setpeeraddr()可以从协议控制块复制外部IP地址和外部端口号,并将它们返回给服务器进程。
注意:在我们调用accept编程接口可以传递一个大于0的backlog参数,这个参数在不同系统下可能具有不同含义,在mac系统下,这个参数可能是半连接和全连接的总数,此时可以通过设置这个参数可以对DDOS攻击有一定的作用(因为这个参数的大小对半连接的数量有限制作用,半连接就是客户端和服务器的第一次连接),然而在linux系统下,这个参数代表全连接队列的最大数量,因此设置这个参数的大小,对于DDOS攻击无能为力,此时可以通过设置反向代理的方式来阻止DDOS攻击。
客户端与服务器建立连接后,服务器与客户端之间就可以进行数据的收发了,我们在最开始学习网络编程的时候,以为调用send/write接口,就是将数据从一端发送了给了另一端。或者调用recv/read接口就是请求从另一端获取数据,其实这2种想法都是错误的。
上图简单的表示了send,recv等读写接口都只作用于用户态,send的作用仅仅是将要发送的数据拷贝到内核态的发送缓冲区,内核具体什么时候发送缓冲区数据我们不知道,对于recv而言,我们也仅仅是将内核的接收缓冲区中拷贝数据到用户态。
注意:这里有很多知识点,比如零拷贝(sendfile)技术。这些面试的时候有的考官喜欢问。
sendmsg ( struct proc∗p, struct sendmsg_args ∗uap, int retval);
struct sendmsg_args
{
int s;
caddr_t msg;
int flags;
};
在send系统调用中
s是套接字描述符。
msg是指向msghdr结构的指针。
flags是控制信息。
有四个系统调用在n/w接口上发送数据:write、writev、sendto和sendmsg。本文只讨论sendmsg()系统调用。所有四个send调用最终都会调用sosend()。send(进程调用的库函数)、sendto和sendmsg系统调用只能在套接字描述符上操作,write和writev系统调用可以在任何类型的描述符上操作。
sendmsg系统调用流程如下:
sendmsg系统调用将要从进程发送的消息复制到内核空间,并调用sendit()。在sendit()中,初始化一个结构,将进程的输出收集到内核的内存缓冲区中。地址和控制信息也会从进程复制到内核。然后调用sosend(),它执行四项任务:
(1)根据sendit()函数传递的值初始化各种参数。
(2)验证套接字的条件和连接的状态,并确定传递消息和报告错误所需的空间。
(3)分配内存并从进程中复制数据。
(4)进行特定于协议的调用,将数据发送到网络。
然后调用 tcp_usrreq(),并根据进程指定的标志,控制切换到 PRU_SEND 或 PRU_SENDOOB(以发送带区外数据)。对于 PRU_SENDOOB,发送缓冲区大小可以超过 512 字节,将释放任何分配的内存并中断控制。否则,sbappend() 和 tcp_output() 函数由 PRU_SEND 和 PRU_SENDOOB 调用。
sbappend() 在发送缓冲区的末尾添加数据,并且 tcp_output() 将该段发送到接口。
recvmsg(struct proc *p, struct recvmsg_args *uap , int *retval);
struct recvmsg_args
{
int s,
struct msghdr *msg,
int flags,
};
在receive系统调用中:
s是套接字描述符。
msg是指向msghdr结构的指针。
flags指定控制信息。
有四个系统调用可用于从连接接收数据:read、readv、recvfrom和recvmsg。虽然recv(进程使用的库函数)、recvfrom和recvmsg只对套接字描述符进行操作,但read和readv可以对任何类型的描述符进行操作。所有读取系统调用最终都会调用soreceive()。
上图展示了recv的系统调用流程,recvmsg()和recvit()函数初始化各种数组和结构,以将接收到的数据从内核发送到进程。recvit()调用soreceive(),它将接收到的数据从套接字缓冲区传输到接收缓冲区进程。soreceive函数的作用是执行各种检查,例如:
(1)是否设置了MSG_OOB标志。
(2)进程是否正在尝试接收数据。
(3)是否应该在足够的数据到达之前阻塞。
(4)将读取的数据传输到进程。
(5)检查数据是否为带外数据或常规数据,并进行相应处理。
(6)数据接收完成时通知协议。
当设置MSG_OOB标志或数据接收完成时,soreceive()函数会发出依赖于协议的请求。在接收带外数据的情况下,协议层检查不同的条件,以验证接收到的数据是OOB,然后将其返回到套接字层。在后一种情况下,协议层调用tcp_output(),将窗口更新段发送到网络。这会通知另一端任何可用于接收数据的空间。
当服务器与客户端不在需要进行数据的收发时,可以使用close编程接口关闭socket,socket关闭需要经过四次挥手过程。
偷懒一下:四次挥手过程:TCP四次挥手过程
soo_close(struct file ∗fp , struct proc ∗p);
fp是指向文件结构的指针。
p是指向调用进程的proc结构的指针。
close系统调用关闭或中止套接字上任何挂起的连接。
soo_close()只调用so_close()函数,该函数首先检查要关闭的套接字是否是侦听套接字(接受传入连接的套接字)。如果是,则遍历两个套接字队列以检查是否存在任何挂起的连接。对于每个挂起的连接,都会调用soabort(),它会发出带有PRU_ABORT的tcp_usrreq()请求,可选的系统调用tcp_drop()会检查套接字的状态。
如果状态是 SYN_RCVD,则通过将状态设置为 CLOSED 并调用 tcp_output() 发送 RST 段。tcp_close() 函数然后关闭套接字。tcp_close 函数更新路由度量结构的三个变量,然后释放套接字持有的资源。
如果状态是 SYN_RCVD,则通过将状态设置为 CLOSED 并调用 tcp_output() 发送 RST 段。tcp_close() 函数然后关闭套接字。tcp_close 函数更新路由度量结构的三个变量,然后释放套接字持有的资源。
如果套接字不是侦听套接字,则控制开始使用 soclose(),以检查是否已存在附加到套接字的控制块。如果不存在,则 sofree() 释放套接字。如果存在,则调用具有 PRU_DETACH 的 tcp_usrreq() 将协议与套接字分离。PRU_DETACH 的切换实例调用 tcp_disconnect(),以检查连接状态是否为 ESTABLISHED。如果不是,则 tcp_disconnect() 调用 tcp_close(),以释放 Internet 控制块。否则,tcp_disconnect() 检查延迟时间和延迟套接字选项。如果设置了该选项,并且延迟时间为零,则调用 tcp_drop()。如果未设置,则调用 tcp_usrclosed(),以设置套接字的状态,并调用 tcp_output()(如果需要发送 FIN 段)。
close系统调用如下:
涉及到挥手过程的状态的一些问题
fin_wait1和last_ack状态不会存在太久,因为如果另一方没有发送ack回应时,到了一定时间会超时重传,但是fin_wait_2的时间可能会存在很长的时间,这是为什么呢?
出现这种问题的可能原因是服务器没有close(可能在调用close之前的业务比较耗时,这时候可以将这些业务放到其他线程或者将close放在业务之前),导致fin_wait_2会等待较长时间,这时候的解决方法可以设置一定的超时时间,如果没有收到服务器的FIN,客户端可以直接关闭进程.
为什么会出现CLOSING状态
CLOSING:这个状态是一个比较特殊的状态,也比较少见,正常情况下不会出现,但是当双方同时都作为主动的一方。