网络编程套接字(小结)
·
一、核心概念:IP、端口、网络字节序
1. IP 地址(复习+编程视角)
- 本质:
in_addr结构体,一个 32 位无符号整数。 - 常用表示:点分十进制字符串
"192.168.1.10"。 - 编程转换函数:
#include <arpa/inet.h>
// 点分十进制字符串 → 网络字节序的32位整数
int inet_pton(int af, const char *src, void *dst);
// 网络字节序32位整数 → 点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
过时的
inet_addr()和inet_ntoa()已不推荐(不支持 IPv6,线程不安全)。
2. 端口号(Port)
- 本质:16 位无符号整数,范围 0~65535。
- 0~1023:知名端口(HTTP=80,HTTPS=443,SSH=22),需要 root 权限绑定。
- 1024~49151:注册端口。
- 49152~65535:动态/私有端口(客户端临时使用)。
作用:让传输层知道把数据交给哪个应用进程。
3. 网络字节序(大端 vs 小端)
-
问题:不同 CPU 存储多字节数据的方式不同。
- 大端(Big Endian):高位字节存低地址(网络传输用这个)。
- 小端(Little Endian):低位字节存低地址(x86、ARM 默认)。
-
转换函数(
<arpa/inet.h>):
uint16_t htons(uint16_t hostshort); // 主机序 → 网络序(16位,用于端口)
uint32_t htonl(uint32_t hostlong); // 主机序 → 网络序(32位,用于IPv4)
uint16_t ntohs(uint16_t netshort); // 网络序 → 主机序
uint32_t ntohl(uint32_t netlong);
口诀:host to network short / long。
二、Socket API 核心函数速览
| 函数 | 作用 | 关键参数 |
|---|---|---|
socket() |
创建套接字 | 协议族(AF_INET)、类型(SOCK_STREAM/SOCK_DGRAM) |
bind() |
绑定本地地址和端口 | struct sockaddr_in |
listen() |
将套接字设为被动监听(仅 TCP) | 最大等待连接数 |
accept() |
从已完成连接队列取一个连接(仅 TCP) | 返回新的 client_fd |
connect() |
主动发起连接(客户端) | 服务器地址 |
send() / recv() |
TCP 收发数据 | 已连接套接字 |
sendto() / recvfrom() |
UDP 收发数据 | 需指定对方地址 |
close() |
关闭套接字 | 文件描述符 |
通用地址结构:
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口(网络字节序)
struct in_addr sin_addr; // IP地址(网络字节序)
};
三、UDP 客户端/服务器实现
UDP 是无连接、不可靠、面向数据报的。代码最简单。
服务器端核心步骤
// 1. 创建 socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 循环接收
char buf[1024];
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
while (1) {
ssize_t n = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr*)&client_addr, &len);
// 处理数据,可通过 sendto 回复
sendto(sockfd, buf, n, 0, (struct sockaddr*)&client_addr, len);
}
客户端核心步骤
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
sendto(sockfd, "hello", 5, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
注意:UDP 客户端可以不显式
bind,系统会自动分配临时端口。
四、TCP 单连接版本(迭代服务器)
TCP 是面向连接、可靠、字节流协议。
服务器(一次只能处理一个客户端)
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind 同上 ...
listen(listenfd, 5); // 最大等待队列长度
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &len);
// 处理这个客户端,直到它断开
char buf[1024];
while (1) {
ssize_t n = recv(connfd, buf, sizeof(buf), 0);
if (n <= 0) break; // 对方关闭或出错
send(connfd, buf, n, 0); // 回声
}
close(connfd);
}
客户端
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// ... connect 到服务器 ...
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 发送请求,接收响应
send(sockfd, "hello", 5, 0);
char buf[1024];
recv(sockfd, buf, sizeof(buf), 0);
close(sockfd);
缺点:一个客户端占用期间,其他客户端只能排队等 accept。
五、TCP 多进程版本(经典并发模型)
核心思路:accept 返回后,fork() 一个子进程专门处理这个客户端,父进程继续 accept。
int listenfd = socket(...);
bind(...);
listen(...);
while (1) {
int connfd = accept(listenfd, ...);
pid_t pid = fork();
if (pid == 0) {
// 子进程
close(listenfd); // 子进程不需要监听套接字
// 处理客户端...
close(connfd);
exit(0);
} else {
// 父进程
close(connfd); // 父进程不需要已连接套接字
// 可选:回收子进程避免僵尸进程(信号处理)
}
}
必须注意的两点:
- 僵尸进程:父进程需调用
waitpid或忽略SIGCHLD信号(signal(SIGCHLD, SIG_IGN))。 - 文件描述符泄漏:子进程复制了父进程的
listenfd和connfd,需要按需close。
六、TCP 多线程版本(轻量级并发)
核心思路:每来一个连接,创建一个线程去处理。
void *client_handler(void *arg) {
int connfd = *(int*)arg;
free(arg); // 如果动态分配了内存
// 处理客户端...
close(connfd);
return NULL;
}
int main() {
int listenfd = socket(...);
// ... bind, listen ...
while (1) {
int *connfd_ptr = malloc(sizeof(int));
*connfd_ptr = accept(listenfd, ...);
pthread_t tid;
pthread_create(&tid, NULL, client_handler, connfd_ptr);
pthread_detach(tid); // 分离线程,自动回收资源
}
}
对比多进程:
- 线程:共享地址空间,创建销毁开销小,但需注意线程安全。
- 进程:隔离性好,一个子进程崩溃不影响主进程,但开销较大。
七、TCP 连接建立、数据传输、断开全流程
用代码视角重温三次握手和四次挥手。
1. 建立连接(三次握手)
| 步骤 | 客户端调用 | 服务器调用 | TCP 状态变化 |
|---|---|---|---|
| ① | connect() 阻塞 |
(listen 后等待) |
客户端发 SYN → SYN_SENT |
| ② | 仍阻塞 | accept() 阻塞 |
服务器收到 SYN → SYN_RCVD;回复 SYN+ACK |
| ③ | connect() 返回 |
accept() 返回新 fd |
客户端回复 ACK → ESTABLISHED |
若
listen的已完成连接队列满了,新的 SYN 会被忽略或回复 RST。
2. 数据传输
send():将数据拷贝到内核发送缓冲区,默认阻塞直到有空闲空间。recv():从内核接收缓冲区取数据,默认阻塞直到有数据。
TCP 是字节流,没有消息边界,需应用层自己处理粘包/拆包(如长度前缀、分隔符)。
3. 断开连接(四次挥手)
| 步骤 | 主动关闭方(如客户端) | 被动关闭方(服务器) | TCP 状态 |
|---|---|---|---|
| ① | close() → 发 FIN |
recv() 返回 0 |
FIN_WAIT_1 |
| ② | — | 收到 FIN,回复 ACK | CLOSE_WAIT(客户端进 FIN_WAIT_2) |
| ③ | — | 处理完剩余数据,close() 发 FIN |
LAST_ACK |
| ④ | 收到 FIN,回复 ACK | 收到 ACK | TIME_WAIT(客户端) / CLOSED(服务器) |
关键细节:
recv()返回 0 表示对方已关闭连接(正常断开)。- 主动关闭方会进入 TIME_WAIT 状态(约 2MSL,60 秒),确保最后的 ACK 能到达对方。
八、补充:常见陷阱与调试技巧
bind失败:Address already in use- 原因:上次程序退出后,端口仍在 TIME_WAIT。
- 解决:设置
SO_REUSEADDR选项。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
send返回值小于预期- TCP 可能只发送了部分数据,需循环发送剩余部分。
-
recv返回值 0- 不是错误,是对端正常关闭连接。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)