Socket:UDP协议小白
UDP(user datagram protocol)用戶數(shù)據(jù)報協(xié)議,屬于傳輸層。
首先要搞清楚網(wǎng)絡(luò)通信的幾個層次:
OSI 是 Open System Interconnection 的縮寫,譯為“開放式系統(tǒng)互聯(lián)”。
OSI 模型把網(wǎng)絡(luò)通信的工作分為 7 層,從下到上分別是物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、會話層、表示層和應(yīng)用層。
OSI 只是存在于概念和理論上的一種模型,它的缺點是分層太多,增加了網(wǎng)絡(luò)工作的復(fù)雜性,所以沒有大規(guī)模應(yīng)用。后來人們對 OSI 進(jìn)行了簡化,合并了一些層,最終只保留了 4 層,從下到上分別是接口層、網(wǎng)絡(luò)層、傳輸層和應(yīng)用層,這就是大名鼎鼎的 TCP/IP 模型。
我們平常使用的程序(或者說軟件)一般都是通過應(yīng)用層來訪問網(wǎng)絡(luò)的,程序產(chǎn)生的數(shù)據(jù)會一層一層地往下傳輸,直到最后的網(wǎng)絡(luò)接口層,就通過網(wǎng)線發(fā)送到互聯(lián)網(wǎng)上去了。數(shù)據(jù)每往下走一層,就會被這一層的協(xié)議增加一層包裝,等到發(fā)送到互聯(lián)網(wǎng)上時,已經(jīng)比原始數(shù)據(jù)多了四層包裝。整個數(shù)據(jù)封裝的過程就像俄羅斯套娃。
當(dāng)另一臺計算機接收到數(shù)據(jù)包時,會從網(wǎng)絡(luò)接口層再一層一層往上傳輸,每傳輸一層就拆開一層包裝,直到最后的應(yīng)用層,就得到了最原始的數(shù)據(jù),這才是程序要使用的數(shù)據(jù)。
給數(shù)據(jù)加包裝的過程,實際上就是在數(shù)據(jù)的頭部增加一個標(biāo)志(一個數(shù)據(jù)塊),表示數(shù)據(jù)經(jīng)過了這一層,我已經(jīng)處理過了。給數(shù)據(jù)拆包裝的過程正好相反,就是去掉數(shù)據(jù)頭部的標(biāo)志,讓它逐漸現(xiàn)出原形。
我們所說的 socket 編程,是站在傳輸層的基礎(chǔ)上,所以可以使用 TCP/UDP 協(xié)議,但是不能干「訪問網(wǎng)頁」這樣的事情,因為訪問網(wǎng)頁所需要的 http 協(xié)議位于應(yīng)用層。
兩臺計算機進(jìn)行通信時,必須遵守以下原則:
- 必須是同一層次進(jìn)行通信,比如,A 計算機的應(yīng)用層和 B 計算機的傳輸層就不能通信,因為它們不在一個層次,數(shù)據(jù)的拆包會遇到問題。
- 每一層的功能都必須相同,也就是擁有完全相同的網(wǎng)絡(luò)模型。如果網(wǎng)絡(luò)模型都不同,那不就亂套了,誰都不認(rèn)識誰。
- 數(shù)據(jù)只能逐層傳輸,不能躍層。
- 每一層可以使用下層提供的服務(wù),并向上層提供服務(wù)。
TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百個互為關(guān)聯(lián)的協(xié)議,其中 TCP 和 IP 是最常用的兩種底層協(xié)議,所以把它們統(tǒng)稱為“TCP/IP 協(xié)議族”。
也就是說,“TCP/IP模型”中所涉及到的協(xié)議稱為“TCP/IP協(xié)議族”。
個人學(xué)習(xí)的socket 編程是基于 TCP 和 UDP 協(xié)議的,它們的層級關(guān)系如下圖所示:
UDP是面向非連接的協(xié)議,它不與對方建立連接,而是直接把數(shù)據(jù)報發(fā)給對方。UDP無需建立類如三次握手的連接,使得通信效率很高。因此UDP適用于一次傳輸數(shù)據(jù)量很少、對可靠性要求不高的或?qū)崟r性要求高的應(yīng)用場景。
服務(wù)端:
(1)使用函數(shù)socket(),生成套接字文件描述符;
(2)通過struct?sockaddr_in 結(jié)構(gòu)設(shè)置服務(wù)器地址和監(jiān)聽端口;
(3)使用bind() 函數(shù)綁定監(jiān)聽端口,將套接字文件描述符和地址類型變量(struct?sockaddr_in?)進(jìn)行綁定;
(4)接收客戶端的數(shù)據(jù),使用recvfrom() 函數(shù)接收客戶端的網(wǎng)絡(luò)數(shù)據(jù);
(5)向客戶端發(fā)送數(shù)據(jù),使用sendto() 函數(shù)向服務(wù)器主機發(fā)送數(shù)據(jù);
(6)關(guān)閉套接字,使用close() 函數(shù)釋放資源;
客戶端:
(1)使用socket(),生成套接字文件描述符;
(2)通過struct?sockaddr_in 結(jié)構(gòu)設(shè)置服務(wù)器地址和監(jiān)聽端口;
(3)向服務(wù)器發(fā)送數(shù)據(jù),sendto() ;
(4)接收服務(wù)器的數(shù)據(jù),recvfrom() ;
(5)關(guān)閉套接字,close() ;
(1)socket()函數(shù)解讀:生成套接字文件描述符
socket() 函數(shù)用來創(chuàng)建套接字,確定套接字的各種屬性,然后服務(wù)器端要用 bind() 函數(shù)將套接字與特定的 IP 地址和端口綁定起來,只有這樣,流經(jīng)該 IP 地址和端口的數(shù)據(jù)才能交給套接字處理。類似地,客戶端也要用 connect() 函數(shù)建立連接。
int socket(int af, int type, int protocol);//Linux下//在 Linux 下使用 <sys/socket.h> 頭文件中 socket() 函數(shù)來創(chuàng)建套接字
SOCKET socket(int af, int type, int protocol);//windows下
//兩者除了返回值類型不同,其他都是相同的。Windows 不把套接字作為普通文件對待,而是返回 SOCKET 類型的句柄。請看下面的例子:
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //創(chuàng)建TCP套接字
1) af 為地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫,INET是“Inetnet”的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如?1030::C9B4:FF12:48AA:1A2B。
2) type 為數(shù)據(jù)傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字/面向連接的套接字)?和?SOCK_DGRAM(數(shù)據(jù)報套接字/無連接的套接字)
3) protocol 表示傳輸協(xié)議,常用的有?IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協(xié)議和 UDP 傳輸協(xié)議。
有了地址類型和數(shù)據(jù)傳輸方式,還不足以決定采用哪種協(xié)議嗎?為什么還需要第三個參數(shù)呢?(因為有時TCP和UDP同時滿足兩種情況,這時候就不知道用哪種了。)
正如大家所想,一般情況下有了 af 和 type 兩個參數(shù)就可以創(chuàng)建套接字了,操作系統(tǒng)會自動推演出協(xié)議類型,除非遇到這樣的情況:有兩種不同的協(xié)議支持同一種地址類型和數(shù)據(jù)傳輸類型。如果我們不指明使用哪種協(xié)議,操作系統(tǒng)是沒辦法自動推演的。
本教程使用 IPv4 地址,參數(shù) af 的值為 PF_INET。如果使用 SOCK_STREAM 傳輸數(shù)據(jù),那么滿足這兩個條件的協(xié)議只有 TCP,因此可以這樣來調(diào)用 socket() 函數(shù):
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP協(xié)議
這種套接字稱為 TCP 套接字。
如果使用 SOCK_DGRAM 傳輸方式,那么滿足這兩個條件的協(xié)議只有 UDP,因此可以這樣來調(diào)用 socket() 函數(shù):
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP協(xié)議
這種套接字稱為 UDP 套接字。
上面兩種情況都只有一種協(xié)議滿足條件,可以將 protocol 的值設(shè)為 0,系統(tǒng)會自動推演出應(yīng)該使用什么協(xié)議,如下所示:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //創(chuàng)建TCP套接字 int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字
大家需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址,后面的教程會經(jīng)常用到。
你也可以使用 PF 前綴,PF 是“Protocol Family”的簡寫,它和 AF 是一樣的。例如,PF_INET 等價于 AF_INET,PF_INET6 等價于 AF_INET6。
(2)struct?sockaddr_in 結(jié)構(gòu)體解讀:設(shè)置服務(wù)器地址和監(jiān)聽端口
上述提到的sockaddr_in是什么?還有一個是sockaddr ?這倆個分別是什么有什么區(qū)別
struct sockaddr?和?struct sockaddr_in?這兩個結(jié)構(gòu)體用來處理網(wǎng)絡(luò)通信的地址。
下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數(shù)字表示所占用的字節(jié)數(shù)):
sockaddr_in 結(jié)構(gòu)體,sockaddr在頭文件#include <sys/socket.h>中定義,sockaddr的缺陷是:sa_data把目標(biāo)地址和端口信息混在一起了:
struct sockaddr{sa_family_t sin_family; //地址族(Address Family),也就是地址類型char sa_data[14]; //IP地址和端口號
};
sockaddr_in在頭文件#include<netinet/in.h>或#include <arpa/inet.h>中定義,該結(jié)構(gòu)體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變量中,如下:?
struct sockaddr_in{sa_family_t sin_family; //地址族(Address Family),也就是地址類型uint16_t sin_port; //16位的端口號struct in_addr sin_addr; //32位IP地址char sin_zero[8]; //不使用,一般用0填充
};
1) sin_family 和 socket() 的第一個參數(shù)的含義相同,取值也要保持一致。
地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫,INET是“Inetnet”的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如?1030::C9B4:FF12:48AA:1A2B。
2) sin_prot 為端口號。uint16_t 的長度為兩個字節(jié),理論上端口號的取值范圍為 0~65536,但 0~1023 的端口一般由系統(tǒng)分配給特定的服務(wù)程序,例如 Web 服務(wù)的端口號為 80,FTP 服務(wù)的端口號為 21,所以我們的程序要盡量在 1024~65536 之間分配端口號。
端口號需要用 htons() 函數(shù)轉(zhuǎn)換,為什么呢?
3) sin_addr 是 struct in_addr 結(jié)構(gòu)體類型的變量,下面會詳細(xì)講解。
4) sin_zero[8] 是多余的8個字節(jié),沒有用,一般使用 memset() 函數(shù)填充為 0。上面的代碼中,先用 memset() 將結(jié)構(gòu)體的全部字節(jié)填充為 0,再給前3個成員賦值,剩下的 sin_zero 自然就是 0 了。
該結(jié)構(gòu)體中提到的另外一個結(jié)構(gòu)體in_addr 結(jié)構(gòu)體,該結(jié)構(gòu)體只包含一個成員,如下所示:
struct in_addr{in_addr_t s_addr; //32位的IP地址
};
in_addr_t 在頭文件 <netinet/in.h> 中定義,等價于 unsigned long,長度為4個字節(jié)。也就是說,s_addr 是一個整數(shù),而IP地址是一個字符串,所以需要?inet_addr() 函數(shù)進(jìn)行轉(zhuǎn)換,例如:
unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);
運行結(jié)果:
13278343
至于為什么要搞的這么復(fù)雜?
為什么要搞這么復(fù)雜,結(jié)構(gòu)體中嵌套結(jié)構(gòu)體,而不用 sockaddr_in 的一個成員變量來指明IP地址呢?socket() 函數(shù)的第一個參數(shù)已經(jīng)指明了地址類型,為什么在 sockaddr_in 結(jié)構(gòu)體中還要再說明一次呢,這不是啰嗦嗎?
這些繁瑣的細(xì)節(jié)確實給初學(xué)者帶來了一定的障礙,我想,這或許是歷史原因吧,后面的接口總要兼容前面的代碼。各位讀者一定要有耐心,暫時不理解沒有關(guān)系,根據(jù)教程中的代碼“照貓畫虎”即可,時間久了自然會接受。
總結(jié):
- 二者長度一樣,都是16個字節(jié),即占用的內(nèi)存大小是一致的,因此可以互相轉(zhuǎn)化。二者是并列結(jié)構(gòu),指向sockaddr_in結(jié)構(gòu)的指針也可以指向sockaddr。
- sockaddr常用于bind、connect、recvfrom、sendto等函數(shù)的參數(shù)(為社么呢??),指明地址信息,是一種通用的套接字地址。?
bind() 第二個參數(shù)的類型為 sockaddr,而代碼中卻使用 sockaddr_in,然后再強制轉(zhuǎn)換為 sockaddr,這是為什么呢?
因為sockaddr 和 sockaddr_in 的長度相同,都是16字節(jié),只是將IP地址和端口號合并到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和端口號,例如”127.0.0.1:80“,遺憾的是,沒有相關(guān)函數(shù)將這個字符串轉(zhuǎn)換成需要的形式,也就很難給 sockaddr 類型的變量賦值,所以使用 sockaddr_in 來代替。這兩個結(jié)構(gòu)體的長度相同,強制轉(zhuǎn)換類型時不會丟失字節(jié),也沒有多余的字節(jié)。
- sockaddr_in 是internet環(huán)境下套接字的地址形式。所以在網(wǎng)絡(luò)編程中我們會對sockaddr_in結(jié)構(gòu)體進(jìn)行操作,使用sockaddr_in來建立所需的信息,最后使用類型轉(zhuǎn)化就可以了。一般先把sockaddr_in變量賦值后,強制類型轉(zhuǎn)換后傳入用sockaddr做參數(shù)的函數(shù):sockaddr_in用于socket定義和賦值;sockaddr用于函數(shù)參數(shù)。
可以認(rèn)為,sockaddr 是一種通用的結(jié)構(gòu)體,可以用來保存多種類型的IP地址和端口號,而 sockaddr_in 是專門用來保存 IPv4 地址的結(jié)構(gòu)體。另外還有 sockaddr_in6,用來保存 IPv6 地址,它的定義如下:
struct sockaddr_in6 { sa_family_t sin6_family; //(2)地址類型,取值為AF_INET6in_port_t sin6_port; //(2)16位端口號uint32_t sin6_flowinfo; //(4)IPv6流信息struct in6_addr sin6_addr; //(4)具體的IPv6地址uint32_t sin6_scope_id; //(4)接口范圍ID
};
正是由于通用結(jié)構(gòu)體 sockaddr 使用不便,才針對不同的地址類型定義了不同的結(jié)構(gòu)體。
?
擴展:
兩個函數(shù) htons() 和 inet_addr()。
htons()作用是將端口號由主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的整數(shù)值。(host to net)
inet_addr()作用是將一個IP字符串轉(zhuǎn)化為一個網(wǎng)絡(luò)字節(jié)序的整數(shù)值,用于sockaddr_in.sin_addr.s_addr。
inet_ntoa()作用是將一個sin_addr結(jié)構(gòu)體輸出成IP字符串(network to ascii)。比如:
printf("%s",inet_ntoa(mysock.sin_addr));
htonl()作用和htons()一樣,不過它針對的是32位的(long),而htons()針對的是兩個字節(jié),16位的(short)。
與htonl()和htons()作用相反的兩個函數(shù)是:ntohl()和ntohs()。?
? ?
(3)bind()函數(shù)解讀:綁定監(jiān)聽端口,將套接字文件描述符和地址類型變量(struct?sockaddr_in?)進(jìn)行綁定
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
下面的代碼,將創(chuàng)建的套接字與IP地址 127.0.0.1、端口 1234 綁定:
//創(chuàng)建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//創(chuàng)建sockaddr_in結(jié)構(gòu)體變量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節(jié)都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port = htons(1234); //端口//將套接字和IP、端口綁定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
sock 為 socket 文件描述符,addr 為 sockaddr 結(jié)構(gòu)體變量的指針,addrlen 為 addr 變量的大小,可由 sizeof() 計算得出。
(4)recvfrom() 函數(shù)解讀:服務(wù)器端接收客戶端的網(wǎng)絡(luò)數(shù)據(jù)
int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
返回值說明:
成功則返回實際接收到的字符數(shù),失敗返回-1,錯誤原因會存于errno 中。
參數(shù)說明:
s:? ? ? ? ??socket描述符;
buf:? ? ? ?UDP數(shù)據(jù)報緩存區(qū)(包含所接收的數(shù)據(jù));?
len:? ? ? ?緩沖區(qū)長度。?
flags:? ? 調(diào)用操作方式(一般設(shè)置為0)。?
from:? ? ?指向發(fā)送數(shù)據(jù)的客戶端地址信息的結(jié)構(gòu)體(sockaddr_in需類型轉(zhuǎn)換);
fromlen:指針,指向from結(jié)構(gòu)體長度值。
(5)sendto() 函數(shù)解讀:向客戶端/服務(wù)器主機發(fā)送數(shù)據(jù)
int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
返回值說明:
成功則返回實際傳送出去的字符數(shù),失敗返回-1,錯誤原因會存于errno 中。
參數(shù)說明:
s:? ? ? socket描述符;
buf:? UDP數(shù)據(jù)報緩存區(qū)(包含待發(fā)送數(shù)據(jù));
len:? ?UDP數(shù)據(jù)報的長度;
flags:調(diào)用方式標(biāo)志位(一般設(shè)置為0);
to: 指向接收數(shù)據(jù)的主機地址信息的結(jié)構(gòu)體(sockaddr_in需類型轉(zhuǎn)換);
tolen:to所指結(jié)構(gòu)體的長度;
示例:
(1)服務(wù)器端
//服務(wù)器端
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>#define MAXLINE 4096
#define UDPPORT 8001
#define SERVERIP "192.168.255.129"using namespace std;int main(){int serverfd;unsigned int server_addr_length, client_addr_length;char recvline[MAXLINE];char sendline[MAXLINE];struct sockaddr_in serveraddr , clientaddr;// 使用函數(shù)socket(),生成套接字文件描述符;if( (serverfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){perror("socket() error");exit(1);}// 通過struct sockaddr_in 結(jié)構(gòu)設(shè)置服務(wù)器地址和監(jiān)聽端口;bzero(&serveraddr,sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);serveraddr.sin_port = htons(UDPPORT);server_addr_length = sizeof(serveraddr);// 使用bind() 函數(shù)綁定監(jiān)聽端口,將套接字文件描述符和地址類型變量(struct sockaddr_in )進(jìn)行綁定;if( bind(serverfd, (struct sockaddr *) &serveraddr, server_addr_length) < 0){perror("bind() error");exit(1);}// 接收客戶端的數(shù)據(jù),使用recvfrom() 函數(shù)接收客戶端的網(wǎng)絡(luò)數(shù)據(jù);client_addr_length = sizeof(sockaddr_in);int recv_length = 0;recv_length = recvfrom(serverfd, recvline, sizeof(recvline), 0, (struct sockaddr *) &clientaddr, &client_addr_length);cout << "recv_length = "<< recv_length <<endl;cout << recvline << endl;// 向客戶端發(fā)送數(shù)據(jù),使用sendto() 函數(shù)向服務(wù)器主機發(fā)送數(shù)據(jù);int send_length = 0;sprintf(sendline, "hello client !");send_length = sendto(serverfd, sendline, sizeof(sendline), 0, (struct sockaddr *) &clientaddr, client_addr_length);if( send_length < 0){perror("sendto() error");exit(1);}cout << "send_length = "<< send_length <<endl;//關(guān)閉套接字,使用close() 函數(shù)釋放資源;close(serverfd);return 0;
}
(2)客戶端
//客戶端
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>#define MAXLINE 4096
#define UDPPORT 8001
#define SERVERIP "192.168.255.129"using namespace std;int main(){int confd;unsigned int addr_length;char recvline[MAXLINE];char sendline[MAXLINE];struct sockaddr_in serveraddr;// 使用socket(),生成套接字文件描述符;if( (confd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){perror("socket() error");exit(1);}//通過struct sockaddr_in 結(jié)構(gòu)設(shè)置服務(wù)器地址和監(jiān)聽端口;bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(SERVERIP);serveraddr.sin_port = htons(UDPPORT);addr_length = sizeof(serveraddr);// 向服務(wù)器發(fā)送數(shù)據(jù),sendto() ;int send_length = 0;sprintf(sendline,"hello server!");send_length = sendto(confd, sendline, sizeof(sendline), 0, (struct sockaddr *) &serveraddr, addr_length);if(send_length < 0 ){perror("sendto() error");exit(1);}cout << "send_length = " << send_length << endl;// 接收服務(wù)器的數(shù)據(jù),recvfrom() ;int recv_length = 0;recv_length = recvfrom(confd, recvline, sizeof(recvline), 0, (struct sockaddr *) &serveraddr, &addr_length);cout << "recv_length = " << recv_length <<endl;cout << recvline << endl;// 關(guān)閉套接字,close() ;close(confd);return 0;
}
參考:https://blog.csdn.net/qingzhuyuxian/article/details/79736821
?
總結(jié)
以上是生活随笔為你收集整理的Socket:UDP协议小白的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 随笔:送给初次使用PCL库的小伙伴
- 下一篇: C++:多线程中的小白(1)基础概念