├── .gitignore ├── pb ├── gen.sh ├── message.proto └── message.pb.go ├── cmd ├── example_client │ ├── create_room.sh │ └── main.go └── example_server │ ├── main.go │ └── api │ ├── api.go │ └── index.html ├── pkg ├── packet │ └── pb_packet │ │ ├── testdata │ │ ├── testdata.proto │ │ └── testdata.pb.go │ │ ├── pb_packet.go │ │ └── protocol_test.go ├── ipx │ └── ip.go ├── kcp_server │ ├── server.go │ └── example_test.go ├── network │ ├── protocol.go │ ├── server.go │ └── conn.go └── log4gox │ └── color_logger.go ├── .travis.yml ├── go.mod ├── server ├── server.go └── router.go ├── logic ├── game │ ├── player.go │ ├── lockstep.go │ └── game.go ├── manager.go └── room │ └── room.go ├── README.md ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | *.code-workspace 4 | *.test 5 | debug 6 | *.exe 7 | -------------------------------------------------------------------------------- /pb/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | protoc --go_out=. --go_opt=paths=source_relative *.proto -------------------------------------------------------------------------------- /cmd/example_client/create_room.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl 'http://localhost/create?room=1&member=1,2' -------------------------------------------------------------------------------- /pkg/packet/pb_packet/testdata/testdata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package testdata; 4 | option go_package = "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet/testdata;testdata"; 5 | 6 | 7 | //消息ID 8 | enum ID { 9 | 10 | MSG_BEGIN = 0; 11 | 12 | 13 | MSG_Test = 60; 14 | 15 | MSG_END = 255; 16 | } 17 | 18 | 19 | // TestMsg 20 | message TestMsg { 21 | optional int32 sid = 1; //操作id 22 | optional int32 x = 2; //操作位置x 23 | optional int32 y = 3; //操作位置y 24 | optional uint32 frameID = 4; //帧ID 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.17.x 5 | 6 | 7 | env: 8 | - TRAVIS=1 PYTHONWARNINGS=ignore 9 | 10 | services: 11 | #- mysql 12 | #- mongodb 13 | #- redis-server 14 | 15 | #before_install: 16 | #go 17 | #- DEP_OS_ARCH=`go env GOHOSTOS`-`go env GOHOSTARCH` 18 | #- DEP_LATEST_RELEASE_URL="https://github.com/golang/dep/releases/download/v0.3.2/dep-${DEP_OS_ARCH}" 19 | #- wget ${DEP_LATEST_RELEASE_URL} -O /tmp/dep 20 | #- export PATH=$PATH:/tmp/ 21 | #- chmod +x /tmp/dep 22 | 23 | #- mysql -e 'CREATE DATABASE goworld;' 24 | 25 | 26 | script: 27 | #- go test -v ./... 28 | - cd example 29 | - go build -------------------------------------------------------------------------------- /pkg/ipx/ip.go: -------------------------------------------------------------------------------- 1 | package ipx 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // GetExternalIP 获取公网IP 11 | func GetExternalIP() string { 12 | resp, err := http.Get("http://myexternalip.com/raw") 13 | if err != nil { 14 | return "" 15 | } 16 | defer resp.Body.Close() 17 | content, _ := ioutil.ReadAll(resp.Body) 18 | return strings.TrimSpace(string(content)) 19 | } 20 | 21 | // GetLocalIP 获得内网IP 22 | func GetLocalIP() string { 23 | addrs, err := net.InterfaceAddrs() 24 | 25 | if err != nil { 26 | return "" 27 | } 28 | 29 | for _, address := range addrs { 30 | 31 | // 检查ip地址判断是否回环地址 32 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 33 | if ipnet.IP.To4() != nil { 34 | return ipnet.IP.String() 35 | } 36 | 37 | } 38 | } 39 | return "" 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/byebyebruce/lockstepserver 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/alecthomas/log4go v0.0.0-20180109082532-d146e6b86faa 7 | github.com/golang/protobuf v1.5.0 8 | github.com/xtaci/kcp-go v5.4.20+incompatible 9 | google.golang.org/protobuf v1.26.0 10 | ) 11 | 12 | require ( 13 | github.com/klauspost/cpuid v1.2.3 // indirect 14 | github.com/klauspost/reedsolomon v1.9.3 // indirect 15 | github.com/pkg/errors v0.9.1 // indirect 16 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect 17 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect 18 | github.com/tjfoc/gmsm v1.3.0 // indirect 19 | github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 // indirect 20 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect 21 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect 22 | golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/byebyebruce/lockstepserver/logic" 5 | "github.com/byebyebruce/lockstepserver/pkg/kcp_server" 6 | "github.com/byebyebruce/lockstepserver/pkg/network" 7 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet" 8 | ) 9 | 10 | // LockStepServer 帧同步服务器 11 | type LockStepServer struct { 12 | roomMgr *logic.RoomManager 13 | udpServer *network.Server 14 | totalConn int64 15 | } 16 | 17 | // New 构造 18 | func New(address string) (*LockStepServer, error) { 19 | s := &LockStepServer{ 20 | roomMgr: logic.NewRoomManager(), 21 | } 22 | networkServer, err := kcp_server.ListenAndServe(address, s, &pb_packet.MsgProtocol{}) 23 | if err != nil { 24 | return nil, err 25 | } 26 | s.udpServer = networkServer 27 | return s, nil 28 | } 29 | 30 | // RoomManager 获取房间管理器 31 | func (r *LockStepServer) RoomManager() *logic.RoomManager { 32 | return r.roomMgr 33 | } 34 | 35 | // Stop 停止服务 36 | func (r *LockStepServer) Stop() { 37 | r.roomMgr.Stop() 38 | r.udpServer.Stop() 39 | } 40 | -------------------------------------------------------------------------------- /pkg/kcp_server/server.go: -------------------------------------------------------------------------------- 1 | package kcp_server 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/byebyebruce/lockstepserver/pkg/network" 8 | "github.com/xtaci/kcp-go" 9 | ) 10 | 11 | func ListenAndServe(addr string, callback network.ConnCallback, protocol network.Protocol) (*network.Server, error) { 12 | dupConfig := &network.Config{ 13 | PacketReceiveChanLimit: 1024, 14 | PacketSendChanLimit: 1024, 15 | ConnReadTimeout: time.Second * 5, 16 | ConnWriteTimeout: time.Second * 5, 17 | } 18 | 19 | l, err := kcp.Listen(addr) 20 | if nil != err { 21 | return nil, err 22 | } 23 | 24 | server := network.NewServer(dupConfig, callback, protocol) 25 | go server.Start(l, func(conn net.Conn, i *network.Server) *network.Conn { 26 | 27 | // 普通模式 28 | // setKCPConfig(32, 32, 0, 40, 0, 0, 100, 1400) 29 | 30 | // 极速模式 31 | // setKCPConfig(32, 32, 1, 10, 2, 1, 30, 1400) 32 | 33 | // 普通模式:ikcp_nodelay(kcp, 0, 40, 0, 0); 极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1); 34 | 35 | kcpConn := conn.(*kcp.UDPSession) 36 | kcpConn.SetNoDelay(1, 10, 2, 1) 37 | kcpConn.SetStreamMode(true) 38 | kcpConn.SetWindowSize(4096, 4096) 39 | kcpConn.SetReadBuffer(4 * 1024 * 1024) 40 | kcpConn.SetWriteBuffer(4 * 1024 * 1024) 41 | kcpConn.SetACKNoDelay(true) 42 | 43 | return network.NewConn(conn, server) 44 | }) 45 | 46 | return server, nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/example_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/byebyebruce/lockstepserver/cmd/example_server/api" 12 | "github.com/byebyebruce/lockstepserver/pkg/log4gox" 13 | "github.com/byebyebruce/lockstepserver/server" 14 | 15 | l4g "github.com/alecthomas/log4go" 16 | ) 17 | 18 | var ( 19 | httpAddress = flag.String("web", ":80", "web listen address") 20 | udpAddress = flag.String("udp", ":10086", "udp listen address(':10086' means localhost:10086)") 21 | debugLog = flag.Bool("log", true, "debug log") 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | l4g.Close() 28 | l4g.AddFilter("debug logger", l4g.DEBUG, log4gox.NewColorConsoleLogWriter()) 29 | 30 | s, err := server.New(*udpAddress) 31 | if err != nil { 32 | panic(err) 33 | } 34 | _ = api.NewWebAPI(*httpAddress, s.RoomManager()) 35 | 36 | sigs := make(chan os.Signal, 1) 37 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, os.Interrupt) 38 | ticker := time.NewTimer(time.Minute) 39 | defer ticker.Stop() 40 | 41 | l4g.Info("[main] start...") 42 | // 主循环 43 | QUIT: 44 | for { 45 | select { 46 | case sig := <-sigs: 47 | l4g.Info("Signal: %s", sig.String()) 48 | break QUIT 49 | case <-ticker.C: 50 | // todo 51 | fmt.Println("room number ", s.RoomManager().RoomNum()) 52 | } 53 | } 54 | l4g.Info("[main] quiting...") 55 | s.Stop() 56 | } 57 | -------------------------------------------------------------------------------- /pkg/network/protocol.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | type Packet interface { 10 | Serialize() []byte 11 | } 12 | 13 | type Protocol interface { 14 | ReadPacket(conn io.Reader) (Packet, error) 15 | } 16 | 17 | type DefaultPacket struct { 18 | buff []byte 19 | } 20 | 21 | func (this *DefaultPacket) Serialize() []byte { 22 | return this.buff 23 | } 24 | 25 | func (this *DefaultPacket) GetBody() []byte { 26 | return this.buff[4:] 27 | } 28 | 29 | func NewDefaultPacket(buff []byte) *DefaultPacket { 30 | p := &DefaultPacket{} 31 | 32 | p.buff = make([]byte, 4+len(buff)) 33 | binary.BigEndian.PutUint32(p.buff[0:4], uint32(len(buff))) 34 | copy(p.buff[4:], buff) 35 | 36 | return p 37 | } 38 | 39 | type DefaultProtocol struct { 40 | } 41 | 42 | func (this *DefaultProtocol) ReadPacket(r io.Reader) (Packet, error) { 43 | var ( 44 | lengthBytes []byte = make([]byte, 4) 45 | length uint32 46 | ) 47 | 48 | // read length 49 | if _, err := io.ReadFull(r, lengthBytes); err != nil { 50 | return nil, err 51 | } 52 | if length = binary.BigEndian.Uint32(lengthBytes); length > 1024 { 53 | return nil, errors.New("the size of packet is larger than the limit") 54 | } 55 | 56 | buff := make([]byte, length) 57 | 58 | // read body ( buff = lengthBytes + body ) 59 | if _, err := io.ReadFull(r, buff); err != nil { 60 | return nil, err 61 | } 62 | 63 | return NewDefaultPacket(buff), nil 64 | } 65 | -------------------------------------------------------------------------------- /logic/game/player.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/byebyebruce/lockstepserver/pkg/network" 7 | ) 8 | 9 | type Player struct { 10 | id uint64 11 | idx int32 12 | isReady bool 13 | isOnline bool 14 | loadingProgress int32 15 | lastHeartbeatTime int64 16 | sendFrameCount uint32 17 | client *network.Conn 18 | } 19 | 20 | func NewPlayer(id uint64, idx int32) *Player { 21 | p := &Player{ 22 | id: id, 23 | idx: idx, 24 | } 25 | 26 | return p 27 | } 28 | 29 | func (p *Player) Connect(conn *network.Conn) { 30 | p.client = conn 31 | p.isOnline = true 32 | p.isReady = false 33 | p.lastHeartbeatTime = time.Now().Unix() 34 | } 35 | 36 | func (p *Player) IsOnline() bool { 37 | return nil != p.client && p.isOnline 38 | } 39 | 40 | func (p *Player) RefreshHeartbeatTime() { 41 | p.lastHeartbeatTime = time.Now().Unix() 42 | } 43 | 44 | func (p *Player) GetLastHeartbeatTime() int64 { 45 | return p.lastHeartbeatTime 46 | } 47 | 48 | func (p *Player) SetSendFrameCount(c uint32) { 49 | p.sendFrameCount = c 50 | } 51 | 52 | func (p *Player) GetSendFrameCount() uint32 { 53 | return p.sendFrameCount 54 | } 55 | 56 | func (p *Player) SendMessage(msg network.Packet) { 57 | 58 | if !p.IsOnline() { 59 | return 60 | } 61 | 62 | if nil != p.client.AsyncWritePacket(msg, 0) { 63 | p.client.Close() 64 | } 65 | } 66 | 67 | func (p *Player) Cleanup() { 68 | 69 | if nil != p.client { 70 | p.client.Close() 71 | } 72 | p.client = nil 73 | p.isReady = false 74 | p.isOnline = false 75 | 76 | } 77 | -------------------------------------------------------------------------------- /logic/manager.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/byebyebruce/lockstepserver/logic/room" 8 | ) 9 | 10 | // RoomManager 房间管理器 11 | type RoomManager struct { 12 | room map[uint64]*room.Room 13 | wg sync.WaitGroup 14 | rw sync.RWMutex 15 | } 16 | 17 | // NewRoomManager 构造 18 | func NewRoomManager() *RoomManager { 19 | m := &RoomManager{ 20 | room: make(map[uint64]*room.Room), 21 | } 22 | return m 23 | } 24 | 25 | // CreateRoom 创建房间 26 | func (m *RoomManager) CreateRoom(id uint64, typeID int32, playerID []uint64, randomSeed int32, logicServer string) (*room.Room, error) { 27 | m.rw.Lock() 28 | defer m.rw.Unlock() 29 | 30 | r, ok := m.room[id] 31 | if ok { 32 | return nil, fmt.Errorf("room id[%d] exists", id) 33 | } 34 | 35 | r = room.NewRoom(id, typeID, playerID, randomSeed, logicServer) 36 | m.room[id] = r 37 | 38 | go func() { 39 | m.wg.Add(1) 40 | defer func() { 41 | m.rw.Lock() 42 | delete(m.room, id) 43 | m.rw.Unlock() 44 | 45 | m.wg.Done() 46 | }() 47 | r.Run() 48 | 49 | }() 50 | 51 | return r, nil 52 | } 53 | 54 | // GetRoom 获得房间 55 | func (m *RoomManager) GetRoom(id uint64) *room.Room { 56 | 57 | m.rw.RLock() 58 | defer m.rw.RUnlock() 59 | 60 | r, _ := m.room[id] 61 | return r 62 | } 63 | 64 | // RoomNum 获得房间数量 65 | func (m *RoomManager) RoomNum() int { 66 | 67 | m.rw.RLock() 68 | defer m.rw.RUnlock() 69 | 70 | return len(m.room) 71 | } 72 | 73 | // Stop 停止 74 | func (m *RoomManager) Stop() { 75 | 76 | m.rw.Lock() 77 | for _, v := range m.room { 78 | v.Stop() 79 | } 80 | m.room = make(map[uint64]*room.Room) 81 | m.rw.Unlock() 82 | 83 | m.wg.Wait() 84 | } 85 | -------------------------------------------------------------------------------- /logic/game/lockstep.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/byebyebruce/lockstepserver/pb" 5 | ) 6 | 7 | type frameData struct { 8 | idx uint32 9 | cmds []*pb.InputData 10 | } 11 | 12 | func newFrameData(index uint32) *frameData { 13 | f := &frameData{ 14 | idx: index, 15 | cmds: make([]*pb.InputData, 0), 16 | } 17 | 18 | return f 19 | } 20 | 21 | type lockstep struct { 22 | frames map[uint32]*frameData 23 | frameCount uint32 24 | } 25 | 26 | func newLockstep() *lockstep { 27 | l := &lockstep{ 28 | frames: make(map[uint32]*frameData), 29 | } 30 | 31 | return l 32 | } 33 | 34 | func (l *lockstep) reset() { 35 | l.frames = make(map[uint32]*frameData) 36 | l.frameCount = 0 37 | } 38 | 39 | func (l *lockstep) getFrameCount() uint32 { 40 | return l.frameCount 41 | } 42 | 43 | func (l *lockstep) pushCmd(cmd *pb.InputData) bool { 44 | f, ok := l.frames[l.frameCount] 45 | if !ok { 46 | f = newFrameData(l.frameCount) 47 | l.frames[l.frameCount] = f 48 | } 49 | 50 | // 检查是否同一帧发来两次操作 51 | for _, v := range f.cmds { 52 | if v.Id == cmd.Id { 53 | return false 54 | } 55 | } 56 | 57 | f.cmds = append(f.cmds, cmd) 58 | 59 | return true 60 | } 61 | 62 | func (l *lockstep) tick() uint32 { 63 | l.frameCount++ 64 | return l.frameCount 65 | } 66 | 67 | func (l *lockstep) getRangeFrames(from, to uint32) []*frameData { 68 | ret := make([]*frameData, 0, to-from) 69 | 70 | for ; from <= to && from <= l.frameCount; from++ { 71 | f, ok := l.frames[from] 72 | if !ok { 73 | continue 74 | } 75 | ret = append(ret, f) 76 | } 77 | 78 | return ret 79 | } 80 | 81 | func (l *lockstep) getFrame(idx uint32) *frameData { 82 | 83 | return l.frames[idx] 84 | } 85 | -------------------------------------------------------------------------------- /pkg/network/server.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type Config struct { 10 | PacketSendChanLimit uint32 // the limit of packet send channel 11 | PacketReceiveChanLimit uint32 // the limit of packet receive channel 12 | ConnReadTimeout time.Duration // read timeout 13 | ConnWriteTimeout time.Duration // write timeout 14 | } 15 | 16 | type Server struct { 17 | config *Config // server configuration 18 | callback ConnCallback // message callbacks in connection 19 | protocol Protocol // customize packet protocol 20 | exitChan chan struct{} // notify all goroutines to shutdown 21 | waitGroup *sync.WaitGroup // wait for all goroutines 22 | closeOnce sync.Once 23 | listener net.Listener 24 | } 25 | 26 | // NewServer creates a server 27 | func NewServer(config *Config, callback ConnCallback, protocol Protocol) *Server { 28 | return &Server{ 29 | config: config, 30 | callback: callback, 31 | protocol: protocol, 32 | exitChan: make(chan struct{}), 33 | waitGroup: &sync.WaitGroup{}, 34 | } 35 | } 36 | 37 | type ConnectionCreator func(net.Conn, *Server) *Conn 38 | 39 | // Start starts service 40 | func (s *Server) Start(listener net.Listener, create ConnectionCreator) { 41 | s.listener = listener 42 | s.waitGroup.Add(1) 43 | defer func() { 44 | s.waitGroup.Done() 45 | }() 46 | 47 | for { 48 | select { 49 | case <-s.exitChan: 50 | return 51 | 52 | default: 53 | } 54 | 55 | conn, err := listener.Accept() 56 | if err != nil { 57 | continue 58 | } 59 | 60 | s.waitGroup.Add(1) 61 | go func() { 62 | create(conn, s).Do() 63 | s.waitGroup.Done() 64 | }() 65 | } 66 | } 67 | 68 | // Stop stops service 69 | func (s *Server) Stop() { 70 | s.closeOnce.Do(func() { 71 | close(s.exitChan) 72 | s.listener.Close() 73 | }) 74 | 75 | s.waitGroup.Wait() 76 | } 77 | -------------------------------------------------------------------------------- /cmd/example_server/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/byebyebruce/lockstepserver/logic" 13 | ) 14 | 15 | //go:embed index.html 16 | var index string 17 | 18 | // WebAPI http api 19 | type WebAPI struct { 20 | m *logic.RoomManager 21 | } 22 | 23 | // NewWebAPI 构造 24 | func NewWebAPI(addr string, m *logic.RoomManager) *WebAPI { 25 | r := &WebAPI{ 26 | m: m, 27 | } 28 | 29 | http.HandleFunc("/", r.index) 30 | http.HandleFunc("/create", r.createRoom) 31 | 32 | go func() { 33 | fmt.Println("web api listen on", addr) 34 | e := http.ListenAndServe(addr, nil) 35 | if nil != e { 36 | panic(e) 37 | } 38 | }() 39 | 40 | return r 41 | } 42 | 43 | func (h *WebAPI) index(w http.ResponseWriter, r *http.Request) { 44 | 45 | query := r.URL.Query() 46 | if 0 == len(query) { 47 | t, err := template.New("test").Parse(index) 48 | if err != nil { 49 | w.Write([]byte("error")) 50 | } else { 51 | t.Execute(w, nil) 52 | } 53 | return 54 | } 55 | } 56 | 57 | func (h *WebAPI) createRoom(w http.ResponseWriter, r *http.Request) { 58 | 59 | ret := "error" 60 | 61 | defer func() { 62 | w.Write([]byte(ret)) 63 | }() 64 | 65 | query := r.URL.Query() 66 | 67 | roomStr := query.Get("room") 68 | roomID, _ := strconv.ParseUint(roomStr, 10, 64) 69 | 70 | ps := make([]uint64, 0, 10) 71 | 72 | members := query.Get("member") 73 | if len(members) > 0 { 74 | 75 | a := strings.Split(members, ",") 76 | 77 | for _, v := range a { 78 | id, _ := strconv.ParseUint(v, 10, 64) 79 | ps = append(ps, id) 80 | } 81 | 82 | } 83 | 84 | room, err := h.m.CreateRoom(roomID, 0, ps, 0, "test") 85 | if nil != err { 86 | ret = err.Error() 87 | } else { 88 | ret = fmt.Sprintf("room.ID=[%d] room.Secret=[%s] room.Time=[%d], room.Member=[%v]", room.ID(), room.SecretKey(), room.TimeStamp(), members) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /cmd/example_server/api/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 |41 | 42 |
43 | 44 |45 | 46 |
47 | 48 | 49 |50 | 51 |
52 | 53 | 54 | 55 | 56 | 89 | 90 | -------------------------------------------------------------------------------- /pkg/log4gox/color_logger.go: -------------------------------------------------------------------------------- 1 | package log4gox 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | l4g "github.com/alecthomas/log4go" 9 | ) 10 | 11 | var stdout io.Writer = os.Stdout 12 | 13 | /* 14 | 前景色 背景色 颜色 15 | --------------------------------------- 16 | 30 40 黑色 17 | 31 41 红色 18 | 32 42 绿色 19 | 33 43 黃色 20 | 34 44 蓝色 21 | 35 45 紫红色 22 | 36 46 青蓝色 23 | 37 47 白色 24 | */ 25 | var ( 26 | levelColor = [...]int{30, 30, 32, 37, 37, 33, 31, 34} 27 | levelStrings = [...]string{"FNST", "FINE", "DEBG", "TRAC", "INFO", "WARN", "EROR", "CRIT"} 28 | ) 29 | 30 | /* 31 | fmt.Printf("%c[%dm(1 2 3 4)%c[0m ", 0x1B, 30, 0x1B) 32 | 33 | for b := 40; b <= 47; b++ { // 背景色彩 = 40-47 34 | for f := 30; f <= 37; f++ { // 前景色彩 = 30-37 35 | for d := range []int{0, 1, 4, 5, 7, 8} { // 显示方式 = 0,1,4,5,7,8 36 | fmt.Fprintf(os.Stderr, " %c[%d;%d;%dm%s(1 2 3 4 )%c[0m ", 0x1B, d, b, f, "", 0x1B) 37 | } 38 | fmt.Println("") 39 | } 40 | fmt.Println("") 41 | } 42 | */ 43 | 44 | const ( 45 | colorSymbol = 0x1B 46 | ) 47 | 48 | // This is the standard writer that prints to standard output. 49 | type ConsoleLogWriter chan *l4g.LogRecord 50 | 51 | // This creates a new ConsoleLogWriter 52 | func NewColorConsoleLogWriter() ConsoleLogWriter { 53 | records := make(ConsoleLogWriter, l4g.LogBufferLength) 54 | go records.run(stdout) 55 | return records 56 | } 57 | 58 | func (w ConsoleLogWriter) run(out io.Writer) { 59 | var timestr string 60 | var timestrAt int64 61 | 62 | for rec := range w { 63 | if at := rec.Created.UnixNano() / 1e9; at != timestrAt { 64 | timestr, timestrAt = rec.Created.Format("01/02/06 15:04:05"), at 65 | } 66 | fmt.Fprintf(out, "%c[%dm[%s] [%s] (%s) %s\n%c[0m", 67 | colorSymbol, 68 | levelColor[rec.Level], 69 | timestr, 70 | levelStrings[rec.Level], 71 | rec.Source, 72 | rec.Message, 73 | colorSymbol) 74 | } 75 | } 76 | 77 | // This is the ConsoleLogWriter's output method. This will block if the output 78 | // buffer is full. 79 | func (w ConsoleLogWriter) LogWrite(rec *l4g.LogRecord) { 80 | w <- rec 81 | } 82 | 83 | // Close stops the logger from sending messages to standard output. Attempts to 84 | // send log messages to this logger after a Close have undefined behavior. 85 | func (w ConsoleLogWriter) Close() { 86 | close(w) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/example_client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/byebyebruce/lockstepserver/pb" 9 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet" 10 | "github.com/golang/protobuf/proto" 11 | 12 | "github.com/xtaci/kcp-go" 13 | ) 14 | 15 | var ( 16 | addr = flag.String("udp", "127.0.0.1:10086", "connect udp address") 17 | msg = flag.String("msg", "PING", "message you want to send") 18 | room = flag.Uint64("room", 1, "room id") 19 | id = flag.Uint64("id", 1, "my id") 20 | ) 21 | 22 | func main() { 23 | 24 | flag.Parse() 25 | 26 | fmt.Println("addr", *addr, "room", *room, "id", *id) 27 | 28 | ms := &pb_packet.MsgProtocol{} 29 | 30 | c, e := kcp.Dial(*addr) 31 | if nil != e { 32 | panic(e) 33 | } 34 | defer c.Close() 35 | 36 | // read 37 | go func() { 38 | for { 39 | // c.SetReadDeadline(time.Now().Add(10*time.Second)) 40 | n, e := ms.ReadPacket(c) 41 | if nil != e { 42 | fmt.Println("read error:", e.Error()) 43 | return 44 | } 45 | 46 | // n.Serialize() 47 | ret := n.(*pb_packet.Packet) 48 | id := pb.ID(ret.GetMessageID()) 49 | fmt.Println("receive msg ", id.String()) 50 | switch id { 51 | case pb.ID_MSG_Connect: 52 | msg := &pb.S2C_ConnectMsg{} 53 | proto.Unmarshal(ret.GetData(), msg) 54 | if msg.GetErrorCode() != pb.ERRORCODE_ERR_Ok { 55 | panic(msg.GetErrorCode()) 56 | } 57 | fmt.Println(msg) 58 | case pb.ID_MSG_Frame: 59 | msg := &pb.S2C_FrameMsg{} 60 | proto.Unmarshal(ret.GetData(), msg) 61 | fmt.Println(msg) 62 | default: 63 | 64 | } 65 | } 66 | }() 67 | 68 | // connect 69 | if _, e := c.Write(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), &pb.C2S_ConnectMsg{ 70 | PlayerID: proto.Uint64(*id), 71 | BattleID: proto.Uint64(*room), 72 | }).Serialize()); nil != e { 73 | panic(fmt.Sprintf("write error:%s", e.Error())) 74 | } 75 | time.Sleep(time.Second) 76 | // ready 77 | if _, e := c.Write(pb_packet.NewPacket(uint8(pb.ID_MSG_JoinRoom), nil).Serialize()); nil != e { 78 | panic(fmt.Sprintf("write error:%s", e.Error())) 79 | } 80 | time.Sleep(time.Second) 81 | // ready 82 | if _, e := c.Write(pb_packet.NewPacket(uint8(pb.ID_MSG_Ready), nil).Serialize()); nil != e { 83 | panic(fmt.Sprintf("write error:%s", e.Error())) 84 | } 85 | time.Sleep(time.Second) 86 | // write 87 | for i := 0; i < 10; i++ { 88 | p := pb_packet.NewPacket(uint8(pb.ID_MSG_Input), &pb.C2S_InputMsg{ 89 | Sid: proto.Int32(int32(i)), 90 | }) 91 | 92 | if _, e := c.Write(p.Serialize()); nil != e { 93 | panic(fmt.Sprintf("write error:%s", e.Error())) 94 | } 95 | 96 | time.Sleep(time.Second) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/packet/pb_packet/pb_packet.go: -------------------------------------------------------------------------------- 1 | package pb_packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | 8 | l4g "github.com/alecthomas/log4go" 9 | "github.com/byebyebruce/lockstepserver/pkg/network" 10 | "github.com/golang/protobuf/proto" 11 | ) 12 | 13 | const ( 14 | DataLen = 2 15 | MessageIDLen = 1 16 | 17 | MinPacketLen = DataLen + MessageIDLen 18 | MaxPacketLen = (2 << 8) * DataLen 19 | MaxMessageID = (2 << 8) * MessageIDLen 20 | ) 21 | 22 | /* 23 | 24 | s->c 25 | 26 | |--totalDataLen(uint16)--|--msgIDLen(uint8)--|--------------data--------------| 27 | |-------------2----------|---------1---------|---------(totalDataLen-2-1)-----| 28 | 29 | */ 30 | 31 | // Packet 服务端发往客户端的消息 32 | type Packet struct { 33 | id uint8 34 | data []byte 35 | } 36 | 37 | func (p *Packet) GetMessageID() uint8 { 38 | return p.id 39 | } 40 | 41 | func (p *Packet) GetData() []byte { 42 | return p.data 43 | } 44 | 45 | func (p *Packet) Serialize() []byte { 46 | buff := make([]byte, MinPacketLen, MinPacketLen) 47 | 48 | dataLen := len(p.data) 49 | binary.BigEndian.PutUint16(buff, uint16(dataLen)) 50 | 51 | buff[DataLen] = p.id 52 | return append(buff, p.data...) 53 | } 54 | 55 | func (p *Packet) Unmarshal(m interface{}) error { 56 | return proto.Unmarshal(p.data, m.(proto.Message)) 57 | } 58 | 59 | func NewPacket(id uint8, msg interface{}) *Packet { 60 | 61 | p := &Packet{ 62 | id: id, 63 | } 64 | 65 | switch v := msg.(type) { 66 | case []byte: 67 | p.data = v 68 | case proto.Message: 69 | if mdata, err := proto.Marshal(v); err == nil { 70 | p.data = mdata 71 | } else { 72 | l4g.Error("[NewPacket] proto marshal msg: %d error: %v", 73 | id, err) 74 | return nil 75 | } 76 | case nil: 77 | default: 78 | l4g.Error("[NewPacket] error msg type msg: %d", id) 79 | return nil 80 | } 81 | 82 | return p 83 | } 84 | 85 | type MsgProtocol struct { 86 | } 87 | 88 | func (p *MsgProtocol) ReadPacket(r io.Reader) (network.Packet, error) /*Packet*/ { 89 | 90 | buff := make([]byte, MinPacketLen, MinPacketLen) 91 | 92 | // data length 93 | if _, err := io.ReadFull(r, buff); err != nil { 94 | return nil, err 95 | } 96 | dataLen := binary.BigEndian.Uint16(buff) 97 | 98 | if dataLen > MaxPacketLen { 99 | return nil, errors.New("data max") 100 | } 101 | 102 | // id 103 | msg := &Packet{ 104 | id: buff[DataLen], 105 | } 106 | 107 | // data 108 | if dataLen > 0 { 109 | msg.data = make([]byte, dataLen, dataLen) 110 | if _, err := io.ReadFull(r, msg.data); err != nil { 111 | return nil, err 112 | } 113 | } 114 | 115 | return msg, nil 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lock Step Server 2 | 3 | [](https://godoc.org/github.com/byebyebruce/lockstepserver) 4 | [](https://travis-ci.org/byebyebruce/lockstepserver) 5 | [](https://goreportcard.com/report/github.com/byebyebruce/lockstepserver) 6 | 7 | _**用golang写的帧同步服务器,目标是作为一个可以横向扩展,完全脱离玩法逻辑的帧同步服务器。**_ 8 | 9 | ### 特性 10 | * 采用KCP(可根据需求改成其他协议)作为网络底层 11 | * 采用帧同步方式 12 | * protobuf作为传输协议 13 | * 支持断线重连 14 | 15 | 16 | ### 运行example server 17 | 1. 启动server `go run cmd/example_server/main.go` 18 | 1. 创建房间: 19 | * 方法1. 浏览器打开 http://localhost 点创建 20 | * 方法2. 命令 `sh cmd/example_client/create_room.sh` 21 | 22 | ### 运行example client 23 | 1. 启动1号客户端 `go run cmd/example_client/main.go -room=1 -id=1` 24 | 1. 启动2号客户端 `go run cmd/example_client/main.go -room=1 -id=2` 25 | 26 | ### 网络层 27 | * 初始化网络层,使用的[kcp-go](https://github.com/xtaci/kcp-go),可以根据需求切换成其他的 28 | * 消息包格式 29 | ``` 30 | |-----------------------------message-----------------------------------------| 31 | |----------------------Header------------------|------------Body--------------| 32 | |------Body Length-------|--------Msg ID-------|------------Body--------------| 33 | |---------uint16---------|---------uint8-------|------------bytes-------------| 34 | |-----------2------------|----------1----------|-----------len(Body)----------| 35 | ``` 36 | 37 | ### 客户端接入流程 38 | [**proto文件**](pb/message.proto) 39 | 40 | 41 | * 消息流程 42 | 1. 客户端发送第一个连接的消息包 43 | C->S: `MSG_Connect & C2S_ConnectMsg` 44 | 1. 服务端给返回连接结果 45 | S->C: `MSG_Connect & S2C_ConnectMsg` 46 | 1. 如果2返回ok,客户端向服务端发送进入房间消息 47 | C->S: `MSG_JoinRoom` 48 | 1. 服务端返回进入房间消息 49 | S->C: `MSG_Connect & S2C_JoinRoomMsg` 50 | 1. 客户端这时进入读条,并广播读条进度,其他客户端收到广播读条进度 51 | C->S: `MSG_Progress & C2S_ProgressMsg` 52 | S->C: `MSG_Progress & S2C_ProgressMsg` **注:广播者收不到这个消息** 53 | 1. 客户端告诉服务端自己已经准备好 54 | C->S: `MSG_Ready` 55 | S->C: `MSG_Ready` 56 | 1. 当所有客户端都已经准备好,服务端广播开始 57 | S->C: `MSG_Start` 58 | 1. 客户端可以进入游戏状态,客户端不停的向服务端发送操作,服务端不停的广播帧数据 59 | ∞ C->S: `MSG_Input & C2S_InputMsg` 60 | ∞ S->C: `MSG_Frame & S2C_FrameMsg` 61 | 1. 当客户端游戏逻辑结束告诉服务端自己结束 62 | C->S: `MSG_Result & C2S_ResultMsg` 63 | S->C: `MSG_Result` 64 | 1. 当客户端收到`MSG_Result`或者`MSG_Close`客户端断开网络连接进入其他流程 65 | **注:客户端收到MSG_Result表示服务端已经收到并处理的客户端发来的结果** 66 | **注:客户端收到MSG_Close表示服务端房间已经关闭,客户端如果游戏流程没完也要强制退出** 67 | 68 | 69 | 70 | ### 断线重连 71 | 72 | * 客户端只要发 C->S: `MSG_Connect & C2S_ConnectMsg` **(前提是当前游戏房间还存在)**即可进入房间,服务端会把之前的帧分批次发给客户端。(这里可以考虑改成客户端请求缺失的帧) 73 | 74 | 75 | 76 | ### 客户端工程 77 | [https://github.com/byebyebruce/lockstep-client-unity](https://github.com/byebyebruce/lockstep-client-unity) 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /pb/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | option go_package = "github.com/byebyebruce/lockstepserver/pb;pb"; 5 | 6 | 7 | //消息ID 8 | enum ID { 9 | 10 | MSG_BEGIN = 0; 11 | 12 | MSG_Connect = 1; //连接(客户端发来第一个消息) 13 | MSG_Heartbeat = 2; //心跳(服务端返回Connect成功之后每隔1秒发送一个心跳包) 14 | 15 | MSG_JoinRoom = 10; //进入 16 | MSG_Progress = 20; //进度 17 | MSG_Ready = 30; //准备 18 | MSG_Start = 40; //开始 19 | MSG_Frame = 50; //帧数据 20 | MSG_Input = 60; //输入 21 | MSG_Result = 70; //结果 22 | 23 | MSG_Close = 100; //房间关闭 24 | 25 | MSG_END = 255; 26 | } 27 | 28 | //错误码 29 | enum ERRORCODE { 30 | ERR_Ok = 0; //OK 31 | ERR_NoPlayer = 1; //没有这个玩家 32 | ERR_NoRoom = 2; //没有房间 33 | ERR_RoomState = 3; //房间状态不正确 34 | ERR_Token = 4; //Token验证失败 35 | } 36 | 37 | //客户端发来的第一个消息 38 | message C2S_ConnectMsg { 39 | optional uint64 playerID = 1; //唯一ID 40 | optional uint64 battleID = 2; //战斗ID 41 | optional string token = 10; //令牌 42 | } 43 | 44 | //服务端返回连接结果 45 | message S2C_ConnectMsg { 46 | optional ERRORCODE errorCode = 1; //错误码 47 | } 48 | 49 | //服务端返回进入房间消息 50 | message S2C_JoinRoomMsg { 51 | optional int32 roomseatid = 1; //自己的位置索引id(1~N) 52 | repeated uint64 others = 2; //其他人的id 53 | repeated int32 pros = 3; //其他人的进度 54 | optional int32 randomSeed = 4; //随机种子 55 | } 56 | 57 | //服务端广播开始游戏消息 58 | message S2C_StartMsg { 59 | optional int64 timeStamp = 1; //同步时间戳 60 | 61 | } 62 | 63 | //读条进度 64 | message C2S_ProgressMsg { 65 | optional int32 pro = 1; //进度值0~100 66 | } 67 | 68 | //读条进度 69 | message S2C_ProgressMsg { 70 | optional uint64 id = 1; //id 71 | optional int32 pro = 2; //进度值0~100 72 | } 73 | 74 | //操作输入消息 75 | message C2S_InputMsg { 76 | optional int32 sid = 1; //操作id 77 | optional int32 x = 2; //操作位置x 78 | optional int32 y = 3; //操作位置y 79 | optional uint32 frameID = 4; //帧ID 80 | } 81 | 82 | //帧存储操作输入 83 | message InputData { 84 | optional uint64 id = 1; //id 85 | optional int32 sid = 2; //操作id 86 | optional int32 x = 3; //操作位置x 87 | optional int32 y = 4; //操作位置y 88 | optional int32 roomseatid = 5; //操作者的位置索引id(1~N) 89 | } 90 | 91 | //帧数据 92 | message FrameData { 93 | optional uint32 frameID = 1; //帧ID 94 | repeated InputData input = 2; //操作输入 95 | } 96 | 97 | //广播帧消息 98 | message S2C_FrameMsg { 99 | repeated FrameData frames = 1; //帧数据 100 | } 101 | 102 | //结果消息 103 | message C2S_ResultMsg { 104 | optional uint64 winnerID = 1; //胜利者ID 105 | } 106 | 107 | -------------------------------------------------------------------------------- /pkg/packet/pb_packet/protocol_test.go: -------------------------------------------------------------------------------- 1 | package pb_packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet/testdata" 11 | "github.com/golang/protobuf/proto" 12 | ) 13 | 14 | func Test_SCPacket(t *testing.T) { 15 | 16 | var sID int32 = 19234333 17 | msg := &testdata.TestMsg{ 18 | Sid: proto.Int32(sID), 19 | X: proto.Int32(10), 20 | Y: proto.Int32(20), 21 | } 22 | raw, _ := proto.Marshal(msg) 23 | p := NewPacket(uint8(testdata.ID_MSG_Test), msg) 24 | if nil == p { 25 | t.Fail() 26 | } 27 | 28 | buff := p.Serialize() 29 | 30 | dataLen := binary.BigEndian.Uint16(buff[0:]) 31 | if dataLen != uint16(len(raw)) { 32 | t.Error("dataLen != uint16(len(raw))") 33 | } 34 | 35 | if MinPacketLen+dataLen != MinPacketLen+uint16(len(raw)) { 36 | t.Error("MinPacketLen+dataLen != MinPacketLen+uint16(len(raw))") 37 | } 38 | 39 | id := buff[DataLen] 40 | if p.id != id { 41 | t.Error("uint8(ID_C2S_Connect) != id") 42 | } 43 | 44 | msg1 := &testdata.TestMsg{} 45 | if err := proto.Unmarshal(buff[MinPacketLen:], msg1); nil != err { 46 | t.Error(err) 47 | } 48 | 49 | if msg.GetSid() != msg1.GetSid() || msg.GetX() != msg1.GetX() || msg.GetY() != msg1.GetY() { 50 | t.Error("msg.Sid != data1.Sid || msg.X != data1.X || msg.Y != data1.Y") 51 | } 52 | } 53 | 54 | func Benchmark_SCPacket(b *testing.B) { 55 | 56 | var sID int32 = 19234333 57 | msg := &testdata.TestMsg{ 58 | Sid: proto.Int32(sID), 59 | X: proto.Int32(10), 60 | Y: proto.Int32(20), 61 | } 62 | 63 | for i := 0; i < b.N; i++ { 64 | NewPacket(uint8(testdata.ID_MSG_Test), msg) 65 | } 66 | 67 | } 68 | 69 | func Test_Packet(t *testing.T) { 70 | var sID int32 = 19234333 71 | msg := &testdata.TestMsg{ 72 | Sid: proto.Int32(sID), 73 | X: proto.Int32(10), 74 | Y: proto.Int32(20000), 75 | } 76 | 77 | temp, _ := proto.Marshal(msg) 78 | 79 | p := &Packet{ 80 | id: uint8(testdata.ID_MSG_Test), 81 | data: temp, 82 | } 83 | 84 | b := p.Serialize() 85 | 86 | r := strings.NewReader(string(b)) 87 | 88 | proto := &MsgProtocol{} 89 | 90 | ret, err := proto.ReadPacket(r) 91 | if nil != err { 92 | t.Error(err) 93 | } 94 | 95 | packet, _ := ret.(*Packet) 96 | if packet.GetMessageID() != p.id { 97 | t.Error("packet.GetMessageID() != uint8(ID_MSG_Input)") 98 | } 99 | 100 | if len(packet.data) != len(p.data) { 101 | t.Error("len(packet.data)!=len(p.data)") 102 | } 103 | 104 | msg1 := &testdata.TestMsg{} 105 | err = packet.Unmarshal(msg1) 106 | if nil != err { 107 | t.Error(err) 108 | } 109 | if msg.GetSid() != msg1.GetSid() || msg.GetX() != msg1.GetX() || msg.GetY() != msg1.GetY() { 110 | t.Error("msg.Sid != data1.Sid || msg.X != data1.X || msg.Y != data1.Y") 111 | } 112 | } 113 | 114 | func Benchmark_Packet(b *testing.B) { 115 | var sID int32 = 19234333 116 | msg := &testdata.TestMsg{ 117 | Sid: proto.Int32(sID), 118 | X: proto.Int32(10), 119 | Y: proto.Int32(20000), 120 | } 121 | 122 | temp, _ := json.Marshal(msg) 123 | 124 | p := &Packet{ 125 | id: uint8(testdata.ID_MSG_Test), 126 | data: temp, 127 | } 128 | 129 | buf := p.Serialize() 130 | 131 | // strings.NewReader(string(b)) 132 | 133 | proto := &MsgProtocol{} 134 | 135 | r := bytes.NewBuffer(nil) 136 | 137 | for i := 0; i < b.N; i++ { 138 | r.Write(buf) 139 | if _, err := proto.ReadPacket(r); nil != err { 140 | b.Error(err) 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | 7 | "github.com/byebyebruce/lockstepserver/pb" 8 | "github.com/byebyebruce/lockstepserver/pkg/network" 9 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet" 10 | 11 | l4g "github.com/alecthomas/log4go" 12 | ) 13 | 14 | // TODO 15 | func verifyToken(secret string) string { 16 | return secret 17 | } 18 | 19 | // OnConnect 链接进来 20 | func (r *LockStepServer) OnConnect(conn *network.Conn) bool { 21 | count := atomic.AddInt64(&r.totalConn, 1) 22 | l4g.Debug("[router] OnConnect [%s] totalConn=%d", conn.GetRawConn().RemoteAddr().String(), count) 23 | // TODO 可以做一些check,不合法返回false 24 | return true 25 | } 26 | 27 | // OnMessage 消息处理 28 | func (r *LockStepServer) OnMessage(conn *network.Conn, p network.Packet) bool { 29 | 30 | msg := p.(*pb_packet.Packet) 31 | 32 | l4g.Info("[router] OnMessage [%s] msg=[%d] len=[%d]", conn.GetRawConn().RemoteAddr().String(), msg.GetMessageID(), len(msg.GetData())) 33 | 34 | switch pb.ID(msg.GetMessageID()) { 35 | case pb.ID_MSG_Connect: 36 | 37 | rec := &pb.C2S_ConnectMsg{} 38 | if err := msg.Unmarshal(rec); nil != err { 39 | l4g.Error("[router] msg.Unmarshal error=[%s]", err.Error()) 40 | return false 41 | } 42 | 43 | // player id 44 | playerID := rec.GetPlayerID() 45 | // room id 46 | roomID := rec.GetBattleID() 47 | // token 48 | token := rec.GetToken() 49 | 50 | ret := &pb.S2C_ConnectMsg{ 51 | ErrorCode: pb.ERRORCODE_ERR_Ok.Enum(), 52 | } 53 | 54 | room := r.roomMgr.GetRoom(roomID) 55 | if nil == room { 56 | ret.ErrorCode = pb.ERRORCODE_ERR_NoRoom.Enum() 57 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), ret), time.Millisecond) 58 | l4g.Error("[router] no room player=[%d] room=[%d] token=[%s]", playerID, roomID, token) 59 | return true 60 | } 61 | 62 | if room.IsOver() { 63 | ret.ErrorCode = pb.ERRORCODE_ERR_RoomState.Enum() 64 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), ret), time.Millisecond) 65 | l4g.Error("[router] room is over player=[%d] room==[%d] token=[%s]", playerID, roomID, token) 66 | return true 67 | } 68 | 69 | if !room.HasPlayer(playerID) { 70 | ret.ErrorCode = pb.ERRORCODE_ERR_NoPlayer.Enum() 71 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), ret), time.Millisecond) 72 | l4g.Error("[router] !room.HasPlayer(playerID) player=[%d] room==[%d] token=[%s]", playerID, roomID, token) 73 | return true 74 | } 75 | 76 | // 验证token 77 | if token != verifyToken(token) { 78 | ret.ErrorCode = pb.ERRORCODE_ERR_Token.Enum() 79 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), ret), time.Millisecond) 80 | l4g.Error("[router] verifyToken failed player=[%d] room==[%d] token=[%s]", playerID, roomID, token) 81 | return true 82 | } 83 | 84 | conn.PutExtraData(playerID) 85 | 86 | // 这里只是先给加上身份标识,不能直接返回Connect成功,又后面Game返回 87 | // conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), ret), time.Millisecond) 88 | return room.OnConnect(conn) 89 | 90 | case pb.ID_MSG_Heartbeat: 91 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_Heartbeat), nil), time.Millisecond) 92 | return true 93 | 94 | case pb.ID_MSG_END: 95 | // 正式版不会提供这个消息 96 | conn.AsyncWritePacket(pb_packet.NewPacket(uint8(pb.ID_MSG_END), msg.GetData()), time.Millisecond) 97 | return true 98 | } 99 | 100 | return false 101 | 102 | } 103 | 104 | // OnClose 链接断开 105 | func (r *LockStepServer) OnClose(conn *network.Conn) { 106 | count := atomic.AddInt64(&r.totalConn, -1) 107 | 108 | l4g.Info("[router] OnClose: total=%d", count) 109 | } 110 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/log4go v0.0.0-20180109082532-d146e6b86faa h1:0zdYOLyuQ3TWIgWNgEH+LnmZNMmkO1ze3wriQt093Mk= 2 | github.com/alecthomas/log4go v0.0.0-20180109082532-d146e6b86faa/go.mod h1:iCVmQ9g4TfaRX5m5jq5sXY7RXYWPv9/PynM/GocbG3w= 3 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 4 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 5 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= 8 | github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 9 | github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= 10 | github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= 14 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= 15 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI= 16 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= 17 | github.com/tjfoc/gmsm v1.3.0 h1:qhgkrZru95jFP9NbVPknJvc9vgkMXhOEzkOASKdc0oQ= 18 | github.com/tjfoc/gmsm v1.3.0/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= 19 | github.com/xtaci/kcp-go v5.4.20+incompatible h1:TN1uey3Raw0sTz0Fg8GkfM0uH3YwzhnZWQ1bABv5xAg= 20 | github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= 21 | github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= 22 | github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 25 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 26 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 27 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 28 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 29 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 32 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 35 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 36 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 37 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 38 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 39 | -------------------------------------------------------------------------------- /pkg/network/conn.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | // Error type 12 | var ( 13 | ErrConnClosing = errors.New("use of closed network connection") 14 | ErrWriteBlocking = errors.New("write packet was blocking") 15 | ErrReadBlocking = errors.New("read packet was blocking") 16 | ) 17 | 18 | // Conn exposes a set of callbacks for the various events that occur on a connection 19 | type Conn struct { 20 | srv *Server 21 | conn net.Conn // the raw connection 22 | extraData interface{} // to save extra data 23 | closeOnce sync.Once // close the conn, once, per instance 24 | closeFlag int32 // close flag 25 | closeChan chan struct{} // close chanel 26 | packetSendChan chan Packet // packet send chanel 27 | packetReceiveChan chan Packet // packeet receive chanel 28 | callback ConnCallback // callback 29 | } 30 | 31 | // ConnCallback is an interface of methods that are used as callbacks on a connection 32 | type ConnCallback interface { 33 | // OnConnect is called when the connection was accepted, 34 | // If the return value of false is closed 35 | OnConnect(*Conn) bool 36 | 37 | // OnMessage is called when the connection receives a packet, 38 | // If the return value of false is closed 39 | OnMessage(*Conn, Packet) bool 40 | 41 | // OnClose is called when the connection closed 42 | OnClose(*Conn) 43 | } 44 | 45 | // NewConn returns a wrapper of raw conn 46 | func NewConn(conn net.Conn, srv *Server) *Conn { 47 | return &Conn{ 48 | srv: srv, 49 | callback: srv.callback, 50 | conn: conn, 51 | closeChan: make(chan struct{}), 52 | packetSendChan: make(chan Packet, srv.config.PacketSendChanLimit), 53 | packetReceiveChan: make(chan Packet, srv.config.PacketReceiveChanLimit), 54 | } 55 | } 56 | 57 | // GetExtraData gets the extra data from the Conn 58 | func (c *Conn) GetExtraData() interface{} { 59 | return c.extraData 60 | } 61 | 62 | // PutExtraData puts the extra data with the Conn 63 | func (c *Conn) PutExtraData(data interface{}) { 64 | c.extraData = data 65 | } 66 | 67 | // GetRawConn returns the raw net.TCPConn from the Conn 68 | func (c *Conn) GetRawConn() net.Conn { 69 | return c.conn 70 | } 71 | 72 | // Close closes the connection 73 | func (c *Conn) Close() { 74 | c.closeOnce.Do(func() { 75 | atomic.StoreInt32(&c.closeFlag, 1) 76 | close(c.closeChan) 77 | close(c.packetSendChan) 78 | close(c.packetReceiveChan) 79 | c.conn.Close() 80 | c.callback.OnClose(c) 81 | }) 82 | } 83 | 84 | // IsClosed indicates whether or not the connection is closed 85 | func (c *Conn) IsClosed() bool { 86 | return atomic.LoadInt32(&c.closeFlag) == 1 87 | } 88 | 89 | func (c *Conn) SetCallback(callback ConnCallback) { 90 | c.callback = callback 91 | } 92 | 93 | // AsyncWritePacket async writes a packet, this method will never block 94 | func (c *Conn) AsyncWritePacket(p Packet, timeout time.Duration) (err error) { 95 | if c.IsClosed() { 96 | return ErrConnClosing 97 | } 98 | 99 | defer func() { 100 | if e := recover(); e != nil { 101 | err = ErrConnClosing 102 | } 103 | }() 104 | 105 | if timeout == 0 { 106 | select { 107 | case c.packetSendChan <- p: 108 | return nil 109 | 110 | default: 111 | return ErrWriteBlocking 112 | } 113 | 114 | } else { 115 | select { 116 | case c.packetSendChan <- p: 117 | return nil 118 | 119 | case <-c.closeChan: 120 | return ErrConnClosing 121 | 122 | case <-time.After(timeout): 123 | return ErrWriteBlocking 124 | } 125 | } 126 | } 127 | 128 | // Do it 129 | func (c *Conn) Do() { 130 | if !c.callback.OnConnect(c) { 131 | return 132 | } 133 | 134 | asyncDo(c.handleLoop, c.srv.waitGroup) 135 | asyncDo(c.readLoop, c.srv.waitGroup) 136 | asyncDo(c.writeLoop, c.srv.waitGroup) 137 | } 138 | 139 | func (c *Conn) readLoop() { 140 | defer func() { 141 | recover() 142 | c.Close() 143 | }() 144 | 145 | for { 146 | select { 147 | case <-c.srv.exitChan: 148 | return 149 | 150 | case <-c.closeChan: 151 | return 152 | 153 | default: 154 | } 155 | 156 | c.conn.SetReadDeadline(time.Now().Add(c.srv.config.ConnReadTimeout)) 157 | p, err := c.srv.protocol.ReadPacket(c.conn) 158 | if err != nil { 159 | return 160 | } 161 | 162 | c.packetReceiveChan <- p 163 | } 164 | } 165 | 166 | func (c *Conn) writeLoop() { 167 | defer func() { 168 | recover() 169 | c.Close() 170 | }() 171 | 172 | for { 173 | select { 174 | case <-c.srv.exitChan: 175 | return 176 | 177 | case <-c.closeChan: 178 | return 179 | 180 | case p := <-c.packetSendChan: 181 | if c.IsClosed() { 182 | return 183 | } 184 | c.conn.SetWriteDeadline(time.Now().Add(c.srv.config.ConnWriteTimeout)) 185 | if _, err := c.conn.Write(p.Serialize()); err != nil { 186 | return 187 | } 188 | } 189 | } 190 | } 191 | 192 | func (c *Conn) handleLoop() { 193 | defer func() { 194 | recover() 195 | c.Close() 196 | }() 197 | 198 | for { 199 | select { 200 | case <-c.srv.exitChan: 201 | return 202 | 203 | case <-c.closeChan: 204 | return 205 | 206 | case p := <-c.packetReceiveChan: 207 | if c.IsClosed() { 208 | return 209 | } 210 | if !c.callback.OnMessage(c, p) { 211 | return 212 | } 213 | } 214 | } 215 | } 216 | 217 | func asyncDo(fn func(), wg *sync.WaitGroup) { 218 | wg.Add(1) 219 | go func() { 220 | fn() 221 | wg.Done() 222 | }() 223 | } 224 | -------------------------------------------------------------------------------- /logic/room/room.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/byebyebruce/lockstepserver/logic/game" 9 | "github.com/byebyebruce/lockstepserver/pkg/network" 10 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet" 11 | 12 | l4g "github.com/alecthomas/log4go" 13 | ) 14 | 15 | const ( 16 | Frequency = 30 // 每分钟心跳频率 17 | TickTimer = time.Second / Frequency // 心跳Timer 18 | TimeoutTime = time.Minute * 5 // 超时时间 19 | ) 20 | 21 | type packet struct { 22 | id uint64 23 | msg network.Packet 24 | } 25 | 26 | // Room 战斗房间 27 | type Room struct { 28 | wg sync.WaitGroup 29 | 30 | roomID uint64 31 | players []uint64 32 | typeID int32 33 | closeFlag int32 34 | timeStamp int64 35 | secretKey string 36 | logicServer string 37 | 38 | exitChan chan struct{} 39 | msgQ chan *packet 40 | inChan chan *network.Conn 41 | outChan chan *network.Conn 42 | 43 | game *game.Game 44 | } 45 | 46 | // NewRoom 构造 47 | func NewRoom(id uint64, typeID int32, players []uint64, randomSeed int32, logicServer string) *Room { 48 | r := &Room{ 49 | roomID: id, 50 | players: players, 51 | typeID: typeID, 52 | exitChan: make(chan struct{}), 53 | msgQ: make(chan *packet, 2048), 54 | outChan: make(chan *network.Conn, 8), 55 | inChan: make(chan *network.Conn, 8), 56 | timeStamp: time.Now().Unix(), 57 | logicServer: logicServer, 58 | secretKey: "test_room", 59 | } 60 | 61 | r.game = game.NewGame(id, players, randomSeed, r) 62 | 63 | return r 64 | } 65 | 66 | // ID room ID 67 | func (r *Room) ID() uint64 { 68 | return r.roomID 69 | } 70 | 71 | // SecretKey secret key 72 | func (r *Room) SecretKey() string { 73 | return r.secretKey 74 | } 75 | 76 | // TimeStamp time stamp 77 | func (r *Room) TimeStamp() int64 { 78 | return r.timeStamp 79 | } 80 | 81 | // IsOver 是否已经结束 82 | func (r *Room) IsOver() bool { 83 | return atomic.LoadInt32(&r.closeFlag) != 0 84 | } 85 | 86 | // HasPlayer 是否有这个player 87 | func (r *Room) HasPlayer(id uint64) bool { 88 | for _, v := range r.players { 89 | if v == id { 90 | return true 91 | } 92 | } 93 | 94 | return false 95 | } 96 | 97 | func (r *Room) OnJoinGame(id, pid uint64) { 98 | l4g.Warn("[room(%d)] onJoinGame %d", id, pid) 99 | } 100 | func (r *Room) OnGameStart(id uint64) { 101 | l4g.Warn("[room(%d)] onGameStart", id) 102 | } 103 | 104 | func (r *Room) OnLeaveGame(id, pid uint64) { 105 | l4g.Warn("[room(%d)] onLeaveGame %d", id, pid) 106 | } 107 | func (r *Room) OnGameOver(id uint64) { 108 | atomic.StoreInt32(&r.closeFlag, 1) 109 | 110 | l4g.Warn("[room(%d)] onGameOver", id) 111 | 112 | r.wg.Add(1) 113 | 114 | go func() { 115 | defer r.wg.Done() 116 | // TODO 117 | // http result 118 | }() 119 | 120 | } 121 | 122 | // OnConnect network.Conn callback 123 | func (r *Room) OnConnect(conn *network.Conn) bool { 124 | 125 | conn.SetCallback(r) // SetCallback只能在OnConnect里调 126 | r.inChan <- conn 127 | l4g.Warn("[room(%d)] OnConnect %d", r.roomID, conn.GetExtraData().(uint64)) 128 | 129 | return true 130 | } 131 | 132 | // OnMessage network.Conn callback 133 | func (r *Room) OnMessage(conn *network.Conn, msg network.Packet) bool { 134 | 135 | id, ok := conn.GetExtraData().(uint64) 136 | if !ok { 137 | l4g.Error("[room] OnMessage error conn don't have id") 138 | return false 139 | } 140 | 141 | p := &packet{ 142 | id: id, 143 | msg: msg, 144 | } 145 | r.msgQ <- p 146 | 147 | return true 148 | } 149 | 150 | // OnClose network.Conn callback 151 | func (r *Room) OnClose(conn *network.Conn) { 152 | r.outChan <- conn 153 | if id, ok := conn.GetExtraData().(uint64); ok { 154 | l4g.Warn("[room(%d)] OnClose %d", r.roomID, id) 155 | } else { 156 | l4g.Warn("[room(%d)] OnClose no id", r.roomID) 157 | } 158 | 159 | } 160 | 161 | // Run 主循环 162 | func (r *Room) Run() { 163 | r.wg.Add(1) 164 | defer r.wg.Done() 165 | defer func() { 166 | /* 167 | err := recover() 168 | if nil != err { 169 | l4g.Error("[room(%d)] Run error:%+v", r.roomID, err) 170 | }*/ 171 | r.game.Cleanup() 172 | l4g.Warn("[room(%d)] quit! total time=[%d]", r.roomID, time.Now().Unix()-r.timeStamp) 173 | }() 174 | 175 | // 心跳 176 | tickerTick := time.NewTicker(TickTimer) 177 | defer tickerTick.Stop() 178 | 179 | // 超时timer 180 | timeoutTimer := time.NewTimer(TimeoutTime) 181 | 182 | l4g.Info("[room(%d)] running...", r.roomID) 183 | 184 | LOOP: 185 | for { 186 | select { 187 | case <-r.exitChan: 188 | l4g.Error("[room(%d)] force exit", r.roomID) 189 | return 190 | case <-timeoutTimer.C: 191 | l4g.Error("[room(%d)] time out", r.roomID) 192 | break LOOP 193 | case msg := <-r.msgQ: 194 | r.game.ProcessMsg(msg.id, msg.msg.(*pb_packet.Packet)) 195 | case <-tickerTick.C: 196 | if !r.game.Tick(time.Now().Unix()) { 197 | l4g.Info("[room(%d)] tick over", r.roomID) 198 | break LOOP 199 | } 200 | case c := <-r.inChan: 201 | id, ok := c.GetExtraData().(uint64) 202 | if ok { 203 | if r.game.JoinGame(id, c) { 204 | l4g.Info("[room(%d)] player[%d] join room ok", r.roomID, id) 205 | } else { 206 | l4g.Error("[room(%d)] player[%d] join room failed", r.roomID, id) 207 | c.Close() 208 | } 209 | } else { 210 | c.Close() 211 | l4g.Error("[room(%d)] inChan don't have id", r.roomID) 212 | } 213 | 214 | case c := <-r.outChan: 215 | if id, ok := c.GetExtraData().(uint64); ok { 216 | r.game.LeaveGame(id) 217 | } else { 218 | c.Close() 219 | l4g.Error("[room(%d)] outChan don't have id", r.roomID) 220 | } 221 | } 222 | } 223 | 224 | r.game.Close() 225 | 226 | for i := 3; i > 0; i-- { 227 | <-time.After(time.Second) 228 | l4g.Info("[room(%d)] quiting %d...", r.roomID, i) 229 | } 230 | } 231 | 232 | // Stop 强制关闭 233 | func (r *Room) Stop() { 234 | close(r.exitChan) 235 | r.wg.Wait() 236 | } 237 | -------------------------------------------------------------------------------- /pkg/kcp_server/example_test.go: -------------------------------------------------------------------------------- 1 | package kcp_server 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/byebyebruce/lockstepserver/pkg/network" 11 | 12 | "github.com/xtaci/kcp-go" 13 | ) 14 | 15 | const ( 16 | latency = time.Millisecond * 10 17 | ) 18 | 19 | type testCallback struct { 20 | numConn uint32 21 | numMsg uint32 22 | numDiscon uint32 23 | } 24 | 25 | func (t *testCallback) OnMessage(conn *network.Conn, msg network.Packet) bool { 26 | 27 | atomic.AddUint32(&t.numMsg, 1) 28 | 29 | // fmt.Println("OnMessage", conn.GetExtraData(), string(msg.(*network.DefaultPacket).GetBody())) 30 | conn.AsyncWritePacket(network.NewDefaultPacket([]byte("pong")), time.Second*1) 31 | return true 32 | } 33 | 34 | func (t *testCallback) OnConnect(conn *network.Conn) bool { 35 | id := atomic.AddUint32(&t.numConn, 1) 36 | conn.PutExtraData(id) 37 | // fmt.Println("OnConnect", conn.GetExtraData()) 38 | return true 39 | } 40 | 41 | func (t *testCallback) OnClose(conn *network.Conn) { 42 | atomic.AddUint32(&t.numDiscon, 1) 43 | 44 | // fmt.Println("OnDisconnect", conn.GetExtraData()) 45 | } 46 | 47 | func Test_KCPServer(t *testing.T) { 48 | 49 | l, err := kcp.Listen(":10086") 50 | if nil != err { 51 | panic(err) 52 | } 53 | 54 | config := &network.Config{ 55 | PacketReceiveChanLimit: 1024, 56 | PacketSendChanLimit: 1024, 57 | ConnReadTimeout: latency, 58 | ConnWriteTimeout: latency, 59 | } 60 | 61 | callback := &testCallback{} 62 | server := network.NewServer(config, callback, &network.DefaultProtocol{}) 63 | 64 | go server.Start(l, func(conn net.Conn, i *network.Server) *network.Conn { 65 | kcpConn := conn.(*kcp.UDPSession) 66 | kcpConn.SetNoDelay(1, 10, 2, 1) 67 | kcpConn.SetStreamMode(true) 68 | kcpConn.SetWindowSize(4096, 4096) 69 | kcpConn.SetReadBuffer(4 * 1024 * 1024) 70 | kcpConn.SetWriteBuffer(4 * 1024 * 1024) 71 | kcpConn.SetACKNoDelay(true) 72 | 73 | return network.NewConn(conn, server) 74 | }) 75 | defer server.Stop() 76 | 77 | time.Sleep(time.Second) 78 | 79 | wg := sync.WaitGroup{} 80 | const max_con = 100 81 | for i := 0; i < max_con; i++ { 82 | wg.Add(1) 83 | time.Sleep(time.Nanosecond) 84 | go func() { 85 | defer wg.Done() 86 | 87 | c, e := kcp.Dial("127.0.0.1:10086") 88 | if nil != e { 89 | t.FailNow() 90 | } 91 | defer c.Close() 92 | 93 | c.Write(network.NewDefaultPacket([]byte("ping")).Serialize()) 94 | b := make([]byte, 1024) 95 | c.SetReadDeadline(time.Now().Add(latency)) 96 | if _, e := c.Read(b); nil != e { 97 | t.Fatalf("error:%s", e.Error()) 98 | } 99 | 100 | // time.Sleep(time.Second) 101 | }() 102 | } 103 | 104 | wg.Wait() 105 | time.Sleep(time.Second * 2) 106 | 107 | n := atomic.LoadUint32(&callback.numConn) 108 | if n != max_con { 109 | t.Errorf("numConn[%d] should be [%d]", n, max_con) 110 | } 111 | 112 | n = atomic.LoadUint32(&callback.numMsg) 113 | if n != max_con { 114 | t.Errorf("numMsg[%d] should be [%d]", n, max_con) 115 | } 116 | 117 | n = atomic.LoadUint32(&callback.numDiscon) 118 | if n != max_con { 119 | t.Errorf("numDiscon[%d] should be [%d]", n, max_con) 120 | } 121 | } 122 | 123 | func Benchmark_KCPServer(b *testing.B) { 124 | 125 | l, err := kcp.Listen(":10086") 126 | if nil != err { 127 | panic(err) 128 | } 129 | 130 | config := &network.Config{ 131 | PacketReceiveChanLimit: 1024, 132 | PacketSendChanLimit: 1024, 133 | } 134 | 135 | callback := &testCallback{} 136 | server := network.NewServer(config, &testCallback{}, &network.DefaultProtocol{}) 137 | 138 | go server.Start(l, func(conn net.Conn, i *network.Server) *network.Conn { 139 | kcpConn := conn.(*kcp.UDPSession) 140 | kcpConn.SetNoDelay(1, 10, 2, 1) 141 | kcpConn.SetStreamMode(true) 142 | kcpConn.SetWindowSize(4096, 4096) 143 | kcpConn.SetReadBuffer(4 * 1024 * 1024) 144 | kcpConn.SetWriteBuffer(4 * 1024 * 1024) 145 | kcpConn.SetACKNoDelay(true) 146 | 147 | return network.NewConn(conn, server) 148 | }) 149 | 150 | time.Sleep(time.Millisecond * 100) 151 | 152 | wg := sync.WaitGroup{} 153 | var max_con uint32 = 0 154 | c, e := kcp.Dial("127.0.0.1:10086") 155 | if nil != e { 156 | b.FailNow() 157 | } 158 | 159 | go func() { 160 | for { 161 | buf := make([]byte, 1024) 162 | c.SetReadDeadline(time.Now().Add(time.Second * 2)) 163 | _, er := c.Read(buf) 164 | if nil != er { 165 | // b.FailNow() 166 | return 167 | } 168 | wg.Done() 169 | } 170 | 171 | }() 172 | 173 | for i := 0; i < b.N; i++ { 174 | max_con++ 175 | 176 | wg.Add(1) 177 | go func() { 178 | 179 | c.Write(network.NewDefaultPacket([]byte("ping")).Serialize()) 180 | 181 | // time.Sleep(time.Second) 182 | }() 183 | } 184 | 185 | wg.Wait() 186 | // time.Sleep(time.Second * 2) 187 | server.Stop() 188 | 189 | n := atomic.LoadUint32(&callback.numMsg) 190 | b.Logf("numMsg[%d]", n) 191 | if n != callback.numMsg { 192 | b.Errorf("numMsg[%d] should be [%d]", n, max_con) 193 | } 194 | /* 195 | n = atomic.LoadUint32(&numConn) 196 | b.Logf("numConn[%d]", n) 197 | if n != max_con { 198 | b.Errorf("numConn[%d] should be [%d]", n, max_con) 199 | } 200 | 201 | 202 | 203 | n = atomic.LoadUint32(&numDiscon) 204 | b.Logf("numDiscon[%d]", n) 205 | if n != numDiscon { 206 | b.Errorf("numDiscon[%d] should be [%d]", n, max_con) 207 | } 208 | */ 209 | } 210 | 211 | func Test_TCPServer(t *testing.T) { 212 | 213 | l, err := net.Listen("tcp", ":10086") 214 | if nil != err { 215 | panic(err) 216 | } 217 | 218 | config := &network.Config{ 219 | PacketReceiveChanLimit: 1024, 220 | PacketSendChanLimit: 1024, 221 | ConnReadTimeout: time.Millisecond * 50, 222 | ConnWriteTimeout: time.Millisecond * 50, 223 | } 224 | 225 | callback := &testCallback{} 226 | server := network.NewServer(config, callback, &network.DefaultProtocol{}) 227 | 228 | go server.Start(l, func(conn net.Conn, i *network.Server) *network.Conn { 229 | return network.NewConn(conn, server) 230 | }) 231 | 232 | time.Sleep(time.Second) 233 | 234 | wg := sync.WaitGroup{} 235 | const max_con = 100 236 | for i := 0; i < max_con; i++ { 237 | wg.Add(1) 238 | time.Sleep(time.Nanosecond) 239 | go func() { 240 | defer wg.Done() 241 | c, e := net.Dial("tcp", "127.0.0.1:10086") 242 | if nil != e { 243 | t.FailNow() 244 | } 245 | defer c.Close() 246 | c.Write(network.NewDefaultPacket([]byte("ping")).Serialize()) 247 | b := make([]byte, 1024) 248 | c.SetReadDeadline(time.Now().Add(time.Second * 2)) 249 | c.Read(b) 250 | // time.Sleep(time.Second) 251 | }() 252 | } 253 | 254 | wg.Wait() 255 | // time.Sleep(max_sleep) 256 | server.Stop() 257 | 258 | n := atomic.LoadUint32(&callback.numConn) 259 | if n != max_con { 260 | t.Errorf("numConn[%d] should be [%d]", n, max_con) 261 | } 262 | 263 | n = atomic.LoadUint32(&callback.numMsg) 264 | if n != max_con { 265 | t.Errorf("numMsg[%d] should be [%d]", n, max_con) 266 | } 267 | 268 | n = atomic.LoadUint32(&callback.numDiscon) 269 | if n != max_con { 270 | t.Errorf("numDiscon[%d] should be [%d]", n, max_con) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /pkg/packet/pb_packet/testdata/testdata.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.3 5 | // source: testdata.proto 6 | 7 | package testdata 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | //消息ID 24 | type ID int32 25 | 26 | const ( 27 | ID_MSG_BEGIN ID = 0 28 | ID_MSG_Test ID = 60 29 | ID_MSG_END ID = 255 30 | ) 31 | 32 | // Enum value maps for ID. 33 | var ( 34 | ID_name = map[int32]string{ 35 | 0: "MSG_BEGIN", 36 | 60: "MSG_Test", 37 | 255: "MSG_END", 38 | } 39 | ID_value = map[string]int32{ 40 | "MSG_BEGIN": 0, 41 | "MSG_Test": 60, 42 | "MSG_END": 255, 43 | } 44 | ) 45 | 46 | func (x ID) Enum() *ID { 47 | p := new(ID) 48 | *p = x 49 | return p 50 | } 51 | 52 | func (x ID) String() string { 53 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 54 | } 55 | 56 | func (ID) Descriptor() protoreflect.EnumDescriptor { 57 | return file_testdata_proto_enumTypes[0].Descriptor() 58 | } 59 | 60 | func (ID) Type() protoreflect.EnumType { 61 | return &file_testdata_proto_enumTypes[0] 62 | } 63 | 64 | func (x ID) Number() protoreflect.EnumNumber { 65 | return protoreflect.EnumNumber(x) 66 | } 67 | 68 | // Deprecated: Use ID.Descriptor instead. 69 | func (ID) EnumDescriptor() ([]byte, []int) { 70 | return file_testdata_proto_rawDescGZIP(), []int{0} 71 | } 72 | 73 | // TestMsg 74 | type TestMsg struct { 75 | state protoimpl.MessageState 76 | sizeCache protoimpl.SizeCache 77 | unknownFields protoimpl.UnknownFields 78 | 79 | Sid *int32 `protobuf:"varint,1,opt,name=sid,proto3,oneof" json:"sid,omitempty"` //操作id 80 | X *int32 `protobuf:"varint,2,opt,name=x,proto3,oneof" json:"x,omitempty"` //操作位置x 81 | Y *int32 `protobuf:"varint,3,opt,name=y,proto3,oneof" json:"y,omitempty"` //操作位置y 82 | FrameID *uint32 `protobuf:"varint,4,opt,name=frameID,proto3,oneof" json:"frameID,omitempty"` //帧ID 83 | } 84 | 85 | func (x *TestMsg) Reset() { 86 | *x = TestMsg{} 87 | if protoimpl.UnsafeEnabled { 88 | mi := &file_testdata_proto_msgTypes[0] 89 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 90 | ms.StoreMessageInfo(mi) 91 | } 92 | } 93 | 94 | func (x *TestMsg) String() string { 95 | return protoimpl.X.MessageStringOf(x) 96 | } 97 | 98 | func (*TestMsg) ProtoMessage() {} 99 | 100 | func (x *TestMsg) ProtoReflect() protoreflect.Message { 101 | mi := &file_testdata_proto_msgTypes[0] 102 | if protoimpl.UnsafeEnabled && x != nil { 103 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 104 | if ms.LoadMessageInfo() == nil { 105 | ms.StoreMessageInfo(mi) 106 | } 107 | return ms 108 | } 109 | return mi.MessageOf(x) 110 | } 111 | 112 | // Deprecated: Use TestMsg.ProtoReflect.Descriptor instead. 113 | func (*TestMsg) Descriptor() ([]byte, []int) { 114 | return file_testdata_proto_rawDescGZIP(), []int{0} 115 | } 116 | 117 | func (x *TestMsg) GetSid() int32 { 118 | if x != nil && x.Sid != nil { 119 | return *x.Sid 120 | } 121 | return 0 122 | } 123 | 124 | func (x *TestMsg) GetX() int32 { 125 | if x != nil && x.X != nil { 126 | return *x.X 127 | } 128 | return 0 129 | } 130 | 131 | func (x *TestMsg) GetY() int32 { 132 | if x != nil && x.Y != nil { 133 | return *x.Y 134 | } 135 | return 0 136 | } 137 | 138 | func (x *TestMsg) GetFrameID() uint32 { 139 | if x != nil && x.FrameID != nil { 140 | return *x.FrameID 141 | } 142 | return 0 143 | } 144 | 145 | var File_testdata_proto protoreflect.FileDescriptor 146 | 147 | var file_testdata_proto_rawDesc = []byte{ 148 | 0x0a, 0x0e, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 149 | 0x12, 0x08, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x22, 0x85, 0x01, 0x0a, 0x07, 0x54, 150 | 0x65, 0x73, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x15, 0x0a, 0x03, 0x73, 0x69, 0x64, 0x18, 0x01, 0x20, 151 | 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x03, 0x73, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x11, 0x0a, 152 | 0x01, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x01, 0x78, 0x88, 0x01, 0x01, 153 | 0x12, 0x11, 0x0a, 0x01, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x02, 0x52, 0x01, 0x79, 154 | 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x04, 155 | 0x20, 0x01, 0x28, 0x0d, 0x48, 0x03, 0x52, 0x07, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x88, 156 | 0x01, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x73, 0x69, 0x64, 0x42, 0x04, 0x0a, 0x02, 0x5f, 0x78, 157 | 0x42, 0x04, 0x0a, 0x02, 0x5f, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 158 | 0x49, 0x44, 0x2a, 0x2f, 0x0a, 0x02, 0x49, 0x44, 0x12, 0x0d, 0x0a, 0x09, 0x4d, 0x53, 0x47, 0x5f, 159 | 0x42, 0x45, 0x47, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x53, 0x47, 0x5f, 0x54, 160 | 0x65, 0x73, 0x74, 0x10, 0x3c, 0x12, 0x0c, 0x0a, 0x07, 0x4d, 0x53, 0x47, 0x5f, 0x45, 0x4e, 0x44, 161 | 0x10, 0xff, 0x01, 0x42, 0x4e, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 162 | 0x6d, 0x2f, 0x62, 0x79, 0x65, 0x62, 0x79, 0x65, 0x62, 0x72, 0x75, 0x63, 0x65, 0x2f, 0x6c, 0x6f, 163 | 0x63, 0x6b, 0x73, 0x74, 0x65, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 164 | 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x2f, 0x70, 0x62, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 165 | 0x74, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x3b, 0x74, 0x65, 0x73, 0x74, 0x64, 166 | 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 167 | } 168 | 169 | var ( 170 | file_testdata_proto_rawDescOnce sync.Once 171 | file_testdata_proto_rawDescData = file_testdata_proto_rawDesc 172 | ) 173 | 174 | func file_testdata_proto_rawDescGZIP() []byte { 175 | file_testdata_proto_rawDescOnce.Do(func() { 176 | file_testdata_proto_rawDescData = protoimpl.X.CompressGZIP(file_testdata_proto_rawDescData) 177 | }) 178 | return file_testdata_proto_rawDescData 179 | } 180 | 181 | var file_testdata_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 182 | var file_testdata_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 183 | var file_testdata_proto_goTypes = []interface{}{ 184 | (ID)(0), // 0: testdata.ID 185 | (*TestMsg)(nil), // 1: testdata.TestMsg 186 | } 187 | var file_testdata_proto_depIdxs = []int32{ 188 | 0, // [0:0] is the sub-list for method output_type 189 | 0, // [0:0] is the sub-list for method input_type 190 | 0, // [0:0] is the sub-list for extension type_name 191 | 0, // [0:0] is the sub-list for extension extendee 192 | 0, // [0:0] is the sub-list for field type_name 193 | } 194 | 195 | func init() { file_testdata_proto_init() } 196 | func file_testdata_proto_init() { 197 | if File_testdata_proto != nil { 198 | return 199 | } 200 | if !protoimpl.UnsafeEnabled { 201 | file_testdata_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 202 | switch v := v.(*TestMsg); i { 203 | case 0: 204 | return &v.state 205 | case 1: 206 | return &v.sizeCache 207 | case 2: 208 | return &v.unknownFields 209 | default: 210 | return nil 211 | } 212 | } 213 | } 214 | file_testdata_proto_msgTypes[0].OneofWrappers = []interface{}{} 215 | type x struct{} 216 | out := protoimpl.TypeBuilder{ 217 | File: protoimpl.DescBuilder{ 218 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 219 | RawDescriptor: file_testdata_proto_rawDesc, 220 | NumEnums: 1, 221 | NumMessages: 1, 222 | NumExtensions: 0, 223 | NumServices: 0, 224 | }, 225 | GoTypes: file_testdata_proto_goTypes, 226 | DependencyIndexes: file_testdata_proto_depIdxs, 227 | EnumInfos: file_testdata_proto_enumTypes, 228 | MessageInfos: file_testdata_proto_msgTypes, 229 | }.Build() 230 | File_testdata_proto = out.File 231 | file_testdata_proto_rawDesc = nil 232 | file_testdata_proto_goTypes = nil 233 | file_testdata_proto_depIdxs = nil 234 | } 235 | -------------------------------------------------------------------------------- /logic/game/game.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/byebyebruce/lockstepserver/pb" 7 | "github.com/byebyebruce/lockstepserver/pkg/network" 8 | "github.com/byebyebruce/lockstepserver/pkg/packet/pb_packet" 9 | "github.com/golang/protobuf/proto" 10 | 11 | l4g "github.com/alecthomas/log4go" 12 | ) 13 | 14 | // GameState 游戏状态 15 | type GameState int 16 | 17 | const ( 18 | k_Ready GameState = 0 // 准备阶段 19 | k_Gaming = 1 // 战斗中阶段 20 | k_Over = 2 // 结束阶段 21 | k_Stop = 3 // 停止 22 | ) 23 | 24 | const ( 25 | MaxReadyTime int64 = 20 // 准备阶段最长时间,如果超过这个时间没人连进来直接关闭游戏 26 | MaxGameFrame uint32 = 30*60*3 + 100 // 每局最大帧数 27 | BroadcastOffsetFrames = 3 // 每隔多少帧广播一次 28 | kMaxFrameDataPerMsg = 60 // 每个消息包最多包含多少个帧数据 29 | kBadNetworkThreshold = 2 // 这个时间段没有收到心跳包认为他网络很差,不再持续给发包(网络层的读写时间设置的比较长,客户端要求的方案) 30 | ) 31 | 32 | type gameListener interface { 33 | OnJoinGame(uint64, uint64) 34 | OnGameStart(uint64) 35 | OnLeaveGame(uint64, uint64) 36 | OnGameOver(uint64) 37 | } 38 | 39 | // Game 一局游戏 40 | type Game struct { 41 | id uint64 42 | startTime int64 43 | randomSeed int32 44 | State GameState 45 | players map[uint64]*Player 46 | logic *lockstep 47 | clientFrameCount uint32 48 | 49 | result map[uint64]uint64 50 | 51 | listener gameListener 52 | 53 | dirty bool 54 | } 55 | 56 | // NewGame 构造游戏 57 | func NewGame(id uint64, players []uint64, randomSeed int32, listener gameListener) *Game { 58 | g := &Game{ 59 | id: id, 60 | players: make(map[uint64]*Player), 61 | logic: newLockstep(), 62 | startTime: time.Now().Unix(), 63 | randomSeed: randomSeed, 64 | listener: listener, 65 | result: make(map[uint64]uint64), 66 | } 67 | 68 | for k, v := range players { 69 | g.players[v] = NewPlayer(v, int32(k+1)) 70 | } 71 | 72 | return g 73 | } 74 | 75 | // JoinGame 加入游戏 76 | func (g *Game) JoinGame(id uint64, conn *network.Conn) bool { 77 | 78 | msg := &pb.S2C_ConnectMsg{ 79 | ErrorCode: pb.ERRORCODE_ERR_Ok.Enum(), 80 | } 81 | 82 | p, ok := g.players[id] 83 | if !ok { 84 | l4g.Error("[game(%d)] player[%d] join room failed", g.id, id) 85 | return false 86 | } 87 | 88 | if k_Ready != g.State && k_Gaming != g.State { 89 | msg.ErrorCode = pb.ERRORCODE_ERR_RoomState.Enum() 90 | p.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), msg)) 91 | l4g.Error("[game(%d)] player[%d] game is over", g.id, id) 92 | return true 93 | } 94 | 95 | // 把现有的玩家顶掉 96 | if nil != p.client { 97 | // TODO 这里有多线程操作的危险 如果调 p.client.Close() 会把现有刚进来的玩家提调 98 | p.client.PutExtraData(nil) 99 | l4g.Error("[game(%d)] player[%d] replace", g.id, id) 100 | } 101 | 102 | p.Connect(conn) 103 | 104 | p.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Connect), msg)) 105 | 106 | g.listener.OnJoinGame(g.id, id) 107 | 108 | return true 109 | 110 | } 111 | 112 | // LeaveGame 离开游戏 113 | func (g *Game) LeaveGame(id uint64) bool { 114 | 115 | p, ok := g.players[id] 116 | if !ok { 117 | return false 118 | } 119 | 120 | p.Cleanup() 121 | 122 | g.listener.OnLeaveGame(g.id, id) 123 | 124 | return true 125 | } 126 | 127 | // ProcessMsg 处理消息 128 | func (g *Game) ProcessMsg(id uint64, msg *pb_packet.Packet) { 129 | 130 | player, ok := g.players[id] 131 | if !ok { 132 | l4g.Error("[game(%d)] processMsg player[%d] msg=[%d]", g.id, player.id, msg.GetMessageID()) 133 | return 134 | } 135 | l4g.Info("[game(%d)] processMsg player[%d] msg=[%d]", g.id, player.id, msg.GetMessageID()) 136 | 137 | msgID := pb.ID(msg.GetMessageID()) 138 | 139 | switch msgID { 140 | case pb.ID_MSG_JoinRoom: 141 | msg := &pb.S2C_JoinRoomMsg{ 142 | Roomseatid: proto.Int32(player.idx), 143 | RandomSeed: proto.Int32(g.randomSeed), 144 | } 145 | 146 | for _, v := range g.players { 147 | if player.id == v.id { 148 | continue 149 | } 150 | msg.Others = append(msg.Others, v.id) 151 | msg.Pros = append(msg.Pros, v.loadingProgress) 152 | } 153 | 154 | player.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_JoinRoom), msg)) 155 | 156 | case pb.ID_MSG_Progress: 157 | if g.State > k_Ready { 158 | break 159 | } 160 | m := &pb.C2S_ProgressMsg{} 161 | if err := msg.Unmarshal(m); nil != err { 162 | l4g.Error("[game(%d)] processMsg player[%d] msg=[%d] UnmarshalPB error:[%s]", g.id, player.id, msg.GetMessageID(), err.Error()) 163 | return 164 | } 165 | player.loadingProgress = m.GetPro() 166 | msg := pb_packet.NewPacket(uint8(pb.ID_MSG_Progress), &pb.S2C_ProgressMsg{ 167 | 168 | Id: proto.Uint64(player.id), 169 | Pro: m.Pro, 170 | }) 171 | g.broadcastExclude(msg, player.id) 172 | 173 | case pb.ID_MSG_Heartbeat: 174 | player.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Heartbeat), nil)) 175 | player.RefreshHeartbeatTime() 176 | case pb.ID_MSG_Ready: 177 | if k_Ready == g.State { 178 | g.doReady(player) 179 | } else if k_Gaming == g.State { 180 | g.doReady(player) 181 | // 重连进来 TODO 对重连进行检查,重连比较耗费 182 | g.doReconnect(player) 183 | l4g.Warn("[game(%d)] doReconnect [%d]", g.id, player.id) 184 | } else { 185 | l4g.Error("[game(%d)] ID_MSG_Ready player[%d] state error:[%d]", g.id, player.id, g.State) 186 | } 187 | 188 | case pb.ID_MSG_Input: 189 | m := &pb.C2S_InputMsg{} 190 | if err := msg.Unmarshal(m); nil != err { 191 | l4g.Error("[game(%d)] processMsg player[%d] msg=[%d] UnmarshalPB error:[%s]", g.id, player.id, msg.GetMessageID(), err.Error()) 192 | return 193 | } 194 | if !g.pushInput(player, m) { 195 | l4g.Warn("[game(%d)] processMsg player[%d] msg=[%d] pushInput failed", g.id, player.id, msg.GetMessageID()) 196 | break 197 | } 198 | 199 | // 下一帧强制广播(客户端要求) 200 | g.dirty = true 201 | case pb.ID_MSG_Result: 202 | m := &pb.C2S_ResultMsg{} 203 | if err := msg.Unmarshal(m); nil != err { 204 | l4g.Error("[game(%d)] processMsg player[%d] msg=[%d] UnmarshalPB error:[%s]", g.id, player.id, msg.GetMessageID(), err.Error()) 205 | return 206 | } 207 | g.result[player.id] = m.GetWinnerID() 208 | l4g.Info("[game(%d)] ID_MSG_Result player[%d] winner=[%d]", g.id, player.id, m.GetWinnerID()) 209 | player.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Result), nil)) 210 | default: 211 | l4g.Warn("[game(%d)] processMsg unknown message id[%d]", msgID) 212 | } 213 | 214 | } 215 | 216 | // Tick 主逻辑 217 | func (g *Game) Tick(now int64) bool { 218 | 219 | switch g.State { 220 | case k_Ready: 221 | delta := now - g.startTime 222 | if delta < MaxReadyTime { 223 | if g.checkReady() { 224 | g.doStart() 225 | g.State = k_Gaming 226 | } 227 | 228 | } else { 229 | if g.getOnlinePlayerCount() > 0 { 230 | // 大于最大准备时间,只要有在线的,就强制开始 231 | g.doStart() 232 | g.State = k_Gaming 233 | l4g.Warn("[game(%d)] force start game because ready state is timeout ", g.id) 234 | } else { 235 | // 全都没连进来,直接结束 236 | g.State = k_Over 237 | l4g.Error("[game(%d)] game over!! nobody ready", g.id) 238 | } 239 | } 240 | 241 | return true 242 | case k_Gaming: 243 | if g.checkOver() { 244 | g.State = k_Over 245 | l4g.Info("[game(%d)] game over successfully!!", g.id) 246 | return true 247 | } 248 | 249 | if g.isTimeout() { 250 | g.State = k_Over 251 | l4g.Warn("[game(%d)] game timeout", g.id) 252 | return true 253 | } 254 | 255 | g.logic.tick() 256 | g.broadcastFrameData() 257 | 258 | return true 259 | case k_Over: 260 | g.doGameOver() 261 | g.State = k_Stop 262 | l4g.Info("[game(%d)] do game over", g.id) 263 | return true 264 | case k_Stop: 265 | return false 266 | } 267 | 268 | return false 269 | } 270 | 271 | // Result 战斗结果 272 | func (g *Game) Result() map[uint64]uint64 { 273 | return g.result 274 | } 275 | 276 | // Close 关闭游戏 277 | func (g *Game) Close() { 278 | msg := pb_packet.NewPacket(uint8(pb.ID_MSG_Close), nil) 279 | g.broadcast(msg) 280 | } 281 | 282 | // Cleanup 清理游戏 283 | func (g *Game) Cleanup() { 284 | for _, v := range g.players { 285 | v.Cleanup() 286 | } 287 | g.players = make(map[uint64]*Player) 288 | 289 | } 290 | 291 | func (g *Game) doReady(p *Player) { 292 | 293 | if p.isReady == true { 294 | return 295 | } 296 | 297 | p.isReady = true 298 | 299 | msg := pb_packet.NewPacket(uint8(pb.ID_MSG_Ready), nil) 300 | p.SendMessage(msg) 301 | } 302 | 303 | func (g *Game) checkReady() bool { 304 | for _, v := range g.players { 305 | if !v.isReady { 306 | return false 307 | } 308 | } 309 | 310 | return true 311 | } 312 | 313 | func (g *Game) doStart() { 314 | 315 | g.clientFrameCount = 0 316 | g.logic.reset() 317 | for _, v := range g.players { 318 | v.isReady = true 319 | v.loadingProgress = 100 320 | } 321 | g.startTime = time.Now().Unix() 322 | msg := &pb.S2C_StartMsg{ 323 | TimeStamp: proto.Int64(g.startTime), 324 | } 325 | ret := pb_packet.NewPacket(uint8(pb.ID_MSG_Start), msg) 326 | 327 | g.broadcast(ret) 328 | 329 | g.listener.OnGameStart(g.id) 330 | } 331 | 332 | func (g *Game) doGameOver() { 333 | 334 | g.listener.OnGameOver(g.id) 335 | } 336 | 337 | func (g *Game) pushInput(p *Player, msg *pb.C2S_InputMsg) bool { 338 | 339 | cmd := &pb.InputData{ 340 | Id: proto.Uint64(p.id), 341 | Sid: proto.Int32(msg.GetSid()), 342 | X: proto.Int32(msg.GetX()), 343 | Y: proto.Int32(msg.GetY()), 344 | Roomseatid: proto.Int32(p.idx), 345 | } 346 | 347 | return g.logic.pushCmd(cmd) 348 | } 349 | 350 | func (g *Game) doReconnect(p *Player) { 351 | 352 | msg := &pb.S2C_StartMsg{ 353 | TimeStamp: proto.Int64(g.startTime), 354 | } 355 | ret := pb_packet.NewPacket(uint8(pb.ID_MSG_Start), msg) 356 | p.SendMessage(ret) 357 | 358 | framesCount := g.clientFrameCount 359 | var i uint32 = 0 360 | c := 0 361 | frameMsg := &pb.S2C_FrameMsg{} 362 | 363 | for ; i < framesCount; i++ { 364 | 365 | frameData := g.logic.getFrame(i) 366 | if nil == frameData && i != (framesCount-1) { 367 | continue 368 | } 369 | 370 | f := &pb.FrameData{ 371 | FrameID: proto.Uint32(i), 372 | } 373 | 374 | if nil != frameData { 375 | f.Input = frameData.cmds 376 | } 377 | frameMsg.Frames = append(frameMsg.Frames, f) 378 | c++ 379 | 380 | if c >= kMaxFrameDataPerMsg || i == (framesCount-1) { 381 | p.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Frame), frameMsg)) 382 | c = 0 383 | frameMsg = &pb.S2C_FrameMsg{} 384 | } 385 | } 386 | 387 | p.SetSendFrameCount(g.clientFrameCount) 388 | 389 | } 390 | 391 | func (g *Game) broadcastFrameData() { 392 | 393 | framesCount := g.logic.getFrameCount() 394 | 395 | if !g.dirty && framesCount-g.clientFrameCount < BroadcastOffsetFrames { 396 | return 397 | } 398 | 399 | defer func() { 400 | g.dirty = false 401 | g.clientFrameCount = framesCount 402 | }() 403 | 404 | /* 405 | msg := &pb.S2C_FrameMsg{} 406 | 407 | for i := g.clientFrameCount; i < g.logic.getFrameCount(); i++ { 408 | frameData := g.logic.getFrame(i) 409 | 410 | if nil == frameData && i != (g.logic.getFrameCount()-1) { 411 | continue 412 | } 413 | 414 | f := &pb.FrameData{} 415 | f.FrameID = proto.Uint32(i) 416 | msg.Frames = append(msg.Frames, f) 417 | 418 | if nil != frameData { 419 | f.Input = frameData.cmds 420 | } 421 | 422 | } 423 | if len(msg.Frames) > 0 { 424 | g.broadcast(pb_packet.NewPacket(uint8(pb.ID_MSG_Frame), msg)) 425 | } 426 | */ 427 | now := time.Now().Unix() 428 | 429 | for _, p := range g.players { 430 | 431 | // 掉线的 432 | if !p.IsOnline() { 433 | continue 434 | } 435 | 436 | if !p.isReady { 437 | continue 438 | } 439 | 440 | // 网络不好的 441 | if now-p.GetLastHeartbeatTime() >= kBadNetworkThreshold { 442 | continue 443 | } 444 | 445 | // 获得这个玩家已经发到哪一帧 446 | i := p.GetSendFrameCount() 447 | c := 0 448 | msg := &pb.S2C_FrameMsg{} 449 | for ; i < framesCount; i++ { 450 | frameData := g.logic.getFrame(i) 451 | if nil == frameData && i != (framesCount-1) { 452 | continue 453 | } 454 | 455 | f := &pb.FrameData{ 456 | FrameID: proto.Uint32(i), 457 | } 458 | 459 | if nil != frameData { 460 | f.Input = frameData.cmds 461 | } 462 | msg.Frames = append(msg.Frames, f) 463 | c++ 464 | 465 | // 如果是最后一帧或者达到这个消息包能装下的最大帧数,就发送 466 | if i == (framesCount-1) || c >= kMaxFrameDataPerMsg { 467 | p.SendMessage(pb_packet.NewPacket(uint8(pb.ID_MSG_Frame), msg)) 468 | c = 0 469 | msg = &pb.S2C_FrameMsg{} 470 | } 471 | 472 | } 473 | 474 | p.SetSendFrameCount(framesCount) 475 | 476 | } 477 | 478 | } 479 | 480 | func (g *Game) broadcast(msg network.Packet) { 481 | for _, v := range g.players { 482 | v.SendMessage(msg) 483 | } 484 | } 485 | 486 | func (g *Game) broadcastExclude(msg network.Packet, id uint64) { 487 | for _, v := range g.players { 488 | if v.id == id { 489 | continue 490 | } 491 | v.SendMessage(msg) 492 | } 493 | } 494 | 495 | func (g *Game) getPlayer(id uint64) *Player { 496 | 497 | return g.players[id] 498 | } 499 | 500 | func (g *Game) getPlayerCount() int { 501 | 502 | return len(g.players) 503 | } 504 | 505 | func (g *Game) getOnlinePlayerCount() int { 506 | 507 | i := 0 508 | for _, v := range g.players { 509 | if v.IsOnline() { 510 | i++ 511 | } 512 | } 513 | 514 | return i 515 | } 516 | 517 | func (g *Game) checkOver() bool { 518 | // 只要有人没发结果并且还在线,就不结束 519 | for _, v := range g.players { 520 | if !v.isOnline { 521 | continue 522 | } 523 | 524 | if _, ok := g.result[v.id]; !ok { 525 | return false 526 | } 527 | } 528 | 529 | return true 530 | } 531 | 532 | func (g *Game) isTimeout() bool { 533 | return g.logic.getFrameCount() > MaxGameFrame 534 | 535 | return true 536 | } 537 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc.