Linux利用多线程和线程同步实现一个简单的聊天服务器

1. 概述

本文实现一个基于TCP/IP的简单多人聊天室程序。它包含一个服务器端和一个客户端:服务器能够接收多个客户端的连接,并将任何一个客户端发来的消息广播给所有其他连接的客户端;客户端则可以连接到服务器,发送消息并接收来自其他人的消息。该Demo运用了网络编程(Socket API)、多线程(Pthreads)以及线程同步(互斥锁)技术,以实现并发处理和数据共享安全。


2. 核心技术

  • 网络编程(Sockets)

    • TCP/IP: 选择面向连接的TCP协议,保证数据传输的可靠性。
    • 服务器端流程:
      1. socket(): 创建套接字。
      2. memset()/struct sockaddr_in: 配置服务器地址和端口。
      3. bind(): 绑定套接字到指定地址和端口。
      4. listen(): 设置套接字为监听状态,等待连接。
      5. accept(): 接受客户端连接,为每个连接创建一个新的套接字。
    • 客户端流程:
      1. socket(): 创建套接字。
      2. memset()/struct sockaddr_in: 配置服务器地址和端口。
      3. 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_msgrecv_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_socksclnt_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);  // 退出程序
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/80608.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu系统 | dify+ollama+deepseek搭建本地应用

1、安装 Ollama 下载并安装 Ollama (llm) wangqiangwangqiang:~$ curl -fsSL https://ollama.ai/install.sh | bash >>> Installing ollama to /usr/local >>> Downloading Linux amd64 bundle0.3% curl -fsSL https://ollama.ai/install.sh &#xff08;下…

从纸质契约到智能契约:AI如何改写信任规则与商业效率?​——从智能合约到监管科技,一场颠覆传统商业逻辑的技术革命

一、传统合同的“低效困境”&#xff1a;耗时、昂贵、风险失控 近年来&#xff0c;全球商业环境加速向数字化转型&#xff0c;但合同管理却成为企业效率的“阿喀琉斯之踵”。据国际商会&#xff08;International Chamber of Commerce&#xff09;数据显示&#xff0c;全球企业…

【机器学习|学习笔记】基于生成对抗网络的孪生框架(GAN-based Siamese framework,GSF)详解,附代码。

