socket工作原理深入分析

本節目錄

一 iso七層模型

看到這裏相信大家對iso七層模型已經有所瞭解了,如果沒有了解,趕緊去惡補一下啦~~

直接上圖:

 

 

iso七層模型圖片

二 什麼是socket

首先我們python基礎部分已經學完了,而socket是我們基礎進階的課程,也就是說,你自己現在完全可以寫一些小程序了,但是前面的學習和練習,我們寫的代碼都是在自己的電腦上運行的,雖然我們學過了模塊引入,文件引入import等等,我可以在程序中獲取到另一個文件的內容,對吧,但是那麼突然有一天,你的朋友和你說:"把你電腦上的一個文件通過你自己寫的程序發送到我的電腦上",這時候怎麼辦?你是不是會想,what?這怎麼搞?就在此時,突然靈感來了,我可以通過qq、雲盤、微信等發送給他啊,可是人家說了,讓你用自己寫的程序啊,嗯,這是個問題,此時又來一個靈感,我給他發送文件肯定是通過網絡啊,這就產生了網絡,對吧,那我怎麼讓我的程序能夠通過網絡來聯繫到我的朋友呢,並且把文件發送給他呢,那麼查了一下,發現網絡通信通過socket可以搞,但是怎麼搞呢?首先,查詢結果是對的,socket就是網絡通信的工具,任何一門語言都有socket,他不是任何一個語言的專有名詞,而是大家通過自己的程序與其他電腦進行網絡通信的時候都用它。知道爲什麼要學習socket了吧~~朋友們~~而你使用自己的電腦和別人的電腦進行聯繫併發送消息或者文件等操作就叫做網絡通信,而網絡通信需要使用socket作爲工具。看下圖(圖一)

 

圖一:各協議所處層次

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

 

圖二:socket在內的各層

那麼,在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常見的值有:

  • 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實例

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

 

四 socket接口

下面我們看一下socket提供的這些接口(實例方法)都是做什麼的,裏面的參數又是什麼意思

4.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:對應tcp

    SOCK_DGRAM:對應udp

    SOCK_RAW:自定義協議或者直接對應ip層

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

    IPPROTO_TCP,TCP協議

    IPPROTO_UDP,UPD協議

    0,如果指定爲0,表示由內核根據so_type指定默認的通信協議

舉例:對於socket(AF_INET, SOCK_RAW, IPPROTO_IP),其原型爲int socket (int domain, int type, int protocol)

  1. 參數protocol用來指明所要接收的協議包,如果是象IPPROTO_TCP(6)這種非0、非255的協議,當操作系統內核碰到ip頭中protocol域和創建socket所使用參數protocol相同的IP包,就會交給這個raw socket來處理,因此,一般來說,要想接收什麼樣的數據包,就應該在參數protocol裏來指定相應的協議。當內核向此raw socket交付數據包的時候,是包括整個IP頭的,並且已經是重組好的IP包。
  2. 如果protocol是IPPROTO_RAW(255),這時候,這個socket只能用來發送IP包,而不能接收任何的數據。發送的數據需要自己填充IP包頭,並且自己計算校驗和。
  3. 對於protocol爲0(IPPROTO_IP)的raw socket。用於接收任何的IP數據包。其中的校驗和和協議分析由程序自己完成。

這是include/linux/in.h裏對各類型的IPPROTO的定義: /* Standard well-defined IP protocols. */ enum { IPPROTO_IP = 0, /* Dummy protocol for TCP */ IPPROTO_ICMP = 1, /* Internet Control Message Protocol */ IPPROTO_IGMP = 2, /* Internet Group Management Protocol */ IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */ IPPROTO_TCP = 6, /* Transmission Control Protocol */ IPPROTO_EGP = 8, /* Exterior Gateway Protocol */ IPPROTO_PUP = 12, /* PUP protocol */ IPPROTO_UDP = 17, /* User Datagram Protocol */ IPPROTO_IDP = 22, /* XNS IDP protocol */ IPPROTO_DCCP = 33, /* Datagram Congestion Control Protocol */ IPPROTO_RSVP = 46, /* RSVP protocol */ IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */ IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */ IPPROTO_ESP = 50, /* Encapsulation Security Payload protocol */ IPPROTO_AH = 51, /* Authentication Header protocol */ IPPROTO_BEETPH = 94, /* IP option pseudo header for BEET */ IPPROTO_PIM = 103, /* Protocol Independent Multicast */ IPPROTO_COMP = 108, /* Compression Header protocol */ IPPROTO_SCTP = 132, /* Stream Control Transport Protocol */ IPPROTO_UDPLITE = 136, /* UDP-Lite (RFC 3828) */ IPPROTO_RAW = 255, /* Raw IP packets */ IPPROTO_MAX };

再具體的參數解析可以參考這篇文章socket函數的domain、type、protocol解析

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

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

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

 

圖四 socket層次圖

4.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參數的原因,不同的地址類型,其地址長度不一樣:

 

圖五 地址結構圖

4.3 connect接口

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

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

4.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.翻譯:TCP套接字上的積壓參數的行爲隨着Linux 2.2而改變。現在,它指定等待被接受的完全建立的套接字的隊列長度,而不是不完整連接請求的數量。不完整套接字隊列的最大長度可以使用/PRO/sys/NET/IPv4/TCPPMAX Syth-ByLoSQL來設置。當啓用同步功能時,沒有邏輯最大長度,並且忽略該設置。有關更多信息,請參見TCP(7)。
  • 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.如果backlog參數大於/proc/sys/net/core/somaxconn中的值,那麼它將被悄悄地截斷爲該值;該文件中的默認值爲128。在2.4.25之前的內核中,這個限制是一個硬編碼的值,SOMAXCONN,值爲128。

4.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的這兩個屬性被初始化了。

4.6 listen、connect、accept流程及原理

以AF_INET,SOCK_STREAM,IPPROTO_TCP三個參數實例化的socket爲例,通過一個副圖來講解這三個函數的工作流程及粗淺原理,看圖六

 

圖六 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函數中發生了兩次握手。

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

4.7 tcp的send接口

沒有摘錄源碼進行分析(這個靠你自己啦),這裏之說一下大致原理

send函數只負責將數據提交給協議層。 當調用該函數時,send先比較待發送數據的長度len和套接字s的發送緩衝區的長度,如果len大於s的發送緩衝區的長度,該函數返回SOCKET_ERROR; 如果len小於或者等於s的發送緩衝區的長度,那麼send先檢查協議是否正在發送s的發送緩衝中的數據; 如果是就等待協議把數據發送完,如果協議還沒有開始發送s的發送緩衝中的數據或者s的發送緩衝中沒有數據,那麼send就比較s的發送緩衝區的剩餘空間和len; 如果len大於剩餘空間大小,send就一直等待協議把s的發送緩衝中的數據發送完,如果len小於剩餘空間大小,send就僅僅把buf中的數據copy到剩餘空間裏(注意並不是send把s的發送緩衝中的數據傳到連接的另一端的,而是協議傳的,send僅僅是把buf中的數據copy到s的發送緩衝區的剩餘空間裏)。 如果send函數copy數據成功,就返回實際copy的字節數,如果send在copy數據時出現錯誤,那麼send就返回SOCKET_ERROR; 如果send在等待協議傳送數據時網絡斷開的話,那麼send函數也返回SOCKET_ERROR。要注意send函數把buf中的數據成功copy到s的發送緩衝的剩餘空間裏後它就返回了,但是此時這些數據並不一定馬上被傳到連接的另一端。 如果協議在後續的傳送過程中出現網絡錯誤的話,那麼下一個Socket函數就會返回SOCKET_ERROR。(每一個除send外的Socket函數在執行的最開始總要先等待套接字的發送緩衝中的數據被協議傳送完畢才能繼續,如果在等待時出現網絡錯誤,那麼該Socket函數就返回SOCKET_ERROR)

4.8 tcp的recv接口

沒有摘錄源碼進行分析(這個靠你自己啦),這裏之說一下大致原理

recv先檢查套接字s的接收緩衝區,如果s接收緩衝區中沒有數據或者協議正在接收數據,那麼recv就一直等待,直到協議把數據接收完畢。當協議把數據接收完畢,recv函數就把s的接收緩衝中的數據copy到buf中(注意協議接收到的數據可能大於buf的長度,所以在這種情況下要調用幾次recv函數才能把s的接收緩衝中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的),recv函數返回其實際copy的字節數。如果recv在copy時出錯,那麼它返回SOCKET_ERROR;如果recv函數在等待協議接收數據時網絡中斷了,那麼它返回0 。對方優雅的關閉socket並不影響本地recv的正常接收數據;如果協議緩衝區內沒有數據,recv返回0,指示對方關閉;如果協議緩衝區有數據,則返回對應數據(可能需要多次recv),在最後一次recv時,返回0,指示對方關閉。

4.9 send和recv的緩存區設置

既然提到了send和recv的大致原理,那麼擔心大家會問如何設置緩衝區大小啊,那麼在這裏做一下解釋

通過setsockopt設置SO_SNDBUF、SO_RCVBUF這連個默認緩衝區的值,再用getsockopt獲取設置的值。

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