1. 程式人生 > >《Linux高性能服務器編程》學習總結(五)——Linux網絡編程基礎API

《Linux高性能服務器編程》學習總結(五)——Linux網絡編程基礎API

讀數 p s file src prot 非線程安全 設立 無法 set

第五章 Linux網絡編程基礎API

  對於網絡編程,首先要了解的就是字節序的問題,字節序分為主機字節序和網絡字節序,主機字節序又稱小端字節序,是低字節存放在地地址,而網絡字節序又稱大端字節序,是低字節放在高地址。當數據在不同的機器上傳播時,就需要統一字節順序以保證不出現錯誤。在發送數據前,先將需要轉變的數據轉成網絡字節序再發送,接收時先轉成主機字節序再處理,要特別註意的是,即使是本機的兩個進程通信,也要考慮字節序的問題,比如JAVA的虛擬機就使用大端字節序。使用如下代碼可以查看本機的字節順序:

 1 /*************************************************************************
2 > File Name: 5-1.cpp 3 > Author: Torrance_ZHANG 4 > Mail: [email protected] 5 > Created Time: Thu 01 Feb 2018 12:28:00 AM PST 6 ************************************************************************/ 7 8 #include<iostream> 9 #include<stdio.h> 10 using namespace
std; 11 12 void byteorder() { 13 union { 14 short value; 15 char union_bytes[sizeof(short)]; 16 }test; 17 test.value = 0x0102; 18 if((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)) { 19 printf("big endian\n"); 20 } 21 else if((test.union_bytes[1
] == 1) && (test.union_bytes[0] == 2)) { 22 printf("little endian\n"); 23 } 24 else printf("unknown...\n"); 25 } 26 27 int main() { 28 byteorder(); 29 }

  在socket網絡編程接口中,用來表示socket地址的是結構體sockaddr,但由於其無法容納所有協議族的信息,所以又有了sockaddr_in、sockaddr_in6和sockaddr_un等專用socket地址結構,在編程使用中只需要將對應的地址結構填好再強轉成sockaddr類型即可,這樣做的好處就是可以簡化socket接口,只需要設立一個通用接口即可提供給不同協議族使用。

  我們平常使用的IP地址是點分十進制形式的字符串,但是在網絡連接中我們需要將其轉換成對應的unsigned int類型的數才能使用,所以,API中為我們提供了幾個函數:

1 #include<arpa/inet.h>
2 in_addr_t inet_addr(const char* strptr);
3 int inet_aton(const char* cp, struct in_addr* inp);
4 char* inet_ntoa(struct in_addr in);
5 inti net_pton(int af, const char* src, void* dst);
6 const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

  其中前三個只適用於ipv4,而後兩個適用於ipv4和ipv6。值得註意的是,inet_ntoa函數是不可重入的,其內部使用了一個靜態變量來存儲結果,函數返回的是靜態內存。所以多次調用這個函數返回的是同一塊內存,其多次的值都為最後一次的結果。

  socket的本質就是一個文件描述符,下面我們總結一下常用的socket函數:

 1 #include<sys/types.h>
 2 #include<sys/socket.h>
 3 int socket(int domain, int type, int protocol);
 4 int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
 5 int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
 6 int listen(int sockfd, int backlog);
 7 int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
 8 int close(int fd);
 9 int shutdown(int sockfd, int howto);
10 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
11 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
12 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
13 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

  前三個函數較為簡單,不再贅述。對於監聽函數listen,第二個參數是backlog,表示內核監聽隊列的最大長度,如果數量超過了這個值,則服務器將不受理新的客戶連接,下面我們用一個實驗來測試一下這個最大長度和backlog有什麽關系:

 1 /*************************************************************************
 2     > File Name: 5-3.cpp
 3     > Author: Torrance_ZHANG
 4     > Mail: [email protected]
 5     > Created Time: Thu 01 Feb 2018 02:06:39 AM PST
 6  ************************************************************************/
 7 
 8 #include<iostream>
 9 #include<stdio.h>
10 #include<sys/socket.h>
11 #include<netinet/in.h>
12 #include<arpa/inet.h>
13 #include<signal.h>
14 #include<unistd.h>
15 #include<stdlib.h>
16 #include<assert.h>
17 #include<string.h>
18 using namespace std;
19 
20 static bool stop = false;
21 static void handle_term(int sig) {
22     stop = true;
23 }
24 
25 int main(int argc, char **argv) {
26     signal(SIGTERM, handle_term);
27     if(argc <= 3) {
28         printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
29         return 1;
30     }
31 
32     const char* ip = argv[1];
33     int port = atoi(argv[2]);
34     int backlog = atoi(argv[3]);
35 
36     int sock = socket(AF_INET, SOCK_STREAM, 0);
37     assert(sock >= 0);
38 
39     struct sockaddr_in address;
40     bzero(&address, sizeof(address));
41     address.sin_family = AF_INET;
42     address.sin_port = htons(port);
43     inet_pton(AF_INET, ip, &address.sin_addr);
44 
45     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
46     assert(ret != -1);
47 
48     ret = listen(sock, backlog);
49     assert(ret != -1);
50 
51     while(!stop) {
52         sleep(1);
53     }
54     close(sock);
55     return 0;
56 }

  運行服務器,監聽12345端口並設定最大監聽隊列為5。用telnet模擬客戶端連接,發現當telnet運行到第7個時就連接不上,隔一段時間後返回連接超時,而此時使用netstat -nt | grep 12345查看,發現有6個已經建立的連接,第7個處於SYN_SENT狀態。

技術分享圖片

技術分享圖片

  綜上,我們設定的backlog值加1就是監聽隊列最大能監聽的數量。

  對於接受連接的accept函數,它從監聽隊列中取出一個連接,與其建立連接,而不管其處於ESTABLISHED或CLOSE_WAIT狀態,更不關心任何網絡變化。

  當連接結束時,我們調用close將其關閉,但是close並不總是關閉連接,而是將這個文件描述符的引用計數減1,只有當這個文件描述符的引用計數為0時才會真正關閉。在多進程程序中,一次fork就會使得父進程中打開的文件描述符引用計數加1,所以這種情況下我們就應該對父子進程中的文件描述符都執行一次close。如果要立即終止連接,就可以使用下面的shutdown函數,參數howto的取值分別為SHUT_RD、SHUT_WR和SHUT_RDWR。

  接下來的函數是重頭戲,分別是TCP和UDP的發送和接收數據函數,先看TCP的,最後一個參數flags提供了一些額外的控制,一般情況下取0即可,或者是若幹個宏的邏輯或,常用的有MSG_OOB用來發送和接收緊急數據等。我們來通過一個服務器和客戶端的例子來說明如何發送帶外數據。

 1 /*************************************************************************
 2     > File Name: 5-6.cpp
 3     > Author: Torrance_ZHANG
 4     > Mail: [email protected]
 5     > Created Time: Thu 01 Feb 2018 04:37:10 AM PST
 6  ************************************************************************/
 7 
 8 #include"head.h"
 9 using namespace std;
10 
11 int main(int argc, char **argv) {
12     if(argc <= 2) {
13         printf("usage: %s ip_address port_number\n", basename(argv[0]));
14         return 1;
15     }
16     const char* ip = argv[1];
17     int port = atoi(argv[2]);
18 
19     struct sockaddr_in server_address;
20     bzero(&server_address, sizeof(server_address));
21     server_address.sin_family = AF_INET;
22     inet_pton(AF_INET, ip, &server_address.sin_addr);
23     server_address.sin_port = htons(port);
24 
25     int sockfd = socket(AF_INET, SOCK_STREAM, 0);
26     assert(sockfd >= 0);
27     if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
28         printf("connection failed\n");
29     }
30     else {
31         const char* oob_data = "abc";
32         const char* normal_data = "123";
33         send(sockfd, normal_data, strlen(normal_data), 0);
34         send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
35         send(sockfd, normal_data, strlen(normal_data), 0);
36     }
37     close(sockfd);
38     return 0;
39 }
 1 服務器端:
 2 /*************************************************************************
 3     > File Name: 5-7.cpp
 4     > Author: Torrance_ZHANG
 5     > Mail: [email protected]
 6     > Created Time: Thu 01 Feb 2018 04:44:00 AM PST
 7  ************************************************************************/
 8 
 9 #include"head.h"
