TCP协议

TCP协议

[toc]

TCP协议的特点

不同网络的两台主机上的应用进程如果想要进行通信,就需要通过物理层、数据链路层、网络层这三层进行数据包转发,再通过传输层把数据包发送给主机中的指定进程,所以网络模型中的传输层至关重要。

传输层中最为常见的两个协议分别是传输控制协议 TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol),想要掌握这两种协议,则需要阅读协议的标准文件,TCP标准内容如下:

Untitled

image

通过阅读RFC 793规范可以知道,TCP协议是面向连接的、可以实现端对端通信的可靠的协议。

Untitled

TCP协议可以实现让处于不同网络的但是互联的主机中的进程与进程之间进行可靠的进程间通信,所以TCP是基于一对一通信的协议。

注意:由于TCP协议是面向连接的,所以只能支持一对一服务,不提供广播服务和组播服务。

TCP协议可靠性

TCP的报头

https://img-blog.csdnimg.cn/20200416192534946.png

想要掌握TCP连接的握手机制,以及了解序列号和应答标志等字段,则需要阅读TCP协议头:

  1. 源端口号

    源端口号指的是发送端的进程端口号,是一个16bit的无符号短整型数,需要注意端口范围。

  2. 目标端口

    目标端口指的是接收端的进程端口号,是一个16bit的无符号短整型数,需要注意端口范围。

  3. 序列号

    Untitled

    序列号指的是数据段中的第一个字节的序列号,当然,有一种情况除外:就是数据段中存在SYN标志位。

    如果数据段中存在SYN标志位,则序列号为初始序列号(ISN),那么数据段中的第一个字节的序列号等于 ISN + 1。

    序列号其实是建立TCP连接时由计算机生成的随机数作为序列号初始值,这个序列号初始值由SYN(SYN指的是希望建立连接)包发送给接收端,序列号主要是为了解决网络包乱序的问题。

  4. 确认应答号

    Untitled

  5. 头部长度

    Untitled

    指的是TCP头部的长度,单位是字(一个字等于32bit),所以TCP头部长度是一个32bit的整数,用于表示数据的开始位置。

    Untitled

    可以看到,TCP协议头的长度至少是20字节,options字段是可选的,如果使用options选项字段,则头部长度会更长。

  6. 控制位

    Untitled

    可以看到TCP协议头中存在6bit控制位,每个控制位的含义各不相同,其中较为常用的控制位是ACK(确认应答)、SYN(建立连接)、FIN(断开连接)。

    ACK:该位是确认应答位,如果设置为1则表示支持确认应答,TCP规定该位必须设置为1。

    SYN:该位是希望建立连接,另外可以在字段中会对序列号进行初始化,初始化为ISN的值。

    FIN:该位设置为1表示不再发送数据,就是希望结束连接,通信结束时双方主机交换即可。

  7. 校验和

    TCP协议可靠的一个原因是当网络层传递的数据出现异常的时候可以有对应的处理方案,比如序列号就是用于解决数据出现丢包、重复、无序的问题,而校验和就是用于解决数据出现损坏的问题。

    TCP协议可靠的一个原因是当网络层传递的数据出现异常的时候可以有对应的处理方案,比如序列号就是用于解决数据出现丢包、重复、无序的问题,而校验和就是用于解决数据出现损坏的问题。

  8. 窗口号

    TCP协议可靠的一个原因是当网络层传递的数据出现异常的时候可以有对应的处理方案,比如序列号就是用于解决数据出现丢包、重复、无序的问题,而校验和就是用于解决数据出现损坏的问题。



三次握手

Untitled

https://img-blog.csdnimg.cn/20200416212900129.png


四次挥手

https://img-blog.csdnimg.cn/20200416213807250.png


如何建立可靠的

  1. 需要停止等待确认,若设定时间内未收到确认则进行超时重传。这样可以保证对方确认收到后,再发送下一段报文。

  2. 流水线传输方式

    发送方可连续发送多个分组,不必每发完一个分组就停顿下来等待对方的确认。由于信道上一直有数据不间断的传送,这种传输方式可获得很高的信道利用率。


TCP的数据缓冲区

对于TCP通信而言,是具有发送缓冲区和接收缓冲区的,发送端具有发送缓冲区,接收端具有接收缓冲区。

接收缓冲区

TCP的流量控制

Untitled

https://img-blog.csdnimg.cn/20200416223338222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzMTQ1MDcy,size_16,color_FFFFFF,t_70

设置套接字属性

接收缓冲区的大小属于Linux系统套接字文件的属性选项之一,所以想要设置接收缓冲区的大小,则需要通过设置套接字的属性选项实现,Linux系统中提供了两个函数接口来获取和设置套接字的属性选项,分别是getsockopt()和setsockopt(),使用规则如下所示:

![Untitled](tcp协议/Untitled 0.png)

  • 函数参数

可以看到,setsockopt()函数和getsockopt()函数有5个参数,另外,关于套接字选项的描述,需要通过man手册的第7章了解,输入 man 7 socket即可

第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。

第二个参数:level指的是选项对应的协议级别,一般建议把该参数设置为SOL_SOCKET即可。

第三个参数:optname指的是选项的名称,比如设置接收缓冲区的宏是SO_RCVBUF,如下:

Untitled

Untitled

第四个参数:optval指的是要设置的选项值,比如要设置接收缓冲区,则设置为某个有效值。

第五个参数:optlen指的是选项值的长度,一般可以通过sizeof计算对应选项值的长度大小。

注意:如果打算设置TCP接收缓冲区大小,应该在调用listen()函数之前进行设置才会生效!!!

提示:修改TCP的接收缓冲区的目的是可以让通信双方收发固定大小的有效的结构化数据块

思考:把TCP的接收缓冲区设置为某个固定值,正常情况下发送端应该发送这么多数据时接收端才能接收到,但是实际上发送端发送1个字节,接收端也可以正常接收,请问是为什么?

回答:可以把TCP接收缓冲区理解为一个水池,原则上只有等水池的水满了(接收的数据填满了缓冲区),应用层才能读取到数据,但是为了更灵活,TCP增加了一个叫水位线的概念。

并且规定:当接收数据量超过水位线时,就触发套接字的读就绪状态,这样应用层调用 recv()/read() 函数可以正常读取数据。

Untitled

同样,水位线可以通过setsockopt()函数进行设置,只不过选项名称发生变化,可以通过man手册的第7章阅读socket进行了解,如下:

Untitled

可以看到,Linux系统中接收缓冲区和发送缓冲区的最小字节数都被初始化为1,并且Linux系统的发送缓冲区的最小字节数不可以被修改,只有接收缓冲区在内核2.4版本允许被修改


发送缓冲区

TCP的发送缓冲区用来为数据的丢失重发做准备,在Linux下TCP的发送缓冲区大小一般介于 4608-425984 之间,不同主机的检测结果可能有所不同。发送缓冲区的基本作用,是当接收方发现数据丢失要求发送方重发时,发送方有备份数据可以重新发送。

TCP的OOB带外数据

思考:刚才学习到通过设置TCP的接收缓冲区的大小和TCP接收缓冲的最小字节数,可以让接收端每次读取一块完整的结构化数据,但是如果接收端设置了较大的缓冲区和较高的水位线,由于接收的数据量必须要达到水位线才能使套接字读就绪,因此在接收较少数据时,发送方发出的数据就会滞留在接收方的缓冲区中,如果此时恰好有一些比较紧急的数据,那这些数据就会被被迫滞留在缓冲区中无法被接收端读取,造成逻辑异常,应该如何解决?

回答:可以将紧急数据设定为带外(Out of Band)数据,通过特殊的标志位,让数据到达接收方后可以不受缓冲区和水位线的限制,让接收方可以优先读取,在man手册第7章关于TCP协议的描述中有详细说明:

Untitled

注意:OOB带外数据每次只能发送一个字节,但是可以发送多次,由于recv()函数和send()函数的第4个参数才可以指定MSG_OOB标志,所以紧急带外数据只能通过这两个函数进行收发。

注意:带外数据也是通过内核缓冲区发送出去,所以接收端接收到带外数据之后也需要从内核缓冲区中读取出来,但是由于带外数据属于紧急情况下为了防止阻塞和水位线的限制提供的一种数据发送模式,所以发送端每次发送带外数据时只能发送一个字节,并且接收端需要对SIGURG信号进行注册,但是由于信号触发之后会中断程序执行,应该确保信号响应接口只是完成接收动作,应该确保响应函数立即结束。

提示:对于接收端而言,应该先调用signal()把SIGURG信号和自定义的接口进行关联,然后再调用fcntl()函数指定自己为信号的接收者,让收到信号之后,会自动跳转到信号的响应接口,然后应该调用recv()进行带外数据的接收。


套接字的超时控制

TCP和UDP这两种协议都要求通信双方创建socket套接字,尤其是TCP协议是面向连接的、可靠的、基于字节流的全双工通信,所以要求通信双方要建立连接之后才可以收发数据。流程如下:

Untitled

首先客户端向服务器发起连接请求,然后服务器接受客户端的连接请求,此时完成TCP三次握手,表示成功建立连接。但是还存在两种情况:

服务器默认设置是阻塞模式,服务器运行之后如果一直没有客户端发起连接请求,此时服务器会阻塞在accept()函数的位置,这样会导致服务器程序无法继续向下执行。

把服务器的套接字设置为非阻塞模式,如果服务器没有监听到有客户端发起连接请求,则服务器的accept()函数会立刻返回。

这两种情况都过于绝对,所以Linux系统提供了超时机制避免这两种情况导致程序逻辑异常。
超时控制属于套接字文件的属性选项之一,需要调用setsockopt()函数进行设置,规则如下:

Untitled

Untitled

可以看到,当套接字设置为非阻塞模式并且设置了超时时间,超时时间内没有收发任何数据并且超时时间到达,则会立刻返回-1。如果超时时间设置为0,则操作永远不会超时。

另外,如果要设置超时时间,需要利用名称叫做struct timeval的结构体,该结构体定义在time.h头文件中。

Untitled

Untitled

可以看到结构体有2个成员,成员tv_sec指的是秒数,成员tv_usec指的是微秒数,如果服务器端设置了接收超时,并且假设超时时间为n秒,则有两种情况:

提示:如果服务器还未和客户端建立连接,并且服务器处于监听状态,则最多等待对方n秒。

提示:如果服务器已经和客户端建立连接,则服务器等待客户端发送数据的时间最多是n秒。、

服务器的调度策略

非堵塞轮询

一种方案就是将所有的套接字都设置为非阻塞模式,这样就不用担心客户端不发出数据导致服务器端卡死的问题,但是非阻塞套接字也无法妥善地告知服务器数据何时到达,所以服务器需要不断地尝试读取客户端数据,这就是采用轮询方式实现。

由于非阻塞模式属于套接字的属性,而套接字在Linux系统属于文件,所以通过Linux提供了fcntl()函数可以设置或者获取套接字文件的属性。

Untitled

注意:为了防止套接字文件的原有属性被破坏,所以一定要先用 F_GETFL 获取套接字已有属性,然后通过位或运算加上非阻塞属性 O_NONBLOCK,然后再用 F_SETFL 进行设定。

Untitled

多任务并发

多任务并发模型就是利用多进程或者多线程来达到同时处理多个套接字的目的。一般而言,进程用于具有较完整逻辑块的整合。如果只是处理网络套接字的数据,那么一般使用多线程。

对于UDP而言,由于不存在连接的问题,因此服务端一个UDP套接字可以接收任意的客户端发来的数据,可直接将该套接字交由一条专用于收发数据的线程管理即可。

对于TCP而言,首先创建一条专门的线程处理监听套接字,用于随时监听和接受客户端的连接请求。另外由于每当有一个客户端连接成功,服务端都会产生一个新的连接套接字来与之通信,那么就应该每产生一个套接字就分配一条线程与之对应,便可形成多任务并发的服务器IO模型。

提示:可以使用链表来记录客户端的信息,当客户端断开连接,则从链表中删除客户端信息。

Untitled

异步信号

指的是用信号来驱使服务器妥善处理多个远端套接字,每当远端有数据到达,那么就在本端触发信号,然后利用信号的异步特性来处理这些远端信息。

Untitled

注意: SIGIO信号 默认会杀死目标进程,因此必须要设定其响应函数。另外,SIGIO信号由内核针对套接字产生,而内核套接字可以在多个应用层程序中有效(例如父进程将套接字遗传给各个子进程),因此必须指定该信号的宿主。

默认情况下,套接字收到数据时不会触发 SIGIO信号,所以必须将套接字设定为异步工作模式,它才会触发该信号。

由于不管套接字收到何种数据,内核都会触发 SIGIO信号,但是这种方式却不适用于 TCP 协议。

因此在 TCP 中,当客户端发来连接请求、普通数据、数据回执,甚至是断开请求、断开请求的回执等等情况,都触发一样的信号,这就使得服务端光凭这一个信号无法知道下一步要做什么,因此信号驱动的服务器模型,一般只适用于UDP协议。

多路复用

多路复用指的是通过某个特定的接口来同时监听多路阻塞IO,这就达到既无需多进程多线程,又可以同时处理多个阻塞套接字的目的。

Linux系统中提供了两个函数select()或poll()实现同时监控多个套接字,当发现一个或多个套接字的某种状态就绪(读状态、写状态)时,再调用相应的函数去处理的过程。

Untitled

三个套接字集合分别关注三个不同的就绪状态,如果需要同时监控某个套接字sockfd的不同就绪状态,则需要将此套接字放入相应的套接字集合中。

Untitled

注意:当 select() 返回时,三个集合中未处于就绪状态的套接字将被自动清零,因此如果要重复监控它们就需要重新设置这些套接字集合。

posted @ 2024-06-11 01:20  晖_IL  阅读(7)  评论(0编辑  收藏  举报