一、IP和端口号
在互联网中,一台主机可以拥有多个 IP 地址(如多网卡、虚拟机、内外网 IP 共存),但在同一网络范围内,每个 IP 地址通常是唯一的。当我们使用软件客户端时,服务端要与客户端进程通信,本质上是通过IP + 端口来定位通信端点。
例如:你的电脑同时有:
- WiFi IP:
192.168.1.10 - 网线 IP:
192.168.2.20 - 虚拟机 IP:
192.168.100.5
- 访问家里路由器后台 → 走 WiFi IP
- 插着网线访问公司内网 → 走 网线 IP
- 虚拟机和主机互传文件 → 走 虚拟网卡 IP
客户端找服务端
服务端通常会使用固定的 IP 地址和固定监听端口,这些地址信息一般在开发阶段就配置在客户端中(或通过配置中心、域名解析获取)。用户启动客户端后,客户端主动根据预设的服务端 IP 和端口发起连接请求,从而找到并连接到对应的服务端进程。
服务端找客户端
- 客户端启动并发起网络连接时,操作系统会为该连接随机分配一个临时端口(部分内部开发场景会约定固定端口)。
- 网络通信中,IP 用于定位唯一主机,端口用于定位主机上的具体进程,服务端正是通过客户端的 IP 和端口来识别并与对应客户端进程通信。
- 但在公网环境下,大多数客户端位于内网并经过 NAT 转换,服务端看到的并非客户端真实内网 IP + 端口,而是 NAT 映射后的公网地址 。因此服务端通常无法主动发起连接,而是依赖客户端先建立连接并保持长连接,再通过已建立的链路进行双向通信。

二、进程ID和端口号
**进程ID(PID)**是操作系统用于标识本机内部运行程序的编号,只在本机有效,用于系统管理进程,每次启动程序PID通常会变化,外部网络无法通过PID找到对应程序;
端口号则是用于网络通信的标识,作用是让操作系统收到网络数据时能准确转发给对应的进程,可固定也可随机分配,外部设备通过IP+端口定位目标机器上的服务进程。
一个进程可绑定多个端口,但同一端口同一时间只能被一个进程占用,简单来说PID是系统内部管程序用的,端口是网络找程序用的,网络通信只认IP+端口,不认PID。
三、源端口和目的端口
源端口号是客户端发起网络请求时系统随机分配的本地端口,用来标识本机上的通信进程,方便服务端回复数据时能准确找到对应客户端程序;
目的端口号 是客户端要访问的服务端固定监听端口(如80、443、3306),用来定位服务端上的具体服务进程,数据传输时双方通过数据包里的源IP+源端口、目的IP+目的端口建立唯一通信通道,服务端接收请求后就以客户端的源IP和源端口作为目的来回传数据。
IP 地址用来标识网络中对应的主机(同一网段内唯一),端口号用来标识该主机上进行网络通信的进程。IP+Port 就可以唯一标识网络中的一个通信端点。
通信的本质,就是两个网络进程之间的数据交互,由 {源 IP, 源端口,目的 IP, 目的端口} 这个四元组唯一标识一条连接、区分两个进程。
因此,网络通信本质上也属于跨主机的进程间通信,而我们把 IP+Port 这个组合称为一个套接字(Socket)。
四、TCP和UDP协议初识
TCP 和 UDP 都是传输层协议,作用都是基于 IP+Port 实现进程间通信,只是可靠性和使用场景完全不同。
TCP 面向连接、可靠传输,通信前必须三次握手建立连接,会保证数据不丢、不乱、不重复,像打电话,必须接通才能说话,适合文件传输、网页浏览、接口请求等需要可靠数据的场景;
UDP 无连接、不可靠,不用建立连接直接发包,不管对方是否收到、数据是否乱序,速度更快、延迟更低,像寄信或广播,适合视频通话、直播、游戏、DNS 查询等允许少量丢包但追求速度的场景,简单说就是 TCP 求稳,UDP 求快。
传输的可靠性应该视为特性,而不是缺陷。
五、网络字节序
首先,网络数据流是一串连续字节,同样存在字节先后顺序的问题,和内存、文件一样有大小端之分。目前计算机有大端和小端两种模式:大端序是高位字节存放在低地址,先发高位;小端序是低位字节存放在低地址,先发低位。
如果两种不同字节序的机器直接通信,就会出现数据解析错乱,比如大端发送 11223344,小端可能收到 44332211。为了解决这个问题,TCP/IP 规定网络通信统一使用大端字节序,也就是网络字节序,发送和接收时的字节序转换由操作系统自动完成。
字节序转化函数

函数命名规则很清晰:h 代表 host(主机),n 代表 network(网络),l 代表 32 位长整数,s 代表 16 位短整数。
htons:把 16 位端口从主机序转网络序;htonl:把 32 位 IP 从主机序转网络序;ntohs、ntohl 则是反向转换。
-
主机是小端:把字节翻转,转成大端再返回;
-
主机是大端:本来就符合网络要求,直接原样返回,不做任何转换。
//把本机端口号转换成网络字节序,赋值给套接字地址结构体的端口字段。 local.sin_port = htons(_port);
六、Socket编程接口
Socket常见函数
| 函数原型 | 作用 | ||
|---|---|---|---|
| int socket(int domain, int type, int protocol); | 创建 socket 文件描述符 | ||
| int bind(int sockfd, const struct sockaddr *addr, socklen_t len); | 绑定 IP + 端口 | ||
| int listen(int sockfd, int backlog); | 启动监听,等待连接 | ||
| int accept(int sockfd, ...); | 接受客户端连接 | ||
| int connect(int sockfd, ...); | 主动连接服务器 |
socket
int socket(int domain, int type, int protocol);
作用:创建一个套接字(网络通信的基础文件描述符),客户端、服务端、TCP/UDP 都必须先调用它。
参数:
- domain:地址族,常用
AF_INET(IPv4)。 - type:套接字类型,
SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。 - protocol:协议,一般填 0 自动匹配。
返回值:成功返回一个文件描述符(fd),失败返回 -1。
bind
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);
作用:给服务端 socket 绑定固定的 IP 和端口,让客户端能找到。
参数:
- sockfd:socket () 返回的文件描述符。
- address:结构体,存放要绑定的 IP、端口、协议。
- address_len:上述结构体的长度。
返回值:成功 0,失败 -1。
listen
int listen(int sockfd, int backlog);
作用:TCP 服务端专用,将 socket 设为监听模式,开始等待客户端连接。
参数:
- sockfd:监听用的 socket fd。
- backlog:等待连接队列的最大长度。
返回值:成功 0,失败 -1。
accept
int accept(int sockfd, struct sockaddr *address, socklen_t *address_len);
作用:TCP 服务端专用,阻塞等待并接收一个客户端连接,返回用于通信的新 fd。
参数:
- sockfd:监听 socket 的 fd。
- address:输出客户端的 IP 和端口。
- address_len:输入输出长度。
返回值 :成功返回新的通信 fd,失败返回 -1。
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:TCP 客户端专用,主动向服务端发起连接(三次握手)。
参数:
- sockfd:客户端的 socket fd。
- addr:服务端的 IP + 端口。
- addrlen:结构体长度。
返回值:成功 0,失败 -1。
Sockaddr结构体

struct sockaddr:通用地址结构体("基类")
这是所有地址结构体的父类型 / 通用类型 ,作用是给 bind、connect、accept 等系统调用提供统一的参数类型,让函数能兼容不同协议的地址。
struct sockaddr {
sa_family_t sa_family; // 16位地址类型(地址族)
char sa_data[14];// 14字节的地址数据(占位用)
};
sa_family:核心标识,用来区分地址类型:AF_INET:IPv4 网络通信(对应sockaddr_in)AF_UNIX/AF_LOCAL:本地进程间通信(对应sockaddr_un)AF_INET6:IPv6 网络通信(对应sockaddr_in6)
sa_data[14]:占位缓冲区,用来存放具体的地址信息,实际开发中不会直接操作它。
所有网络编程的系统调用(bind、connect 等)的参数都定义为 struct sockaddr*,这样函数可以接收任意类型的地址结构体,通过 sa_family 区分具体类型,实现接口统一。
struct sockaddr_in:IPv4 专用地址结构体(最常用)
这是IPv4 网络通信专用 的地址结构体,用来存「IP 地址 + 端口号」,是我们写 TCP/UDP 网络程序时真正用来赋值的结构体。
struct sockaddr_in {
sa_family_t sin_family; // 16位地址类型,固定填 AF_INET
in_port_t sin_port; // 16位端口号(必须用网络字节序)
struct in_addr sin_addr; // 32位IPv4地址(必须用网络字节序)
//注意sin_addr是结构体类型
unsigned char sin_zero[8]; // 8字节填充,对齐内存,无实际意义
};
// 其中 in_addr 是IP地址的封装
struct in_addr {
uint32_t s_addr; // 32位IPv4地址,网络字节序
};
sin_family:必须填AF_INET,标识这是 IPv4 地址sin_port:端口号,必须用htons()转成网络字节序再赋值sin_addr.s_addr:IPv4 地址,必须用inet_addr()/inet_pton()转成网络字节序再赋值sin_zero[8]:填充字节,为了和struct sockaddr总长度对齐(16 字节),必须全填 0
核心作用
实际开发中,我们只操作 sockaddr_in ,赋值完成后,再强制类型转换为 struct sockaddr* 传给系统调用,比如:
struct sockaddr_in serv_addr;
// 给 serv_addr 赋值...
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
struct sockaddr_un:本地进程间通信(IPC)专用结构体
这是UNIX 域套接字(本地通信)专用的地址结构体,用来存本地文件路径,实现同一台机器上两个进程的通信(不需要走网络协议栈,速度更快)。
struct sockaddr_un {
sa_family_t sun_family; // 16位地址类型,固定填 AF_UNIX/AF_LOCAL
char sun_path[108];// 108字节的本地文件路径名
};
sun_family:必须填AF_UNIX(或AF_LOCAL,二者等价)sun_path:本地套接字文件的路径,比如/tmp/my_socket,进程通过这个文件建立连接
核心作用
用于本地进程间通信,和网络通信的 sockaddr_in 完全隔离,通过 sa_family 区分。
关键注意事项
- 永远不要直接操作
struct sockaddr:它只是通用接口,实际赋值、读取都用专用结构体(sockaddr_in/sockaddr_un) - 字节序问题 :
sockaddr_in中的端口号和 IP 地址,必须转成网络字节序(大端),否则会出现端口错乱、IP 解析错误 - IPv6 扩展 :还有
struct sockaddr_in6用于 IPv6,结构和sockaddr_in类似,只是 IP 地址长度为 128 位
struct sockaddr 是通用地址壳子 ,给系统调用做统一接口;sockaddr_in/sockaddr_un 是具体地址内容,分别对应网络通信和本地通信,开发时用专用结构体赋值,再转成通用结构体传参。