利用select函数在Linux环境下实现一个聊天室程序

本文详细介绍了如何在Linux环境下使用select函数设计一个聊天室程序,涉及服务器端和客户端的实现,包括监听新连接、处理指令、广播消息等功能。

利用select函数在Linux环境下实现一个聊天室程序

实验目的

利用select函数实现linux环境下的聊天室程序

实验要求

  1. 用户默认处于广播模式,一个客户在其客户端发送的消息,其它客户端用户全部可以收到;
  2. 程序支持下列命令:
    /help:显示帮助信息(思考:信息是放在客户端还是服务器端);
    /quit:用户退出聊天室,同时将退出信息广播给其他用户;
    /who:显示在线用户;
    /send 用户名 消息:向指定用户发送点到点消息

程序设计

server
  • 定义一个结构体client_info,用于存储客户端的信息,包括套接字id、客户名字和是否是第一次访问等。

  • 初始化一些变量,包括需要扫描的套接字集合、服务器和客户端的地址信息、接收和发送缓冲区、套接字属性等。

  • 创建套接字,绑定端口并开始监听。

  • 在主循环中,程序使用select函数监听所有套接字是否有数据到来,并根据不同的情况进行处理。

    • 如果是监听套接字被激活,说明有新的客户端连接,需要使用accept函数进行处理,并将客户端信息保存到client_info数组中。
    • 如果是连接套接字被激活,说明有客户端发送了消息,程序需要根据消息内容进行处理,包括处理指令、点对点发送消息和广播消息等。
  • 程序清空发送和接收缓冲区,继续循环等待新的事件到来
    在这里插入图片描述

client
  • 定义一些变量和结构体,包括套接字、套接字集合、服务器和客户端地址等。

  • 根据命令行参数创建一个套接字并连接到服务器。

  • 通过标准输入获取用户的昵称,并将昵称发送给服务器。

  • 进入循环,不断监听套接字集合中的套接字和标准输入是否有数据到来。

    • 如果是套接字被激活,表示服务器有信息传过来,客户端进行接收处理并打印
    • 如果是标准输入被激活,表示客户有消息要发送出去,客户端进行发送处理并向服务器发送消息
      • 如果用户输入"/help",则输出帮助信息
      • 如果用户输入"/quit",则关闭该用户套接字,同时将推出消息广播给其他用户
      • 如果用户输入"/who",则显示在线用户
      • 如果用户输入"/send user message",向指定用户发送点到点消息
  • 最后,关闭套接字并返回0
    在这里插入图片描述

相关函数
  • setsockopt函数
    用于设置套接字的选项。
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

其中,sockfd为套接字描述符,SOL_SOCKET表示设置的选项级别为套接字级别,SO_REUSEADDR表示允许在同一端口上启动同一服务器的多个实例。&opt为选项值的指针,sizeof(opt)为选项值的大小。

  • FD_ZERO函数
    用于将指定的套接字集合清空,接收一个参数:套接字集合的指针。
FD_ZERO(&m_fds);
FD_ZERO(&r_fds);

其中,allset表示需要扫描的所有套接字集合,rset表示select过后的套接字集合。

  • FD_SET函数
    用于将指定的套接字加入到套接字集合中,接收两个参数:套接字描述符和套接字集合的指针。
FD_SET(sockfd, &m_fds);
FD_SET(newfd, &m_fds);

其中,sockfd为监听套接字描述符,confd为新的客户端套接字描述符。

  • select函数
    用于监听多个套接字是否有数据到来,接收五个参数:最大套接字描述符加1、读集合、写集合、异常集合和超时时间。
if(-1 == select(maxfd+1, &r_fds, NULL, NULL, NULL))
{
    perror("select function error!\n");
    exit(1);
}

其中,maxfd为需要监听的最大套接字描述符加1,rset为需要监听的读集合,写集合,例外集合,timeout设为空。如果有数据到来,则函数返回大于0的值,否则返回0或-1。

实验代码

chatserver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/time.h>
#include <sys/types.h>

#define PORT 1573
#define BACKLOG 10
#define BUFSIZE 2048

// 定义一个结构体,使得客户的信息可以结合到一起
struct client_info
{
	int id;					// 客户端连接套接字
	char name[256]; // 客户的名字
	int first;			// 表示用户是否第一次登录
};

int main()
{
	fd_set m_fds; // 需要扫描的所有套接字
	fd_set r_fds; // select过后的套接字
	struct sockaddr_in server;
	struct sockaddr_in client;
	int maxfd;
	int sockfd;
	int newfd;
	char recvbuf[BUFSIZE];
	char sendbuf[BUFSIZE];
	int recvnum;
	int sendnum;
	int opt; // 定义套接字属性
	opt = SO_REUSEADDR;
	int length; // 用于connect函数
	length = sizeof(struct sockaddr);

	int tmp_i;
	int tmp_j;
	char str1[256];
	char str2[256];
	char str3[256];
	int tmpid = -1; // 用于进行实际处理的套接字,在关系上是通过tmpfd得到的
	int tmpfd = -1; // 用来在新一轮循环中替换掉tmp_i,使得tmp_i的信息可以保存
	struct client_info clientinfo[BACKLOG];

	// 初始化套接字集合
	FD_ZERO(&m_fds);
	FD_ZERO(&r_fds);

	if (-1 == (sockfd = socket(AF_INET, SOCK_STREAM, 0)))
	{
		perror("create socket error!\n");
		exit(1);
	}

	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	memset(&server, 0, sizeof(server));
	memset(sendbuf, 0, BUFSIZE);
	memset(recvbuf, 0, BUFSIZE);
	int i;
	// 初始化客户的信息
	for (i = 0; i < BACKLOG; i++)
	{
		clientinfo[i].id = -1;
		clientinfo[i].name[0] = '\0';
		clientinfo[i].first = -1;
	}
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	server.sin_port = htons(PORT);

	if (-1 == bind(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)))
	{
		perror("bind socket error!\n");
		exit(1);
	}

	// 对侦听套接字进行侦听
	if (-1 == listen(sockfd, BACKLOG))
	{
		perror("listen error!\n");
		exit(1);
	}

	// 侦听套接字放入读集合
	FD_SET(sockfd, &m_fds);
	maxfd = sockfd;
	printf("server is ok!\n");

	while (1)
	{
		r_fds = m_fds;
		if (-1 == select(maxfd + 1, &r_fds, NULL, NULL, NULL))
		{
			perror("select function error!\n");
			exit(1);
		}

		for (tmp_i = sockfd; tmp_i <= maxfd; tmp_i++)
		{
			// 处理:如果是监听套接字被激活
			if (FD_ISSET(tmp_i, &r_fds))
			{
				if (tmp_i == sockfd)
				{
					newfd = accept(sockfd, (struct sockaddr *)&client, &length);
					if (newfd == -1)
					{
						perror("accept error!\n");
						exit(1);
					}
					clientinfo[newfd].id = newfd;
					clientinfo[newfd].first = 1; // 将first置为1,用于第一个接收包的名字

					FD_SET(newfd, &m_fds);
					if (newfd > maxfd)
						maxfd = newfd;
				}
				else
				{
					// 处理:如果是连接套接字被激活
					recvnum = read(tmp_i, recvbuf, sizeof(recvbuf));
					if (clientinfo[tmp_i].first == 1) // 由上,得到客户的名字
					{
						strcpy(clientinfo[tmp_i].name, recvbuf);
						clientinfo[tmp_i].first = -1;
					}
					if (0 > recvnum)
					{
						perror("recieve error!\n");
						exit(1);
					}
					if (recvbuf[0] == '/')
					{
						// 处理:以‘/’开始的接收包表示现在是指令
						if (strcmp(recvbuf, "/who\n") == 0)
						{
							// 请求现在有哪些用户在线
							for (tmpfd = sockfd; tmpfd <= maxfd; tmpfd++)
							{
								if (FD_ISSET(tmpfd, &m_fds))
									strcat(sendbuf, clientinfo[tmpfd].name);
							}
							// 因为只是当前输入“/who”指令的用户想要知道谁在线
							// 只把内容返回,用continue重新新的一轮循环
							write(tmp_i, sendbuf, sizeof(sendbuf));
							continue;
						}
						if (strcmp(recvbuf, "/quit\n") == 0)
						{
							// 当前客户请求退出
							printf("client:%s exit!\n", clientinfo[tmp_i].name);
							FD_CLR(tmp_i, &m_fds);
							close(tmp_i);
							strcat(sendbuf, clientinfo[tmp_i].name);
							strcat(sendbuf, " was exit!");
						}
						// 初始化字符串,用于分别存储/send usr msg中的各个部分
						memset(str1, 0, sizeof(str1));
						memset(str2, 0, sizeof(str2));
						memset(str3, 0, sizeof(str3));
						sscanf(recvbuf, "%s %s %s", str1, str2, str3);
						strcat(str2, "\n");
						if (strcmp(str1, "/send") == 0)
						{
							tmpid = -1; // 以防在新的循环中tmpid的值被上一次循环所改变
							int j = 0;
							for (tmpfd = sockfd; tmpfd <= maxfd; tmpfd++)
							{
								// 查询到指定名字下的客户的套接字
								if (FD_ISSET(tmpfd, &m_fds))
								{
									if (strcmp(str2, clientinfo[tmpfd].name) == 0)
										tmpid = tmpfd;
								}
							}
							if (tmpid == -1)
							{
								// 表示并没有当前客户与之匹配,返回消息给发送端
								strcat(sendbuf, "user isn't online!");
								write(tmp_i, sendbuf, sizeof(sendbuf));
								continue;
							}
							strcat(sendbuf, clientinfo[tmp_i].name);
							strcat(sendbuf, str3);
							// 因为这里是点对点,所以不用进入下面的部分,continue跳过
							write(tmpid, sendbuf, sizeof(sendbuf));
							continue;
						}
					}
					else
					{
						strcat(sendbuf, clientinfo[tmp_i].name);
						strcat(sendbuf, " said: ");
						strcat(sendbuf, recvbuf);
					}
					for (tmp_j = sockfd + 1; tmp_j <= maxfd; tmp_j++)
					{
						// 实现信息的广播
						if (FD_ISSET(tmp_j, &m_fds))
						{
							write(tmp_j, sendbuf, strlen(sendbuf));
						}
					}
				}
			}
		}
		// 清空sendbuf和recvbuf
		memset(&sendbuf, 0, BUFSIZE);
		memset(&recvbuf, 0, BUFSIZE);
	}

	return 0;
}

chatclient.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/time.h>
#include <sys/types.h>

#define PORT 1573
#define BUFSIZE 2048

int main(int argc, char *argv[])
{
	int sockfd;
	fd_set sockset; // 套接字集合,用于判断是套接字还是I/O输入
	struct sockaddr_in server;
	struct sockaddr_in client;
	int recvnum;
	char sendbuf[BUFSIZE];
	char recvbuf[BUFSIZE];
	int length;

	if (2 > argc)
	{
		printf("please input ip!\n");
		exit(1);
	}

	if (-1 == (sockfd = socket(AF_INET, SOCK_STREAM, 0)))
	{
		perror("create client socket error!\n");
		exit(1);
	}

	memset(&server, 0, sizeof(server));
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = inet_addr(argv[1]);
	server.sin_port = htons(PORT);

	if (-1 == connect(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)))
	{
		perror("client connect error!\n");
		exit(1);
	}

	memset(sendbuf, 0, 2048);
	fprintf(stderr, "welcome to visit the chat server\n");
	fprintf(stderr, "please input your name:");
	fgets(sendbuf, 256, stdin);

	if (0 > send(sockfd, sendbuf, strlen(sendbuf), 0))
	{
		perror("sending data error!\n");
		close(sockfd);
		exit(1);
	}

	// 初始化集合
	FD_ZERO(&sockset);
	FD_SET(sockfd, &sockset);
	FD_SET(0, &sockset);

	while (1)
	{
		memset(recvbuf, 0, sizeof(recvbuf));
		memset(sendbuf, 0, sizeof(sendbuf));
		select(sockfd + 1, &sockset, NULL, NULL, NULL);
		if (FD_ISSET(sockfd, &sockset))
		{
			// 处理:如果是套接字被激活,表示服务器有信息传过来,进行接收处理
			recvnum = read(sockfd, recvbuf, sizeof(recvbuf));
			recvbuf[recvnum] = '\0';
			printf("%s\n", recvbuf);
			printf("\n");
			fflush(stdout);
		}
		if (FD_ISSET(0, &sockset))
		{
			// 处理:如果是I/O被激活,表示客户有消息要发送出去,进行发送处理
			fgets(sendbuf, sizeof(sendbuf), stdin);
			length = strlen(sendbuf);
			sendbuf[length] = '\0';

			if (strcmp(sendbuf, "/help\n") == 0)
			{
				// 处理,输入"/help"表示想要得到帮助信息,帮助信息是存储在客户端的,不用向服务器发送信息
				// 跳过继续执行循环
				printf("\n");
				fprintf(stderr, "/help show the help message\n");
				fprintf(stderr, "/send usage:/send user message send message to user\n");
				fprintf(stderr, "/who show who is online\n");
				fprintf(stderr, "/quit quit from server\n");
				printf("\n");
				continue;
			}
			write(sockfd, sendbuf, sizeof(sendbuf));
			if (strcmp(sendbuf, "/quit\n") == 0)
			{
				// 处理,客户想要退出,关闭套接字,用户程序退出
				printf("quiting from chat room!\n");
				close(sockfd);
				exit(1);
			}
		}
		FD_ZERO(&sockset);
		FD_SET(sockfd, &sockset);
		FD_SET(0, &sockset);
	}
	close(sockfd);
	return 0;
}

编译运行

实验环境 ubuntu20.04
登入,注册用户名
在这里插入图片描述

/who
在这里插入图片描述

/send usr msg 点对点通信
在这里插入图片描述

/help
在这里插入图片描述

/quit 广播退出信息

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值