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);

思考一个问题:当前的服务端每次只能处理一个客户端的请求,如果我们要做一套聊天系统,它必须同时处理多个客户端请求,那又该怎样实现呢?