《Unity3D高级编程之进阶主程》第六章,网络层(四) - 封装HTTP

HTTP协议原理

HTTP俗称短连接,由于其连接的时间较短,不受前端所控制,所以在互联网开发圈内,通常被以‘短’字开头。

在Unity3D短连接可以用.NET库来编写,也可以用U3D的内置API的WWW来写。差别不是很大,WWW对.Net做了封装,其功能已经完全够用在游戏开发上了,即使有情况不够用再用.NET补充也是很容易的事。(WWW在2018及以后的版本中都被废弃了,取代它的是UnityWebRequest,应该说封装的更好,但自定义部分则被削弱了。)

其两者的区别主要还是WWW把.NET库封装后再加了层用协程使得我们开发者在使用时更加的便捷,.NET库则直接用了线程,导致开发者还需要关注主线程与子线程的资源锁。另外WWW里面已经实现了IP6转换,而使用.Net库则还需要自己解析IP6地址,虽然对程序员来说自己用.NET实现更加接近底层实现,但同时也带来了更多需要花精力关注的地方,夺走了程序员些许宝贵的精力。

不管协程还是线程,其实两边都需要缓存和队列,如果没有缓存和队列,当网络请求量放大时就会出现数据丢失的情况。所以缓存机制在网络层是不可或缺的。

===

我们所说的HTTP,最形象的描述就是网页(Web)形式的请求与回调。它被运用在最多的就是网页(Web)请求上,它是网页(Web) 上进行数据交换的基础。

HTTP是一种 请求/响应式 的协议。也就是说,请求通常是由像浏览器这样的User- Agent发起的,当像浏览器接收方收到数据后,再通过数据来处理相应的逻辑,每个请求都是一一对应的,一个请求有且最多只能得到一个响应。

当我们要要展现一个网页时,浏览器首先发送一个请求,目的是从服务器获取页面的HTML文档,再解析文档中的资源信息然后才发送其他请求,例如获取可执行脚本或CSS样式来进行页面布局渲染,以及一些其它页面资源(如图片和视频等)。

浏览器将这些资源整合到一起,展现出一个完整的网页。浏览器执行的脚本可以在之后的阶段发起更多的请求来获取更多信息和资源,从而可以不断的叠加、更新到当前的网页内容上来。

我们常常会在游戏项目中把这套 请求/响应式 协议搬到了游戏中运用,那么为什么要用HTTP呢,用HTTP有什么好处呢?

其最重要的原因是HTTP简单易用,上手成本低,功能扩展难度低,因此被广大互联网程序员所喜爱。

HTTP是应用层上的协议,它并不需要其底层的传输层协议是面向连接的,只需要它是可靠的,或不丢失消息的(至少返回错误)。因此,HTTP依赖于面向连接的TCP进行消息传递,但HTTP本身对连接的需求并不是必须的,也就是说它本身是无状态的。

HTTP本身并没有检测是否连接,数据是否传输准确,是否有数据到达等的机制,而是依赖于TCP协议,由TCP协议来做这些传输层的事情。HTTP在TCP之上,制定了自己的规则。

HTTP在TCP之上制定了自己的规则:

1.HTTP有自己的协议,HEADS和BODY

    GET /root1/module?name1=value1&name2=value2 HTTP/1.1
    Host: localhost:8080
    Accept-Language: fr
    //这里HEADS 和 BODY一定是用空行隔开的
    body content

其中 GET为方式,HTTP有很多种方式,其中GET和POST最为常用。GET方式是把参数值放在地址中,而POST则是把参数值放在协议数据包中,并且POST可以使用二进制作为参数值,而GET则不能。其两者的实质是一样的,都是以Key1=Value1&Key2=Value2的形式作为请求内容,他们两请求的参数都会被写入请求包体中。

其协议中 HTTP/1.1为HTTP协议的版本号,我们现在常用的协议为1.1,也有少部分还在使用1.0。HTTP1.1兼容了1.0,并且在1.0之上改进了诸多内容,比如同一个地址不同host,增加了cache特性,增加Chunked transfer-coding标志切割数据块等。而2.0由于和1.1差别太大,不能兼容1.0和1.1,因此世界被分成了两块,HTTP2.0主要运用在HTTPS上,而HTTP1.1和1.0则运行在原有的HTTP上。

body content是主要存储请求内容的,POST内容都放在这里。

服务器收到请求处理相应逻辑后发送响应数据,其数据格式为:

    HTTP/1.1 200
    Date:Mon,31Dec200104:25:57GMT
    Server:Apache/1.3.14(Unix)
    Content-type:text/html
    Last-modified:Tue,17Apr200106:46:28GMT
    Content-length:xxx

    //这里HEADS 和 BODY一定是用空行隔开的
    body content

其中200为响应后代表请求成功的错误码,其他常用的错误码可以简单理解为 404 找不到请求页,500 服务器程序报错,400 访问请求参数错误,403 被拒绝访问。

2.HTTP是无状态但有会话的,两个执行成功的请求之间是没有关系的。

无状态,是指对于事务处理没有记忆能力,前后两次的请求并没有任何相关性,可以是不同的连接,也可以是不同的客户端,服务器在处理无状态请求时,只关注当下这个连接请求时的可获取到的数据,不会去关心也没有记忆去关联上一次请求的状态。

HTTP的访问请求一般都由软硬件做负载均衡来决定访问哪台物理服务器,进而两次同样地址的访问请求,有可能选择的服务器是不同的。

无状态很好的匹配了这种近乎随机的访问方式,也就是说HTTP客户端可以任意选择一个部署在不同区域的服务器进行访问,得到的结果是相同的。

3.HTTP每次请求访问结束会断开连接。

HTTP是依据什么来断开连接的呢?content-length。

content-length为 heads 上的标记,表示 body 内容长度。

带content-length,body长度是可知的,客户端在接收body内容时,就可以依据这个长度来接受数据。接受完毕后,就表示这个请求完毕了。客户端主动调用close进入四次挥手断开连接。

假如没有content-length 标记,那么body内容长度是不可知的,客户端会一直接受数据,直到服务端主动断开。

HTTP1.1在这个断开规则之上又扩展了一种新规则,即增加了Transfer-encoding标记。如果Transfer-encoding为chunked,则表示body是流式输出,body会被分成多个块,每块的开始会标识出当前块的长度,此时body不需要通过content-length长度来指定了。

HEADS上带有Transfer-encoding:chunked 就表示body被分成很多块,每块的长度也是可知的,当客户端根据长度接受完毕数据后,就会主动断开连接。

假如说Transfer-encoding 和 content-length 这两个标记都没有,那么就只能一直接受数据直到服务器主动断开连接。

那么有没有一种可能,既想使用HTTP协议又不想断开连接的?

使用keep-alive标识,keep-alive标识会让客户端与服务器的连接保持状态,直到服务器发现空闲时间结束而断开连接,在结束时间内我们仍然能发送数据。也在就是说,可以减少多次的与服务器3次握手建立连接,以及多次与服务器4次握手断开连接,提高了传输效率。

而在服务器端上,Nginx的 keepalivetimeout,和Apache的 KeepAliveTimeout 都能设置 Keep-alive 的空闲时间大小,当httpd守护进程发送完一个响应后,理应马上主动关闭相应的TCP连接,但设置 keepalivetimeout后,httpd守护进程会说:”再等等吧,看看客户端还有没有请求过来”,这一等,便是 keepalive_timeout时间。如果守护进程在这个等待的时间里,一直没有收到客户端发过来HTTP请求,则关闭这个HTTP连接。

但也不一定说使用keep-alive标识能提高效率,有时也会反而降低了效率。比如经常会没有数据需要发送,导致长时间的Tcp连接保持导致系统资源无效占用,浪费系统资源,巨量的保持连接会浪费巨量的资源。

如果客户端使用keep-alive,则需要在连续发送数据时,使用同一个HTTP连接实例。并且在发送完毕后要记录空闲时间,以便再次发送时,可以判断是否继续使用该连接,因为通常服务器端主动断开连接后并没有被客户端及时的得知,所以自行判断是否有可能已经被服务器端断开连接为好。还有一个问题是,如果网络环境不好导致发送请求无法到达时,则要尽可能的自己记录和判断,哪些数据是需要重发的。这几个问题增加了HTTP作为keep-alive来保持连接的操作难度,将本来简单便捷的HTTP,推向了有点长连接的味道。

在Unity3D中的HTTP封装

Unity3D的2018版本中原本经常用的WWW类已经被废弃,取而代之的是UnityWebRequest。

API改了但功能都是一样的,我们主要还是围绕HTTP的原理来写程序,也只有这样才能真正写出程序的精髓。

UnityWebRequest里有几个接口对我们来说比较重要,一个是Post(string uri, WWWForm postData)接口用来创建一个带有地址和Post数据的UnityWebRequest实例,一个是SendWebRequest()用来开始发送请求和迭代请求的接口,另一个是SetRequestHeader(string name, string value)用来设置HTTP头的,其中它说下面这些HEAD标记不支持设置:

    These headers cannot be set with custom values on any platform: accept-charset, access-control-request-headers, access-control-request-method, connection, date, dnt, expect, host, keep-alive, origin, referer, te, trailer, transfer-encoding, upgrade, via.

这些标记都是什么意思呢,

    accept-charset: 用于告诉服务器,客户机采用的编码格式

    access-control-request-headers: 在预检请求中,用于通知服务器在真正的请求中会采用哪些请求首部

    access-control-request-method: 在预检请求中,用于通知服务器在真正的请求中会采用哪种 HTTP 方法

    connection: 处理完这次请求后是否断开连接还是继续保持连接

    date: 当前时间值

    dnt:  (Do Not Track) 表明了用户对于网站追踪的偏好。

    expect: 是一个请求消息头,包含一个期望条件,表示服务器只有在满足此期望条件的情况下才能妥善地处理请求。服务器开始检查请求消息头,可能会返回一个状态码为 100 (Continue) 的回复来告知客户端继续发送消息体,也可能会返回一个状态码为417 (Expectation Failed) 的回复来告知对方要求不能得到满足。

    host: 请求头指明了服务器的域名(对于虚拟主机来说),以及服务器监听的TCP端口号。

    keep-alive: 允许消息发送者暗示连接的状态,还可以用来设置超时时长和最大请求数。

    origin: 指示了此次请求发起者来自于哪个站点。

    referer: 表示当前页面是通过此来源页面里的链接进入的,与origin相似。

    te: 指定用户代理希望使用的传输编码类型

    trailer: 允许发送方在分块发送的消息后面添加额外的元信息,这些元信息可能是随着消息主体的发送动态生成的,比如消息的完整性校验,消息的数字签名,或者消息经过处理之后的最终状态等。

    transfer-encoding: 指明了将 entity 安全传递给用户所采用的编码形式。transfer-encoding是一个逐跳传输消息首部,即仅应用于两个节点之间的消息传递,而不是所请求的资源本身。一个多节点连接中的每一段都可以应用不同的Transfer-Encoding 值。

    upgrade: 升级为其他协议

    via: 代理服务器相关的信息

以上这些由HEAD扩展的HTTP功能都不能进行自主的选择,其中包括了我们比较关心的标识 connection 和 keep-alive 保持连接的功能,代表了我们无法用UnityWebRequest来实现一次连接发送多次数据的需求。

另外我们最关心的content-length也不能被自定义设置,而是由API本身来自动设置。

与原来WWWW的API不同的是,UnityWebRequest更加简洁方便,但也失去了一些自定义的功能。接下来我们来使用UnityWebRequest封装HTTP网络层。

我们必须设计一个类,比如建个名称为HTTPRequest的类,每次请求服务器都调用这个类的方法来处理一个请求的操作。把地址,参数,回调传进去,等待服务器相应和回调。

短连接接口相对较少,其中POST,GET,HEAD,Start,这几个接口最为关键。在UnityWebRequest中对应的是

    POST: UnityWebRequest UnityWebRequest.Post(string uri, WWWForm postData) (静态函数)

    GET: UnityWebRequest UnityWebRequest.Get(string uri) (静态函数)

    HEAD: UnityWebRequest.SetRequestHeader(string name, string value) (非静态函数)

    Start: UnityWebRequest.SendWebRequest() (非静态函数)

用这四个API就能实现HTTP基本的发送与接收。

在调用UnityWebRequest发送请求时可以用协程也可以在逻辑更新中进行判断收发过程。

不过在协程中不太可控,并且每次都起一个携程来发送数据也浪费资源,并且协程结束时随着函数调用结束而结束的,而我们时常会有需要暂停,继续等操作。所以一般都会把HTTP的收发判断移到逻辑更新中的Update里去,这样做更容易理解,也更容易掌控。

下面我们建立一个类来封装UnityWebRequest的收发过程:

1.建立实例开始连接和发送请求,设置好这次请求回应的回调句柄。

    void StartRequest(string url, Callback _callback)
    {
        this.web_request = UnityWebRequest.Get(url);
        this.Callback =  _callback;
        this.web_request.SendWebRequest();
    }

    or POST

    void StartRequest(string url, WWWForm wwwform, Callback _callback)
    {
        this.web_request = UnityWebRequest.POST(url, wwwform);
        this.Callback =  _callback;
        this.web_request.SendWebRequest();
    }

2.判断是否完成或者说发送请求是否完毕,并且调用回调函数

    void Update()
    {
        if(web_request != null)
        {
            if(web_request.isDone)
            {
                ProcessResponse(web_request);
                web_request.Dispose();
                web_request = null;
            }
        }
    }

3.处理数据,先判断有错误存在,如果没有错误则对数据进行处理,HTTP回应的数据格式比较多,比如Json格式的,Xml格式等等。对数据进行解析后变为具体的类实例,再传给相应的函数句柄进行调用。所以这里数据格式协议也是关键。

    void ProcessResponse(UnityWebRequest _WebRequest)
    {
        if(_WebRequest.error != null)
        {
            NetworkErrorReport(_www.error);
            return;
        }
        CallbackResponse(_WebRequest.downloadHandler.text);
    }

以上是用UnityWebRequest做HTTP请求的基本步骤。在具体项目中看似简单的连接,发送,接收过程中也有不少事情需要我们做。特别是游戏逻辑中,大量多次,频繁发送HTTP请求的问题的延伸和提出的解决方案。

多次或者连续发送HTTP请求引起的问题

多次或者连续请求HTTP在具体项目中比价常见,比如客户端向服务器请求角色信息,并且请求军团信息,并且请求每日任务信息,然后再显示主界面。

这种连续的多次的请求HTTP,会同时触发多个线程向服务器做请求操作,建立连接,发送请求,关闭连接,而得到服务器返回时,却不知道哪个在先哪个在后,例如军团信息有可能先得到响应,然后再是每日任务信息得到了响应,再是角色信息得到了响应,因为是多个线程发起的多个连接,服务端接受到的数据也是没有顺序的,即使服务器端接受的顺序刚好没有打乱顺序,在处理和回调的时机也是不可知的,导致回调的顺序不可知,所以并不能确定响应的顺序与请求的顺序是一样的。

当接受数据的顺序无法确定时,如果还用顺序接受数据的方式处理出具则有风险,比如我们在发送,军团,任务,角色数据请求后,希望能够在三者都到齐的情况下执行某个程序逻辑,如果得到的数据是顺序的,则我们可以确定在收到角色数据后表明所有的数据都到齐了,于是我们再执行逻辑程序。

解决方案1:多开线程发送请求数据,等待所有数据到齐再调用执行逻辑。

每个HTTP(也可以认为是UnityWebRequest)请求都会开一个线程来向服务器请求数据,多个HTTP请求同时开启,相当于多个线程同时工作提高了网络效率。

我们可以用HTTP这种开线程的方式做,当需要多个请求的回应数据时,开启多个HTTP请求,等到数据全部得到相应后再处理逻辑。

这里有一个限制,我们需要假设多次请求之间没有且不需要逻辑顺序,则可以使用同时发起多次HTTP请求,并且等待所有请求结束后再做逻辑处理。

多次请求的响应数据的需求问题,其本质是‘如何判断我们需要的多条数据是否已经都到达’。

为了能更快的,更高效的得到HTTP请求数据,在同一时间同时向服务器发起多个HTTP请求,并且等待所有请求都得到响应后才执行逻辑程序,即等待并辨认多次请求是否全数收到。

在Unity3D上伪代码及注释如下:

    class multiple_request
    {
        void StartRequest( request_list, call_back )
        {
            lst_req = new List<UnityWebRequest>();
            for( url,wwwform in request_list )
            {
                //开启多个HTTP请求
                UnityWebRequest req = UnityWebRequest.Post(url, wwwform);
                req.SendWebRequest();

                //记录请求实例
                lst_req.Add(req);
            }

            //记录回调函数
            Callback = call_back;
        }

        void Update()
        {
            if(lst_req == null || lst_req.Count <= 0) return;

            for( UnityWebRequest req in lst_req)
            {
                //判断是否完成该请求,只要有一个没完成就继续等待
                if(!req.isDone)
                {
                    return;
                }
            }

            //当全部请求都完成时,执行回调
            Callback(lst_req);

            //请求结束并清空
            lst_req.Clear();
        }
    }

这种方式同时开启多个HTTP连接来向服务器请求数据,极大程度地加速了数据的响应速度,最大限度的提高了服务器端的CPU利用率,但请求彼此之间必须没有顺序关系,只有这样服务器端的执行顺序才无序担心。

如果请求之间是有顺序要求的,服务器端执行顺序无法保障则容易出现逻辑顺序问题,比如会有多个这样的请求,购买物品,出售物品,使用物品,三个请求一起被发起,服务器接受到的请求顺序有可能是,使用物品,出售物品,再购买物品,当顺序不同时就有可能存在逻辑问题,与我们原本设想的逻辑存在偏差,物品可能先被使用或出售,而不是被先购买,导致没有物品可被使用或出售,跟我们的所预期不一致。

提高了网络效率,服务器的CPU利用率,却无法保证预期的逻辑顺序,在需要顺序的功能上必出现诸多错误。

因此这种解决方案在需要逻辑顺序的连接请求上无法运作,但在不需要逻辑顺的连接请求上可以极大的提高网络连接利用率。

解决方案2:逐个发起请求,保证顺序。

逐个发送请求的方式最常见也比较普遍的做法,在网页端,安卓原生端,苹果原生端都使用这种方式来做请求,即每次只处理一个请求,当这个请求结束后再发起下一个请求,即一个请求对应一个功能模块,结束当前功能模块后再发起下一个请求。

例如前面所描述的例子,请求角色信息,并且请求军团信息,并且请求每日任务信息,每次只做一件事,收到角色信息数据后,再发起请求军团信息,收到军团信息后,再发起请求任务信息,收到任务信息后再显示在界面,每个请求都会被动态的分配一个回调函数,当某个请求的数据需要做别的事情的时候,就分配一个不同回调函数。

也就是说,当请求角色信息前,我们可以分配一个自定义句柄,当接受到数据后,回调执行这个自定义句柄。在自定义句柄里,可以继续发送我们需要请求的数据或功能,或者执行其他逻辑。

伪代码为:

    void on_button_click()
    {
        //请求角色数据
        function_request_roleinfo( callback_function1 ) 
    }

    void callback_function1( data )
    {
        //记录角色信息
        role_info.Add(data);

        //请求军团信息
        function_request_groupinfo( callback_function2 )
    }

    void callback_function2( data )
    {
        //记录军团信息
        group_info.Add( data );

        //请求任务信息
        function_request_task( callback_funciton3 );
    }

    void callback_function3( data )
    {
        //记录任务信息
        task_info.Add( data);

        //展示UI
        show_ui();
    }

伪代码展示了发送请求的逻辑顺序,这种逐个发送请求的方式,保证了逻辑顺序,同时也满足了对多样化功能的扩展,同一个请求可以在不同逻辑处拥有自己的自定义回调。

单个连接的自定义方式确实保证了顺序,但也降低了网络连接效率,多个请求需要发起多个TPC连接,每个连接都需要等待上一个连接请求完毕后才能开始下一个请求,每次收到消息后都需要发起4次握手的断开连接‘仪式’,每次发起请求时又必须进行3次握手的建立连接‘仪式’,消耗网络连接资源降低了收发效率。

另一种逐个发送请求的方式,为了保证请求的顺序进行,可以使用发送队列和接收队列来做逐个发送的模式。

队列在请求和响应中起到了缓冲的作用,当连续使用HTTP请求,连续收到HTTP响应的时候,能够依次处理相应的逻辑。

因为全程是只有1个线程在做请求处理,所以并不需要线程锁之类的操作。发送时,向队列推送请求实例。在逻辑更新上,判断是否有请求在队列里,有的话推出来,做HTTP请求操作,并将请求的相关信息存起来,当得到服务器响应时,调用请求信息中的回调函数,处理回调句柄。

发送和接收队列的伪代码:

    //将请求推入队列
    void RequestHttp(Request req)
    {
        ListRequest.Push(req);
    }

    //逻辑更新
    void Update()
    {
        //是否完成HTTP
        If( IsHttpFinished() )
        {
            //开始新的请求
            Request  req = ListRequest.Pop();
            mCurrentRequest = req;
            StartHttpRequest(req);
        }
        else
        {
            //是否收到响应
            If(HttpIsDone())
            {
                //根据响应数据处理逻辑
                ProcessResponse(data);

                //完成HTTP
                FinishHttp();
            }
        }
    }

    //根据数据处理逻辑
    void ProcessResponse(Response data)
    {
        If(mCurrentRequest != null
            && mCurrentRequest.Callback != null)
        {
            //回调句柄
            mCurrentRequest.Callback(data);
            mCurrentRequest = null;
        }
    }

以上阐述了,在发送HTTP请求时的推入队列操作和在收到请求后的句柄响应操作,队列使我们做到了逐个请求以及逐个响应。

解决方案3:多连接与逐个发送,并行使用。

多连接提高网络请求效率但没有一致的响应顺序,逐个发送有一致的响应顺序网络请求效率则不够好。

两者之间没有排斥关系,可以并行使用,既提高了网络请求效率,又保证了顺序。

仍然使用前面解决方案1中的类 multiplerequest ,原本 multiplerequest 类可以传入多个请求数据,并等待全部相应结束后再执行回调。然后再在 multiple_request 中回调中执行下一个请求。

伪代码如下:

    //界面按钮
    void on_click()
    {
        //新建一个请求对象
        request1 = new request();
        request1.url = url1;
        request1.wwwform = new wwwform();
        request1.wwwform.AddField("name1","value1");

        //加入请求列表
        lst_request.Add(request1);

        //兴建一个请求对象
        request2 = new request();
        request2.url = url2;
        request2.wwwform = new wwwform();
        request2.wwwform.AddField("name21","value21");

        //加入请求列表
        lst_request.Add(request2);

        //新建一个请求对象
        request2 = new request();
        request2.url = url3;
        request2.wwwform = new wwwform();
        request2.wwwform.AddField("name22","value22");

        //加入请求列表
        lst_request.Add(request3);

        //新建多请求实例
        multiple_request req = new multiple_request();

        //开始多个请求对象开启HTTP请求
        req.StartRequest(lst_request, callback_function1);
    }

    void callback_fucntion1(lst_request)
    {
        //做些逻辑
        do_some_function(lst_request);

        //新建一个请求对象
        request1 = new request();
        request1.url = url1;
        request1.wwwform = new wwwform();
        request1.wwwform.AddField("name1","value1");

        //加入请求列表
        lst_request.Add(request1);

        //兴建一个请求对象
        request2 = new request();
        request2.url = url2;
        request2.wwwform = new wwwform();
        request2.wwwform.AddField("name21","value21");

        //新建多请求实例
        multiple_request req = new multiple_request();

        //开始多个请求对象开启HTTP请求
        req.StartRequest(lst_request, callback_function2);
    }

    void callback_function2(lst_request)
    {
        //将信息展示到UI
        show_ui(lst_request);
    }

伪代码中展示了如何多请求与逐个请求的并行使用,将多个请求看成一个请求的变体,将多个请求看成一个完整的功能块,就有了多请求与逐个请求的并行使用过程。

伪代码中展示的编码过程太过繁琐,展示出每一步的编码是为了让读者能更容易理解,多请求连接与逐个请求并行使用的理念。

在现实项目中,完全可以为每个网络功能新建一个类,继承 multiple_request 类,并且将所有参数写进子类中,一并发送,并且可以多次使用在多个地方。

伪代码:

    //新建个父类继承 multiple_request 专门用来做某个功能,这个功能里需要请求多条数据
    class do_something : multiple_request
    {
        void StartRequest( int val1 , int val2, int val3, call_back )
        {
            //新建一个请求对象
            request1 = new request();
            request1.url = url1;
            request1.wwwform = new wwwform();
            request1.wwwform.AddField("name1","value1");

            //加入请求列表
            request_list.Add(request1);

            //兴建一个请求对象
            request2 = new request();
            request2.url = url2;
            request2.wwwform = new wwwform();
            request2.wwwform.AddField("name21","value21");

            //加入请求列表
            request_list.Add(request2);

            //新建一个请求对象
            request2 = new request();
            request2.url = url3;
            request2.wwwform = new wwwform();
            request2.wwwform.AddField("name22","value22");

            base.StartRequest(request_list, call_back);
        }
    }

    //ui上的按钮
    void on_click()
    {
        //实例化功能对象
        do_something = new do_something();

        //开始请求
        do_something.StartRequest(1,2,3, call_back_function1);
    }

    //回调1
    void call_back_function1(lst_request)
    {
        //做点事
        do_something(lst_requset);

        //实例化功能2的对象
        do_something2 = do_something2();

        //开始请求
        do_something2.StartRequest(3,2, call_back_function2);
    }

    //回调2
    void call_back_function2(lst_request)
    {
        //展示UI
        show_ui(lst_request);
    }

如上所描述的,在实际项目中可以用子类继承父类功能的方法,复用了请求的步骤,使得需要某个功能时使用起来更加便捷。

解决方案4:合并请求,并逐个发送合并后的请求包。

多个请求与逐一发送合并的解决方案,确实既提高了发送效率,又保证了顺序,但还是有几个缺点。

缺点1,多开连接,连接数增多,服务器建立和断开连接的压力增大。

缺点2,使用操作不方便,必须手动建立新功能类。

缺点3,多功能间的顺序,必须手动排列顺序。

客户端更希望的是多次请求能够被同时发出,收到的响应顺序也要按原来请求的顺序响应,并且要可以自定义回调句柄满足多样化的功能扩展。

合并请求就成了更好的解决方案。合并多个请求的数据进一个请求中,再发起请求,一次打包多个请求,只发一次能全部响应回来,发送这个合并包则可以做到与发起多个连接同样的效果,并且响应也可以做到顺序一致性。

合并请求,减少了连接数量,提高了网络效率,同时也可以保证顺序的一致性。

如何实现呢?

这需要服务端程序员一些配合,服务端需要将原本多个地址,每个地址对应的功能块的模式,改为同一个地址,并且改为用commoand id 字段来对应不同的功能块。

我们以json为例,HTTP数据使用json格式,服务器响应也是json格式。每次请求,客户端都会发送格式给服务器,服务器收到后解析json格式数据,然后提取变量。

我们在对多个请求进行合并时,可以将多个json请求格式,推入一个大的json数据中,为每个json数据添加一个序列号,以代表请求顺序,如下:

    //请求1
    {
        "request-order" : 1,
        "command" : 1001,
        "data1" : "i am text",
        "data2" : "i am num",
    }

    //请求2
    {
        "request-order" : 2,
        "command" : 2011,
        "book" : "i am text",
        "chat" : "i am num",
        "level" : 1,
    }

    //请求3
    {
        "request-order" : 3,
        "command" : 3105,
        "image" : "i am text",
        "doc" : "i am num",
    }

    //将1,2,3合并后的请求数据为
    {
        "data":
        [
            {
                "request-order" : 1,
                "command" : 1001,
                "data1" : "i am text",
                "data2" : "i am num",
            }
            ,
            {
                "request-order" : 2,
                "command" : 2011,
                "book" : "i am text",
                "chat" : "i am num",
                "level" : 1;
            }
            ,
            {
                "request-order" : 3,
                "command" : 3105,
                "image" : "i am text",
                "doc" : "i am num",
            }
        ],
    }

合并后多个json被推入数组中发送给服务器,服务器拿到数据后对data中的数据进行提取和处理,处理顺序按照request-order来做,每个数据块都有一个command字段,用这个字段来判断使用哪个功能模块,将数据传给该功能模块处理。

每个功能模块处理完毕后,将所有响应数据都推入同一个json实例中,并且附上与请求数据中相同的request-order,再把整个响应数据发送给客户端,当客户端收到数据时,就可以通过request-order知道响应数据的顺序了,按照request-order排序后,再进行解析和回调,得到了与请求顺序一致的响应顺序。

合并响应后的例子如下:

    //响应数据1
    {
        "response-order" : 1,
        "command" : 1001,
        "data1" : "response text",
        "data2" : "response num",
        "error_code" : "0", 
    }

    //响应数据2
    {
        "response-order" : 2,
        "command" : 2011,
        "data1" : "response text",
        "data2" : "response num",
        "error_code" : "1",
    }

    //响应数据3
    {
        "response-order" : 3
        "command" : 3105,
        "data1" : "response text",
        "data2" : "response num",
        "error_code" : "2",
    }

    //合并后的响应数据
    {
        "error_code" : 0,
        "data":
        [
            {
                "response-order" : 1,
                "command" : 1001,
                "data1" : "response text",
                "data2" : "response num",
                "error_code" : "0", 
            }
            ,
            {
                "response-order" : 2,
                "command" : 2011,
                "data1" : "response text",
                "data2" : "response num",
                "error_code" : "1",
            }
            ,
            {
                "response-order" : 3
                "command" : 3105,
                "data1" : "response text",
                "data2" : "response num",
                "error_code" : "2",
            }
        ]
    }

得到合并后的响应数据后,客户端要先提取data数据中的所有数据,以respose-order为基准进行排序,头部最小序号的先处理,解析,回调。

不仅仅是合并,我们还要保证发送的顺序,多个合并请求一同发送则会遇到大麻烦,也就是顺序问题,所以需要用队列来规避。

用了队列的方式保证请求的有序性,并且在队列之上进行操作请求合并,能更有效更便捷的进行合并。

队列与合并结合的伪代码如下:

    //将请求推入队列
    void RequestHttp(Request req)
    {
        ListRequest.Push(req);
    }

    //逻辑更新
    void Update()
    {
        //是否完成HTTP
        If( IsHttpFinished() )
        {
            //推出多个请求进行合并
            lst_combine_req.Clear();
            for(int i = 0 ; i<10 ; i++)
            {
                Request  req = ListRequest.Pop();
                lst_combine_req.Add(req);
            }

            combine_request = CombineRequest(lst_combine_req);
            StartHttpRequest(req);
        }
        else
        {
            //是否收到响应
            If(HttpIsDone())
            {
                //排序
                sort(combine_request.lst_response);

                //循环处理每个响应逻辑
                for(int i = 0 ; i<combine_request.lst_response.count ; i++)
                {
                    //根据响应数据处理逻辑
                    data = combine_request.lst_response[i];
                    ProcessResponse(data);
                }

                //完成HTTP
                FinishHttp();
            }
        }
    }

    //根据数据处理逻辑
    void ProcessResponse(Response data)
    {
        If(mCurrentRequest != null
            && mCurrentRequest.Callback != null)
        {
            //回调句柄
            mCurrentRequest.Callback(data);
            mCurrentRequest = null;
        }
    }

具体逻辑中的操作伪代码:

    //按钮事件
    void on_click()
    {
        //请求玩家数据
        request_roleinfo( call_back_function1 );

        //请求军团数据
        request_groupinfo( call_back_function2 );

        //请求任务数据
        request_taskinfo( call_back_function3 );
    }

    //先回调的是这个句柄
    void call_back_function1( data )
    {
        //保存玩家数据
        save_roleinfo(data);
    }

    //再回调的是这个句柄
    void call_back_function2( data )
    {
        //保存军团数据
        save_groupinfo(data);
    }

    //最后收到任务数据时,表明前面的数据都已经收到了,就可以做一些逻辑处理
    void call_back_function3( data )
    {
        //保存任务数据
        save_taskinfo(data);

        do_something();
    }

上述伪代码中表现了,客户端可以随意的发送请求而不需要去关心顺序的问题,大大提高了程序员的网络逻辑编程效率。

对多个请求进行合并后减少HTTP请求的次数,对速度的提升很有效果,但这里还有一些细节问题,比如多少个开始进行合并,或者多少时间内合并一次。

为了让HTTP最大效率的得到提升,必须每次请求得到响应后就应该立即进行下一次HTTP请求,如果有等待合并间隔,反而减低了网络效率。不过每次合并数量可以做一些限制,比如有1000个请求,我们不能统统合并了,为了保证发送数据大小和回调数据大小合适,也保证服务器不会为了一下子需要处理1000个数据请求而当机,我们必须限制一次性合并的个数,比如每次最多合并10个请求数据包,来保证减少连接次数减少的情况下,服务器不会在瞬间压力过大。

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第六章,网络层(四) - 封装HTTP

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号