当前位置: 首页 > news >正文

linux网络编程7——协程设计原理与汇编实现

文章目录

  • 协程设计原理与汇编实现
    • 1. 协程概念
    • 2. 协程的实现
      • 2.1 setjmp
      • 2.2 ucontext
      • 2.3 汇编实现
      • 2.4 优缺点
      • 2.5 实现协程原语
        • 2.5.1 create()
        • 2.5.2 yield()
        • 2.5.3 resume()
        • 2.5.4 exit()
        • 2.5.5 switch()
        • 2.5.6 sleep()
      • 2.6 协程调度器
    • 3. 利用hook使用协程版本的库函数
    • 学习参考

协程设计原理与汇编实现

本文介绍了协程的概念、特征、优势、以及其实现原理。

1. 协程概念

协程是一种轻量级的用户态线程。它允许在单个线程内执行多个任务,使得程序可以在不同的函数之间灵活地切换,以便更好地利用 CPU 资源。这种机制特别适合 IO 密集型任务(如网络请求、文件读写)和异步编程场景。协程可以被暂停和恢复,避免了阻塞等待,同时不需要系统级线程的切换成本。

协程的实现在底层是由执行流的跳转切换机制实现的。一般情况,有一个协程调度器作为每个协程挂起时要切换回的代码。

应用场景

  • webserver
  • kv存储
  • 图床,网络层

同步和异步

"同步"和"异步"主要是指在执行任务时,任务与调用方的相互关系。在同步操作中,调用方会等待任务执行完毕然后继续执行。在异步操作中,调用方会立即返回并继续执行后续的操作,不会等待任务执行完,任务执行完可以通过回调、事件等方式通知调用方。

异步的好处

  • 多线程并发,充分利用cpu,性能好。

异步的坏处

  • 代码复杂,不好理解,需要设置回调函数或者使用事件机制。

协程的好处

  • 同步的编程方式,实现异步的性能。

互联网中协程可能被用到的场景

  1. 浏览器网页加载发送异步HTTP请求时可能用到了协程。
  2. 淘宝商店界面加载商品信息
  3. 直播界面加载评论和视频流
  4. 贴吧加载新的帖子回复
  5. bilibili异步加载新的回复
  6. 网络游戏中加载各种位置信息
  7. 微信聊天时,需要异步加载和发送信息
  8. 音视频通话异步加载流媒体
  9. chatgpt异步发送和接收问答消息
  10. github的git仓库托管服务器可能使用协程处理用户的push、pull等请求

2. 协程的实现

2.1 setjmp

setjmplongjmp 提供了一种低级的非局部跳转机制,适用于需要在 C 程序中实现复杂控制流或异常处理的情况。但由于它们带来的复杂性和潜在风险,使用时需要小心,确保不会影响程序的可维护性和可读性。

代码示例

#include <setjmp.h>
#include <stdio.h>jmp_buf env1, env2, env3;// coroutine1
void func1(void)
{int cur = 0;int ret = setjmp(env1);if (ret == 0)longjmp(env3, 1);printf("func1: %d [%d]\n", ret, cur++);if (ret < 20){longjmp(env2, ++ret);    }
}// coroutine1
void func2(void)
{int cur = 0;int ret = setjmp(env2);printf("func2: %d [%d]\n", ret, cur++);if (ret < 20){longjmp(env1, ++ret);    }
}int main()
{int ret = setjmp(env3);if (ret == 0)func1();elsefunc2();return 0;
}

从实现代码中可以看到setjmp机制需要我们自己保证协程所在的栈空间已被建立,并且还没有退出。协程所在的函数需要先手动执行,才能进行调度。协程的调度也比较麻烦。

2.2 ucontext

ucontext 是一种用于实现协程和用户态线程的机制。它在一些类 Unix 系统(例如 Linux)中提供了在用户态创建、切换和恢复上下文的接口。ucontext 通过保存和恢复 CPU 寄存器、堆栈指针等状态,允许程序在不同执行流之间切换,适用于实现协程和轻量级任务调度等。

