基础知识
传输层
负责数据能够从发送端传输接收端。
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在 TCP/IP 协议中, 用 "源 IP", "源端口号", "目的 IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过 netstat -n 查看);
端口号范围划分
0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的。
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
认识知名端口号
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
ssh 服务器, 使用 22 端口
ftp 服务器, 使用 21 端口
telnet 服务器, 使用 23 端口
http 服务器, 使用 80 端口
https 服务器, 使用 443
执行下面的命令, 可以看到知名端口号:
cat /etc/services
netstat
netstat是用一个来查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态
常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
I 仅列出有在 Listen(监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
pidof
在查看服务器的进程id时非常方便。
语法:pidof[进程名]
功能:通过进程名,查看进程id
UDP
UDP 协议端格式

这是报文,8字节是报头部分,剩下部分就是有效载荷。
16 位 UDP 长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
如果校验和出错, 就会直接丢弃;
UDP特点
UDP 传输的过程类似于寄信.
无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP 协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量;
面向数据报
应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并;
如果发送端调用一次 sendto, 发送 100 个字节, 那么接收端也必须调用对应的一次 recvfrom, 接收 100 个字节; 而不能循环调用 10 次 recvfrom, 每次接收 10 个字节;
UDP 的缓冲区
UDP 没有真正意义上的 发送缓冲区. 调用 sendto 会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP 具有接收缓冲区. 但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致; 如果缓冲区满了, 再到达的 UDP 数据就会被丢弃;------UDP不保证可靠性
基于UDP的应用层协议
NFS: 网络文件系统。
TFTP: 简单文件传输协议。
DHCP: 动态主机配置协议。
BOOTP: 启动协议(用于无盘设备启动) • DNS: 域名解析协议。
DNS: 域名解析协议。
TCP
TCP 全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;(这里的控制很重要)
用户在使用read,write,recv,send接口时,本质并不是将数据发送到网络中,而是将数据拷贝到TCP传输层的发送缓冲区当中,或者是将接收缓冲区的数据拷贝到用户级缓冲区当中。
也就是说,数据何时发送,发送多少,出错了怎么办。这些都是TCP协议自主决定。(所以才叫传输控制协议)
网络传输过程中本质也是缓冲区之间的拷贝。(缓冲区其实就是内存块)
UDP 的 socket 既能读, 也能写, 这个概念叫做 全双工。
TCP 协议段格式
源端口号和目标端口号:
与UDP相同功能,交付给上层。
4位首部长度:
报头的长度,这里的报头包含选项,但是4位的首部长度就说明有4个比特位,那么区间就是[0000,1111]算起来最大也就15字节,如何装得下20字节的标准报头和选项呢?因为计算的时候有基本的单位大小:4字节,也就是乘以4字节,最大就是60字节。
也就是说,实现报头和有效载荷的分离时,只需要读到前20字节,然后看4位首部长度,减去20在拿到选项,就可以实现分离了。
16位窗口大小:
首先要清楚,客户端和服务器基于TCP协议进行通信的时候,互发消息的时候,发送的可是完整的TCP报文,一定要携带完整报头。
在客户端发送数据的时候,服务器的接收缓冲区快要满了,但是客户端不清楚,还在不停的发数据,这时候,服务器缓冲区一旦满了就容易发生丢包的情况,这是不可靠的。
那么就要通知客户端,让客户端发送数据适量或者是别发了,通过控制发送数据的速度,让服务器来得及接收,防止发生大面积丢包的情况,这叫做流量控制。
TCP凭什么保证可靠性?
确认应答。
也就是说,如果客户端给服务器发送一个消息,服务器就要做一次确认应答。(无论服务器有没有想法的消息给客户端,并且这个应答至少要有一个报头)
确认应答就代表会告诉发送方自己的接收缓冲区还有多少空间,从而改变发送方的速度。
对于发送方来讲,发送速度取决于对方接收缓冲区中剩余空间大小决定。
所以16位窗口填充的就是自己的接收缓冲区剩余空间大小。(因为TCP是全双工的)
32位序号与32位确认序号:
首先考虑一个问题,双方在进行聊天的时候,如果都进行确认应答,没有应答的数据,就代表没有可靠性;双方互相发消息,突然中止,那么总有一条消息是最新的,这个消息没有进行应答,所以无法保证发出去的消息是100%可靠的。
所以只需要客户端发送数据,服务器收到应答即可,并且客户端收到这份应答,这个时候客户端并不需要再去发送对服务器的应答,服务器不需要知道客户端是否收到这份应答,客户端知道即可。
同理,对于有服务器发送给客户端数据也一样。
注意:如果客户端长时间没有收到应答,那么客户端就认为发送的数据丢失了。(丢失就要重发)
这样看起来有一点点的麻烦,如果恰好客户端给服务器发消息,服务器收到想应答,并且也想给客户端发消息,那么这个时候可以让应答和自己要发送的TCP数据一同发送过去。(这种叫做捎带应答)
并且,真正TCP发送数据的时候,不会一条发一条应答,而是多条发送过去,多条应答,那么发送的顺序在接收时如何确认呢?(如果不管这个问题,在接收缓冲区就是乱序的,乱序本身就是不可靠的一种)
所以32位序号时保证数据按序到达。
序号是什么?
首先,从用户层拷贝到TCP发送缓冲区时按字节排序的,一行一行的消息也是以\n结尾的。(所以缓冲区相当于一个char类型的数组)
也就是说,每个字符都有自己的编号,编号就是数组的下标。
假设要发送一个数据块,这段数据块的最后一个字符的下标就是序号 。
确认序号方式,填充的是,收到报文的序号+1。(例如客户端发送给服务器1000,服务器发给客户端就应该是1001)
为什么这样规定呢?意义就是确认序号+1之前的数据全部收到了,下一次发送请从确认序号开始发送。
因为确认序号的方式是序号+1,假设客户端发送了1000,2000,3000,客户端收到的应答只有3001,那么就默认服务器收到了3001之前的数据,也就是说,允许数据少量的丢失。
可是TCP的报头还有一个32位确认序号,一个32位序号不就足够了吗?为什么还有32位确认序号呢?
因为一个报文可能有双重身份,上面的捎带应答就是例子,客户端给服务器发消息,服务器要应答,恰好服务器也要给客户端发消息,所以就会有自己的序号和应答客户端的确认序号。
六个标志位与16位紧急指针
客户端发送给服务器TCP报文的时候,有各种请求,例如建立连接,正常数据通信,断开连接,这就导致TCP收到的报文有各种类型,不同的类型决定了服务器做不同的动作。
那么接收方如何得知报头的类型是什么?用的就是六个标志位。(标志位就是为了不发送数据,通过报头进行传递信息)
ACK:确认序号是否有效(也就是应答消息)
SYN:请求建立连接; 我们把携带 SYN 标识的称为同步报文段(TCP连接建立的请求)
FIN:通知对方, 本端要关闭了,我们称携带 FIN 标识的为结束报文段(表示要断开连接)
PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走(流量控制那里,上层拿走TCP的缓冲区数据,有空位就会传输过来新的数据,其实就是生产消费者模型,也就是同步)假设客户端给服务器发消息,服务器的接收缓冲区已经满了,客户端一遍一遍询问什么时候拿走缓冲区中的数据,但是服务器一直没有拿走,客户端又要发送很多必要的数据,这个时候就会在报头当中使用PSH标志位,服务器就会快速给自己的接收缓冲区腾出空间。
RST:对方要求重新建立连接,我们把携带 RST 标识的称为复位报文段(TCP虽然可靠,但是TCP允许连接建立失败)建立连接,客户端会有很多应用和服务器建立连接,这些连接都需要被管理
这里是TCP三次握手的方式,客户端给服务器发送请求建立连接,服务器给回应和请求建立连接,最后客户端给服务器应答,但是在这个过程中,客户端和服务器成功建立连接是不同的,客户端认为,自己发送完ACK就是连接建立成功了,但是服务器是并不是在第一次客户端请求的时候就先建立连接,而是等客户端连接建立成功之后才会进行建立连接。(也就是说客户端的ACK没有应答,服务器没接收到应答,就不会建立连接,建立连接也就是失败了)
如果发生了客户端发送的ACK丢失情况下,客户端给服务器发送报文,那么服务器就会检测出来问题,然后向客户端发送携带RST的报文,然后客户端收到这个消息之后,就会重新进行TCP三次握手建立连接。
URG:紧急指针是否有效(因为数据都是按序到达的,正常情况下不可能出现数据插队,但是总有一些紧急数据需要提前处理,进行插队。设置URG标志位,0为无效;16位紧急指针就无效,1为有效,16位紧急指针就有效,这说明当前报文有紧急数据,紧急指针就是偏移量,从头开始+便宜量就是紧急数据)假如客户端的某个软件使用的时候突然卡住了,这个时候客户端就会向服务器发送一个紧急数据,服务器有自己的历程,专门读取紧急数据(也就一个字节),然后返回软件功能提供状态编号,再通过紧急方式发给客户端(也叫带外数据,普通传输是带内数据),客户端收到消息就会知道原因了。
超时重传机制
场景一:
主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;
如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发;(这就叫超时重传)
场景二:
但是, 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了;
因此主机 B 会收到很多重复数据. 那么 TCP 协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉. 这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.
那么,超时的时间如何确定呢?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包.
TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传.
如果仍然得不到应答, 等待 4 500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
TCP的连接管理机制
在进行三次握手和四次挥手的时候,互相发送的全都是报头,仅仅是6位标记为被修改了而已。
三次握手(建立连接)
客户端在使用connect的时候,先发出SYN后,然后OS进行接手,双方的OS在底层进行TCP建立连接,在这个过程中connect在阻塞等待。服务器的accept也不会参与三次握手的过程,它只负责获取建立好的连接,如果没有建立好的连接,就会一直阻塞住。
并且,在双方建立对应连接的时候,都是有状态的,客户端发送请求连接会处于SYN_SENT(同步发送),服务器收到会处于SYN_RCVD(同步接收),然后在响应给客户端,客户端接收到状态就会变成ESTABUSHED(也就是建立连接成功,注意这时服务器还不是ESTABUSHED,也就是上面所说的在客户端响应到达之后才建立成功),最后客户端发送给服务器响应,服务器接收到状态就变成了ESTABUSHED。
四次挥手(断开连接)
客户端请求断开连接,服务器应答,客户端就单方面断开连接了(服务器并没有和客户端断开连接,还能给客户端发消息),然后服务器也向客户端发送断开连接,客户端进行应答,就完成了四次挥手断开连接。
为什么要进行三次握手和四次挥手?
首先思考一个问题,三次握手真的只是进行了三次握手?实际上是服务器对客户端的应答和请求连接合并在了一起发送,那么四次挥手也可以进行合并,变成三次挥手。
三次握手合并是因为客户端请求建立连接,服务器是服务方,肯定收到请求会接收,所以就可以压缩成一个;四次挥手则不一样,因为客户端断开连接服务器不一定要断开连接,想压缩在一起要有巧合行。
1.三次握手本质就是双方互相发送报头,其实也是在验证全双工通路是否通畅。
2.如果只进行一次握手(客户端给服务器发建立请求,服务器不做应答),这样客户端根本无法知道是否建立成功,并且,如果客户端发送大量的SYN,服务器就要建立连接,创建对应的端口,这样就会造成服务器资源紧缺,全都被一个客户端占用了(这也叫SYN洪水),所以一次握手行不通。
那么二次握手呢?(客户端不发ACK)这样服务器就要在客户端第一次发送SYN的时候就要建立连接,然后回给客户端SYN+ACK,客户端接受之后建立连接,突然客户端崩掉了,出现异常了,服务器的就会一直等待客户端发送消息,如果一直没有消息才会关闭,但是这个周期太长了,很浪费资源,所以二次握手也行不通。
所以三次握手,即便是客户端突然崩掉了,服务器也不会损失,即便是客户端没崩,客户端发送的ACK丢失了,服务器默认建立连接失败,不进行连接创建,失败的成本也转移到了客户端的身上(因为客户端居多,服务器只有一个)。------奇数次握手,可确保一般情况,握手失败的连接成本是嫁接在client端的,并且三次握手也是验证全双工的最小次数。
3.断开连接的本质是没有数据给对方发送了,发送数据是双方都可能发,所以,必须断开两次。
listen的第二个参数
在accept函数接收之前,连接会被管理,连在一起,成队列。(也叫全连接队列)
backlog+1表示底层已经建立好的连接队列的最大长度。
也就是说,这里只要设置成2,最大队列就是3,在客户端想建立第四个连接的时候,服务端看到全连接队列满了,就会将客户端的第四次发送的ACK报文丢弃,状态依然是SYN_RCVD。
并且SYN_RCVD这个状态不会长时间去维护,被建立连接的一方,处于SYN_RCVD,是半连接,也存在自己的队列,不会长时间维护。(在进入全连接之前要先进入半连接,收到ACK之后才会让半连接进入全连接)
为什么listen的第二个参数不能太长也不能为0?
如果全连接队列太长,服务器上层本来就繁忙的情况下还要花大量资源去维护这个队列,就没有必要了,上层会很长时间不会拿走队列当中的数据,不会创造任何价值,反而浪费资源。
如果为0,就是送来来一个处理一个,没有等待的情况下,突然有空余的位置,然后没有队列进行快速补位,这样也会造成资源的浪费。
至于长度多少合适,这个要根据自己机器的硬件来决定。(普通电脑设置32就已经够大了)
为什么在使用完TCP之后不能马上重新连接之前的端口号?
有一个现象,主动断开的一方,在四次挥手之后,断开方要进入time wait状态(假设服务器先发送FIN收到ACK,服务器就进入这个状态),等待若干时长后,自动释放。(连接其实没有被彻底断开,ip和port正在被使用)
这里可以使用这个接口让time wait取消,让这个ip和port重新使用,不要影响后续的使用。
TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态.
我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在TIME_WAIT 期间仍然不能再次监听同样的 server 端口;
MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, Centos7 上默认配置的值是 60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值;
那么为什么要进入这个状态呢?因为在通信的时候,各种管道难免有未消散的数据,这个等待就是等待这些数据消散。(不要影响后续通信)
并且,假设在最后两次传递的过程中,万一FIN或者ACK丢了,在等待的过程中说不定还能收到补发过来的ACK,这会让四次挥手的成功率大大提升。如果长时间没收到丢失的FIN和ACK,时间一到,端口自动关闭,也默认对方关闭了连接。
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过 ACK 端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
那么第一次通信的时候,如何保证发送数据量是合理的?
1.当然是在TCP三次握手的时候,双方互相发送的报文里面就携带了相关信息,协商双方的接收能力。
2.TCP三次握手第三次的时候,是可以携带数据的(捎带应答),也是协商接收能力。

窗口探测与窗口更新通知发送的都是报头。
如果探测和通知一段时间后都无法到达,那么网络就出问题了,这个时候就会关闭这个网络连接。
接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中, 有一个 16 位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么?
实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位。
流量控制既可靠又有效率。
滑动窗口
客户端与服务器通信的时候,客户端给服务器发信息,总有未收到应答的情况,那么这段没被应答的数据是要被TCP暂时保存起来的,并且会存在多个,具体保存在了哪里呢?是被存在了发送缓冲区里。
实际上TCP的发送缓冲区分为好几个部分:
上面所说的数据都被放进了已发送未确认,但是随着时间可能又有确认应答发过来,已发送未确认就会变短,或者是发送的快,这个范围越来越长,这种就叫做滑动窗口。(是发送缓冲区的一部分)
已发送已确认的这个范围是可以被覆盖的,表示无效数据,也就相当于从TCP缓冲区当中移除了。
正是因为又滑动窗口的存在,所以才可以一次发送大量TCP报文。(滑动窗口的大小跟对方的接收窗口有关)
滑动窗口通过指针或者下标进行划分即可。
窗口越大, 则网络的吞吐率就越高。
根据上述结论,可以暂时认为,滑动窗口大小不能超过对面接收窗口大小。
如果丢包了怎么办?
场景一:数据包已经抵达, ACK 被丢了
这里无妨,上面说过,因为确认序号的原因,收到确认序号了,确认序号之前的默认就全收到了,允许少量的ACK丢失。
因为可以根据下标更新滑动窗口,例如1001ACK,3001ACK,4001ACK应答未收到,但是6001有应答,滑动窗口就会将1-6000变成已发送已确认的区域。
场景二:数据包直接丢了
如果整个数据包丢失,那么发送的主机滑动窗口就不会跳过丢失的数据段向后滑动:
例如:3001-4000丢失,左边的下标只会到3001的位置。
这是丢失数据的补发方式,叫做快重传。
传输主机的欢动窗口会一直指向最小未被应答的数据,直到收到应答或者是超时才会继续向后滑动。
(超时重传就是兜底的)
那么滑动窗口是如何滑动的呢?
方向是一直向右滑动,不会向左移动。
移动的时候,大小是动态变化的。
变大:左动,右动的比左快。
变小:左动,右不动。
不变:左右动的频率相同。
int left = 根据确认序号来进行设置。
int right = 确认序号+对方接受的能力的窗口大小(如果对方窗口非常大,但是自己的有效数据特别少,那就选小的)------这里还要考虑拥塞窗口,如果拥塞窗口小,就是确认序号+拥塞窗口,后面会介绍拥塞窗口。
所以由此得知,流量控制是通过滑动窗口来进行的。
滑动窗口会越界吗?
不会的,虽然物理上是线性结构,但是逻辑上是环形结构,类似于循环链表一样。
注意:正常序号不是从0开始的,因为会有历史数据滞留问题,会产生新连接接受老数据的问题,所以每次建立新的连接都是随机序号的最小值开始。
发送数据的时候,确认序号是随机数+数组下标,返回接受时,下次发送数据在缓冲区的位置=确认序号-随机数。
延迟应答
我们清楚:发送方一次发送更多的数据,发送的效率就越高。那么如何让自己一次发送更多数据呢?那就是让对方告诉我它可以接受更多数据。
如何让接收方给对方,更大的一个窗口呢?
收到报文之后不要急着应答,先让上层拿走缓冲区的数据,这样就能应答对方这里的接受能力很强。(这就是延迟应答)
比较推荐的做法:
每次都尽快通过read,recv尽快的把数据全部从内核中拿上来。
假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔 N 个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms;
拥塞控制
双方发送数据,不仅仅只是主机出问题,网络也有可能出问题。
1.如果通信时候出现了少量丢包?(常规情况)
2.如果通信时候出现了大量丢包?(TCP就会判断网络出问题了,网络拥塞了)
这就是网络出了问题。硬件设备出问题(大量数据都超时了),数据量太大从而引起阻塞。
这个时候发送方该怎么办呢?
首先不能立即对报文进行重发。为什么呢?
因为会加重网络拥塞的状况。(就相当于十字路口堵车)
所以我们可以先等一等不发或者是少量发送数据等一等。因为都用TCP协议实现了多主机面对拥塞时是由共识的,所以一旦发生网络拥塞,所有的主机都会执行等一等或者少量发送数据,直到网络不堵塞为止。
拥塞控制策略
注意:每台识别堵塞的机器都要做。
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP 引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念称为拥塞窗口
发送开始的时候, 定义拥塞窗口大小为 1;
每次收到一个 ACK 应答, 拥塞窗口加 1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
上面说过滑动窗口,这里补充一点,其实滑动窗口的大小是取对方接受能力窗口和拥塞窗口这两者的最小值。拥塞窗口考虑的是动态的网络接受能力。
拥塞窗口:主机判断网络健康程度的指标,超过拥塞窗口,会引发网络拥塞,是否不会。
并且,网络是动态的,拥塞窗口也不可能是静态的。
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快。
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
如果网络出现拥塞,发送少量报文,如果都可以,网络就趋于健康了,可以快速恢复正常通信了。
所以采用的是指数做法。
当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
阻塞窗口会一直上升,是不是迟早会发生网络拥塞呢?不会的,因为不仅仅有拥塞窗口限制,双方的接收窗口也会有限制!
面向字节流
用户在发送数据的时候,只关心应用层协议,并不关心网络通信细节(TCP决定)
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用 write 时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用 read 从接收缓冲区拿数据;
另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据.(全双工)
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配(这就叫面向字节流)
例如:
写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节;
读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次;(一个一个读取到自己用户缓冲区里进行拼接)
TCP内核只认识字节数据,不关心上层协议,不关心上层报文格式,只有字节的概念,这就是面向字节流。
粘包问题
用户层读取TCP缓冲区的数据,需要将字节流变成一个一个请求才能处理。
假设用户没有拼接成一个报文的处理,就会有两种状况,一种是读取了一半的报文,发现并不是一个完整的请报文,就会将这个报文丢弃,后续来的半个报文也会被丢弃;另一种是读上来了一整个报文,但是后面又带着下一个报文的上半部分。
这种状况就被称为数据包粘包问题。
解决这个问题就是在用户层定制协议:用特殊字符明确报头与数据,报文与报文之间的边界 。
1.定长报文(上层处理的报文长度)
2.使用特殊字符(比如换行符代表结尾,一行一行去读取)
3.使用自描述字段+定长报头(报头和有效载荷)
4.使用自描述字段+特殊字符(报头与有效载荷分离)
只有用户层处理完报文,才能进行序列化与反序列化。
TCP链接异常
进程终止 :连接和文件是相关的,文件声明周期是随进程的,进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别. (正常进行四次挥手即可)
机器重启 :先杀掉所有的进程,之后和进程终止的情况相同.
机器掉电/网线断开:接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset(让对方进行重建连接). 即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
TCP 小结
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
• 校验和
• 序列号(按序到达) ------还可以去重
• 确认应答
• 超时重发
• 连接管理
• 流量控制
• 拥塞控制------也提高效率
提高性能:
• 滑动窗口
• 快速重传
• 延迟应答
• 捎带应答
其他:
• 定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)
三次握手:
1.建立连接
2.协商起始序号
3.协商双方的接受缓冲区大小
TCP几乎所有策略都是在双方机器上实现的,如果网络通道出了问题,TCP也会考虑的。
如何用UDP实现可靠传输
参考 TCP 的可靠性机制, 在应用层实现类似的逻辑。
例如:
引入序列号, 保证数据顺序;
引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据。
简单理解文件与socket的关系
创建一个套接字,底层其实创建了很多个结构体,并且层层指针叠加,互相指引。
服务器收到的报文是这样的:
封装包头就是head指针向上偏移,解包就是head向下偏移。(本质就是移动指针)