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

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

  • 19
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值