您的位置:首页 > 理论基础 > 计算机网络

Windows网络编程笔记6 --- WinSock I/O 控制方法

2013-11-27 15:21 281 查看
  Windows提供了两种方式“套接字模式”和“套接字I/O模型”,可对一个套接字上的I/O行为加以控制。套接字模式用于决定在随一个套接字调用时,那些 Winsock函数的行为。其中的模型包括括select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、OverlappedI/O(重叠式I/O)以及Completionport(完成端口)等等。

  所有Windows平台都支持套接字以锁定非锁定方式工作。在锁定模式下,在I/O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非锁定模式下,Winsock函数无论如何都会立即返回。

  锁定模式使用线程同步机制完成数据的I/O,扩展性不好,尤其是有多个套接字时。

  非锁定模式的使用需要函数 ioctlsocket(),使用如下:

SOCKET s;
unsigned long ub =1;
int nRet;
s = socketAF_INET,SOCK_STREAM,0);
nRet = ioctlsocket(s,FIOBIO,(unsigned long *)&ub);//设置非锁定模式
if(nRet == SOCKET_ERROR)
{
//进入非锁定模式失败
}


套接字I/O模型

1、select模型

//函数原型
int select(
int nfds,    //与早期程序兼容,可忽略
fd_set FAR * readfds,// 可读性
fd_set FAR * writefds,// 可写性
fd_set FAR * exceptfds,// 例外数据
const struct timeval FAR * timeout//超时时间
);


参数readfds 表示以下几种情况

有数据可读入、连接已经关闭(重设或者终止)、如果已经listen,并且正在建立连接,那么accept函数会返回成功。

参数writefds表示以下几种情况

有数据可发出、如果已完成对一个非锁定连接调用的处理,连接就会成功。

参数exceptfds表示如下

如果已完成对一个非锁定连接调用的处理,连接尝试就会失败。有带外(Out-of-band,OOB)数据可供读取。

其中fd_set结构如下:

typedef struct fd_set {
u_int fd_count;               /* how many are SET? */
SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;


超时时间

struct timeval {
long    tv_sec;         /* seconds */秒
long    tv_usec;        /* and microseconds */毫秒
};


  用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部读、写以及例外fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。

下面是对fd_set进行操作的一些宏

  FD_CLR(s,*set):从set中删除套接字s。

  FD_ISSET(s,*set):检查s是否set集合的一名成员;如答案是肯定的是,则返回TRUE。

  FD_SET(s,*set):将套接字s加入集合set。

  FD_ZERO(*set):将set初始化成空集合。

用select操作一个或多个套接字句柄的全过程:

  1)使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。

  2)使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。

  3)调用select函数,然后等待在指定的fd_set集合中,I/O活动设置好一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。

  4)根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决)的I/O操作—具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。

  5)知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤1),继续进行select处理。

简单过程如下

   SOCKET s;
fd_set fdread;
int ret;
//创建
//bind()
//accept()
//开始
while (1)
{
FD_ZERO(&fdread);//初始化
FD_SET(s,&fdread);//添加
if ((ret = select(0,&fdread,NULL,NULL,NULL)) == SOCKET_ERROR)
{
//添加失败
}
if (ret > 0)
{
if (FD_ISSET(s,&fdread))
{
//已经是集合的一部分,正在读取数据
}
}
//其他操作
}


2、WSAAsyncSelect模型

WSAAsyncSelect 的使用必须要在窗口应用程序中使用,在回调函数中实现处理。

int WSAAsyncSelect(
_In_  SOCKET s,//关心的socket
_In_  HWND hWnd,//窗口句柄
_In_  unsigned int wMsg,//消息
_In_  long lEvent//事件类型
);


事件类型很多,如图

//WSAEnumNetworkEvent 使用举例
#include <windows.h>
#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int main()
{
//-------------------------
WSADATA wsaData;
int iResult;

SOCKET SocketArray[WSA_MAXIMUM_WAIT_EVENTS], ListenSocket;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSANETWORKEVENTS NetworkEvents;
sockaddr_in InetAddr;
DWORD EventTotal = 0;
DWORD Index;
DWORD i;

HANDLE NewEvent = NULL;

// 初始化
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
wprintf(L"WSAStartup failed with error: %d\n", iResult);
return 1;
}

//创建
ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET)
{
wprintf(L"socket function failed with error: %d\n", WSAGetLastError() );
return 1;
}

InetAddr.sin_family = AF_INET;
InetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InetAddr.sin_port = htons(7890);

//绑定
iResult = bind(ListenSocket, (SOCKADDR *) & InetAddr, sizeof (InetAddr));
if (iResult != 0)
{
wprintf(L"bind failed with error: %d\n", WSAGetLastError() );
return 1;
}

// 创建事件
NewEvent = WSACreateEvent();
if (NewEvent == NULL)
{
wprintf(L"WSACreateEvent failed with error: %d\n", GetLastError() );
return 1;
}
// 将事件类型绑定到套接字上
iResult = WSAEventSelect(ListenSocket, NewEvent, FD_ACCEPT | FD_CLOSE);
if (iResult != 0)
{
wprintf(L"WSAEventSelect failed with error: %d\n", WSAGetLastError() );
return 1;
}

// 监听
iResult = listen(ListenSocket, 10);
if (iResult != 0)
{
wprintf(L"listen failed with error: %d\n", WSAGetLastError() );
return 1;
}
// 添加事件
SocketArray[EventTotal] = ListenSocket;
EventArray[EventTotal] = NewEvent;
EventTotal++;

// 等待所有套接字上的事件发生
Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE);
Index = Index - WSA_WAIT_EVENT_0;

//枚举事件类型,直到失败
for (i = Index; i < EventTotal; i++)
{
Index = WSAWaitForMultipleEvents(1, &EventArray[i], TRUE, 1000, FALSE);
if ((Index != WSA_WAIT_FAILED) && (Index != WSA_WAIT_TIMEOUT))
{
WSAEnumNetworkEvents(SocketArray[i], EventArray[i], &NetworkEvents);
}
}

//...
return 0;
}


View Code

4、重叠I/O模式

  重叠I./O模式只支持Winsock2 ,可以使用WSARecv和WSASend实现

  关键字 WSA_FLAG_OVERLAPPED

  函数WSASocket在创建时可以手动指定模式

SOCKET s=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);

而函数socket默认就是重叠I/O模式。

typedef struct _WSAOVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
};
PVOID  Pointer;
};
HANDLE    hEvent;//
} WSAOVERLAPPED, *LPWSAOVERLAPPED;


  前几个参数有系统维护,只有hEvent字段有点儿特殊,它允许应用程序将一个事件对象句柄同一个套接字关联起来。

//结构Overlapped
BOOL WSAAPI WSAGetOverlappedResult(
_In_   SOCKET s,
_In_   LPWSAOVERLAPPED lpOverlapped,
_Out_  LPDWORD lpcbTransfer,//一次收发的实际字节数
_In_   BOOL fWait,//在重叠I/O模式中无效
_Out_  LPDWORD lpdwFlags//接收结果标志
);


对重叠I/O操作进行管理过程

  1)创建一个套接字,开始在指定的端口上监听连接请求。

  2)接受一个进入的连接请求。

  3)为接受的套接字新建一个WSAOVERLAPPED结构,并为该结构分配一个事件对象句柄。也将事件对象句柄分配给一个事件数组,以便稍后由WSAWaitForMultipleEvents函数使用。

  4)在套接字上投递一个异步WSARecv请求,指定参数为WSAOVERLAPPED结构。注意函数通常会以失败告终,返回SOCKET_ERROR错误状态WSA_IO_PENDING(I/O操作尚未完成)。

  5)使用步骤3)的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入“已传信”状态(换言之,等待那个事件的“触发”)。

  6)WSAWaitForMultipleEvents函数完成后,针对事件数组,调用WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理。

  7)使用WSAGetOverlappedResult函数,判断重叠调用的返回状态是什么。

  8)在套接字上投递另一个重叠WSARecv请求。

  9)重复步骤5-8)。

5、完成端口模型

适合在管理成千上万的套接字时使用。

使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求。要做到这一点,需要调用CreateCompletionPort函数。函数有两种功能:

  1. 用于创建一个完成端口对象。

  2. 将一个句柄同完成端口关联到一起。

//创建一个完成端口对象
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,  //关联的文件句柄,可能为套接字句柄
HANDLE ExistingCompletionPort,//已经存在的完成端口
LONG_PTR CompletionKey,// 传送给处理函数的参数
DWORD NumberOfConcurrentThreads//有多少个线程在访问这个消息队列
);


参数ExistingCompletionPort表示已经存在的完成端口。如果为NULL,则为新建一个IOCP(I/O完成端口)。

创建过程

HANDLE completionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

//获取排队完成的状态
BOOL  GetQueuedCompletionStatus(
__in  HANDLE CompletionPort,//完成端口
__out LPDWORD lpNumberOfBytesTransferred,//实际接受的数据
__out PULONG_PTR lpCompletionKey,//“单句柄数据”
__out LPOVERLAPPED *lpOverlapped,//重叠I/O结构
__in  DWORD dwMilliseconds//超时时间
);


一旦所有套接字句柄都已关闭,便需在完成端口上,终止所有工作者线程的运行,此时调用函数PostQueuedCompletionStatus向所有线程发送终止命令。

BOOL PostQueuedCompletionStatus(
__in     HANDLE CompletionPort,//完成端口句柄
__in     DWORD dwNumberOfBytesTransferred,//实际接受数据
__in     ULONG_PTR dwCompletionKey,//完成端口关键字“单句柄数据”
__in_opt LPOVERLAPPED lpOverlapped//重叠I/O结构
);


使用过程

1、创建一个完成端口,第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程

2、判断系统内到底安装了几个处理器

3、创建工作者线程,根据步骤2得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务

4、准备好一个监听套接字

5、使用accept函数,接受进入的请求

6、创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。

7、调用CreateCompletionPort,将之accept返回的新套接字同完成端口关联到一起

8、开始在已接受的连接上进行I/O操作

9、重复5到8,直到服务器终止。

这些操作对套接字I/O至关重要,需要理解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: