IO多路复用之select
select函数原型
int select(int maxfdp,
fd_set *readfds, fd_set *writefds, fd_set *errorfds,
struct timeval *timeout);
在上述的参变量中,结构体fd_set
可以理解为一个存放文件描述符(file descriptor
)的集合,并且可以人为的去操作它:
fd_set set;
FD_ZERO(&set) // 将set清零
FD_SET(fd, &set) // 将fd加入set
FD_CLR(fd, &set) // 将fd从set中清除
FD_ISSET(fd, &set) // 如果fd在set中则为真
而结构体timeval
是一个常用的结构体,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。
select各个参数的含义
maxfdp: 该参数代表一个整数值,是指集合中所有文件描述符的范围,表述的是所有文件描述符的最大值+1。
readfds: 该参数是指向fd_set
结构体的指针,这个参数是用来监视fd_set
这个池子里所有文件描述符"可读"这一属性的。如果这个集合中有一个文件可读,select
就会返回一个大于0的值,表示有文件可读。如果没有文件可读,则根据timeout
参数再判断是否超时,若超出timeout
的时间,select
返回0,若有异常则返回负值。该参数可以传NULL
表示不关心读事件。
writefds: 该参数和上述参数同理,负责监视所有写的描述符,若有可写的文件描述符,则返回大于0的数,传入NULL
表示不关心写事件。
errorfds: 用来监视文件错误异常。
timeout: 是select
的超时时间,这个参数至关重要,它可以使select
处于3种状态:
① 若是传入NULL
作为参数,select
则会变成一个阻塞函数,它会一直等待,直到监听文件描述符集合中某个文件描述符发生变化为止。
② 若是传入参数为0,则select
会变成一个完全的非阻塞函数,不管文件描述符是否有变化,都立刻返回,它就是看一眼有变化返回正值,无变化返回0。
③ 若是传入一个非零数,该非零数就是设置的超时时间,该函数会一直阻塞到超出该时间,超出该时间时,有文件描述符变化就返回大于0的数,没有就返回0,不会继续等待。相当于①和②的综合。
select
返回值是准备就绪的描述符数,若超时则返回0,出错返回-1。
select 简单示例
以下代码定义了一个keyboard
描述符从键盘中获取字符,以及一个readfd
,while
循环中把该描述符加入到readfd
中去,然后用select
监听是否在timeout
内有读事件发生,然后根据规则返回值到ret
中,并进行打印输出。若是在timeout
内没有读入事件,那么就超时退出。
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<sys/select.h>
int main() {
int keyboard, ret;
char c;
fd_set readfd;
struct timeval timeout;
keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK);
assert(keyboard > 0);
while (1) {
timeout.tv_sec = 5, timeout.tv_usec = 0;
FD_ZERO(&readfd);
FD_SET(keyboard, &readfd);
ret = select(keyboard + 1, &readfd, NULL, NULL, &timeout);
if (ret == -1) {
printf("select error!");
} else if (ret) {
if (FD_ISSET(keyboard, &readfd)) {
read(keyboard, &c, 1);
if (c == '\n') continue;
printf("Input is %c\n", c);
if (c == 'q') break;
}
} else if (ret == 0) {
printf("time out \n");
break;
}
}
return 0;
}
服务端客户端举例
我已经把关键的地方写到了注释里面,程序是在linux上才能运行,最多可以对应五个客户端连接。
server端
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define DEFAULT_PORT 6666
int main(int argc, char **argv) {
int serverfd;
struct sockaddr_in serverAddr;
// 创建socket
if ((serverfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("Create socket error!\n");
return -1;
}
printf("socket sucessfully!\n");
// 绑定IP和端口号
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(6666);
serverAddr.sin_addr.s_addr = INADDR_ANY;
if (bind(serverfd, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr)) == -1) {
printf("Bind error!\n");
return -2;
}
printf("bind sucessfully!\n");
// 监听
int num = 10; // 这个变量这里先不用管
if (listen(serverfd, num) == -1) {
printf("listen error!\n");
return -3;
}
printf("listen sucessfully!\n");
// 初始化一些量
fd_set clientfdSet; // 监听文件描述符集合
int clientSockFd[5]; // 把来连接的client的fd放到一个数组中, 这里设置了最多连接5个client
memset(clientSockFd, 0, sizeof(clientSockFd));
struct timeval timeout; // 超时返回时间
int clientNumber = 0; // 用来记录描述符(连接)的数量
int maxSockValue = serverfd; // 描述符的最大值, 一开始只有serverfd, 所以最大的描述符为serverfd
char buffer[1024]; // 缓存数据用的buffer
int ret = 0;
while (1) {
FD_ZERO(&clientfdSet);
FD_SET(serverfd, &clientfdSet);
timeout.tv_sec = 30;
timeout.tv_usec = 0;
// 把非零的fd加到文件描述符集合(池子)中, (我个人喜欢叫做池子)
for (int i = 0; i < 5; i++) {
if (clientSockFd[i] != 0) {
FD_SET(clientSockFd[i], &clientfdSet);
}
}
// select函数
ret = select(maxSockValue + 1, &clientfdSet, NULL, NULL, &timeout);
if (ret < 0) {
printf("Select error!\n");
break;
} else if (ret == 0) {
printf("Timeout!\n");
continue; // 这里只是说30s没有动静会提示超时, 但是没有break
}
// 若返回的是一个大于0的数, 就进行下面的操作, 说明有读事件发生, 但我们不知道具体是哪个连接需要读, 所以要进一步遍历
// 这也是select和epoll的区别
for (int i = 0; i < clientNumber; i++) {
if (FD_ISSET(clientSockFd[i], &clientfdSet)) {
printf("Start receive from client[%d]: \n", i);
ret = recv(clientSockFd[i], buffer, 1024, 0);
if (ret <= 0) {
printf("client[%d] close\n", i);
close(clientSockFd[i]);
FD_CLR(clientSockFd[i], &clientfdSet);
clientSockFd[i] = 0;
} else {
printf("Receive from client[%d]: %s\n", i, buffer);
}
}
}
// 此时我们也要看serverfd是否发生了新连接, 若有新连接则把它加入到池子里
if (FD_ISSET(serverfd, &clientfdSet)) {
struct sockaddr_in client_addr;
size_t size = sizeof(struct sockaddr_in);
int clientSock = accept(serverfd, (struct sockaddr*)(&client_addr), (unsigned int*)(&size));
if (clientSock < 0) {
printf("Accept error\n");
continue;
}
if (clientNumber < 5) {
clientSockFd[clientNumber++] = clientSock;
memset(buffer, 0, sizeof(1024));
strcpy(buffer, "Welcome!\n");
send(clientSock, buffer, 1024, 0);
printf("New connection client[%d] %s: %d\n", clientNumber, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
memset(buffer, 0, sizeof(1024));
ret = recv(clientSock, buffer, 1024, 0);
if (ret < 0) {
printf("Recvive error\n");
close(serverfd);
return -1;
}
printf("Receive: %s\n", buffer);
if (clientSock > maxSockValue) {
maxSockValue = clientSock;
} else {
printf("Max connection!\n");
break;
}
}
}
}
for (int i = 0; i < 5; i++) {
if (clientSockFd[i] != 0) {
close(clientSockFd[i]);
}
}
close(serverfd);
return 0;
}
client端
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#include <string>
#include <iostream>
#define DEFAULT_PORT 6666
using namespace std;
int main(int argc, char **argv) {
int connectFd = 0, cLen = 0;
if(argc < 2) {
printf("Use: clientent [serverIP address]\n");
return -1;
}
struct sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(DEFAULT_PORT);
client.sin_addr.s_addr = inet_addr(argv[1]);
connectFd = socket(AF_INET, SOCK_STREAM, 0);
if (connectFd < 0) {
printf("Socket error!\n");
return -1;
}
if (connect(connectFd, (struct sockaddr*)&client, sizeof(client)) < 0) {
printf("Connect error!\n");
return -1;
}
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
recv(connectFd, buffer, 1024, 0);
printf("Recvive from server: %s\n", buffer);
memset(buffer, 0, sizeof(buffer));
strcpy(buffer, "This is client!\n");
send(connectFd, buffer, 1024, 0);
while (1) {
string buffer; // 为了获取带有空格的字符串
getline(cin, buffer);
// memset(buffer, 0, sizeof(buffer));
// scanf("%s", buffer);
send(connectFd, buffer.c_str(), 1024, 0);
printf("Send buff successfully!\n");
}
close(connectFd);
return 0;
}