Go语言异步服务器框架原理和实现
2013-08-05 14:58
1096 查看
Go语言类库中,有两个官方的服务器框架,一个HTTP,一个是RPC。使用这个两个框架,已经能解决大部分的问题,但是,也有一些需求,这些框架是不够的,这篇文章,我们先分析一下HTTP和RPC服务器的特点,然后结合这两个服务器的特点,我实现了一个新的服务器,这个服务器非常适合客户端和服务器端有大量交互的情况。
HTTP服务器的特点:
HTTP的请求和响应的周期如下:
对于一个HTTP长连接,一个请求必须等到一个响应完成后,才能进行下一个请求。这就是http协议最本质的特点,是串行化的。而这个特点保证了http协议的简洁性,一个请求中间不会插入其他的请求干扰,这样不需要去对应请求和响应。但是,同时也有个弱点,那就是不适合做大量的请求。举个实际中我们遇到的例子,我们要把大量的中国客户的订单送入英国的交易所,交易所的接口是http协议的,从中国到英国,一次http的请求到响应至少需要300ms左右,这样一秒一个连只能发送3个,就算是开十个线程发送(接口对线程总数是有限制的),1s也只能是30个。而最高峰的时候,我们可能1s要发送1万个订单,那采用http协议就不能满足我们的要求了(这个可以通过fix协议解决)。
当然,http可以解决批量提交的需求,只要增加一个批量提交的接口就可以了。但是,这样的实现方式不够自然,而且增加了额外的接口。
RPC服务的特点:
PRC服务器克服了http服务器串流模型,可以并发的提交请求。请求响应的周期图如下:
RPC服务,已经可以客服http服务器的串流的劣势,可以批量提交大量的数据。在局域网的中测试,1s钟可以实现3万次左右的请求。而相同的条件下,http在局域网中,只能实现1500次左右的请求,真实环境下面,延时严重,http性能会急剧下降。在两个不同的机房中,有百兆带宽相连,实际测试rpc请求是两万次左右,http是500次左右,而且http占用很多头部的带宽。
RPC的一个核心特点是类似一次函数调用。这样一个请求只能对应于一个响应。在某些情下,这似乎是不够的。举个实际的例子,我要获取一个报价的行情数据,这个时候,类似一个MessageQueue,服务器会不断的push数据给客户端。也就是一次请求,会有多次返回,持续不断的返回。
当然,RPC的一个非常重要的优势是,你不需要知道怎么去解析数据,你可以当做网络是空气,完全像写本地调用函数一样去调用rpc的函数。
异步服务器:
因为暂时我没有很好的名字来命名这个服务器,所以暂时就叫做异步服务器吧,这个服务器的特点类似一个界面程序的消息体系。我们不断的吧鼠标键盘等各种事件提交给界面程序,界面程序根据消息的类型,参数做出相应的处理。所以,我们就叫做异步服务器吧。经典的金融服务器都是异步服务器,处理机制都类似界面的消息循环机制,比如国内期货最常用的ctp交易系统,还有就是银行间,交易所和银行之间,经常用的一个协议叫做fix,也是这样的架构。请求是一种消息,响应也是一种消息。请求响应的时序图如下:
msg1请求之后,有两个响应,Resp1,resp2,
msg2有一个响应resp3.
借鉴了rpc的特点,请求和响应都自动编码,写服务器不再为编码而烦恼,同时也不需要为是否要压缩而头痛。现在提供三种方式,gob,json,protocolbuffer.并且可以设置是否启用压缩的,以及压缩的格式。我
们把客户端和服务器的交互抽象为一个消息系统,先来看看客户端客户端调用:
[/code]
1-6行,我们建立了一个到服务器的连接,注意,我们这个服务器底层是用http包实现的。jar是用来管理session的,这里暂时忽略,gob是编码,gzip是压缩格式。可以动态设置各种编码和压缩格式。
7-13行,NewRequest的第一个参数是消息的类型(我建议再后面的版本中,改成NewMessage,Client.GO改成client.Send),叫做hello,详细类型为了方便查看也打印,我采用字符串的格式。后面是消息的参数,可以是任何的go的结构,变量。每个请求对应一个回调函数,处理响应的消息,响应的消息保存在call.Resp里面,如果status==StatusDone,表示请求结束了,服务器不会响应任何消息了,status==StatusUpdate,说明,还会有下一个消息过来。
16行Wait函数,其实就是一个消息循环函数,不断的从服务器端读取消息,对应到某个请求的回调函数里面。类似eventloop
我们在Client里面加入心跳函数,保证能检查到链接损坏的情况,如果连接损坏,会自动结束消息循环,错误处理是一个服务器非常重要的一环。
然后我们再来看看服务器端的实现:
[/code]
第7行中,r.GetBody()获取的到是上面NewRequest中的第二个参数。
这样就是一个最简单的helloworld程序。要实现一个实战有用的服务器,的细节当然还有很多,主要的是流量控制。比如,一个用户写错程序了,错误的发起了10万个请求,服务器端不能开个10万个go进行处理,这样的话,会直接拖垮服务器,我们给每个用户设置了一个并发处理数目,最多这个用户可以并发处理多少个请求。还有一个比较重要的,对服务器来说,就是服务器服务的量的限制。我们会实时监控cpu内存,io的使用情况,当发现使用到某个限额的时候,服务会拒绝接受连接(事先要对性能进行测试)这些都是为了防止服务器过载,而实际中的服务器,这个问题其实是很常见的。
实例:可靠消息通知系统。
可靠消息通知系统实际上是一个非常常见的系统。最常用的一个例子就是数据库的masterslave模式。master里面的事件要非常可靠的通知到slave,中间不能有任何的丢失。还有一种比如交易系统中,我们会调用银行或者交易所的接口,银行在交易成功后会给我们一个通知,这个通知的消息必须可靠的被通知到目标,不能有任何的丢失。在我们的系统中,行情数据的复制也是不能有任何数据丢失的情景,为了保证A服务器和B服务器有相同的行情,在从A服务器的消息要被B服务器准确的接收。当然,你也可以做一个聊天系统,这个聊天系统不会丢失任何消息。
那么如何实现这个系统呢,首先,为了保证不在内存中丢失消息,那么消息必须写盘,并且为了检测消息是否丢失,必须给消息编号。消息写盘也可以用我们开发的事务日志系统,如果消息非常的大量,那么还需要批量提交模式(GroupCommit)。大部分情况下,消息丢失不是因为服务器崩溃,而且网络意外中断,这些中断往往时间很短,在1分钟以内,所以,有必要在内存中缓存部分的消息,如果网络中断,客户端再次请求时,发送当时的消息序号,这样就可以补全网络中断丢失的数据。如果时间太长了,内存中的数据不够补了,那么首先要从消息源数据库中下载历史消息,然后再接受实时的消息。整体的思路就是这样的,在这里,我们就看看我们的消息通知系统的实时广播部分的设计。
1.消息广播基本流程:订阅–>广播:
首先客户端向服务器说明,我要订阅哪些消息,比如,masterslave中,我只要写消息就好了,读消息就不需要了。然后,再向服务器请求数据,服务器广播数据给我们。注意,我们这里把订阅和广播分成两个部分,两个请求,那么怎么知道这两个请求是同一个人发出的呢?或者,怎么关联起来呢?这里,我用了一个session的概念,订阅的时候,把订阅的消息类型保存到session,广播的时候,从session中读取消息类型,然后发送对应的数据。
这部分的代码如下:
[/code]
这里,有个数据结构叫做RingBuffer,是一个环状的buffer,非常适合做缓存固定数目的数据,用于广播。广播是用管道来传输数据的,管道的性能实际上已经非常的高,不需要什么无锁队列之类的。在这里也给管道加上buffer使得,消息意外的扰动,不会使得带宽不够用而立马堵塞。
2.接受消息:
在用户登录后,如果有权限,那么就可以作为消息源客户端,消息源的代码如下:
[/code]
第2行:从请求中获取消息事件。
第3行:event.InstrumentId是消息的类型,btickSzie是缓存的数据数目。
第6行:向客户端发送OK,确认消息发送成功。
每个消息是否发送成功,都有确认。这样,客户端就知道上次消息发送到哪里了。
3.订阅:
[/code]
第2行:获取消息的类型,通过这个类型,可以找到对应的广播对象。
第12-30行:这是一个线程安全的session操作,具体看一下session.Get3的实现就知道了:
[/code]
s.get获取session的数据,如果没有session数据,那么为nil。简单的说,这里的意思是:如果session“subscribe”如果还没有设置,那么就新建一个对象,如果已经设置了,那么读取这个对象,并且,这个操作是线程安全的。
这里还添加了一个session撤销时候的操作。
4.广播:
[/code]
read有个depth参数,这是行情的深度。股票期货里面都有后这个概念。传说中的几档行情。
第26行:这里有个close。一般来说,是因为网络拥堵或者异常,无法发送数据了。
还有一点要注意,这里的行情是批量发送的。sub.Read尽可能多的读取数据,减少网络io的次数。
当然,服务器框架本身提供了心跳机制,对消息广播系统,实时性是非常重要的,即时的检查出网络异常,才能保证实时性。
以上是对我们的异步消息服务器框架的一个简单的介绍。设计这框架,非常重要的两个理念:
1.模块化的设计,一个功能,就对应一个函数。
2.模块之间的通讯采用session,而对于比较复杂的通讯,可以自己建立一个线程安全的数据结构,比如这里的Broadcast和Subscribe
HTTP服务器的特点:
HTTP的请求和响应的周期如下:
对于一个HTTP长连接,一个请求必须等到一个响应完成后,才能进行下一个请求。这就是http协议最本质的特点,是串行化的。而这个特点保证了http协议的简洁性,一个请求中间不会插入其他的请求干扰,这样不需要去对应请求和响应。但是,同时也有个弱点,那就是不适合做大量的请求。举个实际中我们遇到的例子,我们要把大量的中国客户的订单送入英国的交易所,交易所的接口是http协议的,从中国到英国,一次http的请求到响应至少需要300ms左右,这样一秒一个连只能发送3个,就算是开十个线程发送(接口对线程总数是有限制的),1s也只能是30个。而最高峰的时候,我们可能1s要发送1万个订单,那采用http协议就不能满足我们的要求了(这个可以通过fix协议解决)。
当然,http可以解决批量提交的需求,只要增加一个批量提交的接口就可以了。但是,这样的实现方式不够自然,而且增加了额外的接口。
RPC服务的特点:
PRC服务器克服了http服务器串流模型,可以并发的提交请求。请求响应的周期图如下:
RPC服务,已经可以客服http服务器的串流的劣势,可以批量提交大量的数据。在局域网的中测试,1s钟可以实现3万次左右的请求。而相同的条件下,http在局域网中,只能实现1500次左右的请求,真实环境下面,延时严重,http性能会急剧下降。在两个不同的机房中,有百兆带宽相连,实际测试rpc请求是两万次左右,http是500次左右,而且http占用很多头部的带宽。
RPC的一个核心特点是类似一次函数调用。这样一个请求只能对应于一个响应。在某些情下,这似乎是不够的。举个实际的例子,我要获取一个报价的行情数据,这个时候,类似一个MessageQueue,服务器会不断的push数据给客户端。也就是一次请求,会有多次返回,持续不断的返回。
当然,RPC的一个非常重要的优势是,你不需要知道怎么去解析数据,你可以当做网络是空气,完全像写本地调用函数一样去调用rpc的函数。
异步服务器:
因为暂时我没有很好的名字来命名这个服务器,所以暂时就叫做异步服务器吧,这个服务器的特点类似一个界面程序的消息体系。我们不断的吧鼠标键盘等各种事件提交给界面程序,界面程序根据消息的类型,参数做出相应的处理。所以,我们就叫做异步服务器吧。经典的金融服务器都是异步服务器,处理机制都类似界面的消息循环机制,比如国内期货最常用的ctp交易系统,还有就是银行间,交易所和银行之间,经常用的一个协议叫做fix,也是这样的架构。请求是一种消息,响应也是一种消息。请求响应的时序图如下:
msg1请求之后,有两个响应,Resp1,resp2,
msg2有一个响应resp3.
借鉴了rpc的特点,请求和响应都自动编码,写服务器不再为编码而烦恼,同时也不需要为是否要压缩而头痛。现在提供三种方式,gob,json,protocolbuffer.并且可以设置是否启用压缩的,以及压缩的格式。我
们把客户端和服务器的交互抽象为一个消息系统,先来看看客户端客户端调用:
[code]client,err:=NewClient("http://localhost:8080",jar,"gob","gzip")
iferr!=nil{
log.Println(err)
return
}
deferclient.Close()
req:=NewRequest("hello","jack",func(call*Call,statusint){
log.Println(call,call.Resp,status)
})
client.Go(req)
req2:=NewRequest("hello","fuck",func(call*Call,statusint){
log.Println(call,call.Resp,status)
})
client.Go(req2)
//waitforallreqisdone
client.Wait()
[/code]
1-6行,我们建立了一个到服务器的连接,注意,我们这个服务器底层是用http包实现的。jar是用来管理session的,这里暂时忽略,gob是编码,gzip是压缩格式。可以动态设置各种编码和压缩格式。
7-13行,NewRequest的第一个参数是消息的类型(我建议再后面的版本中,改成NewMessage,Client.GO改成client.Send),叫做hello,详细类型为了方便查看也打印,我采用字符串的格式。后面是消息的参数,可以是任何的go的结构,变量。每个请求对应一个回调函数,处理响应的消息,响应的消息保存在call.Resp里面,如果status==StatusDone,表示请求结束了,服务器不会响应任何消息了,status==StatusUpdate,说明,还会有下一个消息过来。
16行Wait函数,其实就是一个消息循环函数,不断的从服务器端读取消息,对应到某个请求的回调函数里面。类似eventloop
我们在Client里面加入心跳函数,保证能检查到链接损坏的情况,如果连接损坏,会自动结束消息循环,错误处理是一个服务器非常重要的一环。
然后我们再来看看服务器端的实现:
[code]funchelloWorld(w*ResponseWriter,r*Request){
resp:=w.Resp
resp.MsgType=MsgTString
//表示我已经没有其他数据包了,这个请求已经结束了
resp.Done=true
//向客户端发送请求
w.WriteResponse(resp,"hello:"+r.GetBody().(string))
}
[/code]
第7行中,r.GetBody()获取的到是上面NewRequest中的第二个参数。
这样就是一个最简单的helloworld程序。要实现一个实战有用的服务器,的细节当然还有很多,主要的是流量控制。比如,一个用户写错程序了,错误的发起了10万个请求,服务器端不能开个10万个go进行处理,这样的话,会直接拖垮服务器,我们给每个用户设置了一个并发处理数目,最多这个用户可以并发处理多少个请求。还有一个比较重要的,对服务器来说,就是服务器服务的量的限制。我们会实时监控cpu内存,io的使用情况,当发现使用到某个限额的时候,服务会拒绝接受连接(事先要对性能进行测试)这些都是为了防止服务器过载,而实际中的服务器,这个问题其实是很常见的。
实例:可靠消息通知系统。
可靠消息通知系统实际上是一个非常常见的系统。最常用的一个例子就是数据库的masterslave模式。master里面的事件要非常可靠的通知到slave,中间不能有任何的丢失。还有一种比如交易系统中,我们会调用银行或者交易所的接口,银行在交易成功后会给我们一个通知,这个通知的消息必须可靠的被通知到目标,不能有任何的丢失。在我们的系统中,行情数据的复制也是不能有任何数据丢失的情景,为了保证A服务器和B服务器有相同的行情,在从A服务器的消息要被B服务器准确的接收。当然,你也可以做一个聊天系统,这个聊天系统不会丢失任何消息。
那么如何实现这个系统呢,首先,为了保证不在内存中丢失消息,那么消息必须写盘,并且为了检测消息是否丢失,必须给消息编号。消息写盘也可以用我们开发的事务日志系统,如果消息非常的大量,那么还需要批量提交模式(GroupCommit)。大部分情况下,消息丢失不是因为服务器崩溃,而且网络意外中断,这些中断往往时间很短,在1分钟以内,所以,有必要在内存中缓存部分的消息,如果网络中断,客户端再次请求时,发送当时的消息序号,这样就可以补全网络中断丢失的数据。如果时间太长了,内存中的数据不够补了,那么首先要从消息源数据库中下载历史消息,然后再接受实时的消息。整体的思路就是这样的,在这里,我们就看看我们的消息通知系统的实时广播部分的设计。
1.消息广播基本流程:订阅–>广播:
首先客户端向服务器说明,我要订阅哪些消息,比如,masterslave中,我只要写消息就好了,读消息就不需要了。然后,再向服务器请求数据,服务器广播数据给我们。注意,我们这里把订阅和广播分成两个部分,两个请求,那么怎么知道这两个请求是同一个人发出的呢?或者,怎么关联起来呢?这里,我用了一个session的概念,订阅的时候,把订阅的消息类型保存到session,广播的时候,从session中读取消息类型,然后发送对应的数据。
这部分的代码如下:
[code]varbmusync.Mutex
vardefaultBroadcast=make(map[int64]*Broadcast)
varErrNotRingItemer=errors.New("ErrNotRingItemer")
//基本上可以保证有1个小时的数据
constbtickSize=3600*4
//可以传递任意的数据
funcGetBroadcast(nameint64,nint)(*Broadcast,error){
bmu.Lock()
deferbmu.Unlock()
b,ok:=defaultBroadcast[name]
ifok{
returnb,nil
}
b,err:=NewBroadcast(name,n)
iferr!=nil{
returnnil,err
}
defaultBroadcast[name]=b
returnb,nil
}
typeBroadcaststruct{
musync.RWMutex
targetsmap[int64]*Subscribe
ringbuffer*algo.RingBuffer
nameint64
}
funcNewBroadcast(nameint64,nint)(*Broadcast,error){
b:=&Broadcast{}
b.targets=make(map[int64]*Subscribe)
b.ringbuffer=algo.NewRingBuffer(n,nil)
b.name=name
returnb,nil
}
func(b*Broadcast)GetName()int64{
returnb.name
}
func(b*Broadcast)Sub(idint64,req*Subscribe){
b.mu.Lock()
deferb.mu.Unlock()
b.targets[id]=req
}
func(b*Broadcast)Unsub(idint64){
b.mu.Lock()
deferb.mu.Unlock()
delete(b.targets,id)
}
//是否在buffer内部
func(b*Broadcast)InBuffer(startint64,endint64)(bool,error){
returnb.ringbuffer.InBuffer(start,end)
}
func(b*Broadcast)Query(startint64,endint64,tyint64)(algo.Iterator,error){
find:=&algo.RingFind{start,end,ty}
returnb.ringbuffer.Find(find,true)//模糊查找,不是精确匹配
}
//如果要提供查询功能,那么就要缓存数据,一般采用ringbuffer
//data要满足下面的条件:
//1.存在一个递增着的ID
//2.实现BufferItemer接口
func(b*Broadcast)Push(itemalgo.RingItemer)error{
b.mu.RLock()
deferb.mu.RUnlock()
item2,err:=b.ringbuffer.Push(item)
iferr!=nil{
returnerr
}
for_,v:=rangeb.targets{
//过滤不想发送的
if(v.Check(b.name,item2.Type)){
v.Send(item)
}
}
returnnil
}
func(b*Broadcast)Find(find*algo.RingFind)(algo.Iterator,error){
returnb.ringbuffer.Find(find,true)
}
typeSubscribestruct{
musync.Mutex
chchaninterface{}
tysmap[int64]int64
}
funcNewSubscribe(nint)(*Subscribe){
s:=&Subscribe{}
s.ch=make(chaninterface{},n)
s.tys=make(map[int64]int64)
returns
}
func(s*Subscribe)Add(bnameint64,tyint64){
s.mu.Lock()
defers.mu.Unlock()
s.tys[bname]=ty
}
func(s*Subscribe)Check(bnameint64,datatyint64)bool{
s.mu.Lock()
defers.mu.Unlock()
ty,ok:=s.tys[bname]
if!ok{//没有订阅
returnfalse
}
ifty==algo.AnyType||dataty==ty{
returntrue
}
returnfalse
}
func(s*Subscribe)Read(buf[]interface{})(int){
vari=1
buf[0]=<-s.ch
for{
ifi==len(buf){
returni
}
select{
casedata:=<-s.ch:
buf[i]=data
i++
default:
returni
}
}
panic("nerverreach")
}
func(s*Subscribe)Send(datainterface{}){
select{
cases.ch<-data:
default:
//清除旧的数据
s.Clear()
//发送结束标志位
s.ch<-nil
}
}
func(s*Subscribe)Clear(){
for{
select{
case<-s.ch:
default:
return
}
}
}
[/code]
这里,有个数据结构叫做RingBuffer,是一个环状的buffer,非常适合做缓存固定数目的数据,用于广播。广播是用管道来传输数据的,管道的性能实际上已经非常的高,不需要什么无锁队列之类的。在这里也给管道加上buffer使得,消息意外的扰动,不会使得带宽不够用而立马堵塞。
2.接受消息:
在用户登录后,如果有权限,那么就可以作为消息源客户端,消息源的代码如下:
[code]funcpushTick(w*asyn.ResponseWriter,r*asyn.Request){
event:=r.GetBody().(*response.OrderBookEvent)
b,_:=GetBroadcast(event.InstrumentId,btickSize)
b.Push(event)
asyn.Log().Println(event)
asyn.OKHandle(w,r)
}
[/code]
第2行:从请求中获取消息事件。
第3行:event.InstrumentId是消息的类型,btickSzie是缓存的数据数目。
第6行:向客户端发送OK,确认消息发送成功。
每个消息是否发送成功,都有确认。这样,客户端就知道上次消息发送到哪里了。
3.订阅:
[code]funcsubscribe(w*asyn.ResponseWriter,r*asyn.Request){
instId:=r.GetBody().(int64)
log.Println("sub",instId)
b,err:=GetBroadcast(instId,btickSize)
iferr!=nil{
r.SetErr(err)
asyn.ErrorHandle(w,r)
return
}
//订阅的size
//getandset要成为一个原子操作
session:=r.GetSession()
session.Get3("subscribe",func(datainterface{})interface{}{
ifdata==nil{
data=NewSubscribe(4096)
}
sub:=data.(*Subscribe)
//广播,类型
id:=int64(uintptr(unsafe.Pointer(session)))
sub.Add(instId,algo.AnyType)
b.Sub(id,sub)
session.OnDelete(func(){
b.Unsub(id)
})
returnsub
})
asyn.OKHandle(w,r)
}
[/code]
第2行:获取消息的类型,通过这个类型,可以找到对应的广播对象。
第12-30行:这是一个线程安全的session操作,具体看一下session.Get3的实现就知道了:
[code]func(s*Session)Get3(namestring,callbackfunc(interface{})interface{})interface{}{
s.mu.Lock()
defers.mu.Unlock()
data,err:=s.get(name)
iferr!=nil{
data=nil
}
data=callback(data)
s.set(name,data)
returndata
}
[/code]
s.get获取session的数据,如果没有session数据,那么为nil。简单的说,这里的意思是:如果session“subscribe”如果还没有设置,那么就新建一个对象,如果已经设置了,那么读取这个对象,并且,这个操作是线程安全的。
这里还添加了一个session撤销时候的操作。
4.广播:
[code]//读取广播数据
funcread(w*asyn.ResponseWriter,r*asyn.Request){
session:=r.GetSession()
//从session中获取subscribe对象
sub:=session.Get3("subscribe",func(datainterface{})interface{}{
ifdata==nil{
data=NewSubscribe(4096)
}
returndata
}).(*Subscribe)
depth:=r.GetBody().(int)
log.Println("getsubscribe")
resp:=w.Resp
ifdepth==0{
resp.MsgType="ticks"
}else{
resp.MsgType="ticks1"
}
buf:=make([]interface{},1024)
dg:=make([]*response.OrderBookEvent,1024)
tick1:=make([]*base.TickGo,1024)
for{
n:=sub.Read(buf)
fori:=0;i<n;i++{
ifbuf[i]==nil{
//closebybroadcast
r.SetErr(errors.New("501"))
asyn.ErrorHandle(w,r)
return
}
ifdepth==0{
dg[i]=buf[i].(*response.OrderBookEvent)
}else{
tick1[i]=buf[i].(*response.OrderBookEvent).ToTickGo()
}
}
varerrerror
ifdepth==0{
err=w.WriteResponse(resp,dg[:n])
}else{
err=w.WriteResponse(resp,tick1[:n])
}
iferr!=nil{
r.SetErr(err)
asyn.ErrorHandle(w,r)
return
}
}
}
[/code]
read有个depth参数,这是行情的深度。股票期货里面都有后这个概念。传说中的几档行情。
第26行:这里有个close。一般来说,是因为网络拥堵或者异常,无法发送数据了。
还有一点要注意,这里的行情是批量发送的。sub.Read尽可能多的读取数据,减少网络io的次数。
当然,服务器框架本身提供了心跳机制,对消息广播系统,实时性是非常重要的,即时的检查出网络异常,才能保证实时性。
以上是对我们的异步消息服务器框架的一个简单的介绍。设计这框架,非常重要的两个理念:
1.模块化的设计,一个功能,就对应一个函数。
2.模块之间的通讯采用session,而对于比较复杂的通讯,可以自己建立一个线程安全的数据结构,比如这里的Broadcast和Subscribe
相关文章推荐
- 一个不错的Go语言实现的web框架
- Go语言服务器开发之简易TCP客户端与服务端实现方法
- 【Go】Go语言中反射包的实现原理(The Laws of Reflection)
- Golang语言社区--游戏服务器框架 Leaf/go 分析
- Go语言实现简单的文件服务器
- Go语言实现的简易TCP通信框架
- go 语言实现一个简单的 web 服务器
- Go语言(服务器开发):实现最简单的HTTP GET/POST接口
- go语言的官方包sync.Pool的实现原理和适用场景
- Go语言实现的一个简单Web服务器
- go实现一个简单的游戏服务器框架(lotou)起源
- 使用 Go 语言实现优雅的服务器重启
- Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架
- go语言实现聊天服务器的示例代码
- go实现一个简单的游戏服务器框架(lotou)基本设计
- [android] 异步http框架与实现原理
- Android 异步Http框架简介和实现原理
- Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架
- Go语言中反射包的实现原理(The Laws of Reflection)
- 简单介绍Python的Tornado框架中的协程异步实现原理