博客園是個非常好的學習知識的地方,相信有很多人跟我一樣,園齡3年,從博客園不知道拷了多少代碼,看了多少博客,自己卻一篇博客都沒寫過。真是罪過。 這次準備寫幾篇關於這個項目源碼的閱讀和理解的文章,大家一起相互學習學習,我可能不會單單就寫源碼一類的東西,還會做很多擴展,比如新的c++的語法,其他的一些工 ...
博客園是個非常好的學習知識的地方,相信有很多人跟我一樣,園齡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
四、下一篇
感謝各位看官還能看到這裡,我的這個文筆不是很好,真是委屈各位了。
接下來一篇會聊到UVW里的一個基礎類,Emitter
最近時間不太寬裕,可能要等個幾天。