Socket编程实战
💡 核心结论
Socket是应用层和传输层之间的接口
TCP Socket需要bind, listen, accept建立服务器
UDP Socket无需连接,使用sendto/recvfrom通信
select/poll/epoll实现I/O多路复用,提高并发性能
非阻塞I/O配合事件驱动模型可处理大量并发连接
1. Socket基础
1.1 Socket概念
Socket = IP地址 + 端口号 + 协议
应用层
↓
Socket API ← 这里是Socket
↓
传输层(TCP/UDP)
1.2 Socket类型
// 流式Socket (TCP)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 数据报Socket (UDP)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 原始Socket
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
1.3 地址结构
// IPv4地址结构
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 填充
};
// 通用地址结构
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
// 使用示例
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 主机字节序→网络字节序
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
2. TCP Socket编程
2.1 服务器端流程
socket() → bind() → listen() → accept() → recv/send() → close()
完整示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BACKLOG 10
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
// 1. 创建Socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(1);
}
// 2. 设置地址复用
int reuse = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 3. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
// 4. 监听
if (listen(server_fd, BACKLOG) < 0) {
perror("listen");
exit(1);
}
printf("Server listening on port %d\n", PORT);
// 5. 接受连接并处理
while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Client connected: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// 接收数据
int n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
// 发送响应
const char* response = "HTTP/1.1 200 OK\r\n\r\nHello World!";
send(client_fd, response, strlen(response), 0);
}
close(client_fd);
}
close(server_fd);
return 0;
}
2.2 客户端流程
socket() → connect() → send/recv() → close()
完整示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[1024];
// 1. 创建Socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(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);
// 3. 连接服务器
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
exit(1);
}
printf("Connected to server\n");
// 4. 发送请求
const char* request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
send(sockfd, request, strlen(request), 0);
// 5. 接收响应
int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("Response:\n%s\n", buffer);
}
// 6. 关闭连接
close(sockfd);
return 0;
}
2.3 多客户端处理
方案1:多进程
while (1) {
client_fd = accept(server_fd, NULL, NULL);
pid_t pid = fork();
if (pid == 0) {
// 子进程处理客户端
close(server_fd);
handle_client(client_fd);
close(client_fd);
exit(0);
} else {
// 父进程继续接受连接
close(client_fd);
}
}
方案2:多线程
void* handle_client(void* arg) {
int client_fd = *(int*)arg;
free(arg);
// 处理客户端请求
char buffer[1024];
int n = recv(client_fd, buffer, sizeof(buffer), 0);
// ...
close(client_fd);
return NULL;
}
while (1) {
client_fd = accept(server_fd, NULL, NULL);
int *fd_ptr = malloc(sizeof(int));
*fd_ptr = client_fd;
pthread_t thread;
pthread_create(&thread, NULL, handle_client, fd_ptr);
pthread_detach(thread);
}
3. UDP Socket编程
3.1 服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
// 1. 创建Socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
printf("UDP Server listening on port 8080\n");
// 3. 接收和发送数据
while (1) {
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &client_len);
if (n > 0) {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
// 回送数据
sendto(sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, client_len);
}
}
close(sockfd);
return 0;
}
3.2 客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[1024];
// 1. 创建Socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(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);
// 3. 发送数据
const char* message = "Hello UDP";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
// 4. 接收响应
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, NULL, NULL);
if (n > 0) {
buffer[n] = '\0';
printf("Response: %s\n", buffer);
}
close(sockfd);
return 0;
}
4. I/O多路复用
4.1 select
#include <sys/select.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// bind, listen...
fd_set read_fds;
int max_fd = server_fd;
int client_fds[FD_SETSIZE];
int num_clients = 0;
while (1) {
// 清空集合
FD_ZERO(&read_fds);
// 添加服务器socket
FD_SET(server_fd, &read_fds);
// 添加所有客户端socket
for (int i = 0; i < num_clients; i++) {
FD_SET(client_fds[i], &read_fds);
if (client_fds[i] > max_fd) {
max_fd = client_fds[i];
}
}
// 等待事件
struct timeval timeout = {5, 0}; // 5秒超时
int ready = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (ready < 0) {
perror("select");
break;
}
if (ready == 0) {
// 超时
continue;
}
// 检查服务器socket
if (FD_ISSET(server_fd, &read_fds)) {
int client_fd = accept(server_fd, NULL, NULL);
client_fds[num_clients++] = client_fd;
printf("New client connected\n");
}
// 检查客户端socket
for (int i = 0; i < num_clients; i++) {
if (FD_ISSET(client_fds[i], &read_fds)) {
char buffer[1024];
int n = recv(client_fds[i], buffer, sizeof(buffer), 0);
if (n <= 0) {
// 客户端断开
close(client_fds[i]);
client_fds[i] = client_fds[--num_clients];
i--;
} else {
// 处理数据
buffer[n] = '\0';
printf("Received: %s\n", buffer);
send(client_fds[i], buffer, n, 0);
}
}
}
}
return 0;
}
select缺点:
有fd数量限制(FD_SETSIZE,通常1024)
每次调用需要复制fd_set
需要遍历所有fd查找就绪的
4.2 poll
#include <poll.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// bind, listen...
struct pollfd fds[1000];
int nfds = 1;
// 添加服务器socket
fds[0].fd = server_fd;
fds[0].events = POLLIN;
while (1) {
int ready = poll(fds, nfds, 5000); // 5秒超时
if (ready < 0) {
perror("poll");
break;
}
if (ready == 0) {
continue;
}
// 检查服务器socket
if (fds[0].revents & POLLIN) {
int client_fd = accept(server_fd, NULL, NULL);
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN;
nfds++;
}
// 检查客户端socket
for (int i = 1; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
char buffer[1024];
int n = recv(fds[i].fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
close(fds[i].fd);
fds[i] = fds[--nfds];
i--;
} else {
send(fds[i].fd, buffer, n, 0);
}
}
}
}
return 0;
}
4.3 epoll(Linux)
#include <sys/epoll.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// bind, listen...
// 创建epoll实例
int epollfd = epoll_create1(0);
// 添加服务器socket
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, server_fd, &ev);
while (1) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// 新连接
int client_fd = accept(server_fd, NULL, NULL);
// 设置非阻塞
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 添加到epoll
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = client_fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
// 客户端数据
int fd = events[i].data.fd;
char buffer[1024];
int n = recv(fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
} else {
send(fd, buffer, n, 0);
}
}
}
}
return 0;
}
epoll优点:
无fd数量限制
不需要遍历所有fd
支持边缘触发(ET)和水平触发(LT)
5. Python Socket编程
5.1 TCP服务器
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
server.listen(5)
print("Server listening on port 8080")
while True:
client, addr = server.accept()
print(f"Client connected: {addr}")
data = client.recv(1024)
print(f"Received: {data.decode()}")
response = b"HTTP/1.1 200 OK\r\n\r\nHello World!"
client.send(response)
client.close()
5.2 异步Socket(asyncio)
import asyncio
async def handle_client(reader, writer):
addr = writer.get_extra_info('peername')
print(f"Client connected: {addr}")
data = await reader.read(1024)
message = data.decode()
print(f"Received: {message}")
response = "Echo: " + message
writer.write(response.encode())
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(
handle_client, '0.0.0.0', 8080)
addr = server.sockets[0].getsockname()
print(f"Server listening on {addr}")
async with server:
await server.serve_forever()
asyncio.run(main())
6. 常见问题
Q1: close和shutdown的区别?
A:
// close: 关闭socket,引用计数-1
close(sockfd);
// shutdown: 关闭读/写,不减引用计数
shutdown(sockfd, SHUT_RD); // 关闭读
shutdown(sockfd, SHUT_WR); // 关闭写
shutdown(sockfd, SHUT_RDWR); // 全关闭
Q2: SO_REUSEADDR的作用?
A: 允许立即重用处于TIME_WAIT状态的端口
Q3: 阻塞vs非阻塞?
A:
// 阻塞:没有数据就等待
int n = recv(sockfd, buffer, size, 0);
// 非阻塞:没有数据立即返回-1,errno=EAGAIN
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
Q4: 粘包问题如何解决?
A:
固定长度
分隔符(如\r\n)
长度前缀(TLV格式)
参考资源
《Unix网络编程》(UNP)
Linux man pages: socket(7), tcp(7)
Beej’s Guide to Network Programming
Python socket文档