TCP協議的初始化及socket創建TCP套接字描述符


1.什么是socket

通信雙方不在同一個主機通過socket進行通信,在計算機網絡中我們就學過了tcp/ip協議族,其實使用tcp/ip協議族就能達到我們想要的效果,如下圖

但是為了使用的方便以及可重用性 各種語言都對函數進行了封裝 形成了socket API 來進行對底層的調用,我們這次所要研究的就是socket調用TCP協議時候所進行的初始化過程以及socket是如何創建tcp套接字描述符以及他們的作用。

2.基於TCP/IP協議的服務器和客戶端程序的一般流程,如下圖所示:

tcp/ip建立連接需要三次握手,首先客戶端置SYN = 1,seq = x,服務器收聽后置SYN = 1,ACK = 1,seq = y,ack = x + 1,然后ACK = 1,ack = y + 1,seq = x + 1

釋放需要四次揮手,客戶端置SYN = 1,seq = u,服務器端置ACK = 1,ack = u + 1,seq = v,服務器端置SYN = 1,ACK = 1,ack = u + 1,seq = w,客戶器端置ACK = 1,seq = u + 1,ack = w + 1

這是大概流程,下面我們研究TCP協議的初始化及socket如何創建TCP套接字描述符

3.初始化

1.建立套接字

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

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

  Linux在利用socket()系統調用建立新的套接字時,需要傳遞套接字的地址族標識符、套接字類型以及協議,其函數定義於net/socket.c中:

 asmlinkagelong sys_socket(int family, int type, int protocol)

   {

        int retval;

        struct socket *sock;

 

        retval = sock_create(family, type, protocol,&sock);

        if (retval < 0)

                goto out;

 

        retval = sock_map_fd(sock);

        if (retval < 0)

                goto out_release;

 

out:

        /* It may be already another descriptor 8) Not kernel problem. */

        return retval;

 

out_release:

        sock_release(sock);

        return retval;

}

  實際上,套接字對於用戶程序而言就是特殊的已打開的文件。內核中為套接字定義了一種特殊的文件類型,形成一種特殊的文件系統sockfs,其定義於net/socket.c:

static struct vfsmount *sock_mnt;

 static DECLARE_FSTYPE(sock_fs_type, "sockfs",sockfs_read_super, FS_NOMOUNT);

  在系統初始化時,要通過kern_mount()安裝這個文件系統。安裝時有個作為連接件的vfsmount數據結構,這個結構的地址就保存在一個全局的指針sock_mnt中。所謂創建一個套接字,就是在sockfs文件系統中創建一個特殊文件,或者說一個節點,並建立起為實現套接字功能所需的一整套數據結構。所以,函數sock_create()首先是建立一個socket數據結構,然后將其“映射”到一個已打開的文件中,進行socket結構和sock結構的分配和初始化。

新創建的 BSD socket 數據結構包含有指向地址族專有的套接字例程的指針,這一指針實際就是proto_ops 數據結構的地址。

  BSD 套接字的套接字類型設置為所請求的 SOCK_STREAM 或 SOCK_DGRAM 等。然后,內核利用 proto_ops 數據結構中的信息調用地址族專有的創建例程。

  之后,內核從當前進程的 fd 向量中分配空閑的文件描述符,該描述符指向的 file 數據結構被初始化。初始化過程包括將文件操作集指針指向由 BSD 套接字接口支持的 BSD 文件操作集。所有隨后的套接字(文件)操作都將定向到該套接字接口,而套接字接口則會進一步調用地址族的操作例程,從而將操作傳遞到底層地址族,如下圖所示。

實際上,socket結構與sock結構是同一事物的兩個方面。如果說socket結構是面向進程和系統調用界面的,那么sock結構就是面向底層驅動程序的。可是,為什么不把這兩個數據結構合並成一個呢?

我們說套接字是一種特殊的文件系統,因此,inode結構內部的union的一個成分就用作socket結構,其定義如下:

struct inode {

    …

  union {

     …

         struct socket            socket_i;

       }

  }

  由於套接字操作的特殊性,這個結構中需要大量的結構成分。可是,如果把這些結構成分全都放在socket結構中,則inode結構中的這個union就會變得很大,從而inode結構也會變得很大,而對於其他文件系統,這個union成分並不需要那么龐大。因此,就把套接字所需的這些結構成分拆成兩部分,把與文件系統關系比較密切的那一部分放在socket結構中,把與通信關系比較密切的那一部分則單獨組成一個數據結構,即sock結構。由於這兩部分數據在邏輯上本來就是一體的,所以要通過指針互相指向對方,形成一對一的關系。

2.在 INET BSD 套接字上綁定(bind)地址

  為了監聽傳入的 Internet 連接請求,每個服務器都需要建立一個 INET BSD 套接字,並且將自己的地址綁定到該套接字。綁定操作主要在 INET 套接字層中進行,還需要底層 TCP 層和 IP 層的某些支持。將地址綁定到某個套接字上之后,該套接字就不能用來進行任何其他的通訊,因此,該 socket數據結構的狀態必須為 TCP_CLOSE。傳遞到綁定操作的 sockaddr 數據結構中包含要綁定的 IP地址,以及一個可選的端口地址。通常而言,要綁定的地址應該是賦予某個網絡設備的 IP 地址,而該網絡設備應該支持 INET 地址族,並且該設備是可用的。利用 ifconfig 命令可查看當前活動的網絡接口。被綁定的 IP 地址保存在 sock 數據結構的rcv_saddr 和 saddr 域中,這兩個域分別用於哈希查找和發送用的 IP 地址。端口地址是可選的,如果沒有指定,底層的支持網絡會選擇一個空閑的端口。

  

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

  

  服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。bind()成功返回0,失敗返回-1。

  bind()的作用是將參數sockfd和myaddr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽myaddr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。我們的程序中對myaddr參數是這樣初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

  

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

 

  當底層網絡設備接受到數據包時,它必須將數據包傳遞到正確的 INET 和 BSD 套接字以便進行處理,因此,TCP維護多個哈希表,用來查找傳入 IP 消息的地址,並將它們定向到正確的socket/sock 對。TCP 並不在綁定過程中將綁定的 sock 數據結構添加到哈希表中,在這一過程中,它僅僅判斷所請求的端口號當前是否正在使用。在監聽操作中,該 sock 結構才被添加到 TCP 的哈希表中。

 3.在 INET BSD 套接字上建立連接(connect)

  創建一個套接字之后,該套接字不僅可以用於監聽入站的連接請求,也可以用於建立出站的連接請求。不論怎樣都涉及到一個重要的過程:建立兩個應用程序之間的虛擬電路。出站連接只能建立在處於正確狀態的 INET BSD 套接字上,因此,不能建立於已建立連接的套接字,也不能建立於用於監聽入站連接的套接字。也就是說,該 BSD socket 數據結構的狀態必須為 SS_UNCONNECTED。

 在建立連接過程中,雙方 TCP 要進行三次“握手”,具體過程在 本章第二節——網絡協議一文中有詳細介紹。如果 TCP sock 正在等待傳入消息,則該 sock 結構添加到 tcp_listening_hash 表中,這樣,傳入的 TCP 消息就可以定向到該 sock 數據結構。

 

  由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

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

4.監聽(listen) INET BSD 套接字

int listen(int sockfd, int backlog);

  

  典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的accept()返回並接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未accept的客戶端就處於連接等待狀態,listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接待狀態,如果接收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。

  當某個套接字被綁定了地址之后,該套接字就可以用來監聽專屬於該綁定地址的傳入連接。網絡應用程序也可以在未綁定地址之前監聽套接字,這時,INET 套接字層將利用空閑的端口編號並自動綁定到該套接字。套接字的監聽函數將 socket 的狀態改變為 TCP_LISTEN。

  當接收到某個傳入的 TCP 連接請求時,TCP 建立一個新的 sock 數據結構來描述該連接。當該連接最終被接受時,新的 sock 數據結構將變成該 TCP 連接的內核bottom_half部分,這時,它要克隆包含連接請求的傳入 sk_buff 中的信息,並在監聽 sock 數據結構的 receive_queue 隊列中將克隆的信息排隊。克隆的 sk_buff 中包含有指向新 sock 數據結構的指針。

5.接受連接請求 (accept)

   接受操作在監聽套接字上進行,從監聽 socket 中克隆一個新的 socket 數據結構。其過程如下:接受操作首先傳遞到支持協議層,即 INET 中,以便接受任何傳入的連接請求。相反,接受操作進一步傳遞到實際的協議,例如TCP 上。接受操作可以是阻塞的,也可以是非阻塞的。接受操作為非阻塞的情況下,如果沒有可接受的傳入連接,則接受操作將失敗,而新建立的 socket 數據結構被拋棄。接受操作為阻塞的情況下,執行阻塞操作的網絡應用程序將添加到等待隊列中,並保持掛起直到接收到一個 TCP 連接請求為至。當連接請求到達之后,包含連接請求的 sk_buff 被丟棄,而由 TCP 建立的新 sock 數據結構返回到 INET 套接字層,在這里,sock 數據結構和先前建立的新 socket 數據結構建立鏈接。而新 socket 的文件描述符(fd)被返回到網絡應用程序,此后,應用程序就可以利用該文件描述符在新建立的 INETBSD 套接字上進行套接字操作。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

  

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

注意:服務器接收到傳入的請求后,如果能夠接受該請求,服務器必須創建一個新的套接字來接受該請求並建立通訊連接(用於監聽的套接字不能用來建立通訊連接),這時,服務器和客戶就可以利用建立好的通訊連接傳輸數據


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM