Windows编程——网络概念(二):传输层协议、TCP/UDP

简介

传输层包括:TCP、UDP和SCTP(stream control transmission protocal,流控制协议)。绝大多数的客户端、服务器网络应用使用TCP和UDP。SCTP是为跨因特网传输电话信令而设计的一个交新的协议。这些协议的底层都使用IPV4或IPV6。

  • UDP:一个简单的、不可靠的数据报协议。
  • TCP:一个复杂的、可靠的字节流协议。
  • SCTP:可靠的传输协议,提供消息边界、传输级别的多数组支持以及将头端阻塞减少到最小的一种方法。

关于传输层主要关注的原理:

  • TCP的三路握手
  • TCP的四次挥手
  • TCP的连接终止序列
  • TCP的TIME_WAIT状态
  • 由套接字层提供的TCP、UDP、SCTP缓冲机制
  • SCTP的四路握手
  • SCTP的连接终止

UDP(用户数据报文协议)

使用UDP传输数据时,应用程序通过向一个UDP套接字写入数据,然后该数据被封装到一个UDP数据报中,最终该数据报以IP报文的形式被发往目的地。UDP不可靠的原因:

  • UDP不保证UDP数据报会到达其最终的目的地
  • 不保证各个数据包的先后顺序
  • 不保证每个数据报文只到达一次(在传输阶段被复制),或者一定到达目的的
  • UDP是无连接的服务
  • UDP不进行数据校验
  • UDP无三次握手、无超时与重传机制、没有对端的响应机制

和TCP不一样,每个UDP报文都有一个数据包的长度,如果一个数据报正确的到达其目的地,那么该数据报的长度将随数据一起传递给接收端应用程序。

TCP(传输控制协议)

TCP简介

TCP提供客户端与服务器之间的连接,通过TCP传输数据是可靠的:

  • 三次握手:三次握手不仅是用来建立连接,还计算来初始的RTT值
  • 数据重传:在RTT时间内如果没有接收到响应,则进行数据重传,该重传机制可能持续好几分钟才最终失败。
  • 有序:通过给传输的每个字节关联一个***对所发送的数据进行排序
  • 去重:如果接收到重复的数据,TCP可以根据***判断出数据是否重复,从而丢弃重复数据
  • 流量控制:TCP总是告知对端在任何时候它一次能够从对端接收多少字节的数据,称为通告窗口。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。这个窗口大小就是MSS,可以使用TCP_MAXSEG套接字选项来设置和获取这个值。
  • 全双工:在给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据
  • 数据的可靠性:保证传输的数据不会出错

TCP选项

  • MTU:表示接收方当前的接收缓冲区的大小。可以使用SO_RECVBUF来设置。
  • MSS:表示接收方可以接收的最大分节大小。可以使用TCP_MAXSEG来合适和获取。
  • 时间戳选项:主要是在高速网络中防止失而复现的分节对数据造成损坏。编程人员不需要考虑。

滑动窗口

所谓滑动窗口,实际上就是一个由左右2个游标所确定的窗口。左边的游标只能往右滑动,右边的游标可以*滑动但是不能滑到左边游标的左边。只有处于2个游标之间的数据才能被发送。那么,可以分以下几个情况来讨论:

  • 左游标不动,右游标左移:窗口变小,可发送的数据变少,一般是因为发送缓冲区可用空间减少
  • 左游标不动,右游标右移:窗口变大,可发送的数据变多,一般是因为发送缓冲区可用空间增加
  • 左游标左:表示左边的数据以及得到接收方的确认,窗口右移

TCP连接的建立和终止

三次握手

建立一个TCP连接时,客户端和服务器之间会进行多次的确认,这个确认就叫做三次握手,握手的规则如下:

    1. 服务器创建监听套接字,准备接收来自外部的连接请求,一般由socket、bind、listen三个函数完成,称之为被动打开
    1. 客户端调用 connect 发起主动连接。这导致客户端的 TCP 发送一个 SYN(同步)分节,告诉服务器客户端发起连接时的初始化序列。通常SYN不携带数据。
    1. 服务器接收到客户端的连接请求后,发送一个SYN用于确认(ACK),同时服务器通过发送另外一个SYN告知客户端自己的初始***
    1. 客户端接收到ACK和SYN信号后,确认服务器的SYN信号,也就是发送一个ACK信号给服务器,这个信号是对服务器的***的确认。

SYN信号的确认,实际上就是将对方的***+1。不管是客户端还是服务器,都有自己的SYN***,用于以后的数据收发。

