WinSock 异步I/O模型[1]---选择模型 - select

1.选择(select)模型:


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

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

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

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

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

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

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

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


2.select函数:


int select(
    int nfds,//忽略,只是为了兼容而存在。
    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

4.参数

在三个参数中(readfds、writefds和exceptfds),任何两个都可以是空值;但是,至少有一个不能为空值!最后一个参数timeout对应的是一个指针,它指向一个timeval结构,用于决select最多等待I/O操作完成多久的时间。如timeout是一个空指针,那么select函数会无限期地“等待下去,直到至少有一个套接字符合指定的条件后返回。select成功完成后,会在fd_set集合中,返回未完成的I/O操作的套接字句柄的总量。若超时,便会返回0。不管由于什么原因,假如select调用失败,都会返回SOCKET_ERROR错误。


5.timeval结构体定义:


struct timeval
{
    long tv_sec;//秒数
    long tv_usec;//毫秒数
};


6.fd_set集合:

用select函数对套接字进行监视之前,必须要将套接字分配给一个fd_set集合,设置好读、写以及带外数据的fd_set结构。将一个套接字分配给任何一个集合后,再来调用select进行监视,便可知道一个套接字上是否正在发生上述的I/O活动。Winsock提供了下列宏操
作,对fd_set进行处理和检查:
    FD_ZERO(*set):初始化set
    FD_SET(s, *set):将套接字s加入集合set
    FD_CLR(s, *set):从set中删除套接字s。
    FD_ISSET(s,*set):检查s是否还在集合set上,在调用select函数之前必须对此进行判断。


7.select模型的工作步骤:


(1) 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。
(2) 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。
(3) 调用select函数,等待I/O操作的完成。
(4) 根据select的返回值,我们便可判断出哪些套接字存在着尚未完成(待决)的I/O操作,.具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。
(5) 知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤1 ),继续进行select处理。
(6)select返回后,它会修改每个fd_set结构,删除那些不存在待决I/O操作的套接字句柄。这正是我们在上述的步骤( 4 )中,为何要使用FD_ISSET宏来判断一个特定的套接字是否仍在集合中的原因。

bool 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循环
 
    int ret=select(NULL,&fdsets,NULL,NULL,&tv);//只检查可读性,即fd_set中的fd_read进行操作

    if(ret==SOCKET_ERROR)
    {
         ...
    }
 
    if(res>0)
    {
         //处理数据
         ...
          if(!FD_ISSET(m_socklisten,&fdsets))//检查 s是否set集合的一名成员;如答案是肯定的是,则返回 T R U E。//检查套接字是否还在集合上
          {
             return false;
          }
    
     }
 
   
    return true;
}

8.select优缺点:

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

缺点:

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

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

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

#include "stdafx.h"
#include 
#include 
using namespace std;
 
#include 
 
#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<          //如果在集合中,向下进行相应的IO操作
                nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0);//看是否能正常接收到数据
 
                if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
                {
                    cout<<"Client "<              //i--是因为要重新判断新的i的位置的socket是否失效
                    }
                }
                else
                {
                    cout<

服务器的主要步骤:

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

2.创建工作者线程

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

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

 

工作线程

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

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

2.调用select函数

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

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

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

你可能感兴趣的:(网络编程)