接触到GO之后,GO的网络支持非常令人喜欢。GO实现了在语法层面上可以保持同步语义,但是却又没有牺牲太多性能,底层一样使用了IO路径复用,比如在LINUX下用了EPOLL,在WINDOWS下用了IOCP。
但是在开发服务端程序的时候,很多都是被动触发的,都是客户端发送来的请求需要处理。天生就是一个event-based的程序。而在GO下,因为并发是作为语言的一部分,goroutine, channel等特性则很容易的使程序员在实现功能时从容的在同步与异步之间进行转换。
因为自己的需要,我针对event-based场景的服务端做了简易的封装。具体代码见这里.
设计原则因为GO的IO机制和并发原语的原生支持,再加上对网络API的封装,程序员可以简单的实现一个高效的服务端或者客户端程序。一般的实现就是调用net.Listen(“tcp4”, address)得到一个net.Listener,然后无限循环调用net.Listener.Accept,之后就可以得到一个net.Conn,可以调用net.Conn的接口设置发送和接收缓冲区大小,可以设置KEEPALIVE等。因为TCP的双工特性,所以可以针对一个net.Conn可以专门启动一个goroutine去无限循环接收对端发来的数据,然后解包等。
我的想法是在这个简单实现的基础上做一层薄薄的封装,使其尽量的精简,但是又不失灵活。希望能够适应不同的协议,对使用者造成尽量小的约束。
Session对象该对象就是对net.Conn的一个简易封装,可以通过swnet.Server.AcceptLoop得到,也可以通过swnet.NewSession创建新的对象,这种一般是客户端情境下使用。得到Session对象后,可以调用Start方法开始工作。之所以还暴露出一个方法叫Start是因为在服务端下,可能会有某些需求,比如针对IP设置了ACL,那么,把Start行为交给使用者决定如何调用。但是这里需要注意的是,如果使用者不想Start,使用者有责任自己Close掉,否则会造成资源泄露。
Start后,会启动两个goroutine,一个用于专门接收对端发来的数据,一个专门用来发送数据到对端。想发送数据到对端,可以用AsyncSend方法,该方法会把要发送的数据排队到发送通道。这里使用通道的原因是因为在服务端情境下,有必要对发送的数据进行排队,防止发送很快,但是对端接收很慢,或者过多的调用AsyncSend方法,导致堆积了太多的数据,增加了内存的压力。通过channel来控制发送速率我认为是比较合理的。同时,还提供了方法可以用来修改channel的长度,一是调用NewSession时传入指定大小,二是调用Session.SetSendChannelSize设置大小,但是要注意的是,调用此方法时必须在Start之前完成,否则会产生错误。这样做的原因也是因为没必要动态更改发送通道大小。
如果发送channel满了,AsyncSend方法会返回ErrSendChanBlocking。增加这个错误类型也是因为上面的设计导致的。不返回这个错误,就没有办法让使用者得到处理该问题的机会。使用者如果拿到该错误,可以自己试着分析问题的原因,或者可以尝试循环发送,或者直接丢弃该次的发送数据。总之能够让使用者得到自己处理的机会。
如果Session对象已经Close了,那么调用AsyncSend会返回ErrStoped错误。除此之外,因为AsyncSend是把数据排队到发送channel中,那么使用者有责任确保发送的数据在发送完成前不会修改。
如果数据发送失败,或者其他原因,我的实现是直接粗暴的Close掉该Session。
还有就是,可能有些用例情景下,会发送比较大的数据包,比如64K大小,或者32K大小的数据等,未了避免反复申请内存,特此为Session增加了SetSendCallback方法。可以设置一个回调函数,用于在发送完成后可以调用该回调,给予使用者回收数据对象的机会,比如可以配合sync.Pool使用。虽然我自己测试时并没有太大的效果。
为了方便使用者设置一些net.Conn参数,增加了一个RawConn方法,可以获取到net.Conn 的实例。这里其实是挺纠结的。因为暴露出这个内部资源后,会给予使用者一个非常大的灵活度。它可以直接绕过Session的发送channel,自己玩自己的。不过出于方便使用者使用的目的,我还是这么做了。使用者自己承担相应的责任。其实这里还可以像net.HTTP那样增加一个Hijack方法,让使用者自己接管net.Conn,自己玩自己的。
Session中的很多SET/GET方法都是没有加锁的。一方面是因为很多操作在Start前一次完成,或者是GET的数据不是那么紧密的。
有些时候,如果一个Session被关闭了,可能需要知道这个行为。所以提供了SetCloseCallback方法,可以设置该方法。不设置也没有关系。调用closeCallback时会确保只调用一次。
协议序列化抽象因为目标之一就是能够隔离具体协议格式。所以对协议做了抽象。只需要实现PacketProtocol接口即可: