网络编程基础

网络编程是让不同进程通过网络交换数据的编程技术。两个程序可以在同一台机器、同一个局域网,或互联网的不同位置;它们通常通过操作系统提供的 Socket API 收发数据。

常见场景包括:

  • 浏览器与 Web 服务器通信;
  • 客户端连接游戏、聊天或数据库服务器;
  • 微服务之间的 RPC / HTTP 调用;
  • 文件传输、远程登录、消息队列;
  • 局域网设备发现和实时音视频。

网络编程不等于“调用一次 send()、对方调用一次 recv()”。实际程序还要处理域名解析、连接失败、超时、部分读写、协议解析、并发、资源限制和安全问题。

网络通信的参与者

最常见的模式是客户端—服务端(Client–Server):

1
2
3
4
5
6
7
客户端                                      服务端
创建 socket 创建 socket
解析服务器地址 bind 本地地址和端口
connect 发起连接 listen 开始监听
发送请求 ───────────────────────────────> accept 接收连接
接收响应 <─────────────────────────────── 处理请求并发送响应
close close
角色 典型职责
客户端 主动发起连接或请求;通常不固定绑定端口,由系统分配临时端口。
服务端 在固定地址和端口上等待请求;通常需要 bind()、listen() 和 accept()。
对等节点 在 P2P 等场景中,两端可同时充当客户端和服务端。

客户端和服务端描述的是通信中的角色,不是机器的永久身份:一台机器可以同时运行客户端程序和服务端程序。

网络分层

网络协议按层组织,每层负责不同的问题。应用开发中最常接触以下四层:

层次 代表协议或概念 主要职责
应用层 HTTP、DNS、SMTP、自定义协议 定义业务数据含义,例如“获取页面”“登录”“查询域名”
传输层 TCP、UDP 在两个进程之间传输数据;使用端口区分应用
网络层 IP 在不同网络间寻址和路由;使用 IP 地址定位主机
链路层 Ethernet、Wi-Fi 在本地链路上传输数据帧;使用 MAC 地址

发送时,数据会逐层封装:

1
2
3
4
5
6
7
应用数据
↓ 加 TCP / UDP 首部
传输层报文
↓ 加 IP 首部
IP 数据报
↓ 加以太网 / Wi-Fi 首部
链路层帧

接收端再反向解封装。普通 Socket 程序通常只处理应用层数据;TCP、UDP、IP 首部由操作系统网络协议栈处理。

更多协议层次、TCP/UDP 和 HTTP 的知识见 协议基础.md。

IP 地址:找到目标主机

IP 地址用于在网络中定位主机的网络接口。

类型 位数 示例 说明
IPv4 32 位 192.168.1.10 常用点分十进制表示
IPv6 128 位 2001:db8::10 地址空间更大,使用十六进制与冒号压缩表示

常见特殊地址:

地址 含义
127.0.0.1 / ::1 回环地址,只能访问本机,适合本地开发与测试
0.0.0.0 服务端绑定时表示监听所有 IPv4 网卡;它不是客户端应连接的目标地址
:: IPv6 中与 0.0.0.0 类似的未指定地址,常用于监听所有 IPv6 地址

常见 IPv4 私有地址范围包括 10.0.0.0/8、172.16.0.0/12 和 192.168.0.0/16。这些地址不能直接在公网路由,通常需要 NAT、端口映射或 VPN 才能被公网主机访问。

“服务程序已经启动”不等于“外部一定能访问”。还要检查:服务监听的具体地址、服务器防火墙/安全组、路由、NAT 规则以及客户端网络是否可达。

端口:找到目标服务

同一台主机可能运行多个网络程序,端口号用于将到达主机的数据交给正确的应用服务。端口是 16 位无符号整数,范围为 0 到 65535。

范围 含义
0–1023 知名端口;Unix-like 系统中通常需管理员权限才能绑定
1024–49151 注册端口,普通服务常使用
49152–65535 动态/临时端口,客户端通常由系统自动选择

常见端口:

服务 默认端口 说明
HTTP 80 明文 Web 协议的惯例端口
HTTPS 443 HTTP over TLS 的惯例端口
SSH 22 远程登录
DNS 53 域名解析,通常使用 UDP,也可使用 TCP
MySQL 3306 数据库默认端口
Redis 6379 内存数据库默认端口

端口只是约定,服务可以监听其他端口。例如 HTTP 服务也可以运行在 8080。使用端口 0 进行 bind() 时,系统会自动分配可用端口,可通过 getsockname() 查询实际值。

地址、端口与连接

一个网络端点通常写作:

1
2
3
IP 地址:端口号

192.168.1.10:8080

对于一条已经建立的 TCP 连接,操作系统通常使用四元组区分它:

1
源 IP、源端口、目的 IP、目的端口

例如:

1
2
客户端:192.168.1.100:50000
服务端:192.168.1.10:8080

多个客户端可以同时连接同一服务器的 8080 端口,因为它们的源端口或源 IP 不同。服务端的监听 Socket 负责 accept() 新连接;每个成功的 accept() 会返回一个独立的客户端通信 Socket。

域名与 DNS

人更容易记住 example.com 这样的域名,网络连接最终仍需使用 IP 地址。DNS(Domain Name System)负责将域名解析为 IPv4 地址(A 记录)或 IPv6 地址(AAAA 记录)。

在 C++ 网络程序中,推荐使用 getaddrinfo() 处理域名和地址:它同时支持 IPv4、IPv6、端口字符串以及服务端绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <netdb.h>

