您的位置:首页 > 理论基础 > 计算机网络

Flutter HTTP上传文件使用详解

2020-02-02 12:24 4675 查看

Flutter HTTP 上传文件详解

Flutter HTTP上传文件详解

最近使用Flutter开发新App,需要使用Http上传文件,对Flutter中Http上传进行一些总结

multipart/form-data

一个 HTML 表单中的 enctype 有三种类型

  • application/x-www-urlencoded
  • multipart/form-data
  • text-plain

默认情况下是 application/x-www-urlencoded,当表单使用 POST 请求时,数据会被以 x-www-urlencoded方式编码到 Body 中来传送。

如果要发送大量的二进制数据(non-ASCII),application/x-www-form-urlencoded显然是低效的,因为它需要用 3 个字符来表示一个 non-ASCII 的字符。因此,这种情况下,应该使用multipart/form-data格式。

我们需求中正是需要使用multipart/form-data格式来上传文件。

multipart/form-data请求的内容格式如下:

POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryyb1zYhTI38xpQxBK

------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="city_id"

1

------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="company_id"

2
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryyb1zYhTI38xpQxBK--

每个请求的body中可以包含多个字段,如上面的请求中就包含“city_id”、“company_id”、“file”三个字段,前两个字段的值是字符串,“chrome.png”则是上传的文件,文件以二进制数组转换成字符串来传递,如果文件较大,会分为多个POST请求传递给服务器,前面的POST请求会设置为keep-alive,最后一个POST请求才close。

“------WebKitFormBoundaryyb1zYhTI38xpQxBK”是分隔符(boundary),用于分割body中的每个字段,boundary可以自定义,在header中的boundary字段说明,在body中会以如下格式添加进去:

...
Content-Type: multipart/form-data; boundary=${boundary}

--${boundary}
...
...

--${boundary}--

Flutter Http插件

Flutter中Http请求方式可以直接使用Dart:io中的HttpClient,但是目前不支持multipart/form-data格式,为了方便,我们使用了dart官方封装的http插件

官方文档中介绍了一些简单的使用方法,这里就不再赘述,比较蛋疼的是官方文档中只介绍了一些基础功能的用法,像multipart/form-data的用法都没有介绍。这里主要是介绍一下multipart/form-data的用法。

由于我们的需求是一定要使用multipart/form-data,一开始纠结了好久找不到在Flutter中该如何写代码;网上有人介绍可以使用dio这个库来使用multipart/form-data格式上传文件,但是我们的项目里已经引入了http插件,不想再换成dio,所以还是想最好能够用http插件实现multipart上传功能。

MultipartRequest使用

经过一段时间搜索,终于在http的Github的Issue里找到了一些线索,有人提问关于multipart的问题,我这才发现在http的包里有一个MultipartRequest的类,这个类正是对multipart/form-data格式的实现。

这个类的使用在源码的注释里有简单的说明,具体用法如下:

var uri = Uri.parse("http://pub.dartlang.org/packages/create");
var request = new http.MultipartRequest("POST", uri);
request.fields['user'] = 'nweiz@google.com';
request.files.add(new http.MultipartFile.fromPath(
'package',
'build/package.tar.gz',
contentType: new MediaType('application', 'x-tar'));
var response = await request.send();
if (response.statusCode == 200) print('Uploaded!');

MultipartRequest中,fields是一个Map;files是一个MultipartFile的List:

/// The form fields to send for this request.
final Map<String, String> fields;

/// The private version of [files].
final List<MultipartFile> _files;
/// The list of files to upload for this request.
List<MultipartFile> get files => _files;

fields里存储的key-value就是body中的文本字段,key是name,value是内容。files里存储的就是需要上传的文件,MultipartFile有两个命名构造方法和一个静态方法:

/// Creates a new [MultipartFile] from a byte array.
///
/// [contentType] currently defaults to `application/octet-stream`, but in the
/// future may be inferred from [filename].
factory MultipartFile.fromBytes(String field, List<int> value, {String filename, MediaType contentType})

/// Creates a new [MultipartFile] from a string.
///
/// The encoding to use when translating [value] into bytes is taken from
/// [contentType] if it has a charset set. Otherwise, it defaults to UTF-8.
/// [contentType] currently defaults to `text/plain; charset=utf-8`, but in
/// the future may be inferred from [filename].
factory MultipartFile.fromString(String field, String value, {String filename, MediaType contentType})

// TODO(nweiz): Infer the content-type from the filename.
/// Creates a new [MultipartFile] from a path to a file on disk.
///
/// [filename] defaults to the basename of [filePath]. [contentType] currently
/// defaults to `application/octet-stream`, but in the future may be inferred
/// from [filename].
///
/// Throws an [UnsupportedError] if `dart:io` isn't supported in this
/// environment.
static Future<MultipartFile> fromPath(String field, String filePath, {String filename, MediaType contentType})

最后调用MultipartRequest中的send方法会将fields和files中的内容按照格式生成body,然后发送POST请求。

需要注意的是request.send()返回的是StreamedResponse,和普通的Response还不一样,需要用如下方法才能读取内容:

var respStr = await response.stream.transform(utf8.decoder).join();
LogUtil.i("upload response is $respStr");

Flutter请求抓包问题

一般做HTTP请求都会想要抓包来看一下请求的格式和内容对不对,但是这次连上代理以后发现其他请求都能抓到,只有Flutter里的请求抓不到…
上网搜索了一下,发现已经有人遇到过这个问题,并给出了解决方案,具体的分析这里就不再贴了,详情请看Flutter中http请求抓包解决方案。这里只写一下结论,增加如下代码就可以抓包了,"http_proxy"填代理PC的IP和端口即可。

var httpClient = new HttpClient();
httpClient.findProxy = (url) {
return HttpClient.findProxyFromEnvironment(url, environment: {"http_proxy": 'http://192.168.124.7:8888',});
};

但是有一个问题,我们用的是http插件,不是原生的HttpClient,这又应该怎么设置呢?
遇事不决读源码,在http中的client.dart里,我看到这样的注释:

/// Creates a new client.
///
/// Currently this will create an `IOClient` if `dart:io` is available and
/// a `BrowserClient` if `dart:html` is available, otherwise it will throw
/// an unsupported error.

意思是如果有dart:io,就会创建一个IOClient;如果有dart:html,就会创建一个BrowserClient。HttpClient正是dart:io中的一员,所以我们来看看IOClient的实现:

/// The underlying `dart:io` HTTP client.
HttpClient _inner;

/// Creates a new HTTP client.
IOClient([HttpClient inner]) : _inner = inner ?? new HttpClient();

这就很清晰了,IOClient实际就是HttpClient的封装,那我们只要自己创建一个HttpClient设置好代理后再创建IOClient就可以了,所以我们完整的上传代码就是:

  static Future<bool> upload(BaseUploadData data) async {
var request = await data.getRequest();
HttpClient httpClient = new HttpClient();
httpClient.findProxy = (url) {
return HttpClient.findProxyFromEnvironment(url, environment: {"http_proxy": 'http://10.45.109.70:8088',});
};
IOClient client = IOClient(httpClient);
var response = await client.send(request);
var respStr = await response.stream.transform(utf8.decoder).join();
LogUtil.i("upload response is $respStr");return response.statusCode == 200;
}
  • 点赞
  • 收藏
  • 分享
  • 文章举报
建炜 发布了1 篇原创文章 · 获赞 0 · 访问量 39 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: