TCP基本认识
TCP是面向连接的、可靠的,基于字节流的传输层通信协议。
图片来源小林coding
- 序号:传输方向上字节流的字节编号。初始时序号会被设置一个随机的初始值(ISN),之后每次发送数据时,序号值 = ISN + 数据在整个字节流中的偏移。假设A -> B且ISN = 1024,第一段数据512字节已经到B,则第二段数据发送时序号为1024 + 512。用于解决网络包乱序问题。
- 确认号:接收方对发送方TCP报文段的相应,其值是收到的序号值+1;
- URG:标志紧急指针是否有效;
- ACK:标志确认号是否有效(确认报文段)。用于解决丢包问题;
- PSH:提示接收端立即从缓冲读走数据;
- RST: 表示要求对方重新建立连接;
- SYN:表示请求建立一个连接;
- FIN:表示关闭连接;
- 窗口:用于告知发送方本方的缓冲还能接受多少字节数据。用于解决流量控制;
- 校验和:接收端用CRC检验整个报文段有无损坏。
TCP的特点
- 基于连接:先建立连接,结束之后释放连接
- 全双工:双向传输
- 字节流:不限制数据大小,保证有序接受,重复报文自动丢弃
- 流量缓冲:解决双方处理能力的不匹配问题
- 可靠的传输服务:保证可达,丢包时,通过重发机制实现可靠
- 拥塞控制:防止网络出现恶性拥塞,限制报文大小、频次
- 一对一:每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的
如何唯一确定一个TCP连接
源地址+源端口+目的地址+目标端口
有一个IP的服务端监听了一个端口,他的TCP最大连接数是多少
最
T
C
P
连接数
=
客户端
I
P
数
∗
客户端端口数
最TCP连接数=客户端IP数*客户端端口数
最TCP连接数=客户端IP数∗客户端端口数
当然,服务端最大TCP连接数远远不能达到理论上限,会受到以下因素影响:
- 内存限制: 每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。
- 文件描述符限制:每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
- 系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;
- 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;
- 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
端口有效范围
0-1023为知名端口号,比如其中HTTP是80,FTP是20(数据端口)、21(控制端口)
UDP和TCP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。动态端口的范围是从1024到65535
TCP和UDP
UDP
提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
- 无连接
- 尽最大努力交付
- 面向报文
- 米有拥塞控制
- 支持一对一、一对多、多对多通信
- 首部开销小,只有8个字节
UDP编程的时候,一次发送多少bytes比较好
- 以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的.这个1500字节被称为链路层的MTU(最大传输单元).但这并不是指链路层的长度被限制在1500字节,其实这这个MTU指的是链路层的数据区.并不包括链路层的首部和尾部的18个字节.
- 所以,事实上,这个1500字节就是网络层IP数据报的长度限制。因为IP数据报的首部为20字节,所以IP数据报的数据区长度最大为1480字节.而这个1480字节就是用来放TCP传来的TCP报文段或UDP传来的UDP数据报的.又因为UDP数据报的首部8字节,所以UDP数据报的数据区最大长度为1472字节.这个1472字节就是我们可以使用的字节数。
当我们发送的UDP数据大于1472的时候会怎样呢? 这也就是说IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片(fragmentation). 把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组. 这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便 无法重组数据报.将导致丢弃整个UDP数据报。
因此,在普通的局域网环境下,我建议将UDP的数据控制在1472字节以下为好.
- 进行Internet编程时则不同,因为Internet上的路由器可能会将MTU设为不同的值. 如果我们假定MTU为1500来发送数据的,而途经的某个网络的MTU值小于1500字节,那么系统将会使用一系列的机制来调整MTU值,使数据报能够顺利到达目的地,这样就会做许多不必要的操作.鉴于Internet上的标准MTU值为576字节,所以我建议在进行Internet的UDP编程时. 最好将UDP的数据长度控件在548字节(576-8-20)以内
TCP和UDP区别
图片来自阿秀的学习笔记
TCP和UDP对应的应用层协议
- TCP:FTP:定义了文件传输协议,使用21端口. Telnet:它是一种用于远程登陆的端口,23端口 SMTP:定义了简单邮件传送协议,服务器开放的是25号端口。 POP3:它是和SMTP对应,POP3用于接收邮件。
- UDP:DNS:用于域名解析服务,用的是53号端口 SNMP:简单网络管理协议,使用161号端口 TFTP(Trival File Transfer Protocal):简单文件传输协议,69。
什么是TCP粘包和拆包?发生的原因?
一个完整的业务可能会被TCP拆分成多个包进行发送,这是拆包;发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,这是粘包。
原因
1、应用程序写入数据的字节大小大于套接字发送缓冲区的大小.
2、进行MSS大小的TCP分段。( MSS=TCP报文段长度-TCP首部长度)
3、以太网的payload大于MTU进行IP分片。( MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。)
4、TCP连接复用导致粘包
5、TCP默认使用nagle算法,这个算法会导致粘包问题
6、流量控制、拥塞控制导致粘包
7、接收方不及时接收缓冲区的包,造成多个包接收
解决方法
1、应用层发送数据时定长发送。
2、在包尾部增加回车或者空格符等特殊字符(例如\n\r,\t)进行分割
3、将消息分为消息头和消息尾,头部标记分步接收。在TCP报文的头部加上表示数据长度。
4、使用其它复杂的协议,如RTMP协议等。
5、nagle导致的需要结合应用场景适当关闭该算法
浏览器对于同一个host建立TCP连接到的数量有没有限制
有限制。chrome最多允许对同一个Host建立6个TCP连接。不同的浏览器会有一些区别。
如果图片都是HTPS连接,并且在同一个域名下,那么浏览器在SSL握手之后会和服务器商量能不能用HTTP2,能的话就使用multiplexing功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个TCP连接去获取,但是可以确定的是multiplexing很有可能会被用到。
如果用不到HTTP2或者HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。那浏览器就会在一个HOST上面建立多个TCP连接,连接的最大数量取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求。如果所有的连接都正在发送请求,那其他的请求就只能再等待一段时间。
在浏览器中输入url地址后显示主页的过程
深入探究“在浏览器输入URL到渲染页面”(上)过程剖析
用户输入
用户在浏览器地址栏输入url并回车
URL请求过程
1、浏览器处理用户输入,把处理后的url发送至网络进程
2、 网络进程收到url请求之后查看本地是否缓存了这个资源(强缓存和协商缓存),如果有则该资源返回给浏览器进程
3、如果没有,网络进程向服务器发起HTTP请求(网络请求)以请求资源:
- DNS解析:将域名解析成IP地址
- 如果请求协议HTTPS,那么还需要建立TLS连接
- 利用ip地址和服务器建立TCP连接:浏览器与服务器进行三次握手
- 浏览器端构建请求头信息,并将其发送给服务器
- 服务器处理请求并返回HTTP报文给网络进程
- 处理状态码
断开连接
TCP四次挥手
浏览器进程开始准备渲染进程
浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程。
提交文档
渲染进程准备好之后,需要先向渲染进程提交页面数据,这个是提交文档阶段
渲染阶段
渲染进程接收完文档信息之后,便开始解析页面和加载子资源,完成页面渲染。
三次握手
三次握手就是建立一个TCP连接的时候,需要客户端和服务器总共发送3个包,进行三次握手的主要作用是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传输做准备。
实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?
在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。
所以虽然标准中没有设定,某些服务器对 Connection: keep-alive 的 Header 进行了支持。意思是说,完成这个 HTTP 请求之后,不要断开 HTTP 请求使用的 TCP 连接。这样的好处是连接可以被重新使用,之后发送 HTTP 请求的时候不需要重新建立 TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免。
持久连接:HTTP/1.1 把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。
默认情况下建立 TCP 连接不会断开,只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。
具体过程
刚开始客户端处于closed的状态,服务端处于listen状态,进行三次握手:
- 第一次握手:客户端给服务端发送一个SYN报文,指明客户端初始化序列号,此时客户端处于SYN_SEND状态。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。。
- 第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也制定了自己的初始化序列号,然后把客户端的序列号+1作为ACK的值,表示已经收到了客户端的SYN,此时属于SYN_SEND状态。在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
- 第三次握手:客户端收到SYN报文之后,会发送一个ACK报文,也是把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISHED状态。服务器收到ACK报文之后,也属于ESTABLISHED状态,此时双方建立连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
在socket编程中,客户端执行connect()时,将触发三次握手。
ISN是固定的吗?
ISN = M + F(localhost, localport, remotehost, remoteport)(M为计数器),ISN应该由这个公式确定,F为哈希算法,不是一个简单计数器。
这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。如果 ISN是固定的,攻击者很容易猜出之后的确认号,所以是动态生成的。
第一次第二次握手可以 携带数据吗
不可以,因为如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
第三次握手的时候,客户端已经是establishd状态了,已经建立起连接了并且知道服务器的接收、发送能力是正常的了。
如果是两次握手会怎么样
如果客户端发送请求,但是请求保温丢失了,然后客户端重传一次连接请求。客户端一共发送了两次连接请求报文段,第一个在网络中长时间滞留,第二个到达服务器。连接释放之后,第一个也到达服务端了,结果服务器以为这个是一个新的连接请求,然后就又建立了新连接,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一直等待客户端发送数据,浪费资源。
第一次握手丢失会怎么样
客户端超时重传3次SYN报文之后,由于假设tcp_syn_retries为3(在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5),已经达到最大重传次数,于是再等待一段时间,事件时间为上一次超时时间的两倍,如果还是没有收到服务端的第二次握手,那么客户端就会断开连接
第二次握手丢失会怎么样
- 当客户端超时重传 1 次 SYN 报文后,由于假设 tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
- 当服务端超时重传 2 次 SYN-ACK 报文后,由于假设 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。(将该连接信息从半连接队列中删除,每次重传等待的时间不一定相同,一般是指数增长)
第三次握手丢失会怎么样
当服务端超时重传 2 次 SYN-ACK 报文后,由于假设 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
什么是半连接队列
服务器第一次收到客户端的SYN之后,就会处于SYN_RCVD状态,此时双方还没有完全建立连接,服务器会把此种状态下请求连接放在一个队列里面,这种队列就是半连接队列。
还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中,如果队列满了就有可能会出现丢包现象。
DDos攻击
客户端你想服务端发送请求链接数据包,服务端向客户端发送确认数据包,客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认,没有彻底根治的方法。
DDos 预防
- 限制同时打开SYN半连接的数目
- 缩短SYN半连接的time out时间
- 关闭不必要的服务
SYN攻击
是一种典型的DDos攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。可以通过netstats来检测SYN攻击
netstat -n -p TCP | grep SYN_RECV
防御SYN攻击
- 调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000
- 增大TCP半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:
增大net.ipv4.tcp_max_syn_backlog
增大listen() 函数中的 backlog
增大net.core.somaxconn
- 开启tcp_syncookies
可以在不使用SYN半连接队列的情况下成功建立连接,相当于绕过了SYN半连接来建立连接。
当SYN队列满了之后,后续收到SYN包,不会丢弃,而是根据算法计算出一个cookie值;放到第二次握手报文的序列号里,然后服务端返回给客户端;服务端接受客户端的应答报文的时候会检查这个ACK包的合法性,如果合法就放到accept队列;最后程序通过调用accept接口从accept取出连接。
通过设置,0是关闭功能,2是无条件开启,1是SYN队列放不下就启用
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
- 减少SYN+ACK重传次数
加快处于 SYN_REVC 状态的 TCP 连接断开。SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
四次挥手
- 客户端发送一个FIN报文,报文中指定一个序列号,此时客户端处于FIN_WAIT1状态,主动关闭TCP连接。
- 服务端收到FIN之后,发送ACK报文,把客户端序列号+1作为ACK报文确认号的值,服务端处于CLOSE_WAIT状态。此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端确认之后进入FIN_WAIT2状态。
- 服务端发完数据之后也想断开连接了,发送FIN报文,指定一个序列号,服务端处于LAST_ACK状态,确认号ack和第二次挥手一样
- 客户端收到FIN之后,发送一个ACK报文,把服务端的序列号作为自己的确认号值,此时客户端处于TIME_WAIT状态。服务端收到ACK保温之后,就处于CLOSE状态了。客户端这个时候需要经过2MSL才能进入CLOSED状态。
TIME_WAIT
2MSL等待状态
一个报文段最大生存时间为MSL,因为TCP报文以IP数据报在网络内传输,而IP数据包则有限制其生存时间的TTL字段。
意义
- 保证客户端发送的最后一个ACK报文能够到达服务端。如果没有到达,服务端会超时重传,2MSL内超时重传报文会被客户端收到,然后客户端重新传一次确认报文,最后客户端和服务器都正常到CLOSED状态。
- 防止已失效的历史报文出现在本连接中,客户端在发送完最后一个ACK报文段之后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使得下一个新的连接中不会出现这种旧的连接请求报文段。
TIME_WAIT过多有什么危害
- 占用系统资源,比如文件描述符、内存资源、CPU资源、线程资源
- 占用端口资源
- 客户端TIME_WAIT过多:占用端口资源
- 服务端TIME_WAIT过多:服务器只是监听一个端口,但是这样会导致占用很多系统资源。
优化TIME_WAIT
- 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
- net.ipv4.tcp_max_tw_buckets
- 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。
服务器出现大量CLOSE_WAIT的原因
CLOSE_WAIT是被动关闭方才会有的状态,如果被动关闭方没有调用close函数关闭连接,那么就无法发出FIN报文,无法使得CLOSE_WAIT状态转变为LAST_ACK状态。
还有一种原因:服务器的父进程派生出子进程,子进程继承了socket,收到FIN的时候子进程处理但父进程没有处理该信号,导致socket的引用不为0无法回收。
解决办法
参考小林coding
一个普通的 TCP 服务端的流程:
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
- 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 close
- 第二步没有做,服务端没办法感知事件
- 第三步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接
- 第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了
- 第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等
总结来说,就是1、停止应用程序;2、修改程序里的bug。