Gone With the Wind

一段有意思的Nodejs异步处理代码

首先看下一段代码的输出是什么:

1
setTimeout( () => {
    console.log( 1 );
}, 1 );

setImmediate( () => {
    console.log( 2 );
} );

process.nextTick( () => {
    console.log( 3 );
} );

我猜大概大部分人的电脑上的输出结果是:3 1 2

再看看下一段代码的执行结果:

1
setTimeout( () => {
    console.log( 1 );
}, 1 );

setImmediate( () => {
    console.log( 2 );
} );

其实就是把process.nextTick调用去掉了,这时候你会发现输出开始变得较为随机:1 2或者2 1

那么第一段代码哪些人的电脑输出结果会不一样?土豪!没错就是土豪,土豪不仅仅在生活上碾压你,在代码上甚至都是如此。

NodeJs的EventLoop

有关NodeJs的EventLoop不多说了,网上讲解文章非常多,官网就有一篇讲得非常好也通俗易懂的文章:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

看完上面的文章也许你对第二段代码的随机输出明白了,但是第一段为何始终都是3 1 2呢?

为什么是3 1 2?

也许你会觉得结果应该会是在3 2 13 1 2之间随机,但是你可以在你自己的电脑上面试试看,基本永远都是3 1 2,那么为什么呢?

上面的文章只介绍了事件循环相关的内容,但是对于nextTick以及整个NodeJs执行流程的介绍并不多,这里自己通过查看源码发现以下流程:

  1. NodeJs启动阶段

    • 准备Node环境
    • 执行代码
    • 检查nextTickQueue,如果有就执行。
  2. 开始事件循环,这一步一定是在启动阶段之后,因为事件循环的前提是得有事件触发,事件触发的前提就是执行了我们的代码,如果没有触发事件,那么Node进程就退出了。

    • timer
    • I/O

看到这里,就已经能非常好地解释上面的代码了:

  1. 首先我们的主程序执行完,nextTickQueue中有一个回调,首先执行这个回调,打印一个console.log

  2. 然后开始事件循环,执行TimerI/O等阶段,然而因为我们设置的timer的超时时间是1毫秒,而我们在nextTick里面的回调的console.log还是比较耗时间的,到了Timer阶段,1毫秒早就过去了,所以执行结果永远都是3 1 2

其实我测试了一下,我的电脑设置为setTimeout( ..., 4 ),4毫秒的延迟后,就会开始出现随机结果,也就是一次console.log差不多花费3毫秒的时间,相当耗时啊,并且在process.nextTick回调里面把console.log去掉后,开始出现随机1 2或者2 1的结果。

为什么土豪的执行结果可能不是3 1 2呢?土豪的电脑配置高,一个console.log也许花不了几微秒,Timer阶段检查1毫秒没到,就往下执行了,之后就到了check阶段,执行setImmediate的回调,输出就变成了3 2 1

所以,努力奋斗吧,争取买得起一台输出结果是3 2 1的电脑,哈哈。

强类型?弱类型?强类型!

“灵活”的JavaScript

以前我很喜欢JavaScript,并不仅仅是因为他是我吃饭的工具,更多因为他非常灵活,从学校了写多了C++或者Java,遇到各种诡异的编译链接等等的错误,到JavaScript弱类型动态语言,简直就是满地撒欢。

我可以使用各种奇淫巧技,虽然比不上Python,也足够让我用1行代码实现别人3行代码的功能,当时我以同样的功能下代码字符越少越牛逼为荣。

然而慢慢地,别人开始接手我写的代码,包括自己开始看以前自己写的代码,虽然别人有三行代码,但是一眼就看明白了三行代码所表达的意思,而我的一行代码,看了好几分钟才能看明白所表达的意思,有时候还不一定正确,也许需要跑起来才能知道效果,我开始质疑自己的这种行为,比如以下行为:

  • ~~n, 效果等同于Math.floor( n ),虽然运行更快,但是一眼并不能看出什么意思,而函数Math.floor一眼就能看出什么意思。
  • n === n,效果等同于isNaN( n ),因为NaN并不等于他自己。
  • (![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]甚至包括这种,我还真写过,只是没有这么夸张。

灵活的代价

下面的代码是一个真实的例子,我调用了别人的代码,他的函数:

1
function test() {
    ...

    // 这里返回是一个Promise
    return doSomeAsync();
}

返回的是一个Promise,所以我用await去调用,到这里并没有问题。

而出现问题来源于一次需求的增加,在执行这种函数时,需要判断一些意外情况,如果出现意外,就弹出一个对话框,让用户点击确认后才继续执行,于是他的代码变成了:

1

function test( needConfirm ) {

    ...

    if ( needConfirm ) {
        Dialog.show( {
            content: '您确定要继续执行么?',
            onOk: () => {
                doSomeAsync();
            }
        } );
        return;
    }

    // 这里返回是一个Promise
    return doSomeAsync();

}

这么改动乍一看也没什么问题,如果需要确认,就弹出一个对话框,给用户确认,当用户点击确认的时候,在回调函数里面执行原本的操作。

而对于我的代码来说,我依赖于doSomeAsync执行的结束,所以,当我任然使用await test( true )去调用时,我的代码就报错了,因为true === needConfirm的时候返回的是undefinedawait并不会等待,而是继续执行下面的代码,所以导致了错误的出现。

“束缚”的TypeScript

相信以上的代码绝大部分人都写过,我们再仔细看下,其实上面这段代码的问题在于函数的行为不一致,正常情况是返回一个Promise,而分支情况是返回一个undefined,我们看看在TypeScript下怎么写这段代码:

1
function test( needConfirm: boolean ): Promise<void> {

    ...

    if ( true === needConfirm ) {
        Dialog.show( {
            content: '您确认要继续执行么?',
            onOk: () => {
                doSomeAsync();
            }
        } );
        return;
    }

    return doSomeAsync();
}

如果你任然这么写,那么TypeScript的编译阶段就会报错,因为你规定了函数需要返回一个Promise,但是却存在返回undefined的情况,你不得不去修改你的代码:

1

functon test( needConfirm: boolean ): Promise<void> {

    ...

    if ( true === needConfirm ) {

        let resolveFunc = null;

        Dialog.show( {
            content: '您确定要继续执行么?',
            onOk: async function(): Promise<void> {
                const result = await doSomeAsync();
                resolveFunc( result );
            }
        } );

        return new Promise( ( resolve, reject ) => {
            resolveFunc = resolve;
        } );
    }
    return doSomeAsync();
}

编译通过,函数行为一致,调用者也无需感知里面的逻辑。

最后

强类型写起来限制会更多,然而对于代码的可维护性,可运行性都有很大的提升。

现在前端的复杂性提升地非常快,几年前也许前端仅仅只是更多作为展示型页面存在,但是现在的前端已经承载了太多的东西,用户的交互也发生了很大变化,JavaScript也可以写ReactNative的APP,可以写NodeJs服务,可以用NodeWebkit等工具写跨平台的客户端,使用TypeScript严格按照强类型语言去写你的应用,让你的应用具备更高的可读性和可维护性。

当然事事无绝对,还是得看你的场景,杀鸡焉用牛刀,JavaScript自有存在的意义,至少是目前浏览器官方语言,当你的应用重逻辑时,放手用`TypeScript吧。

在typescript中使用delegate来进行解耦

无处不在的耦合

通常你写的一个React组件,如果需要和别的组件发生交互,那么你会怎么做?

1
public componentDidMount(): void {
	this.refs.mycom.test();
}

public render() {
	return (
		<MyCom ref="mycom" />
	);
}

这种做法相对来说比较普遍,也比较合情合理,那么如果MyCom组件执行了什么操作之后需要通知到父组件呢?

1
public onNotify(): void {

}

public render() {
	return (
		<MyCom notify={ this.onNotify.bind( this ) } ref="mycom" />
	);
}

很多时候我们会选择传进去一个回调函数,当MyCom需要发送通知的时候再调用props.notify

虽然这种方式能实现我们的需求,但也仅仅是实现而已,你会发现这种方式并不是很优雅,而且当你的回调函数多了之后,那简直就是噩梦,后来来使用这个组件的人完全不知道需要传哪些回调函数进去,也不知道哪些回调是可选的,哪些是必选的。

protocol

ProtocolOc里面的概念,类似Java里面的Interface,和TypeScript中的Interface也比较类似,其中有一个用处就是用于两个类之间的通信解耦,而delegate更多的是一种约定俗成的规则,看看具体怎么用:

首先我有一个A类,用

A.h

1
@protocol ADelegate
	- (void) methodFromA;
@end

@interface A : NSObject
// 声明委托变量
@property (assign, nonatomic) id<ADelegate> delegate;
    - (void) test;
@end

A.m

1

// 在A类的实现文件里面,在需要进行回调的时候,只需要调用当前实例的delegate对象,而不用理会到底是谁调用了自己,而且对方肯定实现了delegate指向的protocol
// 需要的时候调用
[self.delegate methodFromA];

此时,我有一个B类,需要调用A类,同时需要监听回调函数:

B.h

1
#import "A.h"

// 这里实现了Adelegate Protocol
@interface B : NSObject <ADelegate>
...
@end

B.m

1
- (void) someMethod {
	A *aInstance = [[A alloc] init];
	// 这里把当前实例赋值给A类实例的delegate属性
	aInstance.delegate = self;	
}

// 实现了ADelegate
- (void) methodFromA {
	当A类调用self.delegate.methodFromA时,就可以在这里执行你自己的代码了
	// ...
}

TypeScript中的delegate

上面可以看到其实delegate的使用非常简单,只是一种约定的规则,同时delegate其实也是一种设计模式,在JS中其实也可以使用,只是JS是弱类型语言,无法通过编译器来约束一些行为,所以依赖于TypeScript的强类型,我们也可以来使用一下delegate

组件A

1
export interface ADelegate {
	methodFromA();
}

export class A extends React.Component<any, any> {
	public delegate: ADelegate;

	public render() {

		// 调用delegate的方法
		this.delegate.methodFromA();

		return (
			<div></div>
		);

	}

}

组件B

1
import { A, ADelegate } from 'A';

// 这里实现了Interface: ADelegate
export class B extends React.Component<any, any> implements ADelegate {
	// 从A实现的回调。
	public methodFromA(): void {

	}

	public componentDidMount(): void {
		const aCom: A = <A>this.refs.acom;
		aCom.delegate = this;
	}

	public render() {
		return (
			<A ref="acom" />
		);
	}
}

效果

上面的代码可以看到,A组件面向的不是调用他的组件,而是他自己声明的delegate,完全无需关心是谁在调用自己,在什么场景下调用自己

而B组件也完全不用关心A组件的具体实现,只要实现A组件提供的Interface:ADelegate,从中选择自己需要的方法即可,两者实现解耦,面向delegate编程。

webpack的chunk id和cdn缓存之间的爱恨情仇

之前项目中引入了一个插件,把webpack的chunk id使用md5代替,使得其无序化,开发环境没有问题,但是在配合CDN使用的时候,却发现每次发布新版本之后,页面直接报错打不开,提示加载某某chunk的时候无法找到,清除缓存之后问题解决,然而把id无序化插件去掉之后就没问题了,看来问题和缓存和chunk id有关。

起因

webpack chunk

先说说webpack的chunk是怎么一回事,如下图:

有四个项目文件,分别为:file1.js, file2.js, file3.js, file4.js.

1
file1.js
file2.js

file3.js
file4.js

其中,file1依赖了file2file3依赖了file4,在webpack里面,通常情况下一个文件就是一个module,有时候也有一个文件多个module的情况,不过不多见,webpack会给每一个module分配一个id,这个id是自增的:

1
file1.js -> module1, id: 1
file2.js -> module2, id: 2

file3.js -> module3, id: 3
file4.js -> module4, id: 4

因为file1file2对于file3file4都没有什么依赖关系,所以webpack把这四个文件分成了两个chunk,同时分别给了一个id,也是自增的:

1
chunkA: ChunkId1
	-> module1
	-> module2

chunkB: ChunkId2
	-> module3
	-> module4

一个chunk就对应于webpack打包之后的文件,于是,打包之后我们现在有两个文件:

1

file1.js
            filea.hash.js
file2.js

----------------------------------

file3.js
            fileb.hash.js
file4.js

两个chunk所对应的打包后的文件名为: ${fileName}.${md5}.js.

CDN缓存

项目中使用了覆盖式的CDN缓存方式,也就是不同发布之间没有版本概念,而是直接覆盖原来的文件,因为文件名中有MD5值,所以只要文件内容改变,那么文件名也就改变。

好处是如果新版本没有变更的文件,可以让浏览器复用之前的缓存,达到最小更新目的。

问题出现

原本这一切都运行得不错,直到后来在项目中加入了一个webpack插件,用来做chunk id的无序化,至于加入这个插件的原因,是因为webpack自带的chunk id的生成规则:

webpack chunk id生成方式

webpack的chunk id是从1开始自增的,具体有自己的规则,比如根据引用顺序来给chunk id给定一个id,文件内容的改变并不会影响id,只要引用顺序不变,那么即使文件内容如何改变,都不会影响chunk id,多次打包之后的值都是一样的。

chunk id无序化

上面说chunk id从1开始自增,也就是不同项目之间,chunk id可能都是一样的,这就导致跨项目间调用chunk变得困难了,为了解决这个问题,就引入了一个插件,可以让chunk id无序化,实质上就是用MD5值来代替了chunk id。

为何在CDN场景下发布新版本会导致找不到chunk呢?

把chunk id用MD5值代替,看起来确实没有问题,在版本化的CDN场景下使用或者说在本地开发环境中使用确实是没有问题的,问题出在和覆盖式的CDN同时使用时候的问题。

问题根源

首先我们看chunka和chunkb打包之后的代码:

chunka:

1
...
__webpack__require.e( '1' ).then( '....' );
...

chunkb:

1
webpackJsonp([1],{
	...
});

chunka中代码,打包前的代码为:

1
import( './file3' );

chunka中需要引用id为1的chunk,然后chunkb的代码中,首先用webpackJsonp方法定义了自己的chunk id为:1,这样,整个代码就联系起来了,也就能正常运行了。

MD5替代chunk id

如果使用MD5来替换原先的chunk id呢?

chunka:

1
...
__webpack__require.e( 'md51' ).then( '....' );
...

chunkb:

1
webpackJsonp(['md51'],{
	...
});

看起来也没什么问题,反正只要引用的chunk id和声明的chunk id对应上就可以了。尝试修改了一些file3的内容,也就是chunkb的内容有了变更之后:

chunka:

1
...
__webpack__require.e( 'md52' ).then( '....' );
...

chunkb:

1
webpackJsonp(['md52'],{
	...
});

chunkb的chunk id变成了: md52,同时引用和声明的chunk id都能对应上,运行没问题,那么问题出在哪里呢?我们看看webpack生成文件的文件名:

1
${fileName}.${md5}.js

因为文件名中有MD5值,所以当修改了chunkb之后,那么chunkb对应的文件名肯定就变更了,这个没问题,而chunka其实内容并没有变更,所以chunka的md5没有变化,也就是说chunka的文件名不变。

然而在引入chunk id无序化插件之前,chunkb的内容变化了,chunk id是不会变的,所以chunk b变化之后,打包后的chunka的文件名和内容都是没有变化的,使用浏览器缓存也不会有任何问题。

缓存带来的问题

chunk id变成MD5之后,chunkb的改变,会导致chunka的文件名没有变,然而chuna的代码其实是变化了的,因为引用chunkb的时候是要填写chunkb的chunk id的,因为chunk b的内容改变,其chunk id也跟着改变,浏览器根据文件名缓存,因为文件名没有变化,所以一直读取的都是以前的文件,也就导致了找不到某某chunk的根源,以前因为内容变化了,chunk id是不会变化的,所以使用并没有问题。

webpack MD5生成

也许你会说chunka的内容其实是变化了,为啥md5没有改变?其实一开始我也是这么想的,后来看过webpack代码之后就知道原因了:

上面是生成hash的事件,而替换代码则远远在这之后,其实之前也猜到个大概,从业务上讲其实chunka的内容并没有任何变更,hash也是不应该改变的。

keyof

typeof

react 泛型组件继承和使用

声明文件的使用

void不能省略

最近对于懒加载的一些思考

记得我刚毕业那会,前端还是jQuery的天下,没有什么项目是一个jQuery搞不定的,如果有,那就再加个插件。

后来慢慢出现了一些前端模块化工具,直至seajs的出现,把模块化推向了一个高潮,期间听说了gmail前端有44万行的JS代码,也是非常震惊。

现在的前端为了更好的用户体验,更快地适应业务,逻辑正在变得越来越复杂,同时也带来越来越多的问题。

体积臃肿的前端

使用webpack的同学对下面这张图很熟悉了,一般我们用webpack打出来的包,都会包含一大堆东西,node_modules文件内容甚至能占到90%以上。

冰山效应

很多时候,用户打开一个页面,同时也加载了很多在这个页面上并不会运行的代码,甚至是永远都不会运行的代码,页面上的这些代码就像海洋里的冰山,运行的只是海平面上很小一部分,很大一部分在海平面下的代码,仅仅只会拖慢我们首页的打开时间。

按需加载

从有模块化起,貌似就有了按需加载功能,很多工具都提供了按需加载的功能,以webpack为例:

1
const jQuery = await import( 'jQuery' );
 
const model = await import( './model/data.js' );

webpack内置函数import很容易就可以实现按需加载的功能,看起来似乎问题就已经解了?然而这只是问题的第一步,仅仅有了按需加载的能力,问题也会随之而来。

预加载

按需加载必然会带来一次网络请求,假设一个场景:

1
用户点击一个按钮,然后跳出一个弹框。

普通情况下,点击按钮之后发生的事情如下:

1
点击按钮(ms) -> 渲染弹框(ms)

如果我们把弹框做按需加载,那么点击按钮之后发生的事情如下:

1
点击按钮(ms) -> 加载弹框代码(s) -> 渲染弹框(ms)

中间多了一个步骤,并且是按秒计算的从网络加载代码,点击一个按钮之后需要等待一段时间,才会弹出弹框,用户操作非常不流畅,如果存在大量这种场景,体验或许还不如不用按需加载。

有什么办法能解这个问题么?一种方案是:预加载,请看下图:

预加载步骤

  1. 首先页面上收集用户的点击行为日志。
  2. 拿到点击行为日志,得出用户最常用的操作路径,这部分数据在静态打包的时候注入到前端。
  3. 每个人的操作路径会有区别,这里可以在前端也记录一份当前用户的点击数据,这里很多时候虽然本地都已经有缓存了,不过提前把缓存加载到内存,也可以节省很多时间。
  4. 在浏览器空闲时,按照第二步和第三步的数据计算权重,最终决定加载顺序。
  5. 同时,前端可以根据鼠标运动轨迹,实时地来预测下一步可能会加载的内容,作为一个旁路辅助。这里目前并没有想到一个很好的办法。

缓存

鉴于目前前端代码普遍存在于CDN的情况,按需加载带来的另外一个非常有优势的地方在于CDN缓存,假设我们有一份jQuery,有两个文件分别依赖了jQuery,情况如下:

a.js

1
var jQuery = function(){ /*jquery代码*/ }

jQuery.query( 'xxx' );

b.js

1
var jQuery = function(){ /*jquery代码*/ }

jQuery.query( 'xxx' );

用户在打开A页面和B页面的时候,分别需要下载a.jsb.js,这时候重复下载了两份jQuery,即使他们是一样的,同时在用户的磁盘,也缓存了两份jQuery

按需加载后的CDN缓存

那么如果我们用按需加载的方式呢?

http://cdn.com/jquery.js

1
var jQuery = function(){ /*jquery代码*/ }

a.js

1
var jQuery = import( 'http://cdn.com/jquery.js' );

jQuery.query( 'xxx' );

b.js

1
var jQuery = import( 'http://cdn.com/jquery.js' );

jQuery.query( 'xxx' );

jQuery.js只会被下载一次,所有页面如果用的都是一个cdn地址,那么只要用户打开过任何一个页面,之后的页面即使用户从未打开过,jQuery也已经被缓存,这带来的是一个1+1>2的效果。

理想中的按需加载

最后说一说我理想中的按需加载,看下图:

  1. 逻辑层和展示层完全剥离。
  2. 用户操作一个页面的顺序,一定是:
    1. 眼睛看到展示层,发现有一个按钮。
    2. 大脑告诉你,这个是一个按钮,是可以点击的。
    3. 用手控制鼠标去点击按钮。
    4. 执行点击按钮之后事情。
  3. 首屏加载的时候,我们可以只加载展示层,逻辑层代码一定是在用户点击按钮之后才会执行。
  4. 首屏加载完成,利用用户看页面,并点击按钮的时间,预加载逻辑层代码,实现最快速的首屏展示。

什么是MTU?为什么MTU值普遍都是1500?

大学那会我玩魔兽世界,我的职业是法师,然后经常有朋友找我我带小号,带小号的方式是冲到血色副本里面把所有怪拉到一起,然后一起用AOE技能瞬间杀掉,在学校玩的时候没什么问题,但是放假在家的时候,我发现每次我拉好怪,放技能AOE的那个瞬间,很大概率会掉线,也不是网速问题,当时很多人也遇到同样的问题,看到个帖子说,把自己的MTU改成1480就行了,当时也不知道啥是MTU,就改了,发现还真的可以,就愉快地打游戏去了,多年以后我才知道MTU的重要性。

什么是MTU

Maximum Transmission Unit,缩写MTU,中文名是:最大传输单元。

这是哪一层网络的概念?

从下面这个表格中可以看到,在7层网络协议中,MTU是数据链路层的概念。MTU限制的是数据链路层的上层协议的payload的大小,例如IP,ICMP等。

OSI中的层 功能 TCP/IP协议族
应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
表示层 数据格式化,代码转换,数据加密 没有协议
会话层 解除或建立与别的接点的联系 没有协议
传输层 提供端对端的接口 TCP,UDP
网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP
数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU
物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2

MTU有什么用?

举一个最简单的场景,你在家用自己的笔记本上网,用的是路由器,路由器连接电信网络,然后访问了www.baidu.com,从你的笔记本出发的一个以太网数据帧总共经过了以下路径:

1
笔记本 -> 路由器 -> 电信机房 -> 服务器

其中,每个节点都有一个MTU值,如下:

1
1500     1500                 1500
笔记本 -> 路由器 -> 电信机房  -> 服务器

假设现在我把笔记本的MTU最大值设置成了1700,然后发送了一个超大的ip数据包(2000),这时候在以外网传输的时候会被拆成2个包,一个1700,一个300,然后加上头信息进行传输。

1
1700     1500                1500
笔记本 -> 路由器 -> 电信机房 -> 服务器

路由器接收到了一个1700的帧,发现大于自己设置的最大值:1500,如果IP包DF标志位为1,也就是不允许分包,那么路由器直接就把这个包丢弃了,根本就不会到达电信机房,也就到不了服务器了,所以,到这里我们就会发现,MTU其实就是在每一个节点的管控值,只要是大于这个值的数据帧,要么选择分片,要么直接丢弃。

为什么是1500?

其实一个标准的以太网数据帧大小是:1518,头信息有14字节,尾部校验和FCS占了4字节,所以真正留给上层协议传输数据的大小就是:1518 - 14 - 4 = 1500,那么,1518这个值又是从哪里来的呢?

假设取一个更大的值

假设MTU值和IP数据包大小一致,一个IP数据包的大小是:65535,那么加上以太网帧头和为,一个以太网帧的大小就是:65535 + 14 + 4 = 65553,看起来似乎很完美,发送方也不需要拆包,接收方也不需要重组。

那么假设我们现在的带宽是:100Mbps,因为以太网帧是传输中的最小可识别单元,再往下就是0101所对应的光信号了,所以我们的一条带宽同时只能发送一个以太网帧。如果同时发送多个,那么对端就无法重组成一个以太网帧了,在100Mbps的带宽中(假设中间没有损耗),我们计算一下发送这一帧需要的时间:

1
( 65553 * 8 ) / ( 100 * 1024 * 1024 ) ≈ 0.005(s)

在100M网络下传输一帧就需要5ms,也就是说这5ms其他进程发送不了任何数据。如果是早先的电话拨号,网速只有2M的情况下:

1
( 65553 * 8 ) / ( 2 * 1024 * 1024 ) ≈ 0.100(s)

100ms,这简直是噩梦。其实这就像红绿灯,时间要设置合理,交替通行,不然同一个方向如果一直是绿灯,那么另一个方向就要堵成翔了。

既然大了不行,那设置小一点可以么?

假设MTU值设置为100,那么单个帧传输的时间,在2Mbps带宽下需要:

1
( 100 * 8 ) / ( 2 * 1024 * 1024 ) * 1000 ≈ 5(ms)

时间上已经能接受了,问题在于,不管MTU设置为多少,以太网头帧尾大小是固定的,都是14 + 4,所以在MTU为100的时候,一个以太网帧的传输效率为:

1
( 100 - 14 - 4 ) / 100 = 82%

写成公式就是:( T - 14 - 4 ) / T,当T趋于无穷大的时候,效率接近100%,也就是MTU的值越大,传输效率最高,但是基于上一点传输时间的问题,来个折中的选择吧,既然头加尾是18,那就凑个整来个1500,总大小就是1518,传输效率:

1
1500 / 1518 =  98.8%

100Mbps传输时间:

1
( 1518 * 8 ) / ( 100 * 1024 * 1024 ) * 1000 = 0.11(ms)

2Mbps传输时间:

1
( 1518 * 8 ) / ( 2 * 1024 * 1024 ) * 1000 = 5.79(ms)

总体上时间都还能接受

最小值被限制在64

为什么是64呢?

这个其实和以太网帧在半双工下的碰撞有关,感兴趣的同学可以自行去搜索。

在我玩游戏的时候,为什么把MTU改成1480就不卡了?

路由器默认值大多都是1500,理论上是没有问题的,那为什么我玩游戏的时候改成1480才能流畅呢?原因在于当时我使用的是ADSL上网的方式,ADSL使用的PPPoE协议。

PPPoE

PPPoE协议介于以太网和IP之间,协议分为两部分,PPP( Point to Point Protocol )和oE( over Ethernet ),也就是以太网上的PPP协议,而PPPoE协议头信息为:

1
| VER(4bit) | TYPE(4bit) | CODE(8bit) | SESSION-ID(16bit) | LENGTH(16bit) |

这里总共是48位,也就是6个字节,那么另外2个字节是什么呢?答案是PPP协议的ID号,占用两个字节,所以在PPPoE环境下,最佳MTU值应该是:1500 - 4 - 2 = 1492。

我的上网方式

当时我的上网路径如下:

1
PC -> 路由器 -> 电信

我在路由器进行拨号,然后PC连接路由器进行上网。

最根本原因

问题就出在路由器拨号,如果是PC拨号,那么PC会进行PPPoE的封装,会按照MTU:1492来进行以太网帧的封装,即使通过路由器,路由器这时候也只是转发而已,不会进行拆包。

而当用路由器拨号时,PC并不知道路由器的通信方式,会以网卡的设置,默认1500的MTU来进行以太网帧的封装,到达路由器时,由于路由器需要进行PPPoE协议的封装,加上8字节的头信息,这样一来,就必须进行拆包,路由器把这一帧的内容拆成两帧发送,一帧是1492,一帧是8,然后分别加上PPPoE的头进行发送。

平时玩游戏不卡,是因为数据量路由器还处理得过来,而当进行群怪AOE的时候,由于短时间数据量过大,路由器处理不过来,就会发生丢包卡顿的情况,也就掉线了。

帖子里面提到的1480,猜测可能是尽量设小一点,避免二次拨号带来的又一次PPPoE的封装,因为时间久远,没办法回到当时的场景再去抓包了。

结论

1518这个值是考虑到传输效率以及传输时间而折中选择的一个值,并且由于目前网络链路中的节点太多,其中某个节点的MTU值如果和别的节点不一样,就很容易带来拆包重组的问题,甚至会导致无法发送。

前端如何获取主域名(根域名)

背景

最近项目中需要获取url的主域名,比如www.baidu.com那么就需要获取baidu.com,看似简单,.号分隔,取到最后两位就行,但是坑爹的是有xxx.com.cn这类域名,还有很多日本的域名,类似toei.aichi.jp等,这些都无法通过这种简单的取最后两位的方式来获取,看来只能枚举了。

Public Suffix List

这问题肯定是早有人就遇到了,于是各路有识之士已经帮你完整得准备好了一个列表,里面全部都是那些奇葩域名,一些jp域名也是让我长见识了,不知道各位老司机在秋名山飙车的时候有没有见过这些个域名:

1
秋田.jp
群馬.jp
香川.jp
高知.jp
鳥取.jp
鹿児島.jp
// jp geographic type names
// http://jprs.jp/doc/rule/saisoku-1.html
*.kawasaki.jp
*.kitakyushu.jp
*.kobe.jp
*.nagoya.jp
*.sapporo.jp
*.sendai.jp
*.yokohama.jp
!city.kawasaki.jp
!city.kitakyushu.jp
!city.kobe.jp
!city.nagoya.jp
!city.sapporo.jp
!city.sendai.jp
!city.yokohama.jp
// 4th level registration
aisai.aichi.jp

感兴趣的朋友可以看看这个github项目:https://github.com/wrangr/psl

这里有各种主域名的列表:https://publicsuffix.org/list/public_suffix_list.dat.

浏览器其实也有内置类似的东西,用来做域名判断,cookie存储之类的事宜。

pls的问题

问题看似好像解决了,已经有现成的脚本去获取,但是仔细一看这脚本竟然有将近200K,而我自己的脚本才10K,既然浏览器已经内置了pls,那浏览器有没有暴露内置接口呢?很遗憾,搜索了一下并没有,而且浏览器那么多,即使chrome暴露了,IE肯定没有,等等,刚刚好像我们说到浏览器用来做域名判断,cookie存储,那我们能不能用这类方式间接地去调用内置pls呢?

最终解决方案

目前想到有两种方式可以间接去调,document.doamindocument.cookie,测试一下就会发现,如果你尝试把当前域名设置为com.cn或者把cookie设置到com.cn上面,浏览器并不会生效,document.domain在第二次设置的时候,firefox会抛错,看来并不是很合适,而且可能多多少少会影响到业务,cookie设置方便,而且清除也方便,上代码:

1
function getMainHost() {
  let key  = `mh_${Math.random()}`;
  let keyR = new RegExp( `(^|;)\\s*${key}=12345` );
  let expiredTime = new Date( 0 );
  let domain = document.domain;
  let domainList = domain.split( '.' );

  let urlItems   = [];
  // 主域名一定会有两部分组成
  urlItems.unshift( domainList.pop() );
  // 慢慢从后往前测试
  while( domainList.length ) {
    urlItems.unshift( domainList.pop() );
    let mainHost = urlItems.join( '.' );
    let cookie   = `${key}=${12345};domain=.${mainHost}`;

    document.cookie = cookie;

    //如果cookie存在,则说明域名合法
    if ( keyR.test( document.cookie ) ) {
      document.cookie = `${cookie};expires=${expiredTime}`;
      return mainHost;
    }
  }
}

拉了差不多几十个pls里面的域名,跑了一下单元测试,没有问题。

iOS大保健 - 基础信息的收集

本文列出了各种iOS的基础信息的获取方式。

系统信息

  • 设备型号
1
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSASCIIStringEncoding];
  • 操作系统名称
1
UIDevice *device = [UIDevice currentDevice];
[device systemName];
  • 操作系统版本
1
UIDevice *device = [UIDevice currentDevice];
[device systemVersion];
  • 设备型号,如: iPad,iPod等。
1
UIDevice *device = [UIDevice currentDevice];
[device model];
  • 屏幕分辨率
1
UIScreen *screen     = [UIScreen mainScreen];
CGSize screenSize    = [[UIScreen mainScreen] bounds].size;
CGFloat scale        = screen.scale;
int screenX          = screenSize.width * scale;
int screenY          = screenSize.height *scale;
NSString *resolution = [NSString stringWithFormat:@"%d*%d", screenX, screenY];
  • 电池电量
1
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
     // Level has changed
     NSLog(@"Battery Level Change");
     NSLog(@"电池电量:%.2f", [UIDevice currentDevice].batteryLevel);
 }];

网络信息

需要引入库:CoreTelephony.framework

  • 网络类型,如:WiFi,4G等。
1
NSString *networktype = nil;
NSArray *subviews = [[[[UIApplication sharedApplication] valueForKey:@"statusBar"] valueForKey:@"foregroundView"]subviews];
NSNumber *dataNetworkItemView = nil;
for (id subview in subviews) {
    if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
        dataNetworkItemView = subview;
        break;
    }
}
switch ([[dataNetworkItemView valueForKey:@"dataNetworkType"]integerValue]) {
    case 0:
        networktype = @"UNKNOW";
        break;
    case 1:
        networktype = @"2G";
        break;
    case 2:
        networktype = @"3G";
        break;
    case 3:
        networktype = @"4G";
        break;
    case 4:
        networktype = @"LTE";
        break;
    case 5:
        networktype = @"Wi-Fi";
        break;
    default:
        break;
}
return networktype;
  • 蜂窝网络类型
1
CTTelephonyNetworkInfo *telephonyInfo = [[CTTelephonyNetworkInfo alloc] init];
if ( [telephonyInfo respondsToSelector:@selector(currentRadioAccessTechnology)] ) {
    id radioAccessType = [telephonyInfo performSelector:@selector(currentRadioAccessTechnology)
                                             withObject:nil];
    
    if ( [radioAccessType isKindOfClass:[NSString class]] ) {
        NSString *accessType = (NSString *)radioAccessType;
        if ( [accessType hasPrefix:@"CTRadioAccessTechnology"] && accessType.length >= 23 ) {
            NSString *accessTypeStr = [accessType substringFromIndex:23];
            return accessTypeStr;
        }
    }
}
    
return @"UNKNOW";
  • 运营商信息
1
CTTelephonyNetworkInfo *telephonyInfo = [[CTTelephonyNetworkInfo alloc] init];
CTCarrier *carrier    = [telephonyInfo subscriberCellularProvider];
NSString *carrierName = [carrier carrierName];
if ( nil != carrierName ) {
    return carrierName;
}
return @"UNKNOW";

应用状态

  • APP启动,或者由后台进入前台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
  • APP从后台进入前台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationBecomeActive) name:UIApplicationWillEnterForegroundNotification object:nil];
  • APP进入后台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
  • APP在前台锁屏
1
#import <notify.h>
#define NotificationLock CFSTR("com.apple.springboard.lockcomplete")
#define NotificationChange CFSTR("com.apple.springboard.lockstate")
#define NotificationPwdUI CFSTR("com.apple.springboard.hasBlankedScreen")

static void screenLockStateChanged(CFNotificationCenterRef center,void* observer,CFStringRef name,const void* object,CFDictionaryRef userInfo)
{
	NSString* lockstate = (__bridge NSString*)name;
	if ([lockstate isEqualToString:(__bridge  NSString*)NotificationLock]) {
	    NSLog(@"locked.");
	} else {
	    NSLog(@"lock state changed.");
	}
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, screenLockStateChanged, NotificationLock, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
	CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, screenLockStateChanged, NotificationChange, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
	return YES;
}
  • APP在后台锁屏,通过循环的方式检测
1
static void setScreenStateCb()
{
uint64_t locked;

__block int token = 0;
notify_register_dispatch("com.apple.springboard.lockstate",&token,dispatch_get_main_queue(),^(int t){
});
	notify_get_state(token, &locked);
	NSLog(@"%d",(int)locked);
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
	while (YES) {
	    setScreenStateCb();
	    sleep(5); // 循环5s
	}
}
  • 获取当前APP的CPU使用率
1
kern_return_t kr;
task_info_data_t tinfo;
mach_msg_type_number_t task_info_count;
    
task_info_count = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) {
    return;
}
    
task_basic_info_t      basic_info;
thread_array_t         thread_list;
mach_msg_type_number_t thread_count;
    
thread_info_data_t     thinfo;
mach_msg_type_number_t thread_info_count;
    
thread_basic_info_t basic_info_th;
uint32_t stat_thread = 0; // Mach threads
    
basic_info = (task_basic_info_t)tinfo;
    
// get threads in the task
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
    return;
}
if (thread_count > 0)
    stat_thread += thread_count;
    
long tot_sec = 0;
long tot_usec = 0;
float tot_cpu = 0;
int j;
    
for (j = 0; j < thread_count; j++)
{
    thread_info_count = THREAD_INFO_MAX;
    kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
                     (thread_info_t)thinfo, &thread_info_count);
    if (kr != KERN_SUCCESS) {
        return;
    }
    
    basic_info_th = (thread_basic_info_t)thinfo;
    
    if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
        tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
        tot_usec = tot_usec + basic_info_th->system_time.microseconds + basic_info_th->system_time.microseconds;
        tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * 100.0;
    }
    
} // for each thread
    
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
    
NSLog(@"CPU Usage: %f \n", tot_cpu);
  • 获取当前APP的内存使用情况
1
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
                                     TASK_BASIC_INFO, (task_info_t)&taskInfo, &infoCount);

if(kernReturn != KERN_SUCCESS) {
    return;
}
    
NSLog(@"Memory Usage: %f", taskInfo.resident_size / 1024.0 / 1024.0);

零门槛学习https--(5)常见的https攻击原理

上一篇:零门槛学习https–(4)https协议详解

看似毫无破绽的TLS协议,其实也存在着诸多漏洞,主要因为TLS协议太复杂了,复杂的东西必然会出现漏洞,下面我们来看看一些常见的攻击手段有哪些,以及一些攻击实例。

伪造证书

攻击原理

这是一种中间人攻击,就是伪造网站证书,虽然浏览器会提示访问了不安全的网址,但是大部分对https没什么了解的人基本都会选择忽略问题继续浏览,这是一种简单,成本低,且效果还可以的攻击方式。

防御方法

打开浏览器设置,如果证书不可信,那么就直接不能访问。这个其实也不能算是TLS的漏洞,只能说是浏览器为了做兼容而没有采取一些强制性措施,或许不久的将来,浏览器默认就会直接拒绝无效证书的连接。

POODLE

攻击原理

上一章看TLS协议的时候,我们看到,因为要处理兼容性问题,客户端和服务端不得不支持很多种加密套件和TLS或者SSL版本,由此,衍生一个问题就是:一些老久的加密套件或者SSL版本通常在现在的计算能力下安全性没那么高,或者早先的协议是有安全漏洞的,所以,针对SSL降级,还有加密降级攻击就成了一种主流的攻击方式。

POODLE的全称是Padding Oracle On Downgraded Legacy Encryption,由谷歌的工程师在2014年发现了这个问题,问题的主要原因在于分块加密。其原理是:

AES 分块加密在原文内容长度不满足时,会在原文后面填充一些内容,保证每个分块的长度一致,这就是Padding Oracle,也是攻击的关键,因为新版TLS不存在这个问题,所以我们需要对协议进行降级,而降级就是Downgraded Legacy Encryption,所以这也是这个漏洞名称的由来。

协议降级很简单,只要在客户端发送Client Hello消息的时候,把客户端支持的加密套件替换成SSL3.0,如果服务端支持,那么后续连接就会使用SSLV3来进行连接,具体的Padding Oracle攻击原理可以看一下这篇文章,写得非常详细。

防御方法

客户端禁用SSLV3协议,服务端也关闭SSLV3的连接。

FREAK

这个攻击方式可以说到一个很有意思的事情,目前基本主流的加密算法都是由美国开发的,90年代的时候,NSA为了能够监听世界上所有的网络通信,出台了对于软件出口规定,规定只有美国境内能够使用高强度的RSA加密,对于出口国外的软件,最高允许512密钥长度的RSA,高安全级别的加密算法被认为是战争武器而禁止对外出口。2000年之后,美国逐渐放宽了这个限制,目前美国任然有一些带有加密算法和函数的软硬件受到出口管制。

攻击原理

由于历史遗留原因,IE6,网景,甚至是手机上的安卓和iOS Safari 浏览器,都支持512密钥长度的RSA加密,这在2015年的时候才被法国工程师发现。据说在当时任然有38%的服务器还支持512长度的密钥,而且讽刺的是,美国一些政府部门网站也存在此漏洞。

攻击者在Client Hello的时候,把高强度的密钥加密算法都剔除掉,留下低强度的算法,如果服务端也支持低强度的加密算法,那么就会选择这一算法。虽然在90年代一般的PC还无法破解512长度密钥,但是目前个人已经完全有能力破解了。

防御方法

苹果已经在当时推出相应补丁,而安卓也在后续版本进行了升级。服务端应该禁止低强度加密算法。

HTTPS Strip

也称为SSLStrip,是一种基于社会学的攻击方法,也是中间人攻击的一种,和任何加密方式都无关,和SSL也无关,也是目前为止我成功实施的一种攻击方式。

攻击原理

通常,我们在浏览器访问一个网址的时候,会直接输入网址,而不会关注协议,比如我要访问天猫,那么我就会输入:tmall.com,而此时浏览器默认会使用http协议访问,因为浏览器并不知道服务端是否开启了https,所以,攻击者直接劫持http访问,然后自己去访问https://www.tmall.com,并把内容通过http协议返回给浏览器,浏览器成功收到数据,就直接开始渲染页面,浏览器端完全就是访问一个http站点。所以,这种攻击方式就叫做SSL剥离攻击。除了浏览器之外的场景,基本不会受到此攻击的影响。

防御方法

HSTS(HTTP Strict Transport Security),这是一种浏览器技术,在客户端访问站点的时候,如果检测到当前域名在HSTS列表中,那么直接使用https协议访问,也就不会被攻击者劫持了。那么如何加入HSTS列表呢?在浏览器第一次访问站点的时候,站点可以在http返回头里面来标示是否启用HSTS,还有过期时间。

这是第一次访问www.taobao.com的场景,http返回头有个:Strict-Transport-Security属性,过期时间是一年。

这是第二次访问www.taobao.com时候,因为已经在HSTS列表中,就直接是一个Internal Redirect,不会请求服务端。

其实你会发现,浏览器第一次访问网站的时候,也是有可能被劫持的,所以现在有些网址会内置在浏览器的HSTS列表中,防止第一次访问被劫持的情况发生。不久的将来,也许浏览器就默认使用https访问,这种攻击方式也就无法发挥作用了。

攻击实例

写个脚本就可以检测网站是否开启了HSTS,对于没有开启HSTS的网站都可以发动攻击。以下是我对tmall.com发起的一次http strip的攻击实例。

1
上图中看到访问tmall.com的跳转逻辑是:
	tmall.com --302--> www.tmall.com --301--> https://www.tmall.com。

301还好,现代浏览器几乎会作为永久缓存,这种情况下如果不清除缓存,其实跟HSTS效果差不多。
302就坑了,基本是不会缓存的。

1
从上图中我们可以看到返回头中虽然包含了STS头,但是过期时间为0

也就是不开启HSTS,用了一个301跳到了https站点

下面我开始准备需要的东西,当然一个前提是你要能捕获流量:

  1. 为了方便我使用本机测试,可以建个DNS服务器,也可以使用更简单的host文件,我选择使用host文件,把tmall.comwww.tmall.com指向127.0.0.1。实际情况中,可以采用ARP欺骗等方式获取局域网流量。
  2. 在本机建立一个web server,具体的代码就不贴了,后面贴一些关键代码。

好,我们可以开始攻击了,具体步骤如下。

  1. 首先使用浏览器访问tmall.com,因为没有开启HSTS,浏览器开始用http方式请求tmall.com
  2. 这时候本次请求已经到我的中间人server,然后server使用IP方式访问真实的tmall.com,这里不能用域名,因为域名已经指向本机了。
  3. 拿到真实tmall.com的返回之后,添加如下代码:

    1
    body += '<script>alert( 'hello' )</script>';
  4. 返回给浏览器,此时,浏览器看起来是这个样子,弹出了hello,并且仔细看会发现https标示没有了,如果不弹出alert,天猫那个具有强烈视觉冲击感的页面,很容易让你忽略https标示没有了这件事。

  5. 再进一步看看,点击登陆按钮的跳转逻辑是:<a href="//:login.tmall.com">登陆</a>,没有写死协议,如果写死了协议,在返回内容的时候,手动替换为http即可。登录页同样没有开启HSTS,我们试试看能不能拿到账号密码。简单分析一下天猫登录页,真正账号密码的输入框采用的是login.taobao.com提供的iframe登录框,有二维码和手动输入账号密码两个选项,二维码无能为力了,看看能不能拿到手动输入账号密码时的内容。
  6. 看了看login.taobao.com,没有写死协议,并且发现同样没有开启HSTS,这就好办了,和tmall.com一样,拦截请求,并在返回内容添加如下代码,插入一个jQuery,并在在输入密码的时候获取输入事件:

    1
    body += 
     "<script src=\"https://code.jquery.com/jquery-latest.js\"></script>" +
     "<script>" +
       "$( '#TPL_username_1' ).on( 'input', function (){" +
         "console.log( this.value );" +
       "} );" + 
       "$( '#TPL_password_1' ).on( 'input', function (){" +
         "console.log( this.value );" +
       "} );" +
     "</script>";
  1. 运行脚本,看看效果:

    login.taobao.com竟然是GBK编码的,导致乱码了,这里我懒得转编码了,可以看到左上角其实浏览器已经显示了不安全三个字,同样估计很少有人会看到这几个字。我们尝试输入一下账号密码试试看:

    已经打印出账号密码,发个请求就能发送到我自己的服务器了,后面再跳回到https站点就神不知鬼不觉了。

  2. 总的来说,本次攻击的需要的条件其实是比较多的,首先因为http://www.tmall.com跳转到https://www.tmall.com是301跳转,而chrome等浏览器把301几乎是当做永久缓存来处理的,除非手动清理缓存,当然很多浏览器对301也不缓存,或者重启就没了。其次,默认的登陆方式是二维码登陆,二维码登陆就无法实施此次攻击了。不过基于天猫访问量巨大,符合条件的流量估计也不少,而且http://login.taobao.com跳往https站点是302,也就是不会缓存了,这个有安全隐患。如果某天天猫开启了HSTS,那么这类攻击就几乎无法生效了,毕竟这对天猫来说也就是一个配置项的事。

其他漏洞

  1. 完全信任证书漏洞。很多android APP采取了不校验证书的方式,导致中间人攻击非常容易,比如早先的亚马逊官方APP、携程APP等。
  2. Heartbleed。OpenSSL在实现心跳的扩展没有对输入进行适当验证,导致过读,可以读取服务器内存中的内容,即使协议没问题,实现有问题同样也会造成漏洞的产生。

总结

TLS因为比较复杂,所以或多或少总会有些漏洞,TLS也在慢慢地不断完善,因为很多东西基于数学原理,或者某天出现了快速因式分解等的方法,瞬间就可以破解TLS,也有人说美国安全局其实早就已经掌握某些技术能够监听全世界的加密流量了,加密和破解总是在不断地博弈,在博弈过程中进行自我升级,随着技术的发展以及大家对隐私和网络安全越来越关注,相信我们的网络环境一定会变得越来越安全。