Nebula3的網(wǎng)絡(luò)子系統(tǒng)提供了基于TCP協(xié)議的簡單C/S通信模式. 它并沒有打算做成大廳,會話管理還有玩家數(shù)據(jù)同步的面向游戲的高級通信. 這些以后會在更高層的Nebula3子系統(tǒng)中出現(xiàn).
使用IP地址
一個IpAddress對象通過主機名字或TCP/IP地址加一個端口號定義了一個通信端點. IpAddress對象可以通過多數(shù)方式建立:
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: // 從主機的第一個合法網(wǎng)絡(luò)適配器的地址和端口號
17: IpAddress ipAddr("self",1234);
18:
19: // 從主機的第一個連接到互聯(lián)網(wǎng)的網(wǎng)絡(luò)適配器的地址和端口號:
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();
建立一個客戶端/服務(wù)器系統(tǒng)
網(wǎng)絡(luò)子系統(tǒng)用TcpServer和TcpClient類實現(xiàn)了一個易用的基于TCP協(xié)議的C/S系統(tǒng). 一個TcpServer可以為任意數(shù)量的TcpClient服務(wù).
建立一個服務(wù)器可以這么做:
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端口監(jiān)聽客戶端連接請求的服務(wù)器.
為了跟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();
這里假設(shè)服務(wù)端和客戶端運行在同一臺機器上(因為客戶端連接到了”localhost”).
像上面那樣非阻塞的情況, Connect()方法不是返回TcpClient::Success(這意味著連接建立好了)就是TcpClient::Connecting, 如果這樣的話, 應(yīng)用程序需要繼續(xù)調(diào)用Connect()方法. 如果連接錯誤, 會返回一個TcpClient::Error的返回值.
如果是阻塞的, Connect()方法直到連接建立(結(jié)果是TcpClient::Success)或發(fā)生錯誤才會返回.
注意:一個交互式應(yīng)用程序不應(yīng)該在網(wǎng)絡(luò)通信時阻塞, 而應(yīng)不斷地為用戶提供反饋.
一旦連接建立, 服務(wù)端會為每個客戶機建立一個TcpClientConnection對象. TcpClientConnection在服務(wù)器上表示客戶機, 并且負責從客戶機收發(fā)數(shù)據(jù).
要進行接收和發(fā)送數(shù)據(jù)的話, 需使用IO::Stream對象. 在通信流上連接IO::StreamReader和IO::StreamWriter對象后, 從流中編碼和解碼數(shù)據(jù)是一件非常容易的事情.
注意:發(fā)送數(shù)據(jù)并不是即時的, 而是在Send()方法被調(diào)用之前會一直保存在發(fā)送流當中.
要客戶端給服務(wù)器發(fā)送一些文本數(shù)據(jù)話, 只要從發(fā)送流獲取一個指針, 向其中寫入數(shù)據(jù)后調(diào)用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: }
在服務(wù)器端接收客戶端數(shù)據(jù), 應(yīng)用程序需要要頻繁地(每幀一次)緩存帶有客戶羰數(shù)據(jù)的TcpClientConnection. 可能不只一個TcpClientConnection在等待處理, 因此處理循環(huán)應(yīng)該像這樣:
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: }
在客戶端獲得服務(wù)器的應(yīng)答, 調(diào)用TcpClient::Recv()方法會在數(shù)據(jù)到達之前一直阻塞(在阻塞模式下), 或者立即返回(在非阻塞模式下), 并在有服務(wù)器數(shù)據(jù)時返回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: }
客戶端也應(yīng)該通過調(diào)用IsConnected()訪求檢查連接是否有效. 如果因為某些原因使連接斷開, 這個方法會返回false.
注意:
TcpServer和TcpClient并沒有為能夠跟不相關(guān)的客戶端和服務(wù)器端而實現(xiàn)一個潛在的通信協(xié)議(例如, 一個TcpServer可以跟標準的Web瀏覽器客戶端一起工作, 還有一個TcpClient類可以跟一個標準的HTTP服務(wù)器通信).
現(xiàn)實世界的情況是, 一個應(yīng)用程序應(yīng)該實現(xiàn)自己的健壯的通信協(xié)議, 它至少會編碼負載數(shù)據(jù)的長度. 如果負載比最大包大小還要大, 數(shù)據(jù)會以多個包發(fā)送并在客戶端接收. 客戶端應(yīng)該把數(shù)據(jù)解碼成一個完整的消息, 否則需要等待消息的數(shù)據(jù)接收完畢.
字節(jié)次序問題
服務(wù)器和客戶端可能運行在不同字節(jié)次序的的CPU上. 如果二進制數(shù)據(jù)通過網(wǎng)絡(luò)發(fā)送, 數(shù)據(jù)必需轉(zhuǎn)換成兩個客戶端都一致的”網(wǎng)絡(luò)字節(jié)順序”. Nebula3在IO::BinaryReader和IO::BinaryWriter類中提供字節(jié)順序的自動轉(zhuǎn)換. 只需要簡單地調(diào)用下面的方法在網(wǎng)絡(luò)通信流上讀寫就可以了:
1: binaryReader->SetStreamByteOrder(System::ByteOrder::Network);
2: binaryWriter->SetStreamByteOrder(System::ByteOrder::Network);
Socket類
網(wǎng)絡(luò)子系統(tǒng)提供了一個把傳統(tǒng)socket函數(shù)包裝成C++接口的Socket類. 一般情況下應(yīng)用程序不直接使用Socket類, 而是使用更高級的像TcpServer這樣的類. 但也不是不可能在有的時候直接使用socket函數(shù)比Socket類更方便.