5.1 socket 地址 API
- 現代CPU的累加器一次都能裝載(至少)4 字節(這里考慮32位機,下同),即一個整
數。那么這4 字節在內存中排列的順序將影響它被累加器裝載成的整數的值。這就是字節序
問題。字節序分為大端字節序(big endian)和小端字節序(little endian)o 大端字節序是指
一個整數的高位字節(23? 31 b it)存儲在內存的低地址處,低位字節(0 ? 7 b it)存儲在 內存的高地址處。小端字節序則是指整數的高位字節存儲在內存的高地址處,而低位字節則
存儲在內存的低地址處。 - 現代PC大多采用小端字節序,因此小端字節序又被稱為主機字節序。
- 當格式化的數據(比如32 bit整型數和16 bit短整型數)在兩臺使用不同字節序的主機 之間直接傳遞時,接收端必然錯誤地解釋之。解決問題的方法是:發送端總是把要發送的
數據轉化成大端字節序數據后再發送,而接收端知道對方傳送過來的數據總是采用大端字
節序,所以接收端可以根據自身采用的字節序決定是否對接收到的數據進行轉換(小端機轉
換,大端機不轉換)。因此大端字節序也稱為網絡字節序,它給所有接收數據的主機提供了
一個正確解釋收到的格式化數據的保證
5.1.2 通用socket地址
- TCP/IP協議族有sockaddr_in和 sockaddr_in6兩個專用socket地址結構體,它們分別用 于 IPv4 和 IPv6
- ?所有專用socket地 址 (以 及 sockaddr storage)類型的變量在實際使用時都需要轉化為 通用socket地址類型sockaddr (強制轉換即可),因為所有socket編程接口使用的地址參數
的類型都是sockaddr
- ?inet_ntop成功時返回目標存儲單元的地址,失敗則返回NULL并設置ermo
5.2 創建 socket
- ?domain參數告訴系統使用哪個底層協議族。對 TCP/IP協議族而言,該參數應該設置為PF_INET ( Protocol Family of In tern et,用于 IPv4) 或 PF_INET6 ( 用于 IPv6) ;對于 UNIX本地域協議族而言,該參數應該設置為PF_UNIX。關于socket系統調用支持的所有協議族, 請讀者自己參考其man手冊。 type參數指定服務類型。服務類型主要有SOCK_STREAM服 務 (流服務)和 SOCK_ UGRAM (數據報)服務。對 TCP/IP協議族而言,其值取SOCK_STREAM表示傳輸層使用TCP協議,取 SOCK_DGRAM表示傳輸層使用UDP協議。
- 值得指出的是,自Linux內核版本2.6.17起,type參數可以接受上述服務類型與下面兩個重要的標志相與的值:SOCK_NONBLOCK和 SOCK_CLOEXEC它們分別表示將新創建的socket設為非阻塞的,以及用fork調用創建子進程時在子進程中關閉該socket。在內核版本2.6.17之前的Linux中,文件描述符的這兩個屬性都需要使用額外的系統調用(比如fcntl) 來設置。
- protocol參數是在前兩個參數構成的協議集合下,再選擇一個具體的協議。不過這個值 通常都是唯一的(前兩個參數已經完全決定了它的值)。幾乎在所有情況下,我們都應該把它設置為0 ,表示使用默認協議。
- socket系統調用成功時返回一個socket文件描述符,失敗則返回-1并設置ermo
5.3 命名 socket
- 創建socket時,我們給它指定了地址族,但是并未指定使用該地址族中的哪個具體 socket地址。將一個socket與 socket地址綁定稱為給socket命名.在服務器程序中,我們通 常要命名socket,因為只有命名后客戶端才能知道該如何連接它。客戶端則通常不需要命名 socket,而是采用匿名方式,即使用操作系統自動分配的socket地址。命名socket的系統調 用是bind,其定義如下:
- ?bind將 my_addr所指的socket地址分配給未命名的sockfd文件描述符,addrlen參數指出該socket地址的長度。
- bind成功時返回0 , 失敗則返回-1并設置ermo其中兩種常見的ermo是 EACCES和EADDRINUSE,它們的含義分別是
- EACCES,被綁定的地址是受保護的地址,僅超級用戶能夠訪問。比如普通用戶將
socket綁定到知名服務端口(端口號為0?1023) 上時,bind將返回EACCES錯誤 - EADDRINUSE,被綁定的地址正在使用中。比如將socket綁定到一個處于TIME_ WAIT狀態的socket地址。
5.4 監聽 socket
socket被命名之后,還不能馬上接受客戶連接,我們需要使用如下系統調用來創建一個 監聽隊列以存放待處理的客戶連接:
- ?sockfd參數指定被監聽的socketo backlog參數提示內核監聽隊列的最大長度。監 聽隊列的長度如果超過backlog,服務器將不受理新的客戶連接,客戶端也將收到ECONNREFUSED錯誤信息。在內核版本2.2之前的Linux中,backlog參數是指所有處于半 連接狀態(SYN_RCVD)和完全連接狀態(ESTABLISHED)的 socket的上限。但自內核版本 2.2之后,它只表示處于完全連接狀態的socket的上限,處于半連接狀態的socket的上限則由 /proc/sys/net/ipv4/tcp_max_syn_backlog 內核參數定義。backlog 參數的典型值是 5。?
- listen成功時返回0 , 失敗則返回-1并設置ermo
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>#include "include/algorithm_text.h"static bool stop = false;
static void handle_term(int sig){stop = true;
}
int main(int argc,char* argv[]) {//參考鏈接 https://blog.51cto.com/u_15284125/2988992//signal函數 第一參數是指需要進行處理的信號,第二個參數是指 處理的方式(系統默認/忽略/捕獲)//SIGTERM 請求中止進程,kill命令缺省發送 交給handle_term函數進行處理signal(SIGTERM,handle_term);if (argc < 3){//basename 參考鏈接 https://blog.csdn.net/Draven_Liu/article/details/38235585//假設路徑為 nihao/nihao/jhhh/txt.c//basename函數并不會關心路徑是否正確,文件是否存在,只不過是把路徑上除了最后的txt.c 這個文件名字其他的東西都刪除了然后返回txt.c而已std::cout << "usage:" <<basename(argv[0]) << "ip_address port_number backlog\n"<<std::endl;}//argv[1] ip地址//argv[2] 端口號//argv[3] 日志級別const char* ip = argv[1];//atoi 把字符串轉換成一個整數//參考鏈接 https://www.runoob.com/cprogramming/c-function-atoi.htmlint port = atoi(argv[2]);int backlog = atoi(argv[3]);//socket編程 第一個參數表示使用哪個底層協議族,對 TCP/IP協議族而言,該參數應該設置為PF_INET//對 TCP/IP協議族而言,其值取SOCK_STREAM表示傳輸層使用TCP協議//第三個參數是在前兩個參數構成的協議集合下,再選擇一個具體的協議 設置為0 ,表示使用默認協議int sock = socket(PF_INET,SOCK_STREAM,0);//斷言 如果不正確 不會往下繼續執行assert(sock>=0);//創建一個IPv4 socket地址//TCP/IP 協議族sockaddr_in 表示IPv4專用socket地址結構體struct sockaddr_in address;// bzero() 會將內存塊(字符串)的前n個字節清零;// s為內存(字符串)指針,n 為需要清零的字節數。// 在網絡編程中會經常用到。bzero(&address,sizeof (address));address.sin_family = AF_INET;//int inet_pton(int af,const char* src,void* dst)//af 指定地址族 AF_INET 或者 AF_INET6//inet_pton函數成功返回1 失敗返回0,并且設置errno//errno 表示各種錯誤// inet_pton 將字符串表示的IP地址src(使用點分十進制表示的IPv4地址和使用十六進制表示的IPv6)轉換成網絡字節序整數表示的IP地址,并把轉換的結果存儲在dst指向的內存中inet_pton(AF_INET,ip,&address.sin_addr);//const char* inet_ntop(int af,const char* src,void* dst,socklen_t cnt)//inet_tpon函數和inet_pton進行相反的轉換,前三個參數的含義與其相同,最后一個參數cnt指定目標存儲單元的大小//成功 返回目標單元的地址 失敗返回NULL 并且設置errnoaddress.sin_port = htons(port);//bind將 my_addr所指的socket地址(2)分配給未命名的sockfd(1)文件描述符,addrlen參數(3)指出該socket地址的長度。int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));assert(ret != -1);ret = listen(sock,backlog);//循環等待連接 直到有SIGTERM信號將其中斷while(!stop){sleep(1);}//關閉 socketclose(sock);return 0;
}
- ?我們改變服務器程序的第3個參數并重新運行之,能發現同樣的規律,即完整連接最多有(backlog+1)個。在不同的系統上,運行結果會有些差 別,不過監聽隊列中完整連接的上限通常比backlog值略大。
5 . 5 接受連接
- sockfd參數是執行過listen系統調用的監聽socket,addr參數用來獲取被接受連接的遠 端 socket地址,該 socket地址的長度由addrlen參數指出。accept成功時返回一個新的連接 socket,該 socket唯一地標識了被接受的這個連接,服務器可通過讀寫該socket來與被接受 連接對應的客戶端通信accept失敗時返回-1并設置ermo
- 們把執行過listen調用、處于LISTEN狀態的socket稱為監聽socket,而所有處于ESTABLISHED狀態的 socket則稱為連接socket
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>#include "include/algorithm_text.h"int main(int argc,char* argv[]) {if (argc <= 2){printf("usage:%s ip_address port_number\n", basename(argv[0]));return 1;}const char* ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in address{};bzero(&address,sizeof (address));address.sin_family = AF_INET;inet_pton(AF_INET,ip,&address.sin_addr);//htons 將一個無符號短整型數值轉換為網絡字節序,即大端模式(big-endian)address.sin_port = htons(port);int sock = socket(PF_INET,SOCK_STREAM,0);assert(sock >= 0);int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));assert(ret != -1);ret = listen(sock,5);//等待20秒 等待客戶端連接和相關操作 (掉線/退出)完成sleep(20);struct sockaddr_in client;socklen_t client_addrlength = sizeof (client);int connfd = accept(sock,(struct sockaddr*)&client,&client_addrlength);if (connfd < 0){printf("errno is:%d\n",errno);} else{//接受連接成功 則打印客戶端的IP地址和端口號char remote[INET_ADDRSTRLEN];printf("connected with ip:%s and port:%d\n",inet_ntop(AF_INET,&client.sin_addr,remote,INET_ADDRSTRLEN),// 將一個無符號短整形數從網絡字節順序轉換為主機字節順序ntohs(client.sin_port));close(connfd);}close(sock);return 0;
}
- accept是從監聽隊列中取出連接,而不論連接處于何種狀態(如上面的 ESTABLISHED狀態和CLOSE_WAIT狀態),更不關心任何網絡狀況的變化
5 .6 發起連接
- 如果說服務器通過listen調用來被動接受連接,那么客戶端需要通過如下系統調用來主 動與服務器建立連接:
- ?sockfd參數由socket系統調用返回一個socket。serv addr參數是服務器監聽的socket地
址,addrlen參數則指定這個地址的長度。? - connect成功時返回0。一旦成功建立連接,sockfd就唯一地標識了這個連接,客戶端就 可以通過讀寫sockfd來與服務器通信。connect失敗則返回-1并設置e rm s 其中兩種常見的
ermo是 ECONNREFUSED和 ETIMEDOUT,它們的含義如下: - ?ECONNREFUSED,目標端口不存在,連接被拒絕
- ETIMEDOUT,連接超時
5 .7 關閉連接
- 關閉一個連接實際上就是關閉該連接對應的socket,這可以通過如下關閉普通文件描述 符的系統調用來完成
- ?fd參數是待關閉的socketo不過,close系統調用并非總是立即關閉一個連接,而是將fd 的引用計數減1。只有當fd的引用計數為。時,才真正關閉連接。多進程程序中,一次fork系統調用默認將使父進程中打開的socket的引用計數加1 ,因此我們必須在父進程和子進程 中都對該socket執行close調用才能將連接關閉。?
- 如果無論如何都要立即終止連接(而不是將socket的引用計數減1 ) ,可以使用如下的 shutdown系統調用(相對于close來說,它是專門為網絡編程設計的)
- ?sockfd參數是待關閉的socketo howto參數決定了 shutdown的行為,它可取表5? 3中的
某個值。
- ?由此可見,shutdown能夠分別關閉socket上的讀或寫,或者都關閉°而 close在關閉連 接時只能將socket上的讀和寫同時關閉o
- shutdown成功時返回0 , 失敗則返回-1并設置ermoo
5 . 8 數據讀寫
5.8.1 TCP數據讀寫
- 對文件的讀寫操作read和 write同樣適用于sockets但是socket編程接口提供了幾個專 門用于socket數據讀寫的系統調用,它們增加了對數據讀寫的控制。其中用于TCP流數據讀 寫的系統調用是:
- recv讀取sockfd上的數據,buf和 Ien?參數分別指定讀緩沖區的位置和大小,flags參數的含義見后文,通常設置為0 即可。即recv成功時返回實際讀取到的數據的長度,它可能小于 我們期望的長度len。因此我們可能要多次調用recv ,才能讀取到完整的數據。recv可能返回 0 , 這意味著通信對方已經關閉連接了。recv出錯時返回-1并設置ermo
- send往 sockfd上寫入數據,buf和 len參數分別指定寫緩沖區的位置和大小。send成功 時返回實際寫入的數據的長度,失敗則返回并設置errno
- flags參數為數據收發提供了額外的控制,它可以取表所示選項中的一個或幾個的邏輯或
?發送端代碼
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>#include "include/algorithm_text.h"int main(int argc,char* argv[]) {if (argc <= 2){printf("usage:%s ip_server_address port_number\n", basename(argv[0]));return 1;}const char* ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in server_address{};bzero(&server_address,sizeof (server_address));server_address.sin_family = AF_INET;inet_pton(AF_INET,ip,&server_address.sin_addr);//htons 將一個無符號短整型數值轉換為網絡字節序,即大端模式(big-endian)server_address.sin_port = htons(port);int sock_fd = socket(PF_INET,SOCK_STREAM,0);assert(sock_fd >= 0);if (connect(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address))<0){printf("connected failed.\n");} else{const char* oob_data = "abc";const char* normal_data = "123";send(sock_fd,normal_data, strlen(normal_data),0);send(sock_fd,oob_data, strlen(oob_data),MSG_OOB);send(sock_fd,normal_data, strlen(normal_data),0);}close(sock_fd);return 0;
}
接收端代碼
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>#include "include/algorithm_text.h"
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc <= 2){printf("usage:%s ip_server_address port_number\n", basename(argv[0]));return 1;}const char* ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in server_address{};bzero(&server_address,sizeof (server_address));server_address.sin_family = AF_INET;inet_pton(AF_INET,ip,&server_address.sin_addr);//htons 將一個無符號短整型數值轉換為網絡字節序,即大端模式(big-endian)server_address.sin_port = htons(port);int sock_fd = socket(PF_INET,SOCK_STREAM,0);assert(sock_fd >= 0);int ret = bind(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address));assert(ret != -1);ret = listen(sock_fd,5);assert(ret != -1);struct sockaddr_in client{};socklen_t client_addrlength = sizeof (client);int connfd = accept(sock_fd,(struct sockaddr*)&client,&client_addrlength);if (connfd < 0){printf("errno is %d\n",errno);} else{char buffer[BUF_SIZE];memset(buffer,'\0',BUF_SIZE);ret = recv(connfd,buffer,BUF_SIZE-1,0);printf("got %d bytes of normal data '%s' \n",ret,buffer);memset(buffer,'\0',BUF_SIZE);ret = recv(connfd,buffer,BUF_SIZE-1,MSG_OOB);printf("got %d bytes of oob data '%s' \n",ret,buffer);memset(buffer,'\0',BUF_SIZE);ret = recv(connfd,buffer,BUF_SIZE-1,0);printf("got %d bytes of normal data '%s' \n",ret,buffer);close(connfd);}close(sock_fd);return 0;
}
?5.8.2 UDP數據讀寫
- ?recvfrom讀取sockfd上的數據,buf和 len參數分別指定讀緩沖區的位置和大小。因為 UDP通信沒有連接的概念,所以我們每次讀取數據都需要獲取發送端的socket地址,即參數 src_addr所指的內容,addrlen參數則指定該地址的長度。
- sendto往 sockfd上寫入數據,buf和 len參數分別指定寫緩沖區的位置和大小。dest_addr
參數指定接收端的socket地址,addrlen參數則指定該地址的長度。 - 這兩個系統調用的flags參數以及返回值的含義均與send/recv系統調用的flags參數及返回值相同。
- 值得一提的是,recvGom/sendto系統調用也可以用于面向連接(STREAM)的 socket的
數據讀寫,只需要把最后兩個參數都設置為NULL以忽略發送端/接收端的socket地址(因 為我們已經和對方建立了連接,所以已經知道其socket地址了)。
5 .8 .3 通用數據讀寫函數
- socket編程接口還提供了一對通用的數據讀寫系統調用。它們不僅能用于TCP流數據, 也能用于UDP數據報:
- ?sockfd參數指定被操作的目標socket,msg參數是msghdr結構體類型的指針,msghdr結構體的定義如下所示
- ?msg name成員指向一個socket地址結構變量。它指定通信對方的socket地址。對于面向連接的TCP協議,該成員沒有意義,必須被設置為 NULL 這是因為對數據流socket而言,對方的地址已經知道。msg namelen成員則指定了 msg_name所 指 socket地址的長度。
- msg iov成員是iovec結構體類型的指針,iovec結構體的定義如下:
- 由上可見,iovec結構體封裝了一塊內存的起始位置和長度。
- msg_iovlen指定這樣的 iovec結構對象有多少個。
- 對于recvmsg而言,數據將被讀取并存放在msg_iovlen塊分散 的內存中,這些內存的位置和長度則由msgiov指向的數組指定,這稱為分散讀(scatter read);
- 對于sendmsg而言,msg iovlen塊分散內存中的數據將被一并發送,這稱為集中寫
- msg contro 1和 msg_controllen成員用于輔助數據的傳送。我們不詳細討論它們,僅在第 13章介紹如何使用它們來實現在進程間傳遞文件描述符。
- msg_flags成員無須設定,它會復制recvmsg/sendmsg的flags參數的內容以影響數據讀寫過程。recvmsg還會在調用結束前,將某些更新后的標志設置到msg flags中。
- recvmsg/sendmsg的 flags參數以及返回值的含義均與send/recv的 flags參數及返回值
相同。
5 .9 帶外標記
- 實際應用中,我們通常無法預期帶外數據何時到來。好在Linux內核檢測到TCP緊急標志時,將通知應用程序有帶外數據 需要接收。內核通知應用程序帶外數據到達的兩種常見方式是:I/O復用產生的異常事件和SIGURG信號。但是,即使應用程序得到了有帶外數據需要接收的通知,還需要知道帶外數據在數據流中的具體位置,才能準確接收帶外數據。這一點可通過如下系統調用實現:
- ?sockatmark判斷sockfd是否處于帶外標記,即下一個被讀取到的數據是否是帶外數據。 如果是,sockatmark返回1 , 此時我們就可以利用帶MSGJDOB標志的recv調用來接收帶外
數據。如果不是,則sockatmark返回0。
5 .1 0 地址信息函數
- 在某些情況下,我們想知道一個連接socket的本端socket地址,以及遠端的socket地 址。下面這兩個函數正是用于解決這個問題:
- ?getsockname獲 取 sockfd對應的本端socket地址,并將其存儲于address參數指定的內
存中,該 socket地址的長度則存儲于addressjen參數指向的變量中。如果實際socket地址 的長度大于address所指內存區的大小,那么該socket地址將被截斷。getsockname成功時返
回0 , 失敗返回-1并設置errno - getpeername獲取sockfd對應的遠端socket地址,其參數及返回值的含義與getsockname
的參數及返回值相同
5.11 socket 選項
- 如果說fcntl系統調用是控制文件描述符屬性的通用POSIX方法,那么下面兩個系統調 用則是專門用來讀取和設置socket文件描述符屬性的方法:
- ?sockfd參數指定被操作的目標socket。level參數指定要操作哪個協議的選項(即屬性),
比如IPv4、IPv6、TCP等。option_iiame參數則指定選項的名字。我們在表5-5中列舉了socket通信中幾個比較常用的socket選項。option value和 option len參數分別是被操作選項
的值和長度。不同的選項具有不同類型的值,如表中 “數據類型” 一列所示。
- ?getsockopt和 setsockopt這兩個函數成功時返回0 , 失敗時返回-1并設置ermo
- 值得指出的是,對服務器而言,有部分socket選項只能在調用listen系統調用前針對監
聽 socket設置才有效。這是因為連接socket只能由accept調用返回,而 accept從 listen監
聽隊列中接受的連接至少已經完成了 TCP三次握手的前兩個步驟(因為listen監聽隊列中 的連接至少已進入SYN_RCVD狀態) , 這說明服務器已經往被接受連接上發送出了 TCP同步報文段。但有的socket選項卻應該在TCP同步報文段中設 置,比如TCP最大報文段選項(該選項只能由同步報文段來發送)。對這種情況,Linux給開發人員提供的解決方案是:對監聽socket設置這些socket選項,那么accept返回的連接socket將自動繼承這些選項。這 些 socket選項包括:SO_DEBUG、SO_ DONTROUTE、SO_KEEPALIVE、SO_LINGER、SOJDOBINLINE、SO_RCVBUF> SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT> TCP_MAXSEG 和 TCP_N0DELAYo 而 對客戶端而言,這些socket選項則應該在調用connect函數之前設置,因為connect調用成功返回之后,TCP三次握手已完成。
5.11.1 SO_REUSEADDR 選項
- TCP連接的TIME_WAIT狀態,并提到服務器程序可以通過設置 socket選項 SO_REUSEADDR來強制使用被處于TIME_WAIT狀態的連接占用的socket地址。具體實現方法如代碼所示。
- 重用本地IP地址
int sock = socket(PF_INET,SOCK_STREAM,0);assert(sock >= 0);int reuse = 1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));struct sockaddr_in address{};bzero(&address,sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET,ip,&address.sin_addr);address.sin_port = htons(port);int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));
- 經過setsockopt的設置之后,即使sock處于TIME_WAIT狀態,與之綁定的socket地址 也可以立即被重用。此外,我們也可以通過修改內核參數/proc/sys/net/ipv4/tcp_tw_recycle來快速回收被關閉的socket,從而使得TCP連接根本就不進入TIME_WAIT狀態,進而允許應 用程序立即重用本地的socket地址。
5.11.2 SO_RCVBUF 和 SO_SNDBUF 選項
- SO_RCVBUF和 SO_SNDBUF選項分別表示TCP接收緩沖區和發送緩沖區的大小。不過,當我們用setsockopt來設置TCP的接收緩沖區和發送緩沖區的大小時,系統都會將其值 加倍,并且不得小于某個最小值。TCP接收緩沖區的最小值是256字節,而發送緩沖區的最小值是2048字節(不過,不同的系統可能有不同的默認最小值)。系統這樣做的目的,主要是確保一個TCP連接擁有足夠的空閑緩沖區來處理擁塞(比如快速重傳算法就期望TCP接收緩沖區能至少容納4個大小為SMSS的TCP報文段)。此外,我們可以直接修改內核參數
/proc/sys/net/ipv4/tcp_rmem 和 /proc/sys/net/ipv4/tcp_wmem 來強制 TCP 接收緩沖區和發送緩沖區的大小沒有最小值限制。我們將在第16章討論這兩個內核參數。
修改TCP發送緩沖區的客戶端程序
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>#include "include/algorithm_text.h"
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc <= 2){printf("usage:%s ip_server_address port_number\n", basename(argv[0]));return 1;}const char* ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in server_address{};bzero(&server_address,sizeof (server_address));server_address.sin_family = AF_INET;inet_pton(AF_INET,ip,&server_address.sin_addr);//htons 將一個無符號短整型數值轉換為網絡字節序,即大端模式(big-endian)server_address.sin_port = htons(port);int sock_fd = socket(PF_INET,SOCK_STREAM,0);assert(sock_fd >= 0);int send_buf = atoi(argv[3]);int len = sizeof (send_buf);//先設置TCP發送緩沖區的大小,然后立即讀取數據setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&send_buf,len);getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&send_buf,(socklen_t*)&len);printf("the tcp send buffer size after setting is %d\n", send_buf);if (connect(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address))!=-1){char buffer[BUF_SIZE];memset(buffer, 'a', BUF_SIZE);send(sock_fd, buffer, BUF_SIZE, 0);}close(sock_fd);return 0;
}
?修改TCP接收緩沖區的服務器程序
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>#include "include/algorithm_text.h"
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc <= 2){printf("usage:%s ip_server_address port_number\n", basename(argv[0]));return 1;}const char* ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in server_address{};bzero(&server_address,sizeof (server_address));server_address.sin_family = AF_INET;inet_pton(AF_INET,ip,&server_address.sin_addr);//htons 將一個無符號短整型數值轉換為網絡字節序,即大端模式(big-endian)server_address.sin_port = htons(port);int sock_fd = socket(PF_INET,SOCK_STREAM,0);assert(sock_fd >= 0);int recv_buf = atoi(argv[3]);int len = sizeof (recv_buf);//先設置TCP接收緩沖區的大小,然后立即讀取數據setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&recv_buf,len);getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&recv_buf,(socklen_t*)&len);printf("the tcp receive buffer size after setting is %d\n", recv_buf);int ret = bind(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address));assert(ret != -1);struct sockaddr_in client{};socklen_t client_addrlength = sizeof (client);int connfd = accept(sock_fd,(struct sockaddr*)&client,&client_addrlength);if (connfd < 0){printf("errno is:%d\n",errno);} else{char buffer[BUF_SIZE];memset(buffer,'\0',BUF_SIZE);while (recv(connfd,buffer,BUF_SIZE-1,0)>0){}close(connfd);}close(sock_fd);return 0;
}
- ?從服務器的輸出來看,系統允許的TCP接收緩沖區最小為256字節。當我們設置TCP
接收緩沖區的大小為50字節時,系統將忽略我們的設置。從客戶端的輸出來看,我們設置
的TCP發送緩沖區的大小被系統增加了一倍。這兩種情況和我們前面討論的一致。
5.11.3 SO_RCVLOWAT 和 SO_SNDLOWAT 選項
- SO_RCVLOWAT和 SO_SNDLOWAT選項分別表示TCP接收緩沖區和發送緩沖區的低水位標記。它們一般被I/O復用系統調用(見第9 章)用來判斷socket是否可讀或可寫。當 TCP接收緩沖區中可讀數據的總數大于其低水位標記時,I/O復用系統調用將通知應用程序可以從對應的socket上讀取數據;當TCP發送緩沖區中的空閑空間(可以寫入數據的空間) 大于其低水位標記時,I/O復用系統調用將通知應用程序可以往對應的socke上寫入數據。
- 默認情況下,TCP接收緩沖區的低水位標記和TCP發送緩沖區的低水位標記均為1字節。
5.11.4 SO_LINGER 選項
- SO_LINGER選項用于控制close系統調用在關閉TCP連接時的行為。默認情況下,當 我們使用close系統調用來關閉一個socket時,close將立即返回,TCP模塊負責把該socket 對應的TCP發送緩沖區中殘留的數據發送給對方。
- 如 表 5?5 所 示 ,設 置 (獲 取 )SO_LINGER選 項 的 值 時 , 我 們 需 要 給 setsockopt ( getsockopt) 系統調用傳遞一個linger類型的結構體,其定義如下:
- ?根據linger結構體中兩個成員變量的不同值,close系統調用可能產生如下3 種行為之一:
- l_onoff等于0 ?此時SO_LINGER選項不起作用,close用默認行為來關閉socket□
- l_onoff不為0, l_linger等于0。此時close系統調用立即返回,TCP模塊將丟棄被關
閉的socket對應的TCP發送緩沖區中殘留的數據,同時給對方發送一個復位報文段 (見 3.5.2小節)。因此,這種情況給服務器提供了異常終止一個連接的方法。 - l onoff不 為 0, l_linger大 于 0。此 時 close的行為取決于兩個條件:一是被關閉
的 socket對應的TCP發送緩沖區中是否還有殘留的數據;二是該socket是阻塞 的,還是非阻塞的。對于阻塞的socket, close將等待一段長為Linger的時間,直到TCP模塊發送完所有殘留數據并得到對方的確認。如果這段時間內TCP模塊沒有發送完殘留數據并得到對方的確認,那么close系統調用將返回-1并設置ermo為 EWOULDBLOCK;如果socket是非阻塞的,close將立即返回,此時我們需要根據其 返回值和ermo來判斷殘留數據是否已經發送完畢。關于阻塞和非阻塞,我們將在第 8章討論。
5 .1 2 網絡信息API
- socket地址的兩個要素,即IP地址和端口號,都是用數值表示的。這不便于記憶,也 不便于擴展(比如從IPv4轉移到IPv6)。因此在前面的章節中,我們用主機名來訪問一臺 機器,而避免直接使用其IP地址。同樣,我們用服務名稱來代替端口號。比如,下面兩條
- telnet命令具有完全相同的作用:
- telnet 127.0.0.1 80
- telnet localhost www
- 上面的例子中,telnet客戶端程序是通過調用某些網絡信息API來實現主機名到IP地 址的轉換,以及服務名稱到端口號的轉換的。下面我們將討論網絡信息API中比較重要的幾個
5.12.1 gethostbyname 和 gethostbyaddr
- gethostbyname函數根據主機名稱獲取主機的完整信息,gethostbyaddr函數根據IP地址獲取主機的完整信息。
- gethostbyname函數通常先在本地的/etc/hosts配置文件中查 找主機,如果沒有找到,再去訪問DNS服務器。這些在前面章節中都討論過。這兩個函數的定義如下:
- name參數指定目標主機的主機名,addr參數指定目標主機的IP地址,len參數指定addr 所指IP地址的長度,type參數指定addi?所指IP地址的類型,其合法取值包括AF_INET (用于 IPv4地 址 )和 AF INET6 (用于IPv6地址)。
- 這兩個函數返回的都是hostent結構體類型的指針,hostent結構體的定義如下:
?5.12.2 getservbyname 和 getservbyport
- getservbyname函數根據名稱獲取某個服務的完整信息,getservbyport函數根據端口號獲取某個服務的完整信息。它們實際上都是通過讀取/etc/services文件來獲取服務的信息的。 這兩個函數的定義如下:
- ?name參數指定目標服務的名字,port參數指定目標服務對應的端口號。proto參數指定 服務類型,給它傳遞“tcp” 表示獲取流服務,給它傳遞“udp” 表示獲取數據報服務,給它 傳遞NULL則表示獲取所有類型的服務。
- 這兩個函數返回的都是servent結構體類型的指針,結構體servent的定義如下:
- ?下面我們通過主機名和服務名來訪問目標服務器上的daytime服務,以獲取該機器的系 統時間,如代碼清單5-12所示。
訪問daytime服務
- ntohs等相關的參數的含義
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <netdb.h>#include "include/algorithm_text.h"
#define BUF_SIZE 1024int main(int argc,char* argv[]) {assert(argc == 2);char* host = argv[1];//獲取目標主機地址信息struct hostent* hostinfo = gethostbyname(host);assert(hostinfo);//獲取daytime服務信息struct servent* servinfo = getservbyname("daytime","tcp");assert(servinfo);printf("daytime port is %d\n", ntohs(servinfo->s_port));struct sockaddr_in address{};address.sin_family = AF_INET;address.sin_port = servinfo->s_port;/* 注意下面示碼,因為h_addr_list本身是使用網絡字節序的地址列表,所以使用其中的IP地址時,* 無須對目標IP地址轉換字節序*/address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;int sockfd = socket(AF_INET,SOCK_STREAM,0);int result = connect(sockfd,(struct sockaddr*)&address,sizeof (address));assert(result != -1);char buffer[128];result = read(sockfd,buffer,sizeof (buffer));assert(result > 0);buffer[result] = '\0';printf("the day tiem is: %s",buffer);close(sockfd);return 0;
}
- ?需要指出的是,上面討論的4個函數都是不可重入的,即非線程安全的。不過netdb.h 頭文件給出了它們的可重入版本。正如Linux下所有其他函數的可重入版本的命名規則那樣, 這些函數的函數名是在原函數名尾部加上_r ( re-entrant) 。
5.12.3 getaddrinfo
- getaddrinfo函數既能通過主機名獲得IP 地 址 (內部使用的是gethostbyname函數),也能通過服務名獲得端口號(內部使用的是getservbyname函數)。它是否可重入取決于其內部調用的gethostbyname和 getservbyname函數是否是它們的可重入版本。該函數的定義如下:
- #include <netdb.h>
- hostname參數可以接收主機名,也可以接收字符串表示的IP地 址 (IPv4采用點分十 進制字符串,IPv6則采用十六進制字符串)。同樣,service參數可以接收服務名,也可以 接收字符串表表示的十進制端口號。hints參數是應用程序給getaddrinfo的一個提示,以對getaddrinfo的輸出進行更精確的控制。hints參數可以被設置為N U L L ,表示允許getaddrinfo反饋任何可用的結果。result參數指向一個鏈表,該鏈表用于存儲getaddrinfo反饋的結果。
- getaddrinfo反饋的每一條結果都是addrinfo結構體類型的對象,結構體addrinfo的定義如下:
?
- ?該結構體中,ai_protocol成員是指具體的網絡協議,其含義和socket系統調用的第三個 參數相同,它通常被設置為0。ai_flags成員可以取表5-6中的標志的按位或。
- ?當我們使用hints參數的時候 ,可以設置其ai_flags, ai_family, ai_socktype和 ai_protocol四個字段,其他字段則必須被設置為NULL。例如,代碼清單 13利用了 hints參數獲取主機emest-laptop 上的 "daytime” 流服務信息
?5.12.4 getnameinfo
- getnameinfo函數能通過socket地址同時獲得以字符串表示的主機名(內部使用的是 gethostbyaddr函數)和 服 務 名 (內部使用的是getservbyport函數)。它是否可重入取決于 其內部調用的gethostbyaddr和 getservbyport函數是否是它們的可重入版本。該函數的定義 如下:
- ?getnameinfo將返回的主機名存儲在host參數指向的緩存中,將服務名存儲在serv 參數指向的緩存中,hostlen和 servlen參數分別指定這兩塊緩存的長度。Hags參數控制 getnameinfo的行為,它可以接收表5-7中的選項。
- ?getaddrinfo和 getnameinfo函數成功時返回0 , 失敗則返回錯誤碼,可能的錯誤碼如表 所示
- ?Linux下 strerror函數能將數值錯誤碼ermo,轉換成易讀的字符串形式。同樣,下面的函 數可將表5-8中的錯誤碼轉換成其字符串形式:
???????
參考鏈接
- socket編程中用到的頭文件
- ntohs, ntohl, htons,htonl的比較和詳解
- 詳解C語言的htons函數
- C++ memset()函數和bzero()函數