select函数处理socket多IO并发
此文接上篇文章
https://blog.csdn.net/liufeng_06/article/details/105007836
简介
网络编程的服务端程序在大多数的情况下,并不只是与一个客户端进行通讯。在嵌入式行业中,设备通常被被要求至少同时需要与5-10个客户端同时通信,而对于嵌入式设备来说,其内部资源是非常有限的,我们不可能使用多进程来实现该功能,在Linux设备中通常是使用多线程和select
函数或者poll
函数进行处理。这里作者之所以选择select
函数,是因为这种处理方式不仅仅在Linux系统中可以使用,在资源更加宝贵的MCU中,我们通过使用 lwip 协议栈可以使用select
函数。
select函数
在Linux下使用该函数需要加上以下头文件
include <sys/select.h>
函数原型为:
int select (int maxfd + 1,fd_set *readset,fd_set *writeset, fd_set *exceptset,const struct timeval * timeout);
参数一 :
最大的文件描述符 + 1。是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!
参数二 :
用于检查可读性,是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
参数三:
用于检查可写性,具体的解释同参数二一致。
参数四:
用于检查文件错误异常,具体的解释同参数二一致。
参数五
用于指定select
函数运行到此处时等待的时间。这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
注意点
参数五经过select
函数使用以后,消耗的时间一直是递减的。所以在select使用前,每次都需要重新给其赋值。如果我在其初始化时赋值为20s,第一次使用消耗了3s,那么下次使用如果任然想让其等待20s(注意这里并不是一定等20s,在等待期间有描述符可操作的话会立即返回的)就必须重新给其赋值为20s
返回值
>0 集合中就绪的个数
-1 出错
0 函数超时
与该函数相关的一些结构体和宏
```
----------------------------------------------------------------
struct timeval
{
long tv_sec; //秒
long tv_usec; //微妙
}
------------------------------------------------------------------
------------------------------------------------------------------
fd_set fd;
void FD_ZERO (fd_set *fdset); //清除整个描述符集
void FD_SET (int fd,fd_set *fdset); //往描述符集里添加一个描述符
void FD_CLR (int fd,fd_set *fdset); // 删除一个描述符集里的特定描述符
int FD_ISSET(int fd,fd_set *fdset); // 检查一个描述集的一个特定描述符是否可读或者可写
------------------------------------------------------------------
```
代码如下
想实现的状态是:TCP服务端可以同时接受多个客户端的连接,并实现回显功能。
实现的大概思路如下:
1、定义一个fds[MAX_CLIENT_NUM]数组,其第0个元素 fds[0]固定为监听是否有新的客户端请求连接,其他元素则为通过accept
函数创建的新的套接字。
2、由于可读、可写、出错描述符集的功能大概相同,为了简单,我只对可读描述符集进行监测。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/un.h>
#include <fcntl.h>
#include <sys/select.h>
#define MAX_CLIENT_NUM 5 //最大监听客户端连接数量为5
int main(int argc,char *argv[])
{
char buf[512];
int sfd,iflgs;
int max_fd;
int fds[MAX_CLIENT_NUM];
fd_set rdset;
if(argc != 3)
{
printf("please input PORT and IP ADDR!\n");
return 0;
}
sfd = socket(AF_INET,SOCK_STREAM,0);
if (sfd == -1)
printf("socker create faild!\n");
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //此处IP地址必须为linux下网口ip地址
//因为socket默认为阻塞,这里需要将其设置为非阻塞模式
iflgs = fcntl(sfd,F_GETFL,0);
fcntl(sfd,F_SETFL,iflgs|O_NONBLOCK);
if(bind(sfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr)) == -1)
{
printf("bind!\n");
close(sfd);
}
if(listen(sfd,MAX_CLIENT_NUM) == -1)
{
printf("listen!\n");
close(sfd);
}
//fds[MAX_CLIENT_NUM]初始化为 -1
for(int i=0; i<MAX_CLIENT_NUM;i++)
{
fds[i] = -1;
}
max_fd = sfd;
fds[0] = sfd; //fds[0]固定为创建socket时返回的套接字描述符
int client_fd;
struct sockaddr_in client_addr;
int socket_len = sizeof(struct sockaddr_in);
bzero(&client_addr,socket_len);
struct timeval timeout;
while(1)
{
FD_ZERO(&rdset); //每个循序都必须清除可读描述符集
for(int i=0; i<MAX_CLIENT_NUM;i++)
{
if(fds[i] != -1)
{
printf("fds[%d] = %d\n",i,fds[i]);
FD_SET(fds[i],&rdset);
if(fds[i] > max_fd) //找到所有描述符中的最大描述符
{
max_fd = fds[i];
}
}
}
//若select没有检测到有可读的描述符,则3s后退出该函数,并返回 0 表示 时间超时
timeout.tv_sec = 3;
timeout.tv_usec = 0;
switch(select(max_fd+1,&rdset,NULL,NULL,&timeout))
{
case -1:
printf("select error!\n");
break;
case 0:
printf("wait timeout !\n");
break;
default: //有准备就绪的可读描述符
for(int i=0; i<MAX_CLIENT_NUM;i++)
{
if(fds[i] == sfd && FD_ISSET(sfd,&rdset)) //有新的客户端请求连接
{
client_fd = accept(sfd,(struct sockaddr*)&client_addr,&socket_len);
if(client_fd == -1)
{
close(sfd);
printf("--accept error-----------");
return 0;
}
else //打印请求连接的客户端IP地址和端口号
{
char *ptr = inet_ntoa(client_addr.sin_addr); //IP地址,inet_ntoa函数将32位无符号的IP地址转化为X.X.X.X的形式
unsigned short port_tmp = ntohs(client_addr.sin_port);
printf("------------------------------------------------------\n");
printf("client ip :%s---port:%d\n",ptr,port_tmp);
printf("------------------------------------------------------\n");
}
//将新的套接字描述符加载到fds[MAX_CLIENT_NUM]中
for(int j=0; j<MAX_CLIENT_NUM;j++)
{
if(fds[j] == -1)
{
fds[j] = client_fd;
break;
}
}
}
else if(fds[i] != -1 && FD_ISSET(fds[i],&rdset))
{
//数据回显功能
int rev_num = recv(fds[i],buf,sizeof(buf),0);
//客户端主动关闭网络连接
if(rev_num == 0)
{
close(fds[i]);
fds[i] = -1;
break;
}
send(fds[i],buf,rev_num,0);
}
}
break;
}
}
close(sfd);
}
功能测试
1、在Linux下编译并运行
2、程序在Linux下运行时,在win下通过多个网络调试助手去连接。
连接成功以后,程序打印如下:
==注意:==其IP地址虽然一致,但是端口号并不同,所以是两个TCP客户端
3、测试两个客户端的回显功能
总结:
关于上文说的有关 timeout的注意点,大家可以把对timeout的赋值放到 while(1)
外,就可以发现,若是没有新的客户端连接请求(fds[0]一直在监测)或者新的可读数据时,程序会一直打印 wait timeout并没有按照设想的,我只赋值一次,每次都会等待3s。