Socket:UDP協議小白

Upupup6發表於2020-10-31

UDP(user datagram protocol)使用者資料包協議,屬於傳輸層。

首先要搞清楚網路通訊的幾個層次:

OSI 是 Open System Interconnection 的縮寫,譯為“開放式系統互聯”。
OSI 模型把網路通訊的工作分為 7 層,從下到上分別是物理層、資料鏈路層、網路層、傳輸層、會話層、表示層和應用層。
OSI 只是存在於概念和理論上的一種模型,它的缺點是分層太多,增加了網路工作的複雜性,所以沒有大規模應用。後來人們對 OSI 進行了簡化,合併了一些層,最終只保留了 4 層,從下到上分別是介面層、網路層、傳輸層和應用層,這就是大名鼎鼎的 TCP/IP 模型

OSI 七層網路模型和 TCP/IP 四層網路模型的對比

我們平常使用的程式(或者說軟體)一般都是通過應用層來訪問網路的,程式產生的資料會一層一層地往下傳輸,直到最後的網路介面層,就通過網線傳送到網際網路上去了。資料每往下走一層,就會被這一層的協議增加一層包裝,等到傳送到網際網路上時,已經比原始資料多了四層包裝。整個資料封裝的過程就像俄羅斯套娃。

當另一臺計算機接收到資料包時,會從網路介面層再一層一層往上傳輸,每傳輸一層就拆開一層包裝,直到最後的應用層,就得到了最原始的資料,這才是程式要使用的資料。

給資料加包裝的過程,實際上就是在資料的頭部增加一個標誌(一個資料塊),表示資料經過了這一層,我已經處理過了。給資料拆包裝的過程正好相反,就是去掉資料頭部的標誌,讓它逐漸現出原形。

我們所說的 socket 程式設計,是站在傳輸層的基礎上,所以可以使用 TCP/UDP 協議,但是不能幹「訪問網頁」這樣的事情,因為訪問網頁所需要的 http 協議位於應用層。

兩臺計算機進行通訊時,必須遵守以下原則:

  • 必須是同一層次進行通訊,比如,A 計算機的應用層和 B 計算機的傳輸層就不能通訊,因為它們不在一個層次,資料的拆包會遇到問題。
  • 每一層的功能都必須相同,也就是擁有完全相同的網路模型。如果網路模型都不同,那不就亂套了,誰都不認識誰。
  • 資料只能逐層傳輸,不能躍層。
  • 每一層可以使用下層提供的服務,並向上層提供服務。

TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百個互為關聯的協議,其中 TCP 和 IP 是最常用的兩種底層協議,所以把它們統稱為“TCP/IP 協議族”。

也就是說,“TCP/IP模型”中所涉及到的協議稱為“TCP/IP協議族”。

個人學習的socket 程式設計是基於 TCP 和 UDP 協議的,它們的層級關係如下圖所示:

UDP是面向非連線的協議,它不與對方建立連線,而是直接把資料包發給對方。UDP無需建立類如三次握手的連線,使得通訊效率很高。因此UDP適用於一次傳輸資料量很少、對可靠性要求不高的或對實時性要求高的應用場景。

使用UDP通訊的過程

服務端:

      (1)使用函式socket(),生成套接字檔案描述符;

      (2)通過struct sockaddr_in 結構設定伺服器地址和監聽埠;

      (3)使用bind() 函式繫結監聽埠,將套接字檔案描述符和地址型別變數(struct sockaddr_in )進行繫結;

      (4)接收客戶端的資料,使用recvfrom() 函式接收客戶端的網路資料;

      (5)向客戶端傳送資料,使用sendto() 函式向伺服器主機傳送資料;

      (6)關閉套接字,使用close() 函式釋放資源;

    客戶端:

      (1)使用socket(),生成套接字檔案描述符;

      (2)通過struct sockaddr_in 結構設定伺服器地址和監聽埠;

      (3)向伺服器傳送資料,sendto() ;

      (4)接收伺服器的資料,recvfrom() ;

      (5)關閉套接字,close() ;

(1)socket()函式解讀:生成套接字檔案描述符

socket() 函式用來建立套接字,確定套接字的各種屬性,然後伺服器端要用 bind() 函式將套接字與特定的 IP 地址和埠繫結起來,只有這樣,流經該 IP 地址和埠的資料才能交給套接字處理。類似地,客戶端也要用 connect() 函式建立連線。

int socket(int af, int type, int protocol);//Linux下//在 Linux 下使用 <sys/socket.h> 標頭檔案中 socket() 函式來建立套接字
SOCKET socket(int af, int type, int protocol);//windows下
//兩者除了返回值型別不同,其他都是相同的。Windows 不把套接字作為普通檔案對待,而是返回 SOCKET 型別的控制程式碼。請看下面的例子: 
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //建立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 為資料傳輸方式/套接字型別,常用的有 SOCK_STREAM(流格式套接字/面向連線的套接字) 和 SOCK_DGRAM(資料包套接字/無連線的套接字)

3) protocol 表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。
有了地址型別和資料傳輸方式,還不足以決定採用哪種協議嗎?為什麼還需要第三個引數呢?(因為有時TCP和UDP同時滿足兩種情況,這時候就不知道用哪種了。)

正如大家所想,一般情況下有了 af 和 type 兩個引數就可以建立套接字了,作業系統會自動推演出協議型別,除非遇到這樣的情況:有兩種不同的協議支援同一種地址型別和資料傳輸型別。如果我們不指明使用哪種協議,作業系統是沒辦法自動推演的。

本教程使用 IPv4 地址,引數 af 的值為 PF_INET。如果使用 SOCK_STREAM 傳輸資料,那麼滿足這兩個條件的協議只有 TCP,因此可以這樣來呼叫 socket() 函式:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP協議

這種套接字稱為 TCP 套接字。

如果使用 SOCK_DGRAM 傳輸方式,那麼滿足這兩個條件的協議只有 UDP,因此可以這樣來呼叫 socket() 函式:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP協議

這種套接字稱為 UDP 套接字。

上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設為 0,系統會自動推演出應該使用什麼協議,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //建立TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字


大家需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址,後面的教程會經常用到。

你也可以使用 PF 字首,PF 是“Protocol Family”的簡寫,它和 AF 是一樣的。例如,PF_INET 等價於 AF_INET,PF_INET6 等價於 AF_INET6。

(2)struct sockaddr_in 結構體解讀:設定伺服器地址和監聽埠

上述提到的sockaddr_in是什麼?還有一個是sockaddr ?這倆個分別是什麼有什麼區別

struct sockaddr 和 struct sockaddr_in 這兩個結構體用來處理網路通訊的地址。

下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數字表示所佔用的位元組數):

sockaddr_in 結構體,sockaddr在標頭檔案#include <sys/socket.h>中定義,sockaddr的缺陷是:sa_data把目標地址和埠資訊混在一起了:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址型別
    char         sa_data[14];  //IP地址和埠號
};

sockaddr_in在標頭檔案#include<netinet/in.h>或#include <arpa/inet.h>中定義,該結構體解決了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() 的第一個引數的含義相同,取值也要保持一致。

地址族(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 的長度為兩個位元組,理論上埠號的取值範圍為 0~65536,但 0~1023 的埠一般由系統分配給特定的服務程式,例如 Web 服務的埠號為 80,FTP 服務的埠號為 21,所以我們的程式要儘量在 1024~65536 之間分配埠號。

埠號需要用 htons() 函式轉換,為什麼呢?

3) sin_addr 是 struct in_addr 結構體型別的變數,下面會詳細講解。

4) sin_zero[8] 是多餘的8個位元組,沒有用,一般使用 memset() 函式填充為 0。上面的程式碼中,先用 memset() 將結構體的全部位元組填充為 0,再給前3個成員賦值,剩下的 sin_zero 自然就是 0 了。

該結構體中提到的另外一個結構體in_addr 結構體,該結構體只包含一個成員,如下所示:

struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

in_addr_t 在標頭檔案 <netinet/in.h> 中定義,等價於 unsigned long,長度為4個位元組。也就是說,s_addr 是一個整數,而IP地址是一個字串,所以需要 inet_addr() 函式進行轉換,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

執行結果:
13278343

sockaddr_in 結構體

至於為什麼要搞的這麼複雜?

為什麼要搞這麼複雜,結構體中巢狀結構體,而不用 sockaddr_in 的一個成員變數來指明IP地址呢?socket() 函式的第一個引數已經指明瞭地址型別,為什麼在 sockaddr_in 結構體中還要再說明一次呢,這不是囉嗦嗎?

這些繁瑣的細節確實給初學者帶來了一定的障礙,我想,這或許是歷史原因吧,後面的介面總要相容前面的程式碼。各位讀者一定要有耐心,暫時不理解沒有關係,根據教程中的程式碼“照貓畫虎”即可,時間久了自然會接受。

總結:

  1. 二者長度一樣,都是16個位元組,即佔用的記憶體大小是一致的,因此可以互相轉化。二者是並列結構,指向sockaddr_in結構的指標也可以指向sockaddr。
  2. sockaddr常用於bind、connect、recvfrom、sendto等函式的引數(為社麼呢??),指明地址資訊,是一種通用的套接字地址。 

bind() 第二個引數的型別為 sockaddr,而程式碼中卻使用 sockaddr_in,然後再強制轉換為 sockaddr,這是為什麼呢?

因為sockaddr 和 sockaddr_in 的長度相同,都是16位元組,只是將IP地址和埠號合併到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和埠號,例如”127.0.0.1:80“,遺憾的是,沒有相關函式將這個字串轉換成需要的形式,也就很難給 sockaddr 型別的變數賦值,所以使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換型別時不會丟失位元組,也沒有多餘的位元組。

  1. sockaddr_in 是internet環境下套接字的地址形式。所以在網路程式設計中我們會對sockaddr_in結構體進行操作,使用sockaddr_in來建立所需的資訊,最後使用型別轉化就可以了。一般先把sockaddr_in變數賦值後,強制型別轉換後傳入用sockaddr做引數的函式:sockaddr_in用於socket定義和賦值;sockaddr用於函式引數。

可以認為,sockaddr 是一種通用的結構體,可以用來儲存多種型別的IP地址和埠號,而 sockaddr_in 是專門用來儲存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來儲存 IPv6 地址,它的定義如下:

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址型別,取值為AF_INET6
    in_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
};

正是由於通用結構體 sockaddr 使用不便,才針對不同的地址型別定義了不同的結構體。

 

擴充套件:

兩個函式 htons() 和 inet_addr()。

htons()作用是將埠號由主機位元組序轉換為網路位元組序的整數值。(host to net)

inet_addr()作用是將一個IP字串轉化為一個網路位元組序的整數值,用於sockaddr_in.sin_addr.s_addr。

inet_ntoa()作用是將一個sin_addr結構體輸出成IP字串(network to ascii)。比如:

printf("%s",inet_ntoa(mysock.sin_addr));

htonl()作用和htons()一樣,不過它針對的是32位的(long),而htons()針對的是兩個位元組,16位的(short)。

與htonl()和htons()作用相反的兩個函式是:ntohl()和ntohs()。 

   

(3)bind()函式解讀:繫結監聽埠,將套接字檔案描述符和地址型別變數(struct sockaddr_in )進行繫結

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

下面的程式碼,將建立的套接字與IP地址 127.0.0.1、埠 1234 繫結:

//建立套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//建立sockaddr_in結構體變數
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每個位元組都用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 結構體變數的指標,addrlen 為 addr 變數的大小,可由 sizeof() 計算得出。

(4)recvfrom() 函式解讀:伺服器端接收客戶端的網路資料

int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen); 

返回值說明:

    成功則返回實際接收到的字元數,失敗返回-1,錯誤原因會存於errno 中。

  引數說明:

    s:          socket描述符;
    buf:       UDP資料包快取區(包含所接收的資料); 
    len:       緩衝區長度。 
    flags:    呼叫操作方式(一般設定為0)。 
    from:     指向傳送資料的客戶端地址資訊的結構體(sockaddr_in需型別轉換);
    fromlen:指標,指向from結構體長度值。

(5)sendto() 函式解讀:向客戶端/伺服器主機傳送資料

int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);

返回值說明:

    成功則返回實際傳送出去的字元數,失敗返回-1,錯誤原因會存於errno 中。

  引數說明:

    s:      socket描述符;
    buf:  UDP資料包快取區(包含待傳送資料);
    len:   UDP資料包的長度;
    flags:呼叫方式標誌位(一般設定為0);
    to:  指向接收資料的主機地址資訊的結構體(sockaddr_in需型別轉換);
    tolen:to所指結構體的長度;

示例:

(1)伺服器端

//伺服器端
#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;

    // 使用函式socket(),生成套接字檔案描述符;
    if( (serverfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){
        perror("socket() error");
        exit(1);
    }

    // 通過struct sockaddr_in 結構設定伺服器地址和監聽埠;
    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() 函式繫結監聽埠,將套接字檔案描述符和地址型別變數(struct sockaddr_in )進行繫結;
    if( bind(serverfd, (struct sockaddr *) &serveraddr, server_addr_length) < 0){
        perror("bind() error");
        exit(1);
    }

    // 接收客戶端的資料,使用recvfrom() 函式接收客戶端的網路資料;
    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;

    // 向客戶端傳送資料,使用sendto() 函式向伺服器主機傳送資料;
    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;

    //關閉套接字,使用close() 函式釋放資源;
    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 結構設定伺服器地址和監聽埠;
    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);

    // 向伺服器傳送資料,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;

    // 接收伺服器的資料,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;

    // 關閉套接字,close() ;
    close(confd);

    return 0;
}

參考:https://blog.csdn.net/qingzhuyuxian/article/details/79736821

 

相關文章