epoll() 多路复用 和 两种工作模式

news2024/9/23 5:18:15

1.epoll API 介绍

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

struct epoll_event {
	uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:
	- EPOLLIN
	- EPOLLOUT
	- EPOLLERR
	
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- op : 要进行什么操作
				EPOLL_CTL_ADD: 添加
				EPOLL_CTL_MOD: 修改
				EPOLL_CTL_DEL: 删除
		- fd : 要检测的文件描述符
		- event : 检测文件描述符什么事情

// 检测函数----检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- events : 传出参数,保存了发送了变化的文件描述符的信息
		- maxevents : 第二个参数结构体数组的大小
		- timeout : 阻塞时间
			- 0 : 不阻塞
			- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
			- > 0 : 阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数 > 0
		- 失败 -1

// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);

// epoll 的使用
// 操作步骤
// 在服务器使用 epoll 进行 IO 多路转接的操作步骤如下:
    1.创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    2.设置端口复用(可选)
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    3.使用本地的IP与端口和监听的套接字进行绑定
    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    4.给监听的套接字设置监听
    listen(lfd, 128);

    5.创建 epoll 实例
    int epfd = epoll_create(100);

    6.将用于监听的套接字添加到 epoll 实例中
    struct epoll_event ev;
    ev.events = EPOLLIN; //检测lfd读缓冲区是否有数据
    ev.data.fd = lfd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    接着创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event evs[1024];

    7.检测添加到epoll实例中的文件描述符是否已经就绪,并将这些已就绪的文件描述符进行处理
    int num = epoll_wait(epfd, evs, size, -1);

    ① 如果监听的是文件描述符,和新客户端建立连接,将得到的文件描述符添加到epoll实例中
    int cfd = accept(curfd,NULL,NULL);
    ev.events = EPOLLIN;
    ev.data.fd = cfd;

    新得到的文件描述符添加到epoll模型中,下一轮循环的时候就可以被检测了
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

    ② 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除
    int len = recv(curfd,buf,sizeof(buf),0);
    if(len == 0) {
        // 将这个文件描述符从epoll实例中删除
        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
        close(curfd);
    }else if(len > 0) {
        send(curfd,buf,len,0);
    }

    8.重复第 7 步的操作

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

int main() {
    
    // 创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd,8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);
    
    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    // 创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event epevs[1024];
    while (1) {
        ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }
        printf("ret = %d\n",ret);
        for(int i = 0;i < ret;i++) {
            int curfd = epevs[i].data.fd;
            if(curfd == lfd) {
                // 监听的文件描述符有数据到达,有客户端连接
                struct sockaddr_in caddr;
                int len = sizeof(caddr);
                int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);

                // epev.events = EPOLLIN | EPOLLOUT;
                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);

            } else {
                // if(epevs[i].events & EPOLLOUT) {
                //     continue;
                // }
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd,buf,sizeof(buf));
                if (len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("recv buf = %s\n",buf);
                    write(curfd,buf,strlen(buf) + 1);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

client.c

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char* argv[]) {
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET,"127.0.0.1",&saddr.sin_addr.s_addr);

    // 连接服务器
    int ret = connect(fd,(struct sockaddr*)&saddr,sizeof(saddr));

    if(ret == -1) {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf,"send data %d",num++);
        write(fd,sendBuf,strlen(sendBuf) + 1);

        // 接收
        int len = read(fd,sendBuf,sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n",sendBuf);
        }else{
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        usleep(1000);
    }
    
    close(fd);
    return 0;
}

2.epoll 的两种工作模式 

Epoll 的工作模式:
	LT 模式 (水平触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
				b.用户只读了一部分数据,epoll会通知
				c.缓冲区的数据读完了,不通知
	
	LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这
	种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操
	作。如果你不作任何操作,内核还是会继续通知你的。

	ET 模式(边沿触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不通知了
				b.用户只读了一部分数据,epoll不通知
				c.缓冲区的数据读完了,不通知

	ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述
	符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,
	并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述
	符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成
	未就绪),内核不会发送更多的通知(only once)。
	
	ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll
	工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写
	操作把处理多个文件描述符的任务饿死。

【注意】 ET模式需要配合循环+非阻塞

(1)LT 模式

epoll_lt.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

int main() {
    
    // 创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd,8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);
    
    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    // 创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event epevs[1024];
    while (1) {
        ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }
        printf("ret = %d\n",ret);
        for(int i = 0;i < ret;i++) {
            int curfd = epevs[i].data.fd;
            if(curfd == lfd) {
                // 监听的文件描述符有数据到达,有客户端连接
                struct sockaddr_in caddr;
                int len = sizeof(caddr);
                int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);

                // epev.events = EPOLLIN | EPOLLOUT;
                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);

            } else {
                // if(epevs[i].events & EPOLLOUT) {
                //     continue;
                // }
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd,buf,sizeof(buf));
                if (len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("recv buf = %s\n",buf);
                    write(curfd,buf,strlen(buf) + 1);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

(2)ET 模式

struct epoll_event {
    uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:
    - EPOLLIN
    - EPOLLOUT
    - EPOLLERR
    - EPOLLET
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    
    // 创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd,8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);
    
    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    // 创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event epevs[1024];
    while (1) {
        ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }
        printf("ret = %d\n",ret);
        for(int i = 0;i < ret;i++) {
            int curfd = epevs[i].data.fd;
            if(curfd == lfd) {
                // 监听的文件描述符有数据到达,有客户端连接
                struct sockaddr_in caddr;
                int len = sizeof(caddr);
                int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);
                
                // 设置cfd属性非阻塞
                int flag = fcntl(cfd,F_GETFL);
                flag |= O_NONBLOCK; 
                fcntl(cfd,F_SETFL,flag);

                epev.events = EPOLLIN | EPOLLET;// 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);

            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }
                
                // 循环读取出所有的数据
                char buf[5];
                int len = 0;
                while ((len = read(curfd,buf,sizeof(buf))) > 0) {
                    // 打印数据
                    // printf("recv data : %s\n",buf);
                    write(STDOUT_FILENO,buf,len);
                    write(curfd,buf,len);
                }

                if(len == 0) {
                    printf("client closed...\n");
                }else if(len == -1) {
                    if(errno == EAGAIN) {
                        printf("data over......\n");
                    } else {
                        perror("read");
                        exit(-1);
                    }
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

client.c

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char* argv[]) {
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET,"127.0.0.1",&saddr.sin_addr.s_addr);

    // 连接服务器
    int ret = connect(fd,(struct sockaddr*)&saddr,sizeof(saddr));

    if(ret == -1) {
        perror("connect");
        return -1;
    }

    int num = 0;
    while (1) {
        char sendBuf[1024] = {0};
        // sprintf(sendBuf,"send data %d",num++);
        fgets(sendBuf,sizeof(sendBuf),stdin);
        write(fd,sendBuf,strlen(sendBuf) + 1);

        // 接收
        int len = read(fd,sendBuf,sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n",sendBuf);
        }else{
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        // usleep(1000);
    }
    
    close(fd);
    return 0;
}

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

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

相关文章

C#---第十九课:不同类型方法的执行顺序(new / virtual / common / override)

本文介绍不同类型的方法&#xff0c;在代码中的执行顺序问题&#xff1a; 构造方法普通方法&#xff08;暂用common代替&#xff09;、虚方法&#xff08;Virtual修饰&#xff09;、New方法&#xff08;new修饰&#xff09;三个优先级相同overide方法&#xff08;会替换virtual…

Arduino驱动TEMT6000传感器(光照传感器篇)

目录 1、传感器特性 2、硬件原理图 3、驱动程序 TEMT6000是一个三极管类型的光敏传感器,其光照强度和基极的电流成正比。用起来也相当简单,可以简单的连接该传感器的基极到模拟电压输入,通过简单的检测电压值就可以判断当前的光照强度。 1、

基于Linux操作系统的keepalived双机热备和keepalived+lvs(DR)基本配置操作

目录 keepalived双机热备 一、概述 &#xff08;一&#xff09;具体工作原理如下&#xff1a; &#xff08;二&#xff09;实验拓补 二、安装NFS、配置 1、第一台机器配置&#xff1a;NFS &#xff1a;192.168.11.101 2、更改配置文件 3、安装NFS进行目录共享 4、编辑…

花5分钟判断,你的Jmeter技能是大佬还是小白!

jmeter 这个工具既可以做接口的功能测试&#xff0c;也可以做自动化测试&#xff0c;还可以做性能测试&#xff0c;其主要用途就是用于性能测试。但是&#xff0c;有些公司和个人&#xff0c;就想用 jmeter 来做接口自动化测试。 你有没有想过呢&#xff1f; 下面我就给大家讲…

C# textBox1.Text=““与textBox1.Clear()的区别

一、区别 textbox.Text "" 和 textbox.Clear() 都可以用于清空文本框的内容&#xff0c;但它们之间有一些细微的区别。 textbox.Text "": 这种方式会将文本框的 Text 属性直接设置为空字符串。这样会立即清除文本框的内容&#xff0c;并将文本框显示为空…

git 基础入门

Git基础入门 Git是一个分布式 版本管理系统&#xff0c;用于跟踪文件的变化和协同开发。 版本管理&#xff1a;理解成档案馆&#xff0c;记录开发阶段各个版本 分布式&集中式 分布式每个人都有一个档案馆&#xff0c;集中式只有一个档案馆。分布式每人可以管理自己的档案…

ThinkPHP 验证码扩展库的使用,以及多应用模式下,如何自定义验证码校验规则

ThinkPHP 验证码扩展库的使用&#xff0c;以及多应用模式下&#xff0c;如何自定义验证码校验规则 一、安装二、页面使用三、验证码相关配置属性1. 自定义验证码配置2. 自定义验证码&#xff08;一&#xff09;普通验证码3. 自定义验证码&#xff08;二&#xff09;算数验证码4…

搭建web网站

1.基于域名www.openlab.com可以访问网站内容为welcome to openlab!!! (1).安装所需软件HTTPD、mod_ssl [rootserver ~]# yum install httpd mod_ssl -y 添加域名映射&#xff1a;vim /etc/hosts (2)创建网站目录及网页&#xff0c;修改主配置文件新建openlab目录网站 配置文…

第二届828 B2B企业节启动,华为云携手上万伙伴共筑企业应用一站购平台

当前&#xff0c;数字技术与实体经济深度融合&#xff0c;为千行百业注入新动力、拓展新空间。数据显示&#xff0c;2022年中国数字经济规模超过50万亿&#xff0c;占GDP比重超过40%&#xff0c;继续保持在10%的高位增长速度&#xff0c;成为稳定经济增长的关键动力。 为加速企…

DevOps系列文章之 Python基础

列表 Python中的列表类似于C语言中的数组的概念&#xff0c;列表由内部的元素组成&#xff0c;元素可以是任何对象 Python中的列表是可变的 简单的理解就是&#xff1a;被初始化的列表&#xff0c;可以通过列表的API接口对列表的元素进行增删改查 1、定义列表 1.可以将列表当成…

网络模型分析

# 用户空间和内核空间 # 阻塞IO # 非阻塞IO # IO多路复用 IO多路复用-select 内核中遍历找到就绪的fd并保留&#xff0c;不匹配的就置为0&#xff0c; 以上的操作重复&#xff0c;知道所有的FD都完成 IO多路复用-poll IO多路复用-epoll IO多路复用-事件通知机制 很多进程都要用…

WebGL非矩阵变换

目录 平移 示例代码&#xff1a; 齐次坐标矢量的最后一个分量w 旋转 p的坐标&#xff0c;可得等式 R1&#xff1a; 使用r、α、β来表示点p的坐标&#xff0c;可得等式 R2&#xff1a; 利用三角函数两角和公式&#xff0c;可得等式 R3&#xff1a; 最后&#xff0c;将…

8.27周报

文章目录 前言论文阅读摘要介绍模型算法 总结 前言 本周学习了GAN论文《Generative Adversarial Nets》&#xff0c;了解GAN主要由两部分组成&#xff1a;生成器和判别器&#xff0c;知道生成器G和判别器D的作用及原理&#xff0c;相比于其他的生成模型&#xff0c;了解GAN的优…

【java】【项目实战】[外卖六]套餐管理业务开发

目录 一、新增套餐 1 需求分析 2 数据模型 3 代码实现 3.1 实体类SetmealDish 3.2 SetmealDto 3.3 SetmealDishMapper 3.4 SetmealDishService 3.5 SetmealDishServiceImpl 3.6 SetmealController 3.7 DishController 3.8 SetmealService 3.9 SetmealServiceImp…

4.21 用了 TCP 协议,数据一定不会丢吗?

目录 数据包的发送流程: 建立连接时丢包 流量控制丢包 网卡丢包 RingBuffer过小导致丢包 网卡性能不足 接收缓冲区丢包 两端之间的网络丢包 ping命令查看丢包&#xff1a; mtr命令&#xff1a; 发生丢包了怎么办 用了TCP协议就一定不会丢包吗​编辑 这类丢包问题怎…

Mysql--技术文档--基本概念--《世界上最流行的关系型数据库之一》

官方网址 MySQL 阿丹&#xff1a; 作为关系型数据库管理的老大哥&#xff0c;一个合格的程序员多多少少一定要了解mysql库。 官方解释 MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管…

浙大MBA冲刺上岸经验分享:紧急备战的大龄考生

前些天我在朋友圈分享了一组我们前段时间在浙大开学前同学聚会的照片。结果&#xff0c;许多朋友都开始询问与浙大有关的情况&#xff0c;并且他们最关心的问题集中在以下几点&#xff1a;我去年备考花费了多少时间&#xff1f;要不要报班学习&#xff1f;现在开始备考还有机会…

postman-使用Postman的模拟服务来模拟(mock)后端数据,完成前端模拟API调用

最近项目上比较忙&#xff0c;任务多时间紧&#xff0c;导致后端开发任务繁多&#xff0c;无法及时开发完毕&#xff0c;但是前端同学已经把对应功能开发完成&#xff0c;需要进行前后端联调来验证API及一些交互问题&#xff1b;这不能因为后端的进度来影响前端的工作完成情况&…

c++ 判断基类指针指向的真实对象类型

在 c 面向对象使用中&#xff0c;我们常常会定义一个基类类型的指针&#xff0c;在运行过程中&#xff0c;这个指针可能指向一个基类类型的对象&#xff0c;也可能指向的是其子类类型的对象&#xff0c;那现在问题来了&#xff0c;我们如何去判断这个指针到底执行了一个什么类型…

FreeMarker使用

说明&#xff1a;FreeMake可以通过设置一个模板&#xff0c;使用一些语法规则&#xff0c;可根据返回的VO数据填充到这个模板中&#xff0c;生成一个静态。这个技术&#xff0c;在项目中可以实现如预览页面的功能&#xff0c;将查询完成的VO数据按照这个模板填充&#xff0c;生…