网络协议栈

网络协议栈

客户端调用connect函数发起连接,此时服务端调用完成listen函数,进入监听状态,这个过程可以类比客户和饭馆,客服先在饭店门口取用餐号(饭店进行记录),饭店通知客户进来用餐,客户进饭店用餐。在协议栈的实现中,当第一个syn发来时,将对应节点加入服务端的syn半连接队列,当最后一个ack发来时,从对应半连接状态中找到对应节点加入服务端的accept全连接队列。这个节点被称为tcb控制块

accept函数要做的是:从accept全连接队列A中取出一个节点并为其分配一个文件描述符fd

三次握手客户端在哪个函数?
connect

三次握手服务端在哪个函数?
并不是listen,listen只是将连接保存在半连接队列,此时三次握手没有完成。所以,服务器并没有发生在那个函数中而是在listen和accept之间完成的,服务端是被动完成的握手

如何在半连接队列中找到对应的tcb(tcp control block)节点?
tcp的每个过程都伴随着tcp头部字段,其中包含了五元组(sip,sport,dip,dport,proto),根据五元组是否相同筛选出位于半连接队列的的tcb控制块,将其移到全连接队列中

send返回一个正数,是不是发送成功了?
send函数仅仅只是将buffer中的数据从用户空间拷贝到内核空间,与内核空间是否发送没有关系

listenfd可不可以收发数据?
listenfd可以收发数据,位于三次握手阶段,用于接收syn与发ack

四次挥手问题

四次挥手不区分客户端还是服务端,只区分主动还是被动。主动方先发送FIN,被动方ACK确认,被动方发送FIN,主动发发送ACK确认

主动方与被动方同时处于established状态,主动方调用close函数进入fin_wait1状态,被动方调用recv函数返回0,并返回确认包使主动方进入fin_wait_2状态,被动方进入close_wait状态,被动方调用close函数发送设置fin标志的数据包进入last_ack状态,主动方收到带有fin标注的数据包计入time_wait状态,主动方回复确认包结束被动方last_ack状态

close()函数就是将FIN写入包结构中,如send() 后立即close() ,那么最后一个数据包中就包含了FIN位,对应的客户端会从FIN_WAIT_1状态直接尽然TIME_WAIT状态,close()仅仅只是关闭fd并发送FIN,会使tcb走向回收,但此时还没有被回收

如果出现大量close_wait如何解决?
说明客户端调用close()后服务端recv() == 0 由于业务原因没有及时调用close(),可以将业务和网络分离开,使close()及时被调用

有没有可能双方同时调用close()?

为什么会有time_wait状态?
TIME_WAIT状态存在的原因有两点:
可靠地终止TCP连接。
保证让迟来的TCP报文段有足够的时间被识别并丢弃。
第一个原因很好理解。假设用于确认服务器结束丢失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像TCP报文段7那样的确认报文段。 在Linux系统上,一个TCP端口不能被同时打开多次(两次及以 上)。当一个TCP连接处于TIME_WAIT状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在 TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的IP地址和端口号)。这 个新的、和原来相似的连接被称为原来的连接的化身(incarnation)。 新的化身可能接收到属于原来的连接的、携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该发生的。这就是TIME_WAIT状态存在的第二个原因。

tcp状态转移

CLOSED是一个假想的起始点,并不是一个实际的状态。

服务器通过listen系统调用进入LISTEN状态,被动等待客户端连接,因此执行的是所谓的被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中, 并向客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功地接收到客户端发送回的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向数据传输的状态。

当客户端主动关闭连接时(通过close或shutdown系统调用向服务器发送结束报文段),服务器通过返回确认报文段使连接进入 CLOSE_WAIT状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端 发送一个结束报文段来关闭连接。这将使连接转移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。

客户端通过connect系统调用主动与服务器建立连接。 connect系统调用首先给服务器发送一个同步报文段,使连接转移到 SYN_SENT状态。如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的连接所占用(见后文),则服务 器将给客户端发送一个复位报文段,connect调用失败。如果目标端口存在,但connect在超时时间内未收到服务器的确 认报文段,则connect调用失败。

