迷你文件下载服务器
印象里,《传奇3》是市面上最早使用微端技术的游戏(之一)。其技术方案,主要都是由传奇工作室时任技术总监范哥设计并实现的,当时范哥给《传奇归来》初步实现了微端功能,而我也在盛大版《传奇3》正式上线之前将微端相关逻辑移植了过来。对于这份技术方案,我的记忆已比较模糊了,只对一些基本的东西还有点印象,譬如和网络游戏服务器端比较类似的架构(如 Gate、ResourceServer 及负载均衡等),那时范哥是基于盛大自有的机房及机器等硬件前提设计了整套框架。
而现在云服务器的应用已比较普遍,借助云服务器的基础设施,(网络游戏)资源服务器程序的设计及实现应可以简化一些,甚至某些情况下,客户端程序直连资源服务器程序也未尝不可。
所以不如就用 Golang 来写个简单的文件下载服务器程序练练手吧,而客户端,就以 Delphi 来写个粗糙的 Demo,然后让两者交互起来。
首先自然要设计通讯协议:
package protocol import ( "bytes" "encoding/binary" "fmt" ) const ( MaxFileNameLen = 24 ) type Msg struct { Signature uint32 Cmd uint16 Param int16 FileName [MaxFileNameLen]byte Len int32 } func (m *Msg) String() string { return fmt.Sprintf("Signature:%d Cmd:%d Param:%d FileName:%s Len:%d", m.Signature, m.Cmd, m.Param, m.FileName, m.Len) } func (m *Msg) Bytes() []byte { var buf bytes.Buffer binary.Write(&buf, binary.LittleEndian, m) return buf.Bytes() } const ( MsgSize = 4 + 2 + 2 + 24 + 4 CustomSignature = 0xFAFBFCFD CM_PING = 100 SM_PING = 200 CM_GETFILE = 1000 SM_GETFILE = 2000 )
既然是文件下载,自然少不了基本的文件处理逻辑(若请求的文件尚未载入,则先载入它,否直接读取其缓存):
package filehandler import ( "errors" "io/ioutil" "strings" "sync" . "github.com/ecofast/sysutils" ) type FileHandler struct { filePath string mutex sync.Mutex fileCaches map[string][]byte } func (fh *FileHandler) Initialize(filePath string) { fh.filePath = filePath fh.fileCaches = make(map[string][]byte) } func (fh *FileHandler) GetFile(filename string) ([]byte, error) { lowername := strings.ToLower(filename) fh.mutex.Lock() defer fh.mutex.Unlock() buf, ok := fh.fileCaches[lowername] if !ok { fullname = fh.filePath + filename if FileExists(fullname) { data, err := ioutil.ReadFile(fullname) if err != nil { return nil, err } fh.add(lowername, data) return data, nil } return nil, errors.New("The required file does not exists: " + filename) } return buf, nil } func (fh *FileHandler) add(filename string, filebytes []byte) { fh.fileCaches[filename] = filebytes }
然后是 Socket 交互相关了(主要是对 TCP 粘包的处理):
package sockhandler import ( "bytes" "fmt" "log" "minifileserver/filehandler" . "minifileserver/protocol" "net" "sync" . "github.com/ecofast/sysutils" ) type ActiveConns struct { mutex sync.Mutex conns map[string]net.Conn } func (cs *ActiveConns) Initialize() { cs.conns = make(map[string]net.Conn) } func (cs *ActiveConns) Add(addr string, conn net.Conn) { cs.mutex.Lock() defer cs.mutex.Unlock() cs.conns[addr] = conn } func (cs *ActiveConns) Remove(addr string) { cs.mutex.Lock() defer cs.mutex.Unlock() delete(cs.conns, addr) } func (cs *ActiveConns) Exists(addr string) bool { cs.mutex.Lock() defer cs.mutex.Unlock() _, ok := cs.conns[addr] return ok } func (cs *ActiveConns) Count() int { cs.mutex.Lock() defer cs.mutex.Unlock() return len(cs.conns) } const ( RecvBufLenMax = 16 * 1024 SendBufLenMax = 32 * 1024 ) var ( Conns ActiveConns FileHandler filehandler.FileHandler ) func Run(port int, filepath string) { listener, err := net.Listen("tcp", "127.0.0.1:"+IntToStr(port)) CheckError(err) defer listener.Close() log.Println("=====服务已启动=====") FileHandler.Initialize(filepath) Conns.Initialize() for { conn, err := listener.Accept() if err != nil { log.Printf("Error accepting: %s\n", err.Error()) continue } go handleConn(conn) } } func handleConn(conn net.Conn) { Conns.Add(conn.RemoteAddr().String(), conn) log.Printf("当前连接数:%d\n", Conns.Count()) var msg Msg var recvBuf []byte recvBufLen := 0 buf := make([]byte, MsgSize) for { count, err := conn.Read(buf) if err != nil { Conns.Remove(conn.RemoteAddr().String()) conn.Close() log.Println("连接断开:", err.Error()) log.Printf("[handleConn] 当前连接数:%d\n", Conns.Count()) break } if count+recvBufLen > RecvBufLenMax { continue } recvBuf = append(recvBuf, buf[0:count]...) recvBufLen += count offsize := 0 offset := 0 for recvBufLen-offsize >= MsgSize { offset = 0 msg.Signature = uint32(uint32(recvBuf[offsize+3])<<24 | uint32(recvBuf[offsize+2])<<16 | uint32(recvBuf[offsize+1])<<8 | uint32(recvBuf[offsize+0])) offset += 4 msg.Cmd = uint16(uint16(recvBuf[offsize+offset+1])<<8 | uint16(recvBuf[offsize+offset+0])) offset += 2 msg.Param = int16(int16(recvBuf[offsize+offset+1])<<8 | int16(recvBuf[offsize+offset+0])) offset += 2 copy(msg.FileName[:], recvBuf[offsize+offset+0:offsize+offset+MaxFileNameLen]) offset += MaxFileNameLen msg.Len = int32(int32(recvBuf[offsize+offset+3])<<24 | int32(recvBuf[offsize+offset+2])<<16 | int32(recvBuf[offsize+offset+1])<<8 | int32(recvBuf[offsize+offset+0])) offset += 4 if msg.Signature == CustomSignature { pkglen := int(MsgSize + msg.Len) if pkglen >= RecvBufLenMax { offsize = recvBufLen break } if offsize+pkglen > recvBufLen { break } switch msg.Cmd { case CM_PING: fmt.Printf("From %s received CM_PING\n", conn.RemoteAddr().String()) reponsePing(conn) case CM_GETFILE: fmt.Printf("From %s received CM_GETFILE\n", conn.RemoteAddr().String()) responseDownloadFile( /*string(msg.FileName[:])*/ msg.FileName, conn) default: fmt.Printf("From %s received %d\n", conn.RemoteAddr().String(), msg.Cmd) } offsize += pkglen } else { offsize++ fmt.Printf("From %s received %d\n", conn.RemoteAddr().String(), msg.Cmd) } } recvBufLen -= offsize if recvBufLen > 0 { recvBuf = recvBuf[offsize : offsize+recvBufLen] } else { recvBuf = nil } } conn.Close() } func reponsePing(conn net.Conn) { var msg Msg msg.Signature = CustomSignature msg.Cmd = SM_PING msg.Param = 0 msg.FileName = [MaxFileNameLen]byte{0} msg.Len = 0 conn.Write(msg.Bytes()) } func responseDownloadFile(filename [MaxFileNameLen]byte, conn net.Conn) { var msg Msg msg.Signature = CustomSignature msg.Cmd = SM_GETFILE msg.FileName = filename var buf bytes.Buffer if data, err := FileHandler.GetFile(BytesToStr(filename[:])); err == nil { msg.Param = 0 msg.Len = int32(len(data)) buf.Write(msg.Bytes()) buf.Write(data) } else { log.Println(err.Error()) msg.Param = -1 msg.Len = 0 buf.Write(msg.Bytes()) } if _, err := conn.Write(buf.Bytes()); err != nil { log.Printf("Write to %s failed: %s", conn.RemoteAddr().String(), err.Error()) } }
借由这几个基础模块,下载服务器程序写起来就简单了:
package main import ( "fmt" "log" "minifileserver/sockhandler" "os" "path/filepath" "time" . "github.com/ecofast/iniutils" . "github.com/ecofast/sysutils" ) const ( listenPort = 7000 reportConnNumInterval = 10 * 60 ) func main() { startService() } func startService() { port := listenPort filePath := GetApplicationPath() tickerInterval := reportConnNumInterval ininame := ChangeFileExt(os.Args[0], ".ini") if FileExists(ininame) { port = IniReadInt(ininame, "setup", "port", port) filePath = IncludeTrailingBackslash(IniReadString(ininame, "setup", "filepath", filePath)) tickerInterval = IniReadInt(ininame, "setup", "reportinterval", tickerInterval) } else { IniWriteInt(ininame, "setup", "port", port) IniWriteString(ininame, "setup", "filepath", filePath) IniWriteInt(ininame, "setup", "reportinterval", tickerInterval) } filePath = filepath.ToSlash(filePath) fmt.Printf("监听端口:%d\n", port) fmt.Printf("文件目录:%s\n", filePath) fmt.Printf("连接数报告间隔:%d\n", tickerInterval) ticker := time.NewTicker(time.Duration(tickerInterval) * time.Second) go func() { for range ticker.C { log.Printf("[Ticker] 当前连接数:%d\n", sockhandler.Conns.Count()) } }() sockhandler.Run(port, filePath) }
代码目录结构如下:
这是下载服务器程序所在目录:
这是客户端测试 Demo:
让两者运行并交互起来:
下载不存在的文件:
退出测试客户端:
从服务器下载文件后的客户端目录:
文件下载服务器代码已上传至 Github。
客户端测试 Demo 下载链接在这里。