Flutter学习指南:文件、存储和网络
Flutter学习指南
交互、手势和动画
UI布局和控件
熟悉Dart语言
编写第一个应用
开发环境搭建
本篇文章我们先学习 Flutter IO 相关的基础知识,然后在 Flutter学习指南:交互、手势和动画 的基础上,继续开发一个 echo 客户端。由于日常开发中 HTTP 比 socket 更常见,我们的 echo 客户端将会使用 HTTP 协议跟服务端通信。Echo 服务器也会使用 Dart 来实现。
文件
为了执行文件操作,我们可以使用 Dart 的 io 包:
import 'dart:io';复制代码
创建文件
在 Dart 里,我们通过类 File 来执行文件操作:
void foo() async { const filepath = "path to your file"; var file = File(filepath); try { bool exists = await file.exists(); if (!exists) { await file.create(); } } catch (e) { print(e); }}复制代码
相对于 CPU,IO 总是很慢的,所以大部分文件操作都返回一个 Future,并在出错的时候抛出一个异常。如果你需要,也可以使用同步版本,这些方法都带一个后缀 Sync:
void foo() { const filepath = "path to your file"; var file = File(filepath); try { bool exists = file.existsSync(); if (!exists) { file.createSync(); } } catch (e) { print(e); }}复制代码
async 方法使得我们可以像写同步方法一样写异步代码,同步版本的 io 方法已经没有太多使用的必要了(Dart 1 不支持 async 函数,所以同步版本的方法的存在是有必要的)。
写文件
写 String 时我们可以使用 writeAsString 和 writeAsBytes 方法:
const filepath = "path to your file";var file = File(filepath);await file.writeAsString('Hello, Dart IO');List<int> toBeWritten = [1, 2, 3];await file.writeAsBytes(toBeWritten);复制代码
如果只是为了写文件,还可以使用 openWrite 打开一个 IOSink:
void foo() async { const filepath = "path to your file"; var file = File(filepath); IOSink sink; try { sink = file.openWrite(); // 默认的写文件操作会覆盖原有内容;如果要追究内容,用 append 模式 // sink = file.openWrite(mode: FileMode.append);
// write() 的参数是一个 Object,他会执行 obj.toString() 把转换后 // 的 String 写入文件 sink.write('Hello, Dart'); //调用 flush 后才会真的把数据写出去 await sink.flush(); } catch (e) { print(e); } finally { sink?.close(); }}复制代码
读文件
读写原始的 bytes 也是相当简单的:
var msg = await file.readAsString();List<int> content = await file.readAsBytes();复制代码
和写文件类似,它还有一个 openRead 方法:
// Stream 是 async 包里的类import 'dart:async';// utf8、LineSplitter 属于 convert 包import 'dart:convert';import 'dart:io';
void foo() async { const filepath = "path to your file"; var file = File(filepath); try { Stream<List<int>> stream = file.openRead(); var lines = stream // 把内容用 utf-8 解码 .transform(utf8.decoder) // 每次返回一行 .transform(LineSplitter()); await for (var line in lines) { print(line); } } catch (e) { print(e); }}复制代码
最后需要注意的是,我们读写 bytes 的时候,使用的对象是 List<int>,而一个 int 在 Dart 里面有 64 位。Dart 一开始设计就是用于 Web,这部分的效率也就不那么高了。
JSON
JSON 相关的 API 放在了 convert 包里面:
import 'dart:convert';复制代码
把对象转换为 JSON
假设我们有这样一个对象:
class Point { int x; int y; String description;
Point(this.x, this.y, this.description);}复制代码
为了把他转换为 JSON,我们给他定义一个 toJson 方法(注意,不能改变他的方法签名):
class Point { // ...
// 注意,我们的方法只有一个语句,这个语句定义了一个 map。 // 使用这种语法的时候,Dart 会自动把这个 map 当做方法的返回值 Map<String, dynamic> toJson() => { 'x': x, 'y': y, 'desc': description };}复制代码
接下来我们调用 json.encode 方法把对象转换为 JSON:
void main() { var point = Point(2, 12, 'Some point'); var pointJson = json.encode(point); print('pointJson = $pointJson');
// List, Map 都是支持的 var points = [point, point]; var pointsJson = json.encode(points); print('pointsJson = $pointsJson');}
// 执行后打印出:// pointJson = {"x":2,"y":12,"desc":"Some point"}// pointsJson = [{"x":2,"y":12,"desc":"Some point"},{"x":2,"y":12,"desc":"Some point"}]复制代码
把 JSON 转换为对象
首先,我们给 Point 类再加多一个构造函数:
class Point { // ...
Point.fromJson(Map<String, dynamic> map) : x = map['x'], y = map['y'], description = map['desc'];
// 为了方便后面演示,也加入一个 toString @override String toString() { return "Point{x=$x, y=$y, desc=$description}"; }}复制代码
为了解析 JSON 字符串,我们可以用 json.decode 方法:
dynamic obj = json.decode(jsonString);复制代码
返回一个 dynamic 的原因在于,Dart 不知道传进去的 JSON 是什么。如果是一个 JSON 对象,返回值将是一个 Map;如果是 JSON 数组,则会返回 List<dynamic>:
void main() { var point = Point(2, 12, 'Some point'); var pointJson = json.encode(point); print('pointJson = $pointJson'); var points = [point, point]; var pointsJson = json.encode(points); print('pointsJson = $pointsJson'); print('');
var decoded = json.decode(pointJson); print('decoded.runtimeType = ${decoded.runtimeType}'); var point2 = Point.fromJson(decoded); print('point2 = $point2');
decoded = json.decode(pointsJson); print('decoded.runtimeType = ${decoded.runtimeType}'); var points2 = <Point>[]; for (var map in decoded) { points2.add(Point.fromJson(map)); } print('points2 = $points2');}复制代码
运行结果如下:
pointJson = {"x":2,"y":12,"desc":"Some point"}pointsJson = [{"x":2,"y":12,"desc":"Some point"},{"x":2,"y":12,"desc":"Some point"}]
decoded.runtimeType = _InternalLinkedHashMap<String, dynamic>point2 = Point{x=2, y=12, desc=Some point}decoded.runtimeType = List<dynamic>points2 = [Point{x=2, y=12, desc=Some point}, Point{x=2, y=12, desc=Some point}]复制代码
需要说明的是,我们把 Map 转化为对象时使用时定义了一个构造函数,但这个是任意的,使用静态方法、Dart 工厂方法等都是可行的。之所以限定 toJson 方法的原型,是因为 json.encode 只支持 Map、List、String、int 等内置类型。当它遇到不认识的类型时,如果没有给它设置参数 toEncodable,就会调用对象的 toJson 方法(所以方法的原型不能改变)。
HTTP
为了向服务器发送 HTTP 请求,我们可以使用 io 包里面的 HttpClient。但它实在不是那么好用,于是就有人弄出了一个 http 包。为了使用 http 包,需要修改 pubspec.yaml:
# pubspec.yamldependencies: http: ^0.11.3+17复制代码
http 包的使用非常直接,为了发出一个 GET,可以使用 http.get 方法;对应的,还有 post、put 等。
import 'package:http/http.dart' as http;
Future<String> getMessage() async { try { final response = await http.get('http://www.xxx.com/yyy/zzz'); if (response.statusCode == 200) { return response.body; } } catch (e) { print('getMessage: $e'); } return null;}复制代码
HTTP POST 的例子我们在下面实现 echo 客户端的时候再看。
使用 SQLite 数据库
包 sqflite 可以让我们使用 SQLite:
dependencies: sqflite: any复制代码
sqflite 的 API 跟 Android 的那些非常像,下面我们直接用一个例子来演示:
import 'package:sqflite/sqflite.dart';
class Todo { static const columnId = 'id'; static const columnTitle = 'title'; static const columnContent = 'content';
int id; String title; String content;
Todo(this.title, this.content, [this.id]);
Todo.fromMap(Map<String, dynamic> map) : id = map[columnId], title = map[columnTitle], content = map[columnContent];
Map<String, dynamic> toMap() => { columnTitle: title, columnContent: content, };
@override String toString() { return 'Todo{id=$id, title=$title, content=$content}'; }}
void foo() async { const table = 'Todo'; // getDatabasesPath() 的 sqflite 提供的函数 var path = await getDatabasesPath() + '/demo.db'; // 使用 openDatabase 打开数据库 var database = await openDatabase( path, version: 1, onCreate: (db, version) async { var sql =''' CREATE TABLE $table (' ${Todo.columnId} INTEGER PRIMARY KEY,' ${Todo.columnTitle} TEXT,' ${Todo.columnContent} TEXT' ) '''; // execute 方法可以执行任意的 SQL await db.execute(sql); } ); // 为了让每次运行的结果都一样,先把数据清掉 await database.delete(table);
var todo1 = Todo('Flutter', 'Learn Flutter widgets.'); var todo2 = Todo('Flutter', 'Learn how to to IO in Flutter.');
// 插入数据 await database.insert(table, todo1.toMap()); await database.insert(table, todo2.toMap());
List<Map> list = await database.query(table); // 重新赋值,这样 todo.id 才不会为 0 todo1 = Todo.fromMap(list[0]); todo2 = Todo.fromMap(list[1]); print('query: todo1 = $todo1'); print('query: todo2 = $todo2');
todo1.content += ' Come on!'; todo2.content += ' I\'m tired'; // 使用事务 await database.transaction((txn) async { // 注意,这里面只能用 txn。直接使用 database 将导致死锁 await txn.update(table, todo1.toMap(), // where 的参数里,我们可以使用 ? 作为占位符,对应的值按顺序放在 whereArgs
// 注意,whereArgs 的参数类型是 List,这里不能写成 todo1.id.toString()。 // 不然就变成了用 String 和 int 比较,这样一来就匹配不到待更新的那一行了 where: '${Todo.columnId} = ?', whereArgs: [todo1.id]); await txn.update(table, todo2.toMap(), where: '${Todo.columnId} = ?', whereArgs: [todo2.id]); });
list = await database.query(table); for (var map in list) { var todo = Todo.fromMap(map); print('updated: todo = $todo'); }
// 最后,别忘了关闭数据库 await database.close();}复制代码
运行结果如下:
query: todo1 = Todo{id=1, title=Flutter, content=Learn Flutter widgets}query: todo2 = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter}updated: todo = Todo{id=1, title=Flutter, content=Learn Flutter widgets. Come on!}updated: todo = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter. I'm tired}复制代码
有 Android 经验的读者会发现,使用 Dart 编写数据库相关代码的时候舒服很多。如果读者对数据库不太熟悉,可以参考《SQL必知必会》。本篇的主要知识点到这里的就讲完了,作为练习,下面我们就一起来实现 echo 客户端的后端。
echo 客户端
HTTP 服务端
在开始之前,你可以在 GitHub 上找到上篇文章的代码,我们将在它的基础上进行开发。
git clone https://github.com/Jekton/flutter_demo.gitcd flutter_demogit checkout ux-basic复制代码
服务端架构
首先我们来看看服务端的架构(说是架构,但其实非常的简单,或者说很简陋):
import 'dart:async';import 'dart:io';
class HttpEchoServer {
final int port; HttpServer httpServer; // 在 Dart 里面,函数也是 first class object,所以我们可以直接把 // 函数放到 Map 里面 Map<String, void Function(HttpRequest)> routes;
HttpEchoServer(this.port) { _initRoutes(); }
void _initRoutes() { routes = { // 我们只支持 path 为 '/history' 和 '/echo' 的请求。 // history 用于获取历史记录; // echo 则提供 echo 服务。 '/history': _history, '/echo': _echo, }; }
// 返回一个 Future,这样客户端就能够在 start 完成后做一些事 Future start() async { // 1. 创建一个 HttpServer httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port); // 2. 开始监听客户请求 return httpServer.listen((request) { final path = request.uri.path; final handler = routes[path]; if (handler != null) { handler(request); } else { // 给客户返回一个 404 request.response.statusCode = HttpStatus.notFound; request.response.close(); } }); }
void _history(HttpRequest request) { // ... }
void _echo(HttpRequest request) async { // ... }
void close() async { var server = httpServer; httpServer = null; await server?.close(); }}复制代码
在服务端框架里,我们把支持的所有路径都加到 routes 里面,当收到客户请求的时候,只需要直接从 routes 里取出对应的处理函数,把请求分发给他就可以了。如果读者对服务端编程没有太大兴趣或不太了解,这部分可以不用太关注。
将对象序列化为 JSON
为了把 Message 对象序列化为 JSON,这里我们对 Message 做一些小修改:
class Message { final String msg; final int timestamp;
Message(this.msg, this.timestamp); Message.create(String msg) : msg = msg, timestamp = DateTime.now().millisecondsSinceEpoch;
Map<String, dynamic> toJson() => { "msg": "$msg", "timestamp": timestamp };
@override String toString() { return 'Message{msg: $msg, timestamp: $timestamp}'; }}复制代码
这里我们加入一个 toJson 方法。下面是服务端的 _echo 方法:
class HttpEchoServer { static const GET = 'GET'; static const POST = 'POST';
const List<Message> messages = [];
// ...
_unsupportedMethod(HttpRequest request) { request.response.statusCode = HttpStatus.methodNotAllowed; request.response.close(); }
void _echo(HttpRequest request) async { if (request.method != POST) { _unsupportedMethod(request); return; }
// 获取从客户端 POST 请求的 body,更多的知识,参考 // https://www.dartlang.org/tutorials/dart-vm/httpserver String body = await request.transform(utf8.decoder).join(); if (body != null) { var message = Message.create(body); messages.add(message); request.response.statusCode = HttpStatus.ok; // json 是 convert 包里的对象,encode 方法还有第二个参数 toEncodable。当遇到对象不是 // Dart 的内置对象时,如果提供这个参数,就会调用它对对象进行序列化;这里我们没有提供, // 所以 encode 方法会调用对象的 toJson 方法,这个方法在前面我们已经定义了 var data = json.encode(message); // 把响应写回给客户端 request.response.write(data); } else { request.response.statusCode = HttpStatus.badRequest; } request.response.close(); }}复制代码
HTTP 客户端
我们的 echo 服务器使用了 dart:io 包里面 HttpServer 来开发。对应的,我们也可以使用这个包里的 HttpRequest 来执行 HTTP 请求,但这里我们并不打算这么做。第三方库 http 提供了更简单易用的接口。
首先把依赖添加到 pubspec 里:
# pubspec.yamldependencies: # ...
http: ^0.11.3+17复制代码
客户端实现如下:
import 'package:http/http.dart' as http;
class HttpEchoClient { final int port; final String host;
HttpEchoClient(this.port): host = 'http://localhost:$port';
Future<Message> send(String msg) async { // http.post 用来执行一个 HTTP POST 请求。 // 它的 body 参数是一个 dynamic,可以支持不同类型的 body,这里我们 // 只是直接把客户输入的消息发给服务端就可以了。由于 msg 是一个 String, // post 方法会自动设置 HTTP 的 Content-Type 为 text/plain final response = await http.post(host + '/echo', body: msg); if (response.statusCode == 200) { Map<String, dynamic> msgJson = json.decode(response.body); // Dart 并不知道我们的 Message 长什么样,我们需要自己通过 // Map<String, dynamic> 来构造对象 var message = Message.fromJson(msgJson); return message; } else { return null; } }}
class Message { final String msg; final int timestamp;
Message.fromJson(Map<String, dynamic> json) : msg = json['msg'], timestamp = json['timestamp'];
// ...}复制代码
现在,让我们把他们和上一节的 UI 结合到一起。首先启动服务器,然后创建客户端:
HttpEchoServer _server;HttpEchoClient _client;
class _MessageListState extends State<MessageList> { final List<Message> messages = [];
@override void initState() { super.initState();
const port = 6060; _server = HttpEchoServer(port); // initState 不是一个 async 函数,这里我们不能直接 await _server.start(), // future.then(...) 跟 await 是等价的 _server.start().then((_) { // 等服务器启动后才创建客户端 _client = HttpEchoClient(port); }); }
// ...}复制代码
class MessageListScreen extends StatelessWidget {
@override Widget build(BuildContext context) { return Scaffold( // ...
floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => AddMessageScreen()) ); // 以下是修改了的地方 if (_client == null) return; // 现在,我们不是直接构造一个 Message,而是通过 _client 把消息 // 发送给服务器 var msg = await _client.send(result); if (msg != null) { messageListKey.currentState.addMessage(msg); } else { debugPrint('fail to send $result'); } }, // ... ) ); }}复制代码
大功告成,在做了这么多工作以后,我们的应用现在是真正的 echo 客户端了,虽然看起来跟之前没什么两样。接下来,我们就做一些跟之前不一样的——把历史记录保存下来。
历史记录存储、恢复
获取应用的存储路径
为了获得应用的文件存储路径,我们引入多一个库:
# pubspec.yamldependencies: # ...
path_provider: ^0.4.1复制代码
通过它我们可以拿到应用的 file、cache 和 external storage 的路径:
import 'package:path_provider/path_provider.dart' as path_provider;
class HttpEchoServer { String historyFilepath;
Future start() async { historyFilepath = await _historyPath();
// ... }
Future<String> _historyPath() async { // 获取应用私有的文件目录 final directory = await path_provider.getApplicationDocumentsDirectory(); return directory.path + '/messages.json'; }}复制代码
保存历史记录
class HttpEchoServer {
void _echo(HttpRequest request) async { // ...
// 原谅我,为了简单,我们就多存几次吧 _storeMessages(); }
Future<bool> _storeMessages() async { try { // json.encode 支持 List、Map final data = json.encode(messages); // File 是 dart:io 里的类 final file = File(historyFilepath); final exists = await file.exists(); if (!exists) { await file.create(); } file.writeAsString(data); return true; // 虽然文件操作方法都是异步的,我们仍然可以通过这种方式 catch 到 // 他们抛出的异常 } catch (e) { print('_storeMessages: $e'); return false; } }}复制代码
加载历史记录
class HttpEchoServer {
// ...
Future start() async { historyFilepath = await _historyPath(); // 在启动服务器前先加载历史记录 await _loadMessages(); httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port); // ... }
Future _loadMessages() async { try { var file = File(historyFilepath); var exists = await file.exists(); if (!exists) return;
var content = await file.readAsString(); var list = json.decode(content); for (var msg in list) { var message = Message.fromJson(msg); messages.add(message); } } catch (e) { print('_loadMessages: $e'); } }}复制代码
现在,我们来实现 _history 函数:
class HttpEchoServer { // ...
void _history(HttpRequest request) { if (request.method != GET) { _unsupportedMethod(request); return; }
String historyData = json.encode(messages); request.response.write(historyData); request.response.close(); }}复制代码
_history 的实现很直接,我们只是把 messages 全都返回给客户端。
接下来是客户端部分:
class HttpEchoClient {
// ...
Future<List<Message>> getHistory() async { try { // http 包的 get 方法用来执行 HTTP GET 请求 final response = await http.get(host + '/history'); if (response.statusCode == 200) { return _decodeHistory(response.body); } } catch (e) { print('getHistory: $e'); } return null; }
List<Message> _decodeHistory(String response) { // JSON 数组 decode 出来是一个 <Map<String, dynamic>>[] var messages = json.decode(response); var list = <Message>[]; for (var msgJson in messages) { list.add(Message.fromJson(msgJson)); } return list; }}
class _MessageListState extends State<MessageList> { final List<Message> messages = [];
@override void initState() { super.initState();
const port = 6060; _server = HttpEchoServer(port); _server.start().then((_) { // 我们等服务器启动后才创建客户端 _client = HttpEchoClient(port); // 创建客户端后马上拉取历史记录 _client.getHistory().then((list) { setState(() { messages.addAll(list); }); }); }); }
// ...}复制代码
生命周期
最后需要做的是,在 APP 退出后关闭服务器。这就要求我们能够收到应用生命周期变化的通知。为了达到这个目的,Flutter 为我们提供了 WidgetsBinding 类(虽然没有 Android 的 Lifecycle 那么好用就是啦)。
// 为了使用 WidgetsBinding,我们继承 WidgetsBindingObserver 然后覆盖相应的方法class _MessageListState extends State<MessageList> with WidgetsBindingObserver {
// ...
@override void initState() { // ... _server.start().then((_) { // ...
// 注册生命周期回调 WidgetsBinding.instance.addObserver(this); }); }
@override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { var server = _server; _server = null; server?.close(); } }}复制代码
现在,我们的应用是这个样子的:
所有的代码可以在 GitHub 上找到:
git clone https://github.com/Jekton/flutter_demo.gitcd flutter_demogit checkout io-basic复制代码
使用 SQLite 数据库
前面的实现中我们把 echo 服务器的数据存放在了文件里。这一节我们改一改,把数据存到 SQLite 中。
别忘了添加依赖:
dependencies: sqflite: any复制代码
初始化数据库
import 'package:sqflite/sqflite.dart';
class HttpEchoServer { // ...
static const tableName = 'History'; // 这部分常量最好是放到 Message 的定义里。为了方便阅读,就暂且放这里吧 static const columnId = 'id'; static const columnMsg = 'msg'; static const columnTimestamp = 'timestamp';
Database database;
Future start() async { await _initDatabase();
// ... }
Future _initDatabase() async { var path = await getDatabasesPath() + '/history.db'; database = await openDatabase( path, version: 1, onCreate: (db, version) async { var sql = ''' CREATE TABLE $tableName ( $columnId INTEGER PRIMARY KEY, $columnMsg TEXT, $columnTimestamp INTEGER ) '''; await db.execute(sql); } ); }}复制代码
加载历史记录
加载历史记录的相关代码在 _loadMessages 方法中,这里我们修改原有的实现,让它从数据库加载数据:
class HttpEchoServer { // ...
Future _loadMessages() async { var list = await database.query( tableName, columns: [columnMsg, columnTimestamp], orderBy: columnId, ); for (var item in list) { // fromJson 也适用于使用数据库的场景 var message = Message.fromJson(item); messages.add(message); } }}复制代码
实际上改为使用数据库来存储后,我们并不需要把所有的消息都存放在内存中(也就是这里的 _loadMessage 是不必要的)。客户请求历史记录时,我们再按需从数据库读取数据即可。为了避免修改到程序的逻辑,这里还是继续保持一份数据在内存中。有兴趣的读者可以对程序作出相应的修改。
保存记录
记录的保存很简单,一行代码就可以搞定了:
void _echo(HttpRequest request) async { // ...
_storeMessage(message);}
void _storeMessage(Message msg) { database.insert(tableName, msg.toJson());}复制代码
使用 JSON 的版本,我们每次都需要把所有的数据都保存一遍。对数据库来说,只要把收到的这一条信息存进去即可。读者也应该能够感受到,就我们的需求来说,使用 SQLite 的版本实现起来更简单,也更高效。
关闭数据库
close 方法也要做相应的修改:
void close() async { // ...
var db = database; database = null; db?.close();}复制代码
这部分代码可以查看 tag echo-db:
git clone https://github.com/Jekton/flutter_demo.gitcd flutter_demogit checkout echo-db复制代码
欢迎扫码关注
Flutter学习指南:文件、存储和网络相关推荐
- flutter scrollview_简单易上手的Flutter学习指南App,2020一起来玩转Flutter吧~
Flutter是谷歌的移动UI框架,可以快速在iOS.Android.Web和PC上构建高质量的原生用户界面. Flutter可以与现有的代码一起工作.在全世界,Flutter正在被越来越多的开发者和 ...
- 文件管理学习之文件存储空间分配管理
磁盘的逻辑组织 一个物理磁盘在逻辑上可分为几个区域,分区信息存放在主引导块分区表中.分区表中保存磁盘各种分区起始和终止的磁头.柱面.扇区.总的扇区数等信息.在主引导块中有三种类型分区:主分区.扩展区和 ...
- Flutter学习指南:UI布局和控件,作为Android开发者
showDialog( // 第一个 context 是参数名,第二个 context 是 State 的成员变量 context: c ...
- 块存储、文件存储和对象存储三者的区别
大家好,学习时间又到啦.今日我们来聊一聊块存储.文件存储和对象存储的区别.这个问题老生常谈了,在网上会出现许多的解释,可是大多数相对比较抽象一些,此次我选择用抽象和简易两种方式给各位分享一下体会心得. ...
- 块存储,文件存储,对象存储
前言:根据不同的分类方式,存储也会被分成不同的类型,但是用途是一致的:存储的最终目的是存放数据. 存储的分类方式: 1.本地存储,外地存储 本地存储:就是你电脑里面内置的存储设备,比如:系统盘,机械盘 ...
- 计算机存储的发展(块存储,文件存储,对象存储)
块存储 DAS SAN 文件存储 块存储和文件存储异同: 对象存储 1.对象 2.对象存储设备 3.元数据服务器(Metadata Server,MDS) 4.对象存储系统的客户端Client 三者之 ...
- 块存储、文件存储、对象存储这三者的区别
参考链接 https://blog.csdn.net/wuxiaobingandbob/article/details/80178502 https://blog.csdn.net/qq_319331 ...
- 如何在Python中将数字文件存储在数据库中
Databases like MySQL and PostgreSQL are all ideal for storing tables with numeric and text data. How ...
- 数据存储机制之文件存储
今天我们来学习下文件存储.文件存储算是Android中最为基础的一种数据存储方式了.它比较适合用于存储一些简单的文本数据后二进制数据. 那么我们就先来看下Android是如何通过文件来保存数据的 将数 ...
最新文章
- 高定位精度的交通标志识别----开源了
- EMW 性能优化二之---并发配置
- java锁的有哪些_「并发编程」Java锁分类和特点有哪些
- 什么是mysql的游标_MySQL游标概念是什么 MySQL游标概念与用法介绍
- Oracle 20c 新特性:原生的 JSON 数据类型(Native JSON Datatype)
- 深入理解JavaScript中的属性和特性
- iOS中内存管理的问题——堆和栈
- 用shell求两个文件的差集
- 基于RabbitMQ RPC实现的主机异步管理
- dataframe groupby_python pandas获取groupby之后的数据
- C# 操作json 序列化 反序列化
- 学生作业信息管理系统
- 简单计算机c++代码
- android游戏 百度云盘下载安装,百度云盘下载,百度网盘app下载安装
- TrueLicense 使用JDK自带的 keytool 工具生成公私钥证书库
- 压在redis身上的三座大山
- 【破解工具】Hashcat加密破解工具
- Tensorflow keras入门教程
- 计算机和通信技术对未来的,谈计算机技术与通信技术的发展
- python 对excel的函数操作(2)