libuv 中文编程指南(四)网络
網(wǎng)絡(luò)
libuv 的網(wǎng)絡(luò)接口與 BSD 套接字接口存在很大的不同, 某些事情在 libuv 下變得更簡(jiǎn)單了, 并且所有接口都是都是非阻塞的, 但是原則上還是一致的. 另外 libuv 也提供了一些工具類的函數(shù)抽象了一些讓人生厭的, 重復(fù)而底層的任務(wù),比如使用 BSD 套接字結(jié)構(gòu)來(lái)建立套接字, DNS 查詢, 或者其他各種參數(shù)的設(shè)置.
libuv 中在網(wǎng)絡(luò) I/O 中使用了 uv_tcp_t 和 uv_udp_t 兩個(gè)結(jié)構(gòu)體.
TCP
TCP 是一種面向連接的流式協(xié)議, 因此是基于 libuv 的流式基礎(chǔ)架構(gòu)上的.
服務(wù)器(Server)
服務(wù)器端的 sockets 處理流程如下:
以下是一個(gè)簡(jiǎn)單的 echo 服務(wù)器的例子:
int main() {loop = uv_default_loop();uv_tcp_t server;uv_tcp_init(loop, &server);struct sockaddr_in bind_addr = uv_ip4_addr("0.0.0.0", 7000);uv_tcp_bind(&server, bind_addr);int r = uv_listen((uv_stream_t*) &server, 128, on_new_connection);if (r) {fprintf(stderr, "Listen error %s\n", uv_err_name(uv_last_error(loop)));return 1;}return uv_run(loop, UV_RUN_DEFAULT); }你可以看到輔助函數(shù) uv_ip4_addr 用來(lái)將人為可讀的字符串類型的 IP 地址和端口號(hào)轉(zhuǎn)換成 BSD 套接字 API 所需要的 struct sockaddr_in 類型的結(jié)構(gòu). 逆變換可以使用 uv_ip4_name 來(lái)完成.
對(duì)于 IPv6 來(lái)說(shuō)應(yīng)該使用 uv_ip6_* 形式的函數(shù).
大部分的設(shè)置(setup)函數(shù)都是普通函數(shù), 因?yàn)樗麄兌际?計(jì)算密集型(CPU-bound), 直到調(diào)用了 uv_listen 我們才回到 libuv 中回調(diào)函數(shù)風(fēng)格. uv_listen 的第二個(gè)參數(shù) backlog 隊(duì)列長(zhǎng)度 – 即連接隊(duì)列最大長(zhǎng)度.
當(dāng)客戶端發(fā)起了新的連接時(shí), 回調(diào)函數(shù)需要為客戶端套接字設(shè)置一個(gè)監(jiān)視器, 并調(diào)用 uv_accept 函數(shù)將客戶端套接字與新的監(jiān)視器在關(guān)聯(lián)一起. 在例子中我們將從流中讀取數(shù)據(jù).
void on_new_connection(uv_stream_t *server, int status) {if (status == -1) {// error!return;}uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, client);if (uv_accept(server, (uv_stream_t*) client) == 0) {uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);}else {uv_close((uv_handle_t*) client, NULL);} }剩余部分的函數(shù)與上一節(jié)流式例子中的代碼相似, 你可以在例子程序中找到具體代碼, 如果套接字不再使用記得調(diào)用 uv_close 關(guān)閉該套接字. 如果你不再接受連接, 你可以在 uv_listen 的回調(diào)函數(shù)中關(guān)閉套接字.
客戶端(Client)
在服務(wù)器端你需要調(diào)用 bind/listen/accept, 而在客戶端你只需要調(diào)用 uv_tcp_connect. uv_tcp_connect 使用了與 uv_listen 風(fēng)格相似的回調(diào)函數(shù) uv_connect_cb 如下:
uv_tcp_t socket; uv_tcp_init(loop, &socket);uv_connect_t connect;struct sockaddr_in dest = uv_ip4_addr("127.0.0.1", 80);uv_tcp_connect(&connect, &socket, dest, on_connect);建立連接后會(huì)調(diào)用 on_connect.
UDP
User Datagram Protocol 提供了無(wú)連接, 不可靠網(wǎng)絡(luò)通信協(xié)議, 因此 libuv 并不提供流式 UDP 服務(wù), 而是通過(guò) uv_udp_t 結(jié)構(gòu)體(用于接收)和 uv_udp_send_t 結(jié)構(gòu)體(用于發(fā)送)以及相關(guān)的函數(shù)給開發(fā)人員提供了非阻塞的 UDP 服務(wù). 所以, 真正讀寫 UDP 的函數(shù)與普通的流式讀寫非常相似.為了示范如何使用 UDP, 下面提供了一個(gè)簡(jiǎn)單的例子用來(lái)從 DHCP 獲取 IP 地址. – DHCP 發(fā)現(xiàn).
Note
你應(yīng)該以 root 用戶運(yùn)行 udp-dhcp, 因?yàn)樵摮绦蚴褂昧硕丝谔?hào)低于 1024 的端口.
uv_loop_t *loop; uv_udp_t send_socket; uv_udp_t recv_socket;int main() {loop = uv_default_loop();uv_udp_init(loop, &recv_socket);struct sockaddr_in recv_addr = uv_ip4_addr("0.0.0.0", 68);uv_udp_bind(&recv_socket, recv_addr, 0);uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);uv_udp_init(loop, &send_socket);uv_udp_bind(&send_socket, uv_ip4_addr("0.0.0.0", 0), 0);uv_udp_set_broadcast(&send_socket, 1);uv_udp_send_t send_req;uv_buf_t discover_msg = make_discover_msg(&send_req);struct sockaddr_in send_addr = uv_ip4_addr("255.255.255.255", 67);uv_udp_send(&send_req, &send_socket, &discover_msg, 1, send_addr, on_send);return uv_run(loop, UV_RUN_DEFAULT); }0.0.0.0 地址可以綁定本機(jī)所有網(wǎng)口. 255.255.255.255 是廣播地址, 意味著網(wǎng)絡(luò)包可以發(fā)送給子網(wǎng)中所有網(wǎng)口, 端口 0 說(shuō)明操作系統(tǒng)可以任意指定端口進(jìn)行綁定.
首先我們?cè)?68 號(hào)端口上設(shè)置了綁定本機(jī)所有網(wǎng)口的接收套接字(DHCP 客戶端), 并且設(shè)置了讀監(jiān)視器. 然后我們利用相同的方法設(shè)置了一個(gè)用于發(fā)送消息的套接字. 并使用 uv_udp_send 在 67 號(hào)端口上(DHCP 服務(wù)器)發(fā)送 廣播消息.
設(shè)置廣播標(biāo)志也是 必要 的, 不然你會(huì)得到 EACCES 錯(cuò)誤 [1]. 發(fā)送的具體消息與本書無(wú)關(guān), 如果你對(duì)此感興趣, 可以參考源碼. 若出錯(cuò), 則讀寫回調(diào)函數(shù)會(huì)收到 -1 狀態(tài)碼.
由于 UDP 套接字并不和特定的對(duì)等方保持連接, 所以 read 回調(diào)函數(shù)中將會(huì)收到用于標(biāo)識(shí)發(fā)送者的額外信息. 如果緩沖區(qū)是由你自己的分配的, 并且不夠容納接收的數(shù)據(jù), 則``flags`` 標(biāo)志位可能是 UV_UDP_PARTIAL. 在這種情況下, 操作系統(tǒng)會(huì)丟棄不能容納的數(shù)據(jù). (這也是 UDP 為你提供的特性).
void on_read(uv_udp_t *req, ssize_t nread, uv_buf_t buf, struct sockaddr *addr, unsigned flags) {if (nread == -1) {fprintf(stderr, "Read error %s\n", uv_err_name(uv_last_error(loop)));uv_close((uv_handle_t*) req, NULL);free(buf.base);return;}char sender[17] = { 0 };uv_ip4_name((struct sockaddr_in*) addr, sender, 16);fprintf(stderr, "Recv from %s\n", sender);// ... DHCP specific code free(buf.base);uv_udp_recv_stop(req); }UDP 選項(xiàng)(UDP Options)
生存時(shí)間TTL(Time-to-live)
可以通過(guò) uv_udp_set_ttl 來(lái)設(shè)置網(wǎng)絡(luò)數(shù)據(jù)包的生存時(shí)間(TTL).
僅使用 IPv6 協(xié)議
IPv6 套接字可以同時(shí)在 IPv4 和 IPv6 協(xié)議下進(jìn)行通信. 如果你只想使用 IPv6 套接字, 在調(diào)用 uv_udp_bind6 [2] 時(shí)請(qǐng)傳遞 UV_UDP_IPV6ONLY 參數(shù).
多播(Multicast)
套接字可以使用如下函數(shù)訂閱(取消訂閱)一個(gè)多播組:
UV_EXTERN int uv_udp_set_membership(uv_udp_t* handle,const char* multicast_addr, const char* interface_addr,uv_membership membership);membership 取值可以是 UV_JOIN_GROUP 或 UV_LEAVE_GROUP.
多播包的本地回路是默認(rèn)開啟的 [3], 可以使用 uv_udp_set_multicast_loop 來(lái)開啟/關(guān)閉該特性.
多播包的生存時(shí)間可以使用 uv_udp_set_multicast_ttl 來(lái)設(shè)置.
DNS 查詢(Querying DNS)
libuv 提供了異步解析 DNS 的功能, 用于替代 getaddrinfo [4]. 在回調(diào)函數(shù)中, 你可以在獲得的 IP 地址上執(zhí)行普通的套接字操作. 讓我們通過(guò)一個(gè)簡(jiǎn)單的 DNS 解析的例子來(lái)看看怎么連接 Freenode 吧:
int main() {loop = uv_default_loop();struct addrinfo hints;hints.ai_family = PF_INET;hints.ai_socktype = SOCK_STREAM;hints.ai_protocol = IPPROTO_TCP;hints.ai_flags = 0;uv_getaddrinfo_t resolver;fprintf(stderr, "irc.freenode.net is... ");int r = uv_getaddrinfo(loop, &resolver, on_resolved, "irc.freenode.net", "6667", &hints);if (r) {fprintf(stderr, "getaddrinfo call error %s\n", uv_err_name(uv_last_error(loop)));return 1;}return uv_run(loop, UV_RUN_DEFAULT); }如果 uv_getaddrinfo 返回非零, 表示在建立連接時(shí)出錯(cuò), 你設(shè)置的回調(diào)函數(shù)不會(huì)被調(diào)用, 所有的參數(shù)將會(huì)在 uv_getaddrinfo 返回后被立即釋放. 有關(guān) hostname, servname 和 hints 結(jié)構(gòu)體的文檔可以在 getaddrinfo 幫助頁(yè)面中找到.
在解析回調(diào)函數(shù)中, 你可以在 struct addrinfo(s) 結(jié)構(gòu)的鏈表中任取一個(gè) IP. 這個(gè)例子也演示了如何使用 uv_tcp_connect. 你在回調(diào)函數(shù)中有必要調(diào)用 uv_freeaddrinfo.
void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {if (status == -1) {fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(uv_last_error(loop)));return;}char addr[17] = {'\0'};uv_ip4_name((struct sockaddr_in*) res->ai_addr, addr, 16);fprintf(stderr, "%s\n", addr);uv_connect_t *connect_req = (uv_connect_t*) malloc(sizeof(uv_connect_t));uv_tcp_t *socket = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));uv_tcp_init(loop, socket);connect_req->data = (void*) socket;uv_tcp_connect(connect_req, socket, *(struct sockaddr_in*) res->ai_addr, on_connect);uv_freeaddrinfo(res); }網(wǎng)絡(luò)接口(Network interfaces)
系統(tǒng)網(wǎng)絡(luò)接口信息可以通過(guò)調(diào)用 uv_interface_addresses 來(lái)獲得, 下面的示例程序?qū)⒋蛴〕鰴C(jī)器上所有網(wǎng)絡(luò)接口的細(xì)節(jié)信息, 因此你可以獲知網(wǎng)口的哪些域的信息是可以得到的, 這在你的程序啟動(dòng)時(shí)綁定 IP 很方便.
#include <stdio.h> #include <uv.h>int main() {char buf[512];uv_interface_address_t *info;int count, i;uv_interface_addresses(&info, &count);i = count;printf("Number of interfaces: %d\n", count);while (i--) {uv_interface_address_t interface = info[i];printf("Name: %s\n", interface.name);printf("Internal? %s\n", interface.is_internal ? "Yes" : "No");if (interface.address.address4.sin_family == AF_INET) {uv_ip4_name(&interface.address.address4, buf, sizeof(buf));printf("IPv4 address: %s\n", buf);}else if (interface.address.address4.sin_family == AF_INET6) {uv_ip6_name(&interface.address.address6, buf, sizeof(buf));printf("IPv6 address: %s\n", buf);}printf("\n");}uv_free_interface_addresses(info, count);return 0; }is_internal 對(duì)于回環(huán)接口來(lái)說(shuō)為 true. 請(qǐng)注意如果物理網(wǎng)口使用了多個(gè) IPv4/IPv6 地址, 那么它的名稱將會(huì)被多次報(bào)告, 因?yàn)槊總€(gè)地址都會(huì)報(bào)告一次.
總結(jié)
以上是生活随笔為你收集整理的libuv 中文编程指南(四)网络的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于EOS主节点竞选
- 下一篇: 计算机动画的主要应用领域,简述计算机的主