libuv 中文編程指南(四)網絡


網絡

libuv 的網絡接口與 BSD 套接字接口存在很大的不同, 某些事情在 libuv 下變得更簡單了, 並且所有接口都是都是非阻塞的, 但是原則上還是一致的. 另外 libuv 也提供了一些工具類的函數抽象了一些讓人生厭的, 重復而底層的任務,比如使用 BSD 套接字結構來建立套接字, DNS 查詢, 或者其他各種參數的設置.

libuv 中在網絡 I/O 中使用了 uv_tcp_tuv_udp_t 兩個結構體.

TCP

TCP 是一種面向連接的流式協議, 因此是基於 libuv 的流式基礎架構上的.

服務器(Server)

服務器端的 sockets 處理流程如下:

  1. uv_tcp_init 初始化 TCP 監視器.
  2. uv_tcp_bind 綁定.
  3. 在指定的監視器上調用 uv_listen 來設置回調函數, 當有新的客戶端連接到來時, libuv 就會調用設置的回調函數.
  4. uv_accept 接受連接.
  5. 使用 stream operations 與客戶端進行通信.

以下是一個簡單的 echo 服務器的例子:

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);
}

你可以看到輔助函數 uv_ip4_addr 用來將人為可讀的字符串類型的 IP 地址和端口號轉換成 BSD 套接字 API 所需要的 struct sockaddr_in 類型的結構. 逆變換可以使用 uv_ip4_name 來完成.

對於 IPv6 來說應該使用 uv_ip6_* 形式的函數.

大部分的設置(setup)函數都是普通函數, 因為他們都是 計算密集型(CPU-bound), 直到調用了 uv_listen 我們才回到 libuv 中回調函數風格. uv_listen 的第二個參數 backlog 隊列長度 – 即連接隊列最大長度.

當客戶端發起了新的連接時, 回調函數需要為客戶端套接字設置一個監視器, 並調用 uv_accept 函數將客戶端套接字與新的監視器在關聯一起. 在例子中我們將從流中讀取數據.

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);
    }
}

剩余部分的函數與上一節流式例子中的代碼相似, 你可以在例子程序中找到具體代碼, 如果套接字不再使用記得調用 uv_close 關閉該套接字. 如果你不再接受連接, 你可以在 uv_listen 的回調函數中關閉套接字.

客戶端(Client)

在服務器端你需要調用 bind/listen/accept, 而在客戶端你只需要調用 uv_tcp_connect. uv_tcp_connect 使用了與 uv_listen 風格相似的回調函數 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);

建立連接后會調用 on_connect.

UDP

User Datagram Protocol 提供了無連接, 不可靠網絡通信協議, 因此 libuv 並不提供流式 UDP 服務, 而是通過 uv_udp_t 結構體(用於接收)和 uv_udp_send_t 結構體(用於發送)以及相關的函數給開發人員提供了非阻塞的 UDP 服務. 所以, 真正讀寫 UDP 的函數與普通的流式讀寫非常相似.為了示范如何使用 UDP, 下面提供了一個簡單的例子用來從 DHCP 獲取 IP 地址. – DHCP 發現.

Note

你應該以 root 用戶運行 udp-dhcp, 因為該程序使用了端口號低於 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 地址可以綁定本機所有網口. 255.255.255.255 是廣播地址, 意味着網絡包可以發送給子網中所有網口, 端口 0 說明操作系統可以任意指定端口進行綁定.

首先我們在 68 號端口上設置了綁定本機所有網口的接收套接字(DHCP 客戶端), 並且設置了讀監視器. 然后我們利用相同的方法設置了一個用於發送消息的套接字. 並使用 uv_udp_send 在 67 號端口上(DHCP 服務器)發送 廣播消息.

設置廣播標志也是 必要 的, 不然你會得到 EACCES 錯誤 [1]. 發送的具體消息與本書無關, 如果你對此感興趣, 可以參考源碼. 若出錯, 則讀寫回調函數會收到 -1 狀態碼.

由於 UDP 套接字並不和特定的對等方保持連接, 所以 read 回調函數中將會收到用於標識發送者的額外信息. 如果緩沖區是由你自己的分配的, 並且不夠容納接收的數據, 則``flags`` 標志位可能是 UV_UDP_PARTIAL. 在這種情況下, 操作系統會丟棄不能容納的數據. (這也是 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 選項(UDP Options)

生存時間TTL(Time-to-live)

可以通過 uv_udp_set_ttl 來設置網絡數據包的生存時間(TTL).

僅使用 IPv6 協議

IPv6 套接字可以同時在 IPv4 和 IPv6 協議下進行通信. 如果你只想使用 IPv6 套接字, 在調用 uv_udp_bind6 [2] 時請傳遞 UV_UDP_IPV6ONLY 參數.

多播(Multicast)

套接字可以使用如下函數訂閱(取消訂閱)一個多播組:

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_GROUPUV_LEAVE_GROUP.

多播包的本地回路是默認開啟的 [3], 可以使用 uv_udp_set_multicast_loop 來開啟/關閉該特性.

多播包的生存時間可以使用 uv_udp_set_multicast_ttl 來設置.

DNS 查詢(Querying DNS)

libuv 提供了異步解析 DNS 的功能, 用於替代 getaddrinfo [4]. 在回調函數中, 你可以在獲得的 IP 地址上執行普通的套接字操作. 讓我們通過一個簡單的 DNS 解析的例子來看看怎么連接 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 返回非零, 表示在建立連接時出錯, 你設置的回調函數不會被調用, 所有的參數將會在 uv_getaddrinfo 返回后被立即釋放. 有關 hostname, servnamehints 結構體的文檔可以在 getaddrinfo 幫助頁面中找到.

在解析回調函數中, 你可以在 struct addrinfo(s) 結構的鏈表中任取一個 IP. 這個例子也演示了如何使用 uv_tcp_connect. 你在回調函數中有必要調用 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);
}

網絡接口(Network interfaces)

系統網絡接口信息可以通過調用 uv_interface_addresses 來獲得, 下面的示例程序將打印出機器上所有網絡接口的細節信息, 因此你可以獲知網口的哪些域的信息是可以得到的, 這在你的程序啟動時綁定 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 對於回環接口來說為 true. 請注意如果物理網口使用了多個 IPv4/IPv6 地址, 那么它的名稱將會被多次報告, 因為每個地址都會報告一次.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM