網路遊戲《叢林戰爭》開發與學習之(一):網路程式設計的基礎知識
《叢林戰爭》是一款完整的網路遊戲案例,運用U3D開發客戶端,Socket開發服務端,其中涉及到了網路程式設計、資料庫和Unity的功能實現,之前通過U3D開發了一個單機遊戲《黑暗之光》,並沒有涉及網路程式設計的知識,通過《叢林戰爭》這個完整的遊戲,系統性地學習網路程式設計,並進一步學習利用U3D開發遊戲。
本篇內容是網路程式設計的基礎知識,主要內容如下:
- 介紹TCP/IP的基本概念以及基礎TCP協議
- 實現伺服器端與客戶端的同步收發
- 實現伺服器端與客戶端的非同步收發
1. TCP/IP基本概念
下圖是一個網路的簡化圖,可以看出IP的作用是在複雜的網路環境中將資料包發給最終的目標地址,本節主要介紹IP、埠號和TCP協議。
1.1 IP
IP分為區域網IP和公網IP,每臺機都有這兩個IP。當一個路由器連線多個主機,相當於路由器與這些主機組成了一個區域網,路由器會給每個主機分配一個區域網IP,可以在Win+R > cmd,小黑窗中輸入ipconfig查到,公網IP則可以通過百度查詢到。
1.2 埠號
主機之間通過路由器進行通訊,以主機B與主機D通訊為例,B通過ip地址找到了主機D,連線建立之後,考慮到進行通訊最終是軟體之間的互動,因此需要搞清是與什麼軟體進行通訊。假設D中有QQ、微信、絕地求生等軟體,需要為每個軟體分配一個埠號。(即ip找機器,埠號找軟體)
下圖就是一個例子,IP資料172.23.12.14找到了主機A,A中各個埠號對應不同應用(圖中以服務端為例),需要通過獨有的埠號找到對應軟體。後期也會學習如何向系統申請埠號。
1.3 TCP協議(三次握手與四次揮手)
在這裡大致解釋一下:(主機A代表張三,B代表李四)
三次握手:
張三:李四你在嗎?(請求連線)
李四:張三我在的(確認應答)
張三:好的我要發資料了(對李四的確認應答)
四次揮手:
張三:李四我沒事了,掛電話了(請求切斷)
李四:好的,知道你沒事了(確認應答)
李四:那就這樣,掛電話吧(請求切斷)
張三:那我掛了(確認應答)
2 利用TCP協議實現服務端與客戶端的通訊
利用C#伺服器控制應用程式實現簡單的通訊,步驟如下圖,圖中所示即為服務端與客戶端的連線步驟,TCP連線都是基於這個規則。
2.1 服務端與客戶端的同步收發
服務端程式碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
namespace TCPServer
{
class Program
{
static void Main(string[] args)
{
Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //第一個引數表示時候用ipv4,第二個Stream表示TCP傳輸用到的資料流,如果是UDP協議可以用Dgram報文
//IPAddress xxx.xx.xx.xx IPEndPoint xxx.xx.xx.x:port 需要用到using System.Net;
IPAddress ipAddr = IPAddress.Parse("127.0.0.1"); //伺服器端申請ip及埠號
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 1200);
socketServer.Bind(ipEndPoint); //進行繫結
socketServer.Listen(10); //最多處理10個連線的佇列
Socket socketClient = socketServer.Accept(); // 接收客戶端連線,之後通過socketClient進行對客戶端資料的收發
//傳送給客戶端
string sendMsg = "hello,客戶端";
byte[] sendData = Encoding.UTF8.GetBytes(sendMsg); //字串轉為bype陣列傳送,用到using System.Text;
socketClient.Send(sendData);
//從客戶端接收
byte[] recvData = new byte[1024]; //分配1024位元組大小的空間儲存
int count = socketClient.Receive(recvData); //接收到的位元組大小
string recvMsg = Encoding.UTF8.GetString(recvData, 0, count); //陣列轉為字串,轉換0~count長度,由於recvData未填滿,直接轉換recvData會出錯
Console.WriteLine(recvMsg);
socketClient.Close();
socketServer.Close();
}
}
}
客戶端程式碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
namespace TCPClient
{
class Program
{
static void Main(string[] args)
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1200));
//從服務端接收資料
byte[] dataRecv = new byte[1024];
int count = clientSocket.Receive(dataRecv);
string msgRecv = Encoding.UTF8.GetString(dataRecv, 0, count);
Console.WriteLine(msgRecv);
//向服務端傳送資料
string msgSend = Console.ReadLine();
byte[] dataSend = Encoding.UTF8.GetBytes(msgSend); //字串轉為bype陣列傳送,用到using System.Text;
clientSocket.Send(dataSend);
Console.ReadKey();
clientSocket.Close();
}
}
}
結果如下。
上述方法是一個同步傳送與接收的例子,當伺服器處於接收狀態時,無法傳送資料給客戶端,反之亦然。因此需要一種非同步的處理方法。
2.2 服務端實現非同步接收
在伺服器端新增非同步接收功能,將之前的程式碼修改為:
static byte[] dataBuffer = new byte[1024];
static void StartAsync()
{
Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //第一個引數表示時候用ipv4,第二個Stream表示TCP傳輸用到的資料流,如果是UDP協議可以用Dgram報文
//IPAddress xxx.xx.xx.xx IPEndPoint xxx.xx.xx.x:port 需要用到using System.Net;
IPAddress ipAddr = IPAddress.Parse("127.0.0.1"); //伺服器端申請ip及埠號
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 1200);
socketServer.Bind(ipEndPoint); //進行繫結
socketServer.Listen(10); //最多處理10個連線的佇列
Socket socketClient = socketServer.Accept(); // 接收客戶端連線,之後通過socketClient進行對客戶端資料的收發
//傳送給客戶端
string sendMsg = "hello,客戶端";
byte[] sendData = Encoding.UTF8.GetBytes(sendMsg); //字串轉為bype陣列傳送,用到using System.Text;
socketClient.Send(sendData);
//從客戶端接收
socketClient.BeginReceive(dataBuffer, 0, 1024, SocketFlags.None, ReceiveCallBack, socketClient); //開始監聽客戶端資料,當接收到資料時呼叫RecvCallBack,實現非同步接收
Console.ReadKey();
socketClient.Close();
socketServer.Close();
}
static void ReceiveCallBack(IAsyncResult ar)
{
Socket clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar);
string msg = Encoding.UTF8.GetString(dataBuffer, 0, count);
Console.WriteLine("接收到的訊息為:" + msg);
clientSocket.BeginReceive(dataBuffer, 0, 1024, SocketFlags.None, ReceiveCallBack, clientSocket); //接收完一條訊息後回掉自身,繼續接收
}
在客戶端傳送資料程式碼前加一個while(true)以達到不斷髮送資料的目的,結果如下。
2.3 服務端接收多個客戶端的非同步資料
若要使服務端同時接收多個客戶端的資料,服務端實現非同步收發需要呼叫AcceptCallBack
socketServer.BeginAccept(AcceptCallBack, socketServer);
服務端程式碼如下
static void StartAsyncAcceptAndRecv()
{
Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //第一個引數表示時候用ipv4,第二個Stream表示TCP傳輸用到的資料流,如果是UDP協議可以用Dgram報文
IPAddress ipAddr = IPAddress.Parse("127.0.0.1"); //伺服器端申請ip及埠號
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 1200);
socketServer.Bind(ipEndPoint); //進行繫結
socketServer.Listen(10); //最多處理10個連線的佇列
//Socket socketClient = socketServer.Accept(); // 接收客戶端連線,之後通過socketClient進行對客戶端資料的收發
socketServer.BeginAccept(AcceptCallBack, socketServer);
}
客戶端不變,此時服務端可以接收多個客戶端的資料,如下圖所示。