技术讨论

技术讨论

使用什么docker image来运行Go程序

有问必答yang11 回复了问题 • 19 人关注 • 19 个回复 • 2644 次浏览 • 2017-08-02 11:05 • 来自相关话题

GOLANG中time.After释放的问题

技术讨论winlin 发表了文章 • 7 个评论 • 934 次浏览 • 2017-07-29 11:58 • 来自相关话题

在谢大群里看到有同学在讨论time.After泄漏的问题,就算时间到了也不会释放,瞬间就惊呆了,忍不住做了试验,结果发现应该没有这么的恐怖的,是有泄漏的风险不过不算是泄漏,先看API的说明:

查看全部
					

在谢大群里看到有同学在讨论time.After泄漏的问题,就算时间到了也不会释放,瞬间就惊呆了,忍不住做了试验,结果发现应该没有这么的恐怖的,是有泄漏的风险不过不算是泄漏,先看API的说明:


// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
return NewTimer(d).C
}

提到了一句The underlying Timer is not recovered by the garbage collector,这句挺吓人不会被GC回收,不过后面还有条件until the timer fires,说明fire后是会被回收的,所谓fire就是到时间了,写个例子证明下压压惊:


package main

import "time"

func main() {
for {
<- time.After(10 * time.Nanosecond)
}
}

显示内存稳定在5.3MB,CPU为161%,肯定被GC回收了的。当然如果放在goroutine也是没有问题的,一样会回收:


package main

import "time"

func main() {
for i := 0; i < 100; i++ {
go func(){
for {
<- time.After(10 * time.Nanosecond)
}
}()
}
time.Sleep(1 * time.Hour)
}

只是资源消耗会多一点,CPU为422%,内存占用6.4MB。因此:



Remark: time.After(d)在d时间之后就会fire,然后被GC回收,不会造成资源泄漏的。



那么API所说的If efficieny is a concern, user NewTimer instead and call Timer.Stop是什么意思呢?这是因为一般time.After会在select中使用,如果另外的分支跑得更快,那么timer是不会立马释放的(到期后才会释放),比如这种:


select {
case time.After(3*time.Second):
return errTimeout
case packet := packetChannel:
// process packet.
}

如果packet非常多,那么总是会走到下面的分支,上面的timer不会立刻释放而是在3秒后才能释放,和下面代码一样:


package main

import "time"

func main() {
for {
select {
case <-time.After(3 * time.Second):
default:
}
}
}

这个时候,就相当于会堆积了3秒的timer没有释放而已,会不断的新建和释放timer,内存会稳定在2.8GB,这个当然就不是最好的了,可以主动释放:


package main

import "time"

func main() {
for {
t := time.NewTimer(3*time.Second)

select {
case <- t.C:
default:
t.Stop()
}
}
}

这样就不会占用2.8GB内存了,只有5MB左右。因此,总结下这个After的说明:



  1. GC肯定会回收time.After的,就在d之后就回收。一般情况下让系统自己回收就好了。

  2. 如果有效率问题,应该使用Timer在不需要时主动Stop。大部分时候都不用考虑这个问题的。


交作业。

GOLANG使用Context实现传值、超时和取消

技术讨论winlin 发表了文章 • 0 个评论 • 424 次浏览 • 2017-06-28 16:24 • 来自相关话题

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。

Context基本的用法参考GOL... 查看全部

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。


Context基本的用法参考GOLANG使用Context管理关联goroutine


实际上,Context还有个非常重要的作用,就是设置超时。比如,如果我们有个API是这样设计的:


type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}

type Stack struct {
}
func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
return
}

一般使用是这样使用,创建context然后调用接口:


ctx,cancel := context.WithCancel(context.Background())
stack := &Stack{}
pkt,err := stack.Read(ctx)

那么,它本身就可以支持取消和超时,也就是用户如果需要取消,比如发送了SIGINT信号,程序需要退出,可以在收到信号后调用cancel


sc := make(chan os.Signal, 0)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range sc {
cancel()
}
}()

如果需要超时,这个API也不用改,只需要调用前设置超时时间:


ctx,cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pkt,err := stack.Read(ctx)

如果一个程序在运行,比如Read在等待,那么在没有人工干预的情况下,那就应该自己运行就好了。而人工干预,也就是需要取消,比如要升级程序了,或者需要停止服务了,都属于这种取消操作。而超时,一般是系统的策略,因为不能一直等下去,就需要在一定时间没有反应时终止服务。实际上context这两个都能支持得很好,而且还不影响Read本身的逻辑,在Read中只需要关注context是否Done:


func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
select {
// case <- dataChannel: // Parse packet from data channel.
case <- ctx.Done():
return nil,ctx.Err()
}
return
}

这是为何context被接纳成为标准库的包的缘故了吧,非常之强大和好用,而又非常简单。一行context,深藏功与名。


另外,Context还可以传递上下文的Key-Value对象,比如我们希望日志中,相关的goroutine都打印一个简化的CID,那么就可以用context.WithValue,参考go-oryx-lib/logger

GOLANG如何避免字符串转义

技术讨论winlin 发表了文章 • 0 个评论 • 314 次浏览 • 2017-06-23 17:41 • 来自相关话题

避免转义字符,例如造个json:

json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)查看全部
					

避免转义字符,例如造个json:


json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)

是不是太简单了点,但是我好像并不总是记得。

GOLANG宽泛接口在测试中的大用处

技术讨论winlin 发表了文章 • 1 个评论 • 216 次浏览 • 2017-06-23 17:26 • 来自相关话题

考虑测试一个函数:

func request(ctx context.Context, hc *http.Client, api string) (err error... 			查看全部
					

考虑测试一个函数:


func request(ctx context.Context, hc *http.Client, api string) (err error) {
var hreq *http.Request
if hreq, err = http.NewRequest("GET", api, nil); err != nil {
return nil, errors.Wrap(err, "create request")
}
var hres *http.Response
if hres, err = hc.Do(hreq.WithContext(ctx)); err != nil {
return nil, errors.Wrap(err, "do request")
}
defer hres.Body.Close()

var body []byte
if body, err = ioutil.ReadAll(hres.Body); err != nil {
return nil, errors.Wrap(err, "read body")
}

// ......
return nil
}

这个函数的参数是一个*http.Client,而不是接口,这个该如何测试?内嵌一个http.Client像这样吗?


type mockHttpClient struct {
http.Client
}

但是,问题是这样总是很恶心不是吗?就像如果是C++中,我们只能写一个mock类从要测试的类继承,但是我们只需要重写Do这个方法啊。



注意:对于C++而言,这是为何要求构造函数只是初始化,而不能包含逻辑,想象一个类在构造函数就访问了数据库,请问如何MOCK它?是做不到的,因此只能在构造函数初始化数据库的IP和账号等信息,提供connect这种函数连接数据库。


备注:上面只是拿数据库连接打个比方,实际上从MOCK角度来说,构造函数只能初始化内存对象,其他的应该啥也不干。



在GOLANG中,有个非常牛逼的方法,就是创建一个私有的接口,使用时用接口:


type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func request(ctx context.Context, hc httpDoer, api string) (err error) {
// ......

可以发现,很神奇的是,调用者也可以给*http.Client,对这个改动一无所知,这难道不是极其巧妙的设计吗?我们在mock中只需要mock这个方法就可以了。


一行代码处,深藏功与名~

GOLANG测试必须用带堆栈的errors

技术讨论winlin 发表了文章 • 2 个评论 • 294 次浏览 • 2017-06-22 18:15 • 来自相关话题

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。

GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。

先看测试序列的填充,参考查看全部

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。


GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。


先看测试序列的填充,参考tricks-2015这个文章,怕你翻不了墙,我把内容粘贴过来就是:


Anonymous structs: test cases (1/2)


These properties enable a nice way to express test cases:


func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
}
}
}

是的,看起来很方便,出错时也知道哪里的问题,但是实际上如果序列中有函数,那就悲剧了。让我们来试试,考虑一个包头的定义,它就是两个字段,然后序列化成[]byte


type MyHeader struct {
Version uint8
Size uint16
}

func (v MyHeader) MarshalBinary() ([]byte, error) {
return []byte{byte(v.Version),0,0},nil // Failed.
}

为了测试设置不同的值,得到不同的字节,我们用两个函数来填充测试序列:


func TestMyHeader_MarshalBinary(t *testing.T) {
mhs := []struct {
set func(h *MyHeader)
compare func(p []byte) error
}{
{func(h *MyHeader) { h.Size = 1 }, func(p []byte) error {
if p[1] != 0x01 {
return fmt.Errorf("p[1] is %v", p[1]) // line 194
}
return nil
}},
{func(h *MyHeader) { h.Size = 2 }, func(p []byte) error {
if p[1] != 0x02 {
return fmt.Errorf("p[1] is %v", p[1]) // line 200
}
return nil
}},
}
for _, mh := range mhs {
h := &MyHeader{}
mh.set(h)
if b, err := h.MarshalBinary(); err != nil {
t.Errorf("error is %+v", err)
} else if err = mh.compare(b); err != nil {
t.Errorf("invalid data, err is %+v", err) // line 211
}
}
}

结果我们就懵逼了,出现的错误行数都是在error那个地方211行是不够的,还需要知道是194还是200出问题了:


--- FAIL: TestMyHeader_MarshalBinary (0.00s)
iprouter_test.go:211: invalid data, err is p[1] is 0
iprouter_test.go:211: invalid data, err is p[1] is 0

怎么办呢?把堆栈信息带上,参考错误最佳实践,改成这样:


import oe "github.com/ossrs/go-oryx-lib/errors"

创建error时用这个package:


            if p[1] != 0x01 {
return oe.Errorf("p[1] is %v", p[1]) // line 194
}
if p[1] != 0x02 {
return oe.Errorf("p[1] is %v", p[1]) // line 200
}

结果可以看到详细的堆栈:


    iprouter_test.go:211: invalid data, err is p[1] is 0
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary.func4
/Users/winlin/git/test/src/core_test.go:200
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary
/Users/winlin/git/test/src/core_test.go:210
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

这样可以嵌套非常多的函数做测试了。

GOLANG最容易做测试MOCK

技术讨论winlin 发表了文章 • 0 个评论 • 742 次浏览 • 2017-06-09 18:26 • 来自相关话题

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:

func digest(data []byte, h ha... 			查看全部
					

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:


func digest(data []byte, h hash.Hash) ([]byte, error) {
if _, err = h.Write(data); err != nil {
return nil, errors.Wrap(err, "hash write")
}

d := h.Sum(nil)
if len(d) != 16 {
return nil, errors.Errorf("digest's %v bytes", len(d))
}
return d,nil
}

难以覆盖的因素有几个:



  1. 私有函数,一般其他语言在utest中只能访问public函数,而golang的utest是和目标在同一个package,所有函数和数据都可以访问。

  2. 有些函数非常难以出错,但是不代表不出错,比如这里的Write方法,一般都是不会有问题的,但是测试如果覆盖不到,保不齐哪天跑到这一行就挂掉了。

  3. MOCK桩对象或者函数,如果总是要把目标全部实现一遍,比如hash这个接口有5个方法,对Write打桩时只需要覆盖这个函数,其他的可以不动。是的,聪明的你可能会想到继承,但是如果这个类是隐藏的呢?比如一个md5的实现是隐藏不能访问的,暴露的只有hash的接口,怎么从md5这个类继承呢?GOLANG提供了类似从实现了接口对象的接口继承的方式,实际上是组合,具体看下面的实现。

  4. 有些古怪的逻辑,比如这里判断摘要是16字节,一般情况下也不会出现错误,当然utest也必须得覆盖到,万一哪天用了一个hash算法跑到这个地方,不能出现问题。



Remark: 注意到这个地方用了一个errors的package,它可以打印出问题出现的堆栈,参考Error最佳实践.



用GOLANG就可以完美解决上面所有的覆盖问题,先上代码:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

就这么简单?对的,但是不要小看这几行代码,深藏功与名~


组合接口


结构体mockMD5Write里面嵌套的不是实现md5哈希的类,而是直接嵌套的hash.Hash接口。这个有什么厉害的呢?假设用C++,看应该怎么搞:


class Hash {
public: virtual int Write(const char* data, int size) = 0;
public: virtual int Sum(const char* data, int size, char digest[16]) = 0;
public: virtual int Size() = 0;
};

class MD5 : public Hash {
// 省略了实现的代码
}

class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
};

是么?错了,mockMD5Write编译时会报错,会提示没有实现其他的接口。应该这么写:


class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
public: int Sum(const char* data, int size, char digest[16]) {
return imp->Sum(data, size, digest);
}
public: int Size() {
return imp->Size();
}
};

对比下够浪的接口组合,因为组合了一个hash.Hash的接口,所以它也就默认实现了,不用再把函数代理一遍了:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

这个可不是少写了几行代码的区别,这是本质的区别,我鸡冻的辩解道~如果这个接口有十个函数,我们要测试100个接口呢?这个MOCK该怎么写?另外,这个实际上是OO和GOLANG的细微差异,GOLANG的接口是契约,只要满足就可以,面向的全是动作,GOLANG像很多函数组合,它没有类体系的概念,也就是它的结构体不用明显符合哪个接口和哪个接口它才是合法的,实际上它可以符合任何适配的接口,也就是Die()这个动作,是自动被所有会Die的对象适配了的,不用显式声明自己会Die,关注的不是声明和实现了接口的关系,而是关注动作或者说接口本身,!@#$%^&*()$%^&*(#$%^&*#$^&不能说了,说多了都懂了我还怎么装逼去~


复杂错误


我们用了errors这个包,用来返回复杂错误,可以看到堆栈信息,对于utest也是一样,能看到堆栈对于解决问题也很重要。可以参考Error最佳实践。比如打印信息:


--- FAIL: TestDigest (0.00s)
digest_test.go:45: digest, mock md5
hash write data
_/Users/winlin/git/test/utility.digest
/Users/winlin/git/test/utility.go:46
_/Users/winlin/git/test/TestDigest
/Users/winlin/git/test/digest_test.go:42
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

测试代码:


func TestDigest(t *testing.T) {
if _, err := digest(nil, &mockMD5Write{md5.New()}); err == nil {
t.Error("should failed")
} else {
t.Errorf("digest, %+v", err)
}
}

当然这个地方是主动把error打印出来,因为用例就是应该要返回错误的,一般情况是:


func TestXXX(t *testing.T) {
if err := pfn(); err != nil {
t.Errorf("failed, %+v", err)
}
}

这样就可以知道堆栈了。

GOLANG空指针崩溃时堆栈消失和解决方案

技术讨论winlin 发表了文章 • 6 个评论 • 651 次浏览 • 2017-06-07 17:04 • 来自相关话题

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的查看全部

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的例子


package main

func main() {
run() // line 4
}
func run() {
causedPanic()
}
func causedPanic() {
//defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

这个程序崩溃时,打印的竟然是:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.main()
/tmp/sandbox277759147/main.go:4 +0x4

神奇的是,把第10行的defer打开,变成这样


func causedPanic() {
defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

堆栈神奇的回来了:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.causedPanic()
/tmp/sandbox416089181/main.go:13 +0x48
main.run()
/tmp/sandbox416089181/main.go:7 +0x20
main.main()
/tmp/sandbox416089181/main.go:4 +0x20

而主动调用panic堆栈也是没有问题的,可以把第10行注释掉,同时打开第11行。这个问题确实很诡异,在go-nuts中发了一篇文章问,strange stack trace when panic,马上就有神回复了:


On Wednesday, June 7, 2017 at 4:25:35 PM UTC+8, Dave Cheney wrote:

Try building your program with -gcflags="-l" to disable inlining.
If that restores the stacktrace, then it's inlining.
The good news is this should be fixed with Go 1.9

果然,运行时加上这个参数(编译时加上也是可以的),禁用内联编译后,堆栈就回来了:


go run -gcflags="-l" t.go

难怪了,主动调用panic时,内联编译不会把函数怼一坨去,如果没有defer和panic这种函数,就可能把函数怼一坨,看起来像是一个函数,堆栈消失了,这样在空指针时就找不到堆栈信息。


解决方案:



  1. 编译时加参数-gcflags="-l"

  2. 可能在GO1.9会解决这个问题。


结贴。

GOLANG错误处理最佳方案

技术讨论winlin 发表了文章 • 23 个评论 • 868 次浏览 • 2017-06-05 10:05 • 来自相关话题

GOLANG的错误很简单的,用error接口,参考golang error handling:

查看全部
					

GOLANG的错误很简单的,用error接口,参考golang error handling:


if f,err := os.Open("test.txt"); err != nil {
return err
}

实际上如果习惯于C返回错误码,也是可以的,定义一个整形的error:


type errorCode int
func (v errorCode) Error() string {
return fmt.Sprintf("error code is %v", v)
}

const loadFailed errorCode = 100

func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return loadFailed
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return loadFailed
}

return nil
}

这貌似没有什么难的啊?实际上,这只是error的基本单元,在实际的产品中,比如有个播放器会打印一个这个信息:


Player: Decode failed.

对的,就只有这一条信息,然后呢?就没有然后了,只知道是解码失败了,没有任何的线索,必须得调试播放器才能知道发生了什么。看我们的例子,如果load失败,也是一样的,只会打印一条信息:


error code is 100

这些信息是不够的,这是一个错误库很流行的原因,这个库是errors,它提供了一个Wrap方法:


_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}

也就是加入了多个error,如果用这个库,那么上面的例子该这么写:


func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return errors.Wrap(err, "open failed")
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return errors.New("content empty")
}

return nil
}

这个库给每个error可以加上额外的消息errors.WithMessage(err,msg),或者加上堆栈信息errors.WithStack(err),或者两个都加上erros.Wrap, 或者创建带堆栈信息的错误errors.Newerrors.Errorf。这样在多层函数调用时,就有足够的信息可以展现当时的情况了。


在多层函数调用中,甚至可以每层都加上自己的信息,例如:


func initialize() error {
if err := load("sys.db"); err != nil {
return errors.WithMessage(err, "init failed")
}

if f,err := os.Open("sys.log"); err != nil {
return errors.Wrap(err, "open log failed")
}
return nil
}

init函数中,调用load时因为这个err已经被Wrap过了,所以就只是加上自己的信息(如果用Wrap会导致重复的堆栈,不过也没有啥问题的了)。第二个错误用Wrap加上信息。打印日志如下:


empty content
main.load
/Users/winlin/git/test/src/demo/test/main.go:160
main.initialize
/Users/winlin/git/test/src/demo/test/main.go:167
main.main
/Users/winlin/git/test/src/demo/test/main.go:179
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
load sys.db failed

这样就可以知道是加载sys.db时候出错,错误内容是empty content,堆栈也有了。遇到错误时,会非常容易解决问题。


例如,AAC的一个库,用到了ASC对象,在解析时需要判断是否数据合法,实现如下(参考code):


func (v *adts) Decode(data []byte) (raw, left []byte, err error) {
p := data
if len(p) <= 7 {
return nil, nil, errors.Errorf("requires 7+ but only %v bytes", len(p))
}

// Decode the ADTS.

if err = v.asc.validate(); err != nil {
return nil, nil, errors.WithMessage(err, "adts decode")
}
return
}

func (v *AudioSpecificConfig) validate() (err error) {
if v.Channels < ChannelMono || v.Channels > Channel7_1 {
return errors.Errorf("invalid channels %#x", uint8(v.Channels))
}
return
}

在错误发生的最原始处,加上堆栈,在外层加上额外的必要信息,这样在使用时发生错误后,可以知道问题在哪里,写一个实例程序:


func run() {
adts,_ := aac.NewADTS()
if _,_,err := adts.Decode(nil); err != nil {
fmt.Println(fmt.Sprintf("Decode failed, err is %+v", err))
}
}

func main() {
run()
}

打印详细的堆栈:


Decode failed, err is invalid object 0x0
github.com/ossrs/go-oryx-lib/aac.(*AudioSpecificConfig).validate
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:462
github.com/ossrs/go-oryx-lib/aac.(*adts).Decode
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:439
main.run
/Users/winlin/git/test/src/test/main.go:13
main.main
/Users/winlin/git/test/src/test/main.go:19
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
adts decode

错误信息包含:



  1. adts decode,由ADTS打印出。

  2. invalid object 0x00,由ASC打印出。

  3. 完整的堆栈,包含main/run/aac.Decode/asc.Decode


如果这个信息是客户端的,发送到后台后,非常容易找到问题所在,比一个简单的Decode failed有用太多了,有本质的区别。如果是服务器端,那还需要加上上下文关于连接的信息,区分出这个错误是哪个连接造成的,也非常容易找到问题。


加上堆栈会不会性能低?错误出现的概率还是比较小的,几乎不会对性能有损失。使用复杂的error对象,就可以在库中避免用logger,在应用层使用logger打印到文件或者网络中。


对于其他的语言,比如多线程程序,也可以用类似方法,返回int错误码,但是把上下文信息保存到线程的信息中,清理线程时也清理这个信息。对于协程也是一样的,例如ST的thread也可以拿到当前的ID,利用全局变量保存信息。对于goroutine这种拿不到协程ID,可以用context.Context,实际上最简单的就是在error中加入上下文,因为Context要在1.7之后才纳入标准库。


一个C++的例子,得借助于宏定义:


struct ComplexError {
int code;
ComplexError* wrapped;
string msg;

string func;
string file;
int line;
};

