始于足下,践于键盘之间
通过前面的学习,趁热打铁,现在就是履行实践的最佳时机。
我们知道:要用MMAP实现一个跨进程通信,只需要调整对mmap函数参数flags进行适当调整即可。
那么接下来由我带大家实现一个简单的框架,并以打游戏的方式向你们一一介绍。
上号
不啰嗦,上号大家都会,不会就点左上角❎
创号
选择Native C++,下一步
输入账号名称,Next
选择C++ 11,喜欢新特性就选择高一些,我猜你也用不到,所以干脆11,点Finish,创号完成。
整理装备
简单梳理下资源,顺便再整理下从头到脚的装备,不能裸奔不是
准备齐全后,教你一个万变不离其宗的设计技巧,那就是分层,划分界限,捋清职责,统一风格(不要五颜六色,切记)。
- MMAPIPC JAVA APP 最上层,创建两个Activity,一个在主进程,一个在子进程,App负责初始化mmipc组件
- MMAPIPC JAVA LIB 第二层,创建mmipc module,专门负责通信模块的封装,提供统一的接口
- MMAPIPC JNI 第三层,负责对接native的具体实现
- MMAPIPC NATIVE 最后一层,负责进程通信具体实现
开始打怪
先打小怪,捏柿子肯定要先挑软的对吧,这是基本常识
创建打怪的角色完成后,给OtherProcessActivity配置下子进程
然后创建App,负责初始化Boss对象,后面就可以打他
创建Module mmpic,副本的关卡入口
到这里我突然想起来,前面创建App,其实不用创建带Native的项目,好吧,你知道就行了,我就跳过了,人岁数大了就容易妥协,本人换账号是不可能了,都练了二三十年了
赶紧扫清障碍,把路铺好,准备打Boss了
路已打通,开始最后的挣扎吧,我一般是在见Boss前,加满血
改下名字,职责单一,保持统一,然后创建我们的大Boss,MMIPC
等等,作为一个资深的游戏玩家,我们需要做个日志备份,防止中途失败后,找不到原因,把日志准备好了,这下可以安心去打Boss了。
干Boss
通过前辈留下的印记,我们发现Boss主要有三个技能
- initMMAP
- setData
- getData
好,我们打Boss前主要就是,见招拆招,只要能躲过这三个技能,我们就可以无伤通关,下面开始拆招
拆招
同样我们通过前辈的了解,发现boss mmap有六个参数
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr 代表映射的虚拟内存起始地址;
- length 代表该映射长度;
- prot 描述了这块新的内存区域的访问权限;
- flags 描述了该映射的类型;
- fd 代表文件描述符;
- offset 代表文件内的偏移值;
关键在于对length、prot、flags、 fd四个参数的调整,来让boss变化形态。boss的核心设计如下
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
- m_ptr 将boss指针交给它,后面通过它来打boss
- m_size 设计boss怒气值,怒气值满了就死掉,不是血条哈,因为这个boss免疫任何伤害,但就怕被人给它东西,一给东西就涨怒气值,自己气自己,你说气人不
- PROT_READ | PROT_WRITE,设计可读可写
- MAP_SHARED,设计共享
- m_fd,boss真身的位置,这个boss比较狡猾,喜欢保护自己,如果突然断电了,这boss能跑掉,你只能下次宰它了
这样一来,我们的boss对象就有着落了,为了打他方便,我们把所有变量都存起来,然后设计一些工具方法来揍他
class MMIPC {
int m_fd = -1; //文件句柄,boss的真身
string m_path; //文件的路径,用于初始化文件句柄,boss真身的位置
char *m_ptr; //mmap内存的指针,boss的替身,对它的伤害最终会作用到真身上
size_t m_size; //mmap内存映射大小,boss的怒气值,越打越大,满了会自杀,估计是被气死了
size_t default_mmap_size; //系统规定的默认缓存页大小,怒气值的大小限制就靠它了
size_t m_position = 0; //当前mmap内存数据记录的位置,当前怒气值位置
public:
~MMIPC() { doCleanMemoryCache(true); } //对象销毁时自动调用
void doCleanMemoryCache(bool forceClean); //boss死的时候要清理战场
bool open();//链接Boss真身
void close();//关闭真身的链接,可以打扫战场了
bool truncate(size_t size);//刷新怒气值
bool mmap();//构建怒气值槽
void reloadMmap(const string &path);//多个人来打boss时,重新链接boss
void setData(const string &key, const string &value);//打他
string getData(const string &key, const string &value);//同步怒气值给别的人
bool isFileValid() { return m_fd >= 0; }//检查这个boss能不能被打
};
通过如上,我们是不是终于摸透了Boss,那就打吧开始,还墨迹啥。
开始
由于initMMAP无法限制游戏角色调几次,所以需要设计一个多次调用的兼容逻辑,防止重新加载出现异常,于是在initMMAP中,调用如下函数
void MMIPC::reloadMmap(const string &path) {
m_path = path;
// 重复加载时,文件句柄已经有效,所以先清理下缓存
if (isFileValid()) {
doCleanMemoryCache(false);
}
// 获取系统默认缓存页大小,也就是boss的怒气槽大小
default_mmap_size = getPageSize();
// 如果打开文件失败就直接打印错误日志
if (!open()) {
ALOGD("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno));
} else {
// 如果打开成功,这里通过m_fd获取实际文件大小,重新给m_size赋值
getFileSize(m_fd, m_size);
// 如果文件大小小于一个缓存页大小,且不等0
if (m_size < default_mmap_size || (m_size % default_mmap_size != 0)) {
// 获取比m_size 小 m_size*default_mmap_size分之一的值,这个大小说明:在怒气槽即将满时,给boss留点空间,好逃跑不是
size_t roundSize = (m_size / default_mmap_size + 1) * default_mmap_size;
// 看下方函数解释
truncate(roundSize);
} else {
// 如果 m_size 大于一个缓存页大小,重新创建一个boss文件真身,继续干他
auto ret = mmap();
if (!ret) {
// 如果创建替身失败了,boss就怂了
doCleanMemoryCache(true);
}
}
}
}
// 此函数的目的是为了改变文件大小,也就是boss怒气槽的大小
bool MMIPC::truncate(size_t size) {
// 文件句柄无效,还打个p,直接收拾装备回家吧,boss跑了
if (!isFileValid()) {
return false;
}
// 如果怒气槽大小和当前大小一样,直接返回,不需要更新
if (size == m_size) {
return true;
}
// 如果传进来的大小和旧的不一样,记录下当前怒气槽大小
auto oldSize = m_size;
// 直接改变当前怒气槽大小
m_size = size;
// 按照m_size开始改变文件m_fd怒气槽的大小
if (::ftruncate(m_fd, static_cast<off_t>(m_size)) != 0) {
ALOGE("truncate failed [%s] to size %zu, %s", m_path.c_str(), m_size, strerror(errno));
m_size = oldSize;
return false;
}
// 改变成功后,判断下旧的是否比现在的大
if (oldSize > m_size) {
// 如果比现在的大,改变文件的读写位置,并在合适的位置进行填充0
if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) {
ALOGE("zeroFile fail [%s] to size %zu, %s", m_path.c_str(), m_size,
strerror(errno));
m_size = oldSize;
return false;
}
}
// 如果发现boss有替身,解除替身的引用,把替身受到的伤害,写入文件里
if (m_ptr) {
if (munmap(m_ptr, oldSize) != 0) {
ALOGE("munmap fail [%s], %s", m_path.c_str(), strerror(errno));
}
}
// 继续创建新的替身,又可以愉快的揍他了
auto ret = mmap();
if (!ret) {
// 如果创建失败,则释放掉
doCleanMemoryCache(true);
}
return ret;
}
boss替身在这里
bool MMIPC::mmap() {
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
if (m_ptr == MAP_FAILED) {
ALOGD("mmap failed");
m_ptr = nullptr;
return false;
}
ALOGD("mmap success");
return true;
}
boss真身在这里
bool MMIPC::open() {
//判断文件是否已经打开过
if (isFileValid()) {
return true;
}
//O_RDWR 可读可写,O_CREAT 如果没有则创建
m_fd = ::open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
if (!isFileValid()) {
//fail to open
return false;
}
//输出文件句柄的值,可以判断m_fd的值是否==-1,来判断是否获取失败
ALOGD("open m_fd[%d], %s", m_fd, m_path.c_str());
return true;
}
void MMIPC::close() {
if (isFileValid()) {
// 关闭
if (::close(m_fd) == 0) {
m_fd = -1;
} else {
//fail to close
}
}
}
- open 通过游戏角色给的路径m_path,我们定位到boss的真身位置,并获取boss文件句柄
- close 关闭boss文件句柄
真实伤害
void MMIPC::setData(const string &key, const string &value) {
// 组合伤害,憋大招
string content = key + ":" + value;
if (m_position != 0) {
content = "," + content;
}
ALOGD("setData content=%s", content.c_str());
size_t numberOfBytes = content.length();
// 如果伤害溢出,则抛异常给上面
if (m_position + numberOfBytes > m_size) {
auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " +
to_string(numberOfBytes) +
", m_size: " + to_string(m_size);
throw out_of_range(msg);
}
// 对替身施加伤害,每次都在上次的m_position之后,累加伤害值
memcpy(m_ptr + m_position, (void *) content.c_str(), numberOfBytes);
// 刷新上次的m_position
m_position += numberOfBytes;
ALOGD("setData success m_ptr=%s", m_ptr);
}
// 其他跨进程的角色也可以同步到伤害的值
string MMIPC::getData(const string &key, const string &value) {
return m_ptr;
}
验证
- App被初始化两次,多进程初始化后都能见到Boss
- MainActivty,揍了两下
- OtherProcessActivity,获取MainActivty在主进程中打的伤害,完美跨进程通信
这里面看到两个有价值的信息
- m_fd = 78 两个进程是同样的虚拟地址,有个疑问,它们是一个虚拟地址么?(答案:不是的,有问题需要自己去探索,去搜吧)
- 缓存页的大小都是 4096,也就是4kb
复盘
打完boss我们做个小复盘,以便后面打的更快更爽。
做的好的
- 首先值得肯定是完成了,很多时候完成是第一步,设计过渡应避免
- 层级划分清晰,各个模块职责分明,功能实现干净利索不拖泥大水
做的不好的
- Boss真身在Sdcard,被别人上大号,连接上后,一刀抢走怎么办
- 伤害值,传输过程被别人拦截怎么办,本来你砍了1万点伤害,被别人改成1点
- 如果跨线程、跨进程都来了伤害,你怎么保证这些伤害都能正常到Boss身上
如何改进
- 有个办法就是将 真身放到data/data/包名目录下,有人说你为啥不用 mmap参数中 MAP_ANONYMOUS 属性,因为它只能父子进程通信,应用的子进程其实是zygote 的子进程,非你的,我把你当兄弟,你把我当儿子,但data/data下也有风险,如果root手机,你的通信数据就有可能被人篡改
- 如何防止被篡改呢?建议pb+加密,pb保证传输快,效率高,加密保证不被篡改
- 如果跨线程可以通过线程锁来实现,如果跨进程怎么办,我看binder实现中用到了MAP_PRIVATE,这个参数有个好处就是,当你往映射内存里写的时候,内核会先copy一份出来再写,这边避免其他进程读的时候不能读,这样一来是不是就好了呢,当然需要验证的(已验证,MAP_PRIVATE方式,拷贝再写入的值,并不会被刷的文件中,也就无法实现共享),多进程的写入,需要写测试用例验证。
以上改进,Doing .......
游戏副本在哪?
说了这么多,游戏副本在哪?欢迎跟我一起来打Boss
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。