使用电脑端体验更佳

从0搭建博客

发布于:1746944146405 | 修改于:1787487130074 | 分类: 网站的诞生 | 浏览: 17

## 写在最前面 写在最前面:曾经在知乎上看到个回答,大概是说大部分人搭建完个人博客后的第一件事情,是马上发一篇博客教人怎么搭博客。 窝也干了 ## 我为什么想要搭建个人博客 搭建博客的障碍和困难从来也永远不会是技术问题,开始的第一步也不是选择哪个博客框架或是选择哪个语言。 在搜索引擎式微,私域流量和移动流量占据主流的如今,第一步应该是拷问自己一遍,我为什么要搭建一个基本上永远不会有人看的个人博客,为什么不是csdn/博客园。 答案很多啊,有为了满足心理需求的(有个个人博客很炫酷的好吧!),有为了满足工作需求的(我一定要让面试官看到!),还有…很多很多原因的 我先给出自己的回答吧,我最早搭建个人博客的时候,主要是觉得个人博客很有意思。现在呢主要是为了记录和纪念,记忆对我更像是化学性的易损品,会随着时间而挥发,有刻意记住的也有刻意遗忘的,但文字总会遵循着当时记录者的内心,不论过了多久都是,我想通过写博客的方式留住这一刻。 接下来就是第二步啦,选择你想使用的技术: 主流的博客框架有 hexo,worldpress 博客框架无疑是最好的选择,巨多的好看模版,不用很复杂的配置,你需要的只是买个服务器,下个框架,运行。 但是作为计算机学生不能止步于此啊 主流的web开发技术有:spingboot,php,nodejs.. 教程多上手快,使用简单,问问题有人回答,你不会的东西一定有人会,你踩过的坑别人也一定踩过,如果懂技术想diy绝对是最好的选择!还能作为简历项目捏 但是我太懒了,我不想学新的技术了呢,我只会cpp怎么办 那么从0构建一个 个人博客吧! 我并不会事无巨细的把我的所有代码讲解一遍,我会更侧重于如何实现,我为什么要这么实现,我的实现相比其他人的有什么好处。 如果你真的在乎怎么实现的话,买一本陈硕的《linux多线程服务端编程》吧!我基本是从重新实现了muduo,然后开始不断叠代新功能/改进。 这个博客的实现并非一篇文章能涵盖,会分为数个部分,包括 tcp服务器的实现,http服务器框架的实现,还有具体业务相关(数据库操作orm等) 但是作为一个序章,我先讲点开胃且简单的东西 # 跨平台网络封装 ## 为了更大的自由 不用关心操作系统差异硬件差异是很奢侈的事情,大部分人并没有感觉是因为站在了更高级的平台上。 用java/js/php写服务端很大的一点好处就是,作为网络库被包含在标准库的语言,你不用关心平台具体实现,就算你想实现一些非常过分的特性,大部分也都有人帮你打成包封装好了。 > 曾经我写java的时候非常疑惑,为什么他们有那么多包,为什么他们那么会找包,学一门语言已经很困难了,他们是怎么学那些包怎么用的... 但是对于那些更加low-level的语言,抹平平台差异就需要靠自己了。比如C++,标准线程库都是很“现代”的东西了,很难想象在C++11之前程序员得先熟悉各个平台的api才能开多线程... 没有平台差异能带来一些更直观的收益,比如你不用ssh到开发机上写代码,你不需要会配置vim插件写vimScript,visual studio很香哟,你也不用对着pdb的黑框框debug,记住那些命令真的很反人类,这可以给开发者带来更大的开发自由 -- 开发工具自由。 > 我有一位同事,他真的真的是神人,在windows上用GNU Emacs写代码,但是我们项目在windows上是用vs solution组织的,所以他编辑完后会打开vs编译一遍再验证正确... > 这个项目可是编译一次release需要1小时的究极大项目啊...他竟然能保证一次编译通过... > 我的意思是vim和ecmas等命令行编辑器确实也好使,但是你想做到“会用”需要付出巨量的成本 ## 一个什么样的封装? 这个封装比较底层,简单来说就是socket的封装。 正如我前面所说,这个封装应该抹除平台的一切差异,有什么函数就能用什么函数,在win和linux上没有任何区别。 当然可以使用一些封装好的C++库,包括boost.asio,libevent,如果想一步跨入http时代 -- libhttp 作为一个单头文件的库也是不错的选择 > 我承认这里有点表述不当,这些库可以说是和netty一个等级的封装了 但我得说这不是我的风格,我更喜欢Diy 这个封装会保证 --- Windows和Linux上的行为一致,OSX路边一条别来沾边 ## 项目结构的组织 首先是项目组织啦 ``` - / | |- Feilib //这个是我的跨平台网络库! <---- 现在关心这! | | | |---3rd | | | |---include //Headers | | | |---src //Source files | |- Client //这个是我测试用的客户端! | |- Server //这个是以后博客的服务端! | |- Tests //这个是测试...各种测试! ``` 这个网络库封装的名字叫---FeiLib,意思是feiqi3的库! 对于项目文件夹该如何组织其实是个很大的问题,我们可以参考几个很有意思的项目,看看他们是怎么组织的: 首先是上个时代的脚本,人见人恨的[LUA](https://github.com/lua/lua) 可以看到所有的.h和.c都被直接被放在了根目录下,非常的...混沌,但是这种结构在开发的时候很!爽!不用切换来切换去创建新的.h和.c,但是lua是作为一个可执行软件而不是库被构建的 大部分CPP人都用过的并发库[TBB](https://github.com/uxlfoundation/oneTBB),它的组织方式和我很像,include一个,src一个,这种组织方式的好处在于:作为一个会被其他人调用,而调用者不会关心实现的库,你可以轻易的把头文件抠出来分发 我选择TBB的文件组织方式,因为我的目标是做一个库。 FeiLib/3rd里面存放的是第三方的库,这部分**不应该**被其他的项目所感知,需要被完全封装在FeiLib中不会对上层暴露。 > 这是我的...怎么说,工程洁癖,你也可以不这么做就是,但是在后面的后面你会发现它的弊端。 ## 构建系统和包管理工具 我讨厌makefile,那东西和bash有什么区别? 我讨厌ninja,因为它的构建代码不是给人写的。 我讨厌xmake,因为吹他的人太多了但我没见过几个开源项目用过.... 我爱Cmake! > 这绝对不是斯德哥尔摩OwO cmake可以生成makeFile vs solution,谁不满意呢? 在这里我用cmake的subdirectory 和 git submodule 来作为三方库的管理工具。 这么说其实不太严谨,应该是第三方源码管理工具。因为C++真的很难做到二进制级别的跨平台包管理,从源码构建是我觉得最好的解决方案。 > C++2x的module还木有成熟,别杠 > Conan 真的是很好的包管理工具么? C++的包管理真的是很复杂的问题,Windows下的MT/MTd/MD/MDd怎么解决,真的要四套bin?windows sdk的问题怎么解决?abi兼容怎么解决?C++11和C++03的string abi breaking change真的搞死我了...就算你编译过了,你能保证vs19的库链接vs22的不会出问题么?依赖于宏开启的选项怎么办? 源码级别的管理其实是最简单的解决方案了。 这个Cmake可以看看[这里](https://github.com/feiqi3/BlogServer/blob/main/CMakeLists.txt) ,我知道写Cmake很复杂,但是你可以让chatGPT写嘛,告诉人家项目结构就好,它什么都愿意做的! ## 开始写代码! 这部分代码在 [这里](https://github.com/feiqi3/BlogServer/blob/main/FeiLib/include/FSocket.h).h 和 [这里](https://github.com/feiqi3/BlogServer/blob/main/FeiLib/src/FSocket.cpp).cpp 现在最大的目标就是封装出:Create,Close,Connect,Bind,Listen,Accept,Send,Recv函数,然后封装出Socket和SocketAddr这两个必备结构。 这里有一点最大的不一样是 Win32上的Socket是需要初始化的! 所以我们还需要一个库初始化函数来完成这些事情... 在linux上也有些情况! 信号会对我之后的服务端产生些问题,比如说SIGPIPE,发生在你在接受/发送信息但是对面关闭了Socket的情况。这个需要手动去设置信号函数忽略... 这个一定得注意,因为SIGPIPE的系统默认解决函数是程序abort,并且因为sigpipe导致的崩溃不会产生dump文件,我当初被这事情苦恼了很久很久... 还有一个信号是SIG_INT,也就是crtl+c会产生的,这个信号会打断某些慢系统调用导致错误EINTR,可以通过把信号标记设置为SA_RESTART,这样就不用写额外的代码了! ```cpp SocketStatus FeiInit() { #ifdef _WIN32 int ret = WSAStartup(MAKEWORD(2, 2), &g_wsaData); if (ret != 0) { return SocketStatus::Fail; } #elif defined(__linux__) || defined(__APPLE__) struct sigaction sa; sa.sa_handler = [](int) {}; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sa.sa_handler = SIG_IGN; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGPIPE, &sa, NULL); #endif return SocketStatus::Success; } SocketStatus FeiUnInit() { SocketStatus status = SocketStatus::Success; #ifdef _WIN32 int ret = WSACleanup(); if (ret != 0) { status = SocketStatus::Fail; } #elif defined(__linux__) #elif defined(__APPLE__) #endif return status; } ``` 接着是Socket的封装 ```cpp F_API SocketStatus Create(Socket &socket); F_API SocketStatus Create(Socket &socket,int type,int protocal); F_API SocketStatus Close(Socket socket); F_API SocketStatus Connect(Socket socket, const char *ip, uint16 port); F_API SocketStatus Connect(Socket socket, FSocketAddr addr); F_API SocketStatus Bind(Socket socket, const char *ip, uint16 port); F_API SocketStatus Bind(Socket socket, FSocketAddr addr); F_API SocketStatus Listen(Socket socket, int backlog); F_API SocketStatus Accept(Socket listen, Socket &client, FSocketAddr *addr); F_API SocketStatus Send(Socket socket, const char *data, int len,int& writeLen); F_API SocketStatus Recv(Socket socket, char *data, int len, RecvFlag flag, int &recv_len); ``` Socket是一个平台相关的类型,我的定义是 ```cpp #if defined(__linux__) or defined(__APPLE__) using Socket = int; #else using Socket = uint64; #endif ``` SocketAddr相对简单点是个对地址的统一封装(暂时不支持v6 ```cpp struct F_API FSocketAddr { // Impl in Socket.cpp FSocketAddr(const char *ip, uint16 port); FSocketAddr() = default; // get port in proper order. uint16 getPort()const; union { struct { uint8 a0; uint8 a1; uint8 a2; uint8 a3; } un_byte; uint32 un_addr; } un; uint16 port; void toHumanFriendyType(char *buf, uint32 len, uint16 *port); }; ``` 其实windows上也是部分支持posix标准的,也就是说那几个函数你基本不用做任何改动就能够跨平台... 哦,差点忘了,还有解析网址IP的几个函数 ```cpp F_API bool ResolveHost(const std::string& hostname, const std::string& port, std::vector<FSocketAddr>& addr); F_API bool ResolveHost(const std::string& hostname, uint16 port, std::vector<FSocketAddr>& addr); ``` ## 跨平台难题?! 再上面我发的代码里,你会发现每个函数前面都带着个F_API声明,它是什么? 从来没有在Windows平台下做过库的人可能会非常疑惑。 其实呢这是一个Windows和Linux关于动态库上的巨大差异。 当我们把代码编译成二进制的时候,其实是有个叫符号的东西被保存下来了,一般来说每个函数都会有对应的符号,在链接阶段连接器会根据符号去查找函数的入口地址完成函数的链接。 但是符号太多了是会导致二进制文件膨胀的,符号也会占地方嘛 对于静态库,符号信息是全部需要被打包进去的,用于在链接的时候解析函数地址。 动态库的函数入口地址是在动态库被加载的时候才能确定的,那么在链接时期连接器只能把一些静态信息(函数参数类型、源位置、调试信息)赋值给填入程序,真正的地址则是在运行时查表 msvc和gcc的差异由此出现。 msvc的动态库选择把动态库的符号和库分开,在链接时需要.lib来提供这些静态信息,那么.dll就不用保存这些信息,内存占用能非常小! 而gcc则是把所有信息都打包打进了.so中,于是链接只需要.so,运行时也只需要.so,内存可能变大了,但是形式上非常简洁。 gcc的符号都是默认导出的。 msvc是静态库所有符号都是默认导出的,与之相反动态库的符号则是默认不导出。 那么就需要手动处理一下来使得msvc导出所需要的符号。 于是诞生了msvc中最神奇的东西`__declspec(dllexport)`。这个声明告诉编译器和链接器哪些符号需要暴露给 DLL。 还有一个扯淡的是导入声明`__declspec(dllimport)`,这个声明告诉编译器你应该去动态库里找符号。 所以对于库的编译,你应该在想导出的函数前面加上`__declspec(dllexport)`,对于库的使用者,应该在要使用的函数的前面加上`__declspec(dllexport)`。 这事情很扯淡,因为你在静态链接的时候应该能从.lib中获取这个信息:这个函数是动态库里的,你应该去动态库取。事实上也确实,你可以不加,也能正常工作。 但是!但是!但是!对于全局变量来说,你必须得加上这些声明,否则编译器默认会生成“内存偏移量”访问;但 DLL 中变量是在运行时加载的,地址并不固定。于是就会发生很神奇的事情:你在A.dll中声明了一个类的静态成员并且在A.dll里初始化了,但是在使用者B.exe中获取到的是个全新未拆封的变量。 这就是很神奇的在windows中跨dll边界的单例问题。 所以F_API就是干这个事情的! 这么定义就好啦! ```cpp #ifdef _WIN32 #ifdef _MSC_VER #pragma warning( once : 4251 ) #endif #ifdef _F_EXPORT #define F_API __declspec(dllexport) #else #define F_API __declspec(dllimport) #endif #elif defined(__linux__) or defined(__APPLE__) #ifdef _F_EXPORT #define F_API __attribute__((visibility("default"))) #else #define F_API #endif #endif ``` 库的编写者在库编译的时候加上 _F_EXPORT 的宏,那么就会自动导出 库的使用者在软件编译的时候就会自动导入了。 > 对于mingw64这个结论也适用 > OSX的结论得另外讨论 至于msvcwarning 4251,这是一个关于C++abi问题的警告,由于我会一次性从源码构建这个项目,我建议是忽略。 所以为什么C++会有这么离谱的问题呢?原因主要出在 C++是一门面向静态链接的语言,标准中并没有规定动态链接的情况...所以各家就自己发挥咯 ----------- 尽管大部分的函数都有双端兼容版本的,但是大部分的变量/宏不是。 比如说 POLLRDHUP ,在linux上这个事件代表着对端关闭了,但是在windows上却并没有这个事件,所以得做额外的区分。 就算一个宏/变量/枚举 两边都存在,也不能他们的值相同,比如说POLLIN 在linux上是1,但是在Windows上却是POLLRDNORM | POLLRDBAND,反正肯定不是1啦 这就导致我不得不把所有的变量宏也要做一遍自己的封装。 当然这部分并不困难。 我们都知道 在windows和linux和OSX上的io多路复用函数并不完全相同,像是他们都有Select,Poll,但是Linux上额外有EPoll和IoUring,但是windows上只有IOCP并没有Epoll相关的函数。但是就算你只用select和poll来实现你的服务端,你会面临更加棘手的问题:[Poll的行为不统一!!!](https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/) > epoll并不是posix标准函数,iocp和io_uring也不是 这就导致我们不额外的付出大量的成本去统一行为,或者..... 选择了用epoll来完成这件事! [wepoll](https://github.com/piscisaureus/wepoll)是个在windows上用iocp实现epoll的函数,性能好到爆。但是在使用这个库的时候有些略微的问题,比如说不支持边沿触发,只能用在Socket上... 说到只能用在Socket上,还有一个问题需要注意。 在Muduo上陈硕用了一个我非常不喜欢的特性,timefd,这个linux独占的特性允许用户像创建文件/socket fd一样创建一个定时器,然后理所应当的可以用poll,epoll来查找所有到期的定时器并且执行。这么做确实有其好处,统一了定时器和socket的行为,我们不用付出额外的成本去查找哪些事件到期了,同时有着很高的精度。 但问题在于我个人不太允许我的代码中出现强平台依赖的代码。。。。 所以我们还额外需要一个定时器的实现,当然这是后话,而且也很简单。 说回到wepoll,这绝对是个工业级强度的库,OpenJDK用其实现了平台无关的epoll,那么我们也可以! ## 最后 这部分应该不太复杂,一个星期内就可以完成并且做完代码测试 对,别忘了写测试,跨平台测试... > 这部分我花了三天做完(‾◡◝) <audio controls> <source src="https://pic.feiqi3.cn/media/Into_the_Unknown.mp3" type="audio/mpeg"> 您的浏览器不支持音频播放。 </audio>


评论