类库大魔王/missdeer 2019-04-20T08:52:09+00:00 me@minidump.info C++中RAII的实现手法 2019-04-20T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2019/04/raii-in-cpp Go语言中有defer可以在退出当前作用域时执行一个函数调用,C++中以前常用的做法是创建一个类的对象,在该类的析构函数中写入需要执行的代码。而这个对象可以创建在栈中,也可以放在std::auto_prt<>std::unique_prt<>之类的地方,只要退出作用域就会被销毁就可以。这在网上能找到不少的讨论,比如C++之父的这段,C++ Core Guidelines中的这段,以及Cpp Reference中的这篇

Boost中也提供了BOOST_SCOPE_EXIT宏,但是用起来相当丑陋,而且像稍早版本的Qt Creator内置的简单的C++语法解析器甚至不能正常解析:

{ // Some local scope.
    ...
    BOOST_SCOPE_EXIT(capture_list) {
        ... // Body code.
    } BOOST_SCOPE_EXIT_END
    ...
}

到了C++11,这个宏有了改进,使用lambda的机制:

void world::add_person(person const& a_person) {
    persons_.push_back(a_person);

    // This block must be no-throw.
    person& p = persons_.back();
    person::evolution_t checkpoint = p.evolution;
    // Capture all by reference `&`, but `checkpoint` and `this` (C++11 only).
    BOOST_SCOPE_EXIT_ALL(&, checkpoint, this) { // Use `this` (not `this_`).
        if(checkpoint == p.evolution) this->persons_.pop_back();
    }; // Use `;` (not `SCOPE_EXIT_END`).

    // ...

    checkpoint = ++p.evolution;

    // Assign new identifier to the person.
    person::id_t const prev_id = p.id;
    p.id = next_id_++;
    // Capture all by value `=`, but `p` (C++11 only).
    BOOST_SCOPE_EXIT_ALL(=, &p) {
        if(checkpoint == p.evolution) {
            this->next_id_ = p.id;
            p.id = prev_id;
        }
    };

    // ...

    checkpoint = ++p.evolution;
}

既然这样,其实我们自己也不是一定要用Boost的实现,可以自己简单实现一个:

#include <functional>

class ScopedGuard
{
    std::function<void(void)> m_f;
public:
    ScopedGuard() = delete;
    ScopedGuard(ScopedGuard&&) =delete;
    ScopedGuard(const ScopedGuard&) =delete;
    ScopedGuard& operator=(ScopedGuard&&)=delete;
    ScopedGuard& operator=(const ScopedGuard&)=delete;
    ScopedGuard(std::function<void(void)> f) : m_f(f) {}
    ~ScopedGuard() { m_f(); }
};

使用示例:

ScopedGuard queryMutexUnlock([this](){m_queryMutex.unlock();});

ScopedGuard postFinishedQueryEvent([this, e](){QCoreApplication::postEvent(this, e);});

有了lambda就是方便。

]]>
SQLCipher Android平台升级踩坑 2019-04-13T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2019/04/sqlcipher-android-upgrade-memior 公司项目里使用SQLCipher用于本地加密存储数据,之前发现在iOS上打开数据库后执行第一条CRUD特别慢,经过几番优化尝试,发现只要在编译选项指定使用Apple Common Crypto,就能比使用OpenSSL提升100倍性能。但是Android平台上也只能使用OpenSSL,性能问题两年多来一直没解决。

一个多月前,公司的安全部门说我们用的SQLCipher版本太老了,要求我们升级,经过大半个月折腾,我们终于把它从3.4.1升级到了当时最新的4.0.1,相关人士都欢欣鼓舞,直到这个星期。

Android team为了适应Google play的要求,把API target升级到了28,显示通知栏图标的逻辑变成了先调用startForegroundService再调用startForeground,结果在这两次调用中间,因为执行数据库的初始化读写操作,导致两次调用时间间隔超过5秒(SQLCipher就是这么慢!),App被系统干掉!于是开始了我们在Android上的优化。

