分散式應用開發的核心技術系列之——基於TCP/IP的原始訊息設計

2023-10-18 12:03:02

本文由葡萄城技術團隊原創並首發。轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

前言

本文的內容主要圍繞以下幾個部分:

  1. TCP/IP的簡單介紹。
  2. 訊息的介紹。
  3. 基於訊息分類的傳輸格式(流型別和XML型別)。
  4. 訊息體系的組成。

TCP/IP的簡單介紹

TCP/IP (傳輸控制協定/網際協定) 是網際網路中的基本通訊語言或協定。它其實是一個兩層的程式,分為高層與低層。高層為傳輸控制協定,負責聚集資訊或把檔案拆分成更小的包。這些包通過網路傳送到接收端的 TCP層,接收端的 TCP 層把包還原為原始檔案。低層是網際協定,它處理每個包的地址部分,使這些包正確地到達目的地。網路上的閘道器計算機根據資訊的地址來進行路由選擇。即使來自同一檔案的分包路由也有可能不同,但最後會在目的地匯合。TCP/IP 使用使用者端/伺服器模式進行通訊。

在架構上,TCP/IP 並不完全符合 0SI 的 7 層參考模型。傳統的開放式系統互連參考模型是一種通訊協定的 7 層抽象的參考模型,其中每一層執行某一特定任務。該模型的目的是使各種硬體在相同的層次上相互通訊。這 7 層是: 物理層、資料鏈路層、網路層、傳輸層、對談層、表示層和應用層。而 TCP/IP 通訊協定採用了 4 層的層級結構,每一層都呼叫它的下一層所提供的網路來完成自己的需求。這 4 層分別為:

  • 應用層:應用程式間溝通的層,如簡單郵件傳輸協定 (SMTP)、檔案傳輸協定 (FTP)、遠端網路存取協定 (Telnet) 等。
  • 傳輸層:在此層中,它提供結點間的資料傳送和應用程式之間的通訊服務,主要功能是資料格式化、資料確認和丟失重傳等。如傳輸控制協定 (TCP)、使用者資料包協定 (UDP) 等,TCP 和 UDP 給封包加入傳輸資料並把它傳送到下一層中,這一層負責傳送資料,並且確定資料已被送達並接收。
  • 互連網路層:負責提供基本的資料封包傳送功能,讓每一個封包都能夠到達目的主機 (但不檢查是否被正確接收),如網際協定 (IP)。
  • 網路介面層 (主機-網路層): 接收 IP 資料包並進行傳輸,從網路上接收物理幀,抽取 IP 資料包轉交給下一層,管理實際的網路媒體,定義如何使用實際網路 (如 Ethernet、Serial Line 等) 來傳送資料。

Tcp/IP中常用的函數

1.Socket函數

int socket(int domain,int type,int protocol),

domain 指明所使用的協定族,通常為 PF INET,表示網際網路協定族(TCP/IP 協定族); type 引數指定 socket 的型別;用於 TCP 的SOCK STREAM 或用於 UDP 的 SOCK DGRAM; protocol 通常賦值[0]。socket函數呼叫返回一個整型 socket 描述符,可以在後面呼叫它。

2.bind函數:

bind 函數將 socket 與本機上的一個埠相關聯,隨後就可以在該埠監聽服務請求。bind 函數原型為:

int bind(int sockfd,struct sockaddr *my addr, int addrlen);

sockfd 是呼叫 socket 函數返回的 socket 描述符;my addr 是一個指向包含有本機 IP 地址及埠號等資訊的 sockaddr 型別的指標:addrlen 常被設定為 sizeof (struct sockaddr)。

3.connect連線函數:

面向連線的客戶程式使用連線 (connect) 函數來設定 socket 並與遠端伺服器建立一個 TCP 連線,其函數原型為:

int connect(int sockfd, struct sockaddr *serv addr,int addrlen);

sockfd 是 socket 函數返回的 socket 描述符; serv addr 是包含遠端主機 IP 地址和埠號的指標; addrlen 是遠端地址結構的長度。connect 函數在出現錯誤時返回-1,並且設定 errno 為相應的錯誤碼。進行使用者端程式設計無須呼叫 bind 0,因為這種情況下只需要知道目的機器的 IP 地址即可,而客戶通過哪個埠與伺服器建立連線並不需要關心socket 執行體程式自動選擇一個未被佔用的埠,並通知程式資料什麼時候到達埠。

4.listen監聽函數:

網路監聽 (listen) 函數使 socket 處於被動的監聽模式,併為該socket 建立一個輸入資料佇列,將到達的服務請求儲存在此佇列中,直到程式處理它們。

int listen(int sockfd, int backlog);

sockfd 是 Socket 系統呼叫返回的 socket 描述符;backlog 指定在請求佇列中允許的最大請求數,進入的連線請求將在佇列中等待接收函數accept 0)(參考下文)。backlog 對佇列中等待服務的請求的數目進行了限制,通常系統預設值為 20。如果一個服務請求到來時,輸入佇列已滿該 socket 將拒絕連線請求,客戶將收到一個出錯資訊。

5.accept接收函數:

accept0函數讓伺服器接收客戶的連線請求。在建立好輸入佇列後,伺服器就呼叫 accept 函數,然後睡眠並等待客戶的連線請求。

int accept(int sockfd, void *addr, int *addrlen);

sockfd 是被監聽的 socket 描述符,addr 通常是一個指向sockaddr_in 變數的指標,該變數用來存放提出連線請求服務的主機的資訊(某臺主機從某個埠發出該請求); addrlen 通常為一個指向值為sizeof (struct sockaddr in) 的整型指標變數。出現錯誤時 accept 函數返回-1 並設定相應的 errno 錯誤碼。

6.sendto函數和recvfrom函數:

int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen):

to 表示目的機的IP 地址和端號資訊,而 tolen 常常被賦值為 sizeof (struct sockaddr)。sendto 函數返回實際傳送的資料位元組長度或在出現傳送錯誤時返回-1。

int recyfrom(int sockfd,void *buf,int len,unsigned int flags,structsockaddr *from,int *fromlen);

from 是一個 struct sockaddr 型別的變數,該變數儲存源主機的 IP 地址及埠號。fromlen 常置為 sizeof (struct sockaddr),當 recvfrom()返回時,fromlen 包含實際存入 from 中的資料位元組數。recvfrom() 函數返回接收到的位元組數或當出現錯誤時返回-1,並設定相應的 errno 錯誤碼。

7.shutdown函數

shutdown函數來關閉該 socket。該函數允許你只停止某個方向上的資料傳輸,而另一個方向上的資料傳輸繼續進行。

int shutdown(int sockfd,int how);

sockfd 是需要關閉的 socket 的描述符。引數 how 允許為 shutdown操作選擇以下幾種方式:

  • 0一一不允許繼續接收資料
  • 1--不允許繼續傳送資料
  • 2一一不允許繼續傳送和接收資料

shutdown 在操作成功時返回 0,在出現錯誤時返回-1 並設定相應errno 錯誤碼。

8.fcntl函數

fcntl函數可以改變已開啟的檔案的性質。

int fcntl (int fields, int cmd, .../* int arg */) ;

9.getsockopt 與 setsockopt 函數

這兩個函數可以獲取或者設定與某個通訊端關聯的選項。為了操作通訊端層的選項,應該將層的值指定為 SOL SOCKET。為了操作其他層的選項控制選項的合適協定號必須給出。例如,為了表示一個選項是由 TCP 解析,層應該設定為協定號 TCP。

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

10.select函數

select 函數是一種用於多路複用(Multiplexing)的系統呼叫或函數。它通常用於處理多個輸入和輸出流,以實現非同步的 I/O 操作。

int select(int n, fd set * readfds, fd set * writefds, fd set * exceptfds,struct timeval * timeout);

引數 n 代表最大的檔案描述詞加 1,引數 readfds、writefds 和exceptfds 稱為描述片語,是用來回傳該描述詞的讀、寫或例外的狀況。

11.poll函數

int poll(struct pollfd fds[], nfds t nfds, int timeout);

其中 fds 是一個 struct pollfd 結構型別的陣列,用於存放需要檢測其狀態的 socket 描述字。struct pollfd 的定義如下:

struct pollfd {
        //descriptor to check
        int fd;
        //events of interest on fd
        short events;
        //events that occurred on fd
        short revents;
}

什麼是訊息

訊息是分散式應用開發中,網路上兩個邏輯實體之間進行通訊時,在程式設計層面的最小單元。

對以上定義,有以下幾點說明:

(1) 訊息的概念存在於開發工作中,位於程式設計層面。在系統執行時,對應用使用者是透明的。

(2) 網路上的兩個邏輯實體,是指兩個可獨立執行的程式,它們可以部署於網路中兩個不同的物理裝置上,也可以部署於同一個物理裝置上,但一般是兩個沒有父子關係的獨立程序 (這一點與 IPC 程式設計中最基本的訊息概念不同)。

(3) 訊息是分散式通訊時程式設計層面的最小單元,即無論參與通訊的資料量是多還是少,程式程式碼中都通過傳送與接收一個或多個訊息來實現。

(4) 網路上兩個應用之間的通訊,包括資料流傳輸與遠端過程(函數)呼叫兩種型別。

(5) 利用訊息可以實現分散式應用之間的結構化資料通訊。也就是說程式設計人員在通訊層面面對的不再是實際位元組流,而是可以由多種資料型別組合而成的結構化資料單元。

其實,這種結構化資料單元本身就是「訊息」,它對外可以表現為結構或者類。因此,當基於以上定義的訊息機制建立起來以後,程式設計師在編碼過程中,當需要進行分散式通訊時,只需要生成相應的訊息,然後呼叫相應的傳送與接收介面方便地實現即可,而不需要了解 TCP/IP 知識,不需要掌握socket 程式設計的基本技能,也不需要考慮序列訊息過多、並行訊息過多、網路流量控制等其他多方面的問題,從而才能真正地將分散式應用開發的精力集中到業務實現上來,極大地提高了分散式系統的開發效率與質量,特別是大型分散式系統。

關於訊息的存在形式,在傳統 C 語言中,可以是一個結構 struct;在物件導向語言中 (C++ 或 Java),則可以是一個類 class。

基於訊息分類的傳輸格式

基於訊息傳輸的格式不同,可以將訊息分為流訊息和XML訊息,流訊息基於二進位制位元組流式格式傳輸,XML訊息基於XML格式的字串傳輸。

流訊息

流訊息是指在計算機系統中,以流(stream)的方式傳遞和處理的訊息。流訊息由一系列連續的資料組成,在傳送端按照一定的順序生成,並以流的形式傳輸到接收端。傳輸過程中,接收端可以逐個讀取流中的資料。,對於流訊息來說,無論程式設計師如何表示訊息,訊息在真正傳送之前,都需要先轉換為二進位制流格式,這個轉換過程稱為流化 (Streamlization),也可稱序列化 (Serilization),

XML訊息

XML訊息是指使用可延伸標示語言(XML)作為訊息格式的資料傳輸方式。XML是一種用於描述和儲存資料的文字標示語言,它使用標籤來定義資料的結構和屬性。在 XML 訊息機制中,程式設計師用 XML 格式表示訊息內容之後,不需要再為傳送傳輸做任何格式轉換工作(不包括為安全傳輸所做的加密工作),直接就可以以 XML 字串格式傳送出去。XML 訊息應用也比較廣泛,如 Web Service 中的 SOAP 協定,就是基於 XML 訊息設計實現的。

舉個例子:基於流訊息的設計與實現方法

下面小編為大家簡單地介紹一下如何在兩個應用程式上傳送和接受一個人的資訊(包括身高、姓名和年齡)

(1)定義一個類存放人的資訊:

struct Person {
        char name[20] ;
        float height;
        int age;
}
struct Person p;
strcpy(p.name ,"Michael Zhang");
height = 170.00;
age = 30;

(2)將資訊序列結構化

char sendStream[1024] = {0};
sprintf(sendStream,"|%s|%f"%d",p.name, p.height, p.age);

(3)傳送方傳送位元組流:

/*注: 這裡省略建立/管理/關閉 TCP 連線的程式碼*/
char datalen[4+1] = (0);
sprintf(datalen,"04d" , strlen (sendStream) );
if(SendBytes ( socket, datalen, 4) == -1) {
        return -l;
}
if(SendBytes(socket, sendStream, strlen(sendStream)) == -1) {
        return -1
}

注意,以上程式碼中的函數 SendBytes 實際上是保證一定長度的位元組流全部成功傳送完畢後才返回,主要是由於在 socket 上呼叫 send 或 write函數不能保證一次能將一定長度的位元組流傳送完。SendBytes 的基本思想是迴圈傳送,直至成功發完所有位元組,其實現程式碼如下所示:

int SendBytes (int sd, const void *buffer, unsigned len) {
        int rez = 0;
        int leftlen = len;
        int readlen = 0:
}
while(true) {
        rez = write (socket, (char *)buffer+readlen, len-readlen);
        if(rez < 0) {
                if (errno != EWOULDBLOCK && errno != EINTR) {
                        ErrorMsg("Error is serious );
                        DisConnect(socket);
        }
    return -l:
    }
    readlen += rez;
    leftlen -= rez;
    if(leftlen <= 0){
    break;
    }
   }
return len:
}

(4)接收方接收位元組流:

char datalen[4+1] = {0};
char receiveStream[1024] = {0};
sprintf(datalen,"%04d", strlen(sendStream)) ;
if(ReceiveBytes(socket, datalen, 4) == -1 {
        return -l;
}
int packet len = atoi(datalen) :
if(ReceiveBytes (socket, receiveStream, packet len) == -1) {
        return -l;
}

ReceiveBytes函數可以參考第三步傳送方傳送該位元組流。

(5)位元組流反序列化得到結構:

struct Person p;
sscanf(receiveStream,"%[`|]|%f|%d", p.name, &p.height, &p.age) ;

總結

本文簡單的介紹了TCP/IP協定及其常用的介面函數,然後介紹了TCP/IP協定中訊息的分類以及傳輸格式,最終以一個簡單的訊息傳送小例子作為收尾。如對內容有何意見建議,歡迎大家在評論區中留言和討論。

參考書籍:《訊息設計與開發——分散式應用開發的核心技術》 何小朝

擴充套件連結:

從表單驅動到模型驅動,解讀低程式碼開發平臺的發展趨勢

低程式碼開發平臺是什麼?

基於分支的版本管理,幫助低程式碼從專案交付走向客製化化產品開發