网络通信的最终目的不是把数据扔给主机,而是把数据交给人正在使用的程序 ------QQ、浏览器、游戏客户端。
这些程序在操作系统中以进程 的形式存在。
所以,网络通信的本质 = 两个不同主机上的进程之间的通信。
🧠 关键认知转变:
之前我们关心"主机到主机",现在开始关心"进程到进程"。
要让进程能收发网络数据,**操作系统提供了一套编程接口------Socket API。**而在使用它之前,必须先搞懂几个核心概念:IP地址、端口号、字节序等。
一、数据到底要传给谁?
明确:数据传输到主机是目的吗 ?
--> 不是,因为数据是给人使用的 。 进程是人在系统的代表 , 只要把数据给进程,人就相当于拿到了数据 。所以数据传输到主机不是目的,而是手段 。 到达主句内部 , 再交给主机内的进程,才是目的 。
- 你用 QQ 发消息 → 数据要给 QQ 进程
- 你打开网页 → 数据要给浏览器进程
- 一台主机同时跑很多进程,必须有办法区分
所以网络通信的真正目标是:从「发送方进程」 → 传到「接收方进程」

但是 , 系统中 , 同时会存在非常多的进程 ,当数据到达目标主机后 , 怎么转发给目标进程 ??? 这就要在网络背景下,在系统中 , 标识主机的唯一性。
二、认识端口号
2.1 源IP与目的IP:找到那台主机
IP地址 (如 192.168.0.1)的作用是在整个互联网中唯一标识一台主机。
-
源IP地址:数据从哪台主机发出来。
-
目的IP地址:数据要送到哪台主机。
当你在浏览器里访问 www.baidu.com,DNS解析出百度服务器的IP地址(如 110.242.68.66),那么你发出的数据包中:
-
源IP = 你电脑的公网IP
-
目的IP =
110.242.68.66
🌰 小比喻:IP地址就像 省+市+街道+门牌号,能让你准确找到一栋大楼。
2.2 端口号
端口号(port)是传输层协议的内容
- 传输层的概念
- 2字节 , 16 位整数(0 ~ 65535)
- 作用:标识主机里的一个进程

一句话:IP 找到哪台主机,端口找到哪个程序。
要点:
- 一个进程可以绑定多个端口
- 一个端口只能被一个进程占用
IP + port 能够便是网络上的某一台主机的某一个进程
2.3 端口范围划分
| 范围 | 分类 | 说明 |
|---|---|---|
| 0 ~ 1023 | 知名端口号 | 系统预留,给标准服务:HTTP=80,HTTPS=443,SSH=22,FTP=21 |
| 1024 ~ 65535 | 动态/私有端口号 | 操作系统动态分配给客户端程序,比如你的QQ、Chrome |
🌰 小比喻:
主机 = 一栋写字楼
端口号 = 楼层+房间号
知名端口 = 星巴克(总在一楼)、顺丰快递点(固定位置)
2.4 端口号 vs 进程ID(PID)
你可能会问:操作系统里每个进程都有唯一的PID,为什么不用PID来代替端口号?
| 对比项 | 端口号 | PID |
|---|---|---|
| 范围 | 0~65535,网络约定 | 系统动态分配,可能很大 |
| 变化 | 相对稳定(服务固定端口) | 每次启动都变 |
| 耦合性 | 网络标准,与系统解耦 | 强依赖特定操作系统实现 |
| 多对一 | 一个进程可绑定多个端口 | 一个进程只有一个PID |
| 一对多 | 一个端口只能被一个进程占用 | 不适用 |
核心原因 :PID是操作系统内部概念,而网络通信需要跨平台、跨系统。如果强绑PID,那么Windows和Linux的PID管理方式不同,无法互通。
端口号是TCP/IP协议栈的一部分,独立于操作系统。
另外,一个进程可以同时监听多个端口(比如一个Web服务器同时监听80和443),而一个端口只能被一个进程占用。
简要总结:
- PID:操作系统内部标识进程
- Port:网络层面标识进程
- 为什么不用 PID?会让网络与系统强耦合,所以专门设计端口号
2.5 源端口与目的端口:谁发的,发给谁
传输层协议(TCP/UDP)的报文头中,有两个关键的字段:
-
源端口号:发送方进程绑定的端口
-
目的端口号:接收方进程绑定的端口
就是在描述 "数据是谁发的,要发给谁 "
这样,对方回复时,就知道把数据回给哪个进程
例如:你的QQ(端口 12345) 发送消息 → 腾讯服务器(端口 80) 数据包中: 源IP = 你的IP,源端口 = 12345 目的IP = 服务器IP,目的端口 = 80
服务器处理完后,回复给你的QQ时,会把源端口和目的端口交换:
-
源IP = 服务器IP,源端口 = 80
-
目的IP = 你的IP,目的端口 = 12345
🌰 小比喻:
你写信给"上海XX公司 财务部 收"(目的端口=财务部)
落款写"北京YY小区 张三 寄"(源端口=张三)
对方回信时,写"北京YY小区 张三 收"(目的端口指向你),落款"上海XX公司 财务部"(源端口不变)
三、理解socket
Socket = IP 地址 + 端口号 --》 唯一标识一个网络进程
1. 把IP地址和端口号组合起来,就能在整个互联网中唯一标识一个进程 。
这个组合,就叫 Socket(套接字)。
-
网络通信本质上就是两个socket之间的对话。
-
更进一步,一个完整的网络连接由四元组唯一确定:
{ 源IP, 源端口, 目的IP, 目的端口 }


