天天看點

socket原理詳解(同篇2)

1、什麼是socket

我們知道程序通信的方法有管道、命名管道、信号、消息隊列、共享記憶體、信号量,這些方法都要求通信的兩個程序位于同一個主機。但是如果通信雙方不在同一個主機又該如何進行通信呢?在計算機網絡中我們就學過了tcp/ip協定族,其實使用tcp/ip協定族就能達到我們想要的效果,如下圖(圖檔來源于《tcp/ip協定詳解卷一》第一章1.3)

         

socket原理詳解(同篇2)

                           圖一 各協定所處層次

當然,這樣做固然是可以的,但是,當我們使用不同的協定進行通信時就得使用不同的接口,還得處理不同協定的各種細節,這就增加了開發的難度,軟體也不易于擴充。于是UNIX BSD就發明了socket這種東西,socket屏蔽了各個協定的通信細節,使得程式員無需關注協定本身,直接使用socket提供的接口來進行互聯的不同主機間的程序的通信。這就好比作業系統給我們提供了使用底層硬體功能的系統調用,通過系統調用我們可以友善的使用磁盤(檔案操作),使用記憶體,而無需自己去進行磁盤讀寫,記憶體管理。socket其實也是一樣的東西,就是提供了tcp/ip協定的抽象,對外提供了一套接口,同過這個接口就可以統一、友善的使用tcp/ip協定的功能了。百說不如一圖,看下面這個圖就能明白了。

         

socket原理詳解(同篇2)

                               圖二 socket所處層次

那麼,在BSD UNIX又是如何實作這層抽象的呢?我們知道unix中萬物皆檔案,沒錯,bsd在實作上把socket設計成一種檔案,然後通過虛拟檔案系統的操作接口就可以通路socket,而通路socket時會調用相應的驅動程式,進而也就是使用底層協定進行通信。(vsf也就是unix提供給我們的面向對象程式設計,如果底層裝置是磁盤,就對磁盤讀寫,如果底層裝置是socket就使用底層協定在網中進行通信,而對外的接口都是一緻的)。下面再看一下socket的結構是怎樣的(圖檔來源于《tcp/ip協定詳解卷二》章節一,1.8描述符),注意:這裡的socket是一個執行個體化之後的socket,也就是說是一個具體的通信過程中的socket,不是指抽象的socket結構,下文還會進行解釋。

         

socket原理詳解(同篇2)

                          圖三 udp socket執行個體的結構

很明顯,unix把socket設計成檔案,通過描述符我們可以定位到具體的file結構體,file結構體中有個f_type屬性,辨別了檔案的類型,如圖,DTYPE_VNODE表示普通的檔案DTYPE_SOCKET表示socket,當然還有其他的類型,比如管道、裝置等,這裡我們隻關心socket類型。如果是socket類型,那麼f_ops域指向的就是相應的socket類型的驅動,而f_data域指向了具體的socket結構體,socket結構體關鍵域有so_type,so_pcb。so_type常見的值有:

  • SOCK_STREAM 提供有序的、可靠的、雙向的和基于連接配接的位元組流服務,當使用Internet位址族時使用TCP。
  • SOCK_DGRAM 支援無連接配接的、不可靠的和使用固定大小(通常很小)緩沖區的資料報服務,當使用Internet位址族使用UDP。
  • SOCK_RAW 原始套接字,允許對底層協定如IP或ICMP進行直接通路,可以用于自定義協定的開發。

so_pcb表示socket控制塊,其又指向一個結構體,該結構體包含了目前主機的ip位址(inp_laddr),目前主機程序的端口号(inp_lport),發送端主機的ip位址(inp_faddr),發送端主體程序的端口号(inp_fport)。so_pcb是socket類型的關鍵結構,不亞于程序控制塊之于程序,在程序中,一個pcb可以表示一個程序,描述了程序的所有資訊,每個程序有唯一的程序編号,該編号就對應pcb;socket也同時是這樣,每個socket有一個so_pcb,描述了該socket的所有資訊,而每個socket有一個編号,這個編号就是socket描述符。說到這裡,我們發現,socket确實和程序很像,就像我們把具體的程序看成是程式的一個執行個體,同樣我們也可以把具體的socket看成是網絡通信的一個執行個體。

2、具體socket執行個體如何辨別

