美文网首页网络编程UnityUnity
Unity发送HTTP请求和文件下载

Unity发送HTTP请求和文件下载

作者: Aodota | 来源:发表于2016-12-07 21:42 被阅读5806次

    Unity发送HTTP请求和文件下载

    本类库封装基于ulua框架LuaFramework

    1. 使用的类库

    HttpWebRequest
    HttpWebResponse
    LuaFramework

    2. 实现了哪些功能

    • 发送GET、POST请求
    • HTTP请求异步化
    • 支持Lua回调
    • 支持Host,采用Proxy的实现方式
    • 支持将HTTP请求内容保存为文件
    • HTTP下载文件,支持断电续传

    3. HTTP实现思路

    • HTTPRequest 发起HTTP请求,异步回调返回HTTPResponse
      HTTPRequest源码
    using System;
    using System.Net;
    using System.IO;
    using System.Text;
    using System.Collections.Generic;
    using LuaFramework;
    using UnityEngine;
    /// <summary>
    /// Http请求
    /// </summary>
    public class HTTPRequest
    {
        private string url;
        private int timeout;
        private Action<HTTPResponse> callback;
        private HttpWebRequest request;
        private string method;
        private string contentType;
        private KeyValuePair<string, int> proxy;
        protected int range = -1;
        // post内容
        private StringBuilder postBuilder;
        /// <summary>
        /// 错误代码
        /// </summary>
        public const int ERR_EXCEPTION = -1;
        /// <summary>
        /// 构造函数, 构造GET请求
        /// </summary>
        /// <param name="url">url地址</param>
        /// <param name="timeout">超时时间</param>
        /// <param name="callback">回调函数</param>
        public HTTPRequest (string url, string method, int timeout, Action<HTTPResponse> callback)
        {
            this.url = url;
            this.timeout = timeout;
            this.callback = callback;
            this.method = method.ToUpper();
        }
        /// <summary>
        /// 设置Post内容
        /// </summary>
        /// <param name="data">内容</param>
        public void SetPostData(string data) {
            if (postBuilder == null) {
                postBuilder = new StringBuilder (data.Length);
            }
            if (postBuilder.Length > 0) {
                postBuilder.Append ("&");
            }
            postBuilder.Append (data);
        }
        /// <summary>
        /// 添加Post内容
        /// </summary>
        /// <param name="key">key值</param>
        /// <param name="value">value值</param>
        public void AddPostData(string key, string value) {
            if (postBuilder == null) {
                postBuilder = new StringBuilder ();
            }
            if (postBuilder.Length > 0) {
                postBuilder.Append ("&");
            }
            postBuilder.Append (key).Append ("=").Append (UrlEncode (value));
        }
        /// <summary>
        /// 设置代理
        /// </summary>
        /// <param name="ip">ip地址</param>
        /// <param name="port">端口号</param>
        public void SetProxy(string ip, int port) {
            this.proxy = new KeyValuePair<string, int> (ip, port);
        }
        /// <summary>
        /// 设置ContentType
        /// </summary>
        /// <value>ContentType value</value>
        public string ContentType {
            set {
                this.contentType = value;
            }
        }
        /// <summary>
        /// 发动请求
        /// </summary>
        public void Start() {
            Debug.Log ("Handle Http Request Start");
            this.request = WebRequest.Create (url) as HttpWebRequest;
            this.request.Timeout = timeout;
            this.request.Method = method;
            if (this.proxy.Key != null) {
                this.request.Proxy = new WebProxy(this.proxy.Key, this.proxy.Value);
            }
            if (this.contentType != null) {
                this.request.ContentType = this.contentType;
            }
            if (this.range != -1) {
                this.request.AddRange (this.range);
            }
            // POST写POST内容
            if (method.Equals ("POST")) {
                WritePostData ();
            }
            try {
                AsyncCallback callback = new AsyncCallback (OnResponse);
                this.request.BeginGetResponse (callback, null);
            } catch (Exception e) {
                CallBack (ERR_EXCEPTION, e.Message);
                if (request != null) {
                    request.Abort ();
                }
            }
        }
        /// <summary>
        /// 处理读取Response
        /// </summary>
        /// <param name="result">异步回到result</param>
        protected void OnResponse(IAsyncResult result) {
            //Debug.Log ("Handle Http Response");
            HttpWebResponse response = null;
            try {
                // 获取Response
                response = request.EndGetResponse (result) as HttpWebResponse;
                if (response.StatusCode == HttpStatusCode.OK) {
                    if ("HEAD".Equals(method)) {
                        // HEAD请求
                        long contentLength = response.ContentLength;
                        CallBack((int)response.StatusCode, contentLength + "");
                        return;
                    }
                    // 读取请求内容
                    Stream responseStream = response.GetResponseStream();
                    byte[] buff = new byte[2048];
                    MemoryStream ms = new MemoryStream();
                    int len = -1;
                    while ((len = responseStream.Read(buff, 0, buff.Length)) > 0) {
                        ms.Write(buff, 0, len);
                    }
                    // 清理操作
                    responseStream.Close();
                    response.Close();
                    request.Abort();
                    // 调用回调
                    CallBack ((int)response.StatusCode, ms.ToArray());
                } else {
                    CallBack ((int)response.StatusCode, "");
                }
            } catch (Exception e) {
                CallBack (ERR_EXCEPTION, e.Message);
                if (request != null) {
                    request.Abort ();
                }
                if (response != null) {
                    response.Close ();
                }
            }
        }
        /// <summary>
        /// 回调
        /// </summary>
        /// <param name="code">编码</param>
        /// <param name="content">内容</param>
        private void CallBack(int code, string content) {
            Debug.LogFormat ("Handle Http Callback, code:{0}", code);
            if (callback != null) {
                HTTPResponse response = new HTTPResponse ();
                response.StatusCode = code;
                response.Error = content;
                callback (response);
            }
        }
        /// <summary>
        /// 回调
        /// </summary>
        /// <param name="code">编码</param>
        /// <param name="content">内容</param>
        private void CallBack(int code, byte[] content) {
            Debug.LogFormat ("Handle Http Callback, code:{0}", code);
            if (callback != null) {
                HTTPResponse response = new HTTPResponse (content);
                response.StatusCode = code;
                callback (response);
            }
        }
        /// <summary>
        /// 写Post内容
        /// </summary>
        private void WritePostData() {
            if (null == postBuilder || postBuilder.Length <= 0) {
                return;
            }
            byte[] bytes = Encoding.UTF8.GetBytes (postBuilder.ToString ());
            Stream stream = request.GetRequestStream ();
            stream.Write (bytes, 0, bytes.Length);
            stream.Close ();
        }
        /// <summary>
        /// URLEncode
        /// </summary>
        /// <returns>encode value</returns>
        /// <param name="value">要encode的值</param>
        private string UrlEncode(string value) {
            StringBuilder sb = new StringBuilder();
            byte[] byStr = System.Text.Encoding.UTF8.GetBytes(value);
            for (int i = 0; i < byStr.Length; i++)
            {
                sb.Append(@"%" + Convert.ToString(byStr[i], 16));
            }
            return (sb.ToString());
        }
    }
    

    HTTPResponse源码

    using System;
    using System.IO;
    using System.Text;
    using UnityEngine;
    using LuaFramework;
    /// <summary>
    /// HTTP返回内容
    /// </summary>
    public class HTTPResponse
    {
        // 状态码
        private int statusCode;
        // 响应字节
        private byte[] responseBytes;
        // 错误内容
        private string error;
        /// <summary>
        /// 默认构造函数
        /// </summary>
        public HTTPResponse ()
        {
        }
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="content">响应内容</param>
        public HTTPResponse (byte[] content)
        {
            this.responseBytes = content;
        }
        /// <summary>
        /// 获取响应内容
        /// </summary>
        /// <returns>响应文本内容</returns>
        public string GetResponseText() {
            if (null == this.responseBytes) {
                return null;
            }
            return Encoding.UTF8.GetString (this.responseBytes);
        }
        /// <summary>
        /// 将响应内容存储到文件
        /// </summary>
        /// <param name="fileName">文件名称</param>
        public void SaveResponseToFile(string fileName) {
            if (null == this.responseBytes) {
                return;
            }
            // FIXME 路径跨平台问题
            string path = Path.Combine (Application.dataPath +"/StreamingAssets", fileName);
            FileStream fs = new FileStream (path, FileMode.Create);
            BinaryWriter writer = new BinaryWriter (fs);
            writer.Write (this.responseBytes);
            writer.Flush ();
            writer.Close ();
            fs.Close ();
        }
        /// <summary>
        /// 获取状态码
        /// </summary>
        /// <value>状态码</value>
        public int StatusCode {
            set { 
                this.statusCode = value;
            }
            get {
                return this.statusCode;
            }
        }
        /// <summary>
        /// 获取错误消息
        /// </summary>
        /// <value>错误消息</value>
        public string Error {
            set {
                this.error = value;
            }
            get {
                return this.error;
            }
        }
    }
    

    NetworkManager中封装了一个发送HTTP请求的方法

    /// <summary>
    /// 创建一个HTTP请求
    /// </summary>
    /// <param name="url">URL.</param>
    /// <param name="callback">Callback</param>
    public HTTPRequest CreateHTTPRequest(string url, string method, LuaFunction callback) {
        HTTPRequest client = new HTTPRequest(url, method, 5000, (HTTPResponse response)=>{
       if (null != callback) {
               callbackEvents.Enqueue(() => {
                   callback.Call(response);
               });
           }
        });
        return client;
    }
    

    Lua中使用范例

        local request = networkMgr:CreateHTTPRequest("http://www.baidu.com", "GET", function(response)
            response:GetResponseText()
        end
        )
        # 支持HOST模式
        #request:SetProxy("42.62.46.224", 80)
        request:Start()
    

    4. 文件下载实现思路

    这里主要借鉴了Unity技术博客 - 客户端断点续传这篇文章的思想
    支持了Lua回调

    HTTPDownLoad源码

    using System;
    using System.Net;
    using System.IO;
    using System.Text;
    using System.Collections.Generic;
    using System.Threading;
    using LuaFramework;
    using UnityEngine;
    /// <summary>
    /// 文件下载
    /// </summary>
    public class HTTPDownLoad
    {
        // 下载进度
        public float Progress {
            get;
            private set;
        }
        // 状态 0 正在下载 1 下载完成 -1 下载出错
        public int Status {
            get;
            private set;
        }
        // 错误信息
        public string Error {
            get;
            set;
        }
        // 总长度
        public long TotalLength {
            get;
            private set;
        }
        // 保存路径
        private string savePath;
        // url地址
        private string url;
        // 超时时间
        private int timeout;
        // 子线程
        private Thread thread;
        // 子线程是否停止标志
        private bool isStop;
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="url">url地址</param>
        /// <param name="timeout">超时时间</param>
        /// <param name="callback">回调函数</param>
        public HTTPDownLoad (string url, string savePath, int timeout)
        {
            this.savePath = savePath;
            this.url = url;
            this.timeout = timeout;
        }
        /// <summary>
        /// 开启下载
        /// </summary>
        public void DownLoad() {
            // 开启线程下载
            thread = new Thread (StartDownLoad);
            thread.IsBackground = true;
            thread.Start ();
        }
        /// <summary>
        /// 开始下载
        /// </summary>
        private void StartDownLoad() {
            try {
                // 构建文件流
                FileStream fs = new FileStream (this.savePath, FileMode.OpenOrCreate, FileAccess.Write);
                // 文件当前长度
                long fileLength = fs.Length;
                // 文件总长度
                TotalLength = GetDownLoadFileSize ();
                Debug.LogFormat("fileLen:{0}", TotalLength);
                if (fileLength < TotalLength) {
                    // 没有下载完成
                    fs.Seek (fileLength, SeekOrigin.Begin);
                    // 发送请求开始下载
                    HttpWebRequest request = WebRequest.Create (this.url) as HttpWebRequest;
                    request.AddRange ((int)fileLength);
                    request.Timeout = this.timeout;
                    // 读取文件内容
                    Stream stream = request.GetResponse().GetResponseStream();
                    if (stream.CanTimeout) {
                        stream.ReadTimeout = this.timeout;
                    }
                    byte[] buff = new byte[4096];
                    int len = -1;
                    while ((len = stream.Read (buff, 0, buff.Length)) > 0) {
                        if (isStop) {
                            break;
                        }
                        fs.Write (buff, 0, len);
                        fileLength += len;
                        Progress = fileLength * 1.0f / TotalLength;
                    } 
                    stream.Close ();
                    stream.Dispose ();
                } else {
                    Progress = 1;
                }
                fs.Close ();
                fs.Dispose ();
                // 标记下载完成
                if (Progress == 1) {
                    Status = 1;
                }
            } catch (Exception e) {
                Error = e.Message;
                Status = -1;
            }
        }
        /// <summary>
        /// 获取下载的文件大小
        /// </summary>
        /// <returns>文件大小</returns>
        public long GetDownLoadFileSize() {
            HttpWebRequest request = WebRequest.Create (this.url) as HttpWebRequest;
            request.Method = "HEAD";
            request.Timeout = this.timeout;
            HttpWebResponse response = request.GetResponse () as HttpWebResponse;
            return response.ContentLength;
        }
        /// <summary>
        /// 关闭下载
        /// </summary>
        public void Close() {
            this.isStop = true;
        }
    }
    

    NetworkManager中封装了一个文件下载的方法

    /// <summary>
    /// 下载文件
    /// </summary>
    /// <param name="url">url</param>
    /// <param name="savePath">保存地址</param>
    /// <param name="callback">callback</param>
    public HTTPDownLoad DownLoadFile(string url, string savePath, LuaFunction callback) {
        // 一次只允许下载一个文件
        if (downFileEvent != null) {
            callback.Call (-1, 0, "其他文件正在下载");
            return null;
        }
        // 下载文件
        HTTPDownLoad download = new HTTPDownLoad (url, savePath, 5000);
        download.DownLoad ();
        // 回调函数
        downFileEvent = delegate(){
            callback.Call (download.Status, download.Progress, download.Error);
            if (download.Status != 0) {
                downFileEvent = null;
            }
        };
        return download;
    }
    

    Lua中使用范例

    local path = "/Workspace/Unity/LuaFramework_UGUI/Assets/StreamingAssets/***.zip"
    local downloadRequest = networkMgr:DownLoadFile("文件下载url", path, function(status, progress, error)
            print("status:" .. status .. ", progress:" .. progress)
            if error ~= nil then
            print("error:" .. error)
            end
    end)
    

    4. 交流与讨论

    欢迎大家交流和讨论O(∩_∩)O~

    相关文章

      网友评论

      • a4a6a67c4eab:callbackEvents这个没有定义,麻烦说明一下子,
        Aodota:这个就是一个Queue<Action>,用于在主循环中调用
      • 小马哥nice:请问xlua适用吗?
        Aodota:@小马哥nice 我这是c#写的,应该很容易适配xlua。xlua没用过,所以未实践过
      • 蓝猫_8666:在Unity5.4中有错误,你们有遇到么
        System.Net.WebException: Error getting response stream (Write: The authentication or decryption has failed.): SendFailure ---> System.IO.IOException: The authentication or decryption has failed. ---> Mono.Security.Protocol.Tls.TlsException: Invalid certificate received from server. Error code: 0xffffffff800b010f
        at Mono.Security.Protocol.Tls.Handshake.Client.TlsServerCertificate.validateCertificates (Mono.Security.X509.X509CertificateCollection certificates) [0x00000] in <filename unknown>:0
        at Mono.Security.Protocol.Tls.Handshake.Client.TlsServerCertificate.ProcessAsTls1 () [0x00000] in <filename unknown>:0
        at Mono.Security.Protocol.Tls.Handshake.HandshakeMessage.Process () [0x00000] in <filename unknown>:0
        at (wrapper remoting-invoke-with-check) Mono.Security.Protocol.Tls.Handshake.HandshakeMessage:Process ()
        at Mono.Security.Protocol.Tls.ClientRecordProtocol.ProcessHandshakeMessage (Mono.Security.Protocol.Tls.TlsStream handMsg) [0x00000] in <filename unknown>:0
        at Mono.Security.Protocol.Tls.RecordProtocol.InternalReceiveRecordCallback (IAsyncResult asyncResult) [0x00000] in <filename unknown>:0
        --- End of inner exception stack trace ---
        at Mono.Security.Protocol.Tls.SslStreamBase.AsyncHandshakeCallback (IAsyncResult asyncResult) [0x00000] in <filename unknown>:0
        --- End of inner exception stack trace ---
        at System.Net.HttpWebRequest.EndGetResponse (IAsyncResult asyncResult) [0x00000] in <filename unknown>:0
        at System.Net.HttpWebRequest.GetResponse () [0x00000] in <filename unknown>:0
        Aodota:@濄去嘞_1d4f 这个服务器支持很简单的,经过项目验证的
        5872ad950dd3:可能有些服务器并不支持断点
        大佬 这个经过项目检验了吗
        Aodota:@蓝猫_8666 你是https吧,https不支持代理,直接使用域名试试呢:blush:
      • Ming_8cde:你好,我是个纯新手,刚接触到这一块,看HTTP请求里的callbackEvents没有定义,这个是什么东西啊?能不能把修改后的NetworkManager发出来看一下
      • zimu2016:感谢楼主 最近刚好在弄这块。有个问题想请教一下,就是如果你做这个APP内下载的话,热更新的版本控制又是怎么实现的呢?
        Aodota:@zimu2016 你是指你们游戏不同用户之间资源不一致吗?是因为高清和低清画质切换导致?这种建议动更检查只做低清资源的校验动更,保证基本的游戏运行正常。对于高清资源,玩家是要主动在游戏内部下载的,可以采取高清资源自己校验高清资源版本号,拉差异包更新。这种放到游戏内部提醒玩家高清资源有更新,让玩家自己手动选择下载。不知道我描述的是不是你们碰到的问题
        zimu2016:@Aodota 恩,热更这一块主要是本地与服务器两处文件MD5 list的对比。但是我现在感到困惑的是,在APP内下载的话可能有些用户下载了,有些用户不想下载。我现在考虑是再添加一份list,和普通的那些文件做区分。不过这样的话就需要搭两个热更服务器了,若是以后有更多一小块一小块的APP内下载,这样似乎也不太合适,所以想请教一下您关于这方面有没有什么想法。叙述有些凌乱,请见谅。
        Aodota:@zimu2016 热更这块我们还没弄,大致思路有了,可以和你分享下。我们准备就是拿本地的资源版本和服务器版本对比,拉取不同版本之间的差异包,拉取之后解压到动更目录。更新本地的资源版本号
      • 6a7e49f16f00:不是最近的tolua ?
        Aodota: @Liaoer 客气,多多交流😀
        6a7e49f16f00:@Aodota 最近在学这个,请多多分享,感谢
        Aodota:@Liaoer 是最近的tolua

      本文标题:Unity发送HTTP请求和文件下载

      本文链接:https://www.haomeiwen.com/subject/kejjmttx.html