博客园是个非常好的学习知识的地方,相信有很多人跟我一样,园龄3年,从博客园不知道拷了多少代码,看了多少博客,自己却一篇博客都没写过。真是罪过。
这次准备写几篇关于这个项目源码的阅读和理解的文章,大家一起相互学习学习,我可能不会单单就写源码一类的东西,还会做很多扩展,比如新的c++的语法,其他的一些工具等等,各位看官不要嫌烦。咱们又不是什么大牛,遇到文中有歧义,不对之处,请在评论区留言,咱们一起讨论,再做改进,避免误人子弟。
废话不多说,现在开始。
最近在看一个项目 uvw 的源码,可能很多人不知道这个东西。搞过一些网络编程的人应该知道 libuv,uvw 是我在github上找到的一个用c++封装 libuv 的项目,源代码作者也在持续更新中。
简单的介绍一下:
libuv:是一个跨平台的网络库,具体可以参考博客:http://www.cnblogs.com/haippy/archive/2013/03/17/2963995.html 以及博主的libuv系列的文章。
uvw:用 c++14 对libuv的封装,作者应该是个外国人,代码质量应该没的说,尤其是注释,非常详尽,值得我等菜鸟学习。
github地址:https://github.com/skypjack/uvw
首先得把代码搞出来,
1、直接下载,地址:https://codeload.github.com/skypjack/uvw/zip/master
2、git clone https://github.com/skypjack/uvw.git
注:文件路径写法: ./src/uvw.hpp 当前目录为代码根目录。
代码基本上在src文件中,切到src,对,你没有看错,全是hpp文件,所以如果你要用这个库,直接把src拷到你工程里就行了。用起来可以说是非常方便,但是你的工程不要忘了包含libuv的头文件和链接libuv库。另外uvw对libuv的版本也有限制,可以在github的tag中查看libuv对应的版本,如果你是用方法2,可以用命令”git tag -l“查看。(关于git这个东西,如果有看官还不了解的,可以参考菜鸟教程:http://www.runoob.com/git/git-tutorial.html 或者去git官网,有非常详细的资料)
一、先来看看怎么用
拷一段代码(./test/main.cpp):
1 #include "../src/uvw.hpp" 2 #include <cassert> 3 #include <iostream> 4 #include <memory> 5 #include <chrono> 6 7 8 void listen(uvw::Loop &loop) { 9 std::shared_ptr<uvw::TcpHandle> tcp = loop.resource<uvw::TcpHandle>(); //创建一个TcpHandle 10 11 tcp->on<uvw::ErrorEvent>([](const uvw::ErrorEvent &, uvw::TcpHandle &) { //注册错误发生函数, 12 std::cout << "error " << std::endl; 13 }); 14 15 tcp->once<uvw::ListenEvent>([](const uvw::ListenEvent &, uvw::TcpHandle &srv) { //注册监听事件函数 16 std::cout << "listen" << std::endl; 17 18 std::shared_ptr<uvw::TcpHandle> client = srv.loop().resource<uvw::TcpHandle>(); //创建一个TcpHandle,用于新的client连接 19 20 client->on<uvw::ErrorEvent>([](const uvw::ErrorEvent &, uvw::TcpHandle &) { //为client注册错误发生函数 21 std::cout << "error " << std::endl; 22 }); 23 24 client->on<uvw::CloseEvent>([ptr = srv.shared_from_this()](const uvw::CloseEvent &, uvw::TcpHandle &) { //注册client关闭函数 25 std::cout << "close" << std::endl; 26 ptr->close(); //这里当client被关闭时,也会关闭server 27 }); 28 29 srv.accept(*client); //server accept 30 31 uvw::Addr local = srv.sock(); 32 std::cout << "local: " << local.ip << " " << local.port << std::endl; 33 34 uvw::Addr remote = client->peer(); 35 std::cout << "remote: " << remote.ip << " " << remote.port << std::endl; 36 37 client->on<uvw::DataEvent>([](const uvw::DataEvent &event, uvw::TcpHandle &) { //注册client接收数据事件函数 38 std::cout.write(event.data.get(), event.length) << std::endl; //event中已经保存有读取的数据,可以直接使用 39 std::cout << "data length: " << event.length << std::endl; 40 }); 41 42 client->on<uvw::EndEvent>([](const uvw::EndEvent &, uvw::TcpHandle &handle) { //注册client数据读取结束函数,当socket没有数据可读时会发送该事件 43 std::cout << "end" << std::endl; 44 int count = 0; 45 handle.loop().walk([&count](uvw::BaseHandle &) { ++count; }); //获取主loop中活跃的套接字,这里有server和client两个 46 std::cout << "still alive: " << count << " handles" << std::endl; 47 handle.close(); //关闭client连接 48 }); 49 50 client->read(); //开始读取数据,这里和uv_read_start的效果相同, 这里和上面的注册事件操作,调用时是不分先后顺序的。 51 }); 52 53 tcp->once<uvw::CloseEvent>([](const uvw::CloseEvent &, uvw::TcpHandle &) { 54 std::cout << "close" << std::endl; 55 }); 56 57 tcp->bind("127.0.0.1", 4242); //bind,这里支持IPv4和IPv6,bind为一个模版函数 58 tcp->listen(); //listen 59 } 60 61 62 void conn(uvw::Loop &loop) { 63 auto tcp = loop.resource<uvw::TcpHandle>(); //下面的基本和listen中类似,不多做注释 64 65 tcp->on<uvw::ErrorEvent>([](const uvw::ErrorEvent &, uvw::TcpHandle &) { 66 std::cout << "error " << std::endl; 67 }); 68 69 tcp->once<uvw::WriteEvent>([](const uvw::WriteEvent &, uvw::TcpHandle &handle) { 70 std::cout << "write" << std::endl; 71 handle.close(); 72 }); 73 74 tcp->once<uvw::ConnectEvent>([](const uvw::ConnectEvent &, uvw::TcpHandle &handle) { 75 std::cout << "connect" << std::endl; 76 77 auto dataTryWrite = std::unique_ptr<char[]>(new char[1]{ 'a' }); //以下操作为向server发送数据 78 int bw = handle.tryWrite(std::move(dataTryWrite), 1); 79 std::cout << "written: " << ((int)bw) << std::endl; 80 81 auto dataWrite = std::unique_ptr<char[]>(new char[2]{ 'b', 'c' }); 82 handle.write(std::move(dataWrite), 2); 83 }); 84 85 tcp->once<uvw::CloseEvent>([](const uvw::CloseEvent &, uvw::TcpHandle &) { 86 std::cout << "close" << std::endl; 87 }); 88 89 tcp->connect("127.0.0.1", 4242); 90 } 91 92 void g() { 93 auto loop = uvw::Loop::getDefault(); //获取默认事件循环 94 listen(*loop); 95 conn(*loop); 96 loop->run(); //开始事件循环 97 loop = nullptr; 98 } 99 100 int main() { 101 g(); 102 }
(话说怎么没有我喜欢的代码字体的)
好像挺长的,这边结构看上去还算是比较清晰。
二、仔细看看
1、server端操作
listen()函数基本上包含了所有server端的操作,基本流程就是:
创建TcpHandle(第9行) --> bind(第57行) --> listen(第58行)
在ListenEvent中,可以看到第18行,又创建了一个TcpHandle client,用来接收客户端的连接:
创建TcpHandle(第18行) --> accept(第29行) --> read(第50行)
除了这些其他的代码就是事件处理的过程,事件处理都是用的Lambda表达式来写的,比如:
1 tcp->on<uvw::ErrorEvent>([](const uvw::ErrorEvent &, uvw::TcpHandle &) { //注册错误发生函数, 2 std::cout << "error " << std::endl; 3 });
Lambda都有两个参数:
{Event}:事件,在代码中可以看很多事件类型,比如CloseEvent,ConnectEvent等(看名字应该就知道是什么事件了)
{Handle}:Source类型,这里可能还会有 UdpHandle等等libuv中出现的类型。以后看到源码再谈。
后经运行调试,事件处理匿名函数里的{Handle}和创建的TcpHandle其实是相同的。
2、client端操作
conn函数里基本就是创建一个TcpHandle,然后调用connect连接到服务器,其他的就是相关的事件。
另外就是client的数据发送:
1 std::cout << "connect" << std::endl; 2 3 auto dataTryWrite = std::unique_ptr<char[]>(new char[1]{ 'a' }); //以下操作为向server发送数据 4 int bw = handle.tryWrite(std::move(dataTryWrite), 1); 5 std::cout << "written: " << ((int)bw) << std::endl; 6 7 auto dataWrite = std::unique_ptr<char[]>(new char[2]{ 'b', 'c' }); 8 handle.write(std::move(dataWrite), 2);
可以看到调用了两个数据写入函数,tryWrite和write,相当于uv_write 和 uv_try_write,我给出作者对tryWrite的注释:
/**
* @brief Queues a write request if it can be completed immediately.
*
* Same as `write()`, but won’t queue a write request if it can’t be
* completed immediately.<br/>
* An ErrorEvent event will be emitted in case of errors.
*
* @param data The data to be written to the stream.
* @param len The lenght of the submitted data.
* @return Number of bytes written.
*/
意思就是tryWrite也会发送数据,但是不会立即完成,也不会保证把数据全部一次性发送完。而write会将没发送完的数据再次加到loop中等待下次发送。
3、总结
可以看出来,作者用大量的Lambda来代替了libuv中的各种回调,相比之下,用Lambda,可读性增加了很多。
另外代码中使用了大量的模板函数来区分事件类型,作者源代码里应该使用了很多泛型,
三、相关知识
1、Lambda
Lambda又叫做匿名函数,这是个博客园帖子,可以稍微学习或者回顾一下:http://www.cnblogs.com/langzou/p/5962033.html
PS:这个匿名函数的英文名,有一堆拼写:Lamda, Lamba,Lamdba,Ladbda。。。真的是千奇百怪。
这里给大家强调一下,虽然名字到底怎么写对咱们学习东西没什么太大影响,但是本着严谨的态度,他的英文名正确拼写应该是
Lambda 读音:lan b(m) da(兰木达)['læmdə]
它是‘λ’的音译,百度百科上也是这个拼写,在《C++ Primer 第5版》的346页,也可以看到,所以大家以后不要记错哦,避免被人笑话了。哈哈。
在第24行中:
1 client->on<uvw::CloseEvent>([ptr = srv.shared_from_this()](const uvw::CloseEvent &, uvw::TcpHandle &) { //注册client关闭函数 2 std::cout << "close" << std::endl; 3 ptr->close(); //这里当client被关闭时,也会关闭server 4 });
大家有没有注意到这边 ptr = srv.shared_from_this() 是个什么东东?
Lambda中 [] 不应该是用来捕获外部变量的吗,怎么这边好像是定义了一个ptr变量,并用shared_from_this()来给它初始化了。但是很明显这个ptr并没有参数类型,在上下文中也没有对ptr的声明。是不是非常奇怪。
查阅了大量书籍资料后,在 http://zh.cppreference.com/w/cpp/language/lambda 中发现下面一段:
1 带初始化器的捕获,行动如同它声明并显示捕获以类型 auto 声明的变量,变量的声明性区域是 lambda 表达式体(即它不在其初始化器的作用域中),除了: 2 若捕获以复制,则闭包的非静态数据成员是另一种指代该自动变量的方式。 3 若捕获以引用在,则引用变量的生存期在闭包对象的生存期结束时结束。 4 这用于捕获仅移动类型,以例如 x = std::move(x) 的捕获 5 int x = 4; 6 auto y = [&r = x, x = x + 1]()->int 7 { 8 r += 2; 9 return x * x; 10 }(); // 更新 ::x 为 6 并初始化 y 为 25 。
可以看出,用的就是这种带初始化器的捕获,这是在c++14中新添加的特性
在第一行中,可以知道,这种带初始化器的捕获会自动将变量声明为auto类型,并且可以对声明的变量进行初始化。切记,是初始化。对于上面的例子,如果写成这样:
1 int x = 4; 2 auto y = [&r = x, r = x + 1]()->int //错误 3 { 4 r += 2; 5 return x * x; 6 }();
是错误的。另外如果你不初始化,也会产生编译错误。
现在再看源代码第24行的代码,应该就没什么问题了吧?
在了解这个之后我又找到一篇介绍这种捕获类型的文章:http://blog.csdn.net/big_yellow_duck/article/details/52473055
大家可以一起学习参考一下,看来我也得买本《Effective Modern C++》来看看了。
2、智能指针
这我就不介绍了,还是博客园的文章,大家可以学习或者回顾一下:http://www.cnblogs.com/qq329914874/p/6653412.html