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 “走路”程序的运行结果