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.pswp.cn/web/80608.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80608.shtml
英文地址,請注明出處:http://en.pswp.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.…