本文由葡萄城技術團隊原創並首發。轉載請註明出處:葡萄城官網,葡萄城爲開發者提供專業的開發工具、解決方案和服務,賦能開發者。
前言
本文的內容主要圍繞以下幾個部分:
- TCP/IP的簡單介紹。
- 消息的介紹。
- 基於消息分類的傳輸格式(流類型和XML類型)。
- 消息體系的組成。
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協議中消息的分類以及傳輸格式,最終以一個簡單的消息發送小例子作爲收尾。如對內容有何意見建議,歡迎大家在評論區中留言和討論。
參考書籍:《消息設計與開發——分佈式應用開發的核心技術》 何小朝
擴展鏈接: