《TCP IP網(wǎng)絡(luò)編程》第六章:基于 UDP 的服務(wù)端/客戶端
UDP 套接字的特點(diǎn):
????????通過寄信來說明 UDP 的工作原理,這是講解 UDP 時(shí)使用的傳統(tǒng)示例,它與 UDP 的特點(diǎn)完全相同。寄信前應(yīng)先在信封上填好寄信人和收信人的地址,之后貼上郵票放進(jìn)郵筒即可。當(dāng)然,信件的特點(diǎn)使我們無法確認(rèn)信件是否被收到。郵寄過程中也可能發(fā)生信件丟失的情況。也就是說,信件是一種不可靠的傳輸方式,UDP 也是一種不可靠的數(shù)據(jù)傳輸方式。
? ? ? ? 不過,這種比喻只是形容協(xié)議工作方式,并不包含數(shù)據(jù)交換速率。實(shí)際正好相反,TCP的速度無法超過UDP,但在收發(fā)某些類型的數(shù)據(jù)時(shí)可能接近UDP。
????????因?yàn)?UDP 沒有 TCP 那么復(fù)雜,所以編程難度比較小,性能也比 TCP 高。在更重視性能的情況下可以選擇 UDP 的傳輸方式。
????????TCP 與 UDP 的區(qū)別很大一部分來源于流控制。也就是說 TCP 的生命在于流控制。
?UDP 的工作原理:
????????從圖中可以看出,IP 的作用就是讓離開主機(jī) B 的 UDP 數(shù)據(jù)包準(zhǔn)確傳遞到主機(jī) A 。但是把 UDP 數(shù)據(jù)包最終交給主機(jī) A 的某一 UDP 套接字的過程是由 UDP 完成的。UDP 的最重要的作用就是根據(jù)端口號(hào)將傳到主機(jī)的數(shù)據(jù)包交付給最終的 UDP 套接字。?
UDP 的高效使用:
????????UDP 也具有一定的可靠性。對(duì)于通過網(wǎng)絡(luò)實(shí)時(shí)傳遞的視頻或者音頻時(shí)情況有所不同。對(duì)于多媒體數(shù)據(jù)而言,丟失一部分?jǐn)?shù)據(jù)也沒有太大問題,這只是會(huì)暫時(shí)引起畫面抖動(dòng),或者出現(xiàn)細(xì)微的雜音。但是要提供實(shí)時(shí)服務(wù),速度就成為了一個(gè)很重要的因素。因此流控制就顯得有一點(diǎn)多余,這時(shí)就要考慮使用 UDP 。TCP 比 UDP 慢的原因主要有以下兩點(diǎn):
- 收發(fā)數(shù)據(jù)前后進(jìn)行的連接設(shè)置及清除過程。
- 收發(fā)過程中為保證可靠性而添加的流控制。
????????如果收發(fā)的數(shù)據(jù)量小但是需要頻繁連接時(shí),UDP 比 TCP 更高效。
實(shí)現(xiàn)基于 UDP 的服務(wù)端/客戶端:
????????UDP 中的服務(wù)端和客戶端不像 TCP 那樣在連接狀態(tài)下交換數(shù)據(jù),因此與 TCP 不同,無需經(jīng)過連接過程。也就是說,不必調(diào)用 TCP 連接過程中調(diào)用的 listen 和 accept 函數(shù)。UDP 中只有創(chuàng)建套接字和數(shù)據(jù)交換的過程。
????????TCP 中,套接字之間應(yīng)該是一對(duì)一的關(guān)系。若要向 10 個(gè)客戶端提供服務(wù),除了守門的服務(wù)器套接字之外,還需要 10 個(gè)服務(wù)器套接字。但在 UDP 中,不管是服務(wù)器端還是客戶端都只需要 1 個(gè)套接字。只需要一個(gè) UDP 套接字就可以向任意主機(jī)傳輸數(shù)據(jù),如圖所示:
????????圖中展示了 1 個(gè) UDP 套接字與 2 個(gè)不同主機(jī)交換數(shù)據(jù)的過程。也就是說,只需 1 個(gè) UDP 套接字就能和多臺(tái)主機(jī)進(jìn)行通信。
????????創(chuàng)建好 TCP 套接字以后,傳輸數(shù)據(jù)時(shí)無需加上地址信息。因?yàn)?TCP 套接字將保持與對(duì)方套接字的連接。換言之,TCP 套接字知道目標(biāo)地址信息。但 UDP 套接字不會(huì)保持連接狀態(tài)(UDP 套接字只有簡(jiǎn)單的郵筒功能),因此每次傳輸數(shù)據(jù)時(shí)都需要添加目標(biāo)的地址信息。這相當(dāng)于寄信前在信件中填寫地址。接下來是 UDP 的相關(guān)函數(shù):?
#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *to, socklen_t addrlen);
/*
成功時(shí)返回發(fā)送的字節(jié)數(shù),失敗時(shí)返回 -1
sock: 用于傳輸數(shù)據(jù)的 UDP 套接字
buff: 保存待傳輸數(shù)據(jù)的緩沖地址值
nbytes: 待傳輸?shù)臄?shù)據(jù)長(zhǎng)度,以字節(jié)為單位
flags: 可選項(xiàng)參數(shù),若沒有則傳遞 0
to: 存有目標(biāo)地址的 sockaddr 結(jié)構(gòu)體變量的地址值
addrlen: 傳遞給參數(shù) to 的地址值結(jié)構(gòu)體變量長(zhǎng)度
*/
????????上述函數(shù)與之前的 TCP 輸出函數(shù)最大的區(qū)別在于,此函數(shù)需要向它傳遞目標(biāo)地址信息。接下來介紹接收 UDP 數(shù)據(jù)的函數(shù)。UDP 數(shù)據(jù)的發(fā)送并不固定,因此該函數(shù)定義為可接受發(fā)送端信息的形式,也就是將同時(shí)返回 UDP 數(shù)據(jù)包中的發(fā)送端信息。
#include <sys/socket.h>
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
/*
成功時(shí)返回接收的字節(jié)數(shù),失敗時(shí)返回 -1
sock: 用于傳輸數(shù)據(jù)的 UDP 套接字
buff: 保存待傳輸數(shù)據(jù)的緩沖地址值
nbytes: 待傳輸?shù)臄?shù)據(jù)長(zhǎng)度,以字節(jié)為單位
flags: 可選項(xiàng)參數(shù),若沒有則傳遞 0
from: 存有發(fā)送端地址信息的 sockaddr 結(jié)構(gòu)體變量的地址值
addrlen: 保存參數(shù) from 的結(jié)構(gòu)體變量長(zhǎng)度的變量地址值。
*/
????????編寫 UDP 程序的最核心的部分就在于上述兩個(gè)函數(shù)。
實(shí)驗(yàn): UDP 的回聲服務(wù)器端/客戶端:
服務(wù)端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
//創(chuàng)建 UDP 套接字后,向 socket 的第二個(gè)參數(shù)傳遞 SOCK_DGRAM
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
error_handling("UDP socket creation eerror");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
//分配地址接受數(shù)據(jù),不限制數(shù)據(jù)傳輸對(duì)象
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
while (1)
{
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr *)&clnt_adr, &clnt_adr_sz);
//通過上面的函數(shù)調(diào)用同時(shí)獲取數(shù)據(jù)傳輸端的地址。正是利用該地址進(jìn)行逆向重傳
sendto(serv_sock, message, str_len, 0,
(struct sockaddr *)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客戶端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
//創(chuàng)建 UDP 套接字
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
while (1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
//向服務(wù)器傳輸數(shù)據(jù),會(huì)自動(dòng)給自己分配IP地址和端口號(hào)
sendto(sock, message, strlen(message), 0,
(struct sockaddr *)&serv_adr, sizeof(serv_adr));
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr *)&from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
實(shí)驗(yàn)結(jié)果:
?UDP 客戶端套接字的地址分配?:
????????UDP 客戶端缺少了把IP和端口分配給套接字的過程。TCP 客戶端調(diào)用 connect 函數(shù)自動(dòng)完成此過程,而 UDP 中連能承擔(dān)相同功能的函數(shù)調(diào)用語句都沒有。究竟在什么時(shí)候分配IP和端口號(hào)呢?
????????UDP 程序中,調(diào)用 sendto 函數(shù)傳輸數(shù)據(jù)前應(yīng)該完成對(duì)套接字的地址分配工作,因此調(diào)用 bind 函數(shù)。當(dāng)然,bind 函數(shù)在 TCP 程序中出現(xiàn)過,但 bind 函數(shù)不區(qū)分 TCP 和 UDP,也就是說,在 UDP 程序中同樣可以調(diào)用。另外,如果調(diào)用 sendto 函數(shù)尚未分配地址信息,則在首次調(diào)用 sendto 函數(shù)時(shí)給相應(yīng)套接字自動(dòng)分配 IP 和端口。而且此時(shí)分配的地址一直保留到程序結(jié)束為止,因此也可以用來和其他 UDP 套接字進(jìn)行數(shù)據(jù)交換。當(dāng)然,IP 用主機(jī)IP,端口號(hào)用未選用的任意端口號(hào)。
綜上所述,調(diào)用 sendto 函數(shù)時(shí)自動(dòng)分配IP和端口號(hào),因此,UDP 客戶端中通常無需額外的地址分配過程。所以之前的示例中省略了該過程。這也是普遍的實(shí)現(xiàn)方式。
UDP 的數(shù)據(jù)傳輸特性和調(diào)用 connect 函數(shù):
????????前面說得 TCP 數(shù)據(jù)傳輸中不存在數(shù)據(jù)邊界,這表示「數(shù)據(jù)傳輸過程中調(diào)用 I/O 函數(shù)的次數(shù)不具有任何意義」
????????相反,UDP 是具有數(shù)據(jù)邊界的下一,傳輸中調(diào)用 I/O 函數(shù)的次數(shù)非常重要。因此,輸入函數(shù)的調(diào)用次數(shù)和輸出函數(shù)的調(diào)用次數(shù)應(yīng)該完全一致,這樣才能保證接收全部已經(jīng)發(fā)送的數(shù)據(jù)。例如,調(diào)用 3 次輸出函數(shù)發(fā)送的數(shù)據(jù)必須通過調(diào)用 3 次輸入函數(shù)才能接收完。通過一個(gè)例子來進(jìn)行驗(yàn)證:
服務(wù)端代碼 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len, i;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&my_adr, 0, sizeof(my_adr));
my_adr.sin_family = AF_INET;
my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
my_adr.sin_port = htons(atoi(argv[1]));
if (bind(sock, (struct sockaddr *)&my_adr, sizeof(my_adr)) == -1)
error_handling("bind() error");
for (i = 0; i < 3; i++)
{
sleep(5);
adr_sz = sizeof(your_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr *)&your_adr, &adr_sz);
printf("Message %d: %s \n", i + 1, message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客戶端代碼:?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char msg1[] = "Hi!";
char msg2[] = "I'm another UDP host!";
char msg3[] = "Nice to meet you";
struct sockaddr_in your_adr;
socklen_t your_adr_sz;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&your_adr, 0, sizeof(your_adr));
your_adr.sin_family = AF_INET;
your_adr.sin_addr.s_addr = inet_addr(argv[1]);
your_adr.sin_port = htons(atoi(argv[2]));
sendto(sock, msg1, sizeof(msg1), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
sendto(sock, msg2, sizeof(msg2), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
sendto(sock, msg3, sizeof(msg3), 0,
(struct sockaddr *)&your_adr, sizeof(your_adr));
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
? ? ? ? 客戶端調(diào)用3次sendto函數(shù)以傳輸數(shù)據(jù),服務(wù)端則調(diào)用了3次recvfrom函數(shù)以接受數(shù)據(jù)。recvfrom函數(shù)調(diào)用間隔為5秒,因此,調(diào)用recvfrom函數(shù)前已調(diào)用了3次sendto函數(shù)。也就是說,此時(shí)數(shù)據(jù)已經(jīng)傳輸?shù)絙ound_host1.c。如果是TCP程序,這時(shí)只需調(diào)用1次輸入函數(shù)即可讀入數(shù)據(jù)。UDP則不同,在這種情況下也需要調(diào)用3次recvfrom函數(shù)。可通過以下運(yùn)行結(jié)果進(jìn)行驗(yàn)證。
? ? ? ? 從服務(wù)端結(jié)果可以看出,共調(diào)用了3次recvfrom函數(shù),這就證明了必須在UDP通信過程中使I/O函數(shù)調(diào)用次數(shù)保持一致。
已連接(connect)UDP 套接字與未連接(unconnected)UDP 套接字?:? ??
????????TCP 套接字中需注冊(cè)待傳傳輸數(shù)據(jù)的目標(biāo)IP和端口號(hào),而在 UDP 中無需注冊(cè)。因此通過 sendto 函數(shù)傳輸數(shù)據(jù)的過程大概可以分為以下 3 個(gè)階段:
- 第 1 階段:向 UDP 套接字注冊(cè)目標(biāo) IP 和端口號(hào)
- 第 2 階段:傳輸數(shù)據(jù)
- 第 3 階段:刪除 UDP 套接字中注冊(cè)的目標(biāo)地址信息。
????????每次調(diào)用 sendto 函數(shù)時(shí)重復(fù)上述過程。每次都變更目標(biāo)地址,因此可以重復(fù)利用同一 UDP 套接字向不同目標(biāo)傳遞數(shù)據(jù)。這種未注冊(cè)目標(biāo)地址信息的套接字稱為未連接套接字,反之,注冊(cè)了目標(biāo)地址的套接字稱為連接 connected 套接字。顯然,UDP 套接字默認(rèn)屬于未連接套接字。當(dāng)一臺(tái)主機(jī)向另一臺(tái)主機(jī)傳輸很多信息時(shí),上述的三個(gè)階段中,第一個(gè)階段和第三個(gè)階段占整個(gè)通信過程中近三分之一的時(shí)間,縮短這部分的時(shí)間將會(huì)大大提高整體性能。
????????創(chuàng)建已連接 UDP 套接字過程格外簡(jiǎn)單,只需針對(duì) UDP 套接字調(diào)用 connect 函數(shù):
sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = inet_addr(argv[1]);
adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr *)&adr, sizeof(adr));
????????上述代碼看似與 TCP 套接字創(chuàng)建過程一致,但 socket 函數(shù)的第二個(gè)參數(shù)分明是 SOCK_DGRAM 。也就是說,創(chuàng)建的的確是 UDP 套接字。當(dāng)然針對(duì) UDP 調(diào)用 connect 函數(shù)并不是意味著要與對(duì)方 UDP 套接字連接,這只是向 UDP 套接字注冊(cè)目標(biāo)IP和端口信息。
????????之后就與 TCP 套接字一致,每次調(diào)用 sendto 函數(shù)時(shí)只需傳遞信息數(shù)據(jù)。因?yàn)橐呀?jīng)指定了收發(fā)對(duì)象,所以不僅可以使用 sendto、recvfrom 函數(shù),還可以使用 write、read 函數(shù)進(jìn)行通信。
習(xí)題:
1、UDP 為什么比 TCP 快?為什么 TCP 傳輸可靠而 UDP?傳輸不可靠?
????????UDP比TCP快的原因是UDP沒有建立連接和擁塞控制的開銷,并且沒有重傳機(jī)制。TCP傳輸可靠的原因是TCP通過建立連接、序號(hào)管理和重傳機(jī)制來確保數(shù)據(jù)的可靠傳輸和完整性。
2、UDP 數(shù)據(jù)報(bào)向?qū)Ψ街鳈C(jī)的 UDP 套接字傳遞過程中,IP 和 UDP 分別負(fù)責(zé)哪些部分?
????????IP的作用就是讓離開主機(jī)的 UDP 數(shù)據(jù)包準(zhǔn)確傳遞到另一個(gè)主機(jī)。但把 UDP 包最終交給主機(jī)的某一 UDP 套接字的過程則是由 UDP 完成的。UDP 的最重要的作用就是根據(jù)端口號(hào)將傳到主機(jī)的數(shù)據(jù)包交付給最終的 UDP 套接字。
3、UDP 一般比 TCP 快,但根據(jù)交換數(shù)據(jù)的特點(diǎn),其差異可大可小。請(qǐng)你說明何種情況下 UDP 的性能優(yōu)于 TCP?
????????如果收發(fā)數(shù)據(jù)量小但需要頻繁連接時(shí),UDP 比 TCP 更高效。
4、客戶端 TCP 套接字調(diào)用 connect 函數(shù)時(shí)自動(dòng)分配IP和端口號(hào)。UDP 中不調(diào)用 bind 函數(shù),那何時(shí)分配IP和端口號(hào)?
????????在首次調(diào)用 sendto 函數(shù)時(shí)自動(dòng)給相應(yīng)的套接字分配IP和端口號(hào)。而且此時(shí)分配的地址一直保留到程序結(jié)束為止。這種臨時(shí)分配的機(jī)制使得UDP套接字可以在不調(diào)用bind
函數(shù)的情況下進(jìn)行通信。UDP套接字的地址和端口號(hào)分配由操作系統(tǒng)自動(dòng)完成,無需用戶顯式指定。這也是UDP相比TCP更加靈活和簡(jiǎn)單的一點(diǎn)。
5、TCP 客戶端必須調(diào)用 connect 函數(shù),而 UDP 可以選擇性調(diào)用。請(qǐng)問,在 UDP 中調(diào)用 connect 函數(shù)有哪些好處?
????????UDP通過 sendto 函數(shù)傳輸數(shù)據(jù)的過程大概可以分為以下 3 個(gè)階段:文章來源:http://www.zghlxwxcb.cn/news/detail-576763.html
- 第 1 階段:向 UDP 套接字注冊(cè)目標(biāo) IP 和端口號(hào)
- 第 2 階段:傳輸數(shù)據(jù)
- 第 3 階段:刪除 UDP 套接字中注冊(cè)的目標(biāo)地址信息。
當(dāng)一臺(tái)主機(jī)向另一臺(tái)主機(jī)傳輸很多信息時(shí),上述的三個(gè)階段中,第一個(gè)階段和第三個(gè)階段占整個(gè)通信過程中近三分之一的時(shí)間,使用connect函數(shù)可以節(jié)省這些時(shí)間。文章來源地址http://www.zghlxwxcb.cn/news/detail-576763.html
到了這里,關(guān)于《TCP IP網(wǎng)絡(luò)編程》第六章的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!