對於網絡編程,首先要了解的就是字節序的問題,字節序分爲主機字節序和網絡字節序,主機字節序又稱小端字節序,是低字節存放在地地址,而網絡字節序又稱大端字節序,是低字節放在高地址。當數據在不一樣的機器上傳播時,就須要統一字節順序以保證不出現錯誤。在發送數據前,先將須要轉變的數據轉成網絡字節序再發送,接收時先轉成主機字節序再處理,要特別注意的是,即便是本機的兩個進程通訊,也要考慮字節序的問題,好比JAVA的虛擬機就使用大端字節序。使用以下代碼能夠查看本機的字節順序:ios
1 /************************************************************************* 2 > File Name: 5-1.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 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有什麼關係:dom
1 /************************************************************************* 2 > File Name: 5-3.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 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狀態。socket
綜上,咱們設定的backlog值加1就是監聽隊列最大能監聽的數量。函數
對於接受鏈接的accept函數,它從監聽隊列中取出一個鏈接,與其創建鏈接,而無論其處於ESTABLISHED或CLOSE_WAIT狀態,更不關心任何網絡變化。測試
當鏈接結束時,咱們調用close將其關閉,可是close並不老是關閉鏈接,而是將這個文件描述符的引用計數減1,只有當這個文件描述符的引用計數爲0時纔會真正關閉。在多進程程序中,一次fork就會使得父進程中打開的文件描述符引用計數加1,因此這種狀況下咱們就應該對父子進程中的文件描述符都執行一次close。若是要當即終止鏈接,就可使用下面的shutdown函數,參數howto的取值分別爲SHUT_RD、SHUT_WR和SHUT_RDWR。spa
接下來的函數是重頭戲,分別是TCP和UDP的發送和接收數據函數,先看TCP的,最後一個參數flags提供了一些額外的控制,通常狀況下取0便可,或者是若干個宏的邏輯或,經常使用的有MSG_OOB用來發送和接收緊急數據等。咱們來經過一個服務器和客戶端的例子來講明如何發送帶外數據。
1 /************************************************************************* 2 > File Name: 5-6.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 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: 597156711@qq.com 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。