#define errors_new(code, fmt, ...) \
_errors_new(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_new(const char* func, const char* file, int line, int code, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->code = code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

#define errors_wrap(err, fmt, ...) \
_errors_wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_wrap(const char* func, const char* file, int line, ComplexError* v, const char* fmt, ...) {
ComplexError* wrapped = (ComplexError*)v;

va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->wrapped = wrapped;
err->code = wrapped->code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

使用时,和GOLANG有点类似:


ComplexError* loads(string filename) {
if (filename.empty()) {
return errors_new(100, "invalid file");
}
return NULL;
}
ComplexError* initialize() {
string filename = "sys.db";
ComplexError* err = loads(filename);
if (err) {
return errors_wrap("load system from %s failed", filename.c_str());
}
return NULL;
}
int main(int argc, char** argv) {
ComplexError* err = initialize();
// Print err stack.
return err;
}

比单纯一个code要好很多,错误发生的概率也不高,获取详细的信息比较好。


另外,logger和error是两个不同的概念,比如对于library,错误时用errors返回复杂的错误,包含丰富的信息,但是logger一样非常重要,比如对于某些特定的信息,access log能看到客户端的访问信息,还有协议一般会在关键的流程点加日志,说明目前的运行状况,此外,还可以有json格式的日志或者叫做消息,可以把这些日志发送到数据系统处理。


对于logger,支持context.Context就尤其重要了,实际上context就是一次会话比如一个http request的请求的处理过程,或者一个RTMP的连接的处理。一个典型的logger的定义应该是:


// C++ style
logger(int level, void* ctx, const char* fmt, ...)
// GOLANG style
logger(level:int, ctx:context.Context, format string, args ...interface{})

这样在文本日志,或者在消息系统中,就可以区分出哪个会话。当然在error中也可以包含context的信息,这样不仅仅可以看到出错的错误和堆栈,还可以看到之前的重要的日志。还可以记录线程信息,对于多线程和回调函数,可以记录堆栈:


[2017-06-08 09:44:10.815][Error][54417][100][60] Main: Run, code=1015 : run : callback : cycle : api=http://127.0.0.1:8080, url=rtmp://localhost/live/livestream, token=16357216378262183 : parse json={"code":0,"data":{"servers":["127.0.0.1:1935"]}} : no data.key
thread #122848: run() [src/test/main.cpp:303][errno=60]
thread #987592: do_callback() [src/test/main.cpp:346][errno=36]
thread #987592: cycle() [src/sdk/test.cpp:3332][errno=36]
thread #987592: do_cycle() [src/sdk/test.cpp:3355][errno=36]
thread #987592: gslb() [src/sdk/test.cpp:2255][errno=36]
thread #987592: gslb_parse() [src/sdk/test.cpp:2284][errno=36]

当然,在ComplexError中得加入uint64_t trdint rerrno,然后new和wrap时赋值就好了。

GOLANG环境设置

技术讨论winlin 发表了文章 • 0 个评论 • 307 次浏览 • 2017-05-25 09:41 • 来自相关话题

原文:http://blog.csdn.net/win_lin/article/details/48265493


GO环境


官网下载GOLANG的安装:golang.org


如果不能翻墙,可以从golangtc或者gocn.io下载。


下载时,如何选择安装包:



  1. MAC,用brew安装(brew install go)或下载包含darwin的pkg或tar(推荐brew安装),pkg可以直接安装但是卸载比较麻烦,tar需要自己设置PATH安装麻烦卸载比较方便,例如go1.8.1.darwin-amd64.pkggo1.8.1.darwin-amd64.tar.gz

  2. Windows,一般就是安装包了,设置GOPATH比较麻烦,根据自己的系统是32位还是64位下载,譬如go1.8.1.windows-amd64.msi或者go1.8.1.windows-386.msi

  3. Linux,一般都是64位的,而且需要手动解压和设置GOPATH,当然还有ARM的,可以选择自己需要的下载,譬如go1.8.1.linux-amd64.tar.gz


如果是安装包,双击就可以安装,可以跳过下面这步。如果是linux这种需要自己解压和设置PATH的,步骤如下:


# 先解压和移动目录,以1.8.1为例
tar xf go1.8.1.linux-amd64.tar.gz &&
sudo mkdir -p /usr/local/ &&
sudo mv go /usr/local/go

# 设置PATH,打开文件
sudo vi /etc/profile

# 在文件末尾输入内容,然后保存
export PATH=$PATH:/usr/local/go/bin

# 应用修改的配置
source /etc/profile

UNIX设置GOPATH,存放项目的路径,GOLANG的项目都是绝对路径,从这个目录开始搜索:


# 创建目录,一般都是$HOME下面的go目录,譬如:/home/winlin/go
mkdir -p $HOME/go

# 设置GOPATH,打开文件
sudo vi /etc/profile

# 在文件末尾输入内容,然后保存
export GOPATH=$HOME/go

# 应用修改的配置
source /etc/profile

这样就可以用go build等编译了。譬如执行命令go version


Mac winlin$ go version
go version go1.8.1 darwin/amd64

go get从GITHUB下载一个项目,可以用到自己的项目中,可以看到下载到了GOPATH中:


Mac winlin$ go get github.com/ossrs/go-oryx-lib

Mac winlin$ ls -lh $GOPATH/src/github.com/ossrs
drwxr-xr-x 22 winlin 748B May 25 09:43 go-oryx-lib

GO的环境就配置成功了。

GOLANG使用Context管理关联goroutine

技术讨论winlin 发表了文章 • 6 个评论 • 683 次浏览 • 2017-05-19 15:08 • 来自相关话题

一般一个业务很少不用到goroutine的,因为很多方法是需要等待的,例如http.Server.ListenAndServe这个就是等待的,除非关闭了Server或Listener,否则是不会返回的。除非是一个API服务器,否... 查看全部

一般一个业务很少不用到goroutine的,因为很多方法是需要等待的,例如http.Server.ListenAndServe这个就是等待的,除非关闭了Server或Listener,否则是不会返回的。除非是一个API服务器,否则肯定需要另外起goroutine发起其他的服务,而且对于API服务器来说,在http.Handler的处理函数中一般也需要起goroutine,如何管理这些goroutine,在GOLANG1.7提供context.Context


先看一个简单的,如果启动两个goroutine,一个是HTTP,还有个信号处理的收到退出信号做清理:


wg := sync.WaitGroup{}
defer wg.Wait()

wg.Add(1)
go func() {
defer wg.Done()

ss := make(os.Signal, 0)
signal.Notify(ss, syscall.SIGINT, syscall.SIGTERM)
for s := ss {
fmt.Println("Got signal", s)
break
}
}()

wg.Add(1)
go func() {
defer wg.Done()

svr := &http.Server{ Addr:":8080", Handler:nil, }
fmt.Println(svr.ListenAndServe())
}

很清楚,起了两个goroutine,然后用WaitGroup等待它们退出。如果它们之间没有交互,不互相影响,那真的是蛮简单的,可惜这样是不行的,因为信号的goroutine收到退出信号后,应该通知server退出。暴力一点的是直接调用svr.Close(),但是如果有些请求还需要取消怎么办呢?最好用Context了:


wg := sync.WaitGroup{}
defer wg.Wait()

ctx,cancel := context.WithCancel(context.Background())

wg.Add(1)
go func() {
defer wg.Done()

ss := make(chan os.Signal, 0)
signal.Notify(ss, syscall.SIGINT, syscall.SIGTERM)
select {
case <- ctx.Done():
return
case s := <- ss:
fmt.Println("Got signal", s)
cancel() // 取消请求,通知用到ctx的所有goroutine
return
}
}()

wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

svr := &http.Server{ Addr:":8080", Handler:nil, }

go func(){
select {
case <- ctx.Done():
svr.Close()
}
}

fmt.Println(svr.ListenAndServe())
}

这个方式可以在新开goroutine时继续使用,譬如新加一个goroutine,里面读写了UDPConn:


wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

var conn *net.UDPConn
if conn,err = net.Dial("udp", "127.0.0.1:1935"); err != nil {
fmt.Println("Dial UDP server failed, err is", err)
return
}

fmt.Println(UDPRead(ctx, conn))
}()

UDPRead = func(ctx context.Context, conn *net.UDPConn) (err error) {
wg := sync.WaitGroup{}
defer wg.Wait()

ctx, cancel := context.WithCancel(ctx)

wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

for {
b := make([]byte, core.MTUSize)
size, _, err := conn.ReadFromUDP(b)
// 处理UDP包 b[:size]
}
}()

select {
case <-ctx.Done():
conn.Close()
}
return
}

如果只是用到HTTP Server,可以这么写:


func run(ctx contex.Context) {
server := &http.Server{Addr: addr, Handler: nil}
go func() {
select {
case <-ctx.Done():
server.Close()
}
}()

http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
})

fmt.Println(server.ListenAndServe())
}

如果需要提供一个API来让服务器退出,可以这么写:


func run(ctx contex.Context) {
server := &http.Server{Addr: addr, Handler: nil}

ctx, cancel := context.WithCancel(ctx)
http.HandleFunc("/quit", func(w http.ResponseWriter, r *http.Request) {
cancel() // 使用局部的ctx和cancel
})

go func() {
select {
case <-ctx.Done():
server.Close()
}
}()

fmt.Println(server.ListenAndServe())
}

使用局部的ctx和cancel,可以避免cancel传入的ctx,只是影响当前的ctx。

GOLANG使用嵌入结构实现接口

技术讨论winlin 发表了文章 • 0 个评论 • 303 次浏览 • 2017-05-19 13:23 • 来自相关话题

考虑一个Packet接口,一般会返回一个Header,例如:

type PacketHeader struct {
    ID uint32
    Timesta... 			查看全部
					

考虑一个Packet接口,一般会返回一个Header,例如:


type PacketHeader struct {
ID uint32
Timestamp uint64
}

type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
Header() *PacketHeader
}

如果是OO的语言,一般会有一个基类,里面包含了Header和实现这个Header:


class BasePacket : public Packet {
protected:
PacketHeader h;
public:
virtual Header() *PacketHeader;
};

class HandshakePacket : public BasePacket {
};

在子类中就都实现了这个Header()方法了,在GOLANG同样可以做到,通过在Header中定义方法,在Packet中包含Header就可以。


func (v *PacketHeader) Header() *PakcetHeader {
return v
}

type HandshakePacket struct {
PacketHeader
}

看起来还差不多的,都可以实现,golang只是代码少一点,清晰一点点而已。考虑要添加一些辅助函数,譬如给Packet添加是否是紧急类型的包,那OO语言得做一次代理:


type Packet interface {
IsErgency() bool
}

class BasePacketHeader {
public:
bool IsErgency() {
return realtime < 3;
}
}

class BasePacket {
public:
bool IsErgency() {
return h.IsErgency();
}
}

而在GOLANG中,只需要在Header实现就好了:


func (v *PacketHeader) IsErgency() bool {
return v.realtime < 3
}

更高级的可以直接嵌入接口。譬如context.Context的实现,cancelCtx直接嵌入了一个接口:


type cancelCtx struct {
Context

通过指定类型,或者初始化的顺序初始化struct


func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}

结构嵌套的方式,让组合实现起来非常便捷,避免频繁的代理。

使用两个context实现CLOSE包的超时等待

技术讨论winlin 发表了文章 • 0 个评论 • 317 次浏览 • 2017-05-18 20:39 • 来自相关话题

在UDP中,一般发送者发送包后,如果一定的时间对方没有收到,就需要重传。例如UDP实现握手的过程,如果握手的包,比如RTMFP协议的IHELLO,发送给对方后,如果一定1秒没有收到,就应该重发一次,然后等3秒、6秒、9秒,如果最后没有收到就是超时了。查看全部

在UDP中,一般发送者发送包后,如果一定的时间对方没有收到,就需要重传。例如UDP实现握手的过程,如果握手的包,比如RTMFP协议的IHELLO,发送给对方后,如果一定1秒没有收到,就应该重发一次,然后等3秒、6秒、9秒,如果最后没有收到就是超时了。


最后一个Close包,发送者不能等待这么长的时间,所以需要设置一个较短的时间做超时退出。一般收发都是一个context,在最后这个Close包时,收到ctx.Done也不能立刻退出,因为还需要稍微等待,譬如600毫秒如果没有收到响应才能退出。


一个可能的实现是这样:


in := make(chan []byte)

func Close(ctx context.Context) (err error) {
timeous := ... // 1s,3s,6s,9s...
for _, to := range timeouts {
// 发送给对方WriteToUDP("CLOSE", peer)
// 另外一个goroutine读取UDP包到in

select {
case <- time.After(to):
case <- in:
fmt.Println("Close ok")
return
case <- ctx.Done():
fmt.Println("Program quit")
return
}
}
return
}

但是这个问题在于,在程序退出时,一般都会cancel ctx然后调用Close方法,这个地方就不会等待任何的超时,就打印"Program quit"然后返回了。解决方案是用另外一个context。但是如何处理之前的ctx的done呢?可以再起一个goroutine做同步:


in := make(chan []byte)

func Close(ctx context.Context) (err error) {
ctxRead,cancelRead := context.WithCancel(context.Background())
go func(){ // sync ctx with ctxRead
select {
case <-ctxRead.Done():
case <-ctx.Done():
select {
case <-ctxRead.Done():
case <-time.After(600*time.Milliseconds):
cancelRead()
}
}
}()

ctx = ctxRead // 下面直接用ctxRead。
timeous := ... // 1s,3s,6s,9s...
for _, to := range timeouts {
// 发送给对方WriteToUDP("CLOSE", peer)
// 另外一个goroutine读取UDP包到in

select {
case <- time.After(to):
case <- in:
fmt.Println("Close ok")
return
case <- ctx.Done():
fmt.Println("Program quit")
return
}
}
return
}

这样在主要的逻辑中,还是只需要处理ctx,但是这个ctx已经是新的context了。不过在实际的过程中,这个sync的goroutine需要确定起来后,才能继续,否则会造成执行顺序不确定:


sc := make(chan bool, 1)
go func(){ // sync ctx with ctxRead
sc <- true
select {
......
}
<- sc

使用context,来控制多个goroutine的执行和取消,是非常好用的,关键可以完全关注业务的逻辑,而不会引入因为ctx取消或者超时机制而造成的特殊逻辑。

GOLANG实现超时对象检测的最好理解的方式

技术讨论winlin 发表了文章 • 0 个评论 • 379 次浏览 • 2017-05-16 17:39 • 来自相关话题

依赖于心跳的系统,都需要超时检测。比如P2P系统中客户端每隔120秒向数据服务器发送一次数据汇总,服务器就需要维护一个超时时间。比如一个UDP服务器,在和客户端之间创建Session之后,如果没有数据包,一般会有Ping包,说明这个Session是存活的... 查看全部

依赖于心跳的系统,都需要超时检测。比如P2P系统中客户端每隔120秒向数据服务器发送一次数据汇总,服务器就需要维护一个超时时间。比如一个UDP服务器,在和客户端之间创建Session之后,如果没有数据包,一般会有Ping包,说明这个Session是存活的,服务器在发现Session超时后也需要清理。


首先,服务器一般需要维护一个列表,以Peer为例:


type Peer struct {
id uint64
heartbeat time.Time
}

type Server struct {
peers map[uint64]*Peer
lock sync.Mutex
}

创建Peer,同时在收到Ping消息后,更新Peer的心跳时间:


func (v *Server) Create(id uint64) *Peer {
v.lock.Lock()
defer v.lock.UnLock()

p = &Peer { id:id, heartbeat: time.Now(), }
v.peers[id] = p
return p
}

func (v *Server) OnPing(id uint64) {
v.lock.Lock()
defer v.lock.UnLock()

if p,ok := v.peers[id]; ok {
p.heatbeat = time.Now()
}
}

当然,需要起一个goroutine定期扫描这个列表, 假设300秒超时:


go func(v *Server) {
for {
func(){
v.lock.Lock()
defer v.lock.UnLock()

now := time.Now()
for id,p := range v.peers {
if p.heartbeat.Add(300 * time.Second).Before(now) {
delete(v.peers, id)
}
}
}()
time.Sleep(30 * time.Second)
}
}(server)

如果Peers的数目非常多,那么扫描时每次都需要锁定v.peers,会导致其他的业务都无法进行。特别是清理Peer这个过程如果比较复杂,譬如需要发起io请求,是一个费时的操作时,就会造成系统的等待。


一般来说,超时的Peer不会很多,因此可以用chan放一个超时的peer,每个peer专门起一个goroutine来看什么时候超时,这样就可以在检测超时时避免用锁了:


timeout := make(chan *Peer)

func (v *Server) Create(id uint64) *Peer {
v.lock.Lock()
defer v.lock.UnLock()

p = &Peer { id:id, heartbeat: time.Now(), }
v.peers[id] = p
return p

go func(p *Peer) {
for {
tm := p.heartbeat
<- time.After(300 * time.Second)
if tm.Equal(p.heartbeat) {
timeout <- p
break
}
}
}(p)
}

go func(v *Server){
for gw := range timeout {
func(){
lgateways.Lock()
defer lgateways.Unlock()

delete(gateways, gw.port)
}()

// Do something cleanup about the gateway.
}
}(server)

这样就只有在有Peer超时时,才真正锁住Server.peers


检测超时时,除了用heartbeat心跳时间,还可以用keepAlive chan bool保活消息来实现:


type Peer struct {
keepAlive chan bool
}

func (v *Server) OnPing(id uint64) {
if p,ok := v.peers[id]; ok {
select {
case p.keepAlive <- true:
default:
}
}
}

这样检测超时也更简单:


go func(p *Peer) {
for {
select {
case <- time.After(300 * time.Second):
timeout <- p
case <- p.keepAlive:
}
}
}(p)

这样就是两个chan联合完成这个任务:


keepAlive => timeout

GOLANG接口适配,组合方式的灵活接口演化

技术讨论winlin 发表了文章 • 0 个评论 • 415 次浏览 • 2017-05-15 20:49 • 来自相关话题

在OO(Object Oriented)原则中,有一条叫做:优先使用组合,而不是继承。虽然GOLANG并不是OO的语言(没有继承和多态),但是不妨碍GOLANG使用这条原则,而GOLANG的作者就强调过这一点,在GOLANG中是使用组合而非继承来扩展。<... 查看全部

在OO(Object Oriented)原则中,有一条叫做:优先使用组合,而不是继承。虽然GOLANG并不是OO的语言(没有继承和多态),但是不妨碍GOLANG使用这条原则,而GOLANG的作者就强调过这一点,在GOLANG中是使用组合而非继承来扩展。


装逼的说来,继承是一种名词化的语言体系,先进行业务抽象然后设计类体系和继承关系。而组合,强制使用接口,因为组合中使用的总是另外一个对象的接口,通过动词的组合,实现目标,比如不管是什么只要有Write([]byte)(int,error)这个动作,就实现了这个接口,其他对象组合这个接口后,对外也看起来就是个io.Writer的接口。


比如,GOALNG1.8支持了writev,一般在面向对象会这么的搞:


class Socket {
int Write(void*, int);
int Writev(const iovec*, int);
};

对的吧?一个Socket可以写数据,也可以用writev写iovec向量,就是一次性写入多个内存块。



Note: 有时候内存块是不连续的,比如一个Video帧,发送给不同的客户端时,Header是需要修改的,但是Payload都一样,那么可以针对每个客户端只创建一个header,然后公用payload,但是这时候两个内存指针是不连续的,特别是需要同时写入多个视频帧时,writev就很神奇的避免了内存拷贝writev(header+payload),具体参考下writev的资料哈。



这样有个问题,并非所有系统都支持Writev的,并非所有Socket都支持Writev的,如果是自己写个代码,当然是可以随便这么搞的,但是作为标准库,GOLANG当然是不能这么做的。GOLANG就加了一个接口(一个新动作)叫做net.buffersWriter,如果实现了这个接口就用writev。先看用法:


    conn,err := net.Dial("tcp", "127.0.0.1:1935")

buffers := Buffers{
[]byte("once upon a time in "),
[]byte("Gopherland ... "),
}

buffers.WriteTo(conn)

在Buffers的WriteTo方法会判断是否是writev的接口,如果是则用writev写,否则就一个个的写:


func (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {
if wv, ok := w.(buffersWriter); ok {
return wv.writeBuffers(v)
}

实际上conn是net.TcpConn,里面有个fd *net.netFD,它实现了net.buffersWriter接口,所以最后调用的就是(fd *netFD) writeBuffers(v *Buffers)


func (c *conn) writeBuffers(v *Buffers) (int64, error) {
n, err := c.fd.writeBuffers(v)

func (fd *netFD) writeBuffers(v *Buffers) (n int64, err error) {
iovecs = append(iovecs, syscall.Iovec{Base: &chunk[0]})
wrote, _, e0 := syscall.Syscall(syscall.SYS_WRITEV,
uintptr(fd.sysfd),
uintptr(unsafe.Pointer(&iovecs[0])),
uintptr(len(iovecs)))

对于其他没有实现这个接口的对象,就每个向量循环的写。


在看一个例子http.Get(url string),客户端发起一个HTTP请求:


http.Get("http://localhost:1985/api/v1/versions")
// 实际上调用的是:
func (c *Client) Get(url string)
// 然后调用:
(c *Client) Do(req *Request)

在GOLANG1.7中引入了context的概念,用来支持cancel,怎么用的:


ctx,cancel := context.WithCancel(context.Background())

select {
case <- ctx.Done():
// Cancelled.
case <- time.After(...):
// Timeout
case <- other events:
// Other events.
}

如何支持取消的HTTP请求呢?给http.Get加个ctx参数?例如http.Get(ctx, url)这样?那改动得多大啊,而且还不能兼容之前的API,泪奔~看看GOLANG的解决:


ctx,cancel := context.WithCancel(context.Background())
go func(){
req,err := http.NewRequest("http://...")
res,err := http.DefaultClient.Do(req.WithContext(ctx))
defer res.Body.Close()
// 读取res响应结果。
}()

select {
case <- ctx.Done():
case <- time.After(3 * time.Second):
cancel() // Timeout to cancel all requests.
}

使用组合,通过req.WithContext再返回一个*http.Request,实现同样的目的。

GOLANG中time.After释放的问题

技术讨论winlin 发表了文章 • 7 个评论 • 934 次浏览 • 2017-07-29 11:58 • 来自相关话题

在谢大群里看到有同学在讨论time.After泄漏的问题,就算时间到了也不会释放,瞬间就惊呆了,忍不住做了试验,结果发现应该没有这么的恐怖的,是有泄漏的风险不过不算是泄漏,先看API的说明:

查看全部
					

在谢大群里看到有同学在讨论time.After泄漏的问题,就算时间到了也不会释放,瞬间就惊呆了,忍不住做了试验,结果发现应该没有这么的恐怖的,是有泄漏的风险不过不算是泄漏,先看API的说明:


// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
return NewTimer(d).C
}

提到了一句The underlying Timer is not recovered by the garbage collector,这句挺吓人不会被GC回收,不过后面还有条件until the timer fires,说明fire后是会被回收的,所谓fire就是到时间了,写个例子证明下压压惊:


package main

import "time"

func main() {
for {
<- time.After(10 * time.Nanosecond)
}
}

显示内存稳定在5.3MB,CPU为161%,肯定被GC回收了的。当然如果放在goroutine也是没有问题的,一样会回收:


package main

import "time"

func main() {
for i := 0; i < 100; i++ {
go func(){
for {
<- time.After(10 * time.Nanosecond)
}
}()
}
time.Sleep(1 * time.Hour)
}

只是资源消耗会多一点,CPU为422%,内存占用6.4MB。因此:



Remark: time.After(d)在d时间之后就会fire,然后被GC回收,不会造成资源泄漏的。



那么API所说的If efficieny is a concern, user NewTimer instead and call Timer.Stop是什么意思呢?这是因为一般time.After会在select中使用,如果另外的分支跑得更快,那么timer是不会立马释放的(到期后才会释放),比如这种:


select {
case time.After(3*time.Second):
return errTimeout
case packet := packetChannel:
// process packet.
}

如果packet非常多,那么总是会走到下面的分支,上面的timer不会立刻释放而是在3秒后才能释放,和下面代码一样:


package main

import "time"

func main() {
for {
select {
case <-time.After(3 * time.Second):
default:
}
}
}

这个时候,就相当于会堆积了3秒的timer没有释放而已,会不断的新建和释放timer,内存会稳定在2.8GB,这个当然就不是最好的了,可以主动释放:


package main

import "time"

func main() {
for {
t := time.NewTimer(3*time.Second)

select {
case <- t.C:
default:
t.Stop()
}
}
}

这样就不会占用2.8GB内存了,只有5MB左右。因此,总结下这个After的说明:



  1. GC肯定会回收time.After的,就在d之后就回收。一般情况下让系统自己回收就好了。

  2. 如果有效率问题,应该使用Timer在不需要时主动Stop。大部分时候都不用考虑这个问题的。


交作业。

GOLANG使用Context实现传值、超时和取消

技术讨论winlin 发表了文章 • 0 个评论 • 424 次浏览 • 2017-06-28 16:24 • 来自相关话题

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。

Context基本的用法参考GOL... 查看全部

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。


Context基本的用法参考GOLANG使用Context管理关联goroutine


实际上,Context还有个非常重要的作用,就是设置超时。比如,如果我们有个API是这样设计的:


type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}

type Stack struct {
}
func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
return
}

一般使用是这样使用,创建context然后调用接口:


ctx,cancel := context.WithCancel(context.Background())
stack := &Stack{}
pkt,err := stack.Read(ctx)

那么,它本身就可以支持取消和超时,也就是用户如果需要取消,比如发送了SIGINT信号,程序需要退出,可以在收到信号后调用cancel


sc := make(chan os.Signal, 0)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range sc {
cancel()
}
}()

如果需要超时,这个API也不用改,只需要调用前设置超时时间:


ctx,cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pkt,err := stack.Read(ctx)

如果一个程序在运行,比如Read在等待,那么在没有人工干预的情况下,那就应该自己运行就好了。而人工干预,也就是需要取消,比如要升级程序了,或者需要停止服务了,都属于这种取消操作。而超时,一般是系统的策略,因为不能一直等下去,就需要在一定时间没有反应时终止服务。实际上context这两个都能支持得很好,而且还不影响Read本身的逻辑,在Read中只需要关注context是否Done:


func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
select {
// case <- dataChannel: // Parse packet from data channel.
case <- ctx.Done():
return nil,ctx.Err()
}
return
}

这是为何context被接纳成为标准库的包的缘故了吧,非常之强大和好用,而又非常简单。一行context,深藏功与名。


另外,Context还可以传递上下文的Key-Value对象,比如我们希望日志中,相关的goroutine都打印一个简化的CID,那么就可以用context.WithValue,参考go-oryx-lib/logger

GOLANG如何避免字符串转义

技术讨论winlin 发表了文章 • 0 个评论 • 314 次浏览 • 2017-06-23 17:41 • 来自相关话题

避免转义字符,例如造个json:

json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)查看全部
					

避免转义字符,例如造个json:


json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)

是不是太简单了点,但是我好像并不总是记得。

GOLANG宽泛接口在测试中的大用处

技术讨论winlin 发表了文章 • 1 个评论 • 216 次浏览 • 2017-06-23 17:26 • 来自相关话题

考虑测试一个函数:

func request(ctx context.Context, hc *http.Client, api string) (err error... 			查看全部
					

考虑测试一个函数:


func request(ctx context.Context, hc *http.Client, api string) (err error) {
var hreq *http.Request
if hreq, err = http.NewRequest("GET", api, nil); err != nil {
return nil, errors.Wrap(err, "create request")
}
var hres *http.Response
if hres, err = hc.Do(hreq.WithContext(ctx)); err != nil {
return nil, errors.Wrap(err, "do request")
}
defer hres.Body.Close()

var body []byte
if body, err = ioutil.ReadAll(hres.Body); err != nil {
return nil, errors.Wrap(err, "read body")
}

// ......
return nil
}

这个函数的参数是一个*http.Client,而不是接口,这个该如何测试?内嵌一个http.Client像这样吗?


type mockHttpClient struct {
http.Client
}

但是,问题是这样总是很恶心不是吗?就像如果是C++中,我们只能写一个mock类从要测试的类继承,但是我们只需要重写Do这个方法啊。



注意:对于C++而言,这是为何要求构造函数只是初始化,而不能包含逻辑,想象一个类在构造函数就访问了数据库,请问如何MOCK它?是做不到的,因此只能在构造函数初始化数据库的IP和账号等信息,提供connect这种函数连接数据库。


备注:上面只是拿数据库连接打个比方,实际上从MOCK角度来说,构造函数只能初始化内存对象,其他的应该啥也不干。



在GOLANG中,有个非常牛逼的方法,就是创建一个私有的接口,使用时用接口:


type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func request(ctx context.Context, hc httpDoer, api string) (err error) {
// ......

可以发现,很神奇的是,调用者也可以给*http.Client,对这个改动一无所知,这难道不是极其巧妙的设计吗?我们在mock中只需要mock这个方法就可以了。


一行代码处,深藏功与名~

GOLANG测试必须用带堆栈的errors

技术讨论winlin 发表了文章 • 2 个评论 • 294 次浏览 • 2017-06-22 18:15 • 来自相关话题

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。

GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。

先看测试序列的填充,参考查看全部

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。


GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。


先看测试序列的填充,参考tricks-2015这个文章,怕你翻不了墙,我把内容粘贴过来就是:


Anonymous structs: test cases (1/2)


These properties enable a nice way to express test cases:


func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
}
}
}

是的,看起来很方便,出错时也知道哪里的问题,但是实际上如果序列中有函数,那就悲剧了。让我们来试试,考虑一个包头的定义,它就是两个字段,然后序列化成[]byte


type MyHeader struct {
Version uint8
Size uint16
}

func (v MyHeader) MarshalBinary() ([]byte, error) {
return []byte{byte(v.Version),0,0},nil // Failed.
}

为了测试设置不同的值,得到不同的字节,我们用两个函数来填充测试序列:


func TestMyHeader_MarshalBinary(t *testing.T) {
mhs := []struct {
set func(h *MyHeader)
compare func(p []byte) error
}{
{func(h *MyHeader) { h.Size = 1 }, func(p []byte) error {
if p[1] != 0x01 {
return fmt.Errorf("p[1] is %v", p[1]) // line 194
}
return nil
}},
{func(h *MyHeader) { h.Size = 2 }, func(p []byte) error {
if p[1] != 0x02 {
return fmt.Errorf("p[1] is %v", p[1]) // line 200
}
return nil
}},
}
for _, mh := range mhs {
h := &MyHeader{}
mh.set(h)
if b, err := h.MarshalBinary(); err != nil {
t.Errorf("error is %+v", err)
} else if err = mh.compare(b); err != nil {
t.Errorf("invalid data, err is %+v", err) // line 211
}
}
}

结果我们就懵逼了,出现的错误行数都是在error那个地方211行是不够的,还需要知道是194还是200出问题了:


--- FAIL: TestMyHeader_MarshalBinary (0.00s)
iprouter_test.go:211: invalid data, err is p[1] is 0
iprouter_test.go:211: invalid data, err is p[1] is 0

怎么办呢?把堆栈信息带上,参考错误最佳实践,改成这样:


import oe "github.com/ossrs/go-oryx-lib/errors"

创建error时用这个package:


            if p[1] != 0x01 {
return oe.Errorf("p[1] is %v", p[1]) // line 194
}
if p[1] != 0x02 {
return oe.Errorf("p[1] is %v", p[1]) // line 200
}

结果可以看到详细的堆栈:


    iprouter_test.go:211: invalid data, err is p[1] is 0
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary.func4
/Users/winlin/git/test/src/core_test.go:200
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary
/Users/winlin/git/test/src/core_test.go:210
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

这样可以嵌套非常多的函数做测试了。

GOLANG最容易做测试MOCK

技术讨论winlin 发表了文章 • 0 个评论 • 742 次浏览 • 2017-06-09 18:26 • 来自相关话题

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:

func digest(data []byte, h ha... 			查看全部
					

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:


func digest(data []byte, h hash.Hash) ([]byte, error) {
if _, err = h.Write(data); err != nil {
return nil, errors.Wrap(err, "hash write")
}

d := h.Sum(nil)
if len(d) != 16 {
return nil, errors.Errorf("digest's %v bytes", len(d))
}
return d,nil
}

难以覆盖的因素有几个:



  1. 私有函数,一般其他语言在utest中只能访问public函数,而golang的utest是和目标在同一个package,所有函数和数据都可以访问。

  2. 有些函数非常难以出错,但是不代表不出错,比如这里的Write方法,一般都是不会有问题的,但是测试如果覆盖不到,保不齐哪天跑到这一行就挂掉了。

  3. MOCK桩对象或者函数,如果总是要把目标全部实现一遍,比如hash这个接口有5个方法,对Write打桩时只需要覆盖这个函数,其他的可以不动。是的,聪明的你可能会想到继承,但是如果这个类是隐藏的呢?比如一个md5的实现是隐藏不能访问的,暴露的只有hash的接口,怎么从md5这个类继承呢?GOLANG提供了类似从实现了接口对象的接口继承的方式,实际上是组合,具体看下面的实现。

  4. 有些古怪的逻辑,比如这里判断摘要是16字节,一般情况下也不会出现错误,当然utest也必须得覆盖到,万一哪天用了一个hash算法跑到这个地方,不能出现问题。



Remark: 注意到这个地方用了一个errors的package,它可以打印出问题出现的堆栈,参考Error最佳实践.



用GOLANG就可以完美解决上面所有的覆盖问题,先上代码:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

就这么简单?对的,但是不要小看这几行代码,深藏功与名~


组合接口


结构体mockMD5Write里面嵌套的不是实现md5哈希的类,而是直接嵌套的hash.Hash接口。这个有什么厉害的呢?假设用C++,看应该怎么搞:


class Hash {
public: virtual int Write(const char* data, int size) = 0;
public: virtual int Sum(const char* data, int size, char digest[16]) = 0;
public: virtual int Size() = 0;
};

class MD5 : public Hash {
// 省略了实现的代码
}

class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
};

是么?错了,mockMD5Write编译时会报错,会提示没有实现其他的接口。应该这么写:


class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
public: int Sum(const char* data, int size, char digest[16]) {
return imp->Sum(data, size, digest);
}
public: int Size() {
return imp->Size();
}
};

对比下够浪的接口组合,因为组合了一个hash.Hash的接口,所以它也就默认实现了,不用再把函数代理一遍了:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

这个可不是少写了几行代码的区别,这是本质的区别,我鸡冻的辩解道~如果这个接口有十个函数,我们要测试100个接口呢?这个MOCK该怎么写?另外,这个实际上是OO和GOLANG的细微差异,GOLANG的接口是契约,只要满足就可以,面向的全是动作,GOLANG像很多函数组合,它没有类体系的概念,也就是它的结构体不用明显符合哪个接口和哪个接口它才是合法的,实际上它可以符合任何适配的接口,也就是Die()这个动作,是自动被所有会Die的对象适配了的,不用显式声明自己会Die,关注的不是声明和实现了接口的关系,而是关注动作或者说接口本身,!@#$%^&*()$%^&*(#$%^&*#$^&不能说了,说多了都懂了我还怎么装逼去~


复杂错误


我们用了errors这个包,用来返回复杂错误,可以看到堆栈信息,对于utest也是一样,能看到堆栈对于解决问题也很重要。可以参考Error最佳实践。比如打印信息:


--- FAIL: TestDigest (0.00s)
digest_test.go:45: digest, mock md5
hash write data
_/Users/winlin/git/test/utility.digest
/Users/winlin/git/test/utility.go:46
_/Users/winlin/git/test/TestDigest
/Users/winlin/git/test/digest_test.go:42
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

测试代码:


func TestDigest(t *testing.T) {
if _, err := digest(nil, &mockMD5Write{md5.New()}); err == nil {
t.Error("should failed")
} else {
t.Errorf("digest, %+v", err)
}
}

当然这个地方是主动把error打印出来,因为用例就是应该要返回错误的,一般情况是:


func TestXXX(t *testing.T) {
if err := pfn(); err != nil {
t.Errorf("failed, %+v", err)
}
}

这样就可以知道堆栈了。

GOLANG空指针崩溃时堆栈消失和解决方案

技术讨论winlin 发表了文章 • 6 个评论 • 651 次浏览 • 2017-06-07 17:04 • 来自相关话题

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的查看全部

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的例子


package main

func main() {
run() // line 4
}
func run() {
causedPanic()
}
func causedPanic() {
//defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

这个程序崩溃时,打印的竟然是:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.main()
/tmp/sandbox277759147/main.go:4 +0x4

神奇的是,把第10行的defer打开,变成这样


func causedPanic() {
defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

堆栈神奇的回来了:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.causedPanic()
/tmp/sandbox416089181/main.go:13 +0x48
main.run()
/tmp/sandbox416089181/main.go:7 +0x20
main.main()
/tmp/sandbox416089181/main.go:4 +0x20

而主动调用panic堆栈也是没有问题的,可以把第10行注释掉,同时打开第11行。这个问题确实很诡异,在go-nuts中发了一篇文章问,strange stack trace when panic,马上就有神回复了:


On Wednesday, June 7, 2017 at 4:25:35 PM UTC+8, Dave Cheney wrote:

Try building your program with -gcflags="-l" to disable inlining.
If that restores the stacktrace, then it's inlining.
The good news is this should be fixed with Go 1.9

果然,运行时加上这个参数(编译时加上也是可以的),禁用内联编译后,堆栈就回来了:


go run -gcflags="-l" t.go

难怪了,主动调用panic时,内联编译不会把函数怼一坨去,如果没有defer和panic这种函数,就可能把函数怼一坨,看起来像是一个函数,堆栈消失了,这样在空指针时就找不到堆栈信息。


解决方案:



  1. 编译时加参数-gcflags="-l"

  2. 可能在GO1.9会解决这个问题。


结贴。

GOLANG错误处理最佳方案

技术讨论winlin 发表了文章 • 23 个评论 • 868 次浏览 • 2017-06-05 10:05 • 来自相关话题

GOLANG的错误很简单的,用error接口,参考golang error handling:

查看全部
					

GOLANG的错误很简单的,用error接口,参考golang error handling:


if f,err := os.Open("test.txt"); err != nil {
return err
}

实际上如果习惯于C返回错误码,也是可以的,定义一个整形的error:


type errorCode int
func (v errorCode) Error() string {
return fmt.Sprintf("error code is %v", v)
}

const loadFailed errorCode = 100

func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return loadFailed
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return loadFailed
}

return nil
}

这貌似没有什么难的啊?实际上,这只是error的基本单元,在实际的产品中,比如有个播放器会打印一个这个信息:


Player: Decode failed.

对的,就只有这一条信息,然后呢?就没有然后了,只知道是解码失败了,没有任何的线索,必须得调试播放器才能知道发生了什么。看我们的例子,如果load失败,也是一样的,只会打印一条信息:


error code is 100

这些信息是不够的,这是一个错误库很流行的原因,这个库是errors,它提供了一个Wrap方法:


_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}

也就是加入了多个error,如果用这个库,那么上面的例子该这么写:


func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return errors.Wrap(err, "open failed")
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return errors.New("content empty")
}

return nil
}

这个库给每个error可以加上额外的消息errors.WithMessage(err,msg),或者加上堆栈信息errors.WithStack(err),或者两个都加上erros.Wrap, 或者创建带堆栈信息的错误errors.Newerrors.Errorf。这样在多层函数调用时,就有足够的信息可以展现当时的情况了。


在多层函数调用中,甚至可以每层都加上自己的信息,例如:


func initialize() error {
if err := load("sys.db"); err != nil {
return errors.WithMessage(err, "init failed")
}

if f,err := os.Open("sys.log"); err != nil {
return errors.Wrap(err, "open log failed")
}
return nil
}

init函数中,调用load时因为这个err已经被Wrap过了,所以就只是加上自己的信息(如果用Wrap会导致重复的堆栈,不过也没有啥问题的了)。第二个错误用Wrap加上信息。打印日志如下:


empty content
main.load
/Users/winlin/git/test/src/demo/test/main.go:160
main.initialize
/Users/winlin/git/test/src/demo/test/main.go:167
main.main
/Users/winlin/git/test/src/demo/test/main.go:179
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
load sys.db failed

这样就可以知道是加载sys.db时候出错,错误内容是empty content,堆栈也有了。遇到错误时,会非常容易解决问题。


例如,AAC的一个库,用到了ASC对象,在解析时需要判断是否数据合法,实现如下(参考code):


func (v *adts) Decode(data []byte) (raw, left []byte, err error) {
p := data
if len(p) <= 7 {
return nil, nil, errors.Errorf("requires 7+ but only %v bytes", len(p))
}

// Decode the ADTS.

if err = v.asc.validate(); err != nil {
return nil, nil, errors.WithMessage(err, "adts decode")
}
return
}

func (v *AudioSpecificConfig) validate() (err error) {
if v.Channels < ChannelMono || v.Channels > Channel7_1 {
return errors.Errorf("invalid channels %#x", uint8(v.Channels))
}
return
}

在错误发生的最原始处,加上堆栈,在外层加上额外的必要信息,这样在使用时发生错误后,可以知道问题在哪里,写一个实例程序:


func run() {
adts,_ := aac.NewADTS()
if _,_,err := adts.Decode(nil); err != nil {
fmt.Println(fmt.Sprintf("Decode failed, err is %+v", err))
}
}

func main() {
run()
}

打印详细的堆栈:


Decode failed, err is invalid object 0x0
github.com/ossrs/go-oryx-lib/aac.(*AudioSpecificConfig).validate
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:462
github.com/ossrs/go-oryx-lib/aac.(*adts).Decode
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:439
main.run
/Users/winlin/git/test/src/test/main.go:13
main.main
/Users/winlin/git/test/src/test/main.go:19
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
adts decode

错误信息包含:



  1. adts decode,由ADTS打印出。

  2. invalid object 0x00,由ASC打印出。

  3. 完整的堆栈,包含main/run/aac.Decode/asc.Decode


如果这个信息是客户端的,发送到后台后,非常容易找到问题所在,比一个简单的Decode failed有用太多了,有本质的区别。如果是服务器端,那还需要加上上下文关于连接的信息,区分出这个错误是哪个连接造成的,也非常容易找到问题。


加上堆栈会不会性能低?错误出现的概率还是比较小的,几乎不会对性能有损失。使用复杂的error对象,就可以在库中避免用logger,在应用层使用logger打印到文件或者网络中。


对于其他的语言,比如多线程程序,也可以用类似方法,返回int错误码,但是把上下文信息保存到线程的信息中,清理线程时也清理这个信息。对于协程也是一样的,例如ST的thread也可以拿到当前的ID,利用全局变量保存信息。对于goroutine这种拿不到协程ID,可以用context.Context,实际上最简单的就是在error中加入上下文,因为Context要在1.7之后才纳入标准库。


一个C++的例子,得借助于宏定义:


struct ComplexError {
int code;
ComplexError* wrapped;
string msg;

string func;
string file;
int line;
};

#define errors_new(code, fmt, ...) \
_errors_new(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_new(const char* func, const char* file, int line, int code, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->code = code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

#define errors_wrap(err, fmt, ...) \
_errors_wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_wrap(const char* func, const char* file, int line, ComplexError* v, const char* fmt, ...) {
ComplexError* wrapped = (ComplexError*)v;

va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->wrapped = wrapped;
err->code = wrapped->code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

使用时,和GOLANG有点类似:


ComplexError* loads(string filename) {
if (filename.empty()) {
return errors_new(100, "invalid file");
}
return NULL;
}
ComplexError* initialize() {
string filename = "sys.db";
ComplexError* err = loads(filename);
if (err) {
return errors_wrap("load system from %s failed", filename.c_str());
}
return NULL;
}
int main(int argc, char** argv) {
ComplexError* err = initialize();
// Print err stack.
return err;
}

比单纯一个code要好很多,错误发生的概率也不高,获取详细的信息比较好。


另外,logger和error是两个不同的概念,比如对于library,错误时用errors返回复杂的错误,包含丰富的信息,但是logger一样非常重要,比如对于某些特定的信息,access log能看到客户端的访问信息,还有协议一般会在关键的流程点加日志,说明目前的运行状况,此外,还可以有json格式的日志或者叫做消息,可以把这些日志发送到数据系统处理。


对于logger,支持context.Context就尤其重要了,实际上context就是一次会话比如一个http request的请求的处理过程,或者一个RTMP的连接的处理。一个典型的logger的定义应该是:


// C++ style
logger(int level, void* ctx, const char* fmt, ...)
// GOLANG style
logger(level:int, ctx:context.Context, format string, args ...interface{})

这样在文本日志,或者在消息系统中,就可以区分出哪个会话。当然在error中也可以包含context的信息,这样不仅仅可以看到出错的错误和堆栈,还可以看到之前的重要的日志。还可以记录线程信息,对于多线程和回调函数,可以记录堆栈:


[2017-06-08 09:44:10.815][Error][54417][100][60] Main: Run, code=1015 : run : callback : cycle : api=http://127.0.0.1:8080, url=rtmp://localhost/live/livestream, token=16357216378262183 : parse json={"code":0,"data":{"servers":["127.0.0.1:1935"]}} : no data.key
thread #122848: run() [src/test/main.cpp:303][errno=60]
thread #987592: do_callback() [src/test/main.cpp:346][errno=36]
thread #987592: cycle() [src/sdk/test.cpp:3332][errno=36]
thread #987592: do_cycle() [src/sdk/test.cpp:3355][errno=36]
thread #987592: gslb() [src/sdk/test.cpp:2255][errno=36]
thread #987592: gslb_parse() [src/sdk/test.cpp:2284][errno=36]

当然,在ComplexError中得加入uint64_t trdint rerrno,然后new和wrap时赋值就好了。

GOLANG环境设置

技术讨论winlin 发表了文章 • 0 个评论 • 307 次浏览 • 2017-05-25 09:41 • 来自相关话题

原文:http://blog.csdn.net/win_lin/article/details/48265493


GO环境


官网下载GOLANG的安装:golang.org


如果不能翻墙,可以从golangtc或者gocn.io下载。


下载时,如何选择安装包:



  1. MAC,用brew安装(brew install go)或下载包含darwin的pkg或tar(推荐brew安装),pkg可以直接安装但是卸载比较麻烦,tar需要自己设置PATH安装麻烦卸载比较方便,例如go1.8.1.darwin-amd64.pkggo1.8.1.darwin-amd64.tar.gz

  2. Windows,一般就是安装包了,设置GOPATH比较麻烦,根据自己的系统是32位还是64位下载,譬如go1.8.1.windows-amd64.msi或者go1.8.1.windows-386.msi

  3. Linux,一般都是64位的,而且需要手动解压和设置GOPATH,当然还有ARM的,可以选择自己需要的下载,譬如go1.8.1.linux-amd64.tar.gz


如果是安装包,双击就可以安装,可以跳过下面这步。如果是linux这种需要自己解压和设置PATH的,步骤如下:


# 先解压和移动目录,以1.8.1为例
tar xf go1.8.1.linux-amd64.tar.gz &&
sudo mkdir -p /usr/local/ &&
sudo mv go /usr/local/go

# 设置PATH,打开文件
sudo vi /etc/profile

# 在文件末尾输入内容,然后保存
export PATH=$PATH:/usr/local/go/bin

# 应用修改的配置
source /etc/profile

UNIX设置GOPATH,存放项目的路径,GOLANG的项目都是绝对路径,从这个目录开始搜索:


# 创建目录,一般都是$HOME下面的go目录,譬如:/home/winlin/go
mkdir -p $HOME/go

# 设置GOPATH,打开文件
sudo vi /etc/profile

# 在文件末尾输入内容,然后保存
export GOPATH=$HOME/go

# 应用修改的配置
source /etc/profile

这样就可以用go build等编译了。譬如执行命令go version


Mac winlin$ go version
go version go1.8.1 darwin/amd64

go get从GITHUB下载一个项目,可以用到自己的项目中,可以看到下载到了GOPATH中:


Mac winlin$ go get github.com/ossrs/go-oryx-lib

Mac winlin$ ls -lh $GOPATH/src/github.com/ossrs
drwxr-xr-x 22 winlin 748B May 25 09:43 go-oryx-lib

GO的环境就配置成功了。

GOLANG使用Context管理关联goroutine

技术讨论winlin 发表了文章 • 6 个评论 • 683 次浏览 • 2017-05-19 15:08 • 来自相关话题

一般一个业务很少不用到goroutine的,因为很多方法是需要等待的,例如http.Server.ListenAndServe这个就是等待的,除非关闭了Server或Listener,否则是不会返回的。除非是一个API服务器,否... 查看全部

一般一个业务很少不用到goroutine的,因为很多方法是需要等待的,例如http.Server.ListenAndServe这个就是等待的,除非关闭了Server或Listener,否则是不会返回的。除非是一个API服务器,否则肯定需要另外起goroutine发起其他的服务,而且对于API服务器来说,在http.Handler的处理函数中一般也需要起goroutine,如何管理这些goroutine,在GOLANG1.7提供context.Context


先看一个简单的,如果启动两个goroutine,一个是HTTP,还有个信号处理的收到退出信号做清理:


wg := sync.WaitGroup{}
defer wg.Wait()

wg.Add(1)
go func() {
defer wg.Done()

ss := make(os.Signal, 0)
signal.Notify(ss, syscall.SIGINT, syscall.SIGTERM)
for s := ss {
fmt.Println("Got signal", s)
break
}
}()

wg.Add(1)
go func() {
defer wg.Done()

svr := &http.Server{ Addr:":8080", Handler:nil, }
fmt.Println(svr.ListenAndServe())
}

很清楚,起了两个goroutine,然后用WaitGroup等待它们退出。如果它们之间没有交互,不互相影响,那真的是蛮简单的,可惜这样是不行的,因为信号的goroutine收到退出信号后,应该通知server退出。暴力一点的是直接调用svr.Close(),但是如果有些请求还需要取消怎么办呢?最好用Context了:


wg := sync.WaitGroup{}
defer wg.Wait()

ctx,cancel := context.WithCancel(context.Background())

wg.Add(1)
go func() {
defer wg.Done()

ss := make(chan os.Signal, 0)
signal.Notify(ss, syscall.SIGINT, syscall.SIGTERM)
select {
case <- ctx.Done():
return
case s := <- ss:
fmt.Println("Got signal", s)
cancel() // 取消请求,通知用到ctx的所有goroutine
return
}
}()

wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

svr := &http.Server{ Addr:":8080", Handler:nil, }

go func(){
select {
case <- ctx.Done():
svr.Close()
}
}

fmt.Println(svr.ListenAndServe())
}

这个方式可以在新开goroutine时继续使用,譬如新加一个goroutine,里面读写了UDPConn:


wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

var conn *net.UDPConn
if conn,err = net.Dial("udp", "127.0.0.1:1935"); err != nil {
fmt.Println("Dial UDP server failed, err is", err)
return
}

fmt.Println(UDPRead(ctx, conn))
}()

UDPRead = func(ctx context.Context, conn *net.UDPConn) (err error) {
wg := sync.WaitGroup{}
defer wg.Wait()

ctx, cancel := context.WithCancel(ctx)

wg.Add(1)
go func() {
defer wg.Done()
defer cancel()

for {
b := make([]byte, core.MTUSize)
size, _, err := conn.ReadFromUDP(b)
// 处理UDP包 b[:size]
}
}()

select {
case <-ctx.Done():
conn.Close()
}
return
}

如果只是用到HTTP Server,可以这么写:


func run(ctx contex.Context) {
server := &http.Server{Addr: addr, Handler: nil}
go func() {
select {
case <-ctx.Done():
server.Close()
}
}()

http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
})

