diff --git a/README.md b/README.md index e6951a2..d39863a 100644 --- a/README.md +++ b/README.md @@ -1429,6 +1429,149 @@ TCP 是内容较多的一个协议,而本章中的 UDP 内容较少,但是 ### 6.1 理解 UDP +#### 6.1.1 UDP 套接字的特点 + +通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应现在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。 + +因为 UDP 没有 TCP 那么复杂,所以编程难度比较小,性能也比 TCP 高。在更重视性能的情况下可以选择 UDP 的传输方式。 + +TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生命在于流控制。 + +#### 6.1.2 UDP 的工作原理 + +如图所示: + +![](https://i.loli.net/2019/01/17/5c3fd29c70bf2.png) + +从图中可以看出,IP 的作用就是让离开主机 B 的 UDP 数据包准确传递到主机 A 。但是把 UDP 数据包最终交给主机 A 的某一 UDP 套接字的过程是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字。 + +#### 6.1.3 UDP 的高效使用 + +UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: + +- 收发数据前后进行的连接设置及清楚过程。 +- 收发过程中为保证可靠性而添加的流控制。 + +如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效。 + +### 6.2 实现基于 UDP 的服务端/客户端 + +#### 6.2.1 UDP 中的服务端和客户端没有连接 + +UDP 中的服务端和客户端不像 TCP 那样在连接状态下交换数据,因此与 TCP 不同,无需经过连接过程。也就是说,不必调用 TCP 连接过程中调用的 listen 和 accept 函数。UDP 中只有创建套接字和数据交换的过程。 + +#### 6.2.2 UDP 服务器和客户端均只需一个套接字 + +TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管事服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示: + +![](https://i.loli.net/2019/01/17/5c3fd703f3c40.png) + +图中展示了 1 个 UDP 套接字与 2 个不同主机交换数据的过程。也就是说,只需 1 个 UDP 套接字就能和多台主机进行通信。 + +#### 6.2.3 基于 UDP 的数据 I/O 函数 + +创建好 TCP 套接字以后,传输数据时无需加上地址信息。因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态(UDP 套接字只有简单的邮筒功能),因此每次传输数据时都需要添加目标的地址信息。这相当于寄信前在信件中填写地址。接下来是 UDP 的相关函数: + +```c +#include +ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, + struct sockaddr *to, socklen_t addrlen); +/* +成功时返回传输的字节数,失败是返回 -1 +sock: 用于传输数据的 UDP 套接字 +buff: 保存待传输数据的缓冲地址值 +nbytes: 待传输的数据长度,以字节为单位 +flags: 可选项参数,若没有则传递 0 +to: 存有目标地址的 sockaddr 结构体变量的地址值 +addrlen: 传递给参数 to 的地址值结构体变量长度 +*/ +``` + +上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 + +```c +#include +ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, + struct sockaddr *from, socklen_t *addrlen); +/* +成功时返回传输的字节数,失败是返回 -1 +sock: 用于传输数据的 UDP 套接字 +buff: 保存待传输数据的缓冲地址值 +nbytes: 待传输的数据长度,以字节为单位 +flags: 可选项参数,若没有则传递 0 +from: 存有发送端地址信息的 sockaddr 结构体变量的地址值 +addrlen: 保存参数 from 的结构体变量长度的变量地址值。 +*/ +``` + +编写 UDP 程序的最核心的部分就在于上述两个函数,这也说明二者在 UDP 数据传输中的地位。 + +#### 6.2.4 基于 UDP 的回声服务器端/客户端 + +下面是实现的基于 UDP 的回声服务器的服务器端和客户端: + +代码: + +- [uecho_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_client.c) +- [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) + +编译运行: + +```shell +gcc uecho_client.c -o uclient +gcc uecho_server.c -o userver +./server 9190 +./uclient 127.0.0.1 9190 +``` + +结果: + +![](https://i.loli.net/2019/01/17/5c3feb85baa83.png) + +TCP 客户端套接字在调用 connect 函数时自动分配IP地址和端口号,既然如此,UDP 客户端何时分配IP地址和端口号? + +#### 6.2.5 UDP 客户端套接字的地址分配 + +仔细观察 UDP 客户端可以发现,UDP 客户端缺少了把IP和端口分配给套接字的过程。TCP 客户端调用 connect 函数自动完成此过程,而 UDP 中连接能承担相同功能的函数调用语句都没有。究竟在什么时候分配IP和端口号呢? + +UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的地址分配工作,因此调用 bind 函数。当然,bind 函数在 TCP 程序中出现过,但 bind 函数不区分 TCP 和 UDP,也就是说,在 UDP 程序中同样可以调用。另外,如果调用 sendto 函数尚未分配地址信息,则在首次调用 sendto 函数时给相应套接字自动分配 IP 和端口。而且此时分配的地址一直保留到程序结束为止,因此也可以用来和其他 UDP 套接字进行数据交换。当然,IP 用主机IP,端口号用未选用的任意端口号。 + +综上所述,调用 sendto 函数时自动分配IP和端口号,因此,UDP 客户端中通常无需额外的地址分配过程。所以之前的示例中省略了该过程。这也是普遍的实现方式。 + +### 6.3 UDP 的数据传输特性和调用 connect 函数 + +#### 6.3.1 存在数据边界的 UDP 套接字 + +前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 + +相反,UDP 是具有数据边界的下一,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证: + +- [bound_host1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host1.c) +- [bound_host2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host2.c) + +编译运行: + +```shell +gcc bound_host1.c -o host1 +gcc bound_host2.c -o host2 +./host1 9190 +./host2 127.0.0.1 9190 +``` + +运行结果: + +![](https://i.loli.net/2019/01/17/5c3ff966a8d34.png) + +host1 是服务端,host2 是客户端,host2 一次性把数据发给服务端后,结束程序。但是因为服务端每隔五秒才接收一次,所以服务端每隔五秒接收一次消息。 + +**从运行结果也可以证明 UDP 通信过程中 I/O 的调用次数必须保持一致** + +#### 6.3.2 已连接(connect)UDP 套接字与未连接(unconnected)UDP 套接字 + +TCP 套接字中需注册待传传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: + + + diff --git a/ch05/op_client.c b/ch05/op_client.c index ae782bd..d762cfa 100644 --- a/ch05/op_client.c +++ b/ch05/op_client.c @@ -5,7 +5,7 @@ #include #include #define BUF_SIZE 1024 -#define RLT_SIZE 4 +#define RLT_SIZE 4 //字节大小数 #define OPSZ 4 void error_handling(char *message); diff --git a/ch06/README.md b/ch06/README.md new file mode 100644 index 0000000..46a1b68 --- /dev/null +++ b/ch06/README.md @@ -0,0 +1,8 @@ +## 第 6 章 基于 UDP 的服务端/客户端 + +本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 + +TCP 是内容较多的一个协议,而本章中的 UDP 内容较少,但是也很重要。 + +### 6.1 理解 UDP + diff --git a/ch06/bound_host1.c b/ch06/bound_host1.c new file mode 100644 index 0000000..4a1108f --- /dev/null +++ b/ch06/bound_host1.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 30 +void error_handling(char *message); + +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 \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); + } + close(sock); + return 0; +} + +void error_handling(char *message) +{ + fputs(message, stderr); + fputc('\n', stderr); + exit(1); +} \ No newline at end of file diff --git a/ch06/bound_host2.c b/ch06/bound_host2.c new file mode 100644 index 0000000..42b9104 --- /dev/null +++ b/ch06/bound_host2.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 30 +void error_handling(char *message); + +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 \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, msg3, sizeof(msg3), 0, + (struct sockaddr *)&your_adr, sizeof(your_adr)); + close(sock); + return 0; +} + +void error_handling(char *message) +{ + fputs(message, stderr); + fputc('\n', stderr); + exit(1); +} diff --git a/ch06/uecho_client.c b/ch06/uecho_client.c new file mode 100644 index 0000000..0e7c817 --- /dev/null +++ b/ch06/uecho_client.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 30 +void error_handling(char *message); + +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 \n", argv[0]); + exit(1); + } + //创建 UDP 套接字 + 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; + //向服务器传输数据,会自动给自己分配IP地址和端口号 + 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; +} + +void error_handling(char *message) +{ + fputs(message, stderr); + fputc('\n', stderr); + exit(1); +} diff --git a/ch06/uecho_server.c b/ch06/uecho_server.c new file mode 100644 index 0000000..c6301ed --- /dev/null +++ b/ch06/uecho_server.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include + +#define BUF_SIZE 30 +void error_handling(char *message); + +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 \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 eerror"); + + 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); + } + close(serv_sock); + return 0; +} + +void error_handling(char *message) +{ + fputs(message, stderr); + fputc('\n', stderr); + exit(1); +} \ No newline at end of file