RakNet是RakkarSoft的一个网络引擎(http://www.rakkarsoft.com/) 。该引擎有商业版和非商业版之分,非商业版意味着免费,同时也意味着代码质量不会很高,因为它不可能傻到把免费版做来比商业版要好。虽然2.454版本的文档明确了可以使用完成端口(注意:RakNet后续版本都不再支持完成端口),但是在实际代码中,完成端口部分的代码却不能工作。由于项目需要,我对其进行了修复。
下面是在修改过程中对RakNet连接过程及错误的简要分析:
下图展示了利用RakNet提供的C/S聊天程序进行连接的过程。无论是否采用完成端口,该过程都是相同的。
下图为状态转换图,清晰地显示了创建连接的全过程。
在RakNet 2.454版本中,完成端口不能正常工作主要原因如下:
1、 将加密套接字与完成端口绑定的时机不对。从连接图我们可以看出客户端最好的时机是在客户端收到ID_CONNECTION_REQUEST_ACCEPTED,服务器最好的时机为在收到ID_NEW_INCOMING_CONNECTION的时候。
2、 在程序中,作者直接在创建利用完成端口机制通讯的套接字时,创建了一个与原来监听套接字完全重复捆绑的套接字,导致只能有很少的客户能连入[1]。其实,从代码中可以看出,作者已经意识到这个问题,因而他提供了如下解决函数(但是,不知怎么的,始终没有调用该函数)
#ifdef __USE_IO_COMPLETION_PORTS
bool RakPeer::SetupIOCompletionPortSocket( int index )
{
SOCKET newSocket;
if ( remoteSystemList[ index ].reliabilityLayer.GetSocket() != INVALID_SOCKET )
closesocket( remoteSystemList[ index ].reliabilityLayer.GetSocket() );
newSocket = SocketLayer::Instance()->CreateBoundSocket( myPlayerId.port + index + 1, false , NULL ); // 用端口号不同来得到不同的socket
SocketLayer::Instance()->Connect( newSocket, remoteSystemList[ index ].playerId.binaryAddress, remoteSystemList[ index ].playerId.port ); // port is the port of the client
remoteSystemList[ index ].reliabilityLayer.SetSocket( newSocket );
// Associate our new socket with a completion port and do the first read
return SocketLayer::Instance()->AssociateSocketWithCompletionPortAndRead( newSocket, remoteSystemList[ index ].playerId.binaryAddress, remoteSystemList[ index ].playerId.port, this );
}
#endif
因此,在服务器端需要创建完成端口的时候调用该函数就能够解决上述问题。
点击此处[下载]修改后的源代码。
[1] 在代码中作者在创建套接字的时候采用了SO_REUSEADDR选项。《UNIX网络编程 第1卷:套接口API》(第三版)180页中对该选项的说明:SO_REUSEADDR允许完全重复的捆绑,当一个IP地址和端口已经绑定到某个套接口上时,如果传输协议支持,同样的IP地址和端口还可以捆绑到另一个套接口上,一般来说本特性仅支持UDP套接口。本特性用于多播时,允许在同一个主机上同时运行同一个应用程序的多个副本。当一个UDP数据报需由这些重复捆绑套接口中的一个接收时,所用规则为:如果该数据报的宿地址是一个广播地址或多播地址,那就给每个匹配的套接口递送一个该数据报的拷贝;但是如果该数据报的宿地址是一个单播地址,那么它只递送给单个套接口。在单播数据报情况下,如果有多个套接口匹配该数据报,那么它该由哪个套接口接受的选择取决于实现。