详细讲解传输层的网络协议,为什么TCP是可靠连接协议,凭什么能做到不丢包,有哪些机制保证可靠呢?
TCP/UDP
- UDP
- TCP
- **三次握手和四次挥手**
- **滑动窗口**
- **拥塞控制**
- (socket套接字)**listen的第二个参数**
UDP
UDP也是传输层协议,当我们在应用层把数据序列化之后,并不是直接通过网络就能发送到对方主机,需要向下交付,交给传输层然后经过传输层协议进行封装报头,再向下交付。
这就是udp的整体数据,前八个字节就是udp的报头,是定长的,会拿结构体表示。后面的数据就是应用层数据。
如果收到的udp报文不完整,校验和会检查出来
TCP
下面是TCP的数据报头格式
- 源端口号是我们发送方进程的端口
- 目的端口号是接收方进程的端口
- 32位序列号和确认序列号: tcp是面向字节流的,当应用层把数据拷贝到传输层的发送缓冲区内时,缓冲区可以使一个char类型的数组存储,例如发送1000个字节,确认序列号可以是0或者任意,如果是0开始,0+1000字节,服务端返回的报头中确认序列号就是1001,代表可以从1001个字节开始发送确认序号不但是对当前报文做确认,还是对之前所有报文做确认,代表前面所有报文都已经收到。
- urg代表紧急指针是否有效,如果有效,16位紧急指针代表在有效载荷的偏移量,紧急数据只有一个字节,标志位就是表示不同类型的报文。
三次握手和四次挥手
下面是三次握手图解
tcp是一个可靠的传输协议,在服务端和客户端进行通信的时候,需要进行连接,被称为三次握手。
一台主机可能建立多个链接,所以OS需要把链接管理起来,采用struct结构体的方式,先描述,再组织,struct可能是位段
三次握手是由操作系统自动完成的。
为什么是3次握手?
- 没有明显的设计漏洞,一旦建立连接出现异常,可以嫁接给client,server成本较低
- 验证双方通信信道的通畅情况,三次握手是验证全双工通信信道通畅的最小成本。更多奇数次会有更高成本。
- 如果是两次,容易收到攻击,SYN洪水。三次是奇数次,客户端一定是最后一个发送ACK的。
下面是四次挥手图解
四次挥手是由客户端向服务端发起FIN请求,然后服务端维持CLOSE_WAIT状态,然后ack回复。等到服务端把该发的数据全部发送给客户端之后,然后发送FIN请求,客户端收到这个请求维持TIME_WAIT状态一段时间,服务端维持LAST_ACK状态,等收到客户端回复的ACK,服务端关闭。
如果服务端收到来自客户端的第一次FIN请求,维持CLOSE_WAIT状态,如果不close(fd),服务端会一直维持CLOSE_WAIT状态。
客户端和服务端是平等的,上面的状态 是客户端请求服务端,如果是服务端先发送请求,也是一样的。
所以就解释了为什么云服务器上写代码时,如果服务端先断开连接,那么端口号就不能用了,因为最后一次发送ACK会进入TIME_WAIT状态,端口号还在被占用,会等待一段时间之后才会关闭连接。
//端口号复用,如果不想让server端进行time_wait的等待状态,可以调用下面接口。
int opt=1;
setsockopt(_sock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
滑动窗口
双方使用TCP协议,有发送缓冲区和接受缓冲区,每次发送的报文都要收到对应的ACK应答,应答报文里面有16位窗口大小,这个16位窗口大小代表了对方接收缓冲区的大小,防止报文发送过快引起报文丢失造成资源浪费,或者报文发送过慢引起效率底下的问题。
发送方和接收方有两种发送和接受的模式,一种是串行,就是发送一个报文等收到应答报文之后,才能发送下一个报文,这种效率低下。还有一种就是可以先发送多条报文给对方,然后对方对这些报文一一发送应答报文,这种是并行。
而在发送缓冲区里面有一个滑动窗口,滑动窗口里面的数据是可以不收到前面报文的应答报文,然后直接发送给对端的数据,例如像下面这样
那么这个窗口大小是多少呢?答案是一定不能超过对方接收缓冲区的大小。
TCP是面向字节流的,所以发送缓冲区也是以字节为单位的。winstart就是起始下标,winend就是结束下标,start 和end同时++,就是窗口在滑动。
补充知识:
滑动窗口只能向右滑,不能向左
滑动窗口根据对方接受能力可以变大变小,start不动end++就变大。end不动start++就变小。
每次的ACK报头中响应的确认序号就是下一次要发送的起始下标,起始下标加上窗口大小就是滑动窗口大小。
滑动窗口不会越界,因为可以把缓冲区设置成环形队列。
如果第一个报文或者中间报文丢失分为两种情况:
- ACK丢失,这个不必担心,因为后面报文的ACK如果收到了,就代表前面的一定收到了,确认序号就代表前面报文全部收到,下一次发送请从最大的确认序号开始发送。
- 数据丢失了,第一个数据或者中间的丢失了都是同理,如果第一个数据是1001-2000,第二个是2001-3000,以此类推,后面的数据返回的ACK报文的确认序号一定是1001,因为尽管2000后面的收到了,也不能不管1001~2000,后面多个ACK报文都是1001的确认序号,操作系统就知道报文丢失了,就会进行补发1001-2000这段数据。
拥塞控制
学习了上面的滑动窗口,我们明白了两端主机在进行通行时,已经把双方主机的情况考虑的特别全面了,例如确认应答,超时重传,三次握手四次挥手,还有基于流量控制的滑动窗口。但是我们的报文是在网络中进行传输的,网络中可是不止我们两台主机在通信,而是成千上万乃至上亿的主机在通信,所以网络中一定是同时存在大量的数据的,如果传输过程中因为网络的原因发生大量的丢包,这个怎么办呢?为什么呢?
如果是因为网络的原因丢包可能是因为网络中的数据太多发送拥塞,如果真的是因为发送了拥塞而丢包导致的,那么我们的通信双方第一时间可能会启动超时重传,但是网络中因为发生了堵塞的情况,这个时候网络中的多台主机同时启动超时重传机制,网络中本来就已经有大量的数据了,这样只会导致拥塞情况更加严重,所以这时候绝对不能再发送大量的数据了。
当发送大量的丢包时,OS就判断可能是网络中出现了拥塞,所以就需要对网络中的情况进行探测,试探的发送一个报文,如果没有回应,就说明网络拥塞严重,等待一段时间再探测。如果收到了ACK回应,就发送两个报文,如果再收到回应就发送四个报文。就这样以2的指数级别进行增长。以指数级别增长开始慢,后来就会特别快,当然也不会一直以指数级别增长下去,到达一定的阈值之后就会以线性的方式增长,这种机制叫做慢启动,用来摸清网络的承受能力。
OS需要一直来试探网络中数据的承受能力,来监听网络的状态,所有就有了拥塞窗口,来进行拥塞控制。
所以上面的滑动窗口的大小其实是不但要考虑接收方缓冲区的大小,还要考虑拥塞窗口的大小,如果拥塞窗口比接受缓冲区数据承载能力大,就不必考虑拥塞窗口,反之就要以拥塞窗口的大小为滑动窗口的大小。
(socket套接字)listen的第二个参数
之前学习listen函数有两个参数,第一个就是socket返回的文件描述符,但是第二个参数就没有讲解,我们现在理解一下第二个参数的含义。
使用TCP协议,先创建socket套接字之后,然后listen函数监听和server端连接的所有客户端,收到和server连接的客户端的请求,OS在底层会自动完成三次握手,然后进入ESTABLISHED状态。
然后用accept函数和客户端进行通信,在accept被调用之前,客户端和服务端就已经建立好连接了,accept只不过是把已经建立好的连接拿到上层,之前我们把第二个参数设置成了32或者更大,就是按照上面的流程完成了连接通信,如果我们把第二个参数设置成1,会发生什么?
如果设置成1,并且不调用accept,不让上层把建立好的连接取走,那么前两个客户端的连接是正常的,客户端和服务端都会进入ESTABLISHED状态,到了第三个就会出现问题,第三个客户端也会维持ESTABLISHED状态,但是服务端是SYN_RCVD状态,也就代表服务端给客户端响应SYN+ACK之后并没有进入ESTABLISHED状态。
这是因为OS底层会保持两个队列,一个全连接队列,一个半连接队列,全连接队列的最大个数就是第二个参数加一,只要全连接队列中没有连接被accept取走,之后来的所有连接请求只能保持在半连接队列里面,并且这个半连接队列的生命周期很短。
这样并不是服务器只能同时和两个连接的客户端通信,而是没有被accept来得及读取到的连接就先放在全连接队列里面,一旦accept读取到了这个全连接,这个全连接就会被上层读走,就会被移除。然后半连接队列就能向全连接队列push一个连接。