当前位置:   article > 正文

网络原理之TCP_IP_tcp/ip网络套接字编程的工作原理

tcp/ip网络套接字编程的工作原理

一、网络初识 回顾

网络编程背景

网络中的基本概念中最重要的概念:协议。要想进行网络通信,一定是离不开协议的。协议就是针对数据格式的约定,发送方和接收方以相同的方式进行信息的交互。那么这里又聊到了协议的分层,首先我们要明确,网络通信是一个很复杂的工作。如果使用一个协议来完成所有的工作,这个协议就会复杂无比,就会使得我们的学习成本非常的高,所以我们的程序员就会把这个大的协议拆分成多个小的协议,每个协议负责一部分工作。当拆分完之后就有了另外的问题:协议的数量很多。那么为了更好的归置这些协议,就按照协议的功能/作用进行不同程度的分类,也就是协议分层了。为了让协议之间更好的配合,约定,上层协议调用下层协议,下层协议给上层协议提供服务(协议之间不能跨层交互)。于是就构成了“协议栈”,这个协议栈的具体划分有两种方式:OSI七层模型、TCP/IP五层模型。我们主要讨论TCP/IP五层模型。

这五层模型由应用层、传输层、网络层、数据链路层、物理层构成。那么这五个层次各自有各自的用途。

物理层:信息传输的公路。(基础设施:修路)

数据链路层:负责相邻节点之间的数据传输(相邻节点:通过网线/光纤/无线直接相连的两个路由器/交换机/主机……)

网络层:负责任意两个节点之间的传输(路径规划)

传输层:只考虑起点和终点(更加抽象,不再关注中间传输过程,考虑发送方、接收方)

应用层:数据具体要怎么使用(数据用于显示还是计算)

网络数据传输的基本流程:封装与分用

封装:发送方,把数据从上到下层层加上协议报头(包装快递)

分用:接收方,把数据从下层到上层进行解析(拆快递)

需要明确的是网络传输并不是一个简单的事情,需要经过复杂的层层封装与层层分用才能完成一次网络传输。

这里用简图说明:

注:这里的交换机只是封装分用到数据链路层就可以完成后续的转发(也叫做二层转发);

路由器封装分用到网络层就能完成后续转发(也叫做三层转发)。

我们这里指的都是“传统意义”的路由器和交换机,也就是教科书里写的路由器和交换机。实际上真实情况是不一定的,有些路由器/交换机是可以封装分用到应用层的(这主要是站在运营商的角度看待,交换机路由器这些网络设备,都是运营商的,运营商有时需要做其他的事情,就可能会进行应用层级别的分装分用(一个典型的例子:舆情分析,网民通过互联网传播一些言论,运营商可以检测到这些数据是什么时候发送到哪里的))。

二、网络编程 回顾

网络编程我们主要认识的是Socket(网络编程套接字), 那么我们操作系统给我们程序员提供的网络编程API就是Socket套接字,那么Java里面针对这个API进行了封装,封装了之后依然是叫做Socket。那么针对Socket API,Java提供了一组类来完成网络通信,这里面分成UDP Socket和TCP Socket两个大类。

UDP和TCP都是传输层的协议,这两个协议差别很大,以至于使用他们的风格时差别也很大。

UDP:无连接,不可靠传输,面向数据报(DatagramPacket),全双工

TCP:有连接,可靠传输,面向字节流,全双工

具体到代码的编写中其基本功能:

UDP的Socket:

DatagramSocket:代表一个socket对象(本质上是一个文件,相当于 网卡 的抽象),并提供send(发送)、receive(接收)、close等方法的使用

DatagramPacket:代表一个UDP的数据报。(构造对象,指定字节数组作为缓冲区。)

TCP的Socket:

ServerSocket:专门给服务器用的。提供了accept方法用于接收一个连接。

Socket:服务器客户端都会用到。客户端使用Socket和服务器建立连接,并且进行后续传输。服务器使用Socket和客户进行交互。

Socket提供了getInputStream和getOutputStream获取到输入输出流,进一步通过这些流对象来完成数据传输。

三、传输层重点协议

按照TCP/IP协议栈的每个层次,介绍这里的关键协议和关键知识。

应用层

这里我们知道应用层和应用程序是密切相关的,它描述了应用程序如何理解和使用网络中的通信数据,应用层是和程序员打交道最多的地方,在这里程序员最主要的工作就是自定义协议。

下四层都是在系统内核/驱动程序/硬件中已经实现好的,我们只能去了解,不能修改,而应用层协议则是我们可以自定义的。

自定义协议主要做两件事:

  1. 根据需求,明确协议数据要传递哪些信息。
  2. 明确数据组织格式。(比如纯文本、xml、json、protobuffer)

应用层除了上述自定义的协议之外,也有一些现成的广泛使用的协议,最典型的是HTTP协议,这里先不具体展开介绍。

传输层

传输层虽然是操作系统内核已经实现好了,但是程序员写代码要调用系统提供的socket API完成网络编程的。socket就是属于传输层的部分。

关于传输层主要涉及到的协议就是UDP和TCP协议。

我们在学习一个协议最重要的就是要理解报头结构。


补充一些概念:

端口号:用于区分一个主机上具体的应用程序

要求:在同一个主机上,一个端口号不能被多个进程绑定。比如说现在有一个进程A绑定了端口3306,进程B也尝试绑定3306,这是进程B的绑定操作就会失败(抛出异常)

注:端口号是传输层协议的概念。接下来介绍的TCP/UDP协议报头中都会包含源端口和目的端口,这两个都是使用2个字节,16个bit位来表示的,一个端口号的取值分会是0~65535。

但是我们自己写程序的时候,我们所绑定的端口得从1024起,因为0~1023之间的端口号,称为“知名/具名端口号”,这些端口号是属于已经分配给了一些知名的广泛使用的应用程序了。

而如果要用1023以内的端口,就需要确定所使用的端口当前没有程序在绑定,并且要确定是拥有管理员权限的情况下,才能使用。


UDP协议

UDP的基本特点:无连接、不可靠传输、面向数据报、全双工

UDP协议的报文结构

UDP会把载荷数据(通过UDP Socket,也就是send方法拿来的数据的基础上 在前面拼装上几个字节的报头(相当于字符串拼接,此处是二进制的,不是文本的)),就构成了整个UDP报文结构。

UDP报头里包含了一些特定的属性,这些属性携带了一些重要的信息。

注:

  1. 不同的协议功能不同,报头中带有的属性信息就不同。对于UDP来说,报头一共就是8个字节,分成4个部分(每个部分两个字节):源端口、目的端口、UDP报文长度、校验和
  2. UDP报文长度也是用2个字节表示的,2个字节表示范围为0~65535,换算kb为64KB,所以一个UDP数据报,最大只能传输64KB的数据。那么如果应用层数据报超过了64KB要怎么办?第一个办法,需要在应用层通过代码的方式,针对应用层数据报进行手动的分包,拆成多个包通过多个UDP数据报进行传输(这个方案工作量很大)。第二个办法我们就不用UDP协议了,换成TCP,TCP则没有这样的限制。
  3. 校验和:通信双方验证传输的数据是否正确。

网络传输过程中,可能会受到一些干扰,在这些干扰下就可能出现“比特反转”的情况。

比特反转:1变成0,0变成1,出现这种情况主要是由于网络传输本质上就是光信号/电信号,这些可能会受到一些外界物理环境的影响(电场、磁场、高能射线……),这些干扰就可能导致本来要传输的数据如果是1111 0000,在被干扰之后可能变为1111 0001,一旦数据变了,对于数据的含义就可能是致命的,就比如说在程序中经常使用1表示某个功能开启,0表示关闭,而如果在传输过程中数据报是想进行开启的功能,但是因为比特反转,就导致变成了关闭了。

而这样的现象是客观存在不可避免的,我们能做的就是及时识别出当前的数据是否有出现问题。

因此引入了校验和来进行鉴别。

校验和 能针对数据的所有字节内容进行一系列数学运算,得到一个比较短的结果(2字节或其他比较短的字节 ),如果数据内容一定,得到的校验和结果就一定,如果数据变了,得到的校验和结果也就变了。


针对网络传输的数据来说,生成校验和的算法有很多种,其中比较知名的有一下几个:

  1. CRC

循环冗余校验。它的特点是比较简单粗暴,把数据的每个字节循环往上累加,如果累加溢出了,高位就丢弃。但是这种方法的缺点比较明显,如果要校验的数据同时变动了两个bit位(前一个字节少1,后一个字节多1),就会出现内容变了,CRC没变这种情况。

  1. MD5

MD5是经过一系列数学公式进行数学变换的方式生成校验码。它的特点如下:

(1)定长。无论原始数据多长,得到的MD5值都是固定长度(4字节版本,也有8字节版本……)

(2)冲突概率很小。原始数据哪怕只变动一个地方,算出来的MD5值都会差别很大(让MD5结果更分散了)

(3)不可逆。通过原始数据计算MD5,很容易,但是通过MD5还原成原始数据就会非常难,理论上是不可能实现的(计算量极大)

MD5这样的特点让MD5作用更多了:首先可以作为校验和,其次可以作为计算hash值的方式,还可以进行密码的加密操作等。

  1. SHA1

TCP协议

TCP协议相比于UDP协议来说更复杂不少,也比UDP更加重要。

TCP的特点:有连接、可靠传输、面向字节流、全双工

  1. 16位源端口号、16位目的端口号:和UDP一样,都是表示端口号。
  2. 4位首部长度:描述了TCP报头有多长。另外选项之前的部分是固定长度(20字节)。

一个报文是由报头+载荷构成的,其中报头就是首部。那么TCP的报头为4位首部长度,其长度为可变的,不是像UDP一样固定8个字节,TCP的长度就不一定。

注:首部长度在这里是4bit位,它的的单位是4字节,而不是字节。比如说首部长度值是5,则整个TCP报头是20字节(相当于没有选项),如果首部长度是值15,则整个TCP报头是60字节。

  1. 选项:对TCP报文的一些属性进行解释说明。(可有可无,数量也不确定)选项部分长度=首部长度-20字节。

因此可以通过首部长度来描述出报头有多长,进一步说,由于选项前面的这部分是固定长度的,所以选项这部分有多长也可以通过首部长度来计算。

  1. 保留(6位):将6位的空间空出以备后续需要扩展时考虑。

对于网络协议来说,扩展升级是一件成本极高的事情,就UDP来说,其报文长度是2字节,因此它的一个包最大为64KB,那么如果想把UDP协议升级,让它能够支持更大的长度,所要付出的成本是极高的,世界上大部分计算机的操作系统只支持2字节长度的UDP,若要升级则所有计算机都需要升级,综上引入保留位,大幅降低了上述的升级成本,此时对于TCP报头结构的影响是比较小的,不升级的设备也更容易兼容。

注:程序员进行程序开发时的一个重点考虑的事情就是程序的可扩展性

  1. 16位校验和:和UDP校验和同理。

----------------------------------------------------------------------------------------------------------

  1. 序号:对传输的数据进行编号即为序号。
  2. 确认序号,对接收方传回的应答报文的编号即为确认序号。

其余的后续解释。


TCP内部的工作机制

TCP是一个比较复杂的协议,里面有非常多的机制,我们在这里主要讨论TCP提供的10个比较核心的机制。

一、确认应答

我们知道,TCP是可靠传输的,那么确认应答则是TCP实现可靠传输的最核心机制。

比如说A给B发了个消息,B收到之后就会返回一个应答报文(ACK),此时A收到应答之后,就知道了刚才发的数据已经顺利到达B了。

接下来我们再讨论更复杂一点的情况,当A给B连续发两条消息时,会出现后发先至的情况,也就是在两个主机之间,存在多条能够发送数据报的路线,比如说数据报1走的是a路线,而数据报2走的是b路线,那么两个数据报到达B的顺序就会存在变数。所以应答报文到达的顺序也是可能发生变动的,此时就需要考虑如何规避这种顺序错乱带来歧义。

实际上方法很简单,我们只需要给传输的数据和应答报文都进行编号即可。

注意:任何一条数据(包括应答报文)都是有序号的。确认序号则应是只有应答报文才有。(普通报文的确认序号字段里的值无意义)。那么报文是否是应答报文,取决于上图中在 保留字(6位) 后的标志位ACK,如果ACK为1,就表示是应答报文,如果为0,则表示不是应答报文。

当然,由于TCP是面向字节流的,所以在编序号的时候也是按照字节来编号的。


举个例子,要从A给B发送1000个字节的数据,假设从1开始编号,此时第一个字节的序号就是1,第二个字节的序号就是2……,但是由于这1000个字节都属于同一个TCP报文,TCP报头里就只记录当前的第一个字节的序号。此处的报头的序号写的是1。那么如果再次从A向B发送第二条数据,那么此时第二个TCP数据报的头一个字节序号就相当于是1001。如果长度是1000,此时最后一个字节序号是2000。由于1001~2000都是属于同一个TCP数据报,所以报头里只需要填写1000就行了。


TCP的字节的序号是一次累加的,这个依次累加的过程,对于后一条数据来说,起始字节的序号就是上一个数据的最后一个字节的序号。每个TCP数据报报头填写的序号只需要写TCP数据的头一个字节的序号即可。TCP知道了头一个字节的序号,再根据TCP报文的长度,就很容易知道每个字节的序号。

小结:TCP的可靠传输能力,最主要就是通过确认应答机制来保证的,通过应答报文,就可以让发送方清楚的知道传输是否成功。进一步引入了序号和确认序号,针对多组数据进行详细的区分。

二、超时重传

在上文中讨论确认应答的时候,只讨论了顺利传输的情况,那么如果丢包了呢?

首先丢包有两种情况:1. 发送的数据丢失;2. 返回的ACK丢失。但是对于发送方来说,就是没有收到ACK,它是无法区分是哪种情况引起的丢包,于是就都认为是丢包了。

不过,在通常情况下,丢包的概率是比较小的,如果重新发一下这个数据报,还是有很大的概率成功传输的。因此TCP就引入了重传机制,在丢包的时候,就会重新再发一次同样的数据。

TCP的重传机制是TCP引入了一个时间阈值,在发送方发了一个数据之后,就会等待ACK并开始计时,如果在时间阈值之内无论是什么情况,只要没有收到ACK,就直接视为丢包了。

超时重传:超过一定时间,还没有相应,就重新传输。

于是会有一个情况出现:一条消息发出去两次,假如是支付请求是两次的话,那么问题就十分严重了。

那么TCP对于这种重复数据的传输,会进行去重的特殊处理。

在TCP中存在一个“接收缓冲区”(每个socket对象都有一个接收缓冲区(其实也有一个发送缓冲区))这样的存储空间(接收方操作系统内核里的一段内存),当主机B接收到主机A发送的数据时,实际上是B的网卡读到数据,然后将数据放至B的对应socket接收缓冲区中。

后续应用程序使用getInputStream,进一步的使用read从接收缓冲区来读取数据。

注:接收缓冲区可以是一个阻塞队列,那么根据数据的序号,TCP可以很容易识别出当前接收缓冲区里的这两条数据是否是重复的,如果重复,则把后来的这份数据直接丢弃了,就保证了应用程序调用read读取到的数据一定是不重复的。

小结:由于去重和重新排序机制的存在,发送方只要发现ACK没有按时到达,就会重传数据。即使重复或者顺序乱了,接收方都能很好的处理好(去重和排序都依赖TCP报头的序号)


重传的数据有可能再次丢包,所以超时重传可能会重传许多次,不过在重传到达一定次数的时候,就不会再继续重传,TCP会认为网络出现故障,接下来尝试进行重置连接(相当于断开重连),如果重置还是失败,就会彻底断开连接。


可靠传输的TCP最核心的部分,TCP的可靠传输就是通过确认应答+超时重传来进行体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出现问题的情况。这两者相互配合,共同支撑整体的TCP的可靠性。

三、连接管理

连接管理就是 连接+管理。

首先要注意区分 连接 与 链接 这两个概念。

连接指的是通信双方进行通信的“证件”,互相记录和对方的连接关系以建立连接。而链接泛指“快捷方式”,快捷方式其实就是一个文件,保存了目标程序的路径,通过这个快捷方式可以更快速的找到所需资源。

而管理就描述了上述的连接是如何创建、断开等。

TCP的建立连接(三次握手)

建立连接就是通信双方各自记录对方信息,彼此之间相互认同的过程。

其中双方需要各自发起“建立连接”的请求,同时,回应给对方一个ACK,那么其实这里其实是有四次信息交互在里面的,只不过中间的两次交互是可以合并成一次交互,所以构成了“三次握手”。

解释一下为什么中间的两次可以合并?

原因就在于封装分用,每一个TCP数据报都需要经过各个层次的封装分用,这里的封装分用是有开销的,那么分装分用一次一定比封装分用两次付出的成本是要更高的。

三次握手的另一个作用:验证通信双方的接收与发送能力是否正常。所以三次握手也一定程度上保证了TCP传输的可靠性(只是辅助作用,而不是关键作用)

三次握手的意义:

  1. 让通信双方各自建立对对方的“认同”
  2. 验证通信双方各自发送能力和接收能力是否ok
  3. 在握手过程中,双方来协商一些重要的参数。

TCP在通信过程中,有些数据是通信双方需要相互同步的,此时就需要有这样的交互过程,那么这时就恰好可以利用三次握手的机会来完成数据的同步。

注意:TCP协议特点中“有连接”和此处三次握手的联系:因为TCP是有连接的,所以TCP需要能够建立连接/断开连接,其中建立连接的流程是三次握手。


接下来我们看看三次握手具体是什么样子的。

  1. 客户端主动给服务器发起的建立连接请求,称为“SYN”,也叫做同步报文段。
  2. TCP的状态

在建立连接阶段,主要有两个状态:

(1)LISTEN 服务器的状态

表示服务器已经准备就绪,随时可以有客户端来建立连接了

(2)ESTABLISHED 客户端和服务器都有的状态

表示连接建立完成,通信双方可以正常通信了。


TCP的断开连接(四次挥手)

四次挥手和三次握手非常类似,都是客户端服务器之间的数据交互,通信双方各自向对方发起一个断开连接的请求,在各自给对方一个回应。

注意:在断开连接的过程中,中间两次通常情况下不能像三次握手那样合并!要注意必须两个数据发送的时机相同,才能合并,如果是是不同的时机,就合并不了。


三次握手的中间两次能够合并的原因是因为SYN+ACK是同一时机发送的,具体来说,在三次握手这三次交互过程中,是在纯内核中完成的,应用程序感知不到,也干预不了。服务器的系统内核收到SYN之后,就会立即发送SYN+ACK。


FIN的发起,不是由内核控制的,而是由应用程序调用socket的close方法(或者进程退出)才会触发FIN。

而ACK是由内核控制的,由内核收到FIN之后立即返回。


四次挥手中涉及到的两个重要的状态

  1. CLOSE_WAIT:出现在被动发起断开连接的一方。等待关闭(等待调用close方法关闭socket)

注:建立连接一定是客户端主动发起请求。断开连接则不一定,可能是客户端或者服务器中的一个主动发起。

  1. TIME_WAIT:出现在主动发起断开连接的一方。

假设是客户端主动断开连接,当客户端进入TIME_WAIT状态时,相当于四次挥手已经结束了。但此时这里的TIME_WAIT要保持当前的TCP连接状态不要立即释放,因为这个时候在最后一个ACK发出的时候还是会有丢包的可能的,(在TCP建立连接断开连接的三次握手四次挥手的过程中,也会有超时重传机制,下注)TIME_WAIT也会等一段时间,一段时间之后如果也没收到重传的FIN,就会认为最后一个ACK没有丢,再彻底释放连接。

注:三次握手和四次挥手中的超时重传机制:

如果是最后一个ACK丢包了,站在服务器的视角来看,服务器是不知道是因为ACK丢了,还是自己发的FIN丢了,那么就会统一认为是FIN丢了,统一进行重传操作。

TIME_WAIT具体保持多长时间才释放?2MSL。(MSL为互联网上两个节点之间数据传输消耗的最大时间)

四、滑动窗口

前面写到的确认应答,超时重传,连接管理,都是给TCP的可靠性提供了支持,其中前两个是最核心的,而第三个是作辅助作用的。

我们要注意,TCP引入可靠性是以付出传输效率为代价的。所以可靠性和传输效率是冲突的,因此前面的UDP虽然没有可靠性,但是传输效率要比TCP高,不过TCP也会竭尽可能的提高传输效率(本质上是补救措施,它再怎么提高,也不可能比UDP完全不考虑可靠性的效率高,但是至少可以让自己的效率不太拉胯)

滑动窗口本质上就是降低了确认应答,等待ACK消耗的时间。在保证可靠性的基础上,来尽可能的提高传输效率(尽量降低效率的折损)

进行IO操作时,其实时间成本主要是两个部分:

  1. 等;2. 数据传输(数据拷贝)

大多数情况下,IO花的时间成本大头都是在等。


对于基本确认应答的情况来说,每次发送一次数据都是需要等待ACK来了再接着发下一个数据。

滑动窗口的本质就是不等待的批量发送一组数据,然后使用一份时间来等待着一组数据的多个ACK。

那么在此处我们就把不需要等待就能直接发送的最大数据量称为“窗口大小”,在上图中的窗口大小就是4000。

当批量发送了窗口大小这些数据之后,发送方就要等待ACK了,当有一个ACK到达了之后,就继续往下发下一条数据。于是对于上图来说等待的ACK始终都是4条。

上图则描述了滑动窗口的“滑动”。


上述情况下,我们讨论一下在传输过程中发生丢包情况,TCP是如何处理的。

  1. 数据已经抵达,ACK丢了

实际上这种情况下,不需要做任何处理。确认序号(即应答报文ACK)的意义在于表示该序号往前的所有数据都已经确认抵达。比如上图中,2001的数据成功传回,表明2001之前的数据已经确认到达,所以即使1001这个ACK没有传回,2001也已经涵盖了1001这个ACK的信息。

  1. 数据报未抵达,在传输途中丢了。

当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001" 一样;

如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送;

这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;

五 、流量控制

流量控制是一种干预发送的窗口大小的机制。

前面的滑动窗口中,我们能看到,滑动窗口越大,传输效率就越高(一份时间,等的ACK就越多),但是这个窗口也不能无限大。

如果滑动窗口无限大的话,一方面由于完全不等ACK,就不一定能保证可靠性。同时窗口太大的话,也会消耗大量的系统资源。再一方面,发送的速度太快,接收方处理不过来,发了也是白发。

所以我们窗口大小也是要有一些约束条件的。

  1. 接收方的处理能力。

发送方发的速度,不能超出接收方的处理能力。流量控制就是需要根据及诶收房的处理能力,协调发送方的发送速率。

可以直接看接收方接收缓冲区的剩余大小来衡量接收方的处理能力。

对于接收缓冲区,我们可以把它想象成一个蓄水池,缓冲区剩余空间大小即可用于衡量发送方发送速率的指标。

每次A给B发了个数据,B就需要计算一下缓冲区大小,然后把这个值通过ACK返回给A,A就根据这个值来决定接下来的发送速率是多少(窗口大小是多少)

那么接下来它是怎么通过ACK报文传回给A的呢,我们再回看TCP的报头结构。

在报头结构中有一个16位的窗口大小,我们就在报头中通过16位窗口大小,来向发送方反馈当前发送的速度,这个字段只在ACK中生效。那么发送方接下来就会根据这个数字确定下一轮发送的窗口大小。

16位隐含的含义是64k,但是并不意味着窗口大小最大是64KB,因为TCP为了让窗口更大,在选项属性部分引入了窗口扩展因子。(比如窗口大小已经是64KB,扩展因子里写了2,就意味着让64 << 2 = 256KB)

注:由于接收方缓冲区剩余空间一直在动态变化,所以每次返回ACK带的窗口大小也都在变化,发送方也是在随着传输过程的进行不断的动态调整窗口大小。


当窗口大小为0,发送方就要暂停发送,暂停发送的,会定期给B发送 窗口探测报文,这个报文不携带具体的业务数据,只是为了出发ACK查询的窗口大小。

六、拥塞控制

上述的发送方的窗口大小实际上由流量控制拥塞控制共同决定。

流量控制考虑的是接收方的处理能力,而拥塞控制描述的是传输过程中中间节点的处理能力。

在我们正常的网络传输中,从A到B是经过一系列路由器交换机进行转发的。

注意我们前面考虑A的发送速率,只是考虑了B的处理能力,而没有考虑中间节点。如果B处理能力很强,但是中间某一个节点很慢,即使A的速度再快,在中间节点传输的时候也会出现问题。接收方的处理能力是容易量化衡量的,但是中间节点的处理能力则并不好衡量。

那么既然TCP既然不好量化处理能力,TCP就引入了一种“实验”的方式来逐渐找到一个合适的窗口大小(合适的发送速率)。


上图描述了拥塞控制的“实验”过程。(只是描述其策略,而不是具体的过程)

拥塞窗口:尝试以多大的窗口大小进行发送。

最开始时,第0轮,窗口大小为1,以非常慢的速度发送数据(注意此处的1不是代表1字节,而是1单位)

接下来如果传输顺利,没有丢包发生,就扩大窗口。

然后是第一轮,窗口大小扩大一倍为2。如果还不丢包,继续扩大一倍……

所以初始阶段,由于初始比较小,每一轮不丢包都会使窗口扩大一倍(呈指数增长)。而当增长速率达到阈值之后,就成为了线性增长。(注意增长的前提是不丢包)

再接下来,当传输过程中一旦发生丢包了,此时就说明发送的速率已经接近网络的极限,那么就会把窗口一下缩成很小的值,并且重复刚才指数增长和线性增长的过程了。

注:拥塞窗口不是固定的值,而是一直动态变化的。随着时间的推移,逐渐达到一个动态平衡的过程。这样做既能够把问题解决掉,同时也能随着网络的动态变化而动态变化。

综上,拥塞窗口和流量控制的窗口,共同决定了发送方实际的发送窗口(元素窗口和流量控制窗口的较小值)


七、延时应答

延时应答也是提升效率的机制,在滑动窗口的基础之上,在提高传输速度。也就是说,要在接收方能够处理得了的情况下,尽可能的放大滑动窗口。

举个例子,CPU有一个主频,这个主频决定了CPU的处理速度,那么在这里面CPU有一个超频的机制,比如在工作时的正常电压是1.5V,但是可以把电压提高到1.7V,这样CPU的工作效率就会更高,频率也就上升了。

延时应答也是类似的机制,在收到数据之后,不是立即返回ACK,而是等待一段时间再返回。在等待的时间里,接收方的应用程序,就能够把接收缓冲区的数据进行处理,这样剩余空间就会更大。

实际上延时应答采取的方式,就是在滑动窗口下,ACK不在每一条数据都返回了,比如在上图就是隔一条返回一个ACK。

注意实际上剩余空间的大小的变化是一个复杂的过程,既取决于发送方的发送,也取决于接收方的处理。

八、捎带应答

捎带应答也是提高效率的方式,其是在延时应答的基础上,引入的捎带应答。

来看下面这个例子,A与B进行一段信息的交互。

在图中我们注意到,“我看看”这个ACK是由B内核立即返回的,那么接下来的“11:42”是B业务上的响应,由B的应用程序发送。以上这两个信息本身是不同的时机,但是由于我们的TCP存在延时应答机制,在A等待ACK的过程中,B就要给A发送业务数据了,那么就可以让业务数据捎带上这个ACk一起发过去,如下图。

本来是不同的时机发送的两条信息,在延时应答下,就会可能成为相同的时机合并了。

九、面向字节流

面向字节流前面文章有具体写过,不再赘述,但是面向字节流有一个比较大的问题,就是粘包问题。

接收缓冲区会将收到的多个数据放到一起,而应用程序在read读取的时候能否读到一个完整的数据报就成了一个问题,因为其是面向字节流的,TCP并没有在socket API中告诉我们应该读几个字节,那么就需要我们程序员自己处理。

所以我们就可以在应用层事先约定好应用层协议,尤其是要明确应用层数据报和应用层数据报之间的边界。可以约定好分隔符,也可以约定每个包的长度。

  1. //1. 分隔符
  2. while(true){
  3. byte b = s.read();
  4. if(b == 分隔符){
  5. break;
  6. }
  7. }
  8. //2. 长度
  9. byte[] lenBuffer = new byte[4];
  10. s.read(lenBuffer);
  11. int len = parseInt(lenBuffer);
  12. byte[] dataBuffer = new byte[len];
  13. s.read(dataBuffer);

十、异常情况

异常情况就是在传输过程中出现了不可抗力。

  1. 进程崩溃

进程没了,对应的PCB就没了,对应的文件描述符表释放了,相当于socket.close(),此时内核会继续完成四次挥手,此时其实仍然是一个正常断开的流程。

  1. 主机关机(按照正常流程关机)

主机关机要先杀进程,然后才正式关机(杀死进程的过程中,和上面一样出发四次挥手)

  1. 主机掉电
  2. 网线断开

主机掉电和网线断开都是来不及正常四次挥手。

假设接收方掉电,发送方仍然在继续发数据,发完数据要等待ACK,但ACK没有等到,而且超时重传也无法收到ACK,重置TCP连接也失败,就会单方面放弃连接。

假如是发送方掉电,此时接收方无法明确是发送方挂了还是发送方会稍等再发,接收方就会先等待,所以接收方就会有如下机制:周期性给发送方发送一个小希,确认对方工作状态是否正常(该机制也叫心跳包,用于确认通信双方是否处在正常的工作状态中)。

四、网络层重点协议——IP协议

网络层主要做的是地址管理和路由选择.

那么网络层主要就是地址的管理, 需要明确两个点的地址以及中间每个节点的地址, 从而在两个点之间找到具体的路径, 进行网络传输. 有了地址之后, 就可以进行路由选择了(即路径的规划).

网络层的代表协议即IP协议

IPv4的报文结构

4位版本: 此处的取值只有4, 6(4即IPv4, 6即IPv6)

4位首部长度: 描述了IP报头多长(IP报头是变长的). 和TCP类似, 后面也有一个选项部分, 是变长的, 是可有可无的. 此处的单位也是4字节.

8位服务类型(TOS): 实际只有4位, 表示了IP协议的4种形态/四种工作模式. (这4位中只有1位可以是1, 其他的都是0)

4种形态分别为: 最小延时, 最大吞吐量, 最高可靠性, 最小成本. 那么实际开发中就可以根据需要, 灵活的切换四种模式, 以达到最优效果.

16位总长度(字节数): 描述了IP数据包的长度(头+载荷). 这个长度减去前面的IP报头长度, 剩下的就是载荷长度, 也即一个完整的TCP/UDP数据报长度.

注: 当一个IP数据报携带的载荷太长了, 超过了64KB, 就会在网络层针对数据进行拆分. 将一个数据拆分成多个IP数据报, 然后交给数据链路层, 由以太网封装成多个数据帧. 接收方的数据链路层会针对这些数据帧进行分用, 得到多个IP数据报, 交给网络层. 网络层再针对这些IP数据报进行解析, 把里面的载荷拼成一个,交给传输层...

16位标识: 同一个数据报拆分成的多个包的标识是一样的.

13位片偏移: 标识了多个包的先后顺序.

3位标志: 结束顺序. 查看当前包是否是整个拆包过程中的最后一个.

注: 16位标识, 13位片偏移, 3位标志 这几个部分都是提供辅助拆包/组包.

8位生存时间(TTL): 一个数据报在网络上传输的最大时间.

这个时间的单位是"次数"(不是"秒"). 一个数据报构造出来, 会有一个初始的TTL数值(比如32或者64, 或者128...), 这个报每次经过一个路由器转发, 就让TTL-1. 如果TTL为0了, 还没有达到目标, 此时就认为这个包永远也到达不了, 可以丢弃.

8位协议: 描述了当前载荷部分内容是属于哪个协议(TCP/UDP)的.

16位首部检验和: 针对首部进行校验(载荷部分(TCP/UDP数据报)自身已经有校验和了).

注: 如果校验和不一致, 会直接丢弃, IP不负责重传. 如果上层使用TCP, TCP会在没收到ACK之后就重传.

32位源IP地址, 32位目的IP地址: 这是IP协议中最重要的部分. 此处看到的IP地址是32位的整数, 它非常的长, 所以我们就会把它按照点分十进制的方法, 使用三个 . 把32位, 4个字节的数字给分隔开, 分成4个部分. 每个部分分别用0-255十进制整数表示. 所得结果就是我们所看到的, 那么原来的32位数字就是给计算机所看的.


这里还隐含了一个很重要的问题.

对于地址, 我们是期望每个设备都不相同的, 每个设备能有唯一的地址.

但是, 32位的数字是表示-2146473648~~2146473647, 只有约42亿个整数, 在这个背景下, 我们是期望来表示全世界所有的上网设备的. 那么很显然世界上的上网设备数量是远超这个数字的(万物可联网).

于是为了解决这个IP地址不够用的问题, 引入了以下两种机制.

  1. 动态分配IP地址

这个方法可以剩下一批IP地址, 但是治标不治本, 没有从根本上增加IP地址, 只是提高了利用率.

  1. NAT网络地址转换(本质是使用一个IP地址代表一批设备, 使用端口号区分同一个IP的不同设备, 也能够大大提高了IP地址的利用率)

在NAT背景下, 就把IP地址分为两类:

  1. 内网IP(私有IP)

10.* 172.16.* - 172.31.* 192.168.*

  1. 外网IP(公网IP)

除去以上三种IP以外的所有IP都是公网IP.

NAT要求公网IP必须唯一, 而私网IP可以在不同的局域网中重复出现. 如果某个私网里的设备想访问公网的设备, 就需要对应的NAT设备(路由器), 把IP地址进行映射, 从而完成网络访问.

总之,公网是设备无法直接访问私网的设备.不同局域网的私网的设备没法直接相互访问.


我们可以在搜狗,百度中 搜索"IP地址",可以看到网页的IP是什么.

如上的218.19.145.85 就是一个外网IP, 此时就相当于:

我的电脑要连上运营商路由器, 然后运营商路由器再访问对方的路由器.

这里的运营商路由器的IP即上面的218.19.145.85

我的电脑给对方服务器发送IP数据报, 经过NAT设备(运营商路由器)之后, 就会更改源IP. 站在服务器视角, 我的IP地址就是中间运营商路由器的IP地址.

此时, 只要这个电脑经过运营商路由器转发给服务器, 服务器看到的源IP都一样. 如果是多个电脑同时访问一个服务器, 服务器的响应就会先发给路由器, 路由器就会根据这些电脑不同的端口号来进行区分, 决定发给哪个设备.

因此, 服务器只能拿到路由器IP, 而不能拿到我的电脑的内网IP. 如果我的电脑不主动和服务器联系, 服务器也就不知道我的端口, 从而无法主动找到我......


NAT机制能够有效的解决IP不够用的问题, 但是带来的副作用就是使得网络环境变得更加复杂, 于是就有了IPv6, 它在根本上解决了IP不够用的问题. IPv6使用16字节(128位)表示IP地址.

16字节表示的地址个数, 其与之前4字节为单位的IPv4是指数的关系, 即4字节^4 => 非常非常大的数字. 可见IPv6的地址空间是非常大的.

地址管理

IP地址的格式: IP地址是一个32位的二进制数. 那么在这个IP地址里面我们把它分成两个部分:

  • 网络号: 标识网段(局域网), 保证相互连接的两个网段具有不同的标识;

一个常见的设定, 比如: 192.168.0.10 在这个IP地址里面它的网络号就是192.168.0, 主机号就是10. 那么就意味着当前的局域网下的设备, 网络号都是192.168.0.

  • 主机号: 标识主机, 同一网段内, 主机之间具有相同的网络号, 但必须有不同的主机号.

划分网络号和主机号的目的在于: 为了组网. 而一个IP地址由哪里到哪里是网络号, 哪里到哪里是主机号, 这其中的分界线则由子网掩码决定. 子网掩码的左侧部分都是1, 右侧部分都是0, 1的部分就描述了IP有多少位是网络号. 例如255.255.255.0(即1111 1111 1111 1111 1111 1111 0000 0000)就是一个常见的子网掩码, 当然子网掩码我们可以自己配置.

