📝摘要:本文梳理TCP并发服务器4种实现方案:多进程、多线程、线程池、IO多路复用(select/poll/epoll),同时完整讲解SQLite嵌入式数据库基础、SQL常用语句、C语言API接口。
🔖标签:
#Linux网络编程#IO多路复用#select#epoll#SQLite#嵌入式数据库
1. TCP并发服务器概述
- 单循环服务器:同一时刻只能处理一个客户端连接,处理完一个才能处理下一个。
- 并发服务器:一对多,同时处理多个客户端连接请求。
UDP服务端天然具备并发能力;TCP面向连接,默认accept阻塞,原生单循环模型无法同时服务多客户端。
TCP并发四种实现手段:
- 多进程
- 多线程
- 线程池(生产者消费者模型)
- IO多路复用(select / poll / epoll)
2. TCP并发服务器四种实现方案
2.1 多进程模型
每来一个客户端accept得到connfd,调用fork()创建子进程;子进程负责和客户端收发数据,父进程继续等待新连接。
socket();
bind();
listen();
while (1)
{
int connfd = accept(lfd, NULL, NULL);
pid_t pid = fork();
if (pid > 0)
{
//父进程,继续等待新连接,不处理业务
close(connfd); //父进程不需要该fd,关闭避免fd泄漏
}
else if (0 == pid)
{
close(lfd); //子进程不需要监听fd
while (1)
{
recv(connfd, buf, sizeof(buf),0);
send(connfd, buf, strlen(buf),0);
}
close(connfd);
exit(0);
}
}
✅优点:进程之间相互隔离,一个客户端崩溃不会影响其他客户端。
❌缺点:进程资源开销大,进程PCB占用内存,进程间通信复杂;并发上限低。
⚠️修正笔记遗漏关键点:fork之后父子进程共享文件描述符,父进程必须
close(connfd),子进程必须close(lfd),否则fd不会真正释放。
2.2 多线程模型
每接收到一个客户端连接,创建子线程处理业务,主线程继续等待新连接。
socket();
bind();
listen();
while (1)
{
int connfd = accept(lfd, NULL, NULL);
pthread_t tid;
//把connfd传递给线程函数
pthread_create(&tid, NULL, client_handle, (void*)&connfd);
pthread_detach(tid); //设置分离属性,线程结束自动回收资源
}
✅优点:线程共享进程地址空间,资源开销比进程小,同等硬件条件下并发量高于多进程。
❌缺点:每次有新连接就要创建销毁线程,频繁创建销毁存在CPU时间开销;一个线程崩溃会导致整个进程全部退出。
⚠️坑点:直接把局部变量connfd地址传给子线程,存在栈内存竞争问题,建议使用malloc堆分配传递参数。
2.3 线程池模型
解决多线程模型频繁创建销毁线程的开销,使用生产者‑消费者模式
- 主线程:生产者 :
accept获取客户端连接,把任务(connfd)投递到任务队列。 - 线程池工作线程:消费者:预先创建一批工作线程,阻塞等待任务队列,取出connfd处理客户端业务。
✅优点:线程预先创建,避免频繁创建销毁线程的开销;可控最大并发工作线程数量。
❌缺点:队列满需要做任务限流,代码复杂度上升。
2.4 IO多路复用模型
不创建新进程、不创建新线程,单进程内同时监控多个IO文件描述符 。
内核帮我们检测多个fd上是否有IO事件(可读、可写、异常);当fd就绪,用户层再去调用accept/recv/send处理事件。
IO多路复用通用流程:
- 设置需要监控的文件描述符集合
- 将集合交给内核,内核阻塞检测IO事件
- 内核返回就绪事件结果
- 用户层遍历处理已经就绪的IO
Linux下三种实现:
select、poll、epoll。
3. IO多路复用 select/poll/epoll对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 存储fd集合 | 位图数组,最大1024个fd | 链表存储 | 内核红黑树存储 |
| fd上限 | 默认1024,可改内核编译选项 | 无硬性上限 | 无硬性上限,查找效率高 |
| 数据拷贝 | 用户态与内核态每次调用都拷贝集合 | 每次调用拷贝集合 | fd集合保存在内核,仅拷贝就绪事件 |
| 返回结果 | 返回全部集合,用户层遍历全部fd判断是否就绪 | 返回全部集合,用户层遍历判断 | 只返回已经就绪的fd数组 |
| 触发模式 | 仅水平触发LT | 仅水平触发LT | 水平触发LT + 边沿触发ET |
名词解释
- LT水平触发:只要缓冲区有数据,每次多路复用调用都会通知事件就绪。
- ET边沿触发:仅当IO状态发生变化一瞬间通知一次,必须一次性读完缓冲区全部数据,否则事件不会再次触发。
4. select核心API与代码框架
4.1 宏函数操作fd_set位图集合
void FD_ZERO(fd_set *set); //清空集合全部bit
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); //判断fd是否在就绪集合中
4.2 select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明
nfds:待监控的最大文件描述符 + 1;readfds:监控读事件fd集合;传NULL代表不监控读;writefds:监控写事件fd集合;传NULL代表不监控写;exceptfds:监控异常事件集合;传NULL代表不监控异常;timeout超时:NULL:永久阻塞等待事件;- 填时间结构体:等待指定时间,超时直接返回0。
返回值
0:就绪IO事件总个数;
- 0:超时到达,没有事件;
- -1:出错。
select TCP服务端简易框架
int lfd = socket(AF_INET,SOCK_STREAM,0);
bind(lfd,...);
listen(lfd,128);
fd_set rdfds;
int maxfd = lfd;
while(1)
{
FD_ZERO(&rdfds);
FD_SET(lfd,&rdfds);
//此处需要维护把所有客户端connfd加入rdfds
int ret = select(maxfd+1, &rdfds, NULL, NULL, NULL);
if(FD_ISSET(lfd,&rdfds))
{
//监听fd就绪,新客户端连接
int connfd = accept(lfd,NULL,NULL);
FD_SET(connfd,&rdfds);
if(connfd>maxfd) maxfd = connfd;
}
//遍历所有fd判断哪些客户端可读
for(int i = lfd+1; i <= maxfd; i++)
{
if(FD_ISSET(i,&rdfds))
{
char buf[1024];
int n = recv(i,buf,sizeof(buf),0);
if(n<=0){
close(i);
FD_CLR(i,&rdfds);
}else{
send(i,buf,n,0);
}
}
}
}
⚠️select每次返回之后集合会被内核改写,每一轮循环都需要重新构建fd_set集合。
5. epoll核心API详解
5.1 epoll_create 创建epoll实例
int epoll_create(int size);
- size:早期版本提示监控fd最大数量,现代内核该参数已经被忽略。
- 返回:epoll实例句柄epfd;失败返回‑1。
5.2 epoll_ctl 添加/修改/删除监控fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op操作选项:
EPOLL_CTL_ADD:添加fd进入红黑树监控EPOLL_CTL_MOD:修改已经存在fd的监控事件EPOLL_CTL_DEL:从epoll移除fd
struct epoll_event结构体
typedef union epoll_data {
void *ptr;
int fd; //保存文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* 监控事件 EPOLLIN / EPOLLOUT */
epoll_data_t data; /* 用户数据 */
};
事件宏:
EPOLLIN:读事件,缓冲区有数据可读EPOLLOUT:写事件,缓冲区可写
返回值:成功返回0,失败返回‑1。
5.3 epoll_wait 阻塞等待事件就绪
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
events:输出参数,内核将已经就绪的事件填充到此数组;maxevents:最多一次获取多少个就绪事件;timeout:‑1阻塞等待;0非阻塞;大于0毫秒超时。
返回值:就绪事件数量;0超时;‑1出错。
epoll优势:只会返回就绪fd,不需要遍历全部监控fd。
6. 数据库基础理论
普通文件与数据库文件对比
- 普通文件:自己写代码实现增删改查逻辑,数据量大时查找效率低。
- 数据库:封装底层存储算法,提供标准化SQL语句,增删改查高效、使用方便。
数据库分类
- 关系型数据库 :数据组织为二维表格;行代表记录,列代表字段。
- 大型:Oracle、DB2
- 中型:MySQL、SQLServer
- 小型嵌入式:SQLite
- 非关系型数据库NoSQL:键值对存储,结构灵活;例如 Redis、MongoDB。
SQLite嵌入式数据库特点
- 开源免费,C语言开发;
- 代码量少,库体积10M以内,适合嵌入式设备;
- 文件型数据库:整个数据库就是一个磁盘文件,可以拷贝迁移;
- 理论最大数据容量可达2TB。
Ubuntu安装SQLite3
#命令行交互工具
sudo apt‑get install sqlite3
#C语言开发库头文件
sudo apt‑get install libsqlite3‑dev
7. SQLite3基础使用与SQL语法
7.1 交互终端点命令(.开头,不要分号)
#打开/创建磁盘数据库
sqlite3 stu.db
.help --查看帮助
.tables --查看库中所有表
.headers on --打开查询结果表头
.mode column --列对齐格式化输出
.quit --退出交互终端
.schema --查看建表语句
⚠️直接输入
sqlite3不带文件名,打开内存临时数据库,退出全部数据丢失。
7.2 SQLite数据类型
| 类型 | 说明 |
|---|---|
| NULL | 空值 |
| INTEGER | 整型数字 |
| REAL | 浮点小数 |
| TEXT | 字符串文本,单/双引号包裹 |
| BLOB | 二进制原始字节数据 |
7.3 SQL常用语句(SQL语句末尾必须带;)
-
创建表
create table class1(id INTEGER, name TEXT, score REAL);
主键自增:PRIMARY KEY AUTOINCREMENT
create table class1(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score REAL);
-
插入数据
insert into class1 values(1, "zhangsan", 88.5);
-
查询数据
--查询全部列
select * from class1;
--查询指定列
select id,name from class1;
--条件查询
select * from class1 where id=1;
--多条件 and or
select * from class1 where score>60 and score<90;
--模糊查询 %任意多字符 _单个字符
select * from class1 where name like "wa%";
--排序 ASC升序 DESC降序
select * from class1 order by score DESC; -
删除数据
delete from class1 where id=2;
-
更新修改数据
update class1 set score=95 where name="zhangsan";
-
删除整张表
drop table class1;
7.4 可视化工具
sudo apt‑get install sqlitebrowser
sqlitebrowser stu.db
8. SQLite3 C语言API编程详解+代码示例
8.1 核心API函数原型
sqlite3_open:打开数据库
int sqlite3_open(
const char *filename, /*数据库文件名*/
sqlite3 **ppDb /*输出:数据库句柄指针*/
);
返回值:成功返回SQLITE_OK;失败返回错误码。
sqlite3_errmsg:获取错误字符串
const char *sqlite3_errmsg(sqlite3* pdb);
sqlite3_exec:执行SQL语句
int sqlite3_exec(
sqlite3* pdb,
const char *sql,
int (*callback)(void*,int,char**,char**),
void *arg,
char **errmsg
);
参数说明:
pdb:数据库句柄;sql:待执行SQL字符串;callback:查询select的时候回调函数;每查询到一条记录回调执行一次 ;非select语句填NULL;arg:传递给callback的用户参数;errmsg:出错信息输出指针。
回调函数原型
int (*callback)(void *arg,int n,char** value,char** name);
arg:sqlite3_exec传入的用户参数;n:本条记录的列数量;value[]:每一列的数据,全部以字符串形式返回;name[]:每一列字段表头名字;- 返回值:必须返回0代表正常,非0代表出错。
注意:select查询出来的所有数据,API全部以字符串形式返回。
8.2 完整可运行C示例
#include <stdio.h>
#include <sqlite3.h>
//select查询回调函数
int select_cb(void *arg,int n,char** value,char** name)
{
for(int i=0;i<n;i++)
{
printf("%s = %s | ", name[i], value[i]?value[i]:"NULL");
}
printf("\n");
return 0;
}
int main(void)
{
sqlite3 *pdb;
int ret = sqlite3_open("stu.db", &pdb);
if(ret != SQLITE_OK)
{
printf("open db err:%s\n", sqlite3_errmsg(pdb));
return -1;
}
char *errmsg = NULL;
//建表
const char *sql_create = "create table if not exists class1(id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT,score REAL);";
ret = sqlite3_exec(pdb, sql_create, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
printf("sql err:%s\n",errmsg);
sqlite3_free(errmsg);
}
//插入数据
const char *sql_insert = "insert into class1(name,score) values('lisi',92.0);";
sqlite3_exec(pdb, sql_insert, NULL, NULL, &errmsg);
//查询,传入回调函数
const char *sql_select = "select * from class1;";
sqlite3_exec(pdb, sql_select, select_cb, NULL, &errmsg);
sqlite3_close(pdb);
return 0;
}
编译(必须链接sqlite库)
gcc test.c -o test -lsqlite3
./test
9. 英文缩写全称对照表
| 缩写 | 英文全称 | 中文释义 |
|---|---|---|
| IO | Input Output | 输入输出 |
| PCB | Process Control Block | 进程控制块 |
| fd | file descriptor | 文件描述符 |
| LT | Level Trigger | 水平触发 |
| ET | Edge Trigger | 边沿触发 |
| SQL | Structured Query Language | 结构化查询语言 |
| API | Application Programming Interface | 应用程序编程接口 |
| NoSQL | Not Only SQL | 非关系型数据库 |
| Oracle | Oracle | 甲骨文商业数据库 |
| DB2 | Database 2 | IBM商业关系数据库 |
| MySQL | My Structured Query Language | 开源关系数据库 |
| SQLite | Structured Query Language lite | 轻量嵌入式关系数据库 |
| Redis | Remote Dictionary Server | 远程字典服务,键值数据库 |
| MongoDB | Mongo Database | 文档型非关系数据库 |
💡拓展笔记总结
- select受限于1024位图;poll解决上限但是仍要全量遍历;epoll内核红黑树只返回就绪事件,高并发场景性能最优。
- SQLite点命令不带分号;SQL语句必须带分号;内存数据库退出数据丢失。
- sqlite3_exec执行select查询必须注册回调函数,每一条记录触发一次回调。