首页 > 编程语言 >多线程并发聊天室简单实现代码详解 -- 涉及网络编程,多线程和线程同步的知识

多线程并发聊天室简单实现代码详解 -- 涉及网络编程,多线程和线程同步的知识

时间:2024-03-21 17:00:13浏览次数:19  
标签:addr socket -- server client 线程 pthread 多线程 客户端

        本项目主要完成多线程并发聊天室的基础功能,即多个客户端之间通过服务器可以实现群发消息,重点在于学习网络编程,多线程和线程同步的基础知识(基于Linux)。

        下面我会详解每一部分的代码。

1.主线程

        1.1首先由于是自己在电脑里面测试,所以可以自己开线程去验证,所以main函数如下

#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>
#include <semaphore.h>
sem_t semid;//信号量
char name[64] = "[DEFAULT]";//姓名,默认default
#define MAX_CLIENT 100  //最大可以连接的客户端数量
int client_socks[MAX_CLIENT]; //保存100个客户端的套接字信息
pthread_mutex_t mutex;//互斥量,线程同步

//上面为一些变量的定义和初始化
int main(int argc, char* argv[])
{
	memset(client_socks, -1, sizeof(client_socks));//初始化全为无效套接字
	invoke(argv[1]);//传入命令行的输入参数
	return 0;
}

1.2 main函数接收来自控制台的命令,然后开始执行Invoke函数来开启服务端和客户端,代码如下


void invoke(const char * arg)
{
	if (strcmp(arg, "s") == 0) {//检查输入的命令行的第二个参数,如果为s,表示开启服务端,否则开启客户端
		Server();
	}
	else {//其他任意字母
		Client();
	}
}

2.服务端

        2.1输入控制台的命令带s表示启动服务端,否则启动客户端,服务端要先启动,代码如下


void Server()
{
	//初始化
	int server_socket, client_socket;
	struct sockaddr_in server_addr, client_addr;
	server_socket = socket(PF_INET, SOCK_STREAM, 0);
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//在所有IP上监听
	server_addr.sin_family = AF_INET;//
	server_addr.sin_port = htons(9000);//端口
	pthread_mutex_init(&mutex, NULL);//初始化互斥量

	if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == -1){
		printf("bind error %d");
		close(server_socket);
		return;
	}
	if (listen(server_socket, 5) == -1) {
		printf("listen error\n");
		close(server_socket);
		return;
	}
	while (1) {
		socklen_t client_addr_size = sizeof(client_addr);
		client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
		if (client_socket == -1) {
			printf("accept error\n");
		}
		//将客户端处理交个线程
		pthread_mutex_lock(&mutex);//上锁
		int i = 0;
		for (; i < MAX_CLIENT; i++)//寻找空闲的数组位置,来存储客户端套接字
		{
			if (client_socks[i] == -1 ) {//找到空闲位置
				client_socks[i] = client_socket;//将得到的客户端套接字保存到客户端数套接字数组
				break;
			}
		}
		pthread_mutex_unlock(&mutex);//解锁
		pthread_t thread_id;//声明线程ID
		pthread_create(&thread_id, NULL, handle_client, &client_socks[i]);//创建线程 ,取到这个客户端套接字传给新线程去处理
	}
	close(server_socket);
	pthread_mutex_destroy(&mutex);//销毁互斥量
}

        

        2.2服务端主要设计网络套接字的初始化,然后循环接收来自客户端的连接,并开启一个线程去保持和客户端连接,保证主线程不阻塞,即实现了多线程,提高连接效率,减少客户端的等待时间。开启线程函数设计的函数指针是handle_client,即客户端处理函数,该函数代码如下。


void* handle_client(void* arg)//处理客户端消息
{
	pthread_detach(pthread_self());
	int client_sock = *(int*)arg;//解析套接字
	char msg[1024] = "";
	ssize_t str_len = 0;
	while ((str_len = read(client_sock, msg, sizeof(msg))) > 0) {//整个表达式就是read读到的字节数
		sendMessage(msg, str_len);
	}
	pthread_mutex_lock(&mutex);//上锁
	*(int*)arg = -1;//arg为对应套接字的地址,设置套接字
	pthread_mutex_lock(&mutex);//解锁
	close(client_sock);
	pthread_exit(NULL);
}

        2.3该函数循环和客户端保持连接,一旦收到客户端的消息就可以群发给其他在线客户端,服务段发送消息给所以客户端sendMessage函数如下

void sendMessage(const char* msg, ssize_t str_len)//转发消息给所有的客户端
{
	pthread_mutex_lock(&mutex);//上锁
	for (int i = 0; i < MAX_CLIENT; i++)
	{
		if (client_socks[i] !=-1) {//还没给客户端发过消息 我才发送
			write(client_socks[i], msg, str_len);//发送数据
		}
		else {
			break;//后面的一定无效,因为每次都往数从前往后存放客户端套接字到client_socks
		}
	}
	pthread_mutex_unlock(&mutex);//解锁
}