10 using namespace std;
11 
12 #define BUF_SIZE 1024
13 
14 int main(int argc, char **argv) {
15     if(argc <= 2) {
16         printf("usage: %s ip_address port_number\n", basename(argv[0]));
17         return 1;
18     }
19     const char* ip = argv[1];
20     int port = atoi(argv[2]);
21 
22     struct sockaddr_in address;
23     bzero(&address, sizeof(address));
24     address.sin_family = AF_INET;
25     inet_pton(AF_INET, ip, &address.sin_addr);
26     address.sin_port = htons(port);
27 
28     int sock = socket(AF_INET, SOCK_STREAM, 0);
29     assert(sock >= 0);
30 
31     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
32     assert(ret != -1);
33 
34     ret = listen(sock, 5);
35     assert(ret != -1);
36 
37     struct sockaddr_in client;
38     socklen_t client_addrlength = sizeof(int);
39     int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
40     if(connfd < 0) {
41         printf("errno is: %d\n", errno);
42     }
43     else {
44         char buffer[BUF_SIZE];
45 
46         memset(buffer, 0, sizeof(buffer));
47         ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
48         printf("got %d bytes of normal data ‘%s‘\n", ret, buffer);
49 
50         memset(buffer, 0, sizeof(buffer));
51         ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);
52         printf("got %d bytes of oob data ‘%s‘\n", ret, buffer);
53 
54         memset(buffer, 0, sizeof(buffer));
55         ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
56         printf("got %d bytes of normal data ‘%s‘\n", ret, buffer);
57 
58         close(connfd);
59     }
60     close(sock);
61     return 0;
62 }

技術分享圖片

  運行結果如圖,從這個實驗中可以看出三點:首先,帶外數據即緊急數據只能有1字節,因為緊急指針指向的位置是帶外數據的下一個字節,而TCP首部並沒有緊急數據長度的字段,所以只用緊急數據的最後一個字節作為了帶外數據,其余還是普通數據;其次,我們看到第一次和第二次調用了send,但是接收的時候是一同接受了前兩次中的普通數據,這就很好地說明了TCP是基於流的協議;最後,我們發現當整個數據報中存在緊急數據的時候,其余普通數據就會被緊急數據分隔開,不能一同讀取。

  UDP的發送和接收函數與TCP的類似,區別在於多了兩個參數用來表示對端的socket地址結構。而這兩個函數也可用於面向連接時候,只需要把後兩個參數設定為NULL即可。

  API中還定義了兩個不太常用的通用數據讀寫系統調用recvmsg和sendmsg,還有sockatmark函數用來判斷sockfd是否處於帶外標記狀態,用getsockname和getpeername函數獲取本端和對端的socket地址結構,使用較為簡單。

  socket選項信息是用來對socket的文件屬性進行讀取和設置的,其兩個函數原型為:

1 #include<sys/socket.h>
2 int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t* restrict option_len);
3 int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t* option_len);

  在參數中,level指定了要操作哪個協議的屬性,option_name則指定選項的名字,這兩項都有固定的搭配,接下來的option_value和option_len是值的大小和長度。值得註意的是,有些選項需要在TCP連接建立之前就設置好,即在調用listen和connect函數之前就設置,這是因為某些選項是TCP連接時需要互相協商的選項,而調用這兩個函數時表示已經開始連接或完成連接。

  選項種類有很多,常用的有:SO_REUSEADDR選項用來重用TCP端口;SO_RCVBUF和SO_SNDBUF選項用來設置接收及發送緩沖區大小,SO_RCVLOWAT和SO_SNDLOWAT設置緩沖區的低水位標記,意思是如果緩沖區的可讀數據或可寫空間大於低水位標記系統才通知應用程序從緩沖區讀出數據或寫入數據,一般默認低水位標記為1字節;SO_LINGER選項用來控制close在關閉TCP時的行為。

  socket也提供了幾個網絡信息API,分別用來根據主機名和ip地址獲取主機信息,根據名稱或端口獲取服務信息,其函數原型如下:

1 #include<netdb.h>
2 struct hostent* gethostbyname(const char* name);
3 struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
4 struct servent* getservbyname(const char* name, const char* proto);
5 struct servent* getservbyport(int port, const char* proto);

  需要指出的是,以上四個函數都是不可重入的,即非線程安全的,不過netdb.h頭文件也給出了他們的可重入版本,就是在函數名後加上_r。

《Linux高性能服務器編程》學習總結(五)——Linux網絡編程基礎API