從一個(gè)例子開始
我們先從一個(gè)客戶端例子開始大溜,在這個(gè)例子中织中,客戶端在 UDP 套接字上調(diào)用 connect 函數(shù)苗缩,之后將標(biāo)準(zhǔn)輸入的字符串發(fā)送到服務(wù)器端崭参,并從服務(wù)器端接收處理后的報(bào)文相寇。當(dāng)然慰于,和服務(wù)器端發(fā)送和接收?qǐng)?bào)文是通過(guò)調(diào)用函數(shù) sendto 和 recvfrom 來(lái)完成的。
#include "lib/common.h"
# define MAXLINE 4096
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: udpclient1 <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
if (connect(socket_fd, (struct sockaddr *) &server_addr, server_len)) {
error(1, errno, "connect failed");
}
struct sockaddr *reply_addr;
reply_addr = malloc(server_len);
char send_line[MAXLINE], recv_line[MAXLINE + 1];
socklen_t len;
int n;
while (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = sendto(socket_fd, send_line, strlen(send_line), 0, (struct sockaddr *) &server_addr, server_len);
if (rt < 0) {
error(1, errno, "sendto failed");
}
printf("send bytes: %zu \n", rt);
len = 0;
recv_line[0] = 0;
n = recvfrom(socket_fd, recv_line, MAXLINE, 0, reply_addr, &len);
if (n < 0)
error(1, errno, "recvfrom failed");
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
exit(0);
}
我對(duì)這個(gè)程序做一個(gè)簡(jiǎn)單的解釋:
9-10 行創(chuàng)建了一個(gè) UDP 套接字唤衫;
12-16 行創(chuàng)建了一個(gè) IPv4 地址婆赠,綁定到指定端口和 IP;
20-22 行調(diào)用 connect 將 UDP 套接字和 IPv4 地址進(jìn)行了“綁定”,這里 connect 函數(shù)的名稱有點(diǎn)讓人誤解休里,其實(shí)可能更好的選擇是叫做 setpeername蛆挫;
31-55 行是程序的主體,讀取標(biāo)準(zhǔn)輸入字符串后妙黍,調(diào)用 sendto 發(fā)送給對(duì)端悴侵;之后調(diào)用 recvfrom 等待對(duì)端的響應(yīng),并把對(duì)端響應(yīng)信息打印到標(biāo)準(zhǔn)輸出拭嫁。
在沒有開啟服務(wù)端的情況下可免,我們運(yùn)行一下這個(gè)程序:
$ ./udpconnectclient 127.0.0.1
g1
now sending g1
send bytes: 2
recvfrom failed: Connection refused (111)
看到這里你會(huì)不會(huì)覺得很奇怪?不是說(shuō)好 UDP 是“無(wú)連接”的協(xié)議嗎做粤?不是說(shuō)好 UDP 客戶端只會(huì)阻塞在 recvfrom 這樣的調(diào)用上嗎浇借?怎么這里冒出一個(gè)“Connection refused”的錯(cuò)誤呢?
UDP connect 的作用
從前面的例子中怕品,你會(huì)發(fā)現(xiàn)妇垢,我們可以對(duì) UDP 套接字調(diào)用 connect 函數(shù),但是和 TCP connect 調(diào)用引起 TCP 三次握手肉康,建立 TCP 有效連接不同闯估,UDP connect 函數(shù)的調(diào)用,并不會(huì)引起和服務(wù)器目標(biāo)端的網(wǎng)絡(luò)交互迎罗,也就是說(shuō)睬愤,并不會(huì)觸發(fā)所謂的”握手“報(bào)文發(fā)送和應(yīng)答。
那么對(duì) UDP 套接字進(jìn)行 connect 操作到底有什么意義呢纹安?
其實(shí)上面的例子已經(jīng)給出了答案尤辱,這主要是為了讓應(yīng)用程序能夠接收”異步錯(cuò)誤“的信息。
如果我們回想一下第 6 篇不調(diào)用 connect 操作的客戶端程序厢岂,在服務(wù)器端不開啟的情況下光督,客戶端程序是不會(huì)報(bào)錯(cuò)的,程序只會(huì)阻塞在 recvfrom 上塔粒,等待返回(或者超時(shí))结借。
在這里,我們通過(guò)對(duì) UDP 套接字進(jìn)行 connect 操作卒茬,將 UDP 套接字建立了”上下文“船老,該套接字和服務(wù)器端的地址和端口產(chǎn)生了聯(lián)系,正是這種綁定關(guān)系給了操作系統(tǒng)內(nèi)核必要的信息圃酵,能夠?qū)⒉僮飨到y(tǒng)內(nèi)核收到的信息和對(duì)應(yīng)的套接字進(jìn)行關(guān)聯(lián)柳畔。
我們可以展開討論一下。
事實(shí)上郭赐,當(dāng)我們調(diào)用 sendto 或者 send 操作函數(shù)時(shí)薪韩,應(yīng)用程序報(bào)文被發(fā)送,我們的應(yīng)用程序返回,操作系統(tǒng)內(nèi)核接管了該報(bào)文俘陷,之后操作系統(tǒng)開始嘗試往對(duì)應(yīng)的地址和端口發(fā)送罗捎,因?yàn)閷?duì)應(yīng)的地址和端口不可達(dá),一個(gè) ICMP 報(bào)文會(huì)返回給操作系統(tǒng)內(nèi)核拉盾,該 ICMP 報(bào)文含有目的地址和端口等信息桨菜。
如果我們不進(jìn)行 connect 操作,建立(UDP 套接字——目的地址 + 端口)之間的映射關(guān)系捉偏,操作系統(tǒng)內(nèi)核就沒有辦法把 ICMP 不可達(dá)的信息和 UDP 套接字進(jìn)行關(guān)聯(lián)雷激,也就沒有辦法將 ICMP 信息通知給應(yīng)用程序。
如果我們進(jìn)行了 connect 操作告私,幫助操作系統(tǒng)內(nèi)核從容建立了(UDP 套接字——目的地址 + 端口)之間的映射關(guān)系屎暇,當(dāng)收到一個(gè) ICMP 不可達(dá)報(bào)文時(shí),操作系統(tǒng)內(nèi)核可以從映射表中找出是哪個(gè) UDP 套接字擁有該目的地址和端口驻粟,別忘了套接字在操作系統(tǒng)內(nèi)部是全局唯一的根悼,當(dāng)我們?cè)谠撎捉幼稚显俅握{(diào)用 recvfrom 或 recv 方法時(shí),就可以收到操作系統(tǒng)內(nèi)核返回的”Connection Refused“的信息蜀撑。
收發(fā)函數(shù)
在對(duì) UDP 進(jìn)行 connect 之后挤巡,關(guān)于收發(fā)函數(shù)的使用,很多書籍是這樣推薦的:
- 使用 send 或 write 函數(shù)來(lái)發(fā)送酷麦,如果使用 sendto 需要把相關(guān)的 to 地址信息置零
- 使用 recv 或 read 函數(shù)來(lái)接收矿卑,如果使用 recvfrom 需要把對(duì)應(yīng)的 from 地址信息置零。
其實(shí)不同的 UNIX 實(shí)現(xiàn)對(duì)此表現(xiàn)出來(lái)的行為不盡相同沃饶。
在我的 Linux 4.4.0 環(huán)境中母廷,使用 sendto 和 recvfrom,系統(tǒng)會(huì)自動(dòng)忽略 to 和 from 信息糊肤。在我的 macOS 10.13 中琴昆,確實(shí)需要遵守這樣的規(guī)定,使用 sendto 或 recvfrom 會(huì)得到一些奇怪的結(jié)果馆揉,切回 send 和 recv 后正常业舍。
考慮到兼容性,我們也推薦這些常規(guī)做法升酣。所以在接下來(lái)的程序中舷暮,我會(huì)使用這樣的做法來(lái)實(shí)現(xiàn)。
服務(wù)器端 connect 的例子
一般來(lái)說(shuō)噩茄,服務(wù)器端不會(huì)主動(dòng)發(fā)起 connect 操作下面,因?yàn)橐坏┤绱耍?wù)器端就只能響應(yīng)一個(gè)客戶端了巢墅。不過(guò)诸狭,有時(shí)候也不排除這樣的情形,一旦一個(gè)客戶端和服務(wù)器端發(fā)送 UDP 報(bào)文之后君纫,該服務(wù)器端就要服務(wù)于這個(gè)唯一的客戶端驯遇。
一個(gè)類似的服務(wù)器端程序如下:
#include "lib/common.h"
static int count;
static void recvfrom_int(int signo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
}
int main(int argc, char **argv) {
int socket_fd;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
socklen_t client_len;
char message[MAXLINE];
message[0] = 0;
count = 0;
signal(SIGINT, recvfrom_int);
struct sockaddr_in client_addr;
client_len = sizeof(client_addr);
int n = recvfrom(socket_fd, message, MAXLINE, 0, (struct sockaddr *) &client_addr, &client_len);
if (n < 0) {
error(1, errno, "recvfrom failed");
}
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
if (connect(socket_fd, (struct sockaddr *) &client_addr, client_len)) {
error(1, errno, "connect failed");
}
while (strncmp(message, "goodbye", 7) != 0) {
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
if (rt < 0) {
error(1, errno, "send failed ");
}
printf("send bytes: %zu \n", rt);
size_t rc = recv(socket_fd, message, MAXLINE, 0);
if (rc < 0) {
error(1, errno, "recv failed");
}
count++;
}
exit(0);
}
我對(duì)這個(gè)程序做下解釋:
- 11-12 行創(chuàng)建 UDP 套接字;
- 14-18 行創(chuàng)建 IPv4 地址蓄髓,綁定到 ANY 和對(duì)應(yīng)端口叉庐;
- 20 行綁定 UDP 套接字和 IPv4 地址;
- 27 行為該程序注冊(cè)一個(gè)信號(hào)處理函數(shù)会喝,以響應(yīng) Ctrl+C 信號(hào)量操作陡叠;
- 32-37 行調(diào)用 recvfrom 等待客戶端報(bào)文到達(dá),并將客戶端信息保持到 client_addr 中肢执;
- 39-41 行調(diào)用 connect 操作枉阵,將 UDP 套接字和客戶端 client_addr 進(jìn)行綁定;
- 43-59 行是程序的主體预茄,對(duì)接收的信息進(jìn)行重新處理兴溜,加上”Hi“前綴后發(fā)送給客戶端,并持續(xù)不斷地從客戶端接收?qǐng)?bào)文耻陕,該過(guò)程一直持續(xù)拙徽,直到客戶端發(fā)送”goodbye“報(bào)文為止。
注意這里所有收發(fā)函數(shù)都使用了 send 和 recv诗宣。
接下來(lái)我們實(shí)現(xiàn)一個(gè) connect 的客戶端程序:
#include "lib/common.h"
# define MAXLINE 4096
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: udpclient3 <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
if (connect(socket_fd, (struct sockaddr *) &server_addr, server_len)) {
error(1, errno, "connect failed");
}
char send_line[MAXLINE], recv_line[MAXLINE + 1];
int n;
while (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
if (rt < 0) {
error(1, errno, "send failed ");
}
printf("send bytes: %zu \n", rt);
recv_line[0] = 0;
n = recv(socket_fd, recv_line, MAXLINE, 0);
if (n < 0)
error(1, errno, "recv failed");
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
exit(0);
}
我對(duì)這個(gè)客戶端程序做一下解讀:
9-10 行創(chuàng)建了一個(gè) UDP 套接字膘怕;
12-16 行創(chuàng)建了一個(gè) IPv4 地址,綁定到指定端口和 IP召庞;
20-22 行調(diào)用 connect 將 UDP 套接字和 IPv4 地址進(jìn)行了“綁定”岛心;
27-46 行是程序的主體,讀取標(biāo)準(zhǔn)輸入字符串后篮灼,調(diào)用 send 發(fā)送給對(duì)端鹉梨;之后調(diào)用 recv 等待對(duì)端的響應(yīng),并把對(duì)端響應(yīng)信息打印到標(biāo)準(zhǔn)輸出穿稳。
注意這里所有收發(fā)函數(shù)也都使用了 send 和 recv存皂。
接下來(lái),我們先啟動(dòng)服務(wù)器端程序逢艘,然后依次開啟兩個(gè)客戶端旦袋,分別是客戶端 1、客戶端 2它改,并且讓客戶端 1 先發(fā)送 UDP 報(bào)文疤孕。
服務(wù)器端:
$ ./udpconnectserver
received 2 bytes: g1
send bytes: 6
客戶端 1:
./udpconnectclient2 127.0.0.1
g1
now sending g1
send bytes: 2
Hi, g1
客戶端 2:
./udpconnectclient2 127.0.0.1
g2
now sending g2
send bytes: 2
recv failed: Connection refused (111)
我們看到,客戶端 1 先發(fā)送報(bào)文央拖,服務(wù)端隨之通過(guò) connect 和客戶端 1 進(jìn)行了“綁定”祭阀,這樣鹉戚,客戶端 2 從操作系統(tǒng)內(nèi)核得到了 ICMP 的錯(cuò)誤,該錯(cuò)誤在 recv 函數(shù)中返回专控,顯示了“Connection refused”的錯(cuò)誤信息抹凳。
性能考慮
一般來(lái)說(shuō),客戶端通過(guò) connect 綁定服務(wù)端的地址和端口伦腐,對(duì) UDP 而言赢底,可以有一定程度的性能提升。
這是為什么呢柏蘑?
因?yàn)槿绻皇褂?connect 方式幸冻,每次發(fā)送報(bào)文都會(huì)需要這樣的過(guò)程:
連接套接字→發(fā)送報(bào)文→斷開套接字→連接套接字→發(fā)送報(bào)文→斷開套接字 →………
而如果使用 connect 方式,就會(huì)變成下面這樣:
連接套接字→發(fā)送報(bào)文→發(fā)送報(bào)文→……→最后斷開套接字
我們知道咳焚,連接套接字是需要一定開銷的洽损,比如需要查找路由表信息。所以革半,UDP 客戶端程序通過(guò) connect 可以獲得一定的性能提升趁啸。
總結(jié)
在今天的內(nèi)容里,我對(duì) UDP 套接字調(diào)用 connect 方法進(jìn)行了深入的分析督惰。之所以對(duì) UDP 使用 connect不傅,綁定本地地址和端口,是為了讓我們的程序可以快速獲取異步錯(cuò)誤信息的通知赏胚,同時(shí)也可以獲得一定性能上的提升访娶。