13

Flutter HttpClient Overview

 3 years ago
source link: https://yrom.net/blog/2020/06/18/flutter-httpclient-overview/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:ioHttpClient 所提供的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<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 关键代码:

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

大致流程:

    _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<Uint8List>, IOSink {...}

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

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

_NativeSocket 的关键代码:

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 的代码:

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<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();
        }));
  }
}

总结一下大致流程:

_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 通过 MethodChannelmethod , 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<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 实现类关键方法:

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) {
            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<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 又近了一步(*/ω\*)。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK