关于开发高性能、大负载网络服务端问题的讨论

TOMWLD 2004-12-16 03:52:27
前段时间用indy做了一套即时通讯系统,由于indy是基于阻塞式和线程的,一直担心它的负载能力,昨天写了一个测试程序,发现在开启不到3000个客户端到服务器,服务端即会崩溃,其根本在于indy是阻塞式的,每一个客户端连接在服务端就会启动一个线程。在网上搜索资料,都提到indy的阻塞模式在windows下难以胜任服务大量客户端的环境。
不知以上我对indy的理解是否正确,对于这种开发在系统架构上有何不足,或者用什么技术可以解决这个问题,请大家指教。
...全文
529 41 打赏 收藏 转发到动态 举报
写回复
用AI写文章
41 条回复
切换为时间正序
请发表友善的回复…
发表回复
getit911 2005-01-14
  • 打赏
  • 举报
回复
IdWorkOpUnit.pas
{
{ Rev 1.15 2003.07.17 4:42:06 PM czhower
{ More IOCP improvements.
}
安装supercore包,偶在delphi2005/D7上都可以安装,没时间测试
TOMWLD 2005-01-14
  • 打赏
  • 举报
回复
没看出indy10对iocp的支持
getit911 2005-01-06
  • 打赏
  • 举报
回复
Indy10支持IOCP和纤程了(纤程对效率好象没帮助),要安装supercore包,楼主有机会测试一下。
alphax 2005-01-04
  • 打赏
  • 举报
回复
>>其根本在于indy是阻塞式的,每一个客户端连接在服务端就会启动一个线程
很自然的,通用组件就是这样的,indy也有线城池,并且也不会有什么效率上的很可怕的开销,
就算你用complete proc,也还是线城池的技术,不过是系统提供的罢了

没有必要考虑那么多,3000个客户,什么软件都搞不定的,不要把系统复杂化,indy已经很好了
增加服务器吧

nyf1220 2005-01-04
  • 打赏
  • 举报
回复
一定要在技术方面来分减负担啊,能不能考虑增加服务器来减少负担。。
呵呵,没弄过,不知道讲的对不对

类似qq的,就是多服务器
masuz1 2005-01-04
  • 打赏
  • 举报
回复
// 开始创建监听Socket...
if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("WSASocket() failed with error %d\n", WSAGetLastError());
return;
}

InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(PORT);

// 把监听socket和绑定TCP/IP协议栈做一个绑定...
if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
{
printf("bind() failed with error %d\n", WSAGetLastError());
return;
}

// 设置一次能接受5个连接
if (listen(Listen, 5) == SOCKET_ERROR)
{
printf("listen() failed with error %d\n", WSAGetLastError());
return;
}

//
// 下面是服务器程序标准的循环了...
//
while(TRUE)
{
if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
{
printf("WSAAccept() failed with error %d\n", WSAGetLastError());
return;
}

//
// 接收到一个连接就new一个新的socket并承揽下连接进来的socket的接下来的接待/处理工作
// 为什么不用new呢?我想是因为用Windows提供的内存管理函数,这样的话在业务量大的情况下
// 出现内存匮乏的情况下不至于直接就异常了,怎么着也就是该次的内存分配失败...
//
if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return;
}

// Associate the accepted socket with the original completion port.

printf("Socket number %d connected\n", Accept);
PerHandleData->Socket = Accept;

//
// 把accept socket和IO完成端口关联/绑定起来,并把accept socket传给IOCP机制这样的话
// 当调用GetQueuedCompletionStatus的时候就可以得到该值.
//
if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
0) == NULL)
{
printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
return;
}

// 下面这个结构体也是会被传到工作线程中去的,怎么传呢?注意看下面红色部分
if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return;
}

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
PerIoData->BytesSEND = 0;
PerIoData->BytesRECV = 0;
PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;

Flags = 0;

//
// 在accept socket上面发起IO请求:WSARecv()以此激发IOCP工作机制..
// 注意WSARecv的第五个参数,也就是说我这时候告诉WSARecv的不仅仅是
// PerIoData->Overlapped的地址同时也是PerIoData的地址...
//
if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return;
}
}
}
}

