一、前言

1.UnityWebRequest描述

官方描述:

UnityWebRequest 提供了一个模块化系统,用于构成 HTTP 请求和处理 HTTP 响应。UnityWebRequest 系统的主要目标是让 Unity 游戏与 Web 浏览器后端进行交互。该系统还支持高需求功能,例如分块 HTTP 请求、流式 POST/PUT 操作以及对 HTTP 标头和动词的完全控制。

2.WWW类描述

其实5.4版本的时候就出了新的API UnityWebRequest用于替代WWW,有些较大的文件下载需要断点续传的功能(即文件下载了一部分突然中断下载,然后再次下载会直接从上次下载的地方继续下载,而不是从头重新下载)就需要使用HttpWebRequest或UnityWebRequest,在2017版本中WWW是还没有被弃用的,2018版本我没有试过,2019是已经被弃用的,使用的时候可以看到这个类被画上了绿色波浪。

二、UnityWebRequest常用方法

UnityWebRequest由三个元素组成:

  • UpLoadHandler:处理数据将数据上传到服务器的对象
  • DownLoadHandler:从服务器下载数据的对象
  • UnityWebRequest:负责与HTTP通信并管理上面两个对象。还处理 HTTP 流量控制。此对象是定义自定义标头(Head)和 URL 的位置,也是存储错误和重定向信息的位置。 

更多描述请看官网手册:https://docs.unity.cn/cn/2019.4/Manual/UnityWebRequest.html 

1.构造函数

用来创建一个与Web服务器通信的 UnityWebRequest对象。一般UnityWebRequest对象都需要设置URL、Method方法(如 GET、POST 等)和下载或上传处理器这三个基本参数,部分构造函数在new时已经进行参数赋值,一般有以下四个构造可供使用。

PS:在构造中Method方法默认为GET可以不用设置。但是 URL 和 上传/下载处理器 需要手动设置。

参数解释 

参数含义
URLurl网址信息或本地文件路径信息
method相当于方法名,只有GET, POST, PUT, HEAD四种,默认为GET,一旦调用SendWebRequest(),就无法更改
downloadHandler

下载处理器,用于接收处理服务器的响应

uploadHandler

上传处理器,设置我们要上传的数据

①UnityWebRequest()

无参构造函数,这种函数需要你手动配置请求的 URL、Method方法(如 GET、POST 等)和下载或上传处理器。

//构造
UnityWebRequest request = new UnityWebRequest();

//设置url
request.url = "https://downsc.chinaz.net/Files/DownLoad/sound1/201808/10447.wav";//设置url

//设置访问方法
request.method = UnityWebRequest.kHttpVerbGET;//或者使用request.method = "GET"; Method方法

//下载处理器
request.downloadHandler = new DownloadHandlerBuffer();

 ②UnityWebRequest(Uri uri)

使用Uri参数构造函数,使用此函数就不需要手动配置URL地址了,但是Method方法(如 GET、POST 等)、下载或上传处理器还是需要手动设置的

 //创建Uri
 Uri uri = new Uri("https://downsc.chinaz.net/Files/DownLoad/sound1/201808/10447.wav");

 //构造
 UnityWebRequest request = new UnityWebRequest(uri);

 //设置请求方法
 request.method = UnityWebRequest.kHttpVerbGET;//或者request.method = "GET";
 
 //设置下载处理器
 request.downloadHandler = new DownloadHandlerBuffer();

③UnityWebRequest(Uri uri,string method)

使用Uri、method参数构造函数,使用此函数就不需要手动配置URL和Method方法了,但是下载或上传处理器还是需要手动设置的

//创建uri
Uri uri = new Uri("https://downsc.chinaz.net/Files/DownLoad/sound1/201808/10447.wav");

//构造
UnityWebRequest request = new UnityWebRequest(uri, "GET");

//设置下载处理器
request.downloadHandler = new DownloadHandlerBuffer();

 ④UnityWebRequest(Uri,method,DownloadHandler,UploadHandler )

使用Uri、method、上传/下载参数构造函数,使用此函数就不需要手动配置这三个基本参数。

 //创建url
 Uri uri = new Uri("https://downsc.chinaz.net/Files/DownLoad/sound1/201808/10447.wav");

 //准备要上传的数据
 byte[] dataToUpload = System.Text.Encoding.UTF8.GetBytes("Hello, Server!");
 //创建一个UploadHandlerRaw来处理上传的数据
 UploadHandler uploadHandler = new UploadHandlerRaw(dataToUpload);

 //创建一个DownloadHandlerBuffer来处理服务器响应的数据
 DownloadHandler downloadHandler = new DownloadHandlerBuffer();      
 
 //构造
 UnityWebRequest request = new UnityWebRequest(uri, "GET", downloadHandler, uploadHandler);

2.常用方法:

方法作用
SendWebRequest()开始与远程服务器通信。在调用此方法之后,有必要的话UnityWebRequest将执行DNS解析,将HTTP请求发送到目标URL的远程服务器并处理服务器的响应。
Get(url)创建一个HTTP为传入URL的UnityWebRequest对象
Post(url)向Web服务器发送表单信息
Put(url)将数据上传到Web服务器
Abort()直接结束联网
Head()创建一个为传输HTTP头请求的UnityWebRequest对象
GetResponseHeader()返回一个字典,内容为在最新的HTTP响应中收到的所有响应头

 下面介绍一下一些经常用的接口。介绍之前先简单聊聊GET、POST和PUT这三种方式的区别

1.Get:一般用于向服务器获取信息,举例:后台服务器有一个接口http://127.0.0.1/Get/?studentName=张三,这个接口负责返回学生名字为张三的数据,我们在提交get的时候,服务器会接收studentName下的值,通过这个值来进行逻辑处理,使用Get我们要访问得值是暴露在浏览器中的,如果是用户名密码这样的重要信息被暴露后果将不堪设想,所以像网页中搜索栏需要条件来获取信息的功能,就可以使用Get的方法来实现。

2.Post:这种方式就是为了解决Get访问时信息暴露的危险,使用Post访问时表单中的内容不会暴露,安全性更高,一般用于网页用户登录等重要信息上

3.Put:这种方式用于将数据发送到远程的服务器。比如文件上传。

① Get方法

(1) 使用静态类创建UnityWebRequest获取Txt文本信息

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Networking;
using System;

public class AAA : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("GetDataB", "Test.txt");
    }

    #region 创建persistentDataPath文件夹
    IEnumerator GetDataB(string fileName)
    {
        //1.url地址
        string fromPath = Application.streamingAssetsPath + "/" + fileName;
        //2.创建一个UnityWebRequest类 method属性为Get
        UnityWebRequest request = UnityWebRequest.Get(fromPath);
        //3.等待响应时间,超过5秒结束
        request.timeout = 5;
        //4.发送请求信息
        yield return request.SendWebRequest(); 

        //5.判断是否下载完成
        if (request.isDone)
        {
            //6.判断是否下载错误
            if (request.isHttpError || request.isNetworkError)
                Debug.Log(request.error);
            else
                Debug.Log(request.downloadHandler.text);
        }

 #region 下载状态判断(可隐藏)
 if (request.isDone)
 {
     switch (request.result)
     {
         case UnityWebRequest.Result.InProgress:
             Debug.Log("请求还没有完成");
             break;
         case UnityWebRequest.Result.Success:

             Debug.Log("请求成功");
             config = request.downloadHandler.text;
             captureData = JsonUtility.FromJson<CaptureData>(request.downloadHandler.text);
             DataInit(); //数据初始化

             break;
         case UnityWebRequest.Result.ConnectionError:
             Debug.Log("日志含义与服务器通信失败。例如,请求无法连接或无法建立安全通道。");
             Debug.Log("Error: " + request.error);
             break;
         case UnityWebRequest.Result.ProtocolError:
             Debug.Log("服务器返回了一个错误响应。请求与服务器通信成功,但收到连接协议定义的错误。");
             break;
         case UnityWebRequest.Result.DataProcessingError:
             Debug.Log("处理数据出错。请求与服务器通信成功,但在处理接收到的数据时遇到错误。例如,数据损坏或格式不正确。");
             break;
         default:
             break;
     }

 }
 #endregion

    }

    #endregion
}

(2) 使用构造来创建(可参考上面 “构造函数” )

注意:这里为什么要用两种方式呢?因为静态类创建(UnityWebRequest.Get)的Request是自带DownloadHandler和UploadHandler的,而构造创建(new UnityWebRequest )是没有的,需要自己手动创建赋值,注意不要踩坑哦!!

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Networking;
using System;

public class AAA : MonoBehaviour
{
    void Start()
    {
        StartCoroutine("GetDataB", "Test.txt");
    }

    #region 创建persistentDataPath文件夹
    IEnumerator GetDataB(string fileName)
    {

        string fromPath = Application.streamingAssetsPath + "/" + fileName;
        Uri uri = new Uri(fromPath);
        //UnityWebRequest request = UnityWebRequest.Get(fromPath);
        UnityWebRequest request =new UnityWebRequest(uri); //使用构造
        request.timeout = 5;//等待响应时间,超过5秒结束

        /*使用构造没有DownloadHandler和UploadHandler,所以要创建赋值,这里只用到了下载,所以可以不用创建UploadHandler
         * 下面罗列了三种不同类型的DownloadHandler
         1.DownloadHandlerBuffer 读取文件存储
         2.DownloadHandlerTexture 读取图片
         3.DownloadHandlerFile 下载文件到本地
         */
        DownloadHandlerBuffer Download = new DownloadHandlerBuffer();
        request.downloadHandler = Download;

        yield return request.SendWebRequest();

        if (request.isDone)
        {
            if (request.isHttpError || request.isNetworkError)
                Debug.Log(request.error);
            else 
                Debug.Log(request.downloadHandler.text);
        }
        
    }

    #endregion

}

 ② Post方法

Post方法将一个表上传到远程的服务器,一般来说我们登陆某个网站的时候会用到这个方法,我们的账号密码会以一个表单的形式传过去。

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 网络请求测试
/// </summary>
public class ChinarWebRequest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Post());
    }
    /// <summary>
    /// 开启一个协程,发送请求
    /// </summary>
    /// <returns></returns>
    IEnumerator Post()
    {
        WWWForm form = new WWWForm();
        //键值对
        form.AddField("key",  "value");
        form.AddField("name", "Chinar");
        //请求链接,并将form对象发送到远程服务器
        UnityWebRequest webRequest = UnityWebRequest.Post("http://www.baidu.com", form);

        yield return webRequest.SendWebRequest();
        if (webRequest.isHttpError || webRequest.isNetworkError)
        {
            Debug.Log(webRequest.error);
        }
        else
        {
            Debug.Log("发送成功"); 
        }
    }
}

③ Put方法

Put方法将数据发送到远程的服务器。例如:文件上传

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 网络请求测试
/// </summary>
public class ChinarWebRequest : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Upload());
    }
    
    /// <summary>
    /// 开启协程
    /// </summary>
    /// <returns></returns>
    IEnumerator Upload()
    {
        byte[] myData = System.Text.Encoding.UTF8.GetBytes("Chinar的测试数据");
        using (UnityWebRequest uwr = UnityWebRequest.Put("http://www.baidu.com", myData))
        {
            yield return uwr.SendWebRequest();

            if (uwr.isNetworkError || uwr.isHttpError)
            {
                Debug.Log(uwr.error);
            }
            else
            {
                Debug.Log("上传成功!");
            }
        }
    }
}

④ Abort方法

Abort方法会尽快结束联网,可以随时调用此方法。
如果 UnityWebRequest尚未完成,那么 UnityWebRequest将尽快停止上传或下载数据。

中止的 UnityWebRequests被认为遇到了系统错误。isNetworkError或isHttpError属性将返回true,error属性将为“User Aborted”。

⑤ Head方法 

Head方法与Get方法用法一致,都是传入一个URL。

关于head这个方法解释推荐文章:https://www.jianshu.com/p/49ebc4a78474

根据这个文章特点的截图 我简单聊聊Head作用 :

 1.第一条就是只请求资源头部,网页的body主体是不显示的。大家可以先用get请求一个www.baidu.com查看

接下来我们使用Head来请求

2.检查超链接有效性,当链接出现问题时会返回一个错误码,上方链接文章有对应错误码的中文描述。使用get、post也可以测试有效性,但是这些方式访问成功会返回body主体,所以使用head访问可以节省网络资源 

3.网页是否被修改,举个例子,我们将一个静态网页使用MD5加密后存入数据库,在head中加入这个html被加密后的数据进行数据库访问判断,如果这个静态网页被修改访问就会出现问题。不过这是很久之前的版本用的方法,现在基本使用的是Token进行验证

4.第四点就是头包含的这些信息了,例如我们需要获取安全验证信息来进心判断操作,如果使用GET、PUST等方式访问,就会连带body主体一起获取,大大浪费资源。

下面是UnityWebRequest.Head的请求方式:一般与下面的GetResponseHeader方法配合使用获取文件大小的,后面断电续传中会有用到。

    /// <summary>
    /// 开启一个协程,发送请求
    /// </summary>
    /// <returns></returns>
    IEnumerator SendRequest()
    {
        UnityWebRequest uwr = UnityWebRequest.Head("www.baidu.com");       //创建UnityWebRequest对象
        yield return uwr.SendWebRequest();                                 //等待返回请求的信息
        if (uwr.isHttpError || uwr.isNetworkError)                         //如果请求失败,或是 网络错误
        {
            Debug.Log(uwr.error); //打印错误原因
        }
        else //请求成功
        {
            Debug.Log("Head:请求成功");
        }
    }

⑥ GetResponseHeader方法

GetResponseHeader方法可以用来获取请求文件的长度 传入参数 "Content-Length"字符串,表示获取文件内容长度。

 IEnumerator SendRequest()
    {
        UnityWebRequest uwr = UnityWebRequest.Head("www.baidu.com"); //创建UnityWebRequest对象
        yield return uwr.SendWebRequest();                                 //等待返回请求的信息
        if (uwr.isHttpError || uwr.isNetworkError)                         //如果其 请求失败,或是 网络错误
        {
            Debug.Log(uwr.error); //打印错误原因
        }
        else //请求成功
        {
            long totalLength = long.Parse(uwr.GetResponseHeader("Content-Length")); //首先拿到文件的全部长度
            Debug.Log($"totalLength:{totalLength}" );//打印文件长度
        }
    }

三、常用属性

属性类型含义
timeoutint等待时间(秒)超过此数值是 UnityWebReqest的尝试连接将终止
isHttpErrorboolHTTP响应出现出现错误
isNetworkErrorbool系统出现错误
errorstring描述 UnityWebRequest对象在处理HTTP请求或响应时遇到的任何系统错误
downloadProgressfloat表示从服务器下载数据的进度
uploadProgressfloat表示从服务器上传数据的进度
isDonebool是否完成与远程服务器的通信
SendWebRequestUnityWebRequestAsyncOperation发送信息访问

四、案例 

1.断点续传

断点续传是一种在文件下载中断后,可以从中断的位置继续下载而不必重新开始的技术。这通常需要以下几个步骤:

  • 获取文件总长度:在下载之前,获取文件的总大小。
  • 检查已下载部分:如果之前下载了一部分文件,记录已经下载的大小。
  • 从断点继续下载:根据已经下载的部分,从文件的特定位置继续下载
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;


//url访问参数
[Serializable]
public class UrlParameter
{
    public string url;
    public string saveFilePath = "书写格式:xxx文件夹/xxx.后缀名";
    public string Token="";//ToKen令牌 如果发送信息请求出现“ArgumentNullException: Value cannot be null.” 需要设置Token访问令牌 

}

public class ChinarBreakpointRenewal : MonoBehaviour
{
    private bool _isStop;           //是否暂停

    public UrlParameter urlParameter; //url访问参数

    public Slider ProgressBar;      //进度条
    public Text SliderValue;        //滑动条值
    public Button startBtn;         //开始按钮
    public Button pauseBtn;         //暂停按钮


    /// <summary>
    /// 初始化UI界面及给按钮绑定方法
    /// </summary>
    void Start()
    {

        if (urlParameter == null || urlParameter.url == "")
        {
            Debug.Log("使用案例测试");
            urlParameter = new UrlParameter()
            {
                url = "https://downsc.chinaz.net/Files/DownLoad/sound1/201808/10447.wav",
                saveFilePath = "wav/test.wav"
            };
        }

        //初始化进度条和文本框
        ProgressBar.value = 0;
        SliderValue.text = "0.0%";

        //开始、暂停按钮事件监听
        startBtn.onClick.AddListener(OnClickStartDownload);
        pauseBtn.onClick.AddListener(OnClickStop);
    }


    //开始下载按钮监听事件
    public void OnClickStartDownload()
    {
        //开启协程 *注意真机上要用Application.persistentDataPath路径*
        StartCoroutine(DownloadFile(urlParameter, CallBack));
    }


    /// <summary>
    /// 协程:下载文件
    /// </summary>
    /// <param name="urlParameter">请求的url数据</param>
    /// <param name="callBack">下载完成的回调函数</param>
    /// <returns></returns>
    IEnumerator DownloadFile(UrlParameter urlParameter, Action callBack)
    {
        UnityWebRequest huwr = UnityWebRequest.Head(urlParameter.url);
        Debug.Log("开始下载");
        //huwr.timeout = 20;//响应时间:超出则报错响应超时(单位:秒)
        if (urlParameter.Token != "")
            huwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);

        //不使用yield return 循环判断当前下载进度
        huwr.SendWebRequest();
        while (!huwr.isDone) 
        {
            //
            Debug.Log("下载进度: " + huwr.downloadProgress);
            yield return null;
        }

        if (huwr.isNetworkError || huwr.isHttpError)
        {
            Debug.Log(huwr.error);
        }
        else
        {
            long totalLength = long.Parse(huwr.GetResponseHeader("Content-Length"));// 获取文件的全部长度
            Debug.Log("获取下载文件总长度: " + totalLength);


            string dirPath = Path.GetDirectoryName($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}");//获取文件的上一级目录

            if (!Directory.Exists(dirPath))
            {
                Directory.CreateDirectory(dirPath);// 不存在文件则创建
            }


            /*作用:创建一个文件流,指定路径为filePath,模式为打开或创建,访问为写入
             * 使用using(){}方法原因: 当同一个cs引用了不同的命名空间,但这些命名控件都包括了一个相同名字的类型的时候,可以使用using关键字来创建别名,这样会使代码更简洁。注意:并不是说两个名字重复,给其中一个用了别名,另外一个就不需要用别名了,如果两个都要使用,则两个都需要用using来定义别名的
             * using(类){} 括号中的类必须是继承了IDisposable接口才能使用否则报错
             * 这里没有出现不同命名空间出现相同名字的类属性可以不用using(){}
             */
            using (FileStream fs = new FileStream($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}", FileMode.OpenOrCreate, FileAccess.Write))
            {
                long nowFileLength = fs.Length;//当前文件长度,即断点前已下载的文件长度
                Debug.Log("已写入本地的文件长度: " + nowFileLength);

                //初始最大下载量为4MB 后面逐步增加 (主要防止下载量太大导致unity崩溃,这个值可根据电脑性能自动设置)
                long chunkSize = 4 * 1024 * 1024;

                /*————————————第四部分——————————————————*/

                while (nowFileLength < totalLength)//只要文件未下载完成,就继续下载
                {
                    if (_isStop)// 如果暂停下载,跳出循环
                    {
                        Debug.Log("下载已暂停");
                        break;
                    }

                    /*使用Seek方法 可以随机读写文件
                   * Seek()  ----------有两个参数 第一参数规定文件指针以字节为单位移动的距离。第二个参数规定开始计算的位置
                   * 第二个参数SeekOrigin 有三个值:Begin  Current   End
                   * fs.Seek(8,SeekOrigin.Begin);表示 将文件指针从开头位置移动到文件的第8个字节
                   * fs.Seek(8,SeekOrigin.Current);表示 将文件指针从当前位置移动到文件的第8个字节
                   * fs.Seek(8,SeekOrigin.End);表示 将文件指针从最后位置移动到文件的第8个字节
                   */
                    fs.Seek(nowFileLength, SeekOrigin.Begin);// 将文件指针移动到当前已下载的字节位置

                    // 调整chunkSize以尽量利用可用内存和带宽
                    chunkSize = Math.Min(chunkSize, totalLength - nowFileLength);

                    UnityWebRequest uwr = UnityWebRequest.Get(urlParameter.url);
                    //uwr.timeout = 1;//响应时间:超出则报错响应超时(单位:秒)
                    uwr.SetRequestHeader("Range", "bytes=" + nowFileLength + "-" + (nowFileLength + chunkSize - 1));// 修改请求头以指定下载范围
                    if (urlParameter.Token != "")
                        uwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);
                    yield return uwr.SendWebRequest();

                    if (uwr.isNetworkError || uwr.isHttpError)
                    {
                        Debug.Log(uwr.error);
                        break;// 如果出错,跳出循环
                    }
                    else
                    {
                        byte[] data = uwr.downloadHandler.data;//获取下载的数据
                        if (data != null && data.Length > 0)
                        {
                            fs.Write(data, 0, data.Length); //将数据写入文件
                            nowFileLength += data.Length;   //更新已下载的文件长度

                            ProgressBar.value = (float)nowFileLength / totalLength; //更新进度条
                            SliderValue.text = Math.Floor((float)nowFileLength / totalLength * 100) + "%";

                            //在处理较大数据块时,进行帧让步
                            yield return null;
                        }

                        if (nowFileLength >= totalLength)
                        {
                            ProgressBar.value = 1;
                            SliderValue.text = "100%";
                            callBack?.Invoke();
                            break;
                        }
                    }

                    // 调整chunkSize,逐步增加以找到稳定的最大值
                    chunkSize = Math.Min(chunkSize * 2, 16 * 1024 * 1024); // 最大增加到16MB
                }

                /*————————————第四部分——————————————————*/


            }
        }
    }



    /// <summary>
    /// 下载完成后的回调函数
    /// </summary>
    void CallBack()
    {
        Debug.Log("下载完成");
    }

    /// <summary>
    /// 暂停下载
    /// </summary>
    public void OnClickStop()
    {
        if (_isStop)
        {
            pauseBtn.GetComponentInChildren<Text>().text = "暂停下载";
            Debug.Log("继续下载");
            _isStop = !_isStop;
            OnClickStartDownload();
        }
        else
        {
            pauseBtn.GetComponentInChildren<Text>().text = "继续下载";
            Debug.Log("暂停下载");
            _isStop = !_isStop;
        }
    }
}

 注意

我使用此方法在gitlab上下载大型文件(约2.2G),在请求Head头数据时会卡在while(!huwr.isDone) 中并且打印的下载进度一直为0,经过长时间等待或者先使用浏览器下载成功后,才可以获取头文件。

其实这并不是卡住而是GitLab 的服务器可能在处理初次下载大文件时,采取了一些优化措施。

  1. 延迟提供 Content-Length 头信息:初次请求时,服务器可能需要较长时间来计算或准备完整的响应,特别是大文件。只有在某些条件满足后(如文件被完全加载或缓存)才会返回 Content-Length 头信息
  2. 服务器缓存机制:服务器可能在首次下载后将文件缓存起来,这样在后续请求中(如通过浏览器或 Postman),可以更快速地响应并返回完整的头信息。
  3. 下载行为触发完整响应:当通过浏览器实际下载文件时,服务器可能会执行某些额外的处理或优化(如生成缓存、优化下载路径),导致在下一次请求时能更快地返回 Content-Length 头。

如果以上有什么不对,或者可以优化的地方欢迎大家指正,感谢。

五、问题解答

非常感谢 Asunaの楠世 提出的问题,后面我在检查和测试时的确发现了相关问题并对上方断点续传脚本进行了修改。

1.没有等uwr.SendWebRequest(); 返回就判断有没有error了?

在DownloadFile协程方法中使用uwr.SendWebRequest();的确忘记添加yeild return了,这样会导致程序没有等待数据完成就执行后面的if语句,如果网络数据请求出现问题时是无法进行判断的,还会导致While死循环。

下面我做了一个测试,设置响应时间为1秒并且删除yield return会出现无法进行error错误判断而且unity因为死循环陷入了崩溃。

新改版的脚本中访问头文件添加了一个使用while循环来代替yeild return判断是否响应成功的方法。

2.downloadHandler应该不是实时更新的吧?

当我们执行UnityWebRequest.SendWebRequest();时,它会启动网络请求,这是个异步操作,而且网络请求是在后台进行的,不会立即完成,也不会影响Unity主线程的操作。

这个后台网络请求依赖操作系统底层的网络堆栈来完成实际的网络通信,不同平台(如 Windows、macOS、iOS、Android)会有不同的底层实现,而Unity会在每一帧检查请求的状态,并在请求完成时调用回调或继续执行等待该请求完成的代码。当我们的请求完成后获取的数据就会存储在downloadHandler.data中。

 3.不知道这个while的意义在哪,都是等到完成之后一次性写入?

我后面改版的脚本对while的操作意义更加明确,不过为了更加清楚的理解他的作用我下面会对DownloadFile协程方法进行讲解:

第一部分我先获取我们访问的网络资源的总长度大小。

 IEnumerator DownloadFile(UrlParameter urlParameter, Action callBack)
 {
     /*————————————第一部分——————————————————*/

     UnityWebRequest huwr = UnityWebRequest.Head(urlParameter.url);
     Debug.Log("开始下载");
     //huwr.timeout = 20;//响应时间:超出则报错响应超时(单位:秒)
     if (urlParameter.Token != "")
         huwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);

     //不使用yield return 循环判断当前下载进度
     huwr.SendWebRequest();
     while (!huwr.isDone) 
     {
         // 输出当前的上传和下载进度
         Debug.Log("下载进度: " + huwr.downloadProgress);
         yield return null;
     }

     if (huwr.isNetworkError || huwr.isHttpError)
     {
         Debug.Log(huwr.error);
     }
     else
     {
         long totalLength = long.Parse(huwr.GetResponseHeader("Content-Length"));// 获取文件的全部长度
         Debug.Log("获取下载文件总长度: " + totalLength);
         /*————————————第一部分——————————————————*/

     }
 }

第二部分设置保存到本地的文件夹是否存在,不存在就创建文件夹,用于后面的存储网络下载的文件数据

  IEnumerator DownloadFile(UrlParameter urlParameter, Action callBack)
  {

      UnityWebRequest huwr = UnityWebRequest.Head(urlParameter.url);
      Debug.Log("开始下载");
      //huwr.timeout = 20;//响应时间:超出则报错响应超时(单位:秒)
      if (urlParameter.Token != "")
      huwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);

      //不使用yield return 循环判断当前下载进度
      huwr.SendWebRequest();
      while (!huwr.isDone) 
      {
          // 输出当前的上传和下载进度
          Debug.Log("下载进度: " + huwr.downloadProgress);
          yield return null;
      }

      if (huwr.isNetworkError || huwr.isHttpError)
      {
          Debug.Log(huwr.error);
      }
      else
      {
          long totalLength = long.Parse(huwr.GetResponseHeader("Content-Length"));// 获取文件的全部长度
          Debug.Log("获取下载文件总长度: " + totalLength);


          /*————————————第二部分——————————————————*/
          string dirPath = Path.GetDirectoryName($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}");//获取文件的上一级目录

          if (!Directory.Exists(dirPath))
          {
              Directory.CreateDirectory(dirPath);// 不存在文件则创建
          }
          /*————————————第二部分——————————————————*/

      }
  }

第三部分 获取本地已经加载写入到本地的网络数据长度,并设置每次下载网络数据的总量

IEnumerator DownloadFile(UrlParameter urlParameter, Action callBack)
{

    UnityWebRequest huwr = UnityWebRequest.Head(urlParameter.url);
    Debug.Log("开始下载");
    //huwr.timeout = 20;//响应时间:超出则报错响应超时(单位:秒)
    if (urlParameter.Token != "")
    huwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);

    //不使用yield return 循环判断当前下载进度
    huwr.SendWebRequest();
    while (!huwr.isDone) 
    {
        // 输出当前的上传和下载进度
        Debug.Log("下载进度: " + huwr.downloadProgress);
        yield return null;
    }

    if (huwr.isNetworkError || huwr.isHttpError)
    {
        Debug.Log(huwr.error);
    }
    else
    {
        long totalLength = long.Parse(huwr.GetResponseHeader("Content-Length"));// 获取文件的全部长度
        Debug.Log("获取下载文件总长度: " + totalLength);


        string dirPath = Path.GetDirectoryName($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}");//获取文件的上一级目录

        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);// 不存在文件则创建
        }

        /*————————————第三部分——————————————————*/

        /*作用:创建一个文件流,指定路径为filePath,模式为打开或创建,访问为写入
         * 使用using(){}方法原因: 当同一个cs引用了不同的命名空间,但这些命名控件都包括了一个相同名字的类型的时候,可以使用using关键字来创建别名,这样会使代码更简洁。注意:并不是说两个名字重复,给其中一个用了别名,另外一个就不需要用别名了,如果两个都要使用,则两个都需要用using来定义别名的
         * using(类){} 括号中的类必须是继承了IDisposable接口才能使用否则报错
         * 这里没有出现不同命名空间出现相同名字的类属性可以不用using(){}
         */
        using (FileStream fs = new FileStream($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}", FileMode.OpenOrCreate, FileAccess.Write))
        {
            long nowFileLength = fs.Length;//当前文件长度,即断点前已下载的文件长度
            Debug.Log("已写入本地的文件长度: " + nowFileLength);

            //初始最大下载量为4MB 后面逐步增加 (主要防止下载量太大导致unity崩溃,这个值可根据电脑性能自动设置)
            long chunkSize = 4 * 1024 * 1024;
        }

        /*————————————第三部分——————————————————*/

    }
}

第四部分就是在while循环中判断只要写入到本地数据量小于远程网络数据量就持续执行,每次获取的网络数据量都是从中断点进行获取,直到所有的数据获取并写入到本地才算完成

IEnumerator DownloadFile(UrlParameter urlParameter, Action callBack)
{

    UnityWebRequest huwr = UnityWebRequest.Head(urlParameter.url);
    Debug.Log("开始下载");
    //huwr.timeout = 20;//响应时间:超出则报错响应超时(单位:秒)
    if (urlParameter.Token != "")
    huwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);

    //不使用yield return 循环判断当前下载进度
    huwr.SendWebRequest();
    while (!huwr.isDone) 
    {
        // 输出当前的上传和下载进度
        Debug.Log("下载进度: " + huwr.downloadProgress);
        yield return null;
    }

    if (huwr.isNetworkError || huwr.isHttpError)
    {
        Debug.Log(huwr.error);
    }
    else
    {
        long totalLength = long.Parse(huwr.GetResponseHeader("Content-Length"));// 获取文件的全部长度
        Debug.Log("获取下载文件总长度: " + totalLength);


        string dirPath = Path.GetDirectoryName($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}");//获取文件的上一级目录

        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);// 不存在文件则创建
        }


        /*作用:创建一个文件流,指定路径为filePath,模式为打开或创建,访问为写入
         * 使用using(){}方法原因: 当同一个cs引用了不同的命名空间,但这些命名控件都包括了一个相同名字的类型的时候,可以使用using关键字来创建别名,这样会使代码更简洁。注意:并不是说两个名字重复,给其中一个用了别名,另外一个就不需要用别名了,如果两个都要使用,则两个都需要用using来定义别名的
         * using(类){} 括号中的类必须是继承了IDisposable接口才能使用否则报错
         * 这里没有出现不同命名空间出现相同名字的类属性可以不用using(){}
         */
        using (FileStream fs = new FileStream($"{Application.streamingAssetsPath}/{urlParameter.saveFilePath}", FileMode.OpenOrCreate, FileAccess.Write))
        {
            long nowFileLength = fs.Length;//当前文件长度,即断点前已下载的文件长度
            Debug.Log("已写入本地的文件长度: " + nowFileLength);

            //初始最大下载量为4MB 后面逐步增加 (主要防止下载量太大导致unity崩溃,这个值可根据电脑性能自动设置)
            long chunkSize = 4 * 1024 * 1024;

            /*————————————第四部分——————————————————*/

            while (nowFileLength < totalLength)//只要文件未下载完成,就继续下载
            {
                if (_isStop)// 如果暂停下载,跳出循环
                {
                    Debug.Log("下载已暂停");
                    break;
                }

                /*使用Seek方法 可以随机读写文件
               * Seek()  ----------有两个参数 第一参数规定文件指针以字节为单位移动的距离。第二个参数规定开始计算的位置
               * 第二个参数SeekOrigin 有三个值:Begin  Current   End
               * fs.Seek(8,SeekOrigin.Begin);表示 将文件指针从开头位置移动到文件的第8个字节
               * fs.Seek(8,SeekOrigin.Current);表示 将文件指针从当前位置移动到文件的第8个字节
               * fs.Seek(8,SeekOrigin.End);表示 将文件指针从最后位置移动到文件的第8个字节
               */
                fs.Seek(nowFileLength, SeekOrigin.Begin);// 将文件指针移动到当前已下载的字节位置

                // 调整chunkSize以尽量利用可用内存和带宽
                chunkSize = Math.Min(chunkSize, totalLength - nowFileLength);

                UnityWebRequest uwr = UnityWebRequest.Get(urlParameter.url);
                //uwr.timeout = 1;//响应时间:超出则报错响应超时(单位:秒)
                uwr.SetRequestHeader("Range", "bytes=" + nowFileLength + "-" + (nowFileLength + chunkSize - 1));// 修改请求头以指定下载范围
                uwr.SetRequestHeader("PRIVATE-TOKEN", urlParameter.Token);
                yield return uwr.SendWebRequest();

                if (uwr.isNetworkError || uwr.isHttpError)
                {
                    Debug.Log(uwr.error);
                    break;// 如果出错,跳出循环
                }
                else
                {
                    byte[] data = uwr.downloadHandler.data;//获取下载的数据
                    if (data != null && data.Length > 0)
                    {
                        fs.Write(data, 0, data.Length); //将数据写入文件
                        nowFileLength += data.Length;   //更新已下载的文件长度

                        ProgressBar.value = (float)nowFileLength / totalLength; //更新进度条
                        SliderValue.text = Math.Floor((float)nowFileLength / totalLength * 100) + "%";

                        //在处理较大数据块时,进行帧让步
                        yield return null;
                    }

                    if (nowFileLength >= totalLength)
                    {
                        ProgressBar.value = 1;
                        SliderValue.text = "100%";
                        callBack?.Invoke();
                        break;
                    }
                }

                // 调整chunkSize,逐步增加以找到稳定的最大值
                chunkSize = Math.Min(chunkSize * 2, 16 * 1024 * 1024); // 最大增加到16MB
            }

            /*————————————第四部分——————————————————*/


        }
    }
}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