Flutter 上应该怎么请求http?很简单,直接用 dart:io包下的 HttpClient,如下代码:

1
2
3
4
5
6
7
8
9
10
HttpClient client = HttpClient();
client.getUrl(Uri.parse("https://yrom.net/"))
.then((HttpClientRequest request) => request.close())
.then((HttpClientResponse response) {
print('Response status: ${response.statusCode}');
response.transform(utf8.decoder).listen((contents) {
print('${contents}');
});
});
// client.close();

但你肯定也发现了 dart:ioHttpClient 所提供的API太过底层了,所以一般不会直接用,而是用Dart官方提供的 http 包(package:http),如下代码:

1
2
3
4
5
6
7
8
9
10
11
import 'package:http/http.dart';

Client client = Client();
var url = 'https://yrom.net';
var response = await client.get(url);
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

print(await client.read('https://yrom.net'));

// client.close();

或者有很多人喜欢的 dio

但无论怎么封装API,底层都还是 dart:io里的HttpClient

你可能一直有疑问,不是说 Flutter 是单线程的,那 http 请求难道不会卡住 UI 线程,导致 UI 无响应吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ExampleState extends State<Example> {
String data = '';

Future<void> requestData(url) async {
setState(() {
data = 'Loading $url';
});
// 这里为什么不会导致 UI 卡顿呢?
var readed = await http.read(url);
if (!mounted) return;

setState(() {
data = readed;
});
}
@override
void initState() {
super.initState();
requestData('https://yrom.net/blog/2020/06/18/flutter-httpclient-overview/');
}

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Text(data),
);
}
}

答案是,不会。但…why?

带你从源码里找找 HttpClient 的本质。

dart:io HttpClient

通过阅读HttpClient的默认实现_HttpClient(lib/_http/http_impl.dart)类的源码可知,实现Http请求的 TCP Socket 连接和读写都是由 _NativeSocket (lib/_internal/vm/bin/socket_patch.dart) 实现,而_NativeSocket又经由_IOService(lib/_internal/vm/bin/io_service_patch.dart)进行 DNS 解析获取到服务器的ip地址(InternetAddress),最终和服务器进行TCP通信。

_HttpClient 关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Future<_HttpClientRequest> _openUrl(String method, Uri uri) {
...
bool isSecure = (uri.scheme == "https");
int port = uri.port;
if (port == 0) {
port =
isSecure ? HttpClient.defaultHttpsPort : HttpClient.defaultHttpPort;
}
...
return _getConnection(uri.host, port, proxyConf, isSecure, timeline).then(
(_ConnectionInfo info) {
_HttpClientRequest send(_ConnectionInfo info) {
return info.connection
.send(uri, port, method.toUpperCase(), info.proxy, timeline);
}

if (info.connection.closed) {
return _getConnection(uri.host, port, proxyConf, isSecure, timeline)
.then(send);
}
return send(info);
});
}

大致流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    _HttpClient
+
| _getConnection
v
_HttpClientConnection
+
| startConnect
v
Socket
+
| _startConnect
v
RawSocket
+
| startConnect
v
_NativeSocket
+
|
v dispatch(socketLookup)
_IOService

_HttpClientConnection 主要负责维持Socket连接和读写,通过_HttpParser实现Http协议包的封装和解析。

Dart 中的 Socket 既是个Stream可用于消费(读),还是个IOSink用于生产(写):

1
abstract class Socket implements Stream<Uint8List>, IOSink {...}

撑起 Socket 家族的是_NativeSocket,小心翼翼的维护着 Native 中创建的 Socket。

_Socket -> _RawSocket -> _NativeSocket -> dart::bin::Socket

_NativeSocket的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class _NativeSocket {
...
static Future<List<InternetAddress>> lookup(String host,
{InternetAddressType type: InternetAddressType.any}) {
return _IOService._dispatch(_IOService.socketLookup, [host, type._value])
.then((response) {
if (isErrorResponse(response)) {
throw createError(response, "Failed host lookup: '$host'");
} else {
return response.skip(1).map<InternetAddress>((result) {
var type = InternetAddressType._from(result[0]);
return _InternetAddress(type, result[1], host, result[2], result[3]);
}).toList();
}
});
}
...
Uint8List nativeRead(int len) native "Socket_Read";
int nativeWrite(List<int> buffer, int offset, int bytes)
native "Socket_WriteList";
nativeCreateConnect(Uint8List addr, int port, int scope_id)
native "Socket_CreateConnect";
...
}

通过阅读_NativeSocket 在Dart VM Runtime 层的对应的 C++ 代码可知,Socket 实现是非阻塞式(non-blocking) socket,再结合epoll 从而实现了async socket IO。

题外话,不阻塞读写是因为操作的是 buffer,而不会直接进行IO操作,需要用户进程不停地询问(poll)kernel 这个 buffer 是否就绪,再根据返回值做进一步处理(读/写/…)。还是没有明白?建议复习一下 Linux IO model,或者 Java 中的 NIO。

runtime/bin/socket_android.cc 中创建 socket 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
static intptr_t Create(const RawAddr& addr) {
intptr_t fd;
fd = NO_RETRY_EXPECTED(socket(addr.ss.ss_family, SOCK_STREAM, 0));
if (fd < 0) {
return -1;
}
if (!FDUtils::SetCloseOnExec(fd) || !FDUtils::SetNonBlocking(fd)) {
FDUtils::SaveErrorAndClose(fd);
return -1;
}
return fd;
}

在创建 socket 完毕后,并不会立马进行读写,而是通过一个叫EventHandler 实现的 IO 多路复用(IO multiplexing):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class _NativeSocket {
...
void setHandlers({read, write, error, closed, destroyed}) {
eventHandlers[readEvent] = read;
eventHandlers[writeEvent] = write;
eventHandlers[errorEvent] = error;
eventHandlers[closedEvent] = closed;
eventHandlers[destroyedEvent] = destroyed;
}

void setListening({bool read: true, bool write: true}) {
sendReadEvents = read;
sendWriteEvents = write;
if (read) issueReadEvent();
if (write) issueWriteEvent();
if (!flagsSent && !isClosing) {
flagsSent = true;
int flags = 1 << setEventMaskCommand;
if (!isClosedRead) flags |= 1 << readEvent;
if (!isClosedWrite) flags |= 1 << writeEvent;
sendToEventHandler(flags);
}
}

// Multiplexes socket events to the socket handlers.
void multiplex(Object eventsObj) {
...
}
void sendToEventHandler(int data) {
int fullData = (typeFlags & typeTypeMask) | data;
assert(!isClosing);
connectToEventHandler();
_EventHandler._sendData(this, eventPort.sendPort, fullData);
}

void connectToEventHandler() {
assert(!isClosed);
if (eventPort == null) {
eventPort = new RawReceivePort(multiplex);
}
...
}
}

其中,EventHandler 是通过 epoll 实现对 Socket IO 事件的异步处理,存活在一个独立线程,当 poll 到事件时和上述代码中的 eventPort 进行通信( 还是 isolate 那套(#°Д°) ),从而实现 IO 多路复用。_RawSocket 根据底层拉取到的事件最终分发到 _Socket类中进行读、写、关闭、销毁等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class _RawSocket extends Stream<RawSocketEvent> implements RawSocket {
final _NativeSocket _socket;
_RawSocket(this._socket) {
var zone = Zone.current;
_controller = new StreamController(
sync: true,
onListen: _onSubscriptionStateChange,
onCancel: _onSubscriptionStateChange,
onPause: _onPauseStateChange,
onResume: _onPauseStateChange);
_socket.setHandlers(
read: () => _controller.add(RawSocketEvent.read),
write: () {
// The write event handler is automatically disabled by the
// event handler when it fires.
writeEventsEnabled = false;
_controller.add(RawSocketEvent.write);
},
closed: () => _controller.add(RawSocketEvent.readClosed),
destroyed: () {
_controller.add(RawSocketEvent.closed);
_controller.close();
},
error: zone.bindBinaryCallbackGuarded((e, st) {
_controller.addError(e, st);
_socket.close();
}));
}
}

总结一下大致流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_HttpConnection
^
|
+
_Socket
^
| handle IO events
+
_RawSocket
^
| issue IO events
+
_NativeSocket
^
| multiplex
+
RawReceivePort
^
| handleEvents
+
EventHandler (epoll)

那么问题来了,有没有办法自己实现一个HttpClient用来替换dart:io默认的实现?

HttpOverrides

可以通过 HttpOverrides 提供自定义的 HttpClient实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyHttpClient implements HttpClient {
...
}

class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext context) {
return new MyHttpClient(context);
}
}
void main() {
HttpOverrides.global = MyHttpOverrides();
...
}

然而,如前文所述,HttpClient 提供的API都太底层了,如果完全实现,工作量巨大,还得自己处理Proxy、SSL证书校验、Socket读写、TCP包的解析。。。

通过 MethodChannel 实现 HttpClient

从客户端APP开发角度,Android 和 iOS 的各类 httpclient 实现都已经很成熟稳定了,自然而然想到能不能直接用原生代码来实现一个。

考虑HttpClient API 太过于底层,对于桥接原生实现不太友好,这里可以直接将接口收束到 http包的 Client类,这样实现起来简单多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
          +-----------+
| Client | package:http/http.dart
+------^----+
| implement
|
+----------+-------+
| |
+-----+-----+ +------+-----------+
| IOClient | | HttpClientPlugin |
+-----------+ +------+-----------+
dart:io | MethodChannel('httpclient')
|
+-----------+-----------+
| |
|Android | iOS
+---v---+ +---v--------+
|OkHttp | |NSURLSession|
+-------+ +------------+

而 Flutter 层用到 http 包的地方完全不用修改,只需要更换Client实现类即可。

1
2
Client client = HttpClientPlugin()
print(await client.read('https://yrom.net'));

大致流程上,HttpClientPlugin通过MethodChannelmethod, url, body, headers这些http请求参数直接分发给原生,原生再进一步调用相应的 HttpClient 接口。

Dart HttpClientPlugin类继承 BaseClient(package:http/http.dart 中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import 'dart:typed_data';

import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';

class HttpClientPlugin extends BaseClient {

static const MethodChannel channel = MethodChannel('httpclient');

@override
Future<StreamedResponse> send(BaseRequest request) async {
final String method = request.method;
assert(request != null && method != null);
Map<String, String> headers = CaseInsensitiveMap.from(request.headers);
var stream = request.finalize();
Uint8List bodyBytes = stream != null ? await stream.toBytes() : Uint8List(0);
Uri url = _validateUrl(request);

try {
var response = await channel.invokeMapMethod<String, dynamic>('send', {
'method': method,
'url': url.toString(),
if (requiresRequestBody(method)) 'body': bodyBytes,
'headers': headers,
});

Uint8List bytes = response['body'];
assert(bytes != null);
int contentLength = response['content_length'];
// -1 is unknown length
if (contentLength == null || contentLength == -1) {
contentLength = bytes.lengthInBytes;
}
int statusCode = response['status_code'];

Map<String, String> responseHeaders = CaseInsensitiveMap.from(response['headers'].cast<String, String>());

return StreamedResponse(
ByteStream.fromBytes(bytes),
statusCode,
contentLength: contentLength,
request: request,
headers: responseHeaders,
reasonPhrase: response['reason'],
);
} catch (e, stack) {
throw ClientException('Failed to send request via channel', request.url);
}
}

Uri _validateUrl(BaseRequest request) {
Uri url = request.url;
if (url == null) {
throw ArgumentError.notNull('request.url');
}
if (url.host.isEmpty) {
throw ArgumentError("No host specified in URI $url");
}
if (!url.isScheme('http') && !url.isScheme('https')) {
throw ArgumentError.value(
url.scheme,
'request.sheme',
);
}
return url;
}
}

bool requiresRequestBody(String method) {
return method == 'PUT' || method == 'POST' || method == 'PATCH';
}

Android HttpclientPlugin 实现类关键方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class HttpclientPlugin : FlutterPlugin, MethodCallHandler {
...
val client: OkHttpClient
fun sendRequest(
method: String,
url: String,
body: ByteArray?,
headers: Map<String, String>?,
result: Result
) {
val httpHeaders = if (headers != null) Headers.of(headers) else Headers.Builder().build()
val requestBody = if (body == null) null else object : RequestBody() {
override fun contentType(): MediaType? {
return httpHeaders["Content-Type"]?.let { MediaType.parse(it) }
}

override fun contentLength(): Long = body.size.toLong()

override fun writeTo(sink: BufferedSink) {
sink.write(body)
}
}
val request = Request.Builder().apply {
method(method, requestBody)
url(url)
headers(httpHeaders)
}.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
mainHandler.post { result.error("22", e.message, Log.getStackTraceString(e)) }
}

override fun onResponse(call: Call, response: Response) {
val map = response.use {
linkedMapOf(
"status_code" to it.code(),
"reason" to it.message(),
"headers" to response.headers().toStringMap(),
"content_length" to it.body()!!.contentLength(),
"body" to it.body()!!.bytes()
)
}
mainHandler.post {
result.success(map)
}
}
})
}
}

iOS 实现类关键方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void sendHttpRequest(NSString* method,
NSString* url,
FlutterStandardTypedData* body,
NSDictionary<NSString*, NSString*>* headers,
FlutterResult fResult) {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: url]];
if (method) {
[request setHTTPMethod:method];
}
if (body && [body elementCount] > 0) {
[request setHTTPBody: body.data];
}
if (headers && [headers count] > 0) {
[request setAllHTTPHeaderFields: headers];
}

NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSInteger statusCode;
NSString *reason;
NSDictionary* headers;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
statusCode = [(NSHTTPURLResponse *)response statusCode];
reason = [NSHTTPURLResponse localizedStringForStatusCode:statusCode];
headers = [(NSHTTPURLResponse *)response allHeaderFields];
} else {
statusCode = 200;
}

dispatch_async(dispatch_get_main_queue(), ^{
if (error == nil) {
NSMutableDictionary<NSString*, id>* dict = [[NSMutableDictionary alloc] init];
[dict setValue:[NSNumber numberWithInteger:statusCode] forKey:@"status_code"];
[dict setValue:reason forKey:@"reason"];
[dict setValue:[FlutterStandardTypedData typedDataWithBytes:data] forKey:@"body"];
[dict setValue:headers forKey:@"headers"];
fResult(dict);
} else {
fResult([FlutterError errorWithCode:[NSNumber numberWithInteger:[error code]].stringValue message:[error localizedFailureReason] details: [ error localizedDescription]]);
}
});
}];
[task resume];
}

总结

通过本文,相信你跟着我一起阅读了一遍 dart:io 中的 HttpClient源码,也了解了 Socket 大概实现,又复习了 IO Model。还通过 MethodChannel 实现了一个自己的 HttpClient Flutter 插件…

那么,恭喜你离精通 Flutter 又近了一步(*/ω\*)。

若发现文中错误欢迎评论指出,若有疑问也欢迎在评论区讨论。


广告环节

哔哩哔哩漫画招 Flutter 开发,欢迎投简历到邮箱 wangyongrong@bilibili.com