前言: 传输层是实现端到端通信的核心,主要涉及TCP和UDP

16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度。
如果校验和出错,则会直接丢弃该报文。
无连接:直到对端的IP和端口号就直接进行传输,不需要建立连接。
不可靠:没有确认机制,没有重传机制;如果因为网络故障数据无法发送到对方,UDP协议层也不会给应用层返回任何信息。
面向数据报:不能够灵活的控制读写数据的次数;
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。
比如用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节。那么接受端必须调用一次recvfrom,接受100个字节的数据;而不能循环调用10次recvform,每次接受10个字节的数据。
对于TCP,上层在调用write发送数据的时候,本质是将数据拷贝到TCP的发送缓冲区中,什么时候发,发多少由操作系统据决定。发送发将数据发送给对端,本质是将数据拷贝到对端的接受缓冲区中,对端在调用 read读取数据的时候,本质是从接受缓冲区中读取数据。
所以TCP有两个缓冲区:发送缓冲区和接受缓冲区。
而对于UDP而言,是不需要发送缓冲区,它只有一个接受缓冲区。在调用sendto发送数据的时候,会直接将数据交给内核,由内核将数据交给网络层进行后序的传输动作。
UDP具有接受缓冲区,但这个接受缓冲区不能保证收到的UDP报的顺序和发送的UDP报的顺序一致;如果接受缓冲区满了,再到达的数据报就会被丢弃。
UDP协议首部有一个16位的最大长度,也就是说一个UDP能传输的数据报的最大长度是64KB(包含UDP首部)。
然而64KB在当前的互联网环境下,是一个非常小的数字。
如果我们需要传输的数据超过64KB,就需要在应用层手动的进行分包,多次发送,并在接收端手动拼装。
NFS:网络文件系统
TFTP:简单文件传输系统
DHCP:动态主机配置协议
DNS:域名解析协议

操作系统是基于中断运行的,如果应用层正在进行报文的解析,到收到新的报文时,外围设备就绪触发中断,操作系统就会从设备中(网卡)读取数据,拿到数据报。
那么此时操作系统中一定会存在大量的报文,所以操作系统就需要对这些报文进行管理。如何管理?先描述,再组织:
sk_buff结构就是用来描述一个数据报的。在Linux内核中的部分源代码如下:
struct sk_buff{
struct sk_buff* next;
struct sk_buff* prev;
//……
//……
unsigned char *head,
*data,
*tail,
*end;
}
sk_buff结构中重要的4个指针指向一段缓冲区 。有点类似于进程PCB和进程对应的代码和数据。
当数据报详细交付时,当从应用层交付到传输层时,data指针向上移动一段空间,如果使用UDP协议,移动UDP报头的大小,然后填充对应UDP报头。如果使用TCP协议,移动一个TCP报头的大小,然后填充 TCP报头。当数据报从传输层交付到网络层时,data指针向上移动,然后再填充上对应的报头 ,依次类推。
如果是向上层移动,那么就是data指针向下移动。
所以,对于数据报的解包和分用,其实就是 data指针在对应 缓冲区中的指向!!!
tcp协议,全称为"传输控制协议"。人如其名,要对数据的传输进行 详细的控制。

源/目的端口号:表示数据从哪个进程来,要到哪个进程去。
16位校验和:发送端填充,CRC校验,接收端不通过,则认为数据有问题。此处的校验,不仅包含TCP首部,也包含TCP数据部分。
4位首部长度:表示TCP报头的大小(包含选项),单位是4字节。比如,1111表示15,对应的报头大小为15*4=60字节。又因为TCP报头大小至少为20字节(不包含选项),所以4位首部长度最小的数值是5,最大是15.
在TCP报文的格式中,没有对应的数据部分的大小,只有报头的大小。
而UDP报文中记录了整个报文的大小,通过减去报头的大小,我们就可以获取到对应数据的大小。所以,对于UDP报文,数据和数据之间的边界是很明显的。
而在TCP中,是没有的,也是不需要有的。因为TCP不能保证接受上来的报文就是一个完整的请求,它将接受到的报文去掉报头,放入到接受缓冲区中,上层进行读取数据,由上层自己判断是不是一个完整的请求。而UDP可以保证,它接受上来的报文就是一个完整的请求。
所以说UDP是面向数据报的,TCP是面向字节流的。

在两台主机通过TCP进行通信的时候,如上图,主机A给主机B发送数据,主机A发送了数据,但是无法确定主机B是否成功收到了数据,此时需要主机B的操作系统给主机A发送一个应答报文。 表示主机B收到了数据,这个应答报文没有数据部分,并且6个标志位中的ACK设为1。表示该报文是一个应答报文。
注:应答是由操作系统自己完成的。主机B在接收到数据后,主机B的操作系统会自动构建一个应答报文返回给主机B。
但是有时候主机B也要给主机A发送数据,所以在主机B给主机A发送数据的时候,会携带上应答。
这种应答方式称为捎带应答。


当主机A向主机 B发送数据的时候,在TCP报头部分会填充上序号的值,同时主机B在做应答的时候,会根据序号,计算出确认序号。确认序号=序号+1。
对于主机A,也就是发送端,会收到应答报文(或者捎带应答),比如收到确认序号为2001的应答报文,主机A就会认为序号为2001之前的所有报文,对端已经全部收到了。这种方式可以大大提高数据传输的效率。
对于主机B,也就是接收端。当主机A发送数据的时候,每个数据到达主机B的时间可能不同,有的报文发的早,但是到达主机B的时间晚,有的报文发的晚,但是到达主机B的时间早。通过序号,可以将收到的报文进行排序,并且实现去重的目的(有的数据报可能会发送多次)。

主机A给主机B发送数据之后,可能因为网络拥堵等原因,数据无法到达主机B。
如果主机A在特定的时间间隔内没有收到B发来的确认应答,就会重发数据。
但是,主机A未收到主机B发来的应答时,也可能是因为应答(ACK)丢失了。

此时主机A仍然会进行超时重传,所以主机B就会收到多个重复的数据报。TCP协议是可以区分出来这些重复的数据报的(通过序号,很容易做到去重的效果),并且把重复的丢弃掉。
超时的时间是如何确定的???
这个时间应该和网络当前的状态是有关的。而网络状态时刻在变化,所以这个值应该也是一个变化的。 TCP为了保证无论在任何环境下都能够比较高效的通信,因此一般会动态计算这个超时时间。 Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位 进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
前面提到过,TCP有发送缓冲区和接受缓冲区。
当发送方 发送数据的时候,对端(接收方)的接受缓冲区可能满了,放不下数据了 。那么此时对端在收到数据后,就会将该数据直接丢弃。
这是不合理的,因为数据报在网络传输中会消耗电力,带宽等等网络资源。如果直接丢弃,这就是一种浪费资源的现象。
为了解决这种问题,发送方就需要知道对端的接受能力,也就是对端接受缓冲区剩余空间的大小。从而决定发送多少数据。
在TCP报头中,16位窗口大小,该字段就表示对端的接受能力的大小。这种机制叫做流量控制。
那么问题来了,16位表示的最大数字是65535,那么TCP接受缓冲区最大就是65535字节吗??? 实际上,TCP首部40字节选项中还包含一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位。
通过流量控制机制,一方面可以提高TCP传输的可靠性,另一方面,可以提高传输的效率。
在TCP报头中,存在六个标志位:URG,ACK,PSH,RST,SYN,FIN
首先,在使用TCP进行通信的时候,双方需要先建立连接。(三次握手) 双方结束通信的时候,需要释放对应连接。(四次挥手) 三次握手和四次挥手重点在连接管理机制部分说明。
这六个标志位,在TCP报头中对应一个比特位,要么为0,要么为1。为1表示给标志生效。
SYN:同步标志位,请求建立连接,握手过程使用 的标志位。
ACK:确认序号是否有序。ACK为1,表示该报文是一个应答报文(或者捎带应答)。
PSH:提示接受端应用程序立刻从TCP缓冲区把数据读走。
RST:对方要求重新建立连接。我们 把携带RST的报文称为复位报文段。
FIN:通知对方,本端要关闭连接了,我们称携带FIN标识的为结束报文段。
URG:标识TCP报头中的16位紧急指针是否有效。如果将URG设置为1,表示当前报文中包含紧急数据,需要将该报文立即交给上层处理。
举个例子: 当我们使用百度网盘上传文件时,将数据发送到服务器的接受缓冲区中。 上传过程中,如果发现上传错了,想要取消上传。取消上传也是以报文的形式发送给服务器的。 此时就希望服务器立马处理这个报文。而不是先将该报文放到接受缓冲区中,当前面的报文处理完了,才处理该报文。 所以此时就可以将URG置为1,表示该报文是一个 紧急报文,需要立即处理。 暂停上传也是类似的思路。 实际上,为了实现这个功能,不一定非要使用URG标志位,也可以通过建立两条连接的方式,一条连接用来发送数据,另一条连接用来发送指令。
URG为1时,表示16位紧急指针有效,假设该16位有效指针表示的数字是n。意味着在该报文有效载荷中,偏移量为n处,有紧急数据,该紧急数据的大小是一个字节。这个紧急数据本身就是一个标志位,比如,标志位为1表示取消,标志位为2表示暂停等等。
正常情况下,TCP要通过三次握手建立连接,通过四次挥手断开连接。

上图中,诸如SYN_SENT,ESTABLISHED,FIN_WAIT1等等,都表示的是当前服务器或者客户端所处的状态,其本质就是一个数字, 通过宏定义出来的。
三次握手的大致过程:客户端发起建立连接的请求SYN,服务器收到请求后。向客户端发送ACK(确认应答 ),表示收到请求了。同时服务器端向客户端发送建立连接的请求(SYN),这两个合成一个报文。最后,客户但发送ACK(确认应答),表示收到请求了。至此建立连接完成,三次握手成功。
客户端调用connect发起三次握手,向服务器发起建立连接的请求。而服务器端通过listen将自己设置为监听状态。
本质上,connect是发起三次握手,但是不参与三次握手的。服务器端通过accept获取底层建立好的连接,也不参与三次握手。三次握手由双方客户端和服务器端操作性系统自动完成。
为什么要进行三次握手??? 1,通过三次握手,可以以最短的方式进行验证双方的全双工通信。本质是验证双方所处的网络是否通常,是否能够支持全双工。 2,以最小的成本,100%确认双方通信的意愿。
四次挥手的大致过程:通信双方断开连接的过程。客户端发送FIN,表示本端想要关闭连接,服务器端收到后发送确认应答(ACK)。接着服务器端也发送FIN,表示本段也想要关闭连接,客户端收到后,发送确认应答(ACK),服务器端收到应答。至此,四次挥手完成,双方断开连接。
四次挥手也是由操作系统自动完成的。本质是建立双方断开连接的共识。客户端(服务端)认为要发送的数据已经发送完了,调用close(fd)断开连接。
在三次握手中,将SYN+ACK合并了,本质上是4次握手,降级成了三次握手。
而对于四次挥手,不一定能进行合并,当客户端数据发送完了,向服务器发送断开连接的请求时,服务器先要回复一个确认应答(ACK),但此时由于服务器要发送的数据可能还没发完,所以服务器要等待一段时间,将这些数据全部发送完,才会向客户端发起断开连接的请求,客户端收到后向服务器发送确认应答(ACK)。至此,双方完成断开连接。因此,对于断开连接的过程,中间两步一般无法合并成 一次,所以大部分情况下是四次挥手。
TIME_WAIT状态: 关于四次挥手,还有一个细节,下图中,客户端主动发起断开连接,四次挥手完成后最后客户端会先处于TIME_WAIT状态,而不是立即处于CLOSED状态,经过一段时间,才会处于CLOSED状态。而对于服务器端,四次挥手完成后,会立即处于CLOSED状态。

这是因为,TCP协议规定:主动关闭连接的一方要先处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能会得到CLOSED状态。MSL是TCP报文的最大生存时间。 原因:当通信双方想要断开连接的时候,历史发出的报文可能还在网络中进行转发,还没到达接收端,这时双方关闭连接。当双方重新建立连接的时候,这个在网络中残留的报文此时可能就会到来,根据目的IP和目的端口找到对应的主机,但此时连接都还没建立,三次握手都还没完成。所以这个报文是会影响 通信双方重新建立连接和发送数据的,等待2个MSL是为了让这个报文在网络中消散。

站在应用角度,一般都是既要实现TIME_WAIT,还要服务器可以立即重启。 使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示端口号复用。 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

TIME_WAIT+序号,通过以上的处理方式,就可以大大减少陈旧报文对新连接建立产生的影响。
在上面我们讨论了确认应答机制,对每一个发送的数据段,都需要给一个ACK应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差,尤其是数往返时间较长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
主机A向主机B发送数据,1~1000表示的是数据的大小,当发送的数据大小是1~1000时,序号就是1000,主机B收到后,进行应答,确认序号=序号+1,所以确认序号就是1001,同时下一个数据从1001开始发送,选后是依次增大的。发送前4个报文的时候,不需要等待ACK,可以直接发送。

滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。
在前面提到过,TCP有自己的接受缓冲区和发送缓冲区。如下图所示:

我们可以将发送缓冲区看成是一个char类型的数组,滑动窗口看成是发送缓冲区的一部分 。start指向滑动窗口的开始,end指向滑动窗口的结束。
根据滑动窗口,可以将发送缓冲区划分为三部分:

滑动窗口大致工作过程如下图:

注意:窗口大小是可能会改变的,变大或者变小,取决于对方的接受能力。
发送端在发送数据的时候,可能会存在丢包问题。
大致分为三种:滑动窗口最左侧丢失,中间丢失,最右侧丢失。
如果中间报文丢失,那么滑动窗口首先会向右滑动,滑倒丢失的位置,此时就又变成了最左侧丢失问题。如果最右侧报文丢失,同理,也是最左侧丢失问题。
最左侧丢失,又分为两种情况,最左侧对应的应答丢失,最左侧对应的数据真的丢了。:
情况一:数据包真的丢失了

这种机制被称为 "高速重发控制"(也叫 "快重传")。
上述的情况只能确认1001~2000的报文丢失了,不能确定中间的是否丢失了。假设中间的4001~5000也丢失了,由于我们的通信一直再进行着,比如我们下面还会发送7001~8000,8001~9000的数据,那么此时的确认应答就是40001。也就是如果中间部分丢失了,会有后面的通信做兜底。同时,还有超时重传机制来确保。
快重传vs超时重传 快重传:收到三个相同的确认应答时进行重传,可以提高效率。 超时重传:超时并且没有收到应答,用来做兜底的。
总结:发出数据后,没有收到应答 ,必须让对应的报文暂时保存起来,以方便后续的而重传!
保存在滑动窗口中,也就是左侧不移动。
情况二:数据已抵达,ACK丢失

这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认。
因为确认序号的定义就是该序号之前的所有报文对端都已经收到了。 所以,滑动窗口可以正常进行工作。
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量数据,但是如果一开始发送大量的数据,也可能会出现问题。
因为网络上又很多计算机,可能当前的网络状态已经很拥堵了。在不清楚当前网络状态的前提下,贸然发送大量数据,是很有可能造成雪上加霜的。
TCP引入慢启动机制,先发送少量的数据,探探路,摸清当前的网络状况,再决定按照多大的速度传输数据。
为了衡量当前网络的拥塞状态,TCP引入了一个新的概念——拥塞窗口。
刚开始发送数据的时候,拥塞窗口大小为2^0。
当收到一个ACK应答时,拥塞窗口大小为2^1。
以此类推,2^2,2^3......呈现指数形式增长。如下图所示:

在前面滑动窗口 部分提到过,滑动 窗口的大小是对方接受能力的大小(也是就是对端缓冲区剩余空间的大小),现在再加一个因素,就是拥塞窗口的大小。取两者的最小值。
滑动窗口大小=min(对端接受能力大小,拥塞控制大小),之所以取最小值,是因为对端接受能力很强,但是网络太拥堵了,发送太多数据会造成大量的丢包;同理,网络状态很好,但是对端接受能力太差,同样发送给大量数据也会造成大面积丢包。
但是关于 拥塞窗口的大小,不能一直这么增长下去,因为可能会造成指数爆炸,需要通过慢启动机制来控制。
前面指数增长阶段,本质是解决拥塞,尝试回复网络通信的阶段。

刚开始拥塞窗口一直在增加,但是发送的数据不一定一直在增长。
当拥塞窗口大小一直再增大,增大到某一个数值时,此时发送数据发送大量丢包,那么说明此时的拥塞窗口大小就是当前网络的拥塞状况。这个过程就是我们 在探测当前网络的拥塞情况。之后,重新开始满开始 增长,此时的阈值就是上次拥塞窗口值的一半。
这整个过程本质是不断探测当前网络的拥塞窗口的值。
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区。
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配, 例如:
写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次
write, 每次写一个字节。读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节, 也可以一次 read 一个字节, 重复 100 次;
首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
对于 UDP 协议来说, 是否也存在 "粘包问题" 呢?
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能。
可靠性:
提高性能: