利用select函数在Linux环境下实现一个聊天室程序
实验目的
利用select函数实现linux环境下的聊天室程序
实验要求
- 用户默认处于广播模式,一个客户在其客户端发送的消息,其它客户端用户全部可以收到;
- 程序支持下列命令:
/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 广播退出信息

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

被折叠的 条评论
为什么被折叠?



