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