8 个开发更安全代码的简单规则
非常荣幸在过去的几年中曾经与数千位出色的开发人员一起工作,他们希望了解如何编写更安全的软件。在此期间,我也从构建安全系统方面表现出色的人员那里学到了很多东西,这使我开始思考一个问题。我在想“安全开发人员”之间是否有共同的技能或习惯。答案是当然有!本文介绍了安全代码开发人员之间共有的一系列习惯。
非常荣幸在过去的几年中曾经与数千位出色的开发人员一起工作,他们希望了解如何编写更安全的软件。在此期间,我也从构建安全系统方面表现出色的人员那里学到了很多东西,这使我开始思考一个问题。我在想“安全开发人员”之间是否有共同的技能或习惯。答案是当然有!本文介绍了安全代码开发人员之间共有的一系列习惯。
| |
早在两年前我就已经能很熟练的运用完成端口这种技术了,只是一直没有机会将它用在什么项目中,这段时间见到这种技术被过分炒作,过分的神秘化,就想写一篇解释它如何工作的文章.想告诉大家它没有传说中的那么高深难懂!有什么错误的地方还请高人指正.转载请注明出处及作者,谢谢!
以一个文件传输服务端为例,在我的机器上它只起两个线程就可以为很多个个客户端同时提供文件下载服务,程序的性能会随机器内CPU个数的增加而线性增长,我尽可能做到使它清晰易懂,虽然程序很小却用到了NT 5的一些新特性,重叠IO,完成端口以及线程池,基于这种模型的服务端程序应该是NT系统上性能最好的了.
首先.做为完成端口的基础,我们应该理解重叠IO,这需要你已经理解了内核对象及操作系统的一些概念概念,什么是信号/非信号态,什么是等待函数,什么是成功等待的副作用,什么是线程挂起等,如果这些概令还没有理解,你应该先看一下Windows 核心编程中的相关内容.如果已经理解这些,那么重叠IO对你来说并不难.
你可以这样认为重叠IO,现在你已经进入一个服务器/客户机环境,请不要混淆概念,这里的服务器是指操作系统,而客户机是指你的程序(它进行IO操作),是当你进行IO操作(send,recv,writefile,readfile….)时你发送一个IO请求给服务器(操作系统),由服务器来完成你需要的操作,然后你什么事都没有了,当服务器完成IO请求时它会通知你,当然在这期间你可以做任何事,一个常用的技巧是在发送重叠IO请求后,程序在一个循环中一边调用PeekMessage,TranslateMessage和DispatchMessage更新界面,同时调用GetOverlappedResult等待服务器完成IO操作,更高效一点的做法是使用IO完成例程来处理服务器(操作系统)返回的结果,但并不是每个支持重叠IO操作的函数都支持完成例程如TransmitFile函数.
例1.一次重叠写操作过程(GetOverlappedResult方法):
1.填写一个OVERLAPPED结构
2.进行一次写操作,并指定重叠操作参数(上面的OVERLAPPED结构变量的指针)
3.做其它事(如更新界面)
4.GetOverlappedResult取操作结果
5.如果IO请求没有完成,并且没有出错则回到期3
6.处理IO操作结果
例2.一次重叠写操作过程(完成例程方法):
1.填写一个OVERLAPPED结构
2.进行一次写操作,并指定重叠操作参数(上面的OVERLAPPED结构变量的指针),并指定完成例程
3.做其它事(如更新界面)
4.当完成例程被调用说明IO操作已经完成或出错,现在可以对操作结果进行处理了
如果你已经理解上面的概念,就已经很接近IO完成端口了,当然这只是很常规的重叠操作它已经非常高效,但如果再结合多线程对一个File或是Socket进行重叠IO操作就会非常复杂,通常程序员很难把握这种复杂度.完成端口可以说就是为了充分发挥多线程和重叠IO操作相结合的性能而设计的.很多人都说它复杂,其实如果你自己实现一个多线程的对一个File或是Socket进行重叠IO操作的程序(注意是多个线程对一个HANDLE或SOCKET进行重叠IO操作,而不是启一个线程对一个HANDLE进行重叠IO操作)就会发现完成端口实际上简化了多线程里使用重叠IO的复杂度,并且性能更高,性能高在哪?下面进行说明.
我们可能写过这样的服务端程序:
例3.主程序:
1.监听一个端口
2.等待连接
3.当有连接来时
4.启一个线程对这个客户端进行处理
5.回到2
服务线程:
1.读客户端请求
2.如果客户端不再有请求,执行6
3.处理请求
4.返回操作结果
5.回到1
6.退出线程
这是一种最简单的网络服务器模型,我们把它优化一下
例4.主程序:
1.开一个线程池,里面有机器能承受的最大线程数个线程,线程都处于挂起(suspend)状态
1.监听一个端口
2.等待连接
3.当有连接来时
4.从线程池里Resume一个线程对这个客户端进行处理
5.回到2
服务线程与例3模型里的相同,只是当线程处理完客户端所有请求后,不是退出而是回到线程池,再次挂起让出CPU时间,并等待为下一个客户机服务.当然在此期间线程会因为IO操作(服务线程的第1,5操作,也许还有其它阻塞操作)挂起自己,但不会回到线程池,也就是说它一次只能为一个客户端服务.
这可能是你能想到的最高效的服务端模型了吧!它与第一个服务端模型相比少了很多个用户态到内核态的CONTEXT Switch,反映也更加快速,也许你可能觉得这很微不足道,这说明你缺少对大规模高性能服务器程序(比如网游服务端)的认识,如果你的服务端程序要对几千万个客户端进行服务呢?这也是微软Windows NT开发组在NT 5以上的系统中添加线程池的原因.
思考一下什么样的模型可以让一个线程为多个客户端服务呢!那就要跳出每来一个连接启线程为其服务的固定思维模式,我们把线程服务的最小单元分割为单独的读或写操作(注意是读或写不是读和写),而不是一个客户端从连接到断开期间的所有读写操作.每个线程都使用重叠IO进行读写操作,投递了读写请求后线程回到线程池,等待为其它客户机服务,当操作完成或出错时再回来处理操作结果,然后再回到线程池.
看看这样的服务器模型:
例5.主程序:
1.开一个线程池,里面有机器内CPU个数两倍的线程,线程都处于挂起(suspend)状态,它们在都等处理一次重叠IO操作的完成结果
1.监听一个端口
2.等待连接
3.当有连接来时
4.投递一个重叠读操作读取命令
5.回到2
服务线程:
1.如果读完成,则处理读取的内容(如HTTP GET命令),否则执行3
2.投递一个重叠写操作(如返回HTTP GET命令需要的网页)
3.如果是一个写操作完成,可以再投递一个重叠读操作,读取客户机的下一个请求,或者是关闭连接(如HTTP协议里每发完一个网页就断开)
4.取得下一个重叠IO操作结果,如果IO操作没有完成或没有IO操作则回到线程池
假设这是一个WEB服务器程序,可以看到工作者线程是以读或写为最小的工作单元运行的,在主程序里面进行了一次重叠读操作
当读操作完成时一个线程池中的一个工作者线程被激活取得了操作结果,处理GET或POST命令,然后发送一个网页内容,发送也是一个重叠操作,然后处理对其它客户机的IO操作结果,如果没有其它的东西需要处理时回到线程池等待.可以看到使用这种模型发送和接收可以是也可以不是一个线程.
当发送操作完成时,线程池中的一个工作者线程池激活,它关闭连接(HTTP协议),然后处理其它的IO操作结果,如果没有其它的东西需要处理时回到线程池等待.
看看在这样的模型中一个线程怎么为多个客户端服务,同样是模拟一个WEB服务器例子:
假如现在系统中有两个线程,ThreadA,ThreadB它们在都等处理一次重叠IO操作的完成结果
当一个客户机ClientA连接来时主程序投递一个重叠读操作,然后等待下一个客户机连接,当读操作完成时ThreadA被激活,它收到一个HTTP GET命令,然后ThreadA使用重叠写操作发送一个网页给ClientA,然后立即回到线程池等待处理下一个IO操作结果,这时发送操作还没有完成,又有一个客户机ClientB连接来,主程序再投递一个重叠读操作,当读操作完成时ThreadA(当然也可能是ThreadB)再次被激活,它重复同样步骤,收到一个GET命令,使用重叠写操作发送一个网页给ClientB,这次它没有来得及回到线程池时,又有一个连接ClientC连入,主程序再投递一个重叠读操作,读操作完成时ThreadB被激活(因为ThreadA还没有回到线程池)它收到一个HTTP GET命令,然后ThreadB使用重叠写操作发送一个网页给ClientC,然后ThreadB回到线程池,这时ThreadA也回到了线程池.
可以想象现在有三个挂起的发送操作分别是ThreadA发送给ClientA和ClientB的网页,以及ThreadB发送给ClientC的网页,它们由操作系统内核来处理.ThreadA和ThreadB现在已经回到线程池,可以继续为其它任何客户端服务.
当对ClientA的重叠写操作已经完成,ThreadA(也可以是ThreadB)又被激活它关闭与ClientA连接,但还没有回到线程池,与此同时发送给ClientB的重叠写操作也完成,ThreadB被激活(因为ThreadA还没有回到线程池)它关闭与ClientB的连接,然后回到线程池,这时ClientC的写操作也完成,ThreadB再次被激活(因为ThreadA还是没有回到线程池),它再关闭与ClientC的连接,这时ThreadA回到线程池,ThreadB也回到线程池.这时对三个客户端的服务全部完成.可以看到在整个服务过程中,“建立连接”,“读数据”,“写数据"和"关闭连接"等操作是逻辑上连续而实际上分开的.
到现在为止两个线程处理了三次读操作和三次写操作,在这些读写操作过程中所出现的状态机(state machine)是比较复杂的,我们模拟的是经过我简化过的,实际上的状态要比这个还要复杂很多,然而这样的服务端模型在客户端请求越多时与前两个模型相比的性能越高.而使用完成端口我们可以很容易实现这样的服务器模型.
微软的IIS WEB服务器就是使用这样的服务端模型,很多人说什么阿帕奇服务器比IIS的性能好什么什么的我表示怀疑,除非阿帕奇服务器可以将线程分割成,为更小的单元服务,我觉得不太可能!这种完成端口模型已经将单个读或写操作作为最小的服务单元,我觉得在相同机器配置的情况下IIS的性能要远远高于其它WEB服务器,这也是从实现机理上来分析的,如果出现性能上的差别可能是在不同的操作系统上,也许Linux的内核比Windows的要好,有人真的研究过吗?还是大家一起在炒作啊.
对于状态机概念,在很多方面都用到,TCPIP中有,编译原理中有,OpengGL中有等等,我的离散数学不好(我是会计专业不学这个),不过还是搞懂了些,我想如果你多花些时间看,还是可以搞懂的.最后是一个简单的文件传输服务器程序代码,只用了两个线程(我的机器里只有一块CPU)就可以服务多个客户端.我调试时用它同时为6个nc客户端提供文件下载服务都没有问题,当然更多也不会有问题,只是略为使用了一下NT 5的线程池和完成端口技术就可以有这样高的性能,更不用说IIS的性能咯!
希望大家不要陷在这个程序的框架中,Ctrl+C,Ctrl+V没有什么意义,要理解它的实质.程序使用Visual C++ 6.0 SP5+2003 Platform SDK编译通过,在Windows XP Professional下调试运行通过.程序运行的最低要求是Windows 2000操作系统.
/
created: 2005/12/24
created: 24:12:2005 20:25
modified: 2005/12/24
filename: d:\vcwork\iocomp\iocomp.cpp
file path: d:\vcwork\iocomp
file base: iocomp
file ext: cpp
author: kruglinski(kruglinski_at_gmail_dot_com)
purpose: 利用完成端口技术实现的高性能文件下载服务程序
*/
#define _WIN32_WINNT 0x0500
#include <cstdlib>
#include <clocale>
#include <ctime>
#include <iostream>//一使用输入输出流程序顿时增大70K
#include <vector>
#include <algorithm>
#include <winsock2.h>
#include <mswsock.h>
using namespace std;
#pragma comment(lib,“ws2_32.lib”)
#pragma comment(lib,“mswsock.lib”)
const int MAX_BUFFER_SIZE=1024;
const int PRE_SEND_SIZE=1024;
const int QUIT_TIME_OUT=3000;
const int PRE_DOT_TIMER=QUIT_TIME_OUT/80;
typedef enum{IoTransFile,IoSend,IoRecv,IoQuit} IO_TYPE;
typedef struct
{
SOCKET hSocket;
SOCKADDR_IN ClientAddr;
}PRE_SOCKET_DATA,*PPRE_SOCKET_DATA;
typedef struct
{
OVERLAPPED oa;
WSABUF DataBuf;
char Buffer[MAX_BUFFER_SIZE];
IO_TYPE IoType;
}PRE_IO_DATA,PPRE_IO_DATA;
typedef vector<PPRE_SOCKET_DATA> SocketDataVector;
typedef vector<PPRE_IO_DATA> IoDataVector;
SocketDataVector gSockDataVec;
IoDataVector gIoDataVec;
CRITICAL_SECTION csProtection;
char TimeNow(void)
{
time_t t=time(NULL);
tm localtm=localtime(&t);
static char timemsg[512]={0};
strftime(timemsg,512,"%Z: %B %d %X,%Y”,localtm);
return timemsg;
}
BOOL TransFile(PPRE_IO_DATA pIoData,PPRE_SOCKET_DATA pSocketData,DWORD dwNameLen)
{
//这一句是为nc做的,你可以修改它
pIoData->Buffer[dwNameLen-1]=’\0’;
HANDLE hFile=CreateFile(pIoData->Buffer,GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL);
BOOL bRet=FALSE;
if(hFile!=INVALID_HANDLE_VALUE)
{
cout<<“Transmit File “<<pIoData->Buffer<<” to client”<<endl;
pIoData->IoType=IoTransFile;
memset(&pIoData->oa,0,sizeof(OVERLAPPED));
reinterpret_cast<HANDLE>(pIoData->Buffer)=hFile;
TransmitFile(pSocketData->hSocket,hFile,GetFileSize(hFile,NULL),PRE_SEND_SIZE,reinterpret_cast<LPOVERLAPPED>(pIoData),NULL,TF_USE_SYSTEM_THREAD);
bRet=WSAGetLastError()==WSA_IO_PENDING;
}
else
cout<<“Transmit File “<<“Error:"<<GetLastError()<<endl;
return bRet;
}
DWORD WINAPI ThreadProc(LPVOID IocpHandle)
{
DWORD dwRecv=0;
DWORD dwFlags=0;
HANDLE hIocp=reinterpret_cast<HANDLE>(IocpHandle);
DWORD dwTransCount=0;
PPRE_IO_DATA pPreIoData=NULL;
PPRE_SOCKET_DATA pPreHandleData=NULL;
while(TRUE)
{
if(GetQueuedCompletionStatus(hIocp,&dwTransCount,
reinterpret_cast<LPDWORD>(&pPreHandleData),
reinterpret_cast<LPOVERLAPPED>(&pPreIoData),INFINITE))
{
if(0==dwTransCount&&IoQuit!=pPreIoData->IoType)
{
cout<<“Client:"
<<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
<<”:"<<ntohs(pPreHandleData->ClientAddr.sin_port)
<<” is closed”<<endl;
closesocket(pPreHandleData->hSocket);
EnterCriticalSection(&csProtection);
IoDataVector::iterator itrIoDelete=find(gIoDataVec.begin(),gIoDataVec.end(),pPreIoData);
gIoDataVec.erase(itrIoDelete);
SocketDataVector::iterator itrSockDelete=find(gSockDataVec.begin(),gSockDataVec.end(),pPreHandleData);
gSockDataVec.erase(itrSockDelete);
LeaveCriticalSection(&csProtection);
delete *itrIoDelete;
delete *itrSockDelete;
continue;
}
switch(pPreIoData->IoType){
case IoTransFile:
cout<<“Client:"
<<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
<<”:"<<ntohs(pPreHandleData->ClientAddr.sin_port)
<<” Transmit finished"<<endl;
CloseHandle(reinterpret_cast<HANDLE>(pPreIoData->Buffer));
goto LRERECV;
case IoSend:
cout<<“Client:"
<<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
<<”:"<<ntohs(pPreHandleData->ClientAddr.sin_port)
<<" Send finished"<<endl;
LRERECV:
pPreIoData->IoType=IoRecv;
pPreIoData->DataBuf.len=MAX_BUFFER_SIZE;
memset(&pPreIoData->oa,0,sizeof(OVERLAPPED));
WSARecv(pPreHandleData->hSocket,&pPreIoData->DataBuf,1,
&dwRecv,&dwFlags,
reinterpret_cast<LPWSAOVERLAPPED>(pPreIoData),NULL);
break;
case IoRecv:
cout<<“Client:"
<<inet_ntoa(pPreHandleData->ClientAddr.sin_addr)
<<”:"<<ntohs(pPreHandleData->ClientAddr.sin_port)
<<" recv finished"<<endl;
pPreIoData->IoType=IoSend;
if(!TransFile(pPreIoData,pPreHandleData,dwTransCount))
{
memset(&pPreIoData->oa,0,sizeof(OVERLAPPED));
strcpy(pPreIoData->DataBuf.buf,“File transmit error!\r\n”);
pPreIoData->DataBuf.len=strlen(pPreIoData->DataBuf.buf);
WSASend(pPreHandleData->hSocket,&pPreIoData->DataBuf,1,
&dwRecv,dwFlags,
reinterpret_cast<LPWSAOVERLAPPED>(pPreIoData),NULL);
}
break;
case IoQuit:
goto LQUIT;
default:
;
}
}
}
LQUIT:
return 0;
}
HANDLE hIocp=NULL;
SOCKET hListen=NULL;
BOOL WINAPI ShutdownHandler(DWORD dwCtrlType)
{
PRE_SOCKET_DATA PreSockData={0};
PRE_IO_DATA PreIoData={0};
PreIoData.IoType=IoQuit;
if(hIocp)
{
PostQueuedCompletionStatus(hIocp,1,
reinterpret_cast<ULONG_PTR>(&PreSockData),
reinterpret_cast<LPOVERLAPPED>(&PreIoData));
cout<<“Shutdown at “<<TimeNow()<<endl<<“wait for a moment please”<<endl;
//让出CPU时间,让线程退出
for(int t=0;t<80;t+=1)
{
Sleep(PRE_DOT_TIMER);
cout<<”.”;
}
CloseHandle(hIocp);
}
int i=0;
for(;i<gSockDataVec.size();i++)
{
PPRE_SOCKET_DATA pSockData=gSockDataVec[i];
closesocket(pSockData->hSocket);
delete pSockData;
}
for(i=0;i<gIoDataVec.size();i++)
{
PPRE_IO_DATA pIoData=gIoDataVec[i];
delete pIoData;
}
DeleteCriticalSection(&csProtection);
if(hListen)
closesocket(hListen);
WSACleanup();
exit(0);
return TRUE;
}
LONG WINAPI MyExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
ShutdownHandler(0);
return EXCEPTION_EXECUTE_HANDLER;
}
u_short DefPort=8182;
int main(int argc,char *argv)
{
if(argc==2)
DefPort=atoi(argv[1]);
InitializeCriticalSection(&csProtection);
SetUnhandledExceptionFilter(MyExceptionFilter);
SetConsoleCtrlHandler(ShutdownHandler,TRUE);
hIocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
WSADATA data={0};
WSAStartup(0x0202,&data);
hListen=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET==hListen)
{
ShutdownHandler(0);
}
SOCKADDR_IN addr={0};
addr.sin_family=AF_INET;
addr.sin_port=htons(DefPort);
if(bind(hListen,reinterpret_cast<PSOCKADDR>(&addr),
sizeof(addr))==SOCKET_ERROR)
{
ShutdownHandler(0);
}
if(listen(hListen,256)==SOCKET_ERROR)
ShutdownHandler(0);
SYSTEM_INFO si={0};
GetSystemInfo(&si);
si.dwNumberOfProcessors<<=1;
for(int i=0;i<si.dwNumberOfProcessors;i++)
{
QueueUserWorkItem(ThreadProc,hIocp,WT_EXECUTELONGFUNCTION);
}
cout<<“Startup at “<<TimeNow()<<endl
<<“work on port “<<DefPort<<endl
<<“press CTRL+C to shutdown”<<endl<<endl<<endl;
while(TRUE)
{
int namelen=sizeof(addr);
memset(&addr,0,sizeof(addr));
SOCKET hAccept=accept(hListen,reinterpret_cast<PSOCKADDR>(&addr),&namelen);
if(hAccept!=INVALID_SOCKET)
{
cout<<“accept a client:"<<inet_ntoa(addr.sin_addr)<<”:"<<ntohs(addr.sin_port)<<endl;
PPRE_SOCKET_DATA pPreHandleData=new PRE_SOCKET_DATA;
pPreHandleData->hSocket=hAccept;
memcpy(&pPreHandleData->ClientAddr,&addr,sizeof(addr));
CreateIoCompletionPort(reinterpret_cast<HANDLE>(hAccept),
hIocp,reinterpret_cast<DWORD>(pPreHandleData),0);
PPRE_IO_DATA pPreIoData=new(nothrow) PRE_IO_DATA;
if(pPreIoData)
{
EnterCriticalSection(&csProtection);
gSockDataVec.push_back(pPreHandleData);
gIoDataVec.push_back(pPreIoData);
LeaveCriticalSection(&csProtection);
memset(pPreIoData,0,sizeof(PRE_IO_DATA));
pPreIoData->IoType=IoRecv;
pPreIoData->DataBuf.len=MAX_BUFFER_SIZE;
pPreIoData->DataBuf.buf=pPreIoData->Buffer;
DWORD dwRecv=0;
DWORD dwFlags=0;
WSARecv(hAccept,&pPreIoData->DataBuf,1,
&dwRecv,&dwFlags,
reinterpret_cast<WSAOVERLAPPED>(pPreIoData),NULL);
}
else
{
delete pPreHandleData;
closesocket(hAccept);
}
}
}
return 0;
}
参考资料:
《MSDN 2001》
《Windows 网络编程》
《Windows 核心编程》
《TCP/IP详解》
她的日记:
昨天晚上他真的是非常非常古怪。我们本来约好了一起去一个餐厅吃晚饭。
但是我白天和我好朋友去购物了,结果就去晚了一会儿,可能就因此让他不高兴了。他一直不理睬我,气氛僵极了。后来我主动让步,说我们都退一步,好好的交流一下吧。他虽然同意了,但是还是继续沉默,一副无精打采心不在焉的样子。我问他到底怎么了,他只说'没事。后来我就问他,是不是我惹他生气了。他说,这不关我的事,让我不要管。在回家的路上我对他说,我爱他。但是他只是继续开车,一点反应也没有。我真的不明白啊,我不知道他为什么不再说'我也爱你'了。我们到家的时候我感觉,我可能要失去他了,因为他已经不想跟我有什么关系了,他不想理我了。他坐在那儿什么也不说,就只是闷着头的看电视, 继续发呆,继续无精打采。后来我只好自己上床睡去了。10分钟以后他爬到床上来了,他一直都在想别的什么。他的心思根本不在我这里!这真的是太让我心痛了。我决定要跟他好好的谈一谈。但是他居然就已经睡着了!我只好躺在他身边默默的流泪,后来哭着哭着睡着了。我现在非常的确定,他肯定是有了别的女人了。这真的像天塌下来了一样。天哪,我真不知道我活着还有什么意义。
WinDbg for Windows
WinDbg for Windows, 32bit version 6.6.7.5 [15.2MB]
http://msdl.microsoft.com/download/symbols/debuggers/dbg_x86_6.6.07.5.exe
WinDbg for Windows, 64bit Itanium version 6.6.7.5 [19.9MB]
http://msdl.microsoft.com/download/symbols/debuggers/dbg_ia64_6.6.07.5.exe
WinDbg for Windows, 64bit x86 version 6.6.7.5 [12.6MB]
http://msdl.microsoft.com/download/symbols/debuggers/dbg_amd64_6.6.07.5.exe
Windows Server 2003 symbols with no Service Pack
Itanium checked symbols, all languages [123MB]
http://msdl.microsoft.com/download/symbols/packages/windows2003/windows2003.ia64.chk.rtm.symbols.exe
Itanium retail symbols, all languages [105MB]
http://msdl.microsoft.com/download/symbols/packages/windows2003/windows2003.ia64.fre.rtm.symbols.exe
x86 checked symbols, all languages [163MB]
http://msdl.microsoft.com/download/symbols/packages/windows2003/Windows2003.x86.chk.rtm.symbols.exe
x86 retail symbols, all languages [168MB]
http://msdl.microsoft.com/download/symbols/packages/windows2003/Windows2003.x86.fre.rtm.symbols.exe
Windows Server 2003 with Service Pack 1 symbols
Itanium-based checked symbols, all languages [123MB]
http://msdl.microsoft.com/download/symbols/packages/windows2003/Windows2003_sp1.ia64.chk.rtm.symbols.exe
2001年,当SUN提出SUN.ONE构架的那一天,XX大学毕业的牛在“牛狼之家”
聊天战碰到了一个公司的Coder
在进入游戏业之前,以下的情况你了解么?
组织一个开发团队需要至少20人磨合6~8个月,而需要50个这样的团队才有可能产生一个世界级的制作人;
开发一个大型MMO需要3年或者更长时间;
游戏开发的核心人员与新手的薪资相差悬殊;
大型游戏的代码量往往在20万行以上,而策划文本则可能超过50万字;
制作人在MMO项目制作期内主持超过300次会议,并且累计收发5万余封的工作信件;
在我负责面试新手的过程中,只有不到20%的应聘者做好了准备,更多的人对游戏开发的了解仅限于想当然的程度,不少新人将游戏业想象成“好玩的工作”,只需要玩的技能就可以有前途的职业;有人认为自己对于游戏有很多“独特而伟大”的想法,更多人进入游戏业的最重要理由是“我从小就在玩FC”。错误估计从事游戏开发工作的难度和所需要的能力,对于一个新人,不仅仅是能否通过面试这么简单。有些人在工作中被淘汰,或者在频繁的跳槽中一无建树,其原因仅仅是:他没准备好。看看下面的对话,你准备好了么?
欢迎阅读此篇IOCP教程。我将先给出IOCP的定义然后给出它的实现方法,最后剖析一个Echo程序来为您拨开IOCP的谜云,除去你心中对IOCP的烦恼。OK,但我不能保证你明白IOCP的一切,但我会尽我最大的努力。以下是我会在这篇文章中提到的相关技术:
I/O端口
同步/异步
堵塞/非堵塞
服务端/客户端
多线程程序设计
Winsock API 2.0
在这之前,我曾经开发过一个项目,其中一块需要网络支持,当时还考虑到了代码的可移植性,只要使用select,connect,accept,listen,send还有recv,再加上几个#ifdef的封装以用来处理Winsock和BSD套接字[socket]中间的不兼容性,一个网络子系统只用了几个小时很少的代码就写出来了,至今还让我很回味。那以后很长时间也就没再碰了。
前些日子,我们策划做一个网络游戏,我主动承担下网络这一块,想想这还不是小case,心里偷着乐啊。网络游戏好啊,网络游戏为成百上千的玩家提供了乐趣和令人着秘的游戏体验,他们在线上互相战斗或是加入队伍去战胜共同的敌人。我信心满满的准备开写我的网络,于是乎,发现过去的阻塞同步模式模式根本不能拿到一个巨量多玩家[MMP]的架构中去,直接被否定掉了。于是乎,就有了IOCP,如果能过很轻易而举的搞掂IOCP,也就不会有这篇教程了。下面请诸位跟随我进入正题。
什么是IOCP?
先让我们看看对IOCP的评价
I/O完成端口可能是Win32提供的最复杂的内核对象。
[Advanced Windows 3rd] Jeffrey Richter
这是[IOCP]实现高容量网络服务器的最佳方法。
[Windows Sockets2.0:Write Scalable Winsock Apps Using Completion Ports]
Microsoft Corporation
完成端口模型提供了最好的伸缩性。这个模型非常适用来处理数百乃至上千个套接字。
[Windows网络编程2nd] Anthony Jones & Jim Ohlund
I/O completion ports特别显得重要,因为它们是唯一适用于高负载服务器[必须同时维护许多连接线路]的一个技术。Completion ports利用一些线程,帮助平衡由I/O请求所引起的负载。这样的架构特别适合用在SMP系统中产生的”scalable”服务器。
[Win32多线程程序设计] Jim Beveridge & Robert Wiener
看来我们完全有理由相信IOCP是大型网络架构的首选。那IOCP到底是什么呢?
微软在Winsock2中引入了IOCP这一概念 。IOCP全称I/O Completion Port,中文译为I/O完成端口。IOCP是一个异步I/O的API,它可以高效地将I/O事件通知给应用程序。与使用select()或是其它异步方法不同的是,一个套接字[socket]与一个完成端口关联了起来,然后就可继续进行正常的Winsock操作了。然而,当一个事件发生的时候,此完成端口就将被操作系统加入一个队列中。然后应用程序可以对核心层进行查询以得到此完成端口。
这里我要对上面的一些概念略作补充,在解释[完成]两字之前,我想先简单的提一下同步和异步这两个概念,逻辑上来讲做完一件事后再去做另一件事就是同步,而同时一起做两件或两件以上事的话就是异步了。你也可以拿单线程和多线程来作比喻。但是我们一定要将同步和堵塞,异步和非堵塞区分开来,所谓的堵塞函数诸如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和网络又有什么关系?
int main()
{
WSAStartup(MAKEWORD(2, 2), &wsaData);
ListeningSocket = socket(AF_INET, SOCK_STREAM, 0);
bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
listen(ListeningSocket, 5);
int nlistenAddrLen = sizeof(ClientAddr);
while(TRUE)
{
NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &nlistenAddrLen);
HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (void*) NewConnection, 0, &dwTreadId);
CloseHandle(hThread);
}
return 0;
}
相信只要写过网络的朋友,应该对这样的结构在熟悉不过了。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函数,主要的就两个,我们逐一的来看一下:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle, // handle to file
HANDLE ExistingCompletionPort, // handle to I/O completion port
ULONG_PTR CompletionKey, // completion key
DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);
在讨论各参数之前,首先要注意该函数实际用于两个截然不同的目的:
1.用于创建一个完成端口对象
2.将一个句柄[HANDLE]和完成端口关联到一起
在创建一个完成一个端口的时候,我们只需要填写一下NumberOfConcurrentThreads这个参数就可以了。它告诉系统一个完成端口上同时允许运行的线程最大数。在默认情况下,所开线程数和CPU数量相同,但经验给我们一个公式:
线程数 = CPU数 * 2 + 2
要使完成端口有用,你必须把它同一个或多个设备相关联。这也是调用CreateIoCompletionPort完成的。你要向该函数传递一个已有的完成端口的句柄,我们既然要处理网络事件,那也就是将客户的socket作为HANDLE传进去。和一个完成键[对你有意义的一个32位值,也就是一个指针,操作系统并不关心你传什么]。每当你向端口关联一个设备时,系统向该完成端口的设备列表中加入一条信息纪录。
另一个API就是
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
);
第一个参数指出了线程要监视哪一个完成端口。很多服务应用程序只是使用一个I/O完成端口,所有的I/O请求完成以后的通知都将发给该端口。简单的说,GetQueuedCompletionStatus使调用线程挂起,直到指定的端口的I/O完成队列中出现了一项或直到超时。同I/O完成端口相关联的第3个数据结构是使线程得到完成I/O项中的信息:传输的字节数,完成键和OVERLAPPED结构的地址。该信息是通过传递给GetQueuedCompletionSatatus的lpdwNumberOfBytesTransferred,lpdwCompletionKey和lpOverlapped参数返回给线程的。
根据到目前为止已经讲到的东西,首先来构建一个frame。下面为您说明了如何使用完成端口来开发一个echo服务器。大致如下:
1.初始化Winsock
2.创建一个完成端口
3.根据服务器线程数创建一定量的线程数
4.准备好一个socket进行bind然后listen
5.进入循环accept等待客户请求
6.创建一个数据结构容纳socket和其他相关信息
7.将连进来的socket同完成端口相关联
8.投递一个准备接受的请求
以后就不断的重复5至8的过程
那好,我们用具体的代码来展示一下细节的操作。
至此文章也该告一段落了,我带着您做了一趟旋风般的旅游,游览了所谓的完成端口。
水是什么形状的?
乍一看这个问题似乎问得很没有道理,其实仔细想想,水正是自然界中“多态”的完美体现。不是么?用圆柱形容器装水,那么水就是圆柱形的;换用圆锥形容器盛之,水则又会成为圆锥形的了。在这个过程中,我们并不需要关心水是如何改变形状的,亦无需关心水在改变形状的过程中具体做了哪些事情;我们所要关心的,只是提供给它一个什么形状的容器,这就足够了。
昨日写个小程序,加了几个CStatic,不知道为什么,只显示一个,搞了半天,最后为每个CStatic加一个ID才行,不知道有没有遇到同样的问题的朋友.