[{"data":1,"prerenderedAt":770},["ShallowReactive",2],{"blog-page-24":3,"blog-count":769},[4,160,398,476,622],{"id":5,"title":6,"body":7,"date":136,"description":13,"extension":137,"meta":138,"navigation":141,"path":153,"seo":154,"stem":155,"tags":156,"__hash__":159},"blogs\u002F_legacy\u002F2015\u002F2015-05-21-ue4-fmod-visulization.md","UE4与FMod音乐可视化验证",{"type":8,"value":9,"toc":132},"minimark",[10,14,17,20,31,34,37,40,46,49,55,58,61,67,70,76,81,84,90,93,99,102,108,111,117,120,123,129],[11,12,13],"p",{},"制作音游的一个重要步骤，就是验证引擎的相关技术特性，以此来决定是否更换引擎或者更改设计。",[11,15,16],{},"当前UE版本4.7.6；FMod版本1.06.02；",[11,18,19],{},"由于需要播放用户提供的音乐文件，所以需要读取用户设备上的文件。在翻阅文档之后，发现可以使用PlatformFileManager读取外部文件相关信息，同时也有获取目录信息的相关接口。",[21,22,27],"pre",{"className":23,"code":25,"language":26},[24],"language-text","FString CompleteFilePath = \"H:\u002Fmusic\u002Ftest.mp3\";\nif (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*CompleteFilePath))\n{\nOutputDebugStringA(\"Error\");\nreturn 0;\n}\n\nconst int32 FileSize = FPlatformFileManager::Get().GetPlatformFile().FileSize(*CompleteFilePath);\n","text",[28,29,25],"code",{"__ignoreMap":30},"",[11,32,33],{},"通过上面的代码，成功获得文件大小。外部文件相关验证完成。",[11,35,36],{},"接下来就是音乐播放相关的实现了，FMod在UE4上有插件实现。基本可以满足使用需求。",[11,38,39],{},"下载并将FMod插件解压到UE4目录之后，首先在项目的*.Build.cs中添加下面的代码",[21,41,44],{"className":42,"code":43,"language":26},[24],"public class mpop : ModuleRules\n{\npublic mpop(TargetInfo Target)\n{\nPublicDependencyModuleNames.AddRange(new string[] { \"Core\", \"CoreUObject\", \"Engine\", \"InputCore\" });\n\nPrivateDependencyModuleNames.AddRange(new string[] { \"FMODStudio\" });\n}\n}\n",[28,45,43],{"__ignoreMap":30},[11,47,48],{},"这样一来在需要使用到FMod地方进行文件包含就不会遇到报错了",[21,50,53],{"className":51,"code":52,"language":26},[24],"#include \"fmod_studio.hpp\"\n",[28,54,52],{"__ignoreMap":30},[11,56,57],{},"如果在编译时遇到LNK1181之类的编译错误，建议按照提示查找FMod插件中是否提供了对应的Lib文件。如果没有的话可能需要重新编译插件或者使用更早期有提供的版本。",[11,59,60],{},"使用FMod底层API的第一步是得到一个底层的API System，根据使用方式的不同有两条路径。",[11,62,63],{},[64,65,66],"strong",{},"获取插件系统",[11,68,69],{},"适合与EndPlay和BeginPlay之类的事件挂钩进行使用。不需要自主使用update，插件内部已经有更新机制了。",[21,71,74],{"className":72,"code":73,"language":26},[24],"result = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime)->getLowLevelSystem(&system);\n\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = system->getVersion(&version);\n\nif (result != FMOD_RESULT::FMOD_OK) break;\n",[28,75,73],{"__ignoreMap":30},[11,77,78],{},[64,79,80],{},"自主创建系统",[11,82,83],{},"适合配合单例模式使用，和插件建立的系统相对独立。不过依然不需要自己进行update操作。与直接获取不同的是，需要自己进行初始化操作。",[21,85,88],{"className":86,"code":87,"language":26},[24],"result = FMOD::System_Create(&system);\n\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = system->getVersion(&version);\n\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = system->init(32, FMOD_INIT_NORMAL, extradriverdata);    \u002F\u002F \u003C进行初始化\n\nif (result != FMOD_RESULT::FMOD_OK) break;\n",[28,89,87],{"__ignoreMap":30},[11,91,92],{},"系统创建之后，音乐播放使用createsound即可",[21,94,97],{"className":95,"code":96,"language":26},[24],"result = system->createSound(\"H:\u002Fmusic\u002Ftest.mp3\", FMOD_LOOP_NORMAL | FMOD_2D, 0, &sound);\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = sound->getNumSubSounds(&numsubsounds);\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nif (numsubsounds)\n{\nsound->getSubSound(0, &sound_to_play);\nif (result != FMOD_RESULT::FMOD_OK) break;\n}\nelse\n{\nsound_to_play = sound;\n}\n\nresult = system->playSound(sound_to_play, 0, false, &channel);\nif (result != FMOD_RESULT::FMOD_OK) break;\n",[28,98,96],{"__ignoreMap":30},[11,100,101],{},"要对音乐进行可视化，最基础的数据获取方式是对音乐进行快速傅里叶变换（FFT）。FMod本身有提供FFT的功能，因此直接使用即可，无需再造车轮。FMod提供的FFT是DSP的一种，可以加载在Channel、ChannelGroup、ChannelControl上。这里，我们选择在系统创建并初始化之后直接加载到ChannelGroup上。",[21,103,106],{"className":104,"code":105,"language":26},[24],"FMOD::ChannelGroup *mastergroup;\n\nresult = system->createDSPByType(FMOD_DSP_TYPE_FFT, &fftdsp);\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = system->getMasterChannelGroup(&mastergroup);\nif (result != FMOD_RESULT::FMOD_OK) break;\n\nresult = mastergroup->addDSP(0, fftdsp);\nif (result != FMOD_RESULT::FMOD_OK) break;\n",[28,107,105],{"__ignoreMap":30},[11,109,110],{},"在每一次Ontick时进行FFT数据的获取：",[21,112,115],{"className":113,"code":114,"language":26},[24],"FMOD_DSP_PARAMETER_FFT *fft;\n\nchar* ts = new char[40];\nfftdsp->getParameterData(FMOD_DSP_FFT_SPECTRUMDATA, (void **)&fft, 0, 0, 0);\nOutputDebugStringA(\"\u003CFMODHolders> - Update Begin -\\n\");\nfor (int channel = 0; channel \u003C fft->numchannels; channel++){\nOutputDebugStringA(\"> -c-\");\nfor (int bin = 0; bin \u003C fft->length; bin++)\n{\nfloat val = fft->spectrum[channel][bin];\nsprintf(ts, \"> spec: %f \\n\", val);\nOutputDebugStringA(ts);\n}\n}\nOutputDebugStringA(\"\u003CFMODHolders> - Update End -\\n\");\ndelete ts;\n",[28,116,114],{"__ignoreMap":30},[11,118,119],{},"开始播放之后就可以看到fft数据已经正常输出了。",[11,121,122],{},"为了方便对fft的数据进行解析，需要获得采样率。",[21,124,127],{"className":125,"code":126,"language":26},[24],"system->getSoftwareFormat(&tnp, NULL, NULL);\n",[28,128,126],{"__ignoreMap":30},[11,130,131],{},"这样基本上音乐数据一块的验证就完成了。",{"title":30,"searchDepth":133,"depth":134,"links":135},2,3,[],"2015-05-21","md",{"layout":139,"status":140,"published":141,"author":142,"author_login":143,"author_email":144,"author_url":145,"wordpress_id":146,"wordpress_url":147,"date_gmt":148,"excerpt":149},"post","publish",true,{"display_name":143,"login":143,"email":144,"url":145},"chaoshikari","chaoshikari@gmail.com","\u002F",1272,"\u002F\u002F?p=1272","2015-05-21 04:09:36 +0000",{"type":8,"value":150},[151],[11,152,13],{},"\u002F2015-05-21-ue4-fmod-visulization",{"title":6,"description":13},"_legacy\u002F2015\u002F2015-05-21-ue4-fmod-visulization",[157,158],"UE4","FMod","53jn81bS0zOR_BQlQpG7tZSyEzEGNvhY4okZF-i8EkU",{"id":161,"title":162,"body":163,"date":382,"description":167,"extension":137,"meta":383,"navigation":141,"path":392,"seo":393,"stem":394,"tags":395,"__hash__":397},"blogs\u002F_legacy\u002F2015\u002F2015-05-02-ue4-and-mmd.md","UE4与MMD",{"type":8,"value":164,"toc":377},[165,168,196,199,204,207,214,222,227,230,233,240,243,247,250,253,256,261,264,270,273,278,281,284,290,295,298,303,306,310,313,323,326,332,335,341,344,347,350,355,358,363,366,371,374],[11,166,167],{},"上次对MMD的导入进行探索之后已经有大半年过去了，各个工具的版本都更新了不少。导入的方式也发生了很多改变，经过了这些时间的研究和学习，对UE4本身的了解也变深了。因此对mmd的导入进行重新的整理。",[11,169,170,171,177,178,183,184,189,190,195],{},"当前",[172,173,157],"a",{"href":174,"rel":175},"https:\u002F\u002Fwww.unrealengine.com\u002Fzh-CN\u002F",[176],"nofollow","版本4.7.6，",[172,179,182],{"href":180,"rel":181},"https:\u002F\u002Fwww.blender.org\u002F",[176],"blender","版本2.7.4，",[172,185,188],{"href":186,"rel":187},"http:\u002F\u002Fstereoarts.jp\u002F",[176],"pmx2fbx","版本Beta_20150428_2，",[172,191,194],{"href":192,"rel":193},"https:\u002F\u002Fgithub.com\u002Fsugiany\u002Fblender_mmd_tools",[176],"blender_mmd_tools","版本0.5。",[11,197,198],{},"导入路径方面，还是采用原先得出的两种方式。事实是，mmd几乎在所有的主流3d建模软件上都有相关的读取工具被制作出来，但是要同时兼顾对pmx格式的兼容以及保留流畅的ue4导入的话，还是这两种导入路径比较方便一些。随着时间不断的前进，各个社区说不定也会对相关工具进行改进。目前而言，blender_mmd_tools算得上是非常好的选择了。",[200,201,203],"h2",{"id":202},"路径一pmx2fbx","路径一：PMX2FBX",[11,205,206],{},"使用pmx2fbx会生成如下这些文件",[11,208,209],{},[210,211],"img",{"alt":212,"src":213},"image","\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb.png",[11,215,216,217,221],{},"我们需要的只有shana.fbx文件，使用",[172,218,220],{"href":219},"\u002F?p=964","转义工具","转换编码之后，在UE4中导入",[11,223,224],{},[210,225],{"alt":212,"src":226},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb1.png",[11,228,229],{},"注意去掉Use T0As Ref Pos这个选项，否则导入的模型就有可能使用错误的基础姿势，导致形变动画出错。",[11,231,232],{},"由于这个工具本身是为了Unity而设计的，所以贴图信息被另外的保存了，导入到UE4时就需要对贴图进行重新调整。使用这个方式导入的主要优点是，可以同时进行多个vmd动作的导入。缺陷是，vmd动作的形变动画无法正确载入，以及需要对材质进行大量的修改。如果使用的模型非常复杂的话，工作量将会变得很大。而在骨骼相同的情况下，动画动作是可以共用的。",[11,234,235,236],{},"[更新]",[237,238,239],"span",{},"2015-8-15",[11,241,242],{},"经过测试发现，当前最新版本下，VMD中的形变动画是可以手动通过曲线添加到动画中去的。因此，在动画动作比较多的情况下，推荐使用这条路径进行导入。如果材质较为复杂，可以采用路径二中的blender导入方式，但只是单独的导出Mesh，这样就可以利用其中生成的材质了。",[200,244,246],{"id":245},"路径二blender","路径二：Blender",[11,248,249],{},"收益于Blender社区的配合和UE的不断改进，目前通过blender进行mmd导入的操作已经非常的方便。",[11,251,252],{},"在blender方面，已经不需要对fbx的导出插件进行修改就可以直接使用了。blender_mmd_tools的版本也已经进行了更新，当前版本已经可以通过blender进行MMD模型的导入然后重新导出为pmd格式而不出错了。",[11,254,255],{},"导入路径上，首先的操作依然是对模型进行导入。比例和原先一样，依然是8。",[11,257,258],{},[210,259],{"alt":212,"src":260},"\u002Fwp-content\u002Fuploads\u002F2014\u002F08\u002Fimage_thumb3.png",[11,262,263],{},"导入成功后，选中整个模型，点击RigidBody下的build",[11,265,266],{},[210,267],{"alt":268,"src":269},"SNAGHTML647002","\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002FSNAGHTML647002_thumb.png",[11,271,272],{},"这个版本的mmd_tools会自动添加一个名为“両目.shadow”的骨骼。为了防止多根骨错误，需要对骨骼的顺序进行调整。进入编辑模式，选中“両目.shadow”，将其父级指定为“全ての親”。",[11,274,275],{},[210,276],{"alt":212,"src":277},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb2.png",[11,279,280],{},"然后选中模型，点击mmd_tools工具面板里的Import Motion导入vmd动作。",[11,282,283],{},"动作导入完成之后，可以添加灯光进行渲染测试。也可以直接进行导出。目前的版本下已经不需要对世界的单位进行修改了，直接进行导出即可。",[11,285,286,287,289],{},"通过",[172,288,220],{"href":219},"进行转换之后，接下来就是在UE4中导入了，导入选项如下。",[11,291,292],{},[210,293],{"alt":212,"src":294},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb3.png",[11,296,297],{},"导入之后除了可能的少许材质修正之外，基本可以直接使用了。",[11,299,300],{},[210,301],{"alt":212,"src":302},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb4.png",[11,304,305],{},"这种导入方式最大的好处是，vmd动作中的形变动画也会被保留。同时材质上大体都是正常的，需要进行修改的部分比较少。",[11,307,235,308],{},[237,309,239],{},[11,311,312],{},"本路径受到blender_mmd_tools的版本更新的影响较大。当前版本下，有如下变动：",[314,315,316,320],"ol",{},[317,318,319],"li",{},"导出之前需要像以前一样，将世界单位改为“公制”-“0.01”",[317,321,322],{},"需要将rigidbodies一列节点从模型的树结构中移出，否则会报多重骨骼以及骨骼重名错误",[11,324,325],{},"rigidbodies的移出结果大致如下图：",[11,327,328],{},[210,329],{"alt":330,"src":331},"tq","\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Ftq.jpg",[11,333,334],{},"同时，对于复杂的模型依然会有材质出错的情况。一般情况下出错的修复方式如下，具体情况会有所不同，这种做法权做快速修复：",[11,336,337],{},[210,338],{"alt":339,"src":340},"2015-08-15_21-27-08","\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002F2015-08-15_21-27-08.jpg",[11,342,343],{},"参照原始模型进行快速修改即可，之后在游戏制作过程中根据设计的不同再进行材质的进一步加工。",[200,345,346],{"id":346},"形变动画报错",[11,348,349],{},"以上两种路径中，在导入过程中，会出现如下错误提示",[11,351,352],{},[210,353],{"alt":212,"src":354},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb5.png",[11,356,357],{},"主要原因是材质没有启用形变，虽然可以通过手动对提示的材质进行修改。但更加快速的方法是直接拖动右侧的形变动画进行预览，那么相应的材质就会自动的被启用形变了。",[11,359,360],{},[210,361],{"alt":212,"src":362},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb6.png",[11,364,365],{},"等待编译完成并保存之后，错误提示就会消失。形变动画也可以正常使用了。",[11,367,368],{},[210,369],{"alt":212,"src":370},"\u002Fwp-content\u002Fuploads\u002F2015\u002F05\u002Fimage_thumb7.png",[372,373],"hr",{},[11,375,376],{},"到目前为止，将MMD作为人物模型导入使用已经基本上可以正常的使用了。对于建模比较苦手的话，直接采用别人建设好的模型和动作数据比较快捷一些。",{"title":30,"searchDepth":133,"depth":134,"links":378},[379,380,381],{"id":202,"depth":133,"text":203},{"id":245,"depth":133,"text":246},{"id":346,"depth":133,"text":346},"2015-05-02",{"layout":139,"status":140,"published":141,"author":384,"author_login":143,"author_email":144,"author_url":145,"wordpress_id":385,"wordpress_url":386,"date_gmt":387,"excerpt":388},{"display_name":143,"login":143,"email":144,"url":145},1250,"\u002F\u002F?p=1250","2015-05-02 10:10:17 +0000",{"type":8,"value":389},[390],[11,391,167],{},"\u002F2015-05-02-ue4-and-mmd",{"title":162,"description":167},"_legacy\u002F2015\u002F2015-05-02-ue4-and-mmd",[157,396],"MMD","j4XgdJDtCdrZptWG5tu886-lK2BLq4nU8AwzmmjSdhY",{"id":399,"title":400,"body":401,"date":457,"description":405,"extension":137,"meta":458,"navigation":141,"path":470,"seo":471,"stem":472,"tags":473,"__hash__":475},"blogs\u002F_legacy\u002F2015\u002F2015-05-01-pluto-and-new-horizons.md","新视野号与冥王星",{"type":8,"value":402,"toc":455},[403,406,409,420,423,426,431,434,437,440,443,446,449,452],[11,404,405],{},"或许是因为科幻类的电影和动漫看得太多了，在近日新视野号有关的新闻不断传出时，心中居然没有泛起一点涟漪。",[11,407,408],{},"其实这是一件非常伟大的事情，至此，太阳系内的“九大行星”就都已经被人类“造访”过了。之所以要加引号，是因为在新视野号离开地球后不久，于2006年8月24日，行星的定义被重新确立，冥王星被从九大行星中除名。国际天文联合（IAU）对行星的资格要求变为了如下三条：",[314,410,411,414,417],{},[317,412,413],{},"轨道是环绕着太阳；",[317,415,416],{},"有足够的质量能维持流体静力平衡（接近球体的形状）；",[317,418,419],{},"能清除“相似轨道上”的其它天体。",[11,421,422],{},"这样一来，冥王星由于只有轨道上所有天体质量之和的7%而无法满足条件三而成为了矮行星。然而这丝毫不会减少本次新视野号探测任务的历史重要性。",[11,424,425],{},"目前天文学中，太阳系被分为三个区域。第一个区域是包括地球在内的岩石星球区域、第二个区域是木星等气态星球区域、而第三个区域就是以冥王星为代表的柯伊伯带。柯伊伯带星体大多数由冷冻的甲烷、氨、水等物质构成，这些“冰块”是40亿年前太阳系形成时就存在的遗迹。对这个区域的探索能够让人类更好的理解太阳系以及地球本身。",[11,427,428],{},[210,429],{"alt":212,"src":430},"\u002Fwp-content\u002Fuploads\u002F2015\u002F08\u002Fimage_thumb.png",[11,432,433],{},"新视野号目前距离地球约47亿公里，电波信号要经过4小时25分钟才能到达地球。而且由于距离太远造成的信号衰减，以及传输时间持续问题，必须使用“深空网络”才能成功的接收信息。",[11,435,436],{},"深空网络是NASA为支援宇宙探索任务而建立的国际天线网络。深空网络由三处呈120度分布的深空通信设施构成，一处在美国加州的戈尔德斯通，处于巴斯托市附近的莫哈维沙漠之中；一处位于西班牙马德里附近；另一处位于澳大利亚的堪培拉附近。",[11,438,439],{},"在没有科幻作品中所使用的虫洞、量子通信等超级科技的情况下，要将一个探测器送到47亿公里之外是一件非常复杂的系统工程。而实际上，探测器本身能够对冥王星进行拍照探测的时间是非常短的。由于延迟实在是太长，要对探测器进行精度要求非常高的轨道变换几乎是不可能的。",[11,441,442],{},"在7月12日传输了最后一张照片之后，新视野号进入了最紧张的拍摄和探测工作之中。在此期间，探测器无法与地球进行联系，直到探测器超过最近点12小时之后才会重新与地球进行联系。",[11,444,445],{},"这三天的等待时间是非常漫长的，任何一点小的意外，例如出现在探测器轨道上的碎片，都将导致探测器被摧毁。而对于人类而言，要想知道如此遥远的地方的探测器究竟发生了什么是不可能的。",[11,447,448],{},"好在一切都在计划之中，新视野号已经完成了拍摄探测的任务，重新与地球取得了联络。在离开最近点之后，紧张的拍摄和联络也依旧要持续到7月18日。",[11,450,451],{},"在今后的15个月的时间中，这些珍贵的数据将会陆续的传回地球，而科学家们将会对其进行分析与研究。",[11,453,454],{},"目前为止，新视野号已经为我们带来了很多惊喜以及一些等待解释的新发现。相信随着科技的不断进步，人类最终登上这颗遥远的星球的时刻并不会太遥远。",{"title":30,"searchDepth":133,"depth":134,"links":456},[],"2015-05-01",{"layout":139,"status":140,"published":141,"author":459,"author_login":461,"author_email":462,"wordpress_id":463,"wordpress_url":464,"date_gmt":465,"excerpt":466},{"display_name":460,"login":461,"email":462,"url":30},"风铃","flinkor","flinkor@foxmail.com",1424,"\u002F\u002F?p=1424","2015-05-01 07:01:32 +0000",{"type":8,"value":467},[468],[11,469,405],{},"\u002F2015-05-01-pluto-and-new-horizons",{"title":400,"description":405},"_legacy\u002F2015\u002F2015-05-01-pluto-and-new-horizons",[474],"天文","DJQeph2Roer0_K-sfuH-At2hw7G5M1ytjRNxaDIa_uk",{"id":477,"title":478,"body":479,"date":605,"description":483,"extension":137,"meta":606,"navigation":141,"path":615,"seo":616,"stem":617,"tags":618,"__hash__":621},"blogs\u002F_legacy\u002F2015\u002F2015-04-30-%e6%8b%be%e9%81%97.md","拾遗",{"type":8,"value":480,"toc":599},[481,484,488,491,494,501,504,508,511,514,520,523,529,532,535,538,544,547,553,556,559,565,568,572,575,578,584,587,593,596],[11,482,483],{},"有的问题，花费了时间解决之后，由于短时间内不会用到，过了一段之后遇到了又需要重新寻找解决方案。故而在此记录下来，以节省时间花费。",[200,485,487],{"id":486},"qq登录","QQ登录",[11,489,490],{},"QQ登录的功能相对简单，但是在进行应用调试的时候可能会遇到这样的问题：",[11,492,493],{},"QQ登录回调时出现如下的报错：redirect uri is illegal(100010)",[11,495,496,497,500],{},"解决方法是，在URL的结尾加上斜杠，如",[172,498,499],{"href":145},"\u002F\u002F","。",[11,502,503],{},"QQ互联官方对信息的修改有一定延迟，测试时需要适当的拉出空隙。提交之前也最好等待一下，否则审核的时候可能会因为不能正常使用而被拒绝。",[200,505,507],{"id":506},"iptables","IPTABLES",[11,509,510],{},"Ubuntu上的iptables设置，网上教程很多，但是要做出适合自己需求的配置则需要仔细的研究。",[11,512,513],{},"配置需求：开放22、80端口。同时服务器需要进行抓取、回调等任务。",[21,515,518],{"className":516,"code":517,"language":26},[24],"iptables -A INPUT -i lo -j ACCEPT \niptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT \niptables -A INPUT -p tcp --dport 22  -j ACCEPT \niptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT \niptables -A INPUT -p tcp -s 0\u002F0 --dport 80 -j ACCEPT \niptables -A OUTPUT  -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT \niptables -A INPUT -p udp --sport 53 -m state --state ESTABLISHED -j ACCEPT \niptables -A OUTPUT  -p tcp --dport 80 -j ACCEPT \niptables -A INPUT -p tcp -s 0\u002F0 --sport 80 -m state --state ESTABLISHED -j ACCEPT \niptables -A INPUT -j DROP\n",[28,519,517],{"__ignoreMap":30},[11,521,522],{},"常用命令：",[21,524,527],{"className":525,"code":526,"language":26},[24],"#保存配置\niptables-save\n\n#列出当前配置\niptables -L\n",[28,528,526],{"__ignoreMap":30},[200,530,531],{"id":531},"服务器空间问题",[11,533,534],{},"服务器出现\u002Fdev\u002Fxvda1达到100%或者文件写入被拒绝write failed: No space left on device时的解决方法。",[11,536,537],{},"使用命令",[21,539,542],{"className":540,"code":541,"language":26},[24],"df –h\n",[28,543,541],{"__ignoreMap":30},[11,545,546],{},"检查是否有除此之外的异常配比，如果没有的情况下，切换到根目录，通过目录大小进行排查",[21,548,551],{"className":549,"code":550,"language":26},[24],"du -h --max-depth=1 .\u002F\n",[28,552,550],{"__ignoreMap":30},[11,554,555],{},"通过列出的大小逐级寻找，可以定位出问题文件夹。",[11,557,558],{},"如果在根目录列出的大小和\u002Fdev\u002Fxvda1的大小差异极大，那么问有可能是文件删除之后依然被某些进程打开，导致无端占用磁盘空间。列出所有这些文件",[21,560,563],{"className":561,"code":562,"language":26},[24],"lsof | grep \"(deleted)\"\n",[28,564,562],{"__ignoreMap":30},[11,566,567],{},"之后就是寻找这些文件产生原因的地狱之旅了……紧急情况下可以使用重启的方法来暂时缓解危机。",[200,569,571],{"id":570},"bootsrap与wordpress","Bootsrap与Wordpress",[11,573,574],{},"当使用bootstrap做主题时，顶部的置顶导航条会和管理功能的admin-bar产生冲突。解决方法是，",[11,576,577],{},"首先，对body的class进行修改(通常在header.php中)",[21,579,582],{"className":580,"code":581,"language":26},[24],"\u003Cbody \u003C?php body_class(); ?>>\n",[28,583,581],{"__ignoreMap":30},[11,585,586],{},"然后添加对应的css：",[21,588,591],{"className":589,"code":590,"language":26},[24],"@media ( min-width: 782px ) {body{ padding-top: 70px !important; }}\n\nbody.body-logged-in .navbar-fixed-top{ top: 28px !important; }\n\nbody.logged-in .navbar-fixed-top{ top: 28px !important; }\n",[28,592,590],{"__ignoreMap":30},[11,594,595],{},"刷新一下之后，应该就不会出现相互遮盖的问题了。",[11,597,598],{},"注意：当前使用的wordpress版本4.2.1，版本不同的情况下解决方法可能会出现不同。",{"title":30,"searchDepth":133,"depth":134,"links":600},[601,602,603,604],{"id":486,"depth":133,"text":487},{"id":506,"depth":133,"text":507},{"id":531,"depth":133,"text":531},{"id":570,"depth":133,"text":571},"2015-04-30",{"layout":139,"status":140,"published":141,"author":607,"author_login":143,"author_email":144,"author_url":145,"wordpress_id":608,"wordpress_url":609,"date_gmt":610,"excerpt":611},{"display_name":143,"login":143,"email":144,"url":145},1218,"\u002F\u002F?p=1218","2015-04-30 03:52:24 +0000",{"type":8,"value":612},[613],[11,614,483],{},"\u002F2015-04-30-拾遗",{"title":478,"description":483},"_legacy\u002F2015\u002F2015-04-30-%e6%8b%be%e9%81%97",[619,506,620,487],"wordpress","服务器","oiOqIKC7LS2SNjcmK2YEY8tkS9Wnq4z6QLTHWVp-kTM",{"id":623,"title":624,"body":625,"date":750,"description":629,"extension":137,"meta":751,"navigation":141,"path":760,"seo":761,"stem":762,"tags":763,"__hash__":768},"blogs\u002F_legacy\u002F2015\u002F2015-03-29-phalcon%e5%90%8e%e5%8f%b0cli%e9%82%ae%e4%bb%b6%e5%8f%91%e9%80%81%e5%8f%8a%e9%80%9a%e7%94%a8%e8%84%9a%e6%9c%ac%e8%bf%9b%e7%a8%8b%e5%ae%88%e6%8a%a4.md","Phalcon后台CLI邮件发送及通用脚本进程守护",{"type":8,"value":626,"toc":746},[627,630,633,638,647,652,655,661,664,670,673,679,682,688,691,695,698,701,704,710,713,716,722,725,731,734,737,743],[11,628,629],{},"服务器经常需要使用到一些比较耗费时间，却又不需要太强的用户实时性的任务。例如电子邮件的发送、资源抓取之类的。这些任务转到服务器后台执行相对来说比较有效率。",[11,631,632],{},"当前环境：Ubuntu14.04 LTS，Php5.6.7，Phalcon1.3.4",[634,635,637],"h3",{"id":636},"一phalcon内部的工作","一、Phalcon内部的工作",[11,639,640,641,646],{},"Phalcon提供了CLI的模块，不过以结果而言基本没用上核心的功能。因为CLI不是面向用户的，就一切从简了。PHP的邮件发送功能使用的是",[172,642,645],{"href":643,"rel":644},"https:\u002F\u002Fgithub.com\u002FPHPMailer\u002FPHPMailer",[176],"PHPMailer","，使用简单，包括Wordpress在内的很多项目都有在使用它。首先要做的是将其包装成Phalcon的一个组件，在Library目录内新建一个组件Mail.php。同时将phpmailer拷贝到这个目录中。",[11,648,649],{},[210,650],{"alt":212,"src":651},"\u002Fwp-content\u002Fuploads\u002F2015\u002F03\u002Fimage_thumb1.png",[11,653,654],{},"当前使用中的代码是这样的：",[21,656,659],{"className":657,"code":658,"language":26},[24],"\u003C?php\n\nuse Phalcon\\Mvc\\User\\Component;\nuse Phalcon\\Mvc\\View;\nuse Phalcon\\Config\\Adapter\\Ini as ConfigIni;\nrequire 'phpmailer\u002FPHPMailerAutoload.php';\n\nclass Mail extends Component\n{\n    \u002F* 发送确认邮件 *\u002F\n    public function ConfirmMail($toaddr, $data){\n        \u002F* 解析邮件数据 *\u002F\n        $jsdata = json_decode($data, true);\n        $addr = $jsdata['addr'];\n        $time = $jsdata['time'];\n        $toname = $jsdata['name'];\n        \u002F* 生成邮件模板|这里的simpleView在下面的cli-bootstrap中构造 *\u002F\n        $view = clone $this->simpleView;\n\n        $view->sitename = '发件人';\n        $view->siteaddr = '#';\n        $view->addr     = $addr;\n        $view->curtime     = $time;\n        $view->username = $toname;\n        $view->render('emails\u002Fconfirm');\n\n        $content = $view->getContent();\n        $sub = $view->sitename.\"邮箱认证\";\n        \u002F* 发送邮件 *\u002F\n        return $this->_SendMail($toaddr, $toname, $sub, $content);\n    }   \n\n    \u002F* ...... 一些其他功能 ...... *\u002F\n    \u002F* 发送邮件 *\u002F\n    protected function _SendMail($addr, $name, $sub, $content)\n    {\n        \u002F* 这里的参数说明请参照phpmailer的文档 *\u002F\n        $mail = new PHPMailer;\n        $mail->setLanguage('zh_cn');\n        $mail->CharSet = \"UTF-8\";\n        $mail->isSMTP();\n        $mail->SMTPDebug = 2;   \n        $mail->Debugoutput = 'html';\n        $mail->Host = \"smtp.qq.com\";\n        $mail->Port = 25;\n        $mail->SMTPAuth = true;\n        $mail->Username = \"no-reply@xxxx.com\";\n        $mail->Password = \"password\";\n        $mail->setFrom('no-reply@xxxx.com', '发件人');\n        $mail->addReplyTo('no-reply@xxxx.com', '发件人');\n        $mail->addAddress($addr, $name);\n        $mail->Subject = $sub;\n        $mail->msgHTML($content);   \n\n        if (!$mail->send()) {\n            echo \"邮件发送错误: \" . $mail->ErrorInfo;\n            return false;\n        } else {\n            echo \"邮件发送成功!\";\n            return true;\n        }\n    }\n\n}\n",[28,660,658],{"__ignoreMap":30},[11,662,663],{},"模块部分就像上面一样就可以了，CLI部分的结构参照官方给出的CLI结构进行。首先写一个通用的cli-bootstrap。所有的CLI都放在一个无法被外部访问的目录比较安全，如果是和我一样仿照Invo的文件结构的话，在根目录新建一个文件夹专门存放CLI脚本即可。当前使用的cli-bootsrap如下。",[21,665,668],{"className":666,"code":667,"language":26},[24],"\u003C?php\n\nerror_reporting(E_ALL);\n\nuse Phalcon\\Mvc\\Application;\nuse Phalcon\\Config\\Adapter\\Ini as ConfigIni;\n\ntry {\n\n    define('APP_PATH', realpath('..') . '\u002F');\n\n    $config = new ConfigIni(APP_PATH . 'app\u002Fconfig\u002Fconfig.ini');\n    require APP_PATH . 'app\u002Fconfig\u002Floader.php';\n    require APP_PATH . 'app\u002Fconfig\u002Fservices.php';\n    \u002F* 使用一个独立的view来为CLI生成网页 *\u002F\n    $di->set('simpleView', function() use ($config) {\n        $view = new Phalcon\\Mvc\\View\\Simple();\n        $view->setViewsDir(APP_PATH . $config->application->viewsDir);\n        \u002F* 这个步骤非常重要，否则在一个di中将无法正常的生成多个网页模板 *\u002F\n        $view->registerEngines(array(      \n            '.phtml' => function ($view, $di) use ($config) {\n                $phtml = new \\Phalcon\\Mvc\\View\\Engine\\Php($view, $di);\n                return $phtml;\n            },     \n            '.volt' => function ($view, $di) use ($config) {\n                $volt = new  \\Phalcon\\Mvc\\View\\Engine\\Volt($view, $di);          \n                return $volt;\n            }\n        ));\n        return $view;\n    });\n\n    $di = new \\Phalcon\\DI\\FactoryDefault();\n} catch (Exception $e){\n    echo $e->getMessage();\n}\n",[28,669,667],{"__ignoreMap":30},[11,671,672],{},"然后编写需要的邮件发送脚本就好了，邮件需要使用到的图片等文件保存在脚本所在目录。",[21,674,677],{"className":675,"code":676,"language":26},[24],"\u003C?php\n\nrequire 'cli-bootstrap.php';\n\nuse Phosphorum\\Mail\\SendSpool;\n\nclass SendSpoolTask extends Phalcon\\DI\\Injectable\n{\n\n    public function run()\n    {\n        \u002F* 从数据库中读取需要发送的邮件配置 *\u002F\n        $csmails = CSMail::find(array(\n            \"status = 'N' and retry \u003C3\",\n            \"order\" => \"id\",\n            \"limit\" => 10\n        ));\n\n        if(count($csmails)){\n            $res = false;\n            foreach ($csmails as $csma) {\n            $res = false;\n            $type = $csma->type;\n                switch($type){\n                    case 1:\n                    {\n                        \u002F* 通过Mail模块进行邮件发送 *\u002F\n                        $res = $this->mail->ConfirmMail($csma->to, $csma->data);\n                        break;\n                    }\n                    case 2:\n                    {\n                        $res = $this->mail->RecpassMail($csma->to, $csma->data);\n                        break;\n                    }\n                }\n                if($res) {\n                    $csma->status = 'Y';\n                }else{\n                    $csma->retry += 1;\n                }\n                $csma->save();\n            }\n\n            unset($csma);\n        }else{\n            sleep(60);        \u002F\u002F 检测间隔\n        }\n\n    }\n}\n\ntry {\n    $task = new SendSpoolTask($config);\n    while(true){\n        $task->run();\n    }\n} catch(Exception $e) {\n    echo $e->getMessage(), PHP_EOL;\n    echo $e->getTraceAsString();\n}\n",[28,678,676],{"__ignoreMap":30},[11,680,681],{},"在上面的代码中没有包含mail模块的注册代码，如果之前没有进行过类似的工作的话，请参考下面的代码：",[21,683,686],{"className":684,"code":685,"language":26},[24],"\u002F* 注册邮件发送类 *\u002F\n$di->set('mail', function(){\n    return new Mail();\n});\n",[28,687,685],{"__ignoreMap":30},[11,689,690],{},"至此，phalcon内部的操作就算是完成了，手工进行脚本执行就会开始读取数据库并发送邮件。",[634,692,694],{"id":693},"二后台执行和进程守护","二、后台执行和进程守护",[11,696,697],{},"由于之前并没有进行过这方面的操作，在完成上面的代码之后。发现要让脚本后台执行会遇到不少问题，要保证服务器在大多数情况下都能持续的执行我们想要的操作会有不少未知的问题。虽然Phalcon官方提供了Beanstalk队列的实现，php也有很多Resque这样的队列解决方案，但是在功能代码完成的情况下要再对这些进行修改并且再去学习相关的队列的操作学习的话，时间成本上实在太高。而且我们需要的无间断执行和消息队列的功能设计上也有微妙的差别。在这里犹豫了一段之后，果断决定自己动手，丰衣足食。",[11,699,700],{},"好在linux系统上要自己编码的成本还是比较低的。努力回想起关于信号量的知识，参考Google搜索的资料完成了下面的代码。",[11,702,703],{},"|2015-0501| - 更新到实际生产环境使用的版本，修复bug，简化代码，删除调试输出。",[21,705,708],{"className":706,"code":707,"language":26},[24],"#include \u003Ciostream>\n#include \u003Cfstream>\n#include \u003Clist>\n#include \u003Cmap>\n#include \u003Cstdio.h>\n#include \u003Cunistd.h>\n#include  \u003Csys\u002Fwait.h>  \u002F* for waitpid *\u002F\n#include \u003Csys\u002Ftypes.h>\n#include \u003Csys\u002Fipc.h>\n#include \u003Csys\u002Fsem.h>\n\n#define  RETRY_INTER  30  \u002F* 重试间隔 *\u002F\n\nusing namespace std;\n\nconst int       SEM_PRMS    = 0644;\nconst key_t    SEM_KEY        =  342345;\n\nbool Exists(key_t _nKey)\n{\n    int nID = semget(_nKey, 0, 0);\n    if (nID == -1) return false;\n\n    return semctl(nID, 0, GETVAL, NULL) > 0;\n}\n\nbool Mark(key_t _nKey)\n{\n    int nID = semget(_nKey, 0, 0);\n    if (nID == -1)\n        nID = semget(_nKey, 1, IPC_CREAT | IPC_EXCL | SEM_PRMS);\n\n    struct sembuf buf[2];\n    buf[0].sem_num = 0;\n    buf[0].sem_op = 0;\n    buf[0].sem_flg = IPC_NOWAIT;\n    buf[1].sem_num = 0;\n    buf[1].sem_op = 1;\n    buf[1].sem_flg = SEM_UNDO;\u002F\u002F进程退出时自动回滚\n    return semop(nID, &buf[0], 2) == 0;\n}\n\nbool Unmark(key_t _nKey)\n{\n    int nID = semget(_nKey, 0, 0);\n    if (nID == -1) return true;\n    return semctl(nID, 0, IPC_RMID, 0) == 0;\n}\n\n\u002F\u002F 维护PHP脚本的运行\nbool PhpFork(const string & tsn)\n{\n\n    pid_t tpid = fork();\n    if(tpid == 0){\n        printf(\"执行进程开始: %s|pid = %d\\n\", tsn.c_str(),getpid());\n        key_t tk = ftok(tsn.c_str(),SEM_KEY);\n        if(Exists(tk)){\n            cout \u003C\u003C \"执行进程出错: 信号量已经存在: \"\u003C\u003C  tsn  \u003C\u003C endl;\n            sleep(RETRY_INTER);\n            return true;\n        }\n\n        if(!Mark(tk)){\n            cout \u003C\u003C \"执行进程出错：锁定信号量失败\" \u003C\u003C endl;\n            sleep(RETRY_INTER);\n            return true;\n        }\n        \n        sleep(RETRY_INTER);\n        cout \u003C\u003C \"执行进程即将开始。\" \u003C\u003C endl;\n\n        printf(\"执行进程出错：%s|%d \\n\", tsn.c_str(),execl(\"\u002Fusr\u002Fbin\u002Fphp\",\"\u002Fusr\u002Fbin\u002Fphp\",tsn.c_str(), (char *)NULL) );\n        return true;\n    }else{\n        return false;\n    }\n\n}\n\nint main(void)\n{\n\n    if(Exists(SEM_KEY)){\n        \u002F\u002Fcout \u003C\u003C \"失败：已经有一个Guard实例\" \u003C\u003C endl;\n        return 1;\n    }\n\n    if(!Mark(SEM_KEY)){\n        \u002F\u002Fcout \u003C\u003C \"失败：尝试进行信号量锁定失败\" \u003C\u003C endl;\n        return 2;\n    }\n\n    printf(\"Guard开始运许|pid = %d\\n\", getpid());\n\n    \u002F\u002F 读取列表\n    list\u003Cstring>    SList;\n    \u002F\u002Fcout \u003C\u003C \"开始读取文件列表\" \u003C\u003C endl;\n    ifstream fin(\"List.txt\");\n    string s;\n    while( fin >> s )\n    {\n        \u002F\u002Fcout \u003C\u003C \"- 文件: \" \u003C\u003C s \u003C\u003C endl;\n        SList.push_back(s);\n    }\n    fin.close();\n\n    if(SList.size() == 0){\n        \u002F\u002Fcout \u003C\u003C \"失败：文件类表不存在或内容为空\" \u003C\u003C endl;\n        return 3;\n    }\n    \u002F\u002Fcout \u003C\u003C \"文件列表读取结束\" \u003C\u003C endl;\n    \u002F\u002F 对列表进行信号量检测\n    for(list\u003Cstring>::iterator it = SList.begin(); it != SList.end(); ++it){\n        string ts = *it;\n\n       \u002F\u002F cout \u003C\u003C \"- 开始检测脚本\" \u003C\u003C ts \u003C\u003C endl;\n        key_t tk = ftok(ts.c_str(),SEM_KEY);\n        if(tk == -1){\n            \u002F\u002Fcout \u003C\u003C \"失败：文件未找到\" \u003C\u003C endl;\n            return 4;\n        }\n\n        if(Exists(tk)){\n            \u002F\u002Fcout \u003C\u003C \"失败：信号量已经存在，请先检查是否还有Guard后台进程没有被关闭。如果没有在运行的进程，请更换本脚本的名称。\" \u003C\u003C endl;\n            return 5;\n        }\n\n        \u002F\u002Fcout \u003C\u003C \"- 正常\" \u003C\u003C endl;\n    }\n   \u002F\u002F cout \u003C\u003C \"文件列表检测结束\" \u003C\u003C endl;\n\n    \u002F\u002F 开始执行脚本\n    for(list\u003Cstring>::iterator it = SList.begin(); it != SList.end(); ++it){\n        if(PhpFork(*it)) return 9;\n    }\n\n    pid_t bck = 0;\n    while(true){\n        bck = wait(NULL);\n        if(bck == -1) break;\n        printf(\"子进程返回：pid = %d \\n\", bck);\n\n        int ci = 0;\n        for(list\u003Cstring>::iterator it = SList.begin(); it != SList.end(); ++it){\n            string tsn = *it;\n            key_t tk = ftok(tsn.c_str(),SEM_KEY);\n            if(!Exists(tk)){\n                printf(\"尝试重启脚本：%s \\n\", tsn.c_str());\n                ++ci;\n                if(PhpFork(*it)) return 9;\n                else break;\n            }   \n        }\n        if(ci) sleep(5);\n    }\n\n    cout \u003C\u003C \"Guard执行结束\" \u003C\u003C endl;\n    \n    return 0;\n}\n\n",[28,709,707],{"__ignoreMap":30},[11,711,712],{},"功能上的一些解释：使用信号量来对进程的状态进行管理，通过读取List.txt中的脚本列表来执行对应的脚本。主guard进程会锁定信号量，防止crontab重复的运行guard进程。信号量的SEM_UNDO参数会使得系统在进程退出时自动清除其锁定的信号量，而ftok会根据文件路径生成一个唯一的token来作为信号量名称。使用wait等待进程的结束信号，在万一的情况下对脚本运行进行恢复。而guard的持续运行则依靠linux的cron服务进行管理。",[11,714,715],{},"将代码上传到脚本同一个目录，对其进行编译然后执行，然后使用crontab进行守护即可。对于某些操作不是很熟悉的童鞋可以参考下面的命令：",[21,717,720],{"className":718,"code":719,"language":26},[24],"#编译\ng++ guard.cpp -o guard\n#执行\n.\u002Fguanrd\n\n",[28,721,719],{"__ignoreMap":30},[11,723,724],{},"如果在使用进程查看命令",[21,726,729],{"className":727,"code":728,"language":26},[24],"ps -ef | grep guard\n",[28,730,728],{"__ignoreMap":30},[11,732,733],{},"确定后台没有guard的进程的话，极有可能是和别的进程的信号量冲突了，将代码中guard的信号量修改后重新编译即可。脚本的信号量冲突时对脚本进行重命名即可，同样在操作之前确认下是否有后台进程仍在执行。",[11,735,736],{},"crontab相关指令",[21,738,741],{"className":739,"code":740,"language":26},[24],"# 清空cron列表\n\ncrontab –r\n\n#列出crontab的状态\ncrontab –l \n\n# 编辑cron\ncrontab –e\n\n# 每五分钟尝试重启guard并进行记录\n\n*\u002F5 * * * * cd \u002Fhome\u002Fwwwroot\u002Fwww.xxxx.com\u002Fscripts\u002F && .\u002Fguard >> \u002Fhome\u002Fwwwroot\u002Fwww.xxxx.com\u002Fscripts\u002Fguard.log 2>&1\n\n# 为了保险起见\n\nservice cron restart\n",[28,742,740],{"__ignoreMap":30},[11,744,745],{},"至此基本的守护操作就完成了，由于不是面向用户的，所以很多地方都以功能实现为主了。",{"title":30,"searchDepth":133,"depth":134,"links":747},[748,749],{"id":636,"depth":134,"text":637},{"id":693,"depth":134,"text":694},"2015-03-29",{"layout":139,"status":140,"published":141,"author":752,"author_login":143,"author_email":144,"author_url":145,"wordpress_id":753,"wordpress_url":754,"date_gmt":755,"excerpt":756},{"display_name":143,"login":143,"email":144,"url":145},1199,"\u002F\u002F?p=1199","2015-03-29 07:38:58 +0000",{"type":8,"value":757},[758],[11,759,629],{},"\u002F2015-03-29-phalcon后台cli邮件发送及通用脚本进程守护",{"title":624,"description":629},"_legacy\u002F2015\u002F2015-03-29-phalcon%e5%90%8e%e5%8f%b0cli%e9%82%ae%e4%bb%b6%e5%8f%91%e9%80%81%e5%8f%8a%e9%80%9a%e7%94%a8%e8%84%9a%e6%9c%ac%e8%bf%9b%e7%a8%8b%e5%ae%88%e6%8a%a4",[764,765,766,767],"phalcon","php","cron","守护","aPmw416zecTYPbox973oWx3wz3nTuwyOQyOuqa-aSsE",222,1788763184209]