Linux网络并发服务器模型与SQLite数据库学习笔记

📝摘要:本文梳理TCP并发服务器4种实现方案:多进程、多线程、线程池、IO多路复用(select/poll/epoll),同时完整讲解SQLite嵌入式数据库基础、SQL常用语句、C语言API接口。

🔖标签:#Linux网络编程 #IO多路复用 #select #epoll #SQLite #嵌入式数据库


1. TCP并发服务器概述

  • 单循环服务器:同一时刻只能处理一个客户端连接,处理完一个才能处理下一个。
  • 并发服务器:一对多,同时处理多个客户端连接请求。

UDP服务端天然具备并发能力;TCP面向连接,默认accept阻塞,原生单循环模型无法同时服务多客户端。

TCP并发四种实现手段:

  1. 多进程
  2. 多线程
  3. 线程池(生产者消费者模型)
  4. 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多路复用通用流程:

  1. 设置需要监控的文件描述符集合
  2. 将集合交给内核,内核阻塞检测IO事件
  3. 内核返回就绪事件结果
  4. 用户层遍历处理已经就绪的IO

Linux下三种实现:selectpollepoll

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);

参数说明

  1. nfds:待监控的最大文件描述符 + 1;
  2. readfds:监控读事件fd集合;传NULL代表不监控读;
  3. writefds:监控写事件fd集合;传NULL代表不监控写;
  4. exceptfds:监控异常事件集合;传NULL代表不监控异常;
  5. 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操作选项:

  1. EPOLL_CTL_ADD:添加fd进入红黑树监控
  2. EPOLL_CTL_MOD:修改已经存在fd的监控事件
  3. 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. 数据库基础理论

普通文件与数据库文件对比

  1. 普通文件:自己写代码实现增删改查逻辑,数据量大时查找效率低。
  2. 数据库:封装底层存储算法,提供标准化SQL语句,增删改查高效、使用方便。

数据库分类

  1. 关系型数据库 :数据组织为二维表格;行代表记录,列代表字段。
    • 大型:Oracle、DB2
    • 中型:MySQL、SQLServer
    • 小型嵌入式:SQLite
  2. 非关系型数据库NoSQL:键值对存储,结构灵活;例如 Redis、MongoDB。

SQLite嵌入式数据库特点

  1. 开源免费,C语言开发;
  2. 代码量少,库体积10M以内,适合嵌入式设备;
  3. 文件型数据库:整个数据库就是一个磁盘文件,可以拷贝迁移;
  4. 理论最大数据容量可达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语句末尾必须带;

  1. 创建表

    create table class1(id INTEGER, name TEXT, score REAL);

主键自增:PRIMARY KEY AUTOINCREMENT

复制代码
create table class1(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score REAL);
  1. 插入数据

    insert into class1 values(1, "zhangsan", 88.5);

  2. 查询数据

    --查询全部列
    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;

  3. 删除数据

    delete from class1 where id=2;

  4. 更新修改数据

    update class1 set score=95 where name="zhangsan";

  5. 删除整张表

    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
);

参数说明:

  1. pdb:数据库句柄;
  2. sql:待执行SQL字符串;
  3. callback:查询select的时候回调函数;每查询到一条记录回调执行一次 ;非select语句填NULL
  4. arg:传递给callback的用户参数;
  5. 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 文档型非关系数据库

💡拓展笔记总结

  1. select受限于1024位图;poll解决上限但是仍要全量遍历;epoll内核红黑树只返回就绪事件,高并发场景性能最优。
  2. SQLite点命令不带分号;SQL语句必须带分号;内存数据库退出数据丢失。
  3. sqlite3_exec执行select查询必须注册回调函数,每一条记录触发一次回调。
相关推荐
10mAh1 小时前
【Linux】CPU 100% 怎么排查?——top、pidstat、jstack 到线程定位实战
linux·运维·服务器
深念Y1 小时前
开机启动优化记录
linux·开机
昵称画1 小时前
POC验证怎么设计用例?不走过程的实操要点
大数据·数据库·人工智能·低代码·excel
2601_966799041 小时前
酷嗨米J300:硬件级多通道分发采集设备,为矩阵直播打造独立信号通道
服务器·网络·负载均衡
TDengine (老段)1 小时前
TDengine Catalog 与元数据缓存
大数据·数据库·物联网·缓存·时序数据库·tdengine
落樱弥城2 小时前
RHI 设计考量:不同 API 差异分析
服务器·前端·性能优化
Titan20242 小时前
网络基础:传输层协议知识梳理
linux·服务器·网络
CarIise2 小时前
MySQL数据库表关系与多表查询:从表间关系设计到JOIN联表查询
数据库·mysql
一叶龙洲2 小时前
Ubuntu从图标到卸载应用
linux·windows·ubuntu