IOCP的一个效率问题,犹豫了2,3年

已查看 9 次
跳至第一个未读帖子

Darwin Lalo

未读,
2006年4月29日 01:56:332006/4/29
收件人 高性能网络编程邮件列表
大家好,一般来说

GetQueuedCompletionStatus 函数的
PULONG_PTR lpCompletionKey
是用来存放Socket相关的数据的,一般是一个
指针,里面存放 SOCKET 等数据

在我现有的一个服务器中,这个值是一个DWORD
整形值,这个DWORD充当索引,SOCKET相关的数据是查 map
得到的,也就是说每次 Send,Recv
都要访问这个map,通过这个DWORD来查到SOCKET
等数据,然后操作

Send(DWORD dwPerSockID, memblock block, size_t nLength){
SOCKET socket = 0;
m_mapPerSocket.find(dwPerSockID, socket);
WSASend(socket, DataBuf, 1, &NumBytes, 0, &PerIOData->Overlapped,
0);
...
}
ThreadProc(void* lpParam){
DWORD nSocketID = 0;
GetQueuedCompletionStatus(m_hIOCP, &NumBytes, &nSocketID,
&lpOverlapped, INFINITE);
....
OnSend(nSocketID, PerIoData, NumBytes);
}

这个服务器可以 24*7
运行,我不是很想动他,但是这几年,老是觉得有点犹豫
dwPerSockID
成为一个指针,固然可以不用查map,直接指针指向的数据

但是有另外一些问题.

我想问一下,大家觉得有严重的效率问题吗?有人这么干吗?谢谢大家。。

HuYi

未读,
2006年4月29日 02:10:232006/4/29
收件人 dev4s...@googlegroups.com
我觉得要找回之前的状态信息,至少需要一次查表操作。

--
☆==☆☆==☆☆==☆☆==☆☆==☆☆==☆
NEC Solutions(China)Co.,Ltd.
ソフトウェア開発事業部
  大連第一開発部
  胡 毅(コ キ)
♪連絡先
◇ TEL (86)0411-84754455-232
◇ e-Mail hu...@dl.necsl.com.cn
◇ GTalk huy...@gmail.com
◇ BLOG www.cppblog.com/huyi
☆==☆☆==☆☆==☆☆==☆☆==☆☆==☆

sunway

未读,
2006年4月29日 02:10:242006/4/29
收件人 高性能网络编程邮件列表
我的没有这个map,跑了2年都木有问题/

sunway

未读,
2006年4月29日 02:14:242006/4/29
收件人 高性能网络编程邮件列表
效率肯定有问题,压力大的情况下比较慢,因为map的O(logN)也是一笔开销。

Darwin Lalo

未读,
2006年4月29日 02:21:232006/4/29
收件人 高性能网络编程邮件列表
谢谢,你是把状态信息存在 PULONG_PTR lpCompletionKey
里面的吧?
是这样的,我需要给上层一个 id,
每次收到数据,上层每次发送数据也需要
一个 id, 现在就是我说的这个 DWORD, 如果这儿传 SOCKET
或者 内存地址
应该是不行的,因为 SOCKET 和内存地址 不停的
再重用,不能保证唯一性。
这儿不知道该怎么办。。


sunway wrote:
> 效率肯定有问题,压力大的情况下比较慢,因为map的O(logN)也是一笔开销。

Darwin Lalo

未读,
2006年4月29日 02:23:412006/4/29
收件人 高性能网络编程邮件列表

为什么,如果在 PULONG_PTR lpCompletionKey
里面存信息,就不用查表啊

HuYi

未读,
2006年4月29日 02:30:592006/4/29
收件人 dev4s...@googlegroups.com

如果是异步模式,确实可以携带一些数据。
在普通的select中呢

Darwin Lalo

未读,
2006年4月29日 02:48:242006/4/29
收件人 高性能网络编程邮件列表

我刚才又测试一下,我个人觉得这种查表的开销是很小的。。
假设有2500个用户,每个用户每秒400个包,下面代码用时15毫秒

hash_map<DWORD,DWORD> _map;
for(int i = 0; i < 2500; ++i){
_map.insert(make_pair(i, i*i));
}

DWORD now = GetTickCount();
for(int j = 0; j < 400; ++j){
for(int i = 0; i < 2500; ++i){
hash_map<DWORD,DWORD>::iterator iter = _map.find(i);
if(iter == _map.end()){
printf("o..\n");
}else{
if(iter->first * iter->first != iter->second){
printf("k..\n");
}
}
}
}

printf("%d\n", GetTickCount() - now);

su yu

未读,
2006年4月29日 08:00:252006/4/29
收件人 dev4s...@googlegroups.com
用hashmap
(0-1)

 

胡人

未读,
2006年5月1日 11:49:262006/5/1
收件人 dev4s...@googlegroups.com
本来就不需要的查表为什么要查呢,何必多此一举?
要达到最高效率和看上去最优雅的代码,当然是要把这个查表拿掉,当然如果不愿意冒改变代码带来的风险而仅仅是维护另当别论。
 

HuYi

未读,
2006年5月1日 21:50:492006/5/1
收件人 dev4s...@googlegroups.com
胡人 wrote:
> 本来就不需要的查表为什么要查呢,何必多此一举?
> 要达到最高效率和看上去最优雅的代码,当然是要把这个查表拿掉,当然如果不
> 愿意冒改变代码带来的风险而仅仅是维护另当别论。
>
> >
我想向胡人大哥请教一下
现在我们是这样的情况,有一组服务器专门负责和客户端打交道,职责的设计也很
简单,仅仅是转发一下数据包而已。如果是完成端口,操作系统会负责通知合适的
对象来进行下一步处理。比如说1秒钟收了n个包,该对象会把这n个包,这n个包来
自m个客户端,一次性发送给处理业务的服务器,在业务服务器上,要针对每个包
选择合适的处理器,找出m个用户各自的状态对象,在这个时候,不知道什么方法
可以避免查表。
不知道你们所使用的结构,是不是接受连接的服务器,直接就靠完成端口提供的通
知功能找出处理器,把包处理掉?

zhaoh

未读,
2006年5月1日 23:07:272006/5/1
收件人 dev4s...@googlegroups.com
@HuYi
socket通讯处理一般可分成两种
一种是按连接(socket)关联,每个socket对应一个客户端,直接根据socket调用对应的客户对象进行处理
另一种是无关联处理,不考虑客户端与socket的对应关系,根据数举包内容来确定客户端,以及选择客户对象进行处理

HuYi所说的业务服务器应该就属于第二种情况,我通常都用下面这种方法处理:
 
服务器程序中用数组来管理客户对象,数组元素为对象指针,客户连接登录时生成对象并存入数组,客户退录时释放对象数组元素置空,数据包中要包括索引号和客户ID,根据索引号直接定位客户对象,然后匹配客户ID以保证有效性,因为该索引位置可能已被其它客户对象占用,正常情况下不会这样,取决于程序实现方式。
 
这样可以避免查表,也防止了直接用返回的对象指针访问客户对象时,可能由于对象释放指针失效导致非法访问问题。
 
 

HuYi

未读,
2006年5月2日 01:41:272006/5/2
收件人 dev4s...@googlegroups.com
zhaoh wrote:
> @HuYi
> socket通讯处理一般可分成两种
> 一种是按连接(socket)关联,每个socket对应一个客户端,直接根据 socket调
> 用对应的客户对象进行处理
> 另一种是无关联处理,不考虑客户端与socket的对应关系,根据数举包内容来确
> 定客户端,以及选择客户对象进行处理
>
> HuYi所说的业务服务器应该就属于第二种情况,我通常都用下面这种方法处理:
>
> 服务器程序中用数组来管理客户对象,数组元素为对象指针,客户连接登录时生
> 成对象并存入数组,客户退录时释放对象数组元素置空,数据包中要包括索引号
> 和客户ID,根据索引号直接定位客户对象,然后匹配客户ID以保证有效性,因为
> 该索引位置可能已被其它客户对象占用,正常情况下不会这样,取决于程序实现
> 方式。
>
> 这样可以避免查表,也防止了直接用返回的对象指针访问客户对象时,可能由于
> 对象释放指针失效导致非法访问问题。
>
感谢zhaoh的解答
我想继续询问一下更具体的做法。
数组中和对象对应的index,是要求客户端提供(放入发送给服务器的每个数据包
中),还是当服务器接到包后根据连接句柄算出这个index?假若是算出来,用什
么样的算法比较好?是不是hash的key生成函数那样计算index值?觉得这样又是变
相查hash表了。

但如果是用客户端的包来携带服务端内部处理需要,但又和业务无关的数据(比如
说index或者是指针),我觉得这样的设计并不佳,原因是把服务器内部的实现和
客户端耦合了,客户端根本不需要服务器内部是采用的什么实现方式,对吧。如果
今后改服务器的具体实现,就需要修改通讯协议,这并不好。

zhaoh23

未读,
2006年5月2日 05:55:582006/5/2
收件人 高性能网络编程邮件列表
> 我想继续询问一下更具体的做法。
> 数组中和对象对应的index,是要求客户端提供(放入发送给服务器的每个数据包
> 中),还是当服务器接到包后根据连接句柄算出这个index?假若是算出来,用什
> 么样的算法比较好?是不是hash的key生成函数那样计算index值?觉得这样又是变
> 相查hash表了。

1.
创建一个对象指针数组,采用动态增量分配内存方式,最大上限10000(按需要修改),每次增量256,元素初始化为NULL,也可以直接声明一个10000元数的数组

2.
创建一个FIFO空闲队列,存放所有NULL数组元素的索引,主要是为了加快用户登录时,定位客户对象存放位置的速度,简化的话就直接扫描数组找NULL元素

3.
每个socket关联一个Postman对象,该对象负责数据收发、半包连包处理用户连接,用Postman对象指针作为CreateIoCompletionPort()的CompletionKey与socket关联,GetQueuedCompletionStatus()返回时CompletionKey转换成对象指针处理数据,处理后的完整数据包传递给Dispatcher

4.
Dispatcher对象是一个静态对象,负责把Postman收到的数据包分派给正确的处理对象,以次分界,Postman、socket作为网络层,Customer为业务层(当然不仅仅是Customer)

5.
数据包由包头和业务消息组成,包头包括:协议版本,数据包长度,消息类型标志,会话索引,会话ID,业务消息格式可变,具体类型由消息类型标志确定,如果要增加修改业务类型,改变对应的业务消息定义即可

6.
用户连接、登录,登录包中会话索引和会话ID都初始化为(-1)

7.
Dispatcher判断是初始会话,创建客户对象,从空闲队列中取到空闲数组元素索引,如果无空闲位置,则增长对象指针数组,初始化新元素并加入到空闲队列,根据空闲位置索引存入客户对象指针,然后修改包头中会话索引=数组索引,会话ID=客户对象指针,最后把数据包交给客户对象处理,同时传递给客户对象的还有Postman对象,用于发送应答数据包

8.
此后会话中的所有数据包保持会话索引、会话ID,Dispatcher根据会话索引取得客户对象指针,并与会话ID匹配无误后,把数据包分派给取到的客户对象进行处理,对客户对象的处理过程进行异常保护,如果发生重大异常,释放对象及相关资源,保证其它客户正常,提高服务器健壮性

9.
会话结束后,释放客户对象,对应的数组元素置NULL,空闲位置索引加入到空闲队列等待重利用


如果仅仅利用对象指针作为会话ID的话,每次根据数据包头的会话ID转为对象指针时,无法保证指针的绝对有效性,因为对象可能由于各种原因失效,比如对会话关闭没处理好,对象已释放仍然从IOCP的队列中得到排队的数据包等

如果仅用索引号,则由于数组位置的重用,如果在对象释放然后位置被新对象占用后,收到延迟的数据包,就可能导致用错误的客户对象处理数据包

如果程序框架能严格的保证不会出现类似情况,就可以仅使用这两个之一


> 但如果是用客户端的包来携带服务端内部处理需要,但又和业务无关的数据(比如
> 说index或者是指针),我觉得这样的设计并不佳,原因是把服务器内部的实现和
> 客户端耦合了,客户端根本不需要服务器内部是采用的什么实现方式,对吧。如果
> 今后改服务器的具体实现,就需要修改通讯协议,这并不好。

这个问题提得很好,我也曾经为此困扰过很久。

不过后来我觉得针对性与通用性矛盾的地方,这种方法本来是用来实现前面提到的连接无关处理方式的一个框架,它所解决的是通讯会话与业务对象的关联效率问题,本身是属于应用层的范围,不可能象TCP/IP等通讯协议一样对各种应用通用。

可以把它看作是业务与网络中间的一层协议,与具体的业务消息内容无关,会话ID就相当于TCP中的端口号一样,与TCP数据内容无关。我们实现socket客户端和服务端程序的时候,也算是由于端口号而耦合了吧:-)

由于这中方法是作为一种处理框架,一旦确定需要修改的机会应该很少,大多数时候是改业务实现。如果找到更合适的框架,确实可能连客户端的网络层也要一起修改,但你想,如果把一个TCP服务器改用COBAR、DCOM实现,客户端可以不改吗?

HuYi

未读,
2006年5月2日 09:53:302006/5/2
收件人 高性能网络编程邮件列表
zhaoh23的解答很详尽,感谢。
看来数据包中携带一定类似指针的数据还是有必要的。
关于查表对整体性能的影响,不知道有没有一个大概的数据统计。
我认为每台服务器1w连接的话,用2分法查应该是很快速的。
另外不知道大家有没有对数据库的索引扫描算法比较熟悉的。

Darwin Lalo

未读,
2006年5月2日 10:49:362006/5/2
收件人 高性能网络编程邮件列表

> 8.
> 此后会话中的所有数据包保持会话索引、会话ID,Dispatcher根据会话索引取得客户对象指针,并与会话ID匹配无误后,把数据包分派给取到的客户对象进行处理,对客户对象的处理过程进行异常保护,如果发生重大异常,释放对象及相关资源,保证其它客户正常,提高服务器健壮性
>
> 9.
> 会话结束后,释放客户对象,对应的数组元素置NULL,空闲位置索引加入到空闲队列等待重利用
>
>
> 如果仅仅利用对象指针作为会话ID的话,每次根据数据包头的会话ID转为对象指针时,无法保证指针的绝对有效性,因为对象可能由于各种原因失效,比如对会话关闭没处理好,对象已释放仍然从IOCP的队列中得到排队的数据包等
>
> 如果仅用索引号,则由于数组位置的重用,如果在对象释放然后位置被新对象占用后,收到延迟的数据包,就可能导致用错误的客户对象处理数据包
>
> 如果程序框架能严格的保证不会出现类似情况,就可以仅使用这两个之一
>

非常谢谢,查表正是为了从框架上避免
上述的各种情况 "对象释放然后位置被新对象占用",
"无法保证指针的绝对有效性" 等等。

因为 "会话中的所有数据包保持会话索引、会话ID"
的成本高,如果是一个语音服务器,每秒 50个
音频包,每个包多4~8字节是很昂贵的,
一般的音频 8kbps 的数据 现在变成
9.6kbps~11.2kbps,而且从数据包内容来判断来源是一个危险的事情,比如游戏服务器中作弊问题。

而查表,可以避免上诉问题,而且很好,

1. DWORD 索引 通过 InterlockedIncrement(&index)
这种操作,这个索引在 服务器的有效期内
都是不会重的,而SOCKET不行,SOCKET的数值在不停重用

2. DWORD 作为索引,服务器
上下层(网络层,逻辑层)之间,交换的都是这个DWORD索引,
各层保存相关的数据在各层自己的hash_map里面,
来了查一下, 处理, 然后给上下层处理也是传这个索引
上下层通过这个DWORD查到了,就处理,发数据等
说到优雅,这种方式很优雅,而且hash_map很快,可负担,这种结构我觉得很清晰

Darwin Lalo

未读,
2006年5月2日 11:26:272006/5/2
收件人 高性能网络编程邮件列表
我觉得你的设计中有很多可以改良的成份
协议层和网络层可以完全分开的。
你现在的协议层和网络层在一起,和逻辑层分开了。
呵呵,
而且你的数组就是一种形式的查表

如果协议层可以分2层,一层基础协议(就是你说的"协议版本,数据包长度,消息类型标志"),我觉得session
id应该在协议层的第二层

Darwin Lalo

未读,
2006年5月2日 11:42:052006/5/2
收件人 高性能网络编程邮件列表

HuYi wrote:
> zhaoh23的解答很详尽,感谢。
> 看来数据包中携带一定类似指针的数据还是有必要的。

我觉得没有必要,如果写出这种东西,肯定很头痛

回复全部
回复作者
转发
0 个新帖子