socket原理詳解

一、什麼是socket

咱們知道進程通訊的方法有管道、命名管道、信號、消息隊列、共享內存、信號量,這些方法都要求通訊的兩個進程位於同一個主機。可是若是通訊雙方不在同一個主機又該如何進行通訊呢?在計算機網絡中咱們就學過了tcp/ip協議族,其實使用tcp/ip協議族就能達到咱們想要的效果,以下圖(圖片來源於《tcp/ip協議詳解卷一》第一章1.3)linux

         程序員

                           圖一 各協議所處層次編程

固然,這樣作當然是能夠的,可是,當咱們使用不一樣的協議進行通訊時就得使用不一樣的接口,還得處理不一樣協議的各類細節,這就增長了開發的難度,軟件也不易於擴展。因而UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通訊細節,使得程序員無需關注協議自己,直接使用socket提供的接口來進行互聯的不一樣主機間的進程的通訊。這就比如操做系統給咱們提供了使用底層硬件功能的系統調用,經過系統調用咱們能夠方便的使用磁盤(文件操做),使用內存,而無需本身去進行磁盤讀寫,內存管理。socket其實也是同樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就能夠統1、方便的使用tcp/ip協議的功能了。百說不如一圖,看下面這個圖就能明白了。服務器

         

                               圖二 socket所處層次cookie

那麼,在BSD UNIX又是如何實現這層抽象的呢?咱們知道unix中萬物皆文件,沒錯,bsd在實現上把socket設計成一種文件,而後經過虛擬文件系統的操做接口就能夠訪問socket,而訪問socket時會調用相應的驅動程序,從而也就是使用底層協議進行通訊。(vsf也就是unix提供給咱們的面向對象編程,若是底層設備是磁盤,就對磁盤讀寫,若是底層設備是socket就使用底層協議在網中進行通訊,而對外的接口都是一致的)。下面再看一下socket的結構是怎樣的(圖片來源於《tcp/ip協議詳解卷二》章節一,1.8描述符),注意:這裏的socket是一個實例化以後的socket,也就是說是一個具體的通訊過程當中的socket,不是指抽象的socket結構,下文還會進行解釋。網絡

         

                          圖三 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常見的值有:dom

  • 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當作是網絡通訊的一個實例。socket

二、具體socket實例如何標識

咱們知道具體的一個文件能夠用一個路徑來表示,好比/home/zzy/src_code/client.c,那麼具體的socket實例咱們該如何表示呢,其實就是使用上面提到的so_pcb的那幾個關鍵屬性,也就是使用so_type+ip地址+端口號。若是咱們使用so_type+ip地址+端口號實例一個socket,那麼互聯網上的其餘主機就能夠與該socket實例進行通訊了。因此下面咱們看一下socket如何進行實例化,看看socket給咱們提供了哪些接口,而咱們又該如何組織這些接口tcp

三、socket編程接口

3.一、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對外提供了一個統1、方便的接口來進行網絡通訊,但對內核來講,每個接口背後都是及其複雜的,同一個接口對應了不一樣協議,而內核有不一樣的實現,幸運的是,若是肯定了這三個參數,那麼相應的接口的映射也就肯定了。在實現上,BSD就把socket分類描述,每個類別都有進行通訊的詳細操做,分類見下圖。而對socket的分類,就比如對unix設備的分類,咱們對設備write和read時,底層的驅動是有各個設備本身提供的,而socket也同樣,當咱們指定不一樣的so_type時,底層提供的通訊細節也由相應的類別提供。

                  

                                  圖4 socket層次圖

更詳細的socket()函數參數描述請移步:
http://blog.csdn.net/liuxingen/article/details/44995467

http://blog.csdn.net/qiuchangyong/article/details/50099927

3.二、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參數的緣由,不一樣的地址類型,其地址長度不同:

                  

                              圖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.三、connect接口

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

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

 

3.四、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.五、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.六、listen、connect、accept流程及原理

以AF_INET,SOCK_STREAM,IPPROTO_TCP三個參數實例化的socket爲例,經過一個副圖來說解這三個函數的工做流程及粗淺原理(圖片改自http://blog.csdn.net/russell_tao/article/details/9111769

       

                           圖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創建鏈接流程及原理請移步下面這個博文,該博文博主是個大牛,講解的通熟易懂而且有深度:

http://blog.csdn.net/russell_tao/article/details/9111769

3.七、發送消息接口

#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.八、接受消息接口

#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中講解的很是清楚。

四、socket編程流程及tcp狀態變遷

先作一個說明,下面的圖都不是原創,是本人收藏已久的一些原理圖,來源已經不記得了,若是你們知道來源的能夠留言。

socket編程的通常模型是固定的,下面我就以幾幅圖來講明,因爲插圖中已經有說明,我就不在作補充說明了。

4.1 c/s模式之TCP

 

 

                             圖8 c/s模型tcp編程流程圖及tcp狀態變遷圖

4.2 c/s模式之UDP

                                圖9 c/s模型udp編程流程圖

 

參考資料:

《tcp/ip協議詳解卷1、卷二》

socket函數的domain、type、protocol解析

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

陶輝:高性能網絡編程(一)----accept創建鏈接

什麼是帶外數據

Linux的SOCKET編程詳解

相關文章
相關標籤/搜索