Linux网络编程基础(二):彻底理解局域网通信、MAC与IP地址、数据封装、端口号、网络字节序及Socket接口

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 | 《Linux的学习之旅》 ✨ 数据即知识,压缩即智能
文章目录
- Linux网络编程基础(二):彻底理解局域网通信、MAC与IP地址、数据封装、端口号、网络字节序及Socket接口
- 一、局域网中的两台主机能否直接通信?
-
- [1.1 局域网中可能存在多台主机](#1.1 局域网中可能存在多台主机)
- 二、认识MAC地址
-
- [2.1 什么是MAC地址?](#2.1 什么是MAC地址?)
- [2.2 MAC地址长什么样?](#2.2 MAC地址长什么样?)
- [2.3 MAC地址有什么作用?](#2.3 MAC地址有什么作用?)
- [2.4 MAC地址是绝对唯一且永远无法改变的吗?](#2.4 MAC地址是绝对唯一且永远无法改变的吗?)
- [2.5 Linux怎样查看MAC地址?](#2.5 Linux怎样查看MAC地址?)
- 三、局域网中的数据是怎样传输的?
-
- [3.1 先理解早期共享式以太网](#3.1 先理解早期共享式以太网)
- [3.2 什么是数据碰撞?](#3.2 什么是数据碰撞?)
- [3.3 什么是碰撞域?](#3.3 什么是碰撞域?)
- [3.4 为什么需要碰撞检测?](#3.4 为什么需要碰撞检测?)
- [3.5 现代交换式以太网也会这样碰撞吗?](#3.5 现代交换式以太网也会这样碰撞吗?)
- 四、主机怎样判断收到的帧是不是发给自己的?
-
- [4.1 交换机在其中做什么?](#4.1 交换机在其中做什么?)
- 五、同一个局域网中,两台主机怎样完成网络通信?
-
- [5.1 发送方:从上向下交付](#5.1 发送方:从上向下交付)
- [5.2 接收方:从下向上交付](#5.2 接收方:从下向上交付)
- [5.3 为什么要经过每一层?](#5.3 为什么要经过每一层?)
- 六、报头、有效载荷和报文
-
- [6.1 什么是报头?](#6.1 什么是报头?)
- [6.2 什么是有效载荷?](#6.2 什么是有效载荷?)
- [6.3 什么是报文?](#6.3 什么是报文?)
- 七、什么是数据封装?
-
- [7.1 从应用数据开始](#7.1 从应用数据开始)
- [7.2 传输层添加自己的协议头](#7.2 传输层添加自己的协议头)
- [7.3 网络层继续添加自己的协议头](#7.3 网络层继续添加自己的协议头)
- [7.4 数据链路层进一步封装](#7.4 数据链路层进一步封装)
- [7.5 为什么称为封装?](#7.5 为什么称为封装?)
- 八、什么是解包?
-
- [8.1 数据链路层处理](#8.1 数据链路层处理)
- [8.2 网络层处理](#8.2 网络层处理)
- [8.3 传输层处理](#8.3 传输层处理)
- [8.4 应用层处理](#8.4 应用层处理)
- 九、不同协议层的数据为什么有不同名称?
-
- [9.1 传输层](#9.1 传输层)
- [9.2 网络层](#9.2 网络层)
- [9.3 数据链路层](#9.3 数据链路层)
- [9.4 为什么需要不同名称?](#9.4 为什么需要不同名称?)
- 十、分用:解包以后怎样知道交给谁?
-
- [10.1 数据链路层向上分用](#10.1 数据链路层向上分用)
- [10.2 网络层向上分用](#10.2 网络层向上分用)
- [10.3 传输层继续向上交付](#10.3 传输层继续向上交付)
- [10.4 为什么分用很重要?](#10.4 为什么分用很重要?)
- 十一、跨网络通信为什么需要路由器?
-
- [11.1 为什么必须经过路由器?](#11.1 为什么必须经过路由器?)
- [11.2 什么是下一跳?](#11.2 什么是下一跳?)
- [11.3 先到达路由器,再到达远端主机](#11.3 先到达路由器,再到达远端主机)
- 十二、认识IP地址
-
- [12.1 IP协议的两个主要版本](#12.1 IP协议的两个主要版本)
- [12.2 IPv4地址有多长?](#12.2 IPv4地址有多长?)
- [12.3 什么是点分十进制?](#12.3 什么是点分十进制?)
- [12.4 IP地址有什么作用?](#12.4 IP地址有什么作用?)
- 十三、IP地址与MAC地址有什么区别?
-
- [13.1 IP地址关注什么?](#13.1 IP地址关注什么?)
- [13.2 MAC地址关注什么?](#13.2 MAC地址关注什么?)
- [13.3 跨路由器传输时,哪些信息会变化?](#13.3 跨路由器传输时,哪些信息会变化?)
- [13.4 IP地址真的绝对不变吗?](#13.4 IP地址真的绝对不变吗?)
- [13.5 为什么路由器需要重新封装?](#13.5 为什么路由器需要重新封装?)
- 十四、再次理解IP网络层存在的意义
- 十五、为什么数据到达主机还不够?
- 十六、认识端口号
-
- [16.1 什么是端口号?](#16.1 什么是端口号?)
- [16.2 端口号有什么作用?](#16.2 端口号有什么作用?)
- [16.3 端口号等于进程ID吗?](#16.3 端口号等于进程ID吗?)
- [16.4 为什么不直接使用PID进行网络通信?](#16.4 为什么不直接使用PID进行网络通信?)
- [16.5 一个进程能使用多个端口吗?](#16.5 一个进程能使用多个端口吗?)
- [16.6 一个端口只能属于一个进程吗?](#16.6 一个端口只能属于一个进程吗?)
- [16.7 常见端口范围](#16.7 常见端口范围)
- 十七、源端口号与目的端口号
-
- [17.1 源端口号表示什么?](#17.1 源端口号表示什么?)
- [17.2 目的端口号表示什么?](#17.2 目的端口号表示什么?)
- 十八、认识网络通信四元组
-
- [18.1 为什么需要源地址与目的地址?](#18.1 为什么需要源地址与目的地址?)
- [18.2 网络通信是否也是进程间通信?](#18.2 网络通信是否也是进程间通信?)
- 十九、什么是Socket?
-
- [19.1 Socket的基本含义](#19.1 Socket的基本含义)
- [19.2 Socket与IP地址、端口号有什么关系?](#19.2 Socket与IP地址、端口号有什么关系?)
- [19.3 Linux中的Socket与文件描述符](#19.3 Linux中的Socket与文件描述符)
- 二十、初步认识TCP协议
-
- [20.1 面向连接](#20.1 面向连接)
- [20.2 可靠传输](#20.2 可靠传输)
- [20.3 面向字节流](#20.3 面向字节流)
- 二十一、初步认识UDP协议
-
- [21.1 无连接](#21.1 无连接)
- [21.2 不可靠传输](#21.2 不可靠传输)
- [21.3 面向数据报](#21.3 面向数据报)
- [21.4 TCP与UDP的基本对比](#21.4 TCP与UDP的基本对比)
- 二十二、什么是网络字节序?
-
- [22.1 什么是大端?](#22.1 什么是大端?)
- [22.2 什么是小端?](#22.2 什么是小端?)
- [22.3 为什么字节序会影响网络通信?](#22.3 为什么字节序会影响网络通信?)
- [22.4 TCP/IP的网络字节序](#22.4 TCP/IP的网络字节序)
- 二十三、主机字节序与网络字节序的转换函数
-
- [23.1 如何记住函数名称?](#23.1 如何记住函数名称?)
- [23.2 四个转换函数](#23.2 四个转换函数)
- [23.3 为什么IP地址通常对应32位转换?](#23.3 为什么IP地址通常对应32位转换?)
- [23.4 为什么端口号通常对应16位转换?](#23.4 为什么端口号通常对应16位转换?)
- 二十四、网络字节序实验
- [二十五、Linux Socket编程常见接口](#二十五、Linux Socket编程常见接口)
- 二十六、socket():创建套接字
-
- [26.1 domain](#26.1 domain)
- [26.2 type](#26.2 type)
- [26.3 protocol](#26.3 protocol)
- [26.4 返回值](#26.4 返回值)
- 二十七、bind():绑定地址
-
- [27.1 为什么服务器通常需要bind?](#27.1 为什么服务器通常需要bind?)
- 二十八、listen():监听连接请求
- 二十九、accept():接收连接
- 三十、connect():发起连接
- 三十二、为什么Socket接口需要sockaddr?
- 三十三、sockaddr结构
- 三十四、sockaddr_in:IPv4地址结构
-
- [34.1 为什么需要sockaddr_in?](#34.1 为什么需要sockaddr_in?)
- [34.2 sin_family](#34.2 sin_family)
- [34.3 sin_port](#34.3 sin_port)
- [34.4 sin_addr](#34.4 sin_addr)
- [34.5 sin_zero](#34.5 sin_zero)
- 三十五、in_addr:IPv4地址的内部表示
- 三十六、一个sockaddr_in初始化示例
- 三十七、为什么bind和connect要进行类型转换?
-
- [37.1 为什么不直接使用sockaddr_in参数?](#37.1 为什么不直接使用sockaddr_in参数?)
- [37.2 通用接口与具体数据类型](#37.2 通用接口与具体数据类型)
- 三十八、从网络原理到Linux编程:完整知识链路
-
- [38.1 从网络协议角度](#38.1 从网络协议角度)
- [38.2 从应用程序角度](#38.2 从应用程序角度)
- 三十九、常见问题总结
-
- [39.1 MAC地址和IP地址是一回事吗?](#39.1 MAC地址和IP地址是一回事吗?)
- [39.2 只要知道目标MAC,就一定能跨互联网发送数据吗?](#39.2 只要知道目标MAC,就一定能跨互联网发送数据吗?)
- [39.3 为什么交换机和路由器的作用不同?](#39.3 为什么交换机和路由器的作用不同?)
- [39.4 数据从应用程序发出以后,是否直接进入网线?](#39.4 数据从应用程序发出以后,是否直接进入网线?)
- [39.5 什么是封装?](#39.5 什么是封装?)
- [39.6 什么是解包?](#39.6 什么是解包?)
- [39.7 IP地址在经过路由器时一定不变吗?](#39.7 IP地址在经过路由器时一定不变吗?)
- [39.8 MAC地址在每一跳都必然相同吗?](#39.8 MAC地址在每一跳都必然相同吗?)
- [39.9 IP地址为什么不能替代端口号?](#39.9 IP地址为什么不能替代端口号?)
- [39.10 端口号就是PID吗?](#39.10 端口号就是PID吗?)
- [39.11 Socket是不是简单等于IP+Port?](#39.11 Socket是不是简单等于IP+Port?)
- [39.12 TCP和UDP都属于传输层吗?](#39.12 TCP和UDP都属于传输层吗?)
- [39.13 为什么网络字节序通常使用大端?](#39.13 为什么网络字节序通常使用大端?)
- [39.14 bind和connect为什么使用sockaddr?](#39.14 bind和connect为什么使用sockaddr?)
- [39.15 sockaddr_in和in_addr有什么区别?](#39.15 sockaddr_in和in_addr有什么区别?)
- 四十、总结
前言:一台计算机的数据究竟怎样到达另一台计算机?
在上一篇文章中,我们已经建立了计算机网络的基本认识:
text
计算机之间需要交换数据
|
v
通信双方需要共同的协议
|
v
复杂协议采用分层设计
|
v
TCP/IP协议族
也已经认识了 TCP/IP 五层模型:
text
应用层
传输层
网络层
数据链路层
物理层
但是,我们仍然没有解决网络通信中最重要的实际问题:
计算机A发送的一条消息,究竟怎样到达计算机B?
例如:
text
计算机A发送:"你好"
计算机B接收:"你好"
看起来只是传递两个汉字,但在实际网络通信过程中,需要考虑许多问题:
- 如果两台计算机在同一个局域网中,怎样识别目标设备?
- 为什么网卡需要 MAC 地址?
- 为什么应用数据必须经过多层协议处理?
- 什么是报头、有效载荷、封装和解包?
- 如果通信双方位于不同网络中,路由器怎样进行转发?
- 已经知道目标计算机的 IP 地址,为什么还需要端口号?
- 什么是 Socket?
- 为什么网络协议要规定字节序?
- Linux 提供了哪些网络编程接口?
本文按照从网络通信原理到 Linux 网络编程接口的顺序,逐步解决这些问题。
一、局域网中的两台主机能否直接通信?
首先考虑最简单的情况:
text
计算机A与计算机B位于同一个局域网
例如:
text
计算机A ──┐
│
LAN
│
计算机B ──┘
假设它们已经正确连接,并配置了可以通信的网络环境。
那么两台计算机可以通过局域网交换数据。
但接下来需要思考一个问题:
计算机A怎样知道自己应该把数据交给计算机B,而不是局域网中的其他主机?
1.1 局域网中可能存在多台主机
例如:
text
主机A
主机B
主机C
主机D
都连接在同一个局域网中。
如果主机A需要给主机C发送数据,就必须有办法表示:
text
这份数据应该交给主机C
因此,需要在数据链路层识别相应的网络节点。
这就引出了:
MAC地址。
二、认识MAC地址
2.1 什么是MAC地址?
MAC 是:
text
Media Access Control
即介质访问控制。
MAC 地址可以用于标识数据链路层中的相应网络接口或节点。
在常见的以太网中,MAC 地址通常是一个:
text
48位
的标识。
也就是:
48 ÷ 8 = 6 字节 48\div8=6\text{字节} 48÷8=6字节
2.2 MAC地址长什么样?
例如:
text
08:00:27:03:fb:19
可以分成六组:
text
08
00
27
03
fb
19
每组由两个十六进制数字表示一个字节。
因此:
text
6组 × 8位 = 48位
2.3 MAC地址有什么作用?
假设:
text
主机A的MAC地址:MAC_A
主机B的MAC地址:MAC_B
当主机A向主机B发送以太网帧时,可以在相应帧头中填写:
text
源MAC:MAC_A
目的MAC:MAC_B
这样相关网络设备和接收主机就能够根据数据链路层地址处理这份数据。
2.4 MAC地址是绝对唯一且永远无法改变的吗?
不是。
网卡通常具有厂商分配的硬件地址,但很多系统也允许配置或修改当前使用的 MAC 地址。
例如虚拟机和虚拟网络接口,也可能使用软件生成的 MAC 地址。
因此不能认为:
text
MAC地址在任何情况下都不可修改
在这里重点理解:
MAC地址在相应数据链路层网络中,为节点识别与帧转发提供重要依据。
2.5 Linux怎样查看MAC地址?
可以执行:
bash
ip link show
或者:
bash
ip addr
例如:
text
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP>
link/ether 00:0c:29:12:34:56
其中:
text
link/ether
后面的地址就是对应网络接口使用的以太网 MAC 地址。
Windows 可以使用:
powershell
ipconfig /all
查看网络适配器相关信息。

三、局域网中的数据是怎样传输的?
3.1 先理解早期共享式以太网
为了理解局域网通信,可以先使用一种比较简单的共享通信介质模型。
例如:
text
主机A ──┬── 主机B
|
├── 主机C
|
└── 主机D
在传统共享式、半双工以太网中,多台设备可能共享同一通信介质。
如果两个设备在同一碰撞域中同时发送信号,就可能发生碰撞。
3.2 什么是数据碰撞?
假设:
text
主机A正在发送
同时:
text
主机B也开始发送
由于它们共享相应通信介质,发送信号可能互相干扰。
接收方无法正确识别受到碰撞影响的帧。
这就是:
碰撞(Collision)。
3.3 什么是碰撞域?
可以把碰撞域理解为:
在共享介质或相应半双工网络中,可能因为同时发送而发生碰撞的一组网络接口所在的范围。
例如,传统同轴电缆共享式以太网可以形成一个碰撞域。
传统集线器连接的半双工以太网,也可以用类似方式理解。
3.4 为什么需要碰撞检测?
在经典共享式以太网中,可以通过:
text
CSMA/CD
即载波侦听多路访问和碰撞检测机制,协调介质访问。
可以先理解为:
text
准备发送
|
v
检查介质是否空闲
|
v
发送数据
|
v
检测是否发生碰撞
|
+-- 没有碰撞:完成发送
|
+-- 发生碰撞:按照规则重新尝试
这只是帮助理解经典以太网共享介质的基本工作方式。
3.5 现代交换式以太网也会这样碰撞吗?
现代常见的交换式全双工以太网,通常不再使用共享式以太网中的 CSMA/CD 碰撞竞争方式。
交换机可以将不同端口上的通信进行相应隔离。
因此,不能将:
text
同一时间只允许一台设备发送
当作所有现代以太网的普遍规律。
这一部分主要帮助理解:底层通信介质的组织方式,会影响多个网络节点怎样共享传输资源。

四、主机怎样判断收到的帧是不是发给自己的?
假设局域网中有:
text
主机A
主机B
主机C
主机A发送一份以太网帧。
目的 MAC 地址为:
text
MAC_B
那么接收设备可以根据帧中的目的 MAC 地址判断是否应该处理该帧。
在简化模型中:
text
主机A发送帧
|
v
局域网进行传输
|
v
主机B识别目的MAC
|
v
交给后续协议处理
如果一个普通单播帧的目的 MAC 与某接收接口不匹配,该接口通常不会将其作为发给自己的普通帧继续处理。
当然,局域网还存在广播、多播等情况。
这里先重点理解:
数据链路层需要根据相应链路地址,识别和交付网络帧。
4.1 交换机在其中做什么?
在典型交换式以太网中,交换机可以根据 MAC 地址信息进行转发。
例如:
text
主机A ──┐
│
交换机 ── 主机B
│
主机C ──┘
主机A向主机B发送数据时,交换机会根据其转发信息决定从哪个端口发送相关帧。
因此:
交换机的典型工作重点是数据链路层的帧转发。
五、同一个局域网中,两台主机怎样完成网络通信?
现在我们已经认识了 MAC 地址。
接下来,将它与 TCP/IP 分层模型联系起来。
假设:
text
主机A向主机B发送:"你好"
两台主机都具有相应的协议栈:
text
应用层
传输层
网络层
数据链路层
物理层
从应用程序角度:
text
发送方希望发送一条消息
但实际传输时,数据需要经过相应协议层的处理。
5.1 发送方:从上向下交付
可以理解为:
text
主机A应用层
|
v
主机A传输层
|
v
主机A网络层
|
v
主机A数据链路层
|
v
通过网络介质发送
5.2 接收方:从下向上交付
主机B收到相应信号以后:
text
主机B数据链路层
|
v
主机B网络层
|
v
主机B传输层
|
v
主机B应用层
|
v
应用程序获得消息
因此:
发送方通常逐层向下处理数据,接收方则逐层向上处理数据。
5.3 为什么要经过每一层?
因为每一层负责解决不同的问题。
例如:
text
应用层:
怎样解释业务消息?
传输层:
怎样提供传输服务?
网络层:
怎样根据网络地址处理数据?
数据链路层:
怎样在相应链路上交付数据帧?
因此,应用程序不能只考虑自己的消息内容。
在实际通信过程中,还需要其他层提供配合。
六、报头、有效载荷和报文
前面已经知道,每一层都可能需要自己的协议字段。
现在需要明确三个概念:
text
报头
有效载荷
报文
6.1 什么是报头?
报头(Header)保存某一层协议需要的控制信息。
例如:
text
源地址
目的地址
协议类型
长度
这些都是常见的协议字段类别。
不同协议拥有各自的报头格式。
6.2 什么是有效载荷?
有效载荷(Payload)是相对于当前协议层而言,需要承载的数据内容。
例如,某层协议接收到上层交付的数据。
它不需要将上层数据的每个字段都当成自己的控制信息。
而是可以把这部分内容作为:
text
Payload
进行传递。
6.3 什么是报文?
从简化角度可以理解为:
text
报文 = 报头 + 有效载荷
例如:
text
+----------------+----------------+
| 报头 | 有效载荷 |
+----------------+----------------+
但需要注意,一些协议除了报头和有效载荷,还可能在末尾添加尾部信息。
例如以太网帧中通常具有相应的帧校验字段。
因此,上面是帮助理解的基本抽象,而不是所有协议报文的完整格式。
七、什么是数据封装?
7.1 从应用数据开始
假设应用层准备:
text
你好
那么应用程序首先产生一份业务数据。
例如:
text
[应用数据]
7.2 传输层添加自己的协议头
传输层需要根据对应协议组织信息。
可以概念化为:
text
[传输层报头][应用数据]
这样形成传输层对应的数据单元。
7.3 网络层继续添加自己的协议头
网络层也需要自己的控制信息。
例如:
text
[IP报头][传输层报头][应用数据]
这可以理解为一个网络层 IP 数据报的结构示意。
7.4 数据链路层进一步封装
以以太网为例:
text
[以太网首部][IP报头][传输层报头][应用数据][以太网尾部]
这就形成了用于以太网传输的帧。
7.5 为什么称为封装?
因为每向下一层,就把上一层的数据作为当前层的有效载荷,再添加本层需要的协议控制信息。
可以理解为:
text
应用数据
|
v
传输层封装
|
v
网络层封装
|
v
链路层封装
其本质是:
不同协议层按照自己的规则,为上层数据附加当前层所需的协议信息。

八、什么是解包?
有封装,就有对应的解包过程。
假设主机B收到了一个以太网帧:
text
[以太网首部][IP报头][TCP报头][应用数据][以太网尾部]
接收方需要从底层向上处理。
8.1 数据链路层处理
首先检查并解释以太网帧相关信息。
例如:
text
目的MAC
帧类型
帧校验
在确认能够继续处理后,将相应有效载荷交给上层协议。
8.2 网络层处理
网络层取得:
text
[IP报头][TCP报头][应用数据]
然后根据 IP 协议报头进行对应处理。
对于需要交给本机上层协议的数据,将其有效载荷继续向上传递。
8.3 传输层处理
传输层取得:
text
[TCP报头][应用数据]
根据 TCP 协议规则处理。
随后应用数据通过对应接口交付给应用程序。
8.4 应用层处理
最终应用程序获得:
text
你好
整个过程可以概括为:
text
收到以太网帧
|
v
处理以太网信息
|
v
处理IP报头
|
v
处理TCP报头
|
v
交付应用数据
因此:
封装是逐层向下添加协议信息,解包是逐层向上处理并移除相应层的封装信息。
九、不同协议层的数据为什么有不同名称?
在网络学习中,经常看到:
text
Segment
Datagram
Frame
它们用于描述不同层的数据单元。
9.1 传输层
例如 TCP 的数据单元通常称为:
text
Segment
即段。
UDP 的数据单元通常称为:
text
Datagram
即数据报。
9.2 网络层
IP 层的数据单元通常称为:
text
IP Datagram
即 IP 数据报。
9.3 数据链路层
数据链路层的数据单元通常称为:
text
Frame
即帧。
例如:
text
Ethernet Frame
表示以太网帧。
9.4 为什么需要不同名称?
因为每一层组织的数据格式不同。
例如:
text
TCP段
与:
text
IP数据报
并不是同一种协议格式。
通过不同名称,可以明确我们讨论的是哪一层的数据。
十、分用:解包以后怎样知道交给谁?
现在思考一个问题:
假设数据链路层已经取出了以太网帧中的有效载荷。
那么它怎样知道:
text
这份数据应该交给IP协议?
还是交给其他协议?
答案是:
根据对应协议中的类型字段或其他规定的识别信息进行交付。
这就是:
分用(Demultiplexing)。
10.1 数据链路层向上分用
例如,以太网帧头中的类型信息可以帮助识别其承载的上层协议。
在常见场景中:
text
Ethernet
|
+-- IPv4
|
+-- ARP
不同类型的数据交给相应处理模块。
这里先认识 ARP 的名称,不深入它的工作原理。
10.2 网络层向上分用
IP 协议中也具有识别上层协议的信息。
例如:
text
IP
|
+-- TCP
|
+-- UDP
|
+-- ICMP
这样网络层就能够根据相应字段选择接收方的上层协议处理模块。
10.3 传输层继续向上交付
传输层需要根据自身协议的规则,将相应数据交付给对应的应用通信端点。
后面学习端口号时,就能理解这一过程。
10.4 为什么分用很重要?
因为一台主机可能同时支持很多网络协议。
例如:
text
TCP
UDP
ICMP
如果收到的数据无法识别所属协议,就无法正确执行后续处理。
因此:
每层不仅需要知道如何解释自己的报头,还需要知道怎样把有效载荷交给正确的上层协议。
学习一种协议时,可以优先关注两个问题:
第一:
text
该协议怎样识别和处理自己的报头?
第二:
text
该协议怎样决定将有效载荷交给哪个上层协议?
这两个问题能够帮助我们理解大多数网络协议的基本组织方式。

十一、跨网络通信为什么需要路由器?
前面主要考虑:
text
两台主机位于同一个局域网
的情况。
但实际网络中,两台主机可能位于不同的网络。
例如:
text
主机A
|
LAN A
|
路由器
|
LAN B
|
主机B
此时不能简单地认为:
text
主机A直接把一个以太网帧发送到远端主机B
因为以太网帧主要服务于所在链路的数据传递。
如果目的主机位于其他网络,就需要通过相应路由设备进行转发。
11.1 为什么必须经过路由器?
原因是:
text
源主机与目的主机位于不同网络
而路由器能够根据网络层地址和路由信息,决定将数据向哪个下一跳转发。
11.2 什么是下一跳?
例如:
text
主机A → 路由器1 → 路由器2 → 主机B
对于主机A:
text
下一跳:路由器1
对于路由器1:
text
下一跳:路由器2
对于路由器2:
text
下一跳:目标网络中的目的主机或其他转发设备
因此,一次跨网络通信可以由多个阶段组成。
11.3 先到达路由器,再到达远端主机
可以这样理解:
text
最终目标是主机B
但:
text
当前链路上的交付目标可能是路由器
这两个目标并不是同一个概念。
所以我们需要区分:
text
最终目标地址
和:
text
当前链路上的下一跳地址
这就引出了 IP 地址与 MAC 地址之间的重要区别。
十二、认识IP地址
12.1 IP协议的两个主要版本
IP 协议包括:
text
IPv4
IPv6
本篇先使用 IPv4 理解网络地址和路由转发。
12.2 IPv4地址有多长?
IPv4 地址是:
text
32位
也就是:
text
4字节
例如:
text
192.168.1.10
它使用四组十进制数字表示。
每组对应一个字节:
text
192
168
1
10
取值范围是:
text
0 ~ 255
因为:
2 8 = 256 2^8=256 28=256
12.3 什么是点分十进制?
例如:
text
192.168.1.10
四个数字通过点分隔。
这种表示方式称为:
点分十进制。
它比直接编写32位二进制地址更加方便阅读。
12.4 IP地址有什么作用?
在简化的网络模型中,可以把 IP 地址理解为:
用于在IP网络中标识相应网络接口并支持寻址、转发的地址。
因此,网络层可以根据目的 IP 地址进行路由处理。
例如:
text
主机A的IP:192.168.1.10
主机B的IP:192.168.2.20
如果两者处于不同的 IP 网络,就可能需要路由器转发。
十三、IP地址与MAC地址有什么区别?
这是理解跨网络传输的关键。
13.1 IP地址关注什么?
从概念上看:
text
IP地址
用于帮助网络层表示源与目的,并进行跨网络寻址和转发。
可以把目的 IP 理解为:
长期目标地址。
例如:
text
目标主机B的IP地址
决定了网络层希望最终将数据送向哪里。
13.2 MAC地址关注什么?
MAC 地址主要用于当前数据链路层网络中的节点识别与帧交付。
可以把目的 MAC 理解为:
当前链路上的交付目标。
例如:
text
主机A到路由器1
对应以太网帧中的目的 MAC 可以是路由器1相关接口的 MAC 地址。
13.3 跨路由器传输时,哪些信息会变化?
假设:
text
主机A
|
v
路由器1
|
v
路由器2
|
v
主机B
在一个没有进行网络地址转换的普通 IPv4 转发场景中:
text
源IP与目的IP通常保持不变
但不同链路上的:
text
以太网源MAC
以太网目的MAC
通常会随下一跳发生变化。
例如:
| 传输阶段 | 源MAC | 目的MAC |
|---|---|---|
| 主机A → 路由器1 | MAC_A | MAC_R1 |
| 路由器1 → 路由器2 | MAC_R1' | MAC_R2 |
| 路由器2 → 主机B | MAC_R2' | MAC_B |
这里:
text
MAC_R1'
表示路由器1用于下一条链路的接口 MAC,不一定与进入路由器时的接口相同。
13.4 IP地址真的绝对不变吗?
不是。
上面的结论是一个基础转发模型。
实际网络中存在:
text
NAT
等网络地址转换机制,可能修改 IP 地址字段。
IPv4 报文在路由转发过程中也会修改 TTL 等字段。
因此更准确的表述是:
在不涉及地址转换的普通IP路由转发过程中,源IP和目的IP通常不变;每经过一条新的以太网链路,相应的链路层封装则需要重新组织。
13.5 为什么路由器需要重新封装?
路由器从某个接口收到数据后,可以处理原来的链路层封装,并根据路由信息决定下一跳。
向新的以太网链路发送时,需要使用对应的:
text
源MAC
目的MAC
重新组织新的以太网帧。
这就体现了:
text
网络层负责跨网络寻址和转发
而:
text
数据链路层负责相应链路中的帧交付
两者职责不同。

十四、再次理解IP网络层存在的意义
现在可以思考一个更大的问题。
如果不同地区使用不同类型的底层网络:
text
以太网
无线局域网
其他链路技术
那么上层应用程序是不是需要分别掌握每一种底层网络技术?
如果所有应用程序都要直接处理各种底层网络差异,程序会非常复杂。
因此,IP 网络层具有重要价值:
它通过统一的网络层寻址与转发抽象,帮助不同类型的底层网络实现互联。
可以理解为:
text
应用程序
|
v
传输层
|
v
IP网络层
|
+-- 以太网
|
+-- 无线局域网
|
+-- 其他适用链路
不同底层网络技术可以通过适当的链路接入与路由机制,共同支持 IP 数据传递。
这就是互联网能够连接大量不同类型网络的重要基础。
十五、为什么数据到达主机还不够?
到这里,我们已经初步解决:
怎样把数据发送到目标主机?
但网络通信的任务还没有完全结束。
假设一台计算机同时运行:
text
聊天程序
浏览器
下载程序
它们都是在同一个操作系统中运行的不同应用程序。
如果一份网络数据已经到达这台计算机,那么应该交给哪个程序?
例如:
text
收到聊天消息
显然应该交给对应的聊天程序。
而:
text
收到浏览器请求对应的数据
应该交给相应的网络通信端点。
因此:
数据到达目标主机,只完成了网络传输中的一部分工作,还需要交付给正确的应用程序。
这就需要端口号。
十六、认识端口号
16.1 什么是端口号?
端口号:
text
Port
属于传输层的重要概念。
TCP和UDP都具有端口号字段。
端口号通常是:
text
16位无符号整数
也就是:
text
2字节
因此范围是:
0 ∼ 2 16 − 1 0\sim 2^{16}-1 0∼216−1
即:
text
0 ~ 65535
16.2 端口号有什么作用?
例如:
text
主机A运行三个网络程序
分别提供:
text
聊天服务
网页服务
其他服务
为了把收到的数据交给正确的通信端点,操作系统需要进一步区分这些不同服务。
因此可以使用端口号参与定位。
例如:
text
192.168.1.10:8080
其中:
text
192.168.1.10
表示 IP 地址。
text
8080
表示端口号。
可以将:
text
IP地址 + 端口号
理解为某个协议环境下的网络通信端点地址。
16.3 端口号等于进程ID吗?
不是。
进程 ID:
text
PID
属于操作系统的进程管理标识。
端口号:
text
Port
属于网络传输协议的寻址机制。
二者解决的问题不同。
例如:
text
PID = 12345
表示某个进程在操作系统中的标识。
而:
text
Port = 8080
表示某个网络通信端点使用的端口编号。
16.4 为什么不直接使用PID进行网络通信?
因为 PID 是操作系统内部的进程管理概念。
不同主机分别维护自己的 PID 空间。
而网络通信需要相对独立的传输层寻址机制。
如果直接把网络通信设计成依赖各个操作系统的 PID,可能使网络协议与具体操作系统的进程管理强烈耦合。
因此需要使用端口号等传输层信息定位相应通信端点。
16.5 一个进程能使用多个端口吗?
可以。
例如一个服务器进程可能同时提供多个网络服务。
它可以创建多个 Socket,并使用不同端口。
16.6 一个端口只能属于一个进程吗?
不能将其作为绝对规则。
在基础学习中,可以先理解:
text
特定的IP地址与端口组合,需要遵守相应绑定规则。
但是实际 Socket 绑定还受到:
text
传输协议
本地地址
套接字状态
地址复用选项
等因素影响。
因此不能简单认为:
text
整个操作系统中一个端口号永远只能由一个进程使用
例如,TCP和UDP可以分别使用相同的数字端口;特定条件下还存在相应的地址或端口复用机制。
16.7 常见端口范围
一些经常遇到的服务端口:
| 端口 | 典型服务 |
|---|---|
| 21 | FTP控制连接 |
| 22 | SSH |
| 25 | SMTP |
| 53 | DNS |
| 80 | HTTP |
| 443 | HTTPS |
需要说明,这些是常用或注册的服务端口,并不意味着相应服务只能使用这些端口。
从 IANA 注册分类角度,端口范围通常分为:
text
0 ~ 1023:系统/知名端口
1024 ~ 49151:注册端口
49152 ~ 65535:动态/私有端口
实际操作系统选择临时端口时,使用的具体范围可能与上述注册分类不同。

十七、源端口号与目的端口号
假设:
text
客户端
希望连接:
text
服务器
例如:
text
客户端IP:192.168.1.10
客户端端口:50000
服务器:
text
服务器IP:192.168.1.20
服务器端口:8080
那么从客户端发送到服务器的数据可以具有:
text
源IP:192.168.1.10
源端口:50000
目的IP:192.168.1.20
目的端口:8080
17.1 源端口号表示什么?
源端口号表示发送端使用的传输层端口。
例如:
text
50000
17.2 目的端口号表示什么?
目的端口号表示希望到达的接收端口。
例如:
text
8080
因此可以简单记忆:
text
源IP、源Port:
数据从哪里来
目的IP、目的Port:
数据希望到哪里去
十八、认识网络通信四元组
在 TCP 网络编程中,经常需要同时考虑:
text
源IP
源Port
目的IP
目的Port
它们组合在一起:
text
{srcIP, srcPort, dstIP, dstPort}
称为:
四元组。
例如:
text
{
192.168.1.10,
50000,
192.168.1.20,
8080
}
在给定传输协议和网络环境下,可以用相应地址组合区分网络通信。
对于常见 TCP 连接,四元组是区分连接的重要依据。
而在更一般的描述中,还可以把:
text
TCP或UDP等传输协议
加入标识,形成包含协议类型的五元组。
18.1 为什么需要源地址与目的地址?
因为:
text
接收方需要知道数据从哪里来
以及:
text
发送方需要知道数据要发给谁
例如服务器可能同时服务多个客户端。
每个客户端都有不同的地址组合。
因此服务器需要区分不同通信对象。
18.2 网络通信是否也是进程间通信?
从应用程序角度:
网络通信可以看作跨主机的进程间通信。
例如:
text
计算机A上的进程A
|
v
网络协议栈
|
v
计算机网络
|
v
计算机B上的网络协议栈
|
v
进程B
这与以前学习的:
text
管道
共享内存
消息队列
在目标上具有相似之处:
都是为了让不同执行环境中的程序交换信息。
但网络通信还需要解决:
text
跨主机地址
跨网络传输
不同主机的协议兼容
等问题。
十九、什么是Socket?
19.1 Socket的基本含义
Socket 通常翻译为:
套接字。
在网络编程中,可以将 Socket 理解为:
操作系统向应用程序提供的一种通信端点抽象。
应用程序通过 Socket 与操作系统的网络协议栈交互。
例如:
text
应用程序
|
v
Socket
|
v
操作系统网络协议栈
|
v
网络
19.2 Socket与IP地址、端口号有什么关系?
对于 TCP/IP Socket,可以使用相应的:
text
IP地址
端口号
描述本地或远端通信地址。
例如:
text
192.168.1.20:8080
经常用来表示一个网络服务的地址。
但是从实现角度看:
Socket并不只是IP地址加Port的简单字符串。
Socket 还是操作系统管理的通信对象,可以关联协议类型、通信状态、发送和接收缓冲区等信息。
19.3 Linux中的Socket与文件描述符
在 Linux 中,创建 Socket 可以获得:
c
int sockfd;
这样的文件描述符。
这与之前学习:
c
open()
获取文件描述符的方式存在相似性。
因此网络编程可以利用文件描述符及相关接口完成通信操作。
这也说明:
Linux网络编程与之前学习的文件IO、系统调用知识具有直接联系。

二十、初步认识TCP协议
TCP 全称:
text
Transmission Control Protocol
中文为:
传输控制协议。
它属于传输层协议。
现阶段先掌握三个特点。
20.1 面向连接
TCP 通信通常需要建立相应连接,然后进行数据交换。
例如:
text
客户端
|
v
建立连接
|
v
交换数据
20.2 可靠传输
TCP 提供可靠、有序的字节流传输语义。
可以帮助通信双方处理丢失、乱序等传输问题。
但这并不代表应用程序永远不会遇到连接失败或网络故障。
20.3 面向字节流
TCP 向应用程序提供的是:
text
字节流
而不是天然保留每次应用写入操作边界的消息队列。
这一特点会在后面学习 TCP 数据传输时进一步理解。
二十一、初步认识UDP协议
UDP 全称:
text
User Datagram Protocol
中文为:
用户数据报协议。
同样属于传输层协议。
21.1 无连接
UDP 不要求像 TCP 那样,在发送数据之前先建立 TCP 连接。
21.2 不可靠传输
UDP 本身不保证:
text
数据一定到达
数据一定按顺序到达
数据不会重复
这些传输性质需要根据具体应用需求另行考虑。
21.3 面向数据报
UDP 保留数据报的基本消息边界。
例如应用程序发送一个 UDP 数据报,接收方以数据报为单位进行接收。
但如果接收缓冲区不足,也可能发生截断等情况。
21.4 TCP与UDP的基本对比
| 特点 | TCP | UDP |
|---|---|---|
| 所属层 | 传输层 | 传输层 |
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 提供可靠字节流服务 | 不自行保证可靠交付 |
| 数据形式 | 字节流 | 数据报 |
当前只需先建立这些基本认识,不进一步展开 TCP 与 UDP 的内部协议机制。
二十二、什么是网络字节序?
现在进入网络编程中另一个重要问题:
不同计算机怎样以相同顺序解释多字节数据?
例如需要传输一个32位整数:
text
0x12345678
它由四个字节组成:
text
12
34
56
78
但不同计算机可能采用不同的字节存储顺序。
这就需要认识:
text
大端字节序
小端字节序
22.1 什么是大端?
大端(Big-Endian)表示:
多字节数值的高有效字节存放在较低的内存地址。
例如:
text
0x12345678
在大端机器中的排列:
text
低地址 高地址
12 34 56 78
22.2 什么是小端?
小端(Little-Endian)表示:
多字节数值的低有效字节存放在较低的内存地址。
例如:
text
0x12345678
在小端机器中的排列:
text
低地址 高地址
78 56 34 12
22.3 为什么字节序会影响网络通信?
假设:
text
计算机A是小端机器
计算机B是大端机器
如果直接将本机内存中某个32位整数的四个字节发送过去,而双方没有统一规定,就可能产生不同解释。
因此需要一种统一规则。
22.4 TCP/IP的网络字节序
TCP/IP 中许多标准多字节协议字段规定使用:
大端字节序(Big-Endian)。
也称为:
text
Network Byte Order
即网络字节序。
需要注意:
这表示相应协议字段必须按照协议指定的字节顺序组织。
并不是说任何应用程序发到网络上的任意数据,都必须无条件转换为大端。
例如应用层字符串通常按照相应字符编码组织,而不需要对每个字符执行整数大小端转换。
二十三、主机字节序与网络字节序的转换函数
Linux网络编程中提供:
c
htonl()
htons()
ntohl()
ntohs()
这些函数。
需要包含:
c
#include <arpa/inet.h>
23.1 如何记住函数名称?
可以将名称拆分:
text
h = host
n = network
l = 32位整数
s = 16位整数
例如:
text
htonl
可以理解为:
text
Host To Network Long
即将32位整数从主机字节序转换为网络字节序。
23.2 四个转换函数
c
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
| 函数 | 含义 |
|---|---|
htonl |
32位主机序 → 网络序 |
htons |
16位主机序 → 网络序 |
ntohl |
32位网络序 → 主机序 |
ntohs |
16位网络序 → 主机序 |
23.3 为什么IP地址通常对应32位转换?
因为 IPv4 地址占:
text
32位
因此相关32位整数表示可能需要使用:
c
htonl()
或:
c
ntohl()
进行转换。
23.4 为什么端口号通常对应16位转换?
因为端口号占:
text
16位
因此通常使用:
c
htons()
或:
c
ntohs()
转换。
二十四、网络字节序实验
下面观察:
text
0x1234
在网络字节序下的实际字节排列。
endian.c:
c
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
int main(void)
{
uint16_t host = 0x1234;
uint16_t network = htons(host);
const unsigned char* bytes =
(const unsigned char*)&network;
printf("Network bytes: %02x %02x\n",
bytes[0],
bytes[1]);
uint16_t restored = ntohs(network);
printf("Restored value: 0x%04x\n",
restored);
return 0;
}
编译:
bash
gcc endian.c -o endian
运行:
bash
./endian
预期输出:
text
Network bytes: 12 34
Restored value: 0x1234
这里需要特别理解:
c
htons()
返回的仍然是一个:
c
uint16_t
类型的整数值。
但这个整数在当前主机内存中的字节排列,已经适合按网络字节序处理。
如果主机本身就是大端字节序,转换可能不需要改变字节排列。
如果主机是小端字节序,则需要进行相应转换。

二十五、Linux Socket编程常见接口
现在已经认识:
text
IP地址
端口号
Socket
网络字节序
接下来,就可以初步认识 Linux 网络编程接口。
这些接口多数属于系统提供的 Socket API。
本节先认识函数用途,不急着完整实现 TCP 服务器。
二十六、socket():创建套接字
函数原型:
c
#include <sys/socket.h>
int socket(
int domain,
int type,
int protocol
);
26.1 domain
用于指定地址族。
例如:
c
AF_INET
表示 IPv4。
c
AF_INET6
表示 IPv6。
26.2 type
用于指定 Socket 类型。
例如:
c
SOCK_STREAM
常用于 TCP 字节流通信。
c
SOCK_DGRAM
常用于 UDP 数据报通信。
26.3 protocol
用于指定具体协议。
如果使用:
c
0
通常表示由系统根据地址族与 Socket 类型选择适当的默认协议。
26.4 返回值
成功时返回:
text
Socket文件描述符
失败时返回:
text
-1
例如:
c
int sockfd = socket(
AF_INET,
SOCK_STREAM,
0
);
表示创建一个用于 IPv4 字节流通信的 Socket。
二十七、bind():绑定地址
函数原型:
c
int bind(
int sockfd,
const struct sockaddr* addr,
socklen_t addrlen
);
作用:
将 Socket 与指定的本地通信地址关联。
例如服务器可以通过 bind() 指定要使用的本地 IP 地址和端口号。
27.1 为什么服务器通常需要bind?
假设服务器准备提供:
text
192.168.1.20:8080
这个网络服务地址。
那么服务器需要向操作系统说明:
text
我希望使用这个本地地址和端口接收相应通信
这就是 bind() 的典型用途。
二十八、listen():监听连接请求
函数原型:
c
int listen(
int sockfd,
int backlog
);
作用:
让相应的流式 Socket 进入监听状态。
常用于 TCP 服务器。
其中:
c
backlog
用于指定监听队列相关的积压参数。
当前阶段先知道它与等待连接的队列管理有关,不进一步展开细节。
二十九、accept():接收连接
函数原型:
c
int accept(
int sockfd,
struct sockaddr* addr,
socklen_t* addrlen
);
典型用途:
从监听 Socket 上接受一个连接,并获得用于该连接通信的新 Socket 文件描述符。
例如:
text
服务器监听端口8080
|
v
客户端请求连接
|
v
服务器accept
|
v
获得通信Socket
这里先区分两个概念:
text
监听Socket
主要负责接受新的连接。
text
连接Socket
主要负责与已建立连接的对应通信对象交换数据。
三十、connect():发起连接
函数原型:
c
int connect(
int sockfd,
const struct sockaddr* addr,
socklen_t addrlen
);
典型用途:
让客户端 Socket 发起与指定远端地址的连接操作。
例如客户端希望访问:
text
192.168.1.20:8080
就需要准备相应的目标地址信息。
对于 TCP,connect() 用于发起连接建立。
三十二、为什么Socket接口需要sockaddr?
现在观察:
c
bind()
和:
c
connect()
的参数。
它们都使用:
c
const struct sockaddr*
这样的通用地址参数。
但不同通信协议需要不同的地址格式。
例如:
text
IPv4需要IP地址和端口
IPv6需要IPv6地址和端口
Unix Domain Socket需要本地地址信息
如果每种地址都使用完全独立的接口,就会增加接口设计复杂度。
因此,Socket API 使用通用地址结构接口,再通过地址族与具体结构体表示不同地址格式。
这也是一种接口抽象的思想。
三十三、sockaddr结构
在 Linux 环境中,可以看到类似结构:
c
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
这是常见的通用地址结构表示。
其中:
c
sa_family
用于标识地址族。
例如:
c
AF_INET
AF_INET6
AF_UNIX
而:
c
sa_data
表示相关地址数据的通用存储部分。
实际编程时,通常不会直接手工把 IPv4 地址和端口按字节填入 sa_data。
而是使用专门的 IPv4 地址结构。
这就是:
c
struct sockaddr_in
三十四、sockaddr_in:IPv4地址结构
34.1 为什么需要sockaddr_in?
因为 IPv4 通信需要保存:
text
地址族
端口号
IPv4地址
因此可以定义专门结构。
在常见 Linux 环境中,相关结构可以概念化为:
c
struct sockaddr_in
{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
实际字段的具体定义以系统头文件为准。
使用时需要:
c
#include <netinet/in.h>
34.2 sin_family
c
sin_family
用于保存地址族。
例如:
c
addr.sin_family = AF_INET;
表示使用 IPv4。
34.3 sin_port
c
sin_port
用于保存16位端口号。
例如:
c
addr.sin_port = htons(8080);
这里为什么需要 htons()?
因为:
IPv4 Socket地址结构中的端口字段使用网络字节序。
34.4 sin_addr
c
sin_addr
用于保存 IPv4 地址。
其类型是:
c
struct in_addr
34.5 sin_zero
c
sin_zero
是常见实现中的填充字段。
当前可以先理解为用于相关结构布局的填充部分,不需要手工赋予复杂含义。
三十五、in_addr:IPv4地址的内部表示
在常见 Linux 头文件中,in_addr 可以表示为:
c
struct in_addr
{
in_addr_t s_addr;
};
其中:
c
s_addr
用于保存32位 IPv4 地址。
例如:
text
192.168.1.10
可以转换为相应的二进制 IPv4 地址表示。
但实际开发中,不需要自己手工完成点分十进制字符串的每个字段转换。
可以使用系统提供的地址转换接口。
此处先理解:
in_addr把IPv4地址作为结构化的32位地址字段保存。
三十六、一个sockaddr_in初始化示例
下面只展示如何准备 IPv4 Socket 地址。
address.cpp:
cpp
#include <iostream>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
sockaddr_in addr{};
// 使用IPv4
addr.sin_family = AF_INET;
// 设置端口号
addr.sin_port = htons(8080);
// 设置IP地址
int ret = inet_pton(
AF_INET,
"127.0.0.1",
&addr.sin_addr
);
if (ret != 1)
{
std::cerr << "invalid IP address\n";
return 1;
}
std::cout << "IPv4 address prepared\n";
return 0;
}
编译:
bash
g++ address.cpp -std=c++11 -o address
运行:
bash
./address
预期输出:
text
IPv4 address prepared
这里的:
c
inet_pton()
用于把文本形式的 IP 地址转换成适合网络地址结构使用的二进制表示。
它只是为了完整演示地址初始化所使用的辅助函数,此时不需要深入研究。
三十七、为什么bind和connect要进行类型转换?
观察:
c
int bind(
int sockfd,
const struct sockaddr* addr,
socklen_t addrlen
);
第二个参数要求:
c
const struct sockaddr*
但我们实际创建的是:
c
struct sockaddr_in
例如:
cpp
sockaddr_in addr{};
因此调用:
cpp
bind(
sockfd,
reinterpret_cast<sockaddr*>(&addr),
sizeof(addr)
);
需要将地址转换为通用 Socket 地址指针。
37.1 为什么不直接使用sockaddr_in参数?
因为 Socket API 需要支持:
text
IPv4
IPv6
Unix Domain Socket
等不同类型的地址。
如果所有网络地址都使用不同的函数,就不方便统一接口。
因此:
text
sockaddr
可以作为通用接口类型。
而:
text
sockaddr_in
则用于保存具体 IPv4 地址。
37.2 通用接口与具体数据类型
可以建立如下关系:
text
通用Socket接口
|
v
sockaddr*
|
+-- sockaddr_in:IPv4
|
+-- sockaddr_in6:IPv6
|
+-- sockaddr_un:Unix Domain
这种设计的意义在于:
通过统一接口接收不同地址类型,再根据地址族选择相应的处理方式。

三十八、从网络原理到Linux编程:完整知识链路
现在可以把整个网络通信过程整理起来。
假设:
text
客户端程序A
希望向:
text
服务器程序B
发送数据。
首先:
text
应用程序准备消息
例如:
text
hello
然后:
text
传输层处理相关通信信息
其中涉及:
text
源端口
目的端口
接着:
text
网络层处理IP地址与路由
其中涉及:
text
源IP
目的IP
再经过:
text
数据链路层封装
使用相应链路地址交付数据帧。
经过网络传输与必要的路由转发以后,目标主机收到数据。
接收方:
text
数据链路层处理帧
|
v
网络层处理IP数据报
|
v
传输层处理TCP/UDP数据
|
v
交付对应Socket
|
v
应用程序取得数据
因此,我们可以从两个不同角度理解同一次网络通信。
38.1 从网络协议角度
重点关注:
text
如何封装?
如何转发?
如何解包?
如何向上分用?
38.2 从应用程序角度
重点关注:
text
向谁发送?
使用什么传输协议?
使用哪个Socket?
调用什么系统接口?
这两个角度并不冲突。
网络协议提供底层通信机制,Socket API 则让应用程序能够使用这些机制。
三十九、常见问题总结
39.1 MAC地址和IP地址是一回事吗?
不是。
MAC 地址用于相应数据链路层的节点识别与帧交付。
IP 地址用于 IP 网络的寻址与转发。
39.2 只要知道目标MAC,就一定能跨互联网发送数据吗?
不能。
MAC 地址主要服务于相关链路的帧交付,跨网络通信通常还需要 IP 地址与路由机制。
39.3 为什么交换机和路由器的作用不同?
普通二层交换机主要根据链路层信息转发帧。
路由器主要根据网络层信息进行跨网络转发。
39.4 数据从应用程序发出以后,是否直接进入网线?
不是。
通常需要经过本机协议栈的相应封装处理。
39.5 什么是封装?
每层协议把上层数据作为有效载荷,并附加本层所需的控制信息。
39.6 什么是解包?
接收方按照协议格式逐层处理并移除对应封装信息,将有效载荷向上交付。
39.7 IP地址在经过路由器时一定不变吗?
不是绝对的。
在没有地址转换的普通IP转发过程中,源和目的IP通常不变,但实际网络中存在 NAT 等可能修改地址的机制。
39.8 MAC地址在每一跳都必然相同吗?
不是。
在经过路由器进入新的以太网链路时,通常需要使用新的链路层源MAC和目的MAC。
39.9 IP地址为什么不能替代端口号?
因为数据到达目标主机以后,还需要交付到正确的传输层通信端点。
39.10 端口号就是PID吗?
不是。
端口号属于传输层寻址机制,PID属于操作系统进程管理机制。
39.11 Socket是不是简单等于IP+Port?
不能完全等同。
IP地址和端口号用于描述网络通信地址,而Socket是操作系统提供的通信端点抽象。
39.12 TCP和UDP都属于传输层吗?
是的。
TCP提供面向连接的可靠字节流传输。
UDP提供无连接的数据报传输,不自行保证可靠交付。
39.13 为什么网络字节序通常使用大端?
因为相应多字节协议字段需要采用统一的字节排列规则,以保证不同主机能够按照相同格式理解数据。
39.14 bind和connect为什么使用sockaddr?
因为 Socket API 需要支持不同地址族,因此使用通用地址结构作为接口参数。
39.15 sockaddr_in和in_addr有什么区别?
sockaddr_in 表示 IPv4 Socket 地址,通常包含地址族、端口号和IPv4地址。
in_addr 则主要用于表示其中的32位IPv4地址。
四十、总结
本文从局域网通信出发,逐步分析了一条网络消息怎样到达目标应用程序。
首先,我们认识了:
text
MAC地址
以及经典共享式以太网中的碰撞域、碰撞检测等概念。
随后,将局域网通信与 TCP/IP 分层模型结合起来,理解:
text
发送方逐层向下封装
接收方逐层向上解包
并进一步认识:
text
报头
有效载荷
段
数据报
帧
等概念。
通过协议分用,我们理解了:
每一层不仅需要知道如何处理自己的协议字段,还需要知道怎样将有效载荷交给正确的上层协议。
随后,借助跨网络通信案例,认识了:
text
IP地址
MAC地址
路由器
下一跳
之间的区别。
最重要的认识是:
IP地址主要用于跨网络寻址和路由转发,MAC地址主要用于相应链路中的帧交付。
接下来,通过进程通信需求引出了:
text
端口号
并理解了:
text
源IP
源端口
目的IP
目的端口
组成的网络通信地址信息。
然后,又认识了:
text
TCP
UDP
Socket
的基础概念。
最后,介绍了网络字节序与 Linux Socket 编程常见接口:
c
htonl()
htons()
ntohl()
ntohs()
socket()
bind()
listen()
accept()
connect()
以及重要的网络地址结构:
c
struct sockaddr
struct sockaddr_in
struct in_addr
至此,我们已经建立了一条完整的网络基础知识链:
text
计算机网络
|
v
通信协议
|
v
协议分层
|
v
局域网通信
|
v
MAC地址
|
v
封装与解包
|
v
跨网络转发
|
v
IP地址
|
v
端口号
|
v
Socket
|
v
Linux网络编程接口
理解这些内容以后,后续学习 UDP Socket、TCP Socket、客户端与服务器编程,就不再只是机械地记忆函数调用顺序,而是能够理解:
应用程序为什么需要这些接口,以及数据从发送到接收究竟经过了怎样的处理过程。