UNIX下的Socket通信-TCP/IP基本概念

总览

All problems in computer science can be solved by another level of indirection

觉得这句话对TCP的分层也很适用。

对TCP/IP协议来说,大体分为五层,

  1. 应用层
  2. 传输层
  3. 网络层
  4. 链路层
  5. 物理层

应用层我们接触的比较多,流行的如HTTP,FTP之类都算,像我在大学里做的PHP开发时基本对HTTP发包没有任何概念,照着教程开eclipse,apache,在浏览器里敲个locahost:8080出来个猫的图案就很开心了。再建个index,上面写句“未满18周岁禁止进入”,就有种天下在手的感觉。

后来做java网站,用到了HttpClient,逐渐对HTTP的抓包发包有了简单地了解。通过这玩意儿就可以做到绕过网页填写,直接后台模拟网页提交表单,那些抢票软件大体上就是这个思路。

再往下便是闻名已久的传输层(TCP,UPD,SCTP etc.)与网络层(IPv4,IPv6 etc.)了。

网络的划分

image

通过上图可以很直观的看到应用层的实体数据经过层层的包装最终封印在了链路层中在物理层中进行传输。其中每一层实体间交换的单位信息称为协议数据单元(protocol data unit,PDU)。每层的PDU作为下层的数据服务单元(service data unit,SDU)传递给下层,并由下层间接完成本层的PDU交换。

为了避免诸如TCP的PDU,IP的PDU这类很不简洁的称呼,国际上给每层的PDU都另外取了自己的名字。传输层的称为segment(分节),网络层的称为IP datagram(IP数据报),链路层的称为frame(帧)

而关于层与层间的打包封印也有它们不得不说的故事,那就是分片。简单的说如果本层PDU的大小超过紧邻下层的最大SDU限制,那么本层还要事先把PDU划分成若干个合适的片段让下层分开载送,再在相反方向把这些片段重组成PDU。同一层内SDU作为PDU的净荷(payload)字段出现,因此可以说上层PDU作为本层的SDU字段由本层PDU承载。如上图所示每层PDU除用于承载紧邻上层的PDU(即承载数据外),也用于承载本层协议内部通讯所需的控制信息(各种header)。

当然这只是个初步的说法,下面会针对传输层与网络层展开具体一下的学习。

1.传输层

一说传输层TCP,讲不通三次握手连接,四次握手关闭都不好意思出来混。

先看连接图示:

image

图中有类似于SYN,ACK的标记量,结合上一章的内容,其实这也是TCP分节的一种。分节除了用于承载数据外,也用于建立连接(SYN分节),终止连接(FIN分节),中止连接(RST分节),确认数据接收(ACK分节),刷送待发数据(PSH分节)和携带紧急数据指针(URG分节),而且这些功能(包括承载数据)可以灵活组合。

也就是说在建立连接的初期,由Socket自带的函数为我们发送分节握手沟通,握手连接建立后,则由用户自行组织分节信息进行通信。

关于上图的握手过程可以简单复述一下:

  1. 服务器必须准备好接受外来的连接,这通常通过调用socket,bind,listen这三个函数来完成,称之为被动打开(passive open)
  2. 客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部,一个TCP首部及可能有的TCP选项。
  3. 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK。
  4. 客户必须确认服务器的SYN。

上述过程中出现了SYN_SENT之类的字样,它们代表了套接字的状态。在socket函数初始化后套接字的状态为CLOSEDconnect函数导致当前套接字从CLOSED状态转移到SYN_SENT状态,若成功则在转移到ESTABLISHED状态。connect失败则该套接字不再可用,必须关闭,不可以对这样的套接字再次调用connect函数,而是在每次失败后,必须close当前的套接字描述符并重新调用socket

再来个关闭图示:

image

  1. 某个应用进程首先调用close,称该端执行主动关闭(active close),该端的TCP于是发送一个FIN终止分节,表示数据发送完毕。
  2. 接收到这个FIN对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其它数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外的数据可接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致了它的TCP也发送一个FIN。
  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

在握手连接的过程中,套接字通过发送SYN(建立连接分节)经历了COLSE,SYN_SENT,ESTABLISHED等状态,达到了可以通信的目的。而在四次关闭握手连接中,套接字也要经历几种状态,达到真正关闭的目的,需要注意的是,因为TCP是全双工的,所以正常情况下的关闭需要经过双方的确认才可以完全关闭,这就需要通信两端都分别发送自己的FIN信号,且回应对方的FIN,因此理论上关闭握手需要四次,每个套接字各两次。

  1. FIN_WAIT_1:主动方套接字在ESTABLISHED状态正常通信时,它想主动关闭连接,于是向对方发送了FIN报文,此时该套接字即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态。
  2. FIN_WAIT_2:主动方通俗的讲便是我已经完成任务并且告知对端完成我方的关闭,然后我在等你发送你的FIN,要是你还有什么话说那赶紧,我这边该说的都说完了…
  3. CLOSE_WAIT:被动方对应的是FIN_WAIT_2状态,便是在正常ESTABLISHED状态下收到了对端的FIN信号,但我还有活没做完,需要搞定后才往你那儿发送FIN终止信号,此时的状态便是CLOSE_WAIT。
  4. LAST_ACK:被动方被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。
  5. TIME_WAIT:主动方表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

在这些状态中,比较不好理解的是TIME_WAIT状态。我们看到执行主动关闭的一方经历了这个状态。该端点停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称之为2MSL。

任何TCP实现都必须为MSL选择一个值。其时间在1分钟到4分钟之间。MSL是任何IP数据报能够在因特网中存活的最长时间。因为每个数据报含有一个称为跳限(hop limit)的8位字段,它的最大值为255.一般上假设:

具有最大跳限(255)的分组在网络中存在的时间不可能超过MSL秒。

所以TIME_WAIT状态有两个存在的理由,

  1. 可靠地实现TCP全双工连接的终止。TCP连接在主动关闭方发送的最后一个ACK(FIN),有可能丢失,这时被动方会重新发FIN, 如果这时主动方处于CLOSED状态 ,就会响应RST而不是ACK。所以主动方要处于TIME_WAIT状态,而不能是CLOSED。
  2. 允许老的重复分节在网络中消逝,因为经过2MSL,上一次连接中所有的重复包都会消失。

最后放一张TCP中套接字的状态大图~

image

2.网络层

因为主体是使用TCP的Socket进行编程,这里对网络层的具体细节就不做探讨了。但关于网络层还会牵扯到分片的问题,这里需要重视起来。

在TCP传输层中,发送端TCP把来自应用进程的字节流数据(即由应用进程通过一次次输出操作写出到发送端TCP套接字中的数据)按顺序经分割后封装在各个分节中传送给接收端TCP,其中每个分节所封装的数据既可能是发送端应用进程单次输出操作的结果,也可能是数次输出操作的结果,而且每个分节所封装的单次输出操作的结果或者首尾两次输出操作的结果既可能是完整的,也可能是不完整的,具体取决于可在连接建立阶段由对端通告的最大分节大小(maximum segment size,MSS)以及外出接口的最大传输单元(maximum transmission unit,MTU)外出路径的路径MTU

MSS的目的是告诉对端其重组缓冲区大小的实际值,从而试图避免分片。MSS经常设置成MTU减去IP和TCP首部的固定长度。

网络层实体间交换的PDU称为IP数据报(IP datagram),其长度有限:IPv4数据报最大长度65535字节,IPv6S数据报最大65575.发送端IP把来自传输层的消息(或TCP分节)整个封装在IP数据报中发送。链路层实体间交换的PDU称为帧(frame),其长度取决于具体的接口。IP数据报由IP首部和所承载的传输层数据(即网络层的SDU)构成。过长的IP数据报无法封装在单个帧中,需要先对其SDU进行分片(fragmentation),再把分成的各个片段(fragment)冠以新的IP首部封装到多个帧中。在一个IP数据报从源端到目的端的传送过程中,分片操作既可能发生在源端,也可能发生在途中,而其逆操作即重组(reassembly)一般只发生在目的端。

TCP/IP协议族为提高效率会尽可能避免IP的分片/重组操作:TCP根据MSS和MTU限定每个分节的大小,且在途中尽量避免分片操作。不论是否分片都由IP作为链路层的SDU传入链路层,并由链路层封装在帧中数据称为分组(packet,俗称包)。可见一个分组既可能是一个完整地IP数据报,也可能是某个IP数据报的SDU的一个片段被冠以新的IP首部后的结果。

下一步

总结了基本套接字函数的使用与TCP/IP的简单概念,下一步将会着手编写一套客户/服务器的通信演示Demo,会考虑实现线程池来提高并发效率。

Comments