Flutter HttpClient Overview

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

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:io
HttpClient
所提供的API太过底层了,所以一般不会直接用,而是用Dart官方提供的 http
包(package:http),如下代码:

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 无响应吗?

class ExampleState extends State {
  String data = '';

  Future 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
关键代码:

Future _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);
  });
}

大致流程:

    _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
用于生产(写):

abstract class Socket implements Stream, IOSink {...}

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

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

_NativeSocket
的关键代码:

class _NativeSocket {
  ...
  static Future<List> 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((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 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 的代码:

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):

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
类中进行读、写、关闭、销毁等操作。

class _RawSocket extends Stream 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();
        }));
  }
}

总结一下大致流程:

_HttpConnection
     ^
     |
     +
  _Socket
     ^
     | handle IO events
     +
 _RawSocket
     ^
     | issue IO events
     +
_NativeSocket
     ^
     | multiplex
     +
RawReceivePort
     ^
     | handleEvents
     +
EventHandler (epoll)

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

HttpOverrides

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

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
类,这样实现起来简单多了:

          +-----------+
          |   Client  | package:http/http.dart
          +------^----+
                 | implement
                 |
      +----------+-------+
      |                  |
+-----+-----+     +------+-----------+
| IOClient  |     | HttpClientPlugin |
+-----------+     +------+-----------+
dart:io                  | MethodChannel('httpclient')
                         |
             +-----------+-----------+
             |                       |
             |Android                | iOS
         +---v---+               +---v--------+
         |OkHttp |               |NSURLSession|
         +-------+               +------------+

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

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

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

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

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 send(BaseRequest request) async {
    final String method = request.method;
    assert(request != null && method != null);
    Map 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('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 responseHeaders = CaseInsensitiveMap.from(response['headers'].cast());

      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 实现类关键方法:

public class HttpclientPlugin : FlutterPlugin, MethodCallHandler {
  ...
  val client: OkHttpClient
  fun sendRequest(
    method: String,
    url: String,
    body: ByteArray?,
    headers: Map?,
    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) {
            mainHandler.post {
              response.use {
                result.success(
                  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()
                  )
                )
              }
            }
          }
        })
  }
}

iOS 实现类关键方法:

void sendHttpRequest(NSString* method,
                     NSString* url,
                     FlutterStandardTypedData* body,
                     NSDictionary* 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* 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 又近了一步(*/ω\*)。
若发现文中错误欢迎评论指出,若有疑问也欢迎在评论区讨论。