int main() {
addrinfo hints{};
hints.ai_family = AF_UNSPEC; // 允许 IPv4 和 IPv6
hints.ai_socktype = SOCK_STREAM; // 希望得到 TCP 地址

addrinfo* result = nullptr;
const int rc = getaddrinfo("example.com", "443", &hints, &result);
if (rc != 0) {
std::cerr << "getaddrinfo: " << gai_strerror(rc) << '\n';
return 1;
}

// result 可能是链表;实际程序应依次尝试 socket() 和 connect()
freeaddrinfo(result);
}

service 参数是端口的字符串,例如 "443" 或 "https",不是整数 443。getaddrinfo() 的失败返回 EAI 错误码,应使用 gai_strerror(),而不是 perror()。

协议:让数据可被双方理解

协议定义通信双方共同遵守的规则。即使 TCP 连接成功,如果双方没有使用同一应用层协议,也无法正确理解对方的数据。

一个协议至少应明确:

  • 消息的边界在哪里;
  • 每个字段的含义、类型、长度和字节序;
  • 请求与响应的顺序;
  • 成功、失败和异常分别如何表示;
  • 超时、重复请求、未知字段和版本升级如何处理。

例如只发送文本 hello 仍存在问题:服务端不知道消息是否已经结束、是否还有下一段内容,以及应该返回什么。对于 TCP,不能用“调用一次 send()”来定义一条消息,因为 TCP 只提供有序字节流。

详细的协议边界、TCP/UDP、HTTP、DNS 与自定义协议设计见 协议基础.md。

TCP 与 UDP:如何选择

对比项 TCP UDP
连接方式 面向连接 无连接
传输语义 可靠、有序的字节流 尽力而为的数据报
消息边界 不保留,应用层必须定义 保留一份数据报边界
常见场景 网页、文件、数据库、远程登录 DNS、实时媒体、游戏状态、局域网发现

简单原则:业务需要完整、可靠、有序的数据时通常选择 TCP;允许少量丢失且更在意实时性时可以考虑 UDP。UDP 不会天然让程序“更快”,可靠性、重传、排序和拥塞处理若由应用承担,复杂度会增加。

Socket:程序使用网络的接口

Socket(套接字)是操作系统提供的网络通信接口。在 Unix-like 系统中,Socket 通常以文件描述符 int fd 表示,因此可以创建、读写、关闭。

Socket 类型 常见协议 特点
SOCK_STREAM TCP 面向连接、可靠、有序、字节流
SOCK_DGRAM UDP 无连接、数据报、尽力而为
SOCK_RAW IP/ICMP 等 直接处理较底层报文,通常需要较高权限

TCP 服务端典型流程:

1
socket → setsockopt(可选)→ bind → listen → accept → recv/send → close

TCP 客户端典型流程:

1
socket → connect → recv/send → close

UDP 接收端典型流程:

1
socket → bind → recvfrom/sendto → close

Socket API 的所有参数、返回值、错误处理和完整 C++ 示例见 2_Socket基础.md。

网络字节序与文本地址

网络协议中的多字节整数通常使用大端序(网络字节序)。端口号和自定义协议中的长度字段不能直接按本机内存布局发送。

函数 用途
htons() / ntohs() 16 位主机序与网络序转换;常用于端口
htonl() / ntohl() 32 位主机序与网络序转换;常用于长度字段
inet_pton() 文本 IP 转为二进制地址;不能解析域名
inet_ntop() 二进制 IP 转为文本
1
2
3
4
5
6
7
#include <arpa/inet.h>
#include <netinet/in.h>

sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &address.sin_addr);

inet_pton() 返回 1 才表示转换成功;生产代码应检查返回值。需要解析域名时使用 getaddrinfo(),不要把域名直接传给 inet_pton()。

阻塞、超时与并发

默认 Socket 通常是阻塞的:没有数据时 recv() 会等待;没有待处理连接时 accept() 会等待。阻塞并不一定有问题,但一个慢客户端可能长期占用处理线程。

常见演进路径:

1
2
3
4
5
少量连接:阻塞 I/O
↓
有限并发:阻塞 I/O + 有上限的线程池
↓
大量空闲长连接:非阻塞 I/O + select/poll/epoll/kqueue + 事件循环

无论是哪种模型,都应设置连接、读取、写入和业务处理超时;否则网络异常、对端静默或慢速攻击可能长期占用资源。

I/O 模型、非阻塞处理与多路复用见 IO模型.md、IO多路复用.md;服务器架构见 服务器并发模型.md。

常见故障排查思路

现象 优先检查
connect() 被拒绝(ECONNREFUSED) 目标 IP 是否正确、服务是否正在监听该端口、监听地址是否仅为 127.0.0.1
连接超时(ETIMEDOUT) 路由是否可达、防火墙/安全组、NAT/端口映射、目标主机是否在线
bind() 返回 EADDRINUSE 端口是否被其他进程占用、旧连接状态、是否需要在正确位置设置 SO_REUSEADDR
域名解析失败 DNS 配置、域名拼写、网络连通性、getaddrinfo() 返回的具体错误
能连接但收不到完整数据 是否错误假设一次 recv() 就是一条消息;是否缺少协议边界和循环读取
服务端被慢客户端拖住 是否使用阻塞 I/O 且每连接独占线程;是否缺少读写超时、连接上限和背压

网络编程的核心可以概括为:

1
2
3
4
5
IP 地址:找到目标主机
端口号:找到主机上的服务
传输协议:决定数据如何传输
应用协议:让双方理解数据含义
Socket:程序调用操作系统网络能力的接口