Kealdish's Studio.

H5游戏接入技术小结(上)

字数统计: 2.5k阅读时长: 10 min
2016/05/17 Share

概览

这段时间忙着做游戏平台H5游戏的接入,趁着刚做完还热乎劲儿,将其中的技术要点总结下来。H5游戏的接入主要分为两个部分:游戏的下载和游戏对战。本篇博客着重讲述游戏的下载部分,下篇博客将着重讲述H5游戏与app通过JS进行数据交互以及在线对战的技术要点。

H5游戏资源本质上是由一些目录以及目录下的文件资源组成,要能够在app中运行H5游戏,就是要在WKWebView的浏览器中用HTTPServer搭建本地服务,然后去加载游戏资源根目录下的index.html文件,换句话说index.html文件是游戏的入口文件。HTTPServer我们采用的是开源的CocoaHTTPServer,关于在app中搭建HTTPServer服务的内容就不多赘述,感兴趣的可以去Github上了解。当然,这也需要H5与iOS开发人员沟通和调试才能真正地在WKWebView中流畅运行。

游戏资源下载思路

知道了怎样在app中运行H5游戏,接下来最重要的就是游戏资源的下载。当用户进入一个游戏后,游戏的状态分为三种:未下载,已下载,暂停下载和需要更新。如何判断游戏此时的状态呢?首先我们需要区分每一个游戏,因此,约定将游戏资源的URL进行MD5加密后的字符串作为游戏的唯一标识符。之后,我们创建game.plist文件,文件中存储一个字典,字典的key为游戏的标识符,value为游戏资源的大小。每当一个游戏开始下载,则在plist文件中添加一条记录。检查游戏的状态时,拿游戏的标识符与plist文件中所有的记录进行对比,若未找到相同的记录则状态为未下载。否则,在存储游戏资源的文件夹中查找以游戏标识符命名的.zip压缩包,若找到将其包的大小与plist文件中找到的记录的大小进行比较,若大小不同,则状态为暂停下载,若未找到压缩包且未找到解压后的资源文件夹显示为未下载。否则,状态为已下载。至于更新的状态则需要与服务器端进行沟通和约定,在此就不讲了。

确定完当前进入游戏的状态后,当游戏处于未下载或者暂停的状态时,用户可以点击下载按钮进行下载。下载的过程分为两种情况:未下载开始下载和暂停继续下载。第一种情况的下载过程是这样:在资源下载目录下创建以游戏标识符命名的.zip压缩文件,用NSURLSession创建NSURLSessionDataTask任务负责资源数据的接收,用NSOutputStream负责数据流的写入工作。写入完毕后,关闭NSOutputStream服务,验证资源包数据的完整性。若资源包数据完整,则对其进行解压缩,得到游戏运行的全部资源文件后便可以开始游戏。第二种情况则是在资源下载目录下查找以游戏标识符命名的.zip压缩包,获取包文件的内容大小,将其放入NSURLRequest的HTTP请求头中,剩下的步骤与第一种情况相同。

游戏资源下载的代码实现

思路厘清了,就需要用代码去实现它。因此,我创建了名为XRDownloadManger的类负责游戏资源的下载工作。为了更好的辅助XRDownloadManager工作,又创建了模型类XRSessionModel来存储游戏资源的相关数据。

下面列出的是XRSessionModel的类结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// XRSessionModel.h
typedef enum {
DownloadStateStart = 0, /** 下载中 */
DownloadStateSuspended, /** 下载暂停 */
DownloadStateCompleted, /** 下载完成 */
DownloadStateFailed /** 下载失败 */
}DownloadState;
@interface XRSessionModel : NSObject
/** 流 */
@property (nonatomic, strong) NSOutputStream *stream;
/** 下载地址 */
@property (nonatomic, copy) NSString *url;
/** 游戏版本号*/
@property (nonatomic, copy) NSString *update;
/** 获得服务器这次请求 返回数据的总长度 */
@property (nonatomic, assign) NSInteger totalLength;
/** 下载进度 */
@property (nonatomic, copy) void(^progressBlock)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress);
/** 下载状态 */
@property (nonatomic, copy) void(^stateBlock)(DownloadState state);
@end

