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,直接指针指向的数据
但是有另外一些问题.
我想问一下,大家觉得有严重的效率问题吗?有人这么干吗?谢谢大家。。
--
☆==☆☆==☆☆==☆☆==☆☆==☆☆==☆
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 wrote:
> 效率肯定有问题,压力大的情况下比较慢,因为map的O(logN)也是一笔开销。
为什么,如果在 PULONG_PTR lpCompletionKey
里面存信息,就不用查表啊
如果是异步模式,确实可以携带一些数据。
在普通的select中呢
我刚才又测试一下,我个人觉得这种查表的开销是很小的。。
假设有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);
但如果是用客户端的包来携带服务端内部处理需要,但又和业务无关的数据(比如
说index或者是指针),我觉得这样的设计并不佳,原因是把服务器内部的实现和
客户端耦合了,客户端根本不需要服务器内部是采用的什么实现方式,对吧。如果
今后改服务器的具体实现,就需要修改通讯协议,这并不好。
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实现,客户端可以不改吗?
非常谢谢,查表正是为了从框架上避免
上述的各种情况 "对象释放然后位置被新对象占用",
"无法保证指针的绝对有效性" 等等。
因为 "会话中的所有数据包保持会话索引、会话ID"
的成本高,如果是一个语音服务器,每秒 50个
音频包,每个包多4~8字节是很昂贵的,
一般的音频 8kbps 的数据 现在变成
9.6kbps~11.2kbps,而且从数据包内容来判断来源是一个危险的事情,比如游戏服务器中作弊问题。
而查表,可以避免上诉问题,而且很好,
1. DWORD 索引 通过 InterlockedIncrement(&index)
这种操作,这个索引在 服务器的有效期内
都是不会重的,而SOCKET不行,SOCKET的数值在不停重用
2. DWORD 作为索引,服务器
上下层(网络层,逻辑层)之间,交换的都是这个DWORD索引,
各层保存相关的数据在各层自己的hash_map里面,
来了查一下, 处理, 然后给上下层处理也是传这个索引
上下层通过这个DWORD查到了,就处理,发数据等
说到优雅,这种方式很优雅,而且hash_map很快,可负担,这种结构我觉得很清晰
如果协议层可以分2层,一层基础协议(就是你说的"协议版本,数据包长度,消息类型标志"),我觉得session
id应该在协议层的第二层
我觉得没有必要,如果写出这种东西,肯定很头痛