关于服务器端通讯程序设计

IOCP The WSAENOBUFS error problem

http://www.codeproject.com/KB/IP/iocp_server_client.aspx

 

http://blog.pfan.cn/xman/45130.html
http://blog.pfan.cn/xman/45129.html

 

用空连接或IO负载很低的连接来测试并发性能根本没有意义
微软自己都承认,使用Winsock的网络应用,有效并发大于250左右操作系统性能就会开始急剧下降
所以才搞出了IOCP机制

IOCP承载10000以上长连接并保持20000次IO/秒,一般没啥问题

 

http://topic.csdn.net/u/20090916/10/07d0c9b5-2653-4af2-b5e3-19d2eb058502.html?58683

 

网络编程之完成端口 (CSDN上摘抄的)

http://hi.baidu.com/tonyfirst1/blog/item/70b887dd71f618315882dd30.html

 

3.3 小节

讲这么点就完了?你一定认为我介绍的东西并没有超过原书中的内容,实事上完成端口编程的精髓就是上面的代码和原书中的有关叙述。如果我再把他们完整的重复 一遍,那又有什么意思呢?根据我的经验,设计网络服务器的真正难点,不在于完成端口技术,所以我想利用小节把自己编程中的一些经验告诉大家。
首先是服务器的管理,一个服务器首先要分析它的设计目标是应对很多的连接还是很大的数据传送量。这样在设计工作者线程时就可以最大限度的提高性能。管理客 户端方面,我们可以将客户端的数据捆绑到Perhand-Data数据结构上,如果还有需要,可以建一个表来记录客户端的宏观情况。
在Ares引擎中,我将文件传送和大容量数据传送功能也封装进了服务器和客户端。我建议服务器和客户端都应该封装这些功能,尽管我们并不是做FTP服务 器,但是当客户端需要和服务器交换文件和大块数据时,你会发现这样做,灵活性和性能都能做得比用单纯的FTP协议来更好,所以在你的服务器和客户端可以传 送数据包以后,把他们都做进去吧。
为了服务器不被黑客攻击,或被BUG弄崩溃,我们还需要认真设计服务器的认证机制,以及密切注意程序中的溢出,一定要在每一个使用缓冲区的地方加上检查代 码。可以说并没有现成的办法来解决这个问题,不然就没有人研究网络安全了,所以我们要做的是尽量减少错误,即使出现错误也不会造成太大损失,在发现错误的 时候能够很快纠正同类错误。
还有就是对客户端情况的检测,比如客户端的正常和非正常断开连接。如果不注意这一点,就会造成服务器资源持续消耗而最终崩溃,因为我们的服务器不可能总是 重启,而是要持续的运行,越久越好。还有比如客户端断开连接后又尝试连接,但是在服务器看来这个客户“仍然在线“,这个时候我们不能单纯的拒绝客户端的连 接,也不能单纯的接收。
讲了几点服务器设计中的问题,他们只是众多问题中的一小部分,限于时间原因,在这个版本的文章中就说这么多。你一定会发现,其实网络编程最困难和有成就的 地方,并不是服务器用了什么模式等等,而是真正深入设计的时候碰到的众多问题。正是那些没有标准答案的问题,值得我们去研究和解决。

 

IOCP普及篇(一)[转]

http://6yy.net/article/progLog/72.htm

 

 

100分求完成端口发送数据的方法

http://topic.csdn.net/t/20041130/20/3603093.html

 

IOCP应用框架的设计

http://blog.csdn.net/robotom/archive/2009/02/23/3929582.aspx

 

 

IOCP编写心得

http://www.slenk.net/viewthread.php?tid=4175

 

 

IOCP的封装类[By Sodme]

http://doserver.net/post/iocp-sodme.php#entrymore

 

进程服务器模型和线程服务器模型

http://doserver.net/read.php/2022.htm

 

浅析Delphi实现IOCP后的优化

http://fxh7622.blog.51cto.com/63841/106035

 


DELPHI中完成端口(IOCP)的简单分析(3)

http://fxh7622.blog.51cto.com/63841/15578

 

 

fxh7622 的BLOG

http://fxh7622.blog.51cto.com/63841/p-1

 

小猪的网络编程+花猫的漫画世界

http://blog.csdn.net/PiggyXP/default.aspx?PageNumber=2

 

牧野的Blog

http://www.cnblogs.com/wzd24/archive/2007/12/24/1011932.html

 

 

http://blog.sina.com.cn/s/blog_484102dd0100bf19.html

 

关于socket的粘包 (2008-11-22 08:53:07)

  以前也被这个困扰,折腾了很久。网上讨论的也巨火,仿佛对socket的性能颇加质疑,总觉得不方便。有用各种方法延时的,有一收一发的,还有检测发送结果的,反正貌似就要跟这个粘包的socket干上了...

  我以前着这么试过,能解决问题,但换来的是性能成倍的下降,现在总算是想通了,我讲讲我的理解。

  首先要承认,TCP的特性就是流传输,所以粘包不是一个不能理解和接受的事实,自动的分片和连续的传输反而是为了提高性能而设计的,这个要承认。要处理粘包的情况,八成是为了接收有结构的数据吧,要么怕收到半个,要么就是1个多,反正是对不上自己的结构,所以会出错。

  其实想想,人家连在一起了,你就真的没法处理了吗?我的做法是,你在内存里再开一个缓冲区,把收到的数据不管是半个还是1个半,都按顺序拼到缓冲区里,然后你从缓冲区里找出一个一个完成的结构,这不就完了。

这样socket就不用再延时了,可以一直发,性能是有保证的,而我们也省心些,就可以把注意力从让人头疼的网络,转移到对自己内存数据的处理上来了。

 

心跳包的接收处理

http://topic.csdn.net/t/20050916/13/4275031.html

 

 

 

请教一下IOCP和线程池搭配使用

http://topic.csdn.net/u/20070425/11/b75cfbee-3f6b-4163-9a77-d61a6d3d4aa8.html

 

 

IOCP在有客户端连到服务器的时候,怎么检索到有哦几个客户端连接到iocp的呢?

http://topic.csdn.net/u/20090223/17/71d5ec7e-906b-49b2-a377-6f1b495a0b88.html

 

 

使用了IOCP的ECHO程序的疑惑

http://topic.csdn.net/t/20040727/11/3213842.html

 

理解I/O完成端口模型 (感谢 PiggyXP 和 nonocast)

http://topic.csdn.net/t/20040512/08/3056877.html

 

完成端口的构架图(tcp和udp的讨论)

http://topic.csdn.net/t/20040507/10/3037970.html

 

 

A simple IOCP Server/Client Class

http://blog.csdn.net/dkink/archive/2009/03/02/3949615.aspx

原作者:spinoza

原文链接:http://www.codeproject.com/KB/IP/iocp_server_client.aspx

翻译:DKink|棼紫

 

[译]IOCP服务器/客户端实现 (转)

—— 译: Ocean    Email: [email protected]

http://hi.baidu.com/songwentao/blog/item/b6d41a7b514f65fe0ad187f7.html

 

This source code uses the advanced IOCP technology which can efficiently serve multiple clients. It also presents some solutions to practical problems that arise with the IOCP programming API, and provides a simple echo client/server with file transfer.

1.1要求

本文希望读者对C++,TCP/IP ,Socket编程,MFC以及多线程比较熟悉

源代码使用Winsock2.0 以及IOCP技术,因此需要:

  •  
    • Windows NT/2000 or later: Requires Windows NT 3.5 or later.
    • Windows 95/98/ME: Not supported.
    • Visual C++ .NET, or a fully updated Visual C++ 6.0.
1.2 摘要

当你开发不同类型的软件时,你总会需要进行C/S的开发。 完成一个完善的C/S代码对于编码人员来说是一件困难的事情。 本文给出了一个简单的但是却是却十分强大的C/S源代码,他可以扩展成任何类型的C/S程序。 源代码使用了IOCP技术,该技术可以有效地处理多客户端。 IOCP 对于“一个客户端一个线程”所有面临的瓶颈(或者其他)问题提出了一种有效的解决方案,他只使用少量的执行线程以及异步的输入输出、接受发送。IOCP计 数被广泛的用于各种高性能的服务器,如Apache等。 源代码同时也提供了一组用于处理通信的常用功能以及在C/S软件中经常用到功能,如文件接受/传输功能以及逻辑线程池操作。本文将主要关注一种围绕 IOCP API在实际中的解决方案,以及呈现源代码的完整文档。 随后,我将展示一个可以处理多连接和文件传输的echo C/S程序。

2.1 介绍

本文阐述了一个类,他可以被同时用于客户端和服务器端代码。 这个类使用IOCP(Input Output Completion Ports) 以及异步(non-blocking) 功能调用。 源代码是基于很多其他源代码和文章的。

使用此源代码,你可以:

- 为多主机进行链接、或者链接到多主机的客户端和服务器

- 异步的发送和接受文件

- 创建和管理一个逻辑工作线程池,他可以处理繁重的C/S请求或计算

找到一段完善的却又简单的、可以处理C/S通信的代码是一件困难的事情。 在网络上找到的代码要么太过于复杂(可能多于20个类),或者不能提供有效的效率。 本代码就是以简单为设计理念的,文档也尽可能的完善。 在本文中,我们可以很简单的使用由Winsock 2.0提供的IOCP技术,我也会谈到一些在编码时会遇到的棘手的问题以及他们的解决方法。

2.2 异步输入输出完成端口(IOCP)的介绍

一个服务器程序要是不能同时处理多客户端,那么我们可以说这个程序是毫无意义的,而我们为此一般会使用异步I/O调用或者多线程技术去实现。 从定义上来看,一个异步I/O 调用可以及时返回而让I/O挂起。 在同一时间点上,I/O异步调用必须与主线程进行同步。 这可以使用各种方式,同步主要可以通过以下实现:

- 使用事件(events) 只要异步调用完成,一个Signal就会被Set。 这种方式主要的缺点是线程必须去检查或者等待这个event 被Set

- 使用GetOverlappedResult 功能。 这种方式和上面的方式有同样的缺点。

- 使用异步例程调用(APC)。 对于这种方式 有几个缺点。 首先,APC总是在调用线程的上下文被调用的,其次,为了执行APCs,调用线程必须被挂起,这被成为alterable wait state

- 使用IOCP。 这种方式的缺点是很多棘手的编码问题必须得以解决。 编写IOCP可能一件让人持续痛苦的事情。

2.2.1 为什么使用IOCP?

使用IOCP,我们可以克服”一个客户端一个线程”的问题。 我们知道,这样做的话,如果软件不是运行在一个多核及其上性能就会急剧下降。 线程是系统资源,他们既不是无限制的、也不是代价低廉的。

IOCP提供了一种只使用一些(I/O worker)线程去“相对公平地”完成多客户端的”输入输出”。线程会一直被挂起,而不会使用CPU时间片,直到有事情做为止。

2.3 什么是IOCP?

我们已经提到IOCP 只不过是一个线程同步对象,和信号量(semaphore)相似,因此IOCP 并不是一个复杂的概念。 一个IOCP 对象是与多个I/O对象关联的,这些对象支持挂起异步IO调用。 知道一个挂起的异步IO调用结束为止,一个访问IOCP的线程都有可能被挂起。

3. IOCP是如何工作的?

要获得更多的信息,我推荐其他的一些文章(译者注,在CodeProject)

当使用IOCP时,你必须处理三件事情:将一个Socket关联到完成端口, 创建一个异步I/O调用,与线程进行同步。 为了获得异步IO调用的结果,比如,那个客户端执行了调用,你必须传入两个参数:the CompletionKey 参数, 和 OVERLAPPED 结构。

3.1 CompletionKey参数

第一个参数是CompletionKey,一个DWORD类型值。 你可以任何你希望的标识值,这将会和对象关联。 一般的,一个包含一些客户端特定对象的结构体或者对象的指针可以使用此参数传入。 在源代码中,一个指向ClientContext结构被传到CompletionKey参数中。

3.2 OVERLAPPED 参数

这个参数一般用于传入被异步IO调用使用的内存buffer。 我们必须主意这个数据必须是锁在内存的,不能被换页出物理内存。 我们稍后会讨论这个。

3.3 将socket与完成端口绑定

一旦一个完成端口创建,我们就可以使用CreateToCompletionPort方法去将socket绑定到完成端口,这看起来像这面这样:

BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket,

HANDLE hCompletionPort, DWORD dwCompletionKey)

{

HANDLE h = CreateIoCompletionPort((HANDLE) socket,

hCompletionPort, dwCompletionKey, m_nIOWorkers);

return h == hCompletionPort;

}

3.4 创建异步IO调用

创建真正的异步调用:可以调用WSASend,WSARecv。 他们也需要一个WSABUF参数,这个参数包含一个指向被使用的buffer的指针。首要规则是,当服务器/客户端试图调用一个IO操作时,他们不是直接 的操作,而是先被传送到完成端口,这将会被IO工作线程去完成操作。 之所以要这样做,我们是想要CPU 调用更加公平。 IO调用可以通过发送(Post)状态到完成端口来实现,看一下代码:

BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,

pOverlapBuff->GetUsed(),

(DWORD) pContext, &pOverlapBuff->m_ol);

3.5 与线程同步

与IO工作线程同步是通过GetQueuedCompletionStatus方法完成的(代码如下)。该方法也提供了CompleteKey参数以及OVERLAPPED 参数

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, // handle to completion port

LPDWORD lpNumberOfBytes, // bytes transferred

PULONG_PTR lpCompletionKey, // file completion key

LPOVERLAPPED *lpOverlapped, // buffer

DWORD dwMilliseconds // optional timeout value

);

3.6 IOCP编码四个棘手的问题以及他们的解决方法

使用IOCP时我们会遇到一些问题,这其中的有一些是不那么直观的。 在使用IOCP的多线程场合中, 线程的控制并不是很直观,因为通信与线程之间是没有关系的。 在本节中,我们将展示四个使用IOCP在开发C/S程序时不同的问题。 他们是:

- WSAENOBUGS 错误问题

- 数据包重排序问题

- 非法访问问题

3.6.1 WSAENOBUGS 错误问题

这个问题不是那么直观并且也很难发现,因为第一感觉是,他看起来像是一个平常的死锁或者内存泄露bug。 假如你开发了你的服务器,他也能工作的很好。 当你对他进行压力测试时, 他突然挂起了。 如果你比较幸运,你可以发现他是与WSAENOBUGS 错误相关的。

每次重叠发送或者接受操作时,被提交的数据buffer都是有可能被锁住的。 当内存锁住时,他就不能被换页到物理内存外。 一个操作系统限制了可以被锁住的内存大小。 当超出了限制时,重叠操作就会因WSAENOBUGS 错误失败。

如果一个服务器在每个连接上进行了许多Overlapped接收,随着连接数量的增加,我们就可能达到这个限制。 如果一个服务器希望处理非常大的突发用户,服务器POST可以从每个链接上接收到0字节的数据,因为已经没有buffer 与接收操作关联了,没有内存需要被锁住了。使用这种方式,每个socket的接收buffer应该被保持完整 因为一旦0字节的接收操作完成,服务器可以简单的进行非阻塞的接收去获取socket 接收buffer中的所有缓存数据。 当非阻塞因为WSAWOULDBLOCK错误失败时,这里就不再会有被挂起的数据了。 这种设计可以用于那种需要最大可能的处理突发访问链接,这是以牺牲吞吐量作为代价的。当然,你对客户端如何与服务器端进行交互知道的越多越好。 在前一个例子中,一个非阻塞的Receive将会在0字节接收完成后马上进行以便去取得缓存的数据。如果服务器知道客户端突然发送了很多数据,那么在接收 0字节数据的Receive完成后,他应该POST一个或者多个Overlapped Reveives以便接收客户端发送的一些数据(大于每个socket接收buffer的最大缓冲buffer,默认是8k)。

一个针对WSAENOBUFFERS错误问题的简单而实际的解决方式在源代码中已经提供了。 我们进行一个使用0字节Buffer的异步WSARead(…)(请查看OnZeroByteRead(…))。 当这个调用完成后,我们知道在TCP/IP栈中存在数据,然后我们使用大小为MAXIMUMPACKAGE buffer进行几个异步的WSARead。 这种解决方法只是在有数据来到时才锁住物理内存,这样可以解决WSAENOBUFS问题。 但是这种解决方式会降低服务器的吞吐量。

3.6.2 数据包重排序问题

这个问题也在参考文献【3】中提到。 虽然使用IO完成端口的提交操作总是按照他们被提交的顺序完成,线程调度问题可能会导致与完成端口绑定的真正任务是以未知的顺序完成的。 例如,如果你有两个IO工作线程, 然后你应该接收到“byte chunk 1, byte chunk 2, byte chunk 3”,你可能会以错误的顺序去处理byte chunk,如“byte chunk 2, byte chunk 1, byte chunk 3”。 这也就是意味着当你POST一个发送请求到IO完成端口进行发送数据时,数据可能会被以另外的顺序进行发送。

这可以通过只使用单个工作线程来解决, 只提交一个IO调用,直到他完成, 但是如果我们这样做的话,我们将会失去IOCP所有的好处。

一个实际的解决方式是添加一个顺序号给我们的buffer类, 只处理buffer中的顺序号正确的buffer数据。 这意味着,buffer如果有不正确的号码必须保存起来以便之后用到, 因为性能的原因,我们将会将buffers保存到一个hash map对象中(如 m_SendBufferMap and m_ReadBufferMap)

要获得这种解决方式更多的信息,请阅读源代码,然后在IOCPS类中查看下面的函数:

  • GetNextSendBuffer (..) and GetNextReadBuffer(..), to get the ordered send or receive buffer.
  • IncreaseReadSequenceNumber(..) and IncreaseSendSequenceNumber(..), to increase the sequence numbers.
3.6.3 异步挂起读以及byte chunk 数据包处理问题

最常用的服务器协议是基于包的协议,该协议中前X字节表示头部, 头部包含了一个完整的包的长度。服务器可以读取头部,查看多少数据需要的,然后继续读取数据直到读完一个包。 这在服务器在一个时间上只进行一个异步调用时可以工作的很好。但是如果我们想挖掘IOCP服务器的所有潜力,我们需要有多个异步Reads 去等待数据的到来。 这意味这几个异步Reads 是完全乱序的(如前所述), byte chunk 流被挂起的reads操作返回来将不再是顺序的了(译者注,实际上这几个Reads操作是资源竞争的,同时读取数据,返回时的顺序不定)。 并且,一个byte chunk 流可以包含一个或者多个数据包,或者半包。 如下图所示:

图1 该图展示了部分包(绿色)以及完整包(黄色)可能会在不同的byte chunk流中异步到达

这意味着我们不得对byte chunk进行处理以便获得完整的数据包。 进一步,我们不得不处理部分包(图中绿色)。 这会使得包的处理变得更加麻烦。 该问题的完整解决方法可以在IOCPS类的ProcessPackage方法中找到。

3.6.4 非法访问问题

这是一个小问题,一般是由于代码的设计导致的, 而不是IOCP特定的问题。 假如一个客户端链接丢失了,而一个IO调用返回了一个错误flag,随后我们知道客户端不存在了。 在CompleteKey参数中,我们将一个DWORD类型指针转型为ClientContext指针,接下来去访问或者删除他? 访问异常就是这么发生的!

这个问题的解决方式是为包含挂起IO调用的结构体添加一个数字(nNumberOfPendlingIO),然后当我们知道这将不会有挂起的IO调 用时才去删除结构体。 这是在方法EnterIoLoop(..) function 和 ReleaseClientContext(..).完成的

3.7 源代码架构

整个源代码是提供一些简单的类去处理在IOCP中需要面对的棘手的问题。 源代码也提供了一些方法,这些方法经常被通信或者软件中用到的文件接收传输、逻辑线程池处理等

图2:上面的图片展示了IOCP类源代码功能

我们拥有几个工作线程去处理来自IOCP的异步IO调用, 这些工作线程调用某些虚方法去把需要大量计算的请求放入到工作队列中。 逻辑工作线程从队列中取得这些任务,处理、然后把结果通过类提供的一些方法发送回去。 GUI通常是通过Windows消息与主class通信的(MFC不是线程安全的),然后调用方法或者使用共享变量。

图3: 上图展示了类的框架

我们在图3中可以看到以下的类:

- CIOCPBuffer: 一个用于管理被异步IO调用使用的buffers

- IOCPS: 用于通信的主要类。

- JobItem:一个包含需要被逻辑工作线程执行的任务的结构。

- ClientContext : 一个包含了客户端特定信息(状态、数据等等)的结构

3.7.1 Buffer设计 —— CIOCPBuffer 类

当时用异步IO调用时,我们必须提供一个私有的buffer去被IO操作使用。 当我们分配buffers时,需要考虑几个问题:

- 分配与释放内存时很昂贵的, 所以我们应该重用已经分配的buffers(内存)。 所以,我们可以使用链表结构去节省buffers:

// Free Buffer List..

CCriticalSection m_FreeBufferListLock;

CPtrList m_FreeBufferList;

// OccupiedBuffer List.. (Buffers that is currently used)

CCriticalSection m_BufferListLock;

CPtrList m_BufferList;

// Now we use the function AllocateBuffer(..)

// to allocate memory or reuse a buffer.

- 某些时候,当一个IO调用完成时,我们可能会在buffer中得到部分的包数据,所以我们需要将buffer分割以便得到完整的消息。 这是通过IOCPS类中的SpiltBuffer方法来实现的。 同时,有时候我们需要在buffer之间拷贝信息,而这是通过IOCPS类中的AddAndFlush()来完成的。

- 我们知道,我们同时需要为我们的buffer添加一个序列号以及一个状态(IOType变量,IOZeroReadCompleted 等等)

- 我们同时还需要一些方法去把byte数据流转换成数据,有些方法也在CIOCPBuffer类中被提供了

我们先前提到的所有问题的解决方案都已经在CIOCPBuffer类中得到支持了。

3.8 如何使用本源代码?

通过从IOCP(见图3)派生你自己的类以及使用虚方法、使用IOCPS类提供的功能(如线程池),这就可以实现任何类型的服务器和客户端,我们可以使用有限数量的线程来有效的应对大量的连接。

3.8.1服务器/客户端的启动和关闭

要启动服务器,调用以下的方法:

BOOL Start(int nPort=999,int iMaxNumConnections=1201,

int iMaxIOWorkers=1,int nOfWorkers=1,

int iMaxNumberOfFreeBuffer=0,

int iMaxNumberOfFreeContext=0,

BOOL bOrderedSend=TRUE,

BOOL bOrderedRead=TRUE,

int iNumberOfPendlingReads=4);

  • nPortt

服务器将进行监听的端口号(如果是客户端的话,我们可以让他是-1)

  • iMaxNumConnections

最大可允许的连接数(使用一个很大的数字)

  • iMaxIOWorkers

输入输出工作线程的数量

  • nOfWorkers

逻辑工作线程的数量

  • iMaxNumberOfFreeBuffer

我们将要节省下来进行重用的buffer最大数量(-1表示不重用,0表示不限制数量)

  • iMaxNumberOfFreeContext

我们将要节省下来进行重用的客户端信息对象的最大数量(-1表示不重用,0表示不限制数量)

  • bOrderedRead

是否需要有序读取(我们已经在3.6.2中讨论过这个)

  • bOrderedSend

是够需要有序的写入(我们已经在3.6.2中讨论过这个)

  • iNumberOfPendlingReads

等待数据而挂起的异步读取循环的数量

建立一个远程连接(客户端模式下nPort = -1) 调用下面的方法:

Connect(const CString &strIPAddr, int nPort)

  • strIPAddr

远程服务器的IP地址

  • nPort

端口

关闭时请确认服务器调用以下的方法:ShutDown() .

For example:

MyIOCP m_iocp;
if(!m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("Error could not start the Client");
….
m_iocp.ShutDown();
4.1 代码描述

更多的代码细节,请阅读源代码中的注释。

4.1.1 虚方法
  • NotifyNewConnection

当新的连接被建立时被调用

  • NotifyNewClientContext

当一个空的ClientContext结构被分配时被调用

  • NotifyDisconnectedClient

当一个客户端断线时被调用

  • ProcessJob

当一个工作线程试图执行一个任务时调用

  • NotifyReceivedPackage

当一个新的包到达时的提示

  • NotifyFileCompleted

当一个文件传输完成时的提示

4.1.2 重要的变量

请注意,所有需要使用共享变量的方法将对进行额外的加锁,这对于避免非法访问和重叠写入非常重要的。所有需要加锁且使用XXX名字的变量需要加锁,他们有一个XXXLock变量

  • m_ContextMapLock;

维护所有的客户端数据(Socket,客户端数据等等)

  • ContextMap m_ContextMap;
  • m_NumberOfActiveConnections

维护已有的连接数量

4.1.2 重要的方法

  • GetNumberOfConnections()

返回连接数

  • CString GetHostAdress(ClientContext* p)

返回给定客户端Context的主机地址

  • BOOL ASendToAll(CIOCPBuffer *pBuff);

发送buffer中的内容给所有的客户端。

  • DisconnectClient(CString sID)

与一个给定唯一标示号的客户端断开连接

  • CString GetHostIP()

返回本地IP地址

  • JobItem* GetJob()

从队列中移除JobItem,如果没有任务的话将会返回NULL

  • BOOL AddJob(JobItem *pJob)

添加任务到队列中

  • BOOL SetWorkers(int nThreads)

设置在任何时刻能被调用的逻辑工作线程数量

  • DisconnectAll();

断开所有的客户端

  • ARead(…)

创建一个异步读取

  • ASend(…)

创建一个异步发送。 发送数据到客户端

  • ClientContext* FindClient(CString strClient)

根据给定的字符串ID查找客户端。 不是线程安全的

  • DisconnectClient(ClientContext* pContext, BOOL bGraceful=FALSE);

断开一个客户端

  • DisconnectAll()

断开所有已有的连接

  • StartSendFile(ClientContext *pContext)

transmitfile(..) function.

发送ClientContext结构中声明的文件,通过使用优化的transmitfile(…)方法

  • PrepareReceiveFile(..)

准备一个接收文件的连接,当你调用这个方法时,所有接收的字节将写入到一个文件

  • PrepareSendFile(..)

打开一个文件以及发送一个包含文件信息的包到远程连接。 这个方法也会把Asread(…)禁用掉,直到文件被传送完成或者终止。

  • DisableSendFile(..)

禁用文件发送模式

  • DisableRecevideFile(..)

禁用接收模式

5.1 文件传输

文件传输使用过Winsock 2.0的TransmitFile方法完成的。 TransmitFile方法使用一个已经连接的socket句柄进行传输文件数据。这个方法使用操作系统的缓存管理器(cache manager)来接收文件数据,他提供了基于socket的高性能文件数据传输。 当我们使用异步文件传输时,这里几个需要注意的地方:

- 除非TransmitFile方法返回,不能在该socket上进行读取和写入,因为这样会损坏文件。因此,所有在PrepareSendFile()之后对ASend的调用都会禁用掉。

- 因为操作系统是有序读取文件数据的,你可以通过使用FILE_FLAG_SEQUENTIAL_SCAN去打开文件句柄来提高缓存的性能。

- 当发送文件时,我们使用内核的异步例程调用(TF_USE_KERNEL_APC). 使用TF_USE_KERNEL_APC可以获得很好的性能。 当我们在Context TransmitFile初始化时使用的线程使用非常繁重的计算任务时,这有可能会阻止APCs的执行。

文件传输是以下面的顺序运作的:服务器通过调用PrepareSendFile(…)初始化文件传输。当客户端接收到文件的信息时,他会调用 PrepareReceiveFile(...) ,然后发送一个包给服务器去开始文件传输。 当一个包到达服务器时,服务器调用StartSendFile(…)方法,这个犯非法使用了高性能的TransmitFile(…)方法去传输特定的文 件。

6 源代码例子

提供的源代码例子时一个echo 客户端/服务器应用程序,他可以支持文件传输(见图4)。 在源代码中,类MyIOCP继承自IOCP, 他通过在4.1.1节中提到的使用虚方法处理客户端和服务器端的交互。

客户端或者服务器端最重要的部分是NotifyReceivePackage, 如下所示:

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;};

}

该方法处理接收到的消息以及执行由远程连接发送的请求。 在本例中,他只是简单的echo或者文件传输而已。 源代码被非常两个项目,IOCP 和 IOCPClient, 一个是服务器端的连接而另外一个时客户端的连接。

6.1 编译上的问题

当使用VC++ 6.0 或者 .NET时,你可能会在使用CFile时得到一些奇怪的错误,如:

“if (pContext->m_File.m_hFile !=

INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "

"from 'void *' to 'unsigned int'”

这个问题可以通过更新头文件或者你的VC++6.0 版本来避免,或者改一下类型转换错误。 在一些修改之后,服务器/客户端代码是可以在没有MFC时使用的。

7. 特殊的考虑以及首要原则

当你在其他类型的应用程序中使用这个代码时,你可能会遇到一些跟本代码相关的陷阱以及“多线程编程”的陷阱。非确定性的错误时那些随机发生的错误, 通过相同的一系列操作是很难重现这些非确定性的错误的。 这些错误是已存在的错误中最糟糕的类型,同时通常他们时因为在核心设计的编码实现时产生的。 当服务器执行多个IO工作线程时,为连接的客户端服务,如果程序员没有很好的搞清楚多线程环境下的编码问题,非确定性错误如非法访问可能就会发生。

原则1

在没有使用context lock(如下面的例子)对客户端Context(如ClientContext)进行加锁时,不要读取/写入。提示(Notification)方法 (如 nofity* (ClientContext * pContext))已经是“线程安全”的了,你可以在不对context进行加锁的情况下访问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();
//…

同时,记住当你加锁一个Context时,其他的线程或者GUI可能会进行等待。

原则2:

避免或者在”context lock”中具有复杂的“context locks”或者其他类型锁的代码中 “特殊考虑”,因为这有可能会引起死锁(比如,A等待B,而B等待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:

不要在Notification方法外访问Client Context(比如,Notify*(ClientContext * pContext)). 如果你你要这么做,你必须使用m_ContextMapLock.Lock();…m_ContstMapLock.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()方法,接收一个新的连接将会被添加到源代码,该方法去处理短连接以及DOS攻击。

2.源代码将会被移植到其他平台,如Win32,STL,WTL

9 FAQ

Q1: 内存使用量(服务器程序)将会在客户端连接增加的时候稳定的增加,这可以从任务管理器看到。然而即使客户端断线时,内存使用量还是没有下降,这是怎么回事?

A1:代码会重用已经分配的buffers而不是不断释放和分配。 你可以通过改变参数iMaxNumberOfFreeBuffer 和 iMaxNumberOfFreeContext来改变这种方式,请阅读3.8.1节。

Q2:我在.NET环境下编译时遇到以下的错误:“error C2446:’!=’ no conversion from 'unsigned int' to 'HANDLE'”等等,这是怎么回事?

A2:这是因为SDK不同的头文件造成的。只要把他转换成HANDLE 编译器就可以让你通过了。 你也可以这是删除一行代码 #define TRANSFERFILEFUNCTIONALITY 然后再编译一下。

Q3:源代码可以在没有MFC的情况下使用吗? 纯Win32或者在一个服务里面?

A3:源代码只是暂时使用了GUI开发的。 我开发这个客户端/服务器解决方案时使用了MFC环境作为GUI。 当然,你可以在一个通常的服务器环境下使用他。很多人已经这么做了。只要把MFC相关的东西,如CString,CPtrList 等等移走,用Win32的类去替换。 我其实也不喜欢MFC,如果你改变的代码,请发一份给我,谢谢。

Q4:做得太好了! 谢谢你所做的工作, 你会在什么时候不是在监听线程中实现AcceptEX(…)?

A4:当代码稳定后。 现在他已经很稳定了,但是我知道一些IO工作线程和挂起的读操作 的整合可能会导致一些问题。 我很高兴你喜欢我的代码,请投我一票!

Q5:为什么启动多个IO工作线程? 如果你没有多线程机器的话就没有必要了?

A5:不,没有必要开启多个IO工作线程。 只要一个线程就能处理所有的连接。 一般的家庭计算机中,一个工作线程就可以有最佳的表现。你也不需要考虑潜在的非法访问。但是当计算机变得越来越强大的时候(比如超线程,双核等等) 多线程的可能性为什么不会有?

Q6:为什么使用多个挂起的读操作?他有什么优势?

A6:这取决都与开发者进行服务器开发采取的策略,也就是说“许多并发连接”还是 “高吞吐量服务器”。 拥有多个挂起的读操作增加了服务器的吞吐量,这是因为TCP/IP包将会被直接写到我们传入的buffer而不是TCP/IP栈(不会有双缓冲)。 如果服务器知道客户端突然发送了大量数据,多个挂起的读操作可以提高性能(高吞吐量)。然而,每个挂起的接收操作(使用WSARevc())会强迫内核去 锁住接收buffers进入非换页池。 这在物理内存满时(很多并发连接时产生)就会引起WSAENBUFFERS错误。这必须被考虑进去。 再者,如果你使用多于一个IO工作线程,访问包的顺序就会被打乱(因为IOCP的结构),这样就需要额外的工作去维护顺序 以便不用多个挂起的读操作。在这个设计中,当IO工作线程的数量大于1个时,多个挂起的读操作是被关闭的,这样就可以不需要处理重排序(重排序的话,序列 号是必须在负载中存在的)。

 

Q7:在先前的文章中,你提到我们使用VirtualAlloc方法而不是new实现内存管理,为什么你没有实现呢?

A7:当你使用new 来分配内存时,内存会被分配在虚拟内存或者物理内存。到底内存被分配到什么地方时不知道的,内存可以被分配到两个页面上。 这意味着当我们访问一个特定的数据时,我们加载了太多的内存到物理内存。 再者,你不知道内存是在虚拟内存还是物理内存,你也不能够高数系统什么时候“写回到磁盘中是不需要的(如果我们在内存中已经不再关心该数据)。但是请注 意!任何使用VirtualAlloc* 的new分配都将会填满到64kB(页面文件大小) 所以你如果你分配一个新的VAS绑定到物理内存,操作系统将会消耗一定量的物理内存去达到页面大小,这将会消耗VAS去执行填满到64kB。使用 VirtualAlloc会比较麻烦: new 和 malloc 在内部使用了 virtualAlloc,但是每次你使用new/delete 分配内存时,很多其他的计算就会被完成,而你不需要控制你的数据(彼此关联的数据)刚好在相同的页面(而不是跨越了两个页面)。 我发现相对于代码的复杂度来说,我能获得的性能提高的非常小的。

 

翻译:高庆余

http://820808.blog.51cto.com/328558/66664

http://820808.blog.51cto.com/328558/68200

前言:源代码使用比较高级的 IOCP 技术,它能够有效的为多个客户端服务,利用 IOCP 编程 API ,它也提供了一些实际问题的解决办法,并且提供了一个简单的带回复的文件传输的客户端 / 服务器。
 
1.1  要求:
l         文章要求读者熟悉 C++, TCP/IP, 套接字 (socket) 编程 , MFC, 和多线程。
l         源代码使用 Winsock 2.0 IOCP 技术,并且要求:
Ø         Windows NT/2000 or later: Requires Windows NT 3.5 or later.
Ø         Windows 95/98/ME: 不支持
Ø         Visual C++ .NET, or a fully updated Visual C++ 6.0.
1.2  摘要:
在你开发不同类型的软件,不久之后或者更晚,你必须得面对客户端 / 服务器端的发展。对程序员来说,写一个全面的客户端 / 服务器的代码是很困难的。这篇文章提供了一个简单的,但却强大的客户端 / 服务器源代码,它能够被扩展到许多客户端 / 服务器的应用程序中。源代码使用高级的 IOCP 技术,这种技术能高效的为多个客户端提供服务。 IOCP 技术提供了一种对 一个线程—一个客户端( one-thread-one client )这种瓶颈问题(很多中问题的一个)的有效解决方案。它使用很少的一直运行的线程和异步输入 / 输出,发送 / 接收。 IOCP 技术被广泛应用于各自高性能的服务器,像 Apache 等。源代码也提供了一系列的函数,在处理通信、客户端 / 服务器接收 / 发送文件函数、还有线程池处理等方面都会经常用到。文章主要关注利用 IOCP 应用 API 函数的实际解决方案,也提供了一个全面的代码文档。此外,也为你呈现了一个能处理多个连接、同时能够进行文件传输的简单回复客户端 / 服务器。
2.1.     介绍:
这片文章提供了一个类,它是一个应用于客户端和服务器的源代码,这个类使用 IOCP 和异步函数,我们稍后会进行介绍。这个源代码是根据很多代码和文章得到的。
利用这些简单的源代码,你能够:
l          服务 / 连接多个客户端和服务器。
l          异步发送和接收文件。
l          为了处理沉重的客户端 / 服务器请求,创建并管理一个逻辑工作者线程池。( logical worker thread pool )。
我们很难找到充分的,但简单的能够应对客户端 / 服务器通信的源代码。在网上发现的源代码即复杂(超过 20 个类),又不能提供足够的功能。本问的代码尽量简单,也有好的文档。我们将简要介绍 Winsock API 2.0 提供的 IOCP 技术,编码时遇到的疑难问题,以及这些问题的应对方案。
2.2.       异步输入 / 输出完成端口( IOCP )简介
一个服务器应用程序,假如不能够同时为多个客户端提供服务,那它就没有什么意义。通常的异步 I/O 调用,还有多线程都是这个目的。准确的说,一个异步 I/O 调用能够立即返回,尽管有阻塞的 I/O 调用。同时, I/O 异步调用的结果必须和主线程同步。这可以用很多种方法实现,同步可以通过下面方法实现:
l          利用事件——当异步调用完成时设定的信号。这种方法的优点是线程必须检查和等待这个信号被设定。
l          使用 GetOverlappedResult 函数——这个方法和上面方法有相同的优点。
l          使用异步程序调用( APC )——这种方法有些缺点。第一, APC 总是在正被调用的线程的上下文中被调用;第二,调用线程必须暂停,等待状态的改变。
l          使用 IOCP ——这种方法的缺点是有些疑难问题必须解决。使用 IOCP 编码多少有些挑战。
2.2.1        为什么使用 IOCP
使用 IOCP ,我们能够克服 一个线程 —— 一个客户端 问题。我们知道,假如软件不是运行在一个真实的多处理器机器上,它的性能会严重下降。线程是系统的资源,它们即不是无限的,也不便宜。
IOCP 提供了一种利用有限的( I/O 工作线程)公平的处理多客户端的输入 / 输出问题的解决办法。线程并不被阻塞,在无事可作的情况下也不使 CPU 循环。
2.3.       什么是 IOCP
我们已经知道, IOCP 仅仅是一个线程同步对象,有点像信号量( semaphore ),因此 IOCP 并不是一个难懂的概念。一个 IOCP 对象和很多支持异步 I/O 调用的 I/O 对象相联系。线程有权阻塞 IOCP 对象,直到异步 I/O 调用完成。
3              IOCP 如何工作
为了得到更多信息,建议你参考其它的文章( 1, 2, 3, see References )。
使用 IOCP ,你必须处理 3 件事情。将一个套接字绑定到一个完成端口,使用异步 I/O 调用,和使线程同步。为了从异步 I/O 调用得到结果,并知道一些事情,像哪个客户端进行的调用,我们必须传递两个参数: CompletionKey 参数 还有 OVERLAPPED 结构体
3.1.     CompletionKey 参数
CompletionKey 参数是第一个参数,是一个 DWORD 类型的变量。你可以给它传递你想要的任何值,这些值总是和这个参数联系。通常,指向结构体的指针,或者包含客户端指定对象的类的指针被传递给这个参数。在本文的源代码中,一个 ClientContext 结构体的指针被传递给 CompletionKey 参数。
3.2.     OVERLAPPED 参数
这个参数通常被用来传递被异步 I/O 调用的内存。要重点强调的是,这个数据要被加锁,并且不要超出物理内存页,我们之后进行讨论。
3.3.     将套接字和完成端口进行绑定
一旦创建了完成端口,通过调用 CreateIoCompletionPort 函数可以将一个套接字和完成端口进行绑定,像下面的方法:
 
BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket, 




               


HANDLE hCompletionPort, DWORD dwCompletionKey)




   


{




       


HANDLE h = CreateIoCompletionPort((HANDLE) socket,




             


hCompletionPort, dwCompletionKey, m_nIOWorkers);




       




return





h == hCompletionPort;



   


}




 
3.4.     进行异步 I/O 调用
通过调用 WSASend , WSARecv 函数,进行实际的异步调用。这些函数也需要包含将要被用到的内存指针的参数 WSABUF 。通常情况下,当服务器 / 客户端想要执行一个 I/O 调用操作,它们并不直接去做,而是发送到完成端口,这些操作被 I/O 工作线程执行。这是因为,要公平的分配 CPU 。通过给完成端口传递一个状态,进行 I/O 调用。象下面这样:
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 




                       


pOverlapBuff-


>





GetUsed(),



                       


(DWORD) pContext, &pOverlapBuff-


>





m_ol);



 
3.5.     线程的同步
通过调用 GetQueuedCompletionStatus 函数进行线程的同步(看下面)。这个函数也提供了 CompletionKey 参数 OVERLAPPED 参数。
BOOL GetQueuedCompletionStatus(




   


HANDLE CompletionPort,


// handle to completion port









 




   


LPDWORD lpNumberOfBytes,


// bytes transferred









 




   


PULONG_PTR lpCompletionKey,


// file completion key









 




   


LPOVERLAPPED *lpOverlapped,


// buffer









 




   


DWORD dwMilliseconds


// optional timeout value









 




   


);




3.6.     四个棘手的 IOCP 编码问题和它们的对策
使用 IOCP 会遇到一些问题,有些问题并不直观。在使用 IOCP 的多线程场景中,并不直接控制线程流,这是因为线程和通信之间并没有联系。在这部分,我们将提出四个不同的问题,在使用 IOCP 开发客户端 / 服务器程序时会遇到它们。它们是:
l       WSAENOBUFS 出错问题。
l         数据包的重排序问题。
l         访问紊乱( access violation )问题。
 
3.6.1   WSAENOBUFS 出错问题。
这个问题并不直观,并且很难检查。因为,乍一看,它很像普通的死锁,或者内存泄露。假设你已经弄好了你的服务器并且能够很好的运行。当你对服务器进行承受力测试的时候,它突然挂机了。如果你幸运,你会发现这和 WSAENOBUFS 出错有关。
伴随着每一次的重叠发送和接收操作,有数据的内存提交可能会被加锁。当内存被锁定时,它不能越过物理内存页。操作系统会强行为能够被锁定的内存的大小设定一个上限。当达到上限时,重叠操作将失败,并发送 WSAENOBUFS 错误。
假如一个服务器在在每个连接上提供了很多重叠接收,随着连接数量的增长,很快就会达到这个极限。如果服务器能够预计到要处理相当多的并发客户端的话,服务器可以在每个连接上仅仅回复一个 0 字节的接收。这是因为没有接收操作和内存无关,内存不需要被锁定。利用这个方法,每一个套接字的接收内存都应该被完整的保留,这是因为,一旦 0 字节的接收操作完成,服务器仅仅为套接字的接收内存的所以数据内存返回一个非阻塞的接收。 利用 WSAEWOULDBLOCK 当非阻塞接收失败时,也没有数据被阻塞。这种设计的目的是,在牺牲数据吞吐量的情况下,能够处理最大量的并发连接。当然,对于客户端如何和服务器交互,你知道的越多越好。在以前的例子中,每当 0 字节的接收完成,返回存储了的数据,马上执行非阻塞接收。假如服务器知道客户端突然发送数据,当 0 字节接收一旦完成,为防止客户端发送一定数量的数据(大于每个套接字默认的 8K 内存大小),它可以投递一个或多个重叠接收。
源代码提供了一个简单的解决 WSAENOBUFS 错误的可行方案。对于 0 字节内存,我们采用 WSARead () 函数 (见 OnZeroByteRead ())。 当调用完成,我们知道数据在 TCP/IP 栈中,通过采用几个异步 WSARead () 函数读取 MAXIMUMPACKAGESIZE 的内存。这个方法在数据达到时仅仅锁定物理内存,解决了 WSAENOBUFS 问题。但是这个方案降低了服务器的吞吐量(见第 9 部分的 Q6 A6 例子)。
3.6.2   数据包的重排序问题
在参考文献 3 中也讨论了这个问题。尽管使用 IOCP ,可以使数据按照它们被发送的顺序被可靠的处理,但是线程表的结果是实际工作线程的完成顺序是不确定的。例如,假如你有两个 I/O 工作线程,并且你应该接收“字节数据块 1 、字节数据块 2 、字节数据块 3 ,你可以按照错误的顺序处理它们,也就是“字节数据块 2 、字节数据块 1 、字节数据块 3 。这也意味着,当你通过把发送请求投递到 IO 完成端口来发送数据时,数据实际上是被重新排序后发送的。
这个问题的一个实际解决办法是,为我们的内存类增加顺序号,并按照顺序号处理内存。意思是,具有不正确号的内存被保存备用,并且因为性能原因,我们将内存保存在希哈表中(例如 m_SendBufferMap m_ReadBufferMap )。
要想得到更多这个方案的信息,请查看源代码,并在 IOCPS 类中查看下面的函数:
l         GetNextSendBuffer (..) GetNextReadBuffer(..) , 为了得到排序的发送或接收内存。
l          IncreaseReadSequenceNumber (..) IncreaseSendSequenceNumber(..) , 为了增加顺序号。
3.6.3   异步阻塞读和 字节块包处理问题
大多数服务器协议是一个包,这个包的基础是第一个 X 位的描述头,它包含了完整包的长度等详细信息。服务器可以解读这个头,可以算出还需要多少数据,并一直解读,直到得到一个完整的包。在一个时间段内,服务器通过异步读取调用是很好的。但是,假若我们想全部利用 IOCP 服务器的潜力,我们应该有很多的异步读操作等待数据的到达。意思是很多异步读无顺序完成(像在 3.6.2 讨论的),通过异步读操作无序的返回字节块流。还有,一个字节块流( byte chunk streams )能包含一个或多个包,或者包的一部分,如图 1 所示:
1
这个图表明部分包(绿色)和完整的包(黄色)在字节块流中是如何异步到达的。
这意味着我们要想成功解读一个完整包,必须处理字节流数据块( byte stream chunks 。还有,我们必须处理部分包,这使得字节块包的处理更加困难。完整的方案可以在 IOCP 类里的 ProcessPackage(..) 函数中找到。
3.6.4   访问紊乱( access violation )问题。
这是一个次要问题,是编码设计的结果,而不是 IOCP 的特有问题。倘若客户端连接丢失,并且一个 I/O 调用返回了一个错误标识,这样我们知道客户端已经不在了。在 CompletionKey 参数中,我们为它传递一个包含了客户端特定数据的结构体的指针。假如我们释放被 ClientContext 结构体占用的内存,被同一个客户端执行 I/O 调用所返回的错误码,我们为 ClientContext 指针传递双字节的 CompletionKey 变量,试图访问或删除 CompletionKey 参数,这些情况下会发生什么?一个访问紊乱发生了。
这个问题的解决办法是为 ClientContext 结构体增加一个阻塞 I/O 调用的计数 ( m_nNumberOfPendlingIO ) 当我们知道没有阻塞 I/O 调用时我们删除这个结构体。 EnterIoLoop(..) 函数和 ReleaseClientContext(..) . 函数就是这样做的。
3.7       源代码总揽
源代码的目标是提供一些能处理与 IOCP 有关的问题的代码。源代码也提供了一些函数,它们在处理通信、客户端 / 服务器接收 / 发送文件函数、还有线程池处理等方面会经常用到。
2 源代码 IOCPS 类函数总揽
我们有很多 I/O 工作线程,它们通过完成端口( IOCP )处理异步 I/O 调用,这些工作线程调用一些能把需要大量计算的请求放到一个工作队列着中的虚函数。逻辑工作线程从队列中渠道任务,进行处理,并通过使用一些类提供的函数将结果返回。图形用户界面( GUI )通常使用 Windows 消息,通过函数调用,或者使用共享的变量,和主要类进行通信。
3
3 显示了类的总揽。
3 中的类归纳如下:
l          CIOCPBuffer :管理被异步 I/O 调用使用的内存的类。
l          IOCPS :处理所有通信的主要类。
l          JobItem :包含被逻辑工作线程所执行工作的结构体。
l          ClientContext :保存客户端特定信息的结构体(例如:状态、数据 )。
 
3.7.1   内存设计—— CIOCPBuffer
当使用异步 I/O 调用时,我们必须为 I/O 操作提供一个私有内存空间。当我们分配内存时要考虑下面一些情况:
l          分配和释放内存是很费时间的,因此我们要反复利用分配好的内存。所以,我们像下面所示将内存保存在一个连接表中。
·                // Free Buffer List..
·                 
·                    CCriticalSection m_FreeBufferListLock;
·                    CPtrList m_FreeBufferList;
·                // OccupiedBuffer List.. (Buffers that is currently used)
·                 
·                    CCriticalSection m_BufferListLock;
·                    CPtrList m_BufferList;
·                // Now we use the function AllocateBuffer(..)
·                 
// to allocate memory or reuse a buffer.
l         有时,当一个异步 I/O 调用完成时,我们可能在内存中有部分包,因此我们为了得到一个完整的消息,需要分离内存。在 CIOCPS 类中的函数 SplitBuffer () 可以实现这一目标。我们有时也需要在两个内存间复制信息, CIOCPS 类中的 AddAndFlush () 函数可以实现。
l          我们知道,我们为我们的内存增加序列号和状态变量( IOZeroReadCompleted () )。
l          我们也需要字节流和数据相互转换的方法,在 CIOCPBuffer 类中提供了这些函数。
在我们的 CIOCPBuffer 类中,有上面所有问题的解决办法。
3.8       如何使用源代码
IOCP 中派生你自己的类,使用虚函数,使用 IOCPS 类提供的函数(例如:线程池)。使用线程池,通过使用少数的线程,为你为各种服务器或客户端高效的管理大量的连接提供了可能。
3.8.1   启动和关闭服务器 / 客户端
启动服务器,调用下面的函数:
BOOL Start(


int





nPort=999,

int





iMaxNumConnections=1201,



   




int





iMaxIOWorkers=1,

int





nOfWorkers=1,



   




int





iMaxNumberOfFreeBuffer=0,



   




int





iMaxNumberOfFreeContext=0,



   


BOOL bOrderedSend=TRUE,




   


BOOL bOrderedRead=TRUE,




   




int





iNumberOfPendlingReads=4);



l        nPortt :服务器将监听的端口号(在客户端模式设为 -1 )。
l        iMaxNumConnections :最多允许连接数。
l         iMaxIOWorkers :输入 / 输出工作线程数。
l         nOfWorkers :逻辑工作者数(在运行时能被改变)。
l         iMaxNumberOfFreeBuffer :保留的重复利用的内存的最大数量( -1 :无 0 :无穷)。
l         iMaxNumberOfFreeContext :保留的重复利用的客户端信息的最大数量( -1 :无 0 :无穷)。
l         bOrderedRead :用来进行顺序读。
l         bOrderedSend :用来进行顺序发送。
l         iNumberOfPendlingReads :等待数据的异步读循环的数量。在连接到一个远端的连接时调用下面的函数:
Connect(


const





CString &strIPAddr,

int





nPort)




l         strIPAddr :远端服务器的 IP 地址。
l         nPort :端口。
关闭服务器,调用函数: ShutDown ()。
例如:
MyIOCP m_iocp;




if






(!m_iocp.Start(-

1





,

1210





,

2





,

1





,

0





,

0





))



AfxMessageBox(


"Error could not start the Client"





);



….




m_iocp.ShutDown();









5.1 文件传输

       使用 Winsock 2.0 TransmitFile 函数传输文件 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 ;};    }

这个函数处理进来的消息和远程连接发送的请求。在这种情形下,它只不过进行一个简单的回复或者传输文件。源代码分为两部分, IOCP IOCPClient
它们是连接的双方。

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 等待 B B 等待 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


引用地址:http://article.china-code.net/9/64/19186/cc3dtQxk.html

编写完成端口网络服务器的一些说明 (1)

 



1. AcceptEx:

BOOL
PASCAL FAR
AcceptEx (
   IN SOCKET sListenSocket,
   IN SOCKET sAcceptSocket,
   IN PVOID lpOutputBuffer,
   IN DWORD dwReceiveDataLength,
   IN DWORD dwLocalAddressLength,
   IN DWORD dwRemoteAddressLength,
   OUT LPDWORD lpdwBytesReceived,
   IN LPOVERLAPPED lpOverlapped
   );

    用来发起一个异步的调用, 接受客户端将要发出的连接请求. 与 accept 不同的是, 你必须先手动创建一个 socket 提供给 AcceptEx, 用 来接受连接 ( accept 是内在地创建一个 socket 接受连接, 并返回值 ). 而且, accept 创建的 socket 会自动继承监听 socket 的属性, AcceptEx 却不会. 因此如果有必要, 在 AcceptEx 成功接受了一个连接之后, 我们必须调用:

    setsockopt( hAcceptSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,  ( char* )&( hListenSocket ), sizeof( hListenSocket ) );

