網絡編程編程
網絡編程的目的就是指直接或間接地經過網絡協議與其餘計算機進行通信。網絡編程中
有兩個主要的問題,一個是如何準確的定位網絡上一臺或多臺主機,另外一個就是找到主機後
如何可靠高效的進行數據傳輸。在TCP/IP協議中IP層主要負責網絡主機的定位,數據傳輸的
路由,由IP地址能夠惟一地肯定Internet上的一臺主機。而TCP層則提供面向應用的可靠的
或非可靠的數據傳輸機制,這是網絡編程的主要對象,通常不須要關心IP層是如何處理數據
的。服務器
目前較爲流行的網絡編程模型是客戶機/服務器(C/S)結構。即通訊雙方一方做爲服務
器等待客戶提出請求並予以響應。客戶則在須要服務時向服務器提出申請。服務器通常做爲
守護進程始終運行,監聽網絡端口,一旦有客戶請求,就會啓動一個服務進程來響應該客
戶,同時本身繼續監聽服務端口,使後來的客戶也能及時獲得服務。網絡
在Internet上IP地址和主機名是一一對應的,經過域名解析能夠由主機名獲得機器的IP,
因爲機器名更接近天然語言,容易記憶,因此使用比IP地址普遍,可是對機器而言只有IP地
址纔是有效的標識符。socket
一般一臺主機上老是有不少個進程須要網絡資源進行網絡通信。網絡通信的對象準確的講
不是主機,而應該是主機中運行的進程。這時候光有主機名或IP地址來標識這麼多個進程顯然
是不夠的。端口號就是爲了在一臺主機上提供更多的網絡資源而採起得一種手段,也是TCP層
提供的一種機制。只有經過主機名或IP地址和端口號的組合才能惟一的肯定網絡通信中的對象:
進程。函數
套接字大數據
所謂socket一般也稱做"套接字",用於描述IP地址和端口,是一個通訊鏈的句柄。應用程
序一般經過"套接字"向網絡發出請求或者應答網絡請求。ui
套接字能夠根據通訊性質分類,這種性質對於用戶是可見的。應用程序通常僅在同一類的
套接字間進行通訊。不過只要底層的通訊協議容許,不一樣類型的套接字間也照樣能夠通訊。套
接字有兩種不一樣的類型:流套接字和數據報套接字。spa
下面的解釋比較抽象,不看也罷。
套接字是通訊的基石,是支持TCP/IP協議的網絡通訊的基本操做單元。能夠將套接字看做
不一樣主機間的進程進行雙向通訊的端點,它構成了單個主機內及整個網絡間的編程界面。套接
字存在於通訊域中,通訊域是爲了處理通常的線程經過套接字通訊而引進的一種抽象概念。套
接字一般和同一個域中的套接字交換數據(數據交換也可能穿越域的界限,但這時必定要執行
某種解釋程序)。各類進程使用這個相同的域互相之間用Internet協議簇來進行通訊。線程
套接字工做原理指針
要經過互聯網進行通訊,你至少須要一對套接字,其中一個運行於客戶機端,咱們稱之爲
ClientSocket,另外一個運行於服務器端,咱們稱之爲ServerSocket。
根據鏈接啓動的方式以及本地套接字要鏈接的目標,套接字之間的鏈接過程能夠分爲三個
步驟:服務器監聽,客戶端請求,鏈接確認。
所謂服務器監聽,是服務器端套接字並不定位具體的客戶端套接字,而是處於等待鏈接的
狀態,實時監控網絡狀態。
所謂客戶端請求,是指由客戶端的套接字提出鏈接請求,要鏈接的目標是服務器端的套接
字。爲此,客戶端的套接字必須首先描述它要鏈接的服務器的套接字,指出服務器端套接字的
地址和端口號,而後就向服務器端套接字提出鏈接請求。
所謂鏈接確認,是指當服務器端套接字監聽到或者說接收到客戶端套接字的鏈接請求,它
就響應客戶端套接字的請求,創建一個新的線程,把服務器端套接字的描述發給客戶端,一旦
客戶端確認了此描述,鏈接就創建好了。而服務器端套接字繼續處於監聽狀態,繼續接收其餘
客戶端套接字的鏈接請求。
struct in_addr { in_addr_t s_addr; // 32-bit IPv4 address //network byte ordered } struct sockaddr_in { sa_family_t sin_family; //AF_INET in_port_t sin_port; //16-bit TCP or UDP port nummber, network byte ordered struct in_addr sin_addr; //32-bit IPv4 address, network byte ordered char sin_zero[8]; //unused }
sockaddr_in是網絡套接字地址結構,大小爲16字節,定義在<netinet/in>頭文件中,通常咱們在程序中是使用該結構體,可是做爲參數傳遞給套接字函數時須要強轉爲sockaddr類型,注意該結構體中port和addr成員是網絡序的(大端結構)。
struct sockaddr { sa_family_t sa_family; //address family: AF_XXX value char sa_data[14]; //protocol-specific address }
sockaddr是經過套接字地址結構,看成爲參數傳遞給套接字函數時,套接字地址結構老是以指針方式來使用,好比bind/accept/connect函數等。
#include <netinet/in.h> uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue);
Linux提供了4個函數來完成主機字節序和網絡字節序之間的轉換。這些函數名字中,h表示host,n表示net,s表示short,l表示long。使用這些函數時,並不關心主機字節序和網絡字節序的真實值,也就是爲大端仍是小端,要作的只是調用適當的函數在主機和網絡字節序之間轉換爲某個特定值。
#include <arpa/inet.h> int inet_aton(const char *strptr, struct in_addr *addrptr); // 返回:若字符有效則爲1,不然爲0 in_addr_t inet_addr(const char *strptr); // 返回:若字符串有效則爲32位二進制網絡字節序地址,不然爲INADDR_NONE char *inet_ntoa(struct in_addr inaddr); // 返回:指向一個點分十進制數串的地址
inet_aton、inet_addr和inet_ntoa在點分十進制數串(好比"192.168.1.1")與它長度爲32位的網絡字節序二進制值間轉換IPv4地址。在調用inet_addr時需特別注意,inet_ntoa函數的輸入參數是unsigned int型的ip地址,返回的倒是指向ip字符串的指針,很明顯,ip字符串所佔的內存是在函數內部分配的,而咱們並不須要釋放該內存,因此,它分配的內存是靜態的,內部使用static變量存儲IP點分十進制數串,也就是說第二次調用該函數時會覆蓋第一次調用該函數時的內存。
#include <arpa/inet.h> int inet_pton(int family, const char *strptr, void *addrptr); // 返回:成功爲1,輸入不是有效表達式返回0,出錯爲-1 const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); // 返回:成功爲指向結果的指針,出錯爲NULL
這兩個函數對於IPv4和IPv6都適用,p表明表達式(presentation)、n表示數值(numeric)。第一個函數嘗試轉化由strptr指針所指的字符串,經過addptr指針存放二進制結果,成功返回1,若是對指定的family而言輸入的不是有效的表達格式,那麼返回0
inet_ntop進行相反的操做,若是len的值過小,不足以存放表達式結果,則返回一個空指針,並置error爲ENOSPC。inet_ntop函數的strptr參數不能夠是一個空指針,調用者必須爲目標存儲單元分配內存並制定其大小,調用成功時,這個指針就是該函數返回值。
爲了執行網絡IO,一個進程必須作的第一件事就是調用socket函數,指按期望的通訊協議類型(好比使用IPv4的TCP、使用IPv6的UDP、Unix域字節流協議)和套接字字類型(字節流、數據報或原始套接字)。
#include <sys/socket.h> int socket(int family, int type, int protocol); // 成功返回非負描述符,出錯-1
family指定協議族,type指定套接字類型,protocol指定某個協議類型常值,或者設爲0。
family的值有:
type的值有:
protocol的值有:
socket函數在成功時返回一個小的非負整數值,與文件描述符相似,成爲套接字描述符,爲了獲得這個描述符,須要指定協議族和套接字類型,可是並無指定本地協議地址和遠端協議地址。
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); // 返回:成功爲0,出錯-1
TCP客戶用connect函數來創建一個與TCP服務器鏈接,sockfd是由socket函數返回的套接字描述符,第二個、第三個參數分別是指向一個套接字地址結構的指針和該結構的大小,套接字結構必須含有服務器的IP地址和端口號。注意:若是connect失敗後,就必須close當前的套接字描述符並從新調用socket。客戶端在調用connect前沒必要非得調用bind函數(好比UDP客戶端編程中通常就不用調用bind),內核會肯定源IP地址,並選擇一個臨時端口做爲源端口。
若是是TCP套接字,調用connect函數將激發TCP的三次握手過程,並且僅在鏈接創建成功或出錯時才返回。注意:connect是在接收到服務端響應的SYN+ACK時的返回的,也就是三次握手的第二次動做以後。
UDP是能夠調用connect函數的,可是UDP的connect函數和TCP的connect函數調用確是截然不同的,這裏沒有三次握手過程。內核只是檢查是否存在當即可知的錯誤(好比目的地址不可達),記錄對端的IP和端口號,而後當即返回調用進程。使用了connect的UDP編程就可沒必要使用sendto函數了,直接使用write/read便可。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 返回:成功爲0,出錯-1
bind函數把一個本地協議地址賦予一個套接字,它只是把一個協議地址賦予一個套接字,至於協議地址的含義則取決於協議自己。第二個參數指向協議地址結構的指針,第三個參數是協議地址的長度,對於TCP,調用bind函數能夠指定一個端口號,或指定一個IP地址,或二者都指定,也能夠二者都不指定。
bind函數綁定特定的IP地址必須屬於其所在主機的網絡接口之一,服務器在啓動時綁定它們衆所周知的端口,若是一個TCP客戶端或服務端不曾調用bind綁定一個端口,當調用connect或listen時,內核就要爲響應的套接字選擇一個臨時端口。讓內核選擇臨時端口對於TCP客戶端來講是正常的額,而後對於TCP服務端來講確實罕見的,由於服務端經過他們衆所周知的端口被你們認識的。
#include <sys/socket.h> int listen(int sockfd, int backlog); // 返回:成功返回0,出錯-1
socket建立一個套接字時,它被假設爲一個主動套接字,也就是說,它是一個將調用connect發起鏈接的一個客戶套接字。listen函數把一個未鏈接的套接字轉換爲一個被動套接字,指示內核應接受指向該套接字的鏈接請求,調用listen函數將致使套接字從CLOSEE狀態轉換到LISTEN狀態。第二個參數規定了內核應爲相應套接字排隊的最大鏈接個數。
圖片來自《UNIX網絡編程-卷一》
backlog參數在不一樣的系統中有不一樣的解釋,不過大體相似。UNP(第3版)給出的定義爲:listen()的backlog應該指定某個給定套接字上內核爲之排隊的最大已完成鏈接數。
當一個客戶端SYN達到時,若這些隊列是滿的,TCP就忽略該分節,也便是不發送RST,這樣作是暫時的,客戶端將從新發送SYN,指望不就就能獲得服務。假如服務端響應一個RST,客戶端的connect就會返回錯誤,而不是讓重傳機制來處理,這樣客戶沒法區分SYN的RST是由於"該端口沒有在監聽"仍是"該端口在監聽,只不過它的隊列滿了"。
在三路握手完成以後,但在服務端調用accept以前到達的數據應由服務端TCP排隊,最大數據量爲相應已鏈接套接字的接收緩衝區大小。
在TCP服務端套接字編程中,執行完listen後,而沒有執行accept,客戶端是能夠成功創建鏈接的,只不過是該鏈接被加入到了已鏈接隊列中,當調用accept時會被提取出來。
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 返回:成功返回已鏈接描述符(非負),出錯-1
accept函數有TCP服務器調用,用於從已完成隊列中列頭返回下一個已完成鏈接,若是已完成隊列爲空,則進程被投入睡眠(若是該套接字爲阻塞方式的話)。若是accept成功,那麼其返回值是由內核自動生成的一個全新套接字,表明與返回客戶的TCP鏈接,函數的第一個參數爲監聽套接字,返回值爲已鏈接套接字。
#include <unistd.h> int close(int sockfd); // 若成功返回0,出錯-1
close一個TCP套接字的默認行爲是把該套接字標記爲已關閉,而後當即返回到調用進程。注意,close實質把該套接字引用值減1,若是該引用值大於0,則對應的套接字不會被真正關掉。
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *localaddr, &addrlen); int getpeername(int sockfd, struct sockaddr *peeraddr, &addrlen); // 返回:成功爲0, 出錯爲-1
getsockname獲取sockfd對應的本端socket地址,並將其存儲於address參數指定的內存地址,該socket長度存儲於addrlen指向的變量中。getpeername獲取遠端的socket地址。
UDP客戶端若是調用connect以後也是可使用getpeername的。
#include <sys/socket.h> ssize recv(int sockfd, void *buff, size_t nbytes, int flags); ssize send(int sockfd, void *buff, size_t nbytes, int flags); // 返回:成功爲讀入或寫入的字節數,出錯爲-1
TCP流數據讀寫操做函數。flag取值以下所示:
注意的是,flags參數只對send和recv的當前調用有效,固然也能夠經過setsockopt系統調用永久性地修 改socket的某些屬性。