资讯动态

Linux聊天室源码解析:C语言Socket多线程与SQLite持久化实战

发布时间:2026/9/16 21:01:33 来源:尧图企业网站定制
简介面向Linux服务器运维及网络编程学习者这是一份基于C/S架构的即时通讯聊天室完整代码工程覆盖多客户端接入、私聊群聊、历史记录存储等核心功能。开发者可借助它理解socket编程、多线程并发、ncurses交互界面以及SQLite数据库落地等关键知识点适合作为课程设计或毕业设计的参考原型。资源压缩包约78KB共53个文件主体为13个.c源码、12个.o目标文件、2个makefile构建脚本及公共头文件同时附带多份聊天记录文本、用户数据库文件和服务端/客户端可执行程序便于直接编译运行并查看运行效果。目前已有1190人学习浏览可见在同类型实训项目中具有较高参考价值。资源内除了完整工程目录结构外还通过多个用户聊天记录样例展示了不同账户间的交互过程下载后可直接对照源码分析消息收发、文件传输与界面刷新逻辑快速上手二次开发。1. 从一份能跑通的聊天室源码说起先看一份 Linux 聊天室资源包里面有 server、client 两个目录十几个 .c 和 .o 文件一个 entry.db以及一堆以用户昵称命名的*_聊天记录文件。它本质上是一个 C/S 架构的即时通讯程序服务端用多线程处理 Socket 连接客户端用 ncurses 绘制终端界面聊天记录落到 SQLite。这类项目在 Linux 运维和服务器开发圈子里很常见经常被拿来当面试题核心就是“高并发 持久化”。拆开它的价值在于能真正看到 accept 循环怎么与线程协作、SQLite 写入和消息广播怎么共享资源、ncurses 在非阻塞输入下有哪些坑。花一个下午把这份代码跑通比刷十篇网络编程笔记更实在。2. 先还原结构server/client 目录与各模块的职责2.1 从压缩包文件清单反推工程布局拿到linux聊天室即时通讯.rar后第一步不是急着编译而是先把压缩包展开看清目录结构。我一般在生产环境的 Linux 服务器上做这件事mkdir chatroom cd chatroom # unrar 解压 rar 文件如果中文名乱码优先用 unar unar linux聊天室即时通讯.rar || unrar x linux聊天室即时通讯.rar find . -type f | sortunar对中文文件名支持比unrar好解码 UTF-8 时不容易出现乱码在 CentOS/RHEL 上用yum install unarUbuntu/Debian 上apt install unar即可。这些命令本身就是 Linux 日常运维里最高频的一组解压、查看、分发。展开后能看到典型的 C 工程结构server_main.c、client_main.c是两端入口myhead.h是共享头文件entry.db是 SQLite 数据库文件大量.o对象文件说明这份源码已经编译过。根据文件名可以还原模块分工文件所属端职责推断server_main.c服务端端口监听、accept 循环deal_accept.c服务端为新连接创建会话线程deal_link.c服务端消息解析与路由deal_sqlite3.c服务端用户/聊天记录写入 SQLitequick_send.c服务端向在线连接快速发送数据client_main.c客户端初始化配置和主循环input.c客户端非阻塞读取键盘输入output.c客户端刷新 ncurses 窗口输出ncurses_chat.c客户端ncurses 窗口生命周期管理interface.c客户端绘制聊天界面布局*_聊天记录这类文件是服务端给每个用户单独落盘的文本日志像狮子_聊天记录、鲤鱼_聊天记录说明消息除了写数据库还会写一份人可读的日志方便运维排障时直接grep关键字。2.2 选型理由C Socket 线程 SQLite ncurses这份聊天室没有用 WebSocket也没有用 Java/Python而是选了 C 语言裸写 Socket。原因很直接即时通讯是 Linux 面试题里的高频场景用 C 能把阻塞 I/O、并发模型、TCP 缓冲区这些底层概念全部暴露出来用框架反而把问题包住了。SQLite 在这里充当轻量级持久化单文件entry.db不需要额外部署数据库服务对课程设计和中小工具足够。客户端选择 ncurses 是因为它在终端里能画出独立的输入框和聊天窗口wgetch带上超时后可以轮询键盘配合select同时监听网络。相比纯readline逐行读ncurses 的交互体验更接近真实聊天软件这也是服务器上跑交互程序的标准做法。2.3 一条消息从发送到落库的完整链路一次完整聊天消息的流转路径是客户端input.c捕获键盘输入 → 按约定协议编码后发给服务端 →deal_link.c解析消息类型 →quick_send.c把消息转发给目标在线用户 →deal_sqlite3.c写入entry.db同时以发送者昵称追加一条文本日志。中间涉及三把锁在线用户表的锁、发送队列的锁、SQLite 写入的锁。任何一个环节不加锁多客户端同时发言时就会出现“你看到我一半的话”或者数据库database is locked错误。这里有个常见误区很多人以为聊天室必须用 select 或 epoll 才“高级”。这份代码选择线程模型其实对数据落库更友好。每个线程独立处理一个连接用户表的锁粒度可以做得非常细写 SQLite 时按线程做顺序化避免多个线程同时写一个句柄。线程开销确实比 epoll 大但 50 人规模完全够用也更容易阅读。从文件布局里得到的主要经验是模块划分非常清楚接收连接、业务转发、数据落库被拆成三个.c后面想扩展心跳、离线消息、群组功能时直接在这些文件里加函数即可没必要重写架构。3. 服务端核心从 accept 到 sqlite 写库3.1 deal_accept 与连接生命周期deal_accept.c是每个新客户端进门的地方。通常做法是socket()→bind()→listen()→ 一个死循环里accept()每成功接受一个连接就pthread_create()一个线程。核心代码长这样while (1) { int cfd accept(sfd, (struct sockaddr*)addr, clen); if (cfd 0) { perror(accept); continue; } pthread_t tid; int *pfd malloc(sizeof(int)); // 堆上传递 fd *pfd cfd; pthread_create(tid, NULL, client_handler, pfd); pthread_detach(tid); // 分离不 join }这里的sfd是监听套接字cfd是新建立的连接套接字。用malloc传递fd是为了避免线程启动时栈变量被复用否则线程里去读cfd可能读到下一个连接的值。pthread_detach让线程结束后自动回收资源调用方不需要维护线程句柄。这个模式适合几十到几百个连接的聊天室再高就要换成线程池或 epoll。3.2 消息协议与 deal_link 转发客户端发来的数据是字节流deal_link.c需要先做消息边界切分。我一般会在消息头放两个字节的长度字段用网络字节序。写入时这样处理uint16_t len htons((uint16_t)strlen(payload)); send(fd, len, 2, 0); // 先发长度 send(fd, payload, ntohs(len), 0); // 再发数据htons把主机字节序换为网络字节序ntohs反向还原。这样对端可以recv够 2 字节后得到要读的数据长度再循环读满避免“粘包”。协议里可以再定义消息类型比如0x01是公共广播0x02是私聊0x03是拉取在线列表0x04是心跳消息类型值处理函数公共广播0x01quick_send 全量发送私聊0x02目标 fd 发送拉取在线列表0x03组装用户名列表心跳0x04刷新超时时间deal_link.c解析出类型后调用对应的发送函数。这里要记住所有发送操作都操作一个全局在线用户表必须用读写锁保护否则一个客户端下线时另一个线程正在遍历列表程序直接段错误。3.3 quick_send 与在线用户表quick_send.c负责把数据推给指定连接。简单实现是维护一个fd - 用户名的哈希表或数组发送时遍历所有在线用户跳过发送者把消息发给其他所有人。代码思路void broadcast(char *msg, int sender_fd) { pthread_rwlock_rdlock(user_lock); for (int i 0; i max_users; i) { if (user_table[i].fd ! -1 user_table[i].fd ! sender_fd) { quick_send(user_table[i].fd, msg); } } pthread_rwlock_unlock(user_lock); }quick_send内部会处理send返回值当返回EPIPE或ECONNRESET时说明对方已经断开需要从用户表移除。发送缓冲区还有一个作用当某个客户端消费速度跟不上时send会被 TCP 背压挡住。如果直接阻塞在send上会拖住整个广播流程正确做法是把消息挂到该连接的待发队列由单独的写线程或 epoll 可写事件负责 flush。quick_send.c从命名看就是封装了这种快速投递逻辑具体实现里队列长度一定要限制否则某个慢速客户端会把内存挤爆。3.4 SQLite 写入entry.db 里的用户表和消息表deal_sqlite3.c直接操作entry.db。建议的表结构是这样的CREATE TABLE IF NOT EXISTS user( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS message( id INTEGER PRIMARY KEY AUTOINCREMENT, sender TEXT NOT NULL, receiver TEXT NOT NULL, content TEXT NOT NULL, sent_at DATETIME DEFAULT CURRENT_TIMESTAMP );插入一条聊天记录用 prepared statement避免拼接 SQL 带来的注入和引号问题sqlite3 *db; sqlite3_open(entry.db, db); sqlite3_stmt *stmt; sqlite3_prepare_v2(db, INSERT INTO message(sender, receiver, content) VALUES(?,?,?), -1, stmt, NULL); sqlite3_bind_text(stmt, 1, 狮子, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, 鲤鱼, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, 今晚一起调 bug, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db);sqlite3_bind_text的第三个参数是值第四个参数是长度传-1表示读取整个NUL结尾字符串SQLITE_STATIC表示 sqlite 不会拷贝这块内存生命周期由调用方保证。注意 sqlite3 默认同一时刻只允许一个写者多线程同时INSERT会出现SQLITE_BUSY常见的兜底做法是写前加互斥锁或设置sqlite3_busy_timeout(1000)sqlite3_busy_timeout(db, 1000); // 等待 1 秒再返回 busy提示sqlite3_busy_timeout解决的是锁等待真正要避免并发写还是得在业务层做队列串行化。这样处理完服务端的核心链路。4. 客户端拆解ncurses 界面与输入输出分离4.1 ncurses 窗口布局ncurses_chat.c里首先初始化屏幕initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); wtimeout(stdscr, 100); // 100ms 超时不阻塞initscr启动 curses 模式cbreak关闭行缓冲按键即时返回noecho不在屏幕上回显输入keypad启用功能键wtimeout设置读取超时让主循环可以边读键盘边处理网络数据。然后划分两个窗口上面聊天记录区下面输入区WINDOW *chatwin newwin(LINES - 4, COLS, 0, 0); WINDOW *inputwin newwin(3, COLS, LINES - 3, 0); scrollok(chatwin, TRUE);scrollok(chatwin, TRUE)允许窗口滚动这样聊天内容多了不会溢出。LINES和COLS是终端总行数和列数。窗口高度用LINES - 4和3把底部三行留给输入框。ncurses 的关键函数可以整理成一张表函数作用备注initscr()启动 curses 模式结束时必须 endwin()cbreak()禁用行缓冲输入立即返回noecho()关闭回显手动 wprintw 控制显示wtimeout(win, ms)设置阻塞超时返回 ERR 表示超时wrefresh(win)刷新窗口调用太频繁会闪屏4.2 input.c 与 output.c 如何分工input.c只做一件事从inputwin窗口wgetch接收用户输入的每一个字符缓存在环形缓冲区里按下回车时把一行内容组装成协议帧并发送。output.c只做另一件事把收到的消息字符串追加到chatwin然后wrefresh刷新。这样读写分离键盘输入和网络接收不会互相打断。输入框还有一个回显问题默认cbreak模式下用户按下的字符会被终端立即显示光标却不会自动定位到输入框底部。所以input.c里要自己维护一行输入缓冲区每次按键后wmove(inputwin, 1, 1)、wclrtoeol、再wprintw输出缓冲区内容模拟类似微信输入框的效果。这里会引入大量wrefresh必须控制刷新频率否则终端会闪烁。一个小细节wgetch返回ERR表示超时即用户什么都没按。此时不要调用wrefresh否则终端会闪烁。这也是 ncurses 聊天室最常见的“屏幕闪”问题来源。4.3 客户端同时监听键盘和网络最常见的实现是让一个子线程专门管网络接收void *recv_thread(void *arg) { int sfd *(int *)arg; char buf[1024]; while (1) { int n recv(sfd, buf, sizeof(buf), 0); if (n 0) break; output_append(buf, n); // 追加到界面 } output_append(连接已断开, 12); return NULL; }主线程里继续wgetch。接收线程只要往output.c提供的外部缓冲区写数据并触发一个wrefresh即可。注意output_append内部要加锁因为主线程也可能在刷新同一个窗口。4.4 在自己机器上运行验证运行前要确保libncurses-dev和libsqlite3-dev已装好。在全新的 Linux 系统安装完成后一般先走一遍开发库安装apt-get install -y libncurses-dev libsqlite3-dev make client server在服务器上启动服务端再开两个终端窗口分别运行./client 服务器IP 8888用不同昵称登录。测试时可以打开系统自带监控工具观察连接数变化。服务端日志也会在server目录下生成对应昵称的*_聊天记录文件。这一步能顺带验证“多客户端并发写库”是否正常工作。5. makefile 链接、数据库锁和断线重连的实战处理5.1 makefile 关键行项目里 makefile 把 server 和 client 拆成两个可执行文件链接库是关键server: server_main.o deal_accept.o deal_link.o deal_sqlite3.o quick_send.o gcc -o server $^ -lsqlite3 -lpthread client: client_main.o input.o output.o ncurses_chat.o interface.o gcc -o client $^ -lncurses -lpthread$^表示所有依赖项。-lsqlite3 -lpthread必须放在.o之后因为链接器从右到左解析符号写在前边会导致undefined reference to -lsqlite3。5.2 运行前检查端口与依赖验证编译出的二进制是否找得到动态库ldd server | grep sqlite ss -lntp | grep :8888这条命令在 Linux 运维里很常用能快速确认二进制运行环境和网络监听状态很多部署失败其实就卡在动态库路径或端口冲突上。若输出sqlite3.so not found需要检查LD_LIBRARY_PATH或重新安装 sqlite 开发包若端口被占用用lsof -i:8888定位 pid。5.3 再进一步粘包、心跳和断线重连协议层已经做了长度帧但还要小心一次recv只能读到半个消息。发送端最好是整帧send接收端必须读满长度字段和内容字段再解析。心跳用定时器或select超时客户端 30 秒没收到服务端数据就重连服务端 60 秒没收到心跳就清理连接。断线重连时客户端要保存用户名和 token重新connect后自动补发登录请求。把 SQLite 的 busy_timeout 调到 1000ms配合整帧收发和心跳重连这份源码就能在 50 人左右的小规模生产环境扛住。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价