//
// 工作线程
//
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort = (HANDLE) CompletionPortID;
DWORD BytesTransferred;
LPOVERLAPPED Overlapped;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;
DWORD SendBytes, RecvBytes;
DWORD Flags;

while(TRUE)
{

//
// 这个函数解释清楚了整个机制也就明白了...
// 首先我们看CompletionPort应该没有问题,是从创建线程的主线程传过来的.
// BytesTransferred指明了这次IO传输了多少Bytes...
// PerHandleData是CreateIoCompletionPort的时候穿过来的,MSDN里面叫CompletionKey.
// 请注意PerIoData这个参数,本来按照MSDN的解释如下:
// "Pointer to a variable that receives the address of the OVERLAPPED structure that was
// specified when the completed I/O operation was started."
// 其实在这这个lpOverlapped传过来的就是上面WSARecv传入的overlapped的地址...
// 这下明白了吧:主线程借着overlapped的名声传了一个结构体指针过来..:-)
//
if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)
{
printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError());
return 0;
}


//
// First check to see if an error has occured on the socket and if so
// then close the socket and cleanup the SOCKET_INFORMATION structure
// associated with the socket.
//

if (BytesTransferred == 0)
{
printf("Closing socket %d\n", PerHandleData->Socket);

if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
{
printf("closesocket() failed with error %d\n", WSAGetLastError());
return 0;
}

GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}

//
// Check to see if the BytesRECV field equals zero. If this is so, then
// this means a WSARecv call just completed so update the BytesRECV field
// with the BytesTransferred value from the completed WSARecv() call.
//

if (PerIoData->BytesRECV == 0)
{
PerIoData->BytesRECV = BytesTransferred;
PerIoData->BytesSEND = 0;
}
else
{
PerIoData->BytesSEND += BytesTransferred;
}

if (PerIoData->BytesRECV > PerIoData->BytesSEND)
{

//
// Post another WSASend() request.
// Since WSASend() is not gauranteed to send all of the bytes requested,
// continue posting WSASend() calls until all received bytes are sent.
//
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;
PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;

if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSASend() failed with error %d\n", WSAGetLastError());
return 0;
}
}
}
else
{
PerIoData->BytesRECV = 0;

// Now that there are no more bytes to send post another WSARecv() request.
Flags = 0;
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;

if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return 0;
}
}
}
}
}
masuz1 2005-01-04
  • 打赏
  • 举报
回复
摘自网络

用Delphi改一下应该能用的,不过我没试过
细说WinSock IOCP C++阐述



1.Socket Modes",什么是"Socket I/O Models"?
Socket Modes : Determines how winsock functions behave when called with a socket.
Socket I/O Models : Describes how an application manages and processes I/O on a socket.

2.那么Winsock提供了哪几种"Socket Modes"给程序员用呢?
  a.Blocking Mode(阻塞式的)
  b.Noblocking Mode(非阻塞式的)

3.详细解释一下阻塞和非阻塞.
我们那最常用的send和recv两个函数来说吧...
比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话...这时候就体现出阻塞和非阻塞的不同之处了:对于阻塞模式的socket send函数将不返回直到系统缓冲区有足够的空间把你要发送的数据Copy过去以后才返回,而对于阻塞的socket来说send会立即返回WSAEWOULDDBLOCK告诉调用者说:"发送操作被阻塞了!!!你想办法处理吧..."
对于recv函数,同样道理,该函数的内部工作机制其实是在等待TCP/IP协议栈的接收缓冲区通知它说:嗨,你的数据来了.对于阻塞模式的socket来说如果TCP/IP协议栈的接收缓冲区没有通知一个结果给它它就一直不返回:耗费着系统资源....对于阻塞模式的socket该函数会马上返回,然后告诉你:WSAEWOULDDBLOCK.

3.对于阻塞模式(Blocking Mode)我采取开线程的方式不就搞定了吗,而且还不用我去判断基本的收/发什么时候完成,多省心?
  没错,这位大虾说的开线程的方式具体的做法如下:我们将应用划分成一个读线程(Reading Thread)一个工作线程(Working Thread),两个线程*同步对象(Synchronization Object,可以是Event,Mutex...)来共享一个数据缓冲区,读线程(Reading Thread)负责从网
络读取数据并将其放到共享缓冲区中,在适当的时机(比如说读线程将工作线程需要的数据接收下来之后)触发一个事件,通知/唤醒工作线程(Working Thread),下面是上述文字的简单代码实现:

