Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目)

本文主要是介绍Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

###异步IO机制
异步IO是Windows给我们读写文件提供的的一种的机制,在我们执行CreateFileEx函数是,通过传递相应的参数,就会向操作系统发送请求,那么CreateFileEx函数就会直接返回,它不会等到这个函数操作完成才返回,返回后,这个线程就可以做一些其它的操作,直到收到操作系统完成文件操作的通知,再去处理文件相关的操作,这样不会导致当前的线程发生阻塞;当操作系统收到这个请求时,就会进行实际的操作文件,当实际的操作完成后,它会通知执行CreateFileEx的线程,告诉线程可以进行文件操作了。

这里写图片描述

###异步操作-CreateFile
我们再来思考一个问题,同步IO为什么会导致程序阻塞?
首先我们先说两个概念,进程和线程。
进程是指的是当前程序运行时所占用的空间,也就是说线程主要是来做存储的事情;
线程是实际的运行单元(工作),是与CPU直接打交道的。
所以当我们执行某些操作导致阻塞时,实际上指的是线程被阻塞了。
我们举一个不是很恰当的例子,进程就好比我们实际生活中的工厂,工厂本身是不能工作的,它只是说明占地面积是多少,拥有多少资源等等,而实际工作的是工厂里面的工人,一个工人就好比一个线程,此时就有同学再问,那工厂不一定只有一个工人吧,应该有多个工人吧?对了,此时我们在程序中就成为多线程。
多线程是一个进程中有多个线程在同时运行,我们称之为多线程。
那么想用实现异步IO操作时,我们可以使用使用创建线程来完成,我们也可以使用系统线程来完成。

###OVERLAPPED结构体
OVERLAPPED结构体定义如下:

typedef struct _OVERLAPPED {ULONG_PTR Internal;ULONG_PTR InternalHigh;union {struct {DWORD Offset;DWORD OffsetHigh;} DUMMYSTRUCTNAME;PVOID Pointer;} DUMMYUNIONNAME;HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

我们先看下面的一个结构,它是用两个DWORD变量组成一个64位的变量,

struct {DWORD Offset;DWORD OffsetHigh;} DUMMYSTRUCTNAME;

我们原来以同步IO方式打开一个对象时,这个对象保存了一个位置,我们可以通过函数来设置这个位置。但是以异步IO方式打开一个对象时, 这个对象里面并没有保存这个对象的位置,来开始进行访问,我们就需要设置这个结构体的值,来设置读取的位置。所以这个结构体设计的很巧妙,这个结构体可以帮我们完成文件分割的功能。
hEvent参数,它是一个事件内核对象,它会以事件的方式来通知我们的线程函数执行情况。实际上我们在实际的工作中,不仅仅只放一个hEvent内核对象,因为HANDLE就是一个void*指针,所以我们完全可以放一些其它对象的。
Internal参数主要是用来保存请求的错误码。
InternalHigh参数用来保存读取成功的字节数。

下面四种方法可以对异步I/O进行提醒

  1. 设备内核对象
  2. 事件内核对象(Windows中用途非常广泛的一种内核对象,它的作用主要用于同步以及交互,与设备是一种完全不同)
  3. 可提醒I/O(不可跨线程)
  4. I/O完成端口,

###异步IO简单实例
1.设备内核对象进行I/O提醒的例子
这里写图片描述

DWORD WINAPI WaitForSingleObject(_In_ HANDLE hHandle,_In_ DWORD  dwMilliseconds
);

Waits until the specified object is in the signaled state or the time-out interval elapses.
CSDN解释是这样的,翻译下来就是:
一直等待下去,直到指定对象达到信号态或者超过指定时间段。

2.事件内核对象对异步I/O进行操作的例子

// 事件内核对象例子
int main()
{HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr);if (hFile != INVALID_HANDLE_VALUE){// ReadBYTE bReadBuffer[MAXBYTE] = { 0 };OVERLAPPED oRead = { 0 };oRead.Offset = 0;oRead.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("ReadEvent"));	// 创建一个事件内核对象ReadFile(hFile, bReadBuffer, sizeof(bReadBuffer), nullptr, &oRead);// WriteBYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };OVERLAPPED oWrite = { 0 };oWrite.Offset = 0;oWrite.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("WriteEvent"));	// 创建一个事件内核对象ReadFile(hFile, bWriteBuffer, sizeof(bWriteBuffer), nullptr, &oWrite);// Do Something// 其它的线程HANDLE hOverlapped[2] = { 0 };hOverlapped[0] = oRead.hEvent;hOverlapped[1] = oWrite.hEvent;while (true){DWORD dwCase = WaitForMultipleObjects(2, hOverlapped, FALSE, INFINITE);switch (dwCase-WAIT_OBJECT_0){case 0:	// 读完成break;case 1:	// 写完成break;default:break;}}}else{GetLastError();}return 0;
}

3.可提醒I/O对异步I/O进行操作的例子

// 设备内核对象和事件内核对象相当于下面的过程
// 1.发送请求
// 2.做自己的事情
// 3.判断请求是否完成// 可提醒I/O相当于下面的过程
// 1.发送请求 -> 完成后,操作系统提醒我
// 2.做自己的事情// (可提醒I/O操作)
// APC
// 工厂(进程)->工人(线程)
//                线程内部有 APC机制,当线程闲置时候(准确的说法是,当线程是可提醒状态时),这是前提,APC列表中的事情自动执行(即一系列的函数,它们会被挨个的被执行)// MessageBox	->	阻塞(闲置下来),但是它并不是可提醒状态
// Wait	Sleep 这些函数才能真正使线程函数闲置下来,变为可提醒状态VOID CALLBACK FileIOCompletionRoutine(_In_    DWORD        dwErrorCode,_In_    DWORD        dwNumberOfBytesTransfered,_Inout_ LPOVERLAPPED lpOverlapped
)
{MessageBoxW(nullptr, TEXT("Read"), TEXT("Tips"), MB_OK);
}// 可提醒I/O例子
int main()
{HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr);if (hFile != INVALID_HANDLE_VALUE){const UINT uLen = 255;BYTE bReadBuf[uLen] = { 0 };OVERLAPPED oRead = { 0 };oRead.Offset = 5;// 注意: 必须是ReadFileEx函数,才能设置线程为可提醒状态ReadFileEx(hFile, bReadBuf, uLen, &oRead, FileIOCompletionRoutine);}// 只有设置为TRUE时候,APC函数才能被调用// 如果使用Sleep函数,则不会弹出对话框// 如果没有SleepEx,那么这个线程不是可提醒状态,所以不弹出对话框SleepEx(100, TRUE);// 除了SleepEx函数外,还有其他的函数,也可以让线程处于可提醒状态,比如Wait等函数// 可提醒I/O实际上是不好用的,因为回调函数里面的参数没有任何作用,因为我们不知道读到了什么值,只是知道多了多少个,因此没什么用。// 所以不建议使用这种方式// 但是APC的这套机制还是很好的(只不过不适合用在I/O上面),它能将我们的函数放到APC列表里面,我们可以理解APC是一个不定时的定时器,只要线程设置为可提醒状态,APC中的函数就能被执行。return 0;
}

运行效果如图所示:
这里写图片描述

4.I/O完成端口对异步I/O进行操作的例子
这里写图片描述

// 完成端口
// 串行模型来京异步I/O操作
// 并行模型 -> 多线程// 1个工人 -> 5天 串行
// 5个工人 -> 1天 并行// 单核 -> 模拟出来的多进程   线程
// 多喝 -> 多进程            核心数 -> #define IOCP_KEY_READ	1// 完成端口例子
// 完成端口时Windows下一系列函数,是Windows给我们提供的一整套工具
// 天生就是一个并行模式
// 所以在Windows下进行异步I/O操作时,使用完成端口效率要高,但是这个也并不是绝对的,一定是在操作大文件时,效率才会提高,对于小文件,有可能效率还会降低
int main()
{// 一个完成端口,会// 创建设备队列// 创建设备操作队列// 创建线程池(多个线程)HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);	// 创建一个完成端口,第四个参数最重要,需要的线程数// 此时传递的0,表示的是默认个数,比如一个核心,它会创建一个线程// 就是有几个核心,就会创建几个线程// 但是并不建议创建太大的线程数,HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr);//HANDLE hIOCP = CreateIoCompletionPort(hFile, nullptr, IOCP_KEY_READ, 0);	// 这一行代码相当于前面的两行代码,创建并绑定// 和设备绑定CreateIoCompletionPort(hFile, hIOCP, IOCP_KEY_READ, 0);// 插入一个请求PostQueuedCompletionStatus(hFile, );// 该如何操作GetQueuedCompletionStatus(hIOCP, );// Windows CopyFile 使用完成端口来实现这个小项目return 0;
}

###使用I/O完成端口实现高效的文件拷贝小项目

