Lv.的博客

windows下的IO模型之选择(select)模型

 

1.选择(select)模型:
选择模型:通过一个fd_set集合管理套接字,在满足套接字需求后,通知套接字。让套接字进行工作。

选择模型的核心是FD_SET集合和select函数。通过该函数,我们可以们判断套接字上是否存在数据,或者能否向一个套接字写入数据。

用途:如果我们想接受多个SOCKET的数据,该怎么处理呢?

由于当前socket是阻塞的,直接处理是一定完成不了要求的

a.我们会想到多线程,的确可以解决线程的阻塞问题,但开辟大量的线程并不是什么好的选择; 

b我们可以想到用ioctlsocket()函数把socket设置成非阻塞的,然后用循环逐个socket查看当前套接字是否有数据,轮询进行。

这种是可以解决问题的,但是会导致频繁切换状态到内核去查看是否有数据到达,浪费时间。

c.于是想办法用只切换一次状态就知道所有socket的接受缓冲区是否有数据,于是有了select模型,select是阻塞的,Select的好处是可以同时处理若干个Socket,

 

select阻塞么

 

一个套接字阻塞或者不阻塞,select就在那里,它可以针对这2种套接字使用,对任何一种套接字的轮询检测,超时时间都是有效的,区别就在于:

 当select完毕,认为该套接字可读时,

 1 .阻塞的套接字,会让read阻塞,直到读到所需要的所有字节; 

2 .非阻塞的套接字,会让read读完fd中的数据后就返回,但如果原本你要求读10个数据,这时只读了8个数据,如果你不再次使用select来判断它是否可读,而是直接read,很可能返回EAGAIN或=EWOULDBLOCK(BSD风格) ,
     此错误由在非阻塞套接字上不能立即完成的操作返回,例如,当套接字上没有排队数据可读时调用了recv()函数。此错误不是严重错误,相应操作应该稍后重试。对于在非阻塞   SOCK_STREAM套接字上调用connect()函数来说,报告EWOULDBLOCK是正常的,因为建立一个连接必须花费一些时间。

    EWOULDBLOCK的意思是如果你不把socket设成非阻塞(即阻塞)模式时,这个读操作将阻塞,也就是说数据还未准备好(但系统知道数据来了,所以select告诉你那个socket可读)。使用非阻塞模式做I/O操作的细心的人会检查errno是不是EAGAIN、EWOULDBLOCK、EINTR,如果是就应该重读,一般是用循环。如果你不是一定要用非阻塞就不要设成这样,这就是为什么系统的默认模式是阻塞。

 通过完善select模型可以得到IO复用模型,详情请看:http://www.cnblogs.com/curo0119/p/8461520.html

一个IO模型的阻塞非阻塞指的是数据访问过程,而不是socket.

select是一个异步阻塞模型。

 

 

2.select函数:
int select(
    int nfds,//忽略,只是为了保持与早期的Berkeley套接字应用程序的兼容

    fd_set FAR* readfds,//可读性检查(有数据可读入,连接关闭,重设,终止),为空则不检查可读性
    fd_set FAR* writefds,//可写性检查(有数据可发出),为空则不检查可写性
    fd+set FAR* exceptfds,//带外数据检查(带外数据),为空则不检查
    const struct timeval FAR* timeout//超时
    );


3.select模型的工作步骤:
(1)定义一个集合fd_set并用fd_zero宏初始化为空

(2)用FD_SET宏,把套接字句柄加入到fd_set集合

(3)调用select函数,检查每个套接字的可读可写性,select完成后,会返回所有在fd_set集合中有数据到达的socket的socket句柄总数,并对每个集合进行更新,即没有数据到达的socket在原集合中会被置成空。
(4)根据select的返回值以及FD_ISSET宏,对FD_SET集合进行检查
(5)知道了每个集合中“待决”的I/O操作后,对相应I/O操作进行处理,返回步骤1,继续select

select函数返回后,会修改FD_SET的结构,删除不存在待决IO操作的套接字,这也就是为什么我们之后要用FD_ISSET判断是否还在集合中的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool UDPNet::SelectSocket()
{
    timeval tv;
    tv.tv_sec =0;
    tv.tv_usec = 100;
    fd_set fdsets;//创建集合
    FD_ZERO(&fdsets); //初始化集合
 
    FD_SET(m_socklisten,&fdsets);//将socket加入到集合中(此例子是一个socket),将多个socket加入时,可以用数组加for循环
 
    select(NULL,&fdsets,NULL,NULL,&tv);//只检查可读性,即fd_set中的fd_read进行操作
 
    if(!FD_ISSET(m_socklisten,&fdsets))//检查 s是否s e t集合的一名成员;如答案是肯定的是,则返回 T R U E。
    {
        return false;
    }
    return true;
}

4.select函数参数详解:  

三个 fd_set参数:一个用于检查可读性(readfds),一个用于检查可写性(writefds),另一个用于例外数据( excepfds)。

从根本上说,fdset数据类型代表着一系列特定套接字的集合。其中,

readfds集合包括符合下述任何一个条件的套接字:

■ 有数据可以读入。
■ 连接已经关闭、重设或中止。
■ 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。

writefds集合包括符合下述任何一个条件的套接字:

■ 有数据可以发出。
■ 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:
■ 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
■ 有带外(out-of-band,OOB)数据可供读取。

最后一个参数timeout:

对应的是一个指针,它指向一个timeval结构,用于决定select最多等待 I / O操作完成多久的时间。

如 timeout是一个空指针,那么select调用会无限期地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。

对timeval结构的定义如下:

struct timeval {
long tv_sec;
long tv_usec;

} ;

若将超时值设置为(0,0),表明select会立即返回,允许应用程序对 select操作进行“轮询”。出于对性能方面的考虑,应避免这样的设置。

select成功完成后,会在 fd_set结构中,返回刚好有未完成的I/O操作的所有套接字句柄的总量。

若超过timeval设定的时间,便会返回0。

如何测试一个套接字是否“可读”?

必须将自己的套接字增添到readfds集合,再等待select函数完成。

select完成之后,必须判断自己的套接字是否仍为readfds集合的一部分。若答案是肯定的,便表明该套接字“可读”,可立即着手从它上面读取数据。

在三个参数中(readfds、writedfss和exceptfds),任何两个都可以是空值(NULL);但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;

否则, select函数便没有任何东西可以等待。

不管由于什么原因,假如select调用失败,都会返回SOCKET_ERROR

5.select优缺点:

优点:可实现单线程处理多个任务

缺点:

a.等待数据到达的过程以及将数据从内核拷贝到用户的过程总也存在一定阻塞

b.管理的set数组有一定上限,最多是64个(可通过重置fd_setsize将上限扩大到1024)

c.select低效是因为每次它都需要轮询。

 

完整代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include "stdafx.h"
#include <WinSock2.h>
#include <iostream>
using namespace std;
 
#include <stdio.h>
 
#pragma comment(lib,"ws2_32.lib")
 
#define PORT 8000
#define MSGSIZE 255
#define SRV_IP "127.0.0.1"
 
int g_nSockConn = 0;//请求连接的数目
 
//FD_SETSIZE是在winsocket2.h头文件里定义的,这里windows默认最大为64
//在包含winsocket2.h头文件前使用宏定义可以修改这个值
 
 
struct ClientInfo
{
    SOCKET sockClient;
    SOCKADDR_IN addrClient;
};
 
ClientInfo g_Client[FD_SETSIZE];
 
DWORD WINAPI WorkThread(LPVOID lpParameter);
 