CRITICAL_SECTION g_data;
HANDLE          g_hEvent;
TCHAR           g_szBuffer[MAX_BUFFER_SIZE];
int               g_nBytes;

void ReadThread(void)
{
   int nTotle = nRead = nLeft = nBytes = 0;

   while(!done)
  {
    nTotal = 0;
    nLeft = NUM_BYTES_REQUIRED; //要读多少的Bytes

    while( nTotal != NUM_BYTE_REQUIRED)
   {
     // 因为涉及线程和全局变量所以说需要用到关键代码段(Critical Section)
     EnterCriticalSection(&g_data);

     nRead = recv(sock,&(g_szBuffer[MAX_BUFFER_SIZE] - nBytes),nLeft);

     if( -1 == nread )
    {
      printf("socket %d error.\n",WSAGetLasError());
      ExitThread();
     }

     nTotal += nRead;
     nLeft -= nRead;

     nBytes += nRead;

     LeaveCriticalSelection(&g_data);
   }

  // 从网络接收完指定的(NUM_BYTES_REQUIRED)Bytes之后通知WorkThread启动
   SetEvent(g_hEvent);
  }
}


void WorkThread(void)
{
  WaitForSingleObject(g_hEvent);

  EnterCriticalSection(&g_data);


  // 主要的处理工作...
  DoSomeWorkOnData(g_szBuffer);

  g_nBytes -= NUM_BYTE_REQUIRED;

  LeaveCriticalSelection(&g_data);
}

  这种方式的一个缺点是:应用程序很难同时通过多个建立好连接的socket通信.当然话说回来我们可以为每一个连接好的socket都开一个Reading Thread and WorkThread,但是呢这会带来很大的开销,而且扩展性很差,试想:如果用这样的模型建立一个服务器端来一个连接开至少两个线程...天..要是业务负担很大/连接的socket很多的话怎么办??所以说别多想了...
这样的方式是很不适合做服务器端的.(注意:我说的服务器端是指C/S中的S端逻辑).

从编写代码的角度解读一下IO中最复杂的IOCP模型

下面的一段代码是Microsoft Win32 SDK里面的例子,这是一个Echo服务器程序:

//
// 这就不说了,为了使用Winsock2必须包含的头文件...
//
#include
#include
#include

#define PORT 5150 // 这个服务器选择5150端口
#define DATA_BUFSIZE 8192 // 把Buffer定义为8K是一个比较被Microsoft推荐的大小

//
// 这个结构体很重要,请一定看清楚,到后面用到的时候别糊涂了
// 特别注意:把Overlapped放最开头是有原因的,你应该明确的是
// 如果有:
// PER_IO_OPERATION_DATA PerIoData;
// &PerIoData->Overlapped 和 &PerIoData是一样的/等价的/
// 指向同一块内存区域的.
//
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
DWORD BytesSEND; // 下面两个参数是用来记录每个connected socket上面的收发的Bytes的
DWORD BytesRECV;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

// 别把这个结构体和上面的结构体混淆了,名字很像的...其实它在这个程序中就相当于是那个和Client端连接的socket
typedef struct
{
SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;


DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);

void main(void)
{
SOCKADDR_IN InternetAddr;
//
// 一个listen和一个Accept socket是服务器程序的老套路了...
//
SOCKET Listen;
SOCKET Accept;

//
// IO完成端口映射到程序就是一个HANDLE
//
HANDLE CompletionPort;
SYSTEM_INFO SystemInfo;

//
// 看清楚,这是指针...不怕你笑话我们处还真有分不清和指针相关的一切概念的人,比如下面的代码:
// 我写了一个DLL,里面有一个这样的函数:
// BOOL GetPlayingPosition(DWORD * dwPos);
// 结果大虾调用的时候是这样的....
// DWORD * p;
// GetPlayingPostion(p);
// 它是调用爽了...我呢...:-(
// 当然了,也不好定义说它就是分不清指针,也许人家就不会写程序呢..对吧..
//
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;

int i;
DWORD RecvBytes;
DWORD Flags;
DWORD ThreadID;
WSADATA wsaData;
DWORD Ret;

// 就像CoInitialize()一样的把Winsock 2.2服务启动起来...
if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
{
printf("WSAStartup failed with error %d\n", Ret);
return;
}

// CreateIoCompletionPort有两个不同的用途:1.创建完成端口;2.把已经创建好的完成端口和一个Socket关联/绑定起来.
// 下面这句话是它的第一个用途:创建完成端口
if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
{
printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());
return;
}

