#Networking
在libuv中使用網絡編程接口不會像在BSD上使用socket接口那么的麻煩,因為libuv上所有的都是非阻塞的,但是原理都是一樣的。可以這么說,libuv提供了覆蓋了惱人的,啰嗦的和底層的任務的抽象函數,比如使用BSD的socket結構的來設置socket,還有DNS查找,libuv還調整了一些socket的參數。
在網絡I/O中會使用到```uv_tcp_t```和```uv_udp_t```。
##TCP
TCP是面向連接的,字節流協議,因此基于libuv的stream實現。
####server
服務器端的建立流程如下:
>1.```uv_tcp_init```建立tcp句柄。
>2.```uv_tcp_bind```綁定。
>3.```uv_listen```建立監聽,當有新的連接到來時,激活調用回調函數。
>4.```uv_accept```接收鏈接。
>5.使用stream處理來和客戶端通信。
####tcp-echo-server/main.c - The listen socket
```c
int main() {
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
你可以調用```uv_ip4_addr()```函數來將ip地址和端口號轉換為sockaddr_in結構,這樣就可以被BSD的socket使用了。要想完成逆轉換的話可以調用```uv_ip4_name()```。
#####note
>對應ipv6有類似的uv_ip6_*
大多數的設置函數是同步的,因為它們不會消耗太多cpu資源。到了```uv_listen```這句,我們再次回到回調函數的風格上來。第二個參數是待處理的連接請求隊列-最大長度的請求連接隊列。
當客戶端開始建立連接的時候,回調函數```on_new_connection```需要使用```uv_accept```去建立一個與客戶端socket通信的句柄。同時,我們也要開始從流中讀取數據。
####tcp-echo-server/main.c - Accepting the client
```c
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
// 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);
}
}
```
上述的函數集和stream的例子類似,在code文件夾中可以找到更多的例子。記得在socket不需要后,調用uv_close。如果你不需要接受連接,你甚至可以在uv_listen的回調函數中調用uv_close。
####client
當你在服務器端完成綁定/監聽/接收的操作后,在客戶端只要簡單地調用```uv_tcp_connect```,它的回調函數和上面類似,具體例子如下:
```c
uv_tcp_t* socket = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);
uv_connect_t* connect = (uv_connect_t*)malloc(sizeof(uv_connect_t));
struct sockaddr_in dest;
uv_ip4_addr("127.0.0.1", 80, &dest);
uv_tcp_connect(connect, socket, dest, on_connect);
```
當建立連接后,回調函數```on_connect```會被調用。回調函數會接收到一個uv_connect_t結構的數據,它的```handle```指向通信的socket。
##UDP
用戶數據報協議(User Datagram Protocol)提供無連接的,不可靠的網絡通信。因此,libuv不會提供一個stream實現的形式,而是提供了一個```uv_udp_t```句柄(接收端),和一個```uv_udp_send_t```句柄(發送端),還有相關的函數。也就是說,實際的讀寫api與正常的流讀取類似。下面的例子展示了一個從DCHP服務器獲取ip的例子。
#####note
>你必須以管理員的權限運行udp-dhcp,因為它的端口號低于1024
####udp-dhcp/main.c - Setup and send UDP packets
```c
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, &recv_addr);
uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);
uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);
uv_udp_init(loop, &send_socket);
struct sockaddr_in broadcast_addr;
uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);
uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);
uv_udp_set_broadcast(&send_socket, 1);
uv_udp_send_t send_req;
uv_buf_t discover_msg = make_discover_msg();
struct sockaddr_in send_addr;
uv_ip4_addr("255.255.255.255", 67, &send_addr);
uv_udp_send(&send_req, &send_socket, &discover_msg, 1, (const struct sockaddr *)&send_addr, on_send);
return uv_run(loop, UV_RUN_DEFAULT);
}
```
#####note
>ip地址為0.0.0.0,用來綁定所有的接口。255.255.255.255是一個廣播地址,這也意味著數據報將往所有的子網接口中發送。端口號為0代表著由操作系統隨機分配一個端口。
首先,我們設置了一個接收的socket,端口號為68,作為DHCP客戶端,然后開始從中讀取數據。它會接收所有來自DHCP服務器的返回數據。我們設置了```UV_UDP_REUSEADDR```標記,用來和其他共享端口的 DHCP客戶端和平共處。接著,我們設置了一個類似的發送socket,然后使用```uv_udp_send```向DHCP服務器(在67端口)發送廣播。
設置廣播發送是非常必要的,否則你會接收到`EACCES`[錯誤](http://beej.us/guide/bgnet/output/html/multipage/advanced.html#broadcast)。和此前一樣,如果在讀寫中出錯,返回碼<0。
因為UDP不會建立連接,因此回調函數會接收到關于發送者的額外的信息。
當沒有可讀數據后,nread等于0。如果`addr`是`null`,它代表了沒有可讀數據(回調函數不會做任何處理)。如果不為null,則說明了從addr中接收到一個空的數據報。如果flag為```UV_UDP_PARTIAL```,則代表了內存分配的空間不夠存放接收到的數據了,在這種情形下,操作系統會丟棄存不下的數據。
####udp-dhcp/main.c - Reading packets
```c
void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf, const struct sockaddr *addr, unsigned flags) {
if (nread < 0) {
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t*) req, NULL);
free(buf->base);
return;
}
char sender[17] = { 0 };
uv_ip4_name((const struct sockaddr_in*) addr, sender, 16);
fprintf(stderr, "Recv from %s\n", sender);
// ... DHCP specific code
unsigned int *as_integer = (unsigned int*)buf->base;
unsigned int ipbin = ntohl(as_integer[4]);
unsigned char ip[4] = {0};
int i;
for (i = 0; i < 4; i++)
ip[i] = (ipbin >> i*8) & 0xff;
fprintf(stderr, "Offered IP %d.%d.%d.%d\n", ip[3], ip[2], ip[1], ip[0]);
free(buf->base);
uv_udp_recv_stop(req);
}
```
####UDP Options
生存時間(Time-to-live)
>可以通過`uv_udp_set_ttl`更改生存時間。
只允許IPV6協議棧
>在調用`uv_udp_bind`時,設置`UV_UDP_IPV6ONLY`標示,可以強制只使用ipv6。
組播
>socket也支持組播,可以這么使用:
```c
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`。
這里有一篇很好的關于組播的[文章](http://www.tldp.org/HOWTO/Multicast-HOWTO-2.html)。
可以使用`uv_udp_set_multicast_loop`修改本地的組播。
同樣可以使用`uv_udp_set_multicast_ttl`修改組播數據報的生存時間。(設定生存時間可以防止數據報由于環路的原因,會出現無限循環的問題)。
##Querying DNS
libuv提供了一個異步的DNS解決方案。它提供了自己的`getaddrinfo`。在回調函數中你可以像使用正常的socket操作一樣。讓我們來看一下例子:
####dns/main.c
```c
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(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
```
如果`uv_getaddrinfo`返回非零值,說明設置錯誤了,因此也不會激發回調函數。在函數返回后,所有的參數將會被回收和釋放。主機地址,請求服務器地址,還有hints的結構都可以在[這里](http://nikhilm.github.io/uvbook/getaddrinfo)找到詳細的說明。如果想使用同步請求,可以將回調函數設置為NULL。
在回調函數on_resolved中,你可以從`struct addrinfo(s)`鏈表中獲取返回的IP,最后需要調用`uv_freeaddrinfo`回收掉鏈表。下面的例子演示了回調函數的內容。
####dns/main.c
```c
void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {
if (status < 0) {
fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(status));
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);
uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res->ai_addr, on_connect);
uv_freeaddrinfo(res);
}
```
libuv同樣提供了DNS逆解析的函數[uv_getnameinfo](http://docs.libuv.org/en/v1.x/dns.html#c.uv_getnameinfo])。
##Network interfaces
可以調用`uv_interface_addresses`獲得系統的網絡接口信息。下面這個簡單的例子打印出所有可以獲取的信息。這在服務器開始準備綁定IP地址的時候很有用。
####interfaces/main.c
```c
#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`可以用來表示是否是內部的IP。由于一個物理接口會有多個IP地址,所以每一次while循環的時候都會打印一次。