zoukankan      html  css  js  c++  java
  • 迷你文件下载服务器

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

  • 相关阅读:
    CenOS下LAMP搭建过程
    CentOS下将自编译的Apache添加为系统服务
    CentOS下编译安装Apache(httpd)
    CentOS6下配置Django+Apache+mod_wsgi+Sqlite3过程
    Python格式化输出
    Python里如何实现C中switch...case的功能
    Python科学计算学习一 NumPy 快速处理数据
    每个程序员都应该学习使用Python或Ruby
    Python IDLE中实现清屏
    Graphviz 可视化代码流程
  • 原文地址:https://www.cnblogs.com/ecofast/p/6378422.html
Copyright © 2011-2022 走看看