3.客户端

        3.1服务器至此结束,下面来看客户端,首先是客户端的初始化代码,每连接一个客户端,我们需要先告诉客户端姓名,然后连接成功后就可以发送消息给服务端了。


void Client()
{
	int client_socket;
	struct sockaddr_in server_addr;
	client_socket = socket(PF_INET, SOCK_STREAM, 0);
	//初始化
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	server_addr.sin_family = AF_INET;//
	server_addr.sin_port = htons(9000);//端口
	printf("[ input name ]: ");
	scanf("%s", name);
	fgetc(stdin);//读取最后一个\n,防止下次调用fgets 的时候读到换行符
	printf("connect success\n");
	if (connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr))==-1) {
		printf("connect error\n");
		close(client_socket);
		return;
	}
	pthread_t send_id, recv_id;
	sem_init(&semid, 0, -1);//初始化信号量为-1
	//创建两个线程分别去处理发送群消息和接收群消息
	pthread_create(&send_id, NULL, client_send_message, (void*)&client_socket);// 此处传的时局部变量的地址,必须保证子线程结束后,该线程才能结束
	pthread_create(&recv_id, NULL, client_recv_message, (void*)&client_socket);//所以下面使用了信号量来等到两个子线程的结束
	sem_wait(&semid);//等待上面两个线程结束,这里用信号量,初始信号量设为-1,上述2线程个执行一次sem_post,信号量变为1,此处就有信号了
	close(client_socket);
}

        


3.2 上面的两个线程分别去处理发送消息和接收消息,代码如下


void* client_send_message(void* arg)
{
	pthread_detach(pthread_self());//搭配pthread_exit使用,线程结束后自动销毁
	int client_socket = *(int*)arg;
	char msg[1000] = "";//输入的消息
	char send_buffer[1000] = "";//要发送的消息
	while (true) {
		memset(send_buffer, 0, sizeof(send_buffer));
		printf("input message to send everyone:\n");
		fgets(msg, sizeof(msg), stdin);//从输入流里面得到消息存取在msg中
		if ((strcmp(msg, "q\n") == 0) || (strcmp(msg, "Q\n") == 0)) {//输入q/Q退出消息
			printf("successful exit\n ");
			break;
		}
		snprintf(send_buffer, sizeof(send_buffer), "=========== [%s say]: %s", name, msg);//拼接 姓名+消息  格式化输出到 send_buffer中
		write(client_socket, send_buffer, strlen(send_buffer));//发送给服务器
		usleep(100);//延时0.1ms
	}
	close(client_socket);
	sem_post(&semid);//信号量+1
	pthread_exit(NULL);

}

        3.3下面是接收来自服务端的代码,如下


void* client_recv_message(void* arg)

{
	pthread_detach(pthread_self());//自动销毁线程函数,pthread_self为当前线程id
	int client_socket = *(int*)arg;
	char msg[512] = "";
	while (1) {
		ssize_t str_len = read(client_socket, msg,sizeof(msg));
		if (str_len <= 0) {
			break;
		}
		fputs(msg, stdout);//输出
		memset(msg, 0, strlen(msg));
	}
	close(client_socket);
	sem_post(&semid);//信号量+1
	pthread_exit(NULL);
}

4.总结

        主要实现了群聊的底层功能,再此基础上,我们还可以实现给某个客户端单独发消息,只需让服务器发送消息的时候找到要发送的套接字,然后只单独发送给客户端。这里先不实现了,我感觉在来点图像化界面,多增加一些功能,类似小型交友软件就有了。

5.输出

首先启动客户端,编译好后输入指令运行,记得在指令后面带上参数s 表示启动服务端

其次启动服务器,找到编译好的文件,后面随便带一个非s的字母,就可启动客户端,可以启动多个客户端,这里以两个为例。

6.整体代码


#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>
#include <semaphore.h>
sem_t semid;//信号量
char name[64] = "[DEFAULT]";//姓名,默认default
#define MAX_CLIENT 100  //最大可以连接的客户端数量
int client_socks[MAX_CLIENT]; //保存100个客户端的套接字信息
pthread_mutex_t mutex;//互斥量,线程同步

void sendMessage(const char* msg, ssize_t str_len)//转发消息给所有的客户端
{
	pthread_mutex_lock(&mutex);//上锁
	for (int i = 0; i < MAX_CLIENT; i++)
	{
		if (client_socks[i] !=-1) {//还没给客户端发过消息 我才发送
			write(client_socks[i], msg, str_len);//发送数据
		}
		else {
			break;//后面的一定无效,因为每次都往数从前往后存放客户端套接字到client_socks
		}
	}
	pthread_mutex_unlock(&mutex);//解锁
}


void* handle_client(void* arg)//处理客户端消息
{
	pthread_detach(pthread_self());
	int client_sock = *(int*)arg;//解析套接字
	char msg[1024] = "";
	ssize_t str_len = 0;
	while ((str_len = read(client_sock, msg, sizeof(msg))) > 0) {//整个表达式就是read读到的字节数
		sendMessage(msg, str_len);
	}
	pthread_mutex_lock(&mutex);//上锁
	*(int*)arg = -1;//arg为对应套接字的地址,设置套接字
	pthread_mutex_lock(&mutex);//解锁
	close(client_sock);
	pthread_exit(NULL);
}

void Server()
{
	//初始化
	int server_socket, client_socket;
	struct sockaddr_in server_addr, client_addr;
	server_socket = socket(PF_INET, SOCK_STREAM, 0);
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//在所有IP上监听
	server_addr.sin_family = AF_INET;//
	server_addr.sin_port = htons(9000);//端口
	pthread_mutex_init(&mutex, NULL);//初始化互斥量

	if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == -1){
		printf("bind error %d");
		close(server_socket);
		return;
	}
	if (listen(server_socket, 5) == -1) {
		printf("listen error\n");
		close(server_socket);
		return;
	}
	while (1) {
		socklen_t client_addr_size = sizeof(client_addr);
		client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
		if (client_socket == -1) {
			printf("accept error\n");
		}
		//将客户端处理交个线程
		pthread_mutex_lock(&mutex);//上锁
		int i = 0;
		for (; i < MAX_CLIENT; i++)//寻找空闲的数组位置,来存储客户端套接字
		{
			if (client_socks[i] == -1 ) {//找到空闲位置
				client_socks[i] = client_socket;//将得到的客户端套接字保存到客户端数套接字数组
				break;
			}
		}
		pthread_mutex_unlock(&mutex);//解锁
		pthread_t thread_id;//声明线程ID
		pthread_create(&thread_id, NULL, handle_client, &client_socks[i]);//创建线程 ,取到这个客户端套接字传给新线程去处理
	}
	close(server_socket);
	pthread_mutex_destroy(&mutex);//销毁互斥量
}

void* client_send_message(void* arg)
{
	pthread_detach(pthread_self());//搭配pthread_exit使用,线程结束后自动销毁
	int client_socket = *(int*)arg;
	char msg[1000] = "";//输入的消息
	char send_buffer[1000] = "";//要发送的消息
	
	while (true) {
		memset(send_buffer, 0, sizeof(send_buffer));
		printf("input message to send everyone:\n");
		fgets(msg, sizeof(msg), stdin);//从输入流里面得到消息存取在msg中
		if ((strcmp(msg, "q\n") == 0) || (strcmp(msg, "Q\n") == 0)) {//输入q/Q退出消息
			printf("successful exit\n ");
			break;
		}
		snprintf(send_buffer, sizeof(send_buffer), "=========== [%s say]: %s", name, msg);//拼接 姓名+消息  格式化输出到 send_buffer中
		write(client_socket, send_buffer, strlen(send_buffer));//发送给服务器
		usleep(100);//延时0.1ms
	}
	close(client_socket);
	sem_post(&semid);//信号量+1
	pthread_exit(NULL);

}

void* client_recv_message(void* arg)

{
	pthread_detach(pthread_self());//自动销毁线程函数,pthread_self为当前线程id
	int client_socket = *(int*)arg;
	char msg[512] = "";
	while (1) {
		ssize_t str_len = read(client_socket, msg,sizeof(msg));
		if (str_len <= 0) {
			break;
		}
		fputs(msg, stdout);//输出
		memset(msg, 0, strlen(msg));
	}
	close(client_socket);
	sem_post(&semid);//信号量+1
	pthread_exit(NULL);
}


void Client()
{
	int client_socket;
	struct sockaddr_in server_addr;
	client_socket = socket(PF_INET, SOCK_STREAM, 0);
	//初始化
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	server_addr.sin_family = AF_INET;//
	server_addr.sin_port = htons(9000);//端口
	printf("[ input name ]: ");
	scanf("%s", name);
	fgetc(stdin);//读取最后一个\n,防止下次调用fgets 的时候读到换行符
	printf("connect success\n");
	if (connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr))==-1) {
		printf("connect error\n");
		close(client_socket);
		return;
	}
	pthread_t send_id, recv_id;
	sem_init(&semid, 0, -1);//初始化信号量为-1
	//创建两个线程分别去处理发送群消息和接收群消息
	pthread_create(&send_id, NULL, client_send_message, (void*)&client_socket);// 此处传的时局部变量的地址,必须保证子线程结束后,该线程才能结束
	pthread_create(&recv_id, NULL, client_recv_message, (void*)&client_socket);//所以下面使用了信号量来等到两个子线程的结束
	sem_wait(&semid);//等待上面两个线程结束,这里用信号量,初始信号量设为-1,上述2线程个执行一次sem_post,信号量变为1,此处就有信号了
	close(client_socket);
}




void invoke(const char * arg)
{
	if (strcmp(arg, "s") == 0) {//检查输入的命令行的第二个参数,如果为s,表示开启服务端,否则开启客户端
		Server();
	}
	else {//其他任意字母
		Client();
	}
}


int main(int argc, char* argv[])
{
	memset(client_socks, -1, sizeof(client_socks));//初始化全为无效套接字
	invoke(argv[1]);//传入命令行的输入参数
	return 0;
}

标签:addr,socket,--,server,client,线程,pthread,多线程,客户端
From: https://blog.csdn.net/qq_68245364/article/details/136912165

相关文章

  • TCL-{} 与“”的区别;$(), $, ${}的区别
    1.tcl中,{}对里面的各种特殊字符都不作处理,仅当做普通的字符串      “”对里面的各种分隔符不作处理,但是对换行符(\n);置换符($;[])会照常处理需要注意的是,在foreach中的{}里面的内容 变量置换和计算 操作仍会正常执行,是因为在foreach中{}作为其中的循......
  • AI Agent目前应用落地有哪些局限性?
    谈到AIAgent目前应用落地有哪些局限性,还是要从概念、应用入手。谈到AIAgent,很多人都认为它是LLM的产物,了解AIAgent的人应该知道,Agent概念并不是当今的产物,而是伴随人工智能而出现的智能实体概念不断进化的结果。一、要弄懂AI领域的agent是什么意思,就要知道AIAgent......
  • 文献学习-21-DaFoEs:混合数据集以推广微创机器人手术中的视觉状态深度学习力估计
    DaFoEs:MixingDatasetsTowardstheGeneralizationofVision-StateDeep-LearningForceEstimationinMinimallyInvasiveRoboticSurgeryAuthors: MikelDeIturrateReyzabal,GraduateStudentMember,IEEE,MingcongChen,WeiHuang,SebastienOurselin,and......
  • opengl日记12-opengl坐标系统
    文章目录环境代码CMakeLists.txtvertexShaderSource.vsmain.cpp总结环境系统:ubuntu20.04opengl版本:4.6glfw版本:3.3glad版本:4.6cmake版本:3.16.3gcc版本:10.3.0在<opengl学习日记11-opengl的transformtions变换示例>的基础上,进行修改,实现坐标系统变换效果。代码CM......
  • opengl日记7-ubuntu20.04开发环境opengl拓展glfw和glad环境搭建
    文章目录ubuntu中安装opengl核心环境安装glfw安装glad测试验证程序vscode的task.json配置如下note参考ubuntu中安装opengl核心环境可执行如下命令进行整体安装:sudoapt-getinstalllibgl1-mesa-dev*或者单独安装1、提供编译程序必须软件包的列表信息sudoapt......
  • c++算法学习笔记 (15) 质数
    1.试除法判断某个数是否为质数#include<iostream>usingnamespacestd;constintN=50005;boolis_prime1(intn){//暴力写法:O(n)if(n<2)returnfalse;for(inti=2;i<n;i++){if(n%i==0)returnfalse;......
  • 十.pandas方法总结&Numpy
    目录十.pandas方法总结1.索引切片2.数据排序3.数据统计Pandas数据计算4.数据查看5.数据清洗6-数据分组查看分组结果7-处理第三方数据csv文件操作Excel文件操作Excel文件读取read_excelExcel文件写入to_excel()SQL操作mysql读取数据保存pandas处理字符串数......
  • 2024.2.29校招 实习 内推 面经
    绿*泡*泡VX:neituijunsir  交流*裙,内推/实习/校招汇总表格1、校招|影石Insta3602024春季校园招聘启动(内推)校招|影石Insta3602024春季校园招聘启动(内推)2、校招|虹软科技2024届校招春招批通道开启(内推)校招|虹软科技2024届校招春招批通道开启(内推)3、校招|......
  • Qt QPolarChart极坐标图(阿基米德线、四叶曲线、六叶花瓣、三叶花瓣、心形曲线)
    QChart还有专门画极坐标的类QPolarChart,它的界面是一个圆盘。注意在使用之前,包括命名空间QT_CHARTS_USE_NAMESPACE,.pro文件中也要增加QT+=charts.1#include<QApplication>2#include<QDebug>3#include<QtCharts/QScatterSeries>4#include<QtCharts/QLineSer......
  • 内存CPU监控
    #-*-coding:utf-8-*-"""--------------------------------Time:2024/3/2111:50Author:NingDescription:xitong_jiankong.py系统内存、cpu使用情况检测--------------------------------"""fromloguruimportlo......