XRDownloadManger采用了单例设计模式,该类主要的工作包括开启任务下载游戏资源并回调下载进度和下载状态,获取资源大小,删除已下载资源,判断游戏资源是否下载完成等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@interface XRDownloadManager : NSObject
/**
* 单例
* @return 返回单例对象
*/
+ (instancetype)sharedInstance;
/**
* 开启任务下载资源
* @param url 下载地址
* @param progressBlock 回调下载进度
* @param stateBlock 下载状态
*/
- (void)download:(NSString *)url update:(NSString *)update progress:(void(^)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress))progressBlock state:(void(^)(DownloadState state))stateBlock;
/**
* 查询该资源的下载进度值
* @param url 下载地址
* @return 返回下载进度值
*/
- (CGFloat)progress:(NSString *)url;
/**
* 获取该资源总大小
* @param url 下载地址
* @return 资源总大小
*/
- (NSInteger)fileTotalLength:(NSString *)url;
/**
* 判断该资源是否下载完成
* @param url 下载地址
* @return YES: 完成
*/
- (BOOL)isCompletion:(NSString *)url;
/**
* 删除该资源
* @param url 下载地址
*/
- (void)deleteFile:(NSString *)url;
/**
* 清空所有下载资源
*/
- (void)deleteAllFile;
/**
* 根据下载地址查找该资源路径
**/
- (NSString *)getPathWithUrl:(NSString *)url;
@end

XRDownloadManger实现文件中包含两个可变字典类型的成员变量taskssessionModelstasks负责保存所有的下载任务,sessionModels负责保存所有的下载资源的相关信息。taskssessionModels均是以url为key,但value分别为NSURLSessionDataTaskXRSessionModel的键值对。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSMutableDictionary *)tasks{
if (!_tasks) {
_tasks = [NSMutableDictionary dictionary];
}
return _tasks;
}
- (NSMutableDictionary *)sessionModels{
if (!_sessionModels) {
_sessionModels = [NSMutableDictionary dictionary];
}
return _sessionModels;
}

download:::方法是负责下载的主要方法。每一个NSURLSessionDataTask对象负责一个游戏资源的下载,并将该对象存入tasks中。当下载完成并验证压缩包内容的完整性后将NSURLSessionDataTask对象移除,当需要暂停的时候,以游戏URL的md5字符串为key拿到NSURLSessionDataTask对象并执行suspend方法将下载任务暂停。暂停后继续下载也与之类似,同样是从tasks中拿到NSURLSessionDataTask对象并执行resume方法继续下载任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* 开启任务下载资源
*/
- (void)download:(NSString *)url update:(NSString *)update progress:(void (^)(NSInteger, NSInteger, CGFloat))progressBlock state:(void (^)(DownloadState))stateBlock
{
if (!url) return;
// 判断资源是否下载完成
if ([self isCompletion:url]) {
stateBlock(DownloadStateCompleted);
return;
}
// 暂停下载
if ([self.tasks valueForKey:XRFileName(url)]) {
[self handle:url];
return;
}
// 创建缓存目录文件
[self createCacheDirectory];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
// 创建流
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:XRFileFullpath(url) append:YES];
// 创建URL请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
// 设置下载内容范围的请求头
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", XRDownloadLength(url)];
[request setValue:range forHTTPHeaderField:@"Range"];
// 创建一个Data任务
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
NSUInteger taskIdentifier = arc4random() % ((arc4random() % 10000 + arc4random() % 10000));
[task setValue:@(taskIdentifier) forKeyPath:@"taskIdentifier"];
// 保存任务
[self.tasks setValue:task forKey:XRFileName(url)];
XRSessionModel *sessionModel = [[XRSessionModel alloc] init];
sessionModel.url = url;
sessionModel.update = update;
sessionModel.progressBlock = progressBlock;
sessionModel.stateBlock = stateBlock;
sessionModel.stream = stream;
[self.sessionModels setValue:sessionModel forKey:@(task.taskIdentifier).stringValue];
[self start:url];
}
/**
* 判断该文件是否下载完成
*/
- (BOOL)isCompletion:(NSString *)url{
if ([self fileTotalLength:url] && XRDownloadLength(url) == [self fileTotalLength:url]) {
return YES;
}
return NO;
}
// 若暂停则继续下载,若正在下载则暂停下载
- (void)handle:(NSString *)url{
NSURLSessionDataTask *task = [self getTask:url];
if (task.state == NSURLSessionTaskStateRunning) {
[self pause:url];
} else {
[self start:url];
}
}
/**
* 根据url获得对应的下载任务
*/
- (NSURLSessionDataTask *)getTask:(NSString *)url{
return (NSURLSessionDataTask *)[self.tasks valueForKey:XRFileName(url)];
}
/**
* 开始下载
*/
- (void)start:(NSString *)url{
NSURLSessionDataTask *task = [self getTask:url];
[task resume];
[self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateStart);
// 删除暂停状态
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:XRPausePath]) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:XRPausePath];
[dict removeObjectForKey:XRFileName(url)];
[dict writeToFile:XRPausePath atomically:YES];
}
}
/**
* 暂停下载
*/
- (void)pause:(NSString *)url{
NSURLSessionDataTask *task = [self getTask:url];
[task suspend];
[self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateSuspended);
// 存储暂停状态
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:XRPausePath];
if (dict == nil) dict = [NSMutableDictionary dictionary];
dict[XRFileName(url)] = @"pause";
[dict writeToFile:XRPausePath atomically:YES];
}
/**
* 根据url获取对应的下载信息模型
*/
- (XRSessionModel *)getSessionModel:(NSUInteger)taskIdentifier{
return (XRSessionModel *)[self.sessionModels valueForKey:@(taskIdentifier).stringValue];
}
/**
* 查询该资源的下载进度值
*/
- (CGFloat)progress:(NSString *)url{
return [self fileTotalLength:url] == 0 ? 0.0 : 1.0 * XRDownloadLength(url) / [self fileTotalLength:url];
}
/**
* 获取该资源总大小
*/
- (NSInteger)fileTotalLength:(NSString *)url{
return [[NSDictionary dictionaryWithContentsOfFile:XRTotalLengthFullpath][XRFileName(url)] integerValue];
}
/**
* 根据下载地址查找该资源路径
**/
-(NSString *)getPathWithUrl:(NSString *)url{
NSString *str = XRFileFullpath(url);
return str;
}

整个网络数据的请求和处理都依赖于NSURLSessionDataDelegate的代理方法。请求得到反馈时根据response拿到游戏资源的完整大小,并将其写入到game.plist文件中。当客户端开始接收服务器返回的数据时,打开NSOutputStream并开始将接收到的数据写入到指定路径下的文件中,并返回下载进度。接收数据完成后对数据包进行解压缩,解压成功则将写入流关闭并将下载任务从tasks中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#pragma mark - NSURLSessionDataDelegate
/**
* 接收到响应
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{
XRSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
// 打开流
[sessionModel.stream open];
// 获得服务器这次请求 返回数据的总长度
NSInteger totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + XRDownloadLength(sessionModel.url);
sessionModel.totalLength = totalLength;
// 存储总长度
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:XRTotalLengthFullpath];
if (dict == nil) dict = [NSMutableDictionary dictionary];
dict[XRFileName(sessionModel.url)] = @(totalLength);
[dict writeToFile:XRTotalLengthFullpath atomically:YES];
// 接收这个请求,允许接收服务器的数据
completionHandler(NSURLSessionResponseAllow);
}
/**
* 接收到服务器返回的数据
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
XRSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
// 写入数据
[sessionModel.stream write:data.bytes maxLength:data.length];
// 下载进度
NSUInteger receivedSize = XRDownloadLength(sessionModel.url);
NSUInteger expectedSize = sessionModel.totalLength;
CGFloat progress = 1.0 * receivedSize / expectedSize;
sessionModel.progressBlock(receivedSize, expectedSize, progress);
}
/**
* 请求完毕(成功|失败)
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
XRSessionModel *sessionModel = [self getSessionModel:task.taskIdentifier];
if (!sessionModel) return;
if ([self isCompletion:sessionModel.url]) {
// 下载完成,解压缩
[SSZipArchive unzipFileAtPath:XRFileFullpath(sessionModel.url) toDestination:XRWebDirectory];
sessionModel.stateBlock(DownloadStateCompleted);
} else if (error){
// 下载失败
sessionModel.stateBlock(DownloadStateFailed);
}
// 关闭流
[sessionModel.stream close];
sessionModel.stream = nil;
// 清除任务
[self.tasks removeObjectForKey:XRFileName(sessionModel.url)];
[self.sessionModels removeObjectForKey:@(task.taskIdentifier).stringValue];
}

结语

以上是对下载过程实现的大概描述,其中的实现细节还有很多值得推敲和优化的地方,接下来会发时间去优化并会将优化后的效果总结出来。如果其中有错误的地方欢迎指正。

CATALOG
  1. 1. 概览
  2. 2. 游戏资源下载思路
  3. 3. 游戏资源下载的代码实现
  4. 4. 结语