Linux利用多线程和线程同步实现一个简单的聊天服务器
1. 概述
本文实现一个基于TCP/IP的简单多人聊天室程序。它包含一个服务器端和一个客户端:服务器能够接收多个客户端的连接,并将任何一个客户端发来的消息广播给所有其他连接的客户端;客户端则可以连接到服务器,发送消息并接收来自其他人的消息。该Demo运用了网络编程(Socket API)、多线程(Pthreads)以及线程同步(互斥锁)技术,以实现并发处理和数据共享安全。
2. 核心技术
-
网络编程(Sockets)
- TCP/IP: 选择面向连接的TCP协议,保证数据传输的可靠性。
- 服务器端流程:
socket()
: 创建套接字。memset()
/struct sockaddr_in
: 配置服务器地址和端口。bind()
: 绑定套接字到指定地址和端口。listen()
: 设置套接字为监听状态,等待连接。accept()
: 接受客户端连接,为每个连接创建一个新的套接字。
- 客户端流程:
socket()
: 创建套接字。memset()
/struct sockaddr_in
: 配置服务器地址和端口。connect()
: 连接到服务器。
- 数据传输:
read()
和write()
用于双向通信。
-
多线程 (Pthreads)
- 服务器端:
- 主线程负责
accept()
连接。 - 每接受一个新客户端,使用
pthread_create()
创建一个新的处理线程 (handle_clnt
)。 - 使用
pthread_detach()
将子线程设置为分离状态,使其结束后资源能自动回收,主线程无需join
。
- 主线程负责
- 客户端:
- 创建两个核心线程:
send_msg
线程:负责获取用户键盘输入并将其发送到服务器。recv_msg
线程:负责接收服务器广播的消息并显示在控制台。
- 这种设计使得用户输入和消息接收可以并行进行,互不阻塞。
- 创建两个核心线程:
- 服务器端:
-
线程同步 (Mutex)
- 场景: 服务器端多个
handle_clnt
线程会并发访问和修改共享资源(如客户端套接字数组clnt_socks
和当前客户端计数clnt_cnt
)。 - 机制: 使用互斥锁 (
mutx
) 保护这些临界区。pthread_mutex_init()
: 初始化互斥锁。pthread_mutex_lock()
: 在访问共享资源前加锁。pthread_mutex_unlock()
: 访问完毕后解锁。
- 关键操作加锁:
- 添加新客户端到
clnt_socks
。 - 从
clnt_socks
移除断开连接的客户端。 send_msg
(服务器端广播函数) 遍历clnt_socks
时。
- 添加新客户端到
- 场景: 服务器端多个
3. 主要模块实现
A. 服务器端 (server
)
main()
函数:- 参数解析 (端口号)。
- 初始化互斥锁。
- 完成socket的创建、绑定、监听。
- 进入无限循环,通过
accept()
接收客户端连接。 - 为每个连接创建
handle_clnt
线程并分离。
handle_clnt(void* arg)
函数:- 获取传递过来的客户端套接字。
- 循环调用
read()
接收该客户端的消息。 - 若
read()
成功,则调用send_msg()
(服务器的) 广播此消息。 - 若
read()
返回0 (客户端关闭连接),则执行清理:加锁 -> 从clnt_socks
移除 ->clnt_cnt--
-> 解锁 ->close()
该客户端套接字。
send_msg(char* msg, int len)
函数 (服务器端):- 加锁。
- 遍历
clnt_socks
数组,将消息write()
给每一个已连接的客户端。 - 解锁。
B. 客户端 (client
)
main()
函数:- 参数解析 (服务器IP, 端口号, 用户名)。
- 创建socket并
connect()
到服务器。 - 创建
send_msg
和recv_msg
两个线程。 pthread_join()
等待这两个线程结束(虽然当前send_msg
中的exit(0)
会提前终止)。
send_msg(void* arg)
函数:- 循环获取用户标准输入 (
fgets
)。 - 检测到 "q" 或 "Q" 时,
close(sock)
并exit(0)
(可改进点)。 - 将用户名和消息格式化后通过
write()
发送给服务器。
- 循环获取用户标准输入 (
recv_msg(void* arg)
函数:- 循环调用
read()
从服务器接收消息。 - 将接收到的消息
fputs()
到标准输出。
- 循环调用
4. 总结
- 互斥锁的必要性: 在多线程环境下,若不使用同步机制保护共享数据,会导致数据竞争和不可预期的结果。
clnt_socks
和clnt_cnt
的并发修改是典型场景。 - 线程分离 vs. 等待: 服务器端
pthread_detach
的使用简化了主线程的管理,适用于这种“即发即忘”的独立工作单元。客户端pthread_join
的意图是等待线程完成,但需配合更优雅的线程退出信号。 - 阻塞I/O与多线程: 每个客户端一个线程,每个线程中的
read()
是阻塞的。这简化了单个线程的逻辑,但当连接数非常大时,线程资源开销会成为瓶颈。 - 客户端非阻塞体验: 通过发送和接收分离到不同线程,客户端用户体验得到了提升,不会因为等待网络消息而卡住输入。
- 基本通信协议: 客户端在发送消息前简单地将用户名预置到消息体中,服务器直接转发这个消息体。这是一个非常初级的“协议”。
具体代码如下:
服务端代码:网络编程 + 多线程 + 线程同步
// 网络编程+多线程+线程同步实现的聊天服务器和客户端#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>#define BUF_SIZE 100 // 定义缓冲区大小
#define MAX_CLNT 256 // 最大客户端数量// 函数声明
void * handle_clnt(void * arg); // 处理客户端连接的线程函数
void send_msg(char * msg, int len); // 向所有客户端发送消息
void error_handling(char * msg); // 错误处理函数int clnt_cnt = 0; // 当前客户端连接数量
int clnt_socks[MAX_CLNT]; // 存储所有客户端的socket描述符
pthread_mutex_t mutx; // 互斥锁,用于同步对共享资源的访问(客户端数组)int main(int argc, char *argv[])
{int serv_sock, clnt_sock; // 服务端socket和客户端socketstruct sockaddr_in serv_adr, clnt_adr; // 服务端和客户端地址int clnt_adr_sz; // 客户端地址结构的大小pthread_t t_id; // 线程IDif(argc != 2) {printf("Usage : %s <port>\n", argv[0]); // 检查输入的端口号参数exit(1);}pthread_mutex_init(&mutx, NULL); // 初始化互斥锁serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 创建服务端socketif(serv_sock == -1) {error_handling("socket() error");}memset(&serv_adr, 0, sizeof(serv_adr)); // 初始化服务端地址结构serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用接口serv_adr.sin_port = htons(atoi(argv[1])); // 使用命令行提供的端口号if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) // 绑定服务端socketerror_handling("bind() error");if(listen(serv_sock, 5) == -1) // 开始监听error_handling("listen() error");while(1){clnt_adr_sz = sizeof(clnt_adr); // 获取客户端地址大小clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); // 接受客户端连接// 添加新的客户端socket到数组pthread_mutex_lock(&mutx); // 获取互斥锁,确保线程安全clnt_socks[clnt_cnt++] = clnt_sock; // 增加客户端到客户端数组pthread_mutex_unlock(&mutx); // 释放互斥锁// 创建新线程来处理客户端pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);pthread_detach(t_id); // 将线程分离,避免主线程等待printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); // 输出客户端IP地址}close(serv_sock); // 关闭服务端socketreturn 0;}// 处理客户端的函数
void * handle_clnt(void * arg)
{int clnt_sock = *((int*)arg); // 获取客户端socketint str_len = 0, i;char msg[BUF_SIZE]; // 缓冲区while((str_len = read(clnt_sock, msg, sizeof(msg))) != 0) // 读取客户端发送的消息send_msg(msg, str_len); // 将消息转发给所有客户端// 客户端断开连接后,移除客户端pthread_mutex_lock(&mutx); // 获取互斥锁for(i = 0; i < clnt_cnt; i++) // 查找并移除断开的客户端{if(clnt_sock == clnt_socks[i]){while(i++ < clnt_cnt - 1) // 将后续客户端前移clnt_socks[i] = clnt_socks[i + 1];break;}}clnt_cnt--; // 客户端数量减一pthread_mutex_unlock(&mutx); // 释放互斥锁close(clnt_sock); // 关闭客户端socketreturn NULL;}// 向所有客户端发送消息
void send_msg(char * msg, int len)
{int i;pthread_mutex_lock(&mutx); // 获取互斥锁,保护共享资源(客户端socket数组)for(i = 0; i < clnt_cnt; i++) // 向所有连接的客户端发送消息write(clnt_socks[i], msg, len);pthread_mutex_unlock(&mutx); // 释放互斥锁
}// 错误处理函数
void error_handling(char * msg)
{fputs(msg, stderr); // 输出错误信息fputc('\n', stderr);exit(1); // 退出程序
}
客户端代码:网络编程 + 多线程
// 客户端程序:网络编程+多线程实现的聊天客户端#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 100 // 定义消息的最大长度
#define NAME_SIZE 20 // 定义用户名的最大长度// 函数声明
void * send_msg(void * arg); // 发送消息的线程函数
void * recv_msg(void * arg); // 接收消息的线程函数
void error_handling(char * msg); // 错误处理函数// 用户名和消息缓冲区
char name[NAME_SIZE] = "[DEFAULT]"; // 默认用户名
char msg[BUF_SIZE]; // 用于存储用户输入的消息int main(int argc, char *argv[])
{int sock;struct sockaddr_in serv_addr; // 服务器地址结构pthread_t snd_thread, rcv_thread; // 发送和接收消息的线程void * thread_return;// 检查命令行参数,确保提供了 IP、端口和用户名if(argc != 4) {printf("Usage : %s <IP> <port> <name>\n", argv[0]);exit(1);}// 设置客户端用户名sprintf(name, "[%s]", argv[3]);// 创建客户端socketsock = socket(PF_INET, SOCK_STREAM, 0);// 初始化服务器地址结构memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 获取服务器的IP地址serv_addr.sin_port = htons(atoi(argv[2])); // 获取服务器的端口号// 连接到服务器if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)error_handling("connect() error");// 创建发送和接收消息的线程pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);// 等待两个线程结束pthread_join(snd_thread, &thread_return);pthread_join(rcv_thread, &thread_return);close(sock); // 关闭客户端socketreturn 0;}// 发送消息的线程函数
void * send_msg(void * arg)
{int sock = *((int*)arg); // 获取客户端socketchar name_msg[NAME_SIZE + BUF_SIZE]; // 用于存储带有用户名的消息while(1) {fgets(msg, BUF_SIZE, stdin); // 获取用户输入的消息// 如果输入为 "q" 或 "Q",则退出程序if(!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) {close(sock); // 关闭socket连接exit(0); // 退出程序}// 将用户名和消息合并成一个字符串sprintf(name_msg, "%s %s", name, msg);// 发送合并后的消息到服务器write(sock, name_msg, strlen(name_msg));}return NULL; // 返回空值}// 接收消息的线程函数
void * recv_msg(void * arg)
{int sock = *((int*)arg); // 获取客户端socketchar name_msg[NAME_SIZE + BUF_SIZE]; // 用于存储带有用户名的消息int str_len;while(1){// 从服务器读取消息str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);if(str_len == -1) // 如果读取失败,返回错误return (void*)-1;name_msg[str_len] = 0; // 将读取的字符串以 null 结尾fputs(name_msg, stdout); // 输出服务器发来的消息}return NULL; // 返回空值}// 错误处理函数
void error_handling(char *msg)
{fputs(msg, stderr); // 将错误消息输出到标准错误fputc('\n', stderr); // 输出换行符exit(1); // 退出程序
}
相关文章:
Linux利用多线程和线程同步实现一个简单的聊天服务器
1. 概述 本文实现一个基于TCP/IP的简单多人聊天室程序。它包含一个服务器端和一个客户端:服务器能够接收多个客户端的连接,并将任何一个客户端发来的消息广播给所有其他连接的客户端;客户端则可以连接到服务器,发送消息并接收来自…...
【计网】作业5
待补充 212.56.132.0/24 212.56.1000 0100.0 212.56.133.0/24 212.56.1000 0101.0 212.56.134.0/24 212.56.1000 0110.0 212.56.135.0/24 212.56.1000 0111.0 最小的212.56.1000 0100.0 四个,2^2 212.56.132.0/22 1111 1111.1111 1111.1111 1100.0000 0000 255.255.…...
15、Python布尔逻辑全解析:运算符优先级、短路特性与实战避坑指南
适合人群:零基础自学者 | 编程小白快速入门 阅读时长:约6分钟 文章目录 一、问题:Python布尔值的底层原理?1、例子1:电路开关模型解析布尔本质2、例子2:特殊的布尔类型值为False3、答案:(1&…...

Nginx基础知识
Nginx是什么? Nginx 是一款高性能的 Web 服务器、反向代理服务器和负载均衡器,以其高并发处理能力和低内存消耗著称。以下是 Nginx 的基础知识和常见配置示例: 1. 核心概念 • 配置文件位置:通常为 /etc/nginx/nginx.conf 或 /us…...

Vue-监听属性
监听属性 简单监听 点击切换名字,来回变更Tom/Jerry,输出 你好,Tom/Jerry 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>监听属性</title><!-- …...

python fastapi + react, 写一个图片 app
1. 起因, 目的: 上厕所的时候,想用手机查看电脑上的图片,但是又不想点击下载。此app 应运而生。 2. 先看效果 单击图片,能放大图片 3. 过程: 过程很枯燥。有时候, 有一堆新的想法。 但是做起来太麻烦,…...
nginx集成防火墙ngx_waf的docker版
由于公网的环境越来与严峻,所以想找一个nginx带防火墙的版本 调研过openresty,大部分集成redis了,感觉还是太重了,有一个不那么重的https://github.com/unixhot/waf 但是维护没有那么勤,最后维护是5年前,倒…...

vscode c++编译onnxruntime cuda 出现的问题
问题描述 将onnx的dll文件和lib文件copy到可执行文件所在文件夹下后,现象: 双击可执行文件能正常运行 在vscode中点击cmake插件的运行按钮出现报错为 c [ONNXRuntimeError] : 1 : FAIL : LoadLibrary failed with error 126 “” when trying to load尝试…...
sts下载安装
windows下STS(Spring Tools Suite,自带spring插件的eclipse)的下载与安装_sts下载-CSDN博客Spring Boot安装与配置教程_spring boot安装配置-CSDN博客...

中服云生产线自动化智能化调度生产系统:打造智能制造新标杆
前言 在当今制造业竞争日益激烈的背景下,实现生产线的自动化与智能化已成为企业提升竞争力的关键。作为国内技术领先的工业物联网平台、数字孪生、自动控制技术厂商,中服云凭借其深厚的技术积累和创新能力,打造了一套完整的生产线自动化智能…...
next.js实现项目搭建
一、创建 Next.js 项目的步骤 1、安装 npx create-next-applatest # 或 yarn create next-app # 或 pnpm create next-app 按照交互式提示配置你的项目: 输入项目名称 选择是否使用 TypeScript 选择是否启用 ESLint 选择是否启用 Tailwind CSS 选择是否使用 s…...
Redisson 四大核心机制实现原理详解
一、可重入锁(Reentrant Lock) 可重入锁是什么? 通俗定义 可重入锁类似于一把“智能锁”,它能识别当前的锁持有者是否是当前线程: 如果是,则允许线程重复获取锁(重入),并…...

云鼎入鼎系统:一站式电商管理解决方案
个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[2435024119qq.com] 📱个人微信&a…...

Leetcode134加油站
题目链接 134 题意图解: 题目给了n个节点,这些节点呈现环状,每次到一个低点要消耗cost[i]的油量。 从中我们可以得出一个结论:看一个点能不能到下一个点,就要用当前的油量减去消耗的量,那么gas[i] - cost…...

关于Android Studio for Platform的使用记录
文章目录 简单介绍如何使用配置导入aosp工程配置文件asfp-config.json 简单介绍 Android Studio for Platform是google最新开发,用来阅读aosp源码的工具 详细的资料介绍: https://developer.android.google.cn/studio/platform 将工具下载下来直接点击…...
Linux的内存泄漏问题及排查方法
内存泄漏是指在计算机程序中,已不再被使用的内存未被正确释放,导致内存占用随时间累积,进而引发系统内存不足、性能下降甚至崩溃的问题。在Linux系统中,开发者和运维人员可通过以下方法排查和解决内存泄漏问题: 1. 使…...

uniapp 微信小程序 获取openId
嗨,我是小路。今天主要和大家分享的主题是“uniapp 微信小程序 获取openId”。 一、主要属性 1.uni.login 二、实例代码 1、前端代码 uni.login({provider: weixin,success: (res) > {uni.showLoading({title: 登录中...,mask: true})let code res.…...

隧道结构安全在线监测系统解决方案
一、方案背景 隧道是地下隐蔽工程,会受到潜在、无法预知的地质因素影响。随着我国公路交通建设的发展,隧道占新建公路里程的比例越来越大。隧道属于线状工程,有的规模较大,可长达几公里或数十公里,往往穿越许多不同环境…...

Docker 运维管理
Docker 运维管理 一、Swarm集群管理1.1 Swarm的核心概念1.1.1 集群1.1.2 节点1.1.3 服务和任务1.1.4 负载均衡 1.2 Swarm安装准备工作创建集群添加工作节点到集群发布服务到集群扩展一个或多个服务从集群中删除服务ssh免密登录 二、Docker Compose与 Swarm 一起使用 Compose 三…...
【Redis】快速列表结构
目录 1、背景2、压缩列表【1】底层结构【2】特性【3】优缺点 1、背景 redis的quicklist(快速列表)是一个双向链表,其中每个节点都是一个ziplist(压缩列表)。这中结构结合了双向链表和压缩列表的优点,在内存…...
阿里巴巴 1688 数据接口开发指南:构建自动化商品详情采集系统
在电商行业数据驱动决策的趋势下,高效获取商品详情数据成为企业洞察市场、优化运营的关键。通过阿里巴巴 1688 数据接口构建自动化商品详情采集系统,能够快速、精准地采集海量商品信息。本文将从开发准备、接口分析、代码实现等方面,详细介绍…...

[SpringBoot]Spring MVC(2.0)
紧接上文,这篇我们继续讲剩下的HTTp请求 传递JSON数据 简单来说:JSON就是⼀种数据格式,有⾃⼰的格式和语法,使⽤⽂本表⽰⼀个对象或数组的信息,因此JSON本质是字符串. 主要负责在不同的语⾔中数据传递和交换 JSON的语法 1. 数据在 键值对(Key/Value) …...

Golang的网络安全策略实践
Golang的网络安全策略实践 一、理解网络安全的重要性 当今的网络环境中,安全问题日益突出,各种类型的攻击如雨后春笋般涌现,给个人和组织的信息资产造成了严重威胁。因此,制定和实施有效的网络安全策略至关重要。 二、Golang在网络…...

STM32外设AD-轮询法读取模板
STM32外设AD-轮询法读取模板 一,什么是轮询?1,轮询法的直观理解2,轮询法缺点 二,CubeMX配置三,模板移植1,adc_app.c文件2,变量声明1,adc_app.c中2,mydefine.h…...
C++编程this指针练习
这段代码是用 C 编写的,定义了一个 Car 类(类是 C 中用于创建对象的蓝图),并通过 main() 函数创建和使用了该类的对象。下面是对整个程序的逐行解释,并在关键部分加上注释说明。 ✅ 代码整体功能: 定义一个…...

iOS音视频解封装分析
首先是进行解封装的简单的配置 /// 解封装配置 class KFDemuxerConfig {// 媒体资源var asset: AVAsset?// 解封装类型,指定是音频、视频或两者都需要var demuxerType: KFMediaType .avinit() {} }然后是实现解封装控制器 import Foundation import CoreMedia i…...

突破智能驾舱边界,Imagination如何构建高安全GPU+AI融合计算架构
日前,“第十二届汽车电子创新大会暨汽车芯片产业生态发展论坛(AEIF 2025)”在上海顺利举办。大会围绕汽车前沿性、关键性和颠覆性技术突破,邀请行业众多专家学者,分享与探讨了汽车电子产业的技术热点与发展趋势。在5月…...

DeepSeek 如何实现 128K 上下文窗口?
DeepSeek 如何实现 128K 上下文窗口?长文本处理技术揭秘 系统化学习人工智能网站(收藏):https://www.captainbed.cn/flu 文章目录 DeepSeek 如何实现 128K 上下文窗口?长文本处理技术揭秘摘要引言技术架构解析1. 动态…...
云计算简介:从“水电”到“数字引擎”的技术革命
云计算简介:从“水电”到“数字引擎”的技术革命 在当今数字化浪潮中,云计算早已从一个技术概念演变为支撑现代社会运转的核心基础设施。无论是你手机里的天气预报、电商购物的推荐系统,还是企业内部的ERP系统,背后都离不开云计算…...
计算圆周率 (python)
使用模特卡罗方法(模拟法),模拟撒点100000次,计算圆周率π 输入格式: 一个整数,表示随机数种子 输出格式: 计算的π值,结果小数点后保留5位数字 输入样例: 在这里给出一组输入。例如: 10…...