注:博主只关注编程实现的方面以及linux部分,部分网络原理讲解和windows实现部分跳过
第 6 章 基于 UDP 的服务端/客户端
6.1 理解 UDP
- Q:UDP原理和使用
- UDP比TCP简单,编程难度较小,性能比TCP高。TCP相对UDP的主要区别是流控制
- IP 的作用就是让离开主机 B 的 UDP 数据包准确传递到主机 A 。但是把 UDP 数据包最终交给主机 A 的某一 UDP 套接字的过程是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字
- UDP 也具有一定的可靠性。在网络实时传递的视频或者音频的场景下,丢失一部分多媒体数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。此时TCP流控制就显得有一点多余,这时就要考虑使用 UDP
- TCP 比 UDP 慢的原因主要有以下两点:
- 收发数据前后进行的连接设置及清除过程
- 收发过程中为保证可靠性而添加的流控制
- 如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效
6.2 实现基于 UDP 的服务端/客户端
Q:基于 UDP 的服务端/客户端的特点
- UDP 的服务端和客户端无需经过连接过程,只有创建套接字和数据交换的过程
- UDP 的服务端和客户端均只需1个套接字
Q:基于 UDP 的数据 I/O 函数
UDP 套接字不会保持连接状态,因此每次传输数据时都需要添加目标的地址信息
# include <sys/socket.h> ssize_t sendto(int sock, void * buff, size_t nbytes, int flags, struct sockaddr * to, socklen_t addrlen); // sock:用于传输数据的UDP套接字文件描述符 // buff:保存待传输数据的缓冲地址值 // nbytes:待传输的数据长度,以字节为单位 // flags:可选参数,若没有则传递0 // to:存有目标地址信息的sockaddr结构体变量的地址值 // addrlen:传递给参数to的地址值结构体变量长度 // 成功时返回传输的字节数,失败时返回 -1
UDP 数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息
# include <sys/socket.h> ssize_t recvfrom(int sock, void * buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen); // sock:用于传输数据的UDP套接字文件描述符 // buff:保存接收数据的缓冲地址值 // nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲大小 // flags:可选项参数,若没有则传递0 // from:存有发送端地址信息的sockaddr结构体变量的地址值 // addrlen:保存参数from的结构体变量长度的变量地址值 // 成功时返回接收的字节数,失败时返回 -1
Q:基于 UDP 的服务端/客户端的程序实现
服务端uecho_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int serv_sock; char message[BUF_SIZE]; int str_len; socklen_t clnt_adr_sz; struct sockaddr_in serv_adr, clnt_adr; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } // 创建 UDP 套接字后,向 socket 的第二个参数传递 SOCK_DGRAM serv_sock = socket(PF_INET, SOCK_DGRAM, 0); if (serv_sock == -1) { error_handling("UDP socket creation error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); // 分配地址接受数据,不限制数据传输对象 if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("bind() error"); } while (1) { clnt_adr_sz = sizeof(clnt_adr); str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); // 通过上面的函数调用同时获取数据传输端的地址。 // 正是利用该地址进行逆向重传 sendto(serv_sock, message, str_len, 0, (struct sockaddr *)&clnt_adr, clnt_adr_sz); } // 上面未加入break语句,因此无限循环,下面不会执行 close(serv_sock); return 0; }
客户端uecho_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char message[BUF_SIZE]; int str_len; socklen_t adr_sz; struct sockaddr_in serv_adr, from_adr; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); while (1) { fputs("Insert message(q to quit): ", stdout); fgets(message, sizeof(message), stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); adr_sz = sizeof(from_adr); str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_adr, &adr_sz); message[str_len] = '\0'; printf("Message from server: %s", message); } close(sock); return 0; }
编译运行
服务端
shiqi@inspiron:~/network$ gcc uecho_server.c -o userver shiqi@inspiron:~/network$ ./userver 9190
客户端
shiqi@inspiron:~/network$ gcc uecho_client.c -o uclient shiqi@inspiron:~/network$ ./uclient 127.0.0.1 9190 Insert message(q to quit): xp Message from server: xp Insert message(q to quit): q
Q:UDP 客户端套接字的地址分配
- 在首次调用sendto函数时,发现尚未分配地址信息,则给相应套接字自动分配 IP 和端口,此时分配的地址一直保留到程序结束为止
6.3 UDP 的数据传输特性和调用 connect 函数
Q:存在数据边界的UDP套接字程序示例
bound_host1.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char message[BUF_SIZE]; struct sockaddr_in my_adr, your_adr; socklen_t adr_sz; int str_len, i; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&my_adr, 0, sizeof(my_adr)); my_adr.sin_family = AF_INET; my_adr.sin_addr.s_addr = htonl(INADDR_ANY); my_adr.sin_port = htons(atoi(argv[1])); if (bind(sock, (struct sockaddr *)&my_adr, sizeof(my_adr)) == -1) { error_handling("bind() error"); } for (i = 0; i < 3; ++i) { sleep(5); adr_sz = sizeof(your_adr); str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&your_adr, &adr_sz); printf("Message %d: %s \n", i + 1, message); } return 0; }
bound_host2.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char msg1[] = "Hi!"; char msg2[] = "I'm another UDP host!"; char msg3[] = "Nice to meet you"; struct sockaddr_in your_adr; socklen_t your_adr_sz; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&your_adr, 0, sizeof(your_adr)); your_adr.sin_family = AF_INET; your_adr.sin_addr.s_addr = inet_addr(argv[1]); your_adr.sin_port = htons(atoi(argv[2])); sendto(sock, msg1, sizeof(msg1), 0, (struct sockaddr *)&your_adr, sizeof(your_adr)); sendto(sock, msg2, sizeof(msg2), 0, (struct sockaddr *)&your_adr, sizeof(your_adr)); sendto(sock, msg2, sizeof(msg2), 0, (struct sockaddr *)&your_adr, sizeof(your_adr)); close(sock); return 0; }
编译运行
bound_host1
shiqi@inspiron:~/network$ gcc bound_host1.c -o host1 shiqi@inspiron:~/network$ ./host1 9190 Message 1: Hi! Message 2: I'm another UDP host! Message 3: I'm another UDP host!
bound_host2
shiqi@inspiron:~/network$ gcc bound_host2.c -o host2 shiqi@inspiron:~/network$ ./host2 127.0.0.1 9190
Q:sendto 函数传输数据的过程的 3 个阶段
- 向 UDP 套接字注册目标 IP 和端口号
- 传输数据
- 删除 UDP 套接字中注册的目标地址信息
Q:未连接UDP套接字和已连接UDP套接字
- 未连接UDP套接字:未注册目标地址信息的套接字,每次都变更目标地址,可重复利用同一 UDP 套接字向不同目标传递数据
- 已连接UDP套接字:注册了目标地址的套接字,要与同一主机长时间通信时,将UDP套接字变成已连接套接字会提高效率
Q:针对UDP套接字调用connect函数创建已连接UDP套接字
sock = socket(PF_INET, SOCK_DGRAM, 0); memset(&adr, 0, sizeof(adr)); adr.sin_family = AF_INET; adr.sin_addr.s_addr = ... adr.sin_port = ... connect(sock, (struct sockaddr *)&adr, sizeof(adr));
因已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信
Q:已连接UDP套接字程序示例
服务端同uecho_server.c
客户端uecho_con_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); while (1) { fputs("Insert message(q to quit): ", stdout); fgets(message, sizeof(message), stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } write(sock, message, strlen(message)); str_len = read(sock, message, sizeof(message) - 1); message[str_len] = '\0'; printf("Message from server: %s", message); } close(sock); return 0; }
编译运行同echo的udp服务器
第 7 章 优雅的断开套接字的连接
7.1 基于 TCP 的半关闭
Q:单方面断开连接带来的问题
- Linux 的 close 函数意味着完全断开连接。即不仅无法传输数据,而且也不能接收数据。因此在某些情况下,通信一方单方面的断开套接字连接,显得不太优雅
- 断开一部分连接,即可以传输数据但是无法接收,或可以接受数据但无法传输。即关闭流的一半
Q:针对优雅断开的 shutdown 函数原型
#include <sys/socket.h> int shutdown(int sock, int howto); // sock:需要断开的套接字文件描述符 // howto:传递断开方式信息 // 成功时返回 0 ,失败时返回 -1
第二个参数决定断开连接的方式
- SHUT_RD:断开输入流
- SHUT_WR:断开输出流
- SHUT_RDWR:同时断开I/O流
Q:基于半关闭的文件传输程序
服务端file_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int serv_sd, clnt_sd; FILE * fp; char buf[BUF_SIZE]; int read_cnt; struct sockaddr_in serv_adr, clnt_adr; socklen_t clnt_adr_sz; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } fp = fopen("file_server.c", "rb"); serv_sd = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); bind(serv_sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); listen(serv_sd, 5); clnt_adr_sz = sizeof(clnt_adr); clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); while (1) { read_cnt = fread((void *)buf, 1, BUF_SIZE, fp); if (read_cnt < BUF_SIZE) { write(clnt_sd, buf, read_cnt); break; } write(clnt_sd, buf, BUF_SIZE); } shutdown(clnt_sd, SHUT_WR); read(clnt_sd, buf, BUF_SIZE); printf("Message from client: %s \n", buf); fclose(fp); close(clnt_sd); close(serv_sd); return 0; }
客户端file_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sd; FILE *fp; char buf[BUF_SIZE]; int read_cnt; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } fp = fopen("receive.dat", "wb"); sd = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); connect(sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); while ((read_cnt = read(sd, buf, BUF_SIZE)) != 0) { fwrite((void *)buf, 1, read_cnt, fp); } puts("Received file data"); write(sd, "Thank you", 10); fclose(fp); close(sd); return 0; }
编译运行
服务端
shiqi@inspiron:~/network$ gcc file_server.c -o fserver shiqi@inspiron:~/network$ ./fserver 9190 Message from client: Thank you
客户端
shiqi@inspiron:~/network$ gcc file_client.c -o fclient shiqi@inspiron:~/network$ ./fclient 127.0.0.1 9190 Received file data
第 8 章 域名及网络地址
8.1 域名系统
8.2 IP地址和域名之间的转换
Q:利用域名获取IP地址的函数gethostbyname
#include <netdb.h> struct hostent * gethostbyname(const char * hostname); // 成功时返回 hostent 结构体地址,失败时返回 NULL 指针
只要传递字符串,就可以返回域名对应的IP地址。返回时地址信息装入 hostent 结构体
struct hostent { char * h_name; // official name char ** h_aliases; // alias list int h_addrtype; // host address type int h_length; // address length char ** h_addr_list; // address list }
h_name:该变量中存有官方域名(Official domain name)。即代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册
h_aliases:可通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得
h_addrtype:gethostbyname 函数不仅支持 IPV4 还支持 IPV6 。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPV4 ,则此变量中存有 AF_INET
h_length:保存IP地址长度。若是 IPV4 地址,因为是 4 个字节,则保存4;IPV6 时,因为是 16 个字节,故保存 16
h_addr_list:通过此变量以整数形式保存域名对应的IP地址。另外,用户较多的网站有可能分配多个IP给同一域名,利用多个服务器进行负载均衡。此时同样可通过此变量获取IP地址信息,注意,字符串指针数组中的元素实际指向的是 in_addr 结构体变量地址值而非字符串,所以需要进行类型转换,并调用 inet_ntoa 函数
调用 gethostbyname 函数后,返回的结构体变量如图所示
Q:gethostbyname函数示例gethostbyname.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <netdb.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int i; struct hostent * host; if (argc != 2) { printf("Usage : %s <addr>\n", argv[0]); exit(1); } host = gethostbyname(argv[1]); if (!host) { error_handling("gethost... error"); } // 输出官方域名 printf("Official name: %s \n", host->h_name); // 输出除官方域名以外的域名 for (i = 0; host->h_aliases[i]; ++i) { printf("Aliases %d: %s \n", i+1, host->h_aliases[i]); } // 看看是不是ipv4 printf("Address type: %s \n", (host->h_addrtype == AF_INET)? "AF_INET": "AF_INET6"); // 输出ip地址信息 for (i = 0; host->h_addr_list[i]; ++i) { printf("IP addr %d: %s \n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i])); } return 0; }
编译运行
shiqi@inspiron:~/network$ gcc gethostbyname.c -o hostname shiqi@inspiron:~/network$ ./hostname www.naver.com Official name: www.naver.com.gccdn.net Aliases 1: www.naver.com Aliases 2: www.naver.com.nheos.com Address type: AF_INET IP addr 1: 43.243.234.60 shiqi@inspiron:~/network$ ./hostname www.baidu.com Official name: www.a.shifen.com Aliases 1: www.baidu.com Address type: AF_INET IP addr 1: 220.181.38.149 IP addr 2: 220.181.38.150
Q:利用IP地址获取域名gethostbyaddr函数
#include <netdb.h> struct hostent * gethostbyaddr(const char * addr, socklen_t len, int family); // addr: 含有IP地址信息的 in_addr 结构体指针。 // 为了同时传递IPv4地址之外的其它信息, // 该变量的类型声明为char指针 // len: 向第一个参数传递的地址信息的字节数,IPV4 时为 4,IPV6 时为 16 // family: 传递地址族信息,ipv4 是 AF_INET ,IPV6是 AF_INET6 // 成功时返回 hostent 结构体变量地址值,失败时返回 NULL 指针
Q:gethostbyaddr函数示例gethostbyaddr.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netdb.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int i; struct hostent *host; struct sockaddr_in addr; if (argc != 2) { printf("Usage : %s <IP>\n", argv[0]); exit(1); } memset(&addr, 0, sizeof(addr)); addr.sin_addr.s_addr = inet_addr(argv[1]); host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET); if (!host) { error_handling("gethost... error"); } // 输出官方域名 printf("Official name: %s \n", host->h_name); // 输出除官方域名以外的域名 for (i = 0; host->h_aliases[i]; ++i) { printf("Aliases %d: %s \n", i+1, host->h_aliases[i]); } // 看看是不是ipv4 printf("Address type: %s \n", (host->h_addrtype == AF_INET)? "AF_INET": "AF_INET6"); // 输出ip地址信息 for (i = 0; host->h_addr_list[i]; ++i) { printf("IP addr %d: %s \n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i])); } return 0; }
编译运行
shiqi@inspiron:~/network$ gcc gethostbyaddr.c -o hostaddr shiqi@inspiron:~/network$ ./hostaddr 8.8.8.8 Official name: dns.google Address type: AF_INET IP addr 1: 8.8.8.8 shiqi@inspiron:~/network$ ./hostaddr 114.114.114.114 Official name: public1.114dns.com Address type: AF_INET IP addr 1: 114.114.114.114