網絡編程--Socket(套接字)


網絡編程

    網絡編程的目的就是指直接或間接地通過網絡協議與其他計算機進行通訊。網絡編程中 
有兩個主要的問題,一個是如何准確的定位網絡上一台或多台主機,另一個就是找到主機后 
如何可靠高效的進行數據傳輸。在TCP/IP協議中IP層主要負責網絡主機的定位,數據傳輸的 
路由,由IP地址可以唯一地確定Internet上的一台主機。而TCP層則提供面向應用的可靠的 
或非可靠的數據傳輸機制,這是網絡編程的主要對象,一般不需要關心IP層是如何處理數據 
的。

    目前較為流行的網絡編程模型是客戶機/服務器(C/S)結構。即通信雙方一方作為服務 
器等待客戶提出請求並予以響應。客戶則在需要服務時向服務器提出申請。服務器一般作為 
守護進程始終運行,監聽網絡端口,一旦有客戶請求,就會啟動一個服務進程來響應該客 
戶,同時自己繼續監聽服務端口,使后來的客戶也能及時得到服務。

    在Internet上IP地址和主機名是一一對應的,通過域名解析可以由主機名得到機器的IP, 
由於機器名更接近自然語言,容易記憶,所以使用比IP地址廣泛,但是對機器而言只有IP地 
址才是有效的標識符。

    通常一台主機上總是有很多個進程需要網絡資源進行網絡通訊。網絡通訊的對象准確的講 
不是主機,而應該是主機中運行的進程。這時候光有主機名或IP地址來標識這么多個進程顯然 
是不夠的。端口號就是為了在一台主機上提供更多的網絡資源而采取得一種手段,也是TCP層 
提供的一種機制。只有通過主機名或IP地址和端口號的組合才能唯一的確定網絡通訊中的對象: 
進程。

套接字

    所謂socket通常也稱作"套接字",用於描述IP地址和端口,是一個通信鏈的句柄。應用程 
序通常通過"套接字"向網絡發出請求或者應答網絡請求。

    套接字可以根據通信性質分類,這種性質對於用戶是可見的。應用程序一般僅在同一類的 
套接字間進行通信。不過只要底層的通信協議允許,不同類型的套接字間也照樣可以通信。套 
接字有兩種不同的類型:流套接字和數據報套接字。

    下面的解釋比較抽象,不看也罷。 
    套接字是通信的基石,是支持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函數等。

htons、ntohs、htonl和ntohl函數

#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。使用這些函數時,並不關心主機字節序和網絡字節序的真實值,也就是為大端還是小端,要做的只是調用適當的函數在主機和網絡字節序之間轉換為某個特定值。

inet_aton、inet_addr和inet_ntoa函數

#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點分十進制數串,也就是說第二次調用該函數時會覆蓋第一次調用該函數時的內存。

inet_pton和inet_ntop函數

#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參數不可以是一個空指針,調用者必須為目標存儲單元分配內存並制定其大小,調用成功時,這個指針就是該函數返回值。

 

socket函數

  為了執行網絡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的值有:

  • AF_INET IPv4協議
  • AF_INET6 Ipv6協議
  • AF_LOCAL Unix協議域
  • AF_ROUTE 路由套接字
  • AF_KEY 秘鑰套接字

type的值有:

  • SOCK_STREAM 字節流套接字
  • SOCK_DGRAM 數據報套接字
  • SOCK_SEQPACKET 有序分組套接字
  • SOCK_RAW 原始套接字

protocol的值有:

  • IPPROTO_CP TCP傳輸協議
  • IPPROTO_UDP UDP傳輸協議
  • IPPROTO_SCTP SCTP傳輸協議

  socket函數在成功時返回一個小的非負整數值,與文件描述符類似,成為套接字描述符,為了得到這個描述符,需要指定協議族和套接字類型,但是並沒有指定本地協議地址和遠端協議地址。

connect函數

#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即可。

bind函數

#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服務端來說確實罕見的,因為服務端通過他們眾所周知的端口被大家認識的。

listen函數

#include <sys/socket.h>
int listen(int sockfd, int backlog); // 返回:成功返回0,出錯-1

  socket創建一個套接字時,它被假設為一個主動套接字,也就是說,它是一個將調用connect發起連接的一個客戶套接字。listen函數把一個未連接的套接字轉換為一個被動套接字,指示內核應接受指向該套接字的連接請求,調用listen函數將導致套接字從CLOSEE狀態轉換到LISTEN狀態。第二個參數規定了內核應為相應套接字排隊的最大連接個數。

  1. 未完成連接隊列:每一個這樣的SYN分節對應其中一項:已由某個客戶發出並到達服務器,而服務器正在等待完成相應的TCP三路握手過程。這些套接字處於SYN_RCVD狀態。
  2. 已完成連接隊列:每個完成TCP三路握手過程的客戶對應其中一項,這些套接字處於ESTABLISHED狀態。

 

 

圖片來自《UNIX網絡編程-卷一》

  backlog參數在不同的系統中有不同的解釋,不過大致類似。UNP(第3版)給出的定義為:listen()的backlog應該指定某個給定套接字上內核為之排隊的最大已完成連接數。

  當一個客戶端SYN達到時,若這些隊列是滿的,TCP就忽略該分節,也即是不發送RST,這樣做是暫時的,客戶端將重新發送SYN,期望不就就能得到服務。假如服務端響應一個RST,客戶端的connect就會返回錯誤,而不是讓重傳機制來處理,這樣客戶無法區分SYN的RST是因為"該端口沒有在監聽"還是"該端口在監聽,只不過它的隊列滿了"。

  在三路握手完成之后,但在服務端調用accept之前到達的數據應由服務端TCP排隊,最大數據量為相應已連接套接字的接收緩沖區大小。

  在TCP服務端套接字編程中,執行完listen后,而沒有執行accept,客戶端是可以成功建立連接的,只不過是該連接被加入到了已連接隊列中,當調用accept時會被提取出來。

accept函數

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); //  返回:成功返回已連接描述符(非負),出錯-1

  accept函數有TCP服務器調用,用於從已完成隊列中列頭返回下一個已完成連接,如果已完成隊列為空,則進程被投入睡眠(如果該套接字為阻塞方式的話)。如果accept成功,那么其返回值是由內核自動生成的一個全新套接字,代表與返回客戶的TCP連接,函數的第一個參數為監聽套接字,返回值為已連接套接字。

close函數

#include <unistd.h>
int close(int sockfd); // 若成功返回0,出錯-1

  close一個TCP套接字的默認行為是把該套接字標記為已關閉,然后立即返回到調用進程。注意,close實質把該套接字引用值減1,如果該引用值大於0,則對應的套接字不會被真正關掉。

 

服務器、客戶端交互流程圖

TCP狀態轉換圖

getsockname和getpeername函數

#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的。

recv和send函數

#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取值如下所示:

  • MSG_OOB 對於send,表明將要發送帶外數據,TCP連接上只有一個字節可以作為帶外數據發送,對於recv,本標志表明即將要讀入的是帶外數據而不是普通數據。
  • MSG_PEEK 該標志適用於recv和recvfrom,它允許我們查看已可讀取的數據,而且在系統不在recv和recvfrom返回丟棄其這些數據

  注意的是,flags參數只對send和recv的當前調用有效,當然也可以通過setsockopt系統調用永久性地修 改socket的某些屬性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM