接 : 一个简单的IOCP(IO完成端口)服务器/客户端类(1/2)

5.1 文件传输

       使用Winsock 2.0TransmitFile 函数传输文件TransmitFile 函数在连接的套接字句柄上传输文件数据。此函数使用操作系统的缓冲管理机制接收文件数据,在套接字上提供高性能的文件传输。在异步文件传输上有以下几个重要方面:
l           除非TransmitFile 函数返回,否则不能再对套接字执行 发送 写入 操作,不然会破坏文件的传输。在执行PrepareSendFile(..) 函数后,所有对ASend函数的调用都是不允许的。
l           由于系统是连续读文件数据,打开文件句柄的FILE_FLAG_SEQUENTIAL_SCAN特性可以提高缓存性能。
l           在发送文件(TF_USE_KERNEL_APC)时,我们使用内核的异步程序调用。TF_USE_KERNEL_APC的使用可以带来明显的性能提升。很可能(尽管不一定),带有TransmitFile 的线程的上下文环境的初始化会有沉重的计算负担;这种情况下可以防止反复执行APC(异步程序调用)。
文件传输的顺序如下:服务器通过调用PrepareSendFile(..)函数初始化文件传输。客户端接收到文件信息时,通过调用PrepareReceiveFile(..)函数准备接收,并且给服务器发送一个包来开始文件传输。在服务器收到包后,它调用使用高性能的TransmitFile函数的StartSendFile(..)函数传输指定的文件。
 

6 源代码例子

提供的源代码是一个模拟客户端/服务器的例子,它也提供了文件传输功能。在源码中,从类IOCP派生出的类MyIOCP处理客户端和服务器端的通信。在4.1.1 部分提到了这个虚函数的用法。
在客户端,或者服务器端的代码中,虚函数NotifyReceivedPackage是重点。描述如下:
void MyIOCP::NotifyReceivedPackage(CIOCPBuffer *pOverlapBuff,
                           int nSize,ClientContext *pContext)
   {
       BYTE PackageType=pOverlapBuff->GetPackageType();
       switch (PackageType)
       {
         case Job_SendText2Client :
             Packagetext(pOverlapBuff,nSize,pContext);
             break;
         case Job_SendFileInfo :
             PackageFileTransfer(pOverlapBuff,nSize,pContext);
             break; 
         case Job_StartFileTransfer: 
             PackageStartFileTransfer(pOverlapBuff,nSize,pContext);
             break;
         case Job_AbortFileTransfer:
             DisableSendFile(pContext);
             break;};
   }
这个函数处理进来的消息和远程连接发送的请求。在这种情形下,它只不过进行一个简单的回复或者传输文件。源代码分为两部分,IOCPIOCPClient
它们是连接的双方。

6.1 编译器问题

在使用VC++ 6.0 或者 .NT时,在处理类CFile时可能会出现一些奇怪的错误。像下面这样:
“if (pContext->m_File.m_hFile != 
INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "
"from 'void *' to 'unsigned int'”
在你更新头文件(*.h),或者更新你的VC++ 6.0版本后,或者只是改变类型转换错误,都可能会解决这些问题。经过一些修改,这个客户端/服务器的源代码在没有MFC的情况下也能使用。

7 注意点和解决规则

在你将此代码用于其它类型的程序时,有一些编程的陷阱和源代码有关,使用“多线程编程”可以避免。不确定的错误是那些随时发生的错误,并且通过执行相同的出错的任务的顺序这种方式很难降低这些不确定的错误。这类错误是存在的最严重的错误,一般情况下,它们出错是因为源代码设计执行的内核的出错上。当服务器运行多个IO工作线程时,为连接的客户端服务,假如编程人员没有考虑源代码的多线程环境,就可能会发生像违反权限这种不确定的错误。

解决规则 #1

像下面例子那样,绝不在使用上下文 “锁”之前锁定客户端的上下文(例如ClientContext)之前进行读/写。通知函数(像:Notify*(ClientContext *pContext))已经是“线程安全的”,你访问ClientContext的成员函数,而不考虑上下文的加锁和解锁。
//Do not do it in this way
 
// … 
 
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
// …
 
 
 
// Do it in this way. 
 
//….
 
pContext->m_ContextLock.Lock(); 
If(pContext->m_bSomeData) 
pContext->m_iSomeData=0; 
pContext->m_ContextLock.Unlock(); 
//…
当然,你要明白,当你锁定一个上下文时,其他的线程或GUI都将等待它。

解决规则 #2

要避免,或者“特别注意”使用那些有复杂的“上下文锁”,或在一个“上下文锁”中有其他类型的锁的代码。因为它们很容易导致“死锁”。(例如:A等待BB等待C,而C等待A  => 死锁)。
pContext-> m_ContextLock.Lock();
//… code code .. 
pContext2-> m_ContextLock.Lock(); 
// code code.. 
pContext2-> m_ContextLock.Unlock(); 
// code code.. 
pContext-> m_ContextLock.Unlock();
上面的代码可以导致一个死锁。

解决规则 #3

绝不要在通知函数(像Notify*(ClientContext *pContext))的外面访问一个客户端的上下文。假如你必须这样做,务必使用m_ContextMapLock.Lock();m_ContextMapLock.Unlock()对它进行封装。如下面代码所示:
ClientContext* pContext=NULL ; 
m_ContextMapLock.Lock(); 
pContext = FindClient(ClientID); 
// safe to access pContext, if it is not NULL
// and are Locked (Rule of thumbs#1:) 
//code .. code.. 
m_ContextMapLock.Unlock(); 
// Here pContext can suddenly disappear because of disconnect. 
// do not access pContext members here.
 
8 下一步的工作
下一步,代码会被更新,在时间顺序上会具有下面的特性:
1.         可以接受新的连接的AcceptEx(..)函数的应用将添加到源代码中,用来处理短时的连接爆炸(short lived connection bursts)和DOS***。
2.         源代码可以很容易的用于其它平台,像Win32, STL, WTL
 
 
说明:最近比较忙,各种事情应接不暇。终于弄完了,呵呵。
        源代码可以到网上下载,我分析了,很好,可以应用到我的项目中,嘿嘿。