一. 什么是Tao
Tao,在英文中的意思是“The ultimate principle of universe”,即“道”,它是宇宙的終極奧義。
“道生一,一生二,二生三,三生無窮。” ——《道德經(jīng)》
Tao同時也是我用Go語言開發(fā)的一個異步的TCP服務(wù)器框架(TCP Asynchronous Go server FramewOrk),秉承Go語言“Less is more”的極簡主義哲學(xué),它能穿透一切表象,帶你一窺網(wǎng)絡(luò)編程的世界,讓你從此徹底擺脫只會寫“socket-bind-listen-accept”的窘境。本文將簡單討論一下這個框架的設(shè)計思路以及自己的一些思考。
1. Tao解決什么問題
1.1 場景
你開發(fā)的產(chǎn)品有一套特有的業(yè)務(wù)邏輯,要通過互聯(lián)網(wǎng)得到服務(wù)端的支持才能為你的客戶提供服務(wù)。
1.2 問題
怎樣快速穩(wěn)定地實現(xiàn)產(chǎn)品的功能,而不需要耗費大量的時間處理各種底層的網(wǎng)絡(luò)通信細節(jié)。
1.3 解決方案
Tao提供了一種用框架支撐業(yè)務(wù)邏輯的機制。你只需要與客戶端定義好消息格式,然后將對應(yīng)的業(yè)務(wù)邏輯編寫成函數(shù)注冊到框架中就可以了。
2. 50行啟動一個聊天服務(wù)器
讓我們舉一個例子來看看如何使用Tao框架實現(xiàn)一個簡單的群聊天服務(wù)器。服務(wù)器端代碼可以這么寫:
package main
import (
"fmt"
"net"
"github.com/leesper/holmes"
"github.com/leesper/tao"
"github.com/leesper/tao/examples/chat"
)
// ChatServer is the chatting server.
type ChatServer struct {
*tao.Server
}
// NewChatServer returns a ChatServer.
func NewChatServer() *ChatServer {
onConnectOption := tao.OnConnectOption(func(conn tao.WriteCloser) bool {
holmes.Infoln("on connect")
return true
})
onErrorOption := tao.OnErrorOption(func(conn tao.WriteCloser) {
holmes.Infoln("on error")
})
onCloseOption := tao.OnCloseOption(func(conn tao.WriteCloser) {
holmes.Infoln("close chat client")
})
return &ChatServer{
tao.NewServer(onConnectOption, onErrorOption, onCloseOption),
}
}
func main() {
defer holmes.Start().Stop()
tao.Register(chat.ChatMessage, chat.DeserializeMessage, chat.ProcessMessage)
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "0.0.0.0", 12345))
if err != nil {
holmes.Fatalln("listen error", err)
}
chatServer := NewChatServer()
err = chatServer.Start(l)
if err != nil {
holmes.Fatalln("start error", err)
}
}
啟動一個服務(wù)器只需要三步就能完成。首先注冊消息和業(yè)務(wù)邏輯回調(diào),其次填入IP地址和端口,最后Start一下就可以了。這時候客戶端就能夠發(fā)起連接,并開始聊天。業(yè)務(wù)邏輯的實現(xiàn)很簡單,遍歷所有的連接,然后發(fā)送數(shù)據(jù):
// ProcessMessage handles the Message logic.
func ProcessMessage(ctx context.Context, conn tao.WriteCloser) {
holmes.Infof("ProcessMessage")
s, ok := tao.ServerFromContext(ctx)
if ok {
msg := tao.MessageFromContext(ctx)
s.Broadcast(msg)
}
}
3. Go語言的編程哲學(xué)
Go語言是“云計算時代的C語言”,適用于開發(fā)基礎(chǔ)性服務(wù),比如服務(wù)器。它語法類似C語言且標(biāo)準(zhǔn)庫豐富,上手較快,所以開發(fā)效率高;編譯速度快,運行效率接近C,所以運行效率高。
3.1 面向?qū)ο缶幊?/h3>
Go語言面向?qū)ο缶幊痰娘L(fēng)格是“多用組合,少用繼承”,以匿名嵌入的方式實現(xiàn)繼承。比如上面的聊天服務(wù)器ChatServer:
// ChatServer is the chatting server.
type ChatServer struct {
*tao.Server
}
于是ChatServer就自動繼承了Server所有的屬性和方法。當(dāng)然,這里是以指針的方式嵌入的。
3.2 面向接口編程
Go語言的面向接口編程是“鴨子類型”的,即“如果我走起來像鴨子,叫起來像鴨子,那么我就是一只鴨子”。其他的編程語言需要顯示地說明自己繼承某個接口,Go語言卻采取的是“隱式聲明”的方式。比如Tao框架使用的多線程日志庫Holmes實現(xiàn)“每小時創(chuàng)建一個新日志文件”功能的核心代碼如下:
func (ls *logSegment)Write(p []byte) (n int, err error) {
if ls.timeToCreate != nil && ls.logFile != os.Stdout && ls.logFile != os.Stderr {
select {
case current := <-ls.timeToCreate:
ls.logFile.Close()
ls.logFile = nil
name := getLogFileName(current)
ls.logFile, err = os.Create(path.Join(ls.logPath, name))
if err != nil {
fmt.Fprintln(os.Stderr, err)
ls.logFile = os.Stderr
} else {
next := current.Truncate(ls.unit).Add(ls.unit)
ls.timeToCreate = time.After(next.Sub(time.Now()))
}
default:
// do nothing
}
}
return ls.logFile.Write(p)
}
而標(biāo)準(zhǔn)庫中的io.Writer定義如下,那么這里的logSegment就實現(xiàn)了io.Writer的接口,所有以io.Writer作為形參的函數(shù),我都可以傳一個logSegment的實參進去。
type Writer interface {
Write(p []byte) (n int, err error)
}
3.3 一個中心,兩個基本點
掌握Go語言,要把握“一個中心,兩個基本點”。“一個中心”是Go語言并發(fā)模型,即“不要通過共享內(nèi)存來通信,要通過通信來共享內(nèi)存”;“兩個基本點”是Go語言的并發(fā)模型的兩大基石:channel和go-routine。理解了它們就能看懂大部分代碼。下面讓我們正式開始介紹Tao框架吧。
二. Tao的設(shè)計思路
1. 服務(wù)器的啟動
Tao框架支持通過tao.TLSCredsOption()函數(shù)提供傳輸層安全的TLS Server。服務(wù)器的核心職責(zé)是“監(jiān)聽并接受客戶端連接”。每個進程能夠打開的文件描述符是有限制的,所以它還需要限制最大并發(fā)連接數(shù),關(guān)鍵代碼如下:
// Start starts the TCP server, accepting new clients and creating service
// go-routine for each. The service go-routines read messages and then call
// the registered handlers to handle them. Start returns when failed with fatal
// errors, the listener willl be closed when returned.
func (s *Server) Start(l net.Listener) error {
s.mu.Lock()
if s.lis == nil {
s.mu.Unlock()
l.Close()
return ErrServerClosed
}
s.lis[l] = true
s.mu.Unlock()
defer func() {
s.mu.Lock()
if s.lis != nil && s.lis[l] {
l.Close()
delete(s.lis, l)
}
s.mu.Unlock()
}()
holmes.Infof("server start, net %s addr %s\n", l.Addr().Network(), l.Addr().String())
s.wg.Add(1)
go s.timeOutLoop()
var tempDelay time.Duration
for {
rawConn, err := l.Accept()
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay >= max {
tempDelay = max
}
holmes.Errorf("accept error %v, retrying in %d\n", err, tempDelay)
select {
case <-time.After(tempDelay):
case <-s.ctx.Done():
}
continue
}
return err
}
tempDelay = 0
// how many connections do we have ?
sz := s.conns.Size()
if sz >= MaxConnections {
holmes.Warnf("max connections size %d, refuse\n", sz)
rawConn.Close()
continue
}
if s.opts.tlsCfg != nil {
rawConn = tls.Server(rawConn, s.opts.tlsCfg)
}
netid := netIdentifier.GetAndIncrement()
sc := NewServerConn(netid, s, rawConn)
sc.SetName(sc.rawConn.RemoteAddr().String())
s.mu.Lock()
if s.sched != nil {
sc.RunEvery(s.interv, s.sched)
}
s.mu.Unlock()
s.conns.Put(netid, sc)
addTotalConn(1)
s.wg.Add(1)
go func() {
sc.Start()
}()
holmes.Infof("accepted client %s, id %d, total %d\n", sc.GetName(), netid, s.conns.Size())
s.conns.RLock()
for _, c := range s.conns.m {
holmes.Infof("client %s\n", c.GetName())
}
s.conns.RUnlock()
} // for loop
}
如果服務(wù)器在接受客戶端連接請求的時候發(fā)生了臨時錯誤,那么服務(wù)器將等待最多1秒的時間再重新嘗試接受請求,如果現(xiàn)有的連接數(shù)超過了MaxConnections(默認1000),就拒絕并關(guān)閉連接,否則啟動一個新的連接開始工作。
2. 服務(wù)器的優(yōu)雅關(guān)閉
Go語言在發(fā)布1.7版時在標(biāo)準(zhǔn)庫中引入了context包。context包提供的Context結(jié)構(gòu)能夠在服務(wù)器,網(wǎng)絡(luò)連接以及各相關(guān)線程之間建立一種相關(guān)聯(lián)的“上下文”關(guān)系。這種上下文關(guān)系包含的信息是與某次網(wǎng)絡(luò)請求有關(guān)的(request scoped),因此與該請求有關(guān)的所有Go線程都能安全地訪問這個上下文結(jié)構(gòu),讀取或者寫入與上下文有關(guān)的數(shù)據(jù)。比如handleLoop線程會將某個網(wǎng)絡(luò)連接的net ID以及message打包到上下文結(jié)構(gòu)中,然后連同handler函數(shù)一起交給工作者線程去處理:
// handleLoop() - put handler or timeout callback into worker go-routines
func handleLoop(c WriteCloser, wg *sync.WaitGroup) {
//... omitted ...
for {
select {
//... omitted ...
case msgHandler := <-handlerCh:
msg, handler := msgHandler.message, msgHandler.handler
if handler != nil {
if askForWorker {
WorkerPoolInstance().Put(netID, func() {
handler(NewContextWithNetID(NewContextWithMessage(ctx, msg), netID), c)
})
}
}
//... omitted ...
}
}
隨后,在工作者線程真正執(zhí)行時,業(yè)務(wù)邏輯代碼就能在handler函數(shù)中獲取到message或者net ID,這些都是與本次請求有關(guān)的上下文數(shù)據(jù),比如一個典型的echo server就會這樣處理:
// ProcessMessage process the logic of echo message.
func ProcessMessage(ctx context.Context, conn tao.WriteCloser) {
msg := tao.MessageFromContext(ctx).(Message)
holmes.Infof("receving message %s\n", msg.Content)
conn.Write(msg)
}
使用context的另外一個場景是實現(xiàn)服務(wù)器及網(wǎng)絡(luò)連接的“優(yōu)雅關(guān)閉”。服務(wù)器在管理網(wǎng)絡(luò)連接時會將自己的上下文傳遞給它,而網(wǎng)絡(luò)連接啟動新線程時同樣也會將自己的上下文傳遞給這些線程,這些上下文都是可取消(cancelable)的。當(dāng)服務(wù)器需要停機或者連接將要關(guān)閉時,只要調(diào)用cancel函數(shù),所有這些線程就能收到通知并退出。服務(wù)器或者網(wǎng)絡(luò)連接通過阻塞等待這些線程關(guān)閉之后再關(guān)閉,就能最大限度保證正確退出。服務(wù)器關(guān)閉的關(guān)鍵代碼如下:
// Stop gracefully closes the server, it blocked until all connections
// are closed and all go-routines are exited.
func (s *Server) Stop() {
// immediately stop accepting new clients
s.mu.Lock()
listeners := s.lis
s.lis = nil
s.mu.Unlock()
for l := range listeners {
l.Close()
holmes.Infof("stop accepting at address %s\n", l.Addr().String())
}
// close all connections
conns := map[int64]*ServerConn{}
s.conns.RLock()
for k, v := range s.conns.m {
conns[k] = v
}
s.conns.Clear()
s.conns.RUnlock()
for _, c := range conns {
c.rawConn.Close()
holmes.Infof("close client %s\n", c.GetName())
}
s.mu.Lock()
s.cancel()
s.mu.Unlock()
s.wg.Wait()
holmes.Infoln("server stopped gracefully, bye.")
os.Exit(0)
}
3. 網(wǎng)絡(luò)連接模型
在其他的編程語言中,采用Reactor模式編寫的服務(wù)器往往需要在一個IO線程異步地通過epoll進行多路復(fù)用。而因為Go線程的開銷廉價,Go語言可以對每一個網(wǎng)絡(luò)連接創(chuàng)建三個go-routine。readLoop()負責(zé)讀取數(shù)據(jù)并反序列化成消息;writeLoop()負責(zé)序列化消息并發(fā)送二進制字節(jié)流;最后handleLoop()負責(zé)調(diào)用消息處理函數(shù)。這三個協(xié)程在連接創(chuàng)建并啟動時就會各自獨立運行:
// Start starts the server connection, creating go-routines for reading,
// writing and handlng.
func (sc *ServerConn) Start() {
holmes.Infof("conn start, <%v -> %v>\n", sc.rawConn.LocalAddr(), sc.rawConn.RemoteAddr())
onConnect := sc.belong.opts.onConnect
if onConnect != nil {
onConnect(sc)
}
loopers := []func(WriteCloser, *sync.WaitGroup){readLoop, writeLoop, handleLoop}
for _, l := range loopers {
looper := l
sc.wg.Add(1)
go looper(sc, sc.wg)
}
}
3.1 核心代碼分析之readLoop
readLoop做了三件關(guān)鍵的工作。首先調(diào)用消息編解碼器將接收到的字節(jié)流反序列化成消息;然后更新用于心跳檢測的時間戳;最后,根據(jù)消息的協(xié)議號找到對應(yīng)的消息處理函數(shù),如果注冊了消息回調(diào)函數(shù),那么就調(diào)用該函數(shù)處理消息,否則將消息和處理函數(shù)打包發(fā)送到handlerCh中,注意其中的cDone和sDone分別是網(wǎng)絡(luò)連接和服務(wù)器上下文結(jié)構(gòu)中的channel,分別用于監(jiān)聽網(wǎng)絡(luò)連接和服務(wù)器的“關(guān)閉”事件通知(下同)。
/* readLoop() blocking read from connection, deserialize bytes into message,
then find corresponding handler, put it into channel */
func readLoop(c WriteCloser, wg *sync.WaitGroup) {
var (
rawConn net.Conn
codec Codec
cDone <-chan struct{}
sDone <-chan struct{}
setHeartBeatFunc func(int64)
onMessage onMessageFunc
handlerCh chan MessageHandler
msg Message
err error
)
switch c := c.(type) {
case *ServerConn:
rawConn = c.rawConn
codec = c.belong.opts.codec
cDone = c.ctx.Done()
sDone = c.belong.ctx.Done()
setHeartBeatFunc = c.SetHeartBeat
onMessage = c.belong.opts.onMessage
handlerCh = c.handlerCh
case *ClientConn:
rawConn = c.rawConn
codec = c.opts.codec
cDone = c.ctx.Done()
sDone = nil
setHeartBeatFunc = c.SetHeartBeat
onMessage = c.opts.onMessage
handlerCh = c.handlerCh
}
defer func() {
if p := recover(); p != nil {
holmes.Errorf("panics: %v\n", p)
}
wg.Done()
holmes.Debugln("readLoop go-routine exited")
c.Close()
}()
for {
select {
case <-cDone: // connection closed
holmes.Debugln("receiving cancel signal from conn")
return
case <-sDone: // server closed
holmes.Debugln("receiving cancel signal from server")
return
default:
msg, err = codec.Decode(rawConn)
if err != nil {
holmes.Errorf("error decoding message %v\n", err)
if _, ok := err.(ErrUndefined); ok {
// update heart beats
setHeartBeatFunc(time.Now().UnixNano())
continue
}
return
}
setHeartBeatFunc(time.Now().UnixNano())
handler := GetHandlerFunc(msg.MessageNumber())
if handler == nil {
if onMessage != nil {
holmes.Infof("message %d call onMessage()\n", msg.MessageNumber())
onMessage(msg, c.(WriteCloser))
} else {
holmes.Warnf("no handler or onMessage() found for message %d\n", msg.MessageNumber())
}
continue
}
handlerCh <- MessageHandler{msg, handler}
}
}
}
3.2 核心代碼分析之writeLoop
writeLoop做了一件事情,從sendCh中讀取已序列化好的字節(jié)流,然后發(fā)送到網(wǎng)絡(luò)上。但是要注意,該協(xié)程在連接關(guān)閉退出執(zhí)行之前,會非阻塞地將sendCh中的消息全部發(fā)送完畢再退出,避免漏發(fā)消息,這就是關(guān)鍵所在。
/* writeLoop() receive message from channel, serialize it into bytes,
then blocking write into connection */
func writeLoop(c WriteCloser, wg *sync.WaitGroup) {
var (
rawConn net.Conn
sendCh chan []byte
cDone <-chan struct{}
sDone <-chan struct{}
pkt []byte
err error
)
switch c := c.(type) {
case *ServerConn:
rawConn = c.rawConn
sendCh = c.sendCh
cDone = c.ctx.Done()
sDone = c.belong.ctx.Done()
case *ClientConn:
rawConn = c.rawConn
sendCh = c.sendCh
cDone = c.ctx.Done()
sDone = nil
}
defer func() {
if p := recover(); p != nil {
holmes.Errorf("panics: %v\n", p)
}
// drain all pending messages before exit
OuterFor:
for {
select {
case pkt = <-sendCh:
if pkt != nil {
if _, err = rawConn.Write(pkt); err != nil {
holmes.Errorf("error writing data %v\n", err)
}
}
default:
break OuterFor
}
}
wg.Done()
holmes.Debugln("writeLoop go-routine exited")
c.Close()
}()
for {
select {
case <-cDone: // connection closed
holmes.Debugln("receiving cancel signal from conn")
return
case <-sDone: // server closed
holmes.Debugln("receiving cancel signal from server")
return
case pkt = <-sendCh:
if pkt != nil {
if _, err = rawConn.Write(pkt); err != nil {
holmes.Errorf("error writing data %v\n", err)
return
}
}
}
}
}
3.3 核心代碼分析之handleLoop
readLoop將消息和處理函數(shù)打包發(fā)給了handlerCh,于是handleLoop就從handlerCh中取出消息和處理函數(shù),然后交給工作者線程池,由后者負責(zé)調(diào)度執(zhí)行,完成對消息的處理。這里很好的詮釋了Go語言是如何通過channel實現(xiàn)Go線程間通信的。
// handleLoop() - put handler or timeout callback into worker go-routines
func handleLoop(c WriteCloser, wg *sync.WaitGroup) {
var (
cDone <-chan struct{}
sDone <-chan struct{}
timerCh chan *OnTimeOut
handlerCh chan MessageHandler
netID int64
ctx context.Context
askForWorker bool
)
switch c := c.(type) {
case *ServerConn:
cDone = c.ctx.Done()
sDone = c.belong.ctx.Done()
timerCh = c.timerCh
handlerCh = c.handlerCh
netID = c.netid
ctx = c.ctx
askForWorker = true
case *ClientConn:
cDone = c.ctx.Done()
sDone = nil
timerCh = c.timing.timeOutChan
handlerCh = c.handlerCh
netID = c.netid
ctx = c.ctx
}
defer func() {
if p := recover(); p != nil {
holmes.Errorf("panics: %v\n", p)
}
wg.Done()
holmes.Debugln("handleLoop go-routine exited")
c.Close()
}()
for {
select {
case <-cDone: // connectin closed
holmes.Debugln("receiving cancel signal from conn")
return
case <-sDone: // server closed
holmes.Debugln("receiving cancel signal from server")
return
case msgHandler := <-handlerCh:
msg, handler := msgHandler.message, msgHandler.handler
if handler != nil {
if askForWorker {
WorkerPoolInstance().Put(netID, func() {
handler(NewContextWithNetID(NewContextWithMessage(ctx, msg), netID), c)
})
addTotalHandle()
} else {
handler(NewContextWithNetID(NewContextWithMessage(ctx, msg), netID), c)
}
}
case timeout := <-timerCh:
if timeout != nil {
timeoutNetID := NetIDFromContext(timeout.Ctx)
if timeoutNetID != netID {
holmes.Errorf("timeout net %d, conn net %d, mismatched!\n", timeoutNetID, netID)
}
if askForWorker {
WorkerPoolInstance().Put(netID, func() {
timeout.Callback(time.Now(), c.(WriteCloser))
})
} else {
timeout.Callback(time.Now(), c.(WriteCloser))
}
}
}
}
}
4. 消息處理機制
4.1 消息上下文
任何一個實現(xiàn)了Message接口的類型,都是一個消息,它需要提供方法訪問自己的協(xié)議號并將自己序列化成字節(jié)數(shù)組;另外,每個消息都需要注冊自己的反序列化函數(shù)和處理函數(shù):
// Handler takes the responsibility to handle incoming messages.
type Handler interface {
Handle(context.Context, interface{})
}
// HandlerFunc serves as an adapter to allow the use of ordinary functions as handlers.
type HandlerFunc func(context.Context, WriteCloser)
// Handle calls f(ctx, c)
func (f HandlerFunc) Handle(ctx context.Context, c WriteCloser) {
f(ctx, c)
}
// UnmarshalFunc unmarshals bytes into Message.
type UnmarshalFunc func([]byte) (Message, error)
// handlerUnmarshaler is a combination of unmarshal and handle functions for message.
type handlerUnmarshaler struct {
handler HandlerFunc
unmarshaler UnmarshalFunc
}
func init() {
messageRegistry = map[int32]messageFunc{}
buf = new(bytes.Buffer)
}
// Register registers the unmarshal and handle functions for msgType.
// If no unmarshal function provided, the message will not be parsed.
// If no handler function provided, the message will not be handled unless you
// set a default one by calling SetOnMessageCallback.
// If Register being called twice on one msgType, it will panics.
func Register(msgType int32, unmarshaler func([]byte) (Message, error), handler func(context.Context, WriteCloser)) {
if _, ok := messageRegistry[msgType]; ok {
panic(fmt.Sprintf("trying to register message %d twice", msgType))
}
messageRegistry[msgType] = handlerUnmarshaler{
unmarshaler: unmarshaler,
handler: HandlerFunc(handler),
}
}
// GetUnmarshalFunc returns the corresponding unmarshal function for msgType.
func GetUnmarshalFunc(msgType int32) UnmarshalFunc {
entry, ok := messageRegistry[msgType]
if !ok {
return nil
}
return entry.unmarshaler
}
// GetHandlerFunc returns the corresponding handler function for msgType.
func GetHandlerFunc(msgType int32) HandlerFunc {
entry, ok := messageRegistry[msgType]
if !ok {
return nil
}
return entry.handler
}
// Message represents the structured data that can be handled.
type Message interface {
MessageNumber() int32
Serialize() ([]byte, error)
}
對每個消息處理函數(shù)而言,要處理的消息以及發(fā)送該消息的客戶端都是不同的,這些信息被稱為“消息上下文”,用Context結(jié)構(gòu)表示,每個不同的客戶端用一個64位整數(shù)netid標(biāo)識:
// Context is the context info for every handler function.
// Handler function handles the business logic about message.
// We can find the client connection who sent this message by netid and send back responses.
type Context struct{
message Message
netid int64
}
func NewContext(msg Message, id int64) Context {
return Context{
message: msg,
netid: id,
}
}
func (ctx Context)Message() Message {
return ctx.message
}
func (ctx Context)Id() int64 {
return ctx.netid
}
4.2 編解碼器
接收數(shù)據(jù)時,編解碼器(Codec)負責(zé)按照一定的格式將網(wǎng)絡(luò)連接上讀取的字節(jié)數(shù)據(jù)反序列化成消息,并將消息交給上層處理(解碼);發(fā)送數(shù)據(jù)時,編解碼器將上層傳遞過來的消息序列化成字節(jié)數(shù)據(jù),交給下層發(fā)送(編碼):
// Codec is the interface for message coder and decoder.
// Application programmer can define a custom codec themselves.
type Codec interface {
Decode(Connection) (Message, error)
Encode(Message) ([]byte, error)
}
Tao框架采用的是“Type-Length-Data”的格式打包數(shù)據(jù)。Type占4個字節(jié),表示協(xié)議類型;Length占4個字節(jié),表示消息長度,Data為變長字節(jié)序列,長度由Length表示。反序列化時,由Type字段可以確定協(xié)議類型,然后截取Length長度的字節(jié)數(shù)據(jù)Data,并調(diào)用已注冊的反序列化函數(shù)處理。核心代碼如下:
// Codec is the interface for message coder and decoder.
// Application programmer can define a custom codec themselves.
type Codec interface {
Decode(net.Conn) (Message, error)
Encode(Message) ([]byte, error)
}
// TypeLengthValueCodec defines a special codec.
// Format: type-length-value |4 bytes|4 bytes|n bytes <= 8M|
type TypeLengthValueCodec struct{}
// Decode decodes the bytes data into Message
func (codec TypeLengthValueCodec) Decode(raw net.Conn) (Message, error) {
byteChan := make(chan []byte)
errorChan := make(chan error)
go func(bc chan []byte, ec chan error) {
typeData := make([]byte, MessageTypeBytes)
_, err := io.ReadFull(raw, typeData)
if err != nil {
ec <- err
close(bc)
close(ec)
holmes.Debugln("go-routine read message type exited")
return
}
bc <- typeData
}(byteChan, errorChan)
var typeBytes []byte
select {
case err := <-errorChan:
return nil, err
case typeBytes = <-byteChan:
if typeBytes == nil {
holmes.Warnln("read type bytes nil")
return nil, ErrBadData
}
typeBuf := bytes.NewReader(typeBytes)
var msgType int32
if err := binary.Read(typeBuf, binary.LittleEndian, &msgType); err != nil {
return nil, err
}
lengthBytes := make([]byte, MessageLenBytes)
_, err := io.ReadFull(raw, lengthBytes)
if err != nil {
return nil, err
}
lengthBuf := bytes.NewReader(lengthBytes)
var msgLen uint32
if err = binary.Read(lengthBuf, binary.LittleEndian, &msgLen); err != nil {
return nil, err
}
if msgLen > MessageMaxBytes {
holmes.Errorf("message(type %d) has bytes(%d) beyond max %d\n", msgType, msgLen, MessageMaxBytes)
return nil, ErrBadData
}
// read application data
msgBytes := make([]byte, msgLen)
_, err = io.ReadFull(raw, msgBytes)
if err != nil {
return nil, err
}
// deserialize message from bytes
unmarshaler := GetUnmarshalFunc(msgType)
if unmarshaler == nil {
return nil, ErrUndefined(msgType)
}
return unmarshaler(msgBytes)
}
}
這里的代碼存在一些微妙的設(shè)計,需要仔細解釋一下。TypeLengthValueCodec.Decode()函數(shù)會被readLoop協(xié)程用到。因為io.ReadFull()是同步調(diào)用,沒有數(shù)據(jù)可讀時會阻塞readLoop協(xié)程。此時如果關(guān)閉網(wǎng)絡(luò)連接,readLoop協(xié)程將無法退出。所以這里的代碼用到了一個小技巧:專門開辟了一個新協(xié)程來等待讀取最開始的4字節(jié)Type數(shù)據(jù),然后自己select阻塞在多個channel上,這樣就不會忽略其他channel傳遞過來的消息。一旦成功讀取到Type數(shù)據(jù),就繼續(xù)后面的流程:讀取Length數(shù)據(jù),根據(jù)Length讀取應(yīng)用數(shù)據(jù)交給先前注冊好的反序列化函數(shù)。注意,如果收到超過最大長度的數(shù)據(jù)就會關(guān)閉連接,這是為了防止外部程序惡意消耗系統(tǒng)資源。
5. 工作者協(xié)程池
為了提高框架的健壯性,避免因為處理業(yè)務(wù)邏輯造成的響應(yīng)延遲,消息處理函數(shù)一般都會被調(diào)度到工作者協(xié)程池執(zhí)行。設(shè)計工作者協(xié)程池的一個關(guān)鍵是如何將任務(wù)散列給池子中的不同協(xié)程。一方面,要避免并發(fā)問題,必須保證同一個網(wǎng)絡(luò)連接發(fā)來的消息都被散列到同一個協(xié)程按順序執(zhí)行;另一方面,散列一定要是均勻的,不能讓協(xié)程“忙的忙死,閑的閑死”。關(guān)鍵還是在散列函數(shù)的設(shè)計上。
5.1 核心代碼分析
協(xié)程池是按照單例模式設(shè)計的。創(chuàng)建時會調(diào)用newWorker()創(chuàng)建一系列worker協(xié)程。
// WorkerPool is a pool of go-routines running functions.
type WorkerPool struct {
workers []*worker
closeChan chan struct{}
}
var (
globalWorkerPool *WorkerPool
)
func init() {
globalWorkerPool = newWorkerPool(WorkersNum)
}
// WorkerPoolInstance returns the global pool.
func WorkerPoolInstance() *WorkerPool {
return globalWorkerPool
}
func newWorkerPool(vol int) *WorkerPool {
if vol <= 0 {
vol = WorkersNum
}
pool := &WorkerPool{
workers: make([]*worker, vol),
closeChan: make(chan struct{}),
}
for i := range pool.workers {
pool.workers[i] = newWorker(i, 1024, pool.closeChan)
if pool.workers[i] == nil {
panic("worker nil")
}
}
return pool
}
5.2 給工作者協(xié)程分配任務(wù)
給工作者協(xié)程分配任務(wù)的方式很簡單,通過hashCode()散列函數(shù)找到對應(yīng)的worker協(xié)程,然后把回調(diào)函數(shù)發(fā)送到對應(yīng)協(xié)程的channel中。對應(yīng)協(xié)程在運行時就會從channel中取出然后執(zhí)行,在start()函數(shù)中。
// Put appends a function to some worker's channel.
func (wp *WorkerPool) Put(k interface{}, cb func()) error {
code := hashCode(k)
return wp.workers[code&uint32(len(wp.workers)-1)].put(workerFunc(cb))
}
func (w *worker) start() {
for {
select {
case <-w.closeChan:
return
case cb := <-w.callbackChan:
before := time.Now()
cb()
addTotalTime(time.Since(before).Seconds())
}
}
}
func (w *worker) put(cb workerFunc) error {
select {
case w.callbackChan <- cb:
return nil
default:
return ErrWouldBlock
}
}
6. 線程安全的定時器
Tao框架設(shè)計了一個定時器TimingWheel,用來控制定時任務(wù)。Connection在此基礎(chǔ)上進行了進一步封裝。提供定時執(zhí)行(RunAt),延時執(zhí)行(RunAfter)和周期執(zhí)行(RunEvery)功能。這里通過定時器的設(shè)計引出多線程編程的一點經(jīng)驗之談。
6.1 定時任務(wù)的數(shù)據(jù)結(jié)構(gòu)設(shè)計
6.1.1 定時任務(wù)結(jié)構(gòu)
每個定時任務(wù)由一個timerType表示,它帶有自己的id和包含定時回調(diào)函數(shù)的結(jié)構(gòu)OnTimeOut。expiration表示該任務(wù)到期要被執(zhí)行的時間,interval表示時間間隔,interval > 0意味著該任務(wù)是會被周期性重復(fù)執(zhí)行的任務(wù)。
/* 'expiration' is the time when timer time out, if 'interval' > 0
the timer will time out periodically, 'timeout' contains the callback
to be called when times out */
type timerType struct {
id int64
expiration time.Time
interval time.Duration
timeout *OnTimeOut
index int // for container/heap
}
// OnTimeOut represents a timed task.
type OnTimeOut struct {
Callback func(time.Time, WriteCloser)
Ctx context.Context
}
// NewOnTimeOut returns OnTimeOut.
func NewOnTimeOut(ctx context.Context, cb func(time.Time, WriteCloser)) *OnTimeOut {
return &OnTimeOut{
Callback: cb,
Ctx: ctx,
}
}
6.1.2 定時任務(wù)的組織
定時器需要按照到期時間的順序從最近到最遠排列,這是一個天然的小頂堆,于是這里采用標(biāo)準(zhǔn)庫container/heap創(chuàng)建了一個堆數(shù)據(jù)結(jié)構(gòu)來組織定時任務(wù),存取效率達到O(nlogn)。
// timerHeap is a heap-based priority queue
type timerHeapType []*timerType
func (heap timerHeapType) getIndexByID(id int64) int {
for _, t := range heap {
if t.id == id {
return t.index
}
}
return -1
}
func (heap timerHeapType) Len() int {
return len(heap)
}
func (heap timerHeapType) Less(i, j int) bool {
return heap[i].expiration.UnixNano() < heap[j].expiration.UnixNano()
}
func (heap timerHeapType) Swap(i, j int) {
heap[i], heap[j] = heap[j], heap[i]
heap[i].index = i
heap[j].index = j
}
func (heap *timerHeapType) Push(x interface{}) {
n := len(*heap)
timer := x.(*timerType)
timer.index = n
*heap = append(*heap, timer)
}
func (heap *timerHeapType) Pop() interface{} {
old := *heap
n := len(old)
timer := old[n-1]
timer.index = -1
*heap = old[0 : n-1]
return timer
}
6.2 定時器核心代碼分析
TimingWheel在創(chuàng)建時會啟動一個單獨協(xié)程來運行定時器核心代碼start()。它在多個channel上進行多路復(fù)用操作:如果從cancelChan收到timerId,就執(zhí)行取消操作:從堆上刪除對應(yīng)的定時任務(wù);將定時任務(wù)數(shù)量發(fā)送給sizeChan,別的線程就能獲取當(dāng)前定時任務(wù)數(shù);如果從quitChan收到消息,定時器就會被關(guān)閉然后退出;如果從addChan收到timer,就將該定時任務(wù)添加到堆;如果從tw.ticker.C收到定時信號,就調(diào)用getExpired()函數(shù)獲取到期的任務(wù),然后將這些任務(wù)回調(diào)發(fā)送到TimeOutChannel中,其他相關(guān)線程會通過該channel獲取并執(zhí)行定時回調(diào)。最后tw.update()會更新周期性執(zhí)行的定時任務(wù),重新調(diào)度執(zhí)行。
func (tw *TimingWheel) update(timers []*timerType) {
if timers != nil {
for _, t := range timers {
if t.isRepeat() {
t.expiration = t.expiration.Add(t.interval)
heap.Push(&tw.timers, t)
}
}
}
}
func (tw *TimingWheel) start() {
for {
select {
case timerID := <-tw.cancelChan:
index := tw.timers.getIndexByID(timerID)
if index >= 0 {
heap.Remove(&tw.timers, index)
}
case tw.sizeChan <- tw.timers.Len():
case <-tw.ctx.Done():
tw.ticker.Stop()
return
case timer := <-tw.addChan:
heap.Push(&tw.timers, timer)
case <-tw.ticker.C:
timers := tw.getExpired()
for _, t := range timers {
tw.GetTimeOutChannel() <- t.timeout
}
tw.update(timers)
}
}
}
6.3 定時器是怎么做到線程安全的
用Tao框架開發(fā)的服務(wù)器一開始總是時不時地崩潰。有時候運行了幾個小時服務(wù)器就突然退出了。查看打印出來的調(diào)用棧發(fā)現(xiàn)。每次程序都在定時器上崩潰,原因是數(shù)組訪問越界。這就是并發(fā)訪問導(dǎo)致的問題,為什么呢?因為定時器的核心函數(shù)在一個協(xié)程中操作堆數(shù)據(jù)結(jié)構(gòu),與此同時其提供的添加,刪除等接口卻有可能在其他協(xié)程中調(diào)用。多個協(xié)程并發(fā)訪問一個沒有加鎖的數(shù)據(jù)結(jié)構(gòu),必然會出現(xiàn)問題。解決方法很簡單:將多個協(xié)程的并發(fā)訪問轉(zhuǎn)化為單個協(xié)程的串行訪問,也就是將添加,刪除等操作發(fā)送給不同的channel,然后在start()協(xié)程中統(tǒng)一處理:
// AddTimer adds new timed task.
func (tw *TimingWheel) AddTimer(when time.Time, interv time.Duration, to *OnTimeOut) int64 {
if to == nil {
return int64(-1)
}
timer := newTimer(when, interv, to)
tw.addChan <- timer
return timer.id
}
// Size returns the number of timed tasks.
func (tw *TimingWheel) Size() int {
return <-tw.sizeChan
}
// CancelTimer cancels a timed task with specified timer ID.
func (tw *TimingWheel) CancelTimer(timerID int64) {
tw.cancelChan <- timerID
}
6.4 應(yīng)用層心跳
陳碩在他的《Linux多線程服務(wù)端編程》一書中說到,維護長連接的服務(wù)器都應(yīng)該在應(yīng)用層自己實現(xiàn)心跳消息:
“在嚴肅的網(wǎng)絡(luò)程序中,應(yīng)用層的心跳協(xié)議是必不可少的。應(yīng)該用心跳消息來判斷對方進程是否能正常工作。”
要使用一個連接來同時發(fā)送心跳和其他業(yè)務(wù)消息,這樣一旦應(yīng)用層因為出錯發(fā)不出消息,對方就能夠立刻通過心跳停止感知到。值得注意的是,在Tao框架中,定時器只有一個,而客戶端連接可能會有很多個。在長連接模式下,每個客戶端都需要處理心跳包,或者其他類型的定時任務(wù)。將框架設(shè)計為“每個客戶端連接自帶一個定時器”是不合適的——有十萬個連接就有十萬個定時器,會有較高的CPU占用率。定時器應(yīng)該只有一個,所有客戶端注冊進來的定時任務(wù)都由它負責(zé)處理。但是如果所有的客戶端連接都等待唯一一個定時器發(fā)來的消息,就又會存在并發(fā)問題。比如client 1的定時任務(wù)到期了,但它現(xiàn)在正忙著處理其他消息,這個定時任務(wù)就可能被其他client執(zhí)行。所以這里采取了一種“先集中后分散”的處理機制:每一個定時任務(wù)都由一個TimeOut結(jié)構(gòu)表示,該結(jié)構(gòu)中除了回調(diào)函數(shù)還包含一個context。客戶端啟動定時任務(wù)的時候都會填入net ID。TCPServer統(tǒng)一接收定時任務(wù),然后從定時任務(wù)中取出net ID,然后將該定時任務(wù)交給相應(yīng)的ServerConn或ClientConn去執(zhí)行:
// Retrieve the extra data(i.e. net id), and then redispatch timeout callbacks
// to corresponding client connection, this prevents one client from running
// callbacks of other clients
func (s *Server) timeOutLoop() {
defer s.wg.Done()
for {
select {
case <-s.ctx.Done():
return
case timeout := <-s.timing.GetTimeOutChannel():
netID := timeout.Ctx.Value(netIDCtx).(int64)
if sc, ok := s.conns.Get(netID); ok {
sc.timerCh <- timeout
} else {
holmes.Warnf("invalid client %d\n", netID)
}
}
}
}
三. 也談并發(fā)編程的核心問題和基本思路
當(dāng)我們談?wù)摬l(fā)編程的時候,我們在談?wù)撌裁矗坑靡痪湓捀爬ǎ?strong>當(dāng)多個線程同時訪問一個未受保護的共享數(shù)據(jù)時,就會產(chǎn)生并發(fā)問題。那么多線程編程的本質(zhì)就是怎樣避免上述情況的發(fā)生了。這里總結(jié)一些,有三種基本的方法。
1. 對共享數(shù)據(jù)結(jié)構(gòu)進行保護
這是教科書上最常見的方法了。用各種信號量/互斥鎖對數(shù)據(jù)結(jié)構(gòu)進行保護,先加鎖,然后執(zhí)行操作,最后解鎖。舉個例子,Tao框架中用于網(wǎng)絡(luò)連接管理的ConnMap就是這么實現(xiàn)的:
// ConnMap is a safe map for server connection management.
type ConnMap struct {
sync.RWMutex
m map[int64]*ServerConn
}
// NewConnMap returns a new ConnMap.
func NewConnMap() *ConnMap {
return &ConnMap{
m: make(map[int64]*ServerConn),
}
}
// Clear clears all elements in map.
func (cm *ConnMap) Clear() {
cm.Lock()
cm.m = make(map[int64]*ServerConn)
cm.Unlock()
}
// Get gets a server connection with specified net ID.
func (cm *ConnMap) Get(id int64) (*ServerConn, bool) {
cm.RLock()
sc, ok := cm.m[id]
cm.RUnlock()
return sc, ok
}
// Put puts a server connection with specified net ID in map.
func (cm *ConnMap) Put(id int64, sc *ServerConn) {
cm.Lock()
cm.m[id] = sc
cm.Unlock()
}
// Remove removes a server connection with specified net ID.
func (cm *ConnMap) Remove(id int64) {
cm.Lock()
delete(cm.m, id)
cm.Unlock()
}
// Size returns map size.
func (cm *ConnMap) Size() int {
cm.RLock()
size := len(cm.m)
cm.RUnlock()
return size
}
// IsEmpty tells whether ConnMap is empty.
func (cm *ConnMap) IsEmpty() bool {
return cm.Size() <= 0
}
2 多線程并行轉(zhuǎn)化為單線程串行
這種方法在前面已經(jīng)介紹過,它屬于無鎖化的一種編程方式。多個線程的操作請求都放到一個任務(wù)隊列中,最終由一個單一的線程來讀取隊列并串行執(zhí)行。這種方法在并發(fā)量很大的時候還是會有性能瓶頸。
3 采用精心設(shè)計的并發(fā)數(shù)據(jù)結(jié)構(gòu)
最好的辦法還是要從數(shù)據(jù)結(jié)構(gòu)上入手,有很多技巧能夠讓數(shù)據(jù)結(jié)構(gòu)適應(yīng)多線程并發(fā)訪問的場景。比如Java標(biāo)準(zhǔn)庫中的java.util.concurrent,包含了各種并發(fā)數(shù)據(jù)結(jié)構(gòu),其中ConcurrentHashMap的基本原理就是分段鎖,對每個段(Segment)加鎖保護,并發(fā)寫入數(shù)據(jù)時通過散列函數(shù)分發(fā)到不同的段上面,在SegmentA上加鎖并不影響SegmentB的訪問。
處理并發(fā)多線程問題,一定要小心再小心,思考再思考,一不注意就會踩坑
四. 特別鳴謝
- 《Linux多線程服務(wù)端編程》以及Muduo網(wǎng)絡(luò)編程庫 - by陳碩
- 《Go程序設(shè)計語言》 - Donovan & Kernighan
- gotcp - A Go package for quickly building tcp servers
- syncmap - A thread safe map implementation for Golang
- leaf - A pragmatic game server framework in Go