一文搞懂網絡套接字編程

什麼是端口號?

  • 端口號(port)是傳輸層協議的內容.
  • 端口號是一個2字節16位的整數;
  • 端口號用來標識一個進程, 告訴操作系統, 當前的這個數據要交給哪一個進程來處理;
  • IP地址 + 端口號能夠標識網絡上的某一臺主機的某一個進程;
  • 一個端口號只能被一個進程佔用.
源端口和目的端口號
  • 傳輸層協議(TCP和UDP)的數據段中有兩個端口號, 分別叫做源端口號和目的端口號. 就是在描述 “數據是誰發的, 要發給誰”;

TCP協議與UDP協議簡介

TCP協議 UDP協議
傳輸層協議 傳輸層協議
有連接 無連接
可靠傳輸 不可靠傳輸
面向字節流 面向數據報

網絡字節序

  • 發送主機通常將發送緩衝區中的數據按內存地址從低到高的順序發出;
  • 接收主機把從網絡上接到的字節依次保存在接收緩衝區中,也是按內存地址從低到高的順序保存;
  • 因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,後發出的數據是高地址.
  • TCP/IP協議規定,網絡數據流應採用大端字節序,即低地址高字節.
  • 不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規定的網絡字節序來發送/接收數據;
  • 如果當前發送主機是小端, 就需要先將數據轉成大端; 否則就忽略, 直接發送即可;

爲使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯後都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
在這裏插入圖片描述

  • h表示host,n表示network,l表示32位長整數,s表示16位短整數。
  • 例如htonl表示將32位的長整數從主機字節序轉換爲網絡字節序,例如將IP地址轉換後準備發送。
  • 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然後返回;
  • 如果主機是大端字節序,這些 函數不做轉換,將參數原封不動地返回。

socket編程接口

socket 常見API

// 創建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務器)
int socket(int domain, int type, int protocol);
 
// 綁定端口號 (TCP/UDP, 服務器)      
int bind(int socket, const struct sockaddr *address,
          socklen_t address_len);
 
// 開始監聽socket (TCP, 服務器)
int listen(int socket, int backlog);
 
// 接收請求 (TCP, 服務器)
int accept(int socket, struct sockaddr* address,
          socklen_t* address_len);
 
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr,
          socklen_t addrlen);

sockaddr結構

socket API是一層抽象的網絡編程接口,適用於各種底層網絡協議,如IPv4、IPv6. 然而, 各種網絡協議的地址格式並不相同.

  • IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位地址類型, 16位端口號和32位IP地址.
  • IPv4、IPv6地址類型分別定義爲常數AF_INET、AF_INET6. 這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容.
  • socket API可以都用struct sockaddr *類型表示, 在使用的時候需要強制轉化成sockaddr_in; 這樣的好處是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各種類型的sockaddr結構體指針做爲參數;
sockaddr結構

在這裏插入圖片描述

sockaddr_in結構

在這裏插入圖片描述
雖然socket api的接口是sockaddr, 但是我們真正在基於IPv4編程時, 使用的數據結構是sockaddr_in; 這個結構裏主要有三部分信息: 地址類型, 端口號, IP地址.

in_addr結構

在這裏插入圖片描述
in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數;

地址轉換函數

本篇博客只介紹基於IPv4的socket網絡編程,sockaddr_in中的成員struct in_addr sin_addr表示32位 的IP 地址,但是我們通常用點分十進制的字符串表示IP 地址,以下函數可以在字符串表示 和in_addr表示之間轉換;

字符串轉in_addr的函數:

在這裏插入圖片描述

in_addr轉字符串函數:

在這裏插入圖片描述
其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void *addrptr。
用例:

#include<stdio.h>                                                                                                    
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
	struct sockaddr_in addr;
	inet_aton("127.0.0.0",&addr.sin_addr);
	uint32_t *ptr = (uint32_t*)(&addr.sin_addr);
	printf("%x\n",*ptr);
	printf("%s\n",inet_ntoa(addr.sin_addr));
	return 0;
}

運行結果:
在這裏插入圖片描述

關於inet_ntoa

  • inet_ntoa這個函數返回了一個char*, 很顯然是這個函數自己在內部爲我們申請了一塊內存來保存ip的結果.
  • 因爲inet_ntoa把結果放到自己內部的一個靜態存儲區, 這樣第二次調用時的結果會覆蓋掉上一次的結果.
  • 在多線程環境下, 推薦使用inet_ntop, 這個函數由調用者提供一個緩衝區保存結果, 可以規避線程安全問題;
多線程調用inet_ntoa代碼的用例
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
 
void* Func1(void* p) {
  struct sockaddr_in* addr = (struct sockaddr_in*)p;
  while (1) {
    char* ptr = inet_ntoa(addr->sin_addr);
    printf("addr1: %s\n", ptr);
  }
  return NULL;
}
 
void* Func2(void* p) {
  struct sockaddr_in* addr = (struct sockaddr_in*)p;
  while (1) {
    char* ptr = inet_ntoa(addr->sin_addr);
    printf("addr2: %s\n", ptr);
  }
  return NULL;
}
 
