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

news2025/5/20 12:24:52

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和客户端socket
    struct sockaddr_in serv_adr, clnt_adr;  // 服务端和客户端地址
    int clnt_adr_sz;  // 客户端地址结构的大小
    pthread_t t_id;  // 线程ID

    if(argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);  // 检查输入的端口号参数
        exit(1);
    }
    
    pthread_mutex_init(&mutx, NULL);  // 初始化互斥锁
    
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);  // 创建服务端socket
    if(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)  // 绑定服务端socket
        error_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);  // 关闭服务端socket
    return 0;

}

// 处理客户端的函数
void * handle_clnt(void * arg)
{
    int clnt_sock = *((int*)arg);  // 获取客户端socket
    int 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);  // 关闭客户端socket
    return 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]);
    
    // 创建客户端socket
    sock = 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);  // 关闭客户端socket
    return 0;

}

// 发送消息的线程函数
void * send_msg(void * arg)
{
    int sock = *((int*)arg);  // 获取客户端socket
    char 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);  // 获取客户端socket
    char 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.coloradmin.cn/o/2379958.html

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

相关文章

Nginx基础知识

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

Vue-监听属性

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

python fastapi + react, 写一个图片 app

1. 起因&#xff0c; 目的: 上厕所的时候&#xff0c;想用手机查看电脑上的图片&#xff0c;但是又不想点击下载。此app 应运而生。 2. 先看效果 单击图片&#xff0c;能放大图片 3. 过程: 过程很枯燥。有时候&#xff0c; 有一堆新的想法。 但是做起来太麻烦&#xff0c;…

vscode c++编译onnxruntime cuda 出现的问题

问题描述 将onnx的dll文件和lib文件copy到可执行文件所在文件夹下后&#xff0c;现象&#xff1a; 双击可执行文件能正常运行 在vscode中点击cmake插件的运行按钮出现报错为 c [ONNXRuntimeError] : 1 : FAIL : LoadLibrary failed with error 126 “” when trying to load尝试…

中服云生产线自动化智能化调度生产系统:打造智能制造新标杆

前言 在当今制造业竞争日益激烈的背景下&#xff0c;实现生产线的自动化与智能化已成为企业提升竞争力的关键。作为国内技术领先的工业物联网平台、数字孪生、自动控制技术厂商&#xff0c;中服云凭借其深厚的技术积累和创新能力&#xff0c;打造了一套完整的生产线自动化智能…

云鼎入鼎系统:一站式电商管理解决方案

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务) &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1;个人微信&a…

Leetcode134加油站

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

关于Android Studio for Platform的使用记录

文章目录 简单介绍如何使用配置导入aosp工程配置文件asfp-config.json 简单介绍 Android Studio for Platform是google最新开发&#xff0c;用来阅读aosp源码的工具 详细的资料介绍&#xff1a; https://developer.android.google.cn/studio/platform 将工具下载下来直接点击…

uniapp 微信小程序 获取openId

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

隧道结构安全在线监测系统解决方案

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

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 三…

[SpringBoot]Spring MVC(2.0)

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

Golang的网络安全策略实践

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

STM32外设AD-轮询法读取模板

STM32外设AD-轮询法读取模板 一&#xff0c;什么是轮询&#xff1f;1&#xff0c;轮询法的直观理解2&#xff0c;轮询法缺点 二&#xff0c;CubeMX配置三&#xff0c;模板移植1&#xff0c;adc_app.c文件2&#xff0c;变量声明1&#xff0c;adc_app.c中2&#xff0c;mydefine.h…

iOS音视频解封装分析

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

突破智能驾舱边界,Imagination如何构建高安全GPU+AI融合计算架构

日前&#xff0c;“第十二届汽车电子创新大会暨汽车芯片产业生态发展论坛&#xff08;AEIF 2025&#xff09;”在上海顺利举办。大会围绕汽车前沿性、关键性和颠覆性技术突破&#xff0c;邀请行业众多专家学者&#xff0c;分享与探讨了汽车电子产业的技术热点与发展趋势。在5月…

DeepSeek 如何实现 128K 上下文窗口?

DeepSeek 如何实现 128K 上下文窗口&#xff1f;长文本处理技术揭秘 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 DeepSeek 如何实现 128K 上下文窗口&#xff1f;长文本处理技术揭秘摘要引言技术架构解析1. 动态…

Python 实现图片浏览和选择工具

实现将截图预览&#xff0c;并按照顺序加入一个pdf文件中&#xff0c;实现照片管理尤其对于喜欢看教程截图做笔记的网友们。 C:\pythoncode\new\python-image-pdf-processor.py 界面展示 &#x1f9f1; 一、核心结构概述 主类 ImageViewer(wx.Frame) 是主窗口类&#xff0c;…

Python实现的在线词典学习工具

Python实现的在线词典学习工具 源码最初来自网络&#xff0c;根据实际情况进行了修改。 主要功能&#xff1a; 单词查询 通过Bing词典在线获取单词释义&#xff08;正则提取网页meta描述&#xff09;&#xff0c;支持回车键快速查询 内置网络请求重试和异常处理机制 在线网页…

BGP综合实验(2)

一、实验需求 1、实验拓扑图 2、实验需求 使用 PreVal 策略&#xff0c;让 R4 经 R2 到达 192.168.10.0/24 。 使用 AS_Path 策略&#xff0c;让 R4 经 R3 到达 192.168.11.0/24 。 配置 MED 策略&#xff0c;让 R4 经 R3 到达 192.168.12.0/24 。 使用 Local Preference 策…