网络编程
Outlinks (0)
No outlinks found
Backlinks (1)
Backlinks (1)
1 · 网络编程
1.1 · 1. Socket 是什么#
Socket 本质上是一个文件描述符 (file descriptor)。Unix 哲学 “everything is a file”,网络通信也不例外——通过 socket() 系统调用获取一个 fd,然后用它来 send() / recv()。
1.1.1 · Internet Socket 的两种类型#
| 类型 | 常量 | 协议 | 特点 |
|---|---|---|---|
| Stream Socket | SOCK_STREAM | TCP | 可靠、有序、面向连接 |
| Datagram Socket | SOCK_DGRAM | UDP | 不可靠、无连接、速度快 |
- TCP (Stream): 数据按序到达、无差错。适用于 HTTP、SSH、telnet
- UDP (Datagram): fire-and-forget,可能丢包、乱序。适用于游戏、音视频流、DHCP
- 如果需要在 UDP 上实现可靠传输,可以在应用层加 ACK 机制(如 TFTP)
1.1.2 · 数据封装与网络分层模型
数据在发送时逐层封装 header:
Application Data
→ + TFTP Header
→ + UDP Header
→ + IP Header
→ + Ethernet Header
Unix 网络模型(简化 OSI):
| 层 | 说明 |
|---|---|
| Application Layer | telnet, ftp, HTTP… |
| Transport Layer (Host-to-Host) | TCP, UDP |
| Internet Layer | IP, 路由 |
| Network Access Layer | Ethernet, Wi-Fi |
1.2 · 2. IP 地址、结构体与字节序#
1.2.1 · 2.1 IPv4 与 IPv6#
- IPv4: 4 字节,点分十进制
192.0.2.111 - IPv6: 16 字节,冒号分隔十六进制
2001:0db8:63b3:0001::3490
1.2.1.1 · 子网 (Subnet)#
- IP = 网络部分 + 主机部分
- 用 netmask 区分:
192.0.2.12/24表示前 24 位是网络 - CIDR 记法允许任意位数的 netmask
1.2.1.2 · 端口号 (Port)#
- 16-bit 数字 (0-65535)
- IP 地址 = 酒店地址,端口 = 房间号
- < 1024 的端口是特权端口
1.2.1.3 · 私有网络
10.x.x.x,192.168.x.x,172.16-31.x.x(RFC 1918)
1.2.2 · 2.2 字节序 (Byte Order)#
- Big-Endian = Network Byte Order(高位在前)
- Little-Endian = 大多数 Intel CPU 的 Host Byte Order(低位在前)
转换函数:
| 函数 | 含义 |
|---|---|
htons() | Host to Network Short (16-bit) |
htonl() | Host to Network Long (32-bit) |
ntohs() | Network to Host Short |
ntohl() | Network to Host Long |
规则:数据上线前
htonX(),下线后ntohX()
1.2.3 · 2.3 关键结构体#
// 通用地址结构(旧式,14字节手动填)
struct sockaddr {
unsigned short sa_family; // AF_INET 或 AF_INET6
char sa_data[14]; // 地址 + 端口
};
// IPv4 专用(可与 sockaddr 互相强转)
struct sockaddr_in {
short int sin_family; // AF_INET
unsigned short int sin_port; // 端口,必须 htons()
struct in_addr sin_addr; // IP 地址
unsigned char sin_zero[8]; // 填充,memset 为 0
};
struct in_addr {
uint32_t s_addr; // 32-bit IPv4 地址,Network Byte Order
};
// IPv6 专用
struct sockaddr_in6 {
u_int16_t sin6_family; // AF_INET6
u_int16_t sin6_port; // 端口,Network Byte Order
u_int32_t sin6_flowinfo; // IPv6 flow info
struct in6_addr sin6_addr; // IPv6 地址
u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
unsigned char s6_addr[16]; // 128-bit IPv6 地址
};
// 通用存储:足够大以容纳 IPv4 和 IPv6
struct sockaddr_storage {
sa_family_t ss_family; // 检查此字段决定强转类型
// ... 内部填充
};
// DNS 查询结果,链表
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // 0 = any
size_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next; // 链表下一项
};
1.2.4 · 2.4 IP 地址转换函数#
// 字符串 → 二进制(pton = presentation to network)
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr));
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
// 返回值: 1 成功, 0 地址格式错误, -1 出错
// 二进制 → 字符串(ntop = network to presentation)
char ip4[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
inet_addr()/inet_aton()/inet_ntoa()已弃用,不支持 IPv6
1.3 · 3. 核心系统调用#
1.3.1 · 调用流程概览
TCP Server: TCP Client:
getaddrinfo() getaddrinfo()
socket() socket()
setsockopt(SO_REUSEADDR) connect()
bind() send() / recv()
listen() close()
accept()
→ fork() 子进程处理
send() / recv()
close()
UDP:
getaddrinfo()
socket()
bind() (接收端)
sendto() / recvfrom()
close()
1.3.2 · 3.1 getaddrinfo() — 准备地址信息#
int getaddrinfo(const char *node, // 主机名或 IP
const char *service, // 端口号或服务名 "http"
const struct addrinfo *hints,
struct addrinfo **res); // 结果链表
- 设
hints.ai_flags = AI_PASSIVE+node = NULL→ 绑定本机所有接口 - 设
hints.ai_family = AF_UNSPEC→ IPv4/IPv6 皆可 - 用完后
freeaddrinfo(res)
1.3.3 · 3.2 socket() — 创建套接字#
int socket(int domain, int type, int protocol);
// 通常直接用 getaddrinfo 返回的值:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
1.3.4 · 3.3 bind() — 绑定端口#
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
- 服务器端必须 bind,客户端通常不需要(内核自动分配)
- “Address already in use” 错误 → 设置
SO_REUSEADDR:
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
1.3.5 · 3.4 connect() — 连接远端#
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
- 客户端调用,无需先
bind() - 返回 -1 表示出错
1.3.6 · 3.5 listen() — 开始监听#
int listen(int sockfd, int backlog);
backlog: 等待队列大小(通常设 5~20)
1.3.7 · 3.6 accept() — 接受连接#
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 返回新的 socket fd 用于通信,原 sockfd 继续 listen
addr填入对端地址信息
1.3.8 · 3.7 send() / recv() — TCP 数据传输#
int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, int flags);
send()返回实际发送字节数,可能小于 len(需处理 partial send)recv()返回 0 表示对端关闭连接- 两者都是阻塞调用
1.3.9 · 3.8 sendto() / recvfrom() — UDP 数据传输#
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, socklen_t tolen);
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
- UDP 无连接,需要指定目标/来源地址
- 如果对 UDP socket 调用了
connect(),也可以用send()/recv()
1.3.10 · 3.9 close() / shutdown() — 关闭连接#
close(sockfd); // 关闭 fd,不再读写
int shutdown(int sockfd, int how);
// how: 0 = 禁止接收, 1 = 禁止发送, 2 = 两者都禁止
shutdown()只改变可用性,不释放 fd(仍需close())
1.3.11 · 3.10 辅助调用#
// 获取对端地址
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
// 获取本机主机名
int gethostname(char *hostname, size_t size);
1.4 · 4. 客户端-服务器模型#
1.4.1 · TCP Server 基本模式#
// 1. 准备地址
getaddrinfo(NULL, PORT, &hints, &servinfo);
// 2. 遍历结果,创建 socket 并 bind
for (p = servinfo; p != NULL; p = p->ai_next) {
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
bind(sockfd, p->ai_addr, p->ai_addrlen);
}
// 3. 监听
listen(sockfd, BACKLOG);
// 4. accept 循环
while (1) {
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (!fork()) { // 子进程
close(sockfd); // 子进程不需要 listener
send(new_fd, "Hello, world!", 13, 0);
close(new_fd);
exit(0);
}
close(new_fd); // 父进程不需要这个连接
}
1.4.2 · TCP Client 基本模式#
getaddrinfo(hostname, PORT, &hints, &servinfo);
for (p = servinfo; p != NULL; p = p->ai_next) {
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
connect(sockfd, p->ai_addr, p->ai_addrlen);
}
freeaddrinfo(servinfo);
recv(sockfd, buf, MAXDATASIZE-1, 0);
close(sockfd);
1.4.3 · UDP Server/Client#
- Server:
socket()→bind()→recvfrom()→close() - Client:
socket()→sendto()→close() - 无需
listen()/accept()/connect()
1.5 · 5. 进阶技术#
1.5.1 · 5.1 阻塞与非阻塞#
默认 socket 是阻塞的(accept()、recv() 等会 sleep 等待数据)
设置非阻塞:
#include <fcntl.h>
fcntl(sockfd, F_SETFL, O_NONBLOCK);
- 非阻塞下
recv()无数据返回 -1,errno设为EAGAIN或EWOULDBLOCK - 忙轮询浪费 CPU,应使用 I/O 多路复用
1.5.2 · 5.2 poll() — I/O 多路复用#
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
struct pollfd {
int fd; // socket fd
short events; // 监听事件 (POLLIN, POLLOUT)
short revents; // 返回的就绪事件
};
| 事件 | 含义 |
|---|---|
POLLIN | 有数据可读 / 有新连接可 accept |
POLLOUT | 可写不阻塞 |
POLLHUP | 对端关闭 |
timeout毫秒,-1 表示永久等待,0 表示立即返回
1.5.3 · 5.3 select() — 经典 I/O 多路复用#
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// fd_set 操作宏
FD_SET(fd, &set); // 添加 fd 到集合
FD_CLR(fd, &set); // 从集合移除 fd
FD_ISSET(fd, &set); // 检查 fd 是否就绪
FD_ZERO(&set); // 清空集合
select()会修改传入的 fd_set,需要每次调用前重新拷贝- listener socket 就绪 = 有新连接待 accept
- 已连接 socket 就绪且
recv()返回 0 = 对端关闭
大量连接时
poll()/select()性能差,考虑epoll(Linux) /kqueue(BSD) 或 libevent
1.5.4 · 5.4 处理 Partial Send#
send() 可能不会一次发完所有数据,需要循环发送:
int sendall(int s, char *buf, int *len) {
int total = 0;
int bytesleft = *len;
int n;
while (total < *len) {
n = send(s, buf + total, bytesleft, 0);
if (n == -1) break;
total += n;
bytesleft -= n;
}
*len = total;
return n == -1 ? -1 : 0;
}
1.5.5 · 5.5 数据封装与协议设计#
TCP 是字节流,没有消息边界。接收端必须自己划分消息,常见方法:
- 固定长度消息:简单但浪费带宽
- 长度前缀:header 中包含 payload 长度(推荐)
- 分隔符:如
\r\n(文本协议常用)
长度前缀示例:
[1 byte len][8 bytes username][N bytes message]
18 B e n j a m i n H e y ...
- 长度字段使用 Network Byte Order
- 发送用
sendall() - 接收需循环
recv()直到收满完整包(可能收到 partial packet)
1.5.6 · 5.6 序列化#
- 文本编码: 人可读,但慢且占空间
- 原始二进制: 快,但不可移植(字节序、对齐、浮点格式差异)
- 推荐使用标准序列化方案(如 Protocol Buffers, MessagePack)
1.5.7 · 5.7 广播 (Broadcast)#
- 仅限 UDP,使用
setsockopt()设置SO_BROADCAST - 发送到广播地址(如
192.168.1.255),同一子网所有 listener 都能收到