Linux Socket编程
TCP服务器编程模型
基本流程
// 1. 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址和端口
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));
// 3. 开始监听
listen(listfd, 3);
// 4. 接受连接
int conn = accept(listfd, (SA)&cli, &len);
// 5. 通信
recv() / send()
// 6. 关闭
close();
关键结构体
struct sockaddr_in {
sa_family_t sin_family; // 地址族: AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
};
// 字节序转换函数
htons() // 主机字节序转网络字节序(short)
htonl() // 主机字节序转网络字节序(long)
ntohs() // 网络字节序转主机字节序(short)
ntohl() // 网络字节序转主机字节序(long)
select模型详解
核心代码分析
tcp_select_ser.c
cpp
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
typedef struct sockaddr*(SA);
int main(int argc, char** argv)
{
// 1. 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));
// 3. 监听
listen(listfd, 3);
// 4. select初始化
fd_set rd_set, tmp_set;
FD_ZERO(&rd_set);
FD_ZERO(&tmp_set);
FD_SET(listfd, &tmp_set);
int maxfd = listfd;
while (1)
{
rd_set = tmp_set;
// 5. 等待事件
select(maxfd + 1, &rd_set, NULL, NULL, NULL);
// 6. 处理事件
for(int i = listfd; i < maxfd + 1; i++)
{
if(FD_ISSET(i, &rd_set) && i == listfd) // 监听socket有新连接
{
int conn = accept(listfd, (SA)&cli, &len);
FD_SET(conn, &tmp_set);
if(conn > maxfd) maxfd = conn;
}
else if(FD_ISSET(i, &rd_set)) // 已连接socket有数据
{
// 处理数据收发
char buf[512] = {0};
int ret = recv(i, buf, sizeof(buf), 0);
if(ret <= 0) // 客户端断开
{
FD_CLR(i, &tmp_set);
close(i);
}
else
{
// 处理数据并回复
send(i, buf, strlen(buf), 0);
}
}
}
}
close(listfd);
return 0;
}
select模型特点
| 特点 | 说明 |
|---|---|
| 优点 | 1. 跨平台性好 2. 实现相对简单 3. 支持多个文件描述符 |
| 缺点 | 1. 每次调用需要复制fd_set 2. 需要遍历所有fd 3. 默认最大支持1024个连接 4. 内核每次都要遍历所有fd |
| 适用场景 | 连接数较少(<1000)的场景 |
select相关函数
// 清除集合中所有fd
void FD_ZERO(fd_set *set);
// 将fd加入集合
void FD_SET(int fd, fd_set *set);
// 将fd从集合移除
void FD_CLR(int fd, fd_set *set);
// 检查fd是否在集合中
int FD_ISSET(int fd, fd_set *set);
// 等待事件发生
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select 模型工作流程

epoll模型详解
核心代码分析
tcp_epollser.c
cpp
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/epoll.h>
typedef struct sockaddr*(SA);
// 添加fd到epoll
int add_fd(int epfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
// 从epoll移除fd
int del_fd(int epfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
return epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
}
int main(int argc, char** argv)
{
// 1. 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定和监听
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));
listen(listfd, 3);
// 3. 创建epoll实例
int epfd = epoll_create(100);
// 4. 添加监听socket到epoll
add_fd(epfd, listfd);
struct epoll_event rev[100] = {0};
while (1)
{
// 5. 等待事件
int ep_ret = epoll_wait(epfd, rev, 100, -1);
// 6. 处理事件
for(int i = 0; i < ep_ret; i++)
{
if(rev[i].data.fd == listfd) // 新连接
{
int conn = accept(listfd, (SA)&cli, &len);
add_fd(epfd, conn);
}
else // 数据可读
{
int conn = rev[i].data.fd;
char buf[512] = {0};
int ret = recv(conn, buf, sizeof(buf), 0);
if(ret <= 0) // 客户端断开
{
del_fd(epfd, conn);
close(conn);
}
else
{
// 处理并回复数据
send(conn, buf, strlen(buf), 0);
}
}
}
}
close(listfd);
return 0;
}
epoll模型特点
| 特点 | 说明 |
|---|---|
| 优点 | 1. 事件驱动,无需遍历所有fd 2. 支持边缘触发(ET)和水平触发(LT) 3. 内存拷贝少,性能高 4. 支持大量连接(万级别) |
| 缺点 | 1. Linux特有,跨平台性差 2. 编程复杂度稍高 |
| 适用场景 | 高并发、大量连接场景 |
epoll相关函数
// 创建epoll实例
int epoll_create(int size); // size已废弃,但需>0
// 控制epoll事件
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
// 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
// epoll_event结构
struct epoll_event {
uint32_t events; // EPOLLIN, EPOLLOUT等
epoll_data_t data; // 用户数据
};
union epoll_data {
void *ptr;
int fd; // 常用fd
uint32_t u32;
uint64_t u64;
};
epoll触发模式
// 水平触发(默认) - 只要缓冲区有数据就会触发
ev.events = EPOLLIN;
// 边缘触发 - 只在状态变化时触发一次
ev.events = EPOLLIN | EPOLLET;
// 同时监听读和写事件
ev.events = EPOLLIN | EPOLLOUT;
epoll 模型工作流程

