http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html(本文轉載於此網址,轉載請註明源連接)html
http://blog.csdn.net/bingxx11/article/details/7822450 (很是經典的異步的TCP通訊模型的解析,強烈推薦)編程
圖2.1 TCP服務器/客戶端響應方式服務器
圖2.2 TCP 三次握手創建鏈接網絡
圖2.3 TCP 四次握手斷開鏈接dom
關於消息發送後的狀態轉換可參考:異步
http://www.cnblogs.com/qlee/archive/2011/07/12/2104089.htmlsocket
int socket(int domain, int type, int protocol);
socket函數對應於普通文件的打開操做。普通文件的打開操做返回一個文件描述字,而socket()用於建立一個socket描述符(socket descriptor),它惟一標識一個socket。這個socket描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些讀寫操做。函數
正如能夠給fopen的傳入不一樣參數值,以打開不一樣的文件。建立socket的時候,也能夠指定不一樣的參數建立不一樣的socket描述符,socket函數的三個參數分別爲:ui
注意:並非上面的type和protocol能夠隨意組合的,如SOCK_STREAM不能夠跟IPPROTO_UDP組合。當protocol爲0時,會自動選擇type類型對應的默認協議。spa
當咱們調用socket建立一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。若是想要給它賦值一個地址,就必須調用bind()函數,不然就當調用connect()、listen()時系統會自動隨機分配一個端口。
正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數的三個參數分別爲:
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 */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
ipv6對應的是:
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 */
};
Unix域對應的是:
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
一般服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就能夠經過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲何一般服務器端在listen以前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
主機字節序就是咱們日常說的大端和小端模式:不一樣的CPU有不一樣的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫作主機序。引用標準的Big-Endian和Little-Endian的定義以下:
a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
網絡字節序:4個字節的32 bit值如下面的次序傳輸:首先是0~7bit,其次8~15bit,而後16~23bit,最後是24~31bit。這種傳輸次序稱做大端字節序。因爲TCP/IP首部中全部的二進制整數在網絡中傳輸時都要求以這種次序,所以它又稱做網絡字節序。字節序,顧名思義字節的順序,就是大於一個字節類型的數據在內存中的存放順序,一個字節的數據沒有順序的問題了。
因此:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序同樣使用的是Big-Endian。因爲這個問題曾引起過血案!公司項目代碼中因爲存在這個問題,致使了不少莫名其妙的問題,因此請謹記對主機字節序不要作任何假定,務必將其轉化爲網絡字節序再賦給socket。
若是做爲一個服務器,在調用socket()、bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數的第一個參數即爲要監聽的socket描述字,第二個參數爲相應socket能夠排隊的最大鏈接個數。socket()函數建立的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的鏈接請求。
connect函數的第一個參數即爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。客戶端經過調用connect函數來創建與TCP服務器的鏈接。
TCP服務器端依次調用socket()、bind()、listen()以後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()以後就想TCP服務器發送了一個鏈接請求。TCP服務器監聽到這個請求以後,就會調用accept()函數取接收請求,這樣鏈接就創建好了。以後就能夠開始網絡I/O操做了,即類同於普通文件的讀寫I/O操做。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函數的第一個參數爲服務器的socket描述字,第二個參數爲指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數爲協議地址的長度。若是accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,表明與返回客戶的TCP鏈接。
注意:accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字;而accept函數返回的是已鏈接的socket描述字。一個服務器一般一般僅僅只建立一個監聽socket描述字,它在該服務器的生命週期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立了一個已鏈接socket描述字,當服務器完成了對某個客戶的服務,相應的已鏈接socket描述字就被關閉。
萬事具有隻欠東風,至此服務器與客戶已經創建好鏈接了。能夠調用網絡I/O進行讀寫操做了,即實現了網咯中不一樣進程之間的通訊!網絡I/O操做有下面幾組:
我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上能夠把上面的其它函數都替換成這兩個函數。它們的聲明以下:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); 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 recv(int sockfd, 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 recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的字節數,若是返回的值是0表示已經讀到文件的結束了,小於0表示出現了錯誤。若是錯誤爲EINTR說明讀是由中斷引發的,若是是ECONNREST表示網絡鏈接出了問題。
write函數將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,並設置errno變量。 在網絡程序中,當咱們向套接字文件描述符寫時有倆種可能。1)write的返回值大於0,表示寫了部分或者是所有的數據。2)返回的值小於0,此時出現了錯誤。咱們要根據錯誤類型來處理。若是錯誤爲EINTR表示在寫的時候出現了中斷錯誤。若是爲EPIPE表示網絡鏈接出現了問題(對方已經關閉了鏈接)。
其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。
在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的socket描述字,比如操做完打開的文件要調用fclose關閉打開的文件。
#include <unistd.h> int close(int fd);
close一個TCP socket的缺省行爲時把該socket標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲read或write的第一個參數。
注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。
網絡進程如何通訊
網絡層的「ip地址」能夠惟一標識網絡中的主機,而傳輸層的「協議+端口」能夠惟一標識主機中的應用程序(進程),這樣利用三元組(ip地址,協議,端口)就能夠標識網絡的進程了,網絡中的進程通訊就能夠利用這個標誌與其它進程進行交互。
套接字
① 流式套接字(SOCK--STREAM):該類套接字提供了面向鏈接的、可靠的、數據無錯而且無重複的數據發送服務。並且發送的數據是按順序接收的。全部利用該套接字進行傳遞的數據均被視爲連續的字節流的而且無長度限制。這對數據的穩定性、正確性和發送/接受順序要求嚴格的應用十分適用,TCP使用該類接口。
② 數據報式套接字(SOCK--DGRAM):數據報式套接字提供了面向無鏈接的服務,不提供正確性檢查,也不保證各數據包的發送順序,所以,可能出現數據的重發、丟失等現象,而且接收順序由具體路由決定。然而,與流式套接字相比,使用數據報式套接字對網絡線路佔用率較低。在TCP協議組中,UDP使用該類套接字。
③ 原始套接字(SOCK--RAW):該套接字通常不會出如今高級網絡接口的現中,由於它是直接針對協議的較低層(如IP、TCP、UDP等)直接訪問的。用於檢驗新的協議實現或訪問現有服務中配置的新設備,使用原始套接字存在絡應用程序的兼容性問題,因此通常不推薦使用原始套接字。
主機字節序就是咱們日常說的大端和小端模式:不一樣的CPU有不一樣的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫作主機序。