印象里,《传奇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
", err.Error())
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
Conns.Add(conn.RemoteAddr().String(), conn)
log.Printf("当前连接数:%d
", 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
", 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
", conn.RemoteAddr().String())
reponsePing(conn)
case CM_GETFILE:
fmt.Printf("From %s received CM_GETFILE
", conn.RemoteAddr().String())
responseDownloadFile( /*string(msg.FileName[:])*/ msg.FileName, conn)
default:
fmt.Printf("From %s received %d
", conn.RemoteAddr().String(), msg.Cmd)
}
offsize += pkglen
} else {
offsize++
fmt.Printf("From %s received %d
", 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
", port)
fmt.Printf("文件目录:%s
", filePath)
fmt.Printf("连接数报告间隔:%d
", tickerInterval)
ticker := time.NewTicker(time.Duration(tickerInterval) * time.Second)
go func() {
for range ticker.C {
log.Printf("[Ticker] 当前连接数:%d
", sockhandler.Conns.Count())
}
}()
sockhandler.Run(port, filePath)
}
代码目录结构如下:

这是下载服务器程序所在目录:

这是客户端测试 Demo:

让两者运行并交互起来:

下载不存在的文件:

退出测试客户端:

从服务器下载文件后的客户端目录:

文件下载服务器代码已上传至 Github。
客户端测试 Demo 下载链接在这里。