【Linux网络编程】计算机网络基础:局域网与广域网、OSI七层模型与TCP/IP五层模型、封装解包与MAC/IP地址、端口号与socket编程接口

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言
我们了解完系统编程,在计算机的世界中更离不开网络,而网络是如何实现的呢?下面一个系列我们就以Linux网络编程为例来了解计算机网络的世界。
刚学完系统编程面对现实中的网络可能会有这些疑问:数据是怎么定位到对方进程的?同样一串二进制,对方的机器怎么知道如何解析?
| 现象 | 你可能以为的原因 | 真正发生的事情 |
|---|---|---|
| 数据能发到对方 | 一根网线把数据直接送过去了 | 数据先自上而下逐层封装,到对端再自底向上解包分用 |
| 双方能看懂彼此的数据 | 数据本身就是看得懂的 | 每一层都按事先约定好的协议结构体加头、去头 |
| 知道该发给哪台机器 | 靠 MAC 地址一路走完全程 | IP 地址定长远目标,MAC 地址只管下一站 |
| 数据能落到正确的进程 | 找到主机就够了 | IP + 端口号才定位到主机上的那一个进程 |
问题来了: 协议为什么要分层?报文在协议栈里到底经历了什么?跨网段的时候地址是怎么换的?socket 又凭什么能代表一次网络通信?
答案就是------分层 + 封装解包!网络协议栈把「把数据送到对端进程」这件事拆成一层层各管一摊,层与层之间靠约定好的报头对接。所谓网络通信,说到底还是进程间通信,只是主机之间的距离变远了。
通过本文,你将掌握:
| 技能 | 应用场景 |
|---|---|
| 局域网 LAN 与广域网 WAN | 看懂网络是怎么从独立模式一步步连起来的 |
| 协议分层、OSI 七层与 TCP/IP 五层模型 | 理解分层是为了模块化解耦合,知道工程落地为什么是五层 |
| 封装、解包与分用 | 解释「报文 = 报头 + 有效载荷」,读懂每层报头的作用 |
| MAC 地址与 IP 地址 | 区分局部的下一站和整体的长远目标 |
| 端口号与 Socket | 用 IP + Port 定位互联网上唯一的一个进程 |
| TCP / UDP 与网络字节序 | 选对传输层协议,避开大小端的坑 |
| socket 接口与 sockaddr | 看懂 socket / bind / listen / accept / connect 的调用套路 |
前置知识: 进程间通信、操作系统分层、C 语言结构体与指针基础。
文章目录
- 【Linux网络编程】计算机网络基础:局域网与广域网、OSI七层模型与TCP/IP五层模型、封装解包与MAC/IP地址、端口号与socket编程接口
- 正文:
- 一、计算机网络背景
- 二、计算机网络协议
- 三、网络传输
-
- 局域网网络传输流程
- 协议栈的通信过程
- 广域网网络(跨网络)传输过程
- IP地址
- 四、socket编程基础
- 端口号
- port与pid
- 理解socket
- [五、 传输层的典型代表](#五、 传输层的典型代表)
- 六、网络字节序接口与socket编程接口
- 网络字节序接口
- socket接口
- sockaddr
正文:
一、计算机网络背景
计算机最开始发明出来是各个实验室的计算机大佬们用来做自己的实验和研究用的,但是人与人之间需要通信,于是随着时代的发展计算机网络就被提出来了
网络发展:
独立模式→网络互联


像这样在小范围内使用的网络就称为局域网LAN,通过交换机和路由器相连接。

但是随着时间的推移,越来越多的设备被接入网络当中,便形成了广域网WAN。

局域网和广域网其实只是一个相对概念之分,比如说我们国家的内网其实可以看作是一个范围很大的局域网。
站在我们后人的角度来看,网络的出现是必然的,因为人与人之间需要协同合作来解决问题,所以计算机计算机之间也必须联合起来,于是便有了网络。
二、计算机网络协议
首先我们来说一下为什么会有协议这种东西。
协议其实就是约定,知道计算机发送的数据都是二进制,而这个二进制如何解析?发送给谁?发送的过程怎么样?这些都是通过协议来标定的。所以协议的存在就是为了减少通信成本,快速形成共识。
使用计算机的人比较少的时候,这个约定比较简单,而随着使用计算机的人数越来越多,便需要一个国际的标准协议。
而且在计算机生产的过程中,会有许多的厂家参与。比如说计算机的生产厂家、操作系统制作者、网络硬件设备的制作者。所以制定一个协议来让大家都遵守,这是刚需。
所以这个时候,国际标准化组织ISO就定义了网络通信的七层协议结构,OSI(开放系统互联,Open System Interconnection)
那么为什么是七层协议结构?为什么有层状结构这个东西呢?
协议分层
其实层状结构这个东西很好理解,比如说操作系统可以分为用户层、系统调用、操作系统层、驱动层和硬件层:

那么分层的好处是什么呢?实际上我们在系统阶段也说过,分层在设计上是为了更好地进行模块化解耦合。而协议的本质也是软件,所以它也要进行模块化解耦合,既让系统更加的高内聚低耦合。
而协议分层之后:
① 在普通用户的视角中,是人和人、主机和主机之间进行通信。在工程师的视角中,是通过多层连接来让机器进行通信。
②每一层之间,可以看作同层之间直接通信。
③分层后可以无障碍地替换掉任意一层。
OSI七层模型
OSI七层模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。
它把网络从逻辑上分为了七层,每一层都有相关相对应的物理设备。
它最大的优点是将服务、接口、协议这三个概念明确的区分开来,概念清楚,理论也比较完整。通过七个层次化的结构模型,使不同的系统、不同的网络之间实现可靠的通讯。


七层之多,在⽹络⻆度,OSI定的协议7层模型其实⾮常完善,但是在实际操作的过程中,会话层、表⽰层是不可能接⼊到操作系统中的,所以在⼯程实践中,最终落地的是5层协议,即我们主要应用的是TCP/IP协议。
TCP/IP五层模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
- 物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。
- 数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网,无线LAN等标准。交换机(Switch)工作在数据链路层。
- 网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
- 传输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。
- 应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。

物理层我们考虑的⽐较少,我们只考虑软件相关的内容. 因此很多时候我们直接称为TCP/IP四层模型
协议的本质
我们在之前的系统编程中了解过进程间通信,而进程间通信在物理结构上来看,实际上就是计算机的不同部件之间在进行通信,信息由某片内存转移到了另一片内存。
而把这个关系放到不同的主机,也就是网络世界当中,其实是相当类似,甚至可以说是一模一样的,唯一的区别是主机之间的距离变远了。
而距离的变化就导致了其他的问题:
一、如何处理数据?二、数据丢失怎么办?三、如何定位目标主机?四、路由器的节点太多,下一个节点是什么?

所以协议在其中就是为了解决这些问题的,所以协议的本质是一种解决方案。而网络通信的本质也就是进程间通信。
TCP/IP协议和操作系统的关系
我们知道网络协议是分层的,而操作系统也是分层的,先抛出结论:实际上网络协议的分层是由操作系统的分层所决定的。
在我们上述所提到的几个问题中,应用层被用来处理协议与用户层相绑定,传输层用来处理数据丢失与OS操作系统所绑定,网络层用来解决主机定位问题,也与操作系统所绑定数据链路层用来解决节点选择问题,与网卡驱动(网卡)相绑定。

协议的朴素理解
协议简单来看,其实就是一个被所有操作系统内核写入的结构体,只有这个样子,每个操作系统,每个机器才能直接解析它从网络上获取的数据。
下面用 C 语言写一个简单的 data 结构体,用来说明"协议"的本质------双方约定好的数据格式:
c
// 假设双方约定的协议格式:
// [1字节 类型a][4字节 长度b][最多256字节 数据c]
typedef struct data {
unsigned char a; // 类型:表示这是什么消息,比如 0=请求, 1=响应
unsigned int b; // 长度:表示后面 c 字段中有效数据的字节数
char c[256]; // 数据:实际传输的内容
} data_t;
用这个结构体说明"协议":
发送方按这个格式填数据:
c
data_t msg;
msg.a = 0; // 标记为请求
msg.b = strlen("hello"); // 记录数据长度
strcpy(msg.c, "hello"); // 填入数据
send(sockfd, &msg, sizeof(msg), 0);
接收方按同样的格式解析:
c
data_t msg;
recv(sockfd, &msg, sizeof(msg), 0);
if (msg.a == 0) { // 按约定解释 a
for (int i = 0; i < msg.b; i++) // 按约定解释 b
putchar(msg.c[i]); // 按约定解释 c
}
核心思想:
- 所谓协议,就是通信双方事先约定好数据的每个字节代表什么含义
- 发送方按格式封装 ,接收方按格式解析
a、b、c的含义、顺序、大小写,只要双方都遵守同一份约定,通信就能正常进行- TCP/IP 协议簇中的每一层,本质上都是类似这样的结构体层层嵌套(加头/去头),例如 TCP 报头、IP 报头,都是结构化的字节布局
以快递单为例,在收发快递的时候,我们并不需要知道对方什么样子,我们只要把快递单填好,把需要寄的东西放进快递箱,对方在签收的时候确认这就是我的快递,打开即可确认收货。

三、网络传输
首先说明,当前网络体系是在多种局域网标准和一种广域网通信标准下协同工作的。局域网在这个系统中是作为广域网底层的技术存在的。
局域网网络传输流程
MAC地址
MAC地址 用来识别数据链路层中相连的节点;
- 长度为 48 比特位,即 6 个字节。一般用 16 进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)
- 在网卡出厂时就确定了,不能修改。MAC地址通常是唯一的(虚拟机中的MAC地址不是真实的MAC地址,可能会冲突;也有些网卡支持用户配置MAC地址)。
- Windows:
ipconfig /all
局域网传输原理(以太网为例)