int main() {
pthread_t tid1 = 0;
  struct sockaddr_in addr1;
  struct sockaddr_in addr2;
  addr1.sin_addr.s_addr = 0;
  addr2.sin_addr.s_addr = 0xffffffff;
  pthread_create(&tid1, NULL, Func1, &addr1);
  pthread_t tid2 = 0;
  pthread_create(&tid2, NULL, Func2, &addr2);
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);
  return 0;
}

TCP socket API 詳解

下面介紹程序中用到的socket API,這些函數都在sys/socket.h中。

socket():

在這裏插入圖片描述

  • socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符;
  • 應用程序可以像讀寫文件一樣用read/write在網絡上收發數據;
  • 如果socket()調用出錯則返回-1;
  • 對於IPv4, family參數指定爲AF_INET;對於IPv6,family的參數指定爲AF_INET6;
  • 對於TCP協議,type參數指定爲SOCK_STREAM, 表示面向流的傳輸協議
  • protocol參數的介紹從略,指定爲0即可。
bind():

在這裏插入圖片描述

  • 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接; 服務器需要調用bind綁定一個固定的網絡地址和端口號;
  • bind()成功返回0,失敗返回-1。
  • bind()的作用是將參數sockfd和myaddr綁定在一起, 使sockfd這個用於網絡通訊的文件描述符監聽myaddr所描述的地址和端口號;
  • 前面講過,struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度;

myaddr的初始化:

在這裏插入圖片描述

  1. 將整個結構體清零;
  2. 設置地址類型爲AF_INET;
  3. 網絡地址爲INADDR_ANY, 這個宏表示本地的任意IP地址,因爲服務器可能有多個網卡,每個網卡也可能綁定多個IP 地址, 這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP 地址;
  4. 端口號爲SERV_PORT, 我們定義爲9999;

listen():

在這裏插入圖片描述

  • listen()聲明sockfd處於監聽狀態, 並且最多允許有backlog個客戶端處於連接等待狀態, 如果接收到更多的連接請求就忽略, 這裏設置不會太大(一般是5);
  • listen()成功返回0,失敗返回-1;

accept():

在這裏插入圖片描述

  • 三次握手完成後, 服務器調用accept()接受連接;
  • 如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來;
  • addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號;
  • 如果給addr 參數傳NULL,表示不關心客戶端的地址;
  • addrlen參數是一個傳入傳出參數(value-result argument), 傳入的是調用者提供的, 緩衝區addr的長度以避免緩衝區溢出問題, 傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調用者提供的緩衝區);
服務器結構:

在這裏插入圖片描述

connect():

在這裏插入圖片描述

  • 客戶端需要調用connect()連接服務器;
  • connect和bind的參數形式一致, 區別在於bind的參數是自己的地址, 而connect的參數是對方的地址;
  • connect()成功返回0,出錯返回-1;

基於TCP協議的客戶端/服務器程序的一般流程:

在這裏插入圖片描述

服務器初始化:

  • 調用socket, 創建文件描述符;
  • 調用bind, 將當前的文件描述符和ip/port綁定在一起; 如果這個端口已經被其他進程佔用了, 就會bind失敗;
  • 調用listen, 聲明當前這個文件描述符作爲一個服務器的文件描述符, 爲後面的accept做好準備;
  • 調用accecpt, 並阻塞, 等待客戶端連接過來;

建立連接的過程:

  • 調用socket, 創建文件描述符;
  • 調用connect, 向服務器發起連接請求;
  • connect會發出SYN段並阻塞等待服務器應答;
  • (第一次)服務器收到客戶端的SYN, 會應答一個SYN-ACK段表示"同意建立連接";
  • (第二次)客戶端收到SYN-ACK後會從connect()返回, 同時應答一個ACK段; (第三次)
    這個建立連接的過程, 通常稱爲 三次握手;

數據傳輸的過程

  • 建立連接後,TCP協議提供全雙工的通信服務; 所謂全雙工的意思是, 在同一條連接中, 同一時刻, 通信雙方可以同時寫數據; 相對的概念叫做半雙工, 同一條連接在同一時刻, 只能由一方來寫數據;
  • 服務器從accept()返回後立刻調用read(), 讀socket就像讀管道一樣, 如果沒有數據到達就阻塞等待;
  • 這時客戶端調用write()發送請求給服務器, 服務器收到後從read()返回,對客戶端的請求進行處理, 在此期間客戶端調用read()阻塞等待服務器的應答;
  • 服務器調用write()將處理結果發回給客戶端, 再次調用read()阻塞等待下一條請求;
  • 客戶端收到後從read()返回, 發送下一條請求,如此循環下去;

斷開連接的過程:

  • 如果客戶端沒有更多的請求了, 就調用close()關閉連接, 客戶端會向服務器發送FIN段(第一次);
  • 此時服務器收到FIN後, 會迴應一個ACK, 同時read會返回0 (第二次);
  • read返回之後, 服務器就知道客戶端關閉了連接, 也調用close關閉連接, 這個時候服務器會向客戶端發送一個FIN; (第三次)
  • 客戶端收到FIN, 再返回一個ACK給服務器; (第四次)
    這個斷開連接的過程, 通常稱爲 四次揮手
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章