我們已經知道如何使用I/O與文件通信,還知道了如何讓同一計算機上的兩個進程進行通信,這篇文章將創建具有服務器和客戶端功能的程序
互聯網中大部分的底層網絡代碼都是用C語言寫的。 網絡程序通常有兩部分組成:服務器和客戶端。
工具介紹: telnet
為了測試功能,我們使用一個叫做telnet的客戶端程序連接服務器,telnet 接受兩個參數:一個是服務器地址,另一個是服務器運行的端口號,
如果在運行服務器的那台計算機上運行telnet,地址可填寫127.0.0.1
這樣使用:假設端口號是30000
telnet 127.0.0.1 30000
我們先說服務器這一端的:
服務器連接網絡分為四部曲:①綁定(Bind) ②監聽(Listen) ③接受(Accept) ④開始(Begin)
把每個首字母連起來就是BLAM
如果想寫一個與網絡通信的程序,就需要一種新的數據流---套接字
#include <sys/socket.h>
int listener_d = socket(PF_INET, SOCK_STREAM, 0); if (listener_d == -1) { error("不能打開套接字"); }
其中 listener_d 是套接字描述符 / 0 是協議號,一般填0就行
1.綁定(Bind)
計算機可能同時運行多個服務器程序,一個發送網頁,一個發送郵件,另一個運行聊天服務器。為了防止不同對話發生混淆,每項服務必須使用不同的端口(port)。
端口就好比電視頻道,我們在不同的端口使用不同的網絡服務,就像我們在不同頻道收看不同的電視節目。
#include <arpa/inet.h>
// 綁定端口 struct sockaddr_in name; name.sin_family = PF_INET; name.sin_port = (in_port_t)htons(30000); name.sin_addr.s_addr = htonl(INADDR_ANY); int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name)); if (c == -1) { error("無法綁定端口"); }
2.監聽(Listen)
通常會有很多客戶端連接到服務器,如果我們想要客戶端排隊等待連接,就要使用listen()來告訴操作系統你希望隊列有多長。
// 監聽 if (listen(listener_d, 10) == -1) { error("無法監聽"); }
調用listen()把隊列長度設為10,也就是說最多可以有10個客戶端可以嘗試連接服務器,他們並不會立刻得到相應,但是可以排隊等待,而第11個客戶端會被告知服務器太忙了。
3.接受連接(Accept)
對於服務器端來說,當我們已經綁定完了端口,設置了監聽隊列,唯一可做的就是等待了。服務器一生都在等待客戶端來連接他們,accept()調用會一直等待,知道有客戶端鏈接服務器時,他會返回第二個套接字描述符,然后就可以通信了。
// 接受鏈接 struct sockaddr_storage client_addr; // 保存鏈接客戶端的相信信息 unsigned int address_size = sizeof(client_addr); int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); if (connect_d == -1) { error("無法打開副套接字"); }
套接字並不是傳統意義上的數據流
我們知道的數據流有:文件,標准輸入,標准輸出。都可以使用fprintf和fscanf函數和他們通信,這倆個函數都是單向的,但套接字不同,套接字是雙向的,既可以用作輸出,也可以用作輸入,因此需要別的函數。
輸出:send() 輸入:recv()
我們先介紹send函數
char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>"; if (send(connect_d, msg, strlen(msg), 0) == -1) { error("send"); }
好了,讓我們先用一個例子來演示一下上邊的功能怎么用,先看代碼:
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <arpa/inet.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <stdlib.h> 7 #include <unistd.h> 8 9 void error(char *msg) { 10 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 11 exit(1); 12 } 13 14 15 int main(int argc, const char * argv[]) { 16 17 18 char *advice[] = { 19 "你為什么這么帥!\r\n", 20 "有沒有人誇過你帥?", 21 "傻逼牛頭,笨鱉", 22 "牛,你是第六人嗎?", 23 "拔插座了吧"}; 24 25 26 // 打開 27 int listener_d = socket(PF_INET, SOCK_STREAM, 0); 28 if (listener_d == -1) { 29 error("不能打開套接字"); 30 } 31 32 // int reuse = 1; 33 // if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) { 34 // error("無法設置套接字的“重新使用端口”選項"); 35 // } 36 // 綁定端口 37 struct sockaddr_in name; 38 name.sin_family = PF_INET; 39 name.sin_port = (in_port_t)htons(30000); 40 name.sin_addr.s_addr = htonl(INADDR_ANY); 41 int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name)); 42 if (c == -1) { 43 error("無法綁定端口"); 44 } 45 46 // 監聽 47 if (listen(listener_d, 10) == -1) { 48 error("無法監聽"); 49 } 50 51 puts("等待鏈接..."); 52 53 while (1) { 54 // 接受鏈接 55 struct sockaddr_storage client_addr; // 保存鏈接客戶端的相信信息 56 unsigned int address_size = sizeof(client_addr); 57 int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size); 58 if (connect_d == -1) { 59 error("無法打開副套接字"); 60 } 61 62 // 通信 63 // char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>"; 64 char *msg = advice[rand() % 5]; 65 if (send(connect_d, msg, strlen(msg), 0) == -1) { 66 error("send"); 67 } 68 69 if (close(connect_d) == -1) { 70 error("無法關閉鏈接"); 71 } 72 73 } 74 75 76 return 0; 77 }
// Mac 下編譯運行
gcc socket.c -o socket ./socket
終端顯示成這樣
我們打開另一個終端來模擬客戶端
太棒了,服務器和客戶端能夠連接且服務器能夠給客戶端發送數據了,但是這樣的程序還是有問題的,當我們快速使用Ctrl-C結束服務器的程序,在用./socket打開機會出現這樣的錯誤
為什么會出現這個錯誤呢?因為綁定端口是有延時的。
當你在某個端口綁定了一個程序,系統不允許在30秒內再綁定其他的程序,也包括上一次綁定這個端口的程序。只要在綁定前設置套接字的某個選項就能解決這個問題
把上邊的代碼注釋的地方打開
int reuse = 1; if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) { error("無法設置套接字的“重新使用端口”選項"); }
重復之前的的操作,Ctrl-C ./socket 就沒這問題了。
然而,在現實世界中,我們不僅需要給客戶端發消息,我們還要能在客戶端讀消息。
答案就是recv()函數。
需要注意下邊幾點:
1.接受到的字符串並不是以'\0'結尾的
2.當用戶在telnet輸入文本並按了回車后,接受到的字符串是以'\r\n'結尾的
3.recv() 返回字符串的個數,如果發生錯誤就返回-1,如果客戶端關閉了鏈接就返回0
4.recv()調用不一定能一次性收到所有的字符串,可能分幾次返回也就是多次調用recv()
由於上邊4所造成的需要調用多次的情況,因此recv()使用起來還是很繁瑣的,最好能封裝到一個方法中;
1 // 從客戶端讀取數據 2 int read_in(int socket, char *buf, int len) { 3 char *s = buf; 4 int slen = len; 5 int c = (int)recv(socket, s, slen, 0); 6 while ((c > 0) && (s[c-1] != '\n')) { 7 s += c; 8 slen -= c; 9 c = (int)recv(socket, s, slen, 0); 10 } 11 12 if (c < 0) { 13 return c; 14 }else if (c == 0) { 15 buf[0] = '\0'; 16 }else { 17 s[c - 1] = '\0'; 18 } 19 20 return len - slen; 21 }
下邊我們就寫一個服務器和客戶端能夠交互的程序,這個程序其實跟HTTP協議的原理很像,都是在雙方必須遵守某項定好的協議前提下進行通信的。我們把上面講的通信前的准備都封裝成了單獨的函數,比如
// 錯誤處理函數 void error(char *msg) // 開啟socket int open_listener_socket() // 綁定端口 void bind_to_port(int socket, int port) // 向客戶端發消息 int say(int socket, char *s) // 處理服務中斷 void handle_shutdown(int sig) // 監聽信號 int catch_signal(int sig, void (*handler)(int)) // 從客戶端讀取數據 int read_in(int socket, char *buf, int len)
代碼如下
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <arpa/inet.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <stdlib.h> 7 #include <unistd.h> 8 #include <signal.h> 9 10 int listener_d; 11 12 // 錯誤處理函數 13 void error(char *msg) { 14 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 15 exit(1); 16 } 17 18 // 開啟socket 19 int open_listener_socket() { 20 int s = socket(PF_INET, SOCK_STREAM, 0); 21 if (s == -1) { 22 error("Can't open socket"); 23 } 24 return s; 25 } 26 27 // 綁定端口 28 void bind_to_port(int socket, int port) { 29 struct sockaddr_in name; 30 name.sin_family = PF_INET; 31 name.sin_port = (in_port_t)htons(port); 32 name.sin_addr.s_addr = htonl(INADDR_ANY); 33 int reuse = 1; 34 if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) { 35 error("Can't set the reuse option on the socket"); 36 } 37 int c = bind(socket, (struct sockaddr*)&name, sizeof(name)); 38 if (c == -1) { 39 error("Can't bind to socket"); 40 } 41 } 42 43 // 向客戶端發消息 44 int say(int socket, char *s) { 45 int result = (int)send(socket, s, strlen(s), 0); 46 if (result == -1) { 47 fprintf(stderr, "%s: %s \n","和客戶端通信發生錯誤",strerror(errno)); 48 } 49 return result; 50 } 51 52 // 處理服務中斷 53 void handle_shutdown(int sig) { 54 if (listener_d) { 55 close(listener_d); 56 } 57 fprintf(stderr, "Bye! \n"); 58 exit(0); 59 } 60 61 // 監聽信號 62 int catch_signal(int sig, void (*handler)(int)) { 63 // 創建一個新動作 64 struct sigaction action; 65 // 想讓計算機調用哪個函數,這個被包裝的my_custom_fun函數就叫做處理器 66 action.sa_handler = handler; 67 // 使用掩碼過濾信號,通常會用一個空的掩碼 68 sigemptyset(&action.sa_mask); 69 // 一些附加的標志位,置為0就行了 70 action.sa_flags = 0; 71 72 return sigaction(sig, &action, NULL); 73 } 74 75 // 從客戶端讀取數據 76 int read_in(int socket, char *buf, int len) { 77 char *s = buf; 78 int slen = len; 79 int c = (int)recv(socket, s, slen, 0); 80 while ((c > 0) && (s[c-1] != '\n')) { 81 s += c; 82 slen -= c; 83 c = (int)recv(socket, s, slen, 0); 84 } 85 86 if (c < 0) { 87 return c; 88 }else if (c == 0) { 89 buf[0] = '\0'; 90 }else { 91 s[c - 1] = '\0'; 92 } 93 94 return len - slen; 95 } 96 int main(int argc, const char * argv[]) { 97 98 // 監聽中斷 99 if (catch_signal(SIGINT, handle_shutdown) == -1) { 100 error("Can not set the interrupt handler"); 101 } 102 103 // 打開socket 104 listener_d = open_listener_socket(); 105 106 // 綁定端口 107 bind_to_port(listener_d, 30000); 108 109 // 監聽 110 if (listen(listener_d, 1) == -1) { 111 error("Can't listen"); 112 } 113 114 puts("Waiting for connection"); 115 116 // 客戶端 117 struct sockaddr_storage client_addr; 118 unsigned int addr_size = sizeof(client_addr); 119 120 char buf[255]; 121 122 while (1) { 123 124 // 鏈接 125 int connect_d = accept(listener_d, (struct sockaddr*) &client_addr, &addr_size); 126 if (connect_d == -1) { 127 error("Can't open secondary socket"); 128 } 129 130 // 子進程 131 //if (!fork()) { 132 133 // close(listener_d); 134 135 if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) { 136 137 read_in(connect_d, buf, sizeof(buf)); 138 if (strncasecmp("Who's there?", buf, (2))) { 139 say(connect_d, "You should say 'Who's there?' !"); 140 }else { 141 if (say(connect_d, "Oscar\r\n>") != -1) { 142 read_in(connect_d, buf, sizeof(buf)); 143 144 if (strncasecmp("Oscar who?", buf, (0))) { 145 say(connect_d, "You should say 'Oscar who?' !"); 146 }else { 147 say(connect_d, "Oscar silly question, you set a silly answer!\r\n"); 148 } 149 } 150 } 151 152 } 153 154 // close(connect_d); 155 // exit(0); 156 // } 157 158 close(connect_d); 159 } 160 161 162 return 0; 163 }
編譯並運行后
我們打開另一個終端
我們現在已經能夠接受客戶端的數據,並且能夠按照我們自定義的協議進行通信了。
但是我們還需要想的更多,現在是和一個客戶端通信,如果跟多個客戶端呢?
打開我們上邊代碼中注釋的部分,恢復后的代碼是這樣的
1 // 子進程 2 if (!fork()) { 3 4 close(listener_d); 5 6 if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) { 7 8 read_in(connect_d, buf, sizeof(buf)); 9 if (strncasecmp("Who's there?", buf, (2))) { 10 say(connect_d, "You should say 'Who's there?' !"); 11 }else { 12 if (say(connect_d, "Oscar\r\n>") != -1) { 13 read_in(connect_d, buf, sizeof(buf)); 14 15 if (strncasecmp("Oscar who?", buf, (0))) { 16 say(connect_d, "You should say 'Oscar who?' !"); 17 }else { 18 say(connect_d, "Oscar silly question, you set a silly answer!\r\n"); 19 } 20 } 21 } 22 23 } 24 25 close(connect_d); 26 exit(0); 27 }
通過對比可以看出,當我們接受到客戶端的數據的時候,我們創建一個子進程,這樣我們就只使用父進程監聽連接,子進程處理各自的任務了
多打開幾個終端試試。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
到這里我們已經能夠寫服務器端的代碼了,能夠發消息和接受消息。
但這遠遠不夠,我現在就想手寫一個客戶端,通過我的請求能夠獲取服務器端的某些數據。這其實也很簡單
這時候主動權就在我們手里了。
客戶端和服務器段都是用套接字來進行通信,但是兩者獲取套接字的方式不同。
服務器端使用的是BLAM :服務器連接網絡分為四部曲:①綁定(Bind) ②監聽(Listen) ③接受(Accept) ④開始(Begin)
客戶端只需要兩步就可以了 ①連接遠程端口 ②開始通信
服務器在網絡連接時必須決定使用哪個端口,而客戶端除了要端口號還需要知道遠程服務器的IP地址
但是這樣太不容易記憶了,人們更喜歡使用域名:www.baidu.com
接下來就讓我們編寫一段代碼。實現網絡請求的任務,下邊的代碼需要能夠連接外網才行,也就是需要FQ
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <arpa/inet.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <stdlib.h> 7 #include <unistd.h> 8 #include <signal.h> 9 #include <netdb.h> 10 11 12 // 錯誤處理函數 13 void error(char *msg) { 14 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 15 exit(1); 16 } 17 18 // 向客戶端發消息 19 int say(int socket, char *s) { 20 int result = (int)send(socket, s, strlen(s), 0); 21 if (result == -1) { 22 fprintf(stderr, "%s: %s \n","和客戶端通信發生錯誤",strerror(errno)); 23 } 24 return result; 25 } 26 27 28 29 // 根據域名和端口開啟socket 30 int open_socket(char *host, char *port) { 31 32 struct addrinfo *res; 33 struct addrinfo hints; 34 memset(&hints, 0, sizeof(hints)); 35 hints.ai_family = PF_UNSPEC; 36 hints.ai_socktype = SOCK_STREAM; 37 if (getaddrinfo(host, port, &hints, &res) == -1) { 38 error("Can't resolve the address"); 39 } 40 41 int d_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 42 if (d_sock == -1) { 43 error("Can't open socket"); 44 } 45 46 int c = connect(d_sock, res->ai_addr, res->ai_addrlen); 47 if (c == -1) { 48 error("Can't connect to socket"); 49 } 50 51 return d_sock; 52 } 53 54 55 int main(int argc, const char * argv[]) { 56 57 58 59 int d_sock; 60 d_sock = open_socket("en.wikipedia.org", "80"); 61 62 char buf[255]; 63 sprintf(buf, "GET /wiki/%s http/1.1\r\n",argv[1]); 64 65 say(d_sock, buf); 66 say(d_sock, "Host: en.wikipedia.org\r\n\r\n"); 67 68 char rec[256]; 69 int bytesRcvd = recv(d_sock, rec, 255, 0); 70 while (bytesRcvd) { 71 if (bytesRcvd == -1) { 72 error("Can't read from server"); 73 } 74 75 rec[bytesRcvd] = '\0'; 76 printf("%s",rec); 77 bytesRcvd = recv(d_sock, rec, 255, 0); 78 } 79 80 close(d_sock); 81 82 return 0; 83 }