迷你文件下载服务器

  印象里,《传奇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 下载链接在这里

posted @ 2017-02-08 15:41  ecofast  阅读(2176)  评论(1编辑  收藏  举报