--- creator: zach tags: 計算機網路, socket created: 2021-09-20 --- # 網路是怎樣連接的(五)Socket API ## 思考重點 - 如何將應用程序消息委託給協議棧發送? - socket是調用那些函式進行收發操作? ## 核心知識 ![](https://i.imgur.com/DjOw83s.png) <center>委託協議棧收發重點</center> ## 協議棧如何進行收發操作 現在將擁有的數據整理一下,首先HTTP消息封包已經由應用程式打包完成,服務器IP地址也已經透過DNS[^1]請求機制獲得。在兩個前提條件都滿足的狀況下,我們就可以著手思考要怎麼將這些數據發給對方服務器的應用程式 發送數據其實是調用**多個socket庫函式**達成的,藉由委託多個函式API進行一連串的任務交互,每個任務完成的項目不同,有建立連接部分、斷開連線等等,這些操作的用意就是為了保證雙方是否接收到消息與回應是否正常[^2] ## 使用socket實現 為了使雙方應用程式之間建立一條專屬的溝通管道,我們調用了socket庫函式,很多書上將建立socket形容成搭建一條無形的*通道*,雙方可透過這條通道來實現消息的收發操作,不過並不是說建立socket後計算機才被允許與網路進行通訊,其實早在建立socket之前計算機就可以向網路收發消息了,**建立socket比較像是彼此確定我們該走哪一條傳輸通道找到對方的應用程式** 順著這個思路這小節將介紹應用程式是如何調用socket庫函式向下層委託收發。我喜歡用一個網路訂房的比喻來形容建立socket連線的步驟,就像我們是使用手機app進行訂房,委託系統進行操作,真正的流程實際上是不得而知的,這就像站在應用層的視角看待socket連線[^3] ### 創建階段 **目的**: 依照指定類型創建socket > 就下載這個訂房app吧! #### 程式案例 我們先來看看socket create部分: ```c:socket_create int socket(int domain, int type, int protocol); ``` ![](https://i.imgur.com/ES3W38o.png) <center>socket種類表</center> - domain - 決定socket在網路傳輸中要使用哪個通訊協定的家族系列 - AF_INET為TCP/IP網路通訊協定 - type - 指定socket的類型 - SOCK_STREAM對應的是TCP協定 - SOCK_DGRAM對應的是UDP協定 - SOCK_RAW可以是IP或ICMP - protocol - 通常在設定完domain與type以後通訊種類就大抵完成了,因此 protocol 一般都設為0,表示依照指定類型設定預設協議 ```c:舉例 socket(AF_INET, SOCK_STREAM, 0); // 選擇 TCP socket(AF_INET, SOCK_STREAM, 6); // 還是 TCP socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 依然是 TCP socket(AF_INET, SOCK_DGRAM, 0); // 這次是 UDP ``` ```c:實例 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <sys/socket.h> // #define AF_INET 2 // #define SOCK_STREAM 1 int main(int argc, char *argv[]){ uint32_t socket_identifier = 0; /*創建socket*/ socket_identifier = socket(AF_INET , SOCK_STREAM , 0); switch(socket_identifier){ case 1: // 正數 case 2: ... case n: printf("socket create successfully!\n"); break; case -1: printf("socket create error!\n"); } return 0; } ``` socket創建成功一般都會回傳一個大於0的標示符,若回傳負數則代表socket創建異常 ### 連線階段 **目的**: 使兩個應用程式建立連線通道 >就用這個帳號登入app吧 #### 程式案例 來看看連現階段吧: ```c:socket_connect int connect(int fd, struct sockaddr *server, int addrlen); ``` connect函數是客戶端發起的請求,目的是為了與服務器建立連線,介紹參數前,先來講講sockaddr這個結構體,它裡面裝的主要就是socket連線需要的消息,這裡暫且以IPv4為例: ```c:sockaddr struct in_addr{ ip_addr_t s_addr; }; struct sockaddr{ unsigned char sin_family; //AF_INET所以是IPv4 unsigned short sin_port; // 應用程式端口號 struct in_addr sin_addr; // 服務器IP地址 unsigned char sin_zero[8]; // 不會用到 }; ``` - fd - socket的描述符 - 其實就是上面創建socket的回傳值標示符socket_identifier - server - sockaddr結構體,負責提供socket所有的連線消息 - addrlen - 結構體sockaddr長度 ```c:實例 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <malloc.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define SERV_PORT 8080 typedef sockaddr* info; int main(argc, char* argv[]){ info serv=(info)calloc(0, sizeof(sockaddr)); uint8_t resp; /*創建socket部分*/ /*填入socket消息*/ serv->sin_family = AF_INET; serv->sin_port = htons(SERV_PORT); inet_pton(AF_INET, "127.0.0.1", serv->sin_addr); resp = connect(socket_identifier, (info)serv, sizeof(sockaddr)); if(resp < 0) printf("socket connect error!\n"); return 0; } ``` 藉由創建socket函式API,我們取得本地端的socket編號socket_identifier[^4],接下來的任務就是要跟服務器上的應用程式進行連接,IP地址可以幫助我們找到服務器地址,而端口號[^5]則可以幫我們找到執行在服務器上的應用程式 ### 收發階段 當我們成功建立連接後,資料就可以透過socket在兩個應用程式之間流通,接著我們可以透過使用read()/recv()來獲取資料,使用write()/send()來傳輸資料。read()/write與recv()/send()的不同只差在recv()/send()的輸入參數多了一個描述符flag,這個描述符提供操作更多的細節控制選項,不過我們以下還是使用通用的收發socket API &rarr; read()/write #### 發送消息 **目的**: 將資料寫入 Socket 中並發送出去 >就是這間了,趕緊下單! ##### 程式案例 ```c:socket_write ssize_t write(int fd, const void *buf, size_t nbyte); ``` - fd是描述符 - buf是寫入資料的緩衝區,使用const修飾參數buf防止內容被更改 - nbyte是buffer大小 ```c:實例 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <malloc.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> define MAX 1024 char* buf=(char*)malloc(max); // buffer int main(argc, char* argv[]){ ssize_t s_write; /*創建socket部分*/ /*socket連線部分*/ /*socket write*/ strcpy(buf, "socket test"); s_write = write(socket_identifier, buf, MAX); if(s_write < 0){ printf("socket write error!\n"); } else{ printf("socket write data length=%d\n", s_read); // 送出了多少資料長度 } ... return 0; } ``` 透過調用write函式將資料發送出去,回傳值可以判斷發送的資料長度,若是buffer大小為0會返回0,失敗則回傳-1。它跟待會要介紹的接收消息read()其實就是兩個死對頭,一個急著將資料壓到buffer裡,一個忙著將資料拿出來發出去 #### 接收消息 **目的**: 透過連線中的 Socket讀取資料 >系統提示~您已經下訂成功! ##### 程式案例 ```c:socket_read ssize_t read(int fd, void* buf, size_t nbyte); ``` - fd前面講過了 - buf是讀取資料的緩衝區,socket就是把資料推送到這裡 - nbyte是buffer大小 ```c:實例 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <malloc.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> define MAX 1024 char* buf=(char*)malloc(max); // buffer int main(argc, char* argv[]){ ssize_t s_read; /*創建socket部分*/ /*socket連線部分*/ /*socket read*/ s_read = read(socket_identifier, buf, MAX); if(s_read < 0){ printf("socket read error!\n"); } else{ printf("socket read data length=%d\n", s_read); // 讀取buffer內資料長度 } ... return 0; } ``` 藉由操作read()可以透過回傳值得知讀取狀況,負數代表有錯誤產生,0或者正數代表讀取buffer的資料長度 假如我們得到的回傳值是0可能有幾個特別意思: - buffer空空如也,甚麼都沒有 - 通訊雙方的socket domain不一致,就是上面創建socket講的AF_INET, AF_INET6那些 - 通訊雙方突然有然段開連線時 ### 關閉階段 **目的**: 關閉socket >若完成訂房,請登出帳號 #### 程式範例 ```c:socket_close int close(int fd); ``` - fd不用多說了吧 ```c:實例 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <malloc.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main(argc, char* argv[]){ int s_close; /*創建socket部分*/ /*socket連線部分*/ /*socket的收發操作*/ /*關閉socket*/ s_close = close(socket_identifier); (s_close < 0) ? printf("socket close error!") : printf("close successfully!"); return 0; } ``` 藉由socket斷開雙方之間的通訊,執行成功返回0,若發生錯誤則返回-1 [^1]:[[網路是怎樣連接的(四)DNS]] [^2]:傳輸層使用TCP協議 [^3]:關於socket連線部分將在介紹傳輸層時介紹 [^4]:標示符是應用程式用來識別眾多本地端socket用 [^5]:客戶端服務端通訊間用來識別眾多對方socket用