iT邦幫忙

0

網路是怎樣連接的(五)Socket API

思考重點

  • 如何將應用程序消息委託給協議棧發送?
  • socket是調用那些函式進行收發操作?

核心知識

協議棧如何進行收發操作

現在將擁有的數據整理一下,首先HTTP消息封包已經由應用程式打包完成,服務器IP地址也已經透過DNS[1]請求機制獲得。在兩個前提條件都滿足的狀況下,我們就可以著手思考要怎麼將這些數據發給對方服務器的應用程式

發送數據其實是調用多個socket庫函式達成的,藉由委託多個函式API進行一連串的任務交互,每個任務完成的項目不同,有建立連接部分、斷開連線等等,這些操作的用意就是為了保證雙方是否接收到消息與回應是否正常[2]

使用socket實現

為了使雙方應用程式之間建立一條專屬的溝通管道,我們調用了socket庫函式,很多書上將建立socket形容成搭建一條無形的通道,雙方可透過這條通道來實現消息的收發操作,不過並不是說建立socket後計算機才被允許與網路進行通訊,其實早在建立socket之前計算機就可以向網路收發消息了,建立socket比較像是彼此確定我們該走哪一條傳輸通道找到對方的應用程式

順著這個思路這小節將介紹應用程式是如何調用socket庫函式向下層委託收發。我喜歡用一個網路訂房的比喻來形容建立socket連線的步驟,就像我們是使用手機app進行訂房,委託系統進行操作,真正的流程實際上是不得而知的,這就像站在應用層的視角看待socket連線[3]

創建階段

目的: 依照指定類型創建socket

就下載這個訂房app吧!

程式案例

我們先來看看socket create部分:

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

  • domain
    • 決定socket在網路傳輸中要使用哪個通訊協定的家族系列
    • AF_INET為TCP/IP網路通訊協定
  • type
    • 指定socket的類型
    • SOCK_STREAM對應的是TCP協定
    • SOCK_DGRAM對應的是UDP協定
    • SOCK_RAW可以是IP或ICMP
  • protocol
    • 通常在設定完domain與type以後通訊種類就大抵完成了,因此 protocol 一般都設為0,表示依照指定類型設定預設協議
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
#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吧

程式案例

來看看連現階段吧:

int connect(int fd, struct sockaddr *server, int addrlen);

connect函數是客戶端發起的請求,目的是為了與服務器建立連線,介紹參數前,先來講講sockaddr這個結構體,它裡面裝的主要就是socket連線需要的消息,這裡暫且以IPv4為例:

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長度
#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 → read()/write

發送消息

目的: 將資料寫入 Socket 中並發送出去

就是這間了,趕緊下單!

程式案例
ssize_t write(int fd, const void *buf, size_t nbyte);
  • fd是描述符
  • buf是寫入資料的緩衝區,使用const修飾參數buf防止內容被更改
  • nbyte是buffer大小
#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讀取資料

系統提示~您已經下訂成功!

程式案例
ssize_t read(int fd, void* buf, size_t nbyte);
  • fd前面講過了
  • buf是讀取資料的緩衝區,socket就是把資料推送到這裡
  • nbyte是buffer大小
#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

若完成訂房,請登出帳號

程式範例

int close(int fd);
  • fd不用多說了吧
#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用


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言