Reflect-Metadata 详解

发布在 Frontend

目录

  1. 引言
  2. Metadata
  3. Reflect Metadata
    1. 概念
    2. 安装/使用
    3. 类/属性/方法 装饰器
    4. 原型链查找
    5. 用途
    6. API
  4. 深入 Reflect Metadata
    1. 实现原理
  5. End
  6. 题外话

引言

在 ES6 的规范当中,就已经存在 Reflect API 了。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。具体的关于这个 API 的内容,可以查看这个教程

然而我们在这里讲到的,却是 Reflect 里面还没有的一个规范,那么就是 Reflect Metadata

Metadata

想必对于其他语言的 Coder 来说,比如说 Java 或者 C#,Metadata 是很熟悉的。最简单的莫过于通过反射来获取类属性上面的批注(在 JS 当中,也就是所谓的装饰器)。从而可以更加优雅的对代码进行控制。

而 JS 现在有装饰器,虽然现在还在 Stage2 阶段。但是 JS 的装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。

所以,后文当中我就使用 TypeScript 来进行讲解,因为 TypeScript 已经完整的实现了装饰器。
虽然 Babel 也可以,但是需要各种配置,人懒,不想配置那么多。

但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上。

于是 Reflect Metadata 应运而生。

Reflect Metadata

Relfect Metadata,简单来说,你可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。当然你也可以通过反射来添加这些信息。
就像是下面这个例子所示。

1
2
3
4
5
6
7
8
9
10
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}
Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
// 这里为什么要用 new A(),用 A 不行么?后文会讲到

是不是很简单,那么我简单来介绍一下~

概念

首先,在这里有四个概念要区分一下:

  1. Metadata Key {Any} 后文简写 k。元数据的 Key,对于一个对象来说,他可以有很多元数据,每一个元数据都对应有一个 Key。一个很简单的例子就是说,你可以在一个对象上面设置一个叫做 'name' 的 Key 用来设置他的名字,用一个 'created time' 的 Key 来表示他创建的时间。这个 Key 可以是任意类型。在后面会讲到内部本质就是一个 Map 对象。

  2. Metadata Value {Any} 后文简写 v。元数据的类型,任意类型都行。

  3. Target {Object} 后文简写 o。表示要在这个对象上面添加元数据

  4. Property {String|Symbol} 后文简写 p。用于设置在那个属性上面添加元数据。大家可能会想,这个是干什么用的,不是可以在对象上面添加元数据了么?其实不仅仅可以在对象上面添加元数据,甚至还可以在对象的属性上面添加元数据。其实大家可以这样理解,当你给一个对象定义元数据的时候,相当于你是默认指定了 undefined 作为 Property。下面有一个例子大家可以看一下。

大家明白了上面的概念之后,我之前给的那个例子就很简单了~不用我多说了。

安装/使用

下面不如正题,我们怎么开始使用 Reflect Metadata 呢?
首先,你需要安装 reflect-metadata polyfill,然后引入之后就可以看到在 Reflect 对象下面有很多关于 Metadata 的函数了。因为这个还没有进入正式的协议,所以需要安装垫片使用。

啥,Reflect 是啥,一个全局变量而已。

你不需要担心这个垫片的质量,因为连 Angular 都在使用呢,你怕啥。

之后你就可以安装我上面写的示例,在 TypeScript 当中去跑了。

类/属性/方法 装饰器

看这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const objs = [A, new A, A.prototype]
const res = objs.map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
// 大家猜测一下 res 的值会是多少?

想好了么?再给你 10 秒钟

10
9
8
7
6
5
4
3
2
1

res

1
2
3
4
5
[
['A', undefined, 'A', undefined],
[undefined, 'hello', undefined, undefined],
[undefined, 'hello', undefined, 'hello'],
]

那么我来解释一下为什么回是这样的结果。

首先所有的对类的修饰,都是定义在类这个对象上面的,而所有的对类的属性或者方法的修饰,都是定义在类的原型上面的,并且以属性或者方法的 key 作为 property,这也就是为什么 getMetadata 会产生这样的效果了。

那么带 Own 的又是什么情况呢?

这就要从元数据的查找规则开始讲起了

原型链查找

类似于类的继承,查找元数据的方式也是通过原型链进行的。

就像是上面那个例子,我实例化了一个 new A(),但是我依旧可以找到他原型链上的元数据。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'
Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'

用途

其实所有的用途都是一个目的,给对象添加额外的信息,但是不影响对象的结构。这一点很重要,当你给对象添加了一个原信息的时候,对象是不会有任何的变化的,不会多 property,也不会有的 property 被修改了。
但是可以衍生出很多其他的用途。

  • Anuglar 中对特殊字段进行修饰 (Input),从而提升代码的可读性。

  • 可以让装饰器拥有真正装饰对象而不改变对象的能力。让对象拥有更多语义上的功能。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void
// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void
// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
// 删除元数据
deleteMetadata(k, o, p?): boolean
}

大家可能注意到,针对某些操作,会有 Own 的函数。这是因为有的操作是可以通过原型链进行操作的。这个后文讲解。

深入 Reflect Metadata

实现原理

如果你去翻看官网的文档,他会和你说,所有的元数据都是存在于对象下面的 [[Metadata]] 属性下面。一开始我也是这样认为的,新建一个 Symbol('Metadata'),然后将元数据放到这个 Symbol 对应的 Property 当中。直到我看了源码才发现并不是这样。请看例子

1
2
3
@Reflect.metadata('name', 'A')
class A {}
Object.getOwnPropertySymbols(A) // []

哈哈,并没有所谓的 Symbol,那么这些元数据都存在在哪里呢?

其实是内部的一个 WeakMap 中。他正是利用了 WeakMap 不增加引用计数的特点,将对象作为 Key,元数据集合作为 Value,存到 WeakMap 中去。

如果你认真探寻的话,你会发现其内部的数据结构其实是这样的

1
WeakMap<any, Map<any, Map<any, any>>>

是不是超级绕,但是我们从调用的角度来思考,这就一点都不绕了。

1
weakMap.get(o).get(p).get(k)

先根据对象获取,然后在根据属性,最后根据元数据的 Key 获取最终要的数据。

End

因为 Reflect Metadata 实在是比较简单,这里就不多讲解了。更多内容请查看 Spec

题外话

其实看了源码之后还是挺惊讶的,按照一般的套路,很多 polyfill 会让你提供一些前置的 polyfill 之后,当前的 polyfill 才能使用。但是 reflect-metadata 竟然内部自己实现了很多的 polyfill 和算法。比如 Map, Set, WeakMap, UUID。最惊讶的莫过于 WeakMap 了。不是很仔细的阅读了一下,好像还是会增加引用计数。

注释和共享

Boostnote 是一款通过 Electron 构建的桌面笔记应用。它支持离线存储,无需注册、Markdown 编辑、像 Github Gist 的代码片段的管理等等内容。全球 198 个国家和地区的程序员们在使用这款应用。

Boostnote 就如同器名字一般,最主要的一个用途便是 Markdown 笔记编辑器。你所有的 Markdown 笔记拥有自动保存的功能,并且支持多种展示格式。拥有半实时的预览,所以你可以及时的检查最后的格式是否是和你所输入的一致。

Latex 公式编辑器也内置在 Boostnote 当中,你可以很轻松的插入各种公式在你的笔记中。

不管是笔记还是代码片段都可以通过标签的方式进行管理。

对于代码片段来说,他支持高达 100 多种语言的高亮,其中包括 JavaScript, Python, HTML 和 CSS。当然你也可以在一份代码片段当中存储多段代码,比如说你可以同时存储 HTML,CSS,JS 代码在一份当中。而且不管你是用什么样子的缩进(tab/空格)或者缩进的程度(2个字符,4个字符,8个字符)都可以在文档中进行配置。

最后,将笔记导出成普通文本(.txt) 或者 Markdown(.md) 也是支持的功能。

外观(UI)

丰富的快捷键可以让你让更快的浏览、搜索笔记,以及更快的执行一些重要操作。

对于 Boostnote 的外观来说,你有很多种不同的主题可以选择。当然,编辑器的高亮也是有很多种可以选择的,你可以根据你自己的喜好自由搭配。你可以在 Preferences > UI > Theme 查看支持的主题。

下载

Boostnote 目前是开源的,你可以通过官网下载。不过要注意的是,存储使用的是亚马逊的 s3,所以你需要能够翻墙,否则无法下载。

它目前是支持全平台(Windows, MacOS, Linux,IOS,Android)

注释和共享

目录

vue-lever 是一个使用装饰器模式的插件来帮你管理 true/false 状态

最近一直在写 vue 相关的东西,因为毕竟是前端,所以经常会遇到一种情况就是说,我需要维护一个 true/false 状态,比如说:当按钮点击的时候,显示加载字样,然后等加载完毕之后完成显示。

首先我们考虑出现这种情况的时候,如果我们一行一行的书写,那将是非常麻烦的。

1
2
3
4
5
6
7
8
9
10
11
12
13
new Vue({
data() {
return { loading: false }
}
methods: {
asyncAction() {
this.loading = true
this.$http.doing() // return a promise
.then(() => this.loading = false)
.catch(() => this.loading = false))
}
}
})

我们需要在每一个退出的情况下将 loading 状态设置为 false,这至少意味着你要写 3 遍,非常冗余。而且很容易忘记。

我们可以抽象出来,可以发现每当这种函数运行的时候,首先将相关的变量设置成 true,然后等待异步操作完成或者失败之后,再将变量设置回之前的值。

完成这个操作最简单的方式就是代理,在用户的函数调用之前设置相应的变量,在用户的函数调用完成之后,或者如果函数是异步操作,那么通过返回一个 promise 来表示异步操作。

既然讲到了代理模式,那么在 JS 中有很多,不过我们在这里通过一个 ES6 的新语法,装饰器。

话不多说,上代码

1
2
3
4
5
6
7
8
9
10
import Lever from 'vue-lever'
// import others
new Vue({
methods: {
@Lever.Lever('loading')
asyncAction() {
return this.$http.doing() // return a promise
}
}
})

这个功能和上面的那段代码是一模一样的,是不是感觉用了装饰器之后就变得特别简单了呢?

不过这里有一点需要注意的是,为了方便和隔离,我将所有的变量全都放在 levers 这个变量下面,也就是说你需要通过 levers.loading 来使用,而不是 loading

不过现在暂时没有支持回调函数的方式,也就是你必须要返回一个 promise,否则是不行的。

其次我们这里还有手动模式,也就是如果你不想用装饰器的话,你可以通过 this.$lever(name, value),来更改状态,其中 name 为变量名,value 为 true/false 值。当然这里也提供两个 alias,this.$lever.t(name)this.$lever.f(name)。方便设置变量为 true/false。

还有一些其他的参数,大家可以去 Github 网站查看。这篇文章就写到这里。

注释和共享

目录

17年4月,终于迎来了阿里巴巴的实习招聘,我也很开心的通过朴神内推到阿里云的前端开发。故事就是这么展开了。

还记得当初3月份(我也记不清了)朴神在微博上面发了谁愿意和他一起写 Node ,于是我激动的花了一晚上写了一张简历页面,开开心心的给了朴神,他给我回了一句:你联系方式都没有写,让我怎么联系你呀。QAQ,竟然忘了这么重要的事情,好丢脸。于是赶紧补了一下又给了朴神。

过了许久,等来了内推邮件,打开一看,哎不对啊,为啥是前端开发工程师?不是说好的写 Node 么-_-。于是又仔细看了阿里要的职位,发现原来 Node 是包含在前端开发工程师中的呀。

也不知道是谁定的,Node竟然属于前端开发

不管了,于是又开开心心的填上去了。

Long Long Time Passed,果不其然,等来了第一次的电话面试。

电话面试简单来说他一开始先问了我一些关于 Node 方面的内容,比如对中间件的理解,平时遇到的一些困难,对模块化的运用等等,感觉自己都能答出来。之后他也问了一些前端的内容,我也说了一下之前也玩过手机浏览器内核,对兼容性稍微了解一些。用的时候查一下不就好了,何必要记住呢LOL

之后他说道自己是阿里云这边的,因为阿里云要面向海外市场,需要考虑手机兼容性的事情,问我愿意不愿意去解决兼容性的事情。于是我很老实的回答道:可以去做,但是能少碰到就碰到吧~~现在回想起来,我应该直接回答可以,不用加后面那句话。不过我确实想说一下自己的心声吧。

基本没有太多的卡壳,开开心心的结束了电话面试。原本以为自己说的比较好了,但是很久很久之后,都没有消息。某一天,朋友突然说道:“哎,你不用参加阿里的笔试么?”,我说道:“不用啊,我是内推的。”。说完之后我心里犯嘀咕,话说为啥这么久都没有消息,战战兢兢的打开了 campus.alibaba.com,点击个人中心……

WHAT THE FUCK!!! 被回绝了,为啥连个短信都没有的。心中一万个 🐴 飞过。

不过好事就是,我自动从内推变成了自主投递了,至少还有一次新的机会,这也就是说明今晚我要和他们一起笔试了~~都不明白自己是怎么被回绝的,连个原因都不写的么 😫 不管了,平复一下自己的心情,开始写笔试吧,就算不是内推,自主投递也可以的么。

几天后,笔试开始,前面的选择题就不多说了,没什么太难的,都是一些概念性的问题,包括 js 和 前端的。

// 其实是我忘记有哪些选择题了

主要说一些有一道算法题,就是让你将二进制转成十进制,大家第一个想到的便是 Number.parseInt,当然我也是的。不过想想也知道,肯定不会这么简单,坑的地方在于这个二进制很大,js 在处理超过 52 位二进制数的时候就会有精度问题,但是我的时间不多了,也懒得去写高精度了,要同时去写乘法和加法。不给当时时间太紧,其实是有更好的方法的。

简单来说,其实就是高精度的变种,只不过我们不需要实现乘法和加法。具体我就不讲了。

后面还要一道 canvas 的题目,让你画一个东西,反正我以前玩过,看着以前的代码,很快就撸出来了。

最后一道是简答题,让你说一下在实现自动保存功能的时候会遇到什么问题,多人合作会出现什么问题?WTF,这是什么问题啊。还好平时用过 quip 这种基于行锁的多人文本编辑工具,反正就大体扯了扯,顺便扯了一下如何优化前端的速度,和减轻服务器的负载。

于是,笔试结束了。感觉稍微有点虚,不过还是稍微有点信心的。也算是平复了一下内推被拒的心情吧。

依旧是漫长的等待,转眼已经5月中旬多了。这个时候还没有消息,我感觉都要跪了,都要打算去找别的公司了,因为再不找的话,就要错过找实习的最佳时机了。各大公司基本都是这个时候开始找实习的。(一些小公司的暑期实习除外)

就在我打算放弃的时候,就收到了阿里的远程一面的通知,这个时候挺开心的,于是就开开心心的去准备面试的内容了,也没心思去找别的公司了。

之后远程的一面、二面、hr面,不知不觉就到了6月份了。

一面的时候,面试官还是我们旁边的学校,我是杭电的,他是理工的。他问了一些问题,主要是关于 Node 的。这次真的是一点前端的内容都没有问。终于问了一下 Node 的内存管理机制,不过话说回来,和 java 的有点像,但不完全一样。记得 java 是采用引用计数的方式,node 是标记清除。不过 java 还有很多别的算法。聊了聊平时项目中遇到的问题,聊了聊 redis 的使用中遇到的坑。不过因为我用的太少,基本没什么坑可以踩。顺便问了我一个问题,就是说如果发现一个服务器的压力过大,该怎么处理(原句忘了,不过大体是这个意思)。想了一会,想到应该大体从代码和架构方面下手,一个优化代码,一个布置集群。但是不知道他想问我那个,于是半天没说。结果他说到,其实很简单的,就是将服务拆分,分布在不同的机器上就可以了。想了一下,嗯他说的也对。不知不觉,面试结束了。最后他说了一句,感觉你基础还可以,就是实践有点少,不过我先帮你送到后面去吧。其实他有一句话挺实在的,就是想要在后端有发展,最好要去学学 Java。但是我就是想在 node 混,除非我觉得 node 已经实现不了公司的需求的时候,我再尝试转 java。

就这样,一面结束了,感觉自己好水,其实很多问题都不能准确清楚的答出来。毕竟自己平时只是做项目的时候用了一下,而且项目不大,不会遇到太多性能问题,也就没有太多深入研究(其实是没那么多时间去研究,而且可以玩的技术还有那么多,想去玩新技术,于是就懒得太深入研究了)

过了许久,也不知道几天,反正感觉挺漫长的,终于等来了终面。

阿里定义的终面有点歧义,不知道是实习生的终面还是技术的终面,不管了,既来之则安之。其实终面对于我来说就是技术二面和hr面LOL

二面,问的问题更加具体了一些,没有问太基础的东西。主要是针对我平时项目中遇到的一些问题展开问了一下。感觉比一面问的少,但是聊的还挺开心的。讲了讲平时 docker 分发部署代码的东东。讲了讲为啥想来阿里,是为了技术还是朴神、deadhorse 他们的,我说都有😂。感觉这一面,他更多的是按照我的简历来提问的,反正也是瞎比回答么~~

然后就结束了,我原本以为这样就结束了,过几天再等 hr 面试。然后我就去搞我自己的 minecraft 管理器了。

结果突然间收到一个短信,说我被 hr 翻牌了,赶紧去准备面试。

OMG,这么快的,我都没准备呢。算了,兵来将挡,水来土掩,硬着头皮就上了,反正是 hr 面,不太需要将技术什么的,聊聊天就好了。我也不喜欢在别人面前装,反正我想说啥就说啥吧,没必要为了 hr 而特意去说他喜欢听的话。不过稍微说的好听点,我还是可以的。😂聊了聊阿里的武侠文化,花名文化,倒立文化等等。聊了聊平时的爱好,喜欢做啥。大学当中的收获什么的。说起这个,感觉大学收获最大的就是大一的时候和一些大神们出去瞎混,参加各种活动,d2,day one,coding线下会,黑客马拉松。coding 线下会拿到了一个洋葱猴顺便还认识了现在的 cnode 站长 alsotang,不过那个时候他还是在阿里工作A_A。而且自己不善于交际,打字聊天还可以,张口说话就不知道该说啥了,所以那个时候他也不认识我。不过听 alsotang 和 wph95 聊得挺开心的,才知道钉钉在阿里其实是蛮流行的,不过钉钉没有 gcm ,差评。

不过不得不佩服 wph95 大神,codevs站长。也就是因为他,我知道了 Angular,Docker,Webpack,Stylus,Gulp 等等好玩的东西。虽然他是做 python 后端的,但是他的知识面真的广,前端后端运维都有涉猎。看他和各位同行大佬撕逼的感觉真爽,尤其知道当他们知道他还是一个杭电学生的时候,哈哈哈,想想就好玩。而且大二上就去 Daocloud 实习了~~真的佩服他是怎么认识那么多人的😂。反正大二之后就不怎么和他联系了,挺感激他的

hr 面也就这么结束了,面试我的 hr 有点胖,顺便调侃了一下他能不能倒立,哈哈哈。

反正这几场面试下来,比较自信但是又没有把握。于是我就在纠结,这个时候已经快6月份了,要不要再去看看别的公司,于是发现那些内推的消息或者实习生的消息,基本都是2、3个月以前的了。饿了么,美团似乎连实习生招聘的链接都没有。腾讯估计已经结束了。自己觉得赌一把吧,到时候再说。

于是这一赌,就等来了下面的这个消息。而且依旧没有短信,我也是前几天耐不住性子了,去刷新了一下。你知道从面试中的状态变成已回绝的状态有多绝望么😫

这个时候,我开始找各大公司的实习,心累中。。。你早回绝一下也行,非要等到这么晚才回绝,而且还没有通知。

总结一下吧,就是面试笔试没什么太大难度,最后就被 K.O. 了。感觉就跟哑巴吃黄连,有苦说不出。。。真心不知道自己哪里 gg 了。也许是最后跟 hr 聊天的时候说我自己太懒吧,经常容易拖。经常是立下 flag 说今晚写完 api,然后晚上去玩 hadoop 了🐭。突然想起来 google 的 instant app 什么时候才能普及呢,好喜欢这个,感觉 react native 可以下岗了。

或许有的人说,你应该早就多准备,多投几个公司。说实话,我不是没有想过,投很多公司,假如正好只有一个公司要我,那还好。就算是一个都不要都可以。最怕的就是同时好几个公司要,如何拒绝其他公司,感觉这好为难啊(PS:这并不表明自己有多么自信,只是讲述一种可能)

最后说一句,感觉自己和大神有一个很大的差距便是大神总会有各种各样的获取信息的途径,而我,只能听大神说🐶而且他们感觉有时候就是一个代码机器,维护着好几个库,还能天天 push。

注释和共享

目录

  1. Golang
    1. Timer
    2. Ticker

这一篇文章,我们主要讲解一下不同语言之间关于定时器的操作和坑。

首先说一些这个坑中的一些前提,首先我这里所有的都是和 JavaScript 与 C++ 相比较进行学习的,所以下面不会出现 JavaScript 相关的教程,除非 JavaScript 在这个上面的坑太多,非讲不可。如果你没有相关的语言基础,请去学习。谢谢。

首先先从 golang 语言讲起来

Golang

在 golang 中单次定时器和循环定时器分别是 Timer 和 Ticker。这两个都是一个结构体,结构如下

1
2
3
4
5
6
7
8
type Timer struct {
       C <-chan Time
       // contains filtered or unexported fields
}
type Ticker struct {
       C <-chan Time // The channel on which the ticks are delivered.
       // contains filtered or unexported fields
}

其中 C 是一个 只读的 Time 类型的 channel,定时器根据传入的时间设置,定时向这个 channel 输入时间。你需要做的是等待这个 channel 中的数据。

每一个结构体都有如下方法:

1
2
3
func (t *Timer) Reset(d Duration) bool
func (t *Timer) Stop() bool
func (t *Ticker) Stop()

首先 Timer 和 Ticker 的区别不是很大,下面我们将两者的共性以 Timer 来讲述, 后面会针对两者不同的内容进行讲解。

1
2
3
4
5
6
7
8
9
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second * 2)
time := <- timer.C
   println("Timer expired")
}

参数接受一个时间参数,表示多久后向 channel 传输数据,也就是你想要定时的间隔,上面这个例子表示两秒之后输出 Timer expired

那么每次我们都要这么麻烦的设置一个时间,然后等待 channel,运行制定的函数么?

其实不是的,golfing 给我们内置了一个函数,类似于 JavaScript 的 setTimeout,那就是 AfterFunc。

函数如下:

1
func AfterFunc(d Duration, f func()) *Timer

就是他会给你返回一个 *Timer ,并在指定时间之后,在 goroutine 中运行你的函数。

上面说完了如何启动,那么怎么停止呢?很简单,就是 Stop

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   go func() {
       <- timer.C
       println("Timer expired")
  }()
   stop := timer.Stop()
   println("Timer cancelled:", stop)
}
1
Timer cancelled: true

调用停止之后,会返回一个 Bool 值:

  • true 停止成功

  • false 已经被停止或者已经到期触发

这里有一点需要注意的是,调用停止之后,并不会关闭 channel,如果你想检测那么你可以通过额外添加一个 done channel 来协助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   done := make(chan bool)
   go func() {
     select{
       case <- timer.C:
         println("Timer expired")
       case <- done:
         println("Timer stop")
    }
  }()
   stop := timer.Stop()
   done <- true
   println("Timer cancelled:", stop)
}
1
2
Timer stop
Timer cancelled: true

所以比较麻烦,大家可以自己封装一下。

Timer

单次定时器有一个特殊的方法就是 Reset,他可以将一个定时器的超时时间重新定义,这样你可以重复利用这个定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   go func() {
     <- timer.C
     println("Timer expired")
     <- timer.C
     println("Timer expired again")
  }()
   time.Sleep(time.Second * 2)
   timer.Reset(time.Second)
   time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired again

同理,你可以用在 AfterFunc 中

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "time"

func main() {
   timer := time.AfterFunc(time.Second, func() {
     println("Timer expired")
  })
   time.Sleep(time.Second * 2)
   timer.Reset(time.Second)
   time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired

简直和 JavaScript 中的 setTimeout 像的不能再像了。

Ticker

这里的 Ticker 其实相当于 JavaScript 中的 setInterval,不过他没有类似 Timer 的 AfterFunc,只有一个最基础的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"
import "fmt"

func main() {
   ticker := time.NewTicker(time.Millisecond * 500)
   go func() {
       for t := range ticker.C {
           fmt.Println("Tick at", t)
      }
  }()
   time.Sleep(time.Millisecond * 1500)
   ticker.Stop()
   fmt.Println("Ticker stopped")
}
1
2
3
4
Tick at 2012-09-22 15:58:40.912926 -0400 EDT
Tick at 2012-09-22 15:58:41.413892 -0400 EDT
Tick at 2012-09-22 15:58:41.913888 -0400 EDT
Ticker stopped

Ticker 同样也有同 Timer 一样无法关闭 channel 的问题,解决方法和 Timer 类似,我就不多说了。

不过 Ticker 的 Stop 函数与 Timer 的不太一样,因为他没有是否触发过的问题,所以 Ticker 的 Stop是没有返回值的,这一点需要注意。

暂时先写这一个,以后如果有新的,持续更新。

注释和共享

目录

  1. 简介
  2. 光速开始
    1. .argv 一切的开始,简单的不要不要的
      1. 普通参数
      2. 简写参数
      3. 全写参数
      4. 结果合并
    2. 我就要你在我的身边,.demandOption(key, msg)**
    3. 啥?你嫌我太长?还是太短:).alias
    4. 你要我怎样我就怎样,.boolean .array .number .count .choices
      1. .array(key)
      2. .boolean(key)
      3. .number(key)
      4. .count(key)
      5. .choices(key, list)
    5. 听说你和别人有千丝万缕的关系:( .conflicts .implies
    6. 大家在一起吧 :) .option .options
      1. 有用但是很简单其余参数
      2. 小弟来了 (-_-) **.command**
      3. 这个位置是你的,别人抢不走 [arg1] <arg2>
      4. 默认命令
      5. 方便一点 .commandDir
    7. 从别的地方来 .config .env .coerce
  3. 总结

简介

话说 yargs 是什么呢?简单来说,就是用来解析 cli 参数的。话不多说,我们来简单了解一下。

光速开始

.argv 一切的开始,简单的不要不要的

使用这个最简单的方式就是直接调用 .argv 这个 getter,他会自动对 process.argv 进行解析。并返回解析后的对象。

1
2
3
// argv.js
const argv = require('yargs').argv;
console.dir(argv)
1
2
# node argv.js -v --version --name whos
{ _: [], v: true, version: true, name: 'whos', '$0': 'argv.js' }

什么缩写,什么参数,统统搞定。是不是 so easy。

在默认情况下,所有的参数只有三种值,BooleanStringArray<Boolean|String

而且 $0 代表当前脚本的名称,这个就不多讲了

参数后面可以使用空格或者 = 。例如 -d=ok —name=bill

普通参数

如果参数没有 - 开头,那么将它放入 _ ,简称为普通参数

1
2
# node argv.js a b c
{ _: [ 'a', 'b', 'c' ], '$0': 'argv.js' }

简写参数

如果参数只有一个 - 开头,那么后面的参数为缩写参数,缩写参数的值默认设置成 true

1
2
# node argv.js -a -b -c
{ _: [], a: true, b: true, c: true, '$0': 'argv.js' }

同时,yargs 支持将缩写参数合并在一起书写。

1
2
# node argv.js -abc
{ _: [], a: true, b: true, c: true, '$0': 'argv.js' }

效果和上面是一样的。

如果缩写参数后面跟着普通参数,那么缩写参数的值就会自动设置成普通参数的值而不再是 true

1
2
# node argv.js -a haha -b lala -c hehe
{ _: [], a: 'haha', b: 'lala', c: 'hehe', '$0': 'argv.js' }

那么有人问了,如果我这样写会怎么样? -abc hahaha,let’s try.

1
2
# node argv.js -abc hahaha
{ _: [], a: true, b: true, c: 'hahaha', '$0': 'argv.js' }

结果显示,其实就和

1
# node argv.js -a -b -c hahaha

是一样的,可以见得,代码中其实就是将 -abc 拆成了 -a -b -c 进行解析的。

全写参数

除去上面两种参数,就剩下全写参数(不要吐槽为啥叫全写参数,因为实在是不知道该叫什么名字)

全写参数和缩写参数差不多,只不过他不能合并成一个书写,其他都是一样的

1
2
# node argv.js --version --laugh haha
{ _: [], version: true, laugh: 'haha', '$0': 'argv.js' }

结果合并

作为一个好 Module ,怎么会没有考虑到下面这种奇葩情况呢?

1
# node argv.js -a -a -a -a -a -a -a

大家猜猜会是什么结果 :) 此处略过 10000 秒。

1
2
3
{ _: [],
a: [ true, true, true, true, true, true, true ],
 '$0': 'argv.js' }

没错,yargs 将每一个参数单独处理,然后最后合并成了一个数组,是不是很有意思,也就是说你可以写出下面的东东。

1
2
3
4
# node argv.js --fuck whose --fuck your --fuck daddy --fuck
{ _: [],
fuck: [ 'whose', 'your', 'daddy', true ],
 '$0': 'argv.js' }

最简单的模式,也是最有趣的模式,值得去玩。

我就要你在我的身边,.demandOption(key, msg)**

如果你需要某个参数一定存在,这怎么办呢?难道要自己手动 if 一下,那真的好蠢啊。

.demandOption 就是这么来了

1
2
3
// demand.js
const argv = require('yargs').demandOption('baby').argv
console.dir(argv)

baby 在,世界一切太平,不管他是怎么在我的身边的。

1
2
3
4
5
6
# node demand.js --baby
{ _: [], baby: true, '$0': 'demand.js' }
# node demand.js --baby I
{ _: [], baby: 'I', '$0': 'demand.js' }
# node demand.js --baby --baby --baby --baby I
{ _: [], baby: [ true, true, true, 'I' ], '$0': 'demand.js' }

baby 不在,世界爆炸(exit code != 0)

1
2
3
4
5
# node demand.js
Options:
 --baby                                     [required]

Missing required argument: baby

.demandOption(key, msg)key 支持数组和字符串,分别表示单个和多个 required 的参数。而第二个参数值在没有满足条件的时候显示的文字。

啥?你嫌我太长?还是太短:).alias

俗话说的好,参数太长怎么办,变短一点喽

其实是我自己说的,可以给一个命令取一个别名,不管是变长还是变短,都很简单。

1
2
3
// alias.js
const argv = require('yargs').alias('long', ['l', 'lo']).alias('short', 's').argv
console.dir(argv)
1
2
3
4
5
6
7
8
# node alias.js -l --long --lo -s --short
{ _: [],
l: [ true, true, true ],
long: [ true, true, true ],
lo: [ true, true, true ],
s: [ true, true ],
short: [ true, true ],
 '$0': 'alias.js' }

可以看到 l lo long 是一样的,s short 是一样的,可长可短,自由随意。

你要我怎样我就怎样,.boolean .array .number .count .choices

有的时候,需要某些参数是固定的格式,而不是其他的方式,那么就需要这些方法来描述一个参数的类型。这些参数对于 alias 之后的参数同样也是可以的。

.array(key)

顾名思义,直接将参数的类型设置为数组,他会将后面所有的非普通参数作为当前参数的值。

1
2
3
// array.js
const argv = require('yargs').array('girls').argv
console.dir(argv)
1
2
3
4
5
# node array.js --girls Abby Aimee --stop --girls Alisa Angelia Amanda
{ _: [],
girls: [ 'Abby', 'Aimee', 'Alisa', 'Angelia', 'Amanda' ],
 stop: true,
 '$0': 'array.js' }

.boolean(key)

将参数类型设置为 Boolean 类型。如果后面的类型不是 Boolean 类型(truefalse),那么将不会设置为当前参数的值,并且当有多个的时候,不会合并成数组。

1
2
3
// boolean.js
const argv = require('yargs').boolean('love').argv
console.dir(argv)
1
2
3
4
# node boolean.js I --love you and --love again
{ _: [ 'I', 'you', 'and', 'again' ],
love: true,
 '$0': 'boolean.js' }

.number(key)

将参数类型设置为 Number 类型。基本规则如下:

  1. 如果没有填写值,那么默认是 undefined

  2. 如果设置的值不合法,那么是 NaN

  3. 否则是格式化为数字,使用 Number 构造方法

1
2
3
// number.js
const argv = require('yargs').number(['bust', 'waist', 'hips', 'height']).argv
console.dir(argv)
1
2
# node number.js --bust --waist 24 --hips zz
{ _: [], bust: undefined, waist: 24, hips: NaN, '$0': 'number.js' }

.count(key)

统计一下这个参数被使用了多少次,使用 .count 之后,参数默认就变成了 Boolean 类型,但是只统计他出现的次数。经常用来作为设置 debug 的输出级别。

1
2
3
// count.js
const argv = require('yargs').count('v').count('people').argv
console.log(argv)
1
2
# node count.js -v -vv --people --people false
{ _: [], v: 3, people: 2, '$0': 'count.js' }

.choices(key, list)

设置某个参数只能为某些值,可以和number boolean count 组合。

其本质是 indexOf 操作,也就是 === 做比较操作,所以这也就是为啥 array 不能和他匹配的原因。

1
2
3
4
5
6
7
// choices
const argv = require('yargs')
.choices('look', ['beatuify', 'oh, god'])
.choices('time', [1,2,3,4]).number('time')
.choices('many', [1,2]).count('many')
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# node choices.js --look "oh, god"
{ _: [], look: 'oh, god', '$0': 'choices.js' }

# node choices.js --look no
Invalid values:
Argument: look, Given: "no", Choices: "beatuify", "oh, god"

# node choices.js --time 1
{ _: [], time: 1, '$0': 'choices.js' }

# node choices.js --time 5
Invalid values:
Argument: time, Given: 5, Choices: 1, 2, 3, 4

# node choices.js --many --many
{ _: [], many: 2, '$0': 'choices.js' }

# node choices.js --many --many --many
Invalid values:
Argument: many, Given: 3, Choices: 1, 2

听说你和别人有千丝万缕的关系:( .conflicts .implies

简单一说:

  • .implies(我, 她) 有我先有她,有她不一定有我

  • .confilcts(我, 他) 有我没他,有他没我

如果两个都存在在一个参数上面的时候,implies 优先级会更高。

1
2
3
4
5
6
// imcon.js
const argv = require('yargs')
.conflicts('me', 'him')
.implies('me', 'her')
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# node imcon.js --me --him --her
Arguments me and him are mutually exclusive

# implies 有更高的优先级
# node imcon.js --me --him
Implications failed:
me -> her

# node imcon.js --me
Implications failed:
me -> her

# node imcon.js --me --her
{ _: [], me: true, her: true, '$0': 'imcon.js' }

# node imcon.js --him --her
{ _: [], him: true, her: true, '$0': 'imcon.js' }

大家在一起吧 :) .option .options

其实就是将上面的的所有的命令合并成一个 object,里面的 key 就是对应的函数名,而值就是参数。只不过 .options 是很多 .option 的集合。

这个就请看官网的例子源码

有用但是很简单其余参数

  • .default .defaults 设置默认参数值

  • .describe 对参数的描述

  • .usage 设置命令的提示的使用方法

  • .help 设置帮助的指令,添加 -help ,但是没有 h ,需要手动添加,可以选择是否添加 help 子命令

  • .group 分组,比如可以设置启动参数为一组,停止参数为一组,只是看起来比较舒服一些,并不影响什么内容。

  • .normalize 对参数的值用 path.normalize

  • .version 添加版本显示参数 -version,不过不添加缩写参数

  • .wrap 设置信息输出的最大的列宽度,比如说 -help 显示帮助参数。.wrap(null) 取消列宽限制,.wrap(require('yargs').terminalWidth()) 设置为当前宽度。默认是 Math.min(80, windowWidth

小弟来了 (-_-) **.command**

最简单的就是想实现类似 git 的那样的带有子命令的命令行操作,那么就需要这个东西。

他有如下的参数:

  • .command(cmd, desc, [builder], [handler])

  • .command(cmd, desc, [module])

  • .command(module)

  • builder 是构造器,可以是 Object|yargs => {},如果是对象,那么和 .options 是一样的。如果是函数,参数是 yargs 可以通过上面的函数添加参数。

  • handler 是处理器,当解析完成后,传入解析的结果,此时可以对结果进行处理。

  • module 最简单了,就是有

    • command 命令名

    • aliases 别名

    • describe 描述

    • builder 构造器

    • handler 处理器

当匹配到一个命令的时候, yargs 会做如下处理:

  1. 把当前命令输入到当前作用域中

  2. 清空所有的非全局的配置

  3. 如果传入了 builder,就通过其设置当前命令

  4. 解析和验证参数

  5. 如何一切正常,那么运行 handle,如果传入了的话

  6. 从当前作用域中弹出

这个位置是你的,别人抢不走 [arg1] <arg2>

有的时候希望命令必须要接受一个参数,或者接受一个可选参数,那么可以对命令使用 <>[] 设置他的位置。<> 表示这个命令必须要有,[] 表示这个参数可选。

有如下规则:

  • 通过 | 设置别名,例如 [name|username] ,在最后的解析中,nameusername 是一样的。

  • 最后一个可选参数支持添加 变成可变参数,例如 downloadto <from> [to…] 那么 to 是一个数组,并且必须要是命令中的最后一个可选参数才能变成可变参数。

1
2
3
4
5
6
// like.js
const argv = require('yargs')
.command('like <who>', 'you like who', {}, arg => console.dir(arg))
.command('dislike [who]', 'you dislike who', {}, arg => console.dir(arg))
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# node like.js like you
{ _: [ 'like' ], '$0': 'like.js', who: 'you' }
{ _: [ 'like' ], '$0': 'like.js', who: 'you' }

# node like.js like
like.js like <who>

Not enough non-option arguments: got 0, need at least 1

# node like.js dislike
{ _: [ 'dislike' ], '$0': 'like.js' }
{ _: [ 'dislike' ], '$0': 'like.js' }

# node like.js dislike you
{ _: [ 'dislike' ], '$0': 'like.js', who: 'you' }
{ _: [ 'dislike' ], '$0': 'like.js', who: 'you' }

默认命令

有的时候当没有任何命令匹配到的时候,希望有一个默认匹配的,那么可以用 * 代替普通命令的位置。

1
2
3
4
5
// defaultCommand.js
const argv = require('yargs')
.command('*', 'default command', {}, () => console.log('called command'))
.argv
console.dir(argv)
1
2
3
# node defaultCommand.js --name
called command
{ _: [], name: true, '$0': 'defaultCommand.js' }

方便一点 .commandDir

表示直接从文件夹中动态加载命令。详情请参考文档

从别的地方来 .config .env .coerce

写到这里,作者累了,所以:

  • .config 动态的从命令行中接受一个 json 文件路径,并自动加载。 doc

  • .env 设置环境变量的前缀,自动将这些前缀的环境变量去掉前缀,使用小驼峰和下划线方式加载。doc

  • .coerce 获取到变量值之后转化成别的值。doc

还有很多细节的,不过我觉得文档挺详细的,我就不多说了。

总结

感觉还是不错的,接口很简单,也通俗易懂。相比 commander 是两种不同的风格。commander 上手简单,但是前置知识有一些,而 yargs 相比前置知识的要求比较少,而且更加灵活。

注释和共享

目录

  1. 前言
  2. 工具
  3. 直播过程详解
  4. 直播源与 OBS
    1. OBS
    2. 下载/安装(自行解决)
    3. 概念
      1. 场景 & 来源
      2. 混音器
    4. 直播源
    5. 音频
      1. Step 1: Install iShowU Audio Capture
      2. Step 2:Setup Multi-Output Device
      3. Step 3: Switch to New Output
      4. Step 4:Set New Audio Input In OBS
    6. BUG
  5. 编码/转化/压缩 配置
  6. RTMP 协议传送数据
  7. 观看
  8. 总结

前言

随着现在互联网的发展,直播行业也是越来越火了,但是有一个现象就是大部分的直播客户端都是面向的 Window 系统,大部分的教程也是针对 Window 的,但我是一枚 Macer,所以我再这里将我使用 Macos 开启直播中重要的一些事情记录一下。

工具

直播过程详解

首先如果我们想要直播,那么首先要搞清楚直播的整个流程:

  1. 在不同的直播平台开启直播,然后获取 RTMP 地址和密钥

  2. 将 RTMP 地址和密钥填写到 OBS 的串流配置中

  3. 配置 OBS 直播界面,比如录制整个屏幕,录制某个应用程序,录制一个区域,添加字幕,添加本地影片等等内容

  4. 在 OBS 中开启直播,整个直播就开始了,其他人就可以在直播平台看到你的直播内容了

也就是说整个直播流的流动是这样的:

直播源 -> OBS 录制 -> 编码/转换/压缩 -> 通过 RTMP 协议将数据发送到直播平台 -> 用户打开直播平台 -> 获取数据 -> 直播播放

那么我们就针对整个直播流进行说明:

直播源与 OBS

由于这两部分可以在一起讲解,我们主要是讲解 OBS。

OBS

官网是这样描述这个软件的:

Open Broadcaster Software. Free and open source software for video recording and live streaming.

所以这款软件主要是面向的是视频录制和直播流,但是我不得不说用这个软件还是直播用的多。视频录制简单来说就是类似于 B 站的游戏视频似得,或者相关的视频教程。不过不得不说,如果是游戏录制的话,还是使用 navida 自带的视频录制好一些,那个对性能的影响不是很高。这个 obs 真占用 CPU >__>(后面会有相应的测试)。

下载/安装(自行解决)

在官网一个硕大的 Download,请自行点击,并选择合适的版本下载。

概念

场景 & 来源

如果是学过导播的同学,或许对这个并不陌生。那么我简单来说,用电视台举个例子,一个场景对应的是一个电视台的画面,而一个来源对应的电视台中的一个图层。比如说山东卫视是一个场景,但是山东卫视画面当中的台标是一个来源,电视剧画面是一个来源,底部滚动信息是一个来源,右下角广告是一个来源,这些来源共同组成了这个场景,也就是山东卫视的画面。

混音器

主要是针对输出的音频进行合成的

直播源

在 obs 启动之后,默认只有一个场景,但是场景里面是空的,那么我们需要添加自己需要录制或直播的内容:

在下方的来源中的右下角有一个加号,点击之后会有一个菜单让我们选择添加什么样的来源,有如下:

  • 图像 添加一个图像,类似于电视台添加台标

  • 图像幻灯片放映 添加图像幻灯片,不多解释

  • 场景可以在一个场景中嵌入另一个场景的内容,类似在山东卫视里面播放了浙江卫视的内容

  • 媒体源 添加一个媒体,一般是视频

  • 文本 添加文字

  • 显示捕获 捕获屏幕

  • 窗口捕获 捕获一个窗口的内容

  • 游戏录制(Syphon) 相当于窗口捕获,不过他是利用 Syphon 直接从 GPU
    中获取画面,而且没有窗口边框的,这点对于游戏直播来说还是不错的。

  • BrowserSource 嵌入一个网页

上面就是平时用到的一些内容,具体操作我就不讲解了,不同的来源有不同的配置内容,不过整体相对来说还是简单的,如果有需要的话,大家可以留言,我再详细讲解一下,当然还有音频的来源,这个我们后面再讲。

音频

在解决了画面的问题之后,那么我们剩下的问题就剩下音频了。大家或许想了,添加音频不是很简单的事情么,有啥复杂的。不过我想说的是,在 Window 上面或许确实简单,但是在 Macos 上面,由于 Apple 在 OSX10.11 加入的新的安全机制,导致无法正常的获取系统声音,麦克风的声音还是可以正常获取的。也就是说无法录制游戏内部的声音,但是可以录制你说话的声音。那么如果是 Linux 思维用户的话,那么就会直接把 SIP 关掉,哈哈哈。简单就是智慧,复杂就是愚蠢,😂

不过对于不想关闭的用户来说,还是有办法的。那么我们可以换个思路来思考,我们可以将音频输出到一个虚拟的输出设备,然后这个设备再虚拟成输入设备,在 OBS 从这个虚拟的输入设备中获取输入,就可以成功解决无法录制系统音频的问题了。

不过也有人问了,那这样的话,我不就听不到声音了么???解决办法就是:用另一台设备打开自己的直播网站,播放自己的直播视频,然后放在旁边,只不过可能会有几十秒的延迟而已 ^_^

不闹了,其实是用到了 Mac 自带的 Audio MIDI Setup ,需要这个软件先虚拟一个多输出设备,然后将音频输出到这个多输出设备上,然后由多输出设备将输出一部分送到扬声器,另一部分送到虚拟的输出设备,剩下的照常。

这样我们就解决了因为系统权限问题而无法获取系统声音的问题。

具体数据流见如下图:

Step 1: Install iShowU Audio Capture

首先解释一下 iShowU Audio Capture 是干什么用的,其实主要作用就是生成一个虚拟的输入输出设备。在这里,有很多的替代品,不过我觉得这个是比较纯粹的一个,只有这一个功能。

首先这里工具有官网链接,下载后安装即可。然后打开系统偏好设置 -> 声音,选择输出选项卡,会有一个叫做 iShowU Audio Capure 的选项,并且切换到输入选项卡,也可以看到一个叫做 iShowU Audio Capture 的选项,那么说明你已经安装成功了。

Step 2:Setup Multi-Output Device

打开 Audio MIDI Setup ,点击左下角的加号,选择创建多输出设备,分别在 iShowU Audio CaptureBuilt-in Output 前面勾选使用。

Step 3: Switch to New Output

打开系统偏好设置 -> 声音 -> 输出选项卡,选择我们刚刚添加的多输出设备,将输出定向到这个新的多输出设备上。

Step 4:Set New Audio Input In OBS

打开 OBS,选择添加音频输入来源,选择我们刚刚添加的 iShowU Audio Capture 即可。

下面我们可以进行一下测试,打开一个音乐播放,可以在混音器中看到有音频信号传入,并且扬声器中也有声音发出,那么说明你已经成功了!!!

赶紧打开一瓶饮料庆祝一下吧,不过且慢。🐛来了

BUG

此时如果你带着耳机你会发现麦克风不管用了,麦克风传入的信号也是系统声音,这是什么原因呢?不知。。。不过这里有一个解决方案。

创建一个输入聚合设备,跟创建多输出设备类似,不过这次我们选择创建聚合设备(aggregate device),在 Built-in MicrophoneiShowU Audio Capture 前面选中使用即可。然后再试一下,发现这个 BUG 是不是就解决了。如果还是不行,请在下方留言。

编码/转化/压缩 配置

这里主要说一下推荐的配置,针对 Macbook Pro 来说,建议使用 1280*800 的配置,码率选择 2000Kbps,帧速率 30 即可,这个配置下面可以达到较高的画质,而且 CPU 占用也不是很高。

RTMP 协议传送数据

在这里我就不详细解释 RTMP 了,也不解释 RTMP 和 HLS 的区别和联系了,这个大家可以自行 Google。

OBS 配置 RTMP 的位置是在设置 -> 串流里面,填写入地址和密钥即可。

保存之后,点击开始串流就可以进行直播了。

观看

这里顺便打个广告,我的 B 站直播间,主要是直播 Minecraft 和 Dota,纯属娱乐直播,没有什么意义。有时候可能还是直播 Coding。

总结

这篇文章算不上什么教程,也没有什么通俗易懂的图片之类的,因为大部分的内容都可以 Google 到,我这里只是针对一些可能出现问题的地方记录了一下,方便大家。

注释和共享

目录

2017年到了,我也放假了,正好在这个时间看到了一个叫做 v2ray 的代理软件,发现他非常符合我的需求,至少他有一下几个优点:

  • 支持多入多出,也就是可以同时监听多个接口,接收代理请求,然后通过多个输出端口进行代理。

  • 既然支持多入多出,不支持多协议怎么可以呢?他支持 ss,socks,vmess 等协议

  • 用 golang 写得,效率不会太低(我是说在相同算法和功能上,不是指他协议的加密传输算法)

  • 社区比较活跃,我喜欢

鉴于上面的优点,我去研究了一下这个 v2ray,发现安装起来还是很简单的么。

具体安装方式,请参考 v2ray install ,配置什么的我就不多说什么了,官网上面讲的很清楚。如果有什么问题,欢迎提交 issue 或者进入 telegram group 讨论,作者在里面还是很积极的。

不过现在 v2ray 总体来说对新手还不是很友好,期待以后的改进。而且暂时还没有 gui 管理界面,期待作者的添加。

说完了 v2ray,就不得不说 v2ray 相关的 tcp-bbr,谷歌大大提交的 tcp 拥塞解决算法,现在已经合并到 linux4.9 内核当中,听说对于高延迟长连接的线路有很大的优化作用,那么对于翻墙来说,这可是神器,怎么能不使用一下呢。

关于 bbr 的介绍,可以看这篇知乎的文章

那么我就在我的 linode 服务器上开启一下,不过需要注意的是,不要使用 linode dashboard 中的 linux4.9 内核,因为那个内核是被 linode 修改过的,会无法开启 bbr,我们需要手动安装 linux4.9 内核。

安装教程

是不是很期待呀,下面就是测试

可能由于家里的带宽的原因,提升速度太不明显了2333,等以后有空回学校重新测试一下。

不过我不能确定是不是我的服务器网络质量太好了,导致加速不明显 2333

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China