目录
- [1. 跨网段通信](#1. 跨网段通信)
-
- [1.1 IP 地址](#1.1 IP 地址)
- [1.2 通信流程](#1.2 通信流程)
- [1.3 IP vs Mac](#1.3 IP vs Mac)
- [1.4. 数据包封装和分用](#1.4. 数据包封装和分用)
- [2. 端口号](#2. 端口号)
- [3. TPC && UDP](#3. TPC && UDP)
- [4. 网络字节序](#4. 网络字节序)
网络基础(一) 上一篇文章中,我们介绍了网络协议的基本概念和分层结构。协议是确保不同主机间顺利通信的约定,解决数据准确性和传输问题,并介绍了协议分层的重要性,以OSI 和TCP/IP 模型为例,介绍了整个通信流程。 最后,通过以太网通信介绍了局域网的通信原理,每台主机都有唯一的 MAC 地址,使用特定协议进行数据传输。二为了减少数据碰撞,采用交换机可以优化网络性能,确保多台主机的有效通信。
那么不同局域网该如何建立通信呢?
1. 跨网段通信
跨网段的主机进行通信时(即不在同一个局域网内), 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器,路由器能够两个不同的子网。
-
令牌环网:也是一种局域网技术,它同样保证在同一个局域网内,任何一个时刻只能有一台主机发送消息,但与以太网不同的是,它不检测数据碰撞,而是通过对主机的标识来决定某台主机是否能够发送信息(凡是持有令牌的主机,通过令牌机制来控制访问),所谓持有令牌标识就类似于锁资源,只有持有锁资源,才能够访问临界资源,但是这种思想,在网络中称为令牌。
令牌环网与以太网驱动程序一样,同属于数据链路层,只不过底层是另一种通信协议。但是替换底层协议,并不影响上层的各种协议,这就是协议分层的意义。
1.1 IP 地址
《西游记》唐三藏前往西天取经的故事大家应该都知道,这个事上,唐三藏的起点为东土大唐,重点为西天。但是他并不是一步到位的,而是中途经过很多的历险地。每次到达一个地方之前,唐三藏都会问他的几只宠物:"monkey,下一个地方是什么地方?"
并且每到达地方后,他也都会跟东道主介绍起自己:"我是唐三藏,自东土大唐来,前往西天取经"
在这个故事中,唐三藏每次都会跟东道主介绍起自己从哪来,到哪去(这个地址是一直不变的,即 IP 地址),也都会问自己的宠物们下一站到哪了(随着它们所在地的不同,这个地址是随时在变换的,即 Mac 地址)。比如,当前取经团队位于高老庄,之后离开时会根据最终目的地的方向(即西天),前往白骨山,以此类推,车迟国、火焰山 ...... 西天。
IP地址:一个4字节(32比特位) 的整数,形如 192.168.1.1 的风格。
1.2 通信流程
-
在跨网段通信时,每一层都有协议,也都需要添加报头,从用户发送的信息 "你好" 到达应用层,拼接上报头后,再把数据报传递给下层,直到在网络层时,添加报头中一定会有一个字段<srcip, dstip>,记录着该数据的起始 IP 以及目标 IP(即从哪来,到哪去),接着继续把数据报传递给数据链路层,链路层的添加的报头中就会记录源主机和目标主机的 Mac 地址,即 <mac_src, mac_r>,只不过这里 mac_route,不是最终目的地主机,而是路由器的 Mac 地址(假设路由器的 IP 地址为 IP_R,Mac 地址为 Mac_R)。
-
目标地址为路由器,即说明数据链路层的数据报最终是传递给路由器的,为什么需要传递给路由器?
在跨网段通信时,必须把数据交给路由器,由路由器将数据转发给另一个网段的主机(可能需要进行多次转发)。这个道理就好比,唐三藏要去西天,总不能说了那么久,还没走出东土大唐吧?即,跨网段通信的第一步,即将数据带离自己所在的局域网,那么这个工作就需要依靠路由器来完成。
而把数据交给路由器,本质也是一种局域网通信,因为路由器也是局域网内的设备之一,因此发送端能够直接与路由器进行通信。
-
因此,发送端在数据链路层将报头拼接完成后,就将数据报通过以太网交给路由器所在的链路层(这个过程,发送端所在的局域网内的所有主机都会收到,解析报文后对比目标主机,识别到目标主机不是自己后,丢弃报文),路由器底层同样对报文做解析,识别到目标主机就是自己后,将有效载荷传递给上层(网络层)的路由器。
此时路由器接收到的报文,与发送端在 IP 协议时的报文是一致的(<srcip, dstip>,都记录着数据的起始 IP 以及目标IP)!随后路由器识别到目标 IP 所在的局域网也与自己是相连通的,因此将报文转发给接收端。
需要注意的是,路由器位于网络层,它也不是直接将该数据报传递给接收端的网络层,而是将数据报重新向下层传递给自己的令牌环驱动程序,重新添加令牌环的报头(依旧是源主机和目标主机的 Mac,但这里的源是路由器,即 <mac_r, mac_dst>),接着由路由器的令牌环驱动程序将数据交付给接收端的令牌环驱动程序。
接着,接收端对报文做解析,识别到是自己的数据,就将有效载荷向上层层传递到用户层。
-
整个过程中,上层(应用层、传输层、网络层)的报文,发送端和接收端是一致的,下层(链路层)的报头是不一样的,在发送端将数据交给路由器后,路由器会把发送端的报头去掉,自底向上要进行解包分用,经过路由器的查找,再重新封装目标 IP 所属局域网的报头。在不同的网段中加入路由器,就屏蔽底层以太网、令牌环网等局域网协议的差异,而路由器横跨不同的网段,因此它必定具备两个局域网驱动。
因此,尽管底层协议存在很大的差异,但从 IP 层及其一层的所有协议层都可以保持一致,因为 IP 协议屏蔽了底层网络的差异化,而靠的就是工作在 IP 层的路由器完成的。所以说,IP 实现了全球主机的软件虚拟层,可以理解为一切皆是 IP 报文,因为有了路由器,就可以不再需要关心底层网络的各种差异了(与 linux 一切皆文件相似,有了 struct file 对象,底层各种外设都可以看作一个个文件),而 IP 报文也不会改变,从发送到接收,都是同一个 IP 报文。
这也是为什么,有的计算机底层是以太网驱动、有的是令牌环网,甚至手机的无线 LAN 通信,就是这样各种差异的底层通信技术,但依旧能够互相通信。
1.3 IP vs Mac
IP 地址一般都是不会改变的,用于协助通信时的路径选择,而 Mac 地址离开局域网后,源 Mac 和目标 Mac 都要被丢弃,再由路由器重新封装新的源地址和目标地址。
1.4. 数据包封装和分用
- 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame)。
- 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),即报头,这个过程称为封装(Encapsulation)。
- 首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息。
- 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理。
2. 端口号
-
在进行网络通信时,真正在通信的是双方主机的应用层。
对于网络协议中的下三层(不考虑物理层),主要解决的是数据安全可靠传输(到远端主机) 的问题。用户通过应用层软件完成数据发送与接收。而用户使用应用层软件的前提是启动软件,软件(用户级代码) 启动后,在操作系统中就变为进程,因此我们日常网络通信的本质就是进程间通信,具体地,网络通信时,本质是A进程通过网络协议栈的层层传递,向B进程发起数据请求。
网络通信中,主机之间通过网络协议栈进行通信是手段,最终上层的两个进程互相交换数据是目的。
因此,之前介绍的诸如管道通信、共享内存通信都是单主机的进程间通信,而网络通信是跨主机的进程间通信。单主机通信时,由于进程间具有独立性,因此通信前不同主机需要能够共同访问同一份资源,那么在跨主机的进程间通信也是如此,对于网络通信而言,不同进程间的共享资源就是网络(一个从网络中读取数据,一个写入)。并且,对于单主机的进程间通信时,我们需要有创建管道、共享内存、挂接等系统调用来支撑进程通信,因此网络通信同样也需要提供系统调用来使用网络资源,即网络协议栈(即通过网络协议栈来访问网络资源)。
-
每层协议都要解决报头与有效载荷的分离问题,而当传输层接收到报文后,也要有能力将报文传递给应用层特定的某一个应用,所以传输层与应用层需要约定一种方案,即通信的两台主机的应用,互相绑定对方的端口号(在传输层的报头中绑定)。
例如,服务端的 A 应用选择的端口号是 9000,那么客户端的 A 应用在向服务端发送数据请求时,经由传输层封装报文(记录目标主机应用绑定的端口号),层层传递到服务端的传输层,再经过报文解析,识别报头的端口号为 9000,于是将有效载荷传递给服务端的 A 应用。服务端向客户端发送数据时也如此,只需要绑定客户端特定应用的端口号(即目标端口号)即可。
-
端口号(port) 是传输层协议的内容。
- 端口号是一个 2 字节1 6 位的整数。
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
在公网上,IP 地址能标识唯一一台主机,端口号 port,能够标识该主机上特定一个网络应用层的进程,因此 IP : Port 就能够标识全网唯一一个进程。
即,对于客户端和服务端而言,都有自己的 IP 和 Port,它们各自都能够标识全网唯一一个进程,因此客户端与服务端组合起来,就能够标识全网网络应用层上唯二的两个进程,能够互相标识对方的唯一性,而这种基于IP + Port 的通信方式,称为 Socket。
-
进程 PID 已经能够标识一台主机上进程的唯一性了,为什么在网络通信上,还要设计一个端口号来标识?
-
不是所有的进程都要网络通信,但所有进程都要有 PID。
-
系统和网络功能解耦(重点)。
PID 是操作系统中进程管理模块的东西,如果网络通信中同样是使用 PID 来标识进程的唯一性,那么将来系统的进程模块发生变更,PID 的分配原则、数据类型等变更了,网络模块也就受到非常大的影响,因此操作系统的进程管理与网络通信就强强耦合在一起了。
系统和网络功能的解耦合,也好比我们现实生活中,每个公民都有身份证标识个人的唯一性,但是在不同的场合,依旧还有其它标识唯一性的方式,比如学校的学号、公司的工号等等。
-
-
我们上面刚介绍了传输层收到报文后,是根据报头中绑定的端口号来决定向上交付给应用层哪个进程的,那么传输层是如何把报文转发给上层进程的呢?所谓的绑定端口号是如何绑定的?
所以我们可以理解为,在传输层内部有一张哈希表,哈希表绑定的 value 为进程的 PCB,key 值绑定的就是操作系统中设定好的端口号。因此,当服务端的传输层接收到了客户端传递的报文后,经过解析提取报头,识别目标端口号,将该端口号通过哈希映射找到上层唯一的一个进程,这样就完成了传输层与应用层的交互。
-
客户端如何得知服务器的端口号是多少?(因为如果无法获取服务器的端口,那么客户端就无法绑定端口,报文传输到服务器的传输层后,也就无法交付给上层特定服务端)
每一个服务的端口号必须是众所周知,精心设计,被客户端知晓的,甚至是固定好的,比如 http 端口号就是固定好的 80,mysql 默认就是 8080,https 为 443。而对于用户所不关注的其它应用的端口号(比如抖音、微信),这些应用的客户端和服务端,一定是同一家企业开发的,所以服务器的端口号从一开始就内置在客户端内部了,因此这也是为什么,作为用户,服务器的请求是我们发起的,但是我们从来没有关心过诸如端口号、IP 地址的概念。
-
一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。端口号的绑定是通过哈希所完成的,我们可以理解为每个 key 值都可以映射同一个 value,但是同一个 key 无法映射多个 value,也即一个端口号无法映射多个进程。
3. TPC && UDP
-
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接(使用 TCP 通信时,它需要客户端向服务器发起链接建立请求,链接建立成功之后才能正式发送数据进行通信 。例如我们打电话时,我们需要对方接听了之后,才能够正式进行通话,如果对方不接听,那么你说的话,对方是听不到的,这就是可靠传输的典例)
- 可靠传输(有连接是可靠传输的特点之一。可靠性:如果底层识别到有数据丢失了,传输层它会自动补发数据,上层也即不需要关心数据丢失的问题)
- 面向字节流
-
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接(通信时不建立链接)
- 不可靠传输(面向数据报的特点之一,应用层把数据交给传输层的 UDP 协议,这份数据是否丢失?有没有被接收端收到,发送端的传输层、应用层都不关心,这就是不可靠传输的特点)
- 面向数据报(发送方发一个报文,接收方就自动接收一个报文,例如:邮箱发送不需要经过对方的同意与否,只要发送方将邮件发送出去了,对方要么不打开邮件不接收,接收的话就是一个完整的邮件,这就是面向数据报)
-
TCP 与 UDP 都能给上层提供数据收发的服务,一个是可靠传输、一个是不可靠,但是既然有了 TCP 可靠传输了,为啥还要有 UDP 这个不可靠传输呢?
在技术层面上,所谓可靠和不可靠是一个中性词,只是每个协议的一个特点。我们需要注意的是,即便有了 TCP 可靠传输协议,但是保证其数据传输的可靠性是需要成本的(数据丢失时,TCP要具备得知数据丢失的能力,识别到数据丢失后再进行补发,即数据发送出去那一刻,直到数据被接收这段时间,TCP 都需要一直把这份数据维护起来,当数据丢失后,才能够进行补发 ......),而反观 UDP 则不需要有这些成本,数据发送出去后即不关心数据生死,因此 UDP 在设计和维护上的效率就自然更高。
因此,可靠与不可靠,是给用户提供的一种可选择的方案,用户可自行根据场景需求选择对应的协议特征进行数据通信,比如我们涉及到支付转账等任务时,就必须采用具有可靠传输的TCP,而诸如视频直播这类信息流派发的场景,则更多的使用 UDP 协议。
4. 网络字节序
-
一个保存在内存中的整型变量,需要占 4 个字节(比如0x11223344),而每一个字节的地址都有高地址、低地址的概念,因此对于这 4 个字节的数据如何在内存中存放,就引出了大小端的概念。
即,大小端(Endianness)是计算机存储数据时的字节序问题。在大端模式下,数据的高位字节存储在低地址处,低位字节存储在高地址处;小端模式下,数据的低位字节存储在低地址处,高位字节存储在高地址处。
计算机的诞生是更早的,意味着,数据如何在内存中存储的问题也早就存在,因此在网络出现之前,不管在硬件、软件上就已经有了各自不同的存储方案。而网络诞生之后,由于大小端各自并没有明显的优势让对方达成统一标准,因此大端还是小端的存储方式继续保留了下去。
所以当两台不同的主机在网络通信时,数据的存储方案就是不一样的,双方主机也就无法得知对方主机是大端存储、还是小端存储。那么当对方采用大端,可另一方却采用小端的模式进行数据解析,那么该数据解析出来大概率就是错误的(比如0x11223344,可能就变成了0x44332211)。 即便是在数据通信的报文中加入大小端的字段来区分该数据的存储模式,接收方也照样无法正确解析(因为描述自己是大端机的字段数据,本身就是大端存储的,那么小端存储是解析不出来原本所传递的信息的)。
大小端始终无法统一,最终在网络通信时,就引出了网络字节序的概念。即,网络通信规定,网络中以大端的方式存储数据。如果是大端机,可以直接发送与接收;如果是小端机,则需要做相应的存储转换,再进行发送,接收时不需要关心数据是大小端,因为它一定是大端,只需要以大端的存储模式进行解析数据即可。这样一来,在网络通信中,再无大小端争议的问题,因为网络通信不关心通信的双方主机是大端还是小端,统一以网络字节序为准。
-
总结: 因此,对于内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,而网络数据流的地址是这样定义的:
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略, 直接发送即可。
-
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
NAME htonl, htons, ntohl, ntohs - convert values between host and network byte order SYNOPSIS #include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); 其中的h表示host,n表示network,l表示32位长整数,s表示16位短整数。 例如 htonl 表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!