上接Unity网络开发基础(上):

第6章: 网络通信—文件传输FTP

第1节: FTP工作原理

FTP工作原理

FTP是什么

FTP(File Transfer Protocol)

文件传输协议,是支持Internet文件传输的各种规则所组成的集合

这些规则使Internet用户可以把文件从一台主机拷贝到另一台主机上

除此之外,FTP还提供登录、目录查询以及其他会话控制等功能

说人话:FTP文件传输协议就是一个在网络中上传下载文件的一套规则

FTP的工作原理

划重点:FTP的本质是TCP通信

通过FTP传输文件,双发至少需要建立两个TCP连接

一个称为控制连接,用于传输FTP命令

一个称为数据连接,用于传输文件数据

FTP的数据连接和控制连接方向一般是相反的

举例说明:

用户使用FTP客户端连接FTP服务区请求下载文件

控制连接方向:

客户端主动连接服务器告知其下载命令

数据连接方向:

服务端主动连接客户端下发数据

建立传输模式

当客户端和FTP服务器建立控制连接后

需要告诉服务器采用那种传输模式

1.主动模式(Port模式)

服务器主动连接客户端,然后传输文件

2.被动模式(Passive模式)

客户端主动连接服务器

即控制连接和数据连接都由客户端发起

一般情况下主动模式会受到客户端防火墙影响,所以被动模式使用较

数据传输方式

在使用FTP进行数据传输时

有两种数据传输方式

1.ASCII传输方式

以ASCII编码方式传输数据,适用于传输

仅包含英文的命令和参数或者英文文本文件

2.二进制传输方式(建议使用该方式)

可以指定采用哪种编码传输命令和文件数据

如果传输的文件不是英文文件则应该采用该方式

一般情况下,使用FTP传输文件时

客户端必须先登录服务器,获得相应权限后

才能上传或下载文件

服务器也可以允许用户匿名登录FTP

不需要都拥有一个合法账号

我们如何学习FTP

在实际学习过程中,我们并不需要利用FTP原理来实现FTP通信

FTP工作原理相关知识点,主要做了解

C#中实现了FTP通信需要用到的相关类

FtpWebRequest、FtpWebResponse、NetworkCredential

我们的主要学习的内容就是围绕这三个类来实现

  1. 如何搭建FTP服务器

  2. 上传文件到FTP服务器

  3. 从FTP服务器下载文件到本地

准备:搭建FTP服务器

知识点一 搭建FTP服务器的几种方式

  1. 使用别人做好的FTP服务器软件 (学习阶段建议使用)

  2. 自己编写FTP服务器应用程序,基于FTP的工作原理,用Socket中TCP通信来进行编程(工作后由后端程序员来做)

  3. 将电脑搭建为FTP文件共享服务器 (工作后由后端程序员来做)

第2,3点前端程序主要做了解

一般在工作中不会由前端程序来完成这部分工作

知识点二 使用别人做好的FTP服务器软件来搭建FTP服务器

下载Serv-U等FTP服务器软件

在想要作为FTP服务器的电脑上运行之

  1. 创建域 直接不停下一步即可

  2. 使用单向加密

  3. 创建用于上传下载的FTP 账号和密码

总结

在实际商业项目开发当中,如果需要用FTP来进行文件传输

那么FTP服务器的解决方案都是由后端程序员来完成的

不管它使用哪种方式来搭建FTP服务器

只要能正常上传下载内容并且保证安全性即可

第2节: FTP前端相关操作

FTP关键类

知识点一 NetworkCredential类

命名空间:System.Net

NetworkCredential通信凭证类

用于在Ftp文件传输时,设置账号密码

账号密码来自ftp服务器里面已经设定好的用户。

NetworkCredential n = new NetworkCredential("账号", "密码");

知识点二 FtpWebRequest类

命名空间:System.Net

Ftp文件传输协议客户端操作类

主要用于:上传、下载、删除服务器上的文件

重要方法
//1.Create 创建新的WebRequest,用于进行Ftp相关操作
FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://127.0.0.1/Test.txt")) as FtpWebRequest;
//2.Abort  如果正在进行文件传输,用此方法可以终止传输
req.Abort();
//3.GetRequestStream  获取用于上传的流
Stream s = req.GetRequestStream();
//4.GetResponse  返回FTP服务器响应
//FtpWebResponse res = req.GetResponse() as FtpWebResponse;

重要成员
//1.Credentials 通信凭证,设置为NetworkCredential对象
req.Credentials = n;
//2.KeepAlive bool值,当完成请求时是否关闭到FTP服务器的控制连接(默认为true,不关闭)
req.KeepAlive = false;
//3.Method  操作命令设置
//  WebRequestMethods.Ftp类中的操作命令属性
//  DeleteFile  删除文件
//  DownloadFile    下载文件    
//  ListDirectory   获取文件简短列表
//  ListDirectoryDetails    获取文件详细列表
//  MakeDirectory   创建目录
//  RemoveDirectory 删除目录
//  UploadFile  上传文件
req.Method = WebRequestMethods.Ftp.DownloadFile;
//4.UseBinary 是否使用2进制传输
req.UseBinary = true;
//5.RenameTo    重命名
//req.RenameTo = "myTest.txt";

知识点三 FtpWebResponse类

命名空间:System.Net

它是用于封装FTP服务器对请求的响应

它提供操作状态以及从服务器下载数据

我们可以通过FtpWebRequest对象中的GetResponse()方法获取

当使用完毕时,要使用Close释放

通过它来真正的从服务器获取内容

FtpWebResponse res = req.GetResponse() as FtpWebResponse;

重要方法
//1.Close:释放所有资源
res.Close();
//2.GetResponseStream:返回从FTP服务器下载数据的流
Stream stream = res.GetResponseStream();

重要成员

1.ContentLength:接受到数据的长度

2.ContentType:接受数据的类型

3.StatusCode:FTP服务器下发的最新状态码

4.StatusDescription:FTP服务器下发的状态代码的文本

5.BannerMessage:登录前建立连接时FTP服务器发送的消息

6.ExitMessage:FTP会话结束时服务器发送的消息

7.LastModified:FTP服务器上的文件的上次修改日期和时间

总结

通过C#提供的这3个类

我们便可以完成客户端向FTP服务器

操作文件的需求,比如

上传、下载、删除文件

FTP上传文件

知识点一 使用FTP上传文件关键点

1.通信凭证

进行Ftp连接操作时需要的账号密码

2.操作命令 WebRequestMethods.Ftp

设置你想要进行的Ftp操作

3.文件流相关 FileStream 和 Stream

上传和下载时都会使用的文件流

4.保证FTP服务器已经开启

并且能够正常访问

知识点二 FTP上传

try
{
    //1.创建一个Ftp连接
    FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.50.49/pic.png")) as FtpWebRequest;
    //2.设置通信凭证(如果不支持匿名 就必须设置这一步)
    //将代理相关信息置空 避免 服务器同时有http相关服务 造成冲突
    req.Proxy = null;
    NetworkCredential n = new NetworkCredential("MrTang", "MrTang123");
    req.Credentials = n;
    //请求完毕后 是否关闭控制连接,如果想要关闭,可以设置为false
    req.KeepAlive = false;
    //3.设置操作命令
    req.Method = WebRequestMethods.Ftp.UploadFile;//设置命令操作为 上传文件
    //4.指定传输类型
    req.UseBinary = true;
    //5.得到用于上传的流对象
    Stream upLoadStream = req.GetRequestStream();

    //6.开始上传
    using (FileStream file = File.OpenRead(Application.streamingAssetsPath + "/test.png"))
    {
        //我们可以一点一点的把这个文件中的字节数组读取出来 然后存入到 上传流中
        byte[] bytes = new byte[1024];

        //返回值 是真正从文件中读了多少个字节
        int contentLength = file.Read(bytes, 0, bytes.Length);
        //不停的去读取文件中的字节 除非读取完毕了 不然一直读 并且写入到上传流中
        while (contentLength != 0)
        {
            //写入上传流中
            upLoadStream.Write(bytes, 0, contentLength);
            //写完了继续读
            contentLength = file.Read(bytes, 0, bytes.Length);
        }
        //除了循环就证明 写完了 
        file.Close();
        upLoadStream.Close();
        //上传完毕
        print("上传结束");
    }
}
catch (Exception e)
{
    print("上传出错 失败" + e.Message);
}

总结

C#已经把Ftp相关操作封装的很好了

我们只需要熟悉API,直接使用他们进行FTP上传即可

我们主要做的操作是

把本地文件流读出字节数据写入到要上传的FTP流中

FTP上传相关API也有异步方法

使用上和以前的TCP相关类似

这里不赘述

FTP下载文件

知识点一 使用FTP下载文件关键点

1.通信凭证

进行Ftp连接操作时需要的账号密码

2.操作命令 WebRequestMethods.Ftp

设置你想要进行的Ftp操作

3.文件流相关 FileStream 和 Stream

上传和下载时都会使用的文件流

下载文件流使用FtpWebResponse类获取

4.保证FTP服务器已经开启

并且能够正常访问

知识点二 FTP下载

try
{
    //1.创建一个Ftp连接
    //这里和上传不同,上传的文件名 是自己定义的  下载的文件名 一定是资源服务器上有的
    FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.50.49/实战就业路线.jpg")) as FtpWebRequest;
    //2.设置通信凭证(如果不支持匿名 就必须设置这一步)
    req.Credentials = new NetworkCredential("MrTang", "MrTang123");
    //请求完毕后 是否关闭控制连接,如果要进行多次操作 可以设置为false
    req.KeepAlive = false;
    //3.设置操作命令
    req.Method = WebRequestMethods.Ftp.DownloadFile;
    //4.指定传输类型
    req.UseBinary = true;
    //代理设置为空
    req.Proxy = null;
    //5.得到用于下载的流对象
    //相当于把请求发送给FTP服务器 返回值 就会携带我们想要的信息
    FtpWebResponse res = req.GetResponse() as FtpWebResponse;
    //这就是下载的流
    Stream downLoadStream = res.GetResponseStream();

    //6.开始下载
    print(Application.persistentDataPath);
    using (FileStream fileStream = File.Create(Application.persistentDataPath + "/MrTang112233.jpg"))
    {
        byte[] bytes = new byte[1024];
        //读取下载下来的流数据
        int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
        //一点一点的 下载到本地流中
        while (contentLength != 0)
        {
            //把读取出来的字节数组 写入到本地文件流中
            fileStream.Write(bytes, 0, contentLength);
            //那我们继续读
            contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
        }
        //下载结束 关闭流
        downLoadStream.Close();
        fileStream.Close();
    }
    print("下载结束");
}
catch (Exception e)
{
    print("下载出错" + e.Message);
}

总结

C#已经把Ftp相关操作封装的很好了

我们只需要熟悉API,直接使用他们进行FTP下载即可

我们主要做的操作是

把下载文件的FTP流读出字节数据写入到本地文件流中

关于两种流的区别

我们会发现,上传和下载的ftp服务器的两种流对象是不同的。

数据流的方向
  • 上传(写入操作): 在上传过程中,数据流的方向是从客户端(例如,你的程序)到服务器。你创建一个流,向其中写入数据,这些数据随着流的传输被发送到服务器。这里的关键操作是写入(pushing data)。

  • 下载(读取操作): 在下载过程中,数据流的方向相反,是从服务器到客户端。服务器准备数据,当你从响应中获取流时,你读取(pulling data)这个流中的数据,即从服务器接收数据。

控制流程的发起方
  • 上传: 发起方是客户端。客户端决定何时开始传输,初始化连接,并直接向这个连接写入数据。

  • 下载: 发起方依然是客户端(通过发送下载请求),但服务器需要先响应这个请求,准备好数据,并开始发送数据流。客户端在这个过程中是接收方,等待数据可用来读取。

流的作用
  • 上传中的流(GetRequestStream: 作为数据的发送通道,允许客户端将文件数据写入到这个流中,数据随后被发送到服务器。

  • 下载中的流(GetResponseStream: 作为数据的接收通道,服务器通过这个流发送数据,客户端读取这个流来接收数据。

本质理解

本质上,这两种流体现了网络通信中的两个基本动作:发送和接收。在上传和下载操作中,流的使用体现了这两个过程的不同需求和行为模式。上传涉及到客户端主动推送数据到服务器,因此流用于写入;下载涉及到服务器响应请求并发送数据给客户端,因此流用于读取。

FTP的其它文件操作

知识点一 其它操作指什么?

除了上传和下载,我们可能会对FTP服务器上的内容进行其它操作

最常用的有:

  1. 删除文件

  2. 获取文件大小

  3. 创建文件夹

  4. 获取文件列表

等等

知识点二 实现

我们直接实现一个FTP管理类,包含了我们这一节实现的所有方法:

public class FtpMgr
{
    private static FtpMgr instance = new FtpMgr();
    public static FtpMgr Instance => instance;

    //远端FTP服务器的地址
    private string FTP_PATH = "ftp://192.168.50.49/";
    //用户名和密码
    private string USER_NAME = "MrTang";
    private string PASSWORD = "MrTang123";

    /// <summary>
    /// 上传文件到Ftp服务器(异步)
    /// </summary>
    /// <param name="fileName">FTP上的文件名</param>
    /// <param name="localPath">本地文件路径</param>
    /// <param name="action">上传完毕后想要做什么的委托函数</param>
    public async void UpLoadFile(string fileName, string localPath, UnityAction action = null)
    {
        await Task.Run(() =>
        {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.UploadFile;
                //代理设置为空
                req.Proxy = null;
                //3.上传
                Stream upLoadStream = req.GetRequestStream();
                //开始上传
                using (FileStream fileStream = File.OpenRead(localPath))
                {
                    byte[] bytes = new byte[1024];
                    //返回值 为具体读取了多少个字节
                    int contentLength = fileStream.Read(bytes, 0, bytes.Length);
                    //有数据就上传
                    while (contentLength != 0)
                    {
                        //读了多少就写(上传)多少
                        upLoadStream.Write(bytes, 0, contentLength);
                        //继续从本地文件中读取数据
                        contentLength = fileStream.Read(bytes, 0, bytes.Length);
                    }
                    //上传结束
                    fileStream.Close();
                    upLoadStream.Close();
                }
                Debug.Log("上传成功");
            }
            catch (Exception e)
            {
                Debug.Log("上传文件出错" + e.Message);
            }
        });
        //上传结束后 你想在外部做的事情
        action?.Invoke();
    }

    /// <summary>
    /// 下载文件从Ftp服务器当中(异步)
    /// </summary>
    /// <param name="fileName">FTP上想要下载的文件名</param>
    /// <param name="localPath">存储的本地文件路径</param>
    /// <param name="action">下载完毕后想要做什么的委托函数</param>
    public async void DownLoadFile(string fileName, string localPath, UnityAction action = null)
    {
        await Task.Run(()=> {
            try
            {
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.DownloadFile;
                //代理设置为空
                req.Proxy = null;
                //3.下载
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                Stream downLoadStream = res.GetResponseStream();
                //写入到本地文件中
                using (FileStream fileStream = File.Create(localPath))
                {
                    byte[] bytes = new byte[1024];
                    //读取数据
                    int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    //一点一点的写入
                    while (contentLength != 0)
                    {
                        //读多少 写多少
                        fileStream.Write(bytes, 0, contentLength);
                        //继续读
                        contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    }
                    fileStream.Close();
                    downLoadStream.Close();
                }
                res.Close();

                Debug.Log("下载成功");
            }
            catch (Exception e)
            {
                Debug.Log("下载失败" + e.Message);
            }
        });

        //如果下载结束有想做的事情 在这里调用外部传入的委托函数
        action?.Invoke();
    }


    /// <summary>
    /// 移除指定的文件
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="action">移除过后想做什么的委托函数</param>
    public async void DeleteFile(string fileName, UnityAction<bool> action = null)
    {
        await Task.Run(()=> {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.DeleteFile;
                //代理设置为空
                req.Proxy = null;
                //3.真正的删除
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                res.Close();

                action?.Invoke(true);
            }
            catch (Exception e)
            {
                Debug.Log("移除失败" + e.Message);
                action?.Invoke(false);
            }        
        });
    }


    /// <summary>
    /// 获取FTP服务器上某个文件的大小 (单位 是 字节)
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="action">获取成功后传递给外部 具体的大小</param>
    public async void GetFileSize(string fileName, UnityAction<long> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.GetFileSize;
                //代理设置为空
                req.Proxy = null;
                //3.真正的获取
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                //把大小传递给外部
                action?.Invoke(res.ContentLength);

                res.Close();
            }
            catch (Exception e)
            {
                Debug.Log("获取大小失败" + e.Message);
                action?.Invoke(0);
            }
        });
    }


    /// <summary>
    /// 创建一个文件夹 在FTP服务器上
    /// </summary>
    /// <param name="directoryName">文件夹名字</param>
    /// <param name="action">创建完成后的回调</param>
    public async void CreateDirectory(string directoryName, UnityAction<bool> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.MakeDirectory;
                //代理设置为空
                req.Proxy = null;
                //3.真正的创建
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                res.Close();

                action?.Invoke(true);
            }
            catch (Exception e)
            {
                Debug.Log("创建文件夹失败" + e.Message);
                action?.Invoke(false);
            }
        });
    }

    /// <summary>
    /// 过去所有文件名
    /// </summary>
    /// <param name="directoryName">文件夹路径</param>
    /// <param name="action">返回给外部使用的 文件名列表</param>
    public async void GetFileList(string directoryName, UnityAction<List<string>> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.ListDirectory;
                //代理设置为空
                req.Proxy = null;
                //3.真正的创建
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                //把下载的信息流 转换成StreamReader对象 方便我们一行一行的读取信息
                StreamReader streamReader = new StreamReader(res.GetResponseStream());

                //用于存储文件名的列表
                List<string> nameStrs = new List<string>();
                //一行行的读取
                string line = streamReader.ReadLine();
                while (line != null)
                {
                    nameStrs.Add(line);
                    line = streamReader.ReadLine();
                }
                res.Close();

                action?.Invoke(nameStrs);
            }
            catch (Exception e)
            {
                Debug.Log("获取文件列表失败" + e.Message);
                action?.Invoke(null);
            }
        });
    }
}

总结

FTP对于我们的作用

1.游戏当中的一些上传和下载功能

2.原生AB包上传下载

3.上传下载一些语音内容

只要是上传下载相关的功能 都可以使用Ftp来完成

第7章: 网络通信—超文本传输HTTP

第1节: HTTP的工作原理

HTTP工作原理

HTTP是什么

HTTP(HyperText Transfer Protocol)

超文本传输协议,是因特网上应用最为广泛的一种网络传输协议。最初设计HTTP的

目的是为了提供一种发布和接收由文本文件组成的HTML页面的方法,后来发展到除

了文本数据外,还可以传输图片、音频、视频、压缩文件以及各种程序文件等。

HTTP主要用于超文本传输,因此相对FTP显得更简单一些,目前常见的HTTP标准是

HTTP/1.1.

说人话:HTTP超文本传输协议就是一个在网络中上传下载文件的一套规则

HTTP的工作原理

划重点:HTTP的本质也是TCP通信

HTTP定义了Web客户端(一般指浏览器)如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。

HTTP客户端首先与服务器建立TCP连接然后客户端通过套接字发送HTTP请求,并通过套接字接收HTTP响应

由于HTTP采用TCP传输数据,因此不会丢包、不会乱序。

HTTP的工作原理

HTTP的工作原理主要有以下三个特点

  1. HTTP是以TCP方式工作

  2. HTTP是无状态的

  3. HTTP使用元信息作为标头

HTTP的工作原理

在HTTP/1.0中,客户端和服务器建立TCP连接后,发送一个请求到服务器,服务器发送一个应答给客户端,然后立即断开TCP连接,他们的主要步骤为:

  1. 客户端与服务端建立TCP连接

  2. 客户端向服务端发出请求

  3. 若服务端接受请求,则回送响应码和所需的信息

  4. 客户端与服务端断开TCP连接

需要注意,HTTP/1.1 支持持久连接,即客户端和服务端建立连接后,可以发送请求

和接收应答,然后迅速地发送另一个请求和接收另一个应答。

持久连接也使得在得到上一个请求的应答之前能够发送多个请求,这就是HTTP/1.1与HTTP/1.0的明显不同之处,除此之外,HTTP/1.1可以发送的请求类型也比HTTP/1.0多。

目前市面上的Web服务器软件和浏览器软件基本都是支持HTTP/1.1版本的,目前使

用的基本上都是HTTP/1.1版本

HTTP是无状态的

无状态指:

客户端发送一次请求后,服务端并没有存储关于该客户端的任何状态信息

即使客户端再次请求同一个对象,服务端仍会重新发送这个对象,不会在意之前是否已经向客户端发送过这个对象

说人话:HTTP通信就是客户端要什么来什么,想要多少来多少,服务端不会因为你要过了而不给你,不会记录你要过的状态

HTTP使用元信息作为标头

HTTP通过添加标头(header)的方式向服务端提供本次HTTP请求的相关信

息,即在主要数据前添加一部分额外信息,称为元信息(metainformation)

元信息里主要包含:传送的对象属于哪种类型,采用的是哪种编码等等

说人话:HTTP的元信息标头,类似我们讲解Socket通信时用于区分消息类型、

处理分包黏包时,在消息体前方加的自定义信息。

在HTTP协议中,它也定义了类似的规则,在头部包含了一些额外信息

准备:搭建HTTP服务器

知识点一 搭建HTTP服务器的几种方式

1.使用别人做好的HTTP服务器软件,一般作为资源服务器时使用该方式(学习阶段建议使用)

2.自己编写HTTP服务器应用程序,一般作为Web服务器 或者 短连接游戏服务器 时使用该方式(工作后由后端程序员来做)

一般在工作中不会由我们来完成这部分工作

知识点二 使用别人做好的HTTP服务器软件来搭建HTTP资源服务器

下载hfs等HTTP服务器软件

在想要作为HTTP资源服务器的电脑上运行之

知识点三 使用别人做好的Web服务器进行测试

我们在学习过程中,可以直接在别人做好的Web服务器上获取信息和资源

比如我们可以下载任意网站上可被下载的图片

总结

在实际商业项目开发当中

HTTP 资源服务器 可以自己写也可以用别人做好的软件

HTTP 网站服务器 或 游戏服务器 需要自己根据需求进行实现

这些工作一般都是由后端或者运维程序员来进行制作

我们主要做了解

我们之后主要着重学习前端HTTP相关的知识点

在游戏开发时,我们更多时候需要的是HTTP的资源服务器

除非你要做短连接游戏,那么后端程序可以以HTTP协议为基础来开发服务端应用程序

我们只需要学习前端用于进行HTTP通信的相关知识即可

第2节: HTTP—C#相关类

HTTP关键类

知识点一 HttpWebRequest类

命名空间:System.Net

HttpWebRequest是主要用于发送客户端请求的类

主要用于:发送HTTP客户端请求给服务器,可以进行消息通信、上传、下载等等操作

重要方法

//1.Create 创建新的WebRequest,用于进行HTTP相关操作
HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.109:8000/Http_Server/")) as HttpWebRequest;
//2.Abort  如果正在进行文件传输,用此方法可以终止传输 
req.Abort();
//3.GetRequestStream  获取用于上传的流
Stream s = req.GetRequestStream();
//4.GetResponse  返回HTTP服务器响应
HttpWebResponse res = req.GetResponse() as HttpWebResponse;
//5.Begin/EndGetRequestStream 异步获取用于上传的流
//6.Begin/EndGetResponse 异步获取返回的HTTP服务器响应
重要成员
//1.Credentials 通信凭证,设置为NetworkCredential对象
req.Credentials = new NetworkCredential("", "");
//2.PreAuthenticate 是否随请求发送一个身份验证标头,一般需要进行身份验证时需要将其设置为true
req.PreAuthenticate = true;

//3.Headers 构成标头的名称/值对的集合
//req.Headers
//4.ContentLength 发送信息的字节数 上传信息时需要先设置该内容长度
req.ContentLength = 100;
//5.ContentType 在进行POST请求时,需要对发送的内容进行内容类型的设置
//6.Method  操作命令设置
//  WebRequestMethods.Http类中的操作命令属性
//  Get     获取请求,一般用于获取数据
//  Post    提交请求,一般用于上传数据,同时可以获取
//  Head    获取和Get一致的内容,只是只会返回消息头,不会返回具体内容
//  Put     向指定位置上传最新内容
//  Connect 表示与代理一起使用的 HTTP CONNECT 协议方法,该代理可以动态切换到隧道
//  MkCol   请求在请求 URI(统一资源标识符)指定的位置新建集合

了解该类的更多信息

https://docs.microsoft.com/zh-cn/dotnet/api/system.net.httpwebrequest?view=net-6.0

知识点二 HttpWebResponse类

命名空间:System.Net

它主要用于获取服务器反馈信息的类

我们可以通过HttpWebRequest对象中的GetResponse()方法获取

当使用完毕时,要使用Close释放

重要方法
  1. Close:释放所有资源

  2. GetResponseStream:返回从FTP服务器下载数据的流

重要成员
  1. ContentLength:接受到数据的长度

  2. ContentType:接受数据的类型

  3. StatusCode:HTTP服务器下发的最新状态码

  4. StatusDescription:HTTP服务器下发的状态代码的文本

  5. BannerMessage:登录前建立连接时HTTP服务器发送的消息

  6. ExitMessage:HTTP会话结束时服务器发送的消息

  7. LastModified:HTTP服务器上的文件的上次修改日期和时间

了解该类的更多信息

https://docs.microsoft.com/zh-cn/dotnet/api/system.net.httpwebresponse?view=net-6.0

知识点三 NetworkCredential、Uri、Stream、FileStream类

这些类我们在学习Ftp时已经使用过了

在HTTP通讯时使用方式不变

总结

Http相关通讯类的使用和Ftp非常类似

只有一些细节上的区别

之后我们在学习上传下载时再来着重讲解

HTTP下载数据

知识点一 检测资源可用性

try
{
    //利用Head请求类型,获取信息
    //1.创建HTTP通讯用连接对象HttpWebRequest对象
    HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.49:8000/Http_Server/实战就业路线.jpg")) as HttpWebRequest;
    //2.设置请求类型 或 其它相关参数
    req.Method = WebRequestMethods.Http.Head;
    req.Timeout = 2000;
    //3.发送请求,获取响应结果HttpWebResponse对象
    HttpWebResponse res = req.GetResponse() as HttpWebResponse;

    if (res.StatusCode == HttpStatusCode.OK)
    {
        print("文件存在且可用");
        print(res.ContentLength);
        print(res.ContentType);

        res.Close();
    }
    else
        print("文件不能用" + res.StatusCode);
}
catch (WebException w)
{
    print("获取出错" + w.Message + w.Status);
}

知识点二 下载资源

//利用Get请求类型,下载资源
try
{
    //1.创建HTTP通讯用连接对象HttpWebRequest对象
    HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.49:8000/Http_Server/实战就业路线.jpg")) as HttpWebRequest;
    //2.设置请求类型 或 其它相关参数
    req.Method = WebRequestMethods.Http.Get;
    req.Timeout = 3000;
    //3.发送请求,获取响应结果HttpWebResponse对象
    HttpWebResponse res = req.GetResponse() as HttpWebResponse;
    //4.获取响应数据流,写入本地路径
    if (res.StatusCode == HttpStatusCode.OK)
    {
        print(Application.persistentDataPath);
        using (FileStream fileStream = File.Create(Application.persistentDataPath + "/httpDownLoad.jpg"))
        {
            Stream downLoadStream = res.GetResponseStream();
            byte[] bytes = new byte[2048];
            //读取数据
            int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
            //一点一点的写入本地
            while (contentLength != 0)
            {
                fileStream.Write(bytes, 0, contentLength);
                contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
            }
            fileStream.Close();
            downLoadStream.Close();
            res.Close();
        }
        print("下载成功");
    }
    else
        print("下载失败" + res.StatusCode);
}
catch (WebException w)
{
    print("下载出错" + w.Status + w.Message);
}

知识点三 Get请求类型携带额外信息

我们在进行HTTP通信时,可以在地址后面加一些额外参数传递给服务端

一般在和短连接游戏服务器通讯时,需要携带额外信息

举例:

http://www.aspxfans.com:8080/news/child/index.asp?boardID=5&ID=24618&page=1

这个链接可以分成几部分

1.协议部分:取决于服务器端使用的哪种协议

http:// —— 普通的http超文本传输协议

https:// —— 加密的超文本传输协议

2.域名部分:

www.aspxfans.com

也可以填写服务器的公网IP地址

3.端口部分:

8080

可以不写,如果不写默认为80

4.虚拟目录部分:

news/child/

域名后的/开始,到最后一个/之前的部分

5.文件名部分:

index.asp

?之前的最后一个/后的部分

6.参数部分:

boardID=5&ID=24618&page=1

?之后的部分就是参数部分,多个参数一&分隔开

这里有三个参数

boardID = 5

ID = 24618

page = 1

我们在和服务端进行通信时,只要按照这种规则格式进行通信,就可以传递参数给对象

主要可用于:

1.web网站服务器

2.游戏短连接服务器

总结

1.Head请求类型

主要用于获取文件的一些基础信息 可以用于确定文件是否存在

2.Get请求类型 主要用于传递信息给服务器,用于获取具体信息

服务器返回的信息,可以通过Response中的流来获取

用Get请求时,可以在连接中携带一些额外参数(在链接后面加上 ?参数名=参数值&参数名=参数值&参数名=参数值&。。。。)

正常的http服务器应用程序,都会去解析Get请求时连接中的参数进行逻辑处理(后端程序的工作)

我们主要要掌握的知识点:

1.额外参数按格式书写

2.通过response对象中的流来获取返回的数据(数据的类型多种多样,可以是文件、自定义消息等等,我们按照规则解析即可)

3.在和http服务器通信时,我们经常会使用额外参数的形式传递信息,特别是以后和一些运营平台对接时

4.文件下载功能和Ftp非常类似,只是其中使用的类、协议、请求类型不同而已

HTTP的Post请求概述

知识点一 Get和Post的区别

我们上节课学习的下载数据,主要使用的就是Get请求类型

我们在上传数据时将会使用Post请求类型

那么这两个请求类型他们的主要区别是什么呢?

1.主要用途

Get — 一般从指定的资源请求数据,主要用于获取数据

Post — 一般向指定的资源提交想要被处理的数据,主要用于上传数据

2.相同点

Get和Post都可以传递一些额外的参数数据给服务端

3.不同点

3-1:在传递参数时,Post相对Get更加的安全,因为Post看不到参数

Get传递的参数都包含在连接中(URL资源定位地址),是暴露式的 ?参数名=参数值&参数名=参数值

Post传递的参数放在请求数据中,不会出现在URL中,是隐藏式的

3-2:Get在传递数据时有大小的限制,因为它主要是在连接中拼接参数,而URL的长度是有限制的(最大长度一般为2048个字符)

Post在传递数据时没有限制

3-3:在浏览器中Get请求能被缓存,Post不能缓存

3-4:传输次数可能不同

Get: 建立连接——>请求行、请求头、请求数据一次传输——>获取响应——>断开连接

Post: 建立连接——>传输可能分两次——>请求行,请求头第一次传输——>请求数据第二次传输——>获取响应——>断开

对于前端来说,其实Get和Post都是能够获取和传递数据的,后端只要处理对应逻辑返回响应信息即可

但是由于他们的这些特点

我们在实际使用时建议Get用于获取,Post用于上传

如果想要传递一些不想暴露在外部的参数信息,建议使用Post,它更加的安全

知识点二 Post如何携带额外参数

//关键点:将Content-Type设置为 application/x-www-form-urlencoded 键值对类型
HttpWebRequest req = HttpWebRequest.Create("http://192.168.50.109:8000/Http_Server/") as HttpWebRequest;
req.Method = WebRequestMethods.Http.Post;
req.Timeout = 2000;
//设置上传的内容的类型
req.ContentType = "application/x-www-form-urlencoded";

//我们要上传的数据
string str = "Name=MrTang&ID=2";
byte[] bytes = Encoding.UTF8.GetBytes(str);
//我们在上传之前一定要设置内容的长度
req.ContentLength = bytes.Length;
//上传数据
Stream stream = req.GetRequestStream();
stream.Write(bytes, 0, bytes.Length);
stream.Close();
//发送数据 得到响应结果
HttpWebResponse res = req.GetResponse() as HttpWebResponse;
print(res.StatusCode);

知识点三 ContentType的常用类型

ContentType的构成:

内容类型;charset=编码格式;boundary=边界字符串

text/html;charset=utf-8;boundary=自定义字符串

其中内容类型有:

文本类型text:

text/plain 没有特定子类型就是它(重要)

text/html

text/css

text/javascript

图片类型image:

image/gif

image/png

image/jpeg

image/bm

image/webp

image/x-icon

image/vnd.microsoft.icon

音频类型audio:

audio/midi

audio/mpeg

audio/webm

audio/ogg

audio/wav

视频类型video:

video/webm

video/ogg

二进制类型application:

application/octet-stream 没有特定子类型就是它(重要)

application/x-www-form-urlencoded 传递参数时使用键值对形式(重要)

application/pkcs12

application/xhtml+xml

application/xml

application/pdf

application/vnd.mspowerpoint

复合内容multipart:

multipart/form-data 复合内容,有多种内容组合(重要)

multipart/byteranges 特殊的复合文件

关于ContentType更多内容可以前往

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type

关于媒体类型可以前往

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types

知识点四 ContentType中对于我们来说重要的类型

1.通用2进制类型

application/octet-stream

2.通用文本类型

text/plain

3.键值对参数

application/x-www-form-urlencoded

4.复合类型(传递的信息有多种类型组成,比如有键值对参数,有文件信息等等,上传资源服务器时需要用该类型)

multipart/form-data

总结

这节课的重点知识点是

1.Get和Post的区别

2.ContentType的重要类型

注意:

HTTP通讯中

客户端发送给服务端的Get和Post请求都需要服务端和客户端约定一些规则进行处理

比如传递的参数的含义,数据如何处理等等,都是需要前后端程序制定对应规则来进行处理的

只是我们目前没有后端开发的HTTP服务器,所以我们传递过去的参数和数据没有得到对应处理

我们目前只针对HTTP资源服务器上传下载数据进行学习

他们的通讯原理是一致的,都是通过HTTP通讯交换数据

HTTP上传文件

知识点一 上传文件到HTTP资源服务器需要遵守的规则

上传文件时内容的必备规则

1:ContentType = "multipart/form-data; boundary=边界字符串";

2:上传的数据必须按照格式写入流中

--边界字符串

Content-Disposition: form-data; name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"

Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)

空一行

(这里直接写入传入的内容)

--边界字符串--

3:保证服务器允许上传

4:写入流前需要先设置ContentLength内容长度

知识点二 上传文件

//1.创建HttpWebRequest对象
HttpWebRequest req = HttpWebRequest.Create("http://192.168.50.109:8000/Http_Server/") as HttpWebRequest;
//2.相关设置(请求类型,内容类型,超时,身份验证等)
req.Method = WebRequestMethods.Http.Post;
req.ContentType = "multipart/form-data;boundary=MrTang";
req.Timeout = 500000;
req.Credentials = new NetworkCredential("MrTang3", "123123");
req.PreAuthenticate = true;//先验证身份 再上传数据

//3.按格式拼接字符串并且转为字节数组之后用于上传
//3-1.文件数据前的头部信息
//  --边界字符串
//  Content-Disposition: form-data; name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"
//  Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
//  空一行
string head = "--MrTang\r\n" +
    "Content-Disposition:form-data;name=\"file\";filename=\"http上传的文件.jpg\"\r\n" +
    "Content-Type:application/octet-stream\r\n\r\n";
//头部拼接字符串规则信息的字节数组
byte[] headBytes = Encoding.UTF8.GetBytes(head);

//3-2.结束的边界信息
//  --边界字符串--
byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--MrTang--\r\n");

//4.写入上传流
using (FileStream localFileStream = File.OpenRead(Application.streamingAssetsPath + "/test.png"))
{
    //4-1.设置上传长度
    //总长度 是前部分字符串 + 文件本身有多大 + 后部分边界字符串
    req.ContentLength = headBytes.Length + localFileStream.Length + endBytes.Length;
    //用于上传的流
    Stream upLoadStream = req.GetRequestStream();
    //4-2.先写入前部分头部信息
    upLoadStream.Write(headBytes, 0, headBytes.Length);
    //4-3.再写入文件数据
    byte[] bytes = new byte[2048];
    int contentLength = localFileStream.Read(bytes, 0, bytes.Length);
    while (contentLength != 0)
    {
        upLoadStream.Write(bytes, 0, contentLength);
        contentLength = localFileStream.Read(bytes, 0, bytes.Length);
    }
    //4-4.在写入结束的边界信息
    upLoadStream.Write(endBytes, 0, endBytes.Length);

    upLoadStream.Close();
    localFileStream.Close();
}

//5.上传数据,获取响应
HttpWebResponse res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode == HttpStatusCode.OK)
    print("上传通信成功");
else
    print("上传失败" + res.StatusCode);

总结

HTTP上传文件相对比较麻烦

需要按照指定的规则进行内容拼接达到上传文件的目的

其中相对重要的知识点是

上传文件时的规则

--边界字符串

Content-Disposition: form-data; name="file";filename="传到服务器上使用的文件名"

Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)

空行

(这里直接写入传入的内容)

--边界字符串--

关于其更多的规则,可以前往官网查看详细说明

关于ContentType更多内容可以前往

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type

关于媒体类型可以前往

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types

关于Content-Disposition更多内容可以前往

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition

HTTP管理类封装

实现

public class HttpMgr
{
    private static HttpMgr instance = new HttpMgr();

    public static HttpMgr Instance => instance;


    private string HTTP_PATH = "http://192.168.50.109:8000/Http_Server/";

    private string USER_NAME = "MrTang3";
    private string PASS_WORD = "123123";

    /// <summary>
    /// 下载指定文件到本地指定路径中
    /// </summary>
    /// <param name="fileName">远程文件名</param>
    /// <param name="loacFilePath">本地路径</param>
    /// <param name="action">下载结束后的回调函数</param>
    public async void DownLoadFile(string fileName, string loacFilePath, UnityAction<HttpStatusCode> action)
    {
        HttpStatusCode result = HttpStatusCode.OK;
        await Task.Run(() =>
        {
            try
            {
                //判断文件是否存在 Head 
                //1.创建HTTP连接对象
                HttpWebRequest req = HttpWebRequest.Create(HTTP_PATH + fileName) as HttpWebRequest;
                //2.设置请求类型 和 其它相关参数
                req.Method = WebRequestMethods.Http.Head;
                req.Timeout = 2000;
                //3.发送请求
                HttpWebResponse res = req.GetResponse() as HttpWebResponse;

                //存在才下载
                if(res.StatusCode == HttpStatusCode.OK)
                {
                    res.Close();
                    //下载
                    //1.创建HTTP连接对象
                    req = HttpWebRequest.Create(HTTP_PATH + fileName) as HttpWebRequest;
                    //2.设置请求类型 和 其它相关参数
                    req.Method = WebRequestMethods.Http.Get;
                    req.Timeout = 2000;
                    //3.发送请求
                    res = req.GetResponse() as HttpWebResponse;
                    //4.存储数据到本地
                    if(res.StatusCode == HttpStatusCode.OK)
                    {
                        //存储数据
                        using (FileStream fileStream = File.Create(loacFilePath))
                        {
                            Stream stream = res.GetResponseStream();
                            byte[] bytes = new byte[4096];
                            int contentLength = stream.Read(bytes, 0, bytes.Length);

                            while (contentLength != 0)
                            {
                                fileStream.Write(bytes, 0, contentLength);
                                contentLength = stream.Read(bytes, 0, bytes.Length);
                            }

                            fileStream.Close();
                            stream.Close();
                        }
                        result = HttpStatusCode.OK;
                    }
                    else
                    {
                        result = res.StatusCode;
                    }
                }
                else
                {
                    result = res.StatusCode;
                }

                res.Close();
            }
            catch (WebException w)
            {
                result = HttpStatusCode.InternalServerError;
                Debug.Log("下载出错" + w.Message + w.Status);
            }
        });

        action?.Invoke(result);
    }


    /// <summary>
    /// 上传文件
    /// </summary>
    /// <param name="fileName">传到远端服务器上的文件名</param>
    /// <param name="loacalFilePath">本地的文件路径</param>
    /// <param name="action">上传结束后的回调函数</param>
    public async void UpLoadFile(string fileName, string loacalFilePath, UnityAction<HttpStatusCode> action)
    {
        HttpStatusCode result = HttpStatusCode.BadRequest;
        await Task.Run(() =>
        {
            try
            {
                HttpWebRequest req = HttpWebRequest.Create(HTTP_PATH) as HttpWebRequest;
                req.Method = WebRequestMethods.Http.Post;
                req.ContentType = "multipart/form-data;boundary=MrTang";
                req.Timeout = 500000;
                req.Credentials = new NetworkCredential(USER_NAME, PASS_WORD);
                req.PreAuthenticate = true;

                //拼接字符串 头部
                string head = "--MrTang\r\n" +
                "Content-Disposition:form-data;name=\"file\";filename=\"{0}\"\r\n" +
                "Content-Type:application/octet-stream\r\n\r\n";
                //替换文件名
                head = string.Format(head, fileName);
                byte[] headBytes = Encoding.UTF8.GetBytes(head);

                //尾部的边界字符串
                byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--MrTang--\r\n");

                using (FileStream localStream = File.OpenRead(loacalFilePath))
                {
                    //设置长度
                    req.ContentLength = headBytes.Length + localStream.Length + endBytes.Length;
                    //写入流
                    Stream upLoadStream = req.GetRequestStream();
                    //写入头部
                    upLoadStream.Write(headBytes, 0, headBytes.Length);
                    //写入上传文件
                    byte[] bytes = new byte[4096];
                    int contentLenght = localStream.Read(bytes, 0, bytes.Length);
                    while (contentLenght != 0)
                    {
                        upLoadStream.Write(bytes, 0, contentLenght);
                        contentLenght = localStream.Read(bytes, 0, bytes.Length);
                    }
                    //写入尾部
                    upLoadStream.Write(endBytes, 0, endBytes.Length);

                    upLoadStream.Close();
                    loacalFilePath.Clone();
                }

                HttpWebResponse res = req.GetResponse() as HttpWebResponse;
                //让外部去处理结果 
                result = res.StatusCode;
                res.Close();
            }
            catch (WebException w)
            {
                Debug.Log("上传出错" + w.Status + w.Message);
            }
        });
        action?.Invoke(result);
    }
}

第3节: Unity封装类——WWW类

WWW类

知识点一 WWW类的作用

WWW是Unity提供给我们简单的访问网页的类

对我们前面几节提到的,在C#原先提供的一些网络协议API的基础上进行了更进一步的封装,

我们可以通过该类更为方便的下载和上传一些数据

在使用http协议时,默认的请求类型是Get,如果想要Post上传,需要配合下节课学习的WWWFrom类使用

它主要支持的协议

  1. http://和https:// 超文本传输协议

  2. ftp:// 文件传输协议(但仅限于匿名下载)

  3. file:// 本地文件传输协议,可以使用该协议异步加载本地文件(PC、IOS、Android都支持)

我们本节课主要学习利用WWW来进行数据的下载或加载

注意:

1.该类一般配合协同程序使用

2.该类在较新Unity版本中会提示过时,但是仍可以使用,新版本将其功能整合进了UnityWebRequest类(之后讲解)

知识点二 WWW类的常用方法和变量

常用方法
//1.WWW:构造函数,用于创建一个WWW请求
WWW www = new WWW("http://192.168.50.109:8000/Http_Server/实战就业路线.jpg");
//2.GetAudioClip:从下载数据返回一个音效切片AudioClip对象
//www.GetAudioClip()
//3.LoadImageIntoTexture:用下载数据中的图像来替换现有的一个Texture2D对象
//Texture2D tex = new Texture2D(100, 100);
//www.LoadImageIntoTexture(tex);
//4.LoadFromCacheOrDownload:从缓存加载AB包对象,如果该包不在缓存则自动下载存储到缓存中,以便以后直接从本地缓存中加载
//WWW.LoadFromCacheOrDownload("http://192.168.50.109:8000/Http_Server/test.assetbundle", 1);
常用变量
//1.assetBundle:如果加载的数据是AB包,可以通过该变量直接获取加载结果
//www.assetBundle
//2.audioClip:如果加载的数据是音效切片文件,可以通过该变量直接获取加载结果
//www.GetAudioClip
//3.bytes:以字节数组的形式获取加载到的内容
//www.bytes
//4.bytesDownloaded:过去已下载的字节数
//www.bytesDownloaded
//5.error:返回一个错误消息,如果下载期间出现错误,可以通过它获取错误信息
//www.error != null
//6.isDone:判断下载是否已经完成
//www.isDone
//7.movie:如果下载的视频,可以获取一个MovieTexture类型结果
//www.GetMovieTexture()
//8.progress:下载进度
//www.progress
//9.text:如果下载的数据是字符串,以字符串的形式返回内容
//www.text
//10.texture:如果下载的数据是图片,以Texture2D的形式返回加载结果
//www.texture

知识点三 利用WWW类来异步下载或加载文件

1.下载HTTP服务器上的内容

StartCoroutine(DownLoadHttp());
IEnumerator DownLoadHttp()
{
    //1.创建WWW对象
    WWW www = new WWW("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi2.hdslb.com%2Fbfs%2Farchive%2F8cc2b9a7868b266800f98d42fc5d257021e75103.jpg&refer=http%3A%2F%2Fi2.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654745686&t=b7691b6e546367610e4331039d1a10ec");

    //2.就是等待加载结束
    while (!www.isDone)
    {
        print(www.bytesDownloaded);
        print(www.progress);
        yield return null;
    }

    print(www.bytesDownloaded);
    print(www.progress);

    //3.使用加载结束后的资源
    if (www.error == null)
    {
        image.texture = www.texture;
    }
    else
        print(www.error);
}

2.下载FTP服务器上的内容(FTP服务器一定要支持匿名账户

StartCoroutine(DownLoadFtp());
IEnumerator DownLoadFtp()
{
    //1.创建WWW对象
    WWW www = new WWW("ftp://127.0.0.1/实战就业路线.jpg");

    //2.就是等待加载结束
    while (!www.isDone)
    {
        print(www.bytesDownloaded);
        print(www.progress);
        yield return null;
    }

    print(www.bytesDownloaded);
    print(www.progress);

    //3.使用加载结束后的资源
    if (www.error == null)
    {
        image.texture = www.texture;
    }
    else
        print(www.error);
}

3.本地内容加载(一般移动平台加载数据都会使用该方式)

StartCoroutine(DownLoadLocal());
IEnumerator DownLoadLocal()
{
    //1.创建WWW对象
    WWW www = new WWW("file://" + Application.streamingAssetsPath + "/test.png");

    //2.就是等待加载结束
    while (!www.isDone)
    {
        print(www.bytesDownloaded);
        print(www.progress);
        yield return null;
    }

    print(www.bytesDownloaded);
    print(www.progress);

    //3.使用加载结束后的资源
    if (www.error == null)
    {
        image.texture = www.texture;
    }
    else
        print(www.error);
}

总结

Unity中的WWW类比使用C#中的Http相关类更加的方便

建议大家使用Unity当中为我们封装好的类来处理下载、加载相关逻辑

WWWFrom类和上传

知识点一 WWWFrom类的作用

上节课学习了使用WWW类来下载数据

如果想要使用WWW上传数据时,就需要配合WWWFrom类进行使用了

而WWWFrom主要就是用于集成数据的,我们可以设置上传的参数或者2进制数据

当结合WWWFrom上传数据时

它主要用到的请求类型是Post

它使用Http协议进行上传处理

注意:

使用WWW结合WWWFrom上传数据一般需要配合后端程序制定上传规则

知识点二 WWWFrom类的常用方法和变量

该类当中我们主要就使用方法,相关变量很少使用,我们主要就着重讲解方法

//1.WWWForm:构造函数
WWWForm data = new WWWForm();
//2.AddBinaryData:添加二进制数据
data.AddBinaryData()
//3.AddField:添加字段
data.AddField()

知识点三 WWW结合WWWFrom对象来异步上传数据

StartCoroutine(UpLoadData());

IEnumerator UpLoadData()
{
    WWWForm data = new WWWForm();
    //上传的数据 对应的后端程序 必须要有处理的规则 才能生效
    data.AddField("Name", "MrTang", Encoding.UTF8);
    data.AddField("Age", 99);
    data.AddBinaryData("file", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png"), "testtest.png", "application/octet-stream");

    WWW www = new WWW("http://192.168.50.109:8000/Http_Server/", data);

    yield return www;

    if (www.error == null)
    {
        print("上传成功");
        //www.bytes
    }
    else
        print("上传失败" + www.error);
}

总结

WWW结合WWWFrom上传数据

需要配合后端服务器来指定上传规则

也就是说我们上传的数据,后端需要知道收到数据后应该如何处理

通过这种方式我们没办法像C#类当中完成文件的上传

但是该方式非常适合用于制作短连接游戏的前端网络层

我们可以对WWW进行二次封装,专门用于上传自定义消息给对应的Web服务器

WWW管理类封装

实现

public class NetWWWMgr : MonoBehaviour
{
    private static NetWWWMgr instance;

    public static NetWWWMgr Instance => instance;

    private string HTTP_SERVER_PATH = "http://192.168.50.109:8000/Http_Server/";

    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    /// <summary>
    /// 提供给外部加载资源用的方法
    /// </summary>
    /// <typeparam name="T">资源的类型</typeparam>
    /// <param name="path">资源的路径 http ftp file都支持</param>
    /// <param name="action">加载结束后的回调函数 因为WWW是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>
    public void LoadRes<T>(string path, UnityAction<T> action) where T : class
    {
        StartCoroutine(LoadResAsync<T>(path, action));
    }

    private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class
    {
        //声明www对象 用于下载或加载
        WWW www = new WWW(path);
        //等待下载或者加载结束(异步)
        yield return www;
        //如果没有错误 证明加载成功
        if (www.error == null)
        {
            //根据T泛型的类型  决定使用哪种类型的资源 传递给外部
            if(typeof(T) == typeof(AssetBundle))
            {
                action?.Invoke(www.assetBundle as T);
            }
            else if (typeof(T) == typeof(Texture))
            {
                action?.Invoke(www.texture as T);
            }
            else if (typeof(T) == typeof(AudioClip))
            {
                action?.Invoke(www.GetAudioClip() as T);
            }
            else if (typeof(T) == typeof(string))
            {
                action?.Invoke(www.text as T);
            }
            else if (typeof(T) == typeof(byte[]))
            {
                action?.Invoke(www.bytes as T);
            }
            //自定义一些类型 可能需要将bytes 转换成对应的类型来使用
        }
        //如果错误 就提示别人
        else
        {
            Debug.LogError("www加载资源出错" + www.error);
        }
    }

    /// <summary>
    /// 通过UnityWebRequest去获取数据
    /// </summary>
    /// <typeparam name="T">byte[]、Texture、AssetBundle、AudioClip、object(自定义的 如果是object证明要保存到本地)</typeparam>
    /// <param name="path">远端或者本地数据路径 http ftp file</param>
    /// <param name="action">获取成功后的回调函数</param>
    /// <param name="localPath">如果是下载到本地 需要传第3个参数</param>
    /// <param name="type">如果是下载 音效切片文件 需要穿音效类型</param>
    public void UnityWebRequestLoad<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T : class
    {
        StartCoroutine(UnityWebRequestLoadAsync<T>(path, action, localPath, type));
    }

    private IEnumerator UnityWebRequestLoadAsync<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T:class
    {
        UnityWebRequest req = new UnityWebRequest(path, UnityWebRequest.kHttpVerbGET);

        if (typeof(T) == typeof(byte[]))
            req.downloadHandler = new DownloadHandlerBuffer();
        else if (typeof(T) == typeof(Texture))
            req.downloadHandler = new DownloadHandlerTexture();
        else if (typeof(T) == typeof(AssetBundle))
            req.downloadHandler = new DownloadHandlerAssetBundle(req.url, 0);
        else if (typeof(T) == typeof(object))
            req.downloadHandler = new DownloadHandlerFile(localPath);
        else if (typeof(T) == typeof(AudioClip))
            req = UnityWebRequestMultimedia.GetAudioClip(path, type);
        else//如果出现没有的类型  就不用继续往下执行了
        {
            Debug.LogWarning("未知类型" + typeof(T));
            yield break;
        }

        yield return req.SendWebRequest();

        if(req.result == UnityWebRequest.Result.Success)
        {
            if (typeof(T) == typeof(byte[]))
                action?.Invoke(req.downloadHandler.data as T);
            else if (typeof(T) == typeof(Texture))
                //action?.Invoke((req.downloadHandler as DownloadHandlerTexture).texture as T);
                action?.Invoke(DownloadHandlerTexture.GetContent(req) as T);
            else if (typeof(T) == typeof(AssetBundle))
                action?.Invoke((req.downloadHandler as DownloadHandlerAssetBundle).assetBundle as T);
            else if (typeof(T) == typeof(object))
                action?.Invoke(null);
            else if (typeof(T) == typeof(AudioClip))
                action?.Invoke(DownloadHandlerAudioClip.GetContent(req) as T);
        }
        else
        {
            Debug.LogWarning("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }


    /// <summary>
    /// 发送继承BaseMsg的消息给web服务器
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="msg"></param>
    /// <param name="action"></param>
    public void SendMsg<T>(BaseMsg msg, UnityAction<T> action) where T:BaseMsg
    {
        StartCoroutine(SendMsgAsync<T>(msg, action));
    }

    private IEnumerator SendMsgAsync<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg
    {
        //消息发送
        WWWForm data = new WWWForm();
        //准备要发送的消息数据
        data.AddBinaryData("Msg", msg.Writing());

        WWW www = new WWW(HTTP_SERVER_PATH, data);
        //我们也可以直接传递 2进制字节数组 只要和后端定好规则 怎么传都是可以的
        //WWW www = new WWW("HTTP_SERVER_PATH", msg.Writing());

        //异步等待 发送结束 才会继续执行后面的代码
        yield return www;

        //发送完毕过后 收到响应 
        //认为 后端发回来的内容 也是一个继承自BaseMsg类的一个字节数组对象
        if (www.error == null)
        {
            //先解析 ID和消息长度
            int index = 0;
            int msgID = BitConverter.ToInt32(www.bytes, index);
            index += 4;
            int msgLength = BitConverter.ToInt32(www.bytes, index);
            index += 4;
            //反序列化 BaseMsg
            BaseMsg baseMsg = null;
            switch (msgID)
            {
                case 1001:
                    baseMsg = new PlayerMsg();
                    baseMsg.Reading(www.bytes, index);
                    break;
            }
            if (baseMsg != null)
                action?.Invoke(baseMsg as T);
        }
        else
            Debug.LogError("发消息出问题" + www.error);
    }

    /// <summary>
    /// 上传文件的方法
    /// </summary>
    /// <param name="fileName">上传上去的文件名</param>
    /// <param name="localPath">本地想要上传文件的路径</param>
    /// <param name="action">上传完成后的回调函数</param>
    public void UploadFile(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action)
    {
        StartCoroutine(UploadFileAsync(fileName, localPath, action));
    }

    private IEnumerator UploadFileAsync(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action)
    {
        //添加要上传文件的数据
        List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();
        dataList.Add(new MultipartFormFileSection(fileName, File.ReadAllBytes(localPath)));

        UnityWebRequest req = UnityWebRequest.Post(HTTP_SERVER_PATH, dataList);

        yield return req.SendWebRequest();

        action?.Invoke(req.result);
        //如果不成功
        if (req.result != UnityWebRequest.Result.Success)
        {
            Debug.LogWarning("上传出现问题" + req.error + req.responseCode);
        }
    }

}

第4节: Unity封装类——UnityWebRequest类

UnityWebRequest获取数据

知识点一 UnityWebRequest是什么?

UnityWebRequest是一个Unity提供的一个模块化的系统类

用于构成HTTP请求和处理HTTP响应

它主要目标是让Unity游戏和Web服务端进行交互

它将之前WWW的相关功能都集成在了其中

所以新版本中都建议使用UnityWebRequest类来代替WWW类

它在使用上和WWW很类似

主要的区别就是UnityWebRequest把下载下来的数据处理单独提取出来了

我们可以根据自己的需求选择对应的数据处理对象来获取数据

注意:

  1. UnityWebRequest和WWW一样,需要配合协同程序使用

  2. UnityWebRequest和WWW一样,支持http、ftp、file协议下载或加载资源

  3. UnityWebRequest能够上传文件到HTTP资源服务器

知识点二 UnityWebRequest类的常用操作

  1. 使用Get请求获取文本或二进制数据

  2. 使用Get请求获取纹理数据

  3. 使用Get请求获取AB包数据

  4. 使用Post请求发送数据

  5. 使用Put请求上传数据

知识点三 Get获取操作

获取文本或2进制
StartCoroutine(LoadText());

IEnumerator LoadText()
{
    UnityWebRequest req = UnityWebRequest.Get("http://192.168.50.109:8000/Http_Server/test.txt");
    //就会等待 服务器端响应后 断开连接后 再继续执行后面的内容
    yield return req.SendWebRequest();

    //如果处理成功 结果就是成功枚举
    if(req.result == UnityWebRequest.Result.Success)
    {
        //文本 字符串
        print(req.downloadHandler.text);
        //字节数组
        byte[] bytes = req.downloadHandler.data;
        print("字节数组长度" + bytes.Length);
    }
    else
    {
        print("获取失败:" + req.result + req.error + req.responseCode);
    }
}

获取纹理
StartCoroutine(LoadTexture());

IEnumerator LoadTexture()
{
    //UnityWebRequest req = UnityWebRequestTexture.GetTexture("http://192.168.50.109:8000/Http_Server/实战就业路线.jpg");

    //UnityWebRequest req = UnityWebRequestTexture.GetTexture("ftp://127.0.0.1/实战就业路线.jpg");

    UnityWebRequest req = UnityWebRequestTexture.GetTexture("file://" + Application.streamingAssetsPath + "/test.png");

    yield return req.SendWebRequest();

    if (req.result == UnityWebRequest.Result.Success)
    {
        //(req.downloadHandler as DownloadHandlerTexture).texture
        //DownloadHandlerTexture.GetContent(req)
        //image.texture = (req.downloadHandler as DownloadHandlerTexture).texture;
        image.texture = DownloadHandlerTexture.GetContent(req);
    }
    else
        print("获取失败" + req.error + req.result + req.responseCode);
}
获取AB包
StartCoroutine(LoadAB());

IEnumerator LoadAB()
{
    UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("http://192.168.50.109:8000/Http_Server/lua");

    req.SendWebRequest();

    while (!req.isDone)
    {
        print(req.downloadProgress);
        print(req.downloadedBytes);
        yield return null;
    }
    //yield return req.SendWebRequest();

    print(req.downloadProgress);
    print(req.downloadedBytes);

    if (req.result == UnityWebRequest.Result.Success)
    {
        //AssetBundle ab = (req.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
        AssetBundle ab = DownloadHandlerAssetBundle.GetContent(req);
        print(ab.name);
    }
    else
        print("获取失败" + req.error + req.result + req.responseCode);
}

总结

UnityWebRequest使用上和WWW类很类似

我们需要注意的是

1.获取文本或二进制数据时

使用UnityWebRequest.Get

2.获取纹理图片数据时

使用UnityWebRequestTexture.GetTexture

以及DownloadHandlerTexture.GetContent

3.获取AB包数据时

使用UnityWebRequestAssetBundle.GetAssetBundle

以及DownloadHandlerAssetBundle.GetContent

UnityWebRequest上传数据

知识点一 上传相关数据类

//父接口
//IMultipartFormSection
//数据相关类都继承该接口
//我们可以用父类装子类
List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();

//子类数据
//MultipartFormDataSection
//1.二进制字节数组
dataList.Add(new MultipartFormDataSection(Encoding.UTF8.GetBytes("123123123123123")));
//2.字符串
dataList.Add(new MultipartFormDataSection("12312312312312312dsfasdf"));
//3.参数名,参数值(字节数组,字符串),编码类型,资源类型(常用)
dataList.Add(new MultipartFormDataSection("Name", "MrTang", Encoding.UTF8, "application/...."));
dataList.Add(new MultipartFormDataSection("Msg", new byte[1024], "appl....."));

//MultipartFormFileSection
//1.字节数组
dataList.Add(new MultipartFormFileSection(File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));

//2.文件名,字节数组(常用)
dataList.Add(new MultipartFormFileSection("上传的文件.png", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));
//3.字符串数据,文件名(常用)
dataList.Add(new MultipartFormFileSection("12312313212312", "test.txt"));
//4.字符串数据,编码格式,文件名(常用)
dataList.Add(new MultipartFormFileSection("12312313212312", Encoding.UTF8, "test.txt"));

//5.表单名,字节数组,文件名,文件类型
dataList.Add(new MultipartFormFileSection("file", new byte[1024], "test.txt", ""));
//6.表单名,字符串数据,编码格式,文件名
dataList.Add(new MultipartFormFileSection("file", "123123123", Encoding.UTF8, "test.txt"));

知识点二 Post发送相关

StartCoroutine(Upload());

IEnumerator Upload()
{
    //准备上传的数据 
    List<IMultipartFormSection> data = new List<IMultipartFormSection>();
    //键值对相关的 信息 字段数据
    data.Add(new MultipartFormDataSection("Name", "MrTang"));
    //PlayerMsg msg = new PlayerMsg();
    //data.Add(new MultipartFormDataSection("Msg", msg.Writing()));
    //添加一些文件上传文件
    //传2进制文件
    data.Add(new MultipartFormFileSection("TestTest123.png", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));
    //传文本文件
    data.Add(new MultipartFormFileSection("123123123123123", "Test123.txt"));

    UnityWebRequest req = UnityWebRequest.Post("http://192.168.50.109:8000/Http_Server/", data);

    req.SendWebRequest();

    while (!req.isDone)
    {
        print(req.uploadProgress);
        print(req.uploadedBytes);
        yield return null;
    }

    print(req.uploadProgress);
    print(req.uploadedBytes);

    if (req.result == UnityWebRequest.Result.Success)
    {
        print("上传成功");
        //req.downloadHandler.data
    }
    else
        print("上传失败" + req.error + req.responseCode + req.result);
}

知识点三 Put上传相关

注意:Put请求类型不是所有的web服务器都认,必须要服务器处理该请求类型那么才能有相应

StartCoroutine(UpLoadPut());
IEnumerator UpLoadPut()
{
    UnityWebRequest req = UnityWebRequest.Put("http://192.168.50.109:8000/Http_Server/", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png"));

    yield return req.SendWebRequest();

    if (req.result == UnityWebRequest.Result.Success)
    {
        print("Put 上传成功");
    }
    else
    {

    }
}

总结

我们可以利用Post上传数据或上传文件

Put主要用于上传文件,但是必须资源服务器支持Put请求类型

为了通用性,我们可以统一使用Post请求类型进行数据和资源的上传

它的使用和之前的WWW类似,只要前后端制定好规则就可以相互通信了

UnityWebRequest获取数据的高级操作

知识点一 高级操作指什么?

在常用操作中我们使用的是Unity为我们封装好的一些方法

我们可以方便的进行一些指定类型的数据获取

比如

下载数据时:

1.文本和2进制

2.图片

3.AB包

如果我们想要获取其它类型的数据应该如何处理呢?

上传数据时:

1.可以指定参数和值

2.可以上传文件

如果想要上传一些基于HTTP规则的其它数据应该如何处理呢?

高级操作就是用来处理 常用操作不能完成的需求的

它的核心思想就是:UnityWebRequest中可以将数据处理分离开

比如常规操作中我们用到的

DownloadHandlerTexture 和 DownloadHandlerAssetBundle两个类

就是用来将2进制字节数组转换成对应类型进行处理的

所以高级操作时指 让你按照规则来实现更多的数据获取、上传等功能

知识点二 UnityWebRequest类的更多内容

目前已学的内容

UnityWebRequest req = UnityWebRequest.Get("");

UnityWebRequest req = UnityWebRequestTexture.GetTexture("");

UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("");

UnityWebRequest req = UnityWebRequest.Put()

UnityWebRequest req = UnityWebRequest.Post

req.isDone

req.downloadProgress;

req.downloadedBytes;

req.uploadProgress;

req.uploadedBytes

req.SendWebRequest()

更多内容

1.构造函数

UnityWebRequest req = new UnityWebRequest();

2.请求地址

req.url = "服务器地址";

3.请求类型

req.method = UnityWebRequest.kHttpVerbPOST;

4.进度

req.downloadProgress

req.uploadProgress

5.超时设置

req.timeout = 2000;

6.上传、下载的字节数

req.downloadedBytes

req.uploadedBytes

7.重定向次数 设置为0表示不进行重定向 可以设置次数

req.redirectLimit = 10;

8.状态码、结果、错误内容

req.result

req.error

req.responseCode

9.下载、上传处理对象

req.downloadHandler

req.uploadHandler

更多内容

https://docs.unity.cn/cn/2020.3/ScriptReference/Networking.UnityWebRequest.html

知识点三 获取数据DownloadHandler相关类

关键类:

1.DownloadHandlerBuffer 用于简单的数据存储,得到对应的2进制数据。

2.DownloadHandlerFile 用于下载文件并将文件保存到磁盘(内存占用少)。

3.DownloadHandlerTexture 用于下载图像。

4.DownloadHandlerAssetBundle 用于提取 AssetBundle。

5.DownloadHandlerAudioClip 用于下载音频文件。

以上的这些类,其实就是Unity帮助我们实现好的,用于解析下载下来的数据的类

使用对应的类处理下载数据,他们就会在内部将下载的数据处理为对应的类型,方便我们使用

DownloadHandlerScript 是一个特殊类。就其本身而言,不会执行任何操作。

但是,此类可由用户定义的类继承。此类接收来自 UnityWebRequest 系统的回调,

然后可以使用这些回调在数据从网络到达时执行完全自定义的数据处理。

实战
DownloadHandlerBuffer,DownloadHandlerFile,DownloadHandlerTexture
StartCoroutine(DownLoadTex());

IEnumerator DownLoadTex()
{
    UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/实战就业路线.jpg", 
        UnityWebRequest.kHttpVerbGET);
    //req.method = UnityWebRequest.kHttpVerbGET;
    //1.DownloadHandlerBuffer
    //DownloadHandlerBuffer bufferHandler = new DownloadHandlerBuffer();
    //req.downloadHandler = bufferHandler;

    //2.DownloadHandlerFile
    //print(Application.persistentDataPath);
    //req.downloadHandler = new DownloadHandlerFile(Application.persistentDataPath + "/downloadFile.jpg");

    //3.DownloadHandlerTexture
    DownloadHandlerTexture textureHandler = new DownloadHandlerTexture();
    req.downloadHandler = textureHandler;

    yield return req.SendWebRequest();

    if(req.result == UnityWebRequest.Result.Success)
    {
        //获取字节数组
        //bufferHandler.data

        //textureHandler.texture
        image.texture = textureHandler.texture;
    }
    else
    {
        print("获取数据失败" + req.result + req.error + req.responseCode);
    }
}

DownloadHandlerAssetBundle
StartCoroutine(DownLoadAB());

IEnumerator DownLoadAB()
{
    UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/lua", UnityWebRequest.kHttpVerbGET);
    //第二个参数 需要已知校检码 才能进行比较 检查完整性 如果不知道的话 只能传0 不进行完整性的检查
    //所以一般 只有进行AB包热更新时 服务器发送了 对应的 文件列表中 包含了 验证码 才能进行检查
    DownloadHandlerAssetBundle handler = new DownloadHandlerAssetBundle(req.url, 0);
    req.downloadHandler = handler;

    yield return req.SendWebRequest();

    if (req.result == UnityWebRequest.Result.Success)
    {
        AssetBundle ab = handler.assetBundle;
    
        print(ab.name);
    }
    else
    {
        print("获取数据失败" + req.result + req.error + req.responseCode);
    }
}

DownloadHandlerAudioClip
StartCoroutine(DownLoadAudioClip());

IEnumerator DownLoadAudioClip()
{
    UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip("http://192.168.50.109:8000/Http_Server/音效名.mp3", 
        AudioType.MPEG);
    yield return req.SendWebRequest();

    if (req.result == UnityWebRequest.Result.Success)
    {
        AudioClip a = DownloadHandlerAudioClip.GetContent(req);
    }
    else
    {
        print("获取数据失败" + req.result + req.error + req.responseCode);
    }
}

知识点四 自定义获取数据DownloadHandler相关类

StartCoroutine(DownLoadCustomHandler());

IEnumerator DownLoadCustomHandler()
{
    UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/21.服务端.mp4", UnityWebRequest.kHttpVerbGET);

    //使用自定义的下载处理对象 来处理获取到的 2进制字节数组
    print(Application.persistentDataPath);
    req.downloadHandler = new CustomDownLoadFileHandler(Application.persistentDataPath + "/CustomHandler.mp4");

    yield return req.SendWebRequest();

    if(req.result == UnityWebRequest.Result.Success)
    {
        print("存储本地成功");
    }
    else
    {
        print("获取数据失败" + req.result + req.error + req.responseCode);
    }
}


public class CustomDownLoadFileHandler:DownloadHandlerScript
{
    //用于保存 本地存储时的路径
    private string savePath;

    //用于缓存收到的数据的容器
    private byte[] cacheBytes;
    //这是当前已收到的数据长度
    private int index = 0;

    public CustomDownLoadFileHandler():base()
    {

    }

    public CustomDownLoadFileHandler(byte[] bytes) :base(bytes)
    {

    }

    public CustomDownLoadFileHandler(string path) : base()
    {
        savePath = path;
    }

    protected override byte[] GetData()
    {
        //返回字节数组
        return cacheBytes;
    }

    /// <summary>
    /// 从网络收到数据后 每帧会调用的方法  会自动调用的方法
    /// </summary>
    /// <param name="data"></param>
    /// <param name="dataLength"></param>
    /// <returns></returns>
    protected override bool ReceiveData(byte[] data, int dataLength)
    {
        Debug.Log("收到数据长度:" + data.Length);
        Debug.Log("收到数据长度dataLength:" + dataLength);
        data.CopyTo(cacheBytes, index);
        index += dataLength;
        return true;
    }

    /// <summary>
    /// 从服务器收到 COntent-Length标头时  会自动调用的方法
    /// </summary>
    /// <param name="contentLength"></param>
    protected override void ReceiveContentLengthHeader(ulong contentLength)
    {
        //base.ReceiveContentLengthHeader(contentLength);
        Debug.Log("收到数据长度:" + contentLength);
        //根据收到的标头 决定字节数组容器的大小
        cacheBytes = new byte[contentLength];
    }

    /// <summary>
    /// 当消息收完了 会自动调用的方法
    /// </summary>
    protected override void CompleteContent()
    {
        Debug.Log("消息收完");
        //把收到的字节数组 进行自定义处理 我们在这 处理成 存储到本地
        File.WriteAllBytes(savePath, cacheBytes);
    }

}

总结

我们可以自己设置UnityWebRequest当中的下载处理对象

当设置后,下载数据后它会使用该对象中对应的函数处理数据

让我们更方便的获取我们想要的数据

方便我们对数据下载或获取进行拓展

UnityWebRequest上传数据的高级操作

知识点一 自定义上传数据UploadHandler相关类

注意:

由于UnityWebRequest类的常用操作中

上传数据相关内容已经封装的很好了

我们可以很方便的上传参数和文件

我们使用常用操作已经能够满足常用需求了

所以以下内容主要做了解

UploadHandler相关类

1.UploadHandlerRaw 用于上传字节数组

StartCoroutine(UpLoad());
IEnumerator UpLoad()
{
    UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/", UnityWebRequest.kHttpVerbPOST);

    //1.UploadHandlerRaw  用于上传字节数组
    //byte[] bytes = Encoding.UTF8.GetBytes("123123123123123");
    //req.uploadHandler = new UploadHandlerRaw(bytes);
    //req.uploadHandler.contentType = "类型/细分类型";

    //2.UploadHandlerFile 用于上传文件
    req.uploadHandler = new UploadHandlerFile(Application.streamingAssetsPath + "/test.png");

    yield return req.SendWebRequest();

    print(req.result);
}

2.UploadHandlerFile 用于上传文件

其中比较重要的变量是

contentType 内容类型,如果不设置,模式是 application/octet-stream 2进制流的形式

总结

由于上传数据相关 UnityWebRequest原本已经提供了较为完善的

参数上传、文件上传相关功能

所以高级操作中的 上传数据相关内容拓展较少,使用也较少

我们使用常用操作的上传数据相关功能就足够了

高级操作的上传数据知识点主要做了解

第8章: 消息处理

第1节: 自定义协议生成工具

如何制作协议(消息)生成工具

知识点一 什么是协议生成工具?

协议生成工具,一般指 消息(协议)生成工具

就是专门用于自动化生成消息的程序

我们之前学习了:

  1. 消息的序列化和反序列化

  2. 区分消息类型

  3. 分包黏包

等等关于消息的相关知识

当需要一个新消息时,我们需要手动的按照规则去声明新的类

这部分工作费时又费力,技术含量也不高

如果前后端是统一的语言,我们按照语法声明一次就行

但是如果前后端语言不统一,比如前端用C#,后端用C++或者Java

那么前后端分开去声明,也容易造成沟通不一致,声明不统一的问题

所以如果靠我们手动的去声明消息类

是一件费时、费力、还容易出问题的事情

所以我们在商业游戏开发时,往往就需要使用协程生成工具

来帮助我们自动化的声明消息类

这样做的好处是:

  1. 提升开发效率

  2. 降低沟通成本,避免前后端消息不匹配的问题

知识点二 如何制作协议生成工具

要制作工具,首先要确定需求

对于协议生成工具来说,主要需求如下

  1. 通过配置文件配置消息或数据类 名字、变量等

  2. 工具根据该配置文件信息动态的生成 类文件(脚本文件,代码是自动生成的)

  3. 我们可以在开发中直接使用生成文件中声明好的消息和数据结构类进行开发

根据需求分析,我们需要做

1.确定协议配置方式

可以使用json、xml、自定义格式进行协议配置

主要目的,是通过配置文件确定

1:消息或者数据结构类名字

2:字段名等

2.确定生成格式

最终我们是要自动生成类声明文件

所以具体类应该如何生成需要确定格式

比如:

继承关系 固定写法

序列化、反序列化 固定写法

提取出共同点

3.制作生成工具

基于配置文件 和 生成格式 动态的生成对应类文件

总结

制作协议生成工具的目的是一劳永逸

制作完成后

它可以提升开发效率,避免协议不统一等问题

之后多了不同的语言,按照规则进行编写即可

协议生成工具实现

实现的功能

配置完成xml文件后,可以一键生成枚举,数据结构或消息类的代码。

目前代码只支持C#,今后可以根据需求添加。

功能类

ProtocolTool

编辑器类,创建编辑器目录供开发者开启代码生成

public class ProtocolTool
{
    //配置文件所在路径
    private static string PROTO_INFO_PATH = Application.dataPath + "/Editor/ProtocolTool/ProtocolInfo.xml";

    private static GenerateCSharp generateCSharp = new GenerateCSharp();

    [MenuItem("ProtocolTool/生成C#脚本")]
    private static void GenerateCSharp()
    {
        //1.读取xml相关的信息
        //XmlNodeList list = GetNodes("enum");
        //2.根据这些信息 去拼接字符串 生成对应的脚本
        //生成对应的枚举脚本
        generateCSharp.GenerateEnum(GetNodes("enum"));
        //生成对应的数据结构类脚本
        generateCSharp.GenerateData(GetNodes("data"));
        //生成对应的消息类脚本
        generateCSharp.GenerateMsg(GetNodes("message"));

        //刷新编辑器界面 让我们可以看到生成的内容 不需要手动进行刷新了
        AssetDatabase.Refresh();
    }

    [MenuItem("ProtocolTool/生成C++脚本")]
    private static void GenerateC()
    {
        Debug.Log("生成C++代码");
    }

    [MenuItem("ProtocolTool/生成Java脚本")]
    private static void GenerateJava()
    {
        Debug.Log("生成Java代码");
    }


    /// <summary>
    /// 获取指定名字的所有子节点 的 List
    /// </summary>
    /// <param name="nodeName"></param>
    /// <returns></returns>
    private static XmlNodeList GetNodes(string nodeName)
    {
        XmlDocument xml = new XmlDocument();
        xml.Load(PROTO_INFO_PATH);
        XmlNode root = xml.SelectSingleNode("messages");
        return root.SelectNodes(nodeName);
    }
}

GenerateCSharp

实现生成代码的具体逻辑

public class GenerateCSharp
{
    //协议保存路径
    private string SAVE_PATH = Application.dataPath + "/Scripts/Protocol/";

    //生成枚举
    public void GenerateEnum(XmlNodeList nodes)
    {
        //生成枚举脚本的逻辑
        string namespaceStr = "";
        string enumNameStr = "";
        string fieldStr = "";

        foreach (XmlNode enumNode in nodes)
        {
            //获取命名空间配置信息
            namespaceStr = enumNode.Attributes["namespace"].Value;
            //获取枚举名配置信息
            enumNameStr = enumNode.Attributes["name"].Value;
            //获取所有的字段节点 然后进行字符串拼接
            XmlNodeList enumFields = enumNode.SelectNodes("field");
            //一个新的枚举 需要清空一次上一次拼接的字段字符串
            fieldStr = "";
            foreach (XmlNode enumField in enumFields)
            {
                fieldStr += "\t\t" + enumField.Attributes["name"].Value;
                if (enumField.InnerText != "")
                    fieldStr += " = " + enumField.InnerText;
                fieldStr += ",\r\n";
            }
            //对所有可变的内容进行拼接
            string enumStr = $"namespace {namespaceStr}\r\n" +
                             "{\r\n" +
                                $"\tpublic enum {enumNameStr}\r\n" +
                                "\t{\r\n" +
                                    $"{fieldStr}" +
                                "\t}\r\n" +
                             "}";
            //保存文件的路径
            string path = SAVE_PATH + namespaceStr + "/Enum/";
            //如果不存在这个文件夹 则创建
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            //字符串保存 存储为枚举脚本文件
            File.WriteAllText(path + enumNameStr + ".cs", enumStr);
        }

        Debug.Log("枚举生成结束");
    }

    //生成数据结构类
    public void GenerateData(XmlNodeList nodes)
    {
        string namespaceStr = "";
        string classNameStr = "";
        string fieldStr = "";
        string getBytesNumStr = "";
        string writingStr = "";
        string readingStr = "";

        foreach (XmlNode dataNode in nodes)
        {
            //命名空间
            namespaceStr = dataNode.Attributes["namespace"].Value;
            //类名
            classNameStr = dataNode.Attributes["name"].Value;
            //读取所有字段节点
            XmlNodeList fields = dataNode.SelectNodes("field");
            //通过这个方法进行成员变量声明的拼接 返回拼接结果
            fieldStr = GetFieldStr(fields);
            //通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果
            getBytesNumStr = GetGetBytesNumStr(fields);
            //通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果
            writingStr = GetWritingStr(fields);
            //通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果
            readingStr = GetReadingStr(fields);

            string dataStr = "using System;\r\n" +
                             "using System.Collections.Generic;\r\n" +
                             "using System.Text;\r\n" + 
                             $"namespace {namespaceStr}\r\n" +
                              "{\r\n" +
                              $"\tpublic class {classNameStr} : BaseData\r\n" +
                              "\t{\r\n" +
                                    $"{fieldStr}" +
                                    "\t\tpublic override int GetBytesNum()\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint num = 0;\r\n" +
                                        $"{getBytesNumStr}" +
                                        "\t\t\treturn num;\r\n" +
                                    "\t\t}\r\n" +
                                    "\t\tpublic override byte[] Writing()\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint index = 0;\r\n"+
                                        "\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +
                                        $"{writingStr}" +
                                        "\t\t\treturn bytes;\r\n" +
                                    "\t\t}\r\n" +
                                    "\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint index = beginIndex;\r\n" +
                                        $"{readingStr}" +
                                        "\t\t\treturn index - beginIndex;\r\n" +
                                    "\t\t}\r\n" +
                              "\t}\r\n" +
                              "}";

            //保存为 脚本文件
            //保存文件的路径
            string path = SAVE_PATH + namespaceStr + "/Data/";
            //如果不存在这个文件夹 则创建
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            //字符串保存 存储为枚举脚本文件
            File.WriteAllText(path + classNameStr + ".cs", dataStr);

        }
        Debug.Log("数据结构类生成结束");
    }

    //生成消息类
    public void GenerateMsg(XmlNodeList nodes)
    {
        string idStr = "";
        string namespaceStr = "";
        string classNameStr = "";
        string fieldStr = "";
        string getBytesNumStr = "";
        string writingStr = "";
        string readingStr = "";

        foreach (XmlNode dataNode in nodes)
        {
            //消息ID
            idStr = dataNode.Attributes["id"].Value;
            //命名空间
            namespaceStr = dataNode.Attributes["namespace"].Value;
            //类名
            classNameStr = dataNode.Attributes["name"].Value;
            //读取所有字段节点
            XmlNodeList fields = dataNode.SelectNodes("field");
            //通过这个方法进行成员变量声明的拼接 返回拼接结果
            fieldStr = GetFieldStr(fields);
            //通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果
            getBytesNumStr = GetGetBytesNumStr(fields);
            //通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果
            writingStr = GetWritingStr(fields);
            //通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果
            readingStr = GetReadingStr(fields);

            string dataStr = "using System;\r\n" +
                             "using System.Collections.Generic;\r\n" +
                             "using System.Text;\r\n" +
                             $"namespace {namespaceStr}\r\n" +
                              "{\r\n" +
                              $"\tpublic class {classNameStr} : BaseMsg\r\n" +
                              "\t{\r\n" +
                                    $"{fieldStr}" +
                                    "\t\tpublic override int GetBytesNum()\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint num = 8;\r\n" +//这个8代表的是 消息ID的4个字节 + 消息体长度的4个字节
                                        $"{getBytesNumStr}" +
                                        "\t\t\treturn num;\r\n" +
                                    "\t\t}\r\n" +
                                    "\t\tpublic override byte[] Writing()\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint index = 0;\r\n" +
                                        "\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +
                                        "\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" +
                                        "\t\t\tWriteInt(bytes, bytes.Length - 8, ref index);\r\n" +
                                        $"{writingStr}" +
                                        "\t\t\treturn bytes;\r\n" +
                                    "\t\t}\r\n" +
                                    "\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\tint index = beginIndex;\r\n" +
                                        $"{readingStr}" +
                                        "\t\t\treturn index - beginIndex;\r\n" +
                                    "\t\t}\r\n" +
                                    "\t\tpublic override int GetID()\r\n" +
                                    "\t\t{\r\n" +
                                        "\t\t\treturn " + idStr + ";\r\n" +
                                    "\t\t}\r\n" +
                              "\t}\r\n" +
                              "}";

            //保存为 脚本文件
            //保存文件的路径
            string path = SAVE_PATH + namespaceStr + "/Msg/";
            //如果不存在这个文件夹 则创建
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            //字符串保存 存储为枚举脚本文件
            File.WriteAllText(path + classNameStr + ".cs", dataStr);

        }
        Debug.Log("消息类生成结束");
    }

    /// <summary>
    /// 获取成员变量声明内容
    /// </summary>
    /// <param name="fields"></param>
    /// <returns></returns>
    private string GetFieldStr(XmlNodeList fields)
    {
        string fieldStr = "";
        foreach (XmlNode field in fields)
        {
            //变量类型
            string type = field.Attributes["type"].Value;
            //变量名
            string fieldName = field.Attributes["name"].Value;
            if(type == "list")
            {
                string T = field.Attributes["T"].Value;
                fieldStr += "\t\tpublic List<" + T + "> ";
            }
            else if(type == "array")
            {
                string data = field.Attributes["data"].Value;
                fieldStr += "\t\tpublic " + data + "[] ";
            }
            else if(type == "dic")
            {
                string Tkey = field.Attributes["Tkey"].Value;
                string Tvalue = field.Attributes["Tvalue"].Value;
                fieldStr += "\t\tpublic Dictionary<" + Tkey +  ", " + Tvalue + "> ";
            }
            else if(type == "enum")
            {
                string data = field.Attributes["data"].Value;
                fieldStr += "\t\tpublic " + data + " ";
            }
            else
            {
                fieldStr += "\t\tpublic " + type + " ";
            }

            fieldStr += fieldName + ";\r\n";
        }
        return fieldStr;
    }

    //拼接 GetBytesNum函数的方法
    private string GetGetBytesNumStr(XmlNodeList fields)
    {
        string bytesNumStr = "";

        string type = "";
        string name = "";
        foreach (XmlNode field in fields)
        {
            type = field.Attributes["type"].Value;
            name = field.Attributes["name"].Value;
            if (type == "list")
            {
                string T = field.Attributes["T"].Value;
                bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息
                bytesNumStr += "\t\t\tfor (int i = 0; i < " + name + ".Count; ++i)\r\n";
                //这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用
                bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(T, name + "[i]") + ";\r\n";
            }
            else if (type == "array")
            {
                string data = field.Attributes["data"].Value;
                bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息
                bytesNumStr += "\t\t\tfor (int i = 0; i < " + name + ".Length; ++i)\r\n";
                //这里使用的是 name + [i] 目的是获取 list当中的元素传入进行使用
                bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(data, name + "[i]") + ";\r\n";
            }
            else if (type == "dic")
            {
                string Tkey = field.Attributes["Tkey"].Value;
                string Tvalue = field.Attributes["Tvalue"].Value;
                bytesNumStr += "\t\t\tnum += 2;\r\n";//+2 是为了节约字节数 用一个short去存储信息
                bytesNumStr += "\t\t\tforeach (" + Tkey + " key in " + name + ".Keys)\r\n";
                bytesNumStr += "\t\t\t{\r\n";
                bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tkey, "key") + ";\r\n";
                bytesNumStr += "\t\t\t\tnum += " + GetValueBytesNum(Tvalue, name + "[key]") + ";\r\n";
                bytesNumStr += "\t\t\t}\r\n";
            }
            else
                bytesNumStr += "\t\t\tnum += " + GetValueBytesNum(type, name) + ";\r\n";
        }

        return bytesNumStr;
    }
    //获取 指定类型的字节数
    private string GetValueBytesNum(string type, string name)
    {
        //这里我没有写全 所有的常用变量类型 你可以根据需求去添加
        switch (type)
        {
            case "int":
            case "float":
            case "enum":
                return "4";
            case "long":
                return "8";
            case "byte":
            case "bool":
                return "1";
            case "short":
                return "2";
            case "string":
                return "4 + Encoding.UTF8.GetByteCount(" + name + ")";
            default:
                return name + ".GetBytesNum()";
        }
    }

    //拼接 Writing函数的方法
    private string GetWritingStr(XmlNodeList fields)
    {
        string writingStr = "";

        string type = "";
        string name = "";
        foreach (XmlNode field in fields)
        {
            type = field.Attributes["type"].Value;
            name = field.Attributes["name"].Value;
            if(type == "list")
            {
                string T = field.Attributes["T"].Value;
                writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";
                writingStr += "\t\t\tfor (int i = 0; i < " + name + ".Count; ++i)\r\n";
                writingStr += "\t\t\t\t" + GetFieldWritingStr(T, name + "[i]") + "\r\n";
            }
            else if (type == "array")
            {
                string data = field.Attributes["data"].Value;
                writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Length, ref index);\r\n";
                writingStr += "\t\t\tfor (int i = 0; i < " + name + ".Length; ++i)\r\n";
                writingStr += "\t\t\t\t" + GetFieldWritingStr(data, name + "[i]") + "\r\n";
            }
            else if (type == "dic")
            {
                string Tkey = field.Attributes["Tkey"].Value;
                string Tvalue = field.Attributes["Tvalue"].Value;
                writingStr += "\t\t\tWriteShort(bytes, (short)" + name + ".Count, ref index);\r\n";
                writingStr += "\t\t\tforeach (" + Tkey + " key in " + name + ".Keys)\r\n";
                writingStr += "\t\t\t{\r\n";
                writingStr += "\t\t\t\t" + GetFieldWritingStr(Tkey, "key") + "\r\n";
                writingStr += "\t\t\t\t" + GetFieldWritingStr(Tvalue, name + "[key]") + "\r\n";
                writingStr += "\t\t\t}\r\n";
            }
            else
            {
                writingStr += "\t\t\t" + GetFieldWritingStr(type, name) + "\r\n";
            }
        }
        return writingStr;
    }

    private string GetFieldWritingStr(string type, string name)
    {
        switch (type)
        {
            case "byte":
                return "WriteByte(bytes, " + name + ", ref index);";
            case "int":
                return "WriteInt(bytes, " + name + ", ref index);";
            case "short":
                return "WriteShort(bytes, " + name + ", ref index);";
            case "long":
                return "WriteLong(bytes, " + name + ", ref index);";
            case "float":
                return "WriteFloat(bytes, " + name + ", ref index);";
            case "bool":
                return "WriteBool(bytes, " + name + ", ref index);";
            case "string":
                return "WriteString(bytes, " + name + ", ref index);";
            case "enum":
                return "WriteInt(bytes, Convert.ToInt32(" + name + "), ref index);";
            default:
                return "WriteData(bytes, " + name + ", ref index);";
        }
    }

    private string GetReadingStr(XmlNodeList fields)
    {
        string readingStr = "";

        string type = "";
        string name = "";
        foreach (XmlNode field in fields)
        {
            type = field.Attributes["type"].Value;
            name = field.Attributes["name"].Value;
            if (type == "list")
            {
                string T = field.Attributes["T"].Value;
                readingStr += "\t\t\t" + name + " = new List<" + T + ">();\r\n";
                readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";
                readingStr += "\t\t\tfor (int i = 0; i < " + name + "Count; ++i)\r\n";
                readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(T) + ");\r\n";
            }
            else if (type == "array")
            {
                string data = field.Attributes["data"].Value;
                readingStr += "\t\t\tshort " + name + "Length = ReadShort(bytes, ref index);\r\n";
                readingStr += "\t\t\t" + name + " = new " + data + "["+ name + "Length];\r\n";
                readingStr += "\t\t\tfor (int i = 0; i < " + name + "Length; ++i)\r\n";
                readingStr += "\t\t\t\t" + name + "[i] = " + GetFieldReadingStr(data) + ";\r\n";
            }
            else if (type == "dic")
            {
                string Tkey = field.Attributes["Tkey"].Value;
                string Tvalue = field.Attributes["Tvalue"].Value;
                readingStr += "\t\t\t" + name + " = new Dictionary<" + Tkey + ", " + Tvalue + ">();\r\n";
                readingStr += "\t\t\tshort " + name + "Count = ReadShort(bytes, ref index);\r\n";
                readingStr += "\t\t\tfor (int i = 0; i < " + name + "Count; ++i)\r\n";
                readingStr += "\t\t\t\t" + name + ".Add(" + GetFieldReadingStr(Tkey) + ", " +
                                                            GetFieldReadingStr(Tvalue) + ");\r\n";
            }
            else if (type == "enum")
            {
                string data = field.Attributes["data"].Value;
                readingStr += "\t\t\t" + name + " = (" + data + ")ReadInt(bytes, ref index);\r\n";
            }
            else
                readingStr += "\t\t\t" + name + " = " + GetFieldReadingStr(type) + ";\r\n";
        }

        return readingStr;
    }

    private string GetFieldReadingStr(string type)
    {
        switch (type)
        {
            case "byte":
                return "ReadByte(bytes, ref index)";
            case "int":
                return "ReadInt(bytes, ref index)";
            case "short":
                return "ReadShort(bytes, ref index)";
            case "long":
                return "ReadLong(bytes, ref index)";
            case "float":
                return "ReadFloat(bytes, ref index)";
            case "bool":
                return "ReadBool(bytes, ref index)";
            case "string":
                return "ReadString(bytes, ref index)";
            default:
                return "ReadData<" + type + ">(bytes, ref index)";
        }
    }
}

使用示例

xml
<?xml version="1.0" encoding="UTF-8"?>
<messages>
	<!--枚举配置规则-->
	<enum name="E_MONSTER_TYPE" namespace="GameMonster">
		<field name="NORMAL">2</field>
		<field name="BOSS"/>
	</enum>
	<!--数据结构类配置规则-->
	<data name="PlayerData" namespace="GamePlayer">
		<field type="int" name="id"/>
		<field type="float" name="atk"/>
		<field type="bool" name="sex"/>
		<field type="long" name="lev"/>
		<field type="array" data="int" name="arrays"/>
		<field type="list" T="int" name="list"/>
		<field type="dic" Tkey="int" Tvalue="string" name="dic"/>
		<field type="enum" data="E_HERO_TYPE" name="heroType"/>
	</data>
	<!--消息类类配置规则-->
	<message id="1001" name="PlayerMsg" namespace="GamePlayer">
		<field type="int" name="playerID"/>
		<field type="PlayerData" name="data"/>
	</message>
</messages>

生成的枚举
namespace GameMonster
{
    public enum E_MONSTER_TYPE
    {
       NORMAL = 2,
       BOSS,
    }
}

生成的数据结构类
namespace GamePlayer
{
    public class PlayerData : BaseData
    {
       public int id;
       public float atk;
       public bool sex;
       public long lev;
       public int[] arrays;
       public List<int> list;
       public Dictionary<int, string> dic;
       public E_HERO_TYPE heroType;
       public override int GetBytesNum()
       {
          int num = 0;
          num += 4;
          num += 4;
          num += 1;
          num += 8;
          num += 2;
          for (int i = 0; i < arrays.Length; ++i)
             num += 4;
          num += 2;
          for (int i = 0; i < list.Count; ++i)
             num += 4;
          num += 2;
          foreach (int key in dic.Keys)
          {
             num += 4;
             num += 4 + Encoding.UTF8.GetByteCount(dic[key]);
          }
          num += 4;
          return num;
       }
       public override byte[] Writing()
       {
          int index = 0;
          byte[] bytes = new byte[GetBytesNum()];
          WriteInt(bytes, id, ref index);
          WriteFloat(bytes, atk, ref index);
          WriteBool(bytes, sex, ref index);
          WriteLong(bytes, lev, ref index);
          WriteShort(bytes, (short)arrays.Length, ref index);
          for (int i = 0; i < arrays.Length; ++i)
             WriteInt(bytes, arrays[i], ref index);
          WriteShort(bytes, (short)list.Count, ref index);
          for (int i = 0; i < list.Count; ++i)
             WriteInt(bytes, list[i], ref index);
          WriteShort(bytes, (short)dic.Count, ref index);
          foreach (int key in dic.Keys)
          {
             WriteInt(bytes, key, ref index);
             WriteString(bytes, dic[key], ref index);
          }
          WriteInt(bytes, Convert.ToInt32(heroType), ref index);
          return bytes;
       }
       public override int Reading(byte[] bytes, int beginIndex = 0)
       {
          int index = beginIndex;
          id = ReadInt(bytes, ref index);
          atk = ReadFloat(bytes, ref index);
          sex = ReadBool(bytes, ref index);
          lev = ReadLong(bytes, ref index);
          short arraysLength = ReadShort(bytes, ref index);
          arrays = new int[arraysLength];
          for (int i = 0; i < arraysLength; ++i)
             arrays[i] = ReadInt(bytes, ref index);
          list = new List<int>();
          short listCount = ReadShort(bytes, ref index);
          for (int i = 0; i < listCount; ++i)
             list.Add(ReadInt(bytes, ref index));
          dic = new Dictionary<int, string>();
          short dicCount = ReadShort(bytes, ref index);
          for (int i = 0; i < dicCount; ++i)
             dic.Add(ReadInt(bytes, ref index), ReadString(bytes, ref index));
          heroType = (E_HERO_TYPE)ReadInt(bytes, ref index);
          return index - beginIndex;
       }
    }
}

生成的消息类
namespace GamePlayer
{
    public class PlayerMsg : BaseMsg
    {
       public int playerID;
       public PlayerData data;
       public override int GetBytesNum()
       {
          int num = 8;
          num += 4;
          num += data.GetBytesNum();
          return num;
       }
       public override byte[] Writing()
       {
          int index = 0;
          byte[] bytes = new byte[GetBytesNum()];
          WriteInt(bytes, GetID(), ref index);
          WriteInt(bytes, bytes.Length - 8, ref index);
          WriteInt(bytes, playerID, ref index);
          WriteData(bytes, data, ref index);
          return bytes;
       }
       public override int Reading(byte[] bytes, int beginIndex = 0)
       {
          int index = beginIndex;
          playerID = ReadInt(bytes, ref index);
          data = ReadData<PlayerData>(bytes, ref index);
          return index - beginIndex;
       }
       public override int GetID()
       {
          return 1001;
       }
    }
}

第2节: 第三方协议工具Protobuf

初识和准备Protobuf工具

知识点一 什么是Protobuf

Protobuf全称是 protocol-buffers(协议缓冲区)

是谷歌提供给开发者的一个开源的协议生成工具

它的主要工作原理和我们之前做的自定义协议工具类似

只不过它更加的完善,可以基于协议配置文件生成

C++、Java、C#、Objective-C、PHP、Python、Ruby、Go

等等语言的代码文件

它是商业游戏开发中常常会选择的协议生成工具

有很多游戏公司选择它作为协议工具来进行网络游戏开发

因为它通用性强,稳定性高,可以节约出开发自定义协议工具的时间

protocol-buffers官网

https://developers.google.com/protocol-buffers

注意:Protobuf本身的基础用途是用来方便和高效的实现数据结构,并序列化和反序列化的,并没有直接附带网络通讯的API,这方面需要我们自己实现。

知识点二 Protobuf的使用流程

  1. 下载对应语言要使用Protobuf相关内容

  2. 根据配置规则编辑协议配置文件

  3. 用Protobuf编译器,利用协议配置文件生成对应语言的代码文件

  4. 将代码文件导入工程中进行使用

知识点三 下载Protobuf相关内容——准备DLL文件

1.在官网中前往下载地址

protocol-buffers官网

https://developers.google.com/protocol-buffers

2.下载protobuf-csharp

3.解压后打开csharp\src中的Google.Protobuf.sln

4.选择Google.Protobuf右键生成 dll文件

5.在csharp\src\Google.Protobuf\bin\Debug路径下找到对应.net版本的Dll文件(我们使用4.5即可)

6.将net45中的dll文件导入到Unity工程中的Plugins插件文件夹中

知识点四 下载Protobuf相关内容——准备编译器

1.在官网中前往下载地址

protocol-buffers官网

https://developers.google.com/protocol-buffers

2.下载protoc-版本-win32或者64(根据操作系统而定)

3.解压后获取bin文件夹中的protoc.exe可执行文件,

可将其放入Unity工程中,方便之后的使用(你也可以不放入Unity工程,记住它的路径即可)

总结

Protobuf全称protocol-buffers

是谷歌提供给开发者的开源协议生成工具

我们要使用它主要准备两步

1.下载对应Csharp版本,生成DLL包文件导入工程中(之后的基类,序列化反序列化都基于DLL包中写好的内容)

2.下载对应操作系统的protoc编译器,用于之后生成代码文件(之后根据配置文件生成代码都是通过该应用程序)

Protobuf 配置规则

知识点一 回顾自定义协议生成工具中的配置文件

我们在自定义协议配置工具相关知识点中

使用的是xml文件进行配置

我们只需要基于xml的规则

按照一定规则配置协议信息

之后获取xml数据用于生成代码文件

在Protobuf中原理是一样的

只不过Protobuf中有自己的配置规则

也自定义了对应的配置文件后缀格式

知识点二 配置后缀

Protobuf中配置文件的后缀统一使用

.proto

可以通过多个后缀为.proto的配置文件进行配置

知识点三 配置规则

规则1 注释方式
//方式1
/*方式2*/
规则2 第一行版本号

syntax = "proto3";

如果不写 默认使用proto2

规则3 命名空间

package 命名空间名;

规则4 消息类

message 类名{
字段声明
}

规则5 成员类型和 唯一编号

浮点数:

float、double

整数:

变长编码-int32,int64,uint32,uint64,

固定字节数-fixed32,fixed64,sfixed32,sfixed64

其它类型:

bool,string,bytes

唯一编号 配置成员时 需要默认给他们一个编号 从1开始

这些编号用于标识中的字段消息二进制格式

规则6 特殊标识

1:required 必须赋值的字段

2:optional 可以不赋值的字段

3:repeated 数组

但是需要强调的是,required在3.0版本已经被废弃

规则7 枚举
enum 枚举名{
    常量1 = 0;//第一个常量必须映射到0
    常量2 = 1;
}

规则8 默认值

string-空字符串

bytes-空字节

bool-false

数值-0

枚举-0

message-取决于语言 C#为空

规则9 允许嵌套

可以在类里面声明类。

如message1里面再次声明message2,使用的时候message1.message2即可,和C#类似

规则10 保留字段

如果修改了协议规则 删除了部分内容

为了避免更新时 重新使用 已经删除了的编号

我们可以利用 reserved 关键字来保留字段

这些内容就不能再被使用了

message Foo {

reserved 2, 15, 9 to 11;

reserved "foo", "bar";

}

规则11 导入定义

import "配置文件路径";

如果你在某一个配置中 使用了另一个配置的类型

则需要导入另一个配置文件名

演示

text.proto
syntax = "proto3";//决定了proto文档的版本号
//规则二:版本号

//规则一:注释方式
//注释方式一
/*注释方式二*/

//规则11:导入定义
import "test2.proto";

//规则三:命名空间
package GamePlayerTest;//这决定了命名空间


//规则四:消息类
message TestMsg{
  //规则五:成员类型 和 唯一编号

  //浮点数
  // = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理
  //required 必须赋值的字段  3.0已经被弃用
  //required float testF = 1; //C# - float
  //optional 可以不赋值的字段
  optional double testD = 2; //C# - double

  //变长编码
  //所谓变长 就是会根据 数字的大小 来使用对应的字节数来存储  1 2 4 
  //Protobuf帮助我们优化的部分 可以尽量少的使用字节数 来存储内容
  int32 testInt32 = 3; //C# - int 它不太适用于来表示负数 请使用sint32
  //1 2 4 8
  int64 testInt64 = 4; //C# - long  它不太适用于来表示负数 请使用sint64

  //更实用与表示负数类型的整数
  sint32 testSInt32 = 5; //C# - int 适用于来表示负数的整数
  sint64 testSInt64 = 6; //C# - long 适用于来表示负数的整数

  //无符号 变长编码
  //1 2 4
  uint32 testUInt = 7; //C# - uint 变长的编码
  uint64 testULong = 8; //C# - ulong 变长的编码

  //固定字节数的类型
  fixed32 testFixed32 = 9; //C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节
  fixed64 testFixed64 = 10; //C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节

  sfixed32 testSFixed32 = 11; //C# - int 始终4个字节
  sfixed64 testSFixed64 = 12; //C# - long 始终8个字节

  //其它类型
  bool testBool = 13; //C# - bool 
  string testStr = 14; //C# - string
  bytes testBytes = 15; //C# - BytesString 字节字符串

  //数组List
  repeated int32 listInt = 16; // C# - 类似List<int>的使用
  //字典Dictionary
  map<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用

  //枚举成员变量的声明 需要唯一编码
  TestEnum testEnum = 18;

  //声明自定义类对象 需要唯一编码
  //默认值是null
  TestMsg2 testMsg2 = 19;

  //规则9:允许嵌套
  //嵌套一个类在另一个类当中 相当于是内部类
  message TestMsg3{
   int32 testInt32 = 1;
  }

  TestMsg3 testMsg3 = 20;

  //规则9:允许嵌套
  enum TestEnum2{
   NORMAL = 0; //第一个常量必须映射到0
   BOSS = 1;
  }

  TestEnum2 testEnum2 = 21;

  //int32 testInt3233333 = 22;

  bool testBool2123123 = 23;

  GameSystemTest.HeartMsg testHeart = 24;

  //告诉编译器 22 被占用 不准用户使用
  //之所以有这个功能 是为了在版本不匹配时 反序列化时 不会出现结构不统一
  //解析错误的问题
  reserved 22;

}

//枚举的声明
enum TestEnum{
  NORMAL = 0; //第一个常量必须映射到0
  BOSS = 5;
}

message TestMsg2{
  int32 testInt32 = 1;
}

text2.proto(被text引用)
syntax = "proto3";//决定了proto文档的版本号
package GameSystemTest;//这决定了命名空间
message HeartMsg
{
  int64 time = 1;
}

总结

我们需要掌握Protobuf的配置规则

之后才能使用工具将其转为C#脚本文件

Protobuf 协议生成

知识点一 利用protoc.exe编译器生成脚本文件

1.打开cmd窗口

2.进入protoc.exe所在文件夹(也可以直接将exe文件拖入cmd窗口中)

3.输入转换指令

protoc.exe -I=配置路径 --csharp_out=输出路径 配置文件名

注意:路径不要有中文和特殊符号,避免生成失败

知识点二 生成的脚本在unity中的使用

使用几乎和原版untiy一模一样,但是需要注意的是

  1. dic和list并不是unity原版的对象,而是它创建的新类,但是使用方法和原生也一模一样

  2. 同时它创建的类也自带了一些特殊的API,可以方便开发者的使用。

演示
TestMsg msg = new TestMsg();
msg.TestBool = true;
//对应的和List以及Dictionary使用方式一样的 数组和字典对象
msg.ListInt.Add(1);
print(msg.ListInt[0]);
msg.TestMap.Add(1, "唐老狮");
print(msg.TestMap[1]);

//枚举
msg.TestEnum = TestEnum.Boss;
//内部枚举
msg.TestEnum2 = TestMsg.Types.TestEnum2.Boss;

//其它类对象
msg.TestMsg2 = new TestMsg2();
msg.TestMsg2.TestInt32 = 99;
//其它内部类对象
msg.TestMsg3 = new TestMsg.Types.TestMsg3();
msg.TestMsg3.TestInt32 = 55;
//在另一个生成的脚本当中的类 如果命名空间不同 需要命名空间点出来使用
msg.TestHeart = new GameSystemTest.HeartMsg();

用Unity编辑器直接进行协议生成

问题

上一节我们用cmd来生成脚本,可以看到每次生成的时候要写的内容还是比较繁琐的,并且是外部生成,和unity联系不紧密。

因此,我们不妨直接在Unity编辑器里面设置一个按钮来方便我们快速的在Unity实现代码生成。

实现
public class ProtobufTool
{
    //协议配置文件所在路径
    private static string PROTO_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\proto";
    //协议生成可执行文件的路径
    private static string PROTOC_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\protoc.exe";
    //C#文件生成的路径
    private static string CSHARP_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\csharp";
    //C++文件生成的路径
    private static string CPP_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\cpp";
    //Java文件生成的路径
    private static string JAVA_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\java";


    [MenuItem("ProtobufTool/生成C#代码")]
    private static void GenerateCSharp()
    {
        Generate("csharp_out", CSHARP_PATH);
    }

    [MenuItem("ProtobufTool/生成C++代码")]
    private static void GenerateCPP()
    {
        Generate("cpp_out", CPP_PATH);
    }

    [MenuItem("ProtobufTool/生成Java代码")]
    private static void GenerateJava()
    {
        Generate("java_out", JAVA_PATH);
    }

    //生成对应脚本的方法
    private static void Generate(string outCmd, string outPath)
    {
        //第一步:遍历对应协议配置文件夹 得到所有的配置文件 
        DirectoryInfo directoryInfo = Directory.CreateDirectory(PROTO_PATH);
        //获取对应文件夹下所有文件信息
        FileInfo[] files = directoryInfo.GetFiles();
        //遍历所有的文件 为其生成协议脚本
        for (int i = 0; i < files.Length; i++)
        {
            //后缀的判断 只有是 配置文件才能用于生成
            if (files[i].Extension == ".proto")
            {
                //第二步:根据文件内容 来生成对应的C#脚本 (需要使用C#当中的Process类)
                Process cmd = new Process();
                //protoc.exe的路径
                cmd.StartInfo.FileName = PROTOC_PATH;
                //命令
                cmd.StartInfo.Arguments = $"-I={PROTO_PATH} --{outCmd}={outPath} {files[i]}";
                //执行
                cmd.Start();
                //告诉外部 某一个文件 生成结束
                UnityEngine.Debug.Log(files[i] + "生成结束");
            }
        }
        UnityEngine.Debug.Log("所有内容生成结束");
    }
}

Protobuf 协议使用

导入

本节主要讲解protobuf生成的类脚本如何进行序列化和反序列化。

需要强调的是,本节的演示实际上是用的基础流文件方法,protobuf实际上已经提供了更为简便的API,会在下一节的序列化管理类里面展现。

知识点一 序列化存储为本地文件

主要使用

  1. 生成的类中的 WriteTo方法

  2. 文件流FileStream对象

TestMsg msg = new TestMsg();
msg.ListInt.Add(1);
msg.TestBool = false;
msg.TestD = 5.5;
msg.TestInt32 = 99;
msg.TestMap.Add(1, "唐老狮");
msg.TestMsg2 = new TestMsg2();
msg.TestMsg2.TestInt32 = 88;
msg.TestMsg3 = new TestMsg.Types.TestMsg3();
msg.TestMsg3.TestInt32 = 66;

msg.TestHeart = new GameSystemTest.HeartMsg();
msg.TestHeart.Time = 7777;

print(Application.persistentDataPath);
using (FileStream fs = File.Create(Application.persistentDataPath + "/TestMsg.tang"))
{
    msg.WriteTo(fs);
}

知识点二 反序列化本地文件

主要使用

  1. 生成的类中的 Parser.ParseFrom方法

  2. 文件流FileStream对象

using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/TestMsg.tang"))
{
    TestMsg msg2 = null;
    msg2 = TestMsg.Parser.ParseFrom(fs);
    print(msg2.TestMap[1]);
    print(msg2.ListInt[0]);
    print(msg2.TestD);
    print(msg2.TestMsg2.TestInt32);
    print(msg2.TestMsg3.TestInt32);
    print(msg2.TestHeart.Time);
}

知识点三 得到序列化后的字节数组

主要使用

  1. 生成的类中的 WriteTo方法

  2. 内存流MemoryStream对象

byte[] bytes = null;
using (MemoryStream ms = new MemoryStream())
{
    msg.WriteTo(ms);
    bytes = ms.ToArray();
    print("字节数组长度" + bytes.Length);
}

知识点四 从字节数组反序列化

主要使用

  1. 生成的类中的 Parser.ParseFrom方法

  2. 内存流MemoryStream对象

using (MemoryStream ms = new MemoryStream(bytes))
{
    print("内存流当中反序列化的内容");
    TestMsg msg2 = TestMsg.Parser.ParseFrom(ms);
    print(msg2.TestMap[1]);
    print(msg2.ListInt[0]);
    print(msg2.TestD);
    print(msg2.TestMsg2.TestInt32);
    print(msg2.TestMsg3.TestInt32);
    print(msg2.TestHeart.Time);
}

总结

Protobuf的 序列化和反序列化都要通过

流对象来进行处理

如果是进行本地存储 则可以使用文件流

如果是进行网络传输 则可以使用内存流获取字节数组

Protobuf序列化管理类

实现


public static class NetTool 
{
    //序列化Protobuf生成的对象
    public static byte[] GetProtoBytes( IMessage msg )
    {
        //拓展方法、里氏替换、接口 这些知识点 都在 C#相关的内容当中

        //基础写法 基于上节课学习的知识点
        //byte[] bytes = null;
        //using (MemoryStream ms = new MemoryStream())
        //{
        //    msg.WriteTo(ms);
        //    bytes = ms.ToArray();
        //}
        //return bytes;

        //通过该拓展方法 就可以直接获取对应对象的 字节数组了
        return msg.ToByteArray();
    }

    /// <summary>
    /// 反序列化字节数组为Protobuf相关的对象
    /// </summary>
    /// <typeparam name="T">想要获取的消息类型</typeparam>
    /// <param name="bytes">对应的字节数组 用于反序列化</param>
    /// <returns></returns>
    public static T GetProtoMsg<T>(byte[] bytes) where T:class, IMessage
    {
        //泛型 C#进阶
        //反射 C#进阶
        //得到对应消息的类型 通过反射得到内部的静态成员 然后得到其中的 对应方法
        //进行反序列化
        Type type = typeof(T);
        //通过反射 得到对应的 静态成员属性对象
        PropertyInfo pInfo = type.GetProperty("Parser");
        object parserObj = pInfo.GetValue(null, null);
        //已经得到了对象 那么可以得到该对象中的 对应方法 
        Type parserType = parserObj.GetType();
        //这是指定得到某一个重载函数
        MethodInfo mInfo = parserType.GetMethod("ParseFrom", new Type[] { typeof(byte[]) });
        //调用对应的方法 反序列化为指定的对象
        object msg = mInfo.Invoke(parserObj, new object[] { bytes });
        return msg as T;
    }
}

在GetProtoMsg方法里,由于ParseFrom(用来反序列化字节为类)是静态方法,在泛型方法里面,无法用T.静态方法的手段去调用,所以这里我们运用反射去进行方法的执行。

建议记住泛型方法里面静态方法的反射处理,这可能会在实际开发中经常碰到。

Protobuf-Net

知识点一Protobuf-Net是什么

早期的Protobuf并不支持C#

所以国外大神Marc Gravell在Protobuf的基础上进行了.net环境下的移植

并发布到了GitHub

让我们可以基于Protobuf的规则进行C#的代码生成,对象的序列化和反序列化

Protobuf-Net的Github地址:https://github.com/protobuf-net/protobuf-net

注意:

1. Protobuf不支持.Net3.5及以下版本

所以如果想在Unity的老版本中使用Protobuf我们只能使用Protobuf-Net

而在较新版本的Unity中不存在这个问题

2. 如何判断是否支持?

只要把Protobuf相关dll包导入后能够正常使用不报错,则证明支持

知识点二 下载获取Protobuf-Net

Protobuf-Net的Github地址:https://github.com/protobuf-net/protobuf-net

我们需要在Github上去获取对应的工程生成后获取

1. DLL库文件

2. 根据配置生成脚本的编译器可执行程序

总结

Protobuf-net相对来说是较老的生产方式

但是它可以解决老版本Unity使用Protobuf的问题

它的使用方式和之前学习的Protobuf相关知识类似

只是获取DLL文件、protoc.exe文件的方式不同而已

如果想要详细学习,可以前往Github看相关说明

在这里我们就不详细讲解了

目前我们使用的较新Unity版本直接使用之前学习的Protobuf相关知识完成需求即可

第3节: 其它知识

大小端模式

知识点一 什么是大小端模式

大端模式

是指数据的高字节保存在内存的低地址中

而数据的低字节保存在内存的高地址中

这样的存储模式有点儿类似于把数据当作字符串顺序处理

地址由小向大增加,数据从高位往低位放

符合人类的阅读习惯

小端模式

是指数据的高字节保存在内存的高地址中

而数据的低字节保存在内存的低地址中

举例说明

十六进制数据0x11223344

大端模式存储

11 22 33 44

0 1 2 3

低地址——>高地址

小端模式存储

44 33 22 11

0 1 2 3

低地址——>高地址

知识点二 为什么有大小端模式

大小端模式其实是计算机硬件的两种存储数据的方式

我们也可以称大小端模式为 大小端字节序

对于我们来说,大端字节序阅读起来更加方便,为什么还要有小端字节序呢?

原因是,计算机电路先处理低位字节,效率比较高

计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节

它只知道按顺序读取字节,先读第一个字节,再读第二个字节

如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节

小端字节序正好相反

因为计算机都是从低位开始的

所以,计算机的内部处理都是小端字节序

但是,我们人类的读写习惯还是大端字节序

所以,除了计算机的内部处理

其它场合几乎都是大端字节序,比如网络传输和文件存储

一般情况下,操作系统都是小端模式,而通讯协议都是大端模式

但是具体的模式,还是要根据硬件平台,开发语言来决定

主机不同,开发语言不同 可能采用的大小端模式也会不一致

知识点三 大小端模式对于我们的影响

我们记住一句话:

只有读取的时候,才必须区分大小端字节序,其它情况都不用考虑

因此对于我们来说,在网络传输当中我们传输的是字节数组

那么我们在收到字节数组进行解析时,就需要考虑大小端的问题

虽然TCP/IP协议规定了在网络上必须采用网络字节顺序(大端模式)

但是具体传输时采用哪种模式,都是根据前后端语言、设备决定的

在进行网络通讯时,前后端语言不同时,可能会造成大小端不统一

一般情况下

C# 和 Java/Erlang/AS3 通讯需要进行大小端转换 因为C#是小端模式 Java/Erlang/AS3是大端模式

C# 与 C++通信不需要特殊处理 他们都是小端模式

知识点四 大小端转换

一.判断是大小端哪种模式

print("是否是小端模式:" + BitConverter.IsLittleEndian);

二.简单的转换API 只支持几种类型

转换为网络字节序 相当于就是转为大端模式

1. 本机字节序转网络字节序

int i = 99;
byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(i));

2. 网络字节序转本机字节序

int receI = BitConverter.ToInt32(bytes, 0);
receI = IPAddress.NetworkToHostOrder(receI);

三.通用的转换方式

数组中的倒序API

如果后端需要用到大端模式 那么我们进行判断

如果当前是小端模式 就进行一次 大小端转换

if(BitConverter.IsLittleEndian)
    Array.Reverse(bytes);

总结

大小端模式会根据主机硬件环境不同、语言不同而有所区别

当我们前后端是不同语言开发且运行在不同主机上时

前后端需要对大小端字节序定下统一的规则

一般让前端迎合后端,因为字节序的转换也是会带来些许性能损耗的

网络游戏中要尽量减轻后端的负担

一般情况下

C# 和 Java/Erlang/AS3 通讯需要进行大小端转换 前端C#从小变大

C# 与 C++通信不需要特殊处理

我们不用死记硬背和谁通讯要注意大小端模式

当开发时,发现后端收到的消息和前端发的不一样

在协议统一的情况下,往往就是因为大小端造成的

这时我们再转换模式即可

注意

Protobuf已经帮助我们解决了大小端问题

即使前后端语言不统一

使用它也不用过多考虑字节序转换的问题

消息加密解密

知识点一 什么是消息加密解密

我们在网路传输时,会把数据转换为字节数组以2进制的形式进行传输

理论上来说,如果有人截取篡改了消息,或者从前端发假消息给后端

就可能产生作弊行为

消息的加密解密 可以有效避免作弊行为的产生

加密

采用一些方式对数据进行处理后,使数据从表面上看,已经不能表达出原有的意思

别人就算获取到了你的信息,也无法知道你的内容的含义和规则

这样可以让我们的数据更加的安全,降低被篡改的可能性

解密

通过对加密过的数据采用某些方法,去还原原有数据,从而获取目标数据

这部分知识点 和我们在数据持久化四部曲中——2进制 学习的加密内容类似

其实就是在发消息时,对我们的消息2进制数据进行加密(一般只对消息体加密)

收到消息时,对2进制数据进行解密(一般只对消息体解密)

知识点二 加密是否是100%安全?

一定记住加密只是提高破解门槛,没有100%保密的数据

通过各种尝试始终是可以破解加密规则的,只是时间问题

加密只能提升一定的安全性

对于大多数情况下已经够用了,除非专门有人针对你们的产品进行破解

但是遇到这种情况 也证明你的产品已经足够成功了

知识点三 加密解密的相关名词解释

明文:待加密的报文(内容)

密文:加密后的报文(内容)

密钥:加密过程中或解密过程中输入的数据

算法:将明文和密钥相结合进行处理,生成密文的方法,叫加密算法

将密文和密钥相结合进行处理,生成明文的方法,叫解密算法

知识点四 了解加密算法分类

单向加密

将数据进行计算变成另一种固定长度的值,这种加密是不可逆的

常用算法

MD5、SHA1、SHA256等

用途:这种加密在网络传输中不会使用,主要用到其它功能当中,比如密码的单向加密

对称加密技术

使用同一个密钥,对数据镜像加密和解密(用密钥对明文加密,用密钥对密文解密)

常用算法

DES、3DES、IDEA、AES等

优点:计算量小,加密速度快、效率高

缺点:如果知道了密钥和算法,就可以进行解密

用途:网路通讯中可以使用对称加密技术,这个密钥可以是由后端下发的,每次建立通讯后都会变化的

非对称加密技术

在加密过程中,需要一对密钥,不公开的密钥称为私钥,公开的那一个密钥称为公钥

也可以称为公开密钥加密

从一对密钥中的任何一个密钥都不能计算出另一个密钥

使用一对密钥中的任何一个加密,只有另一个密钥才能解密。如果截获公钥加密数据,没有私钥也无法解密

常用算法

RSA、DSA等

优点:安全性高,即使获取到了公钥,没有私钥也无法进行解密

缺点:算法复杂,加密速度较慢

用途:对安全性要求较高的场景,并且可以接受较慢的加密速度的需求可以使用非对称加密技术

以后在对接一些支付SDK时经常会看到平台提供的就是非对称加密技术

总结

关于这些加密算法有很多的别人写好的第三发加密算法库

可以直接获取用于在程序中对数据进行加密

也可以自己基于加密算法原理来设计自己的规则

知识点五 用简单的异或加密感受加密的作用

//异或加密特点
//密钥为一个整数
//明文 异或 密钥 得到 密文
//密文 异或 密钥 得到 明文

TestMsg msg = new TestMsg();
msg.ListInt.Add(1);
msg.TestBool = false;
msg.TestD = 5.5;
msg.TestInt32 = 99;
msg.TestMap.Add(1, "唐老狮");
msg.TestMsg2 = new TestMsg2();
msg.TestMsg2.TestInt32 = 88;
msg.TestMsg3 = new TestMsg.Types.TestMsg3();
msg.TestMsg3.TestInt32 = 66;

msg.TestHeart = new GameSystemTest.HeartMsg();
msg.TestHeart.Time = 7777;

byte[] bytes = NetTool.GetProtoBytes(msg);
//异或加密算法
//密钥声明
byte s = 55;
//异或加密
for (int i = 0; i < bytes.Length; i++)
    bytes[i] ^= s;

//异或解密
for (int i = 0; i < bytes.Length; i++)
    bytes[i] ^= s;

TestMsg msg2 = NetTool.GetProtoMsg<TestMsg>(bytes);
print(msg2.TestMsg3.TestInt32);

总结

有各种各样的加密算法可以应用在网络通讯的消息加密中

由于加密算法完全是可以单独开一门课来讲解的内容

所以我们在这里只做了解

我们只要知道加密对于我们意义即可

当需要用到时,再去学习对应的加密算法也是可以的

第9章: 实践小项目:完善网络通讯

实践小项目

问题1:解耦接收网络数据

我们上文里面曾经实现了网络层客户端和用户端的设计,但是当接收消息的时候,我们有一个麻烦,就是每次都得根据ID来进行switch case然后书写处理逻辑。

这种需要不断修改框架的方式是不太符合规范的,我们可以考虑将这些步骤解耦到外部类里。

为此,我们专门设计一个消息处理类,来处理信息时的调用。

解决
NetAsyncMgr(异步网络管理类)

代码太长,仅演示关键部分

private Queue<BaseHandler> receiveQueue = new Queue<BaseHandler>();
//消息池对象 用于快速获取消息和消息处理类对象
private MsgPool msgPool = new MsgPool();

    void Update()
    {
        if (receiveQueue.Count > 0)
        {
            //目标二:不要每次新加了消息 就在这里去处理对应消息的逻辑
            //更加自动化的去处理他们 并且不要在网络层这来处理
            //通过消息处理者基类对象 调用处理方法 以后不管添加多少个消息
            //这都不用修改了
            receiveQueue.Dequeue().MsgHandle();
        }
    }


//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(int receiveNum)
{
    int msgID = 0;
    int msgLength = 0;
    int nowIndex = 0;

    cacheNum += receiveNum;

    while (true)
    {
        //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
        msgLength = -1;
        //处理解析一条消息
        if (cacheNum - nowIndex >= 8)
        {
            //解析ID
            msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
            nowIndex += 4;
            //解析长度
            msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
            nowIndex += 4;
        }

        if (cacheNum - nowIndex >= msgLength && msgLength != -1)
        {
            //解析消息体
            //BaseMsg baseMsg = null;
            //BaseHandler handler = null;
            ////目标一:不需要每次手动的去添加代码
            ////添加了消息后 根据这个ID 就能够自动去根据ID得到对应的消息类 来进行反序列化
            ////要更加的自动化
            //switch (msgID)
            //{
            //    case 1001:
            //        baseMsg = new PlayerMsg();
            //        baseMsg.Reading(cacheBytes, nowIndex);

            //        handler = new PlayerMsgHandler();
            //        handler.message = baseMsg;
            //        break;

            //}
            //if (handler != null)
            //    receiveQueue.Enqueue(handler);

            //得到一个指定ID的消息类对象 只不过是用父类装子类
            BaseMsg baseMsg = msgPool.GetMessage(msgID);
            if (baseMsg != null)
            {
                //反序列化
                baseMsg.Reading(cacheBytes, nowIndex);
                //得到一个消息处理器对象
                BaseHandler baseHandler = msgPool.GetHandler(msgID);
                baseHandler.message = baseMsg;
                //把消息处理器对象 放入队列中 稍后在Update中进行处理
                receiveQueue.Enqueue(baseHandler);
            }

            nowIndex += msgLength;
            if (nowIndex == cacheNum)
            {
                cacheNum = 0;
                break;
            }
        }
        else
        {
            if (msgLength != -1)
                nowIndex -= 8;
            //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
            Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
            cacheNum = cacheNum - nowIndex;
            break;
        }
    }

}
BaseHandler(消息处理基类)
/// <summary>
/// 消息处理器基类 主要用于处理消息的逻辑的
/// </summary>
public abstract class BaseHandler 
{
    //处理者处理哪个消息
    public BaseMsg message;

    //真正处理消息的方法
    public abstract void MsgHandle();
}

PlayerMsgHandler(消息处理实现类)

不同信息的具体处理写在重写的方法里面,固定格式就是Msg类名+aHandler

namespace GamePlayer
{
    public class PlayerMsgHandler : BaseHandler
    {
       public override void MsgHandle()
       {
          PlayerMsg msg = message as PlayerMsg;  
          //处理消息逻辑
       }
    }
}

MsgPool(信息池类)

由于在解析信息来源的时候,我们需要根据ID确定msg类型。

我们不妨专门设计一个信息池类类负责这项任务。

public class MsgPool
{
    private Dictionary<int, Type> messsages = new Dictionary<int, Type>();
    private Dictionary<int, Type> handlers = new Dictionary<int, Type>();
    public MsgPool()
    {
       Register(1001, typeof(PlayerMsg), typeof(PlayerMsgHandler));
       Register(1002, typeof(HeartMsg), typeof(HeartMsgHandler));
       Register(1003, typeof(QuitMsg), typeof(QuitMsgHandler));
    }
    private void Register(int id, Type messageType, Type handlerType)
    {
       messsages.Add(id, messageType);
       handlers.Add(id, handlerType);
    }
    public BaseMsg GetMessage(int id)
    {
       if (!messsages.ContainsKey(id))
          return null;
       return Activator.CreateInstance(messsages[id]) as BaseMsg;
    }
    public BaseHandler GetHandler(int id)
    {
       if (!handlers.ContainsKey(id))
          return null;
       return Activator.CreateInstance(handlers[id]) as BaseHandler;
    }
}

问题2:xml生成脚本

上一个问题里面,我们成功拥有了一个不需要改动的联网框架,确保了解耦,但是也带来了另一个问题:

我们实际上需要专门为没一个msg多创建一个专属的handler类,每次需要的创建量变大了,但是这个handler类和msg的命名和代码都是由高度规律和关联的。

MsgPool类在每次添加新msg和handler类时也需要添加注册代码。

因此,我们不妨干脆也让handler类与msg类一样,可以根据xml的配置类快捷生成代码文件。

解决
GenerateCSharp(编辑器脚本生成类)

仅展示新添的部分。

//生成消息类
public void GenerateMsg(XmlNodeList nodes)
{
    string idStr = "";
    string namespaceStr = "";
    string classNameStr = "";
    string fieldStr = "";
    string getBytesNumStr = "";
    string writingStr = "";
    string readingStr = "";

    foreach (XmlNode dataNode in nodes)
    {
        //消息ID
        idStr = dataNode.Attributes["id"].Value;
        //命名空间
        namespaceStr = dataNode.Attributes["namespace"].Value;
        //类名
        classNameStr = dataNode.Attributes["name"].Value;
        //读取所有字段节点
        XmlNodeList fields = dataNode.SelectNodes("field");
        //通过这个方法进行成员变量声明的拼接 返回拼接结果
        fieldStr = GetFieldStr(fields);
        //通过某个方法 对GetBytesNum函数中的字符串内容进行拼接 返回结果
        getBytesNumStr = GetGetBytesNumStr(fields);
        //通过某个方法 对Writing函数中的字符串内容进行拼接 返回结果
        writingStr = GetWritingStr(fields);
        //通过某个方法 对Reading函数中的字符串内容进行拼接 返回结果
        readingStr = GetReadingStr(fields);

        string dataStr = "using System;\r\n" +
                         "using System.Collections.Generic;\r\n" +
                         "using System.Text;\r\n" +
                         $"namespace {namespaceStr}\r\n" +
                          "{\r\n" +
                          $"\tpublic class {classNameStr} : BaseMsg\r\n" +
                          "\t{\r\n" +
                                $"{fieldStr}" +
                                "\t\tpublic override int GetBytesNum()\r\n" +
                                "\t\t{\r\n" +
                                    "\t\t\tint num = 8;\r\n" +//这个8代表的是 消息ID的4个字节 + 消息体长度的4个字节
                                    $"{getBytesNumStr}" +
                                    "\t\t\treturn num;\r\n" +
                                "\t\t}\r\n" +
                                "\t\tpublic override byte[] Writing()\r\n" +
                                "\t\t{\r\n" +
                                    "\t\t\tint index = 0;\r\n" +
                                    "\t\t\tbyte[] bytes = new byte[GetBytesNum()];\r\n" +
                                    "\t\t\tWriteInt(bytes, GetID(), ref index);\r\n" +
                                    "\t\t\tWriteInt(bytes, bytes.Length - 8, ref index);\r\n" +
                                    $"{writingStr}" +
                                    "\t\t\treturn bytes;\r\n" +
                                "\t\t}\r\n" +
                                "\t\tpublic override int Reading(byte[] bytes, int beginIndex = 0)\r\n" +
                                "\t\t{\r\n" +
                                    "\t\t\tint index = beginIndex;\r\n" +
                                    $"{readingStr}" +
                                    "\t\t\treturn index - beginIndex;\r\n" +
                                "\t\t}\r\n" +
                                "\t\tpublic override int GetID()\r\n" +
                                "\t\t{\r\n" +
                                    "\t\t\treturn " + idStr + ";\r\n" +
                                "\t\t}\r\n" +
                          "\t}\r\n" +
                          "}";

        //保存为 脚本文件
        //保存文件的路径
        string path = SAVE_PATH + namespaceStr + "/Msg/";
        //如果不存在这个文件夹 则创建
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);

        //字符串保存 存储为枚举脚本文件
        File.WriteAllText(path + classNameStr + ".cs", dataStr);


        //生成处理器脚本
        //判断消息处理器脚本是否存在 如果存在 就不要覆盖了 避免把写过的逻辑处理代码覆盖了
        //如果想要改变 那就直接把没用的删了 它就会自动生成
        if (File.Exists(path + classNameStr + "Handler.cs"))
            continue;
        string handlerStr = $"namespace {namespaceStr}\r\n" +
                            "{\r\n" +
                                $"\tpublic class {classNameStr}Handler : BaseHandler\r\n" +
                                "\t{\r\n" +
                                    "\t\tpublic override void MsgHandle()\r\n" +
                                    "\t\t{\r\n" +
                                        $"\t\t\t{classNameStr} msg = message as {classNameStr};\r\n" +
                                    "\t\t}\r\n" +
                                "\t}\r\n" +
                            "}\r\n";

        //把消息处理器类的内容保存到本地
        File.WriteAllText(path + classNameStr + "Handler.cs", handlerStr);
        Debug.Log("消息处理器类生成结束");

    }
    Debug.Log("消息生成结束");
}

 //生成消息池 主要就是ID和消息类型以及消息处理器类型的对应关系
    public void GenerateMsgPool(XmlNodeList nodes)
    {
        List<string> ids = new List<string>();
        List<string> names = new List<string>();
        List<string> nameSpaces = new List<string>();

        foreach (XmlNode dataNode in nodes)
        {
            //记录所有消息的ID
            string id = dataNode.Attributes["id"].Value;
            if (!ids.Contains(id))
                ids.Add(id);
            else
                Debug.LogError("存在相同ID的消息" + id);
            //记录所有消息的名字
            string name = dataNode.Attributes["name"].Value;
            if (!names.Contains(name))
                names.Add(name);
            else
                Debug.LogError("存在同名的消息" + name + ",建议即使在不同命名空间中也不要有同名消息");
            //记录所有消息的命名空间
            string msgNamespace = dataNode.Attributes["namespace"].Value;
            if (!nameSpaces.Contains(msgNamespace))
                nameSpaces.Add(msgNamespace);
        }

        //获取所有需要引用的命名空间 拼接好
        string nameSpacesStr = "";
        for (int i = 0; i < nameSpaces.Count; i++)
            nameSpacesStr += $"using {nameSpaces[i]};\r\n";
        //获取所有消息注册相关的内容
        string registerStr = "";
        for (int i = 0; i < ids.Count; i++)
            registerStr += $"\t\tRegister({ids[i]}, typeof({names[i]}), typeof({names[i]}Handler));\r\n";

        //消息池对应的类的字符串信息
        string msgPoolStr = "using System;\r\n" +
                            "using System.Collections.Generic;\r\n" +
                            nameSpacesStr +
                            "public class MsgPool\r\n" +
                            "{\r\n" +
                                "\tprivate Dictionary<int, Type> messsages = new Dictionary<int, Type>();\r\n" +
                                "\tprivate Dictionary<int, Type> handlers = new Dictionary<int, Type>();\r\n" +
                                "\tpublic MsgPool()\r\n" +
                                "\t{\r\n" +
                                    registerStr +
                                "\t}\r\n" +
                                "\tprivate void Register(int id, Type messageType, Type handlerType)\r\n" +
                                "\t{\r\n" +
                                    "\t\tmesssages.Add(id, messageType);\r\n" +
                                    "\t\thandlers.Add(id, handlerType);\r\n" +
                                "\t}\r\n" +
                                "\tpublic BaseMsg GetMessage(int id)\r\n" +
                                "\t{\r\n" +
                                    "\t\tif (!messsages.ContainsKey(id))\r\n" +
                                    "\t\t\treturn null;\r\n" +
                                    "\t\treturn Activator.CreateInstance(messsages[id]) as BaseMsg;\r\n" +
                                "\t}\r\n" +
                                "\tpublic BaseHandler GetHandler(int id)\r\n" +
                                "\t{\r\n" +
                                    "\t\tif (!handlers.ContainsKey(id))\r\n" +
                                    "\t\t\treturn null;\r\n" +
                                    "\t\treturn Activator.CreateInstance(handlers[id]) as BaseHandler;\r\n" +
                                "\t}\r\n" +
                            "}\r\n";

        string path = SAVE_PATH + "/Pool/";
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);
        //保存到本地
        File.WriteAllText(path + "MsgPool.cs", msgPoolStr);

        Debug.Log("消息池生成结束");
    }

问题3:断线重连

我们之前版本的双端,在面临断开连接之后,并没有检测是否是用户主动断开,如果非正常断开重连的功能。

我们接下来加上这个功能。

解决
NetAsyncMgr

仅展示部分代码。

public void Close(bool isSelf = false)
{
    if(socket != null)
    {
        QuitMsg msg = new QuitMsg();
        socket.Send(msg.Writing());
        socket.Shutdown(SocketShutdown.Both);
        socket.Disconnect(false);
        socket.Close();
        socket = null;
    }

    //不是自己主动断开连接的
    if(!isSelf)
    {
        //断线重连 弹出一个面板
    }
}

总结

本课程基本完成。

详细代码请参阅下面网站:

任务学习 - 泰课在线 -- 志存高远,稳如泰山 - 国内专业的在线学习平台|Unity3d培训|Unity教程|Unity教程 Unreal 虚幻 AR|移动开发|美术CG - Powered By EduSoho (taikr.com)