1. 程式人生 > >網路遊戲《叢林戰爭》開發與學習之(一):網路程式設計的基礎知識

網路遊戲《叢林戰爭》開發與學習之(一):網路程式設計的基礎知識

《叢林戰爭》是一款完整的網路遊戲案例,運用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);
        }

客戶端不變,此時服務端可以接收多個客戶端的資料,如下圖所示。