fmt.Println(server.ListenAndServe())
}

如果需要提供一个API来让服务器退出,可以这么写:


func run(ctx contex.Context) {
server := &http.Server{Addr: addr, Handler: nil}

ctx, cancel := context.WithCancel(ctx)
http.HandleFunc("/quit", func(w http.ResponseWriter, r *http.Request) {
cancel() // 使用局部的ctx和cancel
})

go func() {
select {
case <-ctx.Done():
server.Close()
}
}()

fmt.Println(server.ListenAndServe())
}

使用局部的ctx和cancel,可以避免cancel传入的ctx,只是影响当前的ctx。

GOLANG使用嵌入结构实现接口

技术讨论winlin 发表了文章 • 0 个评论 • 303 次浏览 • 2017-05-19 13:23 • 来自相关话题

考虑一个Packet接口,一般会返回一个Header,例如:

type PacketHeader struct {
    ID uint32
    Timesta... 			查看全部
					

考虑一个Packet接口,一般会返回一个Header,例如:


type PacketHeader struct {
ID uint32
Timestamp uint64
}

type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
Header() *PacketHeader
}

如果是OO的语言,一般会有一个基类,里面包含了Header和实现这个Header:


class BasePacket : public Packet {
protected:
PacketHeader h;
public:
virtual Header() *PacketHeader;
};

class HandshakePacket : public BasePacket {
};

在子类中就都实现了这个Header()方法了,在GOLANG同样可以做到,通过在Header中定义方法,在Packet中包含Header就可以。


func (v *PacketHeader) Header() *PakcetHeader {
return v
}

type HandshakePacket struct {
PacketHeader
}

看起来还差不多的,都可以实现,golang只是代码少一点,清晰一点点而已。考虑要添加一些辅助函数,譬如给Packet添加是否是紧急类型的包,那OO语言得做一次代理:


type Packet interface {
IsErgency() bool
}

class BasePacketHeader {
public:
bool IsErgency() {
return realtime < 3;
}
}

class BasePacket {
public:
bool IsErgency() {
return h.IsErgency();
}
}

而在GOLANG中,只需要在Header实现就好了:


func (v *PacketHeader) IsErgency() bool {
return v.realtime < 3
}

更高级的可以直接嵌入接口。譬如context.Context的实现,cancelCtx直接嵌入了一个接口:


type cancelCtx struct {
Context

通过指定类型,或者初始化的顺序初始化struct


func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}

结构嵌套的方式,让组合实现起来非常便捷,避免频繁的代理。

使用两个context实现CLOSE包的超时等待

技术讨论winlin 发表了文章 • 0 个评论 • 317 次浏览 • 2017-05-18 20:39 • 来自相关话题

在UDP中,一般发送者发送包后,如果一定的时间对方没有收到,就需要重传。例如UDP实现握手的过程,如果握手的包,比如RTMFP协议的IHELLO,发送给对方后,如果一定1秒没有收到,就应该重发一次,然后等3秒、6秒、9秒,如果最后没有收到就是超时了。查看全部

在UDP中,一般发送者发送包后,如果一定的时间对方没有收到,就需要重传。例如UDP实现握手的过程,如果握手的包,比如RTMFP协议的IHELLO,发送给对方后,如果一定1秒没有收到,就应该重发一次,然后等3秒、6秒、9秒,如果最后没有收到就是超时了。


最后一个Close包,发送者不能等待这么长的时间,所以需要设置一个较短的时间做超时退出。一般收发都是一个context,在最后这个Close包时,收到ctx.Done也不能立刻退出,因为还需要稍微等待,譬如600毫秒如果没有收到响应才能退出。


一个可能的实现是这样:


in := make(chan []byte)

func Close(ctx context.Context) (err error) {
timeous := ... // 1s,3s,6s,9s...
for _, to := range timeouts {
// 发送给对方WriteToUDP("CLOSE", peer)
// 另外一个goroutine读取UDP包到in

select {
case <- time.After(to):
case <- in:
fmt.Println("Close ok")
return
case <- ctx.Done():
fmt.Println("Program quit")
return
}
}
return
}

但是这个问题在于,在程序退出时,一般都会cancel ctx然后调用Close方法,这个地方就不会等待任何的超时,就打印"Program quit"然后返回了。解决方案是用另外一个context。但是如何处理之前的ctx的done呢?可以再起一个goroutine做同步:


in := make(chan []byte)

func Close(ctx context.Context) (err error) {
ctxRead,cancelRead := context.WithCancel(context.Background())
go func(){ // sync ctx with ctxRead
select {
case <-ctxRead.Done():
case <-ctx.Done():
select {
case <-ctxRead.Done():
case <-time.After(600*time.Milliseconds):
cancelRead()
}
}
}()

ctx = ctxRead // 下面直接用ctxRead。
timeous := ... // 1s,3s,6s,9s...
for _, to := range timeouts {
// 发送给对方WriteToUDP("CLOSE", peer)
// 另外一个goroutine读取UDP包到in

select {
case <- time.After(to):
case <- in:
fmt.Println("Close ok")
return
case <- ctx.Done():
fmt.Println("Program quit")
return
}
}
return
}

这样在主要的逻辑中,还是只需要处理ctx,但是这个ctx已经是新的context了。不过在实际的过程中,这个sync的goroutine需要确定起来后,才能继续,否则会造成执行顺序不确定:


sc := make(chan bool, 1)
go func(){ // sync ctx with ctxRead
sc <- true
select {
......
}
<- sc

使用context,来控制多个goroutine的执行和取消,是非常好用的,关键可以完全关注业务的逻辑,而不会引入因为ctx取消或者超时机制而造成的特殊逻辑。

GOLANG实现超时对象检测的最好理解的方式

技术讨论winlin 发表了文章 • 0 个评论 • 379 次浏览 • 2017-05-16 17:39 • 来自相关话题

依赖于心跳的系统,都需要超时检测。比如P2P系统中客户端每隔120秒向数据服务器发送一次数据汇总,服务器就需要维护一个超时时间。比如一个UDP服务器,在和客户端之间创建Session之后,如果没有数据包,一般会有Ping包,说明这个Session是存活的... 查看全部

依赖于心跳的系统,都需要超时检测。比如P2P系统中客户端每隔120秒向数据服务器发送一次数据汇总,服务器就需要维护一个超时时间。比如一个UDP服务器,在和客户端之间创建Session之后,如果没有数据包,一般会有Ping包,说明这个Session是存活的,服务器在发现Session超时后也需要清理。


首先,服务器一般需要维护一个列表,以Peer为例:


type Peer struct {
id uint64
heartbeat time.Time
}

type Server struct {
peers map[uint64]*Peer
lock sync.Mutex
}

创建Peer,同时在收到Ping消息后,更新Peer的心跳时间:


func (v *Server) Create(id uint64) *Peer {
v.lock.Lock()
defer v.lock.UnLock()

p = &Peer { id:id, heartbeat: time.Now(), }
v.peers[id] = p
return p
}

func (v *Server) OnPing(id uint64) {
v.lock.Lock()
defer v.lock.UnLock()

if p,ok := v.peers[id]; ok {
p.heatbeat = time.Now()
}
}

当然,需要起一个goroutine定期扫描这个列表, 假设300秒超时:


go func(v *Server) {
for {
func(){
v.lock.Lock()
defer v.lock.UnLock()

now := time.Now()
for id,p := range v.peers {
if p.heartbeat.Add(300 * time.Second).Before(now) {
delete(v.peers, id)
}
}
}()
time.Sleep(30 * time.Second)
}
}(server)

如果Peers的数目非常多,那么扫描时每次都需要锁定v.peers,会导致其他的业务都无法进行。特别是清理Peer这个过程如果比较复杂,譬如需要发起io请求,是一个费时的操作时,就会造成系统的等待。


一般来说,超时的Peer不会很多,因此可以用chan放一个超时的peer,每个peer专门起一个goroutine来看什么时候超时,这样就可以在检测超时时避免用锁了:


timeout := make(chan *Peer)

func (v *Server) Create(id uint64) *Peer {
v.lock.Lock()
defer v.lock.UnLock()

p = &Peer { id:id, heartbeat: time.Now(), }
v.peers[id] = p
return p

go func(p *Peer) {
for {
tm := p.heartbeat
<- time.After(300 * time.Second)
if tm.Equal(p.heartbeat) {
timeout <- p
break
}
}
}(p)
}

go func(v *Server){
for gw := range timeout {
func(){
lgateways.Lock()
defer lgateways.Unlock()

delete(gateways, gw.port)
}()

// Do something cleanup about the gateway.
}
}(server)

这样就只有在有Peer超时时,才真正锁住Server.peers


检测超时时,除了用heartbeat心跳时间,还可以用keepAlive chan bool保活消息来实现:


type Peer struct {
keepAlive chan bool
}

func (v *Server) OnPing(id uint64) {
if p,ok := v.peers[id]; ok {
select {
case p.keepAlive <- true:
default:
}
}
}

这样检测超时也更简单:


go func(p *Peer) {
for {
select {
case <- time.After(300 * time.Second):
timeout <- p
case <- p.keepAlive:
}
}
}(p)

这样就是两个chan联合完成这个任务:


keepAlive => timeout

GOLANG接口适配,组合方式的灵活接口演化

技术讨论winlin 发表了文章 • 0 个评论 • 415 次浏览 • 2017-05-15 20:49 • 来自相关话题

在OO(Object Oriented)原则中,有一条叫做:优先使用组合,而不是继承。虽然GOLANG并不是OO的语言(没有继承和多态),但是不妨碍GOLANG使用这条原则,而GOLANG的作者就强调过这一点,在GOLANG中是使用组合而非继承来扩展。<... 查看全部

在OO(Object Oriented)原则中,有一条叫做:优先使用组合,而不是继承。虽然GOLANG并不是OO的语言(没有继承和多态),但是不妨碍GOLANG使用这条原则,而GOLANG的作者就强调过这一点,在GOLANG中是使用组合而非继承来扩展。


装逼的说来,继承是一种名词化的语言体系,先进行业务抽象然后设计类体系和继承关系。而组合,强制使用接口,因为组合中使用的总是另外一个对象的接口,通过动词的组合,实现目标,比如不管是什么只要有Write([]byte)(int,error)这个动作,就实现了这个接口,其他对象组合这个接口后,对外也看起来就是个io.Writer的接口。


比如,GOALNG1.8支持了writev,一般在面向对象会这么的搞:


class Socket {
int Write(void*, int);
int Writev(const iovec*, int);
};

对的吧?一个Socket可以写数据,也可以用writev写iovec向量,就是一次性写入多个内存块。



Note: 有时候内存块是不连续的,比如一个Video帧,发送给不同的客户端时,Header是需要修改的,但是Payload都一样,那么可以针对每个客户端只创建一个header,然后公用payload,但是这时候两个内存指针是不连续的,特别是需要同时写入多个视频帧时,writev就很神奇的避免了内存拷贝writev(header+payload),具体参考下writev的资料哈。



这样有个问题,并非所有系统都支持Writev的,并非所有Socket都支持Writev的,如果是自己写个代码,当然是可以随便这么搞的,但是作为标准库,GOLANG当然是不能这么做的。GOLANG就加了一个接口(一个新动作)叫做net.buffersWriter,如果实现了这个接口就用writev。先看用法:


    conn,err := net.Dial("tcp", "127.0.0.1:1935")

buffers := Buffers{
[]byte("once upon a time in "),
[]byte("Gopherland ... "),
}

buffers.WriteTo(conn)

在Buffers的WriteTo方法会判断是否是writev的接口,如果是则用writev写,否则就一个个的写:


func (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {
if wv, ok := w.(buffersWriter); ok {
return wv.writeBuffers(v)
}

实际上conn是net.TcpConn,里面有个fd *net.netFD,它实现了net.buffersWriter接口,所以最后调用的就是(fd *netFD) writeBuffers(v *Buffers)


func (c *conn) writeBuffers(v *Buffers) (int64, error) {
n, err := c.fd.writeBuffers(v)

func (fd *netFD) writeBuffers(v *Buffers) (n int64, err error) {
iovecs = append(iovecs, syscall.Iovec{Base: &chunk[0]})
wrote, _, e0 := syscall.Syscall(syscall.SYS_WRITEV,
uintptr(fd.sysfd),
uintptr(unsafe.Pointer(&iovecs[0])),
uintptr(len(iovecs)))

对于其他没有实现这个接口的对象,就每个向量循环的写。


在看一个例子http.Get(url string),客户端发起一个HTTP请求:


http.Get("http://localhost:1985/api/v1/versions")
// 实际上调用的是:
func (c *Client) Get(url string)
// 然后调用:
(c *Client) Do(req *Request)

在GOLANG1.7中引入了context的概念,用来支持cancel,怎么用的:


ctx,cancel := context.WithCancel(context.Background())

select {
case <- ctx.Done():
// Cancelled.
case <- time.After(...):
// Timeout
case <- other events:
// Other events.
}

如何支持取消的HTTP请求呢?给http.Get加个ctx参数?例如http.Get(ctx, url)这样?那改动得多大啊,而且还不能兼容之前的API,泪奔~看看GOLANG的解决:


ctx,cancel := context.WithCancel(context.Background())
go func(){
req,err := http.NewRequest("http://...")
res,err := http.DefaultClient.Do(req.WithContext(ctx))
defer res.Body.Close()
// 读取res响应结果。
}()

select {
case <- ctx.Done():
case <- time.After(3 * time.Second):
cancel() // Timeout to cancel all requests.
}

使用组合,通过req.WithContext再返回一个*http.Request,实现同样的目的。

GOLANG使用简单类型,在协议解析的妙用

技术讨论winlin 发表了文章 • 0 个评论 • 996 次浏览 • 2017-05-11 15:41 • 来自相关话题

在协议解析中,经常需要用到转换不同的含义,比如声音的采样率,在FLV中定义和AAC中定义是不同的。在FLV中只有4中采样率5512, 11025, 22050, 44100。而在AAC中有16种采样率96000, 8... 查看全部

在协议解析中,经常需要用到转换不同的含义,比如声音的采样率,在FLV中定义和AAC中定义是不同的。在FLV中只有4中采样率5512, 11025, 22050, 44100。而在AAC中有16种采样率96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350(还有4个是保留的)。也就是说,1在FLV中标识11025Hz,而在AAC中表示的是88200Hz。如何实现这个转换呢?


C++当然先得定义枚举:


enum SrsAudioSampleRate
{
SrsAudioSampleRate5512 = 0,
SrsAudioSampleRate11025,
SrsAudioSampleRate22050,
SrsAudioSampleRate44100,
SrsAudioSampleRateForbidden,
};

C++当然是用函数了:


SrsAudioSampleRate aac_to_flv(int v) {
if (v >= 0 && v <=5) {
return SrsAudioSampleRate44100;
} else if (v >=6 && v <= 8) {
return SrsAudioSampleRate22050;
} else if (v >= 9 && v <= 11) {
return SrsAudioSampleRate11025;
} else if (v == 12) {
return SrsAudioSampleRate5512;
} else {
return SrsAudioSampleRateForbidden;
}
}

看起来还是挺简单的。慢着,还有的时候需要打印出采样率来,所以还得搞个函数:


string srs_audio_sample_rate2str(SrsAudioSampleRate v)
{
switch (v) {
case SrsAudioSampleRate5512: return "5512";
case SrsAudioSampleRate11025: return "11025";
case SrsAudioSampleRate22050: return "22050";
case SrsAudioSampleRate44100: return "44100";
default: return "Forbidden";
}
}

拿到一个AAC的采样率,然后转换成FLV的,并打印出来,是这么使用的:


// 从文件或者流中读取出AAC的采样率的值。
int samplingFrequencyIndex = ...;
// 转换成FLV的采样率。
SrsAudioSampleRate sampleRate = aac_to_flv(samplingFrequencyIndex);
// 转换成字符串格式。
string sSampleRate = srs_audio_sample_rate2str(sampleRate);
// 打印采样率。
printf("SampleRate=%d/%sHz\n", sampleRate, sSampleRate);

有什么麻烦的呢?



  1. 函数和类型之间没有关系,每次使用的时候都得去翻手册啊翻手册。

  2. 如果定义成一个struct,那转换的时候又太麻烦了。


还能不能愉快的玩耍呢?用GOLANG吧!先看用法:


var sampleRate AudioSamplingRate
sampleRate.From(samplingFrequencyIndex)
fmt.Printf("SampleRate=%d/%v\n", sampleRate, sampleRate)

就是这么简单(此处应该有掌声)~


其实实现起来也非常自然:


type AudioSamplingRate uint8

const (
AudioSamplingRate5kHz AudioSamplingRate = iota // 0 = 5.5 kHz
AudioSamplingRate11kHz // 1 = 11 kHz
AudioSamplingRate22kHz // 2 = 22 kHz
AudioSamplingRate44kHz // 3 = 44 kHz
AudioSamplingRateForbidden
)

func (v AudioSamplingRate) String() string {
switch v {
case AudioSamplingRate5kHz:
return "5.5kHz"
case AudioSamplingRate11kHz:
return "11kHz"
case AudioSamplingRate22kHz:
return "22kHz"
case AudioSamplingRate44kHz:
return "44kHz"
default:
return "Forbidden"
}
}

func (v *AudioSamplingRate) From(a int) {
switch a {
case 0, 1, 2, 3, 4, 5:
*v = AudioSamplingRate44kHz
case 6, 7, 8:
*v = AudioSamplingRate22kHz
case 9, 10, 11:
*v = AudioSamplingRate11kHz
case 12:
*v = AudioSamplingRate5kHz
default:
*v = AudioSamplingRateForbidden
}
}


Remark: 代码参考go-oryx-lib flv.



有几个地方非常不同:



  1. 虽然GOLANG只是在uint8上面加了函数,但是使用起来方便很多了,以前在C++中用这两个枚举,每次都要跳到枚举的定义来看对应的函数是什么。

  2. GOLANG的switch比较强大,可以case好几个值,和C++的if有点想,但是GOLANG的case更直观,知道这几个值会被转换成另外的值,而if读起来像是将一个范围的值转换,不好懂。

  3. GOLANG的枚举使用const实现,也可以带类型,而且有个iota很强大,特别是在定义那些移位的枚举时就很好用。


好吧,这只是几个小的改进,虽然用起来很方便。来看看在AMF0中基本类型的妙用,AMF0是一种传输格式,和JSON很像,不过JSON是文本的,而AMF0是字节的,都是用来在网络中传输对象的。因此,AMF0定义了几个基本的类型:String, Number, Boolean, Object,其中Object的属性定义为String的属性名和值,值可以是其他的类型。


先看看C++的实现,首先定义一个AMF0Any对象,可以转换成具体的String或者Object等对象:


class SrsAmf0Any {
// 提供转换的函数,获取实际的值。
virtual std::string to_str();
virtual bool to_boolean();
virtual double to_number();
virtual SrsAmf0Object* to_object();
// 当然还得提供判断的函数,得知道是什么类型才能转。
virtual bool is_string();
virtual bool is_boolean();
virtual bool is_number();
virtual bool is_object();
// 提供创建基本类型的函数。
static SrsAmf0Any* str(const char* value = NULL);
static SrsAmf0Any* boolean(bool value = false);
static SrsAmf0Any* number(double value = 0.0);
static SrsAmf0Object* object();
};

在实现时,String和Number等基本类型可以隐藏起来(在cpp中实现):


namespace _srs_internal {
class SrsAmf0String : public SrsAmf0Any {
public:
std::string value;
// 当然它必须实现编码和解码的函数。
virtual int total_size();
virtual int read(SrsBuffer* stream);
virtual int write(SrsBuffer* stream);
};
}

AMF0Object当然得暴露出来的:


class SrsAmf0Object : public SrsAmf0Any {
public:
virtual int total_size();
virtual int read(SrsBuffer* stream);
virtual int write(SrsBuffer* stream);
// 提供设置和读取属性的方法。
virtual void set(std::string key, SrsAmf0Any* value);
virtual SrsAmf0Any* get_property(std::string name);
};

用起来是这样:


// 设置Object的属性,并发送给服务器。
SrsConnectAppPacket* pkt = NULL;
pkt->command_object->set("app", SrsAmf0Any::str(app.c_str()));
pkt->command_object->set("tcUrl", SrsAmf0Any::str(tcUrl.c_str()));

// 读取服务器的响应,取出服务器的IP等信息。
SrsConnectAppResPacket* pkt = NULL;
SrsAmf0Any* data = pkt->info->get_property("data");
if (si && data && data->is_object()) {
SrsAmf0Object* obj = data->to_objet();

SrsAmf0Any* prop = obj->get_property("srs_server_ip");
if (prop && prop->is_string()) {
printf("Server IP: %s\n", prop->to_str().c_str());
}

prop = obj->get_property("srs_pid");
if (prop && prop->is_number()) {
printf("Server PID: %d\n, prop->to_number());
}
}

看起来巨繁琐吧?快用GOLANG,如果换成GOLANG,可以用基本类型定义AMF0的基本类型,这样使用起来是这样:


pkt := or.NewConnectAppPacket()
pkt.CommandObject.Set("tcUrl", amf0.NewString(tcUrl))
pkt.CommandObject.Set("app", amf0.NewString(app))

var res *or.ConnectAppResPacket
if data, ok := res.Args.Get("data").(*amf0.Object); ok {
if data, ok := data.Get("srs_server_ip").(*amf0.String); ok {
fmt.Printf("Server IP: %s\n", string(*data))
}
if data, ok := data.Get("srs_pid").(*amf0.Number); ok {
fmt.Printf("Server PID: %d\n, int(*data))
}
}

区别在于:



  1. C++由于不能在基本类型上定义方法,导致必须创建struct或者class类型,有比较繁琐的类型转换和判断。

  2. GOLANG的类型判断,提供了ok的方式,一句话就能把类型转换弄好,而且接口和实现struct的对象可以重用变量名。

  3. 不必加很多类型判断,没有多余的变量,干净利索,需要维护的信息比较少。


实现起来更舒服,基本类型不用定义struct:


type String string
func (v *String) Size() int {}
func (v *String) UnmarshalBinary(data []byte) (err error) {}
func (v *String) MarshalBinary() (data []byte, err error) {}

type Object struct {}
func (v *Object) Size() int {}
func (v *Object) UnmarshalBinary(data []byte) (err error) {}
func (v *Object) MarshalBinary() (data []byte, err error) {}


Remark:代码参考go-oryx-lib amf0.



更神奇的是,因为Object、EcmaArray和StrictArray都是类似的结构,但是有些细微的差异,因此使用GOLANG的结构体嵌套可以很直接的解决问题:


type Object struct {
objectBase
eof objectEOF
}
type EcmaArray struct {
objectBase
count uint32
eof objectEOF
}
type StrictArray struct {
objectBase
count uint32
}

可以对比下SRS的实现,C++可以采用继承,而GOLANG直接组合那些基本的单元。


爱生活,爱够浪(此处可以响起掌声了)~

讨论关于Go和相关生态的空间