C語言與套接字


我們已經知道如何使用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 }
復制代碼

 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM