咱們知道進程通訊的方法有管道、命名管道、信號、消息隊列、共享內存、信號量,這些方法都要求通訊的兩個進程位於同一個主機。可是若是通訊雙方不在同一個主機又該如何進行通訊呢?在計算機網絡中咱們就學過了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
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給咱們提供了哪些接口,而咱們又該如何組織這些接口tcp
int socket(int protofamily, int so_type, int protocol);
這裏解釋一下圖三,圖三實際上是使用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
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函數就是給圖三種so_pcb結構中的地址賦值的接口
struct sockaddr實際上是void的typedef,其常見的結構以下圖(圖片來源傳智播客邢文鵬linux系統編程的筆記),這也是爲何須要addrlen參數的緣由,不一樣的地址類型,其地址長度不同:
圖5 地址結構圖
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 */ };
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 */ };
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
這三個參數和bind的三個參數類型一直,只不過此處strcut sockaddr表示對端公開的地址。三個參數都是傳入參數。connect顧名思義就是拿來創建鏈接的函數,只有像tcp這樣面向鏈接、提供可靠服務的協議才須要創建鏈接
int listen(int sockfd, int backlog)
告知內核在sockfd這個描述符上監聽是否有鏈接到來,並設置同時能完成的最大鏈接數爲backlog。3.6節還會繼續解釋這個參數。當調用listen後,內核就會創建兩個隊列,一個SYN隊列,表示接受到請求,但未完成三次握手的鏈接;另外一個是ACCEPT隊列,表示已經完成了三次握手的隊列
關於backlog , man listen的描述以下:
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的這兩個屬性被初始化了。
以AF_INET,SOCK_STREAM,IPPROTO_TCP三個參數實例化的socket爲例,經過一個副圖來說解這三個函數的工做流程及粗淺原理(圖片改自http://blog.csdn.net/russell_tao/article/details/9111769)
圖6 listen、accept、connect流程及原理圖
這就是listen,accept,connect這三個函數的工做流程及原理。從這個過程能夠看到,在connect函數中發生了兩次握手。
更加詳細的accept創建鏈接流程及原理請移步下面這個博文,該博文博主是個大牛,講解的通熟易懂而且有深度:
http://blog.csdn.net/russell_tao/article/details/9111769
#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:
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)).
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.
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.
#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編程的通常模型是固定的,下面我就以幾幅圖來講明,因爲插圖中已經有說明,我就不在作補充說明了。
圖8 c/s模型tcp編程流程圖及tcp狀態變遷圖
圖9 c/s模型udp編程流程圖
參考資料:
《tcp/ip協議詳解卷1、卷二》