三次握手状态转换过程与对应函数

  • SYN_SEND:客户端创建好fd、sockaddr之后,调用connect函数将他们绑定在一起,然后发送SYN请求分节给服务器
  • SYN_RCV:服务器使用listen函数将fd转化为被动 套接字之后,阻塞在accept上,当接收到连接请求之后,发送ACK 和 SYN分节给客户端,此时处于SYN_RCV状态
  • ESTABLISHED:当客户端接收到确认分节后发送ACK,此时转换为稳定状态,而服务器接收到ACK之后也转换为稳定状态

TCP连接终止

TCP建立连接需要三次握手,而终止一个连接则需要四次握手。过程如下:

    1. 某个应用程序首先调用close,我们成该程序主动关闭。于是该端的TCP发送一个FIN分解,表示数据发送完毕。如果主动关闭端调用close后继续写数据到对应的套接字描述符则系统将返回Bad file descriptor的错误信息。
    1. 接收到这个FIN的对端执行被动关闭这个FIN由TCP确认,TCP会将一个EOF写道接收队列中的某一个分节中然后传递给接收者的进程。这就是我们通常为什么要在接收数据的时候判断是否为读到EOF标识符来。此时接收还可以做一些善后处理,至于何时发送FIN分节则需要等到善后处理完成之后由用户进程自行决定。
    1. 接收到文件结束符的应用进程调用close关闭它的套接字,从而导致TCP发送一个FIN分节。
    1. 接收到这个FIN分节的原发送端确认这个FIN分节。

虽然通常情况下关闭一个连接需要4个分节,但实际上某些请反馈下,步骤1的FIN分节随数据一起发送,另外步骤2和3发送的分节都出自执行被关闭的那一段,有可能被合并成一个分节进行发送。

TCP的状态转换图

TCP为连接和断开定义来11种状态:

三次握手时的状态转换

  • CLOSED:起点

  • SYN_SENT:客户端调用connect,此时TCP发送SYN分节,客户端由CLOSED状态转换为SYN_SENT状态

  • LISTEN:服务器调用listen,使fd转变为被动套接字。

  • SYN_RECV:服务器处于listen,接收到客户端的连接请求,并且发送 ACK进行确认,状态将由LISTEN转换为SYN_RECV。

  • ESTABLISHED:客户端阻塞于connect函数的调用,当接收到服务器的ACK和SYN时,将对SYN进行ACK应答。客户端将由SYN_SETN转化为ESTABLISHED。服务器接收到客户端的ACK后也将转换为ESTABLISHED。

四次挥手的状态转换

假设close是由客户端主动调用的。

  • FIN_WAIT1:客户端调用close函数,TCP将发送FIN分节,此时客户端状态由ESTBALISHED转变为FIN_WAIT1
  • CLOSE_WAIT:服务器端的 TCP接收到客户端发送的FIN分节后转换为CLOSE_WAIT状态,并发送ACK确认客户端的FIN分节。此时服务端的TCP将EOF文件结束符写入到接收缓冲区的队列
  • FIN_WAIT2:客户端接收到ACK后将由FIN_WAIT1转换为FIN_WAIT2状态
  • LAST_ACK:当服务器端的用户进程从TCP接收缓冲区读取到EOF符号的时候将主动调用close函数进行被动关闭,并发送FIN分节,此时服务器状态将由CLOSE_WAIT状态转变为LAST_ACK。
  • TIME_WAIT:客户端接收到FIN后将发送ACK给服务器,状态由FIN_WAIT2转变为TIME_WAIT。
  • CLOSE:服务器接收到ACK 后转变为CLOSE状态。

被动关闭端read到EOF的处理

如上所述,当一方调用CLOSE时,另一方在read的时候会接收到EOF结束符。此时一般的做法是调用close关闭连接。但是,事实上对面调用close发送fin分节只是说它不再发送新的分节,但是还可以接收数据,因此这时被动关闭放仍旧可以在当前的fd上写数据。但是,假如主动关闭的一方进程是异常终止的,发送FIN包是OS代劳的,当主动关闭方再次收到该socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。进程对收到RST的socket调用write时,操作系统会给进程发送SIGPIPE,默认处理动作是终止进程。如果希望优雅的处理异常情况,则应该使用信号处理函数捕获异常信号。

TIME_WAIT状态

主动关闭端在发送最后的ACK后并没有立马转变为CLOSE状态,而是要等待一段时间。这段时间一般是最长分节生命期(max segment lifetime,MSL)的2倍,MSL一般为30s到2分钟,也就是所主动关闭端需要等待大概1~4分钟才会由TIME_WAIT状态转变为CLOSED状态。主动关闭端为何要等待这么长时间呢?这里涉及到全双工和重复字节的问题。

  1. 全双工可靠的终止:如果由于网络原因,被动关闭端发送的FIN分节没有到达主动关闭端,则被动关闭端在等待一段时间没有获得响应的时候将重发该FIN分节,如果主动关闭端不等待则被动关闭端重新发送FIN分节之后也将得到一个RST状态,被解释为错误。
  2. 允许老的分节在网络中消失:众所周知,数据重传在TCP中是非常常见的事情,如果一个分节由于路由原因在TLL跳变为0之前终于到达了目的地,但是这中间可能发送端已经进行重发并且提前到达。如果不进行2MSL时间的等待,在相同的端口和IP上重新建立连接并且刚好SYN序列和最后一个发送的分节SYN序列一致,此时新建的连接可能会接收到旧的数据。因此需要等待2MLS,同时在这段时间之内该端口是不可用的。

对已经调用close函数之后的描述符进行读写操作

SO_REUSEADDR——套接字端口重用

这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧 使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期 望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能(因为SYN分节很难一致~)。

Windows编程——网络概念(二):传输层协议、TCP/UDP

Windows编程——网络概念(二):传输层协议、TCP/UDP

TCP_NODELAY选项

使用setsockopt可以设置TCP_NODELAY套接字选项,TCP将关闭指定套接字的nagle算法。
正常情况下,当write数据的时候,如果没有接收到ack则将等待。此时,如果应用层需要发送很多小的请求包,会造成网络的负担太大。解决该问题的办法:

  1. 使用buffer,将多个数据包合并后发送
  2. 关闭nagle算法

如果使用TCP_NODELAY选项,则TCP关闭nagle算法后write数据的时候将不再等待ack响应。

端口

  • 对于服务器,一般使用固定端口或者与客户端的约定端口
  • 对于客户端,一般使用临时的系统自动分配的端口
  • 动态端口:49152~65535是动态端口或者私用端口。一般临时端口使用动态端口。
  • 保留端口:在unix中,0~1024属于保留端口

套接字对(socketpair)

socketpair用来存放连接2端的四元组:本地IP、本地端口、目的IP 、目的端口。套接字对唯一标识一个网络上的每个TCP连接。但是套接字描述符并不是和套接字对唯一对应的,因为如果某一端是多宿主的情况,则会有多个IP地址,因此可能会存在多个套接字对的情况。

TCP发送数据

当用户使用TCP协议发送数据的时候,用户调用write函数时TCP会将用户空间缓冲区的数据拷贝到TCP所在的内核空间。如果write的数据过大则会进行分批拷贝,此时write函数将会阻塞,直到TCP将buf内的数据拷贝完成。write函数的成功返回并不意味着接收端的TCP或者进程已经接到数据,,而仅仅表明数据被成功的拷贝到了内核空间等待发送。(具体详见7.5节SO_LINGER套接字选项的讨论。)

当TCP发送数据时,如果接收到对端的ACK则将对应的分节从内核缓冲区中删除。否则将会保存在内核空间,并进行超市重传。(也就是说,TCP将会保存已发送数据的副本,直到接收到对应的ACK。)一个TCP分节最大的字节数为MSS,同事每个数据块将被添加一个IP头。

TCP消息丢失问题

除了网络原因导致的消息丢失,最常见的消息丢失原因是由于调用close过早,导致TCP发送缓冲区中的数据没有发送完毕,取而代之的是发送了RST给对方导致连接重置,这样如果对方还有消息要发送则之后的数据就接收不到了。可以通过自定义用户协议或者其他办法来规避。

过早调用close为何会造成RST??因为一方调用close后,另一方如果接收到FIN后,read到EOF。

当使用阻塞IO的时候,如果一方发送数据后就调用CLOSE,而另一方在之前发送了数据到对方的接收缓冲区,此时CLOSE将无法正常的触发4次挥手,取而代之的是当接收到对方写数据的时候将触发RST。

正确的做法是,如果服务器需要关闭套接字,可以使用shutdown函数关闭写端,然后进行read,如果read返回大于0则一直循环。客户端调用read返回0的时候客户端自己调用CLOSE来触发四次挥手。

更好的方法是自定义协议。