一、网络通信的核心定位:从主机到进程
1.1 源 IP 地址与目的 IP 地址的作用
- IP(Internet Protocol)的核心功能是在互联网中唯一标识一台主机,确保数据能从源主机传输到正确的目的主机。后续会深入讲解 IP 分类(如 A、B、C 类地址)及子网划分等特性。
- 数据传输的终点并非 "主机",而是 "主机内的用户进程"。例如:
- 聊天数据需交给 QQ 进程,下载数据需交给迅雷进程,网页数据需交给浏览器进程。
- 进程是用户在操作系统中的 "代理",只有将数据交付给目标进程,用户才能获取并使用数据。
1.2 从 "主机定位" 到 "进程定位" 的问题
- 一台主机中会同时运行多个进程(如同时打开 QQ、浏览器、音乐软件),当数据到达目的主机后,操作系统无法仅凭 IP 地址判断应交给哪个进程。
- 解决该问题需引入新的标识 ------端口号,实现 "IP 地址定位主机 + 端口号定位进程" 的二级定位体系。
二、端口号:进程的网络身份标识
2.1 端口号的基础定义
- 端口号是传输层协议的核心字段,本质是一个 2 字节(16 位)的无符号整数,取值范围为 0-65535。
- 核心作用:告知操作系统,当前接收的网络数据应分配给哪一个进程处理。
- 唯一标识公式:IP 地址 + 端口号 = 套接字(Socket),可唯一确定互联网中某台主机上的某个网络进程。
2.2 端口号的范围划分
根据使用场景,端口号分为两类,避免冲突并明确功能定位:
| 端口号范围 | 类型 | 特点与用途 |
|---|---|---|
| 0-1023 | 知名端口号 | 由 IANA(互联网数字分配机构)统一分配,对应固定应用层协议。例如:HTTP 用 80、HTTPS 用 443、SSH 用 22、FTP 用 21。 |
| 1024-65535 | 动态端口号 | 由操作系统在客户端进程发起网络连接时随机分配,使用后释放,避免占用知名端口。 |
2.3 端口号与进程 ID(PID)的区别
- 共性:均能唯一标识进程。
- 差异:
- 作用范围不同:PID 是操作系统本地 的进程标识(如 Windows 的任务管理器、Linux 的 ps 命令查看),仅在本机有效;端口号是网络层面的进程标识,跨主机通信需依赖端口号。
- 绑定规则不同:一个进程可绑定多个 端口号(如一个服务器进程同时监听 80 和 443 端口),但一个端口号只能被一个进程绑定(避免数据分发混乱)。
- 设计目的不同:若用 PID 标识网络进程,会导致 "系统进程管理" 与 "网络通信" 强耦合(PID 会随进程重启变化),端口号的设计实现了两者解耦。
2.4 源端口号与目的端口号
- 传输层协议(TCP/UDP)的数据包中,会同时包含源端口号 和目的端口号 :
- 源端口号:标识数据的发送进程(如客户端的浏览器进程)。
- 目的端口号:标识数据的接收进程(如服务器的 Web 进程)。
- 两者结合,可清晰描述 "数据从哪个进程发出,要发给哪个进程",是网络通信的基础字段。
三、传输层协议:TCP 与 UDP 的核心差异
传输层位于 IP 层(网络层)之上,直接为应用层提供通信服务,核心协议为 TCP 和 UDP,两者设计理念完全不同,适用于不同场景。
3.1 TCP 协议(Transmission Control Protocol,传输控制协议)
- 核心特性:
- 有连接:通信前需先建立 "三次握手" 连接,通信结束后需 "四次挥手" 释放连接,类似 "打电话先拨号,挂电话先道别"。
- 可靠传输:通过确认应答(ACK)、重传机制、流量控制、拥塞控制等,确保数据不丢失、不重复、按序到达。
- 面向字节流:数据以连续的字节流形式传输,无固定 "数据包" 边界,需应用层自行定义数据分割规则(如 HTTP 的 Content-Length 字段)。
3.2 UDP 协议(User Datagram Protocol,用户数据报协议)
- 核心特性:
- 无连接:无需建立连接,直接发送数据,类似 "发短信无需拨号",通信效率高。
- 不可靠传输:不保证数据到达,也不保证顺序,可能丢失或乱序,需应用层自行处理可靠性(如通过重传、校验等机制)。
- 面向数据报:数据以 "数据报" 为单位传输,每个数据报包含完整的源 / 目的端口号和数据,接收方一次接收一个完整数据报,有天然边界。
四、网络字节序:跨主机通信的 "数据格式统一标准"
4.1 字节序的问题来源
- 多字节数据(如 2 字节的端口号、4 字节的 IP 地址)在内存中的存储顺序,不同主机可能不同,分为两种:
- 大端字节序:低地址存储高字节(如数字 0x1234,内存低地址存 0x12,高地址存 0x34)。
- 小端字节序:低地址存储低字节(如数字 0x1234,内存低地址存 0x34,高地址存 0x12)。
- 若直接传输多字节数据,小端主机发送的数据到了大端主机,会被解析为错误值(如 0x1234 变成 0x3412),导致通信失败。
4.2 TCP/IP 协议的字节序规定
- TCP/IP 协议强制要求:所有网络数据流必须采用大端字节序(称为 "网络字节序"),无论发送方和接收方是大端机还是小端机。
- 转换规则:
- 发送方:若本机是小端字节序,需先将数据转为大端字节序再发送;若本机是大端字节序,直接发送。
- 接收方:若本机是小端字节序,需将接收的大端数据转为小端再处理;若本机是大端字节序,直接处理。
4.3 字节序转换函数
为保证程序可移植性(在大端 / 小端主机上均能正常运行),C 语言提供了以下标准库函数,用于主机字节序与网络字节序的转换:
| 函数名 | 功能 | 适用场景 |
|---|---|---|
htons() |
Host to Network Short | 将 16 位短整数(如端口号)从主机字节序转为网络字节序 |
ntohs() |
Network to Host Short | 将 16 位短整数从网络字节序转为主机字节序 |
htonl() |
Host to Network Long | 将 32 位长整数(如 IPv4 地址)从主机字节序转为网络字节序 |
ntohl() |
Network to Host Long | 将 32 位长整数从网络字节序转为主机字节序 |
4.4 关键注意点
- 单字节数据(如
char类型)无需转换,因为字节序不影响单个字节的存储和解析。 - 所有涉及网络传输的多字节数据(端口号、IP 地址),必须通过上述函数转换,否则会出现 "数据解析错误"。
五、Socket 编程接口:网络通信的 "系统调用入口"
Socket(套接字)是操作系统提供的一套网络编程 API,封装了底层 TCP/IP 协议的细节,让开发者无需关注协议实现,只需调用接口即可完成网络通信。
5.1 核心 Socket API
1. int socket(int domain, int type, int protocol)
- 功能:创建一个套接字(socket 文件描述符),用于后续网络通信。
- 参数 :
domain(协议族 / 地址族):指定网络协议的类型,常用值:
AF_INET:IPv4 协议(最常用)。AF_INET6:IPv6 协议。AF_UNIX/AF_LOCAL:UNIX 域套接字(用于本地进程间通信)。type(套接字类型):指定通信方式,常用值:
SOCK_STREAM:流式套接字,对应 TCP 协议(有连接、可靠传输)。SOCK_DGRAM:数据报套接字,对应 UDP 协议(无连接、不可靠传输)。SOCK_RAW:原始套接字,可直接操作底层协议(如 ICMP,需管理员权限)。protocol(协议编号):指定具体协议,通常设为0(自动匹配type对应的默认协议):
- 若
type为SOCK_STREAM,默认协议为IPPROTO_TCP(TCP)。- 若
type为SOCK_DGRAM,默认协议为IPPROTO_UDP(UDP)。- 返回值 :成功返回非负整数(socket 文件描述符),失败返回
-1(并设置errno)。
2. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
- 功能:将套接字与本地 IP 地址和端口号绑定(固定套接字的本地标识)。
- 参数 :
sockfd:socket()返回的套接字文件描述符。addr:指向struct sockaddr(或其子类,如struct sockaddr_in)的指针,存储本地 IP 和端口信息:
- 对于 IPv4,需使用
struct sockaddr_in,并强制转换为struct sockaddr*:
sin_family:必须设为AF_INET。sin_port:端口号(需用htons()转换为网络字节序)。sin_addr.s_addr:IP 地址(INADDR_ANY表示绑定所有本地网卡 IP,需用htonl()转换)。addrlen:addr指向的结构体的字节长度(如sizeof(struct sockaddr_in))。- 返回值 :成功返回
0,失败返回-1(并设置errno,常见错误如端口被占用)。
3. int listen(int sockfd, int backlog)
- 功能:将 TCP 套接字转为监听状态,允许接收客户端连接请求(仅 TCP 服务器使用)。
- 参数 :
sockfd:bind()绑定后的 TCP 套接字文件描述符。backlog:未完成连接队列(处于三次握手阶段的客户端)的最大长度:
- 若超过此值,新的连接请求会被拒绝(客户端可能收到
ECONNREFUSED错误)。- 实际有效长度可能受系统限制(如 Linux 中默认取
backlog与/proc/sys/net/core/somaxconn的最小值)。- 返回值 :成功返回
0,失败返回-1(并设置errno)。
4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
- 功能:从 TCP 监听套接字的连接队列中取出一个已完成三次握手的客户端连接,返回新的套接字用于与该客户端通信(仅 TCP 服务器使用)。
- 参数 :
sockfd:listen()后的监听套接字文件描述符。addr:输出参数,指向struct sockaddr(或struct sockaddr_in),用于存储客户端的 IP 地址和端口号(可设为NULL,表示不关心客户端信息)。addrlen:输入输出参数:
- 输入:
addr指向的结构体长度(如sizeof(struct sockaddr_in))。- 输出:实际存储的客户端地址信息长度(若
addr为NULL,可设为NULL)。- 返回值 :成功返回新的套接字文件描述符(用于与客户端通信),失败返回
-1(并设置errno)。- 注意 :原监听套接字
sockfd不受影响,可继续接收其他客户端连接。
5. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
- 功能:TCP 客户端向服务器发起连接请求(触发三次握手)。
- 参数 :
sockfd:socket()返回的 TCP 套接字文件描述符(客户端未绑定端口时,系统会自动分配动态端口)。addr:指向struct sockaddr(或struct sockaddr_in)的指针,存储服务器的 IP 地址和端口号:
- 对于 IPv4,
sin_family设为AF_INET,sin_port和sin_addr.s_addr需转换为网络字节序。addrlen:addr指向的结构体的字节长度(如sizeof(struct sockaddr_in))。- 返回值 :成功返回
0(三次握手完成),失败返回-1(并设置errno,如服务器不可达、连接被拒绝)。
6. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
- 功能:从套接字接收数据,并获取发送方的地址信息(常用于 UDP 协议,因 UDP 无连接,需每次接收时确认发送方)。
- 参数 :
sockfd:socket()创建的套接字文件描述符(UDP 套接字或未连接的 TCP 套接字)。buf:接收缓冲区,用于存储接收到的数据(用户需提前分配内存)。len:接收缓冲区的最大长度(字节数),避免缓冲区溢出。flags:接收方式标志,通常设为0(默认阻塞接收),常用可选值:
MSG_DONTWAIT:非阻塞接收(若暂无数据,立即返回-1并设errno=EAGAIN)。MSG_PEEK:预览数据(数据仍保留在接收缓冲区,可再次读取)。src_addr:输出参数,指向struct sockaddr(或struct sockaddr_in),用于存储发送方的 IP 地址和端口号(可设为NULL,表示不关心发送方信息)。addrlen:输入输出参数:
- 输入:
src_addr指向的结构体长度(如sizeof(struct sockaddr_in))。- 输出:实际存储的发送方地址信息长度(若
src_addr为NULL,可设为NULL)。- 返回值 :
- 成功:返回实际接收到的字节数(若连接被关闭,返回
0)。- 失败:返回
-1(并设置errno,如EINTR表示被信号中断)。- 注意 :
- UDP 中,
recvfrom一次接收一个完整的数据报(若数据报长度超过len,超出部分会被截断且不报错)。- 若用于 TCP 套接字,需先通过
connect建立连接,此时src_addr和addrlen可设为NULL,功能等同于recv。
7. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
- 功能:通过套接字向指定目标地址发送数据(常用于 UDP 协议,因 UDP 无连接,需每次发送时指定接收方)。
- 参数 :
sockfd:socket()创建的套接字文件描述符(UDP 套接字或未连接的 TCP 套接字)。buf:发送缓冲区,存储待发送的数据(用户需确保数据有效)。len:待发送数据的字节数(若超过协议最大长度,可能导致数据被截断或发送失败)。flags:发送方式标志,通常设为0(默认阻塞发送),常用可选值:
MSG_DONTWAIT:非阻塞发送(若缓冲区满,立即返回-1并设errno=EAGAIN)。MSG_CONFIRM:告知底层协议,该地址有效(用于 UDP 多播等场景,优化路由)。dest_addr:指向struct sockaddr(或struct sockaddr_in)的指针,存储接收方的 IP 地址和端口号:
- 对于 IPv4,需指定
sin_family=AF_INET,sin_port和sin_addr.s_addr需转换为网络字节序。addrlen:dest_addr指向的结构体的字节长度(如sizeof(struct sockaddr_in))。- 返回值 :
- 成功:返回实际发送的字节数(通常等于
len,除非被信号中断)。- 失败:返回
-1(并设置errno,如EINTR表示被信号中断,EHOSTUNREACH表示目标不可达)。- 注意 :
- UDP 中,
sendto一次发送一个完整的数据报,发送成功仅表示数据已进入内核缓冲区,不保证接收方已收到(因 UDP 无确认机制)。- 若用于已通过
connect建立连接的 TCP 套接字,dest_addr和addrlen可设为NULL和0,功能等同于send。
5.2 sockaddr 地址结构
Socket API 需适配多种网络协议(如 IPv4、IPv6、UNIX 域套接字),因此设计了通用的地址结构struct sockaddr,但实际使用时需根据协议类型转换为具体结构:
-
通用结构(
struct sockaddr) :仅作为函数参数的 "占位符",定义如下(简化版):cppstruct sockaddr { sa_family_t sa_family; // 协议族(如AF_INET表示IPv4,AF_INET6表示IPv6) char sa_data[14]; // 存储具体协议的地址信息,长度固定14字节 }; -
IPv4 专用结构(
struct sockaddr_in) :更贴合 IPv4 地址格式,使用时需强制转换为struct sockaddr*类型传给 Socket API:cppstruct sockaddr_in { sa_family_t sin_family; // 协议族,必须设为AF_INET in_port_t sin_port; // 端口号,需用htons()转为网络字节序 struct in_addr sin_addr; // IPv4地址结构体,sin_addr.s_addr为32位IP地址(需用htonl()或inet_addr()转换) unsigned char sin_zero[8]; // 填充字段,需设为0,使结构体长度与sockaddr一致 }; -
关键说明:
sockaddr的设计实现了 "一套 API 适配多协议",但实际开发中,IPv4 场景主要使用struct sockaddr_in,IPv6 场景使用struct sockaddr_in6。