鉴于一直没有更好的像Apple Common Crypto方案,我们直接放弃了提升SQLCipher性能的尝试。而是把所有数据库初始化操作都放到后台线程,然后把几乎所有相关的读写操作都加了锁(简单粗暴)。而后又发现还有主线程会有数据库操作在等后台线程初始化的锁,于是还是很容易遇到ANR。最后实在没办法了,尝试把SQLCipher又回滚到3.4.1版本,发现一切都变好了!就是这么神奇!我看github上也有人报SQLCipher 4.0.1在Android上插入数据很慢,官方并没有好的解决方案,给出的关掉内存清扫的安全选项我测试下来并没多少改善。

吃一堑,长一智。这给我们的教训是:升级第三方库要尽量放在每个release开始时做,做完了还有较多的时间进行内部测试,发现问题最差也能回滚到之前的状态。

]]>
年会中奖KPW3一台 2019-01-09T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2019/01/win-a-kpw3-on-annual-party 昨晚公司,本来都有点不太想去,因为往年的经历开一次年会失一次望,不如回家陪妹子。跟妹子聊起这个事,妹子倒劝我还是去参加吧,抽奖奖品我确实很想要一台带背光的Kindle,家里已有的Kindle4和DXG都没背光,晚上吊顶的灯光不适合看。于是还是去参加了,还当了一回苦力拉了几箱东西,包括所有抽奖奖品去酒店。

等到抽Kindle为奖品的三等奖时,坐在我旁边的去年抽中iPhone X的同事跟我说:“你应该是不要这个Kindle的,都已经有了。”我便回他:“有总比没有好。”话刚说完,大屏幕上就出现了我的头像。

晚上回到家,妹子问能不能拆,我说拆吧,反正也没人送。妹子说这kindle怎么可能送人,要是送人了就要吵架了。

不过想来我主要也就是拿来看看网络小说之类不用动脑的东西,抓小说的小程序已经有一段时间没更新了,后面要花点时间来个大更新,改成开放配置架构,可以只修改配置文件(可以是JSON或XML之类的易读易修改格式)就能支持新的网站。

除此之外倒也可以考虑抓些其他内容,比如微信公众号,上面还是有些不错的内容可以随便看看。还有适合各个年龄段胎教、儿童的故事,可以收集一些来备用。

]]>
2018没有回顾,2019没有计划 2019-01-01T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2019/01/2018-no-review-2019-no-plan 过去几年每年初都会写个新一年计划,但几乎都没实现过,所以我觉得不要做计划了,也不要回顾已经过去的一年了。活着已经很辛苦了,我只希望亲人们身体健康,希望做事都顺顺利利。

就这样。

]]>
更换blog托管 2018-12-03T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/12/blog-hosting-switch 去年把blog托管到了coding pages上,以为可以为大陆提供更好的访问速度,但实际上效果似乎并没有想像的那么好,coding pages貌似是为了规避大陆网页要求备案,把大陆访问IP引导到了香港的主机上,网络一旦出了国,速度就会降很多,另一点是免费的coding pages要求在页面上添加他们的声明信息,虽然就算他们不要求,我也会在页脚处添加各种感谢声明,但强行要求就让我心里总有点疙瘩。

这两天偶然知道开源CDN服务jsDelivr可以直接加速github上的资源,比如我的blog仓库是https://github.com/missdeer/blog,使用gh-pagesbranch,那么如果有一个文件路径是/js/main.js,用jsDelivr则通过URLhttps://cdn.jsdelivr.net/gh/missdeer/blog@gh-pages/js/main.js访问,就可以获得CDN加速的效果,其中https://cdn.jsdelivr.net/是固定的域名,gh表示githubmissdeer/blog表示我的仓库,gh-pages表示所在branch,之后便是文件路径。

在中国大陆,jsDelivr是通过QUANTIL这家CDN(据说QUANTIL是网宿参股公司,QUANTIL国内节点为网宿实际运营)实现的,实测下来速度非常好,而且如果是js或css文件,可以通过添加.min自动获得紧缩版本,比如https://cdn.jsdelivr.net/gh/missdeer/blog@gh-pages/js/main.min.js,不用自己另外单独提供一份。

