《ASCE1885的网络编程》---Winsock APIの套接口I/O处理函数
在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命令有FIONBIO、FIONREAD和SIOCATMARK:
1)FIONBIO:该命令用于在套接口s上允许或禁止非阻塞模式。一个套接口在创建好以后,默认的通信模式是阻塞模式,如果要在程序中对一个创建好的套接口通行模式进行设置,那么ioctlsocket()函数的第二个参数使用命令FIONBIO,第三个参数argp设为0时,表示禁止非阻塞模式;设为非0值,表示允许非阻塞模式。
要注意:如果使用WSAAsyncSelect()函数或WSAEventSelect()函数,则套接口将会自动设置为非阻塞模式。如果已对一个套接口进行了WSAAsyncSelect()操作,则任何用ioctlsocket()调用来套接口重新设置成阻塞模式的操作都将失败,并返回WSAEINVAL错误。为了把套接口重新设置成阻塞模式,应用程序必须把IEvent参数置为0,调用WSAAsyncSelect()函数,以禁止WSAAsyncSelect()调用;或者令INetworkEvents参数等于0,调用WSAEventSelect()函数,从而禁止WSAEventSelect()。
2)FIONREAD:该命令用于确定可从套接口s上自动读入的数据量。参数argp指向一个无符号长整型量,其中存有打算读入的字节数。如果使用WSAIoctl()函数,那么无符号整数是通过lpvOutBuffer返回的。如果套接口s面向数据流(SOCK_STREAM类型)的套接口,则FIONREAD返回一次recv()调用中所接收的数据总量,这通常与套接口中排队的数据总量相同;如果套接口s是面向数据报(SOCK_DGRAM类型)的套接口,则FIONREAD返回套接口上排队的第一个数据报的大小。
3)SIOCATMARK:该命令用于确定是否所有的带外数据都已被读入。这个命令仅适用于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_sec和tv_usec字段都表示等待时间,只是单位不同。它们的设置可分为如下三种情况:
1)如果在调用select()函数时将等待时间tv_sec和tv_usec都设置为0,则select()调用在检查完套接口描述符后立即返回,这可用于探询所选套接口的状态。如果处于这种状态,则select()调用可认为是非阻塞的。
2)如果在调用select()函数时将timeout指向NULL,则进行阻塞等待,即被监视的描述符中只有当其中的任何一个准备好读写操作时,select()调用才返回。
3)如果等待时间tv_sec和tv_usec不全为0,则当等待时间没有超时时,select()函数在被检查的描述符中有任何一个套接口准备好读写时返回。
select()函数可用于检查一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select返回满足条件的套接口数目。
为了方便对readfds、writefds和exceptfds集合进行操作,Winsock实现中已经定义好了如下4个宏,以简化程序的设计:
1)FD_SET(s, *set):向set集合添加套接口描述符s;
2)FD_CLR(s, *set):从set集合中删除套接口描述符s;
3)FD_ISSET(s, *set):检查s是否为set集合的一员,如果是则返回TRUE;
4)FD_ZERO(*set):将set集合初始化为空集;
这样使用select函数对一个或多个套接口进行检查的过程如下:
1)使用FD_ZERO宏,初始化要检查的每一个集合;
2)使用FS_SET宏,将要检查的套接口加入到一个集合中;
3)设置等待时间,即对timeval中的tv_sec和tv_usec字段进行设置;
4)调用select函数;
5)当select函数正确返回时,使用FD_ISSET检查一个选定的套接口是否在指定的集合中。
具体在程序中使用select函数时,还要注意以下几个问题:
1)readfds参数中包括的套接口标识符<有可读入数据的套接口>,<正处于监听listen()状态且有连接请求到达的套接口>,<已经关闭、重设或终止的套接口>。
2)writefds参数中包括的套接口标识符有<可写的套接口>。如果一个套接口正在connect()连接(非阻塞),则可写性意味这连接顺利建立。
3)exceptfds参数中包括的套接口标识符有<带外数据可读>。假如已完成对一个非阻塞连接调用的处理,连接尝试就会失败。注意:如果设置了SO_OOBINLINE选项为FALSE,则只能用这种方法来检查带外数据的存在与否。 对于SO_STREAM类型的套接口,远端造成的连接中止和KEEPALIVE错误都将被作为例外出错。 如果套接口正在进行连接connect()(非阻塞方式),则连接试图的失败将会表现在exceptfds中。
4)如果不想对readfds、writefds和exceptfds进行监视,则可将其值为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_READ、FD_WRITE、FD_OOB和FD_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_READ和FD_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参数的低位字指明了发生的网络事件,高位字则含有一个错误代码。错误代码和事件可以通过WSAGETSELECTERROR和WSAGETSELECTEVENT宏从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),这有两种原因:一是原来的操作已经完成,并且应用程序已经处理了结果消息;二是原来的错误已经完成,但结果消息仍在应用程序窗口队列中等待。