【Linux网络编程】01 · 网络基础概念(从协议分层到Socket预备)
摘要:本文系统梳理计算机网络的核心基础概念------网络发展史、协议与协议分层(OSI七层 / TCP/IP五层)、以太网局域网通信原理、数据封装与分用、跨网络传输中 IP 与 MAC 的分工,以及 Socket 编程前的必备知识(端口号、字节序、sockaddr 结构族)。在课件基础上补充了大量原理拓展、图示与代码片段,适合作为 Linux 网络编程的入门笔记。
关键词:网络协议、OSI七层模型、TCP/IP、MAC地址、IP地址、端口号、网络字节序、Socket
目录
- 一、计算机网络背景与发展
- 二、初识"协议"
- 三、协议分层
- 四、再识协议:协议的朴素理解
- 五、网络传输基本流程(局域网)
- 六、跨网络传输流程
- [七、Socket 编程预备](#七、Socket 编程预备)
- 八、总结与常见面试题
一、计算机网络背景与发展
1.1 网络形态的演进
计算机网络的诞生不是偶然,而是人需要协同工作这一需求的必然结果。
| 阶段 | 形态 | 特点 |
|---|---|---|
| ① 独立模式 | 计算机相互独立 | 单机作业,数据靠人手工拷贝(软盘、U 盘),无数据共享 |
| ② 网络互联 | 多台计算机连在一起 | 完成数据共享,机器之间可以交换数据 |
| ③ 局域网 LAN | 通过交换机和路由器连接 | 计算机数量更多,覆盖范围一栋楼 / 一个园区 |
| ④ 广域网 WAN | 将远隔千里的计算机连在一起 | 跨城市、跨国家,互联网就是最大的广域网 |
1.2 LAN 与 WAN 是相对概念
所谓"局域网"和"广域网"只是一个相对的概念。比如我们有中国特色的广域网,也可以看做一个比较大的局域网。
换句话说:没有绝对的界限。
- 对你家来说,家里的 WiFi 是一个局域网;
- 对你的运营商来说,整个城市的接入网是一个局域网;
- 对国家来说,全国的网络又可以看作一个更大的"局域网"。
一个网络是 LAN 还是 WAN,取决于你站在哪一层看它。
二、初识"协议"
2.1 协议就是一种"约定"
最朴素的理解:协议 = 约定。
比如打电话时约定"铃响一声是快递、响两声是家里人"------这就是一种约定。
2.2 为什么"定好协议"还不够?
计算机之间的传输媒介是光信号和电信号 ,通过"频率"和"强弱"来表示 0 和 1。
思考:只要通信的两台主机约定好协议就可以了吗?
答案是否定的。考虑这个场景:
定好协议,但是你用频率 表示 01,我用强弱表示 01,就好比我讲中国话、你讲葡萄牙语------虽然大家遵守的是"同一套通信规则",但语言不同,即使订好了基本协议,也无法正常通信。
所以,完善的协议需要更多、更细致的规定,并且要求所有参与者都遵守。
2.3 为什么必须有"统一标准"
现实世界的问题在于:
- 计算机生产厂商有很多(联想、戴尔、华为......)
- 计算机操作系统也有很多(Windows、Linux、macOS......)
- 计算机网络硬件设备还是有很多(不同厂商的网卡、交换机、路由器......)
如何让这些不同厂商生产的计算机能够相互顺畅通信?就需要有人站出来,约定一个共同的标准 ,大家都来遵守,这就是网络协议。
拓展:一个协议能成为"标准",通常有两条路
- 事实标准(de facto):先有实现,用的人多了就成了标准。典型如 TCP/IP ------ 它伴随 Internet 一起成长,最终被 IETF 以 RFC 文档固化。
- 法定标准(de jure):由公认的标准化组织自上而下制定。典型如 OSI 参考模型(ISO 制定)。
2.4 谁有资格制定协议标准?
能够制定协议/标准的组织或公司,必须是业界公认、具有"江湖地位"的。常见的有这几类:
| 类别 | 组织 | 主要贡献 |
|---|---|---|
| 国际标准化组织 | IEEE(电气和电子工程师协会) | 制定了全世界电子、电气、计算机科学领域约 30% 的标准,核心是 IEEE 802 系列(802.3 以太网、802.11 WiFi、802.15 蓝牙等),覆盖 LAN 到 WAN |
| ISO(国际标准化组织) | 最著名的是 OSI 七层参考模型。虽然实际工程上 TCP/IP 更普遍,但 OSI 模型在学术与理论研究中地位极高 | |
| ITU(国际电信联盟) | 联合国下属专门机构,ITU-T 制定的标准涵盖电话与网络通信 | |
| 区域标准化组织 | ETSI(欧洲电信标准学会) | 欧洲各国政府资助,研究开发到标准制定一体 |
| ASTAP(亚洲与泛太平洋电信标准化协会) | 1998 年由日本与韩国发起,推动亚太地区信息通信基础设施标准化 | |
| 民间国际团体 | IETF(互联网工程师任务组) | ⭐最重要 。志愿者组织,通过 RFC 文档发布/更新互联网协议标准,TCP/IP 协议族就出自这里 |
| 官方机构 | FCC(美国联邦通信委员会) | 管理无线电、电视、有线通信,对通信产品的技术特性进行审查监督 |
| 公司 | 各类头部厂商 | 自研协议栈(如低功耗蓝牙、Zigbee、Thread、Matter 等)并推动其成为行业标准 |
💡 拓展 :我们常说的 RFC(Request For Comments)就是 IETF 的标准文档。比如:
- RFC 791 → IP 协议
- RFC 793 → TCP 协议
- RFC 768 → UDP 协议
- RFC 2616 → HTTP/1.1
想查任何一个互联网协议的权威定义,直接搜 "RFC xxxx" 即可。
三、协议分层
3.1 为什么协议要分层?
协议本质也是软件 ,在设计上为了更好地进行模块化、解耦合 ,被设计成层状结构。
用打电话的例子理解分层:
语言层: 中国人 ──汉语── 中国人 (上层:只关心"说什么")
↓
通信设备层:电话 ──电话线── 电话 (下层:只关心"怎么传")
- 语言层的人不需要知道电话是怎么工作的;
- 通信设备层不需要知道两个人说的是汉语还是英语;
- 换掉任何一层,另一层都不受影响 ------ 这就是解耦合。
真实的网络通信协议远比这复杂,需要分更多的层,但核心思想一致:
分层可以实现解耦合,让软件维护的成本更低。
3.2 OSI 七层模型
OSI(Open System Interconnection,开放系统互连)七层模型是一个逻辑上的定义和规范:
- 把网络从逻辑上分为 7 层,每层都有对应的物理设备(如路由器、交换机);
- 是一种框架性设计方法,最主要的功能是帮助不同类型的主机实现数据传输;
- 最大优点 :将 服务、接口、协议 这三个概念明确区分开来,概念清楚,理论完整。
| 层号 | 层名 | 功能 | 典型协议 / 设备 |
|---|---|---|---|
| 7 | 应用层 | 为应用程序提供网络服务 | HTTP、FTP、SMTP、DNS |
| 6 | 表示层 | 数据格式转换、加密、压缩 | JPEG、SSL/TLS(部分) |
| 5 | 会话层 | 建立、管理、终止会话 | --- |
| 4 | 传输层 | 端到端的数据传输与可靠性 | TCP、UDP |
| 3 | 网络层 | 地址管理与路由选择 | IP、ICMP、路由器 |
| 2 | 数据链路层 | 相邻设备间数据帧的传送与识别 | 以太网、交换机 |
| 1 | 物理层 | 光/电信号的传递 | 网线、光纤、集线器 |
⚠️ 但它既复杂又不实用 ,所以工程上我们按 TCP/IP 模型 来讲解。
拓展:为什么 OSI 七层"落地不了"?
在网络角度,OSI 定的七层模型其实非常完善;但在实际操作过程中,会话层、表示层是不可能接入到操作系统中的 。
所以在工程实践中,最终落地的是 5 层协议。
原因很现实:会话层和表示层的功能,要么被应用层程序自己实现了(比如 TLS 加密、JSON 序列化都是应用程序干的),要么根本用不上。操作系统内核没有动力为这两层单独提供机制。
3.3 TCP/IP 五层(或四层)模型
TCP/IP 是一组协议的代名词,它还包括许多协议,组成了 TCP/IP 协议簇 。采用了 5 层层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
| 层 | 职责 | 关键细节 | 典型设备 |
|---|---|---|---|
| 应用层 | 负责应用程序间沟通 | SMTP(邮件)、FTP(文件传输)、Telnet(远程登录)等。我们的网络编程主要就是针对应用层 | --- |
| 传输层 | 负责两台主机之间的数据传输 | 如 TCP,确保数据可靠地从源主机发送到目标主机 | --- |
| 网络层 | 负责地址管理 和路由选择 | IP 协议中通过 IP 地址标识一台主机,并通过路由表规划两台主机间的数据传输线路(路由) | 路由器 Router |
| 数据链路层 | 负责设备之间数据帧的传送和识别 | 网卡驱动、帧同步 (从网线上检测到什么信号算作新帧的开始)、冲突检测 (检测到冲突自动重发)、数据差错校验。标准有以太网、令牌环网、无线 LAN | 交换机 Switch |
| 物理层 | 负责光/电信号的传递方式 | 以太网通用的双绞线、早期同轴电缆(现在主要用于有线电视)、光纤、WiFi 使用的电磁波。物理层能力决定了最大传输速率、传输距离、抗干扰性 | 集线器 Hub |
📌 为什么又叫"四层"?
物理层我们考虑得比较少,只考虑软件相关的内容,因此很多时候直接称为 TCP/IP 四层模型(把物理层和数据链路层合并,或直接省略物理层)。
3.4 不同设备实现了哪几层?
宏观上(不绝对):
| 设备 | 实现的层 |
|---|---|
| 主机(你的电脑) | 传输层 → 网络层 → 数据链路层 → 物理层(应用层由用户程序实现) |
| 路由器 | 网络层 → 数据链路层 → 物理层 |
| 交换机 | 数据链路层 → 物理层 |
| 集线器 | 仅物理层 |
主机 路由器 交换机 集线器
┌─────────┐
│ 应用层 │
├─────────┤
│ 传输层 │
├─────────┤
│ 网络层 │ ┌─────────┐
├─────────┤ │ 网络层 │
│ 链路层 │ ├─────────┤ ┌─────────┐
├─────────┤ │ 链路层 │ │ 链路层 │
│ 物理层 │ ├─────────┤ ├─────────┤ ┌─────────┐
└─────────┘ │ 物理层 │ │ 物理层 │ │ 物理层 │
└─────────┘ └─────────┘ └─────────┘
⚠️ 但并非绝对 :很多交换机也实现了网络层的转发(三层交换机);很多路由器也实现了部分传输层的内容(比如端口转发 / NAT)。
四、再识协议:协议的朴素理解
4.1 为什么要有 TCP/IP 协议?
- 单机内部其实也有协议 :其他设备和内存通信有内存协议;和磁盘通信有 SATA、IDE、SCSI 等协议。只不过我们感知不到,而且这些协议都在本地主机各自的硬件中,通信的成本和问题都比较少。
- 网络通信最大的特点就是主机之间变远了。任何通信特征的变化,一定会带来新的问题,有问题就得解决,所以需要新的协议。
本质:通信主机距离变远了。
拓展:距离变远具体带来了哪些新问题? 这正是 TCP/IP 各层存在的理由:
| 新问题 | 对应解决层 |
|---|---|
| 要经过很多台设备转发,怎么找到目标? | 网络层(IP + 路由) |
| 中间线路可能丢包、乱序、出错 | 传输层(TCP 可靠传输) |
| 不同链路介质不同(网线/光纤/WiFi),怎么统一? | 网络层(IP 屏蔽底层差异) |
| 一台主机上有很多进程,数据该给谁? | 传输层(端口号) |
| 二进制数据在链路上怎么成帧、怎么校验? | 数据链路层 |
4.2 TCP/IP 协议是什么?
- TCP/IP 协议的本质是一种解决方案;
- TCP/IP 协议能分层,前提是因为问题本身就能分层。
4.3 协议的朴素理解:结构化的数据类型
OS 源代码一般都是用 C/C++ 写的。看下面这个场景:
主机 A 主机 B
struct protocol { int a; int b; int c; }; struct protocol { int a; int b; int c; };
a=10, b=20, c=30 ────── data ──────> 收到 data
问题:主机 B 能识别 data,并且准确提取 a=10, b=20, c=30 吗?
回答:答案是肯定的! 因为双方都有同样的结构体类型
struct protocol。也就是说,用同样的代码实现协议,用同样的自定义数据类型,天然就具有"共识" ,能够识别对方发来的数据 ------ 这不就是约定吗?
⭐ 关于协议的朴素理解:所谓协议,就是通信双方都认识的结构化的数据类型。
因为协议栈是分层的,所以每层都有协议,同层之间互相可以认识对方的协议。
拓展:用代码感受一下"协议 = 结构体"
c
// 一个极简的"应用层协议":双方约定的结构化数据类型
typedef struct {
uint32_t length; // 报头字段:有效载荷长度(网络字节序)
uint16_t type; // 报头字段:消息类型
uint16_t reserved;
char payload[0]; // 柔性数组:有效载荷
} proto_header_t;
发送方:填好 length 和 type,把数据追加在 payload 后面,整体发出去。
接收方:先读 sizeof(proto_header_t) 拿到报头,再根据 length 取出有效载荷 ------ 这就是"解包"。
📌 从今天开始,学习任何协议都要先宏观建立这样的认识:
- 要学习的协议,是如何做到解包的?(明确了解包,封包也就能理解)
- 要学习的协议,是如何做到将自己的有效载荷交付给上层协议的?(即"分用")
这两个问题,是后面学习每一个具体协议的万能钥匙。
4.4 生活中的类比:网购与快递单
你要寄的商品 (应用层数据)
↓ 卖家装进盒子里,贴上面单
【快递单 + 商品】 (报文 = 报头 + 有效载荷)
↓ 装进集散箱的袋子
【大袋标签 +【快递单 + 商品】】
↓ 装进货车
【车牌/路线 + 大袋标签 + 快递单 + 商品】
每一层只关心自己那张"标签",到了目的地再一层层撕掉 ------ 这就是封装与解包。
五、网络传输基本流程(局域网)
5.1 局域网(以太网)通信原理
问题:两台主机在同一个局域网,是否能够直接通信?
能。 以太网局域网的通信原理类似上课:
老师在讲台上喊一句"张三,你来回答这个问题" ------ 全班每个人都听见了,但只有张三站起来应答,其他人检查发现"不是叫我",就直接丢弃。
对应到以太网:
- 每台主机在局域网上要有唯一的标识 来保证主机的唯一性 → MAC 地址;
- 主机 A 发出一帧数据,局域网内所有主机都能收到;
- 每台主机比对帧中的目标 MAC 地址是否是自己,是就收下,不是就丢弃;
局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标 MAC 地址判定的。
5.2 认识 MAC 地址
| 属性 | 说明 |
|---|---|
| 作用 | 用来识别数据链路层中相连的节点 |
| 长度 | 48 比特位,即 6 个字节 |
| 表示 | 16 进制数字 + 冒号,例如 08:00:27:03:fb:19 |
| 唯一性 | 在网卡出厂时就确定了,不能修改。MAC 地址通常是唯一的 |
| 例外 | 虚拟机中的 MAC 地址不是真实的 MAC 地址,可能会冲突;有些网卡支持用户配置 MAC 地址 |
| 查看 | Windows 下执行 ipconfig /all;Linux 下执行 ifconfig 或 ip link |
后面详细讨论数据链路层时会谈 MAC 帧协议,此处先做了解即可。
拓展:MAC 地址的结构
08 : 00 : 27 : 03 : fb : 19
└─┬─┘ └───────┬───────┘
OUI 厂商分配
(厂商标识) (网卡序列号)
- 前 24 位叫 OUI(组织唯一标识符),由 IEEE 分配给网卡厂商;
- 后 24 位由厂商自行分配。
拓展:既然 MAC 是唯一的,为什么还需要 IP? → 见 [6.3 节](#6.3 节)。
5.3 碰撞域与 CSMA/CD
以太网的核心约束:
以太网中,任何时刻,只允许一台机器向网络中发送数据。
- 如果有多台同时发送,会发生数据干扰 ,我们称之为数据碰撞;
- 所有发送数据的主机要进行碰撞检测 和碰撞避免;
- 没有交换机的情况下,一个以太网就是一个碰撞域。
拓展:CSMA/CD 机制
早期总线型以太网使用 CSMA/CD(Carrier Sense Multiple Access with Collision Detection,载波监听多路访问/冲突检测):
- 载波监听:发送前先听,线路忙就等;
- 多路访问:所有主机共享同一条总线;
- 冲突检测:发送中持续检测,一旦发现冲突立即停止;
- 二进制指数退避:冲突后随机等待一段时间再重试,冲突次数越多,退避窗口越大。
现代以太网普遍用交换机组网,交换机每个端口是一个独立的碰撞域(全双工),基本不再需要 CSMA/CD ------ 这也是网络从"共享式"走向"交换式"的关键进步。
5.4 封装、解包与分用
同一网段内的两台主机发送消息时,数据要自顶向下经过每一层,每层都要进行封装和解包。
明确几个概念:
| 概念 | 定义 |
|---|---|
| 报头(Header) | 对应协议层的结构体字段 |
| 有效载荷(Payload) | 除了报头,剩下的部分 |
| 报文 | 报文 = 报头 + 有效载荷 |
不同层完整报文的叫法:
| 所在层 | 报文名称 |
|---|---|
| 传输层 | 段(segment) |
| 网络层 | 数据报(datagram) |
| 链路层 | 帧(frame) |
封装过程:
-
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header) ,称为封装(Encapsulation);
-
首部信息中包含类似于"首部有多长 "、"载荷(payload)有多长 "、"上层协议是什么"等信息;
-
数据封装成帧后发到传输介质上;
-
到达目的主机后,每层协议再剥掉相应的首部 ,根据首部中的"上层协议字段 "将数据交给对应的上层协议处理 ------ 这叫分用(Demultiplexing)。
主机 A(发送) 主机 B(接收) 用户数据 │ 应用层 ── 添加 App Header ▼ ┌───────────────┐ │ App Hdr │ 数据 │ └───────────────┘ │ 传输层 ── 添加 TCP/UDP 首部(含端口号)→ 【段 segment】 ▼ ┌───────────────────────┐ │ TCP Hdr │ 应用数据 │ └───────────────────────┘ │ 网络层 ── 添加 IP 首部(含 IP 地址)→ 【数据报 datagram】 ▼ ┌──────────────────────────────┐ │ IP Hdr │ TCP Hdr │ 应用数据 │ └──────────────────────────────┘ │ 链路层 ── 添加以太网首部(含 MAC 地址)+ CRC → 【帧 frame】 ▼ ┌────────────────────────────────────────┐ │ Eth Hdr │ IP Hdr │ TCP Hdr │ 应用数据 │ CRC │ └────────────────────────────────────────┘ │ 物理层 ── 转成光/电信号,发到网线 ▼ ═══════════════════════════════════════════════> 传输介质 │ ▼ 逐层解包(分用) 根据 Eth Hdr 中"上层协议=IP"交给 IP 层 根据 IP Hdr 中"上层协议=TCP"交给 TCP 层 根据 TCP Hdr 中"目的端口=8080"交给进程
📌 总结一句话 :在网络传输的过程中,数据不是直接发送给对方主机 的,而是先自顶向下 将数据交付给下层协议,最后由底层发送;然后由对方主机的底层来接收,再自底向上进行向上交付。
拓展:每层靠什么字段完成"分用"?
| 层 | 分用依据字段 |
|---|---|
| 数据链路层(以太网) | 以太网首部中的 类型字段(0x0800=IPv4,0x0806=ARP,0x86DD=IPv6) |
| 网络层(IP) | IP 首部中的 8 位协议号(6=TCP,17=UDP,1=ICMP) |
| 传输层(TCP/UDP) | 首部中的 目的端口号 |
| 应用层 | 你自己定义的协议字段 |
六、跨网络传输流程
6.1 认识 IP 地址
| 属性 | 说明 |
|---|---|
| 版本 | IP 协议有两个版本:IPv4 和 IPv6 。没有特殊说明时,默认指 IPv4 |
| 作用 | 在 IP 协议中,用来标识网络中不同主机的地址 |
| 长度 | 对 IPv4 来说,IP 地址是一个 4 字节、32 位的整数 |
| 表示 | 通常使用"点分十进制 "的字符串表示,例如 192.168.0.1;用点分割的每一个数字表示一个字节,范围是 0 - 255 |
跨网段主机的传输 :数据从一台计算机到另一台计算机传输过程中,要经过一个或多个路由器。
6.2 路由器做了什么?
问题 1:为什么要去目标主机,先要走路由器?
因为源主机和目标主机不在同一个网段,链路层(MAC 寻址)够不着。必须先交给"网关"(路由器),由路由器根据目的 IP 查路由表决定下一跳。
问题 2:目的 IP 的意义?
目的 IP 是整个传输过程的最终目标,路由器据此做路径选择。
路由器对数据的处理(重点):
主机A ──> 路由器 ──> 主机B
路由器收到帧后:
1. 链路层:剥掉以太网首部(看到的是"发给我的 MAC")
2. 网络层:查看目的 IP,查路由表,决定从哪个口转发出去
3. 链路层:重新封装一个新的以太网首部(源 MAC = 路由器出接口 MAC,
目标 MAC = 下一跳设备的 MAC)
4. 物理层:发出去
⭐ 关键理解 :路由器做的是"解包 → 重新封装 "。IP 层以上的数据(TCP 首部 + 应用数据)不会被改动。
6.3 对比 IP 地址和 MAC 地址
| 对比项 | IP 地址 | MAC 地址 |
|---|---|---|
| 所在层 | 网络层 | 数据链路层 |
| 在整个路由过程中 | 一直不变(源 IP 和目的 IP 始终不变)※ | 一直在变(每经过一跳,源/目的 MAC 都被重写) |
| 作用 | 长远目标,路径选择的重要依据 | 下一阶段目标,局域网转发的重要依据 |
| 类比 | 快递单上的"最终收货地址" | 每一段运输的"上一站 → 下一站" |
※ 注:严格来说,经过 NAT 时源 IP 也会变,这里先按"不变"理解,后续再修正。
📌 一句话记忆 :IP 是终点,MAC 是下一站。
6.4 IP 网络层的意义
IP 网络层存在的意义:提供网络虚拟层,让世界的所有网络都是 IP 网络,屏蔽最底层网络的差异。
拓展理解 :底层的链路技术五花八门------以太网、WiFi、光纤、PPP、令牌环......如果应用层要逐个适配,几乎不可能。IP 层在所有这些链路之上提供了一个统一的抽象:不管你底下是什么,到了我这一层,统统都是 IP 数据报。
这正是计算机科学中"任何问题都可以通过增加一个中间层来解决"的经典范例。
HTTP / FTP / SSH ... (应用层:不必关心底层是什么网)
↓
TCP / UDP
↓
★★ IP ★★ ← 虚拟层:屏蔽底层差异
╱ │ ╲
以太网 WiFi 光纤/PPP ... (底层链路:各不相同)
七、Socket 编程预备
7.1 理解源 IP 地址和目的 IP 地址
IP 在网络中用来标识主机的唯一性。
但这里要思考一个问题:数据传输到主机是目的吗?
不是的。因为数据是给人用的。
- 聊天是人在聊天,下载是人在下载,浏览网页是人在浏览;
- 但人是怎么看到聊天信息的?怎么执行下载任务?怎么浏览网页信息?------ 通过启动的 QQ、迅雷、浏览器;
- 而启动的 QQ、迅雷、浏览器都是进程。
换句话说,进程是人在系统中的代表。只要把数据给进程,人就相当于拿到了数据。
所以:
数据传输到主机不是目的,而是手段。到达主机内部,再交给主机内的进程,才是目的。
但系统中同时存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程 ?这就要在网络背景下,在系统中标识进程的唯一性 → 端口号。
7.2 认识端口号
端口号(port)是传输层协议的内容。
| 属性 | 说明 |
|---|---|
| 长度 | 一个 2 字节 16 位的整数 |
| 作用 | 用来标识一个进程,告诉操作系统当前这个数据要交给哪一个进程来处理 |
| 组合 | IP 地址 + 端口号 能够标识网络上某一台主机的某一个进程 |
| 独占性 | 一个端口号只能被一个进程占用 |
端口号范围划分:
| 范围 | 类别 | 说明 |
|---|---|---|
| 0 - 1023 | 知名端口号(Well-Known) | HTTP、FTP、SSH 等广为使用的应用层协议,端口号都是固定的 |
| 1024 - 65535 | 操作系统动态分配的端口号 | 客户端程序的端口号,就是由操作系统从这个范围分配的 |
常见的知名端口:
| 端口 | 服务 |
|---|---|
| 22 | SSH |
| 21 | FTP |
| 23 | Telnet |
| 25 | SMTP |
| 53 | DNS |
| 80 | HTTP |
| 443 | HTTPS |
| 3306 | MySQL |
| 6379 | Redis |
查看知名端口(Linux):
bash
cat /etc/services
7.3 理解"端口号"和"进程 PID"
我们学系统编程时知道 pid 表示唯一一个进程;此处端口号也是唯一表示一个进程。那么这两者之间是怎样的关系?
10086 的例子:
10086 是中国移动的客服热线(相当于端口号 ,稳定不变,谁都能打);
而接电话的那个客服小姐姐的工号(相当于 PID)每次都是随机的,且她下班了就换人。
你只需要记住 10086 就够了,不需要关心今天是谁值班。
核心差异:
| 对比 | PID | 端口号 |
|---|---|---|
| 所属 | 系统概念(进程管理) | 网络概念(传输层) |
| 稳定性 | 每次启动进程都可能变化 | 需要固定(否则客户端找不到服务) |
| 耦合 | 与系统进程管理强绑定 | 与网络独立 |
进程 PID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程;但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
另外两条规则:
- 一个进程可以绑定多个端口号(比如一个 Web 服务器同时监听 80 和 443);
- 但是一个端口号不能被多个进程绑定 (除非设置
SO_REUSEPORT)。
7.4 理解源端口号和目的端口号
传输层协议(TCP 和 UDP)的数据段中有两个端口号,分别叫源端口号 和目的端口号 ,就是在描述"数据是谁发的,要发给谁"。
7.5 理解 socket
综上:
- IP 地址用来标识互联网中唯一的一台主机;
- port 用来标识该主机上唯一的一个网络进程;
- IP + Port 就能表示互联网中唯一的一个进程。
通信的时候,本质是两个互联网进程代表人来进行通信。
{srcIp, srcPort, dstIp, dstPort}这样的 4 元组就能标识互联网中唯二的两个进程。
⭐ 所以,网络通信的本质,也是进程间通信。
我们把 ip + port 叫做套接字 socket。
💡 拓展 :
socket一词的本义是"插座 / 插孔"。就像插头插进插座才有电一样,把(IP, Port)这一对"插头"插进网络这个"插座",通信就建立起来了。
7.6 传输层的典型代表
传输层是属于内核 的。所以我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用。
认识 TCP 协议
TCP(Transmission Control Protocol,传输控制协议):
- ✅ 传输层协议
- ✅ 有连接
- ✅ 可靠传输
- ✅ 面向字节流
认识 UDP 协议
UDP(User Datagram Protocol,用户数据报协议):
- ✅ 传输层协议
- ✅ 无连接
- ✅ 不可靠传输
- ✅ 面向数据报
| 对比 | TCP | UDP |
|---|---|---|
| 是否连接 | 有连接(三次握手) | 无连接 |
| 是否可靠 | 可靠(确认、重传、排序、流量控制、拥塞控制) | 不可靠(尽最大努力交付) |
| 数据形态 | 面向字节流(无边界) | 面向数据报(有边界,一次一整包) |
| 首部开销 | 20~60 字节 | 固定 8 字节 |
| 传输效率 | 较低 | 高 |
| 典型应用 | HTTP、FTP、SSH、邮件 | DNS、音视频直播、游戏、DHCP |
7.7 网络字节序
我们已经知道:
- 内存中的多字节数据相对于内存地址有大端和小端之分;
- 磁盘文件中的多字节数据相对于文件偏移地址也有大端小端之分;
- 网络数据流同样有大端小端之分。
那么如何定义网络数据流的地址?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
因此,网络数据流的地址应这样规定:
先发出的数据是低地址,后发出的数据是高地址。
TCP/IP 协议的规定:
⭐ 网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照 TCP/IP 规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。
📌 网络规定:所有发送到网络上的数据,都必须是大端的!
字节序转换函数
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
c
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host → network, 32位长整数(用于 IP 地址)
uint16_t htons(uint16_t hostshort); // host → network, 16位短整数(用于 端口号)
uint32_t ntohl(uint32_t netlong); // network → host, 32位
uint16_t ntohs(uint16_t netshort); // network → host, 16位
命名规则很好记:
| 字符 | 含义 |
|---|---|
h |
host(主机字节序) |
n |
network(网络字节序) |
l |
32 位长整数(long) |
s |
16 位短整数(short) |
例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端 字节序,这些函数不做转换,将参数原封不动地返回。
拓展:判断当前机器是大端还是小端
c
#include <stdio.h>
// 方法1:指针法
int is_little_endian(void)
{
int i = 1;
return *(char *)&i; // 返回 1 → 小端;返回 0 → 大端
}
// 方法2:联合体法(更经典)
int is_little_endian_union(void)
{
union {
int i;
char c;
} u;
u.i = 1;
return u.c; // 返回 1 → 小端;返回 0 → 大端
}
int main(void)
{
printf("%s\n", is_little_endian() ? "Little Endian" : "Big Endian");
return 0;
}
💡 为什么宿舍常见的 x86 / ARM 都是小端?
小端便于低位字节的取用(比如把
int强转char直接拿到低 8 位),CPU 设计时电路更简单。而网络选择大端,是因为历史上早期网络设备(如 IBM 大型机)多用大端,RFC 干脆统一规定为"网络字节序 = 大端"。
7.8 socket 常见 API
c
#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);
| API | 适用 | 说明 |
|---|---|---|
socket() |
TCP/UDP,客户端+服务器 | 创建一个通信端点,返回文件描述符 |
bind() |
TCP/UDP,主要是服务器 | 把 socket 和 (IP, Port) 绑定 |
listen() |
TCP,服务器 | 把主动 socket 转为被动(监听)socket |
accept() |
TCP,服务器 | 从已完成连接队列中取出一个连接,返回新的 fd |
connect() |
TCP,客户端 | 向服务器发起连接(三次握手) |
💡 拓展 :
socket()返回的本质上是一个文件描述符 。这体现了 Linux "一切皆文件 " 的思想------网络读写用的也是read()/write(),或者专用的send()/recv()。
7.9 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 结构体指针作为参数。
三个结构的关系
c
/* 通用地址结构 ------ API 的参数类型(16 字节) */
struct sockaddr {
sa_family_t sa_family; /* 16位 地址类型 */
char sa_data[14]; /* 14字节 协议地址 */
};
/* IPv4 地址结构 ------ 实际编程用的(16 字节) */
struct sockaddr_in {
sa_family_t sin_family; /* 16位 地址族: AF_INET */
in_port_t sin_port; /* 16位 端口号(网络字节序) */
struct in_addr sin_addr; /* 32位 IPv4 地址 */
unsigned char sin_zero[8]; /* 填充,未使用(保证与 sockaddr 等长) */
};
/* IPv4 地址本体 ------ 其实就是一个 32 位整数 */
struct in_addr {
uint32_t s_addr;
};
┌──────────────────┐
struct sockaddr │ sa_family (16位) │ ← 前 2 字节都是"地址类型"
├──────────────────┤
│ sa_data[14] │
└──────────────────┘
↕ (强制类型转换,按 sa_family 分支)
┌──────────────────┬──────────────────┬──────────────────┐
│ struct sockaddr_in│ struct sockaddr_in6│ struct sockaddr_un │
│ (IPv4, AF_INET) │ (IPv6, AF_INET6) │ (UNIX域, AF_UNIX)│
└──────────────────┴──────────────────┴──────────────────┘
⭐ 虽然 socket API 的接口是
sockaddr,但我们真正在基于 IPv4 编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型、端口号、IP 地址。
典型用法
c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // ⚠️ 必须转网络字节序
addr.sin_addr.s_addr = inet_addr("192.168.1.10"); // ⚠️ 已是网络字节序
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); // ⚠️ 强制类型转换
地址转换函数:
c
#include <arpa/inet.h>
// 点分十进制字符串 ⇄ 32位网络字节序整数(仅 IPv4)
in_addr_t inet_addr(const char *cp); // "192.168.1.1" → 网络序整数
char *inet_ntoa(struct in_addr in); // 网络序整数 → "192.168.1.1"
// 通用(支持 IPv4/IPv6,推荐)
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
⚠️ 常见坑:
sin_port必须用htons()转换,直接写8080会在 x86(小端)上变成 37115;inet_addr()返回的已经是网络字节序 ,不要再对它调用htonl();inet_ntoa()内部使用静态缓冲区 ,多次调用会覆盖之前的结果,且非线程安全 ;多线程请用inet_ntop()。
八、总结与常见面试题
8.1 一图流总结
┌─────────────────────────────────────────────────────────┐
│ 应用层 HTTP / FTP / SSH ... ← 网络编程针对这一层 │
├─────────────────────────────────────────────────────────┤
│ 传输层 TCP(可靠/有连接/字节流) UDP(不可靠/无连接/数据报) │ ← 端口号
├─────────────────────────────────────────────────────────┤
│ 网络层 IP ------ 地址管理 + 路由选择 │ ← IP地址,屏蔽底层差异
├─────────────────────────────────────────────────────────┤
│ 链路层 以太网 ------ 帧传送/帧同步/冲突检测/CRC校验 │ ← MAC地址,局域网转发
├─────────────────────────────────────────────────────────┤
│ 物理层 双绞线 / 光纤 / 电磁波 │ ← 决定速率、距离、抗干扰
└─────────────────────────────────────────────────────────┘
报文 = 报头 + 有效载荷 段(传输层) / 数据报(网络层) / 帧(链路层)
封装 = 自上而下加首部 分用 = 自下而上剥首部,按"上层协议字段"上交给对应协议
IP + Port = socket 四元组 {srcIp, srcPort, dstIp, dstPort} 唯一确定一对通信进程
网络字节序 = 大端 htonl/htons/ntohl/ntohs
8.2 高频面试题速记
| 问题 | 答案要点 |
|---|---|
| OSI 七层和 TCP/IP 五层的区别? | OSI 是理论模型,含表示层/会话层但无法实现;TCP/IP 是工程落地模型,把这两层并入了应用层 |
| 为什么需要分层? | 模块化、解耦合,降低维护成本;每层只关心自己的事,下层对上层透明 |
| 路由器 / 交换机 / 集线器分别工作在哪层? | 网络层 / 数据链路层 / 物理层 |
| IP 和 MAC 的区别? | IP 是网络层的"长远目标",全程不变;MAC 是链路层的"下一站目标",每跳都变 |
| 有了 MAC 为什么还要 IP? | MAC 地址是扁平的、无层级,无法做路由聚合,只能靠广播;IP 有网段划分,可以做路由表聚合、跨网寻址 |
| 端口号和 PID 的区别? | 都是唯一标识,但 PID 是系统概念、每次启动都变;端口号是网络概念、需要固定,避免与进程管理强耦合 |
| 一个端口能被多个进程绑定吗? | 不能(除非 SO_REUSEPORT);但一个进程可以绑定多个端口 |
| 什么是网络字节序? | 大端(低地址高字节);所有发到网络的数据都必须是大端,用 htons/htonl 转换 |
| 为什么说网络通信本质是进程间通信? | 数据是给人用的,进程是人在系统中的代表;数据到主机后还要交给具体进程才算到达 |
| 学习任何一个协议要先问哪两个问题? | ① 如何解包?② 如何把有效载荷交付给上层? |