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類更方便.