在談到socket編程之前,首先我們要知道一點預備知識。
預備知識:
1、網路字節序全部採用大端字節序。
關於字節序的詳解,戳鏈接 查看,這裏不做解釋。
2、在編程之前,我們有必要了解,什麼是socket?
socket,又叫做套接字。我們都應該知道,在網絡中,IP地址+ 端口號,可以唯一表示互聯網中的一個進程,因此,我們將 IP地址+端口號 稱爲socket。
socket API是一套抽象的網絡編程接口,適用於各種底層網絡協議,包括IPv4,IPv6以及UNIX Domain Socket等,但各種網絡協議地址格式並不相同,舉兩個的例子,IPv4和Unix Domain Socket,如圖:
現在網絡協議中最常用的依舊是IPv4,本文以IPv4爲重點進行介紹。
可以發現,IPv4的地址結構大小爲16字節,末尾填充了8字節的其他內容,這個我們不關心,需要我們注意的是上面三段。
a、前16位表示的是地址類型,可以注意到,其他類型的地址結構也有,這是用來區分不同協議類型的部分;
b、16位端口號,指明協議使用的端口;
c、32位IP地址,指明通信時使用的IP地址。
對於不同的協議,地址結構很明顯是不同的,舉個例子,IPv6的IP地址長度和Ipv6的IP地址長度很明顯不同。互聯網中有衆多的協議,難道要針對每種協議提供一套接口?
當然,Linux不會這麼幹,Linux提供了一套抽象出來的標準接口,叫做struct sockaddr。對於不同的網絡協議的地址格式,有一個共同點,就是前16位用來表示地址類型【注1】。那麼對待不同的網絡協議,可以各自定義自己的地址類型,在使用的過程中,只需要強制類型轉化爲標準格式即可。區分不同的地址協議,僅僅需要struct sockaddr的前16位足以。
基於POSIX規範,對於IPv4協議,我們只需要關注整個地址結構中的3個字段sin_family、sin_addr、sin_port(分別表示地址類型,IP地址,port端口號,具體信息後面說)。
【注1】:IPv4的地址類型爲 AF_INET;IPv6的地址類型爲AF_INET6;UNIX Domain Socket的地址類型爲AF_UNIX。
socket通信:
在TCP協議中,當我們使用socket通信時,通信雙方(連接的兩個進程)都需要有一套自己的socket來標識,那麼這兩個socket組成的socket pair就唯一標識了這一組連接。
我們建立的通信是在應用層之上的,TCP/IP協議設計的應用層編程接口又叫做socket API ,本文的重點是如何利用這些API來實現兩個進程之間通過網絡通信。
首先了解一下關於socket通信的整個流程。如下圖。
對上圖首先有一個大致印象即可,下面我們來看TCP協議提供的這一套API。
首先討論server端:
1、創建 socket
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); # creates an endpoint for communication and returns a descriptor # domain:地址類型,上面解釋過 # type:流式套接,SOCK_STREAM代表TCP,SOCK_DGRAM代表UDP # 一般設置爲0 # 返回值爲文件描述符,默認從3開始。失敗返回-1
2、綁定套接字 bind
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); # 參數1 --> 創建socket時獲得的文件描述符 # 參數2 --> 指向特定協議的地址結構的指針 # 參數3 --> 該地址結構的長度 sockaddr_in的長度,即網絡地址的長度 # 成功返回0, 失敗返回-1
在struct sockaddr 結構中,可以指定IP或者端口號(下面說具體如何做),當然可以任意指定其中一個,或者都不指定。
端口:如果 server 沒有使用bind綁定端口號的話,內核會爲該 socket 選擇一個臨時端口,這個端口的選擇往往是隨機的。但是對於TCP服務器而言,它的端口應該是被衆所周知(well_known)的。如果端口都是隨機的話,那麼 client 就不能保證可以正常訪問到 server 。當然對於client而言是,讓內核來選擇端口是一件很正常的事。
IP地址:對於服務器而言,如果綁定了IP地址,這就限定了該socket只接收目的地爲這個IP地址的client連接;如果TCP服務器沒有將IP地址捆綁到該 socket 上,內核就把客戶發送的SYN(TCP/IP建立連接時的握手信號)作爲服務器的源IP地址。
當我們希望內核自動分配一個端口地址的話,必須注意的是,bind函數本身並不返回所選擇的端口號,爲了得到內核選擇的端口號,必須調用 getsockname 函數。
接下來是IPv4的地址空間結構,
struct sockaddr_in { sa_family_t sin_family; __be16 sin_port; struct in_addr sin_addr; unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct i n_addr)]; }; # 參數1 --> 地址類型 # 參數2 --> 端口號 # 參數3 --> IP地址,結構體定義如下 # 參數4 --> 填充位 PAD ,不需要時不用關心該成員 struct in_addr { __be32 s_addr; };
如果想由內核選擇端口,則sin_port值爲0;
如果想由內核選擇IP,則sin_addr.s_addr = htonl(INADDR_ANY);
這裏的 IP地址 和 端口號由於存在大小端的問題,因此需要進行轉換,轉換函數如下:
端口轉換函數
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
IP 地址轉換函數
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char *cp);
3、監聽 listen
監聽函數僅由TCP服務器來調用,主要做兩件事:
a、調用 listen 函數,使得套接字從 CLOSED 轉換到 LISTEN狀態,將一個未連接的主動套接字轉換爲被動套接字,指示內核應該接受指向該套接字的連接請求;
b、該函數的第二個參數指定了內核應該爲相應套接字排隊的最大連接個數。
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); # 參數1 --> 創建socket時獲得的文件描述符 # 參數2 --> 一般設置爲5 # 成返回0, 失敗返回-1
該函數通常在調用socket()、bind()函數之後,accept()函數之前調用。
理解backlog參數:
內核在爲任何一個給定的監聽套接字維護兩個隊列:
a、未完成連接隊列。由client發起連接並且已經完成第一次握手,但尚未完成三次握手,這些套接字處於SYN_RCVD狀態;
b、已完成連接隊列。已經完成三次握手的客戶端對應一項,這些套接字呼籲ESTABLISHED狀態。
注意:永遠不要將backlog設置爲0,即使不想任何客戶連接到你的監聽套接字上。
4、 accept
該函數有TCP服務器調用,用於從已完成的連接隊列頭返回下一個已完成連接。如果已完成連接列隊爲空,那麼進程進入睡眠狀態。套接字默認爲阻塞狀態。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); # 參數1 --> 監聽套接字;創建socket時獲得的文件描述符 # 參數2、3爲輸出型參數,用來獲取連接client端的協議地址 # socket_t len = sizeof(struct socketaddr_in);一變量,兩應用,既做輸入,又做輸出 # 成功返回一個全新的文件描述符,我們把它叫做連接套接字。失敗後-1。返回值是真正實現數據通信的套接字
需要注意的是,server通過socket建立的自己的套接字,這個套接字是well_known的,只是用來建立連接的,並不是真正數據傳輸使用的。真正的數據傳輸是建立在client的套接字上的,server通過accept的返回值獲取client的套接字。
由於套接字也是文件,讀寫可以使用read 和 write 函數,使用完畢,需要close 文件。
這就需要重提一個概念, Linux一切皆文件,只不過是相同的接口,不同的底層實現。
client端:
1、socket
用法同server
2、連接 connect
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); # client 用來發起連接請求(三次握手) # 參數1 --> client的套接字描述符 # 參數2 --> 指向套接字地址結構的指針,需要手動來初始化 # 參數3 --> 地址結構的大小
關於server 和client的讀寫操作,可以直接使用write和read 函數,這裏不再細說,接下來給出測試用例,
//server.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> void Usage(char *msg) { printf("invalid Input!\n"); printf("Usage: %s [ip] [port]\n",msg); } int create_socket(char *port, char *addr) { // 1.create an endpoint for communication int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0) { perror("socket error"); exit(1); } // struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(atoi(port)); local.sin_addr.s_addr = inet_addr(addr); if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0) { perror("bing error"); close(sock); exit(2); } if(listen(sock, 5) < 0) { perror("listen error"); close(sock); exit(3); } return sock; } int main(int argc, char* argv[]) { if(argc != 3) { Usage(argv[0]); return 1; } // create socket int listen_sock = create_socket(argv[2], argv[1]); struct sockaddr_in client; socklen_t len = sizeof(struct sockaddr); while(1){ char buf[1024]; memset(buf, 0, sizeof(buf)); int ret = 0; if((ret = accept(listen_sock, (struct sockaddr*)&client, &len)) < 0) { perror("accept error"); continue; } while(1) { ssize_t _s = read(ret, buf, sizeof(buf)-1); printf("*************************\n"); if(_s > 0) { printf("client# "); fflush(stdout); buf[_s-1] = 0; printf("%s\n", buf); } else if (_s == 0) { printf("client quit!\n"); break; } } } return 0; } /***************************************************************************/ // client.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { // check Input if(argc != 3) { printf("Invalid Input!\n"); printf("Usage# %s [ip] [port]\n"); return 1; } // create socket int client_sock = socket(AF_INET, SOCK_STREAM, 0); if(client_sock < 0) { perror("socket error"); exit(1); } // connect struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); int ret = connect(client_sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)); if(ret < 0) { perror("connect error"); printf("*****************8"); close(client_sock); return 4; } printf("connect success!\n"); // send while(1) { printf("Send# "); fflush(stdout); char buf[1024]; memset(buf, 0, sizeof(buf)); ssize_t _s = read(0, buf, sizeof(buf)); printf("*************************\n"); if(_s < 0) { perror("read error"); close(client_sock); return 5; } write(client_sock ,buf, _s); memset(buf, 0, sizeof(buf)); } return 0; }
在不同的終端下,分別運行server和client,IP地址選擇server端的IP,端口號自定義,然後可以看到下面的打印結果:
這裏可以實現的網絡通信,是建立在局域網範圍內的,當然如果找不下兩臺主機的話,可以使用127.0.0.1的IP地址,做本地環回測試,也就是說server和client的端口都放在了一臺主機上。這裏自定義的端口號,最好選用大於1024的端口,防止造成端口衝突。
上面的代碼只是用來測試使用的,真正服務器上寫出這樣的代碼是要出大麻煩的,之後,我們會談到一些特殊的情況,關於上面的測試代碼,這裏給出鏈接,可以自己下載使用:
https://github.com/muhuizz/Linux/tree/master/Linux%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/code/socket-1
-----muhuizz整理