現代網路應用仰賴穩健的資料收發機制。本文從通訊端出發,剖析recv()recvfrom()send()sendto()等系統呼叫的應用,區分連線導向與非連線導向的差異。接著,文章深入比較三種 I/O 模型:阻塞式 I/O 適合簡單應用但效率較低;非阻塞式 I/O 允許應用程式持續運作,但需自行處理錯誤碼;多工 I/O 則透過非同步操作提升效能,但需額外系統呼叫。文章進一步探討多工 I/O、訊號驅動 I/O 和非同步 I/O 的工作原理、優缺點,並以 Mermaid 語法圖解說明。最後,文章闡述名稱與地址轉換的重要性,介紹 DNS 解析流程與資源記錄型別,並以 gethostbyname()gethostbyaddr()getservbyname()getservbyport()getservent() 等函式說明服務查詢與名稱解析的實務應用,提供程式碼範例與詳細解說。

探討網路通訊中的資料接收與傳送技術

在現代網路通訊中,資料的接收與傳送是核心技術之一。本文將探討如何透過通訊端(socket)進行資料的接收與傳送,並介紹不同的 I/O 模型及其應用場景。

資料接收技術

在網路通訊中,資料接收通常透過 recv()recvfrom() 系統呼叫來完成。這兩者分別適用於連線導向(如 TCP)和連線無導向(如 UDP)的通訊模式。

連線導向的資料接收

recv() 系統呼叫用於接收透過通訊端傳送的資料。其原型如下:

int recv(int sockfd, void *buf, int len, unsigned int flags);
  • sockfd 是用於讀取資料的通訊端描述符。
  • buf 是指向將儲存讀取資料的緩衝區的指標。
  • len 是緩衝區的最大長度(以位元組為單位)。
  • flags 通常設為 0。

連線無導向的資料接收

recvfrom() 系統呼叫用於接收連線無導向的資料包(如 UDP)。其原型如下:

int recvfrom(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *fromaddr, int *fromlen);
  • sockfd 是用於傳送資料的通訊端描述符。
  • msg 是指向將要傳送的資料的指標。
  • len 是資料的長度(以位元組為單位)。
  • flags 通常設為 0。
  • fromaddr 是包含 IP 地址和埠號的 sockaddr 結構,資料將傳送到這個地址。
  • fromlen 是前一個 sockaddr 結構中的大小。

資料傳送技術

在網路通訊中,資料傳送通常透過 send()sendto() 系統呼叫來完成。這兩者分別適用於連線導向和連線無導向的通訊模式。

連線導向的資料傳送

send() 系統呼叫用於透過通訊端傳送資料。其原型如下:

int send(int sockfd, const void *msg, int len, unsigned int flags);

連線無導向的資料傳送

sendto() 系統呼叫用於透過通訊端傳送連線無導向的資料包。其原型如下:

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *toaddr, int addrlen);

單一案例:伺服器與使用者端程式

使用者端程式

以下是一個簡單的使用者端程式範例,展示如何使用 connect()send() 功能來與伺服器進行通訊:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>

#define PORT 5000
#define HOST "example.com"
#define DIRSIZE 8000

int main(int argc, char **argv) {
    char hostname[100];
    char dir[DIRSIZE];
    int sd;
    struct sockaddr_in pin;
    struct hostent *hp;

    strcpy(hostname, HOST);
    if (argc > 2) {
        strcpy(hostname, argv[2]);
    }
    if ((hp = gethostbyname(hostname)) == NULL) {
        perror("gethostbyname");
        exit(1);
    }

    memset(&pin, 0, sizeof(pin));
    pin.sin_family = AF_INET;
    pin.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr))->s_addr;
    pin.sin_port = htons(PORT);

    if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    if (connect(sd, (struct sockaddr *) &pin, sizeof(pin)) == -1) {
        perror("connect");
        exit(1);
    }

    if (send(sd, argv[1], strlen(argv[1]), 0) == -1) {
        perror("send");
        exit(1);
    }

    if (recv(sd, dir, DIRSIZE, 0) == -1) {
        perror("recv");
        exit(1);
    }

    printf("%s\n", dir);
    close(sd);

    return 0;
}

次段落標題:內容解密:

此段程式碼展示了使用者端如何與伺服器建立連線並傳送資料。首先,程式會使用主機名稱取得 IP 地址,然後建立一個 TCP 檔案描述符。接著,透過 connect() 函式將檔案描述符與伺服器建立連線。最後,使用 send() 函式將資料傳輸至伺服器,並使用 recv() 函式接受伺服器回應後顯示出來。

假設伺服器程式處理成功:假設有一個名為 read_dir() 的函式,能夠回覆目錄內容。

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>

#define PORT 5000
#define DIRSIZE 8000

int main() {
    char dir[DIRSIZE];
    int sd, sd_current;
    struct sockaddr_in sin;
    socklen_t addrlen;

    if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(PORT);

    if (bind(sd, (struct sockaddr *) &sin, sizeof(sin)) == -1) {
        perror("bind");
        exit(1);
    }

    if (listen(sd, 5) == -1) {
        perror("listen");
        exit(1);
    }

    addrlen = sizeof(struct sockaddr_in);
    if ((sd_current = accept(sd, (struct sockaddr *) &sin, &addrlen)) == -1) {
        perror("accept");
        exit(1);
    }

     /* read_dir(dir); */

     if (recv(sd_current, dir,sizeof(dir), 0) == -1) {
         perror("recv");
         exit(1);
     }

     read_dir(dir);

     if (send(sd_current , dir , strlen(dir), 0) == -1 ) {
         perror("send");
         exit(1);
     }
     close(sd_current); close(sd);

     sleep(2);

     return 0;
}
小段落標題:內容解密:

此段程式碼展示了伺服器端如何接受來自客戶端請求並回應目錄內容。首先,伺服器端會建立一個 TCP 檔案描述符,然後繫結到指定的埠號並等待客戶端連線。當客戶端成功連線時,伺服器會接受客戶端傳送的目錄名稱並呼叫 read_dir 函式來取得目錄內容。最後,伺服器會將目錄內容回傳給客戶端。

I/O 模型

在網路通訊中,I/O 模型決定了應用程式如何處理資料輸入輸出操作。以下是三種常見的 I/O 模型及其特點:

塊塞 I/O 模型

在塊塞 I/O 模型中,當程式試圖讀取或寫入資料時,如果資料未準備好,程式會被塊塞直到資料可用或寫入完成。這種模型簡單易懂但效率較低。

此圖示展示了塊塞 I/O 模型中的工作流程:

  sequenceDiagram
 participant 應用程式 as 應用程式
 participant 核心 as 核心
 participant デバイス as デバイス

 應用程式->>核心: read()
 核心->>デバイス: 取得資料
 核心-->>應用程式: 資料準備完畢
小段落標題:內容解密:

在此圖示中,「應用程式」表示應用層程式碼,「核心」表示作業系統核心,「儲存裝置」則代表實際儲存裝置。當應用層要求讀取資料時,「應用層」會請求「核心」讀取這些資料。「核心」隨即請求「儲存裝置」提供該些資料。「儲存裝置」準備好之後再回傳「核心」,最終「核心」再將所需之資訊回傳至「應用層」。

####非塊塞 I/O 模型

在非塊塞 I/O 模型中,當程式試圖讀取或寫入資料時,如果資料未準備好,程式會立即傳回一個錯誤 EWOULDBLOCK ,而不是被塊塞。這種模型允許程式繼續執行其他任務。

此圖示展示了非塊塞 I/O 模型中的工作流程:

  sequenceDiagram
 participant 應用程式 as 應用程式
 participant 核心 as 核心
 participant デバイス as デバイス

 應用程式->>核心: read()
 internal Kernel: Check data availability
 核心-->>應用程式: EWOULDBLOCK error or data ready
小段落標題:內容解密:

在此圖示中,「應用層」表示應用層程式碼,「核心」表示作業系統核心。「儲存裝置」則代表實際儲存裝置。「應用層」請求讀取後,「核心」會檢查是否有足夠之資料可以供讀取。「儲存裝置」準備好後就會將所需之資料回傳給「應用層」。若沒有足夠之資料可供供給則會直接回傳給「EWOULDBLOCK」。

I/O 複雜化模型

在 I/O 複雜化模型中,程式會呼叫 select() 或 poll() 型系統呼叫等待多個檔案描述符就緒。這種模型允許程式等待多個檔案描述符同時就緒,提高了效率。

此圖示展示了 I/O 複雜化模型中的工作流程:

  sequenceDiagram
 participant 應用程式 as 應用程式
 participant 核心 as 核心
 participant デバイス as デバイス

 loop 每次select()
     應用程式->>核心: select()
     internal Kernel: Check multiple file descriptors availability.
 end

 核心-->>應用程式: 一或多個檔案描述符就緒.

 注意:
     應用層根據需要處理檔案描述符。
小段落標題:內容解密:

在此圖示中,「應用層」表示應用層程式碼,「核心」表示作業系統核心。「儲存裝置」則代表實際儲存裝置。「應用層」使用 select() 或 poll() 指令等待多個檔案描述符就緒。「核心」則檢查多個檔案描述符是否就緒。「儲存裝置」準備好後就會將所需之資料回傳給「應用層」。

多工 I/O 模式

在現代網路應用中,高效的 I/O 模型是確保系統效能的關鍵。多工 I/O 模式是其中一種常見的模型,它允許應用程式在等待 I/O 操作完成的同時繼續執行其他任務。這種模式透過使用非同步 I/O 操作來實作,從而提高了系統的整體效能。

多工 I/O 模式的工作原理

多工 I/O 模式的核心思想是將 I/O 操作與應用程式的執行分離開來。當應用程式發出一個 I/O 請求時,核心會暫時阻塞這個請求,但應用程式本身仍然處於執行狀態。一旦資料可用,應用程式會執行 recvfrom() 函式來接收資料,並立即傳回。

多工 I/O 模型的優缺點

多工 I/O 模型的主要優點在於它能夠提高系統的平行度,使得應用程式在等待 I/O 操作完成的同時可以繼續執行其他任務。然而,這種模型也有一個顯著的缺點,那就是它需要兩次系統呼叫而不是一次。

優點

  • 提高系統平行度:應用程式可以在等待 I/O 操作完成的同時繼續執行其他任務。
  • 資源利用率高:避免了因為等待 I/O 操作而導致的資源閒置。

缺點

  • 系統呼叫次數增加:需要兩次系統呼叫來完成一個 I/O 操作,這可能會增加一些額外的開銷。

訊號驅動 I/O 模型

訊號驅動 I/O 模型是另一種常見的非同步 I/O 模型。在這種模型中,核心會透過傳送 SIGIO 訊號來通知應用程式資料已經可用。這樣,應用程式可以在不被阻塞的情況下繼續執行其他任務。

工作原理

  1. 啟用訊號處理器:應用程式使用 sigaction() 函式啟用訊號處理器。
  2. 核心通知:當資料在核心端準備就緒時,核心會傳送 SIGIO 訊號給應用程式。
  3. 處理訊號:應用程式透過訊號處理器來處理 SIGIO 訊號,並呼叫 recvfrom() 函式來接收資料。

優點

  • 不阻塞應用程式:應用程式可以在等待資料到達的同時繼續執行其他任務。
  • 高效回應:一旦資料可用,應用程式可以立即進行處理。

缺點

  • 訊號處理複雜度高:需要額外編寫訊號處理邏輯,增加了開發和維護的難度。

非同步 I/O 模型

非同步 I/O 模型是最先進的一種 I/O 模型,它允許應用程式在不被阻塞的情況下進行完整的 I/O 操作。這種模型通常使用 io_read() 函式來定義描述符、緩衝區指標、緩衝區大小等引數。

工作原理

  1. 發起非同步請求:應用程式呼叫 io_read() 函式來發起非同步讀取請求。
  2. 核心處理:核心在後台進行資料讀取操作,不會阻塞應用程式。
  3. 完成通知:當整個操作完成後,核心會通知應用程式資料已經準備好。

優點

  • 完全非同步:應用程式不需要等待 I/O 操作完成,可以完全專注於其他任務。
  • 高效效能:適合需要高效資料處理的場景。

缺點

  • 實作複雜:需要更多的系統支援和更複雜的邏輯來實作。

名稱與地址轉換

名稱與地址轉換是網路通訊中的一個重要環節。DNS(Domain Name System)是實作這一功能的關鍵技術,它負責將主機名稱對映到 IP 地址。

DNS 的工作原理

DNS 使用資源記錄(Resource Records, RR)來儲存網域名稱與 IP 地址之間的對映關係。每個 RR 包含了網域名稱、記錄型別、時間戶戶戶(TTL)、資料長度以及實際資料。

資源記錄型別
  • A 記錄:將主機名稱對映到 32-bit IPv4 地址。
  • AAAA 記錄:將主機名稱對映到 128-bit IPv6 地址。
  • PTR 記錄:將 IP 地址對映到主機名稱。
  • MX 記錄:指定郵件交換伺服器。
  • CNAME 記錄:規範名稱,常用於將多個服務對映到同一個 IP 地址。

gethostbyname() 函式

gethostbyname() 函式是 C 語言中的一個標準函式,它能夠將主機名稱轉換為對應的 IP 地址。這個函式通常在網路程式設計中使用,以便將主機名稱轉換為可以直接使用的 IP 地址。

#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
函式說明

gethostbyname() 函式接受一個主機名稱字串(如 www.yahoo.com),並傳回一個 hostent 結構體指標。這個結構體包含了主機的官方名稱、別名列表、地址型別、地址長度以及地址列表等資訊。

主要元素
struct hostent {
    char *h_name;         // 主機官方名稱
    char **h_aliases;     // 主機別名列表
    int h_addrtype;       // 地址型別(通常是 AF_INET)
    int h_length;         // 地址長度(通常是 4)
    char **h_addr_list;   // 地址列表
};
範例程式碼
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>

int main() {
    struct hostent *he;
    struct in_addr addr;

    he = gethostbyname("www.yahoo.com");
    if (he == NULL) {
        herror("gethostbyname");
        exit(1);
    }

    printf("Official name is: %s\n", he->h_name);
    printf("IP address: %s\n", inet_ntoa(*(struct in_addr *)he->h_addr));

    return 0;
}

內容解密:

#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>

這些包含必要函式庫標頭檔案:stdio.h 用於標準輸入輸出、stdlib.h 用於標準函式庫函式、netdb.h 用於網路資料函式庫函式、arpa/inet.h 用於網際網路協定函式。

int main() {

定義主函式 main 作為程式入口。

struct hostent *he;
struct in_addr addr;

宣告兩個結構體變數 headdr 分別表示主機結構體和網際網路地址結構體。

he = gethostbyname("www.yahoo.com");

使用 gethostbyname() 函式根據給定主機名稱取得主機結構體指標。

if (he == NULL) {

檢查如果取得指標失敗則輸出錯誤訊息並終止執行。

printf("Official name is: %s\n", he->h_name);
printf("IP address: %s\n", inet_ntoa(*(struct in_addr *)he->h_addr));

成功取得後輸出官方名稱及IP位址。

gethostbyaddr() 函式

gethostbyaddr() 函式與 gethostbyname() 函式相反,它根據 IP 地址傳回對應的主機結構體。這個函式通常在需要根據 IP 地址查詢主機名稱時使用。

#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, int len, int type);
函式說明

gethostbyaddr() 函式接受三個引數:

  1. addr:指向包含 IP 地址的緩衝區指標。
  2. len:IP 地址長度。
  3. type:地址型別(通常是 AF_INET)。
主要元素

gethostbyname() 一致,傳回值為包含主機詳細資訊結構體指標 struct hostent*.

範例程式碼
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>

int main() {
    const char *ipstr = "66.94.230.32";
    struct in_addr ip;
    struct hostent *hp;

    if (!inet_aton(ipstr, &ip)) {
        fprintf(stderr, "can't parse IP address %s\n", ipstr);
        exit(1);
    }

    hp = gethostbyaddr((const char *)&ip, sizeof(ip), AF_INET);
    if (hp == NULL) {
        fprintf(stderr, "no name associated with %s\n", ipstr);
        exit(1);
    }

    printf("Host name: %s\n", hp->h_name);

    return 0;
}

內容解密:

#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>

包含必要函式庫標頭檔案:定義標準輸入輸出、stdlib.h 用於標準函式庫函式、netdb.h 用於網路資料函式庫函式、arpa/inet.h 用於網際網路協定函式。

const char *ipstr = "66.94.230.32";
struct in_addr ip;
struct hostent *hp;

宣告變數分別表示IP字串、IP地址及主機結構體指標。

if (!inet_aton(ipstr, &ip)) {

檢查IP字串轉換失敗則輸出錯誤訊息並終止執行。

hp = gethostbyaddr((const char *)&ip, sizeof(ip), AF_INET);
if (hp == NULL) {

根據IP地址取得主機結構體並檢查失敗則輸出錯誤訊息並終止執行。

printf("Host name: %s\n", hp->h_name);

成功取得後輸出該IP對應之官方名稱及IP位址。

Resolver 與服務查詢函式

Resolver 是一種解析器函式庫函式,負責解析 DNS 查詢並傳回相關資訊。Resolver 功能包括讀取 /etc/resolv.conf 檔案來取得 DNS Server 的 IP 地址開始進行解析動作。此外還有兩個重要服務查詢函式:getservbyname()getservbyport() ,前者根據服務名和協定查詢服務;後者根據埠號和協定查詢服務。以下展示如何使用這些函式進行服務查詢:

getservbyname()

這個函式根據服務名稱和協定型別來查詢相應服務並傳回相關結構體指標:

#include <netdb.h>

struct servent *getservbyname(const char *name, const char *proto);

getservbyport()

根據埠號和協定型別來查詢相應服務並傳回相關結構體指標:

struct servent *getservbyport(int port, const char *proto);

getservent()

讀取服務資料函式庫以識別要開啟連線的一系列服務:

struct servent *getservent(void)

以上就是玄貓對於I/O模組、DNS及相關基本API做的一些技術總結及分析說明