要求:用select创建并发服务器,可以与多个客户端进行通信(监听键盘、socket、多个acceptfd)
其(select)基本思想是:
○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行 I/O时函数才返回。
○ 函数返回时告诉进程哪个描述符已就绪,可以进行 I/O操作。
- 构造一张关于文件描述符的表 fd_set
- 清空表 FD_ZERO
- 将关心的文件描述符添加到表中 FD_SET
- 调用 select函数,监听
- 判断是哪一个或者哪些文件描述符发生了事件 FD_ISSET
- 做对应的逻辑处理
思想:
创建一个表,监听服务器的变化,只要有客户端请求连接,就建立连接,同时把建立连接的客户端文件描述符加入到表中。
同时监听客户端的文件描述符,当客户端发送消息时,文件描述符发生变化,此时服务器就响应对应的文件描述符,接收客户端的消息并输出。
但是因为select的特点,每次都会清空未发生变化的文件描述符,所以当一个客户端的文件描述符发生变化时,当服务器响应完之后,其他没有发送消息的客户端对应的文件描述符就会被清除掉,导致下次无法进行监听。
所以我们创建两个表,其中一个专门用来备份(rfds),专门监听客户端的连接请求,并把建立连接的文件描述符添加到这个表里,循环监听客户端时,每次监听完客户端发送的消息清空其他的客户端之后,下次循环开始,把rfds表中的的内容赋值给cli表,这样就会做到每次都可以监听到所有客户端发送的消息。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>
/*
创建并发服务器,可以与多个客户端进行通信
*/
int main(int argc, char const *argv[])
{
int res;
char buf[128] = "";
// 1.创建流式套接字
int serverfd = socket(AF_INET, SOCK_STREAM, 0);
if (serverfd < 0)
{
perror("serverfd socket error");
return -1;
}
printf("server socket scuess\n");
// 2.指定本地的网络信息 struct sockaddr_in
struct sockaddr_in myaddr;
// 长度
socklen_t addrlen = sizeof(myaddr);
memset(&myaddr, 0, addrlen);
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr = INADDR_ANY;
// 端口
myaddr.sin_port = htons(atoi(argv[1]));
// 3.绑定套接字 bind()
// 绑定自己的地址
res = bind(serverfd, (struct sockaddr *)&myaddr, addrlen);
if (res < 0)
{
perror("bind error");
return -1;
}
printf("bind scuess\n");
// 4.监听套接字 listen()
res = listen(serverfd, 5);
if (res < 0)
{
perror("listen error");
return -1;
}
printf("listen scuess\n");
// select部分
// 创建表
fd_set rfds;
fd_set cli;
int acceptfd;
// 清空表
FD_ZERO(&rfds);
FD_ZERO(&cli);
// 将关系你的文件描述符加到表中
FD_SET(0, &rfds);
FD_SET(serverfd, &rfds);
// 最大文件描述符
int maxfd = serverfd;
while (1)
{
// rfds作为表的备份
// cli每次监听都会清空表中没有响应的文件描述符,等下次循环的时候,表中的文件描述符就变化了
// 每次循环开始前,都把备用表rfds的文件描述符给cli,这样就可以做到每次cli都可以监听所有的客户端acceptfd
cli = rfds;
// 调用select,启动监听
int sel = select(maxfd + 1, &cli, NULL, NULL, NULL);
if (sel < 0)
{
perror("server select error");
return -1;
}
if (FD_ISSET(0, &cli)) // 判断终端
{
fgets(buf, sizeof(buf), stdin);
printf("stdin: %s\n", buf);
}
if (FD_ISSET(serverfd, &cli)) // 判断客户端
{
// 5.链接客户端的请求 accept()
struct sockaddr_in acceptaddr;
acceptfd = accept(serverfd, (struct sockaddr *)&acceptaddr, &addrlen);
if (acceptfd < 0)
{
perror("accept error");
return -1;
}
printf("acceptfd: %d \n", acceptfd);
printf("新的连接过来了\n");
printf("ip = %s, port = %d\n", inet_ntoa(acceptaddr.sin_addr),
ntohs(acceptaddr.sin_port));
// 把新连接的客户端的文件描述符加到备用表中,
FD_SET(acceptfd, &rfds);
// 选择新的最大的文件描述符
if (acceptfd > maxfd)
{
maxfd = acceptfd;
}
}
// 循环判断客户端的文件描述符是否发生事件,客户端文件描述符发生事件,即客户端发送了消息
for (int i = serverfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &cli)) // 判断客户端
{
// 6.接收/发送数据 recv()/send()
char buf[128] = "";
ssize_t num;
// 每次接收前清空buf
memset(buf, 0, sizeof(buf));
num = recv(i, buf, sizeof(buf), 0);
// num为接收到的字符个数
if (num < 0) // 接收失败
{
perror("recv error");
return -1;
}
else if (num == 0) // 客户端退出
{
perror("client exit");
// 客户端退出之后,关闭对应的文件描述符,并从表中删除对应文件描述符
close(i);
FD_CLR(i, &rfds);
if (i == maxfd)
{
maxfd--;
}
}
else // 正常接收到字符
{
printf(" acceptfd:%d buf: %s \n", i, buf);
}
}
}
memset(buf, 0, sizeof(buf));
}
close(serverfd);
close(acceptfd);
return 0;
}