1. 程式人生 > >網路程式設計套接字【socket】

網路程式設計套接字【socket】

在學習Linux系統程式設計的時候,程序間的通訊方式包括——管道、訊息佇列、共享記憶體、訊號量等方式。但是這些通訊方式都村子一定的缺陷——都是在同一個機器上的程序間的通訊。為了讓不同機器上的程序之間相互通訊,Linux網路程式設計便可解決。linux系統支援套接字介面,可以通過與使用管道類似的方法來使用套接字,但套接字還包含了計算機網路的通訊。

Linux網路程式設計---套接字。

套接字(socket)是一種通訊機制,憑藉這種機制,客戶/伺服器系統的開發工作既可以在本地單機上進行,也可以跨網路進行。套接字的建立和使用與管道是有區別的,因為套接字明確的將客戶和伺服器區分開來。套接字機制可以實現將多個客戶連線到一個伺服器。

知識儲配:

埠號(port):是傳輸層協議的內容。

1.埠號是一個2位元組16位的整數;

2.埠號用來標識一個程序, 告訴作業系統, 當前的這個資料要交給哪個程序來處理;

3.IP地址 + 埠號能夠標識網路上的某一臺主機的某一個程序;

4.一個埠號通常只能被一個程序佔用。(特例:父子程序)

5.一個程序可以繫結多個埠號,但是一個埠號不能被多個程序繫結。

6.埠號與主機中表示程序的pid不同,一個是網路上的,後者是作業系統核心中分配的。

位元組序:

位元組序分為主機位元組序和網路位元組序:

網路位元組序:是TCP/IP中規定好的一種資料表示格式,它與具體的CPU型別、作業系統等無關,從而可以保證資料在不同主機之間傳輸時能夠被正確解釋。網路位元組順序採用big endian(大端)排序方式。

主機位元組序:不同的主機具有不同的位元組序:可以同過以下方法測試自己系統的主機位元組序。

//小端位元組序:小小小--- 低位放在低地址處為小端
int main()
{
  union tst
  {
    short a;
    char b;
  }tst;
  tst.a = 0x0102;
  if(tst.b == 1)
  {
    printf("big endian\n");
  }
  else{
    printf("little endian\n");
  }
  return 0;
}

當格式化的資料在兩個使用不同位元組序的主機之間直接傳遞時,接收端必然會錯誤的解釋資料,因此,傳送端在傳送的時候講資料轉化成網路位元組序再發送,接收方將資料轉成位元組的主機位元組序接收和解釋。需要指出的是,即使在同一臺機器的兩個程序,有時也要考慮位元組序的問題。Linux提供了4個函式來完成主機位元組序和網路位元組序之間的轉換。

  uint32_t htonl(uint32_t hostlong);      
  uint16_t htons(uint16_t hostshort);            均返回網路位元組序的值
  uint32_t ntohl(uint32_t netlong);
  uint16_t ntohs(uint16_t netshort);             均返回主機位元組序的值    

h表示主機(host)位元組序,n表示網路(network)位元組序,l表示長(long)整數

s表示短(short)整數。

地址轉換函式:

int inet_aton(const char *cp, struct in_addr *inp);   
in_addr_t inet_addr(const char *cp);                
char *inet_ntoa(struct in_addr in);                  

inet_addr將點分十進位制字串表示的IPv4地址轉化為用網路位元組序整數表示的IPv4地址

inet_aton:同inet_addr功能相同,但是將轉化結果儲存在引數inp指向的地址結構中。成功返回1,失敗返回0.   

inet_ntoa:將網路位元組序整數表示成IPv4地址轉化為用點分十進位制字串表示的IPv4地址,但該函式內部用一個靜態變數儲存轉化結果,函式的返回值指向該競態記憶體,因此該函式是不可重入的。在多執行緒環境下, 推薦使用inet_ntop, 這個函式由呼叫者提供一個緩衝區儲存結果, 可以規避執行緒安全問題。

從TCP瞭解套接字介面函式:

1.套接字描述符:socket開啟一個網路通訊埠,如果成功的話,就像open一樣返回一個檔案描述符。應用程式可以像讀寫檔案一樣用read/write在網路上收發資料。

int socket(int domain, int type, int protocol);

domain(域):確定通訊的特性,包括地址格式,各個域有自己的格式表示地址,表示各個域的常數都以AF_開頭,指地址族。

type:確定套接字型別,進一步確認通訊特徵。

protocol:通常為0,表示按給定的域和套接字型別選擇預設協議。

返回值:

 成功: 非負的檔案描述符 。 失敗:-1

2.將套接字與地址繫結(主要針對伺服器,需要繫結一個眾所周知的埠,而不能讓核心去臨時的分配):

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

對於繫結的地址的限制:

1.所執行機器上的指定的地址必須有效

2.地址必須和建立套接字時的地址族所支援的格式相匹配。

3.埠號必須大於1024,除非該程序具有超級特權

4.一般只有套接字端點能夠與地址繫結

socket:要繫結的套接字

address:一個通用的指標型別,可以接受多種協議的sockaddr結構體,而他們的長度各不相同,所以需要第三個引數指定結構體的長度。

address_len:地址指標的長度

返回值:成功返回0,出錯返回-1

3.監聽socket

int listen(int s, int backlog);

返回值:成功返回0,出錯返回-1

listenb僅被TCP伺服器呼叫時,它作了兩件事:

1.當函式socket建立一個套接字介面時,它被假設為一個主動地套介面,

當有客戶端通過connect發起連結這個埠時,listen函式指示核心接受

指向此套介面的請求。呼叫listen函式將導致套介面從CLOSED狀態轉為LISTEN狀態。

2.函式的第二個引數規定了核心為此套介面排隊的最大 連結個數。(典型值為5)

backlog理解(這裡可以類比的記憶海底撈吃飯的例子):對於給定的監聽套接字,核心維護了兩個佇列:

未完成連結佇列:已由客戶端發出併到達伺服器,伺服器正在等待完成相應的TCP三次握手過程。這些套介面都處於SYN_RCVD狀態

已完成連線佇列:唯為每個已完成TCP三次握手的客戶端請求分配一個條目,這些套介面都處於ESTABLISTED狀態。

核心版本2.2之後backlog只表示處於完全連線狀態的socket的上限。

4.接受連結

int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

accept由TCP伺服器呼叫,從已完成連線佇列頭返回下一個已完成的連結,若已完成連結佇列為空,則accept會阻塞直到一個請求到來,如若sockfd處於非阻塞模式,accept會返回-1並將errno設定為EAGAIN或EWOULDBLOCK。

返回值:成功返回檔案描述符,出錯返回-1

addr和addrlen用來返回對端的程序(客戶端)的協議地址,addrlen是結果引數呼叫前,我們將由*addrlen所指的整數值置為由cliaddr所指向的套介面地址結構的長度,返回時,這個整數值即為有核心存在此套介面地址結構內的準確位元組數。

1>三次握手完成後,伺服器呼叫accept()接受連結

2>addr是一個傳出引數,accept()返回時傳出客戶端的地址和埠號;

3>如果給addr 引數傳NULL,表示不關心客戶端的地址;

4>addrlen引數是一個傳入傳出引數(value-result argument), 傳入的是呼叫者提供的, 緩衝區addr的長度以避免緩衝區溢位問題, 傳出的是客戶端地址結構體的實際長度

5.客戶端連結函式

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

返回值:成功0,出錯-1

sockfd:由socket函式返回的套介面描述字

addr:指向套介面地址結構的指標

addrlen:地址結構的大小

客戶端在呼叫connect前不必非得呼叫函式bind,因為如若必要的話核心會選擇源IP地址和一個臨時的埠。如果是TCP套介面的話,函式connect激發TCP的三次握手過程。

客戶端和伺服器建立連線的函式介紹完畢,下面在介紹幾個在資料傳輸時用到的函式。

如果連線建立成功,那麼我們就可以使用read/write來通過套接字進行通訊。但是這兩個函式的功能有些單一,如果想指定選項從多個客戶端接收資料包,或傳送外帶資料,read/write就不能滿足我們的要求。

傳送資料:send, sendto, 和 sendmsg 用於向另一個套接字傳遞訊息.  Send 僅僅用於連線套接字,而 sendto 和sendmsg 可用於任何情況下.它們的返回值都是成功返回傳送的位元組數,出錯返回-1。

int send(int s, const void *msg, size_t len, int flags);

send和write很像,但是可以指定標誌位來改變處理傳輸資料的方式。
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

send和send類似,區別在於sendto允許在無連線的套接字上指定一個目標地址。

對於面向連結的套接字,目標地址是忽略的,因為目標地址蘊含在連線中,對於無連線的套接字,不能使用send除非在呼叫connect時預先設定了目標地址,或者採用sendto來提供另一種傳送報文的方式。
int sendmsg(int s, const struct msghdr *msg, int flags);

sendmsg提供了一個結構體來儲存要傳送的資料。

接收資料:返回值都是成功返回以位元組計數的訊息長度,若無可用訊息或對方已經按序結束則返回0,出錯返回-1

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

        recv和read很像,但是可以指定flags選項來控制如何接收資料。如果傳送者已經呼叫shutdown來結束傳輸,或者網路協議支援預設的順序關閉並且傳送端已經關閉,那麼當所有的資料接收完畢後,recv返回0.

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

       如果需要定位資料的傳送者,可以使用recvfrom來得到資料傳送者的源地址,如果addr非空,它將包含資料傳送者的套接字端點地址。當呼叫recvfrom時,需要設定addrlen引數指向一個包含addr所指的套接字緩衝區位元組大小的整數。返回時,改正數設為改地址的實際位元組大小因為可以獲得傳送者的地址,recvfrom通常用於無連線套接字。

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

為了將收到的資料送人多個緩衝區,或者想接受輔助資料可以使用recvmsg。