學習golang的時間斷斷續續加起來也有將近一個月了,這期間都是偶看翻幾頁書,沒有寫過實際的代碼.最近做一個app項目,是一個展示類 的軟件,當客戶要看某個圖片時首先向服務器發出一個請求,比對圖片的版本,如果版本與本地一致,則直接顯示,如果版本落后了則由服務 器將最新的版本發送給客戶端.
對服務器的需求就是一個簡單的版本比對和文件傳輸,於是打算用go去實現,正好也可以練練手.
在設計上,受到以往框架設計的影響,還是使用了wpacket
,rpacket
和共享buffer
這個方案,不同的地方是go的網絡API不支持 gather io
,所以buffer
沒有被實現為buffer list
,而是一整塊的連續內存,當buffer
空間不足時需要手動的去擴展內存.
下面說點與以往用C設計網絡框架不同的地方,對於已經習慣了使用異步回調方式來使用網絡的我,剛開始用go來做一個網絡框架還是稍有不適. go的網絡API都是同步的,也不存在select,epoll,poll這類東西,這也就意味着需要使用goroutine
來實現並發.
我的做法是這樣的,首先定義了一個類型Tcpconnection
type Tcpsession struct{ Conn net.Conn Packet_que chan interface{} Send_que chan *packet.Wpacket raw bool send_close bool }
其中Conn
成員是連接對象,Packet_que
是一個管道,用於接收收到的網絡包和連接事件.Send_que
是另一個管道,用來將需要發送 的數據包傳給sender。
func NewTcpSession(conn net.Conn,raw bool)(*Tcpsession){ session := &Tcpsession{Conn:conn,Packet_que:make(chan interface{},1024),Send_que:make(chan *packet.Wpacket,1024),raw:raw,send_close:false} if raw{ go dorecv_raw(session) }else{ go dorecv(session) } go dosend(session) return session }
在建立一個新連接之后,啟動兩個goroutine分別用於執行recv和send.
func dorecv_raw(session *Tcpsession){ for{ recvbuf := make([]byte,packet.Max_bufsize) _,err := session.Conn.Read(recvbuf) if err != nil { session.Packet_que <- "rclose" return } rpk := packet.NewRpacket(packet.NewBufferByBytes(recvbuf),true) session.Packet_que <- rpk } } func dosend(session *Tcpsession){ for{ wpk,ok := <-session.Send_que if !ok { return } _,err := session.Conn.Write(wpk.Buffer().Bytes()) if err != nil { session.send_close = true return } if wpk.Fn_sendfinish != nil{ wpk.Fn_sendfinish(session,wpk) } } }
dorecv的工就只是簡單的接收數據,將收到的原始數據打包成rpacket,然后寫到管道Packet_que
中。 dosend則是從'Send_que'中提取出要待發送的wpacket,然后send出去.
然后來看下主過程:
func main(){ service := ":8010" tcpAddr,err := net.ResolveTCPAddr("tcp4", service) if err != nil{ fmt.Printf("ResolveTCPAddr") } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil{ fmt.Printf("ListenTCP") } for { conn, err := listener.Accept() if err != nil { continue } session := tcpsession.NewTcpSession(conn,true) fmt.Printf("a client comming\n") go tcpsession.ProcessSession(session,process_client,session_close) } }
主過程等待在listen上,每當接受一個新的連接就用這個連接作為參數創建一個Tcpsession
,然后啟動一個 goroutine運行ProcessSession
。
func ProcessSession(tcpsession *Tcpsession,process_packet func (*Tcpsession,*packet.Rpacket),session_close func (*Tcpsession)){ for{ msg,ok := <- tcpsession.Packet_que if !ok { fmt.Printf("client disconnect\n") return } switch msg.(type){ case * packet.Rpacket: rpk := msg.(*packet.Rpacket) process_packet(tcpsession,rpk) case string: str := msg.(string) if str == "rclose"{ session_close(tcpsession) close(tcpsession.Packet_que) close(tcpsession.Send_que) tcpsession.Conn.Close() return } } } }
ProcessSession
的工作就是不斷的從Packet_que中取出消息,如果消息是一個rpacket就回調使用者傳進來的process_packet
函數。 如果是一個網絡連接斷開的事件則清理這個Tcpsession
.
對每個連接,使用3個goroutine,2個channel,一個goroutine用於發送,一個用於接收和拆包,一個用於處理數據包.這個模式基本上就是標准的 go模式了.至於性能,在測試程序中我沒有啟用goroutine在多核心上運行的特性,也就說整個進程就是一個單線程的程序.其效率並不比實現同樣功能 的C程序差.