《TCP IP网络编程》第六章

news2024/11/27 12:29:45

《TCP IP网络编程》第六章:基于 UDP 的服务端/客户端

UDP 套接字的特点:

        通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。

        不过,这种比喻只是形容协议工作方式,并不包含数据交换速率。实际正好相反,TCP的速度无法超过UDP,但在收发某些类型的数据时可能接近UDP

        因为 UDP 没有 TCP 那么复杂,所以编程难度比较小,性能也比 TCP 高。在更重视性能的情况下可以选择 UDP 的传输方式。

        TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生命在于流控制

 UDP 的工作原理:

        从图中可以看出,IP 的作用就是让离开主机 B 的 UDP 数据包准确传递到主机 A 。但是把 UDP 数据包最终交给主机 A 的某一 UDP 套接字的过程是由 UDP 完成的。UDP 的最重要的作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字。 

UDP 的高效使用:

        UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点:

  • 收发数据前后进行的连接设置及清除过程。
  • 收发过程中为保证可靠性而添加的流控制。

        如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效。

实现基于 UDP 的服务端/客户端:

        UDP 中的服务端和客户端不像 TCP 那样在连接状态下交换数据,因此与 TCP 不同,无需经过连接过程。也就是说,不必调用 TCP 连接过程中调用的 listen 和 accept 函数。UDP 中只有创建套接字和数据交换的过程。

        TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示:

        图中展示了 1 个 UDP 套接字与 2 个不同主机交换数据的过程。也就是说,只需 1 个 UDP 套接字就能和多台主机进行通信。

        创建好 TCP 套接字以后,传输数据时无需加上地址信息。因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态(UDP 套接字只有简单的邮筒功能),因此每次传输数据时都需要添加目标的地址信息。这相当于寄信前在信件中填写地址。接下来是 UDP 的相关函数: 

#include <sys/socket.h>
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 数据包中的发送端信息。

#include <sys/socket.h>
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 的回声服务器端/客户端:

服务端:

#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);

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 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);
}

客户端:

#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);

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);
    }
    //创建 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);
}

实验结果:

 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 客户端中通常无需额外的地址分配过程。所以之前的示例中省略了该过程。这也是普遍的实现方式。

UDP 的数据传输特性和调用 connect 函数:

        前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」

        相反,UDP 是具有数据边界的下一,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数应该完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证:

服务端代码 :

#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);

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);
    }
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

客户端代码: 

#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);

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, 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);
}

        客户端调用3次sendto函数以传输数据,服务端则调用了3次recvfrom函数以接受数据。recvfrom函数调用间隔为5秒,因此,调用recvfrom函数前已调用了3次sendto函数。也就是说,此时数据已经传输到bound_host1.c。如果是TCP程序,这时只需调用1次输入函数即可读入数据。UDP则不同,在这种情况下也需要调用3次recvfrom函数。可通过以下运行结果进行验证。

        从服务端结果可以看出,共调用了3次recvfrom函数,这就证明了必须在UDP通信过程中使I/O函数调用次数保持一致。

已连接(connect)UDP 套接字与未连接(unconnected)UDP 套接字 :    

        TCP 套接字中需注册待传传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段:

  • 第 1 阶段:向 UDP 套接字注册目标 IP 和端口号
  • 第 2 阶段:传输数据
  • 第 3 阶段:删除 UDP 套接字中注册的目标地址信息。

        每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接 connected 套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。

        创建已连接 UDP 套接字过程格外简单,只需针对 UDP 套接字调用 connect 函数:

sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = inet_addr(argv[1]);
adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr *)&adr, sizeof(adr));

        上述代码看似与 TCP 套接字创建过程一致,但 socket 函数的第二个参数分明是 SOCK_DGRAM 。也就是说,创建的的确是 UDP 套接字。当然针对 UDP 调用 connect 函数并不是意味着要与对方 UDP 套接字连接,这只是向 UDP 套接字注册目标IP和端口信息。

        之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。因为已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/764251.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣142. 环形链表 II

题目 给定一个链表的头节点head&#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回null。 链接&#xff1a;142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 题解 方法一&#xff1a;设置两个指针&#xff0c;一个指针指向链表头结点&#…

重生之我要学C++第一课

时隔三月&#xff0c;继C语言之后&#xff0c;博主学成C归来&#xff0c;欲重开博客&#xff0c;与诸位分享经验&#xff0c;共同学习&#xff0c;今天就开始我们的c第一课——入门篇。 既然要开始学习c&#xff0c;就要了解 什么是c&#xff1a; C 语言是结构化和模块化的语言…

Vue2 ➔ Vue3 都做了哪些改变?

不是吧&#xff0c;兄弟&#xff0c;Vue3 都出来多久了&#xff0c;你还对这个感兴趣&#xff0c;说&#xff01;是不是没好好卷&#xff1f;&#x1f60f; 俺也一样 &#x1f602;&#xff0c;Vue3 出来之后只是简单了解了一下&#xff0c;然后还是转头一直在写 Vue2。当然&a…

如何升级iOS17/iPadOS17公测版?iOS17公测版升级教程

苹果官方发布了iOS 17/iPadOS 17系统首个公测版更新&#xff0c;其版本号及更新内容与iOS 17 beta 3一致&#xff0c;版本号为21A5277j。 对于想升级iOS 17/iPadOS 17 公测版的用户&#xff0c;可以参考本教程进行操作。 升级注意事项&#xff1a; 1. 为防止意外情况&#xf…

AlignPS中的TOIM损失

本文介绍了CVPR2021行人重识别领域中一篇名为AlignPS论文中的TOIM损失函数 论文链接&#xff1a;https://arxiv.org/abs/2109.00211 代码链接&#xff1a;GitHub - daodaofr/AlignPS: Code for CVPR 2021 paper: Anchor-Free Person Search TOIM TOIM Loss OIM Loss Trip…

【STM32零基础入门教程02】STM32环境获取及搭建

(14条消息) 【STM32零基础入门教程01】STM32入门基础知识_小超电子笔记的博客-CSDN博客 工欲善其事必先利其器&#xff0c;在上一章了解完STM32的一些基础知识之后&#xff0c;我们需要对使用的环境进行获取和安装。 一、MDK&#xff08;KEIL&#xff09;的获取 Keil是一种…

CCF 202209-2 何以包邮? (01背包动态规划练习)

一、先温习一下01背包问题 有N件物品和一个容量为V的背包。第i件物品的体积是c[i]&#xff0c;价值是w[i]。求解将哪些物品装入背包可使价值总和最大。 条件汇总 -------- 背包限制容量&#xff1a;Z 此时背包容量&#xff1a;C 物品&#xff1a;1 , i ... …

WSA - root,frida与ida测试

本文旨在配置windows subsystem for android(win安卓子系统)来作为win在开启了hyper-v的情况下的一种轻量的安卓模拟器方案。使用MagiskOnWsa设置root权限&#xff0c;最终使其正常与开发环境、frida、ida打通。 1. Root的WSA 常用的Wsa版本在目前是没有默认root的。在物理机…

matlab滤波器设计-IIR滤波器的设计与仿真

matlab滤波器设计-IIR滤波器的设计与仿真 1 引言 在现代通信系统中&#xff0c;由于信号中经常混有各种复杂成分&#xff0c;所以很多信号的处理和分析都是基于滤波器而进行的。但是&#xff0c;传统的数字滤波器的设计使用繁琐的公式计算&#xff0c;改变参数后需要重新计…

【Matlab】智能优化算法_猎豹优化算法CO)

【Matlab】智能优化算法_猎豹优化算法CO 1.背景介绍2.数学模型2.1 搜索策略2.2 坐等策略2.3 攻击策略2.4 假设 3.文件结构4.伪代码5.详细代码及注释5.1 CO.m5.2 CO_VectorBased.m5.3 Get_Functions_details.m 6.运行结果7.参考文献 1.背景介绍 猎豹&#xff08;Achinonyx jubat…