connect调用失败将使连接立即返回到初始的CLOSED状态。如果客户端成功收到服务器的同步报文段和确认,则connect调用成功返 回,连接转移至ESTABLISHED状态。

当客户端执行主动关闭时,它将向服务器发送一个结束报文段, 同时连接进入FIN_WAIT_1状态。若此时客户端收到服务器专门用于确 认目的的确认报文段,则连接转移至 FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于 CLOSE_WAIT状态,这一对状态是可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入 TIME_WAIT状态。

状态转移图还给出了客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2状态),前提是处于FIN_WAIT_1 状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。

数据的传输

对于数据的发送,有三种情况

  1. 一次send()
  2. 连续send()
  3. send()发送大文件

send(fd , buf , len , 0)仅仅是将数据拷贝到fd所指向的tcb控制块的发送缓冲区中,对于发送是由协议栈自己决定什么时候发送,所以对于连续send()可能出现,两次send()的数据结合为一个数据包发送,也有可能连续send()导致一次send中的数据被分在两个数据包中,这就是所谓的分包和粘包

基于tcp的流式数据即数据顺序不会改变,就有了两种解决分包和粘包的方法

  1. 在应用层协议头中指定包长度
  2. 为每一个包加上分隔符

网线断了,连接会消失吗?
网卡会重启,协议栈会清空,再次连接网线时需要重新建立tcp连接

服务端进程崩溃,客户端会发生什么?
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。

服务端主机宕机后,客户端会发生什么?

当服务端的主机突然断电了,这种情况就是属于服务端主机宕机了。
当服务端的主机发生了宕机,是没办法和客户端进行四次挥手的,所以在服务端主机发生宕机的那一时刻,客户端是没办法立刻感知到服务端主机宕机了,只能在后续的数据交互中来感知服务端的连接已经不存在了。
因此,我们要分两种情况来讨论:

  • 服务端主机宕机后,客户端会发送数据;
  • 服务端主机宕机后,客户端一直不会发送数据;

服务端主机宕机后,如果客户端会发送数据

在服务端主机宕机后,客户端发送了数据报文,由于得不到响应,在等待一定时长后,客户端就会触发超时重传机制,重传未得到响应的数据报文。

当重传次数达到达到一定阈值后,内核就会判定出该 TCP 连接有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是客户端的 TCP 连接就会断开。

服务端主机宕机后,如果客户端一直不发数据

在服务端主机发送宕机后,如果客户端一直不发送数据,那么还得看是否开启了 TCP keepalive 机制 (TCP 保活机制)。

如果没有开启 TCP keepalive 机制,在服务端主机发送宕机后,如果客户端一直不发送数据,那么客户端的 TCP 连接将一直保持存在,所以我们可以得知一个点,在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。

而如果开启了 TCP keepalive 机制,在服务端主机发送宕机后,即使客户端一直不发送数据,在持续一段时间后,TCP 就会发送探测报文,探测服务端是否存活:

  • 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

所以,TCP keepalive 机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。

应用程序如果想使用 TCP 保活机制,需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

Nagle算法

Nagle算法主要用来预防小分组的产生。在广域网上,大量TCP小分组极有可能造成网络的拥塞。

Nagle是针对每一个TCP连接的。它要求一个TCP连接上最多只能有一个未被确认的小分组。在改分组的确认到达之前不能发送其他小分组。TCP会搜集这些小的分组,然后在之前小分组的确认到达后将刚才搜集的小分组合并发送出去。

有时候我们必须要关闭Nagle算法,特别是在一些对时延要求较高的交互式操作环境中,所有的小分组必须尽快发送出去。

我们可以通过编程取消Nagle算法,利用TCP_NODELAY选项来关闭Nagle算法

参考:
《Linux高性能服务器编程》
https://www.51cto.com/article/718024.html