TCP通信服務器端實現第二步:調用bind 網絡API,將套接字文件、ip和端口綁定到一起。【linux】(zzu)

bind函數

函數原型

#include <sys/types.h>          
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能

將指定了通信協議(TCP)的套接字文件與IP以及端口綁定起來。

注意:綁定的一定是自己的ip和端口,不是對方的,比如對於TCP服務器來說,綁定的就是服務器自己的ip和端口。

至於什麼是綁定,爲什麼要綁定,我們後面再詳細解釋。

返回值

成功返回0,失敗返回-1,errno被設置。

參數

sockfd

套接字文件描述符,代表socket創建的套接字文件。
既然要綁定套接字文件、ip和端口,肯定要有一個標誌來代表套接字文件,那麼就是用套接字文件描述符來表示套接字文件。

addr

struct sockaddr 結構體變量的地址,結構體成員用於設置你要綁定的ip和端口。

結構體成員

struct sockaddr 
{
		sa_family_t sa_family;
		char  	sa_data[14];
}

sa_family_t 原型是 unsigned short

sa_family:指定AF_***,表示使用的什麼協議族的IP,前面說過,協議族不同,ip格式就不同。
sa_data:存放ip和端口

大家可以看出,如果將ip和端口直接寫入sa_data數組中,就需要一個字節一個字節的操作,雖然可以做到,但是操作起來有點麻煩,不過好在,我們可以使用更容易操作的
struct sockaddr_in結構體來設置。

bind函數要求使用struct sockaddr 結構體,不過對於TCP/IP協議通信來說在設置的時候使用struct sockaddr_in結構體來設置,設置完之後,把struct sockaddr_in結構體類型強制轉換爲struct sockaddr 結構體類型。

不過struct sockaddr_in這個結構體在在bind函數的手冊中沒有描述。

我們在內核代碼中查找到struct sockaddr_in 結構體:

struct sockaddr_in 
{
sa_family_t			sin_family;	//設置AF_***(地址族)
__be16				sin_port;		//設置端口號 __be16	是unsigned short類型
struct in_addr		sin_addr;		//設置Ip  結構體
													
/* 設置IP和端口時,這個成員用不到,這個成員的作用後面再解釋 */
unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};

struct sockaddr_in 結構體的成員 sin_addr 又是一個結構體。

struct in_addr 
{
__be32	s_addr;    //__be32是32位的unsigned int,因爲IPV4的ip是32位的無符號整形數
};

在struct sockaddr_in結構體中,存放端口和ip的成員是分開的,所以設置起來很方便。

使用struct sockaddr_in設置後,然後將其強制轉換爲struct sockaddr類型,然後作爲參數傳遞給bind函數即可。

爲什麼要強制轉換?
bind函數的參數要求的是使用struct sockaddr結構體,但是設置的時候使用的是struct sockaddr_in結構體,自然要進行類型轉換。

至於爲什麼這麼麻煩,搞出了struct sockaddr和struct sockaddr_in這兩個結構體類型?
我們後面再解釋。

struct sockaddr_in的使用例子:

定義結構體變量:

struct sockaddr_in addr;

設置協議族的ip地址,這裏使用的是IPV4 TCP/IP協議族的ip地址(32位):

addr.sin_family = AF_INET;

指定端口:

addr.sin_port		= htons(5006);

端口號數值的設置以及端口的數值設置爲多少比較合適,我們在後面進行說明。
上面設置端口號需要調用htons(5006);函數把端口號數值進行轉化,然後再賦值給結構體成員。

我們後面再來介紹htons()函數的作用,目前我們先直接使用這個函數,總之就是要知道指定端口的時候必須調用htons()函數對於端口號數值進行轉化。

指定ip地址:

addr.sin_addr.s_addr = inet_addr("192.168.1.105");

192.168.1.105 是點分十進制的字符串的ip地址,必須使用inet_addr("192.168.1.105");轉化爲32爲整型數的ip地址,因爲底層在使用ip地址的時候,ip地址的真正面目是無符號的32位整型數。點分十進制只是爲了方便使用者來看,最終使用的ip還是32位的無符號整型數。所以使用inet_addr函數,把點分十進制的字符串形式的ip轉化爲無符號的32爲整型數的ip。轉化完之後保存在addr.sin_addr.s_addr成員裏面。

裏面的ip寫服務器所在電腦的ip,ip在linux平臺使用ifconfig命令查看:

ifconfig

linux平臺使用ifconfig查看ip地址

因爲到時候服務器是運行在我這臺虛擬機上面的,我這臺虛擬機的ip是192.168.31.162

綁定

ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(addr)); 

上面必須進行強制轉換:(struct sockaddr*)&addr 因爲bind函數第二個參數最終要的是 struct sockaddr結構體類型,而上面在設置的時候使用的是struct sockaddr_in 結構體類型。

注意:如果是跨網通信時,綁定的一定是所在路由器的公網ip。

bind會將sockfd代表的套接字文件與addr結構體中所設置的ip和端口綁定起來。

有關htons和inet_addr這兩個函數,我們後面會詳細解釋,目前我們先用起來。

addrlen

第二個參數所指定的結構體變量的大小。

代碼演示

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"
void  print_err(char * str,int line,int err_no)//出錯處理函數
{
	printf("%d,%s: %s\n",line,str,strerror(err_no));
	exit(-1);
}
int main(void)
{
	int ret = -1;
	int sockfd = -1; //存放套接字文件描述符
	/*創建使用TCP協議通信的套接字文件*/
	sockfd =  socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
	if(-1 == sockfd) //進行出錯處理
		print_err("socket fail",__LINE__,errno);

	/*調用bind函數綁定套接字文件/ip/端口*/
	struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;//指定ip格式
	saddr.sin_port= htons(SPROT);//指定端口
	saddr.sin_addr.s_addr = inet_addr(SIP);//設置ip
	ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
	if(-1 ==ret)
		print_err("binf fail",__LINE__,errno);

	return 0;
}

有關bind函數的一些必須解釋的問題

爲什麼要綁定,或者說綁定的目的是什麼?

雖然bind函數看起來很簡單,但是很多讀者在初次瞭解到bind函數時基本都是似懂非懂,可能你感覺自己好像懂了,但是其實並不懂什麼是綁定,以及爲什麼要綁定。

調用socket創建套接字文件時,只是指定了所使用的協議(比如TCP協議),但是並沒有指定通信時所需要ip地址和端口號。

ip地址作用:ip地址是用來定位和找到對方,如果沒有IP就不叫網絡通信了。
端口號的作用: 區分同一臺計算機上不同的網絡通信進程。

如果在操作系統上面運行了多個網絡通信的進程,那麼每一個進程都必須分配一個端口號來進行區分,不進行網絡通信的進程不需要端口號

假設數據發送了這臺計算機,通過TCP/IP協議一路拆包解包交給應用程序,那麼現在就會出現一個問題,計算機把接受到的數據交給那個正在進行網絡通信的進程呢?這個時候就只能通過端口號來確定。

端口號的作用就是區分同一臺主機上面多個正在進行網絡通信的進程。

不同的主機上面端口號沒有任何關係,只需要在同一臺主機上端口號之間沒有衝突即可。

有了ip和端口後,對方首先通過ip找到目標計算機,然後再通過“端口”找到具體的網絡通信進程。

如果我們不明確的調用bind綁定一個固定的ip和端口的話,會怎麼樣?
答:會被自動指定一個ip和端口,而且是不固定的,而且還不一定是你想用的iP和端口。

自動指定ip和端口好不好?
對於TCP的服務器來說,自動指定ip和端口是不行的,爲什麼?

因爲客戶端向服務器連接時,是由客戶端主動發起三次握手請求的,如果服務器Ip和端口是變着的(不確定),此時客戶端在向服務器請求連接時就抓瞎了,因爲它根本就不知道服務器的ip和端口到底是多少,沒辦法建立連接。

這就好比政府部門是服務器,我們這些羣衆是客戶,如果政府部分的辦公地址天天變來變去,我們這些客戶不就抓瞎了嗎,天天變,上哪聯繫政府去。

所以對於TCP的服務器來說,必須調用bind函數給自己綁定固定的ip和端口號。

到底什麼是綁定?
其實所謂綁定就是讓套接字文件在通信時,使用固定的IP和端口。

對於TCP通信的客戶端來說,自動指定ip和端口是常態

客戶端的IP和端口自動指定的話,服務器怎麼知道客戶的ip和端口是多少呢?
客戶與服務器建立連接時,服務器會從客戶的數據包中提取出客戶的ip和端口,並保存起來,如果是跨網通信,那麼記錄的就是客戶所在路由器的公網Ip 以及端口號。

htons函數和inet_addr函數

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port		= htons(5006);
addr.sin_addr.s_addr = inet_addr("192.168.1.105");
ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

設置端口號時用到的函數:

htons 函數

函數原型

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);

功能

功能有兩個

  1. 將端口號從“主機端序”轉爲“網絡端序”

  2. 如果給的端口不是short類型,將其類型轉換爲short型。

htons:是host to net short的縮寫

host:主機端序,主機端序可能是大端序,也可能是小端序,視OS而定。
net:網絡端序,網絡端序都是固定使用大端序
short:短整形

什麼是大端序和小端序?
有關這個問題,我們在C語言指針博客中有詳細的說明,這裏不再贅述。

參數

主機端序的端口號

返回值

該函數的調用永遠都是成功的,返回轉換後的端口號

爲什麼要進行大小端序的轉換?

  • 網絡通信的過程

    發送計算機 ——————> 網絡 ————————> 接收計算機
    主機端序 ——————> 網絡端序 ———————> 主機端序

發送計算機的端序與接收計算機的端序可能不一致,比如發送者是大端序,而接收者是小端序,如果通信時數據的端序處理不好,數據可能會出現亂碼,甚至導致無法接收到數據。

所以發送數據時,先從發送者的主機端序轉成統一的網絡端序,接收計算機接收到後,再統一的轉成接收計算機的主機端序,如此接收的計算機才能以正確的端序接收數據,否則就會出錯。

所以進行大小端序的轉換目的就是爲了避免發送計算機和接受計算機因爲端序的不同而導致錯誤。

我沒有調用htons函數,直接設置端口號(5006)好像也可以呢!

因爲我們寫的程序是本機測試的,發送機和接收機都是同一臺計算機,碰巧端序是一樣的,
所以不設置端序沒有問題。而且就算你跨機測試時,不進行端序的轉換的話,也有可能是正確的,因爲有可能發送計算機的端序和接收計算機的端序碰巧時一樣,但是如果不一樣就出問題了。

不過我們並不能保證發送和接收計算機的端序一定是一樣的,所以不能冒這樣的風險,我們一定要進行端序的轉換。避免這種風險。

htons的兄弟函數:htonl、ntohs、ltohs

htonl

功能與htons函數的功能是一樣的,都是主機端序端口號轉換爲網絡端序端口號,與htons唯一的區別是,htonl函數轉換完之後的端口號時long,我們不使用這個函數,因爲我們的端口號要的是short。

ntohs

htons的相反情況,網絡端序轉爲主機端序,後面會用到。

ntohl

htonl函數的相反情況。

有關端口號的數值問題

例如:addr.sin_port = htons(5006);

端口號的作用

用於區分同一臺機器上的不同通信程序,對於同一臺計算機來說,不同通信程序(可以是
服務器、也可以客戶端程序),他們的端口號都不能衝突,否者收發數據就會出現問題。

這就好比在同一層樓有兩個房間叫101,客戶就犯難了,不知道應該去按個房間。

不同計算機的通信程序可以使用相同的端口號嗎?
當然可以,相互間毫無影響,這就好比1層有個101,2層也有個101,這是兩個不同樓層的101房間,同名無所謂。

所以A公司的計算機搭建web服務器端口是80,B公司服務器計算機搭建web同樣是80端口,相互間並無干擾。

端口號的選擇範圍

三個範圍:0 ~ 1023、1024 ~ 49151、49152 ~ 65535。

0~1023

這個範圍的端口最好不要用,因爲這個範圍的端口已經被世界公認的各種服務徵用了,比如80就被web服務徵用了,所以所有web服務器程序的端口都是固定的80。

真的就不能用嗎?
我說的是最好不要用,但並沒有說用了就一定會出問題,我們之所以建議不要用的原因是,
如果使用這個範圍的端口號的話,可能會遇到一些麻煩事。

比如自己寫了一個服務器程序,而且使用的是80端口,但是如果恰巧我又在計算機上安裝了一個web服務器,web服務器程序默認使用的是就80端口,這下自己寫的服務器程序的端口與安裝的現成的web服務器的端口衝突了。

現在的web服務器除了使用80端口,有些還是用8080端口。

當然,如果你不安裝web的話,其實也沒有很大的影響,完全可以使用80端口,但是我們建議還是準守規則,不要使用這個範圍的端口,因爲保不齊你可能就安裝了某些現成的全世界公認的服務器程序,如此一來就造成了端口的衝突,將會導致無法通信成功。

總之我們自己在寫服務器程序時,最好不要指定這個範圍的端口號。因爲你的計算機很有可能安裝了全世界公認的某個服務,服務的端口號可能剛好就和你自己使用的端口發生了衝突。所以避免使用 0~ 1023 範圍端口。

1024~49151

自己實現服務器程序,建議使用這個範圍的端口號,比如我寫的TCP服務器所使用的5006,
用的就是這個範圍的端口號。

當我們自己寫的TCP服務器,在我們自己的PC機上測試運行時,難道指定5006就真的沒有衝突的可能嗎?

說實話,還是有可能會衝突的,不過大家儘可以大膽使用,因爲衝突的可能性很小,真要衝突了,我懷疑可能是大家的人品有問題-.

那麼爲什麼指定1024~49151範圍的端口時幾乎不可能衝突呢?

1.全世界公認的有名的服務,使用的都是0~1023範圍的端口,而不是這個範圍。

  1. 這個範圍的端口默認都是給自己寫的服務器程序使用的,對於我的pc機來說,只有可能
    安裝客戶端程序(qq/微信客戶端),幾乎不會安裝網絡服務器程序(pc根本跑不動),所以這個範圍的端口基本不可能會被什麼服務器程序所使用,也就是我自己寫的TCP服務器程序可能會使用。

就算碰巧你的PC上安裝了某個網絡服務程序,使用了1024~49151中的某個端口,但是這個範圍這麼大,真想要衝突上也挺難的。

pc會安裝很多的網絡客戶端程序(qq/微信客戶端),萬一某個客戶端使用也是5006
的話,這個客戶端和我自己寫的TCP服務器的端口5006,不就衝突了嗎?

Pc機確實會大量安裝客戶端程序,但是客戶端的端口都是自動分配的,而自動分配的端口範圍爲49152 ~ 65535,根本就不是1024 ~ 49151這個範圍的,所以與客戶端端口衝突的可能性不大。

49152~65535

這個範圍的端口號用於自動分配的,一般客戶端程序不會綁定固定的ip和端口,因爲客戶端的Ip和端口都是自動分配的,在自動分配端口時,所分配的就是49152~65535範圍的端口。所以寫TCP端口的時候基本上端口號是不可能衝突的。

inet_addr函數

設置 ip 的時候使用的函數。

addr.sin_addr.s_addr = inet_addr("192.168.1.105");

函數原型

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);

功能

1.將字符串形式的Ip"192.168.1.105"(點分十進制),轉爲IPV4的32位無符號整形數的ip
2. 將無符號整形數的ip,從主機端序轉爲網絡端序。

爲什麼要轉換端序?
與端口號端序的轉換原因是一樣的。

參數

字符串形式的ip

返回值

永遠成功,返回網絡端序的、32位無符號整形數的ip。

其它兄弟函數

它的兄弟函數很多,雖然形態上有差異,實際上功能差不多,只是略有區別,這些函數並不難,我不再一一詳細介紹,真的用到其中某一個了,讀者自己去學習和理解即可。

爲什麼區分struct sockaddr和struct sockaddr_in?

struct sockaddr
{
		sa_family_t sa_family;
		char  			sa_data[14];
}
struct sockaddr_in
 {
			sa_family_t			sin_family;
			__be16					sin_port;
			struct in_addr	sin_addr;
/* 填補相比struct sockaddr所缺的字節數,保障強制轉換不要出錯 */
unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};

使用結構體struct sockaddrsa_data[14];成員設置端口和ip的時候,需要一個字節一個字節設置,比較麻煩。所以實際使用結構體struct sockaddr_in來設置。struct sockaddr_in設置端口和ip的時候是分開的,所以操作起來會比較方便,單獨操作設置端口和ip即可。

struct sockaddr_in 的第四個成員是用來幹什麼的呢?
我們知道struct sockaddr_in 結構體最後要強制轉換爲struct sockaddr結構體,在進行強制轉換的時候,結構體的空間大小最好吧保持一致,如果不一致,就會出現越界或者截斷的問題。struct sockaddr_in結構體前三個成員字節數加起來比struct sockaddr結構體的兩個成員所佔字節數加起來要少,所以struct sockaddr_in 結構體使用第四個成員來補充相對struct sockaddr 結構體較少的字節數。也就是說struct sockaddr_in 結構體四個成員的字節數加起來,纔等於struct sockaddr 結構體中兩個成員所佔的字節數。

所以struct sockaddr_in結構體中的第四個成員的存在作用是填補相比struct sockaddr結構體所缺的字節數,保障強制轉換不要出錯。

我們不管使用什麼協議族來通信,底層統一使用struct sockaddr結構體。

那麼爲什麼會有struct sockaddr_in結構體?
struct sockaddr結構體設置起來不方便,爲了方便設置,所以有了sockaddr_in結構體類型。

也就是說設置的時候使用結構體sockaddr_in ,實際使用的時候使用結構體struct sockaddr

注意:sockaddr_in是專門給TCP/IP協議族使用的,如果是其它協議族,對應的是其它的設置結構體,比如“域通信協議族”使用的就是struct sockaddr_un結構體。

struct sockaddr 結構體的底層對應

所有的協議族底層使用的都是 struct_sockaddr 結構體。對應於不同的協議族有專門的設置結構體。然後轉化爲struct sockaddr結構體。

不同的協議族在通信的時候所用到的ip和端口是不一樣的,所以對應使用的設置結構體的內容就是不一樣的。只不過設置完之後統統轉換爲同一個結構體struct sockaddr來使用,方便操作和底層管理。

使用不同的協議通信使用到結構體是爲了設置的方便。

強制轉換時發生了什麼?

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port		= htons(5006);
addr.sin_addr.s_addr = inet_addr("192.168.1.105");
ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

結構體內容並沒有變,但是空間的解釋方式變了,當我們將struct sockaddr_in結構體強制轉爲struct sockaddr結構體時候,struct sockaddr_in結構體的後三個成員被強制解釋爲了struct sockaddr結構體的sa_data成員,如此一來就把ip和端口給設置到了struct sockaddr結構體的sa_data成員中。

強制轉換過程過程對應圖:

強制轉換過程過程對應圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章