TCP/IP 常见问题总结

1. 详细说明TCP协议状态转移过程

创建连接的状态转移过程:

服务器端TCP状态转移:

  1. CLOSED -> LISTEN 应用被动打开
  2. LISTEN -> SYN_RCVD 接受SYN;发送ACK
  3. SYN_RCVD -> ESTABLISED 接受ACK

客户端TCP的状态转移:

  1. CLOSED -> SYN_SENT 应用主动打开;发送SYN
  2. SYN_SENT -> ESTABLISED 接受SYN ACK; 发送ACK

断开连接TCP状态转移过程:

对主动关闭的一端来讲:

  1. ESTABLISED -> FIN_WAIT_1 应用调用close函数,发送FIN
  2. FIN_WAIT_1 -> FIN_WAIT_2 接受ACK
  3. FIN_WAIT_2 -> TIME_WAIT 接受FIN;发送ACK
  4. TIME_WAIT -> CLOSED 超过2MSL

对于被动关闭的一端来讲:

  1. ESTABLISED -> CLOSE_WAIT 接受FIN
  2. CLOSE_WAIT -> LAST_ACK 发送FIN
  3. LAST_ACK-> CLOSED 接受ACK

查看系统tcp状态命令

netstat -tunpla | grep TIME_WAIT

-t或--tcp:显示TCP传输协议的连线状况;
-u或--udp:显示UDP传输协议的连线状况;
-p或--programs:显示正在使用Socket的程序识别码和程序名称;
-n或--numeric:直接使用ip地址,而不通过域名服务器;
-l或--listening:显示监控中的服务器的Socket;
-s或--statistice:显示网络工作信息统计表;

2. 为什么会有TIME_WAIT状态,而不是直接由FIN_WAIT_2 变成CLOSE

对于TIME_WAIT的说明:

  1. 只有主动关闭的一端才会处于TIME_WAIT
  2. TIME_WAIT的状态是FIN_WAIT_2状态接受到FIN后变成

TIME_WAIT 存在的理由是:

  1. 如果最后主动关闭方发送的ACK丢失,被动关闭方会重传最终那个FIN,以期待主动关闭方重新ACK,所以主动关闭方会维持一个状态,再次ACK最终FIN

  2. 关闭方发送FIN到主动方发送ACK确认,正好是2个MSL(Max Segment Life)周期,超过这个周期的报文会被丢弃,如果时间超过2MSL,则在此连接的端口上重新创建一个新连接,也不会接受到老数据

过多TIME_WAIT会消耗系统的资源(维持TCP的状态需要消耗内存),有必要进行相关配置优化

与TIME_WAIT相关的配置:

  1. tcp_tw_reuse 默认关闭,tcp_tw_reuse=1 要想此参数发挥作用还必须tcp_timestamps=1
  2. tcp_tw_recycle=1
  3. tcp_max_buckets 默认是180000

3. 什么是连接元组

一个TCP连接需要[src_port, src_ip, des_port, des_ip, protocol] 5个因子,这5个因子构成一个TCP连接元组

4. SYN ACK 分别有什么用

SYN(Sequence Number ):保证TCP传递数据不会乱序,SYN是根据报文长度增加的

ACK:保证TCP传递数据不会丢失,只会ACK最长连续的报文,比如发送的报文是[1, 2, 4, 5], 报文3丢失,那么ACK回去的是2

SYN超时: 发送端没有接受到对端的ACK,那么会重新发送报文,发送的时间间隔是1s, 2s, 4s, 8s, 16s , 31s 共63s,如果超时,则会断开连接

SYN FLOOD攻击:创建TCP连接时,如果客户端发送SYN后下线,服务端接受到SYN,发送SYN-ACK报文,因为发送端已经下线,无法ACK,所以服务器端会不停重试直到63s关闭连接,大量的客户端发送SYN后,导致服务器端的SYN队列满了,无法处理正常的连接

处理这类问题方式:

  1. tcp_max_syn_backlog: 设置SYN的队列长度
  2. tcp_abort_on_overflow: 当syn队列满了,直接不处理请求
  3. tcp_synack_retries:设置重试次数

5. 如何理解TCP滑动窗口

TCP滑动窗口主要接受端是告诉发送端,自己还能处理的数据大小。

滑动窗口的默认大小是M,已经接受的最大连续包字节数是N, 还能发送的字节数是window = M-N-1

如果缓存区满了,ack对方window的大小是0,这个时候,发送端将不会发送数据。如果服务器端缓冲区又有空间了,怎么通知客户端呢,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒,如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

6. Nagle算法,MTU,sMSS

MTU(Max Transport Unit): 最大的传输字节,对于以太网来说,MTU是1500字节

MSS(Max Segment Size):除去TCP+IP头的40个字节(tcp,ip报文头各占20个字节),真正的数据传输可以有1460。

以太网本来一次可以传递1460个字节,假如一次只传输了1个字节,就显得有些浪费了,开启Nagel算法可以优化,Nagle不是完全禁止了小包的发送,而是禁止了大量的小包发送。

开启socket方式: setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value,sizeof(int));

nginx也可以开启tcp_nodelay选项。

7. 常见的TCP flags有哪些

  1. F : FIN - 结束,结束会话
  2. S : SYN - 同步,表示开始会话请求
  3. R : RST - 复位,中断一个连接
  4. P : PUSH - 推送,数据包立即发送
  5. A : ACK - 应答,也用.表示
  6. W : CWR - 拥塞窗口减少
  7. U : URG - 紧急

8. 什么是TCP已完成的连接队列和未完成的连接队列

半完成的连接队列(incomplete connection queue): 创建连接时,服务器端接受到客户端发送SYN后,会把SYN放到incomplete connection queue

设置半完成的连接队列配置: tcp_max_syn_backlog

已完成的连接队列 (complete connection queue 创建连接时,如果服务器端接受到了ACK,三次握手完成,说明连接已经建立,则把incomplete connection queue里面的syn移到 complete connection queue里面。

可见如果发送SYN Flood,syns queue里面的syn永远无法移除来(没有接受到客户端的ACK),所以syns queue会满。

complete connection queue的大小设置在创建监听套接字的时候传递的参数backlog

listen(sockFd, backlog)

通常认为backlog = complete connection queue + incomplete connection queue

如果TCP连接队列溢出,有哪些指标可以看呢

[root@server ~]#  netstat -s | egrep "listen|LISTEN"
667399 times the listen queue of a socket overflowed
667399 SYNs to LISTEN sockets ignored

[root@server ~]# ss -lnt
Recv-Q Send-Q Local Address:Port  Peer Address:Port
0        50               *:3306             *:*

# Send-Q: Send-Q 表示第三列的listen端口上的全连接队列最大为50

当客户端发送SYN时,如果这些队列是满的,那么服务器端会忽略该分节,也就是说,不发送RST,以期待客户端重新发送SYN (因为队列满状态是暂时的)

服务端调用Accept函数后,会从以完成连接的队列中获取第一个项发回给进程 。如果队列为空,则进程会睡眠

9. ARP(Address Resolution Protocol)协议

在传输层,数据并没有IP信息,当传输层的数据送到网路层,会加入IP信息。

但是数据发送到接收端,除了需要知道接受端IP地址以外,还需要知道接收端的MAC信息,ARP协议就是用来做IP到MAC地址转换的协议

10. Socket, Listen, Bind, Accept Close函数分别有什么用

  1. socket: 创建一个TCP或者UDP套接字 socket(family, type, protocol) family: AF_INET, AF_INET6, protocol:SOCK_STREAM, SOCK_DGRAM

  2. listen: 把一个套接字变成监听套接字,在服务器的生命周期内都不能关闭这个套接字

  3. accept:返回一个已经连接的套接字

  4. connect: 连接对方服务器,发起三次握手,参数有(socketfd, addr, port) 必须传递服务器的端口号和ip地址,client没有收到SYN的响应(ACK), 则会重传这个分节,若75s后还是没有收到ACK应答这返回ETIMEDOUT错误,client收到的是RST应答,则表示服务器没有启动或者连接端口错误
  5. close: 值得注意的是close也许不会立即触发4次握手,在多进程的TCP服务器中,父进程会关闭已经连接的套接字,只是把套接字的引用计数减1,第二点,调用close之后,这个套接字不能再被进程使用,也就是不能作为read或write的参数,但是TCP将会已经排队等待发送给对端的数据发送完了之后,才会发生正常的TCP终止序列

11. IO模型有哪些

Unix统下有5中基本的IO模型:

  1. 阻塞式I/O
  2. 非阻塞式I/O
  3. I/O 复用(select, poll)
  4. 信号驱动式I/O
  5. 异步I/O

首先需要搞清楚I/O是,应用进程访问系统内核读取数据。涉及到应用进程和系统内核之间的通信。

阻塞式I/O:应用进程访问系统内核,比如使用recvfrom函数,如果此时内核数据没有准备好,则应用进程会阻塞。需要清楚“阻塞”是进程的一种状态,如果数据没有准备好,则进程会由运行态变为挂起状态,让出CPU。

当数据准备好的时候,应用进程会有挂起态变为就绪态,等待CPU调用从而处理准备好的数据。

如果进程阻塞了,这个时候就算有其他fd数据准备好也无法处理,因为应用进程因为I/O阻塞而让出CPU了。

所以阻塞式I/O特别低效

非阻塞式I/O: 应用进程访问系统内核,比如使用recvfrom函数,如果此时内核数据没有准备好, 内核会给应用程序返回错误EWOULDBLOCK,如果应用程序反复调用recvfrom,那么内核将持续返回错误EWOULDBLOCK,直到有数据准备好。

应用程序反复调用recvfrom时,我们称为轮询,这个过程会大量消耗CPU。

I/O 复用: 所谓I/O复用是一种能力,应用程序首先告知内核需要监听套接字,内核如果发现应用程序制定的一个或多个I/O条件准备就绪了,内核就会通知应用程序。

通过select或者poll函数,达到我们所期待的效果。应用程序会阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的I/O调用上,一旦select或者poll发现一个或者多个I/O条件准备就绪,就通知应用程序。

信号驱动式I/O:信号驱动I/O是应用程序首先创建一个I/O信号处理函数,如果I/O条件准备就绪,内核会通过事先注册的信号通知到应用程序。

应用程序在整个过程中没有任何阻塞

异步I/O: I/O操作是异步处理的,当I/O操作完成后内核才会通知应用程序。 内核完成I/O操作的过程是自动的。异步I/O和信号驱动I/O的区别是,信号驱动I/O是数据准备好后,内核通知应用程序调用recvfrom函数处理I/O数据,而异步I/O是自动处理I/O,处理完成后通知应用程序I/O操作已经完成

12. 什么是非阻塞式I/O

首先需要明确,套接字默认是阻塞的(包括监听套接字,已连接的套接字),这就说明,如果发出一个不能立即完成的套接字调用时,应用进程会阻塞,让出CPU。

可能阻塞套接字调用的操作有:

  1. 读操作【输入】(read, readv,recv,recvfrom,recvmsg) 如果应用进程对一个阻塞的TCP套接字发起读操作时,数据没有准备好,则进程挂起。

  2. 写操作【输出】(write,writev, send,sendto, sendmsg)如果应用进程对一个阻塞的TCP套接字发起写操作时,如果该套接字发送缓存区没有空间,则进程挂起。

对于非阻塞式套接字,则返回EWOULDBLOCK错误。

对于UDP而言,UDP没有发送缓存区,对于一个阻塞的UDP套接字而言,写操作不会阻塞,内核只是复制应用进程的数据并向下协议栈传递

  1. 接受外来连接,即调用Accept函数,如果应用进程对一个阻塞的TCP监听套接字发起accept操作时,如果此时没有新的连接达到,则进程挂起。

  2. 请求外来连接 即调用Connect函数, 我们知道,只有当三次握手完成后,才算连接建立,所以对一个阻塞的TCP套接字发起Connect请求,应用进程会阻塞直到三次握手完成。

11. 怎么理解套接字的接受缓冲区和发送缓冲区

每个TCP套接字都有一个发送缓冲区,我们可以使用SO_SENDBUF 套接字选项来改变套接字缓冲区的大小。

当应用程序在阻塞的套接字上调用write或者send函数时,如果要发送的数据大于目前发送缓冲区的大小,则程序挂起。

如果有足够的空间保存应用程序的数据,则write返回成功,但这并不代表数据已经发送成功,只是说明数据写入到发送缓冲区成功。

发送端的TCP会提取套接字发送缓冲区的数据并发给另一端,只有当另一端的TCP 确认接收到数据后,发送端才会清空发送缓冲区的数据。

对于UDP套接字而言,没有发送缓冲区。

每个套接字(包括UDP)都有一个接收缓冲区。三次握手完成后,数据发送过来,首先会写入到接收缓冲区里面。

相关配置

SO_RCVBUF: 接受缓冲区的大小 (int)
SO_SNDBUF: 发送缓冲区的大小 (int)
SO_RCVLOWAT: 接受缓冲区的低水位大小 (int=1)
SO_SNDLOWAT: 发送缓冲区的低水位 (int=2048)

12. 为什么 I/O 复用要搭配非阻塞 I/O?

我们知道 I/O 复用阻塞在select,poll系统调用上,当I/O条件准备就绪,内核告知应用进程数据已经准备好了,此时系统进程去发起返回的套接字调用,比如应用程序通过函数readn获取10字节大小的套接字接受的数据,但是套接字里面只有5字节的数据,所以应用程序还是会挂起。

又比如,套接字接受的数据是10字节,应用程序调用readn只读取了3字节,但应用程序再次读取8字节时,应用程序还是会挂起。

也就是说,应用程序保证不被挂起的状态下读取到所有已经接受的数据。

这个时候必须要求套接字是非阻塞的。

15. select,epoll的具体实现逻辑

select而言:

select(int maxfdp1,*readset,*writeset,*exceptset, timeout)

  1. 可以注册可读,可写,异常的相关套接字
  2. timeout取值: None: 一直阻塞知道数据准备好,0: 不阻塞,立即返回,相当于轮询, >0: 阻塞的时间,当超过阻塞的时间还没有数据准备好则返回为空的集合
  3. maxfdp1 = sockfd + 1 select在内核中是轮询的,内核遍历所有注册的fd,查看是否有准备好的fd, 套接字是从0开始的,所以maxfd = max(socket) + 1,这也是select的限制(轮询和描述符数量限制)

epoll而言:

  1. 没有描述符数量的限制
  2. 事件触发机制不需要轮询
  3. 内存管理方式更高效(不需要复制fd, 内部使用红黑树管理fd)
  4. 给套接字注册相关的事件(可读,可写,异常)

epoll 两种触发模式:

LT(level triggered) 是默认/缺省的工作方式,同时支持 block和no_block socket。这种工作方式下,内核会通知你一个fd是否就绪,然后才可以对这个就绪的fd进行I/O操作。就算你没有任何操作,系统还是会继续提示fd已经就绪,不过这种工作方式出错会比较小,传统的select/poll就是这种工作方式的代表。

ET(edge-triggered) 是高速工作方式,仅支持no_block socket,这种工作方式下,当fd从未就绪变为就绪时,内核会通知fd已经就绪,并且内核认为你知道该fd已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个fd进行I/O操作,导致fd变为未就绪时,内核同样不会发送更多的通知,因为only once。所以这种方式下,出错率比较高,需要增加一些检测程序。

LT可以理解为水平触发,只要有数据可以读,不管怎样都会通知。而ET为边缘触发,只有状态发生变化时才会通知,可以理解为电平变化。

16. I/O就绪的条件是什么

17. keepalive选项的意义是什么

18. tcpdump常用命令

19. wireshark基本使用

results matching ""

    No results matching ""