1. 程式人生 > 實用技巧 >計算機網路 實驗之 socket程式設計之 bind()和connect()函式:繫結套接字並建立連線

計算機網路 實驗之 socket程式設計之 bind()和connect()函式:繫結套接字並建立連線

socket() 函式用來建立套接字,確定套接字的各種屬性,然後伺服器端要用 bind() 函式將套接字與特定的 IP 地址和埠繫結起來,只有這樣,流經該 IP 地址和埠的資料才能交給套接字處理。類似地,客戶端也要用 connect() 函式建立連線。

bind() 函式

bind() 函式的原型為:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

下面以 Linux 為例進行講解,Windows 與此類似。

sock 為 socket 檔案描述符,addr 為 sockaddr 結構體變數的指標,addrlen 為 addr 變數的大小,可由 sizeof() 計算得出。
下面的程式碼,將建立的套接字與IP地址 127.0.0.1、埠 1234 繫結

//建立套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//建立sockaddr_in結構體變數
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每個位元組都用0填充
serv_addr.sin_family = AF_INET;  //
使用IPv4地址 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址 serv_addr.sin_port = htons(1234); ////將套接字和IP、埠繫結 bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

這裡我們使用 sockaddr_in 結構體,然後再強制轉換為 sockaddr 型別,後邊會講解為什麼這樣做。

sockaddr_in 結構體

接下來不妨先看一下 sockaddr_in 結構體,它的成員變數如下:

struct
sockaddr_in{ sa_family_t sin_family; //地址族(Address Family),也就是地址型別 uint16_t sin_port; //16位的埠號 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 };

1) sin_family 和 socket() 的第一個引數的含義相同,取值也要保持一致。


2) sin_prot 為埠號。uint16_t 的長度為兩個位元組,理論上埠號的取值範圍為 0~65536,但 0~1023 的埠一般由系統分配給特定的服務程式,例如 Web 服務的埠號為 80,FTP 服務的埠號為 21,所以我們的程式要儘量在 1024~65536 之間分配埠號。


埠號需要用 htons() 函式轉換,後面會講解為什麼。


3) sin_addr 是 struct in_addr 結構體型別的變數,下面會詳細講解。


4) sin_zero[8] 是多餘的8個位元組,沒有用,一般使用 memset() 函式填充為 0。上面的程式碼中,先用 memset() 將結構體的全部位元組填充為 0,再給前3個成員賦值,剩下的 sin_zero 自然就是 0 了。

in_addr 結構體

sockaddr_in 的第3個成員是 in_addr 型別的結構體,該結構體只包含一個成員,如下所示:

struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

in_addr_t 在標頭檔案 <netinet/in.h> 中定義,等價於 unsigned long,長度為4個位元組。也就是說,s_addr 是一個整數,而IP地址是一個字串,所以需要inet_addr() 函式進行轉換,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

執行結果:
16777343


圖解 sockaddr_in 結構體


為什麼要搞這麼複雜,結構體中巢狀結構體,而不用 sockaddr_in 的一個成員變數來指明IP地址呢?socket() 函式的第一個引數已經指明瞭地址型別,為什麼在 sockaddr_in 結構體中還要再說明一次呢,這不是囉嗦嗎?
這些繁瑣的細節確實給初學者帶來了一定的障礙,我想,這或許是歷史原因吧,後面的介面總要相容前面的程式碼。各位讀者一定要有耐心,暫時不理解沒有關係,根據教程中的程式碼“照貓畫虎”即可,時間久了自然會接受。

為什麼使用sockaddr_in 而不使用sockaddr

bind() 第二個引數的型別為 sockaddr,而程式碼中卻使用 sockaddr_in,然後再強制轉換為 sockaddr,這是為什麼呢?

sockaddr 結構體的定義如下:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址型別
    char         sa_data[14];  //IP地址和埠號
};

下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數字表示所佔用的位元組數):


sockaddr 和 sockaddr_in 的長度相同,都是16位元組,只是將IP地址和埠號合併到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和埠號,例如”127.0.0.1:80“,遺憾的是,沒有相關函式將這個字串轉換成需要的形式,也就很難給 sockaddr 型別的變數賦值,所以使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換型別時不會丟失位元組,也沒有多餘的位元組。

可以認為,sockaddr 是一種通用的結構體,可以用來儲存多種型別的IP地址和埠號,而 sockaddr_in 是專門用來儲存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來儲存 IPv6 地址,它的定義如下:

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址型別,取值為AF_INET6
    in_port_t sin6_port;  //(2)16位埠號
    uint32_t sin6_flowinfo;  //(4)IPv6流資訊
    struct in6_addr sin6_addr;  //(4)具體的IPv6地址
    uint32_t sin6_scope_id;  //(4)介面範圍ID
};

正是由於通用結構體 sockaddr 使用不便,才針對不同的地址型別定義了不同的結構體。

connect() 函式

connect() 函式用來建立連線,它的原型為:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

各個引數的說明和 bind() 相同,不再贅述。

學習:here