Windows与Linux下的select网络模型对比分析

本文详细比较了Windows与Linux环境下select模型的实现原理及优劣。深入剖析了两种操作系统中select模型的具体实现方式,包括fd_set结构体的定义、select函数的使用,并通过示例代码展示了模型的工作流程。
摘要由CSDN通过智能技术生成

引言:最近同时接触了Windows与Linux下的select模型,原以为两者相同,其实并非如此。以下便是对Windows与Linux下的select模型进行的比较分析,以供大家学习,共同进步,不足之处请留言。

Windows下select模型

1.Windows下的 fd_set 原型:

typedef struct fd_set {
  u_int fd_count;               //记录连接的套接字的个数
  SOCKET fd_array[FD_SETSIZE]; // 存储连接的套接字的数组
} fd_set;

功能:保存连接的套接字句柄的结构体


2.Windows下的Select函数原型:

int select(
  int nfds,    // 可忽略,无意义,填0
  fd_set FAR* readfds,  // 检查可读性
  fd_set FAR* writefds, // 检查可写性
  fd_set FAR* exceptfds, // (可选指针)要检查错误的一组套接字
  const struct timeval FAR* timeout // 等待时间,使用结构体timeval
);

MSDN:This function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.
功能:轮询检查的套接字信号的到来,将有可读信号的套接字保存在readfds参数中,将有可写信号的套接字保存在writefds参数中。


3.理解 Windows下select实现原理:
select实现的伪代码

DWORD WINAPI SelectModel::ServiceProc(LPARAM lparam)
{
    ..
    fd_set fdRead;  
    while (true)
    {
        FD_ZERO(&fdRead);//清空
        fdRead = g_fdClientSock;
        timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 100;
        nRet = select(0, &fdRead, 0, 0, &tv);//select
        if (nRet == 0)//超时
            continue;
        else if(nRet != SOCKET_ERROR)
        {
            for (int i = 0; i < fdRead.fd_count; i++)//for取出有信号的套接字
            {
                ... ...
            }
        }
    }
    return 0;
}

假设有5个客户端连接,假设它们socket值分别为101、102、103、104、105(并非是linux下的位)。此时
g_fdClientSock.fd_count=5,
g_fdClientSock.fd_array[0]=101
g_fdClientSock.fd_array[1]=102
g_fdClientSock.fd_array[2]=103
g_fdClientSock.fd_array[3]=104
g_fdClientSock.fd_array[4]=105
(1) fdRead = g_fdClientSock;我们将g_fdClientSock赋值给临时变量fdRead,将fdRead作为select检查后存储有数据信号的套接字的结果集。
(2) nRet = select(0, &fdRead, 0, 0, &tv);此时有套接字102、103在超时时间内(timeval)并发到来,返回值nRet此时为2,fdRead的fd_count为2,它的
fd_array[0]=102
fd_array[1]=103
fd_array[2]=103
fd_array[3]=104
fd_array[4]=105
不妨看出,Windows下的select实现就是轮询一遍我们要检查的fdRead,然后将有信号的套接字按照数组内的排序顺序依次放在fdRead的前面,我们再使用的时候只需要根据nRet(有数据到来的信号数量)与fdRead检测到谁有信号到来。
如果实现原理还有不怎么明了的,这里再啰嗦举例两个:

  • 此时101、104有信号到来,返回值nRet=2,fdRead的fd_count为2,fdRead的
    fd_array[0]=101
    fd_array[1]=104
    fd_array[2]=103
    fd_array[3]=104
    fd_array[4]=105
  • 此时103、104、105有信号到来,返回值nRet=3,fdRead的fd_count为3,fdRead的
    fd_array[0]=103
    fd_array[1]=104
    fd_array[2]=105
    fd_array[3]=104
    fd_array[4]=105

(3)数据的接收,我们只需要使用一次for循环就能取出来

for (int i = 0; i < fdRead.fd_count; i++)
{
    ... ...
}

4.select模型的局限与优化
1) FD_SETSIZE的值为64,通常认为select的局限为64,既然为通常,那么我们就会有相关的优化方法
2) 普遍认为的优化是使用自定义的fd_set结构体,但是select函数一次只能轮询64个,如果你定义FD_SETSIZE为640的话,可以加一个循环,循环十次分别使用select检测数据信号的到来,此处也能多加线程,但是要注意线程同步的问题。
3) 少见高效率的优化方法,为libevent库中讲到的动态fd_array的方法,能够达到的效果为万级别,它新定义的fd_set结构体为:

struct win_fd_set {
    u_int fd_count;
    SOCKET fd_array[1];
};

此处fd_array的大小为1,此后使用的是动态申请内存的方法,使得fd_array动态变化:

win_fd_set * Set = (win_fd_set*)malloc(sizeof(win_fd_set) + sizoef(SCOEKT) * n);
//n为1时,fd_array的大小为2,n为100时,fd_array的大小为1001

至于具体的实现方法,大家可以下载libevent看看里面的源码


5.相关宏的实现

#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0, 0)//

#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0, 0)

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)

#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))

FD_CLR(fd, set) //从fd_set中清除一个套接字描述符(句柄)
FD_ZERO(set) //清空fd_set结构体
FD_ISSET(fd, set) //判断某个套接字描述符是否有信号,内部调用的是__WSAFDIsSet,也可以用程序中直接循环fdread的方法,此时就不用调用FD_ISSET实现
FD_SET(fd, set) //将套接字描述符添加到fd_set结构体中

Linux下的select模型

1.Linux下的fd_set原型:

#define __NFDBITS (8 * sizeof(unsigned long))                //每个ulong型可以表示的bit数,8*4=32
#define __FD_SETSIZE 1024                                          //socket最大取值为1024
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)     //1024个bit,共需要多少个ulong?1024/32=32

typedef struct {
    unsigned long fds_bits [__FDSET_LONGS];                 
} __kernel_fd_set;
 / /用ulong数组的bit位来存储套接字信息
0000 0000 0000 00000000 0000 0000 000016行,16列
…
0000 0000 0000 00000000 0000 0000 0000

typedef __kernel_fd_set   fd_set;

从宏__FD_SETSIZE看出,Linux下的select模型的上限为1024。比Windows的select更强大。当然我们也可以多线程,也可以重定义__FD_SETSIZE来改变容量的大小,但是还是得根据你的服务器配置来。


2.Linux下的Select函数原型:
这里的第一个参数与Windows下的不同,第一参数表示select检查的最大连接数
int select(
int nfds, // 监控的套接字最大值+1
fd_set *readfds, // 集合中任意描述字准备好读,则返回
fd_set *writefds, // 集合中任意描述字准备好写,则返回
fd_set *exceptfds, // 集合中任意描述字有异常等待处理,则返回
struct timeval *timeout); // 超时则返回(NULL 则一直等待,0 则立即返回)
返回值:返回值 =0 超时, 返回值<0 错误,返回值>0正常


3.理解Linux下select的实现原理
引自:http://blog.csdn.net/qdx411324962/article/details/42499535
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。


4.Linux下select的局限

(1)每次调用 select 都需要把fd集合从用户态拷贝到内核态,fd很多时开销很大
(2)调用 select 需要内核遍历 fd, fd 很多时开销很大
(3)select 支持文件描述符监视有数量限制,默认 1024


5.相关宏的实现

#define __FD_SET(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] |= (1<<((fd) & 31)))
#define __FD_CLR(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] &= ~(1<<((fd) & 31))) 
#define __FD_ISSET(fd, fdsetp)   ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] & (1<<((fd) & 31))) != 0) 
#define __FD_ZERO(fdsetp)   (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp)))) 

FD_SET:设置对应的bit为1(增加fd 到集合中)
FD_CLR:清除对应的bit位(从集合中清除fd)
FD_ISSET:判断对应的bit是否为1(描述字是否准备好)
FD_ZERO:清空所有的bit位(清空描述符集合)

模型对比解析select的优势

同步阻塞的I/O模型图
I/O同步阻塞
指明一点,在阻塞的通信中,recv并不是直接就获取到数据,进行recv之后,开始的是等待有数据到来的信号,此时内核开始接收数据,结束数据完毕后,发一个数据到来的信号出去,recv接收到这个信号后,就去内核拷贝数据到用户空间(程序缓冲区),所以注意了recv做的只是从内核的缓冲区拷贝到程序的缓冲区的事情!

通过了解之后,我们知道recv阻塞的时间是浪费在接收数据+将数据从内核拷贝到用户空间(程序的缓冲区)中的。这里可以复习下recv与send数据的原理


I/O复用模型(select)
I/O复用模型
select模型究竟相对于同步阻塞模型解决了什么问题?
解决的是“等待数据信号到来的时间”,将等待数据信号到来的时间交给select同时检测多个套接字的多个据到来的消息(通过检测内核中是否有已注册的I/O事件有数据的到来,如果有数据的到来就返回告诉主程序有注册的I/O事件的数据到来),然后recv就直接开始去内核中拷贝数据到程序缓冲区中

select的缺点
1)虽然select能够同时检测多个I/O事件,但是我们注意到select的第五个参数,是轮询检测数据到来的时间,微观来说,在select的这段时间里面是阻塞的,而且它占用的是程序自生的时间片,此时就会浪费cpu资源。
2)前面也有讲到Windows下FD_SETSIZE与Linux下__FD_SETSIZE的限制问题。Linux可以通过sizeof(fd_set)获取select模型的上限
3)select并没有解决数据从内核缓冲区拷贝到程序缓冲区的时间问题,它得到的仅是数据到达内核后的信号,拷贝数据还是得通过recv,而recv占用的也是程序自身的时间片

Windows下select模型的实现

实现流程图
方便理解代码的流程图
Accept部分代码

BOOL SelectModel::Accept()
{
    SOCKADDR_IN clientAddr;
    int nLen = sizeof(SOCKADDR_IN);
    stClientInfo clientInfo;
    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ServiceProc, (LPVOID)this, NULL,NULL);
    while (true)
    {
        if (g_fdClientSock.fd_count >= FD_SETSIZE)
        {
            Sleep(50);
            continue;
        }
        memset(&clientInfo, 0, sizeof(stClientInfo));
        memset(&clientAddr, 0, sizeof(SOCKADDR_IN));
        SOCKET clientSock = accept(m_hSocket, (sockaddr*)&clientAddr, &nLen);
        if (clientSock == INVALID_SOCKET)
            continue;
        strcpy(clientInfo.sIP, inet_ntoa(clientAddr.sin_addr));
        clientInfo.nPort = htons(clientAddr.sin_port);
        g_mapClientInfo[clientSock] = clientInfo;
        FD_SET(clientSock, &g_fdClientSock);
    }
}

通过一个while循环接受(accept)客户端的连接,将接收到的客户端信息添加到fd_set结构体g_fdClientSock中


ServiceProc线程函数

DWORD WINAPI SelectModel::ServiceProc(LPARAM lparam)
{
    SelectModel* pSelect = (SelectModel*)lparam;
    fd_set fdRead;  
    char* recvBuffer = (char*)malloc(sizeof(char) * 1024);
    if (recvBuffer == NULL)
        return -1;
    int nRet = 0;
    while (true)
    {
        FD_ZERO(&fdRead);
        fdRead = g_fdClientSock;
        timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 100;
        nRet = select(0, &fdRead, 0, 0, &tv);
        if (nRet == 0)//超时
            continue;
        else if(nRet != SOCKET_ERROR)
        {
            for (int i = 0; i < fdRead.fd_count; i++)
            {
                memset(recvBuffer, 0, sizeof(char) * 1024);
                nRet = recv(fdRead.fd_array[i], recvBuffer, sizeof(char) * 1024, 0);
                if (nRet == SOCKET_ERROR || nRet == 0)
                {
                    pSelect->EraseClientInfo(g_fdClientSock.fd_array[i]);
                    closesocket(g_fdClientSock.fd_array[i]);
                    FD_CLR(g_fdClientSock.fd_array[i], &g_fdClientSock);
                }
                else
                {
                    pSelect->NetFunc(fdRead.fd_array[i], (void*)recvBuffer, strlen(recvBuffer));
                }
            }
        }
    }
    return 0;
}

通过select检测fdRead中的套接字是否有信号到来,如果有信号到来,通过for循环一次取出相应的套接字,通过recv接受有信号的套接字的数据,接收完数据后,通过消息处理函数NetFunc进行消息的处理。如果客户端断开连接,就关闭套接字(closesocket),然后FD_CLR掉g_fdClientSock中对应的套接字描述符的信息


避免文章过长,linux下源码自行下载查阅,原理和Windows一样:http://pan.baidu.com/s/1pKB9KZh
自己封装的Windows下select模型源码:http://pan.baidu.com/s/1bp74ArP

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值