#include <Windows.h>
#include <iostream>#define IOCP_KEY_READ	1
#define IOCP_KEY_WRITE	2int main()
{LPCTSTR lpstrSrcFilePath = TEXT("Demo.txt");LPCTSTR lpstrDestFilePath = TEXT("Demo-Clone.txt");BOOL bOk = FALSE;BOOL bComplete = FALSE;do{// 1.打开一个设备(用来读)HANDLE hSrcFile = CreateFile(lpstrSrcFilePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr);if (hSrcFile == INVALID_HANDLE_VALUE)break;// 2.打开一个设备(用来写)HANDLE hDestFile = CreateFile(lpstrDestFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, hSrcFile);if (hDestFile == INVALID_HANDLE_VALUE)break;// 3.获取文件大小LARGE_INTEGER liFileSize;if (!GetFileSizeEx(hSrcFile, &liFileSize))break;// 4.设置文件指针if (!SetFilePointerEx(hDestFile, liFileSize, nullptr, FILE_BEGIN))break;// 5.设置文件末尾if (!SetEndOfFile(hDestFile))break;// 6.获取磁盘扇区大小DWORD dwBytePerSector = 0;if (!GetDiskFreeSpace(TEXT("C:"), nullptr, &dwBytePerSector, nullptr, nullptr))break;// 7.获取系统信息SYSTEM_INFO sysInfo = { 0 };GetSystemInfo(&sysInfo);// 8.创建I/O完成端口HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, sysInfo.dwNumberOfProcessors); // 需要注意的是最后一个参数传递0,和传递 sysInfo.dwNumberOfProcessors 效果是一样的,这里就不多加说明了if (hIOCP == NULL){DWORD dwError = GetLastError();if (dwError != ERROR_ALIAS_EXISTS){// 此时才是真正的创建失败break;}}// 9.将读和写的IOCP绑定到设备列表中hIOCP = CreateIoCompletionPort(hSrcFile, hIOCP, IOCP_KEY_READ, sysInfo.dwNumberOfProcessors);hIOCP = CreateIoCompletionPort(hDestFile, hIOCP, IOCP_KEY_WRITE, sysInfo.dwNumberOfProcessors);OVERLAPPED oRead = { 0 }, oWrite = { 0 };// 10.往IOCP完成队列里面发送一个写的项// 否则GetQueuedCompletionStatus函数会一直阻塞的那里,因为此时并没有任意一件事情(读或写)PostQueuedCompletionStatus(hIOCP, 0, IOCP_KEY_WRITE, &oWrite);// 一般在另一个线程做,但是在这里,我们就用本线程来完成DWORD dwByteTrans = 0;ULONG_PTR ulKey = 0;LPOVERLAPPED lpOverlapped = nullptr;// 11.分配空间,和new出来的空间一样SIZE_T sizeLen = dwBytePerSector * 1024;LPVOID lpAddr = VirtualAlloc(nullptr, sizeLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);while (true){BOOL bRet = GetQueuedCompletionStatus(hIOCP, &dwByteTrans, &ulKey, &lpOverlapped, INFINITE);if (bRet == FALSE){if (lpOverlapped == NULL){// 失败或者超时break;}else{continue;}}switch (ulKey){case IOCP_KEY_READ:{// 写操作 WriteFile// 主要对overlapped结构体进行更新,更新offsetWriteFile(hDestFile, lpAddr, sizeLen, nullptr, &oWrite);LARGE_INTEGER liReadLen;liReadLen.QuadPart = dwByteTrans;if (oWrite.Offset + dwByteTrans == liFileSize.LowPart){// 读写完成,程序退出bComplete = TRUE;if (!SetEndOfFile(hDestFile))break;}oRead.Offset += liReadLen.LowPart;oRead.OffsetHigh += liReadLen.HighPart;}break;case IOCP_KEY_WRITE:{// 更新offsetLARGE_INTEGER liWriteLen;liWriteLen.QuadPart = dwByteTrans;oWrite.Offset += liWriteLen.LowPart;oWrite.OffsetHigh += liWriteLen.HighPart;// 判断当前文件长度ReadFile(hSrcFile, lpAddr, sizeLen, nullptr, &oRead);}break;default:break;}if (bComplete){// 实际上完成端口是不应该退出的,应该和程序的生命周期一样的break;}}CloseHandle(hSrcFile);CloseHandle(hDestFile);bOk = TRUE;}while (false);if (!bOk){DWORD dwError = GetLastError();std::cout << "ErrorCode: " << dwError << std::endl;}return 0;
}

###上面文件拷贝大文件小项目的改进
细心的朋友一定会发现,上面高效的文件拷贝小项目貌似并不是很完美,原因是当文件超过4GB的时候,它就会出现问题,为了解决这个问题,将部分代码做了修改,主要是在oRead和oWrite两个结构体变量赋值的时候出现的问题,不仔细说了,直接上代码吧,相信这一次会令您满意的。

#include <Windows.h>
#include <iostream>#define IOCP_KEY_READ   1
#define IOCP_KEY_WRITE  2int main()
{LPCTSTR lpstrSrcFilePath = TEXT("Demo.iso");LPCTSTR lpstrDestFilePath = TEXT("Demo-Clone.iso");BOOL bOk = FALSE;BOOL bComplete = FALSE;do{// 1.打开一个设备(用来读)HANDLE hSrcFile = CreateFile(lpstrSrcFilePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr);if (hSrcFile == INVALID_HANDLE_VALUE)break;// 2.打开一个设备(用来写)HANDLE hDestFile = CreateFile(lpstrDestFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, hSrcFile);if (hDestFile == INVALID_HANDLE_VALUE)break;// 3.获取文件大小LARGE_INTEGER liFileSize;if (!GetFileSizeEx(hSrcFile, &liFileSize))break;// 4.设置文件指针if (!SetFilePointerEx(hDestFile, liFileSize, nullptr, FILE_BEGIN))break;// 5.设置文件末尾if (!SetEndOfFile(hDestFile))break;// 6.获取磁盘扇区大小DWORD dwBytePerSector = 0;if (!GetDiskFreeSpace(TEXT("C:"), nullptr, &dwBytePerSector, nullptr, nullptr))break;// 7.获取系统信息SYSTEM_INFO sysInfo = { 0 };GetSystemInfo(&sysInfo);// 8.创建I/O完成端口HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, sysInfo.dwNumberOfProcessors); // 需要注意的是最后一个参数传递0,和传递 sysInfo.dwNumberOfProcessors 效果是一样的,这里就不多加说明了if (hIOCP == NULL){DWORD dwError = GetLastError();if (dwError != ERROR_ALIAS_EXISTS){// 此时才是真正的创建失败break;}}// 9.将读和写的IOCP绑定到设备列表中hIOCP = CreateIoCompletionPort(hSrcFile, hIOCP, IOCP_KEY_READ, sysInfo.dwNumberOfProcessors);hIOCP = CreateIoCompletionPort(hDestFile, hIOCP, IOCP_KEY_WRITE, sysInfo.dwNumberOfProcessors);OVERLAPPED oRead = { 0 }, oWrite = { 0 };// 10.往IOCP完成队列里面发送一个写的项// 否则GetQueuedCompletionStatus函数会一直阻塞的那里,因为此时并没有任意一件事情(读或写)PostQueuedCompletionStatus(hIOCP, 0, IOCP_KEY_WRITE, &oWrite);// 一般在另一个线程做,但是在这里,我们就用本线程来完成DWORD dwByteTrans = 0;ULONG_PTR ulKey = 0;LPOVERLAPPED lpOverlapped = nullptr;// 11.分配空间,和new出来的空间一样SIZE_T sizeLen = dwBytePerSector * 1024;LPVOID lpAddr = VirtualAlloc(nullptr, sizeLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);LARGE_INTEGER liReadLen = {0}, liWriteLen = {0};while (true){BOOL bRet = GetQueuedCompletionStatus(hIOCP, &dwByteTrans, &ulKey, &lpOverlapped, INFINITE);if (bRet == FALSE){if (lpOverlapped == NULL){// 失败或者超时break;}else{continue;}}switch (ulKey){case IOCP_KEY_READ:{// 写操作 WriteFile// 主要对overlapped结构体进行更新,更新offsetWriteFile(hDestFile, lpAddr, sizeLen, nullptr, &oWrite);liReadLen.QuadPart += dwByteTrans;if (oWrite.Offset + dwByteTrans == liFileSize.LowPart && oWrite.OffsetHigh == liFileSize.HighPart){// 读写完成,程序退出bComplete = TRUE;if (!SetEndOfFile(hDestFile))break;}oRead.Offset = liReadLen.LowPart;oRead.OffsetHigh = liReadLen.HighPart;}break;case IOCP_KEY_WRITE:{// 更新offsetliWriteLen.QuadPart += dwByteTrans;oWrite.Offset = liWriteLen.LowPart;oWrite.OffsetHigh = liWriteLen.HighPart;// 判断当前文件长度ReadFile(hSrcFile, lpAddr, sizeLen, nullptr, &oRead);}break;default:break;}if (bComplete){// 实际上完成端口是不应该退出的,应该和程序的生命周期一样的break;}}CloseHandle(hSrcFile);CloseHandle(hDestFile);bOk = TRUE;}while (false);if (!bOk){DWORD dwError = GetLastError();std::cout << "ErrorCode: " << dwError << std::endl;}return 0;
}

这篇关于Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1036084

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi