作系统加入一个队列中。然后应用程序可以对核心层进行查询以得到此完成端口。
这里我要对上面的一些概念略作补充,在解释[完成]两字之前,我想先简单的提一下同步和异步这两个概念,逻辑上来讲做完一件事后再去做另一件事就是同步,而同时一起做两件或两件以上事的话就是异步了。你也可以拿单线程和多线程来作比喻。但是我们一定要将同步和堵塞,异步和非堵塞区分开来,所谓的堵塞函数诸如accept(…),当调用此函数后,此时线程将挂起,直到操作系统来通知它,”HEY兄弟,有人连进来了”,那个挂起的线程将继续进行工作,也就符合”生产者-消费者”模型。堵塞和同步看上去有两分相似,但却是完全不同的概念。大家都知道I/O设备是个相对慢速的设备,不论打印机,调制解调器,甚至硬盘,与CPU相比都是奇慢无比的,坐下来等I/O的完成是一件不甚明智的事情,有时候数据的流动率非常惊人,把数据从你的文件服务器中以 Ethernet速度搬走,其速度可能高达每秒一百万字节,如果你尝试从文件服务器中读取100KB,在用户的眼光来看几乎是瞬间完成,但是,要知道,你的线程执行这个命令,已经浪费了10个一百万次CPU周期。所以说,我们一般使用另一个线程来进行I/O。重叠IO[overlapped I/O]是Win32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这也就是[完成]的含义。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来完成overlapped I/O。你可以获得线程所有利益,而不需要付出什么痛苦的代价。
完成端口中所谓的[端口]并不是我们在TCP/IP中所提到的端口,可以说是完全没有关系。我到现在也没想通一个I/O设备[I/O Device]和端口[IOCP中的Port]有什么关系。估计这个端口也迷惑了不少人。IOCP只不过是用来进行读写操作,和文件I/O倒是有些类似。既然是一个读写设备,我们所能要求它的只是在处理读与写上的高效。在文章的第三部分你会轻而易举的发现IOCP设计的真正用意。
IOCP和网络又有什么关系? Cpp代码
1. int main() 2. {
3. WSAStartup(MAKEWORD(2, 2), &wsaData);
4. ListeningSocket = socket(AF_INET, SOCK_STREAM, 0);
5. bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAdd
r));
6. listen(ListeningSocket, 5);
7. int nlistenAddrLen = sizeof(ClientAddr); 8. while(TRUE) 9. {
10. NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientA
ddr, &nlistenAddrLen);
11. HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (void
*) NewConnection, 0, &dwTreadId); 12. CloseHandle(hThread); 13. }
14. return 0;
15. }
相信只要写过网络的朋友,应该对这样的结构在熟悉不过了。accept后线程被挂起,等待一个客户发出请求,而后创建新线程来处理请求。当新线程处理客户请求时,起初的线程循环回去等待另一个客户请求。处理客户请求的线程处理完毕后终结。
在上述的并发模型中,对每个客户请求都创建了一个线程。其优点在于等待请求的线程只需做很少的工作。大多数时间中,该线程在休眠[因为recv处于堵塞状态]。
但是当并发模型应用在服务器端[基于Windows NT],Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多 CPU时间来做它们的工作。
大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。
我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?!
这个问题的解决方法就是一个称为I/O完成端口的内核对象,他首次在Windows NT3.5中被引入。
其实我们上面的构想应该就差不多是IOCP的设计机理。其实说穿了IOCP不就是一个消息队列嘛!你说这和[端口]这两字有何联系。我的理解就是 IOCP最多是应用程序和操作系统沟通的一个接口罢了。
至于IOCP的具体设计那我也很难说得上来,毕竟我没看过实现的代码,但你完全可以进行模拟,只不过性能可能…,如果想深入理解IOCP, Jeffrey Ritchter的Advanced Windows 3rd其中第13章和第14张有很多宝贵的内容,你可以拿来窥视一下系统是如何完成这一切的。
实现方法
Microsoft为IOCP提供了相应的API函数,主要的就两个,我们逐一的来看一下:
Cpp代码
1. HANDLE CreateIoCompletionPort (
2. HANDLE FileHandle, // handle to file
3. HANDLE ExistingCompletionPort, // handle to I/O completion port 4. ULONG_PTR CompletionKey, // completion key
5. DWORD NumberOfConcurrentThreads // number of threads to execu
te concurrently 6. );
在讨论各参数之前,首先要注意该函数实际用于两个截然不同的目的: 1.用于创建一个完成端口对象
2.将一个句柄[HANDLE]和完成端口关联到一起
在创建一个完成一个端口的时候,我们只需要填写一下NumberOfConcurrentThreads这个参数就可以了。它告诉系统一个完成端口上同时允许运行的线程最大数。在默认情况下,所开线程数和CPU数量相同,但经验给我们一个公式: 线程数 = CPU数 * 2 + 2
要使完成端口有用,你必须把它同一个或多个设备相关联。这也是调用CreateIoCompletionPort完成的。你要向该函数传递一个已有的完成端口的句柄,我们既然要处理网络事件,那也就是将客户的socket作为HANDLE传进去。和一个完成键[对你有意义的一个32位值,也就是一个指针,操作系统并不关心你传什么]。每当你向端口关联一个设备时,系统向该完成端口的设备列表中加入一条信息纪录。
另一个API就是
Cpp代码
1. BOOL GetQueuedCompletionStatus(
2. HANDLE CompletionPort, // handle to completion port 3. LPDWORD lpNumberOfBytes, // bytes transferred 4. PULONG_PTR lpCompletionKey, // file completion key 5. LPOVERLAPPED *lpOverlapped, // buffer
6. DWORD dwMilliseconds // optional timeout value 7. );
第一个参数指出了线程要监视哪一个完成端口。很多服务应用程序只是使用一个I/O完成端口,所有的I/O请求完成以后的通知都将发给该端口。简单的说,GetQueuedCompletionStatus使调用线程挂起,直到指定的端口的I/O完成队列中出现了一项或直到超时。同I/O完成端口相关联的第3个数据结构是使线程得到完成I/O项中的信息:传输的字节数,完成键和OVERLAPPED结构的地址。该信息是通过传递给 GetQueuedCompletionSatatus的 lpdwNumberO
fBytesTransferred,lpdwCompletionKey和lpOverlapped参数返回给线程的。
根据到目前为止已经讲到的东西,首先来构建一个frame。下面为您说明了如何使用完成端口来开发一个echo服务器。大致如下: 1.初始化Winsock 2.创建一个完成端口
3.根据服务器线程数创建一定量的线程数 4.准备好一个socket进行bind然后listen 5.进入循环accept等待客户请求
6.创建一个数据结构容纳socket和其他相关信息 7.将连进来的socket同完成端口相关联 8.投递一个准备接受的请求 以后就不断的重复5至8的过程
那好,我们用具体的代码来展示一下细节的操作。
(完)
由於這幾天都在學習IOCP的開發 雖然參考了
Windows網路與通訊程式設計 一書 關於IOCP的程式碼
只不過該書提供的程式碼沒有處理掉線與在大量併發連線下會有問題產生 再加上連線關閉就放棄剩下的封包
(因為IOCP有排序問題 想想看 當連線者傳來一個操作之後下線 結果關閉連線封包 比操作封包早被處理 那該操作的封包就被遺棄了) 所以就基於他的介紹自己開發了一個IOCP
目前測試在區網內(外網頻寬太低) 用第二台電腦連接可以達到5000個連接以4K大小資料
持續傳送給主機檢驗封包正確在返回正確與否每次處理只花費100ms~150ms左右 伺服器開在CPU:TURION TL52 記憶體:2gb 的筆電上
好了成果分享後 現在來分享心得嚕
先列出遇到的問題 1.封包的排序處理 2.資源的釋放
3.掉線的用戶移除
雖然書上提供的範例 對封包進行排序處理 但是在兩個封包同時被處理時 A執行緒 取出編號一的封包 之後PO給使用者處理 B執行緒 取出了編號二的封包 又PO給使用者處理 此時將發生一個問題 由於封包可能是被截斷的
1.一個封包被分成了兩個或者2.是一個封包+下個封包的一半 所以我們必須要額外提供一個緩衝區來將不完全的封包存入
此時如果此時發生了2的情況 這樣B執行緒在A處理第一個封包時
將不完全的封包加入了緩衝區 這樣A那邊一半的封包 就會錯亂掉 於是我們要加入一些機制來防範
我的辦法就是只允許一個執行續進行封包處理的動作 同樣情況下 兩個封包同時被處理
不過此時在A取出編號一封包PO給使用者時
B執行緒就只單純把它的封包加入待處理的封包列表然後就返回 A處理完編號一的封包後就在找下一個編號封包處理
如此一來A將負責處理到此連線沒有待處理的封包離開後
其他B.C.D.E.F.G....都不會處理A在處理的連線封包只會單純加入待處理列表 而去服務其他連接 這樣就可以避免很多錯誤的發生
資源的釋放 由於原本用跟書上一樣的作法
當send操作跟recv操作計數器為0時就釋放client的資源 這樣操作在當連線關閉就放棄所有RecvIO操作包 可以很好運作 但是我們不放棄任何一的RecvIO操作包的話
當A.B封包同時進來處理 A為關閉 B為關閉前的最後一個操作 於是在卻認為RECVIO操作時 計數器各減了1 此時計數器歸0了 A檢測長度為0 於是判斷IO操作是都完成了(檢測計數器是否歸0) 結果都為0開始釋放此連接使用的資源
但是B執行緒在A判斷是否關閉時執行到封包處理 這樣就會產生問題 解決方法就跟是用我上面說的方法
因為只有一個執行緒負責處理封包 當讀到最後一個RECVIO包時
就是在每次取出要處理的封包時 判斷他的序號跟要分配給下一個RecvIO操作包的序號 - 1是否一樣
一樣的話代表 連線關閉 所以沒有再投遞IO操作 而IOCP處也沒有RECV的IO包了 便可以釋放此連接的所有資源了 至於Send的話 當send錯誤返回時就直接釋放IOData就可以了
畢竟 反正連線都中斷了 也送不了啥出去了
掉線的移除 查詢網路都是建議使用心跳包
但是由於心跳包是由底層協議去支援的
我在想要是Client程式在被debug的話 那除非是softICE之類的調適器 其他的都還是會正常返回 不如自己做一個檢測
讓Client每隔預定的時間就發個包來 不然就當做掉線close掉 因為在debug中 程序被中斷住是不能發封包的
而debug是看組合語言 這再閱讀上會需要一點時間的XD 雖然沒多大防護作用 算是給debug者 製造些小麻煩罷了
於是我在WSAWaitForMultipleEvents() 設置了一個較小的等待值 讓程式在timeout後檢測一下有沒有人超過時間沒回傳資料的 之後在每次接收到RECVIO包時就從設檢測的時間 當然 不是一次檢測所有連線而是一次檢測一個 順便在檢測accept的超時連線
好了以上是我陷在想到的 晚點想到新的心得在補上