三、【Zinx第三章-基礎路由模組】Golang輕量級併發伺服器框架
三、Zinx框架基礎路由模組
現在我們就給使用者提供一個自定義的conn處理業務的介面吧,很顯然,我們不能把業務處理業務的方法綁死在type HandFunc func(*net.TCPConn, []byte, int) error
這種格式中,我們需要定一些interface{}
來讓使用者填寫任意格式的連線處理業務方法。
那麼,很顯然func是滿足不了我們需求的,我們需要再做幾個抽象的介面類。
3.1 IRequest 訊息請求抽象類
我們現在需要把客戶端請求的連線資訊 和 請求的資料,放在一個叫Request的請求類裡,這樣的好處是我們可以從Request裡得到全部客戶端的請求資訊,也為我們之後拓展框架有一定的作用,一旦客戶端有額外的含義的資料資訊,都可以放在這個Request裡。可以理解為每次客戶端的全部請求資料,Zinx都會把它們一起放到一個Request結構體裡。
A) 建立抽象IRequest層
在ziface
下建立新檔案irequest.go
。
zinx/ziface/irequest.go
package ziface /* IRequest 介面: 實際上是把客戶端請求的連結資訊 和 請求的資料 包裝到了 Request裡 */ type IRequest interface{ GetConnection() IConnection //獲取請求連線資訊 GetData() []byte//獲取請求訊息的資料 }
不難看出,當前的抽象層只提供了兩個Getter方法,所以有個成員應該是必須的,一個是客戶端連線,一個是客戶端傳遞進來的資料,當然隨著Zinx框架的功能豐富,這裡面還應該繼續新增新的成員。
B) 實現Request類
在znet下建立IRequest抽象介面的一個例項類檔案request.go
zinx/znet/request.go
package znet import "zinx/ziface" type Request struct { conn ziface.IConnection //已經和客戶端建立好的 連結 data []byte //客戶端請求的資料 } //獲取請求連線資訊 func(r *Request) GetConnection() ziface.IConnection { return r.conn } //獲取請求訊息的資料 func(r *Request) GetData() []byte { return r.data }
好了現在我們Request類建立好了,稍後我們會用到它。
3.2 IRouter 路由配置抽象類
現在我們來給Zinx實現一個非常簡單基礎的路由功能,目的當然就是為了快速的讓Zinx步入到路由的階段。後續我們會不斷的完善路由功能。
A) 建立抽象的IRouter層
在ziface
下建立irouter.go
檔案
zinx/ziface/irouter.go
package ziface /* 路由介面, 這裡面路由是 使用框架者給該連結自定的 處理業務方法 路由裡的IRequest 則包含用該連結的連結資訊和該連結的請求資料資訊 */ type IRouter interface{ PreHandle(request IRequest)//在處理conn業務之前的鉤子方法 Handle(request IRequest)//處理conn業務的方法 PostHandle(request IRequest) //處理conn業務之後的鉤子方法 }
我們知道router實際上的作用就是,服務端應用可以給Zinx框架配置當前連結的處理業務方法,之前的Zinx-V0.2我們的Zinx框架處理連結請求的方法是固定的,現在是可以自定義,並且有3種介面可以重寫。
Handle
:是處理當前連結的主業務函式
PreHandle
:如果需要在主業務函式之前有前置業務,可以重寫這個方法
PostHandle
:如果需要在主業務函式之後又後置業務,可以重寫這個方法
當然每個方法都有一個唯一的形參IRequest
物件,也就是客戶端請求過來的連線和請求資料,作為我們業務方法的輸入資料。
B) 實現Router類
在znet
下建立router.go
檔案
package znet import "zinx/ziface" //實現router時,先嵌入這個基類,然後根據需要對這個基類的方法進行重寫 type BaseRouter struct {} //這裡之所以BaseRouter的方法都為空, // 是因為有的Router不希望有PreHandle或PostHandle // 所以Router全部繼承BaseRouter的好處是,不需要實現PreHandle和PostHandle也可以例項化 func (br *BaseRouter)PreHandle(req ziface.IRequest){} func (br *BaseRouter)Handle(req ziface.IRequest){} func (br *BaseRouter)PostHandle(req ziface.IRequest){}
我們當前的Zinx目錄結構應該如下:
. ├── README.md ├── ziface │├── iconnnection.go │├── irequest.go │├── irouter.go │└── iserver.go └── znet ├── connection.go ├── request.go ├── router.go ├── server.go └── server_test.go
3.3 Zinx-V0.3-整合簡單路由功能
A) IServer增添路由新增功能
我們需要給IServer類,增加一個抽象方法AddRouter
,目的也是讓Zinx框架使用者,可以自定一個Router處理業務方法。
zinx/ziface/irouter.go
package ziface //定義伺服器介面 type IServer interface{ //啟動伺服器方法 Start() //停止伺服器方法 Stop() //開啟業務服務方法 Serve() //路由功能:給當前服務註冊一個路由業務方法,供客戶端連結處理使用 AddRouter(router IRouter) }
B) Server類增添Router成員
有了抽象的方法,自然Server就要實現,並且還要新增一個Router成員.
zinx/znet/server.go
//iServer 介面實現,定義一個Server服務類 type Server struct { //伺服器的名稱 Name string //tcp4 or other IPVersion string //服務繫結的IP地址 IP string //服務繫結的埠 Port int //當前Server由使用者繫結的回撥router,也就是Server註冊的連結對應的處理業務 Router ziface.IRouter }
然後NewServer()
方法, 初始化Server物件的方法也要加一個初始化成員
/* 建立一個伺服器控制代碼 */ func NewServer (name string) ziface.IServer { s:= &Server { Name :name, IPVersion:"tcp4", IP:"0.0.0.0", Port:7777, Router: nil, } return s }
C) Connection類繫結一個Router成員
zinx/znet/connection.go
type Connection struct { //當前連線的socket TCP套接字 Conn *net.TCPConn //當前連線的ID 也可以稱作為SessionID,ID全域性唯一 ConnID uint32 //當前連線的關閉狀態 isClosed bool //該連線的處理方法router Routerziface.IRouter //告知該連結已經退出/停止的channel ExitBuffChan chan bool }
D) 在Connection呼叫註冊的Router處理業務
zinx/znet/connection.go
func (c *Connection) StartReader() { fmt.Println("Reader Goroutine isrunning") defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!") defer c.Stop() for{ //讀取我們最大的資料到buf中 buf := make([]byte, 512) _, err := c.Conn.Read(buf) if err != nil { fmt.Println("recv buf err ", err) c.ExitBuffChan <- true continue } //得到當前客戶端請求的Request資料 req := Request{ conn:c, data:buf, } //從路由Routers 中找到註冊繫結Conn的對應Handle go func (request ziface.IRequest) { //執行註冊的路由方法 c.Router.PreHandle(request) c.Router.Handle(request) c.Router.PostHandle(request) }(&req) } }
這裡我們在conn讀取完客戶端資料之後,將資料和conn封裝到一個Request中,作為Router的輸入資料。
然後我們開啟一個goroutine去呼叫給Zinx框架註冊好的路由業務。
3.4 Zinx-V0.3程式碼實現
zinx/znet/server.go
package znet import ( "fmt" "net" "time" "zinx/ziface" ) //iServer 介面實現,定義一個Server服務類 type Server struct { //伺服器的名稱 Name string //tcp4 or other IPVersion string //服務繫結的IP地址 IP string //服務繫結的埠 Port int //當前Server由使用者繫結的回撥router,也就是Server註冊的連結對應的處理業務 Router ziface.IRouter } /* 建立一個伺服器控制代碼 */ func NewServer (name string) ziface.IServer { s:= &Server { Name :name, IPVersion:"tcp4", IP:"0.0.0.0", Port:7777, Router: nil, } return s } //============== 實現 ziface.IServer 裡的全部介面方法 ======== //開啟網路服務 func (s *Server) Start() { fmt.Printf("[START] Server listenner at IP: %s, Port %d, is starting\n", s.IP, s.Port) //開啟一個go去做服務端Linster業務 go func() { //1 獲取一個TCP的Addr addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port)) if err != nil { fmt.Println("resolve tcp addr err: ", err) return } //2 監聽伺服器地址 listenner, err:= net.ListenTCP(s.IPVersion, addr) if err != nil { fmt.Println("listen", s.IPVersion, "err", err) return } //已經監聽成功 fmt.Println("start Zinx server", s.Name, " succ, now listenning...") //TODO server.go 應該有一個自動生成ID的方法 var cid uint32 cid = 0 //3 啟動server網路連線業務 for { //3.1 阻塞等待客戶端建立連線請求 conn, err := listenner.AcceptTCP() if err != nil { fmt.Println("Accept err ", err) continue } //3.2 TODO Server.Start() 設定伺服器最大連線控制,如果超過最大連線,那麼則關閉此新的連線 //3.3 處理該新連線請求的 業務 方法, 此時應該有 handler 和 conn是繫結的 dealConn := NewConntion(conn, cid, s.Router) cid ++ //3.4 啟動當前連結的處理業務 go dealConn.Start() } }() } func (s *Server) Stop() { fmt.Println("[STOP] Zinx server , name " , s.Name) //TODOServer.Stop() 將其他需要清理的連線資訊或者其他資訊 也要一併停止或者清理 } func (s *Server) Serve() { s.Start() //TODO Server.Serve() 是否在啟動服務的時候 還要處理其他的事情呢 可以在這裡新增 //阻塞,否則主Go退出, listenner的go將會退出 for { time.Sleep(10*time.Second) } } //路由功能:給當前服務註冊一個路由業務方法,供客戶端連結處理使用 func (s *Server)AddRouter(router ziface.IRouter) { s.Router = router fmt.Println("Add Router succ! " ) }
zinx/znet/conneciont.go
package znet import ( "fmt" "net" "zinx/ziface" ) type Connection struct { //當前連線的socket TCP套接字 Conn *net.TCPConn //當前連線的ID 也可以稱作為SessionID,ID全域性唯一 ConnID uint32 //當前連線的關閉狀態 isClosed bool //該連線的處理方法router Routerziface.IRouter //告知該連結已經退出/停止的channel ExitBuffChan chan bool } //建立連線的方法 func NewConntion(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection{ c := &Connection{ Conn:conn, ConnID:connID, isClosed: false, Router: router, ExitBuffChan: make(chan bool, 1), } return c } func (c *Connection) StartReader() { fmt.Println("Reader Goroutine isrunning") defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!") defer c.Stop() for{ //讀取我們最大的資料到buf中 buf := make([]byte, 512) _, err := c.Conn.Read(buf) if err != nil { fmt.Println("recv buf err ", err) c.ExitBuffChan <- true continue } //得到當前客戶端請求的Request資料 req := Request{ conn:c, data:buf, } //從路由Routers 中找到註冊繫結Conn的對應Handle go func (request ziface.IRequest) { //執行註冊的路由方法 c.Router.PreHandle(request) c.Router.Handle(request) c.Router.PostHandle(request) }(&req) } } //啟動連線,讓當前連線開始工作 func (c *Connection) Start() { //開啟處理該連結讀取到客戶端資料之後的請求業務 go c.StartReader() for { select { case <- c.ExitBuffChan: //得到退出訊息,不再阻塞 return } } } //停止連線,結束當前連線狀態M func (c *Connection) Stop() { //1. 如果當前連結已經關閉 if c.isClosed == true { return } c.isClosed = true //TODO Connection Stop() 如果使用者註冊了該連結的關閉回撥業務,那麼在此刻應該顯示呼叫 // 關閉socket連結 c.Conn.Close() //通知從緩衝佇列讀資料的業務,該連結已經關閉 c.ExitBuffChan <- true //關閉該連結全部管道 close(c.ExitBuffChan) } //從當前連接獲取原始的socket TCPConn func (c *Connection) GetTCPConnection() *net.TCPConn { return c.Conn } //獲取當前連線ID func (c *Connection) GetConnID() uint32{ return c.ConnID } //獲取遠端客戶端地址資訊 func (c *Connection) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() }
3.5 使用Zinx-V0.3完成應用程式
接下來我們在基於Zinx寫伺服器,就可以配置一個簡單的路由功能了。
A) 測試基於Zinx完成的服務端應用
Server.go
package main import ( "fmt" "zinx/ziface" "zinx/znet" ) //ping test 自定義路由 type PingRouter struct { znet.BaseRouter //一定要先基礎BaseRouter } //Test PreHandle func (this *PingRouter) PreHandle(request ziface.IRequest) { fmt.Println("Call Router PreHandle") _, err := request.GetConnection().GetTCPConnection().Write([]byte("before ping ....\n")) if err !=nil { fmt.Println("call back ping ping ping error") } } //Test Handle func (this *PingRouter) Handle(request ziface.IRequest) { fmt.Println("Call PingRouter Handle") _, err := request.GetConnection().GetTCPConnection().Write([]byte("ping...ping...ping\n")) if err !=nil { fmt.Println("call back ping ping ping error") } } //Test PostHandle func (this *PingRouter) PostHandle(request ziface.IRequest) { fmt.Println("Call Router PostHandle") _, err := request.GetConnection().GetTCPConnection().Write([]byte("After ping .....\n")) if err !=nil { fmt.Println("call back ping ping ping error") } } func main(){ //建立一個server控制代碼 s := znet.NewServer("[zinx V0.3]") s.AddRouter(&PingRouter{}) //2 開啟服務 s.Serve() }
我們這裡自定義了一個類似Ping操作的路由,就是當客戶端傳送資料,我們的處理業務就是返回給客戶端"ping...ping..ping..", 為了測試,當前路由也同時實現了PreHandle和PostHandle兩個方法。實際上Zinx會利用模板的設計模式,依次在框架中呼叫PreHandle
、Handle
、PostHandle
三個方法。
B) 啟動Server.go
go run Server.go
C) 客戶端應用測試程式
和之前的Client.go一樣 沒有改變
package main import ( "fmt" "net" "time" ) /* 模擬客戶端 */ func main() { fmt.Println("Client Test ... start") //3秒之後發起測試請求,給服務端開啟服務的機會 time.Sleep(3 * time.Second) conn,err := net.Dial("tcp", "127.0.0.1:7777") if err != nil { fmt.Println("client start err, exit!") return } for { _, err := conn.Write([]byte("Zinx V0.3")) if err !=nil { fmt.Println("write error err ", err) return } buf :=make([]byte, 512) cnt, err := conn.Read(buf) if err != nil { fmt.Println("read buf error ") return } fmt.Printf(" server call back : %s, cnt = %d\n", buf,cnt) time.Sleep(1*time.Second) } }
D) 啟動Client.go
go run Client.go
執行結果如下:
服務端:
$ go run Server.go Add Router succ! [START] Server listenner at IP: 0.0.0.0, Port 7777, is starting start Zinx server[zinx V0.3]succ, now listenning... Reader Goroutine isrunning Call Router PreHandle Call PingRouter Handle Call Router PostHandle Call Router PreHandle Call PingRouter Handle Call Router PostHandle Call Router PreHandle Call PingRouter Handle Call Router PostHandle Call Router PreHandle Call PingRouter Handle Call Router PostHandle Call Router PreHandle Call PingRouter Handle Call Router PostHandle ...
客戶端:
$ go run Client.go Client Test ... start server call back : before ping .... , cnt = 17 server call back : ping...ping...ping After ping ..... , cnt = 36 server call back : before ping .... ping...ping...ping After ping ..... , cnt = 53 server call back : before ping .... ping...ping...ping After ping ..... , cnt = 53 server call back : before ping .... ping...ping...ping After ping ..... , cnt = 53 ...
現在Zinx框架已經有路由功能了,雖然說目前只能配置一個,不過不要著急,很快我們會增加配置多路由的能力。
關於作者:
作者:Aceld(劉丹冰)
簡書號:IT無崖子
mail:[email protected]
github:https://github.com/aceld
原創書籍gitbook:http://legacy.gitbook.com/@aceld