Nebula3学习笔记(7): 网络系统

Nebula3 的网络子系统提供了基于 TCP 协议的简单 C/S 通信模式 . 它并没有打算做成大厅 , 会话管理还有玩家数据同步的面向游戏的高级通信 . 这些以后会在更高层的 Nebula3 子系统中出现 .
使用 IP 地址
一个 IpAddress 对象通过主机名字或 TCP/IP 地址加一个端口号定义了一个通信端点 . IpAddress 对象可以通过多数方式建立 :

1: // 从 TCP/IP 地址和端口号:
2: IpAddress ipAddr ( "192.168.0.2" , 1234 );
3:
4: // 从主机名和端口号:
5: IpAddress ipAddr ( "www.radonlabs.de" , 1234 );
6:
7: // 从本机(127.0.0.1) 和端口号:
8: IpAddress ipAddr ( "localhost" , 1234 );
9:
10: // 从"any" 地址 (0.0.0.0) 和端口号:
11: IpAddress ipAddr ( "any" , 1234 );
12:
13: // 从广播地址 (255.255.255.255) 和端口号:
14: IpAddress ipAddr ( "broadcast" , 1234 );
15:
16: // 从主机的第一个合法网络适配器的地址和端口号
17: IpAddress ipAddr ( "self" , 1234 );
18:
19: // 从主机的第一个连接到互联网的网络适配器的地址和端口号:
20: IpAddress ipAddr ( "insetself" , 1234 );
21:
22: // 从一个定义了主机名的URI和端口号:
23: IpAddress ipAddr ( IO :: URI ( "http://www.radonlabs.de:2100" ));
一个 IpAddress 对象可以用于从主机名查找 TCP/IP 地址 :

1: IpAddress ipAddr ( "www.radonlabs.de" , 0 );
2: String numericalAddr = ipAddr . GetHostAddr ();
建立一个客户端 / 服务器系统
网络子系统用 TcpServer TcpClient 类实现了一个易用的基于 TCP 协议的 C/S 系统 . 一个 TcpServer 可以为任意数量的 TcpClient 服务 .
建立一个服务器可以这么做 :

1: using namespace Net ;
2:
3: Ptr < TcpServer > tcpServer = TcpServer :: Create ();
4: tcpServer -> SetAddress ( IpAddress ( "any" , 2352 ));
5: if ( tcpServer -> Open ())
6: {
7: // TcpServer successfully opened
8: }
这样会建立一个在 2352 端口监听客户端连接请求的服务器 .
为了跟 TcpServer 通信 , 需要在客户端建立一个 TcpClient 对象 :

1: using namespace Net ;
2:
3: Ptr < TcpClient > tcpClient = TcpClient :: Create ();
4: tcpClient -> SetBlocking ( false );
5: tcpClient -> SetAddress ( IpAddress ( "localhost" , 2352 ));
6: TcpClient :: Result res = tcpClient -> Connect ();
这里假设服务端和客户端运行在同一台机器上 ( 因为客户端连接到了 ”localhost”).
像上面那样非阻塞的情况 , Connect() 方法不是返回 TcpClient :: Success( 这意味着连接建立好了 ) 就是 TcpClient :: Connecting, 如果这样的话 , 应用程序需要继续调用 Connect() 方法 . 如果连接错误 , 会返回一个 TcpClient :: Error 的返回值 .
如果是阻塞的 , Connect() 方法直到连接建立 ( 结果是 TcpClient :: Success) 或发生错误才会返回 .
注意 : 一个交互式应用程序不应该在网络通信时阻塞 , 而应不断地为用户提供反馈 .
一旦连接建立 , 服务端会为每个客户机建立一个 TcpClientConnection 对象 . TcpClientConnection 在服务器上表示客户机 , 并且负责从客户机收发数据 .
要进行接收和发送数据的话 , 需使用 IO::Stream 对象 . 在通信流上连接 IO::StreamReader IO::StreamWriter 对象后 , 从流中编码和解码数据是一件非常容易的事情 .
注意 : 发送数据并不是即时的 , 而是在 Send() 方法被调用之前会一直保存在发送流当中 .
要客户端给服务器发送一些文本数据话 , 只要从发送流获取一个指针 , 向其中写入数据后调用 Send() 方法就可以了 :