fork多进程模型
核心代码分析
tcp_forkser.c
cpp
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
typedef struct sockaddr*(SA);
// 子进程退出处理函数
void myhandle(int num)
{
wait(NULL); // 回收子进程
}
int main(int argc, char** argv)
{
// 注册SIGCHLD信号处理,避免僵尸进程
signal(SIGCHLD, myhandle);
// 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定和监听
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));
listen(listfd, 3);
while (1)
{
// 接受连接
int conn = accept(listfd, (SA)&cli, &len);
// 创建子进程处理连接
pid_t pid = fork();
if (pid > 0) // 父进程
{
close(conn); // 父进程关闭连接socket
// 由信号处理函数回收子进程
}
else if (0 == pid) // 子进程
{
close(listfd); // 子进程关闭监听socket
// 处理客户端通信
while (1)
{
char buf[512] = {0};
int ret = recv(conn, buf, sizeof(buf), 0);
if (ret <= 0) break;
// 处理并回复数据
send(conn, buf, strlen(buf), 0);
}
close(conn);
exit(0); // 子进程退出
}
else
{
perror("fork");
continue;
}
}
close(listfd);
return 0;
}
fork模型特点
| 特点 | 说明 |
|---|---|
| 优点 | 1. 编程简单直观 2. 进程间隔离性好 3. 稳定性高(一个进程崩溃不影响其他) |
| 缺点 | 1. 资源消耗大 2. 进程间通信复杂 3. 创建销毁开销大 |
| 适用场景 | 连接数较少,需要高稳定性的场景 |
关键点说明
// 1. 父子进程文件描述符继承
// fork()后,子进程继承父进程所有打开的文件描述符
// 需要及时关闭不需要的fd,避免资源泄露
// 2. 僵尸进程处理
// 子进程退出后,父进程需要wait()回收
// 使用signal(SIGCHLD, SIG_IGN)可让内核自动回收
// 3. 父子进程地址空间
// fork()后,子进程获得父进程地址空间的拷贝
// 父子进程内存空间独立,修改互不影响
fork 模型工作流程
pthread多线程模型
核心代码分析
tcp_threadser.c
cpp
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <semaphore.h>
typedef struct sockaddr*(SA);
sem_t sem_cli; // 信号量,用于线程同步
// 线程处理函数
void* th(void* args)
{
int conn = *(int*)args;
sem_post(&sem_cli); // 通知主线程参数已复制
// 设置线程为分离状态,线程退出后自动回收资源
pthread_detach(pthread_self());
// 处理客户端通信
while (1)
{
char buf[512] = {0};
int ret = recv(conn, buf, sizeof(buf), 0);
if (ret <= 0) break;
// 处理并回复数据
send(conn, buf, strlen(buf), 0);
}
close(conn);
return NULL;
}
int main(int argc, char** argv)
{
// 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定和监听
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));
listen(listfd, 3);
// 初始化信号量
sem_init(&sem_cli, 0, 0);
while (1)
{
// 接受连接
int conn = accept(listfd, (SA)&cli, &len);
// 创建线程处理连接
pthread_t tid;
pthread_create(&tid, NULL, th, &conn);
// 等待线程复制conn参数
sem_wait(&sem_cli);
}
close(listfd);
sem_destroy(&sem_cli);
return 0;
}
线程模型特点
| 特点 | 说明 |
|---|---|
| 优点 | 1. 创建销毁开销小 2. 共享内存,通信方便 3. 资源消耗相对较小 |
| 缺点 | 1. 编程复杂度高 2. 线程安全问题 3. 一个线程崩溃可能影响整个进程 |
| 适用场景 | 需要共享数据、频繁创建销毁的场景 |
线程同步机制
// 1. 信号量(semaphore)
sem_t sem;
sem_init(&sem, 0, 0); // 初始化,初始值0
sem_wait(&sem); // P操作,等待信号量
sem_post(&sem); // V操作,释放信号量
sem_destroy(&sem); // 销毁
// 2. 互斥锁(mutex)
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
// 3. 条件变量(condition variable)
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
pthread_cond_wait(&cond, &mutex);
pthread_cond_signal(&cond);
pthread_cond_destroy(&cond);
线程属性
// 设置线程为分离状态
pthread_detach(thread_id);
// 或创建时指定分离属性
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, th_func, arg);
TCP客户端
统一客户端代码
所有模型的客户端代码基本相同:
cli.c
cpp
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务器
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP
int ret = connect(sockfd, (SA)&ser, sizeof(ser));
// 3. 通信
int i = 5;
while (i)
{
char buf[512] = "this is tcp test";
send(sockfd, buf, strlen(buf), 0);
bzero(buf, sizeof(buf));
recv(sockfd, buf, sizeof(buf), 0);
printf("ser:%s\n", buf);
sleep(1);
i--;
}
// 4. 关闭
close(sockfd);
return 0;
}
客户端关键点
// 1. 连接服务器
// connect()会阻塞直到连接建立或失败
// 2. 数据收发
// send()和recv()默认是阻塞的
// 可以设置为非阻塞模式
// 3. 错误处理
// 连接失败、发送失败、接收失败都需要处理
// 4. 资源释放
// 确保socket被正确关闭
总结与对比
五种模型对比
| 模型 | 并发处理方式 | 资源消耗 | 编程复杂度 | 适用场景 | 性能 |
|---|---|---|---|---|---|
| select | 轮询所有fd | 中等 | 简单 | 连接数<1000 | 低 |
| epoll | 事件通知 | 低 | 中等 | 高并发场景 | 高 |
| fork | 多进程 | 高 | 简单 | 稳定优先 | 中等 |
| pthread | 多线程 | 中等 | 复杂 | 需要共享数据 | 高 |
| 原始模型 | 单线程阻塞 | 低 | 简单 | 学习/测试 | 很低 |
模型对比图

选择建议
-
学习入门:先从简单的fork模型开始
-
小规模应用:select或fork模型
-
高并发服务:epoll模型
-
计算密集型:多线程模型
-
稳定要求高:多进程模型
通用优化建议
// 1. 设置socket选项
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 2. 设置非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// 3. 设置超时
struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// 4. 优雅关闭
shutdown(sockfd, SHUT_RDWR); // 先关闭读写
close(sockfd); // 再关闭socket
常见问题处理
// 1. 地址已在使用
// 设置SO_REUSEADDR选项
// 2. 大量TIME_WAIT
// 调整内核参数或设置SO_REUSEADDR
// 3. 连接数限制
// 调整系统文件描述符限制
// ulimit -n 65535
// 4. 内存泄漏
// 确保所有socket都被正确关闭
// 使用valgrind检查内存泄漏
编译命令汇总
# 通用编译命令
gcc -o server server.c -lpthread # 需要线程库
gcc -o client client.c
# 带调试信息
gcc -g -o server server.c -lpthread
# 优化编译
gcc -O2 -o server server.c -lpthread
# 指定标准
gcc -std=c99 -o server server.c -lpthread