Kubernetes部署服务到集群中的指定节点

# kubectl get node NAME STATUS ROLES AGE VERSION k8s-master Ready master 25h v1.17.3 k8s-node2 Ready <none> 25h v1.17.3 集群只有两个节点&#xff0c;这里打算将应用部署在k8s-node2节点上&#xff0c;需要先记下这个节点的…

【3】Vite Vue3 用户、角色、岗位选择组件封装

在当今前端开发的领域里&#xff0c;快速、高效的项目构建工具以及使用最新技术栈是非常关键的。ViteVue3 组合为一体的项目实战示例专栏将带领你深入了解和掌握这一最新的前端开发工具和框架。 作为下一代前端构建工具&#xff0c;Vite 在开发中的启动速度和热重载方面具有突…

攻不下dfs不参加比赛(十三)

标题 为什么练dfs题目为什么练dfs 相信学过数据结构的朋友都知道dfs(深度优先搜索)是里面相当重要的一种搜索算法,可能直接说大家感受不到有条件的大家可以去看看一些算法比赛。这些比赛中每一届或多或少都会牵扯到dfs,可能提到dfs大家都知道但是我们为了避免眼高手低有的东…

24 - 数组和广义表 - 二维数组

前面我们学习了一维数组、今天来看看二维数组,比一维数组更加复杂! 数组的特点 存储的空间连续 存储类型相同 可以使用地址+偏移快速访问 二维数组定义 二维数组本质上是以数组作为数组元素的数组,即“数组的数组”,语法格式如下: 类型说明符 数组名[常量表达式][常量表达…

977.有序数组的平方

977.有序数组的平方 1.暴力排序 这道题最直观的方法在于&#xff0c;将数组中的每个数平方之后&#xff0c;排个序 public int[] sortedSquares(int[] nums) {int[]ans new int[nums.length];for(int i0;i<nums.length;i){ans[i] nums[i]*nums[i];} Arrays.sort(ans);ret…

人工智能LLM模型:奖励模型的训练、PPO 强化学习的训练、RLHF

人工智能LLM模型&#xff1a;奖励模型的训练、PPO 强化学习的训练 1.奖励模型的训练 1.1大语言模型中奖励模型的概念 在大语言模型完成 SFT 监督微调后&#xff0c;下一阶段是构建一个奖励模型来对问答对作出得分评价。奖励模型源于强化学习中的奖励函数&#xff0c;能对当前…

高通芯片android进入EDL模式 下载 热启动 串口指令

参考&#xff1a;高通方案的Android设备几种开机模式的进入与退出_edl模式怎么退出_Rookie20190715的博客-CSDN博客 切换为EDL模式 向串口发送 4b 65 01 00 54 0f 7e 或者adb reboot edl

Ceph的安装部署

文章目录 一、存储基础1.1 单机存储设备1.2 单机存储的问题1.3分布式存储&#xff08;软件定义的存储 SDS&#xff09; 二、Ceph 简介2.1 Ceph 优势2.2 Ceph 架构2.3 Ceph 核心组件2.4 Pool、PG 和 OSD 的关系&#xff1a;2.5 OSD 存储后端2.6 Ceph 数据的存储过程2.7 Ceph 版本…

PID控制系列--(1、最形象的PID)

目录 1、 比例控制系统的标准结构2、最简单的例子3、第二个例子4、积分控制器6、微分控制7 总结 今天 看到了B站上一个叫洋葱auto的UP主搬来的介绍PID控制的视频&#xff0c;感觉讲得形象易懂&#xff0c;为便于让和我一样看了无数文章还是不能很好理解PID控制本质的人共同分享…

2. DATASETS DATALOADERS

2. DATASETS & DATALOADERS PyTorch提供了两个数据基元&#xff1a;torch.utils.data.DataLoader和torch.uutils.data.data集&#xff0c;允许使用预加载的数据集以及自己的数据。数据集存储样本及其相应的标签&#xff0c;DataLoader在数据集周围包装了一个可迭代项&…