在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
我们已经知道如何使用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 下编译运行 终端显示成这样 我们打开另一个终端来模拟客户端 太棒了,服务器和客户端能够连接且服务器能够给客户端发送数据了,但是这样的程序还是有问题的,当我们快速使用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 } 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 下编译运行 终端显示成这样 我们打开另一个终端来模拟客户端 太棒了,服务器和客户端能够连接且服务器能够给客户端发送数据了,但是这样的程序还是有问题的,当我们快速使用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 } |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论