2

Go 流媒体(直播音视频)服务器 LAL - 开源自荐

 2 years ago
source link: https://www.v2ex.com/t/843953
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.

V2EX  ›  Go 编程语言

Go 流媒体(直播音视频)服务器 LAL - 开源自荐

  notokoy · 1 天前 · 1369 次点击

lal github 地址https://github.com/q191201771/lal

  • ▦ 一. lalserver 简介
    • ✒ lalserver 特性
    • ✒ 协议转换支持情况
  • ▦ 二. lalserver 安装、运行
    • ✒ 方式 1, 源码编译安装
    • ✒ 方式 2, Docker 镜像
    • ✒ 方式 3, 下载编译好的二进制可执行文件
  • ▦ 三. lalserver 使用
    • ✒ 各协议推拉流 url 地址列表
    • ✒ lalserver 配置文件
    • ✒ lalserver HTTP 事件和接口
    • ✒ lalserver 进阶
  • ▦ 四. 重新认识 lal
    • ✒ lal 的三层结构
      • ✦ 其他 demo
      • ✦ 流媒体协议栈库 package/library
      • ✦ Golang 通用基础库-naza
    • ✒ lalext
      • ✦ WebRTC
      • ✦ MCU 合流
  • ▦ 五. 联系作者

▦ 一. lalserver 简介

lalserver是纯 Golang 开发的流媒体(直播音视频网络传输)服务器。目前已支持 RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]/WebSocket[S]-FLV/TS 协议。

lal 特性图

✒ lalserver 特性

  • 全平台
    • 支持 linux/macOS/windows 多系统开发、调试、运行
    • 支持多 arch 运行。比如 amd64/arm64/arm32/ppc64le/mipsle/s390x
    • 支持交叉编译。可在任一平台编译出其他平台的可执行文件
    • 运行无依赖。可执行文件无任何环境、库安装依赖,可单文件独立运行
    • (开放全部源码的同时)提供各平台可执行文件,可免编译直接运行
    • 支持 docker
  • 高性能。多核多线程扩展,支持高并发,性能和同类型c/c++开发服务处于同一水平
  • 高可用。完善的单元测试。并且有多个线上环境应用
  • 多种直播流封装协议
    • 支持 RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]/WebSocket[S]-FLV/TS
    • 支持不同封装协议间相互转换
  • 多种编码格式。视频支持 H264/AVC ,H265/HEVC ,音频支持 AAC
  • 多种格式录制。支持 FLV ,长 MPEGTS ,HLS 录制(HLS 直播与录制可同时开启)
  • HTTPS。支持 HTTPS-FLV ,HTTPS-TS ,HLS over HTTPS 拉流
  • WebSocket/WebSockets。支持 Websocket-FLV ,WebSocket-TS 拉流
  • HLS。支持 LIVE 实时直播、VOD 全列表直播。切片文件支持多种删除方式。支持内存切片
  • RTSP。支持 over TCP(interleaved 模式)。支持 basic/digest auth 验证。支持 GET_PARAMETER
  • RTMP。支持给单视频添加静音音频数据,支持合并发送。兼容对接各种常见 RTMP 实现
  • HTTP API 接口。用于获取服务信息,向服务发送命令。
  • HTTP Notify 事件回调。获取时间通知,业务方可以轻松定制自身的逻辑。
  • 支持多种方式鉴权
  • 支持分布式集群
  • 静态 pull 回源。通过配置文件配置回源地址
  • 静态 push 转推。支持转推多个地址。通过配置文件配置转推地址
  • 支持按需回源。没有观众的回源拉流可以主动关闭
  • CORS 跨域。支持 HTTP-FLV ,HTTP-TS ,HLS 跨域拉流
  • HTTP 文件服务器。比如 HLS 切片文件可直接播放,不需要额外的 HTTP 文件服务器
  • 监听端口复用。HTTP-FLV ,HTTP-TS ,HLS 可使用相同的端口。over HTTPS 类似
  • 秒开播放。GOP 缓冲
  • 支持叠加静音音频
  • 支持先拉流后推流
  • 支持推流断开后重连,拉流端无感知

✒ 协议转换支持情况

见: 附录-协议间转换支持情况

▦ 二. lalserver 安装、运行

lalserver 支持 3 种安装、运行方式:

✒ 方式 1, 源码编译安装

下载源码:

git clone https://github.com/q191201771/lal.git
cd lal
export GO111MODULE=on && export GOPROXY=https://goproxy.cn,https://goproxy.io,direct
make

tips:
如果使用 GoLand 等 IDE 编译,那么编译lal/app/lalserver目录即可。
如果没有安装 Go 编译器,可参考《 CentOS 或 macOS 安装 GoLang 》,windows 操作系统可自行上网搜索教程。

./bin/lalserver -c conf/lalserver.conf.json

注意,windows 平台将路径分隔符/换成\

✒ 方式 2, Docker 镜像

下载镜像:

docker pull q191201771/lal

运行镜像:

$docker run -it -p 1935:1935 -p 8080:8080 -p 4433:4433 -p 5544:5544 -p 8083:8083 -p 8084:8084 -p 30000-30100:30000-30100/udp q191201771/lal /lal/bin/lalserver -c /lal/conf/lalserver.conf.json

如果想构建自己的 Docker 镜像,可参考 附录-构建自己的 Docker 镜像

✒ 方式 3, 下载编译好的二进制可执行文件

lal 提供linux/macos/windows平台编译好的 lal 二进制可执行文件(zip 压缩包形式)。
下载地址见: 《 github lal 最新 release 版本页面》
下载好的文件按方式 1 运行,不再赘述。

▦ 三. lalserver 使用

✒ 各协议推拉流 url 地址列表

lalserver 启动成功后,就可以开始使用了。
作为流媒体服务,最主要的功能就是流数据转发。比如:

使用 ffmpeg 推 rtmp 流:

ffmpeg -re -i demo.flv -c:a copy -c:v copy -f flv rtmp://127.0.0.1:1935/live/test110

使用 ffplay 拉 rtmp 流播放:

ffplay rtmp://127.0.0.1/live/test110

更多协议见: lalserver 各协议推拉流 url 地址列表

tips:
更多第三方客户端的使用方法见: 常见推拉流客户端信息汇总

✒ lalserver 配置文件

如果你想进一步了解 lalserver 的功能,可以看看: lalserver 配置文件说明

✒ lalserver HTTP 事件和接口

通过 lalserver 提供的丰富的 HTTP 接口,业务方可以在保持 lalserver 独立运行的情况下,使用自身熟悉的语言,轻松定制符合自身逻辑的业务系统。

✒ lalserver 进阶

▦ 四. 重新认识 lal

✒ lal 的三层结构

lal 源码架构图

✦ 其他 demo

lal 项目中,除了/app/lalserver这个比较核心的服务之外,在/app/demo目录下还额外提供了一些小应用,比如推、拉流客户端,以及压测工具,流分析工具,lalserver 集群的调度示例程序等。
这些 demo 你既可以直接使用,又向你演示了 lal 所提供的协议栈如何使用。

了解更多请访问: Demo 简介

✦ 流媒体协议栈库 package/library

lal 中的协议栈都是独立的,与应用分层设计的。并且客户端和服务端的协议栈都有。
业务方可以在自身的应用中集成 lal 的协议栈 package 库。

✦ Golang 通用基础库-naza

lal 将非流媒体特有的通用基础库抽象在一个独立的 github repo naza中。

了解更多请访问: 《 naza github 地址》https://github.com/q191201771/naza

✒ lalext

站在巨人的肩膀上才能看的更远。lal 在另一个 github repo lalext中,将 lal 的代码与第三方的库结合,实现了更丰富的功能。

✦ WebRTC

rtmp 转 WebRTC 的网关

了解更多请访问: 《 lalext github 地址》https://github.com/q191201771/lalext

  • lal:
    • 支持国标 GB28181 协议
    • lalserver 插件功能,支持业务方注册自定义协议
  • lalext: 使用 lal 库中流传输以及装封装功能的代码,结合 ffmpeg 库中编解码功能的代码,演示 MCU 合流

▦ 五. 联系作者

作者微信,扫码加好友:

欢迎任何技术和非技术的交流。

21 条回复    2022-04-01 00:49:06 +08:00

Kasumi20

Kasumi20      1 天前

KiseXu

KiseXu      1 天前

厉害了👍

yilelu0509

yilelu0509      1 天前

媒体处理的部分,用 Go 会不会 GC 的问题,不是很懂 Go ,纯好奇

hronro

hronro      1 天前

@yilelu0509 #3 没看过源码,不过我猜大概率是调 ffmpeg ,直接用 Go 处理的可能性不大

yilelu0509

yilelu0509      1 天前

@hronro 协议和封装部分还是 Go 实现的,作者还是花了心思了

dcoder

dcoder      1 天前

@notokoy
底层没有 ffmpeg 或者其他的 C/C++ lib 依赖么?

weak

weak      1 天前 via iPhone

ampedee

ampedee      18 小时 40 分钟前 via iPhone

牛逼👍不过建议文档的高可用部分改成生产可用,高可用一般是指分布式部署故障切换等等。

thinkingbullet

thinkingbullet      18 小时 9 分钟前

流弊的很呐

zhs227

zhs227      18 小时 5 分钟前

利害了。这么多东西都是作者一个人写的吗

notokoy

notokoy      16 小时 45 分钟前

感谢各位的支持与回复。

目前 Go 的 GC ,STW 已经很短了,几乎不影响业务。
有一些知名互联网公司已经在生产环境使用 Go 编写的流媒体服务了。
@yilelu0509

lal (以及内部的 lalserver )没有第三方依赖,不依赖 ffmpeg 也不直接或间接依赖任何第三方 Go 库。
流媒体部分主要做网络传输,协议转封装,存储等部分。并不是非用 ffmpeg 不可。
日志,内存管理,连接管理等等基础功能也全部都自实现。
后续涉及到音视频编解码部分,可能会在另外一个工程 lalext 中依赖 ffmpeg 。但是会做成另外的服务来扩展。用户可按需选用。
lal (以及 lalserver )会一直保持独立性,因为 lal 与同类型项目间的差异性目标是,易用性且完全自主可控(包括代码层面以及开发环境、部署等方面)
@hronro @yilelu0509 @dcoder

lal 中的 lalserver 配合调度服务可以支持分布式集群,故障切换的。所以也算是高可用吧 :)
@ampedee

现在代码基本是我一个人写的,但主体已经基本成型,现在开始推广并希望多吸收一些开发者参与。
流媒体方向的同学直接加入一起卷吧 :)
@zhs227

lesismal

lesismal      16 小时 5 分钟前

并发量大的时候,基于标准库的 net.Conn 还是消耗很大。7 、8 年前记得国外有个团队单机 50w 连接推流,内存、调度成本还是很高
去年我自己的异步框架功能基本完善后,也琢磨支持下流媒体的然后不用每个连接一个协程去循环读了,可以节省太多协程数量、内存、gc ,都可以变得可控,但是我自己暂时没那么多时间精力搞这一大套、得看以后有没有适合的时机了。如果作者有兴趣可以试下搞套异步的

PS ,阿里系开源真是各种 KPI 烂尾,之前看他们有人出了个 livego ,代码感人,低级错误、连 panic 都没处理好稍微测试了下就宕机了,本想 pr 修复下,但是看已经烂尾了所以没再关注了

楼主这个,我之前就有关注过,加油!

markgor

markgor      14 小时 8 分钟前

非该专业,也没怎么接触过,但想问问和 SRS 对比有哪些优点吗?
之前试过用 nginx 配合模块来做直播功能 demo ,但发现 5M 带宽中 1 推 2 拉 延时有 2~3 秒。
后来换了 SRS ,同样环境 1 推 2 啦,延时基本可以忽略。

notokoy

notokoy      13 小时 52 分钟前

@lesismal

老哥一看就是高手,以后多多指教哈。

协程数量方面:
流媒体应用的特点是单个连接吞吐量大,受限于带宽,实际连接数不会太多,所以连接对应的协程数量也不会太多。

线程模型方面:
另外一点我不知道理解的对不对啊,现在的 IO 框架,还有一个优点是 IO 线程可以多个,Go 标准库底层只有一个 IO 线程。
但我觉得在流媒体应用场景下暂时也不会成为瓶颈。

所以目前以我有限的认知前提下,考量到引入 IO 框架后,依赖增加,代码风格不 Go style ,以及后续如果扩展第三方协议(比如 QUIC )库时的兼容性,我暂时不会集成 IO 框架。

如果未来我发现使用 IO 框架可以给 lalserver 带来很大的性能提升,我会考虑集成。

内存管理确实是流媒体应用的一个重点。我一直有考虑并且会持续优化。

再次感谢老哥指教,可以加个微信,有机会多讨论。

notokoy

notokoy      13 小时 42 分钟前

@markgor

优点:
相对比的话,
srs 是非常非常优秀的流媒体项目,
我觉得不敢说优点,只能说最大的特点或者区别是,lal 是 Go 写的,srs 是 c++写的,
因此,可以利用 Go 的一些良好特性,比如对于用户来说,易于搭建的开发环境,跨平台能力,交叉编译,单文件运行部署等。

延时:
lal 项目中有很多测试 demo ,其中就有延时测试工具。
lal 的延时和第一梯队的 c++项目是持平的。

suyuyu

suyuyu      13 小时 26 分钟前

我是第 1k 个 start

doubtlhy

doubtlhy      11 小时 57 分钟前 via iPhone

之前发现一个功能差不多的开源项目 ZLMediaKit ,用了它的 webrtc 播放功能。

lesismal

lesismal      8 小时 37 分钟前

@notokoy #34

微信太闹腾了浪费很多时间,而且歪果仁也基本不用,你有 slack 频道没?可以 slack 弄点频道、还能进军海外扩张一下。也欢迎来我的项目的频道:
https://arpcnbio.slack.com/join/shared_invite/zt-16e6yu1mb-FUsil~2Jmn4Usl~IX_zv3g#/shared-invite/email

多数内部业务、尤其是需要很多功能定制的流媒体服务确实不需要太大在线量,毕竟外面有云、CDN 网络层层分担了在线数量的压力、同一 topic 只需要针对外层基础设施请求回源的节点数量就够用了,这个数量不会太大,而这些基础设施可能主流仍然是 c/cpp 的。
如果用 go 做云、CDN 之类的基础设施的开发、面向用户量很大,net.Conn 的开销也确实是还对应着挺高的硬件成本的,如果能换成 poller 和更多的内存优化,还是能声调不少资金、对能源环境问题也友好。

QUIC 这种也是路漫漫,不知道啥时候能普及,我的 poller 库只支持了 TLS/HTTP1.x/Websocket ,不打算支持 HTTP2.0 因为 2.0 太渣了。目前 go 的 QUIC 实现,也同样是一个连接至少一个协程,海量并发照样也是消耗很多资源。但是因为基于 UDP ,而 go 的 UDP 标准库已经足够简单了,所以我的 poller 库不打算支持 UDP 。至于 QUIC ,以后如果有档期,我也想搞一份不需要每个连接至少一个协程的实现,kcp-go 之类的类似标准库方案的一连接一协程也是同样的对海量并发不友好,海量并发时开销远超过其他语言的性能比较好的异步框架,剩下的只剩同步逻辑的便利,性能和消耗比其他语言异步框架劣势很多、甚至在基础设施领域,同步的便利已经无法弥补这个消耗的成本损失了。
每一样基础设施的工作量都有点大,真是想做很多但时间不够用了:joy:

你说的 go 的 io 线程数量应该是指 poller 线程数,这里先以这个作为前提,否则这样说可能不太准确。
之前我也没注意过 go 的 poller 线程数量是否只有 1 个,刚写代码试了下,net.Listen 0 个到 100 地址的时候,以及连接数 0 到 20w 的不同组合,确实都只有一个 eventfd ,而且即使不 listen 、没有连接也会有 eventfd ,是处理所有类型 fd 的 nonbloking 的:
但 top -Hp 查看,随着 net.Listen 地址数量或者连接数的增加,这 listener 和连接数的增加都对应着线程数的增加,应该是协程数多或者所需资源多时,runtime 动态增加了所需的线程。
考虑到 epoll 的不同用法和线程数量的情况,猜测 golang et 模式、epoll 线程里只是处理事件而不进行实际的读写,事件与 runtime 调度结合去触发用户协程的可读写唤醒,实际的读写则发生在用户的协程里,对应的是整个 runtime 的线程池 MPG 调度时被执行,而这个线程也是前面提到的随着负载增加而由 runtime 去增加的。所以前面我觉得“go 的 io 线程数量只有 1 个”这个说法可能不太准确。

引入 IO 框架倒还好,只是如果支持异步 IO ,编解码器要像 c/cpp 那样实现的是异步流解析,不像使用 net.Conn 同步解析这么简单,需要定制优化的地方也更多。统一实现成异步的编解码后,编解码器只收数据进行解析就可以、其实也就无所谓 IO 层是同步还是异步了,我的框架里目前就是这样,uni*是 epoll/kqueue 异步、windows 是使用 net.Conn 封装了下,框架传递数据给应用层。
并且,IO 这层节省了大量的协程数量,应用层则仍可以使用有限数量的协程池,而且应用层的协程池 1w-10w 这种 size 级别也足够用了,有特殊的阻塞需求还可以定制不同的 pool 来控制整个系统的协程资源,这样我们仍然可以在应用层写同步逻辑,我的库里现在就是这样做的,跑着效果还算挺好

notokoy

notokoy      6 小时 38 分钟前

@lesismal

老哥牛 b ,感谢纠正我的一个认知错误。
我想当然的以为 Go 的底层线程模型是一个 event loop ,并在里面完成 io 操作,然后和上层多个 worker 做交互。
刚才了解了一下,确实是 event loop 只处理事件,调用发生在上层。

不过。我依然觉得即使基于性能考虑,在流媒体场景也不是十分必须。

第一,现在一条直播流的带宽随随便便就超过 Mb 了,以后的趋势应该是越来越大。即使是 CDN 应用,受限于带宽,连接也不会太多吧。
真要是单个节点那么大的量,我觉得可以考虑多进程了。

第二,流媒体场景,还有一点和很多短连接场景不同,就是连接间有很多交互,比如数据转发。

我看过一些 IO 框架。
基本实现是支持多个 event loop ,然后连接绑定到一个固定的 event loop 中。
一些性能测试,是在自身 event loop 中 echo 数据,这种场景有两个特点:
1. 没有涉及到跨协程交互,可以无锁。
2. 理论上,一个 event loop 的所有连接可以共用一块读内存,因为不会长久持有。

这种接口型的业务场景可能对比起来比较明显。


我自己工作中也是写 c++的。以前也写过网络库。 :)

slack 没有使用过。英文不行。。

lesismal

lesismal      3 小时 32 分钟前

@notokoy #19

哥们太客气了,我也能从你的项目中学到不少流媒体的知识,交流下来,大家都有收获就很开心。

> 不过。我依然觉得即使基于性能考虑,在流媒体场景也不是十分必须。

对,所以我一直说的是海量并发场景的问题。我前面和接下来说的也是基于海量并发,普通并发量,标准库绝对足够了。
按照普通硬件配置比如 4c8c ,1-5w 这种连接数,poller 的相应性能没有优势,甚至因为异步流编解码更复杂而不如标准库响应性能好,而内存的节约也是对应交互频率等存在一定的阈值,阈值范围内,标准库更优

> 第一,现在一条直播流的带宽随随便便就超过 Mb 了,以后的趋势应该是越来越大。即使是 CDN 应用,受限于带宽,连接也不会太多吧。
> 真要是单个节点那么大的量,我觉得可以考虑多进程了。

这倒是不一定,多网卡多线,还有不同码率的包 size 未必真就是单节点不会太高连接数。而且单进程的 go 的 net.Conn 如果有瓶颈,改成单硬件上的多进程仍然高成本,只能加硬件,这个倒不是能不能搞定业务的问题,主要是硬件成本、部署节点数量维护成本之类的

> 第二,流媒体场景,还有一点和很多短连接场景不同,就是连接间有很多交互,比如数据转发。

这个确实是,但不管是 net.Conn 、poller 方案,还是 c/cpp 也都有同样的成本。所以这一点上成本的区别不大,无非就是 map 上再加点 buckets 之类的来降低锁的粒度来减少竞争成本。
所以主要的区别仍然是协程数量导致的内存、gc 、调度的压力

> 我看过一些 IO 框架。
> 基本实现是支持多个 event loop ,然后连接绑定到一个固定的 event loop 中。

对于读,姿势有点多,常见的一些用法:
1. LT 模式如果跨协程 /线程去处理 event ,则可能会有事件的多次重复派发导致事件和事件对应的读 syscall 浪费(也可以做标记避免一些重复读的浪费,但事件的重复派发也是内核和应用层的浪费),所以这种基本上是在 epoll 协程 /线程里直接进行
2. ET 模式,可以像 LT 那样直接在 epoll 循环里读,也可以把事件传给专门的 IO 协程 /线程去处理读,因为 ET 是每次有新的数据只会触发一次、不像 LT 那样只要未读完就不停触发,加上协议交互、窗口拥塞机制,所以即使跨协程 /线程处理读、ET 也不会有太多事件的重复派发,但需要注意每个 conn 的读应在确定的协程 /线程中进行,否则多个并发流读的时候可能造成数据顺序的混乱
3. ET+ONESHOT ,可以避免事件重复派发,可以让协程 /线程池中的每个任务比较均衡地获取事件进行处理,不至于像 ET 那样跨并发流传递必须注意单个 conn 的有序读;但是 ONESHOT 因为每次需要重复添加事件,如果每种使用方式实现比较良好的情况下,则 ONESHOT 方式下整体上浪费更多的 epoll syscall ,所以通常性能比 LT 或 ET 的吞吐要差一些

我自己框架中可配置 LT 或者 ET 、不支持 ET+ONESHOT ,因为实测 LT 吞吐要略好所以默认配置是 LT 。另外就是 IO 通常也不会是一个服务的全部 cpu 消耗,所以即使是丢个这几个 IO 协程 /线程,交给系统去自然调度也是能够达到整体均衡的。

对于写,也基本是 conn 直接写的多一些,缓冲区满、写失败了才会把写事件加到 event 里然后等待可写再 flush 。

> 一些性能测试,是在自身 event loop 中 echo 数据,这种场景有两个特点:
> 1. 没有涉及到跨协程交互,可以无锁。
> 2. 理论上,一个 event loop 的所有连接可以共用一块读内存,因为不会长久持有。

如果是针对流媒体服务这种涉及不同连接之间的交互的,你举例子的这里的无锁有两个条件:
1. 只有一个 event loop
2. 所有 IO 、逻辑也都是在这个 event loop 中

如果不满足这两个条件,收到一个连接的数据想推送给另一个,就仍然需要锁,而满足这两个条件付出的代价可比锁大多了:
1. 只能利用单核
2 go 的指令速度还是不够快
3. 虽然是非阻塞,但涉及 IO 毕竟也是 syscall 、不是纯 cpu 消耗,所以仍然是慢

另外关于内存,其实这种场景的内存优化,普通连接数的时候,一不小心甚至比标准库更耗内存,主要区别是:
1. net.Conn 方案是读一个 message 处理一个然后下一个、同一个 buffer 可以复用
2. poller 框架节省协程数量,但是要想不被单个连接线头阻塞、IO 协程读到完整消息后需要丢给不同的协程去处理逻辑,同一个 conn 同时可能几个 message 在等待处理,而且这些 buffer 是跨协程的、生命周期更复杂,没法像标准库方案那样方便复用

结论就是各种消耗,标准方案主要与在线量相关,poller 方案主要与交互频率相关,所以前面提到阈值,不同的在线量、交互频率下,二者各有不同的优劣表现。

我最初也是写 c/cpp 的框架,写 go 的这个发现 go 比 c/cpp 的框架甚至还要复杂些,因为默认带 gc ,使用 sync.Pool 也无法精准控制,需要做更多策略。而且 go 的指令不够强,如果像很多 c/cpp 框架那样做成逻辑单线程则性能不够,所以 go 仍然需要逻辑多协程,而这又涉及到更多的锁、时序、一致性相关的细节优化,所以除了语法简单了,实现姿势其实有更多改变


PS: 最后再重复下,标准库方案绝大多数时候够用了,咱们探讨的 poller 只针对海量并发这种,所以我不是一定要劝楼主搞异步框架,够用就好:joy:

lesismal

lesismal      2 小时 35 分钟前

@notokoy

哦对了,slack 跟用英语没什么关系。我不用微信之类的是因为被一些事情恶心到了,所以跑到 slack 上躲清静。一些国人知名开源项目也在 slack 上开频道了,而且 slack 、discord 这些有现成的 github 相关的 app/功能集成,做自己开源项目还是有些便利的

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK