这周完成了一个断点续传的功能。
我们的游戏里加载地图的逻辑简化而言是这样:
1.首先用本地的md5文件校验地图文件(很多文件)是否完整。(中间有很多步骤,任何步骤失败都认为地图不完整)
2.如果完整,直接加载地图。
3.如果不完整,需要通过一个http协议请求后台服务器传回完整的地图。
现在要增加一个断点续传的功能。也就是地图下载过程中,如果断网,丢包之类,重启游戏地图会继续下载,而不是重新下载。给服务器减少流量。
要解决这个问题,要了解以下问题:
1.正常的http请求流程。
2.http分段请求的原理。
3.具体地图下载流程的处理。
1.http请求的流程
客户端发送的请求被封装成请求报文。
请求报文的内容里面包括请求方法,请求地址,http协议版本,请求首部。其中的请求方法就是所谓的Get,Post,Put,Header,Delete,Options,Trace,Connect这些。这里我们无需详细了解。
服务器收到请求以后,会以响应报文的形式发回客户端。响应报文里面包括了http协议版本,状态码,状态码的原因短语,响应首部,请求的实体主体。
状态码会标记服务端是否正常处理,是个重要的参数。
我们来看下载地图的请求。
因为是需要直接请求实体,当然是用Get方法。另外只需要知道Url就行。网络正常的情况下,服务端会返回状态码200。而返回的实体就是地图文件,通常是一个压缩文件.zip,用二进制模式写到一个文件,再用ziplib之类解压。
2.http分段请求的原理
那么断点续传怎么做呢。在客户端发起的请求报文里,请求首部里面有个可选参数Range,用来标识只请求实体的一部分。
格式形如:Range: bytes=500-999,请求500bytes到999bytes。Range: bytes=500-,请求500bytes开始到结束的部分。
那么断点续传其实就是知道上一次传输的终点,把它作为Range参数的起点,发起请求。
如果请求首部带有Range参数,那么服务器正常情况会返回状态码206和请求的部分实体。
3.具体地图下载流程的处理
地图下载的具体逻辑。我这里这样处理:
服务器返回的实体内容写到一个临时文件,命名为temp.zip。如果下载完整了,就会把名字改掉,改为map_id.zip。如果不完整就不会改名了。
所以每次发起请求前,判断是否需要断点续传,只需要判断存不存在temp.zip即可。
1.首先用本地的md5文件校验地图文件(很多文件)是否完整。(中间有很多步骤,任何步骤失败都认为地图不完整)
2.如果完整直接使用本地地图。
3.如果不完整,判断是否存在temp.zip文件。
4.如果存在,请求头部带有Range参数,起点是temp文件的大小。
5.如果不存在,默认的请求头部,请求完整的文件。
中间有一些细节问题。可能和具体的实现框架有关。想象一次研发测试流程:
1.启动游戏,第一次下载是一次完整下载,下载的过程中断网(下载过程有个回调,回调参数有标识进度的process,在这里打断点,然后断网),把收到的实体(应该是一部分)写到temp.zip。
2.把网接回,再启动游戏,判断是否有temp.zip,有temp.zip,所以将temp.zip的文件大小作为Range参数的起点,发起部分请求。把收到的实体写入temp.zip,但是这次的写入起点是文件的末尾。把文件改名为map_id.zip。
这两个情况都是写在一个函数里的,那么怎么判断temp.zip文件是否已经是完整的地图文件呢?要不要改名了呢?
我这边的框架里面,服务器返回的实体res里面有个error参数,通过这个参数就能知道本次返回的实体,是不是客户端请求的全部实体(包括完整的请求和部分的请求)。
也就是说如果res里面的error是true,那么最后是不会走到改名那步的。下一次请求还是会走断点续传。反之error是false,那么说明本次请求返回的实体已经完整,最后会走到改名那步。
另外一个问题。如果服务器上的文件内容变更了,我们还从旧文件的断点位置请求,这样就会出错了。
这个问题,可以考虑请求首部的If-Range参数和响应首部的ETAG参数。也就是说要客户端记录传回文件的ETAG,下一次请求把此ETAG作为If-Range参数。那么服务器会根据ETAG判断文件是否更新。
如果有更新,会走全量下载,返回状态码200。如果没有更新,会走部分下载,返回状态码206。
也可以用其他方法,比如大家约定好,服务器那边地图改变了,地图就换一个名字(用时间戳做地图名字后缀),那么客户端这边请求的名字就会变了。不需要考虑If-Range参数。也不必在客户端记录ETAG。
引用:
1.《图解http》