
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++、 《Linux操作系统》
🐶学习方向:C++方向学习爱好者
⭐人生格言:得之坦然,失之淡然

🏠博主简介

文章目录
- 前言
- 一、网络通信最后要把数据交给谁
-
- [1.1 IP 地址先解决主机定位](#1.1 IP 地址先解决主机定位)
- [1.2 只有 IP 还不够](#1.2 只有 IP 还不够)
- [二、为什么不能直接使用 PID](#二、为什么不能直接使用 PID)
-
- [2.1 PID 只在本机进程管理中有效](#2.1 PID 只在本机进程管理中有效)
- [2.2 端口号怎样找到进程](#2.2 端口号怎样找到进程)
- [2.3 服务器端口和客户端端口](#2.3 服务器端口和客户端端口)
- 三、端口范围与网络通信四元组
-
- [3.1 端口号的范围](#3.1 端口号的范围)
- [3.2 一个网络端点与一次通信](#3.2 一个网络端点与一次通信)
- [3.3 Socket 为什么仍然表现为文件](#3.3 Socket 为什么仍然表现为文件)
- [四、先认识 TCP 和 UDP](#四、先认识 TCP 和 UDP)
-
- [4.1 TCP 的直观特点](#4.1 TCP 的直观特点)
- [4.2 UDP 的直观特点](#4.2 UDP 的直观特点)
- 五、网络字节序
-
- [5.1 大端和小端在讨论什么](#5.1 大端和小端在讨论什么)
- [5.2 网络统一使用大端](#5.2 网络统一使用大端)
- [六、Socket 编程常见接口](#六、Socket 编程常见接口)
-
- [6.1 一组接口为什么能支持多种协议](#6.1 一组接口为什么能支持多种协议)
- [6.2 `sockaddr` 只是通用接口类型](#6.2
sockaddr只是通用接口类型) - [6.3 IPv4 使用 `sockaddr_in`](#6.3 IPv4 使用
sockaddr_in) - [6.4 `in_addr` 保存 IPv4 地址](#6.4
in_addr保存 IPv4 地址)
- 七、把预备知识串起来
- 总结
前言
前面已经梳理了网络协议的整体框架,但真正开始写网络程序时,还会遇到几个非常具体的问题:数据到达目标主机以后,操作系统怎么知道该交给哪个进程?为什么网络报文里使用端口号,而不是直接填写进程 PID?不同主机的字节序可能不同,双方又怎样正确解释同一段数据?
这些问题不解决,后面直接调用 socket、bind、recvfrom,很容易只记住接口形式,却不明白参数为什么这样设计。
这一篇先不急着写完整服务器,而是把 Socket 编程需要的预备知识串起来。重点包括:
- IP 地址和端口号分别解决什么问题;
- PID 为什么不能替代端口号;
- 一次网络通信怎样由四元组标识;
- TCP 和 UDP 在这一阶段应该怎样理解;
- 网络字节序为什么统一采用大端;
sockaddr、sockaddr_in和in_addr之间是什么关系。
把这些内容理清,下一篇再写 UDP 服务器时,socket 接口里的每个参数就不再是需要死记的东西。
一、网络通信最后要把数据交给谁
1.1 IP 地址先解决主机定位
在网络通信中,IP 地址用来标识通信端点所在的主机或网络接口。初学阶段可以先把它理解为:IP 负责帮助报文找到目标主机。
但数据到达主机并不是最终目的。刷视频时,网络数据最终要交给视频应用;浏览网页时,网页数据要交给浏览器;聊天时,消息也要交给正在运行的聊天程序。
这些应用运行起来以后,本质上都是进程。于是网络通信可以继续向下理解:
- IP 地址先把报文送到目标主机;
- 操作系统再把报文交给目标主机中的某个网络进程;
- 进程读取数据并完成后续处理。

日常上网看起来有很多行为,站在网络程序的角度,无非是两类:从远端服务器获取数据,或者把本地数据发送到远端服务器。网卡负责收发,进程负责处理,二者之间要经过操作系统完成 I/O。
因此可以得到一个很重要的认识:
网络通信表面上是两台主机在通信,本质上是两台主机中的两个进程在交换数据。
在本地进程间通信中,我们会想办法让两个进程看到同一份资源;到了网络环境中,通信双方共同使用的资源就是网络,而操作系统协议栈负责把这份资源抽象成进程可以读写的接口。
1.2 只有 IP 还不够
一台主机上通常同时运行许多进程。假设报文已经送到服务器,但是报文中只有服务器 IP,内核仍然不知道应该交给 Web 服务、数据库服务,还是某个正在运行的测试程序。
所以还需要第二个编号,用来在主机内部标识网络服务,这就是端口号。
端口号属于传输层协议字段,是一个 16 位无符号整数,取值范围是 0~65535。它不是进程自身的编号,而是进程通过 Socket 绑定和使用的网络编号。
可以暂时这样记:
- IP 地址负责找到主机;
- 端口号负责在目标主机中找到对应的网络服务;
- IP 地址和端口号组合起来,得到一个网络端点。
二、为什么不能直接使用 PID
2.1 PID 只在本机进程管理中有效
学习进程时,PID 已经能够唯一标识本机中的一个进程。既然如此,为什么网络协议还要额外设计端口号?
第一个原因是作用范围不同。PID 是操作系统内部管理进程的编号,只要求在当前主机、当前运行周期内有效。两台主机完全可能同时存在 PID 为 1234 的进程,而且它们可能是两个毫无关系的程序。
例如:
- 主机 A 上 PID 为
1234的进程是浏览器; - 主机 B 上 PID 为
1234的进程恰好是游戏; - 如果报文只写"交给 PID 1234",主机 B 根本无法由此判断发送方想访问什么服务。
第二个原因是 PID 会变化。程序重新启动后,操作系统很可能为它分配新的 PID。服务器需要给客户端提供相对稳定的访问入口,而不能要求每个客户端在通信前先询问服务进程当前的 PID。
第三个原因是并非所有进程都需要联网。每个进程都有 PID,但只有需要网络通信的程序才需要创建 Socket、绑定或使用端口。把进程管理编号直接塞进网络协议,会使两个本来可以独立变化的模块产生不必要的耦合。
程序通常不能要求操作系统为自己分配某个固定 PID,却可以为网络服务绑定约定好的端口。 这正是服务器能够长期对外提供稳定入口的基础。
2.2 端口号怎样找到进程
端口并不是独立飘在内核中的一个数字。应用程序创建 Socket 后,内核会建立相应的套接字对象;服务器执行 bind 后,本地 IP、端口、协议等信息会记录到这个对象中,并进入内核用于查找网络端点的数据结构。
网络报文到达传输层时,TCP 或 UDP 首部中带有端口号。内核根据协议、地址和端口等字段查找对应 Socket,再把有效载荷放进该 Socket 的接收队列,最终由进程通过文件描述符读走。


这里需要把一句常见的初学结论说严谨些:同一个端口能否重复绑定,实际还要看传输层协议、本地地址和 Socket 选项。当前先按照最常见的服务器场景理解:在同一种协议、同一个本地地址上,一个端口不能被两个普通 Socket 随意重复占用。
2.3 服务器端口和客户端端口
服务器要对外提供服务,因此端口通常是提前约定的。例如 HTTP 常用 80,HTTPS 常用 443,DNS 常用 53。客户端只有知道目标服务器的 IP 和端口,才能构造目标地址。
客户端自身也需要一个端口,但它通常不必固定。以浏览器访问服务器为例:
text
客户端:192.168.1.10:60123
服务器:203.0.113.20:443
请求报文可以描述为:
text
源端点 192.168.1.10:60123 -> 目的端点 203.0.113.20:443
服务器收到报文后,报文里已经带着客户端的源 IP 和源端口。回复时把方向反过来即可:
text
源端点 203.0.113.20:443 -> 目的端点 192.168.1.10:60123
因此,服务器的端口要稳定,客户端端口只要在本机当前通信中能够区分对应 Socket 即可,通常由操作系统临时分配。
三、端口范围与网络通信四元组
3.1 端口号的范围
端口号占 16 位,所以一共有 2^16 个取值,也就是 0~65535。实际使用时可以先按下面的范围理解:
0~1023:知名端口,常见基础服务会使用这些约定端口;1024~65535:可用于注册服务或动态分配,客户端临时端口通常从系统配置的动态范围中选择。
这里不要把"1024 以上全部都是客户端随机端口"理解得太死。某个业务服务器完全可以主动绑定 8080、8888 等高位端口,只是它们不属于前面的知名端口范围。
3.2 一个网络端点与一次通信
单独的 IP + port 描述的是一个网络端点。一段完整通信还需要同时知道两端信息:
text
{ 源IP,源端口,目的IP,目的端口 }
这四项通常称为四元组。对于一个已经到达传输层的报文,四元组回答了两个问题:数据是谁发来的,要交给谁。
例如客户端访问 Web 服务时:
text
{ 192.168.1.10,60123,203.0.113.20,443 }
响应报文中的源和目的会交换。内核正是根据这些信息以及协议类型,确定报文属于哪个 Socket。
IP 加端口描述网络端点;源 IP、源端口、目的 IP、目的端口共同描述一次通信的两端。Socket 则是内核中承载通信状态、缓冲区和地址信息的对象,不能简单等同于"IP 加端口"这个地址。
3.3 Socket 为什么仍然表现为文件
在 Linux 中,应用程序调用 socket() 后会得到一个文件描述符。这个描述符指向内核创建的 Socket 对象,进程随后通过它调用 recvfrom、sendto、read、write 等接口。
"一切皆文件"在这里体现为统一的访问方式,而不是说网卡、磁盘和普通文件在底层结构上完全相同。内核为不同对象提供不同操作,但向用户层暴露统一的文件描述符入口。
于是应用程序不需要直接操纵网卡:
- 进程把数据交给 Socket;
- 内核协议栈添加各层报头;
- 网卡驱动把最终数据发送出去;
- 接收端按相反方向处理,再把有效载荷交给目标 Socket。
四、先认识 TCP 和 UDP
传输层最常见的两个协议是 TCP 和 UDP。它们都能把数据交给目标端口,但提供的通信语义不同。

4.1 TCP 的直观特点
TCP 是传输控制协议,现阶段先记住四点:
- 面向连接;
- 可靠传输;
- 面向字节流;
- 属于传输层协议。
"面向连接"表示正式传输数据前,双方要先建立连接。它有点像打电话:不能拿起电话就默认对方已经在听,而要先完成拨号和接通。
"可靠"并不表示物理网络永远不丢包,而是 TCP 会通过确认、序号、超时重传、流量控制等机制,尽量向上层提供可靠、有序的字节流。
"面向字节流"表示应用层交给 TCP 的数据,在传输层看来是一串连续字节。发送方调用两次 send,接收方不一定恰好用两次 recv 按原边界取出。应用层需要自行设计消息边界,这也是后面学习粘包问题的原因。
4.2 UDP 的直观特点
UDP 是用户数据报协议,现阶段同样先记住四点:
- 无连接;
- 不保证可靠交付;
- 面向数据报;
- 属于传输层协议。
UDP 发送数据前不需要先建立类似 TCP 的连接。应用程序准备好目标地址后,就可以直接通过 sendto 发送数据。
"不可靠"是协议能力描述,不是贬义。UDP 本身不负责重传、排序和流量控制,因此实现更简单、报头开销也更小。应用层如果需要,可以根据业务自行增加确认和重传机制。
"面向数据报"表示每次发送都有明确边界。发送方调用一次 sendto 交给 UDP 的一份数据,在接收方仍以一份数据报的形式被读取,不会像 TCP 字节流那样天然失去发送边界。当然,如果接收缓冲区太小,超出部分可能被截断,所以缓冲区大小仍要合理设置。
直播、语音、部分实时游戏可能允许少量丢包,更关心及时性;文件传输、网页访问、转账等场景通常更关心完整性。选择哪种协议,取决于业务需要,而不是简单判断哪个协议"更高级"。
五、网络字节序
5.1 大端和小端在讨论什么
一个多字节整数存入内存时,要决定高权值字节和低权值字节分别放在哪个地址。
假设一个 32 位整数的十六进制形式是:
text
0x12 34 56 78
- 大端:高权值字节
0x12放在低地址; - 小端:低权值字节
0x78放在低地址。
大小端本身没有对错,它只是不同体系结构对多字节数据的存储约定。但网络中的两台机器可能采用不同主机字节序。如果直接把内存里的 16 位端口原样发送,接收方就可能按相反顺序解释它。

5.2 网络统一使用大端
为了让不同主机能够正确通信,TCP/IP 规定多字节整数在网络中统一使用大端字节序,也叫网络字节序。
发送方在发送前,把主机字节序转换为网络字节序;接收方拿到数据后,再根据本机情况转换回主机字节序。若主机本来就是大端,相应转换函数通常不会改变数据;若主机是小端,则会交换字节顺序。
常用接口如下:
cpp
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); // 16位:主机序 -> 网络序
uint16_t ntohs(uint16_t netshort); // 16位:网络序 -> 主机序
uint32_t htonl(uint32_t hostlong); // 32位:主机序 -> 网络序
uint32_t ntohl(uint32_t netlong); // 32位:网络序 -> 主机序
函数名可以拆开记:
h:host,主机;n:network,网络;s:short,16 位;l:long,这组接口中表示 32 位。
端口号是 16 位,所以设置 sockaddr_in::sin_port 时使用 htons;读取对端端口时使用 ntohs。

凡是放进网络报文的多字节整数,都要按接口要求转换为网络字节序;从报文中取出后,再转换为主机字节序。
六、Socket 编程常见接口
6.1 一组接口为什么能支持多种协议
Linux 提供的 Socket API 不只服务于 IPv4,还要支持 IPv6、UNIX 域套接字等不同地址类型。如果为每种协议各设计一套完全不同的函数,接口会非常零散。
因此,常见函数统一接收 struct sockaddr*,真正使用时再把具体地址结构转换成这个通用指针。典型接口如下:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
其中:
socket创建套接字,TCP 和 UDP、客户端和服务器都要使用;bind把本地地址绑定到 Socket,服务器最常见;listen、accept用于 TCP 服务器;connect常用于 TCP 客户端,UDP 也可以调用,但语义与 TCP 不同。
6.2 sockaddr 只是通用接口类型
sockaddr 中最重要的字段是地址族。内核读取地址族后,才知道后面的数据应该按 IPv4、IPv6 还是 UNIX 域地址来解释。

它看起来有点像 C++ 中"统一基类指针接收不同派生类型"的效果,但这里并不存在真正的 C++ 继承和虚函数。它只是 C 接口采用的一种统一传参约定。
调用 bind 时经常看到这样的强制类型转换:
cpp
struct sockaddr_in local = {};
int ret = bind(sockfd,
reinterpret_cast<struct sockaddr*>(&local),
sizeof(local));
应用程序实际填写的是 sockaddr_in,传给通用接口时才转成 sockaddr*。内核再根据开头的地址族字段解释具体结构。
6.3 IPv4 使用 sockaddr_in
IPv4 网络编程实际使用的地址结构是 sockaddr_in:

常用成员可以整理为:
cpp
struct sockaddr_in {
sa_family_t sin_family; // 地址族,IPv4填写AF_INET
in_port_t sin_port; // 16位端口,使用网络字节序
struct in_addr sin_addr; // 32位IPv4地址
};
初始化一个 IPv4 地址时,通常先清零,再分别填写三个关键字段:
cpp
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = htonl(INADDR_ANY);
AF_INET 表示 IPv4 地址族;端口要通过 htons 转成网络字节序;INADDR_ANY 表示接受到达本机任意 IPv4 接口、对应端口的数据。
6.4 in_addr 保存 IPv4 地址
sockaddr_in 中的 sin_addr 类型是 struct in_addr,它本质上保存一个 32 位 IPv4 地址。

人更习惯写 192.168.1.10 这样的点分十进制字符串,内核接口却需要 32 位二进制形式。因此后面的 UDP 程序还会使用地址转换函数,例如 inet_pton、inet_ntop,把字符串和网络地址结构互相转换。
七、把预备知识串起来
现在从客户端发送一份 UDP 数据,可以把整个过程连起来:
- 客户端调用
socket(AF_INET, SOCK_DGRAM, 0)创建 UDP Socket; - 应用程序准备服务器的
sockaddr_in,填写地址族、端口和 IP; - 端口通过
htons转成网络字节序,IP 字符串转换为 32 位网络地址; - 客户端调用
sendto,数据进入内核协议栈; - UDP 首部记录源端口和目的端口,IP 首部记录源 IP 和目的 IP;
- 报文到达服务器后,内核根据协议、地址和端口找到对应 Socket;
- 服务器进程调用
recvfrom,从 Socket 接收队列中取走数据。
这个过程中,IP、端口、文件描述符和 Socket 各自承担不同职责:
- IP 负责主机或接口定位;
- 端口负责定位网络服务;
- 文件描述符是进程访问内核 Socket 的入口;
- Socket 是内核管理网络通信状态和缓冲区的对象;
sockaddr_in是应用程序描述 IPv4 地址的结构。
这些概念如果混在一起,后面的代码会越写越乱;分清以后,接口参数反而很好理解。
总结
本文先完成了 Socket 编程前的准备:
- 网络通信最终要把数据交给进程,IP 找主机,端口找服务;
- PID 只服务于本机进程管理,动态变化,不能代替网络端口;
IP + port描述网络端点,通信双方由四元组共同标识;- TCP 提供面向连接、可靠的字节流,UDP 提供无连接的数据报服务;
- 网络统一使用大端字节序,端口常用
htons和ntohs转换; - IPv4 编程实际填写
sockaddr_in,再转换成通用的sockaddr*传给接口。
下一篇在这些概念之上实现 UDP 回显服务器和网络字典,把 socket、bind、recvfrom、sendto 真正串成一条完整链路。
往期博客:
【Linux网络】网络为什么要分层?从协议、OSI 到 TCP/IP,一次搭起网络通信整体框架