socket 的通信過程

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


服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。

數據傳輸的過程:

建立連接後,TCP協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從accept()返回後立刻調用read(),讀socket就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。

如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。注意,任何一方調用close()後,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處於半關閉狀態,仍可接收對方發來的數據。

在學習socket API時要注意應用程序和TCP協議層是如何交互的: *應用程序調用某個socket函數時TCP協議層完成什麼動作,比如調用connect()會發出SYN段 *應用程序如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函數返回就表明TCP協議收到了某些段,再比如read()返回0就表明收到了FIN段

看圖所示的socket 通信過程


圖12.9 socket 的通信過程

1.建立套接字

sys/socket.h中。

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 文件操作集。所有隨後的套接字(文件)操作都將定向到該套接字接口,而套接字接口則會進一步調用地址族的操作例程,從而將操作傳遞到底層地址族,如圖12.10所示。

實際上,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,表示不關心客戶端的地址。


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


轉載地址:http://oss.org.cn/kernel-book/ch12/12.3.4.htm
發佈了79 篇原創文章 · 獲贊 5 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章