原理:发送者将消息发送到局域网中,所有机器都能收到,除接收者全部丢弃该信息,接收者接收。
以太网中,任何时刻,只允许一台机器向网络中发送数据
- 如果有多台同时发送,会发生数据干扰,我们称之为数据碰撞
- 所有发送数据的主机要进行碰撞检测 和碰撞避免
- 没有交换机的情况下,一个以太网就是一个碰撞域
- 局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标MAC地址判定
这里可以试着从系统角度来理解局域网通信原理:以太网的本质就是共享资源,只允许一台主机发消息就是临界资源,碰撞检测避免就是互斥属性。
其他局域网的本质和以太网是一样的,无线LAN的原理和以太网基本一致,只是用的电磁波。令牌环网是谁拥有令牌谁就能发消息,实际上就相当于谁拥有锁谁来做事情。
协议栈的通信过程
在⽹络传输的过程中,数据不是直接发送给对⽅主机的,⽽是先要⾃定向下将数据交付给下层协议,
最后由底层发送,然后由对⽅主机的底层来进⾏接受,在⾃底向上进⾏向上交付,下⾯是⼀张⽰意
图。

在协议栈的通信过程中,其中每层都有协议,所以当我进⾏进⾏上述传输流程的时候,要进⾏封装和解包

封装过程

解包和分用过程

如图所示,数据是一层一层封装的,每一层由自己的报头协议和有效载荷组成。

- 报头部分,就是对应协议层的结构体字段,我们一般叫做报头
- 除了报头,剩下的叫做有效载荷
- 故,报文 = 报头 + 有效载荷
接着我们来明确一下不同层的完整报文的叫法:
- 不同的协议层对数据包有不同的称谓:在传输层叫做段 (Segment),在网络层叫做数据报 (Datagram),在链路层叫做帧(Frame)。
- 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(Header),称为封装(Encapsulation)。
- 首部信息中包含了一些类似于首部有多长、载荷(Payload)有多长、上层协议是什么等信息。
- 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的**"上层协议字段"**将数据交给对应的上层协议处理。
从上述过程中看来,为什么网络协议整体被称为网络协议栈呢?因为网络传输就是入栈封装、出栈解包分用的过程。
不考虑应用层协议,任何协议:
a. 报头必须要能做到和有效载荷进行分离的能力。
b. 必须包含,如何将自己的有效载荷交付给上层具体哪个协议的信息。
有的同学会听过抓包工具这个词,实际上的抓包工具原理,就是在解包和分用的过程中,将网卡调节到混杂模式。
混杂模式:发送给任何主机的消息都向上交付。

广域网网络(跨网络)传输过程
IP地址
IP地址一般有两个版本,IPv4和IPv6,现在广泛使用的是IPv4。在没有特殊提及的情况下,默认IP地址都指IPv4。
- IP 地址是在 IP 协议中,用来标识网络中不同主机的地址;
- 对于 IPv4 来说,IP 地址是一个 4 字节,32 位的整数;
- 我们通常也使用"点分十进制 "的字符串表示 IP 地址,例如
192.168.0.1;用点分割的每一个数字表示一个字节,范围是 0 - 255。
由于现在的联网设备越来越多,IPv4已经不能满足人们的需求,于是IPv6出现了,但是由于历史原因,IPv6不能广泛应用,现在广泛应用的技术是电信级NAT (CGN),这里不做延伸,感兴趣的读者可以自己查询。
跨网段的主机数据传输,数据从一台计算器到另一台计算器的传输过程中,要经过一个或者多个路由器。


而路由器如何定位?这就是IP地址的作用。

那么MAC地址和IP地址的区别是什么呢?

从这张图我们可以看出:
- MAC地址是局部的、短期的。上一站是什么和下一站是什么 ⇔ 源MAC地址和目标MAC地址。
- IP地址是整体的、长期的。从哪来、到哪去 ⇔ 源IP地址、目标IP地址。
- ⽬的IP是⼀种⻓远⽬标,Mac是下⼀阶段⽬标,⽬的IP是路径选择的重要依据,mac地址是局域⽹
转发的重要依据
IP网络层存在的意义:提供网络虚拟层,让世界的所有网络都是IP网络,屏蔽最底层网络的差异。
之前我们提到过,站在系统的视角来看,局域网就是进程间通信,广域网的通信也是一样的,实际上广域网的通信只有两种行为:
- 数据由服务器到用户 && 数据由用户到服务器。
再细一点来看,实际上就是由某个进程通过网卡到达网络,再通过网卡流传到某个进程。这说白了不就是进程间通信吗?
那么如何识别进程就成了一个问题,就引出了端口号。
四、socket编程基础
端口号
相信大家对端口号这个名词并不陌生,我们在各种各样的场景下都能见到端口号,比如说8080、20、23...
- 端口号(Port)是传输层协议的内容。
- 端口号是一个 2 字节 16 位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用。
端口号只能被一个进程所占用的特性,决定了端口号能够用来作为进程寻找的工具。

端口号范围划分
- 0 - 1023:知名端口号,HTTP、FTP、SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。
- 1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
port与pid
我们说过,PID即进程ID,用来表示唯一的进程,那么为什么不用进程 id 来寻找进程?
- 原因是进程ID是一个系统概念,我们在设计软件的时候要把系统和网络进行解耦合。
如此一个进程可以绑定多个端口号,但是一个端口号只能绑定一个进程。 - 单端口 vs. 多端口进程对比
| 特性 | 单端口进程 | 多端口进程 |
|---|---|---|
| 并发处理 | 受限于单一监听队列和锁竞争 | 可分流连接,结合 SO_REUSEPORT 实现内核级负载均衡 |
| 业务承载 | 所有流量混合,逻辑耦合高 | 天然隔离,不同端口对应不同业务/协议/环境,解耦清晰 |
| 安全管控 | 防火墙规则相对粗放 | 可实施精细化、端口级的访问控制和安全策略 |
| 运维监控 | 日志和指标混合,难以细分 | 按端口独立监控、日志和限流,问题定位精准 |
| 可用性 | 单点故障风险高 | 可运行多个实例,或实现灰度发布,提升容错和平滑升级能力 |
既然如此,端口号也就有了源端口号和目的端口号,用来表示发送方的进程和接收方的进程。
理解socket
- 综上,IP 地址用来标识互联网中唯一的一台主机,Port 用来标识该主机上唯一的一个网络进程;
- IP + Port 就能表示互联网中唯一的一个进程;
- 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp,srcPort,dstIp,dstPort} 这样的 4元组 就能标识互联网中唯二的两个进程;
- 所以,网络通信的本质,也是进程间通信;
- 我们把 ip + port 叫做套接字(Socket)。
五、 传输层的典型代表
用户要进行网络传输,必然要经过OS,则一定会有相应的系统调用。
认识TCP协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
此处来解释几个名词:
- 连接:指通信双方在数据交换前,通过协商机制建立并确认的逻辑通信信道。
- 面向数据报:指数据以独立、完整的数据报为传输单元,报文之间具有明确边界,接收方所收到的报文个数与发送方一致。
- 面向字节流:指数据以连续的字节序列形式传输,报文之间不存在明确的边界划分,接收方需自行界定数据边界。
六、网络字节序接口与socket编程接口
网络字节序接口
大端和小端

我们已经知道,内存中的多字节数据相对于内存地址有大端 和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接收到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节;
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的C代码在⼤端和⼩端计算机上编译后都能正常运⾏,可以调⽤以下
库函数做网络字节序和主机字节序的转换:
c
#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);
socket接口
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// 创建 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);
可以看到三个接口里面都有struct sockaddr这个结构体,下面我们来介绍一下
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 API 可以都用 struct sockaddr * 类型表示,在使用的时候需要强制转化成 sockaddr_in;这样的好处是程序的通用性,可以接收 IPv4、IPv6,以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数。
再说人话
看不懂上面的这些不要紧,接下来我会仔细解释一下这是什么和为什么这样做,之前我们了解过system V,这是一种本地进程间通信的原理,而我们现在常用的是posix 标准,用来进网络进程间通信,也可以兼容本地进程通信,用的就是socket。
这里的Socket分为网络Socket与本地Socket(UNIX域间通信),多种Socket意味着多种接口,需要多种通信接口规范。设计者为了更加方便,只留了一种接口,其中使用的就是sockaddr。

下面我们来看一下源码的结构:

sockaddr_in内部还有一个宏,我们来看一下到底是什么:
c
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
原来传入sin_以后就是sin_family
说白了完整的结构就是:
c
struct sockaddr_in
{
sa_family_t sin_family; // 来自宏展开
in_port_t sin_port; // 16位端口号
struct in_addr sin_addr; // 32位IP
unsigned char sin_zero[8]; // 填充,凑够16字节和 sockaddr 对齐
};
sockaddr结构体一张图说清楚
下一篇文章我们来一起书写socket调用实例
- 本节完...
📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!