一文带你搞定TCP面试(一) tcp面经
摘要
- TCP头部格式
- TCP
- TCP三次握手
- 分片
- SYN攻击
TCP头部格式
- 序列号:建立连接时会生成一个随机数初始化该值,然后通过SYN包发送给接收端,每发送一次数据就累加一次数据字节数的大小,序列号主要用来解决乱序问题
- 确认应答号:下一次期望收到的数据序列号,发送端可以认为该应答号以前的数据均被接收端正常接收,主要用来解决丢包问题
- ACK:该位为1,确认应答号字段变为有效,除了建立连接时的SYN包其他的包ACK必须为1
- RST:该位为1,表示TCP连接中出现异常必须强制断开连接
- SYN:该位为1,表示希望建立连接,并在器序列号中设置初始值
- FIN:该位为1,表示希望断开连接。通信结束断开连接时,发送方和接收方会相互发送FIN为1的包。
TCP
什么是TCP?
TCP是面向连接的(只能一对一)、可靠的(确保每一个报文都能到达接收端)、基于字节流(保证字节的有序性,自动去除重复字节)的传输层通信协议。
为什么需要TCP协议?
由于网络层(IP层)是不可靠的,它不保证网络包的交付,不保证网络包的顺序,不保证网络包数据的完整性,因此需要上层传输层的TCP协议来保证。
TCP是传输层中的可靠服务,它能够保证网络包无损坏、无间隔、非冗余和顺序性。
什么是TCP连接
TCP连接就是用于保证可靠性和流量控制维护的某些状态信息,比如以下信息:
- Socket:IP地址加端口
- 序列号:解决乱序问题等
- 窗口大小:用于进行流量控制
如何确定唯一一个TCP连接
四元组:
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址存在于IP头部中,用于IP协议;源端口和目的端口号存在于TCP头部中,用于表明报文发送到主机的哪个进程上。
理论上的服务器的TCP连接数可以达到:
最大连接数(2^48) = 客户端的IP数量(2^32)* 端口数量(2^16)
当然这只是理论,服务器的最大连接数还要受文件描述符和内存的限制:
- Socket都是文件,收到操作系统文件描述符的限制,ulimit可以查看文件描述符的数目
- 每个TCP连接都需要占用内存,因此受限于操作系统的内存
UDP和TCP的区别?
- 连接:UDP不需要建立连接,TCP在传输前必须要建立连接
- 服务对象:TCP是点对点的传输,UDP可以一对一、一对多、多对多通信
- 可靠性:TCP是可靠交付,UDP是不可靠交付
- 拥塞控制和流量控制:TCP有流量控制和拥塞控制,保证数据传输安全性,UDP没有,UDP的发包不会考虑当前的网络状况
- 首部开销:TCP首部较长,在不使用选项字段的情况下是20个字节,UDP首部只有8个字节,固定不变,开销较小
- 传输方式:TCP是流式传输、没有边界,保证顺序和可靠,UDP是按包发送,有边界但可能会丢包和乱序
- 分片不同:TCP数据如果大于MSS大小,则会在传输层进行分片,目标主机收到后同样在传输层组装TCP包,如果中间之丢失了某个分片,只需要重传这个分片;UDP的数据如果大于MTU的大小,会在IP层进行分片,同样的目标主机在IP层进行组装,如果中途丢了一个分片,在实现可靠的UDP中发送端需要重传所有的数据包
TCP和UDP的应用场景
- TCP:FTP文件传输、HTTP/HTTPS等需要可靠交付的场景
- UDP:DNS、SNMP(较少数据量通信);视频、音频等多媒体通信、广播通信
TCP三次握手
TCP三次握手前的客户端和服务端的初始状态均为CLOSED状态,服务端在监听某个端口以后会变为LISTEN状态。
上图是执行netstat -lantp命令获取的结果,在我服务器上我监听了80和443端口,状态位LISTEN。
TCP第一次握手
客户端在发送第一次握手报文时,会随机初始化序列号(client_isn),该序列号会被放置在TCP报文中的序列号中,同时SYN为置1,客户端在发送完改报文以后,会处于SYS_SENT状态。
上图是我通过WireShark抓取的第一次握手报文,可以看出以下信息:
- 源端口号:56654
- 目的端口号:443
- 序列号:0(注意这里看到的是相对序列号,是相对初始化序列号的偏移)
- 确认应答号:0
- 首部长度:32个字节
- 标志位:0x002表明SYN为1
- 窗口大小为:64240字节
- 校验和:0x0fa1
- 紧急指针:0
- 选项:占用12个字节
TCP的第二次握手
服务端收到客户端的SYN报文后,也随机初始化自己的序列号(server_isn),该序号会被放置在TCP报文的序列号中,其次还会把确认应答号置为client_isn+1,接着会把ACK标志位和SYN标志位置1,服务端然后把该报文发送给客户端,发送完成以后,服务端处于SYN_RCVD状态。
上图是我通过WireShark抓取的第二次握手报文,可以看出以下信息:
- 源端口:443
- 目的端口号:56654
- 序列号:0(注意这里看到的是相对序列号,是相对初始化序列号的偏移)
- 确认应答号:1
- 首部长度:32个字节
- 标志位:0x012,表明SYN和ACK标志位为1
- 窗口大小:14600字节
- 校验和:0x4f21
- 紧急指针:0
- 选项:占用12个字节
TCP的第三次握手
客户端收到服务端的报文后,会回复服务端一个报文。该报文中应答号会置为server_isn+1,并且将ACK标志位置为1,客户端在发送完报文以后会变为ESTABLISHED状态,服务器在收到报文后也会进入ESTABLISHED状态。
上图是我通过WireShark抓取的第三次握手报文,可以看出以下信息:
- 源端口号:56654
- 目的端口号:443
- 序列号:1(注意这里看到的是相对序列号,是相对初始化序列号的偏移)
- 确认应答号:1
- 首部长度:20个字节
- 标志位:0x010,表明ACK位为1
- 窗口大小:517个字节
- 校验和:0x0f95
- 紧急指针:0
TCP第三次握手是可以携带数据的,前两次握手不能携带数据。TCP三次握手以后,客户端和服务端就可以正常发送数据了。
TCP握手为什么需要三次?
- 三次握手才能阻止重复历史连接的初始化
- 三次握手才可以同步双方的初始化序列号
- 三次握手才可以避免资源浪费
TCP三次握手如何阻止历史连接初始化
假如一个旧的SYN报文比最新的SYN报文到达,此时服务端回复SYN+ACK报文给客户端,客户端在收到报文以后会判断回复报文中的应答号,通过应答号发现这是一个历史连接,就会发送一个RST报文,中止历史连接。
如果是两次握手的话,不能判断连接是否是历史连接,三次握手就可以在收到第二次握手报文时精准的判断是否是历史连接。
TCP三次握手同步双方的初始化序列号
TCP通信的双方必须要各自维护一个序列号,序列号的主要作用如下:
- 接收方去除重复的数据
- 接收方按照序列号顺序接收
- 标识已发送的数据包哪些是被接受的
通信双方必须要发送各自的初始化序列号才给对方,通过对方的ACK报文确定SYN报文已经接收,看上去四次握手也可以达到效果,但由于服务端的ACK报文和SYN报文可以合并在一个请求中给客户端,因此通过三次握手就可以同步双方的序列号,而且减少了一次请求耗时。
TCP三次握手避免资源浪费
如果是两次握手,假设客户端的SYN报文发生延时阻塞,客户端没有收到来自服务端的ACK报文,就会重发SYN报文,服务器也不清楚客户端是否收到了自己的ACK报文,因此只要收到客户端的SYN报文就必须建立连接,这样会建立多个冗余无效的连接,造成资源的浪费。
初始化序列号如何生成
ISN = M + F
- M:计时器,每隔4ms就+1
- F:哈希算法,根据源IP、目的IP、源端口、目的端口生成一个随机数,但是该哈希算法不能被外部轻易破解
分片
MTU和MSS的区别
- MTU:IP头部+TCP头部+数据的长度,以太网中一般为1500字节
- MSS:出去IP头部和TCP头部,所容纳的数据的最大长度
IP层分片的缺点
在IP层中,如果发现数据包超过了MTU的大小,就会进行分片,但是如果一个分片丢失,由于IP层本身没有超时重传机制,需要借助传输层TCP来负责超时重传,此时就需要重传整个TCP报文,也就是IP所有的分片都需要重传,可以看出效率低下。
所以为了解决这种问题,通常避免在IP层分片,而是在传输层进行分片,因此TCP通信的双方需要协商MSS值,当TCP层数据发现超过了MSS值时,就会进行分片,当然这里也会保证分片后的形成的IP数据包不会超过MTU的大小,这样IP层才不会进行分片。
SYN攻击
什么是SYN攻击
攻击方通过伪造不同的IP短时间内发送很多SYN报文,服务器端发送的ACK+SYN报文客户端不予回应,这样服务端会有很多很多个处于SYN_RCVD的连接,时间越久,半连接队列(SYN队列)里的数量多越来越多,直至无法为正常的客户端服务。
如何避免SYN攻击
- 修改内核参数
# 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。用于控制该队列的的最大值
net.core.netdev_max_backlog
# SYN_RCVD状态连接的最大个数
net.ipv4.tcp_max_syn_backlog
# 超出处理性能时,对新的SYN报文直接回复RST
net.ipv4.tcp_abort_on_overflow
- 启用tcp_syncookies
# 开启tcp_syncookies
net.ipv4.tcp_syncookies=1
开启tcp_syncookies以后,当SYN队列满时,后续的SYN包不再进入队列,而是计算一个cookie值,然后发送ACK+SYN报文给客户端。
客户端在收到报文后也会回复ACK报文给服务端,服务器会检查这个ACK报文的合法性,如果合法就放入全连接队列(accept队列)。