来做到这一点.


    AcceptEx 允许在接受连接的同时接收对方发来的第一组数据, 这当然是出于性能的考虑. 但是这时候 AcceptEx 最少要接收到一个字节的 数据才会返回, 一旦碰到恶意连接它就永远不会返回了. 关闭这项功能的方式是: 把参数 dwReceiveDataLength 至为 0, 打开则相反. 当然了 , 如果一定要启用这个功能, 我们也有防御的办法. 启动一个线程定时地检测每一个 AcceptEx 是否已经连接, 连接时间为多久, 以此判断对 方是否是恐怖分子:

    int iSecs;
    int iBytes = sizeof( int );
    getsockopt( hAcceptSocket, SOL_SOCKET, SO_CONNECT_TIME, (char *)&iSecs, &iBytes );

iSecs 为 -1 表示还未建立连接, 否则就是已经连接的时间.


    调用 AcceptEx 的方式:

    #include   // for WSAID_ACCEPTEX
    typedef BOOL ( WINAPI * PFNACCEPTEX ) ( SOCKET, SOCKET, PVOID, DWORD, DWORD, DWORD, LPDWORD, 
LPOVERLAPPED );
    PFNACCEPTEX pfnAcceptEx;
    DWORD dwBytes;
    GUID guidAcceptEx = WSAID_ACCEPTEX;
    ::WSAIoctl( hListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidAcceptEx, sizeof( guidAcceptEx ), &pfnAcceptEx, sizeof( pfnAcceptEx ), &dwBytes, NULL, NULL );

    DWORD uAddrSize = sizeof( SOCKADDR_IN ) + 16;
    DWORD uDataSize = 0;
    BOOL bRes = pfnAcceptEx( hListenSocket, hAcceptSocket, buffer, uDataSize, uAddrSize, uAddrSize, &uAddrSize, ( LPWSAOVERLAPPED )overlapped );

    其中, buffer 和 overlapped 要根据你自己的用途来定了, 这里只是拿来充数.

    一旦 AcceptEx 调用完成 ( 通过完成端口通知你 ), 接下来的步骤就是 1. 上面讲的 SO_UPDATE_ACCEPT_CONTEXT;

2. 将 hAcceptSocket 绑定到完成端口.

 



2. TransmitFile

   TransmitFile 顾名思义是用来进行文件传输的函数, 全自动无需干涉的. 在 Win NT 专业版/家庭版上无法发挥全部性能. 在这里只讨论它 的一个功能: 关闭一个 SOCKET 并再次初始化它. 创建一个 SOCKET 是很耗时的, 因此这么做可以节约大量时间:

    #include   // for WSAID_TRANSMITFILE
    typedef BOOL ( WINAPI * PFNTRANSMITFILE ) ( SOCKET, HANDLE, DWORD, DWORD, LPOVERLAPPED, 
LPTRANSMIT_FILE_BUFFERS, DWORD );
    PFNACCEPTEX pfnTransmitFile;
    DWORD dwBytes;
    GUID guidTransmitFile = WSAID_TRANSMITFILE;
    ::WSAIoctl( hListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidTransmitFile,
sizeof( guidTransmitFile ), &pfnTransmitFile,
sizeof( pfnTransmitFile ), &dwBytes, NULL, NULL );

    pfnTransmitFile( hAcceptSocket, NULL, 0, 0, NULL, NULL, TF_DISCONNECT | TF_REUSE_SOCKET );

    经过这个函数处理的 SOCKET 可以作为接受连接的 socket 提交给 AcceptEx 再次使用. 当这样的 socket 接受连接成功后, 如果往完成 端口上绑定会出错 - 因为上次接受连接成功时已经绑定过了, 这个错误可以忽略.

 



3. FD_ACCEPT

    即使我们在程序启动时发起了再多的 AcceptEx , 也有可能碰到数目不够用户连不上来的情况. 在 Win2000 或更高版本的系统上, 我们可 以通过 WSAEventSelect 注册一个 FD_ACCEPT 事件. 当 AcceptEx 数目不足以应付大量的连接请求时, 这个事件会被触发. 于是我们就可以发 出更多的 AcceptEx, 而且我们还可以抽空辨别一下 AcceptEx 为什么这么快就用光了, 是不是碰上攻击者了( 辨别方法见上文所述 ) ?

    HANDLE hAcceptExThreadEvent = ::CreateEvent( NULL, TRUE, FALSE, _T("AcceptExThreadEvent") );
    ::WSAEventSelect( hListenSocket, hAcceptExThreadEvent, FD_ACCEPT );

    DWORD WINAPI AcceptExThread( LPVOID lpParameter )
    {
 // 负责保证有足够多的 AcceptEx 可以接受连接请求的线程

        for( UINT i = 0; i < 10; i ++ ) // 程序启动时发起的 AcceptEx
        {  
            pfnAcceptEx( hListenSocket, ... );
        }

        while( TRUE )
        {
            DWORD dwRes = ::WaitForSingleObject( hAcceptExThreadEvent, INFINITE );
     if( dwRes == WAIT_FAILED )
     {
         break;
     }
     ::ResetEvent( hAcceptExThreadEvent );
     if( m_sbWaitForExit )
     {   // 当然, 退出线程也是这个 Event 通知
         break;
     }
     pfnAcceptEx( hListenSocket, ... );

            //
            // ... 在此检查是否被攻击
            //
        }
        return 0;
    }

 
    要说明的是, WSAEventSelect() 所需的 WSAEVENT 和 CreateEvent() 所创建的 EVENT 是通用的.

 



4. WSASend 和 WSARecv

    默认情况下, 每一个 socket 在系统底层都拥有一个发送和接收缓冲区.

    我们发送数据时, 实际上是把数据复制到发送缓冲区中, 然后 WSASend 返回. 但是如果发送缓冲区已经满了, 那么我们在 WSASend 中指 定的缓冲区就会被锁定到系统的非分页内存池中, WSASend 返回 WSA_IO_PENDING. 一旦网络空闲下来, 数据将会从我们提交的缓冲区中直接被 发送出去, 与 " 我们的缓冲区->发送缓冲区->网络 " 相比节省了一次复制操作.

    WSARecv 也是一样, 接收数据时直接把接收缓冲区中的数据复制出来. 如果接收缓冲区中没有数据, 我们在 WSARecv 中指定的缓冲区就会 被锁定到系统的非分页内存池以等待数据, WSARecv 返回 WSA_IO_PENDING. 如果网络上有数据来了, 这些数据将会被直接保存到我们提供的缓 冲区中.
  
   如果一个服务器同时连接了许多客户端, 对每个客户端又调用了许多 WSARecv, 那么大量的内存将会被锁定到非分页内存池. 锁定这些内存
时是按照页面边界来锁定的, 也就是说即使你 WSARecv 的缓存大小是 1 字节, 被锁定的内存也将会是 4k. 非分页内存池是由整个系统共用的 , 如果用完的话最坏的情况就是系统崩溃. 一个解决办法是, 使用大小为 0 的缓冲区调用 WSARecv. 等到调用成功时再换用非阻塞的 recv 接 收到来的数据, 直到它返回 WSAEWOULDBLOCK 表明数据已经全部读完. 在这个过程中没有任何内存需要被锁定, 但坏处是效率稍低.



你可能感兴趣的:(学习笔记)