??xml version="1.0" encoding="utf-8" standalone="yes"?> 2. 完成端口创徏成功后,在Socket和完成端口之间徏立关联。再ơ调用CreateIoCompletionPort 注意QCreateIoCompletionPort函数的第3个参数允许开发h员传入一个类型ؓ(f)ULONG_PTR 完成端口创徏以及(qing)与Socket兌之后Q就要创Z个或多个工作U程来处理完成通知Q?br>每个U程都可以@环地调用GetQueuedCompletionStatus函数Q检查完成端口上的通知事g?br> 在D例说明一个典型的工作U程之前Q我们先讨论一下重叠I/O的过E。到一个重叠I/O 注意QOVERLAPPED成员不一定要求是OVERLAPPELUS扩展l构的一个成员,在获?br>OVERLAPPED指针之后Q可以用CONTAINING_RECORD宏获得相应的扩展l构的指针?/p>
典型的Worker Threadl构Q?br> DWORD WINAPI WorkerThread(LPVOID lpParam) 服务器端最要的功能各技术就是这些,下面介绍客户端?br> 到这客户端的主要模块和机制已基本介绍完。希望好好体?x)一下这U多U程断点l传的方法?
Win32重叠I/O(Overloapped I/O)机制允许发v一个操作,然后在操作完成之后接?br>C息。对于那U需要很长时间才能完成的操作来说Q重叠I/O机制其有用Q因为发?br>重叠操作的线E在重叠h发出后就可以自由地做别的事情了?br> 在Windows NT/2000上,提供真正可扩展的I/O模型是使用完成端口QCompletion
Port)的重叠I/O?br> ……
可以把完成端口看成系l维护的一个队列,操作pȝ把重叠I/O操作完成的事仉知
攑ֈ该队列里Q由于是暴露“操作完成”的事仉知Q所以命名ؓ(f)“完成端口”QCompletion
Ports)。一个Socket被创建后Q可以在M时刻和一个完成端口联pv来?br> 一般来_(d)一个应用程序可以创建多个工作线E来处理完成端口上的通知事g。工?br>U程的数量依赖于E序的具体需要。但是在理想的情况下Q应该对应一个CPU创徏一个线
E。因为在完成端口理想模型中,每个U程都可以从pȝ获得一?#8220;原子”性的旉片,?br>番运行ƈ(g)查完成端口,U程的切换是额外的开销。在实际开发的时候,q要考虑q些U?br>E是否牵涉到其他堵塞操作的情c(din)如果某U程q行堵塞操作Q系l则其挂vQ让别的
U程获得q行旉。因此,如果有这L(fng)情况Q可以多创徏几个U程来尽量利用时间?br> 应用完成端口分两步走Q?br> 1. 创徏完成端口句柄Q?br> HANDLE hIocp;
hIocp=CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if(hIocp==NULL) {
//如果错误
……
}
注意在第1个参?FileHandle)传入INVALID_FILE_HANDLEQ第2个参?ExistingCompletionPort)
传入NULLQ系l将创徏一个新的完成端口句柄,没有MI/O句柄与其兌?
函数Q这一ơ在W?个参数FileHandle传入创徏的Socket句柄Q参数ExistingCompletionPort
为已l创建的完成端口句柄?br> 以下代码创徏了一个Socketq把它和完成端口联系h?br> SOCKET s;
s=Socket(AF_INET,SOCK_STREAM,0);
if(s==INVALID_SOCKET) {
if(CreateIoCompletionPort((HANDLE)s,
hIocp,
(ULONG_PTR)0,
0)==NULL)
{
//如果创徏p|
……
}
}
到此为止QSocket已经成功和完成端口相兌。在此Socketq行的重叠I/O操作l果?br>使用完成端口发出通知?/p>
的数据成员,我们把它UCؓ(f)完成?Completion Key)Q此数据成员可以设计为指向包含Socket
信息的一个结构体的一个指针,用来把相关的环境信息和Socket联系hQ每ơ完成通知?br>到的同时Q该环境信息也随着通知一赯回给开发h员?/p>
被发P一个Overlappedl构体的指针p作ؓ(f)参数传递给pȝ。当操作完成Ӟ
GetQueueCompletionStatus可以返回指向同一个Overlappedl构的指针。ؓ(f)了L认和定位
q个已完成的操作Q开发h员最好定义自qOVERLAPPEDl构Q以包含一些自己定义的关于
操作本n的额外信息。比如:(x)
typedef struct _OVERLAPPELUS {
OVERLAPPED ol;
SOCKET s, sclient;
int OpCode;
WSABUF wbuf;
DWORD dwBytes, dwFlags;
} OVERLAPPELUS;
此结构的W?个成员ؓ(f)默认的OVERLAPPEDl构Q第2和第3个ؓ(f)本地服务Socket和与?br>操作相关的客户socketQ第4个成员ؓ(f)操作cdQ对于SocketQ现在定义的有以?U:(x)
#define OP_READ 0
#define OP_WRITE 1
#define OP_ACCEPT 2
然后q有应用E序的Socket~冲区,操作数据量,标志位以?qing)其他开发h员认为有?br>的信息?br> 当进行重叠I/O操作Q把OVERLAPPELUSl构作ؓ(f)重叠I/O的参数l(f)pOverlapp传递(?br>WSASendQW(xu)ASRecvQ等函数的lpOverlapped参数Q要求传入一个OVERLAPPl构的指针)?br> 当操作完成后QGetQueuedCompletionStatus函数q回一个LPOVERLAPPEDcd的指针,
q个指针其实是指向开发h员定义的扩展OVERLAPPELUSl构Q包含着开发h员早先传入的
全部信息?/p>
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPELUS *OverlapPlus, *newolp;
DWORD dwBytesXfered;
while(1)
{
ret=GetQueuedCompletionStatus(
hIocp,
&dwBytesXfered,
(PULONG_PTR)&PerHandleKey,
&Overlap,
INFINITE);
if(ret==0)
{
//如果操作p|
continue;
}
OverlapPlus=CONTATING_RECORD(Overlap, OVERLAPPELUS, ol);
switch(OverlapPlus->OpCode)
{
case OP_ACCEPT:
CreateIoCompletionPort(
(HANDLE)OverlapPlus->sclient,
hIocp,
(ULONG_PTR)0,
0);
newolp=AllocateOverlappelus();
newolp->s=OverlapPlus->sclient;
newolp->OpCode=OP_READ;
PrepareSendBuffer(&newolp->wbuf);
ret=WSASend(
newolp->s,
&newolp->wbuf,
1,
&newolp->dwBytes,
0,
&newolp.ol,
NULL);
if(ret==SOCKET_ERROR)
{
if(WSAGetLastError()!=WSA_IO_PENDING)
{
//q行错误处理
……
}
}
FreeOverlappelus(OverlapPlus);
SetEvent(hAcceptThread);
break;
case OP_READ:
memset(&OverlapPlus->ol,0,sizeof(OVERLAPPED));
ret=WSARecv(
OverlapPlus->s,
&OverlapPlus->wbuf,
1,
&OverlapPlus->dwBytes,
&OverlapPlus->dwFlags,
&OverlapPlus->ol,
NULL);
if(ret==SOCKET_ERROR)
{
if(WSAGetLastError()!=WSA_IO_PENDING)
{
//错误处理
……
}
}
break;
case OP_WRITE:
break;
}/*switchl束*/
}/*whilel束*/
}/*WorkerThreadl束*/
注意Q如果Overlapped操作立刻p|Q比如,q回SOCKET_ERROR或其他非
WSA_IO_PENDING的错误)Q则没有M完成通知事g?x)被攑ֈ完成端口队列里。反之,
则一定有相应的通知事g被放到端口队列?/p>
(本文源代码运行效果图)
实现Ҏ(gu)QVCQ+Q基于TCP/IP协议Q如下:(x)
仍釆用服务器与客h式,需分别对其设计与编E?br>服务器端较简单,主要是加入待传文gQ监听客P和传送文件。而那些断点箋传的功能Q以?qing)文件的理都放在客L(fng)上?br> 一、服务器?/strong>
首先介绍服务器端Q?br>最开始我们要定义一个简单的协议Q也是定义一个服务器端与客户端听得懂的语a。而ؓ(f)了把问题化,我就让服务器只要听懂两句话,一是客户?#8220;我要L件信?#8221;Q二是“我准备好了,可以传文件了”?br>׃要实现多U程Q必L功能独立出来Q且包装成线E,首先Z个监听线E,主要负责接入客户Qƈ启动另一个客L(fng)E。我用VC++实现如下Q?br>DWORD WINAPI listenthread(LPVOID lpparam)
{
//׃函数传来的套接字
SOCKET pthis=(SOCKET)lpparam;
//开始监?
int rc=listen(pthis,30);
//如果错就昄信息
if(rc<0){
CString aaa;
aaa="listen错误\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
aaa.ReleaseBuffer();
return 0;
}
//q入循环Qƈ接收到来的套接字
while(1){
//新徏一个套接字Q用于客L(fng)
SOCKET s1;
s1=accept(pthis,NULL,NULL);
//l主函数发有入消?
CString aa;
aa="一入!\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aa.GetBuffer(0),1);
aa.ReleaseBuffer();
DWORD dwthread;
//建立用户U程
::CreateThread(NULL,0,clientthread,(LPVOID)s1,0,&dwthread);
}
return 0;
}
接着我们来看用户U程Q?br>先看文g消息cd?/strong>Q?br>
struct fileinfo
{
int fileno;//文g?
int type;//客户端想说什么(前面那两句话Q用1,2表示Q?
long len;//文g长度
int seek;//文g开始位|,用于多线E?
char name[100];//文g?
};
用户U程函数:
DWORD WINAPI clientthread(LPVOID lpparam)
{
//文g消息
fileinfo* fiinfo;
//接收~存
char* m_buf;
m_buf=new char[100];
//监听函数传来的用户套接字
SOCKET pthis=(SOCKET)lpparam;
//M来的信息
int aa=readn(pthis,m_buf,100);
//如果有错p?
if(aa<0){
closesocket (pthis);
return -1;
}
//把传来的信息转ؓ(f)定义的文件信?
fiinfo=(fileinfo*)m_buf;
CString aaa;
//(g)验客h说什?
switch(fiinfo->type)
{
//我要L件信?
case 0:
//L?
aa=sendn(pthis,(char*)zmfile,1080);
//有错
if(aa<0){
closesocket (pthis);
return -1;
}
//发消息给d?
aaa="收到LIST命o(h)\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
break;
//我准备好了,可以传文件了
case 2:
//发文件消息给d?
aaa.Format("%s 文g被请求!%s\n",zmfile[fiinfo->fileno].name,nameph[fiinfo->fileno]);
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
//LӞq传?
readfile(pthis,fiinfo->seek,fiinfo->len,fiinfo->fileno);
//听不懂你说什?
default:
aaa="接收协议错误Q\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
break;
}
return 0;
}
L件函?/strong>
void readfile(SOCKET so,int seek,int len,int fino)
{
//文g?
CString myname;
myname.Format("%s",nameph[fino]);
CFile myFile;
//打开文g
myFile.Open(myname, CFile::modeRead | CFile::typeBinary|CFile::shareDenyNone);
//传到指定位置
myFile.Seek(seek,CFile::begin);
char m_buf[SIZE];
int len2;
int len1;
len1=len;
//开始接Ӟ直到发完整个文g
while(len1>0){
len2=len>SIZE?SIZE:len;
myFile.Read(m_buf, len2);
int aa=sendn(so,m_buf,len2);
if(aa<0){
closesocket (so);
break;
}
len1=len1-aa;
len=len-aa;
}
myFile.Close();
}
二、客L(fng)
客户端最重要Q也最复杂Q它负责U程的管理,q度的记录等工作?br>
大概程如下Q?br>先连接服务器Q接着发送命?Q给我文件信息)Q其中包括文仉度,名字{,然后Ҏ(gu)长度军_分几个线E下载,q初使化下蝲q程Q接着发送命?Q可以给我传文g了)Qƈ记录文gq程。最后,收尾?br>q其中有一个十分重要的c,是cdownloadc,定义如下Q?class cdownload
{
public:
void createthread();//开U程
DWORD finish1();//完成U程
int sendlist();//发命?
downinfo doinfo;//文g信息Q与服务器定义一P
int startask(int n);开始传文gn
long m_index;
BOOL good[BLACK];
int filerange[100];
CString fname;
CString fnametwo;
UINT threadfunc(long index);//下蝲q程
int sendrequest(int n);//发文件信?
cdownload(int thno1);
virtual ~cdownload();
};
下面先介lsendrequest(int n)Q在开始前Q向服务器发获得文g消息命o(h)Q以便让客户端知道有哪些文g可传
int cdownload::sendrequest(int n)
{
//建套接字
sockaddr_in local;
SOCKET m_socket;
int rc=0;
//初化服务器地址
local.sin_family=AF_INET;
local.sin_port=htons(1028);
local.sin_addr.S_un.S_addr=inet_addr(ip);
m_socket=socket(AF_INET,SOCK_STREAM,0);
int ret;
//联接服务?
ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
//有错的话
if(ret<0){
AfxMessageBox("联接错误");
closesocket(m_socket);
return -1;
}
//初化命?
fileinfo fileinfo1;
fileinfo1.len=n;
fileinfo1.seek=50;
fileinfo1.type=1;
//发送命?
int aa=sendn(m_socket,(char*)&fileinfo1,100);
if(aa<0){
closesocket(m_socket);
return -1;
}
//接收服务器传来的信息
aa=readn(m_socket,(char*)&fileinfo1,100);
if(aa<0){
closesocket(m_socket);
return -1;
}
//关闭
shutdown(m_socket,2);
closesocket(m_socket);
return 1;
}
有了文g消息后我们就可以下蝲文g了。在dCQ用法如下:(x)
//下蝲Wclno个文Ӟqؓ(f)它徏一个新cdownloadc?
down[clno]=new cdownload(clno);
//开始下载,q初使化
type=down[clno]->startask(clno);
//建立各线E?
createthread(clno);
下面介绍开始方?/strong>:
//开始方?
int cdownload::startask(int n)
{
//d文g长度
doinfo.filelen=zmfile[n].length;
//d名字
fname=zmfile[n].name;
CString tmep;
//初化文件名
tmep.Format("\\temp\\%s",fname);
//l主函数发消?
CString aaa;
aaa="正在d "+fname+" 信息Q马上开始下载。。。\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
aaa.ReleaseBuffer();
//如果文g长度于0p?
if(doinfo.filelen<=0) return -1;
//Z个以.downl尾的文件记录文件信?
CString m_temp;
m_temp=fname+".down";
doinfo.name=m_temp;
FILE* fp=NULL;
CFile myfile;
//如果是第一ơ下载文Ӟ初化各记录文g
if((fp=fopen(m_temp,"r"))==NULL){
filerange[0]=0;
//文g分块
for(int i=0;i<BLACK;i++)
{
if(i>0)
filerange[i*2]=i*(doinfo.filelen/BLACK+1);
filerange[i*2+1]=doinfo.filelen/BLACK+1;
}
filerange[BLACK*2-1]=doinfo.filelen-filerange[BLACK*2-2];
myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBinary);
//写入文g长度
myfile.Write(&doinfo.filelen,sizeof(int));
myfile.Close();
CString temp;
for(int ii=0;ii<BLACK;ii++){
//初化各q程记录文g信息Q以.downNl尾Q?
temp.Format(".down%d",ii);
m_temp=fname+temp;
myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBinary);
//写入各进E文件信?
myfile.Write(&filerange[ii*2],sizeof(int));
myfile.Write(&filerange[ii*2+1],sizeof(int));
myfile.Close();
}
((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,0,0,0,doinfo.threadno);
}
else{
//如果文g已存在,说明是箋传,Mơ信?
CString temp;
m_temp=fname+".down0";
if((fp=fopen(m_temp,"r"))==NULL)
return 1;
else fclose(fp);
int bb;
bb=0;
//dq程记录的信?
for(int ii=0;ii<BLACK;ii++)
{
temp.Format(".down%d",ii);
m_temp=fname+temp;
myfile.Open(m_temp,CFile::modeRead | CFile::typeBinary);
myfile.Read(&filerange[ii*2],sizeof(int));
myfile.Read(&filerange[ii*2+1],sizeof(int));
myfile.Close();
bb = bb+filerange[ii*2+1];
CString temp;
}
if(bb==0) return 1;
doinfo.totle=doinfo.filelen-bb;
((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,doinfo.totle,1,0,doinfo.threadno);
}
//建立下蝲l束q程timethreadQ以现各进E结束时间?
DWORD dwthread;
::CreateThread(NULL,0,timethread,(LPVOID)this,0,&dwthread);
return 0;
}
下面介绍建立各进E?/strong>函数Q很单:(x)
void CMainFrame::createthread(int threadno)
{
DWORD dwthread;
//建立BLACK个进E?
for(int i=0;i<BLACK;i++)
{
m_thread[threadno][i]= ::CreateThread(NULL,0,downthread,(LPVOID)down[threadno],0,&dwthread);
}
}
downthreadq程函数
DWORD WINAPI downthread(LPVOID lpparam)
{
cdownload* pthis=(cdownload*)lpparam;
//q程引烦Q?
InterlockedIncrement(&pthis->m_index);
//执行下蝲q程
pthis->threadfunc(pthis->m_index-1);
return 1;
}
下面介绍下蝲q程函数,最最核心的东西了
UINT cdownload::threadfunc(long index)
{
//初化联?
sockaddr_in local;
SOCKET m_socket;
int rc=0;
local.sin_family=AF_INET;
local.sin_port=htons(1028);
local.sin_addr.S_un.S_addr=inet_addr(ip);
m_socket=socket(AF_INET,SOCK_STREAM,0);
int ret;
//d~存
char* m_buf=new char[SIZE];
int re,len2;
fileinfo fileinfo1;
//联接
ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
//d各进E的下蝲信息
fileinfo1.len=filerange[index*2+1];
fileinfo1.seek=filerange[index*2];
fileinfo1.type=2;
fileinfo1.fileno=doinfo.threadno;
re=fileinfo1.len;
//打开文g
CFile destFile;
FILE* fp=NULL;
//是第一ơ传的话
if((fp=fopen(fname,"r"))==NULL)
destFile.Open(fname, CFile::modeCreate|CFile::modeWrite | CFile::typeBinary|CFile::shareDenyNone);
else
//如果文g存在Q是l传
destFile.Open(fname,CFile::modeWrite | CFile::typeBinary|CFile::shareDenyNone);
//文g指针Ud指定位置
destFile.Seek(filerange[index*2],CFile::begin);
//发消息给服务器,可以传文件了
sendn(m_socket,(char*)&fileinfo1,100);
CFile myfile;
CString temp;
temp.Format(".down%d",index);
m_temp=fname+temp;
//当各D长度还不ؓ(f)0?
while(re>0){
len2=re>SIZE?SIZE:re;
//dD内?
int len1=readn(m_socket,m_buf,len2);
//有错的话
if(len1<0){
closesocket(m_socket);
break;
}
//写入文g
destFile.Write(m_buf, len1);
//更改记录q度信息
filerange[index*2+1]-=len1;
filerange[index*2]+=len1;
//Ud记录文g指针到头
myfile.Seek(0,CFile::begin);
//写入记录q度
myfile.Write(&filerange[index*2],sizeof(int));
myfile.Write(&filerange[index*2+1],sizeof(int));
//减去q次ȝ长度
re=re-len1;
//加文仉?
doinfo.totle=doinfo.totle+len1;
};
//q块下蝲完成Q收?
myfile.Close();
destFile.Close();
delete [] m_buf;
shutdown(m_socket,2);
if(re<=0) good[index]=TRUE;
return 1;
}
]]>
通常要开发网l应用程序ƈ不是一件轻杄事情Q不q,实际上只要掌握几个关键的原则也就可以了——创建和q接一个套接字Q尝试进行连接,然后收发数据。真正难的是要写Z个可以接U_则一个,多则数千个连接的|络应用E序。本文将讨论如何通过Winsock2在Windows NT ?Windows 2000上开发高扩展能力的Winsock应用E序。文章主要的焦点在客h/服务器模型的服务器这一方,当然Q其中的许多要点Ҏ(gu)型的双方都适用?
API与响应规?/strong>
通过Win32的重叠I/O机制Q应用程序可以提请一I/O操作Q重叠的操作h在后台完成,而同一旉提请操作的线E去做其他的事情。等重叠操作完成后线E收到有关的通知。这U机制对那些耗时的操作而言特别有用。不q,像Windows 3.1上的WSAAsyncSelect()?qing)Unix下的select()那样的函数虽然易于用,但是它们不能满响应规模的需要。而完成端口机制是针对操作pȝ内部q行了优化,在Windows NT ?Windows 2000上,使用了完成端口的重叠I/O机制才能够真正扩大系l的响应规模?/p>
完成端口
一个完成端口其实就是一个通知队列Q由操作pȝ把已l完成的重叠I/Oh的通知攑օ其中。当某项I/O操作一旦完成,某个可以对该操作l果q行处理的工作者线E就?x)收C则通知。而套接字在被创徏后,可以在Q何时候与某个完成端口q行兌?/p>
通常情况下,我们?x)在应用E序中创Z定数量的工作者线E来处理q些通知。线E数量取决于应用E序的特定需要。理想的情况是,U程数量{于处理器的数量Q不q这也要求Q何线E都不应该执行诸如同步读写、等待事仉知{阻塞型的操作,以免U程d。每个线E都分C定的CPU旉Q在此期间该U程可以q行Q然后另一个线E将分到一个时间片q开始执行。如果某个线E执行了d型的操作Q操作系l将剥夺其未使用的剩余时间片q让其它U程开始执行。也是_(d)前一个线E没有充分用其旉片,当发生这L(fng)情况Ӟ应用E序应该准备其它U程来充分利用这些时间片?/p>
完成端口的用分Z步。首先创建完成端口,如以下代码所C:(x)
HANDLE hIocp; hIocp = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, (ULONG_PTR)0, 0); if (hIocp == NULL) { // Error }完成端口创徏后,要把用该完成端口的套接字与之兌h。方法是再次调用CreateIoCompletionPort ()函数Q第一个参数FileHandle设ؓ(f)套接字的句柄Q第二个参数ExistingCompletionPort 设ؓ(f)刚刚创徏的那个完成端口的句柄?br>以下代码创徏了一个套接字Qƈ把它和前面创建的完成端口兌hQ?
SOCKET s; s = socket(AF_INET, SOCK_STREAM, 0); if (s == INVALID_SOCKET) { // Error if (CreateIoCompletionPort((HANDLE)s, hIocp, (ULONG_PTR)0, 0) == NULL) { // Error } ... }
在创Z完成端口、将一个或多个套接字与之相兌之后Q我们就要创q个U程来处理完成通知。这些线E不断@环调用GetQueuedCompletionStatus ()函数q返回完成通知?/p>
下面Q我们先来看看应用程序如何跟t这些重叠操作。当应用E序调用一个重叠操作函数时Q要把指向一个overlappedl构的指针包括在其参C。当操作完成后,我们可以通过GetQueuedCompletionStatus()函数中拿回这个指针。不q,单是Ҏ(gu)q个指针所指向的overlappedl构Q应用程序ƈ不能分LI竟完成的是哪个操作。要实现Ҏ(gu)作的跟踪Q你可以自己定义一个OVERLAPPEDl构Q在其中加入所需的跟t信息?/p>
无论何时调用重叠操作函数ӞL?x)通过其lpOverlapped参数传递一个OVERLAPPEDPLUSl构(例如WSASend?WSARecv{函?。这允怽为每一个重叠调用操作设|某些操作状态信息,当操作结束后Q你可以通过GetQueuedCompletionStatus()函数获得你自定义l构的指针。注意OVERLAPPED字段不要求一定是q个扩展后的l构的第一个字Dc(din)当得到了指向OVERLAPPEDl构的指针以后,可以用CONTAINING_RECORD宏取出其中指向扩展结构的指针?/p>
OVERLAPPED l构的定义如下:(x)
typedef struct _OVERLAPPEDPLUS { OVERLAPPED ol; SOCKET s, sclient; int OpCode; WSABUF wbuf; DWORD dwBytes, dwFlags; // 其它有用的信? } OVERLAPPEDPLUS; #define OP_READ 0 #define OP_WRITE 1 #define OP_ACCEPT 2下面让我们来看看工作者线E的情况?br>
DWORD WINAPI WorkerThread(LPVOID lpParam) { ULONG_PTR *PerHandleKey; OVERLAPPED *Overlap; OVERLAPPEDPLUS *OverlapPlus, *newolp; DWORD dwBytesXfered; while (1) { ret = GetQueuedCompletionStatus( hIocp, &dwBytesXfered, (PULONG_PTR)&PerHandleKey, &Overlap, INFINITE); if (ret == 0) { // Operation failed continue; } OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol); switch (OverlapPlus->OpCode) { case OP_ACCEPT: // Client socket is contained in OverlapPlus.sclient // Add client to completion port CreateIoCompletionPort( (HANDLE)OverlapPlus->sclient, hIocp, (ULONG_PTR)0, 0); // Need a new OVERLAPPEDPLUS structure // for the newly accepted socket. Perhaps // keep a look aside list of free structures. newolp = AllocateOverlappedPlus(); if (!newolp) { // Error } newolp->s = OverlapPlus->sclient; newolp->OpCode = OP_READ; // This function prepares the data to be sent PrepareSendBuffer(&newolp->wbuf); ret = WSASend( newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp.ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } // Put structure in look aside list for later use FreeOverlappedPlus(OverlapPlus); // Signal accept thread to issue another AcceptEx SetEvent(hAcceptThread); break; case OP_READ: // Process the data read // ... // Repost the read if necessary, reusing the same // receive buffer as before memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED)); ret = WSARecv( OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes, &OverlapPlus->dwFlags, &OverlapPlus->ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } break; case OP_WRITE: // Process the data sent, etc. break; } // switch } // while } // WorkerThread其中每句柄键(PerHandleKey)变量的内容,是在把完成端口与套接字进行关联时所讄的完成键参数QOverlap参数q回的是一个指向发出重叠操作时所使用的那个OVERLAPPEDPLUSl构的指针?
要记住,如果重叠操作调用p|?也就是说Q返回值是SOCKET_ERRORQƈ且错误原因不是WSA_IO_PENDING)Q那么完成端口将不会(x)收到M完成通知。如果重叠操作调用成功,或者发生原因是WSA_IO_PENDING的错误时Q完成端口将L能够收到完成通知?br>
Windows NT和W(xu)indows 2000的套接字架构
对于开发大响应规模的Winsock应用E序而言Q对Windows NT和W(xu)indows 2000的套接字架构有基本的了解是很有帮助的。下图是Windows 2000中的Winsock架构Q?br>
与其它类型操作系l不同,W(xu)indows NT和W(xu)indows 2000的传输协议没有一U风格像套接字那L(fng)、可以和应用E序直接交谈的界面,而是采用了一U更为底层的APIQ叫做传输驱动程序界?Transport Driver Interface,TDI)。Winsock的核心模式驱动程序负责连接和~冲区管理,以便向应用程序提供套接字仿真(在AFD.SYS文g中实?Q同时负责与底层传输驱动E序对话?/p>
谁来负责理~冲区?
正如上面所说的Q应用程序通过Winsock来和传输协议驱动E序交谈Q而AFD.SYS负责为应用程序进行缓冲区理。也是_(d)当应用程序调用send()或WSASend()函数来发送数据时QAFD.SYS把数据拯q它自己的内部缓冲区(取决于SO_SNDBUF讑֮?Q然后send()或WSASend()函数立即q回。也可以q么_(d)AFD.SYS在后台负责把数据发送出厅R不q,如果应用E序要求发出的数据超q了SO_SNDBUF讑֮的缓冲区大小Q那么WSASend()函数?x)阻塞,直至所有数据发送完毕?/p>
从远E客L(fng)接收数据的情况也cM。只要不用从应用E序那里接收大量的数据,而且没有出SO_RCVBUF讑֮的|AFD.SYS把数据先拷贝到其内部缓冲区中。当应用E序调用recv()或WSARecv()函数Ӟ数据从内部~冲拯到应用程序提供的~冲区?/p>
多数情况下,q样的架构运行良好,特别在是应用E序采用传统的套接字下非重叠的send()和receive()模式~写的时候。不q程序员要小心的是,管可以通过setsockopt()q个API来把SO_SNDBUF和SO_RCVBUF选项D?(关闭内部~冲?Q但是程序员必须十分清楚把AFD.SYS的内部缓冲区x?x)造成什么后果,避免收发数据时有关的~冲区拷贝可能引L(fng)pȝ崩溃?/p>
举例来说Q一个应用程序通过讑֮SO_SNDBUF?把缓冲区关闭Q然后发Z个阻塞send()调用。在q样的情况下Q系l内怼(x)把应用程序的~冲区锁定,直到接收方确认收C整个~冲区后send()调用才返回。似乎这是一U判定你的数据是否已lؓ(f)Ҏ(gu)全部收到的简z的Ҏ(gu)Q实际上却ƈ非如此。想想看Q即使远端TCP通知数据已经收到Q其实也Ҏ(gu)不代表数据已l成功送给客户端应用程序,比如Ҏ(gu)可能发生资源不的情况,DAFD.SYS不能把数据拷贝给应用E序。另一个更要紧的问题是Q在每个U程中每ơ只能进行一ơ发送调用,效率极其低下?/p>
把SO_RCVBUF设ؓ(f)0Q关闭AFD.SYS的接收缓冲区也不能让性能得到提升Q这只会(x)q接收到的数据在比Winsock更低的层ơ进行缓Ԍ当你发出receive调用Ӟ同样要进行缓冲区拯Q因此你本来想避免缓冲区拯的阴谋不?x)得逞?/p>
现在我们应该清楚了,关闭~冲区对于多数应用程序而言q不是什么好L。只要要应用E序注意随时在某个连接上保持几个WSARecvs重叠调用Q那么通常没有必要关闭接收~冲区。如果AFD.SYSL有由应用E序提供的缓冲区可用Q那么它?yu)没有必要用内部缓冲区?/p>
高性能的服务器应用E序可以关闭发送缓冲区Q同时不?x)损失性能。不q,q样的应用程序必d分小心,保证它L发出多个重叠发送调用,而不是等待某个重叠发送结束了才发Z一个。如果应用程序是按一个发完再发下一个的序来操作,那浪Ҏ(gu)两次发送中间的I档旉QM是要保证传输驱动E序在发送完一个缓冲区后,立刻可以转向另一个缓冲区?/p>
资源的限制条?/strong>
在设计Q何服务器应用E序Ӟ其强健性是主要的目标。也是_(d)
你的应用E序要能够应对Q何突发的问题Q例如ƈ发客戯求数辑ֈ峰倹{可用内存(f)时出C뀁以?qing)其它短旉的现象。这p求程序的设计者注意Windows NT?000pȝ下的资源限制条g的问题,从容地处理突发性事件?/p>
你可以直接控制的、最基本的资源就是网l带宽。通常Q用用h据报协议(UDP)的应用程序都可能?x)比较注意带宽方面的限制Q以最大限度地减少包的丢失。然而,在用TCPq接Ӟ服务器必d分小心地控制好,防止|络带宽q蝲过一定的旉Q否则将需要重发大量的包或造成大量q接中断。关于带宽管理的Ҏ(gu)应根据不同的应用E序而定Q这出了本文讨论的范围?/p>
虚拟内存的用也必须很小心地理。通过谨慎地申请和释放内存Q或者应用lookaside lists(一U高速缓?技术来重新使用已分配的内存Q将有助于控制服务器应用E序的内存开销(原文?#8220;让服务器应用E序留下的脚印小一?#8221;)Q避免操作系l频J地应用程序申L(fng)物理内存交换到虚拟内存中(原文?#8220;让操作系l能够L把更多的应用E序地址I间更多C留在内存?#8221;)。你也可以通过SetWorkingSetSize()q个Win32 API让操作系l分配给你的应用E序更多的物理内存?/p>
在用Winsock时还可能到另外两个非直接的资源不情况。一个是被锁定的内存面的极限。如果你把AFD.SYS的缓冲关闭,当应用程序收发数据时Q应用程序缓冲区的所有页面将被锁定到物理内存中。这是因为内栔R动程序需要访问这些内存,在此期间q些面不能交换出去。如果操作系l需要给其它应用E序分配一些可分页的物理内存,而又没有_的内存时׃(x)发生问题。我们的目标是要防止写出一个病态的、锁定所有物理内存、让pȝ崩溃的程序。也是_(d)你的E序锁定内存Ӟ不要出pȝ规定的内存分|限?/p>
在Windows NT?000pȝ上,所有应用程序d可以锁定的内存大U是物理内存?/8(不过q只是一个大概的估计Q不是你计算内存的依?。如果你的应用程序不注意q一点,当你的发出太多的重叠收发调用Q而且I/O没来得及(qing)完成Ӟ可能偶?dng)发生ERROR_INSUFFICIENT_RESOURCES的错误。在q种情况下你要避免过度锁定内存。同时要注意Q系l会(x)锁定包含你的~冲区所在的整个内存面Q因此缓冲区靠近边界时是有代h(hun)?译者理解,~冲区如果正好超q页面边界,那怕是1个字节,出的这个字节所在的面也会(x)被锁??/p>
另外一个限制是你的E序可能?x)遇到系l未分页池资源不的情况。所谓未分页池是一块永q不被交换出ȝ内存区域Q这块内存用来存储一些供各种内核lg讉K的数据,其中有的内核lg是不能访问那些被交换出去的页面空间的。Windows NT?000的驱动程序能够从q个特定的未分页池分配内存?/p>
当应用程序创Z个套接字(或者是cM的打开某个文g)Ӟ内核?x)从未分|中分配一定数量的内存Q而且在绑定、连接套接字Ӟ内核又会(x)从未分页池中再分配一些内存。当你注意观察这U行为时你将发现Q如果你发出某些I/Oh?例如收发数据)Q你?x)从未分|里再分配多一些内?比如要追t某个待决的I/O操作Q你可能需要给q个操作d一个自定义l构Q如前文所提及(qing)?。最后这可能会(x)造成一定的问题Q操作系l会(x)限制未分内存的用量?/p>
在Windows NT?000q两U操作系l上Q给每个q接分配的未分页内存的具体数量是不同的,未来版本的Windows很可能也不同。ؓ(f)了应用E序的生命期更长Q你׃应该计算Ҏ(gu)分页池内存的具体需求量?/p>
你的E序必须防止消耗到未分|的极限。当pȝ中未分页池剩余空间太时Q某些与你的应用E序毫无关系的内栔R动就?x)发疯,甚至造成pȝ崩溃Q特别是当系l中有第三方讑֤或驱动程序时Q更Ҏ(gu)发生q样的惨?而且无法预测)。同时你q要CQ同一台电(sh)脑上q可能运行有其它同样消耗未分页池的其它应用E序Q因此在设计你的应用E序Ӟ对资源量的预估要特别保守和}慎?/p>
处理资源不的问题是十分复杂的,因ؓ(f)发生上述情况时你不会(x)收到特别的错误代码,通常你只能收C般性的WSAENOBUFS或者ERROR_INSUFFICIENT_RESOURCES 错误。要处理q些错误Q首先,把你的应用程序工作配|调整到合理的最大?译者注Q所谓工作配|,是指应用E序各部分运行中所需的内存用量,请参?http://msdn.microsoft.com/msdnmag/issues/1000/Bugslayer/Bugslayer1000.asp Q关于内存优化,译者另有译?Q如果错误l出玎ͼ那么注意(g)查是否是|络带宽不的问题。之后,L(fng)认你没有同时发出太多的收发调用。最后,如果q是收到资源不的错误,那就很可能是遇到了未分页内存池不的问题了。要释放未分内存池I间Q请关闭应用E序中相当部分的q接Q等待系l自行渡q和修正q个瞬时的错误?br>
接受q接h
服务器要做的最普通的事情之一是接受来自客户端的q接h。在套接字上使用重叠I/O接受q接的惟一API是AcceptEx()函数。有的是,通常的同步接受函数accept()的返回值是一个新的套接字Q而AcceptEx()函数则需要另外一个套接字作ؓ(f)它的参数之一。这是因为AcceptEx()是一个重叠操作,所以你需要事先创Z个套接字(但不要绑定或q接?Qƈ把这个套接字通过参数传给AcceptEx()。以下是一段典型的用AcceptEx()的伪代码Q?
do { -{待上一?AcceptEx 完成 -创徏一个新套接字ƈ与完成端口进行关? -讄背景l构{等 -发出一?AcceptEx h }while(TRUE);作ؓ(f)一个高响应能力的服务器Q它必须发出_的AcceptEx调用Q守候着Q一旦出现客L(fng)q接hqd应。至于发出多个AcceptEx才够Q就取决于你的服务器E序所期待的通信交通类型。比如,如果q入q接率高的情?因ؓ(f)q接持箋旉较短Q或者出C通高?Q那么所需要守候的AcceptEx当然要比那些偶尔q入的客L(fng)q接的情况要多。聪明的做法是,由应用程序来分析交通状况,q调整AcceptEx守候的数量Q而不是固定在某个数量上?
对于Windows2000QW(xu)insock提供了一些机Ӟ帮助你判定AcceptEx的数量是否够。这是Q在创徏监听套接字时创徏一个事Ӟ通过WSAEventSelect()q个APIq注册FD_ACCEPT事g通知来把套接字和q个事g兌h。一旦系l收C个连接请求,如果pȝ中没有AcceptEx()正在{待接受q接Q那么上面的事g收C个信受通过q个事gQ你可以判断你有没有发够的AcceptEx()Q或者检出一个非正常的客戯?下文q?。这U机制对Windows NT 4.0不适用?/p>
使用AcceptEx()的一大好处是Q你可以通过一ơ调用就完成接受客户端连接请求和接受数据(通过传送lpOutputBuffer参数)两g事情。也是_(d)如果客户端在发出q接的同时传输数据,你的AcceptEx()调用在连接创建ƈ接收了客L(fng)数据后就可以立刻q回。这样可能是很有用的Q但是也可能?x)引发问题,因?f)AcceptEx()必须{全部客L(fng)数据都收C才返回。具体来_(d)如果你在发出AcceptEx()调用的同时传递了lpOutputBuffer参数Q那么AcceptEx()不再是一原子型的操作,而是分成了两步:(x)接受客户q接Q等待接收数据。当~少一U机制来通知你的应用E序所发生的这U情况:(x)“q接已经建立了,正在{待客户端数?#8221;Q这意味着有可能出现客L(fng)只发接请求,但是不发送数据。如果你的服务器收到太多q种cd的连接时Q它?yu)拒l连接更多的合法客户端请求。这是黑客q行“拒绝服务”d的常见手法?/p>
要预防此cL击,接受q接的线E应该不时地通过调用getsockopt()函数(选项参数为SO_CONNECT_TIME)来检查AcceptEx()里守候的套接字。getsockopt()函数的选项值将被设|ؓ(f)套接字被q接的时_(d)或者设|ؓ(f)-1(代表套接字尚未徏立连?。这ӞW(xu)SAEventSelect()的特性就可以很好地利用来做这U检查。如果发现连接已l徏立,但是很久都没有收到数据的情况Q那么就应该l止q接Q方法就是关闭作为参数提供给AcceptEx()的那个套接字。注意,在多数非紧急情况下Q如果套接字已经传递给AcceptEx()q开始守候,但还未徏立连接,那么你的应用E序不应该关闭它们。这是因为即使关闭了q些套接字,Z提高pȝ性能的考虑Q在q接q入之前Q或者监听套接字自n被关闭之前,相应的内核模式的数据l构也不?x)被q净地清除?/p>
发出AcceptEx()调用的线E,g与那个进行完成端口关联操作、处理其它I/O完成通知的线E是同一个,但是Q别忘记U程里应该尽力避免执行阻塞型的操作。Winsock2分层l构的一个副作用是调用socket()或WSASocket() API的上层架构可能很重要(译者不太明白原文意思,抱歉)。每个AcceptEx()调用都需要创Z个新套接字,所以最好有一个独立的U程专门调用AcceptEx()Q而不参与其它I/O处理。你也可以利用这个线E来执行其它dQ比如事件记录?/p>
有关AcceptEx()的最后一个注意事:(x)要实现这些APIQƈ不需要其它提供商提供的Winsock2实现。这一点对微YҎ(gu)的其它API也同样适用Q比如TransmitFile()和GetAcceptExSockAddrs()Q以?qing)其它可能?x)被加入到新版Windows的API. 在Windows NT?000上,q些API是在微Y的底层提供者DLL(mswsock.dll)中实现的Q可通过与mswsock.lib~译q接q行调用Q或者通过WSAIoctl() (选项参数为SIO_GET_EXTENSION_FUNCTION_POINTER)动态获得函数的指针?/p>
如果在没有事先获得函数指针的情况下直接调用函?也就是说Q编译时静态连接mswsock.libQ在E序中直接调用函?Q那么性能很受媄响。因为AcceptEx()被置于Winsock2架构之外Q每ơ调用时它都被迫通过WSAIoctl()取得函数指针。要避免q种性能损失Q需要用这些API的应用程序应该通过调用WSAIoctl()直接从底层的提供者那里取得函数的指针?br>
参见下图套接字架构:(x)
TransmitFile ?TransmitPackets
Winsock 提供两个专门为文件和内存数据传输q行了优化的函数。其中TransmitFile()q个API函数在Windows NT 4.0 ?Windows 2000上都可以使用Q而TransmitPackets()则将在未来版本的Windows中实现?/p>
TransmitFile()用来把文件内定w过Winsockq行传输。通常发送文件的做法是,先调用CreateFile()打开一个文Ӟ然后不断循环调用ReadFile() 和W(xu)SASend ()直至数据发送完毕。但是这U方法很没有效率Q因为每ơ调用ReadFile() ?WSASend ()都会(x)涉及(qing)一ơ从用户模式到内核模式的转换。如果换成TransmitFile()Q那么只需要给它一个已打开文g的句柄和要发送的字节敎ͼ而所涉及(qing)的模式{换操作将只在调用CreateFile()打开文g时发生一ơ,然后TransmitFile()时再发生一ơ。这h率就高多了?/p>
TransmitPackets()比TransmitFile()更进一步,它允许用户只调用一ơ就可以发送指定的多个文g和内存缓冲区。函数原型如下:(x)
BOOL TransmitPackets( SOCKET hSocket, LPTRANSMIT_PACKET_ELEMENT lpPacketArray, DWORD nElementCount, DWORD nSendSize, LPOVERLAPPED lpOverlapped, DWORD dwFlags );其中QlpPacketArray是一个结构的数组Q其中的每个元素既可以是一个文件句柄或者内存缓冲区Q该l构定义如下Q?br>
typedef struct _TRANSMIT_PACKETS_ELEMENT { DWORD dwElFlags; DWORD cLength; union { struct { LARGE_INTEGER nFileOffset; HANDLE hFile; }; PVOID pBuffer; }; } TRANSMIT_FILE_BUFFERS;其中各字D|自描q型?self explanatory)?br>dwElFlags字段Q指定当前元素是一个文件句柄还是内存缓冲区(分别通过帔RTF_ELEMENT_FILE 和TF_ELEMENT_MEMORY指定)Q?br>cLength字段Q指定将从数据源发送的字节?如果是文Ӟq个字段gؓ(f)0表示发送整个文?Q?br>l构中的无名联合体:(x)包含文g句柄的内存缓冲区(以及(qing)可能的偏U量)?
使用q两个API的另一个好处,是可以通过指定TF_REUSE_SOCKET和TF_DISCONNECT标志来重用套接字句柄。每当API完成数据的传输工作后Q就?x)在传输层别断开q接Q这栯个套接字又可以重新提供lAcceptEx()使用。采用这U优化的Ҏ(gu)~程Q将减轻那个专门做接受操作的U程创徏套接字的压力(前文q及(qing))?/p>
q两个API也都有一个共同的qQWindows NT Workstation ?Windows 2000 专业版中Q函数每ơ只能处理两个调用请求,只有在Windows NT、Windows 2000服务器版、Windows 2000高服务器版?Windows 2000 Data Center中才获得完全支持?/p>
攑֜一L(fng)?/strong>
以上各节中,我们讨论了开发高性能的、大响应规模的应用程序所需的函数、方法和可能遇到的资源瓶颈问题。这些对你意味着什么呢Q其实,q取决于你如何构造你的服务器和客L(fng)。当你能够在服务器和客户端设计上q行更好地控制时Q那么你能够避开瓉问题?/p>
来看一个示范的环境。我们要设计一个服务器来响应客L(fng)的连接、发送请求、接收数据以?qing)断开q接。那么,服务器将需要创Z个监听套接字Q把它与某个完成端口q行兌Qؓ(f)每颗CPU创徏一个工作线E。再创徏一个线E专门用来发出AcceptEx()。我们知道客L(fng)?x)在发出q接h后立M送数据,所以如果我们准备好接收~冲Z(x)使事情变得更为容易。当Ӟ不要忘记不时地轮询AcceptEx()调用中用的套接?使用SO_CONNECT_TIME选项参数)来确保没有恶意超时的q接?/p>
该设计中有一个重要的问题要考虑Q我们应该允许多个AcceptEx()q行守候。这是因为,每发Z个AcceptEx()时我们都同时需要ؓ(f)它提供一个接收缓冲区Q那么内存中会(x)出现很多被锁定的面(前文说过了,每个重叠操作都会(x)消耗一部分未分页内存池,同时q会(x)锁定所有涉?qing)的~冲?。这个问题很隑֛{,没有一个确切的{案。最好的Ҏ(gu)是把q个值做成可以调整的Q通过反复做性能试Q你可以得出在典型应用环境中最佳的倹{?/p>
好了Q当你测清楚后Q下面就是发送数据的问题了,考虑的重Ҏ(gu)你希望服务器同时处理多少个ƈ发的q接。通常情况下,服务器应该限制ƈ发连接的数量以及(qing){候处理的发送调用。因为ƈ发连接数量越多,所消耗的未分内存池也越多;{候处理的发送调用越多,被锁定的内存面也越?心别超q了极限)。这同样也需要反复测试才知道{案?/p>
对于上述环境Q通常不需要关闭单个套接字的缓冲区Q因为只在AcceptEx()中有一ơ接收数据的操作Q而要保证l每个到来的q接提供接收~冲区ƈ不是太难的事情。但是,如果客户Z服务器交互的方式变一变,客户机在发送了一ơ数据之后,q需要发送更多的数据Q在q种情况下关闭接收缓冲就不太妙了Q除非你惛_法保证在每个q接上都发出了重叠接收调用来接收更多的数据?/p>
l论
开发大响应规模的Winsock服务器ƈ不是很可怕,其实也就是设|一个监听套接字、接受连接请求和q行重叠收发调用。通过讄合理的进行守候的重叠调用的数量,防止出现未分内存池被耗尽Q这才是最主要的挑战。按照我们前面讨论的一些原则,你就可以开发出大响应规模的服务器应用程序?br>
下面我给出支持单个Socket?qing)支持多个Socket的ConsoleE序代码。先来看看支持单个Socket的程?考虑C码简z性,只给一个框Ӟ同时不进行出错处理?br>int main()
{
WSAOVERLAPPED Overlapped;
// 启动Winsock?qing)徏立监听套接?br> listen(hListenSocket, 5);
hClientSocket = accept(hListenSocket, NULL, NULL);
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
nResult = WSARecv(...); // 发出h
for (; ;)
{
bResult = WSAGetOverlappedResult(...);
// 函数q回后进行相应的处理
nResult = WSARecv(...); // 发出另外一个请?br> }
}
上面的程序只是想说明一下过E,E序没有实现什么功能。这样做的目的是节约字数Q用来说明我下面支持多个Socket的Console应用。请l箋看?br> 先看一个自定义的结构体Q单句柄数据l构Q通过该结?ȝE与某个特定的子U程中的套接字相互联pR?br> typedef struct _PER_HANDLE_DATA
{
SOCKET hSocket; // 主键:通信套接?br> char szClientIP[16];// 自定义字D?客户端地址
int nOperateType; // 自定义字D?操作cd
}PER_HANDLE_DATA, FAR* LPPER_HANDLE_DATA;
在上面的l构中还可以加入自己需要的字段。在我下面的例子E序中,szClientIP是用来保存连接上来的客户端的IP的,q样在主U程这个结构体传给子线E后Q在子线E中Ҏ(gu)客户端IPq道目前处理的是哪个客L(fng)了。下面是E序的大部分Q同样除M些简单的出错输出?br>#define LISTEN_PORT 5000
#define DATA_BUFSIZE 8192
#define POST_RECV 0X01
#define POST_SEND 0X02
int main( )
{
LPPER_HANDLE_DATA lpPerHandleData;
SOCKET hListenSocket;
SOCKET hClientSocket;
SOCKADDR_IN ClientAddr;
int nAddrLen;
HANDLE hThread;
// Start WinSock and create a listen socket.
listen(hListenSocket, 5);
for (; ;)
{
nAddrLen = sizeof(SOCKADDR);
hClientSocket = accept(hListenSocket, (LPSOCKADDR)&ClientAddr, &nAddrLen);
lpPerHandleData = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
lpPerHandleData->hSocket = hClientSocket;
// 注意q里连接的客户端的IP地址Q保存到了lpPerHandleData字段中了
strcpy(lpPerHandleData->szClientIP, inet_ntoa(ClientAddr.sin_addr));
// 为本ơ客戯求生子U程
hThread = CreateThread(
NULL,
0,
OverlappedThread,
lpPerHandleData, // lpPerHandleData传给子线E?br> 0,
NULL
);
CloseHandle(hThread);
}
return 0;
}
DWORD WINAPI OverlappedThread(LPVOID lpParam)
{
LPPER_HANDLE_DATA lpPerHandleData = (LPPER_HANDLE_DATA)lpParam;
WSAOVERLAPPED Overlapped;
WSABUF wsaBuf;
char Buffer[DATA_BUFSIZE];
BOOL bResult;
int nResult;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
wsaBuf.buf = Buffer;
wsaBuf.len = sizeof(Buffer);
lpPerHandleData->nOperateType = POST_RECV; // 记录本次操作是Recv(..)
dwFlags = 0;
nResult = WSARecv(
lpPerHandleData->hSocket, // Receive socket
&wsaBuf, // 指向WSABUFl构的指?br> 1, // WSABUF数组的个?br> &dwNumOfBytesRecved, // 存放当WSARecv完成后所接收到的字节?br> &dwFlags, // A pointer to flags
&Overlapped, // A pointer to a WSAOVERLAPPED structure
NULL // A pointer to the completion routine,this is NULL
);
if ( nResult == SOCKET_ERROR && GetLastError() != WSA_IO_PENDING)
{
printf("WSARecv(..) failed.\n");
free(lpPerHandleData);
return 1;
}
while (TRUE)
{
bResult = WSAGetOverlappedResult(
lpPerHandleData->hSocket,
&Overlapped,
&dwBytesTransferred, // 当一个同步I/O完成?接收到的字节?br> TRUE, // {待I/O操作的完?
&dwFlags
);
if (bResult == FALSE && WSAGetLastError() != WSA_IO_INCOMPLETE)
{
printf("WSAGetOverlappdResult(..) failed.\n");
free(lpPerHandleData);
return 1; // 错误退?br> }
if (dwBytesTransferred == 0)
{
printf("客户端已退?断开与之的连?\n");
closesocket(lpPerHandleData->hSocket);
free(lpPerHandleData);
break;
}
// 在这里将接收到的数据q行处理
printf("Received from IP: %s.\ndata: %s\n", lpPerHandleData->szClientIP, wsaBuf.buf);
// 发送另外一个请求操?br> ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
lpPerHandleData->nOperateType = POST_RECV;
dwFlags = 0;
nResult = WSARecv(...);
if (...){}
}
return 0;
}
E序l构比较清晰QlpPerHandleData是主U程与子U程联系的纽带,子线E是通过q个l构获得所处理客户端的情况的。在不同的应用中可以这个结构定义成不同的Ş式,以符合所实现应用的需要。注意结构体的nOperateType在GetOverlappedResultq回时用?可以Ҏ(gu)q个字段定我们下一步的操作。请朋友们多提意见了?/font>
“完成端口”模型是q今为止最为复杂的一UI/O模型。然而,假若一个应用程序同旉要管理ؓ(f)C多的套接字,那么采用q种模型Q往往可以辑ֈ最佳的pȝ性能Q?/p>
从本质上_(d)完成端口模型要求我们创徏一个Win32完成端口对象Q通过指定数量的线E,寚w叠I/Ohq行理Q以便ؓ(f)已经完成的重叠I/Oh提供服务?/p>
使用q种模型之前Q首先要创徏一个I/O完成端口对象Q用它面向Q意数量的套接字句柄,理多个I/Oh。要做到q一点,需要调用CreateCompletionPort函数?br />该函数定义如下:(x)
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
在我们深入探讨其中的各个参数之前Q首先要注意该函数实际用于两个明显有别的目的Q?br />1. 用于创徏一个完成端口对象?br />2. 一个句柄同完成端口兌C赗?
最开始创Z个完成端口时Q唯一感兴的参数便是NumberOfConcurrentThreadsQƈ发线E的数量Q;前面三个参数都会(x)被忽略。NumberOfConcurrentThreads参数的特D之处在于,它定义了在一个完成端口上Q同时允许执行的U程数量。理x况下Q我们希望每个处理器各自负责一个线E的q行Qؓ(f)完成端口提供服务Q避免过于频J的U程“场景”切换。若该参数设ؓ(f)0Q表明系l内安装了多个处理器,便允许同时运行多个U程Q可用下qC码创Z个I/O完成端口Q?/p>
hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
该语句的作用是返回一个句柄,在ؓ(f)完成端口分配了一个套接字句柄后,用来寚w个端口进行标定(引用Q?
一、工作者线E与完成端口
成功创徏一个完成端口后Q便可开始将套接字句柄与对象兌C赗但在关联套接字之前Q首先必dZ个或多个“工作者线E”,以便在I/Oh投递给完成端口对象后,为完成端口提供服务。在q个时候,大家或许?x)觉得奇怪,到底应创建多个U程Q以便ؓ(f)完成端口提供服务呢?q实际正是完成端口模型显得颇为“复杂”的一个方面,因ؓ(f)服务I/Oh所需的数量取决于应用E序的M设计情况。在此要C的一个重点在于,在我们调用CreateIoCompletionPort时指定的q发U程数量Q与打算创徏的工作者线E数量相比,它们代表的ƈ非同一件事情。早些时候,我们曑־议大家用CreateIoCompletionPort函数为每个处理器
都指定一个线E(处理器的数量有多,便指定多线E)以避免由于频J的U程“场景”交换活动,从而媄响系l的整体性能。CreateIoCompletionPort函数的NumberOfConcurrentThreads参数明确指示pȝQ在一个完成端口上Q一ơ只允许n个工作者线E运行。假如在完成端口上创建的工作者线E数量超出n个,那么在同一时刻Q最多只允许n个线E运行。但实际上,在一D较短的旉内,pȝ有可能超q这个|但很快便?x)把它减至事先在CreateIoCompletionPort函数中设定的倹{那么,Z实际创徏的工作者线E数量有时要比CreateIoCompletionPort函数讑֮的多一些呢Q这样做有必要吗Q如先前所qͼq主要取决于
应用E序的M设计情况。假定我们的某个工作者线E调用了一个函敎ͼ比如Sleep或WaitForSingleObjectQ但却进入了暂停Q锁定或挂vQ状态,那么允许另一个线E代替它的位|。换a之,我们希望随时都能执行可能多的线E;当然Q最大的U程数量是事先在CreateIoCompletionPort调用里设定好的。这样一来,假如事先预计到自qU程有可能暂时处于停状态,那么最好能够创建比CreateIoCompletionPort的NumberOfConcurrentThreads参数的值多的线E,以便到时候充分发挥系l的潜力。一旦在完成端口上拥有够多的工作者线E来为I/Oh提供服务Q便可着手将套接字句柄同完成端口兌C赗这要求我们在一个现有的完成端口上,调用CreateIoCompletionPort函数Q同时ؓ(f)前三个参数——FileHandleQExistingCompletionPort和CompletionKey——提供套接字的信息。其中, FileHandle参数指定一个要同完成端口关联在一L(fng)套接字句柄。ExistingCompletionPort参数指定的是一个现有的完成端口。CompletionKeyQ完成键Q参数则指定要与某个特定套接字句柄关联在一L(fng)“单句柄数据”;在这个参CQ应用程序可保存与一个套接字对应的Q意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对
应着与那个套接字句柄兌在一L(fng)数据。可其作ؓ(f)指向一个数据结构的指针Q来保存套接字句柄;在那个结构中Q同时包含了套接字的句柄Q以?qing)与那个套接字有关的其他信息?/p>
Ҏ(gu)我们到目前ؓ(f)止学到的东西Q首先来构徏一个基本的应用E序框架。下面阐qC如何使用完成端口模型Q来开发一个ECHO服务器应用。在q个E序中,我们基本上按下述步骤行事Q?/p>
1) 创徏一个完成端口。第四个参数保持?Q指定在完成端口上,每个处理器一ơ只允许执行一个工作者线E?br />2) 判断pȝ内到底安装了多少个处理器?br />3) 创徏工作者线E,Ҏ(gu)步骤2)得到的处理器信息Q在完成端口上,为已完成的I/Oh提供服务?br />4) 准备好一个监听套接字Q在端口5150上监听进入的q接h?br />5) 使用accept函数Q接受进入的q接h?br />6) 创徏一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄?br />7) 调用CreateIoCompletionPortQ将自acceptq回的新套接字句柄同完成端口兌C赗通过完成键(CompletionKeyQ参敎ͼ单句柄数据l构传递给C(j)reateIoCompletionPort?br />8) 开始在已接受的q接上进行I/O操作。在此,我们希望通过重叠I/O机制Q在新徏的套接字上投递一个或多个异步WSARecv或WSASendh。这些I/Oh完成后,一个工作者线E会(x)为I/Oh提供服务Q同时l处理未来的I/OhQ稍后便?x)在步? )指定的工作者例E中Q体验到q一炏V?br />9) 重复步骤5 ) ~ 8 )Q直x务器中止?/p>
二、完成端口和重叠I/O
套接字句柄与一个完成端口关联在一起后Q便可以套接字句柄ؓ(f)基础Q投递发送与接收hQ开始对I/Oh的处理。接下来Q可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说Q完成端口模型利用了Win32重叠I/O机制。在q种机制中,象WSASend和W(xu)SARecvq样的Winsock API调用?x)立卌回。此Ӟ需要由我们的应用程序负责在以后的某个时_(d)通过一个OVERLAPPEDl构Q来接收调用的结果。在完成端口模型中,要想做到q一点,需要用GetQueuedCompletionStatusQ获取排队完成状态)函数Q让一个或多个工作者线E在完成端口上等待。该函数的定义如下:(x)
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds
);
其中QCompletionPort参数对应于要在上面等待的完成端口。lpNumberOfBytes参数负责在完成了一ơI/O操作后(如WSASend或WSARecvQ,接收实际传输的字节数。lpCompletionKey参数为原先传递进入CreateIoCompletionPort函数的套接字q回“单句柄数据”。如我们早先所qͼ大家最好将套接字句柄保存在q个“键”(KeyQ中。lpOverlapped参数用于接收完成的I/O操作的重叠结果。这实际是一个相当重要的参数Q因为可用它获取每个I/O操作的数据。而最后一个参敎ͼdwMillisecondsQ用于指定调用者希望等待一个完成数据包在完成端口上出现的时间。假如将其设为INFINITEQ调用会(x)无休止地{待下去?
三、单句柄数据和单I/O操作数据
一个工作者线E从GetQueuedCompletionStatusq个API调用接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,?x)包含一些必要的套接字信息。利用这些信息,可通过完成端口Ql在一个套接字上的I/O处理。通过q些参数Q可获得两方面重要的套接字数据:(x)单句柄数据,以及(qing)单I/O操作数据。其中,lpCompletionKey参数包含了“单句柄数据”,因ؓ(f)在一个套接字首次与完成端口关联到一L(fng)时候,那些数据便与一个特定的套接字句柄对应v来了。这些数据正是我们在q行CreateIoCompletionPort API调用的时候,通过CompletionKey参数传递的。如早先所qͼ应用E序可通过该参C递Q意类型的数据。通常情况下,应用E序?x)将与I/Oh有关的套接字句柄保存在这里。lpOverlapped参数则包含了一个OVERLAPPEDl构Q在它后面跟随“单I/O操作数据”。我们的工作者线E处理一个完成数据包Ӟ数据原不动打转回去,接受q接Q投递另一个线E,{等Q,q些信息是它必须要知道的。单I/O操作数据可以是追加到一个OVERLAPPEDl构末尾的、Q意数量的字节。假如一个函数要求用C个OVERLAPPEDl构Q我们便必须这L(fng)一个结构传递进去,以满_的要求。要惛_到这一点,一个简单的Ҏ(gu)是定义一个结构,然后OVERLAPPEDl构作ؓ(f)新结构的W一个元素用。D个例子来_(d)可定义下q数据结构,实现对单I/O操作数据的管理:(x)
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
BOOL OperationType;
}PER_IO_OPERATION_DATA
该结构演CZ通常要与I/O操作兌在一L(fng)某些重要数据元素Q比如刚才完成的那个I/O操作的类型(发送或接收hQ。在q个l构中,我们认ؓ(f)用于已完成I/O操作的数据缓冲区是非常有用的。要惌用一个Winsock API函数Q同时ؓ(f)其分配一个OVERLAPPEDl构Q既可将自己的结构“造型”ؓ(f)一个OVERLAPPED指针Q亦可简单地撤消对结构中的OVERLAPPED元素的引用。如下例所C:(x)
PER_IO_OPERATION_DATA PerIoData;
// 可像下面q样调用一个函?br /> WSARecv(socket, ..., (OVERLAPPED *)&PerIoData);
// 或像q样
WSARecv(socket, ..., &(PerIoData.Overlapped));
在工作线E的后面部分Q等GetQueuedCompletionStatus函数q回了一个重叠结构(和完成键Q后Q便可通过撤消对OperationType成员的引用,调查到底是哪个操作投递到了这个句柄之上(只需返回的重叠l构造型qPER_IO_OPERATION_DATAl构Q。对单I/O操作数据来说Q它最大的一个优点便是允许我们在同一个句柄上Q同时管理多个I/O操作Q读/写,多个读,多个写,{等Q。大家此时或怼(x)产生q样的疑问:(x)在同一个套接字上,真的有必要同时投递多个I/O操作吗?{案在于pȝ的“~性”,或者说“扩展能力”。例如,假定我们的机器安装了多个中央处理器,每个处理器都在运行一个工作者线E,那么在同一个时
候,完全可能有几个不同的处理器在同一个套接字上,q行数据的收发操作?
最后要注意的一处细节是如何正确地关闭I/O完成端口—特别是同时q行了一个或多个U程Q在几个不同的套接字上执行I/O操作的时候。要避免的一个重要问题是在进行重叠I/O操作的同Ӟ释放一个OVERLAPPEDl构。要想避免出现这U情况,最好的办法是针Ҏ(gu)个套接字句柄Q调用closesocket函数QQ何尚未进行的重叠I/O操作都会(x)完成。一旦所有套接字句柄都已关闭Q便需在完成端口上Q终止所有工作者线E的q行。要惛_到这一点, 需要用PostQueuedCompletionStatus函数Q向每个工作者线E都发送一个特D的完成数据包。该函数?x)指C每个线E都“立即结束ƈ退出”。下面是PostQueuedCompletionStatus函数的定义:(x)
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
其中QCompletionPort参数指定惛_其发送一个完成数据包的完成端口对象。而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlappedq三个参数来_(d)每一个都允许我们指定一个|直接传递给GetQueuedCompletionStatus函数中对应的参数。这样一来,一个工作者线E收C递过来的三个GetQueuedCompletionStatus函数参数后,便可Ҏ(gu)p三个参数的某一个设|的Ҏ(gu)|军_何时应该退出。例如,可用dwCompletionPort参数传?|而一个工作者线E会(x)其解释成中止指令。一旦所有工作者线E都已关闭,便可使用CloseHandle函数Q关闭完成端口,最l安全退出程序?/p>
注:(x)CreateIoCompletionPort QPostQueuedCompletionStatus QGetQueuedCompletionStatus {函数的用法说明?/p>
Platform SDK: Storage
I/O Completion Ports
I/O completion ports are the mechanism by which an application uses a pool of threads that was created when the application was started to process asynchronous I/O requests. These threads are created for the sole purpose of processing I/O requests. Applications that process many concurrent asynchronous I/O requests can do so more quickly and efficiently by using I/O completion ports than by using creating threads at the time of the I/O request.
I/O完成端口(s)是一U机Ӟ通过q个机制Q应用程序在启动时会(x)首先创徏一个线E池Q然后该应用E序使用U程池处理异步I/Oh。这些线E被创徏的唯一目的是用于处理I/Oh。对于处理大量ƈ发异步I/Oh的应用程序来_(d)相比于在I/Oh发生时创建线E来_(d)使用完成端口(s)它就可以做的更快且更有效率?
The CreateIoCompletionPort function associates an I/O completion port with one or more file handles. When an asynchronous I/O operation started on a file handle associated with a completion port is completed, an I/O completion packet is queued to the port. This can be used to combine the synchronization point for multiple file handles into a single object.
CreateIoCompletionPort函数?x)一个I/O完成端口与一个或多个文g句柄发生兌。当与一个完成端口相关的文g句柄上启动的异步I/O操作完成Ӟ一个I/O完成包就?x)进入到该完成端口的队列中。对于多个文件句柄来_(d)可以把q些多个文g句柄合ƈ成一个单独的对象Q这个可以被用来l合同步点?
A thread uses the GetQueuedCompletionStatus function to wait for a completion packet to be queued to the completion port, rather than waiting directly for the asynchronous I/O to complete. Threads that block their execution on a completion port are released in last-in-first-out (LIFO) order. This means that when a completion packet is queued to the completion port, the system releases the last thread to block its execution on the port.
调用GetQueuedCompletionStatus函数Q某个线E就?x)等待一个完成包q入到完成端口的队列中,而不是直接等待异步I/Oh完成。线E(们)׃(x)d于它们的q行在完成端口(按照后进先出队列序的被释放Q。这意味着当一个完成包q入到完成端口的队列中时Q系l会(x)释放最q被d在该完成端口的线E?
When a thread calls GetQueuedCompletionStatus, it is associated with the specified completion port until it exits, specifies a different completion port, or frees the completion port. A thread can be associated with at most one completion port.
调用GetQueuedCompletionStatusQ线E就?x)将会(x)与某个指定的完成端口徏立联p,一直gl其该线E的存在周期Q或被指定了不同的完成端口,或者释放了与完成端口的联系。一个线E只能与最多不过一个的完成端口发生联系?
The most important property of a completion port is the concurrency value. The concurrency value of a completion port is specified when the completion port is created. This value limits the number of runnable threads associated with the completion port. When the total number of runnable threads associated with the completion port reaches the concurrency value, the system blocks the execution of any subsequent threads that specify the completion port until the number of runnable threads associated with the completion port drops below the concurrency value. The most efficient scenario occurs when there are completion packets waiting in the queue, but no waits can be satisfied because the port has reached its concurrency limit. In this case, when a running thread calls GetQueuedCompletionStatus, it will immediately pick up the queued completion packet. No context switches will occur, because the running thread is continually picking up completion packets and the other threads are unable to run.
完成端口最重要的特性就是ƈ发量。完成端口的q发量可以在创徏该完成端口时指定。该q发量限制了与该完成端口相关联的可运行线E的数目。当与该完成端口相关联的可运行线E的L目达C该ƈ发量Q系l就?x)阻塞Q何与该完成端口相兌的后l线E的执行Q直C该完成端口相兌的可q行U程数目下降到小于该q发量ؓ(f)止。最有效的假x发生在有完成包在队列中等待,而没有等待被满Q因为此时完成端口达C其ƈ发量的极限。此Ӟ一个正在运行中的线E调用GetQueuedCompletionStatusӞ它就?x)立M队列中取走该完成包。这样就不存在着环境的切换,因ؓ(f)该处于运行中的线E就?x)连l不断地从队列中取走完成包,而其他的U程׃能运行了?
The best value to pick for the concurrency value is the number of CPUs on the machine. If your transaction required a lengthy computation, a larger concurrency value will allow more threads to run. Each transaction will take longer to complete, but more transactions will be processed at the same time. It is easy to experiment with the concurrency value to achieve the best effect for your application.
对于q发量最好的挑选值就是?zhn)计算Zcpu的数目。如果?zhn)的事务处理需要一个O长的计算旉Q一个比较大的ƈ发量可以允许更多U程来运行。虽然完成每个事务处理需要花Ҏ(gu)长的旉Q但更多的事务可以同时被处理。对于应用程序来_(d)很容易通过试q发量来获得最好的效果?
The PostQueuedCompletionStatus function allows an application to queue its own special-purpose I/O completion packets to the completion port without starting an asynchronous I/O operation. This is useful for notifying worker threads of external events.
PostQueuedCompletionStatus函数允许应用E序可以针对自定义的专用I/O完成包进行排队,而无需启动一个异步I/O操作。这点对于通知外部事g的工作者线E来说很有用?
The completion port is freed when there are no more references to it. The completion port handle and every file handle associated with the completion port reference the completion port. All the handles must be closed to free the completion port. To close the port handle, call the CloseHandle function.
在没有更多的引用针对某个完成端口Ӟ需要释放该完成端口。该完成端口句柄以及(qing)与该完成端口相关联的所有文件句柄都需要被释放。调用CloseHandle可以释放完成端口的句柄?