- 所以,网络通信的本质,也是进程间通信
网络通信的本质是进程间通信(IPC)
我们之前学过的进程间通信(管道、消息队列、共享内存)都在同一台主机 上。
而网络通信 ,就是跨主机的进程间通信------只不过借助了网络协议栈。
🧠 这一点非常重要:socket编程,本质就是在写一个能跟另一台机器上的进程"说话"的程序。
四、传输层的典型代表


4.1 TCP(传输控制协议)
- 有连接
- 可靠传输
- 面向字节流
- 像:打电话(先接通再说话)
4.2 UDP(用户数据报协议)
- 无连接
- 不可靠
- 面向数据报
- 像:寄信(直接扔邮箱,不管收没收到)
这部分先记特点,后面深入学。
| 特性 | TCP | UDP |
|---|---|---|
| 全名 | 传输控制协议 | 用户数据报协议 |
| 连接 | 有连接(先打电话,再说话) | 无连接(直接扔包裹,不管收到没) |
| 可靠性 | 可靠传输(确认、重传、排序) | 不可靠传输(丢了不重发,顺序可能乱) |
| 传输方式 | 面向字节流(像水管,无边界) | 面向数据报(一个个独立包,有边界) |
| 速度 | 较慢(因为可靠性开销) | 较快 |
| 典型应用 | HTTP、HTTPS、FTP、SSH、大部分文件下载 | 视频直播、DNS查询、网络游戏(部分) |
五、网络字节序
5.1 什么是字节序?
对于一个多字节整数(比如 0x12345678),在内存中存储有两种方式:
-
大端(Big-Endian):高位字节存在低地址。例如
0x12在低地址。 -
**小端(Little-Endian):****低位字节存在低地址。**例如
0x78在低地址。
我们的x86 PC是小端 的,而很多网络设备、ARM等可能是大端的。
如果不做转换,直接发送内存中的整数,对方会解析错。

5.2 TCP/IP规定:网络字节序 = 大端
为了保证所有设备都能正确解析,发送前,主机字节序 要转为网络字节序(大端);接收后,再转回主机字节序。
操作系统提供了四个便捷函数:
| 函数 | 含义 |
|---|---|
htonl() |
Host to Network Long (32位整数转换) |
htons() |
Host to Network Short (16位整数转换) |
ntohl() |
Network to Host Long |
ntohs() |
Network to Host Short |

h:host(主机)
n:network(网络)
s:short(16 位,端口)
l:long(32 位,IP)
🌰 小比喻:
你和英国人交流,要先把中文(小端)翻译成英文(大端),收到英文回复后再翻译回中文。
在编程中,IP地址(32位整数)和端口号(16位整数) 都需要用这些函数进行转换,然后再填充到socket地址结构中。
六、socket编程接口速览
下面的C语言接口是POSIX标准,几乎所有操作系统都支持。
// 1. 创建socket描述符(类似打开文件)
int socket(int domain, int type, int protocol);
// 2. 绑定端口和IP(服务器必须做)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 3. 监听连接(TCP服务器)
int listen(int sockfd, int backlog);
// 4. 接受连接(TCP服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 5. 建立连接(TCP客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockaddr结构:通用地址

为了支持IPv4、IPv6、Unix域套接字等不同地址格式,socket API使用一个"通用"结构 struct sockaddr。
struct sockaddr {
sa_family_t sa_family; // 地址类型,如 AF_INET
char sa_data[14]; // 地址数据(IP、端口等)
};
实际编程中,我们使用 IPv4专用结构 sockaddr_in:
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址(网络字节序)
};
其中 in_addr 就是一个32位整数:
struct in_addr {
in_addr_t s_addr; // 32位IP地址
};
为什么需要sockaddr?
因为API要统一处理多种协议族的地址,所以设计了一个抽象类型 sockaddr*。
使用时,你把 sockaddr_in 结构体的指针强制转换 为 (struct sockaddr*) 传给函数,函数内部根据 sin_family 字段判断是哪种地址类型。
🌰 小比喻:
sockaddr就像"包裹"的通用标签,里面写着包裹类型(是快递信、还是生鲜、还是文件)。真正寄快递时,你会用一个具体的盒子(
sockaddr_in)去包装,然后告诉快递员"我这是一个通用包裹"(强制转型)。