谈谈Windows网络编程

进程的使用

在Windows下,CreateProcess函数用于创建一个新进程,该进程独立于创建进程运行,其声明如下:

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

线程的使用

#include <windows.h>
HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES 1pThreadAttributes,
	SIZE_T dwStacksize,
	LPTHREAD_START_ROUTINE 1pStartAddress,
	LPVOID lpParameter,
	DWORD dwCreationFlags,
	LPDWORD lpThreadId
);
//成功时返回线程句柄,失败时返回 NULL。
  • lpThreadAttribute:线程安全相关信息,使用默认设置时传递NULL。
  • dwStackSize:要分配给线程的栈大小,传递0时生成默认大小的栈。
  • lpStartAddress:传递线程的main函数信息。
  • lpParameter:调用main函数时传递的参数信息。
  • dwCreationFlags:用于指定线程创建后的行为,传递0时,线程创建后立即进入可执行状态。
  • lpThreadld:用于保存线程ID的变量地址值。

上述定义看起来有些复杂,其实只需要考虑lpStartAddress和lpParameter这2个参数,剩下的只需传递0或NULL即可。

windows线程的销毁时间点
Windows 线程在首次调用的线程main函数返回时销毁(销毁时间点和销毁方法与Linux不同)。还有其他方法可以终止线程,但最好的方法就是让线程main函数终止(返回),故省略其他说明。

创建"使用线程安全标准C函数"的线程

如果线程要调用C/C++标准函数,需要通过如下方法创建线程。因为通过CreateThread函数调用创建出的线程在使用C/C++标准函数时并不稳定。

#include <process.h>
uintptr_t _beginthreadex(
	void * security,
	unsigned stack_size,
	unsigned (*start_address)(void*),
	void * arglist,
	unsigned initflag,
	unsigned * thrdaddr);
};
//成功时返回线程句柄,失败时返回0。

上述函数与之前的CreateThread函数相比,参数个数及各参数的含义和顺序均相同,只是变
量名和参数类型有所不同。因此,用上述函数替换CreateThread函数时,只需适当更改数据类型。下面通过上述函数编写示例。

_beginthreadex函数示例
#include <stdio.h>
#include <windows.h>
#include <process.h>
unsigned WINAPI ThreadFunc(void *arg);

int main(int arg,char *argv[])
{
    HANDLE hThread;
    unsigned threadID;
    int param = 5;
    hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
    if (hThread == 0)
    {
        puts("_beginthreadex() error");
        return -1;
    }
    Sleep(3000);
    puts("end of main");
    return 0;
}

unsigned __stdcall ThreadFunc(void* arg)
{
    int i;
    int cnt = *((int*)arg);
    for (i = 0; i < cnt; i++)
    {
        Sleep(1000);
        puts("running thread");
    }
    return 0;
}

输出:

在这里插入图片描述

内核对象的2种状态

线程内核对象包括终止状态(“signaled状态”)和未终止状态(“non-signed状态”),我们可以通过定义的API查看内核对象的当前状态。

WaitForSingleObject & WaitForMultipleObjects

WaitForSingleObject:查看单个内核对象验证signaled状态。

#include <windows.h>

DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
//成功时返回实践信息,失败时返回WAIT_FAILED。
  • hHandle:查看状态的内核对象句柄。
  • dwMillseconds:以1/1000秒为单位指定超时,传递INFINITE时函数不会返回,直到内核对象变成signaled状态。
  • 返回值:进入signaled状态返回WAIT_OBJECT_O,超时返回WAIT_TIMEOUT。
    该函数由于发生事件(变为signaled状态)返回时,有时会把相应内核对象再次改为non-signaled状态。这种可以再次进人non -signaled状态的内核对象称为“auto-reset模式” 的内核对象,而不会自动跳转到non-signaled状态的内核对象称为“manual-reset模式” 的内核对象。

WaitForMultipleObjects :可以验证多个内核对象状态。

#include <windows.h>
DWORD WaitForMultiple0bjects(
DWORD nCount, const HANDLE * lpHandles,BOOL bWaitAll, DWORD dwMilliseconds);
//成功时返回事件信息,失败时返回WAIT_ FAILED。
  • nCount:需验证的内核对象数。
  • IpHandles:存有内核对象句柄的数组地址值。
  • bWaitAIlI:如果为TRUE,则所有内核对象全部变为signaled时返回;如果为FALSE,则只要有1个验证对象的状态变为signaled就会返回。
  • dwMilliseconds:以1/1000秒为单位指定超时,传递INFINITE时函数不会返回,直到内核对象变为
    signaled状态。

线程同步

用户模式下的CRITICAL_SECTION同步

基于CRITICAL_ SECTION的同步中将创建并运用“CRITICAL_ SECTION对象”,但这并非内核对象。与其他同步对象相同,它是进人临界区的一把“钥匙”(Key)。因此,为了进人临界区,需要得到CRITICAL SECTION对象这把“钥匙”。相反,离开时应上交CRITICAL_ SECTION对象。下面介绍CRITICAL_ SECTION对象的初始化及销毁相关函数。

#include <windows.h>

void InitializeCriticalSection(LPCRITICAL_ SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_ SECTION lpCriticalSection);
  • IpCriticalSection:Init…函数中传入需要初始化的CRITICAL_ SECTION对象的地址值,反之,Del… 函数中传入需要解除的CRITICAL SECTION对象的地址值。

上述函数的参数类型LPCRITICAL_ SECTION是CRITICAL_ SECTION指针类型。另外DeleteCriticalSection并不是销毁CRITICAL SECTION对象的函数。该函数的作用是销毁CRITICAL SECTION对象使用过的(CRITICAL SECTION对象相关的)资源。接下来介绍获取(拥有 )及释放CRITICAL SECTION对象的函数, 可以简单理解为获取和释放“钥匙”的函数。

#include <windows.h>
void EnterCriticalSection(LPCRITICAL_ SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_ SECTION lpCriticalSection);
  • IpCriticalSection:获取(拥有)和释放的CRITICAL_ SECTION对象的地址值。

示例

#include <stdio.h>
#include <windows.h>
#include <process.h>

#define NUM THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);

long long num=0;
CRITICAL_ SECTION Cs;

int main(int argc, char *argv[])
{
	HANDLE tHandles [NUM_ THREAD];
	int i;

	InitializeCriticalSection(&cs);
	for(i=0; i<NUM THREAD; i++)
	{
		if(i%2)
			tHandles[i]=(HANDLE)_ beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
		else
			tHandles[i]=(HANDLE)_ beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
	}

	WaitForMultipleobjects (NUM_ THREAD, tHandles, TRUE, INFINITE);
	DeleteCriticalSection(&cs);
	printf("result: %lld \n", num);
	return 0;
}

unsigned WINAPI threadInc(void * arg)
{
	int i;
	EnterCriticalSection(&cs);
	for(i=0; 1<50000000; 1++)
	num+=1;
	LeaveCriticalSection(&cs);
	return 0;
}

unsigned WINAPI threadDes(void * arg)
{
	int i;
	EnterCriticalSection(&cs);
	for(i=0; i<50000000; 1++)
	num-=1;
	LeaveCriticalSection(&cs);
	return 0;
}

内核模式下的同步技术

典型的内核模式同步方法有基于事件(Event)、信号量、互斥量等内核对象的同步

基于互斥量对象的同步
#include <windows.h>
HANDLE CreateMutex(
LPSECURITY_ ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
//成功时返回创建的互斥量对象句柄,失败时返回NULL。
  • IpMutexAttributes:传递安全相关的配置信息,使用默认安全设置时可以传递NULL。
  • blnitialOwner:如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non-signaled状态;如果为FALSE,则创建出的互斥量对象不属于任何线程,此
    时状态为signaled。
  • IpName:用于命名互斥量对象。传入NULL时创建无名的互斥量对象。

从上述参数说明中可以看到,如果互斥量对象不属于任何拥有者,则将进人signaled状态。利用该特点进行同步。另外,互斥量属于内核对象,所以通过如下函数销毁。

#include <windows.h>
BOOL CloseHandle(HANDLE hObject);
//成功时返回TRUE,失败时返回FALSE。
  • hObject:要销毁的内核对象的句柄。

上述函数是销毁内核对象的函数,所以同样可以销毁即将介绍的信号量及事件。

下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过熟悉的WaitForSingleObject函数完成的。

#include <windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
//成功时返回TRUE,失败时返回FALSE。
  • hMutex:需要释放(解除拥有)的互斥量对象句柄。

接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为signaled状态,释放时(未拥有时)进入signaled状态。因此,可以使用WaitForSingleObject函数验证互斥量是否
已分配。该函数的调用结果有如下2种。

  • 调用后进人阻塞状态:互斥量对象已被其他线程获取,现处于non-signaled状态。
  • 调用后直接返回:其他线程未占用互斥量对象,现处于signaled状态。

互斥量在WaitForsSingleObject函数返回时自动进人non-signaled状态,因为它是“auto-reset" 模式的内核对象。结果,WaitForSingleObject函数成为申请互斥量时调用的函数。因此,基于互斥量的临界区保护代码如下。

WaitForSingle0bject(hMutex, INFINITE);
//临界区的开始
//......
//临界区的结束
ReleaseMutex(hMutex);
基于信号量对象的同步

Windows中基于信号量对象的同步也与Linux下的信号量类似,二者都是利用名为“信号量值"(Semaphore Value)的整数值完成同步的,而且该值都不能小于0。当然,Windows的信号量值注册于内核对象。

下面介绍创建信号量对象的函数,当然,其销毁同样是利用CloseHandle函数进行的。

#include <windows.h>
HANDLE CreateSemaphore(
LPSECURITY_ ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount,
LONG lMaximumCount, LPCTSTR lpName); 
//成功时返回创建的信号量对象的句柄,失败时返回NULL。
  • IpSemaphoreAttributes 安全配置信息,采用默认安全设置时传递NULL。
  • InitialCount:指定信号量的初始值,应大于0小于IMaximumCount。
  • IMaximumCount:信号量的最大值。该值为1时,信号量变为只能表示0和1的二进制信号量。
  • IpName:用于命名信号量对象。传递NULL时创建无名的信号量对象。

可以利用“信号量值为0时进人non-signaled状态,大于0时进人signaled状态”的特性进行同步。向IInitialCount参数传递0时,创建non- signaled状态的信号量对象。而向IMaximumCount传入3
时,信号量最大值为3,因此可以实现3个线程同时访问临界区时的同步。下面介绍释放信号量对象的函数。

#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG
lpPreviousCount);
//成功时返回TRUE,失败时返回FALSE。
  • hSemaphore:传递需要释放的信号量对象。
  • lReleaseCount:释放意味着信号量值的增加,通过该参数可以指定增加的值。超过最大值则不增加,返回FALSE。
  • lpPreviousCount:用于保存修改之前值的变量地址,不需要时可传递NULL。
    信号量对象的值大于0时成为signaled状态,为0时成为nonsignaled状态。因此,调用WaitForSingleObject函数时,信号量大于0的情况下才会返回。返回的同时将信号量值减1,同时
    进入non-signaled状态(当然,仅限于信号量减1后等于0的情况)。可以通过如下程序结构保护临界区。
WaitForSingle0bject(hSemaphore, INFINITE);
//临界区的开始
//......
//临界区的结束
ReleaseSemaphore(hSemaphore, 1, NULL); 
基于事件的同步

事件同步对象与前2种同步方法相比有很大不同,区别就在于,该方式下创建对象时,可以在自动以non-signaled状态运行的auto-reset模式和与之相反的manual-reset模式中任选其一。而事件对象的主要特点是可以创建manual-reset模式的对象。
创建事件对象函数:

#include <windows.h>
HANDLE CreateEvent(
LPSECURITY_ ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
//成功时返回创建的事件对象句柄,失败时返回NULL。
  • lpEventAttributes:安全配置相关参数,采用默认安全配置时传入NULL。
  • bManualReset:传入TRUE时创建manual-reset模式的事件对象,传入FAL SE时创建auto-reset模式的事件对象。
  • blnitialState:传入TRUE时创建signaled状态的事件对象,传入FALSE时创建non-signaled状态的事件对象。
  • IpName:用于命名事件对象。传递NULL时创建无名的事件对象。

上述函数中需要重点关注的是第二个参数。传人TRUE时创建manual-reset模式的事件对象,此时即使WaitForSingleObject函数返回也不会回到non-signaled状态。因此,在这种情况下,需要通过如下2个函数明确更改对象状态。

#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); //to the non-signaled
BOOL SetEvent(HANDLE hEvent); //to the signaled
//成功时返回TRUE,失败时返回FALSE。

传递事件对象句柄并希望改为non-signaled状态时,应调用ResetEvent函数。如果希望改为signaled状态,则可以调用SetEvent函数。

异步通知I/O模型

同步的关键是函数的调用及返回时刻,以及数据传输的开始和完成时刻。
"调用send函数的瞬间开始传输数据,send函数执行完(返回)的时刻完成数据传输。"
"调用reev函数的瞬间开始接收数据,recv函数执行完(返回)的时刻完成数据接收。"
异步I/O是指I/O函数的返回时刻与数据收发的完成时刻不一致。
调用同步的I/O函数:
在这里插入图片描述
调用异步I/O函数:
在这里插入图片描述

同步和异步的优缺点

同步I/O的缺点:进行I/O的过程中函数无法返回,所以不能执行其他任务!
异步I/O的优点:无论数据是否完成交换都返回函数,意味着可以执行其他任务。
总结:异步方式能够比同步方式更有效地使用CPU。

理解异步通知I/O模型

异步通知I/O模型的实现方法有2种:一种是使用本书介绍的WSAEventSelect函数,另一种是使用WSAAsyncSelect函数。使用WSAAsyncSelect函数时需要指定Windows句柄以获取发生的事件(UI相关内容),此处不涉及。

wSAEventSelect 函数和通知

告知I/O状态变化的操作就是“通知”。I/O的状态变化可以分为不同情况。

  • 套接字的状态变化:套接字的I/O状态变化。
  • 发生套接字相关事件:发生套接字I/O相关事件。

这2种情况都意味着发生了需要或可以进行I/O的事件。

WSAEventSelect函数:该函数用于指定某一套接字为事件监视对象。

#include <winsock2.h>
int wSAEventSelect(SOCKET s, WSAEVENT hEventobject,long lNetworkEvents);
//成功时返回日,失败时返回SOCKET_ERROR。
  • s:监视对象的套接字句柄。
  • hEventObject:传递事件对象句柄以验证事件发生与否。
  • lNetworkEvents:希望监视的事件类型信息。

传入参数s的套接字内只要发生INetworkEvents中指定的事件之一,WSAEventSelect函数就将hEventObject句柄所指内核对象改为signaled状态。因此,该函数又称“连接事件对象和套接字的函数”。另外一个重要的事实是,无论事件发生与否,WSAEventSelect函数调用后都会直接返回,所以可以执行其他任务。也就是说,该函数以异步通知方式工作。下面介绍作为该函数第三个参数的事件类型信息,可以通过位或运算同时指定多个信息。

  • FD_READ:是否存在需要接收的数据?
  • FD_WRITE:能否以非阻塞方式传输数据?
  • FD_OOB:是否收到带外数据?
  • FD_ACCEPT:是否有新的连接请求?
  • FD_CLOSE:是否有断开连接的请求?

以上就是WSAEventSelect函数的调用方法。select函数可以针对多个套接字对象调用,但WSAEventSelect函数只能针对1个套接字对象调用。

从前面关于WSAEventSelect函数的说明中可以看出,需要补充如下内容。

  • WSAEventSelect函数的第二个参数中用到的事件对象的创建方法。
  • 调用-wSAEventSelect函数后发生事件的验证方法。
  • 验证事件发生后事件类型的查看方法。

上述过程中只要插入WSAEventSelect函数的调用就与服务器端的实现过程完全一致,下面分别讲解。

manual-reset模式实践对象的其他创建方法

我们之前利用CreateEvent函数创建了事件对象。CreateEvent函数在创建事件对象时,可以在auto-reset模式和manual-reset模式中任选其一。但我们只需要manual-reset模式non-signaled状态的事件对象,所以利用如下函数创建较为方便。

#include <winsock2.h>
wSAEVENT WSAcreateEvent(void);
//成功时返回事件对象句柄。失败时返回wSA_INVALID_EVENT。

上述声明中返回类型WSAEVENT的定义如下:

#define WSAEVENT HANDLE

实际上就是我们熟悉的内核对象句柄,这一点需要注意。另外,为了销毁通过上述函数创建的事件对象,系统提供了如下函数。

#include <winsock2.h>
BOOL WSACloseEvent(WSAEVENT hEvent);
//成功时返回TRUE,失败时返回FALSE。
验证是否发生了事件

为了验证是否发生事件,需要查看事件对象。完成该任务的函数如下,除了多1个参数外,其余部分与WaitForMultipleObjects函数完全相同。

#include <winsock2.h>
DWORD WSAwaitForMultipleEvents(
	DwORD cEvents,const wSAEVENT * lphEvents,BOOL fwaitAll,DWORD dwTimeout,BOOL fAlertable);
//成功时返回发生事件的对象信息,失败时返回wSA_INVALID_EVENT。
  • cEvents:需要验证是否转为signaled状态的事件对象的个数。
  • lphEvents:存有事件对象句柄的数组地址值。
  • fWaitAll:传递TRUE时,所有事件对象在signaled状态时返回;传递FALSE时,只要其中1个变为signaled状态就返回。
  • dwTimeout:以1/1000秒为单位指定超时,传递WSA_INFINITE时,直到变为signaled状态时才会返回。
  • fAlertable:传递TRUE时进入alertable wait(可警告等待)状态。
  • 返回值:返回值减去常量WSA_WAIT_EVENT_0时,可以得到转变为signaled状态的事件对象句柄对应的索引,可以通过该索引在第二个参数指定的数组中查找句柄。如果有多个事件对象变为signaled状态,则会得到其中较小的值。发生超时将返回WAIT_TIMEOUT。

由于发生套接字事件,事件对象转为signaled状态后该函数才返回,所以它非常有利于确认事件发生与否。但由于最多可传递64个事件对象,如果需要监视更多句柄,就只能创建线程或扩展保存句柄的数组,并多次调用上述函数。

区分事件类型

既然已经通过WSAWaitForMultipleEvents函数得到了转为signaled状态的事件对象,最后就要确定相应对象进入signaled状态的原因。为完成该任务,我们引入如下函数。调用此函数时,不仅需要signaled状态的事件对象句柄,还需要与之连接的(由WSAEventSelect函数调用引发的)发生事件的套接字句柄。

#include <winsock2.h>
int wSAEnumNetworkEvents(
	sOCKET s, wSAEVENT hEventobject,LPWSANETWORKEVENTS lpNetworkEvents);
//成功时返回日,失败时返回SOCKET__ERROR。
  • s:发生事件的套接字句柄。
  • hEventObject:与套接字相连的(由WSAEventSelect函数调用引发的)signaled状态的事件对象句柄。
  • lpNetworkEvents:保存发生的事件类型信息和错误信息的WSANETWORKEVENTS结构体变量地址值。

上述函数将manual-reset模式的事件对象改为non-signaled状态,所以得到发生的事件类型后,不必单独调用ResetEvent函数。下面介绍与上述函数有关的WSANETWORKEVENTS结构体。

typedef struct _wSANETWORKEVENTS
{
	long lNetworkEvents;
	int iErrorCode[FD_MAX_EVENTS];
} wSANETwORKEVENTS,* LPwSANETWORKEVENTS;

上述结构体的INetworkEvents成员将保存发生的事件信息。与WSAEventSelect函数的第三个参数相同,需要接收数据时,该成员为FD_READ;有连接请求时,该成员为FD_ACCEPT。因此,可通过如下方式查看发生的事件类型。

wSANETWORKEVENTS netEvents;
......
wSAEnumNetworkEvents(hSock,hEvent,&netEvents);
if(netEvents.lNetworkEvents &FD_ACCEPT)
{
//FD_ACCEPT事件的处理
}
if( netEvents. lNetworkEvents &FD_READ)
{
//FD_READ事件的处理
}
if(netEvents.lNetworkEvents &FD_CLOSE){
//FD_CLOSE事件的处理
}

另外,错误信息将保存到声明为成员的iErrorCodc数组(发生错误的原因可能很多,因此用数组声明)。验证方法如下。
如果发生FD_READ相关错误,则在iErrorCode[FD_READ_BIT]中保存除O以外的其他值。如果发生FD_WRITE相关错误,则iErrorCode[FD_WRITE_BIT]中保存除0以外的其他值。

可通过如下描述理解上述内容。

“如果发生FD_XXX相关错误,则在iErrorCode[FD_XXX_BIT]中保存除0以外的其他值”

因此可以用如下方式检查错误。

wSANETWORKEVENTS netEvents;
......

wSAEnumNetworkEvents(hSock,hEvent,&netEvents);
......
if(netEvents.iErrorcode[FD_READ_BIT] != 0)
{
//发生FD_READ事件相关错误
}

以上就是异步通知I/O模型的全部内容。

重叠I/O模型

异步I/O是指I/O函数的返回时刻与数据收发的完成时刻不一致。而同一线程内部向多个目标传输(或从多个目标接收)数据引起的I/O重叠现象称为"重叠I/O"。为了完成这项任务,调用的I/O函数应立即返回,只有这样才能发送后续数据。从结果上看,利用上述模型收发数据时,最重要的前提条件就是异步I/O。而且,为了完成异步I/O,调用的I/O函数应以非阻塞模式工作。
重叠I/O模型:
在这里插入图片描述
Windows中重叠I/O的重点并非I/O本身,而是如何确认I/O完成时的状态。不管是输人还是输出,只要是非阻塞模式的,就要另外确认执行结果。关于这种确认方法我们还一无所知。确认执行结果前需要经过特殊的处理过程,这就是本章要讲述的内容。Windows中的重叠I/O不仅包含上图所示的I/O(这是基础),还包含确认I/O完成状态的方法。

创建重叠I/O套接字

首先要创建适用于重叠I/O的套接字,可以通过如下函数完成。

#include <winsock2.h>
sOCKET wSASocket(
int af, int type,int protocol,LPwSAPROTOCOL_INFO lpProtocolInfo,GROUP g,DwORD dwFlags);
//成功时返回套接字句柄,失败时返回INVALID_SOCKET。
  • af:协议族信息。
  • type:套接字数据传输方式。
  • protocol:2个套接字之间使用的协议信息。
  • lpProtocollnfo:包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时传递NULL。
  • g:为扩展函数而预约的参数,可以使用0。
  • dwFlags:套接字属性信息。

第四个和第五个参数与目前的工作无关,可以简单设置为NULL和0。可以向最后一个参数传递WSA_FLAG_OVERLAPPED,赋予创建出的套接字重叠I/O特性。总之,可以通过如下函数调用创建出可以进行重叠I/O的非阻塞模式的套接字。

wSASocket(PF_INET,SOCK_STREAM,0NULL,0,wSA_FLAG_OVERLAPPED);

执行重叠I/O的WSASend函数

创建出具有重叠I/O属性的套接字后,接下来2个套接字(服务器端/客户端之间的)连接过程与一般的套接字连接过程相同,但I/O数据时使用的函数不同。先介绍重叠I/O中使用的数据输出函数。

#include <winsock2.h>
int WSASend(
	SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,
	LPDWORD lpNumberofBytesSent,DWORD dwFlags,LPwSAOVERLAPPED
	lpoverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
	//成功时返回日,失败时返回SOCKET_ERROR。
  • s:套接字句柄,传递具有重叠I/O属性的套接字句柄时,以重叠I/O模型输出。
  • lpBuffers:wSABUF结构体变量数组的地址值,WSABUF中存有待传输数据。
  • dwBufferCount:第二个参数中数组的长度。
  • lpNumberOfBytesSent:用于保存实际发送字节数的变量地址值(稍后进行说明)。
  • dwFlags:用于更改数据传输特性,如传递MSG_OOB时发送OOB模式的数据。
  • lpOverlapped:wSAOVERLAPPED结构体变量的地址值,使用事件对象,用于确认完成数据传输。
  • lpCompletionRoutine:传入Completion Routine函数的入口地址值,可以通过该函数确认是否完成数据传输。

上述函数的第二个结构体参数类型,该结构体中存有待传输数据的地址和大小等信息。

typedf struct __wSABUF{
	u_long len;//待传输数据的大小char FAR * buf;//缓冲地址值
}wSABUF,* LPwSABUF;

下面给出上述函数的调用示例。利用上述函数传输数据时可以按如下方式编写代码。

wSAEVENT event;
wSAOVERLAPPED overlapped;
wSABUF dataBuf;
char buf[BUF_SIZE]= {"待传输的数据"};
int recvBytes = 0;
......
event = wSAcreateEvent();
memset(&overlapped,0, sizeof(overlapped)); //所有位初始化为0!
overlapped.hEvent = event;
dataBuf.len = sizeof(buf);
dataBuf.buf = buf;
wSASend(hSocket, &dataBuf,1,&recvBytes,0,&overlapped,NULL);
......

调用WSASend函数时将第三个参数设置为1,因为第二个参数中待传输数据的缓冲个数为1。另外,多余参数均设置为NULL或0,其中需要注意第六个和第七个参数(稍后将具体解释,现阶段只需留意即可)。第六个参数中的WSAOVERLAPPED结构体定义如下。

typedef struct _wSAOVERLAPPE
{
	DWORD Internal;
	DWORD InternalHigh;
	DwORD Offset;
	DWORD OffsetHigh;
	WSAEVENT hEvent;
}wSAOVERLAPPED,*LPwSAOVERLAPPED;

Internal、InternalHigh成员是进行重叠I/O时操作系统内部使用的成员,而Offset、OffsetHigh同样属于具有特殊用途的成员。所以各位实际只需要关注hEvent成员,稍后将介绍该成员的使用方法。

“为了进行重叠I/O,WSASend函数的lpOverlapped参数中应该传递有效的结构体变量地址值,而不是NULL。”
如果向lpOverlapped传递NULL,WSASend函数的第一个参数中的句柄所指的套接字将以阻塞模式工作。还需要了解以下这个事实,否则也会影响开发。
“利用WSASend函数同时向多个目标传输数据时,需要分别构建传入第六个参数的wSAOVERLAPPED结构体变量。”
这是因为,进行重叠I/O的过程中,操作系统将使用WSAOVERLAPPED结构体变量。

WSASend补充

实际上,WSASend函数调用过程中,函数返回时间点和数据传输完成时间点并非总不一致。如果输出缓冲是空的,且传输的数据并不大,那么函数调用后可以立即完成数据传输。此时,WSASend函数将返回o,而lpNumberOfBytesSent中将保存实际传输的数据大小的信息。反之,WSASend函数返回后仍需要传输数据时,将返回SOCKET_ERROR,并将WSA_IO_PENDING注册为错误代码,该代码可以通过WSAGetLastError函数得到。这时应该通过如下函数获取实际传输的数据大小。

#include <winsock2.h>
BOOL wSAGetOverlappedResult(
SOCKET s,LPWSAOVERLAPPED lpoverlapped,LPDWORD lpcbTransfer,BOOL fwait,LPDWORD lpdwFlags);
//成功时返回 TRUE,失败时返回 FALSE。
  • s:进行重叠I/O的套接字句柄。
  • lpOverlapped:进行重叠I/O时传递的WSAOVERLAPPED结构体变量的地址值。
  • lpcbTransfer:用于保存实际传输的字节数的变量地址值。
  • fWait:如果调用该函数时仍在进行I/O,fWait为TRUE时等待I/O完成,fWait为FALSE时将返回FALSE并跳出函数。
  • lpdwFlags:调用WSARecv函数时,用于获取附加信息(例如OOB消息)。如果不需要,可以传递NULL。

通过此函数不仅可以获取数据传输结果,还可以验证接收数据的状态。如果给出示例前进行过多理论说明会使人感到乏味,所以稍后将通过示例讲解此函数的使用方法。

进行重叠I/O的WSARecv函数

重叠I/O中使用的数据输入函数。

#include <winsock2.h>
int WSARecv(
	SOCKETS,LPWSABUF lpBuffers,DwORD dwBufferCount,
	LPDWORD lpNumberOfBytesRecvd,LPDwORD lpFlags,LPwSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpcompletionRoutine
);
//成功时返回e,失败时返回SOCKET_ERROR。
  • s:赋予重叠I/O属性的套接字句柄。
  • lpBuffers:用于保存接收数据的WSABUF结构体数组地址值。
  • dwBufferCount:向第二个参数传递的数组的长度。
  • lpNumberOfBytesRecvd:保存接收的数据大小信息的变量地址值。
  • lpFlags:用于设置或读取传输特性信息。
  • lpOverlapped:wSAOVERLAPPED结构体变量地址值。
  • lpCompletionRoutine:Completion Routine函数地址值。

Gather/Scatter I/O

Gather/Scatter I/O是指,将多个缓冲中的数据累积到一定程度后一次性传输(Gather输出),将接收的数据分批保存(Scatter输入)。writev & readv函数具有Gather/Scatter I/O功能,但Windows下并没有这些函数的定义。不过,可以通过重叠I/O中的WSASend和WSARecv函数获得类似功能。上述函数,从它们的第二个和第三个参数中可以判断其具有Gather/Scatter I/O功能。

重叠I/O的I/O完成确认

重叠I/O中有2种方法确认I/O的完成并获取结果。

  • 利用WSASend、WSARecv函数的第六个参数,基于事件对象。
  • 利用WSASend、WSARecv函数的第七个参数,基于Completion Routine。
使用事件对象

介绍使用示例,需要验证2点。

  • 完成I/O时,WSAOVERLAPPED结构体变量引用的事件对象将变为signaled状态。
  • 为了验证I/O的完成和完成结果,需要调用WSAGetOverlappedResult函数。
示例

注:本示例目的在于整理之前的一系列知识点。

#include <cstdio.h>
#include <stdlib.h>
#include <cwinsock2.h>
void ErrorHandling(char *msg);

int main(int argc,char *argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN sendAdr;

	wSABUF dataBuf;
	char msg[]="Network is computer!";
	int sendBytes=0;
	
	wSAEVENT evObj;
	wSAOVERLAPPED overlapped;

	if(argc!=3){
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if(wSAStartup(MAKEwORD(2,2),&wsaData)!=0)
		ErrorHandiing( "wSAStartup() error!");

	hSocket=wSASocket(PF_INET,SoCK_STREAM,0,NULL,0, WSA_FLAG_OVERLAPPED);
	memset(&sendAdr,0,sizeof(sendAdr));
	sendAdr.sin_family=AF_INET;
	sendAdr.sin_addr.s_addr=inet_addr(argv[1]);
	sendAdr.sin_port=htons(atoi(argv[2]));

	if(connect(hsocket,(SOCKADDR*)&sendAdr,sizeof(sendAdr))==SOCKET_ERROR)
		ErrorHandling("connect() error!");

	evobj=wSACreateEvent();
	memset(&over1apped,e,sizeof(overlapped));
	overlapped. hEvent=evobj;
	dataBuf.len=strlen(msg)+1;
	dataBuf.buf=msg;
	
	if(wSASend(hSocket,&dataBuf,1,&sendBytes,0,&overlapped,NULL)==SOCKET_ERROR)
	{
		if(wSAGetLastError()==wSA_IO_PENDING)
		{
			puts("Background data send");
			wSAwaitForMultipleEvents(1,&evObj,TRUE,WSA_INFINITE,FALSE);
			wSAGetoverlappedResult(hSocket,&overlapped,&sendBytes,FALSE, NULL);
		}
		else{
			ErrorHandling("wSASend() error");
		}
	}
	printf("send data size: %d \n", sendBytes);
	wSACloseEvent(evobj);
	closesocket(hsocket);
	wSACleanup();
	return 0;
}
	
void ErrorHandling(char *msg)
{
	fputs(msg,stderr);
	fputc('\n', stderr);
	exit(1);
}
WSAGetLastError函数
#include <winsock2.h>
int wSAGetLastError(void);
//返回错误代码(表示错误原因)。
使用Completion Routine函数

前面的示例通过事件对象验证了I/O完成与否,下面介绍如何通过WSASend、WSARecv函数的最后一个参数中指定的Completion Routine (以下简称CR)函数验证I/O完成情况。“注册CR”具有如下含义:
“Pending的I/O完成时调用此函数!”
I/O完成时调用注册过的函数进行事后处理,这就是Completion Routine的运作方式。如果执行重要任务时突然调用Completion Routine,则有可能破坏程序的正常执行流。因此,操作系统通常会预先定义规则:
“只有请求I/O的线程处于alertable wait状态时才能调用Completion Routine函数!”
“alertable wait状态”是等待接收操作系统消息的线程状态。调用下列函数时进入alertable wait状态。

  • waitForSingle0bjectEx
  • waitForMultipleobjectsEx
  • wSAMaitForMultipleEvents
  • SleepEx

第一、第二、第四个函数提供的功能与WaitForSingleObject、WaitForMultipleObjects、Sleep函数相同。上述函数只增加了1个参数,如果该参数为TRUE,则相应线程将进入alertable wait状态。另外,以WSA为前缀的函数,该函数的最后一个参数设置为TRUE时,线程同样进入alertable wait状态。因此,启动I/O任务后,执行完紧急任务时可以调用上述任一函数验证I/O完成与否。此时操作系统知道线程进入alertable wait状态,如果有已完成的I/O,则调用相应Completion Routine函数。调用后,上述函数将全部返回WAIT_IO_COMPLETION,并开始执行接下来的程序。

示例
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DwORD,DwORD,LPwSAOVERLAPPED,DwORD);
void ErrorHandling(char *message);

wSABUF dataBuf;
char buf[BUF_SIZE];int recvBytes=e;
int main(int argc,char* argv[]){
	wSADATA wsaData;
	SoCKET hlisnsock, hRecvSock;
	SOCKADDR_IN lisnAdr, recvAdr;
	
	wSAOVERLAPPED overlapped;
	wSAEVENT evobj;
	
	int idx,recvAdrSz,flags=0;
	if(arge!=2){
		printf("Usage: %s<port>\n". argv[0]);
		exit(1);
	}
	if(wSAStartup(MAKEWORD(2,2),&wsaData) != 0)
		ErrorHandling( "wSAStartup(error!");

	hLisnSock=WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,wSA_FLAG_OVERLAPPED);
	memset(&lisnAdr, 0,sizeof( lisnAdr));
	lisnAdr.sin_family=AF_INET;
	lisnAdr.sin_addr.s_addr=hton1(INADDR_ANY);lisnAdr.sin_port=htons(atoi(argv[1]));

	if(bind(hLisnSock,(SOCKADDR*) &lisnAdr,sizeof(lisnAdr))==SOCKET_ERROR)
		ErrorHandling( "bind() error");
	if(listen(hLisnSock,5)==SOCKET_ERROR)
		ErrorHandling("listen() error");

	recvAdrSz=sizeof(recvAdr);
	hRecvSock=accept(hLisnSock,(SOCKADDR*)&recvAdr, &recvAdrSz);
	if(hRecvSock==INVALID_SOCKET)
		ErrorHandling("accept() error");
	memset(&overlapped,0,sizeof(overlapped));
	dataBuf.len=BUF_SIZE;
	dataBuf.buf=buf;
	evobj=wSACreateEvent();//Dummy event object

if(WSARecv(hRecvSock,&dataBuf,1, &recvBytes,&flags,&overlapped,CompRoutine)==SOCKET_ERROR)
{
	if(WSAGetLastError()==NSA_IO_PENDING)
	puts("Background data receive");

	idx=wSAWaitForMultipleEvents(1, &ev0bj,FALSE, WSA_INFINITE,TRUE);
	if(idx==WAIT_IO_COMPLETION)
		puts("Overlapped I/O Completed");
	else	// If error occurred!
		ErrorHandling("wSARecv() error");
	wSAC1oseEvent(evobj);
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	wSACleanup();
	return 0;
}

void CALLBACK CompRoutine(
	DMORD dwError,DwORD szRecvBytes,LPwSAOVERLAPPED lpOverlapped,DwORD flags)
{
	if(dwErrorl=0)
	{
		ErrorHandling("compRoutine() error");
	}
	else
	{
		recvBytes=szRecvBytes;
		printf( "Received message: %s \n", buf);

void ErrorHandling(char *message)
{
	fputs(message,stderr);
	fputc("\n", stderr);
	exit(1);
}

传入WSARecv函数的最后一个参数的Completion Routine函数原型。

void CALLBACK CompletionROUTINE(
DWORD dwError,DwORD cbTransferred,LPWSAOVERLAPPED lpoverlapped,
DlWORD dwFlags);

其中第一个参数中写入错误信息(正常结束时写入0),第二个参数中写入实际收发的字节数。第三个参数中写入WSASend、WSARecv函数的参数lpOverlapped,dwFlags中写入调用I/O函数时传入的特性信息或0。另外,返回值类型void后插入的CALLBACK关键字与main函数中声明的关键字WINAPI相同,都是用于声明函数的调用规范,所以定义Completion Routine函数时必须添加。

IOCP

IOCP(Input Output Completion Port,输入输出完成端口)是性能最好的Windows平台I/O模型。
为了突破select等传统I/O模型的极限,每种操作系统(内核级别)都会提供特有的I/O模型以提高性能。其中最具代表性的有Linux的epoll和Windows的IOCP。

实现非阻塞模式的套接字

在Windows中通过如下函数调用将套接字属性更改为非阻塞模式。

SOCKET hLisnSock;
int mode = 1;
......
hListSock = WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
ioctlsocket(hLisnSock,FIONBIO,&mode);	//for non-blocking socket

上述代码中调用的ioctlsocket函数负责控制套接字I/O方式,其调用具有如下含义:
“将hLisnSock句柄引用的套接字I/O模式(FIONBIO)改为变量mode中指定的形式。”也就是说,FIONBIO是用于更改套接字I/O模式的选项,该函数的第三个参数中传入的变量中若存有0,则说明套接字是阻塞模式的;如果存有非0值,则说明已将套接字模式改为非阻塞模式。改为非阻塞模式后,除了以非阻塞模式进行I/O外,还具有如下特点。

  • 如果在没有客户端连接请求的状态下调用accept函数,将直接返回INVALID_SOCKET.调用WSAGetLastError函数时返回WSAEWOULDBLOCK。
  • 调用accept函数时创建的套接字同样具有非阻塞属性。

因此,针对非阻塞套接字调用accept函数并返回INVALID_SOCKET时,应该通过WSAGetLastError函数确认返回INVALID_SOCKET的理由,再进行适当处理。

以纯重叠I/O方法实现回声服务器端

要想实现基于重叠I/O的服务器端,必须具备非阻塞套接字,所以先介绍了其创建方法。实际上,因为有IOCP模型,所以很少有人只用重叠I/O实现服务器端。但我认为:“为了正确理解 IOCP,应当尝试用纯重叠I/O方式实现服务器端。”
即使坚持不用IOCP,也应具备仅用重叠I/O方式实现类似IOCP的服务器端的能力。这样就可以在其他操作系统平台实现类似IOCP方式的服务器端,而且不会因IOCP的限制而忽略服务器端功能的实现。

纯重叠I/O模型实现回声服务器端略

从重叠I/O模型到IOCP模型

重叠I/O模型回声服务器的缺点:
重复调用非阻塞模式的accept函数和以进入alertable wait状态为目的的SleepEx函数将影响性能!
补救措施:
“让main线程(在main函数内部)调用accept函数,在单独创建1个线程负责客户端I/O”。
其实这就是IOCP中采用的服务器模型。换言之,IOCP将创建专用的I/O线程,该线程负责与所有客户端进行I/O。

负责全部I/O

会误认为IOCP就是创建专职IO线程的一种模型。IOCP为了负责全部IO工作,的确需要创建至少1个线程。但“在服务器端负责全部I/O"实质上相当于负责服务器端的全部工作,因此,它并不是仅负责I/O工作,而是至少创建1个线程并使其负责全部I/O的前后处理。尝试理解IOCP时不要把焦点集中于线程,而是注意观察如下2点。

  • I/O是否以非阻塞模式工作?
  • 如何确定非阻塞模式的I/O是否完成?

分阶段实现IOCP程序

创建"完成端口"

IOCP中已完成的I/O信息将注册到完成端口对象( Completion Port,简称CP对象),但这个过程并非单纯的注册,首先需要经过如下请求过程:
“该套接字的I/O完成时,请把状态信息注册到指定CP对象。”
该过程称为“套接字和CP对象之间的连接请求”。因此,为了实现基于IOCP模型的服务器端,需要做如下2项工作。

  • 创建完成端口对象。
  • 建立完成端口对象和套接字之间的联系。

此时的套接字必须被赋予重叠属性。上述2项工作可以通过1个函数完成,但为了创建CP对象,先介绍如下函数。

#include <windows.h>
HANDLE CreateIocompletionPort(
	HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey,DlWORD NumberOfConcurrentThreads);
//成功时返回CP对象句柄,失败时返回 NULL。
  • FileHandle:创建CP对象时传递INVALID_HANDLE_VALUE。
  • ExistingCompletionPort:创建CP对象时传递NULL。
  • CompletionKey:创建CP对象时传递0。
  • NumberOfConcurrentThreads:分配给CP对象的用于处理I/O的线程数。例如,该参数为2时,说明分配给CP对象的可以同时运行的线程数最多为2个;如果该参数为0,系统中CPU个数就是可同时运行的最大线程数。

以创建CP对象为目的调用上述函数时,只有最后一个参数才真正具有含义。可以用如下代码段将分配给CP对象的用于处理I/O的线程数指定为2。

HANDLE hCpobject;
. . .. . . 
hcpobject = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,2);
连接完成端口对象和套接字

既然有了CP对象,接下来就要将该对象连接到套接字,只有这样才能使已完成的套接字I/O信息注册到CP对象。下面以建立连接为目的再次介绍CreateloCompletionPort函数。

#include <windows.h>
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey,DWORD NumberofConcurrentThreads);
//成功时返回CP对象句柄,失败时返回NULL。
  • FileHandle:要连接到CP对象的套接字句柄。
  • ExistingCompletionPort:要连接套接字的CP对象句柄。
  • CompletionKey:传递已完成I/O相关信息,关于该参数将在稍后介绍的GetQueued-CompletionStatus函数中共同讨论。
  • NumberOfConcurrentThreads:无论传递何值,只要该函数的第二个参数非NULL就会忽略。

上述函数的第二种功能就是将FileHandle句柄指向的套接字和ExistingCompletionPort指向的CP对象相连。该函数的调用方式如下。

HANDLE hCpobject;
SOCKET hSock;
......
createIoCompletionPort((HANDLE)hSock,hcpobject,(DMORD)ioInfo,0);

调用CreateloCompletionPort函数后,只要针对hSock的I/O完成,相关信息就将注册到hCpObject指向的CP对象。

确认完成端口已完成的I/O和线程的I/O处理

确认CP中注册的已完成的I/O,完成该功能的函数如下:

#include <windows.h>
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,LPDWORD 1pNumberOfBytes,PULONG_PTR lpCompletionKey,LPOVERLAPPED * lpoverlapped,DWORD dwMilliseconds);
//成功时返回TRUE,失败时返回FALSE。
  • CompletionPort:注册有已完成I/O信息的CP对象句柄。
  • lpNumberOfBytes:用于保存I/O过程中传输的数据大小的变量地址值。
  • lpCompletionKey:用于保存 CreateloCompletionPort函数的第三个参数值的变量地址值。
  • lpOverlapped:用于保存调用WSASend、WSARecv函数时传递的OVERLAPPED结构体地址的变量地址值。
  • dwMilliseconds:超时信息,超过该指定时间后将返回FALSE并跳出函数。传递INFINITE时,程序将阻塞,直到已完成I/O信息写入CP对象。

虽然只介绍了2个IOCP相关函数,但依然有些复杂,特别是上述函数的第三个和第四个参数更是如此。其实这2个参数主要是为了获取需要的信息而设置的,下面介绍这2种信息的含义。
“通过GetQueuedCompletionStatus函数的第三个参数得到的是以连接套接字和CP对象为目的而调用的CreateCompletionPort函数的第三个参数值。”
“通过GetQueueCompletionStatus函数的第四个参数得到的是调用WSASend 、WSARecv函数时传入的WSAOVERLAPPED结构体变量地址值。”
如前所述,IOCP中将创建全职I/O线程,由该线程针对所有客户端进行I/O。而且CreateIoCompletionPort函数中也有参数用于指定分配给CP对象的最大线程数。

实现基于IOCP的回声服务器端

IOCP回声服务器端的main函数之前的部分
#include <cstdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>
#define BUF_SIZE 188
#define READ 3
#define WRITE 5

//保存与客户端相连的套接字的结构体
typedef struct// socket info
{
	SOCKET hC1ntSock;
	SOCKADDR_IN clntAdr;
}PER_HANDLE_DATA,*LPPER_HANDLE_DATA;
//将I/O中使用的缓冲和重叠I/O中需要的OVERLAPPED结构体变量封装到同一结构体中进行定义
typedef struct	//	buffer info
{
	OVERLAPPED overlapped;
	WSABUF wsaBuf;
	char buffer[BUF_SIZE];
	int rwMode; // READ or WRITE
}PER_IO_DATA,*LPPER_IO_DATA;

DWORD wINAPI EchoThreadMain(LPVOID completionPortIO);
void ErrorHandling(char *message);

需要注意的是:"结构体变量地址值与结构体第一成员的地址值相同。"

main函数部分
int main(int argc,char* argv[]){
	WSADATA wsaData;
	HANDLE hComPort;
	SYSTEM_INFO sysInfo;
	LPPER_IO_DATA ioInfo;
	LPPER_HANDLE_DATA handleInfo;
	
	SOCKET hServSock;
	SOCKADDR_IN servAdr;
	int recvBytes, i,flags=0;
	if(wSAStartup(MAK EWORD(2,2),&wsaData) != 0)
		ErrorHandling("wSAStartup() error!");
	hComPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0,0);
	GetSystemInfo(&sysInfo);
	for(i=0;i < sysInfo.dwNumberOfProcessors; i++)
	_beginthreadex(NULL,0,EchoThreadMain, (LPVOID)hcomPort,0,NULL);

	hServSock=wSASocket(AF_INET,SOCK_STREAM,0,NULL,0, WSA_FLAG_OVERLAPPED);
	memset(&servAdr, 0,sizeof(servAdr));
servAdr.sin_family=AF_INET;
		servAdr.sin_addr.s_addr=hton1(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));

	bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr));
	listen(hServSock,5);
	while(1)
	{
		SOCKET hC1ntSock;
		SOCKADDR_IN c1ntAdr;
		int addrLen=sizeof(c1ntAdr);
		
		hc1ntSock=accept(hServSock,(SOCKADDR*)&clntAdr,&addrLen);
		handleInfo=(LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
		handleInfo->hC1ntSock=hClntSock;
		memcpy(&(handleInfo->cintAdr),&c1ntAdr,addrLen);
		CreateIocompletionPort((HANDLE)hc1ntSock,hcomPort,(DWORD)handleInfo,0);
		ioInfo=(LPPER_IO_DATA)ma1loc(sizeof(PER_IO_DATA));
		memset(&(ioInfo->overlapped), e,sizeof(OVERLAPPED));
		ioInfo->wsaBuf.len=BUF_SIZE;
		ioInfo->wsaBuf.buf=ioInfo->buffer;
		ioInfo->rwMode=READ;
		wSARecv(handleInfo->hClntSock,&(ioInfo->wsaBuf),
		1, &recvBytes,&flags,&(ioInfo->overlapped),NULL);
	}
	return 0;
}
线程的main函数和错误处理函数
DwORD WINAPI EchoThreadMain(LPVOID pComPort)
{
	HANDLE hComPort=(HANDLE)pComPort;
	SOCKET sock;
	DWORD bytesTrans;
	LPPER_HANDLE_DATA handleInfo;
	LPPER_IO_DATA ioInfo;
	DWORD flags=0;
	
	while(1)
		GetQueuedCompletionstatus (hComPort,&bytesTrans,
		(LPDWORD)&handleInfo,(LPOVERLAPPED*)&ioInfo,INFINITE);sock=handleInfo->hClntSock;
		
		if(ioInfo->rwMode==READ)
		{
			puts ("message received! ");
				if(bytesTrans==)//传输EOF时
				{
					closesocket(sock);
					free(handleInfo); free(ioInfo);
					continue;
			}
			
			memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
			ioInfo->wsaBuf.len=bytesTrans;
			ioInfo->rwMode=WRITE;
			WSASend(sock,&(ioInfo->wsaBuf),
			1,NULL, 0, &(ioinfo->over1apped),NULL);
			ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
			memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
			ioInfo->wsaBuf.len=BUF_SIZE;
		ioInfo->wsaBuf.buf=ioInfo->buffer;
		ioInfo->rwMode=READ;
		wSARecv(sock,&(ioInfo->wsaBuf),
		1,NULL,&flags, &(ioInfo->overlapped),NULL);
	}
	else
	{
		puts("message sent!");
		free(ioInfo);
	}
	return 0;
)

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc("\n", stderr);
	exit(1);
}

IOCP性能更优的原因

盲目地认为“因为是IOCP,所以性能更好”的想法并不可取。之前已介绍了Linux和Windows下多种服务器端模型,各位应该可以分析出它们在性能上的优势。将其在代码级别与select进行寸比,可以发现如下特点。

  • 因为是非阻塞模式的I/O,所以不会由I/O引发延迟。
  • 查找已完成I/O时无需添加循环。
  • 无需将作为I/O对象的套接字句柄保存到数组进行管理。
  • 可以调整处理I/O的线程数,所以可在实验数据的基础上选用合适的线程数。

仅凭这些特点也能判断IOCP属于高性能模型,IOCP是Windows特有的功能,所以很大程度上要归功于操作系统。无需怀疑它提供的性能,我认为IOCP和Linux的epoll都是非常优秀的服务器端模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值