http/2 over http tunnel proxy for Dart

Flutter 项目里用到了 http2 包 (https://pub.dev/packages/http2) 。但官方似乎没有支持 http proxy ,转念一想,似乎也对,proxy 这层不属于 http2 协议该负责实现的事。

研究了一下 http/1.1 tunnel proxy 的协议(RFC 2817)发现其实挺简单的。

这里直接放出示例了,仅供参考哦(需要登录的 proxy 就留给你自己实现了)。

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
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http2/http2.dart';

void main() async {
var uri = Uri.parse('https://cn.bing.com/search?q=http2');
var transport = await connectTunnel('127.0.0.1', 8888, uri.host);
var headers = [
Header.ascii(':method', 'GET'),
Header.ascii(':path', uri.hasQuery ? uri.path + '?' + uri.query : uri.path),
Header.ascii(':scheme', uri.scheme),
Header.ascii(':authority', uri.host),
];
var stream = transport.makeRequest(headers, endStream: true);
var bodyStream = StreamController<List<int>>();
utf8.decoder.bind(bodyStream.stream).listen((body) {
print(body);
});
await for (var message in stream.incomingMessages) {
if (message is HeadersStreamMessage) {
for (var header in message.headers) {
var name = utf8.decode(header.name);
var value = utf8.decode(header.value);
print('Header: $name: $value');
}
} else if (message is DataStreamMessage) {
bodyStream.add(message.bytes);
if (message.endStream) {
bodyStream.close();
}
}
}
if (!bodyStream.isClosed) {
await bodyStream.close();
}
await transport.finish();
}

Future<ClientTransportConnection> connectTunnel(String proxyHost, int proxyPort, String targetHost,
[int targetPort = 443]) async {
var proxy = await Socket.connect(proxyHost, proxyPort, timeout: const Duration(seconds: 1));
const CRLF = "\r\n";
proxy.write("CONNECT $targetHost:$targetPort HTTP/1.1"); // request line
proxy.write(CRLF);
proxy.write("Host: $targetHost:$targetPort"); // header
proxy.write(CRLF);
proxy.write(CRLF);
var completer = Completer<bool>.sync();
var sub = proxy.listen((event) {
var response = ascii.decode(event);
var lines = response.split(CRLF);
// status line
var statusLine = lines.first;
if (statusLine.startsWith("HTTP/1.1 200")) {
completer.complete();
} else {
completer.completeError(statusLine);
}
}, onError: completer.completeError);
await completer.future; // established
await sub.pause();

var socket = await SecureSocket.secure(proxy, host: targetHost, supportedProtocols: const ["h2"]);
return ClientTransportConnection.viaSocket(socket);
}

用一个简单示例告诉你怎样提升 Flutter App 中的动画性能

观前提醒:本文假设你已经有一定的 Flutter 开发经验,对Flutter 的 Widget,RenderObject 等概念有所了解,并且知道如何开启 DevTools。

现有一个简单的汽泡动画需要实现,如下图:

bubble animation

直接通过 AnimationController 实现

当看到这个效果图的时候,很快啊,啪一下思路就来了。涉及到动画,有状态,用 StatefulWidget ,State 里创建一个 AnimationController,用两个 Container 对应两个圈,外圈的 Container 的宽高监听动画跟着更新就行。
代码如下:

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
const double size = 56;

class BubbleAnimationByAnimationController extends StatefulWidget {
@override
_BubbleAnimationByAnimationControllerState createState() => _BubbleAnimationByAnimationControllerState();
}

class _BubbleAnimationByAnimationControllerState extends State<BubbleAnimationByAnimationController>
with SingleTickerProviderStateMixin {
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..addListener(() => setState(() {}));
_controller.repeat(reverse: true);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
// 两个 `Container` 对应两个圈
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.tight(
Size.square((1 + _controller.value * 0.2) * size),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[200],
),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
}
}

介绍一位新成员

雄赳赳气昂昂
过年立的 Flag —— 拥有一只猫主子 —— 达成。撒花 ╮( ̄▽ ̄)╭

不要抢我的玩具!
一只原主人卖不出去的雄性布偶。

瞎拍 - 夏至厦门

鼓浪屿
算起来,厦门只去过几回,也没怎么仔细逛过,这次趁人少,顺便去了一回鼓浪屿。

鼓浪屿月光岩上看厦门岛。

Flutter Logging

1. Install package logging

Add logging to pubspec.yaml file:

pubspec.yaml
1
2
dependencies:
logging: ^0.11.4

2. Config Logger

Initialize Logger before runApp() in main.dart file:

main.dart
1
2
3
4
5
6
7
8
9
import 'logging.dart';
void main() {
// config logger before runApp()
initLogging();
runApp(MyApp());

// log a debug message
log.fine('Running my flutter app.');
}
logging.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:logging/logging.dart';

// create Logger by name 'MyApp'
final log = Logger('MyApp');
void initLogging() {
// disable hierarchical logger
hierarchicalLoggingEnabled = false;
// change to another level as needed.
Logger.root.level = Level.INFO;
// skip logging stactrace below the SEVERE level.
recordStackTraceAtLevel = Level.SEVERE;
assert(() {
recordStackTraceAtLevel = Level.WARNING;
// print all logs on debug build.
Logger.root.level = Level.ALL;
return true;
}());
}

3. Transmit log records from Logger to dart:developer

Logger is a producer, but it will not post any log records if no one is listening.

Print to console:

logging.dart
1
2
3
Logger.root.onRecord.listen((event) {
print("${event.time}: [${event.level}] [${event.loggerName}] ${event.message}");
});

Save to file:

logging.dart
1
2
3
4
5
File logFile = ...
Logger.root.onRecord.listen((event) {
logFile.writeAsString("${event.time}: [${event.level}] [${event.loggerName}] ${event.message}",
mode: FileMode.append);
});

It’s strongly recommended to use dart:developer for logging:

logging.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:developer' as developer;

void initLogging() {
...
Logger.root.onRecord.listen((event) {
developer.log(
event.message,
time: event.time,
sequenceNumber: event.sequenceNumber,
level: event.level.value,
name: event.loggerName,
zone: event.zone,
error: event.error,
stackTrace: event.stackTrace,
);
});
}

Flutter HttpClient Overview

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

Flutter 同步系统的 HTTP 代理设置

一般的,在 Flutter APP 里请求 HTTP 使用的是官方提供的 http 包。

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

var url = 'https://jsonplaceholder.typicode.com/posts';
var response = await http.get(url);
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

print(await http.read('https://jsonplaceholder.typicode.com/posts/1'));

但是,有一个问题,在 Android 或者 iOS 上运行 Flutter APP,系统里配置的 HTTP 代理并不生效?

比如在使用 Charles 这种工具通过 HTTP 代理调试 API 请求时候,会发现 Flutter 的 http 请求没有按预期走代理,无论是 Http 还是 Https。

那些初学者实践 Flutter 最常出现的错误

哔哩哔哩漫画APP实践Flutter也有大半年时间了,我针对线上收集到的错误进行分析,挑选出了一些有一般代表性的错误,列在本文,可供实践 Flutter 的初学者们作为一点参考。

典型错误一:无法掌握的Future

典型错误信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.

这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。

异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted,继续调用 setState 就会出现这个错误。

示例代码

一段很常见的获取网络数据的代码,调用 requestApi(),等待Future从中获取response,进而setState刷新 Widget:

1
2
3
4
5
6
7
8
9
10
class AWidgetState extends State<AWidget> {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
setState((){
this.data = response.data;
})
}
}

原因分析

response 的获取为async-await异步任务,完全有可能在AWidgetStatedispose之后才等到返回,那时候和该State 绑定的 Element 已经不在了。故而在setState时需要容错。

解决办法: setState之前检查是否 mounted

1
2
3
4
5
6
7
8
9
10
11
12
class AWidgetState extends State {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
if (mounted) {
setState((){
this.data = response.data;
})
}
}
}

这个mounted检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。

比如,在 FrameCallback里执行一个动画(AnimationController):

1
2
3
4
5
6
@override
void initState(){
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _animationController.forward();
});
}

AnimationController有可能随着 State 一起 dispose了,但是FrameCallback仍然会被执行,进而导致异常。

又比如,在动画监听的回调里搞点事:

1
2
3
4
5
6
7
8
@override
void initState(){
_animationController.animation.addListener(_handleAnimationTick);
}

void _handleAnimationTick() {
if (mounted) updateWidget(...);
}

同样的在_handleAnimationTick被回调前,State 也有可能已经被dispose了。

如果你还不理解为什么,请仔细回味一下Event loop 还有复习一下 Dart 的线程模型。

相亲困境

最近 2019-nCoV 疫情漫延,天天闲在家也没事干,突然觉得非常孤单寂寞冷,今天回顾了一下我相亲这么多回还是孑然一身的原因。以下为正文。。


看不到图请右键新窗口中打开

每逢假期,回老家相亲是不可回避的乡亲们茶余饭后喜闻乐见的节目。

素未谋面的两人抽出时间,凑在一起碰过面之后,如果一方觉得可以进一步发展试试,但却不知对方的态度,接下来就会纠结:是主动表态还是被动等待。

一旦开始了选择,这其实可以将理性的双方看作处于一场非零和博弈中了。

双方组合起来有如下四种策略:

🧑主动🧑等待
👨主动(2,2)(0,3)
👨等待(3,0)(1,1)
  1. 如果🧑👨双方都主动表达好感👩‍❤️‍💋‍👨,意味着可以顺利发展下一步,这时可认为双方获得奖励性回报,双赢,各得2分。
  2. 如果🧑主动表达好感,而👨没有,那么认为👨得到“渣男卡”回报3分,而🧑得0分。
  3. 反过来,👨主动表态,热脸贴冷屁股,👨获得“好人卡”回报0分,🧑3分。
  4. 如果🧑👨都等待对方表态,那多半意味着相亲失败,不过多认识了朋友而已,各得1分。

在实际“博弈”中,理性的🧑👨双方都会站在自己的角度,只考虑到自己的最佳回报,不约而同地选择了等待(“纳什均衡”),亦即这场相亲最后不了了之,bad end。

哔哩哔哩漫画 Flutter 混合开发实践

哔哩哔哩的漫画业务是在18年末正式官宣成立的,之后便有了收(jie)购(pan)网易漫画的事。虽说是“新业务”,其实内部立项之早或可追溯到2017年,APP 的代码饱含着历史沧桑感,流淌来自B站内部多个部门程序员的心血,杂糅了多种技术栈和代码风格,令现维护者头疼到头秃。

2019年初适逢 Flutter 声名鹊起,经过一段时间技术调研和在网易漫画 APP 上试验后,便开始在哔哩哔哩漫画 APP (后文简称B漫)上紧锣密鼓一步一个脚印跟着版本迭代与用户见面。我给 Flutter 在B漫的落地计划起了个名叫“胡辣汤计划”


胡辣汤

一晃时间到了19年底,不过区区 6 人的小组将“胡辣汤计划”已经从试验阶段实施到成熟阶段,开始直接对接新页面和业务功能。iOS 和 Android 双端 APP 已由 Flutter 覆盖了多个主路径页面,重写的页面和原来页面相比用户几乎没有感知到差别,日均超七成活跃用户使用过 Flutter 所呈现的页面功能,并且维持着这些页面功能 0.01% 的低错误率、崩溃率,这不能说不是得益于 Flutter 的优秀设计。


网易漫画和哔哩哔哩漫画