TCP和IP协议承载了整个互联网的生命线,这一章算是本书核心部分,掌握这两个协议也是学好网络编程的基础。
Socket连接
套接字链接在表面上看就是建立连接,交换数据,断开连接,虽然实际上细节肯定没有那么简单,但是大体上的思路基本不变。
协议栈建立连接
这里记住一个前提:向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用 Socket 库中的程序组件。
建立Socket的协议大部分情况都是Tcp/ip协议,Socket收发数据类似在两个主机之间建立一个无形的管道,Socket建立的关键是要按照指定顺序调用Socket程序组件,大致的构建顺序如下:
- 创建Socket(Socket类似管道两边的出入口)
- 绑定客户端的套接字到服务端(类似接管道)
- 交换数据。
- 断开Socket连接,解除绑定。
转化为具体的流程图如下:
创建Socket
过程大致为应用程序会把控制流程会转移到 socket 内部并执行创建套接字的操作,完成之后控制流程又会被移交回应用程序。
创建完套接字之后,协议栈需要返回标识符号也就是描述符用于标识是哪一个套接字在进行传数据,因为我们可能打开很多套接字连接访问不同的网站,具体的效果是我们浏览器会打开很多个页面,这时候每一个页面都可能需要创建套接字,此时就需要识别和区分这些套接字依赖描述符。
绑定客户端的套接字到服务端
连接操作核心是调用Socket的connect连接方法,此方法需要指定描述符、 服务器 IP 地址和端口号这 3 个参数。
connect看上去挺复杂,其实本质上就是完成连接动作而已,连接成功会把IP地址和端口号记录到套接字上面。
描述符在创建Socket的时候已经拿到了,IP地址则是在DNS解析的步骤完成,拿到IP之后会放入到应用程序的某个位置替换保存,而端口号则是需要应用程序事先提供。
端口可以简单看作应用程序的入口,DNS解析的IP只能知道主机在哪但是本身发往哪个应用程序是不清楚的,我们可以想象DNS解析类似地图上告诉我们高速的收费站坐标,但是他并不知道对应数据送往那个闸口)。
这里可以理解为端口就是收费站过站口,计算机会要求程序对待应用程序预设明确的端口参与网络交互。
传递消息
接下来的操作是调用read和write函数完成消息传递动作,这一步就是底层的流读写操作。
断开连接
这一步需要简单理解为需要一方主动发起断开申请浏览器调用read收发数据同时会收到关闭请求,此时客户端确认请求之后将会停止请求并且开始释放Socket连接。
为什么不能用描述符标识应用程序的入口?
- 描述符是和委托创建套接字的 应用程序进行交互时使用的,并不是用来告诉网络连接的另一方。
- 客户端也无法知道服务器上的描述符,客户端也无法通过服务器端的描述符去确定位于服务器上的某 一个套接字。〉
Socket连接中大致介绍了协议栈是如何通过网卡完成和目标服务器的连接、断开、收发数据的过程下面按照顺序讲述各个步骤的细节。
下面我们根据上面所讲的各个步骤按顺序进行详细介绍。
创建套接字
首先来看一下创建套接字的情况,下面是协议栈的内容。
委托分发被拆分为好几个部分,最上面可以看作浏览器,协议栈中主要有两张协议 TCP和UDP, TCP主要是用于和服务器交互收发数据的,UDP则用于较短的控制数据。
IP协议主要控制网络收发操作,主要工作是把一个个拆分的网络包发给通信的目标对象,IP协议包括 ICMP和 ARP协议,前者告知传输过程的错误和控制信息,后者传递以太网MAC地址。
MAC 地址:符合 IEEE 规格的局域网设备都使用同一格式的地址,这种地址被称为 MAC 地址
驱动部分是为了让操作系统能正常使用硬件进行网络收发的一个“适配器”,而所有的电信号最终要通过网卡完成。
套接字和协议栈
协议栈实际上是根据套接字传递的信息来决定做什么操作的,比如发数据要看IP和端口号。
以Windows的套接字为例,直接在CMD
中使用 netstat
操作即可:
C:UsersXander>netstat -ano
协议 本地地址 外部地址 状态 PID
TCP 0.0.0.0:49666 0.0.0.0:0 LISTENING 604
TCP 0.0.0.0:49667 0.0.0.0:0 LISTENING 1892
TCP 0.0.0.0:49668 0.0.0.0:0 LISTENING 4508
TCP 0.0.0.0:57621 0.0.0.0:0 LISTENING 22748
TCP 127.0.0.1:1001 0.0.0.0:0 LISTENING 4
TCP 127.0.0.1:1043 127.0.0.1:1061 ESTABLISHED 8452
TCP 127.0.0.1:1043 127.0.0.1:1063 ESTABLISHED 8452
UDP 192.168.159.1:1900 *:* 3060
UDP 192.168.159.1:5353 *:* 5248
UDP 192.168.159.1:58085 *:* 3060
netstat 命令 的 ano 三个参数主要用于扩展IP地址端口以及PID的显示,以及一些隐式的可能存在的通信也会被记录。 LISTENING:表示等待对方连接 ESTABLISHED :表示完成连接并且进行数据通信操作
套接字和协议栈和应用程序的交互流程如下:
- 协议栈在操作套接字之前,需要事先开辟一块空间来存放用于操作套接字的必要信息。
- 协议栈需要向应用程序返回描述符表示当前连接的是哪一个“管道”。
- 之后应用程序需要和协议栈交互就必须要携带描述符,不过这样也节省了协议栈了解应用程序要和哪一个套接字交互。
连接服务器
连接的目的是为了让两台不再同一个地方的主机能够相互认识对方,这时候不可避免的需要互相提供自己的信息,这样才能正确的建立连接然后使用套接字传输数据。
连接的含义
人和人之间的沟通有时候可以不使用一个语言,只要双方都听懂就行,但是对于计算机是行不通的。
所以连接操作的控制信息要根据通信规则确定,协议栈在通信之前需要依靠一块空间来存放必要数据,这块内存空间称为缓冲区。
连接需要双方各自告知自己的信息,所以连接最开始的时候是没有任何数据交互的,由于是TCP是全双工的协议客户端和服务器都需要建立套接字,不过双方不知道和谁连接,所以需要在客户端和服务端各自开辟一块空间来存放对方的IP和端口等必要的传输信息。
为了让双方既可以正常通信,又可以根据自己的系统设计协议栈和套接字的控制信息处理方式,网络通信设计采用了 控制信息的的方式让不同计算机和系统能相互认识。
所谓的控制信息可以认为是一种 通用语言,只要是符合这个控制信息规范的头部信息就可以被其他的计算机认识。
控制信息分为两类:
- 客户端和服务器的交换的控制信息,主要用于整个通信过程,这些内容在TCP协议进行规定。生活的例子理解是我们和别人通话之前,两边都得知道对方的电话号码和基本身份。
- 保存在套接字中用来控制协议栈操作的信息,这些信息主要用来传输数据,通常需要包括通控制信息和数据块,套接字需要通过控制信息了解到发来的是什么类型的数据,然后协议栈才能配合处理数据。
由于在一开始传输的时候是没有具体数据的,通常是一个空的报文头,所以这个控制信息也被叫做 协议头部, 比如下面提到的TCP头部,IP头部。
第一类:TCP 头部格式
第二类:套接字中的信息
连接的实际操作
连接的实际操作主要是调用CONNECT
函数,协议首先会传递给TCP模块,通过TCP模块交换获取控制信息的头部,以此了解具体要连接的套接字信息,然后把头部的SYN比特设置为1,表示可以连接。
TCP 模块处创建表示连接控制信息的头部,接着便把信息传递给IP模块进行委托发送。
三次握手
交换头部信息之后,接着便是常见的TCP三次握手的过程:
- 第一步:客户端主动打开TCB端口,服务器被动打开TCB端口。发起方携带一个SYN标志,并且携带一个ISN序号Seq=x,但是需要注意的是第一步的过程这个ISN序号是隐藏传递的(因为没有传递数据),因为如果请求不存在数据的交换则不会被显示。客户端发送SYN命令之后进入设置SYN=1,并且设置SYN-SENT(同步-已发送状态)。
- 第二步:服务器收到客户端TCP报文之后,也将SYN=1,并且回送一个新的ISN序号ack=x 1,并且将ACK=1表示自己收到了,然后在返回参数回送自己新的序列号表示自己的确认请求Seq=y,将状态设置为SYN-RCVD(同步收到)状态,(表示希望收到的序号为xxxx1522),最后也是指定MSS。
- 第三步:客户端收到服务器的确认报文之后,还需要向服务端返回确认报文,确认报文的ACK=1,并且回传服务器传递的ISN序号 1(ack = y 1),以及自己的ISN序号 1(Seq = x 1),此时TCP连接进入已连接状态,ACK是可以携带数据的,但是如果不携带数据则不消耗序列号。
- 最后一步:当服务器收到客户端的确认,也进入已连接状态。
经过三次握手连接建立,直到断开连接之前都可以传递数据。
收发数据
收发数据有两个重点:
- 第一点是收发数据并不关心数据的格式,而是根据头部信息来辨别是什么类型的数据,对于协议栈来说接收的的内容都是二进制的数据。
- 第二点是利用缓冲区减少频繁的数据传输提高传输效率。
缓冲区的大小如何控制?
- 每个数据包的数据长度,协议栈会根据一个叫作 MTU的参数来进行判断,但是MTU指的是总长度,除开头部信息之后获得真实的数据长度MSS。
- 时间,这个时间指的是固定的时间内容不管缓冲区有有没有达到MSS长度必须发送数据的时间,目的是防止等待时间过长造成请求延迟。
名词解释: MTU:一个网络包的最大长度,以太网中一般为 1500 字节。MSS:除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度。
但这两个因素实际上并不能完全决定收发数据的效率平衡,TCP协议没有规定协议栈如何平衡,具体需要看操作系统如何决定。
实际上协议栈收发数据是有所保留的,并不是强制按照协议的规定处理,而是给了应用程序一些可控选项,比如浏览器这种要求实时性的应用程序通常不使用缓冲区。Http请求拆分
通常情况http的请求响应内容可以通过一个网络包完成,但是针对POST请求等大表单的数据提交则通常会触发TCP拆包操作。
拆包是根据MSS的参数确定的,发送缓冲区会根据这个参数把一个超过一次请求长度的数据拆分为多个包,但是因为实际上同属一份数据,拆分之后所有的数据包都需要添加相同的头部。
注意:TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。
TCP粘包
TCP除了拆包动作之外还包含粘包的操作,所谓粘包是指TCP协议中发送方发送的若干包数据到接收方接收时粘成一个包,从接收缓冲区角度来看后一个数据的头紧接着前一包数据的尾部。
解决粘包、拆包问题策略?
粘包和拆包需要解决容易造成半包读写的根本问题,解决办法也有很多种,主要的策略基本很多网上资料都有讲到,这里直接搬运结论了:
- 请求消息定长,如果缓冲区不满,则通过补0的方式达到长度,防止粘包和拆包。
- 在包尾增加回车换行符进行分割,例如FTP协议;
- 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。(几乎不用)
ACK号确认网络包收发
ACK号码除了在三次握手的过程中确认对方是否有收到请求之外,还能作为判断接收的数据包是否完整的依据,在进行数据传输的时候,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方,以表示自己到底受到了多少数据,如果中间存在缺少数据则服务端重新传输即可。
当然仅靠ACK号不能完全作为参考依据,并且只使用ACK号是只考虑 单向传输的情况,但是TCP是全双工协议,无法确定数据接收方来自哪一方。
解决这个问题也很简单,实际在进行双向数据传输的时候双方各自会额外计算一个序号,序号其实就是一组随机数,在接收方收到数据之后每次都需要把序号 1回传给发送方表示自己接收到哪一个序号之前的所有数据。
通过ACK 序号的方式确保数据正确传输,这样可以使得其他网络通信组件不需要额外的失败补偿机制,如果发现丢包或者数据不完整的情况,直接根据序号进行重传重发的操作即可。
影响数据传输的因素
主要影响因素是返回ACK号的等待时间。
如果ACK号迟迟没有响应给对方服务器,势必会影响整个网络传输的效率,如果下一个数据已经准备好上一个返回包却没有发回去,很容易造成网络的堵塞,对方迟迟拿不到正确结果。
网络环境的复杂多变,这个等待时间不可能是固定的,所以TCP使用了动态时间的方法进行调整,具体的调整方法就是使用滑动窗口。
滑动窗口
滑动窗口:指的是在不等待ACK返回结果的情况下直接双方互相不间断的发送数据。
双方需要通过各自的缓冲区顺序返回ACK信息,但是如果无限制的发送数据会导致数据无法处理出现丢包,所以滑动窗口的关键是接收方需要告诉发送方自己最多能接收多少数据。
滑动窗口的细节通过一张图更好理解:
关于接收方的接收量,最大能承受处理多少数据是通过缓冲区大小确定的。另外需要注意下面的图只有单向的部分,实际上对于双向来说都是类似的处理。
影响数据传输的次要因素:返回 ACK号和更新窗口的时机。
关于这一点直接记住一个结论,接收方在发送 ACK 号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间,等到其他的通知合并到一起处理,因为ACK号体现的是已经收到的包的数据量,使用这样延迟发送的方式也可以防止过多的更新数据包出现。
最终协议栈收发数据的细节如下:
- 协议栈会根据收到的数据块和TCP或者IP头部解析内容,如果确认收到数据则返回ACK 序号。
- 协议栈会把数据块放到缓冲区进行存储,利用滑动窗口的特性按照顺序处理数据交给应用程序处理。
- 协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序
断开连接
断开连接的部分包含断开连接和删除套接字的操作,断开连接也就是经典的四次挥手的操作,而删除套接字则需要注意在协议栈中并没有规定关闭的时间,但是通常情况下过几分钟之后会删除套接字。
四次挥手端口tcp连接
- 第一步(客户端):TCP发送释放连接的报文,停止发送数据,释放报文首部,把FIN=1,同时发送序列号,根据上一次传送的序列号 1传送Seq = t 1(由于下图是在连接之后立马进行四次挥手,所以序列号没有变),此时客户端进行终止等待1的状态。注意FIN不携带数据也需要消耗序列号。
- 第二步(服务端):服务器回送确认报文,发出确认报文,ACK=1,并且把回传序列号 1回传(ack = t 1),然后再带上自己的序列号Seq = y,此时服务端进入CLOSE-WAIT状态(关闭等待状态),TCP服务器此时需要停止上层应用客户端向服务端请求释放,处于 半关闭 阶段,此时服务端依然可以向客户端发数据并且客户端需要接收并处理,关闭等待状态意味着整个状态还需要持续一段时间。
- 第三步(客户端):客户端接收到服务端确认请求,此时客户端进入到FIN-WAIT-2终止等待2的阶段,等待服务器的释放报文。(还有一部分服务器没有发送完的数据需要处理)
- 第四步(服务端):服务器把最后的数据处理完毕,向客户端发送释放报文,FIN=1,ack=t 1,由于需要把剩下的数据发送完成,假设处理完成之后需要带上自己的序列号Seq=w,服务器进入最后确认状态,等待客户端确认。
- 第五步(客户端):客户端收到报文之后,发出确认 ACK=1,ack=w 1,自己的序列号为Seq = t 1,此时客户端进入到了TIME-WAIT(时间等待状态),此时客户端还是没有释放,必须经过**2 * MSL(最长报文寿命)**之后,客户端撤掉TCB之后才进入CLOSED状态。
- 第六步(服务端):服务器收到客户端的请求立马进入CLOSE状态,同时撤销TCB,结束此次TCP的连接。(服务端结束TCP连接要比客户端早一些)
套接字和协议栈和对方服务器的交互流程细节还是比较多的,这里可以发现实际上三次握手和四次挥手实际上只是网络连接当中很小的一部分,最后是从连接服务到数据收发到断开连接的一张简单总结图,建议当作一个大概的流程参考:
IP和以太网的收发操作
上面的部分比较贴近TCP协议的相关操作,TCP完成连接收发的同时其实都需要IP模块的配合,在了解这两个模块如何配合工作之前需要了解完整的网络包是如何组成的。
包的组成
对于任何一个网络包,都有最外层的抽象概念,那就是头部和数据 两个部分
上面的部分有一个这样的图,里面套接字中的TCP数据,这里需要注意在TCP控制信息的前面就是以太网和IP的控制信息,对于只传输控制信息的网络包虽然没有数据的部分,但是可以把协议的头部信息作为数据部分。
通过下面的图也可以发现,所有的网络包必须要委托以太网和IP控制信息才能完成传输。
把存放数据的网络包进行拆分,可以看到下面的TCP/IP 包结构:
我们可以简单把头部和数据看做是平时的快递,头部是面单,指示从哪里到哪里,然后这个“快递”会通过网络转发设备的查表操作判断传输到那个方向。
转发设备是什么?这里建议看看第一章的关于认识网络传输的基本概念,这里简单提一下:
- 路由器根据目标地址判断下一个路由器的位置
- 集线器在子网中将网络包传输到下一个路由
但是实际上集线器和路由器各自有不同分工,集线器负责管理以太网规则传输包设备,路由器管理IP转发规则,所以上面两个步骤也可以做下面的理解:
- IP协议根据层级规则判断下一个IP转发设备。
- 子网的以太网协议转发给下一个转发设备。
实际上头部部分应该分为 MAC 头部 和 IP头部。为什么要把头部拆分为两个协议?实际上是为了让协议之间可以实现替换,比如MAC可以替换为局域网、ADSL、FTTH。同时因为互联网这样庞大的网络架构,需要更加细化的分工。
小结
实际上网络包的封装应该范围三个部分:
第一部分是TCP模块组织头部信息和数据包(当然也可能没有数据只有控制信息)。
第二部分是把整个TCP模块塞到IP模块的后面,然后经过网卡发送出去。
第三部分是在IP模块前面加上Mac信息。
关键:无论要收发的包是控制包还是数据包,IP 对各种类型的包的收发操作都是相同的。
名词解释ADSL[1]:可以理解为以前宽带使用拨号连接互联网上网的方式。 非对称**数字用户线路**[2](英语:Asymmetric Digital Subscriber Line)又称非对称数字用户环路(Asymmetric Digital Subscriber Loop),简称ADSL。ADSL是一个依靠铜质电话线的数据传输技术比传统的调制器更快。 FTTH[3]:其实就是现在的光纤通信。 光纤到户(英语:Fiber To The Home,缩写:FTTH)是一种光纤通信[4]的传输方法。是直接把光纤[5]接到用户[6]的家中(用户所需的地方)。 这种光纤通信方式及策略与FTTN[7]、FTTC[8]、HFC[9](Hybrid Fiber Coaxial)等也不同,它们都是需要依赖传统的金属电线,包括双绞线[10]及同轴电缆[11]等,作“最后一哩[12]”的信息传输。
IP 协议头部
IP类似快递上的单号,所以实际上IP模块是无法决定自己选择正确的地址了,哪怕应用程序通过TCP告诉IP发的地址式是一个错误地址,IP也无法自行修正只能照做。
从这样的特点可以看出IP头部又有点类似快递员,和网上买东西商家发错地址或者我们填错地址一样,不能把责任赖在快递员上。
IP协议头部的组成类似下面的结构,注意IP地址的长度固定需要32Bit的空间占用。
这里需要注意“发送方的IP”地址不是指计算机的IP,而是指网卡对应的IP,因为IP不是分配和计算机而是网卡的,当一个计算机有多个网卡就会存在多个IP。
那么应该如何判断包发送给哪一个网卡?这里涉及到IP协议规则,无论是路由器的转发还是协议栈的处理都需要按照IP协议转给下一个用户。
查询分配给哪一个网卡在不同操作系统中的查询方式不同,查询发送端需要查询是哪个网卡把包发给了路由器,这个动作只需要简单的根据路由器IP地址和网卡的IP进行比对。
在windows中可以通过命令
route print
查看路由信息。
获取IP和网卡之后,还需要知道包所属的协议,委托内容是固定的,比如TCP模块就是06,UDP就是17,大部分请求都是HTTP,使用TCP的方式传输。
以太网Mac头部
TCP/IP模块只能在传输层上互相了解,但是往下的链路层以太网用同样的规则是行不通的,所以头部加上TCP/IP的头部之后,还需要在头部加上Mac头部,Mac头部包含了发送方和接收方的Mac信息,这里可以简单理解为Mac和IP的作用类似,不过Mac头部是48Bit,而IP头部是32Bit。
需要注意以太类型就是Mac包装的后面的真实数据的类型, 如果是IP就是IP协议。另外需要注意在发送Mac包给接收方之前,由于不知道对方的Mac地址,所以还需要一步查询操作。
注意IP 模块根据路由表 Gateway 栏的内容判断应该把包发送给谁。
下面是Mac头部的组成:
查询Mac地址
查询对方的Mac地址需要用到ARP( Address Resolution Protocol,地址解析协议),ARP通过广播的方法查找到目标地址,所谓广播就是字面意思把消息发给所有的其他互联网用户,等待对方应答。
为了防止每次查询都要带ARP的数据,所以有一块ARP的缓存专门缓存这个地址,但是需要注意这个缓存和IP模块的IP地址一样,过一段时间会被ARP缓存淘汰掉,但是如果IP刚刚变化可能会导致ARP缓存未及时更新导致网络异常。
实际整个工作都是由IP模块完成的,虽然Mac地址是以太网数据传输的必要内容,但是实际上让IP模块负责这些工作是有利的。
为什么需要以太网?
# 有了 IP 地址,为什么还要用 MAC 地址?
以太网基本知识
首先来看看以太网的基本发展,虽然设计结构越来越精细,但是本质上干的活却没有发生变化。
以太网早期原型本质上可以看作是一根网线以及一个用于收发的设备,网络信号发送之后通过广播最终到达所有设备,在开头的收发信息让其他人可以知道信息最终要发给谁,在Mac头部就包含了“收货地址”,而具体发送了什么类型的可以通过上面的“以太类型”进行判断。
以太网在后续的发展中将主干网线替 换成了一个中继式集线器,收发器变成双绞线,虽然形式变了,但是本质的工作没有变。
以太网到了现代最终由交换式集线器完成所有的操作,并且网络请求只有请求方和接收方可以互通,集成度增加以及网络传输安全性能增加。
但是以太网无论怎么发展性质始终没有任何变动:
MAC 地址代表的目的地,用发送方 MAC 地址识别发送方,用以太类型识别包的内容。
IP模块转光(或电)信号
网络信号发送依赖网卡,但是网卡并不是插上电就可以使用的,还需要依赖初始化以及驱动程序才能完成操作,驱动程序和初始化操作在其他很多电脑硬件中比较常见,但是以太网有比较独特的驱动操作,那就是控制以太网收发操作的MAC当中收发MAC地址。
另外网卡还有一个特性是网卡的 ROM 中保存着全世界唯一的 MAC 地址,这是在生产网卡的时候就已经决定。
所以可以看到最终完成IP数据转化的关键是驱动程序,网卡中保存的 MAC 地址会依赖网卡驱动程序读取并分配给 MAC模块。
网络包的控制信息
MAC模块工作在网卡调用MAC包发送请求命令之后,MAC模块的工作是划分网络包的“边界”。
为了划分边界,MAC模块会加上三个控制信息:
- 报头:是一串像 10101010…这样 1 和 0 交替出现的比特序列,长度为** 56 比特**,它的作用是确定包的读取时机。
- 起始帧分界符(SFD):确定帧的起始位置,主要是辅助电信号切分报文头部和真实的网络包边界,并且判断出每个比特的界限。
- FCS:检查包传输过程中因噪声导致的波形紊乱、数据错误,它是一串 32 比特的序列,是通过一个公式对包中从头到尾 的所有内容进行计算而得出来的
响应内容传输从IP给TCP
当服务器接收到网络包之后,首先协议栈会判断以太网头部的以太类型,发现是0800为TCP/IP协议,接下来是IP模块工作,首先是检查IP头部是否正确,IP地址是否正确。
如果接收方是window客户端,因为不会对包进行转发,如果发现包不是发给自己的,会调用ICMP消息回传给请求发送方,IMCP的消息格式如下:
另外接收到的网络请求可能会因为数据包过大出现IP分片,分片的包会在 IP 头部的标志字段中进行标记,IP模块会把分片过的包暂存内部内存空间,等相同ID的包全部接收到缓冲区之后再拼接。
怎么保证拼接的顺序正确呢?可以查看前文IP 头部还有一个分片偏移量(fragment offset)字段,它 表示当前分片在整个包中所处的位置。
IP模块完成数据分片重组之后,数据包交给TCP模块操作,TCP还会再次检查一遍请求方和接收方的IP信息,以及获取端口号找到对应的套接字,找到套接字之后根据应用程序的类型进行不同的操作,这个过程可能是建立连接,也可能是完成应用程序数据的读写操作。
这里可能会觉得IP检查不是IP模块的操作么,TCP去看IP模块的信息是不是“越权”了?实际上这是一种性能开销都考虑而违反“迪米特法则(Law of Demeter)”的一种特例。因为TCP模块需要频繁使用IP模块的信息,如果老是需要数据之间的交互传输非常影响性能。
UDP协议收发操作
TCP/IP为了保证数据准确收发需要使用一系列复杂的模块和过程配合保证数据的完整传输,但是有时候有些应用程序为了保证高效会舍弃使用TCP这种复杂的机制。
UDP协议的要点是尽可能将所有的数据通过一个包解决,UDP 没有 TCP 的接收确认、窗口等机制,因此在收发数据之前也不 需要交换控制信息。UDP的实现非常简单只需要应用程序加入头部,直接交给IP模块完成即可,接收方也只需要检查IP头部的发送方和接收方的IP地址信息,然后再从UDP找到端口号,最后再找到套接字信息把数据给应用程序。
因为UDP不保证传输的稳定性所以无论包是否接收到都无关紧要,只要对方没有回应直接把包进行重发即可。这种不需要保证传输稳定性的场景还是有不少的,比如聊天数据、音频和视频信息,即使丢失一点点也没有关系,最多是卡顿一下而已。
关键:UDP 可发送的数据最大长度为 IP 包的最大长度减去 IP 头部和 UDP 头部 的长度。一般来说 IP 头部为 20 字节,UDP 头部为 8 字节,因此 UDP 的最大数据长度为 65 507 字节。
下面是UDP的头部信息:
小结
在第二章我们了解Socket链接的步骤和大致细节,另外介绍了整个互联网比较重要的两个协议TCP协议和IP协议,在两个协议中需要重点掌握头部设计,IP模块完成TCP模块的数据,TCP数据封装应用程序数据,之后还需要配合Mac以太网模块完成网络包的最后封装,等一切准备工作完成之后,由网卡以及驱动程序把整个包发送出去,所以其实可以看到决定你能不能上网等实际上是网卡和驱动(这不是废话),但是这些内容属于不同层级的内容,需要一一消化。
介绍了TCP之后,在在第二章最后部分简单提到了UDP协议,UDP是一种简单暴力的协议,设计的目的是让所有的数据尽可能通过一个包完成,所以他不需要链接也不需要保证数据安全传输,数据丢了直接传输即可,UDP的应用也是十分广泛的,比如游戏,视频,音乐等等数据的传输,很多时候丢一点根本无关紧要,因为即使找回来这些数据也没有意义。
TCP/IP连接也只是互联网数据传输的一小部分,但是确实最为核心的部分,虽然往下还有以太网和网卡以及网络通信如何上网等细节,但是只有深刻了解TCP/IP协议才能了解整个互联网是如何交互和数据传输的。
参考资料
[1]ADSL: ADSL/ADSL.md
[2]数字用户线路: https://zh.m.wikipedia.org/wiki/數位用戶線路
[3]FTTH: FTTH/FTTH.md
[4]光纤通信: https://zh.m.wikipedia.org/wiki/光纖通訊
[5]光纤: https://zh.m.wikipedia.org/wiki/光纖
[6]用户: https://zh.m.wikipedia.org/wiki/用户
[7]FTTN: https://zh.m.wikipedia.org/wiki/FTTx
[8]FTTC: https://zh.m.wikipedia.org/wiki/FTTx
[9]HFC: https://zh.m.wikipedia.org/w/index.php?title=HFC&action=edit&redlink=1
[10]双绞线: https://zh.m.wikipedia.org/wiki/双绞线
[11]同轴电缆: https://zh.m.wikipedia.org/wiki/同軸電纜
[12]最后一哩: https://zh.m.wikipedia.org/wiki/最後一哩