其中保存协程上下文信息的结构体ucontext_t为

#include <ucontext.h>typedef struct ucontext {ucontext_t *uc_link;       // 执行结束后切换到的上下文sigset_t uc_sigmask;       // 信号屏蔽字stack_t uc_stack;          // 栈信息(地址和大小)mcontext_t uc_mcontext;    // 寄存器状态
} ucontext_t;

ucontext API 提供了几个主要函数来创建和切换上下文:

  1. getcontext(ucontext_t *ucp):获取当前上下文并保存到 ucp
  2. setcontext(const ucontext_t *ucp):恢复指定上下文并跳转到该上下文。
  3. makecontext(ucontext_t *ucp, void (*func)(), int argc, ...):为 ucp 配置要执行的函数 func 及其参数。
  4. swapcontext(ucontext_t *oucp, const ucontext_t *ucp):保存当前上下文到 oucp,然后切换到 ucp 上下文。

代码示例

#include <ucontext.h>
#include <stdio.h>ucontext_t ctx[2];
ucontext_t main_ctx;int count = 0;// coroutine1
void func1(void)
{int cur = 0;while (count++ < 20){printf("func1: %d [%d]\n", count, cur++);// yieldswapcontext(&ctx[0], &ctx[1]);}
}// coroutine1
void func2(void)
{int cur = 0;while (count++ < 20){printf("func2: %d [%d]\n", count, cur++);// yieldswapcontext(&ctx[1], &ctx[0]);}
}int main()
{char stack1[2048] = {0};char stack2[2048] = {0};getcontext(&ctx[0]);ctx[0].uc_stack.ss_sp = stack1;ctx[0].uc_stack.ss_size = sizeof(stack1);// 执行完之后跳转的地方ctx[0].uc_link = &main_ctx;makecontext(&ctx[0], func1, 0);getcontext(&ctx[1]);ctx[1].uc_stack.ss_sp = stack2;ctx[1].uc_stack.ss_size = sizeof(stack2);ctx[1].uc_link = &main_ctx;makecontext(&ctx[1], func2, 0);printf("start\n");swapcontext(&main_ctx, &ctx[0]);return 0;
}

ucontext 机制虽然强大,但需要谨慎使用。现代开发中,通常使用其他更高层的协程库,如 libco、libuv 或 Boost.Context 等。

2.3 汇编实现

使用汇编语言来实现协程的切换:主要操作为恢复和保存寄存器的值

int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);__asm__(
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);

上面的_switch函数实现了协程上下文的切换,和线程切换所作的工作类似

2.4 优缺点

  1. setjmp实现方式复杂,但是跨平台性好
  2. ucontext实现方式简单,但是跨平台性一般
  3. 汇编实现方式复杂,跨平台型差,但是效率高

2.5 实现协程原语

2.5.1 create()

主要工作是创建一个保存协程上下文的数据结构。一个协程的上下文必须包括如下信息:

  • 协程运行的函数和参数信息

  • cpu寄存器上下文

  • 运行时栈上下文

  • 协程状态

  • 协程id

  • 协程所属的调度器

  • 其他信息

一个示例如下:

struct _coroutine_context
{ucontext_t ctx;			// 里面包括寄存器状态和栈上下文proc_coroutine func;	// 协程运行的函数和参数信息void *arg;void *data;coroutine_status status;	// 协程状态scheduler *sched;			// 所属的调度器uint64_t id;
};

创建协程所作的主要工作包括:

  • 分配一个协程上下文并初始化
  • 获取并设置调度器
  • 将改协程加入调度器进行管理
2.5.2 yield()

主要工作是调用swapcontext()或者_switch()切换会协程调度器。

2.5.3 resume()

主要工作是恢复协程的执行。

2.5.4 exit()

主要工作是协程从调度器中删除,然后释放协程上下文。

2.5.5 switch()

协程切换,主要是切换协程的寄存器。

2.5.6 sleep()

让协程停止执行一段时间。

2.6 协程调度器

协程调度器管理协程,包括一个就绪协程队列,一个sleep协程的集合,一个运行时协程队列,一个等待协程集合。可以采用事件机制,当某事件发生时(例如某fd可读),可以将相应的协程从等待集合中取出并恢复执行。

其核心代码如下

while (1)
{// 检查sleep集合,查看是否有协程超时coroutine_context *expired;while ((expired = check_expired(sched))){resume(expired);}// 检查wait结合,查看是否有协程有监听的事件发生coroutine_context *waked;int nready = epoll_wait(epfd, events, EVENTS_SIZE, 1);for (int i = 0; i < nready; ++i){waked = wait_search(events[i].data.fd);resume(waked);}// 恢复ready队列中的协程的运行coroutine_context *rdy;while (!is_ready_empty(sched)){rdt = ready_pop(sched);resyme(rdt);}
}

3. 利用hook使用协程版本的库函数

利用运行时动态链接,可以在运行时将一个函数替换为为使用协程的版本。

例如,以下代码将read函数在运行时替换为了另一个函数:

#include <dlfcn.h>
#include <unistd.h>typedef ssize_t (*readf_t)(int fd, void *buf, size_t count);readf_t readf;void init_hook()
{readf = (readf_t)dlsym(RTLD_NEXT, "read");
}ssize_t read(int fd, void *buf, size_t count)
{if (!readf) init_hook();// 如果对应的fd不可读,那么就挂起协程yield_if_not_ok(fd, POLLIN | POLLERR | POLLHUP);return readf(fd, buf, count);
}

学习参考

学习更多相关知识请参考零声 github。

相关文章:

linux网络编程7——协程设计原理与汇编实现

文章目录 协程设计原理与汇编实现1. 协程概念2. 协程的实现2.1 setjmp2.2 ucontext2.3 汇编实现2.4 优缺点2.5 实现协程原语2.5.1 create()2.5.2 yield()2.5.3 resume()2.5.4 exit()2.5.5 switch()2.5.6 sleep() 2.6 协程调度器 3. 利用hook使用协程版本的库函数学习参考 协程设…...

Ubuntu22.04版本左右,扩充用户可使用内存

1 取得root权限后&#xff0c;输入命令 lsblk 查看所有磁盘和分区&#xff0c;找到想要替换用户可使用文件夹内存的磁盘和分区。若没有进行分区&#xff0c;并转为所需要的分区数据类型&#xff0c;先进行分区与格式化&#xff0c;过程自行查阅。 扩充替换过程&#xff0c;例如…...

基于ArcMap中Python 批量处理栅格数据(以按掩膜提取为例)

注&#xff1a;图片来源于公众号&#xff0c;公众号也是我自己的。 ArcMap中的python编辑器是很多本科生使用ArcMap时容易忽略的一个工具&#xff0c;本人最近正在读一本书《ArcGIS Python 编程基础与应用》&#xff0c;在此和大家分享、交流一些相关的知识。 这篇文章主要分享…...

【flink】之集成mybatis对mysql进行读写

背景&#xff1a; 在现代大数据应用中&#xff0c;数据的高效处理和存储是核心需求之一。Flink作为一款强大的流处理框架&#xff0c;能够处理大规模的实时数据流&#xff0c;提供丰富的数据处理功能&#xff0c;如窗口操作、连接操作、聚合操作等。而MyBatis则是一款优秀的持…...

Java设计模式—观察者模式详解

引言 模式角色 UML图 示例代码 应用场景 优点 缺点 结论 引言 观察者模式&#xff08;Observer Pattern&#xff09;是一种行为设计模式&#xff0c;它定义了对象之间的一对多依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都会得到通知…...

【Cri-Dockerd】安装cri-dockerd

cri-dockerd的作用&#xff1a; 在k8s1.24之前。k8s会通过dockershim来调用docker进行容器运行时containerd&#xff0c;并且会自动安装dockershim&#xff0c;但是从1.24版本之前k8s为了降低容器运行时的调用的复杂度和效率&#xff0c;直接调用containerd了&#xff0c;并且…...

GCC及GDB的使用

参考视频及博客 https://www.bilibili.com/video/BV1EK411g7Li/?spm_id_from333.999.0.0&vd_sourceb3723521e243814388688d813c9d475f https://www.bilibili.com/video/BV1ei4y1V758/?buvidXU932919AEC08339E30CE57D39A2BABF6A44F&from_spmidsearch.search-result.0…...

大数据新视界 -- 大数据大厂之大数据重塑影视娱乐产业的未来(4 - 3)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

数据结构——基础知识补充

1.队列 1.普通队列 queue.Queue 是 Python 标准库 queue 模块中的一个类&#xff0c;适用于多线程环境。它实现了线程安全的 FIFO&#xff08;先进先出&#xff09;队列。 2.双端队列 双端队列&#xff08;Deque&#xff0c;Double-Ended Queue&#xff09;是一种具有队列和…...

只有.git文件夹时如何恢复项目

有时候误删文件但由于.git是隐藏文件夹而幸存&#xff0c;或者项目太大&#xff0c;单单甩给你一个.git文件夹让你自己恢复整个项目&#xff0c;该怎么办呢&#xff1f; 不用担心&#xff0c;只要进行以下步骤&#xff0c;即可把原项目重新搭建起来&#xff1a; 创建一个文件…...

anchor、anchor box、bounding box之间关系

最近学YOLO接触到这些概念&#xff0c;一下子有点蒙&#xff0c;简单总结一下。 anchor和anchor box Anchor&#xff1a;表示一组预定义的尺寸比例&#xff0c;用来代表常见物体的宽高比。可以把它看成是一个模板或规格&#xff0c;定义了物体框的“形状”和“比例”&#xff…...

代码随想录算法训练营第三十天 | 452.用最少数量的箭引爆气球 435.无重叠区间 763.划分字母区间

LeetCode 452.用最少数量的箭引爆气球&#xff1a; 文章链接 题目链接&#xff1a;452.用最少数量的箭引爆气球 思路&#xff1a; 气球的区间有重叠部分&#xff0c;只要弓箭从重叠部分射出来&#xff0c;那么就能减少所使用的弓箭数 **局部最优&#xff1a;**只要有重叠部分…...

海亮科技亮相第84届中国教装展 尽显生于校园 长于校园教育基因

10月25日&#xff0c;第84届中国教育装备展示会&#xff08;以下简称“教装展”&#xff09;在昆明滇池国际会展中心开幕。作为国内教育装备领域规模最大、影响最广的专业展会&#xff0c;本届教装展以“数字赋能教育&#xff0c;创新引领未来”为主题&#xff0c;为教育领域新…...

C语言数据结构学习:栈

C语言 数据结构学习 汇总入口&#xff1a; C语言数据结构学习&#xff1a;[汇总] 1. 栈 栈&#xff0c;实际上是一种特殊的线性表。这里使用的是链表栈&#xff0c;链表栈的博客&#xff1a;C语言数据结构学习&#xff1a;单链表 2. 栈的特点 只能在一端进行存取操作&#x…...

如何快速分析音频中的各种频率成分

从视频中提取音频 from moviepy.editor import VideoFileClip# Load the video file and extract audio video_path "/mnt/data/WeChat_20241026235630.mp4" video_clip VideoFileClip(video_path)# Extract audio and save as a temporary file for further anal…...

MongoDB 6.0 主从复制配置

以下是 MongoDB 6.0 版本配置主从的详细安装步骤&#xff1a; 1. 安装 MongoDB&#xff1a;可以从官网下载 MongoDB 6.0 的安装包并进行安装&#xff0c;或者使用相应的包管理工具进行安装。 2. 配置主节点&#xff1a;在主节点的 MongoDB 配置文件&#xff08;默认路径为 …...

NPU 神经网络处理单元

Ⅰ 什么是 NPU&#xff1f; 当前正处于神经网络和机器学习处理需求爆发的初期。传统的 CPU&#xff08;中央处理器&#xff09;/GPU&#xff08;图形处理器&#xff09;可以执行类似任务&#xff0c;但专门为神经网络优化的 NPU&#xff08;神经处理单元&#xff09;比 CPU/GP…...

安宝特分享 | AR技术引领:跨国工业远程协作创新模式

在当今高度互联的工业环境中&#xff0c;跨国合作与沟通变得日益重要。然而&#xff0c;语言障碍常常成为高效协作的绊脚石。安宝特AR眼镜凭借其强大的多语言自动翻译和播报功能&#xff0c;正在改变这一局面&#xff0c;让远程协作变得更加顺畅。 01 多语言翻译优势 安宝特A…...

Vulkan 开发(五):Vulkan 逻辑设备

图片来自《Vulkan 应用开发指南》 Vulkan 开发系列文章&#xff1a; 1. 开篇&#xff0c;Vulkan 概述 2. Vulkan 实例 3. Vulkan 物理设备 4. Vulkan 设备队列 在 Vulkan 中&#xff0c;逻辑设备&#xff08;Logical Device&#xff09;是与物理设备&#xff08;Physical D…...

Kafka 解决消息丢失、乱序与重复消费

一、引言 在分布式系统中&#xff0c;Apache Kafka 作为一种高吞吐量的分布式发布订阅消息系统&#xff0c;被广泛应用于日志收集、流式处理、消息队列等场景。然而&#xff0c;在实际使用过程中&#xff0c;可能会遇到消息丢失、乱序、重复消费等问题&#xff0c;这些问题可能…...

计算机专业毕业生面试工具推荐:白瓜面试

随着毕业季的临近&#xff0c;计算机专业的毕业生们即将步入职场&#xff0c;面试成为了他们必须面对的挑战。在这个过程中&#xff0c;选择合适的面试工具可以大大提高求职成功率。今天&#xff0c;我要向大家推荐一款专为计算机专业毕业生设计的面试工具——白瓜面试。 为什…...

数字IC开发:布局布线

数字IC开发&#xff1a;布局布线 前端经过DFT&#xff0c;综合后输出网表文件给后端&#xff0c;由后端通过布局布线&#xff0c;将网表转换为GDSII文件&#xff1b;网表文件只包含单元器件及其连接等信息&#xff0c;GDS文件则包含其物理位置&#xff0c;具体的走线&#xff1…...

高空作业未系安全带监测系统 安全带穿戴识别预警系统

在各类高空作业场景中&#xff0c;安全带是保障作业人员生命安全的关键防线。然而&#xff0c;由于人为疏忽或其他原因&#xff0c;作业人员未正确系挂安全带的情况时有发生&#xff0c;这给高空作业带来了巨大的安全隐患。为有效解决这一问题&#xff0c;高空作业未系安全带监…...

k8s的配置和存储(ConfigMap、Secret、Hostpath、EmptyDir以及NFS的服务使用)

ConfigMap 简介 在 Kubernetes 中&#xff0c;ConfigMap 是一种用于存储非敏感信息的 Kubernetes 对象。它用于存储配置数据&#xff0c;如键值对、整个配置文件或 JSON 数据等。ConfigMap 通常用于容器镜像中的配置文件、命令行参数和环境变量等。 ConfigMap 可以通过三种方…...

JS轮播图实现自动轮播、悬浮停止轮播、点击切换,下方指示器与图片联动效果

代码&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><s…...

使用 Kafka 和 MinIO 实现人工智能数据工作流

MinIO Enterprise Object Store 是用于创建和执行复杂数据工作流的基础组件。此事件驱动功能的核心是使用 Kafka 的 MinIO 存储桶通知。MinIO Enterprise Object Store 为所有 HTTP 请求&#xff08;如 PUT、POST、COPY、DELETE、GET、HEAD 和 CompleteMultipartUpload&#xf…...

力扣题86~90

题86&#xff08;中等&#xff09;&#xff1a; python代码 # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution:def partition(self, head: Optional[Li…...

【JavaEE】【多线程】定时器

目录 一、定时器简介1.1 Timer类1.2 使用案例 二、实现简易定时器2.1 MyTimerTask类2.2 实现schedule方法2.3 构造方法2.4 总代码2.5 测试 一、定时器简介 定时器&#xff1a;就相当于一个闹钟&#xff0c;当我们定的时间到了&#xff0c;那么就执行一些逻辑。 1.1 Timer类 …...

CI/CD 的原理

一、CI/CD 的概念 CI/CD是一种软件开发流程&#xff0c;旨在通过自动化和持续的集成、测试和交付实现高质量的软件产品。 CI(Continuous Integration)持续集成 目前主流的开发方式是协同开发&#xff0c;即多位开发人员同事处理同意应用不同模块或功能。 如果企业在同一时间将…...

进一步认识ICMP协议

在日常工作中&#xff0c;我们经常需要判断网络是否连通&#xff0c;相信大家使用较多的命令就是 ping啦。ping命令是基于 ICMP 协议来实现的&#xff0c;那么什么是 ICMP 协议呢&#xff1f;ping命令又是如何基于 ICMP 实现的呢&#xff1f; 今天这篇文章&#xff0c;我们就来…...

网站qq统计/河北百度推广电话

本文实例讲述了Python3使用requests模块实现显示下载进度的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;一、配置request1. 相关资料请求关键参数&#xff1a;streamTrue。默认情况下&#xff0c;当你进行网络请求后&#xff0c;响应体会立即被下载。你可以通过…...

阿勒泰高端网站建设公司/网络营销常见的工具

据权威英国《金融时报》调查&#xff1a;全球25个国家的1000位CEO、基金经理认为&#xff1a;微软仍是最受尊敬的公司&#xff0c;并在企业创新、投资价值两项中排名第一。 调查结果发现&#xff1a;商界舵手最重要的表现&#xff0c;是越来越注重领导的创新能力。在企业增长方…...

广州网站建设高端/手机百度下载免费

2019独角兽企业重金招聘Python工程师标准>>> 1、问题 通过一个死循环将读取键盘对应的设备文件将触发键盘事件在屏幕上打印出来&#xff0c;按esc退出程序 代码是在unbuntu10.04编译执行通过的 2、input_event描述 在Linux内核中&#xff0c;input设备用input_dev结…...

重庆网站空间键词排名/梅州seo

文章原创首发于微信公众号&#xff1a;邓行长(ID&#xff1a;Dhangzhang)&#xff0c;未经授权禁止转载一、你可能会懂银行柜台工作的苦银行的普通柜员工资也就4000-6000之间&#xff0c;拿的工资不算高。虽然个别支行效益好&#xff0c;绩效发的多&#xff0c;但是也不至于太高…...

学建站wordpress/口碑营销的经典案例

前言在 win10 的设置&#xff0c;把时间主要归类为两种格式&#xff1a;短时间和长时间。•短时间&#xff0c;即&#xff1a;时&#xff1a;分&#xff0c;如&#xff1a;10&#xff1a;20.•长时间&#xff0c;即&#xff1a;时&#xff1a;分&#xff1a;秒&#xff0c;如&a…...

网站备案教程/搜狗网站收录提交入口

分享一下我老师大神的人工智能教程&#xff01;零基础&#xff0c;通俗易懂&#xff01;http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识&#xff0c;造福人民&#xff0c;实现我们中华民族伟大复兴&#xff01;有些开发人员喜欢在客户端进行用户输入的检查…...