我們知道具體的一個檔案可以用一個路徑來表示,比如/home/zzy/src_code/client.c,那麼具體的socket執行個體我們該如何表示呢,其實就是使用上面提到的so_pcb的那幾個關鍵屬性,也就是使用so_type+ip位址+端口号。如果我們使用so_type+ip位址+端口号執行個體一個socket,那麼網際網路上的其他主機就可以與該socket執行個體進行通信了。是以下面我們看一下socket如何進行執行個體化,看看socket給我們提供了哪些接口,而我們又該如何組織這些接口

3、socket程式設計接口

3.1、socket接口

int socket(int protofamily, int so_type, int protocol);

  • protofamily 指協定族,常見的值有:

    AF_INET,指定so_pcb中的位址要采用ipv4位址類型

    AF_INET6,指定so_pcb中的位址要采用ipv6的位址類型

    AF_LOCAL/AF_UNIX,指定so_pcb中的位址要使用絕對路徑名

    當然也還有其他的協定族,用到再學習了

  • so_type 指定socket的類型,也就是上面講到的so_type字段,比較常用的類型有:

    SOCK_STREAM

    SOCK_DGRAM

    SOCK_RAW

  • protocol 指定具體的協定,也就是指定本次通信能接受的資料包的類型和發送資料包的類型,常見的值有:

    IPPROTO_TCP,TCP協定

    IPPROTO_UDP,UPD協定

    0,如果指定為0,表示由核心根據so_type指定預設的通信協定

這裡解釋一下圖三,圖三其實是使用AF_INET,SOCK_DGRAM,IPPRTO_UDP執行個體化之後的一個具體的socket。

那為什麼要通過這三個參數來生成一個socket描述符?

答案就是通過這三個參數來确定一組固定的操作。我們說過抽象的socket對外提供了一個統一、友善的接口來進行網絡通信,但對核心來說,每一個接口背後都是及其複雜的,同一個接口對應了不同協定,而核心有不同的實作,幸運的是,如果确定了這三個參數,那麼相應的接口的映射也就确定了。在實作上,BSD就把socket分類描述,每一個類别都有進行通信的詳細操作,分類見下圖。而對socket的分類,就好比對unix裝置的分類,我們對裝置write和read時,底層的驅動是有各個裝置自己提供的,而socket也一樣,當我們指定不同的so_type時,底層提供的通信細節也由相應的類别提供。

                  

socket原理詳解(同篇2)

                                  圖4 socket層次圖

更詳細的socket()函數參數描述請移步:

​​​javascript:void(0)​​

​​javascript:void(0)​​

3.2、bind接口

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

bind函數就是給圖三種so_pcb結構中的位址指派的接口

  • sockfd   是調用socket()函數建立的socket描述符
  • addr     是具體的位址
  • addrlen  表示addr的長度

struct sockaddr其實是void的typedef,其常見的結構如下圖(圖檔來源傳智播客邢文鵬linux系統程式設計的筆記),這也是為什麼需要addrlen參數的原因,不同的位址類型,其位址長度不一樣:

                  

socket原理詳解(同篇2)

                              圖5 位址結構圖

  • AF_INET:
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};      
  • AF_INET6:
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};
struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};      
  • AF_UNIX:
#define UNIX_PATH_MAX    108
struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};      

3.3、connect接口

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

這三個參數和bind的三個參數類型一直,隻不過此處strcut sockaddr表示對端公開的位址。三個參數都是傳入參數。connect顧名思義就是拿來建立連接配接的函數,隻有像tcp這樣面向連接配接、提供可靠服務的協定才需要建立連接配接

3.4、listen接口

int listen(int sockfd, int backlog)

告知核心在sockfd這個描述符上監聽是否有連接配接到來,并設定同時能完成的最大連接配接數為backlog。3.6節還會繼續解釋這個參數。當調用listen後,核心就會建立兩個隊列,一個SYN隊列,表示接受到請求,但未完成三次握手的連接配接;另一個是ACCEPT隊列,表示已經完成了三次握手的隊列

  • sockfd 是調用socket()函數建立的socket描述符
  • backlog 已經完成三次握手而等待accept的連接配接數

關于backlog , man listen的描述如下:

  • The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
  • If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128.

3.5、accept接口

int accept(int listen_sockfd, struct sockaddr *addr, socklen_t *addrlen)

這三個參數與bind的三個參數含義一緻,不過,此處的後兩個參數是傳出參數。在使用listen函數告知核心監聽的描述符後,核心就會建立兩個隊列,一個SYN隊列,表示接受到請求,但未完成三次握手的連接配接;另一個是ACCEPT隊列,表示已經完成了三次握手的隊列。而accept函數就是從ACCEPT隊列中拿一個連接配接,并生成一個新的描述符,新的描述符所指向的結構體so_pcb中的請求端ip位址、請求端端口将被初始化。

從上面可以知道,accpet的傳回值是一個新的描述符,我們姑且稱之為new_sockfd。那麼new_sockfd和listen_sockfd有和不同呢?不同之處就在于listen_sockfd所指向的結構體so_pcb中的請求端ip位址、請求端端口沒有被初始化,而new_sockfd的這兩個屬性被初始化了。

3.6、listen、connect、accept流程及原理

以AF_INET,SOCK_STREAM,IPPROTO_TCP三個參數執行個體化的socket為例,通過一個副圖來講解這三個函數的工作流程及粗淺原理(圖檔改自​​javascript:void(0)​​)

       

socket原理詳解(同篇2)

                           圖6 listen、accept、connect流程及原理圖

  1. 伺服器端在調用listen之後,核心會建立兩個隊列,SYN隊列和ACCEPT隊列,其中ACCPET隊列的長度由backlog指定。
  2. 伺服器端在調用accpet之後,将阻塞,等待ACCPT隊列有元素。
  3. 用戶端在調用connect之後,将開始發起SYN請求,請求與伺服器建立連接配接,此時稱為第一次握手。
  4. 伺服器端在接受到SYN請求之後,把請求方放入SYN隊列中,并給用戶端回複一個确認幀ACK,此幀還會攜帶一個請求與用戶端建立連接配接的請求标志,也就是SYN,這稱為第二次握手
  5. 用戶端收到SYN+ACK幀後,connect傳回,并發送确認建立連接配接幀ACK給伺服器端。這稱為第三次握手
  6. 伺服器端收到ACK幀後,會把請求方從SYN隊列中移出,放至ACCEPT隊列中,而accept函數也等到了自己的資源,從阻塞中喚醒,從ACCEPT隊列中取出請求方,重建立立一個新的sockfd,并傳回。

這就是listen,accept,connect這三個函數的工作流程及原理。從這個過程可以看到,在connect函數中發生了兩次握手。

更加詳細的accept建立連接配接流程及原理請移步下面這個博文,該博文部落客是個大牛,講解的通熟易懂并且有深度:

3.7、發送消息接口

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>

#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

這幾個接口都比較好了解,查一下man pages就知道什麼含義了,man pages中講解的非常清楚。這裡隻說一下flags參數,也是摘抄自man pages。

flags:

  • MSG_DONTWAIT (since Linux 2.2),不阻塞

Enables nonblocking operation; if the operation would block, EAGAIN or EWOULDBLOCK is returned (this can also be enabled using

the O_NONBLOCK flag with the F_SETFL fcntl(2)).

  • MSG_DONTROUTE,資料包不允許通過網關

Don't use a gateway to send out the packet, only send to hosts on directly connected networks. This is usually used only by

diagnostic or routing programs. This is only defined for protocol families that route; packet sockets don't.

  • MSG_OOB,帶外資料

Sends out-of-band data on sockets that support this notion (e.g., of type SOCK_STREAM); the underlying protocol must also sup‐

port out-of-band data.

  • 其他

3.8、接受消息接口

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

#include <sys/types.h>

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

這幾個接口都比較好了解,查一下man pages就知道什麼含義了,man pages中講解的非常清楚。

4、socket程式設計流程及tcp狀态變遷

先做一個說明,下面的圖都不是原創,是本人收藏已久的一些原理圖,來源已經不記得了,如果大家知道來源的可以留言。

socket程式設計的一般模型是固定的,下面我就以幾幅圖來說明,由于插圖中已經有說明,我就不在做補充說明了。

4.1 c/s模式之TCP

 

socket原理詳解(同篇2)
socket原理詳解(同篇2)

                             圖8 c/s模型tcp程式設計流程圖及tcp狀态變遷圖

4.2 c/s模式之UDP

socket原理詳解(同篇2)
socket原理詳解(同篇2)

                                圖9 c/s模型udp程式設計流程圖

參考資料:

《tcp/ip協定詳解卷一、卷二》

​​socket函數的domain、type、protocol解析​​

​​建立socket函數的第三個參數的意義​​

​​陶輝:​​​​高性能網絡程式設計(一)----accept建立連接配接​​

​​什麼是帶外資料​​

​​Linux的SOCKET程式設計詳解​​