基于Linux网络编程socket套接字制作的多人聊天室

1、项目简介

  该项目是一个可以实现多人同时在线的聊天室网络应用,用户各自在登录时自定义一个网名,并以这个网名在多人聊天室中与别人聊天,同时每个用户可以通过用户名辨别其他用户。主要的功能特点是能容纳多人在线聊天,分为服务器端和客户端两大模块。

  关于socket套接字的使用方法可以参考这篇文章:https://blog.csdn.net/mhyasadj/article/details/131181974

2、模块介绍

2.1服务器端

  用户能够以“ client name”的命令行程序运行,并以“name”的用户名登陆聊天室(不需要密码)。客户端能允许用户发言,并实时接收服务器的广播信息。当客户端输入“ quit”时,退出客户端。

  首先是创建服务器端模型,本项目通过TCP传输协议流程,进行服务器端的socket创建、地址填充、绑定和监听设置。同时为了能够实现多人在线聊天的功能,本项目通过SO_REUSEADDR选项,以便可以及时重新使用处于 TIME_WAIT 状态的地址和端口,从而实现端口复用,达到多人在线聊天的效果。

  第二步进行处理客户端的链接请求,服务器初始化时,将 cli_fd 数组中的元素初始化为 -1,即重置清空聊天位置。当收到客户端的链接请求后,会通过 sem_getvalue 函数获取当前信已连接客户端的数量值。然后找到一个没有被占用的聊天位置,并使用 accept 函数接受客户端的连接请求,再将返回的新的客户端套接字描述符赋给 cli_fd[index]。若描述符为负值,请求失败。同时此项目中使用sem_num(即人数上限)来限制连接服务器的客户端数。若客户端连接数达到峰值则拒绝连接,并发送连接失败的消息;若未达到峰值,则回复连接成功的消息,并创建一个server线程来处理客户端的发送过来的信息。

  第三步是群发函数的实现,本项目于服务器端创建一个群发函数,使用 for 循环来遍历群友列表。如果群友列表中的 cli_fd 不等于 -1,则表示该群友的套接字描述符 cli_fd 是有效的。-1 表示没有群友,可能已退出或未被占用。当群友存在时,在服务器端打印群发内容和输出目标客户端的套接字描述符(由于收发多方都会受到群发消息,所以服务器会输出所有在线客户端的套接字描述符)。然后服务器端会将消息内容发送至所有的在线客户端,实现群发消息的项目功能。

  最后是完整的服务器端代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define BUF_SIZE (5120)
#define SEM_SIZE (20) // 群聊上限人数
 
// 信号量--判断群聊人数
sem_t sem;
// 服务端文件描述符
int svr_fd;
// 存储群友,多一个是为了当群人满时,空一个出来接发信息
int cli_fd[SEM_SIZE + 1] = {};
 
struct client
{
	/* data */
	char buf[BUF_SIZE];	 // message
	char name[BUF_SIZE]; // name
	int client_fd;		 // fd
};

//结构体数组
struct client clients[SEM_SIZE];


 
// 群发函数
void *send_all(char *buf)
{
	for (int i = 0; i < SEM_SIZE; i++)
	{
		// 若值为-1,则没有此群友,表示已经退出或未被占有
		if (-1 != cli_fd[i])
		{
			printf("%s\n", buf);
			printf("send to %d\n", cli_fd[i]);
			write(cli_fd[i], buf, strlen(buf) + 1);
		}
	}
}
 
 
// 服务端接收函数
void *server(void *arg)
{
 
	int fd = *(int *)arg;
	char buf[BUF_SIZE];
	char name[BUF_SIZE];
	char ts[BUF_SIZE] = {0};
	
 
	// 获取昵称
	read(fd, clients[fd].name, sizeof(name));
	clients[fd].client_fd = fd;
 
	// printf("clients[fd].name = %s\n", clients[fd].name);
	sprintf(ts, "\n [system]欢迎 %s 进入群聊", clients[fd].name);
	send_all(ts);
 
	while(1)
	{
		// 接收信息,无信息时将阻塞
		int recv_size = read(fd, clients[fd].buf, sizeof(buf));
 
		
		// 收到退出信息
		if (0 >= recv || NULL != strstr(clients[fd].buf, "quit"))
		{
			sprintf(ts, "[system]欢送 %s 离开群聊\n", clients[fd].name);
			int index = 0;
			// 找到要退出的那个人,并将其置为-1
			for (; index < SEM_SIZE; index++)
			{
				if (cli_fd[index] == fd)
				{
					cli_fd[index] = -1;
					break;
				}
			}
			// 群发XXX退出聊天室提示消息
			send_all(ts);
 
			// 群友退出,信号量+1
			int n;
			sem_post(&sem);
			sem_getvalue(&sem, &n);
 
			printf("[system] %s 离开群聊,当前剩余%d人\n", clients[fd].name, SEM_SIZE - n);
			strcpy(clients[fd].buf, "quit");
 
			write(fd, clients[fd].buf, strlen(clients[fd].buf) + 1);
			close(fd);
			pthread_exit(NULL);
		}
		// 群发
			send_all(clients[fd].buf);
	}
}
 
/**
 * quit
 */
void sigint(int signum)
{
	close(svr_fd);
	sem_destroy(&sem);
	printf("\n[system]服务器关闭\n");
	exit(0);
}
 
int main()
{
	signal(SIGINT, sigint);
	// 初始化信号量,群聊上限SEM_SIZE人
	sem_init(&sem, 0, SEM_SIZE);
 
	// 创建socket对象
    printf("[system]服务器已启动\n");
	svr_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > svr_fd)
	{
		perror("socket");
		return -1;
	}
 
 
	//端口复用函数:解决端口号被系统占用的情况
	int on = 1;
	int gg = setsockopt(svr_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if(gg==-1)
	{
		perror("setsockopt");
		return -1;
	}
 
	// 准备通信地址(自己)
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6666);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	socklen_t addrlen = sizeof(addr);
 
	// 绑定socket对象与地址
	if (bind(svr_fd, (struct sockaddr *)&addr, addrlen))
	{
		perror("bind");
		return -1;
	}
 
	// 设置监听和排除数量
	if (listen(svr_fd, 10))
	{
		perror("listen");
		return -1;
	}
 
	printf("[system]等待客户端链接\n");
	// 将初始值置全为-1,表示该聊天位置没有人占领
	memset(cli_fd, -1, sizeof(cli_fd));
	while(1)
	{
		int sem_num;
		sem_getvalue(&sem, &sem_num);
 
		// 找到没有人占领的聊天位
		int index = 0;
		while (-1 != cli_fd[index])
			index++;
		cli_fd[index] = accept(svr_fd, (struct sockaddr *)&addr, &addrlen);//接收客户端请求
 
		if (0 > cli_fd[index])
		{
			perror("accept");
			return -1;
		}
 
		char buf[BUF_SIZE];
		if (0 >= sem_num)
		{
			printf("[system]人数已满,%d号客户端链接失败\n", cli_fd[index]);
			sprintf(buf, "[system]人数已满,客户端链接失败");
			write(cli_fd[index], buf, strlen(buf) + 1);
			close(cli_fd[index]);
			cli_fd[index] = -1;
		}
		else
		{
			sem_trywait(&sem);
			sem_getvalue(&sem, &sem_num);
			char msg[BUF_SIZE] = {};
			printf("[system]%d号客户端链接成功,当前聊天人数%d人\n", cli_fd[index], SEM_SIZE - sem_num);
			sprintf(msg, "[system]客户端链接成功,当前聊天人数%d人,输入quit可退出\n", SEM_SIZE - sem_num);
			write(cli_fd[index], msg, strlen(msg) + 1);
 
			// 创建线程客户端
			pthread_t tid;
			pthread_create(&tid, NULL, server, &cli_fd[index]);
		}
	}
}


2.2客户端

  客户端能够同时接受多用户的登陆,并将每个用户的聊天信息广播到其他用户,广播的时候会附加上发言的用户名。

  首先是创建客户端模型,按照创建服务器端类似的流程来进行创建客户端,创建完毕后,客户端向服务器发送链接请求。

  第二步是昵称输入功能,客户端创建字符串并输入昵称,然后使用write函数将昵称发送给服务器。之后服务器端使用read函数从套接字fd中读取客户端发送的昵称信息,并将其存储到clients[fd].client_fd变量中,然后使用sprintf函数将欢迎信息格式化为字符串,将欢迎信息发送给所有已连接的客户端。

  第三步是客户端发收数据的实现,发送数据前,客户端定义一个名为 msg 的字符数组,用于存储从键盘输入并格式化后的字符串,并使用 sprintf 函数将格式化的字符串存储到 msg 数组中,之后通过 write 函数将 msg 字符串发送给指定的客户端套接字描述符 cli_fd并将其发送到服务器端,之后服务器端再通过调用群发函数将字符串广播至每一个客户端中,实现多人群聊间的信息相互收发。

  第四步是程序退出的设计实现,当其中一个客户端接受到从键盘输入的“quit”并发送给服务器端时,在判断接收数据正常且为quit的情况下,服务器端广播准备退出通信的服务器端的昵称,之后更新信号量的值,在服务器端屏幕上打印群聊的剩余在线情况。而服务器端将显示“通信结束”字样。

  最后是完整的客户端代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define BUF_SIZE (5120)
 
 
/**
 * 读取server转发过来的消息
*/
void* client_read(void* arg)
{
	int cli_fd = *(int*)arg;
	char buf[BUF_SIZE];
	
	//接收数据
	while(1)
	{
		int recv_size = read(cli_fd,buf,BUF_SIZE);
	  	if(0 < recv_size && 0 == strcmp(buf,"quit"))
		{
			close(cli_fd);
			pthread_exit(NULL);
		}
		else if(0 >= recv_size && 0 != strcmp(buf,"quit"))
		{
			printf("服务器已断开,请按任意键退出\n");
			close(cli_fd);
			pthread_exit(NULL);
		}
		printf("%s\n",buf);
	}
}
 
int main()
{
	//创建socket对象
	int cli_fd = socket(AF_INET,SOCK_STREAM,0);
	if(0 > cli_fd)
	{
		perror("socket");
		return -1;
	}
	
	//准备通信地址(服务端)
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6666);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");//此处填写自己的ip地址
	socklen_t addrlen = sizeof(addr);
	
	//链接服务端
	printf("链接服务器中\n");
	if(connect(cli_fd,(struct sockaddr*)&addr,addrlen))
	{
		perror("connect");
		return -1;
	}
	
	char buf[BUF_SIZE];
	
	read(cli_fd,buf,BUF_SIZE);
	if(NULL == strstr(buf,"链接成功"))
	{
		printf("群聊人已满,请稍后再来\n");
		close(cli_fd);
		return 0;
	}
	printf("%s\n",buf);
	
	//链接成功,创建客户端
	pthread_t tid;
	pthread_create(&tid,NULL,client_read,&cli_fd);
	
	//输入昵称
	char name[BUF_SIZE] = {};
	printf("请输入你的昵称:");
	gets(name);
	write(cli_fd,name,strlen(name)+1);
	
	//发送数据
	while(1)
	{
		
		gets(buf); // 获取键盘字符串
		
		char msg[BUF_SIZE]; // 存储格式化后的字符串
		sprintf(msg,"用户%s:%s",name,buf); // 输出字符串
		
		//把msg发送到cli_fd
		int send_size = write(cli_fd,msg,strlen(msg)+1);
		// 如果字符串是quit就退出
		if(0 < send_size && 0 == strcmp(buf,"quit"))
		{
			printf("已手动结束通讯\n");
			break;
			return 0;
		}
		else if(0 >= send_size && 0 != strcmp(buf,"quit"))
		{
			printf("已退出程序\n");
			break;
			return 0;
		}
	}
}

3、结果演示

3.1服务器初始化

基于Linux网络编程socket套接字制作的多人聊天室_第1张图片

 

3.2客户端中用户取名与聊天

基于Linux网络编程socket套接字制作的多人聊天室_第2张图片 

 

3.3客户端主动退出与服务器强制关闭功能

基于Linux网络编程socket套接字制作的多人聊天室_第3张图片  

 

 

你可能感兴趣的:(linux,服务器,网络,c语言)