《ASCE1885的网络编程》---Winsock APIの套接口I/O处理函数

来源:互联网 发布:合肥行知学校在哪 编辑:程序博客网 时间:2024/04/24 20:39

Windows环境下,套接口的通信方式分为两种:阻塞方式和非阻塞方式。阻塞方式下工作的套接口在进行I/O操作时,函数要等待到相关操作完成以后才能返回;非阻塞方式下工作的套接口在进行I/O操作时,无论操作是否成功,调用都会立即返回。

阻塞方式和非阻塞方式各有优缺点,阻塞方式的套接口编程简单,易于实现。因此,一个套接口默认操作模式被设置为阻塞方式。如果要使套接口工作在非阻塞方式下,就要使用ioctlsocket()函数进行设置。阻塞方式的套接口在下面几种情况下显得难于管理:

1)当有多个已建立连接的套接口需要进行管理的;

2)当发送的数据量不均匀或接收的数据量不均匀时;

3)当发送或接收的数据时间不确定时。

在进行程序设计时,应该尽量使用非阻塞方式的操作,但非阻塞方式的套接口较为复杂,并且由于操作常常失败,因此在程序中就要考虑操作失败时应该如何处理了。、

 

1)设置套接口的工作方式---ioctlsocket()WSAIoctl()

int ioctlsocket(

  __in     SOCKET s, //套接字描述字

  __in     long cmd, //预定义好的标志,表示对套接口s的操作控制命令

  __inout  u_long *argp //指向cmd命令所待参数的指针

);

 

Winsock2中引入一个新的功能更强大的函数是WSAIoctl()

int WSAIoctl(

  __in   SOCKET s, //套接字描述字

  __in   DWORD dwIoControlCode, //指示将要进行的操作的控制代码

  __in   LPVOID lpvInBuffer, //指向函数的输入参数(用于描述函数输入参数缓冲区的地址)

  __in   DWORD cbInBuffer, //用于描述输入缓冲区的大小

  __out  LPVOID lpvOutBuffer, //指向函数的返回参数(用于描述函数返回数据缓冲区的地址)

  __in   DWORD cbOutBuffer, //用于描述返回参数缓冲区的大小

  __out  LPDWORD lpcbBytesReturned, //指向函数实际返回的字节数的地址

  __in   LPWSAOVERLAPPED lpOverlapped, //WSAOVERLAPPED结构的地址

  __in   LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //一个指向操作结束后

                                     //调用的例程指针,该参数和lpOverlapped使用在重叠I/O

);

 

Ioctlsocket()WSAIoctl()函数支持的标准I/O命令有FIONBIOFIONREADSIOCATMARK

1FIONBIO该命令用于在套接口s上允许或禁止非阻塞模式。一个套接口在创建好以后,默认的通信模式是阻塞模式,如果要在程序中对一个创建好的套接口通行模式进行设置,那么ioctlsocket()函数的第二个参数使用命令FIONBIO,第三个参数argp设为0时,表示禁止非阻塞模式;设为非0值,表示允许非阻塞模式。

要注意:如果使用WSAAsyncSelect()函数或WSAEventSelect()函数,则套接口将会自动设置为非阻塞模式。如果已对一个套接口进行了WSAAsyncSelect()操作,则任何用ioctlsocket()调用来套接口重新设置成阻塞模式的操作都将失败,并返回WSAEINVAL错误。为了把套接口重新设置成阻塞模式,应用程序必须把IEvent参数置为0,调用WSAAsyncSelect()函数,以禁止WSAAsyncSelect()调用;或者令INetworkEvents参数等于0,调用WSAEventSelect()函数,从而禁止WSAEventSelect()

 

2FIONREAD该命令用于确定可从套接口s上自动读入的数据量。参数argp指向一个无符号长整型量,其中存有打算读入的字节数。如果使用WSAIoctl()函数,那么无符号整数是通过lpvOutBuffer返回的。如果套接口s面向数据流(SOCK_STREAM类型)的套接口,则FIONREAD返回一次recv()调用中所接收的数据总量,这通常与套接口中排队的数据总量相同;如果套接口s是面向数据报(SOCK_DGRAM类型)的套接口,则FIONREAD返回套接口上排队的第一个数据报的大小。

 

3SIOCATMARK该命令用于确定是否所有的带外数据都已被读入。这个命令仅适用于SOCK_STREAM类型的套接口,且该套接口已被设置为可以在线接收带外数据(SO_OOBINLINE)。如无带外数据等待读入,则该操作返回TRUE,否则返回FALSE,下一个recv()revcfrom()操作将检索“标记”前的一些或所有数据。应用程序可用SIOCATMARK操作来确定是否有数据剩余。如果在“紧急”带外数据前有常规数据,则按序接收这些数据(注意:recv()recvfrom()操作不会在一次调用中混淆常规数据和带外数据)。

对于ioctlsocket()函数来说,argp指向一个BOOL值,在其中存入返回值;而对于WSAIoctl()函数来说,会在lpvOutBuffer中返回指向布尔变量的指针。

 

Ioctlsocket函数使用实例如下:

//-------------------------

// Initialize Winsock

WSAData wsaData;

int iResult = WSAStartup(NAKEWORD(2,2), &wsaData);

if(iResult != NO_ERROR)

    printf("Error at WSAStartup/n");

 

//-------------------------

// Create a SOCKET object.

SOCKET m_socket;

m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

if(m_socket == INVALID_SOCKET)

{

    printf("Error at socket():%d/n", WSAGetLastError());

    WSACleanup();

    return;           

}

 

//-------------------------

// Set the socket I/O mode: In this case FIONBIO

// enables or disables the blocking mode for the

// socket based on the numerical value of iMode.

// If iMode = 0, blocking is enabled;

// If iMode != 0, non-blocking mode is enabled.

u_long iMode = 0;

ioctlsocket(m_socket, FIONBIO, &iMode);

 

2)套接口I/O状态查询---select()

使用select()函数的好处是在进行I/O操作之前,可以首先判断能否向一个套接口写入数据,或者套接口上是否存在可读的数据。这样就可以防止应用程序在套接口处于阻塞模式时,对它进行的I/O操作被迫进入等待状态;同时也可以防止在套接口处于非阻塞模式时,产生WSAEWOULDBLOCK错误。

select函数原型如下:

int select(

  __in     int nfds, //本参数被忽略,仅起到与Berkeley API套接口兼容的作用

  __inout  fd_set *readfds, //检查可读性

  __inout  fd_set *writefds, //检查可写性

  __inout  fd_set *exceptfds, //检查例外数据

  __in     const struct timeval *timeout //本次select()调用最长等待时间

);

 

fd_set是一个结构类型说明符,代表这一系列特定套接口的集合:

typedef struct fd_set {

  u_int  fd_count; //套接口的数目

  SOCKET fd_array[FD_SETSIZE]; //表示数组中存放的套接口号,FD_SETSIZE是常量,定义为64

} fd_set;

 

timeval是一个结构类型,定义如下:

typedef struct timeval {

  long tv_sec; //等待的秒数

  long tv_usec; //等待的毫秒数

} timeval;

tv_sectv_usec字段都表示等待时间,只是单位不同。它们的设置可分为如下三种情况:

1)如果在调用select()函数时将等待时间tv_sectv_usec都设置为0,则select()调用在检查完套接口描述符后立即返回,这可用于探询所选套接口的状态。如果处于这种状态,则select()调用可认为是非阻塞的。

2)如果在调用select()函数时将timeout指向NULL,则进行阻塞等待,即被监视的描述符中只有当其中的任何一个准备好读写操作时,select()调用才返回。

3)如果等待时间tv_sectv_usec不全为0,则当等待时间没有超时时,select()函数在被检查的描述符中有任何一个套接口准备好读写时返回。

 

select()函数可用于检查一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select返回满足条件的套接口数目。

为了方便对readfdswritefdsexceptfds集合进行操作,Winsock实现中已经定义好了如下4个宏,以简化程序的设计:

1FD_SET(s, *set):向set集合添加套接口描述符s

2FD_CLR(s, *set):从set集合中删除套接口描述符s

3FD_ISSET(s, *set):检查s是否为set集合的一员,如果是则返回TRUE

4FD_ZERO(*set):将set集合初始化为空集;

 

这样使用select函数对一个或多个套接口进行检查的过程如下:

1)使用FD_ZERO宏,初始化要检查的每一个集合;

2)使用FS_SET宏,将要检查的套接口加入到一个集合中;

3)设置等待时间,即对timeval中的tv_sectv_usec字段进行设置;

4)调用select函数;

5)当select函数正确返回时,使用FD_ISSET检查一个选定的套接口是否在指定的集合中。

 

具体在程序中使用select函数时,还要注意以下几个问题:

1readfds参数中包括的套接口标识符<有可读入数据的套接口><正处于监听listen()状态且有连接请求到达的套接口><已经关闭、重设或终止的套接口>

2writefds参数中包括的套接口标识符有<可写的套接口>。如果一个套接口正在connect()连接(非阻塞),则可写性意味这连接顺利建立。

3exceptfds参数中包括的套接口标识符有<带外数据可读>。假如已完成对一个非阻塞连接调用的处理,连接尝试就会失败。注意:如果设置了SO_OOBINLINE选项为FALSE,则只能用这种方法来检查带外数据的存在与否。   对于SO_STREAM类型的套接口,远端造成的连接中止和KEEPALIVE错误都将被作为例外出错。   如果套接口正在进行连接connect()(非阻塞方式),则连接试图的失败将会表现在exceptfds中。

4)如果不想对readfdswritefdsexceptfds进行监视,则可将其值为NULL,但三组参数不能同时全为NULL

 

select()函数使用实例,从网络上接收数据:

#include <winsock2.h>

#include <stdio.h>

#include <windows.h>

 

#pragma comment(lib, "ws2_32.lib")

 

#define PORT 5150

#define MSGSIZE 1024

 

int g_iTotalConn = 0;

SOCKET g_socketArr[FD_SETSIZE];

 

DWORD WINAPI WorkerThread(LPVOID lpParam);

 

int main()

{

    WSADATA wsaData;

    SOCKET listenSock, clientSock;

    sockaddr_in localAddr, clientAddr;

   

    int iaddrsize = sizeof(sockaddr_in);

    DWORD dwThreadId;

   

    //Initialize Windows socket library

    WSAStartup(MAKEWORD(2,2), &wsaData);

    //Create listening socket

    listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

   

    localAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    localAddr.sin_family = AF_INET;

    localAddr.sin_port = htons(PORT);

   

    bind(listenSock, (struct sockaddr*)&localAddr, sizeof(sockaddr_in));

   

    //Create Worker Thread

    CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);

    while(TRUE)

    {

        clientSock = accept(listenSock, (struct sockaddr*)&clientAddr, &iaddrsize);

        printf("Accepted client:%s:%d/n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

        //Add socket to g_socketArr

        g_socketArr[g_iTotalConn++] = clientSock;

    }

   

    system("pause");

    return 0;   

}

 

//Thread function

DWORD WINAPI WorkerThread(LPVOID lpParam)

{

         int i;

    fd_set fdread;

    int ret;

    struct timeval tv = {1, 0};

    char szMessage[MSGSIZE];

   

    while(TRUE)

    {

        FD_ZERO(&fdread);

        for(i=0; i<g_iTotalConn; i++)

        {

            FD_SET(g_socketArr[i], &fdread);       

        }   //we only care read event

       

        ret = select(0, &fdread, NULL, NULL, &tv);

        if(ret == 0)

        {

            //Time expired

            continue;      

        }

        for(i=0; i<g_iTotalConn; i++)

        {

            if(FD_ISSET(g_socketArr[i], &fdread))

            {

                //A read event happened on g_socketArr

                ret = recv(g_socketArr[i], szMessage, MSGSIZE, 0);

                if(ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))

                {

                    //Client socket closed

                    printf("Client socket %d closed/n", g_socketArr[i]);

                    closesocket(g_socketArr[i]);      

                    if(i < g_iTotalConn - 1)

                    {

                        g_socketArr[i--] = g_socketArr[--g_iTotalConn];    

                    }

                }                        

                else

                {

                    //We received a message from client

                    szMessage[ret] = '/0';

                    send(g_socketArr[i], szMessage, strlen(szMessage), 0);   

                }

            }       

        }

    }

    return 0;

}

 

3)异步事件通知---WSAAsyncSelect()

WSAAsyncSelect()Winsock提供的一个适合于Windows编程使用的函数,它允许在一个套接口上当发生特定的网络事件时,给Windows网络应用程序(窗口或对话框)发送一个消息(事件通知):

int WSAAsyncSelect(

  __in  SOCKET s, //标识一个需要事件通知的套接口描述符

  __in  HWND hWnd, //标识一个在网络事件发生时要想收到消息的窗口或对话框的句柄

  __in  unsigned int wMsg, //在网络事件发生时要接收的消息,该消息会投递到由hWnd句柄指定

                            //的窗口或对话框

  __in  long lEvent //位屏蔽码,用于指明应用程序感兴趣的网络事件集合

);

WSAAsyncSelect函数常用到的网络事件:

 

若应用程序感兴趣的网络事件声明成功,则返回0;如果声明失败,则返回SOCKET_ERROR错误信息。可进一步调用WASGetLastError()函数返回如下的特定错误代码:

WSANOTINITIALISED  //在使用本API之前必须进行一次成功的WSAStartup()调用

WSAENETDOWN //Windows Sockets实现已检测到网络子系统故障

WSAENIVAL   //指定的参数之一是非法的

WSAEINPROGRESS       //一个阻塞的Windows Sockets操作正在进行

 

附加的错误代码可能在应用程序窗口接收到消息时被设置,这些代码可以用WSAGETSELECTERROR宏从lParam中取出,对应于每个网络事件的可能错误代码说明如下:

1)网络事件FD_CONNECT可能的错误代码:

WSAEADDRINUSE         //给定的地址已被使用

WSAEADDRNOTAVAIL        //指定的地址在本地机器不能使用

WSAEAFNOSUPPORT //指定族的地址不能和本套接口同时使用

WSAECONNREFUSED //连接的尝试被拒绝

WSAEDESTADDRREQ  //需要一个目的地址

WSAEFAULT  //namelen参数不正确

WSAEINVAL   //套接口已经约束到一个地址

WSAEISCONN        //套接口已经连接

WSAEMFILE   //没有可用的文件描述符

WSAENETUNREACH    //此时网络不能从该主机访问

WSAENOBUFS       //无可用的缓冲区空间,套接口不能连接

WSAENOTCONN //套接口没有连接

WSAENOTSOCK   //该描述符是文件,不是套接口

WSAETIMEOUT    //试图连接超时,未建立连接

 

2)网络事件FD_CLOSE可能的错误代码是:

WSAENETDOWN //Windows Sockets实现已检测到网络子系统故障

WSAECONNRESET        //连接由远端重建

WSAECONNABORTED         //由于超时或其他失败放弃连接

 

3)网络事件FD_READFD_WRITEFD_OOBFD_ACCEPT可能的错误代码为:

WSAENETDOWN //Windows Sockets实现已检测到网络子系统故障

 

WSAAsyncSelect函数用来请求Windows Sockets DLL为窗口句柄发送一条由IEvent参数指明的网络事件。要发送的消息由wMsg参数表明,在使用时要注意以下问题:

1)若应用程序对一个套接口s调用了WSAAsyncSelect()函数,那么套接口s的模式会自动从阻塞模式变成非阻塞模式。这样一来,假如在程序中调用了像WSARecv()这样的I/O函数,当没有数据可用时,必然会造成调用失败,并返回WSAEWOULDBLOCK错误信息;

2)如果应用程序同时对多个网络事件感兴趣,那么只需对各种类型的网络事件执行按位或的运算即可。例如,当一个网络应用程序对套接口s上的连接、发送以及套接口关闭这三个网络事件感星期时,可以用如下的格式调用WSAAsyncSelect函数:

rc = WSAAsyncSelect(s, hWnd, wMsg, FD_CONNECT | FD_WRITE | FD_CLOSE);

3)特别要注意的是,进行一次WSAAsyncSelect()调用,将使为同一个套接口启动的所有以前的WSAAsyncSelect()调用作废。例如,要接收读写通知,应用程序必须同时用FD_READFD_WRITE调用WSAAsyncSelect()

rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ | FD_WRITE);

而不能使用如下的调用方式,因为第二次调用将使第一次调用的作用失效:

rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ);

rc = WSAAsyncSelect(s, hWnd, wMsg, FD_WRITE);

4)如果要取消所有的通知,也就是指出Windows Sockets的实现不再在套接口上发送任何和网络事件相关的消息,则把IEvent字段置为0,然后调用WSAAsyncSelect()即可:

rc = WSAAsyncSelect(s, hWnd, 0 , 0);

尽管在本例中,WSAAsyncSelect()调用立即使传给该套接口的事件消息无效,但仍可能有消息等待在应用程序的消息队列中,应用程序因此也必须仍准备好接收网络消息(即使消息作废)。用closesocket()关闭一个套接口也同样使WSAAsyncSelect()发送的消息作废,但在调用closesocket()之前,队列中的消息仍然起作用。

5)当某一套接口s上发生了一个已命名的网络事件时,应用程序窗口hWnd会接收到消息wMsg。应用程序窗口例程的wParam参数标识了网络事件发生的套接口,lParam参数的低位字指明了发生的网络事件,高位字则含有一个错误代码。错误代码和事件可以通过WSAGETSELECTERRORWSAGETSELECTEVENT宏从lParam中取出,宏定义如下:

#include <windows.h>

#define WSAGETSELECTEVENT(lParam)       LOWORD(lParam)

#define WSAGETSELECTERROR(lParam)       HIWORD(lParam)

若应用程序发现套接口上没有产生任何错误,接着便会检查lParam的低位字,以弄清到底是哪个网络事件类型造成了这条Windows消息的触发。

 

4)取消正在执行的阻塞调用---WSACancelBlockingCall()

如果应用程序中想取消正在执行的阻塞调用,就要使用WSACancelBlockingCall()函数。要注意的是,在Winsock2的实现规范中已经不包括该函数了:

int WSACancelBlockingCall(void);

 

5)判断是否有阻塞调用---WSAIsBlocking()

该函数用于判断是否有阻塞调用正在进行:

BOOL WSAIsBlocking(void);

如果存在一个尚未完成的阻塞函数在等待完成,则函数返回TRUE,否则返回FALSE

Winsock2的实现规范中已经不包括该函数了,在Winsock1.1中使用该函数时要注意它禁止对每一个线程多于一个未完成的调用。

 

6)取消一个未完成的异步操作---WSACancelAsyncRequest()

int WSACancelAsyncRequest(

  __in  HANDLE hAsyncTaskHandle //指明将要被取消的异步操作

);

如果该操作成功地取消了异步操作,则函数返回0,否则返回SOCKET_ERROR错误信息。可通过WSAGetLastError()调用获得对错误的进一步描述,错误代码如下:

WSANOTINITIALISED  //在使用本API前必须进行一次成功的WSAStartup()调用

WSAENETDOWN //Windows Sockets实现已检测到网络子系统故障

WSAEINVAL   //指示异步操作句柄非法

WSAEINPROGRESS       //一个阻塞的Windows Sockets操作正在进行

WSAEALREADY      //被取消的异步操作已经完成

 

WSACancelAsyncRequest()函数用于取消一次异步操作,该异步操作应该是以一个WSAAsyncGetXByY()函数的形式(例如WSAAsyncGetHostByName())启动的。hAsyncTaskHandle参数标识了要取消的操作,它应由初始函数作为异步任务句柄返回。

试图取消一个已存在的异步操作WSAAsyncGetXByY()可能失败(错误代码是WSAEALREADY),这有两种原因:一是原来的操作已经完成,并且应用程序已经处理了结果消息;二是原来的错误已经完成,但结果消息仍在应用程序窗口队列中等待。