基于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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#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服务器初始化

99d9675267244ece9dd1d09a1c2c4f38.png

 

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

f1a0aea03ad74e67bfd9e11ef68984ad.png 

 

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

27731964f4b34119b7433db829b4d91d.png  

 

 

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的 C 语言网络编程实现的群聊、私聊、上传下载文件的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthread.h> #define MAX_CLIENT_NUM 10 #define BUFFER_SIZE 1024 int client_sockfd[MAX_CLIENT_NUM]; // 所有客户端的 sockfd int client_num = 0; // 当前连接的客户端数量 // 群聊函数 void send_to_all_clients(char *msg, int len) { for (int i = 0; i < client_num; i++) { // 发送给所有客户端 write(client_sockfd[i], msg, len); } } // 私聊函数 void send_to_client(int sockfd, char *msg, int len) { int i; for (i = 0; i < client_num; i++) { // 找到对应的客户端 if (client_sockfd[i] == sockfd) { break; } } if (i == client_num) { printf("Error: client %d not found\n", sockfd); return; } // 向指定客户端发送消息 write(client_sockfd[i], msg, len); } // 线程函数,接收客户端消息并处理 void *handle_client(void *arg) { int sockfd = *(int *)arg; char buffer[BUFFER_SIZE]; while (1) { memset(buffer, 0, sizeof(buffer)); int len = read(sockfd, buffer, BUFFER_SIZE); // 接收消息 if (len == 0) // 客户端断开连接 { printf("Client %d disconnected\n", sockfd); close(sockfd); for (int i = 0; i < client_num; i++) { if (client_sockfd[i] == sockfd) { // 移除客户端 client_num--; for (int j = i; j < client_num; j++) { client_sockfd[j] = client_sockfd[j+1]; } break; } } pthread_exit(NULL); } else if (strcmp(buffer, "quit\n") == 0) // 客户端主动退出 { printf("Client %d quit\n", sockfd); close(sockfd); for (int i = 0; i < client_num; i++) { if (client_sockfd[i] == sockfd) { // 移除客户端 client_num--; for (int j = i; j < client_num; j++) { client_sockfd[j] = client_sockfd[j+1]; } break; } } pthread_exit(NULL); } else if (strncmp(buffer, "to ", 3) == 0) // 私聊消息 { char *ptr = strchr(buffer, ' '); if (ptr == NULL) { printf("Error: invalid message format\n"); } else { int len1 = ptr - buffer - 3; int len2 = len - len1 - 3; char name[20]; strncpy(name, buffer+3, len1); name[len1] = '\0'; printf("Private message to %s: %s", name, ptr+1); send_to_client(sockfd, ptr+1, len2); } } else if (strncmp(buffer, "file ", 5) == 0) // 上传文件 { char *ptr = strchr(buffer, ' '); if (ptr == NULL) { printf("Error: invalid message format\n"); } else { char name[20]; strncpy(name, ptr+1, len-6); name[len-6] = '\0'; printf("File '%s' received from client %d\n", name, sockfd); char filename[50]; sprintf(filename, "recv/%s", name); FILE *fp = fopen(filename, "wb"); if (fp == NULL) { printf("Error: failed to create file\n"); } else { while (1) { memset(buffer, 0, sizeof(buffer)); int len = read(sockfd, buffer, BUFFER_SIZE); if (len <= 0) { break; } fwrite(buffer, 1, len, fp); if (len < BUFFER_SIZE) { break; } } fclose(fp); printf("File '%s' saved as '%s'\n", name, filename); } } } else // 群聊消息 { printf("Message from client %d: %s", sockfd, buffer); send_to_all_clients(buffer, len); } } } int main(int argc, char *argv[]) { int server_sockfd, client_sockfd; struct sockaddr_in server_addr, client_addr; int port = 12345; // 创建套接字 server_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (server_sockfd < 0) { perror("socket"); exit(1); } // 绑定端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(port); if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(1); } // 监听端口 if (listen(server_sockfd, MAX_CLIENT_NUM) < 0) { perror("listen"); exit(1); } printf("Server started, listening on port %d\n", port); while (1) { socklen_t client_len = sizeof(client_addr); // 接受连接请求 client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len); if (client_sockfd < 0) { perror("accept"); continue; } if (client_num >= MAX_CLIENT_NUM) // 客户端数量达到上限 { printf("Error: too many clients\n"); close(client_sockfd); continue; } client_sockfd[client_num] = client_sockfd; client_num++; printf("Client %d connected\n", client_sockfd); pthread_t tid; pthread_create(&tid, NULL, handle_client, &client_sockfd); } return 0; } ``` 注:以上代码仅供参考,实际应用中需要根据具体情况进行修改。例如,文件上传下载功能还需要考虑文件重名、文件不存在等情况。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值