官术网_书友最值得收藏!

1.2 從網絡編程著手

開發游戲服務端,一般會從編寫聯網的程序著手,因為游戲服務端最重要的任務是處理網絡請求。

盡管市面上近乎所有的服務端開發圖書都會先花一大半篇幅講網絡編程,筆者也認同網絡編程很重要,但從“學以致用”的角度來看,先“不擇手段”(用現成的庫)把游戲做出來,再深入了解,也未嘗不可。

1.2.1 用打電話做比喻

理解網絡編程的第一步是了解網絡通信的流程。從圖1-5和圖1-6可以看出,網絡通信(指代TCP)和電話通信很相似。想象一下打電話的過程,拿起手機撥通號碼,等待對方說“喂”,然后開始通話,最后掛斷。游戲網絡通信的流程則是服務端先開啟監聽,等待客戶端的連接,然后交互操作,最后斷開,可見打電話的步驟一一對應著網絡編程的步驟。

圖1-5 用撥打電話比喻TCP通信

圖1-6 TCP通信的流程

1.2.2 最少要掌握的三個概念

理解網絡編程的第二步,是理解以下三個概念,因為任何網絡庫都會涉及它們。

1.IP和端口

在圖1-6中,客戶端和服務端有各自的地址,相當于手機號。網絡編程中的“手機號”由IP和端口兩部分構成。地址“127.0.0.1:8003”中的“127.0.0.3”是IP,代表著一臺設備,“8003”是端口,代表這臺設備中的某個任務。

知識拓展:端口是個邏輯概念。很久很久以前,計算機沒有多任務的概念,也沒有端口的概念,只要有兩臺計算機的地址,便能夠進行網絡通信。就像很久很久以前,每家每戶都住平房,寄信給別人時,只需在信封寫上××路××號一樣。隨著城市的發展,很多人住上了高樓,這時寫信的地址就變成××路××號××層。同樣,隨著計算機多任務系統的發展,人們定義了端口的概念,用于把不同的網絡消息分發給不同的任務。就像寫上門牌號能夠把信發送到每家每戶一樣,使用IP和端口也能夠把信息發送給對應的任務。

2.套接字

網絡連接的每一端都需要存儲一些信息,這些信息至少包括:連接使用的協議、自己的地址、對方的地址、將要發送的數據、接收到的數據等。存儲和處理這些信息的結構稱為套接字(Socket)。圖1-7展示了套接字包含的內容,每個Socket都包含網絡連接中一端的信息。每個客戶端需要一個Socket結構,服務端則需要N+1個Socket結構,其中N為客戶端的連接數,另外一個是服務端打開監聽的套接字。

圖1-7 Socket示意圖

3.Socket標識

既然服務端可以接收多個客戶端的連接,那么它就需要通過一種方法來區分消息來自哪個客戶端。有些語言(Node.js、C#)會直接傳遞Socket對象,而有些(C、C++)則會用一個數字標識符來代表該Socket對象。在圖1-8中,有4個客戶端連接服務端,4條連接分別對應fd1到fd4這4個標識,在監聽階段,服務端也會生成一個監聽標識符(圖中的listenfd),用于應答。

圖1-8 服務端的Socket標識示意圖

1.2.3 搭一個簡單的服務器

理解網絡編程的第三步,是能夠使用較現代的工具搭一臺服務器。如果用C/C++從底層搭起,要考慮的事情很多。網絡模塊是很通用的模塊,現代語言(Node.js、Golang等)會有成熟的封裝,各種游戲后端框架(Skynet、KBEngine等)也提供了網絡模塊。無論語法怎樣,服務端網絡模塊至少會提供“當客戶端連接”“當收到消息”“當客戶端斷開”這三種事件的接口。

說明:盡管本書以Skynet為例,但更重要的是希望讀者能夠掌握服務端開發的一般性方法,不僅僅是使用某個引擎。Skynet由C語言和Lua語言編寫,為了說明原理,書中也會用其他語言、引擎。只要有些許編程基礎,無論讀者是否學習過這些語言,都能看懂程序邏輯。

以Node.js為例,只需十多行代碼就能夠搭建簡單的服務器,見代碼1-1。

代碼1-1 用Node.js搭一個簡單服務器

(資源:Chapter1/1_simple_server.js)


var net = require('net');

var server = net.createServer(function(socket){
    console.log('connected, port:' + socket.remotePort);

    //接收到數據
    socket.on('data', function(data){
        console.log('client send:' + data);
        var ret = "嗯嗯," + data;
        socket.write(ret);
    });

    //斷開連接
    socket.on('close',function(){
        console.log('closed, port:' + socket.remotePort);
    });
});
server.listen(8001);

代碼1-1實現的功能為服務端通過listen監聽8001端口,如果有客戶端連接,它會打印“connected, port:XXXX”;若收到數據,它會打印“client send:XXX”,然后將消息稍作處理返回給客戶端;若客戶端斷開,它會打印“closed, port:XXXX”。注意代碼中兩種關鍵對象的區別,server代表整個服務端,socket代表某一條連接。

現在做個測試,使用可以發送字符串的TCP客戶端連接服務器(例如Linux下的Telnet程序),然后輸入任意內容,看看服務端將會有怎樣的輸出。圖1-9展示了客戶端和服務端的輸出內容,箭頭代表消息的流向,服務端監聽端口為8001,客戶端的端口為11450。

圖1-9 用Telnet連接服務端

說明:大多數游戲服務端部署于Linux系統上,Skynet也運行于Linux系統中,本書的代碼示例都在Linux(CentOS)環境下測試。讀者可以使用虛擬軟件VMware在自己的電腦上虛擬出一臺Linux服務器,也可以購買阿里云、騰訊云最便宜的云服務器來測試。

既然已經搭建了一臺服務器,接下來就要看看怎樣用它編寫游戲功能了。

1.2.4 讓角色走起來

下面將用一個示例說明怎樣編寫游戲功能,在該示例中會開發一套由服務端運算的“走路”程序,客戶端可以發送“left”“right”“up”“down”等文字指令,控制場景中的角色移動。開發這樣的程序涉及如下3個步驟:

1)明確角色有哪些屬性。

2)做好建立和斷開連接的處理。

3)做好收到客戶端數據的處理。

第一步:既是“走路”程序,必然會包含位置坐標。在代碼1-1的基礎上,定義如代碼1-2所示的Role類。

代碼1-2 “走路”服務器的部分偽代碼(Node.js)

(資源:Chapter1/2_run_server.js)


class Role{
    constructor() {
        this.x = 0;
        this.y = 0;
    }
}

第二步:服務端要把角色坐標轉發給所有的客戶端,就得有個結構來保存連接信息,在代碼1-3中定義的一個字典roles就是此結構。當新客戶端連接時,創建一個角色(Role)對象,并以socket為鍵,把它存入roles字典;當客戶端斷開時,刪除角色對象。

代碼1-3 “走路”服務器的部分偽代碼(Node.js)

(資源:Chapter1/2_run_server.js)


var roles = new Map();

var server = net.createServer(function(socket){
    //新連接
    roles.set(socket, new Role())

    //斷開連接
    socket.on('close',function(){
        roles.delete(socket)
    });
});

第三步:當服務端收到客戶端消息時,找到客戶端對應的角色對象,根據指令更新位置,最后把新位置廣播給客戶端,如代碼1-4所示。

代碼1-4 “走路”服務器的部分偽代碼(Node.js)

(資源:Chapter1/2_walk_server.js)


//接收到數據
    socket.on('data', function(data){
       var role = roles.get(socket);
       var cmd = String(data);
       //更新位置
       if(cmd == "left\r\n") role.x--;
       else if(cmd == "right\r\n") role.x++;
       else if(cmd == "up\r\n") role.y--;
       else if(cmd == "down\r\n") role.y++;

       //廣播
       for (let s of roles.keys()) {
           var id = socket.remotePort;
           var str = id + " move to " + role.x + " " + role.y + "\n";
           s.write(str);
       }
   });

程序運行的結果見圖1-10,客戶端A(設端口為51958)發送“向左走”的指令“left”,經由服務端計算,角色從位置(0, 0)移動到(-1, 0),再將新位置廣播給所有客戶端。

圖1-10 “走路”程序的運行結果

主站蜘蛛池模板: 屯留县| 溆浦县| 罗山县| 德化县| 平远县| 隆德县| 北辰区| 兴国县| 略阳县| 文山县| 梁河县| 石河子市| 游戏| 兴城市| 福州市| 枣阳市| 元江| 丰镇市| 张家港市| 丁青县| 华容县| 来宾市| 临清市| 岐山县| 娄底市| 香港| 丽水市| 高雄县| 巨野县| 宝坻区| 固镇县| 北碚区| 沽源县| 洪江市| 清河县| 曲麻莱县| 和平县| 若尔盖县| 南和县| 新巴尔虎右旗| 甘孜|