【机器学习|学习笔记】基于生成对抗网络的孪生框架(GAN-based Siamese framework,GSF)详解,附代码。 【机器学习|学习笔记】基于生成对抗网络的孪生框架(GAN-based Siamese framework,GSF)详解,附代码。 文章目录 【机器学习|学习笔记】基于生成对抗网络的孪生框架(G…

UEFI Spec 学习笔记---33 - Human Interface Infrastructure Overview---33.2.6 Strings

33.2.6 Strings UEFI 环境中的 string 是使用 UCS-2 格式定义&#xff0c;每个字符由 16bit 数据表示。对于用户界面&#xff0c;strings 也是一种可以安装到 HIIdatabase 的一种数据。 为了本土化&#xff0c;每个 string 通过一个唯一标识符来识别&#xff0c;而每一个标识…

Stable Diffusion 学习笔记02

模型下载网站&#xff1a; 1&#xff0c;LiblibAI-哩布哩布AI - 中国领先的AI创作平台 2&#xff0c;Civitai: The Home of Open-Source Generative AI 模型的安装&#xff1a; 将下载的sd模型放置在sd1.5的文件内即可&#xff0c;重启客户端可用。 外挂VAE模型&#xff1a…

并发编程(5)

抛异常时会释放锁。 当线程在 synchronized 块内部抛出异常时&#xff0c;会自动释放对象锁。 public class ExceptionUnlockDemo {private static final Object lock new Object();public static void main(String[] args) {Thread t1 new Thread(() -> {synchronized …

贵州某建筑物挡墙自动化监测

1. 项目简介 某建筑物位于贵州省某县城区内&#xff0c;靠近县城主干道&#xff0c;周边配套学校、医院、商贸城。建筑物临近凤凰湖、芙蓉江等水系&#xff0c;主打“湖景生态宜居”。改建筑物总占地面积&#xff1a;约5.3万平方米&#xff1b;总建筑面积&#xff1a;约15万平…

6个月Python学习计划:从入门到AI实战(前端开发者进阶指南)

作者&#xff1a;一名前端开发者的进阶日志 计划时长&#xff1a;6个月 每日学习时间&#xff1a;2小时 覆盖方向&#xff1a;Python基础、爬虫开发、数据分析、后端开发、人工智能、深度学习 &#x1f4cc; 目录 学习目标总览每日时间分配建议第1月&#xff1a;Python基础与编…

【FAQ】HarmonyOS SDK 闭源开放能力 —Vision Kit (3)

1.问题描述&#xff1a; 通过CardRecognition识别身份证拍照拿到的照片地址&#xff0c;使用该方法获取不到图片文件&#xff0c;请问如何解决&#xff1f; 解决方案&#xff1a; //卡证识别实现页&#xff0c;文件名为CardDemoPage&#xff0c;需被引入至入口页 import { …

AI全域智能监控系统重构商业清洁管理范式——从被动响应到主动预防的监控效能革命

一、四维立体监控网络技术架构 1. 人员行为监控 - 融合人脸识别、骨骼追踪与RFID工牌技术&#xff0c;身份识别准确率99.97% - 支持15米超距夜间红外监控&#xff08;精度0.01lux&#xff09; 2. 作业过程监控 - UWB厘米级定位技术&#xff08;误差&#xff1c;0.3米&…

安全强化的Linux

SElinux简介 SELinux是security-Enhanced Linux的缩写,意思是安全强化的linux SELinux主要由美国国家安全局(NSA)开发,当初开发的目的是为了避免资源的误用。传统的访问控制在我们开启权限后,系统进程可以直接访问 当我们对权限设置不严谨时,这种访问方式就是系统的安全漏洞 在…

机器学习第十六讲:K-means → 自动把超市顾客分成不同消费群体

机器学习第十六讲&#xff1a;K-means → 自动把超市顾客分成不同消费群体 资料取自《零基础学机器学习》。 查看总目录&#xff1a;学习大纲 关于DeepSeek本地部署指南可以看下我之前写的文章&#xff1a;DeepSeek R1本地与线上满血版部署&#xff1a;超详细手把手指南 K-me…

spring中yml配置上下文与tomcat等外部容器不一致问题

结论&#xff1a;外部优先级大于内部 在 application.yml 中配置了&#xff1a; server:port: 8080servlet:context-path: /demo这表示你的 Spring Boot 应用的上下文路径&#xff08;context-path&#xff09;是 /demo&#xff0c;即访问你的服务时&#xff0c;URL 必须以 /d…

论文研读——《AnomalyGPT:使用大型视觉语言模型检测工业异常》

这篇论文提出了 AnomalyGPT&#xff0c;一个基于大型视觉语言模型的工业异常检测框架&#xff0c;首次将通用多模态对话能力引入工业视觉场景&#xff0c;通过引入图像解码器增强像素级感知&#xff0c;设计 Prompt 学习器实现任务自适应控制&#xff0c;并利用合成异常样本解决…

供应链安全检测系列技术规范介绍之一|软件成分分析

软件成分分析的概念及意义 软件成分分析Software Compostition Analysis&#xff08;SCA&#xff09;是一种用于管理开源组件应用安全的方法。软件成分分析系统可以快速跟踪和分析应用软件的开源组件&#xff0c;发现相关组件、支持库以及它们之间直接和间接依赖关系&#xff0…

conda更换清华源

1、概览 anaconda更换速度更快、更稳定的下载源&#xff0c;在linux环境测试通过。 2、conda源查看 在修改之前可以查看下现有conda源是什么&#xff0c;查看conda配置信息&#xff0c;如下&#xff1a; cat ~/.condarc 可以看到你的conda源&#xff0c;以我的conda源举例&am…

Docker配置容器开机自启或服务重启后自启

要将一个 Docker 容器设置为开机自启&#xff0c;你可以使用 docker update 命令或配置 Docker 服务来实现。以下是两种常见的方法&#xff1a; 方法 1&#xff1a;使用 docker update 设置容器自动重启 使用 docker update 设置容器为开机自启 你可以使用以下命令&#xff0c…

Flink 的水印机制

Apache Flink 的 水印机制&#xff08;Watermark Mechanism&#xff09; 主要用于解决 事件时间流中的乱序问题&#xff08;Out-of-Order Events&#xff09;&#xff0c;确保窗口&#xff08;Window&#xff09;能够在合适的时间触发计算&#xff0c;从而提供准确、一致的处理…

【每天一个知识点】embedding与representation

“Embedding&#xff08;嵌入&#xff09;”与“Representation&#xff08;表示&#xff09;”在机器学习、自然语言处理&#xff08;NLP&#xff09;、图神经网络等领域常被使用&#xff0c;它们密切相关&#xff0c;但语义上有一定区别。 一、定义 1. Representation&#…

SpringBoot(二)--- SpringBoot基础(http协议、分层解耦)

目录 前言 一、SpringBoot入门 1.入门程序 2.解析 二、HTTP协议 1.HTTP概述 2.HTTP请求协议 2.1 GET方式的请求协议 2.2 POST方式的请求协议 2.3 两者的区别 2.4 获取请求数据 3.HTTP响应协议 三、分层解耦 1.三层架构 2.IOC&DI 2.1 入门 2.2 IOC详解 2.…