特殊的IP地址

  • 将IP地址中的主机地址全部设为0, 就成为了网络号, 代表这个局域网;
  • 将IP地址中的主机地址全部设为1, 就成为了广播地址, 用于给同一个链路中相互连接的所有主机发送数据报(比如192.168.0.255 这种地址, 如果使用UDP往这个地址上发送数据报,那么整个局域网的所以设备都能收到)
  • 127.0*的IP地址用于本机环回(loop back)测试, 通常是127.0.0.1(用于测试程序能否正常工作)
  • 主机号为1. 比如192.168.0.1, 通常是"网关IP"(不绝对, 都是可以配置的)

路由选择

因为工作中比较少见, 这里简单概括, 路由选择就是数据报寻路的过程, 边走边问路.

五、数据链路层重点协议

数据链路层起到的作用是考虑相邻两个节点(通过网线/光纤/无线直接相连的两个设备)之间的传输。

这里的典型的协议就是“以太网”,其实以太网这个协议规定了数据链路层,也规定了物理层的内容。

我们日常生活中使用的网线,就叫做“以太网线”(遵守以太协议的网线)

以太网帧格式

一个以太网数据帧由 帧头 + 载荷 + 帧尾 组成。

其中帧头由 目的地址, 源地址, 类型 构成; 载荷 即一份完整的IP数据报.

注意上述的 目的地址, 源地址不是使用IP地址表示了, 而是使用MAC地址(物理地址). 这个地址体系和IP地址完全独立, 是另一台地址体系.那么区别就在于IP地址描述的是整个传输过程的起点和终点, MAC则用来描述两个相邻的节点.

那么这个MAC地址用6个字节表示(比IPv4大很多),所以当前每个设备都会有唯一的MAC地址. 而且它不是动态分配的, 而是在 网卡 出厂的时候就已经被设置好.

类型: 以太网数据帧的类型有很多种不同的取值, 如果类型是0800, 载荷部分就是普通的以太网数据帧, 是一个完整的IP数据报; 如果是0806, 则载荷部分就是一个ARP报文; 如果是8035, 则载荷部分就是一个RARP报文.(ARP和RARP是数据链路层的另外一个协议: ARP协议, 通过这个协议, 让某个路由器/交换机, 能够认识局域网里的其他设备. 通过ARP协议会在路由器/交换机里建立一个表, 这个表相当于是一个哈希表, 能建立出IP和MAC之间的映射关系.)

MTU

MTU是数据链路层的数据帧, 表示能够承载数据的最大长度(载荷的长度).

注:载荷具体多长, 和使用的物理介质有很大关系, 当然也和数据链路层使用的协议有很大的关系. 比如以太网协议的MTU是1500(字节)(这个和物理层有很大关系)

正是这个MTU引起了IP协议来进行组包分包, IP的分包组包通常不是根据这个IP长度64k来分的...大多数情况下, 数据链路层的MTU都比64KB要小.

六、应用层重点协议——DNS协议

DNS是应用层的一个协议, 它也是当前互联网的基石. DNS也叫做域名解析系统, 是保证网络安全的一个很重要的东西.

域名其实就是网址, 比如说www.baidu.com. 其实网络上的服务器,要访问它需要的是IP地址, 但是IP地址比较拗口不好记, 于是就使用一些简单的单词构成字符串.也就是说, 每个域名都对应着一个/N个IP地址.

既然如此, 就需要能够把域名和IP对应上.

最原始的做法, 就是使用一个hosts文件(类似于哈希表)建立IP和域名的映射关系. 这个文件在电脑中的这个位置:

这个文件就是最早的DNS系统, 将它打开, 在里面会看到记录一些相应的结果.

注: #表示注释.

在早期的时候, 互联网上的服务器较少, 每个用户手动维护自己的hosts文件是足够的. 但是随着互联网的发展,现在的网络环境已经不适合再采用手工维护的方式, 于是就诞生了DNS服务器.

当我们访问某个域名的时候, 就自动请求一下DNS服务器, DNS服务器就会帮我们进行查询, 把得到的结果返回给我们(结果就是具体的IP地址)

如果我们的DNS服务器的配置错误, 或者DNS服务器挂了, 此时就会出现一个典型现象: QQ能用, 但是网页打不开.


以Windows10为例, 在网络设置的更改适配器选项中的以太网, 双击可查看属性, 再在属性中双击Internet协议版本4(TCP/IPv4), 就可以看到配置IP地址和DNS服务器地址的情况.

那么此处的DNS就是我们手动配置的.

这里面还要注意一个: 当前要求网站的域名不能重复.(保证唯一)针对域名进行分级, 分为 一级域名, 二级域名, 三级域名......

例如pic.sougou.com, 其中一级域名就是.com , .com 就是"公司"的缩写. 类似的一级域名还有org, cn, us... ; sougou就是二级域名; pic就是三级域名.

那么域名分级了, DNS服务器也有这样类似的域名分级. DNS服务器也是分成一级域名服务器, 二级域名服务器.....(一般常见的只分级到四级五级左右~)


比如说现在想要注册一个网站, 就需要申请一个域名, 把域名和服务器的IP地址关联起来, 那么就要把数据写到DNS服务器里.

全世界有千千万万的DNS服务器, 但是这里的这些服务器的数据都是来自于根域名服务器(除了根域名服务器之外, 其他的都是镜像服务器, 数据要从根域名这里同步的)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/158752
推荐阅读
相关标签
  

闽ICP备14008679号