ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux上自建聊天室:WebSocket消息推送与部署调优实践

Linux上自建聊天室:WebSocket消息推送与部署调优实践 简介这是一个基于Linux平台的即时通讯项目源码适合Linux C/C学习者、网络编程初学者及课程设计参考者。项目采用C/S架构包含客户端与服务器端支持多用户在线、私聊/群聊、SQLite存储聊天记录、ncurses终端界面帮助读者将socket网络编程与数据库技术用于真实项目。资源共53个文件压缩包约78KB以.c源文件、.h头文件、Makefile构建脚本为主另含txt说明及聊天记录样本。服务器端含server_main.c、deal_accept.c、deal_sqlite3.c、quick_send.c等模块对应启动、连接、数据库与消息发送客户端含interface.c、ncurses_chat.c、input.c、output.c分别负责界面、渲染、输入与输出编译生成server和client两个可执行程序。已有1190人学习下载。阅读和运行代码可帮助掌握从网络通信到界面交互的完整设计思路理解模块划分与构建方式获得调试排错经验适合作为课程设计或毕业设计的参考。1. 为什么还要在 Linux 上自建一个聊天室很多人觉得聊天室是 Web 1.0 的老古董微信、Slack、钉钉随手就能用没必要自己搭。但当你手上有一台 Linux 服务器想给团队做个内部沟通工具、给线上课程做互动面板、或者想彻底搞懂即时通讯的消息推流原理时现成的 IM 反而成了黑盒。自己搭一个 Linux 聊天室能让你从 TCP 连接、消息广播、心跳保活到 WebSocket 协议栈完整走一遍这些经验直接迁移到物联网设备上报、运维告警推送、客服系统等场景。我见过不少团队用 PHP 写个轮询脚本就上线结果十几个人同时在线就把数据库拖垮。也见过有人直接拿 Netty 全套上功能是强可维护成本立刻上去了。这个标题真正的价值在于用 Linux 服务器上最朴素的手段实现一个能跑、能压测、能根据业务改造成即时通讯IM的方案。适合那些已经会 Linux 常用命令、想动手做点东西的运维和开发也适合准备面试时被问到「WebSocket 和 HTTP 长轮询区别」的求职者。下面这套方案不依赖商业产品或云厂商私有协议每一步你都能在虚拟机里复现。2. 聊天室的核心消息推流与 Linux 下的选型2.1 即时通讯绕不开的三种消息推送模型聊天室本质上是一个多客户端实时收发消息的系统。最原始的模型是 HTTP 短轮询前端每隔几秒发一次 GET 请求问服务器「有没有新消息」。这种模型在 Linux 上用 PHP 或 Python 写起来最简单但服务器会被无意义的空请求刷爆。我做过一次测试50 个客户端、轮询间隔 3 秒Nginx 的每秒请求数直接多了 17 个数据库连接池很快耗尽。第二种是长轮询Long Polling客户端发请求后服务器先挂起连接等有新消息才返回然后客户端再次发起请求。这种模型比短轮询少了很多无效请求但每次消息推送都要重新建立 HTTP 连接握手开销依然存在。第三种是 WebSocket也是目前主流即时通讯的选择。它通过一次 HTTP 握手升级为 TCP 长连接服务器可以主动向客户端推数据双向实时通信。在 Linux 下实现 WebSocket 服务端语言选择很关键。我用一张表格对比常用方案方案并发能力实现复杂度适合场景Python websockets单机数千连接低约 100 行代码学习原型、内部工具Node.js ws单机数万连接低事件驱动生产级轻量 IMPHP Swoole单机数万连接中需扩展已有 PHP 技术栈Go gorilla/websocket单机数十万连接中需处理协程高并发业务如果你只在 Linux 上做实验我建议先用 Python因为 Linux 系统安装 Python 通常只差一条命令。如果考虑生产Node.js 的生态更成熟后面我会给出对应代码。2.2 为什么 WebSocket 在 Linux 上表现更稳定Linux 对 TCP 连接的处理能力由内核参数决定。WebSocket 建立在 TCP 之上所以必须理解几个关键点。首先WebSocket 握手时客户端会发送一个Sec-WebSocket-Key服务端用 SHA-1 加魔数计算后返回Sec-WebSocket-Accept。这个过程的资源开销远小于 HTTP 轮询的头信息重复传输。其次WebSocket 长连接会占用文件描述符FD。Linux 默认ulimit -n是 1024这意味着只开一个聊天室进程超过 1024 个连接就会报 Too many open files。生产环境必须调大。常见做法是修改/etc/security/limits.conf和/etc/sysctl.conf。另外长连接下会有空闲连接占用系统资源所以必须有心跳机制。WebSocket 的 Ping/Pong 帧就是为这个设计的。服务端每 30 秒发一次 Ping客户端回 Pong如果连续三次没收到就判定连接断开释放资源。3. 用 Python 在 Linux 上跑通最小聊天室3.1 环境准备与依赖安装在 Linux 上跑 Python 聊天室先把 Python 装好。不同发行版命令不同# Debian/Ubuntu sudo apt update sudo apt install -y python3 python3-pip # CentOS/RHEL 8 sudo yum install -y python3 python3-pip然后安装 WebSocket 库和异步框架sudo pip3 install websockets我选用websockets这个库因为它基于 asyncio不需要额外装包管理器代码结构非常清晰。如果系统自带 Python 3.6 以下建议先升级因为websockets对 asyncio 的支持在 3.7 版本之后更完善。这里有个提到过的问题Linux 系统安装 Python 之后pip 安装的包可能会被系统包管理器影响所以最好用虚拟环境。常见做法是python3 -m venv chatenv source chatenv/bin/activate pip install websockets虚拟环境的好处是隔离依赖不会污染系统全局的 Python升级系统自带的 Python 包时也不会弄坏聊天室环境。3.2 服务端代码一个支持多房间的聊天室下面是一个完整的服务端实现文件名为chat_server.pyimport asyncio import websockets # 存储所有连接的客户端room_name - set(websocket) rooms {} async def handle_client(websocket, path): # 路径格式/room_name room_name path.strip(/) or lobby if room_name not in rooms: rooms[room_name] set() rooms[room_name].add(websocket) print(f[] {websocket.remote_address} 加入房间 {room_name}当前人数 {len(rooms[room_name])}) try: async for message in websocket: # 收到消息后广播给同房间所有客户端 if room_name in rooms: broadcast f{websocket.remote_address}: {message} # 将消息逐条发送给房间内每个客户端 for client in list(rooms[room_name]): try: await client.send(broadcast) except websockets.ConnectionClosed: # 发送失败的客户端已断开稍后清理 pass except websockets.ConnectionClosed: pass finally: # 客户端离开移除连接并清理空房间 rooms[room_name].discard(websocket) if not rooms[room_name]: del rooms[room_name] print(f[-] {websocket.remote_address} 离开剩余连接{len(rooms.get(room_name, []))}) async def main(): # 监听 0.0.0.0允许外网访问 async with websockets.serve(handle_client, 0.0.0.0, 8765): print(聊天室已启动监听端口 8765) await asyncio.Future() # 永久运行 if __name__ __main__: asyncio.run(main())启动服务python3 chat_server.py这段代码的核心逻辑是每个 WebSocket 连接进来后根据 URL 路径加入对应的房间集合。收到消息后遍历集合中的每个连接并发送消息。注意我用了list(rooms[room_name])来复制一份客户端列表因为在遍历过程中删除元素会报错。如果某个客户端发送失败说明连接已经断开在finally块中统一清理。3.3 客户端测试使用命令行 WebSocket 工具服务端写好了怎么测试如果你用浏览器开发者工具手写 JS 比较麻烦。我建议使用命令行的websocat它在 Linux 下非常轻量# 安装 websocat sudo apt install -y websocat # 连接聊天室路径指定为 test websocat ws://127.0.0.1:8765/test打开两个终端窗口都执行这个命令。在一个窗口输入文本另一个窗口就能收到。如果想模拟浏览器行为用 Python 写个客户端脚本更直观import asyncio import websockets async def client(): async with websockets.connect(ws://127.0.0.1:8765/test) as ws: await ws.send(大家好我是第一个客户端) while True: response await ws.recv() print(f收到: {response}) asyncio.run(client())这样最快能验证服务端是否正常。如果连接失败优先检查防火墙sudo ufw allow 8765或者firewall-cmd --add-port8765/tcp。另外服务端必须监听0.0.0.0而不是127.0.0.1否则外部机器访问不到。4. 把聊天室做成可维护的服务部署、反代与进程管理4.1 用 systemd 托管聊天室进程前面直接python3 chat_server.py启动一旦终端关闭进程就没了。生产环境要用 systemd 托管。在 Linux 下创建/etc/systemd/system/chat.service[Unit] DescriptionPython Chat Server Afternetwork.target [Service] Typesimple Userwww-data WorkingDirectory/opt/chat ExecStart/opt/chat/chatenv/bin/python3 /opt/chat/chat_server.py Restartalways RestartSec3 [Install] WantedBymulti-user.target启用并启动sudo systemctl daemon-reload sudo systemctl enable chat sudo systemctl start chat sudo systemctl status chat关键参数User指定运行用户不要用 root 运行网络服务这能降低提权风险。Restartalways确保进程崩溃后自动拉起。如果改了代码执行sudo systemctl restart chat生效。查看日志用journalctl -u chat -f这个命令能在实时滚动显示聊天室的连接日志。4.2 Nginx 反向代理与 WebSocket 升级聊天室直接暴露 8765 端口不太安全也不方便加 SSL。用 Nginx 做反向代理把/ws/路径转发到本地 WebSocket 服务。这是最稳妥的 Linux 聊天室部署方式。先安装 Nginxsudo apt install -y nginx然后在/etc/nginx/sites-available/chat.conf写配置server { listen 80; server_name chat.example.com; location /ws/ { proxy_pass http://127.0.0.1:8765; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }软链接到启用目录并重载配置sudo ln -s /etc/nginx/sites-available/chat.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx注意proxy_pass http://127.0.0.1:8765;后面没有加路径这样会把完整的/ws/xxx传递给后端。如果你写的是proxy_pass http://127.0.0.1:8765/;Nginx 会自动改写路径把/ws/test变成/test导致房间名出错。proxy_read_timeout必须设置得足够大如果保持默认的 60 秒一个连接 60 秒没有消息交换就被 Nginx 掐断聊天室会频繁掉线。4.3 连接数与内核参数调优聊天室上线后需要根据在线人数调整 Linux 内核参数。我遇到过的问题是连接数一多服务端频繁报EMFILE。这通常是文件描述符限制导致的。修改/etc/security/limits.conf加入www-data soft nofile 65536 www-data hard nofile 65536同时调整内核路由缓存和 TCP 配置在/etc/sysctl.conf追加net.core.somaxconn 1024 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_tw_reuse 1执行sudo sysctl -p生效。somaxconn影响的是服务端监听的连接队列长度如果聊天室有大量突发的连接请求默认 128 可能不够。tcp_fin_timeout缩短半连接关闭时间避免大量 TIME_WAIT 状态的连接占用资源。调完之后用ss -s查看当前 socket 状态如果timewait数量减少数百个说明参数生效。5. 聊天室进阶身份认证、消息持久化与安全加固5.1 用 token 替代裸连接防止匿名刷屏现在的代码允许任何人连接聊天室很容易被垃圾消息刷爆。加一个简单的 token 认证。在客户端连接时携带参数如ws://127.0.0.1:8765/lobby?tokensecret。服务端解析 tokenfrom urllib.parse import urlparse, parse_qs async def handle_client(websocket, path): query urlparse(path).query token parse_qs(query).get(token, [])[0] if token ! my_secret_token: await websocket.close(code4001, reasonunauthorized) return # 继续处理...在 Nginx 层也可以用secure_link或auth_request做前置校验但最简单的还是应用层校验。生产环境应使用动态 token比如登录后发放 JWT聊天室服务不保存用户状态只是校验签名。5.2 消息持久化SQLite 与 Redis 的取舍聊天室的消息是否需要保存如果要做历史记录回放就需要持久化。常见做法是轻量场景用 SQLite高并发场景用 Redis 做消息队列再异步落库。我一般用 Python 内置的sqlite3不引入额外依赖import sqlite3 import json import time conn sqlite3.connect(/opt/chat/history.db) conn.execute(CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, room TEXT, username TEXT, content TEXT, created_at REAL )) def save_message(room, username, content): conn.execute(INSERT INTO messages (room, username, content, created_at) VALUES (?,?,?,?), (room, username, content, time.time())) conn.commit()在广播消息之前调用save_message即可。但注意SQLite 同一时刻只允许一个写入者如果聊天室消息量很大写操作会阻塞 onmessage 循环。改进的做法是把消息放入asyncio.Queue由一个独立的写线程批量落库。这样聊天的实时性不受磁盘 IO 影响。如果消息量更大可以引入 Redis 的RPUSH将消息追加到 List然后定时用LMIGRATE或者LRANGE批量写入数据库。但说句实在话一个团队内部聊天室能到每秒 100 条消息已经需要做性能排查了SQLite 加异步写基本够用。5.3 安全加固防止 Linux 下的提权与注入聊天室作为面向网络的进程是最容易成为攻击目标的入口之一。前面所提到的运行用户千万不要用 root。即便服务被攻破攻击者拿到的也是 www-data 权限无法直接读取/etc/shadow。如果服务本身有漏洞攻击者通过 WebSocket 发送精心构造的消息就可能执行系统命令。所以需要对消息内容做转义。如果前端是浏览器消息里包含script标签就会触发 XSS。服务端不能认为客户端都可信必须做过滤import html safe_content html.escape(message, quoteTrue)把所有,,转成实体。如果还要支持 Markdown 或代码块建议用成熟库解析而不是自己拼 HTML。另外要限制单条消息长度。网络层会给每个客户端设定max_size限制async with websockets.serve(handle_client, 0.0.0.0, 8765, max_size4096, ping_interval30, ping_timeout10):max_size4096表示单条消息最大 4KB超过直接断开连接。ping_interval30和ping_timeout10是心跳参数30 秒发一次 Ping10 秒内收不到 Pong 就断开这样可以保证服务端不会积累僵尸连接导致内存泄漏。验证连接数是否异常可以用ss -tan | grep :8765 | wc -l查看当前连接数。还可以定期用lsof -i :8765查看具体客户端 IP发现大量来自同一 IP 的连接就可以在 Nginx 层做并发限制limit_conn_zone $binary_remote_addr zonechat_addr:10m; limit_conn chat_addr 5;这行配置将同一 IP 的并发连接限制为 5 个有效防止单台机器耗尽服务器连接资源。在经历了这些参数调整和代码加固之后你的 Linux 聊天室已经具备了一个小型生产级即时通讯系统的雏形。最后用systemctl restart chat把改动全部加载再用websocat模拟两个客户端互发消息同时观察journalctl -u chat -f的输出就能确认整个链路是否健康。本文还有配套的精品资源点击获取
返回列表