【学习总结】《Unity3D网络游戏》Part 1

以前都是在客户端这边研究一些逻辑和画面效果,乘着大四最后一个寒假学习一些服务端和客户端交互的内容,正好买了这本书,边看边做边写笔记,希望能用到毕设里面

另外多提一句。微软居然把暴雪收购了!!我超,这下压力来到了索尼这边

Socket相关

socket概念

网络上的两个程序通过一个双侠奴工的通信连接实现数据交换,这个连接的一端称为一个Socket。一个Socket包含了进行网络通信必需的五种信息:
连接使用的协议、本地主机的IP地址、本地的协议端口、远程主机的IP地址和远程协议端口
在这里插入图片描述

Socket通信的流程

  1. 开启一个连接之前,需要创建一个Socket对象(使用API Socket),然后绑定本地使用的端口(使用API Bind)。对服务端而言,绑定的步骤相当于给手机插上SIM卡,确定了“手机号”。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定的步骤
  2. 服务端开启监听(使用API Listen),等待客户端接入。相当于电话开机,等待别人呼叫
  3. 客户端连接服务器(使用API Connect),相当于手机拨号
  4. 服务器接收连接(使用API Accept),相当于接听电话并说出“喂”
  5. 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误
  6. 某一方关闭连接(使用API Close),操作系统会执行“四次挥手”的步骤,关闭双方连接,相当于挂断电话

在这里插入图片描述
上图画出了整个通信流程,而后续的代码基本就是根据这个流程图来写的

TCP和UDP协议

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,与TCP相对应的UDP协议是无连接的、不可靠的、但传输效率较高的协议

Unity异步Echo程序

客户端部分

首先给使用UGUI搭建一个简单的界面
在这里插入图片描述
客户端代码如下

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;

public class Echo : MonoBehaviour
{
    Socket socket;//定义套接字

    //UGUI 这里声明界面元素
    public InputField inputField;
    public Text text;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    string recvStr = "";

    //点击链接按钮
    public void Connection()
    {
        //Socket
        socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream,ProtocolType.Tcp);
        // socket.BeginConnect("127.0.0.1",8888,ConnectCallback,socket);//异步连接
        socket.Connect("127.0.0.1",8888);//直接连接函数
    }

    //Connect回调函数
    public void ConnectCallback(IAsyncResult ar)
    {
        try{
            Socket socket = (Socket) ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);//异步接收
        }
        catch(SocketException ex){
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    public void ReceiveCallback(IAsyncResult ar)
    {
        try{
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndReceive(ar);
            string s = System.Text.Encoding.Default.GetString(readBuff,0,count);
            recvStr = s + "\n" + recvStr;//这里显示历史聊天记录
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);//这里是一个递归的调用
        }
        catch(SocketException ex){
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    //点击发送按钮
    public void Send()
    {
        //Send
        string sendStr = inputField.text;//得到发送的消息
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
        // socket.Send(sendBytes);
    }

    //Send回调
    public void SendCallback(IAsyncResult ar)
    {
        try{
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send Succ" + count);
        }
        catch(SocketException ex){
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

    //Poll型客户端
    private void Update() 
    {
        if(socket == null){
            return;
        }

        if(socket.Poll(0,SelectMode.SelectRead))
        {
            byte[] readBuff = new byte[1024];
            int count = socket.Receive(readBuff);
            string recvStr = System.Text.Encoding.Default.GetString(readBuff,0,count);
            text.text = recvStr;
        }
    }
}

注释部分

  1. Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
    用于创建一个Socket对象,它的三个参数分别代表地址族,套接字类型和协议。地址族知名使用的是IPV4还是IPV6,
    InterNetwork代表IPV4,InterNetworkV6代表IPV6。
    SocketType是套接字类型,游戏中最常用的是字节流套接字,即Stream。ProtocolType指明协议

  2. 客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。
    Connect是一个阻塞方法,程序会卡住直到服务端回应(接受、拒绝或者超时)

  3. 客户端通过socket.send发送数据。GetBytes(字符串)把字符串转换成byte这也是一个阻塞方法。该方法接受一个byte[]类型的参数指明要发送的内容。Send的返回值指明发送数据的长度程序用System.Text.Encoding.Default.[]数组,然后发送给服务端

  4. 客户端使用socket.Receive接受服务端数据。
    Receive也是阻塞方法,没有收到服务端的数据时,程序将卡在Receive不会往下执行
    Receive带有一个byte[]类型的参数,它存储接收到的数据
    Receive的返回值指明接收到的数据的长度。之后使用System.Text.Encoding.Default.GetString(readBuff,0,count)将byte[]

  5. 通过socket.close()关闭连接

  6. 通过BeginConnect和EndConnect来让客户端代码变成异步进行,防止程序卡死
    IAsyncResult是.NET提供的一种异步操作,通过名为BeginXXX和EndXXX的两个方法来为实现原本同步方法的异步调用
    BeginXXX方法中包含同步方法中所需的参数,此外还包含两个参数:一个AsyncCallback委托和一个用户定义的状态对象委托用来调用回调方法,状态对象用来向回调方法传递状态信息,且BeginXXX方法返回一个实现IAsyncResult接口的对象,EndXXX方法用于结束异步操作并且返回结果
    EndXXX方法含有一个IAsyncResult参数,用于获取异步操作是否完成的信息,它的返回值与同步方法相同

  7. BeginReceive的参数为(readBuff,0,1024,ReceiveCallback,socket)
    第一个参数readBuff表示接收缓冲区,第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关
    第三个参数1024代表每次最多接收1024个字节,假如服务端回应一串长长的数据,那一次也只会收到1024个字节

  8. BeginReceive的调用位置
    程序在两个地方调用了BeginReceive,一个是ConnectCallback,在连接成功后,就开始接收数据,接收到数据之后,回调函数ReceiveCallback被调用
    另一个是BeginReceive内部,接受完一串数据之后,等待下一串数据的到来

  9. Update和recvStr
    在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出异常信息,所以只能在主线程中

  10. 异步BeginSend参数说明
    buffer Byte类型的数组,包含需要发送的数据
    offset 从Buffer中的offset位置开始发送
    size 将要发送的字节数
    socketFlags SocetFlags值的按位组合,这里设置为0
    callback 回调函数,一个AsyncCallbakc委托
    state 一个用户定义对象,其中包含发送操作的相关信息。当操作完成时,此对象会传递给EndSend委托

服务器部分

服务器这边使用Visual Studio建立一个C#控制台程序,代码如下

using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EchoServer
{
    //该类用于保存客户端的信息
    class ClientState
    {
        public Socket socket;
        public byte[] readBuff = new byte[1024];
    }

    class Program
    {
        //监听Socket
        static Socket listenfd;

        //定义一个字典集合保存所有客户端的信息,这样的结构可以通过clientState = clients[Socket]进行快速访问
        static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();

        static void Main(string[] args)
        {
            Console.WriteLine("Hello world");

            //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("[服务器]启动成功");

            //Select服务端所需列表
            List<Socket> checkRead = new List<Socket>();
            while(true)
            {
                //填充checkRead列表
                checkRead.Clear();
                checkRead.Add(listenfd);
                foreach(ClientState s in clients.Values)
                {
                    checkRead.Add(s.socket);
                }
                //select
                Socket.Select(checkRead, null, null,1000);
                //检查可读对象
                foreach (Socket s in checkRead)
                {
                    if(s==listenfd)
                    {
                        ReadListenfd(s);
                    }
                    else
                    {
                        ReadClientfd(s);
                    }
                }
            }

            //Poll 服务端
            //while(true)
            //{
            //    //检查listenfd
            //    if (listenfd.Poll(0, SelectMode.SelectRead))
            //    {
            //        ReadListenfd(listenfd);
            //    }
            //    //检查clientfd
            //    foreach(ClientState s in clients.Values)
            //    {
            //        Socket clientfd = s.socket;
            //        if (clientfd.Poll(0, SelectMode.SelectRead))
            //        {
            //            if (!ReadClientfd(clientfd))
            //            {
            //                break;
            //            }
            //        }
            //    }

            //    System.Threading.Thread.Sleep(1);//防止CPU占用过高
            //}

            //Accept 异步服务端
            //listenfd.BeginAccept(AccpetCallback,listenfd);

            //waiting
            Console.ReadLine();
        }

        //读取Listenfd
        public static void ReadListenfd(Socket listenfd)
        {
            Console.WriteLine("Accept");
            Socket clientfd = listenfd.Accept();//创建一个Socket用于处理应答
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);//将当前应答的Socket加入列表
        }

        //读取clientfd
        public static bool ReadClientfd(Socket clientfd)
        {
            ClientState state = clients[clientfd];//读取对应的Scoket
            //接收
            int count = 0;
            try
            {
                count = clientfd.Receive(state.readBuff);
            }
            catch(SocketException ex)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Receive SocketException" + ex.ToString());
                return false;
            }

            //客户端关闭
            if(count == 0)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return false;
            }

            //广播
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
            Console.WriteLine("Receive" + recvStr);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            foreach(ClientState cs in clients.Values)
            {
                cs.socket.Send(sendBytes);
            }
            return true;
        }

        //Accept回调
        //处理三件事情:
        //1.给新的连接分配ClientState,并且把它添加到clients列表之中
        //2.异步接收客户端数据
        //3.再次调用BeginAccept实现循环
        public static void AccpetCallback(IAsyncResult ar)
        {
            try
            {
                Console.WriteLine("[服务器]Accept");
                Socket listenfd = (Socket)ar.AsyncState;
                Socket clientfd = listenfd.EndAccept(ar);

                //clients列表,将连接上的客户端Socket加入列表
                ClientState state = new ClientState();
                state.socket = clientfd;
                clients.Add(clientfd,state);

                //接收数据BeginReceive
                clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);

                //继续接收Accept
                listenfd.BeginAccept(AccpetCallback,listenfd);
            }
            catch(SocketException ex)
            {
                Console.WriteLine("Socket Accept fail" + ex.ToString());
            }
        }

        //ReceiveCallBack是BeginReceive的回调函数,它也处理了三件事情
        //1.服务端收到消息之后,回应客户端
        //2.如果收到客户端关闭连接的信号“if(count==0)”,断开连接
        //3.继续调用BeginReceive接收下一个数据
        public static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                ClientState state = (ClientState)ar.AsyncState;
                Socket clientfd = state.socket;
                int count = clientfd.EndReceive(ar);

                //客户端关闭
                if(count == 0)
                {
                    clientfd.Close();
                    clients.Remove(clientfd);
                    Console.WriteLine("Socket Close");
                    return;
                }

                string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
                byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo"+ recvStr);

                foreach(ClientState s in clients.Values)
                {
                    s.socket.Send(sendBytes);//广播到所有用户
                }
                clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            }
            catch(SocketException ex)
            {
                Console.WriteLine("Socket Receive fail" + ex.ToString());
            }
        }
    }
}

//注释部分
//1.绑定Bind
//listenfd.Bind(ipEp)将给listenfd套接字绑定Ip和端口,这里也可以更改为真实的IP地址,一样有效

//2.监听Listen
//服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可以容纳等待接受的连接数,0代表不限制

//3.应答Accept
//开启监听后,服务器调用listenfd.Accept()接受客户端连接。Accept()返回了一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket,用来监听和应答客户端的连接
//对每一个客户端还有一个专门的Socket用来处理该客户端的数据

//4.IPAddress和IPEndPoint
//使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口

//5.Receive方法将接收到的字节流保存到readBuff上

//6.Poll方法的参数:返回的是一个BOOL值,用于检测Socket状态
//int microSeconds  等待回应的时间,以微秒为单位,如果该参数为-1,表示一直等待,如果为0,表示非阻塞
//mode 有三种可选模式 分别为:
//SelectRead:如果Socket可读,返回true,否则返回false
//SelectWrite:如果Socket可写,返回true,否则返回false
//SelectError:如果连接使白,返回true,否则返回false
//Poll方法将会检查Socket的状态,如果指定mode参数为SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为SelectMode.SelectWrite,可以确定是否为可写;设定为SelectMode.SelectError可以检测错误条件
//Poll将在指定的时段内阻止执行,如果希望无限期地等待响应,可以将microSeconds设置为一个负证数;如果希望不阻塞,可将其设置为0

//7.Poll方法的代码相对来说比异步要简洁一些(毕竟是去主动检测连接状态,而不是分给其它线程来做)
//但是对于服务器来说,POLL服务端的弊端也很明显,如果没有收到客户端数据,服务端也一直在循环,浪费了CPU,因此又有了后来的多路复用

//8.多路复用Select
//这是为了解决上述Poll服务端CPU性能浪费的问题出现的方法
//同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个或者多个Socket可读(或者可写,或者错误),那就返回这些可读的Socekt。如果没有,那就阻塞
//Select方法的参数如下
//checkRead 检测是否有可读的Scoket列表
//checkWrite 检测是否有可写的Socket列表
//checkError 检测是否有出错的Socket列表
//microSeconds 等待回应的时间,以微秒为单位,如果该参数为-1则表示一直等待,如果为0则表示非阻塞
//Select 可以确定一个或者多个Socket对象的状态。使用它时,需要先将一个或者多个套接字放入IList中。通过调用Selcet,可检查Socekt是否具有可读性
//在调用Select之后,Select将修改IList列表,仅仅保留那些漫组条件的套接字
//把包含6个Socket的列表传给Select,Select方法将会阻塞,等待超时或者某个Socket可读时返回,并且修改checkRead列表,仅仅保存可读的Socket
//当没有仍和可读Socket时,程序将会阻塞,不占用CPU资源


//9.Select服务端流程如下
//将监听Socekt和客户端Socket添加到待检测Socket可读状态的列表checkList中
//调用Select,程序中设置超时时间为1ms,若1ms内没有仍和刻度信息,Select方法将checkList列表变成空列表然后返回
//对Select处理后的每一个Socket做处理,如果监听Socket(listenfd)可读,说明有客户端连接,需要调用Accept。如果客户端Socket可读,说明客户端发送了消息,然后将消息发送给所有客户端

测试

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值