int _tmain(int argc, _TCHAR* argv[])
{//基本步骤就不解释了,网络编程基础那篇博客里讲的很详细了
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2,2),&wsaData);
 
    SOCKET sockListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
 
    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP);
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(PORT);
 
    bind(sockListen,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
 
    listen(sockListen,64);
 
    DWORD dwThreadIDRecv = 0;
    DWORD dwThreadIDWrite = 0;
 
    HANDLE hand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用来处理手法消息的进程
    if (hand == NULL)
    {
        cout<<"Create work thread failed\n";
        getchar();
        return -1;
    }
 
    SOCKET sockClient;
    SOCKADDR_IN addrClient;
    int nLenAddrClient = sizeof(SOCKADDR);//这里用0初试化找了半天才找出错误
 
    while (true)
    {
        sockClient = accept(sockListen,(SOCKADDR*)&addrClient,&nLenAddrClient);//第三个参数一定要按照addrClient大小初始化
        //输出连接者的地址信息
        //cout<<inet_ntoa(addrClient.sin_addr)<<":"<<ntohs(addrClient.sin_port)<<"has connect !"<<endl;
 
        if (sockClient != INVALID_SOCKET)
        {
            g_Client[g_nSockConn].addrClient = addrClient;//保存连接端地址信息
            g_Client[g_nSockConn].sockClient = sockClient;//加入连接者队列
            g_nSockConn++;
        }
 
 
    }
 
    closesocket(sockListen);
    WSACleanup();
 
    return 0;
}
 
DWORD WINAPI WorkThread(LPVOID lpParameter)
{
    FD_SET fdRead;
    int nRet = 0;//记录发送或者接受的字节数
    TIMEVAL tv;//设置超时等待时间
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    char buf[MSGSIZE] = "";
 
    while (true)
    {
        FD_ZERO(&fdRead);
        for (int i = 0;i < g_nSockConn;i++)
        {
            FD_SET(g_Client[i].sockClient,&fdRead);
        }
 
        //只处理read事件,不过后面还是会有读写消息发送的
        nRet = select(0,&fdRead,NULL,NULL,&tv);
 
        if (nRet == 0)
        {//没有连接或者没有读事件
            continue;
        }
 
        for (int i = 0;i < g_nSockConn;i++)
        {
            if (FD_ISSET(g_Client[i].sockClient,&fdRead))
            {<br>          //如果在集合中,向下进行相应的IO操作
                nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0);//看是否能正常接收到数据
 
                if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
                {
                    cout<<"Client "<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<"closed"<<endl;
                    closesocket(g_Client[i].sockClient);
 
                    if (i < g_nSockConn-1)
                    {
                        //将失效的sockClient剔除,用数组的最后一个补上去
                        g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient;<br>              //i--是因为要重新判断新的i的位置的socket是否失效
                    }
                }
                else
                {
                    cout<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<": "<<endl;
                    cout<<buf<<endl;
                    cout<<"Server:"<<endl;
                    //gets(buf);
                    strcpy(buf,"Hello!");
                    nRet = send(g_Client[i].sockClient,buf,strlen(buf)+1,0);
                }
            }
        }
    }
    return 0;
}

  

服务器的主要步骤:

1.创建监听套接字,绑定,监听

2.创建工作者线程

3.创建一个套接字组,用来存放当前所有活动的客户端套接字,没accept一个连接就更新一次数组

4.接收客户端的连接,因为没有重新定义FD_SIZE宏,服务器最多支持64个并发连接。最好是记录下连接数,不要无条件的接受连接

 

工作线程

工作线程是一个死循环,依次循环完成的动作是:

1.将当前客户端套接字加入到fd_read集中

2.调用select函数

3.用FD_ISSET查看时候套接字还在读集中,如果是就接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,,则

   表示客户端套接字主动关闭,我们要释放这个套接字资源,调整我们的套接字数组(让下一个补上)。上面还有个nRet==0的判断,

   就是因为select函数会立即返回,连接数为0会陷入死循环。

posted @ 2021-10-27 16:11  Avatarx  阅读(332)  评论(0编辑  收藏  举报