目录
1.TCP
1.1.TCP报头
1.2.TCP如何保证可靠性
1.2.1.校验和
1.2.2.确认应答与序列号
1.2.3.超时重传
1.2.4.流量控制(滑动窗口)
1.2.5.拥塞控制
1.3.三次握手
1.3.0.三次握手过程
?1.3.1.为什么序列号syn是随机值?
1.3.2.TCP三次握手,前2次握手的序列号有关系吗?
1.3.3.为什么返回时ack值是seq值加一?
?1.3.4.为什么要进行3次握手?而不是2次或者4次?
1.3.5. listen的第2个参数(两个队列:syn请求队列/已连接队列)
1.3.6. SYN泛洪攻击/DDOC攻击
?1.3.7.??????连接建立是在accept函数么?
?编辑
1.3.8.非阻塞套接字Connect
1.4.四次挥手
1.4.0.四次挥手过程
1.4.1.为什么是4次挥手?不是3次?
1.4.2.close/shutdown、引用计数
1.4.3.TIME-WAIT存在的原因? 为什么主动方在TIME-WAIT阶段要等待2MSL?
1.4.4.TIME-WAIT过多? 危害? 应该怎么解决?
1.4.5.SO_LINGER与RST
1.4.6.CLOSE_WAIT过多?应该怎么解决?
1.4.7. Nagle算法
1.4.8.延迟确认Delay ACK
1.4.9.在Delay ACK开启时,一定要关闭Nagle算法
2.UDP
2.1.UDP报文
2.2. UDP的connect
2.2.1.UDP/TCP的connect的区别
3.QA
1.TCP
1.1.TCP报头
| TCP首部20字节 ???“选项和填充”字段的长度最大为40字节 | 源Port/目的Port | 加上IP首部的源IP地址和目的IP地址可以唯一确定一个TCP连接 | 数据序号 | 保证发送数据的顺序性 | 确认序号 | 仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号 | 6 个标志位 | URG | 紧急指针有效 | ACK | 确认序号有效 | RST | 连接重置 | SYN | 同步序号用来发起一个连接 | FIN | 终止一个连接 | PSH | 接收方应尽快将这个报文交给应用层 | 校验和 | 发送主机根据数据计算出一个数值,接受主机接的数值与源主机要一致(证明数据的有效性) | 窗口字段 | 窗口的字节容量, TCP的标准窗口最大为2^16 - 1 = 65535Byte |
?TCP粘包\拆包
现象 | 因为TCP是流式套接字,是没有界限的一串数据,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以,在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把多个小包封装成一个大的包发送。这就是所谓的粘包和拆包问题 | 本质 原因 | 要发送的数据小于TCP发送缓冲区的大小,多次写入缓冲区的数据一次发送出去,发生粘包 | 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包 | 待发送的数据大于MSS(最大报文长度),TCP在传输前将进行拆包 | 解决 方案 | 由于底层TCP无法理解上层的业务数据,所以在底层无法保证数据包的粘包和拆包,这个问题只能在上层应用协议栈控制 | 1. | 消息定长 | 发送端将每个数据包封装为固定长度的数据块(长度不够补0填充) | 2. | 设置消息边界 | 包尾部添加\n\r,如:FTP协议 | 3. | 消息封装 | 将消息数据封装为struct {int len; void* data;},指定数据的长度 |
1.2.TCP如何保证可靠性
- TCP的链接是基于三次握手,断开是基于四次挥手,确保链接和断开的可靠性。
- TCP的可靠性,还体现在“有状态”
- TCP通过校验和、序列号机制、ACK应答、超时重传,来保证数据包的按序到达,保证数据传输不出差错
- TCP通过流量控制(滑动窗口)和拥塞控制来控制发送方发送速率
1.2.1.校验和
- TCP校验和的计算和UDP一样,在计算时要加上12byte的伪首部,检验和总共计算3部分:TCP首部、TCP数据、TCP伪首部。
- 计算方法为:①在发送方将整个报文段分为多个16位的段,然后将所有段进行反码相加,将结果存在校验和字段中;②接收方用相同的方法进行计算,③将计算的结果,与检验字段存储的结果比较,相等则正确,不等则错误
1.2.2.确认应答与序列号
- 序列号的作用
- 保证可靠性(当接收到的数据总少了某个序号时,能马上知道)
- 保证数据的按序到达
- 提高效率,可实现多次发送,一次确认
- 去除重复数据
1.2.3.超时重传
????????当报文发出后在一定的时间内未收到接收方的确认,发送方就会进行重传(通常是在发出报文段后设定一个闹钟,到点了还没有收到应答则进行重传)。
重传时间的确定:
????????报文段发出到收到应答中有一个报文段的往返时间RTT,显然超时重传时间RTO会略大于RTT,TCP会根据网络情况动态的计算RTT,即RTO是不断变化的。在Linux中,超时以500ms为单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。其规律为:如果重发一次仍得不到应答,就等待2500ms后再进行重传,如果仍然得不到应答就等待4500ms后重传,依次类推,以指数形式递增,重传次数累计到一定次数后,TCP认为网络或对端主机出现异常,就会强行关闭连接。
Q: 超时重传时间间隔、重传次数:指数退避方式
A: 第一次发送数据后,设置的超时时间是1.5s,此后每次重传时间都增加1倍,一直到64s,一共重传12次,大约9min才放弃
1.2.4.流量控制(滑动窗口)
目的:控制发送端的发送速度,防止数据发送过快,导致数据接收不过来。(接收端处理数据的速度是有限的,如果发送方发送数据过快,会导致缓冲区满,而发送方继续发送,会造成丢包,继而引起丢包重传等一系列反应)
原理:接收方每次接收到数据,向发送方返回ACK的时候,携带一个接收窗口大小(接收窗口的大小,是接收方根据自己的处理能力来确定的)。这就是滑动窗口机制
为什么引入滑动窗口?
- 在ACK确认策略中,一发送一应答,应答后才继续发送,否则,将阻塞。这样的缺点是:发送性能差。引入滑动窗口,可以一次发送一个窗口中的多条数据,提高了发送性能
滑动窗口的实现方式
- 发送方每次向接收方发送一个窗口大小的数据,发送完成后等待ACK
- 发送端接收到ACK后,滑动窗口会一直向右滑动
- 在窗口滑动的过程中,如果窗口中的某个pos的数据没有收到ACK,可能会导致窗口不能向前滑动,当超时后,超时定时器会重发数据,直到接收到数据ACK位置
- 如此反复执行1~3,直到滑动窗口滑动到数据的尾部,数据全部发送完毕
零窗口
????????当接收缓冲区满时,接收方在回复ACK的时候,会告诉发送方自己的接收窗口rwnd=0了,此时发送方接收到窗口大小为0后,就会停止发送数据。(此时会启动坚持定时器)
Q1: 为什么引入坚持定时器?
A1:?① B发送给A窗口为0的确认包后,A之后就不会再给A发送数据。② 等待一段时间后,B缓冲区有数据,就会向A发送一个报文,通知A现在可以发送数据了。③ 但是该报文在传输的过程中一旦丢失了,就会形成下面的“双方死等现象”:A等待B通知它发送数据,B发送报文后等待A发来数据。
Q2: 零窗口探测报文机制
A2:?坚持计时器(在A接收到一个窗口为0的报文后,将每隔一段时间,发送给B探测报文,即探测接收窗口大小的报文,一旦接收窗口大小不为0,就可以继续发送数据)
1.2.5.拥塞控制
为什么要有拥塞控制? | 答:流量控制、拥塞控制完全是为了解决不同的问题产生的。① 流量控制主要是接收端为了防止发送方发送数据的速度,一直给发送方应答一个接收窗口。② 拥塞控制主要是通过拥塞窗口cwnd来防止过多的数据注入到网络中,导致网络拥堵。 实现手段:通过“发送方”的拥塞窗口cwnd与门限值ssthresh大小对比,根据比对结果,调用响应的拥塞控制算法。 | 拥塞控制的工作过程:拥塞窗口cwnd/门限值ssthresh | 1. 慢启动 ????????一开始,建立连接后,将cwnd设为1,cwnd是指数增长的;直到cwnd增大到门限值ssthresh,就会进入拥塞避免阶段 | 2. 拥塞避免 ????????此时,拥塞避免阶段,cwnd是线性增长的(“加法增大”);直到发生【网络超时】,就会进行调整:门限值ssthresh设置为窗口的1/2,cwnd设置为1,继续执行慢开始算法 | 3. 快重传(3次应答-->丢包-->不等超时,直接重传数据) ????????当发送方收到同一个包的连续3次重复确认时(说明存在网络丢包),此时,不用等待【网络超时】,直接重传数据数据给接收方。(即:接收方向发送方回复了1,2,4包的应答,且连续对包2进行了3次应答,说明包3丢失了) | 4. 快恢复(与快重传一起使用) ????????当发送方连续收到同一个包的连续3次重复确认时,执行“乘法减小”算法(即:门限值门限值ssthresh设置为窗口的1/2,但cwnd设为门限值ssthresh,执行拥塞避免算法) |
1.3.三次握手
1.3.0.三次握手过程
1. | C向S发送一个同步请求数据包去建立连接,(C进入SYN-SEND状态) | | 2. | S接收到这个同步请求数据包后(结束LISTEN状态),会对C回复一个同步确认包,(S进入SYN-RCVD) | 3. | C收到这个同步确认包后,再对服务器再发送一个同步确认包,C/S一起进入ESTABLISHED状态 ?
- 序列号seq=X+1,表示收到S的确认号ack,将其作为自己的seq
- 确认号ack=Y+1,表示收到S的序列号seq,将其值加1作为自己的确认号
| | 注意 | ① | ACK标记和ack序列号不是一个东西 | ② | ack序列号的值,一定是请求号seq+1 | ③ | C是主动方,S是被动方 | ④ | 步骤1,2中的seq是随机生成的 |
?1.3.1.为什么序列号syn是随机值?
????????① 防止因为网络延迟导致后到的网络包建立连接,产生错误
????????② 防止黑客伪造攻击
1.3.2.TCP三次握手,前2次握手的序列号有关系吗?
答: 没有关系,都是随机值
1.3.3.为什么返回时ack值是seq值加一?
????????仅当ACK标志为1时有效。确认号ack表示期望收到的下一个字节的序号
?1.3.4.为什么要进行3次握手?而不是2次或者4次?
如果只进行2次握手,描述如下:C发送了创建链接后,S端回复了应答ACK后,就认为链接建立成功。
分析:
????????假设S回复的应答ACK丢失了,①因为C没有收到应答ACK,C会一直等待S的应答,②而S认为链接已经建立了,S就会想C发送数据,③C端接收到S端发来的数据,它是不会接收的,因为C要先收到S端的应答ACK后,才能接收S端到来的数据。 ===> 这样就会形成“死锁”,即①C一直等待S的应答ACK,②S一直向C发送数据
1.3.5. listen的第2个参数(两个队列:syn请求队列/已连接队列)
????????listen函数将主动套接字转换为被动监控套接字,其第二个参数backlog决定了内核的连接缓存队列长度。对于一个给定的监听套接字,内核维护两个队列:
① 未就绪队列,存放没有完成三路握手的连接,监听套接字收到SYN并返回ACK+SYN,连接处于SYN_RECV状态,等待对端发送ACK。如果已完成队列非满,则接收ACK,连接握手完成,进入已完成队列;如果已完成队列满则丢弃ACK,对端重发ACK(对端看到的连接是ESTABLISED状态),若未就绪队列中的SYN_RECV等待直到超时还没进入已完成队列则丢弃连接(对端不知道,只有在读写套接字时才知道)。
② 已完成队列,存放已经完成三路握手的连接(ESTABLISHED),等待accept取走连接。
????????backlog决定了两个队列的长度之和(并不是说两个队列之和等于backlog,而是存在个转换,依赖于具体实现)。
如果未就绪队列满则忽略新到来的SYN请求,对端重发,如果一直不能进入未就绪队列则对端connect失败返回。
当监听套接字关闭时:① 会对已完成队列中的每个连接发送复位分节RST,对端捕获RST被动关闭连接;② 直接释放未就绪队列的连接,这时对端不知道,对端的连接状态依然保持ESTABLISHED状态,直到对端主动关闭连接,由于监听端已经关闭连接,所以以RST响应对端的FIN,对端收到RST直接关闭连接。(类似于半打开连接)
1.3.6. SYN泛洪攻击/DDOC攻击
原理 | 攻击者构造虚假的IP,向服务器发送SYN包,服务器接收到SYN后,将会将该连接请求加入未连接队列,这样,之后再来的连接请求将会被丢弃(导致其他的连接无法进入);另外,维护这些SYN请求,也需要消耗大量的资源 | 防御 | 限制SYN的并发数量 | 防火墙禁止某些恶意的IP地址 | tcp_syncookies:将半连接请求数量超出tcp_max_syn_backlog时,内核会自动启用SYN cokkie机制,不再把半连接请求放入队列,而是用SYN cookie来检查。 根据请求包头信息计算cookie,返回给发送端,(此时并不把连接请求加入请求队列);接收发送端回复的ACK,查看ACK和cookie中的序列号是否一致,如果一致,就是正确的连接。 |
?1.3.7.??????连接建立是在accept函数么?
socket客户端连接上服务端是在listen之后而非在accept之时 | | 在刚刚接触网络编程时,很长一段时间都以为只有服务端调用accept后,客户端才会connect成功,但是实际上①只要服务端开启listen,客户端调用connect时,就会连接成功(即三次握手成功)。②accept,只是将fd从连接队列中移除 【结论】:此时,即使服务端在listen后调用了sleep后,没调用accept。客户端仍然可以调用send发送数据,也会发送成功。 |
1.3.8.非阻塞套接字Connect
① | 如果connect返回值为-1,如果错误码不是EINPROGRESS,说明出现错误,直接return;如果错误码是EINPROGRESS,意味着:表示此时TCP三次握手仍在进行。之后可以使用select检查连接是否建立成功 | ② | 给select设置等待时间,并将打开的socket添加至select监控(即使用select函数等待正在后台连接的connect函数):
- 如果只可写,说明连接成功
- 如果描述符既可读又可写,分为两种情况
- socket连接错误(这是系统规定的,可读可写时可能是connect连接成功后远程主机断开了连接close(socket)
- connect连接成功,socket读缓冲区接收到了远程主机发送的数据,可以用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int))来得到error的值来判断,如果为0,则说明socket connect成功。否则,也是失败。
|
1.4.四次挥手
1.4.0.四次挥手过程
1. | C向S发送TCP报文(FIN标记位,seq=随机),试图断开连接,随后C进入FIN-WAIT-1阶段,即半关闭状态(即:停止C向S发送数据,但是C仍然能接收S传来的数据) | 半关闭状态:关闭了“写”方向,保留了“读”方向 | 2. | S接收到C发来的TCP报文后,回复确认报文,结束ESTABLISHED阶段,S进入CLOSE-WAIT阶段,即半关闭状态 此时,C接收到S回复的确认报文后,将进入FIN-WAIT-2阶段 | 3. | S端经过CLOSE-WAIT后,将发送带有FIN的断开连接的报文,S随即进入LAST-ACK阶段。 C端收到S端发来的报文后,得知了S端已经做好释放连接的准备,随即进入TIME-WAIT阶段 | 4. | C在进入TIME-WAIT状态时,会立即向S发送最后的确认报文。S收到该报文后进入CLOSED状态。 |
1.4.1.为什么是4次挥手?不是3次?
三次握手,是因为第二次的ack和seq是同时发送的,而四次挥手不行,因为
- 四次挥手要保证全双工的通道断开
- 第一对FIN和ACK,只是断开了发送通道,而此时接收方的缓冲区中可能还有数据未接收,因此要等到缓冲区中无数据后,才能发送第二队FIN和ACK,断开接收通道
1.4.2.close/shutdown、引用计数
close | close函数会关闭套接字,但是如果调用close时,有其他进程共享着该套接字,那么该连接仍然是打开的,该连接仍然可以被其他打开的进程读写。 | shutdown | shutdown会切断进程共享的套接字的所有连接,不管引用计数是否为0,都会关闭。 关闭的方向由第2个参数决定SHUT_RD/SHUT_WR/SHUT_RDWR。 | ※ 陈硕之muduo源码 | 分析:在陈硕写的muduo源码库中的TcpCOnnection,没有提供close,而只是提供了shutdown,这么做的目的是为了保证收发数据的完整性。 | 它相当于是将“主动关闭连接”这件事分两步做,①A调用shutdown关闭写方向/保留读方向后,B read将返回0,紧接着B将手动调用shutdown关闭读方向/保留写方向。(此时A/B都进入半关闭状态)②当B发送完数据后,B调用shutdown关闭写方向后,A read将返回0,紧接着A将手动调用shutdown关闭读方向(此时A/B都进入全关闭状态) | ????显然,这样做的好处是:能够保证数据收发的完整性,即:当某个方向上没有数据收发时,可以手动地控制。保留另外一个方向数据的传递! |
1.4.3.TIME-WAIT存在的原因? 为什么主动方在TIME-WAIT阶段要等待2MSL?
2MSL | 1MSL,即一段TCP报文在传输过程中的最大生命周期(centos-30s,unix-60s) | 存在 原因 | 等待2MSL的根本原因:为了实现TCP全双工连接的可靠释放。 在第4次挥手时,C端向S端发送最后的ACK,也可能会丢失,导致S端收不到该确认报文。 ==> 当S端在1MSL时间内没有接收到C端发送的ACK确认报文时,(由于TCP的重传机制)S端就会再次向C端重新发送FIN报文,这样C端接收到FIN报文后,会重新发送最后一个ACK报文,直到S端能成功的接收到ACK报文后,连接才真正的断开! | | 为了使旧的数据包在网络中因过期而消失,防止新的连接接收到旧的数据报文 先假设没有TIME-WAIT状态的限制,如果当前有一个TCP连接(local_ip, local_port, remote_ip,remote_port)在断开的同时,以相同的4元组去建立一个新连接。那么TCP协议栈就无法区分前后两条TCP连接是不同的,在它看来,这根本就是同一条连接。(导致==> 前一条TCP已经关闭的连接发送出去的数据,通过之后的新连接,仍然可以发送给S端)。 由于TIME_WAIT阶段会等待2MSL,那么旧的数据包一定会超时而消失,就不存在上面的问题了! |
1.4.4.TIME-WAIT过多? 危害? 应该怎么解决?
常识 | 主动调用close的一方,会在发送最后一个ACK后,进入TIME-WAIT状态 | TIME-WAIT过多的原因:大量的短连接 | | 许多短连接正在被快速的创建和关闭,由于关闭时需要等待2MSL,可能导致出现大量的TIME_WAIT | 危害 | TIME-WAIT的每个套接字都会消耗套接字,导致fd被耗尽 | 套接字还绑定了端口/地址,如果不设置地址复用,在TIME-WAIT期间不能重用该地址 | 解决 方案 |
- 优化系统,减少短连接的次数
- 参数配置
- setsockopt设置地址复用SO_REUSEADDR选项(作用:通知内核,如果端口port忙,但TCP状态为TIME-WAIT状态,可以重用端口)
- setsockopt设置 SO_LINGER 选项 (后面详细介绍)
| |
1.4.5.SO_LINGER与RST
作用 | 设置close()关闭TCP连接时的行为。当socket发送缓冲区有数据残留时,
- 默认的行为,一直等待数据发送给对方,数据缓冲区中没数据后,才close
- 可以设置的行为
- 立即关闭该连接。直接丢弃发送缓冲区中的数据,并向向对方发送RST标记,来关闭该连接。主动关闭的一方将跳过TIMEWAIT状态,直接进入CLOSED状态。
- 给关闭连接设置一个超时时间。[1] 如果超时时间没到,且缓冲区中的数据被全部发送,内核将用FIN/ACK/FIN/ACK的方式关闭连接。[2] 如果超时时间到了,且缓冲区中依然有数据,则采用方式a的处理方式。
【备注】网上很多人想用方式a避免TIMEWAIT,但是,这并不是一个好的注意,这种关闭方式的用途不在这里,实际用于在于服务器在应用层的需求! | | | 分析 | 从“作用”可以看出,发送RST是一种【异常关闭】的方式,它会丢弃缓冲区中的数据,发送RST强制关闭。 【正常关闭】是指,通过四次挥手,等缓冲区的数据被清空后,再安全可靠的关闭 | 异常关闭的优点 | | 丢弃任何待发的无意义数据,立即发送RST报文,跳过TIMEWAIT,进入CLOSED状态 | | RST接收方,可以利用关闭方式来区分主动关闭方是正常关闭还是异常关闭 | 补充 | | | 值得注意的是,接收RST的一方的行为是什么呢? | | ? ? ? ? 接收RST的一方不会向发送方做任何的响应,它会立即终止连接 |
1.4.6.CLOSE_WAIT过多?应该怎么解决?
原因 | 多半是程序的原因,还是交给程序猿吧!是因为被动关闭方未调用close() | 危害 | 大量的CLOSE_WAIT会消耗掉系统资源,导致fd不被释放。 | 分析 | 若被动关闭方不关闭发送FIN给主动关闭方,此时,被动关闭方就会进入CLOSE_WAIT状态。一个CLOSE_WAIT会维持至少2个小时的时间(系统默认超时时间的是7200秒,也就是2小时)。 | 场景 | Client发送FIN给Server,Server并没有调用close函数发送FIN,那么Server进入了CLOSE_WAIT状态。 | 解决 方案 | 修改TCP/IP配置参数 | tcp_keepalive_time | CLOSE_WAIT状态的时间,默认2h(改小) | tcp_keepalive_probes | 发从探测的次数(改小) | tcp_keepalive_intvl | 重新发送探测的频度(改小) | 代码需要判断socket,一旦read返回0,断开连接,read返回负,检查一下errno,如果不是AGAIN,也断开连接 | 心跳检测:定期向socket发送指定格式的心跳数据包,如果接收到对方的RST报文,说明对方已经关闭了socket,那么也关闭这个socket。 |
1.4.7. Nagle算法
背景 | 在使用一些协议通讯的时候,比如Talnet,会有一个字节一个字节发送数据的场景,(但是,每次发送一个字节的有用数据,却需要产生20个字节的IP头/20个字节的TCP头),这就导致了发送一个字节的数据需要携带至少40个字节的协议头。当发送频率很高的时候,会有很多小包没得到确认,无疑会产生很大的浪费,造成网络上充斥着很多small packet时,会造成网络拥塞。 | 思想 | Nagle算法思想 | | 发送方发送数据后,在没有收到确认ACK前,不能继续发送其他数据(它保证了TCP连接上最多只能有一个未被确认的发送数据) | 目的 | Nagle算法的目的是:避免网络中出现大量的Small packet,但与此同时,因为只能一个小包得到ack后,才可以发送下一个小包,所以会造成网络传输速度下降(但是Nagle算法不会那么傻,它会将小包合并,一起发送) | 实验 | 通过实验来看Nagle算法对发送的优化 | | 实验设计:发送方每次发送一个字节的数据给接收方 | | 应用层调用send连续5次发送数据,由于Nagle算法,会导致①多个数据合并成一个数据,一起发送,这样大大减少了small packet的数量,(增加了TCP传输的效率)。②数据不会连续被发出,发送第一个数据后,会等待接收到确认ACK后,才发送第二个数据包,(这样会导致实时性不强) | 场景 | Nagle算法默认是开启的,该算法比较适用的场景是:发送方发送大批量的小数据,且接收方会及时的做出回应。 | 选项 | setsockopt(client_fd, SOL_TCP, TCP_NODELAY,(int[]){1}, sizeof(int)); |
1.4.8.延迟确认Delay ACK
原理 | ① A给B发送数据后,B不会直接给A发送应答(Delay ACK);当且仅当B有数据发送给A后,顺便将应答放在数据包中,一起发送给A。 ② 如果等了一段时间(约40ms),没有数据从B发送给A,就回复一个“纯”确认ACK给A。 【总结】显然,上面过程,不是直接应答,中间发生了延迟确认。 | | 这样做的好处是:相当于一个司机拉客人,他将客人拉到目的地后,不直接空车返回,而是等接到返程的客人,一起返回。 很明显,这样的方式是存在一定收益的!都是为了提高TCP性能 |
1.4.9.在Delay ACK开启时,一定要关闭Nagle算法
分析 | Nagle算法本身的立意是好的,避免网络充斥着过多的小包,提高网络传输的效率。与此同时,Delay ACK也是为了提高TCP的性能,不过二者遇到了,就比较悲剧了。 | | | | 如果同时开启了Nagle算法和Delay ACK,对于write(header)-wirte(body)-read(response)场景,会产生极大的副作用。 | 原因 |
- 第一个header一定是能够发送出去的
- 由代码可知,接收方接收到TCP报文头header后,发现是不完全的,还会再次等待Body数据的到来,由于开启了Delay ACK,因此,不会立即回复ACK给发送方
- 而,发送方由于开启了Nagle算法,由于没有收到ACK,则不会再发送body给接收方。
- 直到接收方的Delay ACK超时时,才会向发送方应答确认ACK
- 接收方接收到应答ACK后,才能继续发送body数据。
| 分析 | 显然,发送方每次都要等待Delay ACK超时的应答到来后,才可以继续发送body数据,这会产生巨大的延迟。 | | | | 这个问题的产生,主要是Nagle、Delay ACK副作用,以及write-write-read程序造成的,一般写程序的时候,不推荐这样的写法。 | 如何 解决 | 方案1:禁止掉Nagle算法。但是这样做,会导致网络中充斥着大量的small packet(数据header),降低效率。 方案2:常规的解决方案,都是要避免应用程序出现write-write-read的写法 |
2.UDP
2.1.UDP报文
与TCP相比
- 都有端口
- 但是没有复杂的其他标识
- 多了数据长度(报文)
可以看到,UDP与TCP相比,没有bind/connect函数。 S端:它在bind后,就可以直接调用sendto/recvfrom函数收发数据;不需要listen/accept C端:直接就可以调用sendto/recvfrom函数收发数据;不需要connect? | |
2.2. UDP的connect
????????connect(client_fd,?(struct?sockaddr*)&server_addr,?socklen)
2.2.1.???????UDP/TCP的connect的区别
先说明 | UDP中的connect与TCP中的connect有着本质上的区别:
- TCP的connect会引起三次握手;UDP不会。
- 由于UDP是无连接的,在调用connect时其实没有向外发包,只是在协议栈中记录了该状态,
| | 【补充】采用connect的UDP发送接受报文可以调用send,write和recv,read操作.当然也可以调用sendto,recvfrom |
- 异步ICMP错误不会返回给unconnect的UDP套接字,调用connect后,可以接收到异步ICMP错误
- 提高发送数据的效率
- ???????普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
-
采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点, ?每次发送报文内核都由可能要做路由查询???????
Q1: UDP是否可以多次调用connect?
A1:?UDP可以多次调用connect,而TCP只能调用一次connect
Q2:????????UDP多次调用connect的目的?
A2:?断开和之前的ip,port的连结,与新的ip,port连接
3.QA
Q1: TCP和UDP的区别
A1:
TCP | UDP | 面向连接(可靠) | 面向非连接(不可靠) | 流协议(引起粘包问题) | 报文(有边界) | UDP处理/传输速度更快 | UDP不具有TCP那样的流量控制,它是尽自己最大可能的速度传输数据 | TCP维持数据的可靠有序发送,成本更高,消耗更大 | 数据流协议,包大小不受限制 | 包大小不超过MTU(1400),超过会报错or丢包 | TCP头大小20 | UDP头大小8 |
Q2: 既然TCP可靠,为什么很多项目仍然选择UDP?
A2:
- UDP不用考虑数据的可靠性,传输效率更快,尽最大可能投递数据。很多项目如果不用太考虑可靠性,但是需要很快的传输效率时,就会使用UDP。比如:抖音视频、语音视频,都是采用UDP。这几种应用允许数据丢包,如果非要使用TCP,那么丢包时将会重传数据,造成卡顿!(一般都是在应用层实现可靠UDP方案)
-
物联网中的应用:长连接耗电 音视频通话:TCP延迟大
Q3:?TCP是可靠的,为什么通信会丢包? 怎么解决?
A3:?
????????例如服务器给客户端发大量数据,Send的频率很高,那么就有可能在Send时发生错误(原因可能是又多种,可能是程序处理逻辑问题,多线程同步问题,缓冲区溢出问题等等),如果没有对Send失败做处理重发数据,那么客户端收到的数据就会比理论应该收到的少,就会造成丢数据,丢包的现象。
????????这种现象,其实本质上来说不是丢包,也不是丢数据,只是因为程序处理有错误,导致有些数据没有成功地被socket发送出去。
????????常用的解决方法如下:拆包、加包头、发送,组合包,如果客户端、服务端掉线,常采用心跳测试。
Q4: KCP
A4: 可靠的UDP
Q5: 数据发送的整个过程
A5: 参考链接
Q6: 数据传输的过程中,哪些情况会丢包?
A6:?
- 在两台 VM 连接之间,可能会发生传输失败的错误,比如网络拥塞、线路错误等;
- 在网卡收包后,环形缓冲区可能会因为溢出而丢包;
- 在链路层,可能会因为网络帧校验失败、QoS 等而丢包;
- 在 IP 层,可能会因为路由失败、组包大小超过 MTU 等而丢包;
- 在传输层,可能会因为端口未监听、资源占用超过内核限制等而丢包;
- 在套接字层,可能会因为套接字缓冲区溢出而丢包;
- 在应用层,可能会因为应用程序异常而丢包;
- 此外,如果配置了 iptables 规则,这些网络包也可能因为 iptables 过滤规则而丢包
|