//
// 下面这一些代码说得是:根据该服务器跑的机器上有几个CPU建立2*CPU数量的Thread.
// 这涉及到完成端口工作线程应该创建多少个的问题,在早期(MSDN 2000)Microsoft说
// 是WorkThreadNums = 2 * CPU + 2,现在的MSDN 2003说是WorkThreadNums = 2 * CPU;
//
GetSystemInfo(&SystemInfo);

for(i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE ThreadHandle;

// 创建一个线程并把上面创建的完成端口传给线程
if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
0, &ThreadID)) == NULL)
{
printf("CreateThread() failed with error %d\n", GetLastError());
return;
}

// 这个风格我喜欢,创建完Thread就把人家的Handle给release,省得要是线程异常退出搞得资源泄漏之类的麻烦事情.
CloseHandle(ThreadHandle);
}

TOMWLD 2005-01-04
  • 打赏
  • 举报
回复
DXServerCore
Ancestor to all of the DXSock™ Protocol implementations - consisting of the "VCL" component, the listener thread, and client or session thread definitions. DXServerCore and all descendants inherit our high-performance socket layer (DXSock), along with the 4 different threading models contained in the DXServerCore listener thread. As a new connection is detected, the listener thread spawns a new client thread - properly configuring the socket layer object, and fires the OnNewConnect event with the associated client thread as the property. This design allows the listener thread to process millions of connections a second, and off-loads latency to each individual client thread; which is approximately 0.001ms per new session.

另一套控件dxsock,它网站上的说明,不知有没有它所宣称的那样的效率
TOMWLD 2005-01-04
  • 打赏
  • 举报
回复
也许用c++和ace是不错的选择,可我对c++实在陌生,郁闷中
有用过ace的朋友多多帮忙
ilovevc 2005-01-03
  • 打赏
  • 举报
回复
indy不应该用在server端,真要高并发的服务器,就不能再和客户端一样搞个控件了。
lance09 2005-01-03
  • 打赏
  • 举报
回复
看看代码能不能再优化一番吧,如果真有3000以上的用户长时间在线,哪就先考虑用的硬件资源,换OS应该也是不错的选择
TOMWLD 2005-01-03
  • 打赏
  • 举报
回复
提前一下
muroachanf 2004-12-30
  • 打赏
  • 举报
回复
IOCP,我做过,不过是C的
ZhuJunfeng 2004-12-30
  • 打赏
  • 举报
回复
即时通信的话,可以用udp比较节省资源,如果非要用tcp的话,要专门做一个线程对其他线程进行清理,关闭那些掉死的,空闲的,超时的线程
xx_adam 2004-12-30
  • 打赏
  • 举报
回复
up
TOMWLD 2004-12-30
  • 打赏
  • 举报
回复
为公司发一份招聘信息,有意向的可联系

高级程序员:
1、 精通java、delphi两种开发语言之一,能熟练运用设计模式,能用UML进行交流,具备面向对象设计开发经验
2、 熟悉tcp/ip、udp等网络协议,有从事高性能网络服务端开发的经验
3、 至少熟练掌握Oracle或SQL Server一种大型数据库系统
4、 具有良好的团队合作精神,勤于思考,善于研究

公司是福建省泉州市电信投资的国有民营企业,具体情况请见:http://www.qzgy.com.cn

有意者可先与我联系,013074963881,小吴

tsk 2004-12-29
  • 打赏
  • 举报
回复
是啊,大家讨论一下吧
yinzhiw 2004-12-22
  • 打赏
  • 举报
回复
学习!
ygflydream 2004-12-22
  • 打赏
  • 举报
回复
学习!
beyondtkl 2004-12-22
  • 打赏
  • 举报
回复
完成端口 是個不錯的選擇 再加上線程池 。。性能挺好。
加载更多回复(21)

1,593

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 网络通信/分布式开发
社区管理员
  • 网络通信/分布式开发社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