测试与FastAPI应用数据之间的差异
【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等
当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时,可能会出现以下错误:
-
在测试中,在数据库中创建了一个对象(测试会话)。
-
在应用程序中发出一个请求,在此请求中修改了这个对象(应用会话)。
-
在测试中从数据库加载对象,但其中没有所需的更改(测试会话)。
让我们找出发生了什么。
我们通常在应用程序和测试中使用两个不同的会话。
此外,在测试中,我们通常将会话包装在一个准备数据库进行测试的fixture中,测试完成后,所有内容都会被清理。
以下是应用程序的示例。
一个带有数据库连接的文件app/database.py:
""" Database settings file """
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base
DATABASE_URL = "postgresql+asyncpg://user:password@host:5432/dbname"
engine = create_async_engine(DATABASE_URL, echo=True, future=True)
async_session = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
async def get_session() -> AsyncGenerator:""" Returns async session """async with async_session() as session:yield session
Base = declarative_base()
一个带有模型描述的文件app/models.py:
""" Model file """
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from .database import Base
class Lamp(Base):
""" Lamp model """
__tablename__ = 'lamps'
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
status: Mapped[str] = mapped_column(String, default="off")
一个带有端点描述的文件app/main.py:
""" Main file """
import logging
from fastapi import FastAPI, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .database import get_session
from .models import Lamp
app = FastAPI()
@app.post("/lamps/{lamp_id}/on")
async def check_lamp(
lamp_id: int,
session: AsyncSession = Depends(get_session)
) -> dict:
""" Lamp on endpoint """
results = await session.execute(select(Lamp).where(Lamp.id == lamp_id))
lamp = results.scalar_one_or_none()
if lamp:
logging.error("Status before update: %s", lamp.status)
lamp.status = "on"
session.add(lamp)
await session.commit()
await session.refresh(lamp)
logging.error("Status after update: %s", lamp.status)
return {}
我特意在示例中添加了日志记录和一些其他请求,以使其更加清晰。
这里,使用Depends创建了一个会话。
以下是带有测试示例的文件tests/test_lamp.py:
""" Test lamp """
import logging
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.database import Base, engine
from app.main import app, Lamp
@pytest_asyncio.fixture(scope="function", name="test_session")
async def test_session_fixture() -> AsyncGenerator:""" Async session fixture """async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:async with engine.begin() as conn:await conn.run_sync(Base.metadata.create_all)
yield session
async with engine.begin() as conn:await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.mark.asyncio
async def test_lamp_on(test_session):""" Test lamp switch on """lamp = Lamp()test_session.add(lamp)await test_session.commit()await test_session.refresh(lamp)
logging.error("New client status: %s", lamp.status)assert lamp.status == "off"
async with AsyncClient(app=app, base_url="http://testserver") as async_client:response = await async_client.post(f"/lamps/{lamp.id}/on")assert response.status_code == 200results = await test_session.execute(select(Lamp).where(Lamp.id == lamp.id))new_lamp = results.scalar_one_or_none()logging.error("Updated status: %s", new_lamp.status)
assert new_lamp.status == "on"
这是一个常规的Pytest,它在一个fixture中获取到数据库的会话。在返回会话之前,此fixture会先创建所有的表格,使用之后,它们会被删除。
请再次注意,在测试中,我们使用来自test_session fixture的会话,而在主代码中,我们使用来自app/database.py文件的会话。尽管我们使用相同的引擎,但是生成的会话是不同的。这一点很重要。
预期的数据库请求序列
应从数据库返回status = on。
在测试中,我首先在数据库中创建一个对象。这是通过来自测试的会话进行的常规INSERT。我们称其为Session 1。此时,只有这个会话连接到了数据库。应用程序会话尚未连接。
在创建对象之后,我执行了一个刷新操作。这是通过Session 1对新创建的对象进行的SELECT,并通过实例更新。
结果,我确保对象正确创建,并且status字段填充了所需的值 - off。
然后,我对/lamps/1/on端点执行一个POST请求。这是打开灯的操作。为了使示例更短,我没有使用fixture。一旦请求开始工作,将创建一个新的数据库会话。我们称其为Session 2。使用这个会话,我从数据库加载所需的对象。我将状态输出到日志。它是off。之后,我更新了这个状态并在数据库中保存了更新。数据库收到了一个请求:
BEGIN (implicit)
UPDATE lamps SET status=$1::VARCHAR WHERE lamps.id = $2::INTEGER
parameters: ('on', 1)
COMMIT
请注意,也存在COMMIT命令。尽管事务是隐式的,但其结果在其他会话中在COMMIT之后立即可用。
接下来,我使用refresh发出请求,从数据库获取更新后的对象。我输出状态。现在它的值是on。
看起来一切都应该正常工作。端点停止工作,关闭Session 2,并将控制权转移到测试。
在测试中,我从Session 1发出常规请求,以获取修改过的对象。但在状态字段中,我看到的值是off。
以下是代码中操作序列的方案。
代码中的操作序列
代码中的操作序列 同时,根据所有日志,最后一个SELECT请求被执行并返回了status = on。此刻,数据库中的其值肯定等于on。这是engine asyncpg响应SELECT请求时接收的值。
那么,发生了什么?
发生了以下情况。
结果是,为获取新对象而做的请求并没有更新当前的对象,而是找到并使用了现有的对象。一开始,我使用ORM添加了一个灯泡对象。我在另一个会话中更改了它。当更改完成时,当前会话对此更改一无所知。并且在Session 2中进行的提交并未在Session 1中请求expire_all方法。
为了修复这个问题,你可以做以下操作之一:
-
对于测试和应用程序使用共享会话。
-
刷新实例,而不是尝试从数据库中获取它。
-
强制使实例过期。
-
关闭会话。
依赖性覆盖
为了使用相同的会话,您可以简单地使用我在测试中创建的那个覆盖应用程序中的会话。这很简单。
为此,我们需要在测试中添加以下代码:
async def _override_get_db():
yield test_session
app.dependency_overrides[get_session] = _override_get_db
如果你愿意,可以将这部分包装成一个夹具,以便在所有测试中使用。
所得到的算法将如下所示:
代码中使用依赖性覆盖时的步骤
下面是带有会话替代的测试代码:
@pytest.mark.asyncio
async def test_lamp_on(test_session):
""" Test lamp switch on """
async def _override_get_db():
yield test_session
app.dependency_overrides[get_session] = _override_get_db
lamp = Lamp()
test_session.add(lamp)
await test_session.commit()
await test_session.refresh(lamp)
logging.error("New client status: %s", lamp.status)
assert lamp.status == "off"
async with AsyncClient(app=app, base_url="http://testserver") as async_client:
response = await async_client.post(f"/lamps/{lamp.id}/on")
assert response.status_code == 200
results = await test_session.execute(select(Lamp).where(Lamp.id == 1))
new_lamp = results.scalar_one_or_none()
logging.error("Updated status: %s", new_lamp.status)
assert new_lamp.status == "on"
但是,如果应用程序使用多个会话(这是可能的),那么这可能不是最佳方法。此外,如果在被测试的函数中没有调用commit或rollback,那么这也将无济于事。
刷新
第二种解决方案是最简单和最有逻辑的。我们不应该创建一个新的请求来获取一个对象。为了更新,处理端点请求后立即调用刷新就足够了。在内部,它调用expires,这导致保存的实例不用于新的请求,并且数据重新填充。这种解决方案是最有逻辑的,也最容易理解。
await test_session.refresh(lamp)
之后,你不需要再试图加载new_lamp对象,只需检查相同的lamp。
以下是使用刷新的代码方案。
使用刷新时的代码中的步骤
以下是带有更新的测试代码。
@pytest.mark.asyncio
async def test_lamp_on(test_session):
""" Test lamp switch on """
lamp = Lamp()
test_session.add(lamp)
await test_session.commit()
await test_session.refresh(lamp)
logging.error("New client status: %s", lamp.status)
assert lamp.status == "off"
async with AsyncClient(app=app, base_url="http://testserver") as async_client:
response = await async_client.post(f"/lamps/{lamp.id}/on")
assert response.status_code == 200
await test_session.refresh(lamp)
logging.error("Updated status: %s", lamp.status)
assert lamp.status == "on"
过期
但是,如果我们更改了很多对象,最好调用expire_all。然后,所有实例都将从数据库中读取,一致性不会被破坏。
test_session.expire_all()
你还可以在特定实例甚至实例属性上调用expire。
test_session.expire(lamp)
在这些调用之后,你将不得不手动从数据库中读取对象。
以下是使用过期时代码中的步骤序列。
使用过期时的代码中的步骤
使用过期时的代码中的步骤 以下是带有过期的测试代码。
@pytest.mark.asyncio
async def test_lamp_on(test_session):
""" Test lamp switch on """
lamp = Lamp()
test_session.add(lamp)
await test_session.commit()
await test_session.refresh(lamp)
logging.error("New client status: %s", lamp.status)
assert lamp.status == "off"
async with AsyncClient(app=app, base_url="http://testserver") as async_client:
response = await async_client.post(f"/lamps/{lamp.id}/on")
assert response.status_code == 200
test_session.expire_all()
# OR:
# test_session.expire(lamp)
results = await test_session.execute(select(Lamp).where(Lamp.id == 1))
new_lamp = results.scalar_one_or_none()
logging.error("Updated status: %s", new_lamp.status)
assert new_lamp.status == "on"
关闭
实际上,使用会话终止的最后一种方法也调用了expire_all,但会话可以进一步使用。当读取新数据时,我们将获得最新的对象。
await test_session.close()
这应该在应用程序请求完成之后并在检查开始之前立即调用。
以下是使用关闭时代码中的步骤。
使用关闭时的代码中的步骤
以下是带有会话关闭的测试代码。
@pytest.mark.asyncio
async def test_lamp_on(test_session):
""" Test lamp switch on """
lamp = Lamp()
test_session.add(lamp)
await test_session.commit()
await test_session.refresh(lamp)
logging.error("New client status: %s", lamp.status)
assert lamp.status == "off"
async with AsyncClient(app=app, base_url="http://testserver") as async_client:
response = await async_client.post(f"/lamps/{lamp.id}/on")
assert response.status_code == 200
await test_session.close()
results = await test_session.execute(select(Lamp).where(Lamp.id == 1))
new_lamp = results.scalar_one_or_none()
logging.error("Updated status: %s", new_lamp.status)
assert new_lamp.status == "on"
调用 rollback() 也会有帮助。它也调用 expire_all,但它明确地回滚了事务。如果需要执行事务,commit() 也会执行 expire_all。但在这个例子中,既不需要回滚也不需要提交,因为测试中的事务已经完成,应用程序中的事务不会影响来自测试的会话。
实际上,此功能仅在SQLAlchemy ORM的异步模式中在事务中工作。然而,在代码中我确实向数据库发出请求以获取新对象的行为看起来似乎不合逻辑,如果它仍然返回一个缓存的对象,而不是从数据库强制接收的对象。当调试代码时,这有点令人困惑。但当正确使用时,这就是它应有的样子。
结论
在异步模式下使用SQLAlchemy ORM,您必须并行跟踪事务和线程中的会话。如果所有这些看起来太复杂,那么使用SQLAlchemy ORM的同步模式。它里面的一切都简单得多。
作者:Aleksei Sharypov
更多内容请关注公号【云原生数据库】
squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。
相关文章:
测试与FastAPI应用数据之间的差异
【squids.cn】 全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时,可能会出现以下错误: 在测试中,在数据库中创建了一个对象&#x…...
WebStorm 2023年下载、安装教程、亲测有效
文章目录 简介安装步骤常用快捷键 简介 WebStorm 是JetBrains公司旗下一款JavaScript 开发工具。已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的…...
k8s储存卷
卷的类型 In-Tree存储卷插件 ◼ 临时存储卷 ◆emptyDir ◼ 节点本地存储卷 ◆hostPath, local ◼ 网络存储卷 ◆文件系统:NFS、GlusterFS、CephFS和Cinder ◆块设备:iSCSI、FC、RBD和vSphereVolume ◆存储平台:Quobyte、PortworxVolume、Sto…...
【解决Win】“ 无法打开某exe提示无法成功完成操作,因为文件包含病毒或潜在的垃圾软件“
在下载某个应用程序,打开时出现了“无法成功完成操作因为文件包含病毒或潜在垃圾”的提示,遇到这个情况怎么解决? 下面为大家分享故障原因及具体的处理方法。 故障原因 是由于杀毒 防护等原因引起的。 解决方案 打开Windows 安全中心 选择…...
SpringBoot调用ChatGPT-API实现智能对话
目录 一、说明 二、代码 2.1、对话测试 2.2、单次对话 2.3、连续对话 2.4、AI绘画 一、说明 我们在登录chatgpt官网进行对话是不收费的,但需要魔法。在调用官网的API时,在代码层面上使用,通过API KEY进行对话是收费的,不过刚…...
element-table出现错位解决方法
先看示例图,这个在开发中还是很常遇到的,在table切换不同数据时或者切换页面时,容易出现: 解决方法很简单,官方有提供方法: 我们可以在重新渲染数据后: this.$nextTick(() > {this.$refs.…...
DC电源模块具有不同的安装方式和安全规范
BOSHIDA DC电源模块具有不同的安装方式和安全规范 DC电源模块是将低压直流电转换为需要的输出电压的装置。它们广泛应用于各种领域和行业,如通信、医疗、工业、家用电器等。安装DC电源模块应严格按照相关的安全规范进行,以确保其正常运行和安全使用。 D…...
zabbix自定义监控、钉钉、邮箱报警
目录 一、实验准备 二、安装 三、添加监控对象 四、添加自定义监控项 五、监控mariadb 1、添加模版查看要求 2、安装mariadb、创建用户 3、创建用户文件 4、修改监控模版 5、在上述文件中配置路径 6、重启zabbix-agent验证 六、监控NGINX 1、安装NGINX,…...
短信、邮箱验证码本地可以,部署到服务器接口却不能使用
应对公司双验证要求,对本系统做邮箱、短信验证码登录,本地开发正常发送,到服务器上部署却使用失败,已全部解决,记录坑。 一、nginx拦截 先打开你的服务器 nginx.conf 看看有没有做接口拦截。(本地可能做Sp…...
Java web基础知识
Servlet Servlet是sun公司开发的动态web技术 sun在API中提供了一个接口叫做 Servlet ,一个简单的Servlet 程序只需要完成两个步骤 编写一个实现了Servlet接口的类 把这个Java部署到web服务器中 一般来说把实现了Servlet接口的java程序叫做,Servlet 初步…...
【Linux学习】01Linux初识与安装
Linux(B站黑马)学习笔记 01Linux初识与安装 文章目录 Linux(B站黑马)学习笔记前言01Linux初识与安装操作系统简述Linux初识虚拟机介绍安装VMware Workstation虚拟化软件VMware中安装CentOS7 Linux操作系统下载CentOS操作系统VMwa…...
android 将数据库中的 BLOB 对象动态加载为 XML,并设置到 Android Activity 的内容视图上
以下是一个示例代码,演示如何将数据库中的 BLOB 对象动态加载为 XML,并设置到 Android Activity 的内容视图上: ```java import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import and…...
Android12之强弱智能指针sp/wp循环引用死锁问题(一百六十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…...
springboot自定义Json序列化返回,实现自动转换字典值
自定义序列化 原理 当你使用Spring Boot的Spring Data或者Spring MVC等组件来处理JSON序列化时,Spring框架会在需要将Java对象转换为JSON字符串时调用JsonSerializer。这通常发生在控制器方法返回JSON响应时,或者在将对象保存到数据库等操作中。 // 注册…...
Lostash同步Mysql数据到ElasticSearch(二)logstash脚本配置和常见坑点
1. logstash脚本编写(采用单文件对应单表实例) 新建脚本文件夹 cd /usr/local/logstash mkdir sql & cd sql vim 表名称.conf #如: znyw_data_gkb_logstash.conf 建立文件夹,保存资源文件更新Id mkdir -p /data/logstash/data/last_r…...
兔兔答题企业版1.0.0版本全网发布,同时开源前端页面到unicloud插件市场
项目说明 兔兔答题是用户端基于uniapp开发支持多端适配,管理端端采用TypeScriptVue3.jselement-plus,后端采用THinkPHP6PHP8Golang开发的一款在线答题应用程序。 问题反馈 线上预览地址 相关问题可以通过下方的二维码,联系到我。了解更多 …...
76、SpringBoot 整合 MyBatis------使用 sqlSession 作为 Dao 组件(就是ssm那一套,在 xml 写sql)
就是 ssm 那套,在xml 上面写sql ★ 基于SqlSession来实现DAO组件的方式 - MyBatis提供的Starter会自动在Spring容器中配置SqlSession(其实SqlSessionTemplate实现类)、并将它注入其他组件(如DAO组件)- DAO组件可直接…...
【ROS】RViz、Gazebo和Navigation的关系
1、RViz RViz(Robot Visualization,机器人可视化)是一个用于可视化机器人系统的开源工具,用于显示和调试机器人的传感器数据、状态信息和运动规划等。它是ROS(Robot Operating System)的一部分,是ROS中最常用的可视化工具之一。 RViz:“我们不生产数据只做数据的搬运…...
智能井盖:提升城市井盖安全管理效率
窨井盖作为城市基础设施的重要组成部分,其安全管理与城市的有序运行和群众的生产生活安全息息相关,体现城市管理和社会治理水平。当前,一些城市已经将智能化的窨井盖升级改造作为新城建的重要内容,推动窨井盖等“城市部件”配套建…...
JavaWeb开发-06-SpringBootWeb-MySQL
一.MySQL概述 1.安装、配置 官网下载地址:https://dev.mysql.com/downloads/mysql/ 2.数据模型 3.SQL简介 二.数据库设计-DDL 1.数据库 官网:http:// https://www.jetbrains.com/zh-cn/datagrip/ 2.表(创建、查询、修改、删除) #…...
十六、垃圾回收相关概念
目录 一、System.gc()的理解二、内存溢出和内存泄漏2、内存泄漏 三、Stop the World1、什么是 stop the word ? 四、垃圾回收的并行和并发1、并发和并发2、垃圾回收的并行和并发 五、安全点与安全区域1、什么是安全点?2、安全区域 六、强引用(不可回收&…...
hive、spark、presto 中的增强聚合-grouping sets、rollup、cube
目录 1、什么是增强聚合和多维分析函数? 2、grouping sets - 指定维度组合 3、with rollup - 上卷维度组合 4、with cube - 全维度组合 5、Grouping__ID、grouping() 的使用场景 6、使用 增强聚合 会不会对查询性能有提升呢? 7、对grouping sets、…...
elasticsearch bulk 批量操作
1:bulk 是 elasticsearch 提供的一种批量增删改的操作API bulk 对 JSON串 有着严格的要求。每个JSON串 不能换行 ,只能放在同一行,同时, 相邻的JSON串之间必须要有换行 (Linux下是\n;Window下是\r\n&#…...
力扣11、 盛最多水的容器
方法一:双指针 考察: 贪心、数组、双指针 说明 本题是一道经典的面试题,最优的做法是使用「双指针」。如果读者第一次看到这题,不一定能想出双指针的做法。 复杂度分析 时间复杂度:O(N),双指针总计最多…...
IIC协议详解
目录 1.IIC协议概述 2.IIC总线传输 3.IIC-51单片机应用 1.起始信号 2.终止信号 3.应答信号 4.数据发送 4.IIC-32单片机应用 用到的库函数: 1.IIC协议概述 IIC全称Inter-Integrated Circuit (集成电路总线)是由PHILIPS公司在80年代开发的两线式串行总线&…...
element ui-表头自定义提示框
版本 “element-ui”: “^2.15.5”,需求:鼠标悬浮到该列表头,显示提示框代码 <el-table:data"xxxx"><el-table-column label"序号" width"40" type"index" /><el-table-columnv-for"(ite…...
Python 图形化界面基础篇:创建顶部菜单
Python 图形化界面基础篇:创建顶部菜单 引言 Tkinter 库简介步骤1:导入 Tkinter 模块步骤2:创建 Tkinter 窗口步骤3:创建顶部菜单栏步骤4:处理菜单项的点击事件步骤5:启动 Tkinter 主事件循环 完整示例代码…...
java实现十大排序算法
文章目录 冒泡排序选择排序插入排序希尔排序归并排序快速排序堆排序桶排序基数排序计数排序验证各个排序的时间复杂度和空间复杂度 冒泡排序 冒泡排序(Bubble Sort)是一种简单的比较排序算法,它的基本思想是重复地交换相邻的两个元素&#x…...
Linux日志管理-logrotate(crontab定时任务、Ceph日志转储)
文章目录 一、logrotate概述二、logrotate基本用法三、logrotate运行机制logrotate参数 四、logrotate是怎么做到滚动日志时不影响程序正常的日志输出呢?Linux文件操作机制方案一方案二 五、logrotate实战--Ceph日志转储参考 一、logrotate概述 logrotate是一个用于…...
用PHP异步协程控制python爬虫脚本,实现多协程分布式爬取
背景 公司需要爬取指定网站的产品数据。但是个人对python的多进程和协程不是特别熟悉。所以,想通过php异步协程,发起爬取url请求控制python爬虫脚本,达到分布式爬取的效果。 准备 1.准备一个mongodb数据库用于存放爬取数据2.引入flask包&a…...
广州市网站建设 骏域动力/广东全网推广
Rsyncsersync2的数据推复制(数据的快速同步,类似于实时同步):也就是说当服务器的数据发生变化,就推新数据给备份服务器。***************************************************************************特点࿱…...
建设电子商务网站的预期收益/手机免费建站系统
2019独角兽企业重金招聘Python工程师标准>>> 日期转换帮助类 博客分类: java /*** 转换微博时间的工具类* * * */ public class ConvertDateUtil {/*** 转换字符类型的时间为日期* * param time* 1. 60分钟前 * 2. 今天 13:05 *…...
网站外部链接合理建设/关键词优化系统
IDM下载器安卓版是国外热门的多线程下载工具,一款非常优秀的下载神器,支持多媒体下载、自动捕获链接、自动识别文件名、静默下载、批量下载、计划下载任务、站点抓取、队列与网盘支持等 IDM下载速度据说比普通下载器快500%,基本能达到带宽的…...
推广是什么/seo关键词查询工具
Jmeter测试结果分析这一篇,我打算分成上下两部分。上篇,主要讲述如何使用jmeter中Assertion对结果进行简单的分类;下篇,主要讲述的是当我们拿到测试结果后,我们应该如何去看待这些测试结果。 用过LoadRunner的人都知道…...
关于单位建设网站的申请/优秀网站设计
微信聊天不但能够文字、图片,还能发送视频,给我们带来了很多趣味,不过此前发送的视频只能顺序播放,而无法拖动快进,又有些不便。好消息来了,今天iOS版微信迎来6.2.2小更新,针对聊天中的视频增加…...
做移门图的 网站有哪些/seo北京公司
概要 电影文件有很多基本的组成部分。首先,文件本身被称为容器Container,容器的类型决定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接着,你有一组流,例如,你经常有的是一个音频流和一个视频流。&…...