我把blog上几乎所有除了HTML外的静态资源,比如js,css,图片,字体文件等,全部替换成jsDelivr加速了。blog原本在coding pages上是用一个二级域名https://blog.minidump.info的,切换回github pages后不再用这二级域名了,而是顶级域名下加一级路径https://minidump.info/blog/,可以直接使用github pages的SSL证书。原来的二级域名就在linode上用nginx加了个301跳转到新地址。

最后,是修正leancloud上的评论记录,把原本在https://blog.minidump.info/上的评论全部转移到https://minidump.info/blog/上了。

所以现在时间可能主要花在blog域名的DNS解析(cloudflare家在大陆似乎并不好),301跳转,HTML文件下载这三部分了,其他上了CDN的静态资源应该比以前会快很多吧。

另外有一个问题,更新了blog,新增/修改的内容不能立即同步到https://cdn.jsdelivr.net/gh/missdeer/blog@gh-pages/这个路径下,要把gh-pages换成git commit的hash id,可能要过2天才会同步,这点略显繁琐。

]]>
imchenwen进度:优化 2018-12-01T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/12/imchenwen-WIP-optimization 这段时间主要是做了些优化。

终于找到了“关于”和“偏好设置”没有国际化的原因,需要装入qt_*.qm文件,比如简体中文则是qt_zh_CN.qm,在mac上运行macdeployqt并不会复制这些qm文件,Windows上的windeployqt则会复制一些其他语言的,但没有中文的。

Application menu

增加了“解析并播放视频”的菜单项,主要是想增加快捷键,并对用户明显,所以就加到主菜单去了。

play menu

在“快捷方式”菜单中增加了“在线电影”的分类,国内网上有很多在线看电影的网站,都是通过Flash播放在线视频资源,很多热门资源可以直接观看。

同时修改了视频地址嗅探操作方式,原本VIP类型的视频需要自己一个一个尝试使用解析网站获取视频地址,这样比较低效和麻烦,现在另外新开一个进程,单独嗅探某个URL下的视频地址,所以可以并发多个解析网站进行解析嗅探,另一个好处是也可以在后台嗅探在线电影网站的视频地址嗅探了。

另一个改动是把“快捷方式”的网站定义和VIP视频解析网站地址都写到网上了,每次程序启动从网上加载列表,所以列表可以自定义,这就很灵活了,特别是VIP视频解析网站可能变化很频繁,用户也可以自己维护一个列表。

shortcut menu

调整了“偏好设置”对话框,在顶部增加了一个图标工具栏。QDialog并不能在Qt Designer中直接拖拽增加一个工具栏,但可以通过代码增加,所以我的方法是在顶部放一个空的占位QWidget,然后在代码中用QToolBar替换掉,代码如下:

QToolBar* toolbar = new QToolBar(this);
layout()->replaceWidget(widgetPlaceholder, toolbar);
toolbar->setIconSize(QSize(48, 48));
toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);

效果如下:

configuration

]]>
MSVC与Go语言混合开发 2018-11-24T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/11/msvc-and-go 用C++写程序时,有些事情发现用Go做很容易,用C++则比较折腾,所以就想用Go实现,然后通过cgo链接到C++程序中。但是cgo在Windows上只能使用gcc实现,有时我又有必须使用MSVC的理由。

Go在Windows上可以直接用gcc生成静态库或动态库,但要让MSVC也能用上,需要做一些处理。

首先,生成静态库,比如:

set CGO_ENABLED=1
go build -buildmode=c-archive -o libgo.a main.go

然后,创建一个def文件,声明导出符号,比如libgo.def

EXPORTS
    Function1

接着,用gcc从该def和静态库生成一个dll:

gcc libgo.def libgo.a -shared -lwinmm -lWs2_32 -o libgo.dll -Wl,--out-implib,libgo.dll.a

这时,应该已经有了一个libgo.dll文件和一个libgo.def文件,用MSVC自带的lib程序生成MSVC可用的.lib文件:

lib /def:libgo.def /name:libgo.dll /out:godll.lib /MACHINE:X86

如果是64位环境,则将最后一个参数改为/MACHINE:X64

最后,修改Go编译器生成的libgo.h文件,将其中2行line #1开头的行删掉,2行_Complex相关的代码删掉。

这时,MSVC程序可以直接#include "libgo.h",代码中可以直接调用函数Function1,并在链接时添加对godll.lib的引用。

其实Go是可以直接生成dll文件的,但会把所有Go runtime的函数全部导出,略显冗余,所以我这里就通过自己创建的def文件来声明需要导出的函数。

]]>
imchenwen进度:内置播放器和DLNA投屏 2018-11-13T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/11/imchenwen-builtin-player-and-dlna 大约一年半前因为感觉遇到解决不了的技术问题,于是imchenwen的坑就扔下不管了。上个月的时候突然意识到,妹子喜欢看网卡的视频,优酷腾讯芒果爱奇艺等等,但是因为身体原因捧着个手机或iPad看会觉得头疼,看电视就要好很多,所以就萌生了在电视机上看网络视频的想法。

说干就干,首先在淘宝花了一百多块钱买了个“山寨”投屏器,折腾了一下发现可以把优酷腾讯爱奇艺这几个大厂的app投屏过去,但是芒果app的投屏功能就不稳定,B站app压根没有投屏功能,更别说其他杂七杂八的网站和视频源了。

于是翻出一年半前就停滞开发的imchenwen,经过三周的折腾,基本实现了内置播放器和DLNA投屏的功能。目前内置播放器功能比较完善,可以流畅观看绝大多数的视频包括电视直播,DLNA投屏倒是能投了,但是进度条要么不好拖,要么干脆不能拖,不过目前勉强够用了。

一些screenshots:

内置播放器

选项设置

电视直播

地址解析

大概说一下开发要点:

  1. 内置播放器是通过引入libmpv解决的,github上有libmpv官方的Qt使用的示例,非常简单易用,但是用老式的被deprecated的opengl-cb接口,新的render API用法官方示例是没有,但我通过看另一个项目MoonPlayer的用法,再自己连蒙带猜也试出来了,不直接用那代码的原因是imchenwen是基于QWidget的实现,MoonPlayer是基于QML的实现。后来发现另外有个Media Player Classic Qute Theater的项目,就是直接用QWidget+render API的实现的,看到得晚了。

  2. 无论是被deprecated的opengl-cb还是render API,视频输出都只能使用opengl-cb/opengl或libmpv,如果换成其他的,比如direct3d之类的,libmpv就会自己创建个窗口进行视频输出,所以不能用。网上倒是有OpenGL ES接口转DirectX或转Metal的库,可以试一下。

  3. 一年半前遇到的一个问题是如果视频被切成多段,用播放器播放会断一下,这个问题可以通过合成一个m3u8,给mpv设置一个prefetch-playlist的参数解决。

  4. mpv/libmpv对网络视频的支持比较好,提供了比其他播放器更多的参数,比如可以设置http的headers,可以单独设置referer,甚至可以设置cookie,更不用提http GET方法在URL后面加的query参数了。HLS流和RTSP流也能直接播放。

  5. DLNA投屏限制比较多,据我观察下来,它能支持的URL和文件格式就比较严格,比如URL后面query参数比较多的话,基本不能播放,文件格式m3u8似乎就不行,大概只支持最常见的mp4、flv、ts等。也不知道是因为我买的投屏器比较山寨,还是说业界都是这么搞的。

  6. 所以从网上弄来的视频地址有几种情况,要分别处理。最简单的一种情况是直接一个裸的诸如http://www.xxx.com/path/media.mp4这种URL,可以直接提交给投屏器播放。

  7. 如果是带query参数的,比如http://www.xxx.com/path/media.mp4?query1=value1&query2=value2&query3=value3,需要把URL转换一下,我的办法是在本地起一个http server,把本地的URL比如http://192.168.1.100:12345/path/media.mp4提交给投屏器,http server接收到GET请求时,程序自己启动一个http client去请求视频原始的URL,把收到的视频数据从http server转发给投屏器。

  8. 如果是一个m3u8,比如http://www.xxx.com/path.m3u8,则本地起一个http server,并提供一个投屏器支持的格式比如http://192.168.1.100:12345/path/media.ts,再程序自己去请求原始m3u8,解析出m3u8中视频的真实地址再继续请求回来视频数据,再转发给投屏器。

  9. m3u8本身也有几种情况,一种是里面就是一组视频真实地址,诸如http://www.xxx.com/path/media.flv?query1=value1&query2=valu2等等,这种就照第7步做。另一种是里面是相对路径,比如rpath/media.flv这种,要先拼出绝对路径的URL,再照第7步做。还有一种是里面是另一个m3u8的地址,可能是绝对路径,或能是相对路径,在得到绝对路径后再请求m3u8内容,直到最后得到视频真实地址,再照之前的做。

  10. 视频有可能是已经被分段的几个URL,需要自己拼成一个m3u8,再照第8步和第9步做。拼成m3u8时要加入#EXT-X-TARGETDURATION:#EXTINF:,后面的数字可以随意,只要前者的比后者的大就行,播放器在收到视频数据时能自动计算出视频的真实时长。

  11. 投屏器不像mpv有prefetch-playlist这样的参数,所以我现在的做法是把多段视频用FFmpeg合并成一段,全都只转封装成ts流,不转编码,那样就不会太耗时和耗CPU,我用2013Mid的MBA跑,也能即时转出来给投屏器用,基本不会卡,电视机分辨率不高的话,转个720p的就够了。

  12. FFmpeg转出来的ts流直接存到本地硬盘文件,http server再读硬盘文件转发给投屏器,经过试验,http header中不要带Content-Length,而且Content-Type必须是application/octet-stream,投屏器才会不断地接收http server喂给的数据,其他文件格式、Content-LengthContent-Type都可能会导致投屏器要么干脆没反应,要么有声音没图像,要么只有前面一段内容。

  13. 爱奇艺有视频有.265ts的格式,跟其他网站基本用H264的不同,用FFmpeg转封装要用不同命令。目前我分别用这两条目前转封装:

    ffmepg -y -protocol_whitelist "file,http,https,tcp,tls" -i http://127.0.0.1:12345/media.m3u8 -c:v libx265 -c:a aac -copy ts media.ts
    
    ffmepg -y -protocol_whitelist "file,http,https,tcp,tls" -i http://127.0.0.1:12345/media.m3u8 -c:v copy -c:a aac -copy ts media.ts
    

    FFmpeg了解不多,不知道有没有更好的办法。

  14. 现在的问题是:不支持HLS/RTSP投屏;没有手机端;mac上用QWebEngine太重了,用系统的WebView大概会好一些。

]]>
试用基于有赞云的个人网站在线收款解决方案 2018-08-07T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/08/try-youzan-pay 老早就注册有赞云了,之前随便玩了一下,感觉可以用,加上自己对web开发并不了解,就放下了。

今天突然心血来潮,用Go折腾了一下,用于demo是很简单的,有赞的文档也还不错,虽然并不了解web开发,但拿别人的代码过来改改问题不大。

主要的工作流程为:

  1. 从web上获取相关信息,比如价格、客户资料等,创建收款二维码
  2. 有赞云返回二维码的id、url及base64编码后的图像信息,程序记录二维码id及用户信息的对应关系,后面有用
  3. web页用js通过websocket获取二维码url或base64编码后的图像信息,并显示
  4. 用户使用微信或支付宝扫一扫二维码进行支付
  5. 扫完二维码后会跳转到有赞的一个页面,点击该页面上的支付按钮,有赞会推送消息到后台设置的回调地址上,状态是WAIT_BUYER_PAY
  6. 用户支付完成后,有赞会再次推送消息,状态是TRADE_SUCCESS
  7. 有赞推送的消息中只包含订单号,程序要通过订单号反查对应的二维码id,再查到用户信息完成一次交易

这个方案的优点是无需公司资质,无需接入支付宝和微信,可使用支付宝和微信扫码支付,支持储蓄卡和信用卡。

缺点是只能在网站上使用,在手机上不能唤起支付宝和微信app。另外据说有赞的风控比较严格,动不动就被冻结资金不能提现,需要找客服解冻。

程序代码在这里,代码很乱,仅供演示。在线demo在这里,可以输入小额金额进行支付体验。

]]>
tinc后续 2018-07-26T00:00:00+00:00 类库大魔王/missdeer https://minidump.info//2018/07/tinc-follow-up 之前用tinc构建了虚拟专网,实现了在不同局域网内的机器通过tinc互相访问,但是遇到一个问题,我想从公司里访问到家里其他没有装tinc的机器,或者从家里其他没有装tinc的机器访问到公司里装了tinc的机器。经过一番简单的设置便可以达到想要的效果。

家里一台HTPC装了tinc,新增IP为192.168.88.3,内网IP为192.168.66.110,家里所有机器网关都指向软路由,假设IP为192.168.66.1,修改相应配置。

修改所有机器上的/etc/tinc/bignet/hosts/shhtpc文件,头部增加一行:

Subnet = 192.168.66.0/24

刷新tinc的配置,要用sudo

sudo tincd -n bignet -kHUP

设置流量转发,修改/etc/sysctl.conf

net.ipv4.ip_forward=1

执行以下命令使其生效:

sudo sysctl -p

修改路由器192.68.66.1上的静态路由:

sudo ip route add 192.168.88.0/24 via 192.168.66.110

至此,家里所有机器的流量先全部流到路由器,路由器发现目标IP地址是192.168.88.0/24范围内的,重路由到HTPC上(192.168.66.110)。

修改公司所有机器上的/etc/tinc/bignet/tinc-up文件,增加一条静态路由:

sudo route add -net 192.168.66.0/24 192.168.88.3 255.255.255.0

即把192.168.66.0/24网段的流量路由到HTPC上。

因为我家里的路由器和HTPC使用的操作系统都是Linux,公司的电脑都是macOS系统,所以增加静态路由使用不同的命令。

至此,公司可以直接访问192.168.66.0/24网段内的所有机器。


tinc装在Windows上只需要装好tap-Windows驱动,设置好虚拟IP和子网掩码,就不需要手动设置路由,但在上面这种情况下,需要另外加一条静态路由,以管理员身份运行cmd.exe,输入命令:

route add 192.168.66.0 mask 255.255.255.0 192.168.88.6 metric 3

命令中192.168.88.6是本机的tinc虚拟IP,我猜改成HTPC的IP应该也可以,没试过。


由于不能控制公司的路由器,所以不能给公司所有主机修改路由设置,如果要从家里访问公司里其他主机,比如访问公司内网的网站,我认为比较简单的方法是修改家中的路由器设置:

  1. 开一个SSH隧道到公司任意一台装了tinc也装了OpenSSH server的机器:

    ssh -D 8089 -f -C -q -N username@192.168.88.4
    

    其中192.168.88.4就是公司某一台装了tinc也装了OpenSSH server的机器。

  2. 装redsocks,并指向SSH隧道开的本地端口:

    ip = 127.0.0.1;
    port = 8089;
    type = socks5;
    

    假设redsocks监听在58096端口上。

  3. 修改iptables防火墙设置:

    -A SS -d 10.0.0.0/8 -p tcp -j REDIRECT --to-ports 58096
    -A SS -d 173.36.0.0/14 -p tcp -j REDIRECT --to-ports 58096
    -A SS -d 172.0.0.0/8 -p tcp -j REDIRECT --to-ports 58096
    -A SS -d 171.68.0.0/14 -p tcp -j REDIRECT --to-ports 58096
    -A SS -d 72.163.0.0/16 -p tcp -j REDIRECT --to-ports 58096
    

    把所有公司内网用到的IP段流量都重定向到58096端口。

  4. 在公司某一台装了tinc的机器上装一个DNS server,在家里把所有公司内网用到的域名都使用该DNS server进行解析,比如dnsmasq设置为:

    server=/.cisco.com/192.168.88.4#35353
    server=/.webex.com/192.168.88.4#35353
    server=/.webexconnect.com/192.168.88.4#35353
    server=/.wbx2.com/192.168.88.4#35353
    

    其中192.168.88.4就是公司某一台装了DNS server的机器,监听在35353端口。

这时,家里所有机器应该都可以访问公司内网的网站,登录收发邮件等等。

我之前并没有使用SSH隧道,而是在公司机器上使用polipo提供http代理,发现它并不能正确代理非http端口的流量请求,而SSH隧道就没这个问题,不知是polipo的限制,还是因为SSH隧道使用socks5协议是会话层协议的缘故。

]]>