目录
前言:
一、网络传输基本流程
1.1、认识MAC地址
1.2、认识IP地址
二、socket编程预备
2.1、端口号
2.2、传输层的代表
2.3、网络字节序
2.4、sockaddr 结构
总结:
前言:
大家好,上一篇文章,我们说到了协议。
我们说,协议,就是一种约定!!协议是分层的,本质原因是因为问题是分层的。
在代码层面上,协议就是通信双方都认识的数据结构。
那么我们今天继续来学习网络基础的概念。
一、网络传输基本流程
两台独立的主机用一根网线连接起来,他们之间是否能相互通信?
是的。
几个独立的主机通过网线与交换机连接起来,就形成了局域网。
每台主机在局域网上,要有唯一的标识来保证主机的唯一性:
mac
地址。
1.1、认识MAC地址
MAC 地址是 网络设备的物理硬件地址,用于在 局域网(LAN) 内唯一标识一台设备(如电脑、手机、路由器等)。它工作在 OSI 模型的第 2 层(数据链路层),主要用于 以太网(Ethernet)、Wi-Fi(802.11)等本地网络通信。
MAC地址是用来识别数据链路层中相连的节点,一般来说长度为48位,及6个字节。
一般用16进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)
MAC地址在网卡出厂时就确定了(每个物理网卡在制造时会被分配一个唯一的 MAC 地址),不能进行修改,MAC地址通常是唯一的。
后面我们详细谈论数据链路层的时候,会谈
mac
帧协议,此处我们做一个了解即可。
以以太网为例,在以太网中的任意时刻,都只允许一台机器向网络中发送数据。如果有多台机器同时发送,就会发生数据干扰。我们称之为:数据碰撞。
而所有发送数据的主机都需要进行碰撞检测与碰撞避免。
所谓碰撞避免,就是发现此时有主机发送数据了,你就进行休眠,等一会再发送数据。
在没有交换机的情况下,一个以太网就是一个碰撞域。
在我们的局域网通信的过程中,主机对收到的报文确认是否是发送给自己的,是由目标的MAC地址来判定的。
目标MAC地址一样,就说明这个报文是发给自己的。
以太网就是一个临界资源
主机abcd发送数据时执行的代码是临界区
我们采用碰撞避免碰撞检测来保证临界资源使用时数据的原子性
这个碰撞也是为什么人一多,我们的网就卡起来了,因为碰撞的次数增多了,休眠的时间就多了。
再来看同一个网段内的两台主机进行发送消息的过程:
而其中每层都有协议,所以当我进行进行上述传输流程的时候,要进行封装和解包。
当应用程序发送网络数据时,数据必须经过操作系统的网络协议栈处理,最终由网卡(硬件)发送出去。这个过程是 “从上到下” 的,即:
应用层(用户态) →
传输层(TCP/UDP) →
网络层(IP) →
数据链路层(MAC/ARP) →
物理层(网卡驱动) →
网卡硬件(NIC)
为什么是这样从上到下的贯穿?
这是因为网卡是硬件,管理工作由操作系统来进行,今天要访问的网络功能网络层传输层是属于内核的。
所以你发数据一定会使用系统调用,使用系统调用就一定会贯穿操作系统的,所以一定会走这条路
让我们来解释一下,为什么同层之间,都认为自己在和对方同层协议在直接通信。
在我们用户发送一个“你好”的信息后,用户层会给这个信息添加一个用户层的报头。
报头(Header)本质上是一种数据结构。它在计算机网络中用于存储协议控制信息,通常以固定或可变格式的二进制数据块的形式存在,由协议规范明确定义其字段和排列方式。
以快递为例,报头类似于快递单。
这个应用层汇报自己的报头加有效载荷(这里是“你好”),传递给下一次传输层,由传输层再添加一个传输层报头。随后让传输层报头把自己的报头加有效载荷(这里的有效载荷指的是应用层报头+“你好”),传递给下一层网络层。
一段报文,等于自己的报头+有效载荷。
不同的协议层对数据包有不同的称谓
,
在传输层叫做段
(segment),
在网络层叫做数据报 (datagram),
在链路层叫做帧
(frame)。
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),这个层层加封的过程,我们就称为:封装。
首部信息中包含了一些类似于首部有多长
,
载荷
(payload)
有多长
,
上层协议是什么等信息。
数据封装成帧后发到传输介质上
,
到达目的主机后每层协议再剥掉相应的首部,
根据首部中的 "
上层协议字段
"
将数据交给对应的上层协议处理。
这个把自己的报头剥离,随后把有效载荷传递给上层的过程,我们叫做解包。
在网络传输的过程中,数据不是直接发送给对方主机的,而是先要自定向下将数据交付给下层协议,最后由底层发送,然后由对方主机的底层来进行接受,在自底向上进行向上交付,这就导致了一种情况。
那么用户b怎么知道网卡此时已经有数据了,要一步一步往上传:
硬件中断!!!
在通信的双方的同一层看来,报文都具有一样的报头,一样的地址。所以就像同层之间都在和对方协议在直接通信一样。
在Linux系统中,我们可以通过ifconfig命令来查看自己机器的MAC地址:
ether后面就是mac地址。
在Windows上可以通过命令:ipconfig /all来看到自己的物理地址(即mac地址)。
1.2、认识IP地址
IP
协议有两个版本
, IPv4
和
IPv6。本文及后面的文章,如果没有进行特殊说明,谈到IP,都是指的是IPv4.
IP地址是在IP协议中,用来标识网络中不同主机的地址。
对于IPv4来说,IP地址是一个4字节,32位的整数。
我们通常也用“点分十进制”的字符串来表示IP地址,这样做的好处是可读性高,例如:192.168.0.1;用点分隔的每一个数字表示一个字节,范围是0-255。
跨网段的主机的数据传输
.
数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。
我们如何理解IP地址呢?
我们从一台主机跨网段向另外一台主机发送信息,就跟你人生的发展阶段一样。
我们的出生的婴儿就是源IP地址,我们的死亡的时候的老人就是目标IP地址。
在我们从婴儿到死老人的过程中,不是一蹴而就的,而是划分了许多个阶段。我们从婴儿会长到小孩,又会从小孩长到青少年,最后是青年,壮年,中年,中老年,老年。
IP地址就像人生的起点与终点——从出生的婴儿(源IP)到离世的老人(目标IP),代表通信双方
永恒不变的身份标识。
而
MAC地址则是人生每个阶段的
临时身份,随着成长不断变化。
就像从婴儿到老人需要经历童年、青年、中年等阶段,数据包跨越不同网络时,它的
源MAC和目标MAC会在每一跳动态更新(如从家庭路由器到运营商网关,再到目标服务器),但
IP地址始终如一。
这正体现了:
IP负责全局寻址(人生方向),MAC负责局部传输(阶段性的脚步)。
也许这里你还不能对IP地址有一个深刻的理解。没关系,我们先继续往下面看。
这张图的通信双方并不处于同一个局域网中,甚至于二者的局域网类型都不一样。
他们之间如何进行通信呢?
这就要通过我们的路由器了,所谓路由,就是进行路径的选择。
左侧的FTP客户,通过我们说的封装过程,将IP层的IP报文封装成以太网帧,添加源MAC(客户端网卡)和目标MAC(默认网关,如路由器)。
这样就将信息传给了路由器,随后路由器的以太网接口收到帧,校验MAC地址(目标MAC是否匹配自身接口)。剥离以太网头,解析IP包,查询路由表决定下一跳去向哪里。
由于另外一侧是令牌网,路由器就重新封装IP包为令牌环帧(格式与以太网不同,如使用令牌控制访问)。随后更新源MAC(路由器令牌环接口MAC)和目标MAC(下一跳设备MAC)。
若目标服务器在令牌环网中,服务器网卡就接收令牌环帧,校验MAC是否正确后剥离帧头,将IP包上传至内核协议栈。IP层检查目标IP是否为本机,TCP层按端口号交付给FTP服务进程。
( 路由器通过多个接口连接不同子网,每个接口的IP充当该子网的网关。网关是路由器接口的IP地址,主机需配置网关才能访问外部子网。)
抽象点的图看看:
这个过程更详细的来说,就是封装到了网络层,此时进行IP的封装,确定好目标的IP地址与源IP地址,发现此时的IP地址前面的跟我们的局域网的IP地址不一样(一般来说同一个局域网中的IP地址,点分十进制的话,前面三个数都是一样的,所以我们就能判断是否目标地址与我在同一个局域网,如果不是,就将目标MAC设为默认网关(路由器)的地址),随后传递给链路层,进行mac地址的封装。
封装好之后,就要开始传递信息了。
在以太网中(以太网是一个局域网)有另外的其他许多主机,但是他们的mac地址都匹配不上,最后我们就匹配上了路由器的对应以太网接口的mac地址。
此时路由器的网卡收到了消息,开始网上解包,到了网络层解包出了源IP地址与目标IP地址,查询路由表:若目标IP在直连网络中,就重新封装帧(更新源/目标MAC)并从对应接口转发;否则交给下一跳路由器。
找到目标局域网后,这个路由器此时就继续往下封装成mac帧,由负责该局域网的网卡发出信息,在这个局域网下的主机都会收到消息。
随后通过目标mac地址是否匹配来判断是否接收。
最后目标主机就匹配成功了,随后逐渐网上解包,把消息给了用户。
IP 网络层存在的意义:提供网络虚拟层,让世界的所有网络都是 IP 网络,屏蔽最底层网络的差异
最初通信依赖MAC地址,但MAC只能在局域网生效,且无法分层管理。IP层的引入屏蔽了底层网络差异(如以太网、Wi-Fi、光纤),通过逻辑化的IP地址实现全球路由,让不同技术的网络能无缝互联。
二、socket编程预备
IP在网络中,用来标识一个主机的唯一性。
但是这里我们要思考一个问题:数据传输到主机是目的吗?
不是的!!因为数据是给人使用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览。
但是人怎么看到聊天记录的呢?怎么执行下载?浏览任务的呢?
是通过启动的qq微信,网盘,浏览器。
而启动的这些都是进程。换句话说,进程是人在系统中的代表,只要把数据传给进程,就相当于人拿到了数据。
所以,数据传输到主机不是目的,而是手段。到达主机内部后,交给主机里的进程,才是目的。
但是系统中,同时会存在许多个进程,当数据到达主机之后,怎么转发给目标进程?
如何把数据交付给进程?这就依赖于传输层的两大协议:TCP与UDP,以及它们的“地址簿”——端口号(Port)。
2.1、端口号
端口号
(port)
是传输层协议的内容,通常是一个2字节十六位的整数。
端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
所以我们的IP地址+端口好能够表示网络的某一台主机的某一个进程。
而一个进程可以占用多个端口号,可是一个端口号只能被一个进程占用。
0 – 1023:
知名端口号
, HTTP, FTP, SSH
等这些广为使用的应用层协议
,
他们的端口号都是固定的.
而1024 – 65535:
操作系统动态分配的端口号
.
客户端程序的端口号
,
就是由操作系统从这个范围分配的。
我们之前在学习系统编程的时候, 学习了
pid
表示唯一一个进程
;
此处我们的端口号也是唯一表示一个进程.
那么这两者之间是怎样的关系
?
我们可以用“10086客服热线”来进行类比
-
PID:相当于客服工号(如“工号1024”),用于内部管理,客户无需知道。
-
端口号:相当于客服热线号码(如“10086”),客户通过拨打号码找到服务。
-
进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
IP
地址用来标识互联网中唯一的一台主机,
port
用来标识该主机上唯一的一个网络进程
IP+Port
就能表示互联网中唯一的一个进程
所以,通信的时候,本质是两个互联网进程代表人来进行通信,
{srcIp
,srcPort,
dstIp
,
dstPort}
这样的
4
元组就能标识互联网中唯二的两个进程
所以,网络通信的本质,也是进程间通信
这两个进程具有十足的独立性,运行在不同的主机上,但是他们之间也存在共享的资源:网络!!两个进程间通过共享的“网络”这一公共资源交换数据。
另外,一个主机不断从外部获取数据到内核缓冲区,进程不断的从内核缓冲区读取数据。这不就是消费者生产者模型吗?
我们把
ip+port
叫做套接字
socket。
2.2、传输层的代表
如果我们了解了系统,也了解了网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。
传输层主要有两种核心协议:TCP(传输控制协议) 和 UDP(用户数据报协议)。它们为应用层提供不同的数据传输服务,是互联网通信的基础。
我们现在先来简单的了解一下这两种协议。
TCP(Transmission Control Protocol)是一种面向连接的、可靠的传输层协议,它为应用程序提供有序、无差错的数据流传输服务。
主要特点就是以下四个:
传输层协议
有连接
可靠传输
面向字节流
我们只需要知道就行了,后面会详细解释。
UDP(User Datagram Protocol)是一种无连接的、不可靠的传输层协议,专注于低延迟和简单性。
传输层协议
无连接
不可靠传输
面向数据报
这里的面向字节流与面向数据报我们简单的说一下,面向字节流就是我们读写文件那样的,数据像“水流”一样无边界连续传输,发送方写入的字节数和接收方读取的字节数无需一一对应。就是我们写入的时候是十个字节的写入的,读取的时候不要求以十字节的方式读出来。
而面向数据报是什么?就是你一次发送信息,就得对应一次接收一次。数据以独立报文为单位传输,且
报文长度固定,不会拆分或合并。
2.3、网络字节序
我们已经知道
,
内存中的多字节数据相对于内存地址有大端和小端之分
,
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,
网络数据流同样有大端小端之分.
那么如何定义网络数据流的地址呢
?
发送主机通常将发送缓冲区中的数据按照内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存到接收缓冲区中,也是按照内存地址从低到高的顺序保存。
因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
所以TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
不管这台主机是大端机还是小端机,都会按照这个协议规定的网络字节序来发送和接收数据。如果当前发送主机是小段,就需要先把数据转化成大端。
为使网络程序具有可移植性
,
使同样的
C
代码在大端和小端计算机上编译后都能正常运行,
可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记
,h
表示
host,n
表示
network,l
表示
32
位长整数
,s
表示
16
位短整数。
例如
htonl
表示将
32
位的长整数从主机字节序转换为网络字节序
,
例如将
IP
地址转换后准备发送。
如果主机是小端字节序
,
这些函数将参数做相应的大小端转换然后返回
;
如果主机是大端字节序,
这些函数不做转换
,
将参数原封不动地返回。
2.4、sockaddr 结构
socket API
是一层抽象的网络编程接口
,
适用于各种底层网络协议
,
如
IPv4
、
IPv6,
以及后面要讲的 UNIX Domain Socket.
然而
,
各种网络协议的地址格式并不相同。
IPv4
和
IPv6
的地址格式定义在
netinet/in.h
中
,IPv4
地址用
sockaddr_in
结构体表示,
包括
16
位地址类型
, 16
位端口号和
32
位
IP
地址
.
IPv4
、
IPv6
地址类型分别定义为常数
AF_INET
、
AF_INET6.
这样
,
只要取得某种 sockaddr
结构体的首地址
,
不需要知道具体是哪种类型的
sockaddr
结构体
,
就可以根据地址类型字段确定结构体中的内容
这样有什么好处呢?
大家想一下,我们在使用
socket的常见接口时,只需要把对应的结构体指针类型强制转化为const struct sockaddr *,这样我们不就能统一接口了吗?
所以就降低了耦合性,提升代码的通用性和可扩展性,无需为每种协议重复实现。
socket
常见
API接口:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
这也就是为什么这些接口里的相关参数结构体指针类型都是const struct sockaddr *的原因。
转化后,根据我们之前定义的常数 AF_INET、AF_INET6(因为这两个结构体前16位被定义了常数,我们就可以通过if等判断出来原来是什么结构体)
这其实也是C语言实现多态的一种用法。
总结:
我们的网络基础概念就先讲到这里。
后面我们会继续深化学习,更加深刻的理解网络。
评论前必须登录!
注册