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 <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會陷入死循環。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章