1: using namespace Net ;
2: using namespace IO ;
3:
4: // obtain pointer to client's send stream and attach a TextWriter
5: const Ptr < Stream >& sendStream = tcpClient -> GetSendStream ();
6: Ptr < TextWriter > textWriter = TextWriter :: Create ();
7: textWriter -> SetStream ( sendStream );
8: textWriter -> Open ())
9: textWriter -> WriteString ( "Hello Server" );
10: textWriter -> Close ();
11:
12: // send off the data to the server
13: if ( this -> tcpClient -> Send ())
14: {
15: // data has been sent
16: }
在服务器端接收客户端数据 , 应用程序需要要频繁地 ( 每帧一次 ) 缓存带有客户羰数据的 TcpClientConnection . 可能不只一个 TcpClientConnection 在等待处理 , 因此处理循环应该像这样 :

1: // get array of client connections which received data since the last time
2: Array < Ptr < TcpClientConnection >> recvConns = tcpServer -> Recv ();
3: IndexT i ;
4: for ( i = 0 ; i < recvConns . Size (); i ++)
5: {
6: // get receive stream from current connection, attach a text reader and read content
7: Ptr < TextReader > textReader = TextReader :: Create ();
8: textReader -> SetStream ( recvConns [ i ] -> GetRecvStream ());
9: textReader -> Open ();
10: String str = textReader -> ReadString ();
11: textReader -> Close ();
12:
13: // process received string and send response back to client
14: // create a TextWriter and attach it to the send stream of the client connection
15: Ptr < TextWriter > textWriter = TextWriter :: Create ();
16: textWriter -> SetStream ( recvConns [ i ] -> GetSendStream ());
17: textWriter -> Open ();
18: textWriter -> WriteString ( "Hello Client" );
19: textWriter -> Close ();
20:
21: // finally send the response back to the client
22: recvConns [ i ] -> Send ();
23: }
在客户端获得服务器的应答 , 调用 TcpClient: :Recv() 方法会在数据到达之前一直阻塞 ( 在阻塞模式下 ), 或者立即返回 ( 在非阻塞模式下 ), 并在有服务器数据时返回 true:

1: // check if data is available from the server
2: if ( tcpClient -> Recv ())
3: {
4: // yep, data is available, get the recv stream and read the data from it
5: const Ptr < Stream >& recvStream = tcpClient -> GetRecvStream ();
6: Ptr < TextReader > textReader = TextReader :: Create ();
7: textReader -> SetStream ( recvStream );
8: textReader -> Open ();
9: String responseString = textReader -> ReadString ();
10: n_printf ( "The server said: %s/n" , responseString . AsCharPtr ());
11: textReader -> Close ();
12: }
客户端也应该通过调用 IsConnected() 访求检查连接是否有效 . 如果因为某些原因使连接断开 , 这个方法会返回 false.
注意 :
TcpServer TcpClient 并没有为能够跟不相关的客户端和服务器端而实现一个潜在的通信协议 ( 例如 , 一个 TcpServer 可以跟标准的 Web 浏览器客户端一起工作 , 还有一个 TcpClient 类可以跟一个标准的 HTTP 服务器通信 ).
现实世界的情况是 , 一个应用程序应该实现自己的健壮的通信协议 , 它至少会编码负载数据的长度 . 如果负载比最大包大小还要大 , 数据会以多个包发送并在客户端接收 . 客户端应该把数据解码成一个完整的消息 , 否则需要等待消息的数据接收完毕 .
字节次序问题
服务器和客户端可能运行在不同字节次序的的 CPU . 如果二进制数据通过网络发送 , 数据必需转换成两个客户端都一致的 网络字节顺序 ”. Nebula3 IO::BinaryReader IO::BinaryWriter 类中提供字节顺序的自动转换 . 只需要简单地调用下面的方法在网络通信流上读写就可以了 :

1: binaryReader -> SetStreamByteOrder ( System :: ByteOrder :: Network );
2: binaryWriter -> SetStreamByteOrder ( System :: ByteOrder :: Network );
Socket
网络子系统提供了一个把传统 socket 函数包装成 C++ 接口的 Socket . 一般情况下应用程序不直接使用 Socket , 而是使用更高级的像 TcpServer 这样的类 . 但也不是不可能在有的时候直接使用 socket 函数比 Socket 类更方便 .
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值