1.3 开始网络编程:Echo
1.3.1 什么是Echo程序
图1-12 Echo程序示意图
Echo程序是网络编程中最基础的案例。建立网络连接后,客户端向服务端发送一行文本,服务端收到后将文本发送回客户端(见图1-12)。
Echo程序分为客户端和服务端两个部分,客户端部分使用Unity实现,为了技术的统一,服务端使用C#语言实现。
1.3.2 编写客户端程序
由于本书偏重于开发网络游戏,重点讲解网络相关的内容。假定你对Unity的基本操作、UGUI有一定的了解(如果你对此还不是很了解,推荐阅读本书第1版中的入门章节)。
打开Unity,新建名为Echo的项目,制作简单的UGUI界面。在场景中添加两个按钮(右击Hierarchy面板,选择UI→Button,分别命名为ConnButton和SendButton。Unity会自动添加名为Canvas的画布和名为EventSystem的事件系统),添加一个输入框(命名为InputField)和一个文本框(命名为Text),如图1-13和表1-3所示。
图1-13 添加按钮和文本
表1-3 客户端UGUI界面部件说明
建立界面后,就可以开始写代码了。新建名为Echo.cs的脚本,输入下面的代码。(这段代码的结构和1.2.4节中的客户端流程一样,客户端通过Connect命令连接服务器,然后向服务器发送输入框中的文本;发送后等待服务器回应,并把服务器回应的字符串显示出来;代码中标有底纹的语句表示需要特别注意。)
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Net.Sockets; using UnityEngine.UI; public class Echo : MonoBehaviour { //定义套接字 Socket socket; //UGUI public InputField InputFeld; public Text text; //点击连接按钮 public void Connection() { //Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //Connect socket.Connect("127.0.0.1", 8888); } //点击发送按钮 public void Send() { //Send string sendStr = InputFeld.text; byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr); socket.Send(sendBytes); //Recv byte[] readBuff = new byte[1024]; int count = socket.Receive(readBuff); string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count); text.text = recvStr; //Close socket.Close(); } }
是否对代码有疑惑?不用怕,一句一句弄懂它。
1.3.3 客户端代码知识点
1.3.2节中的代码涉及不少网络编程的知识点,它们的含义如下。
(1)using System.Net.Sockets
Socket编程的API(如Socket、AddressFamily等)位于System.Net.Sockets命名空间中,需要引用它。
(2)创建Socket对象
Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)这一行用于创建一个Socket对象,它的三个参数分别代表地址族、套接字类型和协议。
表1-4 AddressFamily的含义
❑ 地址族指明使用IPv4还是IPv6,其含义如表1-4所示,本例中使用的是IPv4,即InterNetwork。
❑ SocketType是套接字类型,类型如表1-5所示,游戏开发中最常用的是字节流套接字,即Stream。
表1-5 SocketType的含义
❑ ProtocolType指明协议,本例使用的是TCP协议,部分协议类型如表1-6所示。若要使用传输速度更快的UDP协议而不是较为可靠的TCP(回顾1.2.5节的内容),需要更改协议类型“Socket(AddressFamily.InterNetwork, SocketType.Dgram, Protocol-Type.Udp)”。
表1-6 常用的协议
(3)连接Connect
客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。Connect是一个阻塞方法,程序会卡住直到服务端回应(接收、拒绝或超时)。
(4)发送消息Send
客户端通过socket.Send发送数据,这也是一个阻塞方法。该方法接受一个byte[]类型的参数指明要发送的内容。Send的返回值指明发送数据的长度(例子中没有使用)。程序用System.Text.Encoding.Default.GetBytes(字符串)把字符串转换成byte[]数组,然后发送给服务端。
(5)接收消息Receive
客户端通过socket.Receive接收服务端数据。Receive也是阻塞方法,没有收到服务端数据时,程序将卡在Receive不会往下执行。Receive带有一个byte[]类型的参数,它存储接收到的数据。Receive的返回值指明接收到数据的长度。之后使用System.Text.Encoding. Default.GetString(readBuff,0, count)将byte[]数组转换成字符串显示在屏幕上。
(6)关闭连接Close
通过socket.Close关闭连接。
1.3.4 完成客户端
编写完代码后,将Echo.cs拖曳到场景中任一物体上,并且给InputField和Test两个属性赋值(将对应游戏物体拖曳到属性右侧的输入框上),如图1-14所示。
图1-14 Echo组件
在属性面板中给ConnButton添加点击事件,设置为Echo组件的Connection方法。使得玩家点击连接按钮时,调用Echo组件的Connection方法,如图1-15所示(图中的游戏物体显示为“Main Camera”,是因为把Echo组件挂在了相机上,如果挂在其他物体上,需选择对应的物体)。采用同样的方法,给SendButton添加点击事件,设置为Echo组件的Send方法。
图1-15 设置点击事件
图1-16 连接服务端失败
由于服务端尚未开启,此时运行客户端,点击连接按钮,会提示无法连接,属于正常现象,如图1-16所示。
1.3.5 创建服务端程序
游戏服务端可以使用各种语言开发,为了与客户端统一,本书使用C#编写服务端程序。打开位于Unity安装目录下的MonoDevelop(也可以使用Visual Studio等工具),选择File→New→Solution创建一个控制台(Console)程序,如图1-17所示。
图1-17 创建控制台程序
MonoDevelop为我们创建了图1-18左侧所示的目录结构。打开Program.cs将能看到使用Console.WriteLine("Hello World! ")在屏幕上输出“Hello World! ”的代码。
图1-18 默认目录结构
选择Run→Restart Without Debugging即可运行程序(如图1-19所示)。如果程序一闪而过,可以在Console.WriteLine后面加上一行“Console.Read (); ”,让程序等待用户输入。读者还可以在程序目录下的bin\Debug找到对应的exe文件,直接执行。
图1-19 运行控制台程序
1.3.6 编写服务端程序
服务器遵照Socket通信的基本流程,先创建Socket对象,再调用Bind绑定本地IP地址和端口号,之后调用Listen等待客户端连接。最后在while循环中调用Accept应答客户端,回应消息。代码如下:
using System; using System.Net; using System.Net.Sockets; namespace EchoServer { class MainClass { public static void Main (string[] args) { Console.WriteLine ("Hello World! "); //Socket Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //Bind IPAddress ipAdr = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888); listenfd.Bind(ipEp); //Listen listenfd.Listen(0); Console.WriteLine("[服务器]启动成功"); while (true) { //Accept Socket connfd = listenfd.Accept (); Console.WriteLine ("[服务器]Accept"); //Receive byte[] readBuff = new byte[1024]; int count = connfd.Receive (readBuff); string readStr = System.Text.Encoding.Default.GetString (readBuff, 0, count); Console.WriteLine ("[服务器接收]" + readStr); //Send byte[] sendBytes = System.Text.Encoding.Default.GetBytes (readStr); connfd.Send(sendBytes); } } } }
图1-20 运行着的服务端程序
运行程序,读者将能看到如图1-20所示的界面,此时服务器阻塞在Accept方法。下面会详细解释这一段代码的含义。
1.3.7 服务端知识点
上一节的代码涉及不少网络编程的知识点,它们的含义如下。
(1)绑定Bind
listenfd.Bind(ipEp)将给listenfd套接字绑定IP和端口。程序中绑定本地地址“127.0.0.1”和8888号端口。127.0.0.1是回送地址,指本地机,一般用于测试。读者也可以设置成真实的IP地址,然后在两台计算机上分别运行客户端和服务端程序。
(2)监听Listen
服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可容纳等待接受的连接数,0表示不限制。
(3)应答Accept
开启监听后,服务器调用listenfd.Accept()接收客户端连接。本例使用的所有Socket方法都是阻塞方法,也就是说当没有客户端连接时,服务器程序卡在listenfd.Accept()不会往下执行,直到接收了客户端的连接。Accept返回一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket(例子中的listenfd)用来监听(Listen)和应答(Accept)客户端的连接,对每个客户端还有一个专门的Socket(例子中的connfd)用来处理该客户端的数据。
(4)IPAddress和IPEndPoint
使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口。
(5)System.Text.Encoding.Default.GetString
Receive方法将接收到的字节流保存到readBuff上,readBuff是byte型数组。GetString方法可以将byte型数组转换成字符串。同理,System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。
1.3.8 测试Echo程序
运行服务端和客户端程序,点击客户端的连接按钮。在文本框中输入文本,点击发送按钮后,客户端将会显示服务端的回应信息“Hello Unity”,如图1-21所示。
图1-21 Echo程序
图1-22 时间查询程序
读者可能会觉得Echo程序没太大用处,其实只要稍微修改一下,就能够制作有实际作用的程序,比如制作一个时间查询程序。更改服务端代码,发送服务端当前的时间,如果服务器时间是准确的,客户端便可以获取准确的时间,如图1-22所示。
//Send string sendStr = System.DateTime.Now.ToString(); byte[] sendBytes = System.Text.Encoding.Default.GetBytes (sendStr); connfd.Send (sendBytes);
思考一个问题:当前的服务端每次只能处理一个客户端的请求,如果我们要做一套聊天系统,它必须同时处理多个客户端请求,那又该怎样实现呢?