发布基础组件包-KISS(日常用作游戏服务器开发,但不限于游戏服务器开发)

nothollyhigh · 2019-07-14 21:13:20 · 3563 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2019-07-14 21:13:20 的主题,其中的信息可能已经有所发展或是发生改变。

KISS - Keep It Simple & Stupid

MIT licensed Go Report Card

安装

  • go get github.com/nothollyhigh/kiss/...

KISS可以用来做什么?

  • 有的人喜欢"框架"这个词,KISS的定位是提供一些基础组件方便搭积木实现架构方案,组件不限于项目类型

  • 作者主要从事游戏和web服务器开发,常用来构建游戏服务器,一些示例:

  1. 单进程服务器示例

  2. kissgate网关,支持kiss格式的tcp/websocket连接反向代理到tcp服务,支持线路检测、负载均衡、realip等,常用来做游戏集群的网关,kiss协议格式详见 net包

  3. 集群是不同功能服务的拆分和实现,每个游戏的需求都可能不一样,请根据实际需求自行设计和实现

KISS组件包简介

一、net,网络包

  1. Tcp 可以用做游戏服务器,支持自定义协议格式、压缩、加密等

  2. Websocket 可以用做游戏服务器,支持自定义协议格式、压缩、加密等

  3. Rpc 可以灵活使用任意序列化、反序列化,给用户更多自由,如protobuf、json、msgpack、gob等 支持服务端异步处理,服务端不必须在方法中处理完调用结果,可以异步处理结束后再发送结果 不像GRPC等需要生成协议、按格式写那么多额外的代码,用法上像写 net/http 包的路由一样简单

  4. Http 支持优雅退出、pprof等

  5. 详见 net

二、log,日志包

  • 自己实现 log 包之前,我简单尝试过标准库的 log 和一些三方的日志包,但是对日志文件落地不太友好, 比如日志文件按目录拆分、文件按时间和size切分、日志位置信息等

  • KISS 的 log 包日志支持:

  • 日志位置信息,包括文件、行数

  • 支持文件日志,支持bufio
  • 文件日志按时间拆分目录
  • 文件日志按时间格式切分
  • 文件日志按size切分
  • 支持钩子对日志做结构化或其他自定义处理

  • 详见 log

三、sync包

  • 开启debug支持死锁告警 web相关无状态服务通常不需要锁,游戏逻辑多耦合,不小心可能导致死锁,可以用这个包来排查

  • WaitSession用法类似标准库的WaitGroup,但是可以指定session进行等待,支持超时

  • 详见 sync

四、timer,定时器

  • 标准库的 time.AfterFunc 触发时会创建一个协程来调用回调函数,大量定时器短时间内集中触发时开销较大

  • KISS 的 timer 是小堆实现的,一个实例只需要一个协程就可以管理大量定时器,支持同步异步接口 主要用于优化标准库 time.AfterFunc 的协程开销,但要注意线头阻塞的问题 耗时较长的定时器回调建议仍使用 time.AfterFunc

  • 详见 timer

五、event,事件包

  • 进程内的发布订阅组件,观察者模式,可以用于模块间解耦

  • 详见 event

六、util,杂货铺

  • 最常用的 Go,HandlePanic,Safe,处理panic,打印异常调用栈信息

  • Workers,任务池,用于控制一定数量的协程异步处理任务 WorkersLink,也是任务池,但在Workers基础上做了一点扩展,支持异步处理的顺序组装

  • Qps,方便统计、打印一些qps功能

  • 详见 util


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3563 次点击  
加入收藏 微博
7 回复  |  直到 2019-07-22 12:27:51
nothollyhigh
nothollyhigh · #1 · 6年之前

tcp echo server

package main

import (
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
    "time"
)

var ()

const (
    addr = ":8888"

    CMD_ECHO = uint32(1)
)

func onEcho(client *net.TcpClient, msg net.IMessage) {
    log.Info("tcp server onEcho from %v: %v", client.Conn.RemoteAddr().String(), string(msg.Body()))
    client.SendMsg(msg)
}

func main() {
    server := net.NewTcpServer("Echo")

    // 初始化协议号
    server.Handle(CMD_ECHO, onEcho)

    server.Serve(addr, time.Second*5)
}

tcp echo client

package main

import (
    "fmt"
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
    "time"
)

var (
    addr = "127.0.0.1:8888"

    CMD_ECHO = uint32(1)
)

func onConnected(c *net.TcpClient) {
    log.Info("TcpClient OnConnected")
}

func onEcho(client *net.TcpClient, msg net.IMessage) {
    log.Debug("tcp client onEcho from %v: %v", client.Conn.RemoteAddr().String(), string(msg.Body()))
}

func main() {
    autoReconn := true
    netengine := net.NewTcpEngine()

    // 初始化协议号
    netengine.Handle(CMD_ECHO, onEcho)

    client, err := net.NewTcpClient(addr, netengine, nil, autoReconn, onConnected)
    if err != nil {
        log.Panic("NewTcpClient failed: %v, %v", client, err)
    }

    for i := 0; true; i++ {
        err = client.SendMsg(net.NewMessage(CMD_ECHO, []byte(fmt.Sprintf("hello %v", i))))
        if err != nil {
            log.Error("tcp client echo failed: %v", err)
        }
        time.Sleep(time.Second)
    }
}
nothollyhigh
nothollyhigh · #2 · 6年之前

websocket echo server

package main

import (
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
)

var (
    addr = ":8888"

    CMD_ECHO = uint32(1)
)

func onEcho(client *net.WSClient, msg net.IMessage) {
    log.Info("ws server onEcho from %v: %v", client.Conn.RemoteAddr().String(), string(msg.Body()))
    client.SendMsg(msg)
}

func main() {
    server, err := net.NewWebsocketServer("echo", addr)
    if err != nil {
        log.Panic("NewWebsocketServer failed: %v", err)
    }

    // 初始化http ws路由
    server.HandleWs("/ws/echo")

    // 初始化协议号
    server.Handle(CMD_ECHO, onEcho)

    server.Serve()
}

websocket echo client

package main

import (
    "fmt"
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
    "time"
)

var (
    addr = "ws://localhost:8888/ws/echo"

    CMD_ECHO = uint32(1)
)

func onEcho(client *net.WSClient, msg net.IMessage) {
    log.Debug("ws client onEcho from %v: %v", client.Conn.RemoteAddr().String(), string(msg.Body()))
}

func main() {
    client, err := net.NewWebsocketClient(addr)
    if err != nil {
        log.Panic("NewWebsocketClient failed: %v, %v", err, time.Now())
    }

    // 初始化协议号
    client.Handle(CMD_ECHO, onEcho)

    for i := 0; true; i++ {
        err = client.SendMsg(net.NewMessage(CMD_ECHO, []byte(fmt.Sprintf("hello %v", i))))
        if err != nil {
            log.Error("ws client echo failed: %v", err)
            break
        }
        time.Sleep(time.Second)
    }
}
nothollyhigh
nothollyhigh · #3 · 6年之前

rpc server

package main

import (
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
    "time"
)

var (
    addr = "0.0.0.0:8888"
)

type HelloRequest struct {
    Message string
}

type HelloResponse struct {
    Message string
}

// Hello方法
func Hello(ctx *net.RpcContext) {
    req := &HelloRequest{}

    err := ctx.Bind(req)
    if err != nil {
        log.Error("Hello failed: %v", err)
        return
    }

    // 直接回包
    // err = ctx.Write(&HelloResponse{Message: req.Message})
    // if err != nil {
    //     log.Error("Hello failed: %v", err)
    //     return
    // }
    // log.Info("HelloRequest: %v", req.Message)

    // 支持异步回包
    go func() {
        err = ctx.Write(&HelloResponse{Message: req.Message})
        if err != nil {
            log.Error("Hello failed: %v", err)
            return
        }

        log.Info("HelloRequest: %v", req.Message)
    }()
}

func main() {
    server := net.NewRpcServer("Rpc")

    // 初始化方法,类似http初始化路由
    server.HandleRpcMethod("Hello", Hello)

    // 启动服务
    server.Serve(addr, time.Second*5)
}

rpc client

package main

import (
    "github.com/nothollyhigh/kiss/log"
    "github.com/nothollyhigh/kiss/net"
    "time"
)

var (
    addr = "0.0.0.0:8888"
)

type HelloRequest struct {
    Message string
}

type HelloResponse struct {
    Message string
}

func onConnected(c *net.TcpClient) {
    log.Info("RpcClient OnConnected")
}

func main() {
    engine := net.NewTcpEngine()
    client, err := net.NewRpcClient(addr, engine, nil, onConnected)
    if err != nil {
        log.Panic("NewReqClient Error: %v", err)
    }

    for {
        req := &HelloRequest{Message: "kiss"}
        rsp := &HelloResponse{}

        // 调用Hello方法
        err = client.Call("Hello", req, rsp, time.Second*3)
        if err != nil {
            log.Error("Hello failed: %v", err)
        } else {
            log.Info("HelloResponse: %v", rsp.Message)
        }

        time.Sleep(time.Second)
    }
}
nothollyhigh
nothollyhigh · #4 · 6年之前

自己顶

nothollyhigh
nothollyhigh · #5 · 6年之前

日志

一、日志文件切割

package main

import (
    "fmt"
    "github.com/nothollyhigh/kiss/log"
    "io"
    "os"
    "time"
)

func main() {
    fileWriter := &log.FileWriter{
        RootDir:     "./logs/" + time.Now().Format("20060102150405/"), //日志根目录,每次启动新建目录
        DirFormat:   "200601021504/",                                  //按时间格式分割日志文件子目录,""则不拆分子目录;此处测试按分钟
        FileFormat:  "20060102150405.log",                             //按时间格式切割日志文件,此处测试按秒
        MaxFileSize: 1024 * 256,                                       //按最大size切割日志文件
        EnableBufio: false,                                            //是否启用bufio,重要日志建议不开启
    }

    out := io.MultiWriter(os.Stdout, fileWriter)
    log.SetOutput(out)

    log.SetLevel(log.LEVEL_WARN)

    i := 0
    for {
        i++
        log.Debug(fmt.Sprintf("log %d", i))
        log.Info(fmt.Sprintf("log %d", i))
        log.Warn(fmt.Sprintf("log %d", i))
        log.Error(fmt.Sprintf("log %d", i))
    }
}

二、在gin中使用kiss log

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/nothollyhigh/kiss/log"
    "os"
    "time"
)

func main() {
    router := gin.New()

    router.Use(func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery

        c.Next()

        end := time.Now()
        latency := end.Sub(start)
        clientIP := c.ClientIP()
        method := c.Request.Method
        statusCode := c.Writer.Status()
        comment := c.Errors.ByType(gin.ErrorTypePrivate).String()
        if raw != "" {
            path = path + "?" + raw
        }
        logLevel := log.LEVEL_INFO
        if latency > time.Second {
            logLevel = log.LEVEL_WARN
        }

        fmt.Fprintf(os.Stdout, log.LogWithFormater(logLevel, log.DefaultLogDepth-1, log.DefaultLogTimeLayout, "| %3d | %13v | %15s | %-7s %s\n%s",
            statusCode,
            latency,
            clientIP,
            method,
            path,
            comment,
        ))
    })

    //gin.SetMode(gin.ReleaseMode)

    n := 0
    router.GET("/hello", func(c *gin.Context) {
        n++
        if n%2 == 1 {
            time.Sleep(time.Second)
        }
        c.String(200, "hello")
    })

    router.Run(":8080")
}

三、自定义日志处理

package main

import (
    "encoding/json"
    "fmt"
    "github.com/nothollyhigh/kiss/log"
    "runtime"
    "strings"
)

var (
    // 按天切割日志文件,日志根目录下不设子目录,不限制单个日志文件大小
    fileWriter = &log.FileWriter{
        RootDir:     "./logs/",        //日志根目录
        DirFormat:   "20060102-1504/", //日志根目录下无子目录
        FileFormat:  "20060102.log",   //日志文件命名规则,按天切割文件
        MaxFileSize: 1024 * 1024,      //单个日志文件最大size,0则不限制size
        EnableBufio: false,            //是否开启bufio
    }
)

// 实现log.ILogWriter接口
type LogWriter struct{}

func (w *LogWriter) WriteLog(l *log.Log) (n int, err error) {
    _, file, line, ok := runtime.Caller(l.Depth + 1)
    if !ok {
        file = "???"
        line = -1
    } else {
        pos := strings.LastIndex(file, "/")
        if pos >= 0 {
            file = file[pos+1:]
        }
    }

    l.File = file
    l.Line = line

    data, _ := json.Marshal(l)
    data = append(data, '\n')
    n, err = fileWriter.Write(data)
    fmt.Printf(string(data))

    return n, err
}

func main() {
    // 设置为nil则默认的日志不处理,如果需要可以同时使用
    log.SetOutput(nil)

    // 设置自定义日志处理接口
    log.SetStructOutput(&LogWriter{})

    log.SetLevel(log.LEVEL_WARN)

    for i := 0; i < 10000; i++ {
        log.Debug("log %d", i)
        log.Info("log %d", i)
        log.Warn("log %d", i)
        log.Error("log %d", i)
    }
}
nothollyhigh
nothollyhigh · #6 · 6年之前

自己顶~

nothollyhigh
nothollyhigh · #7 · 6年之前

再顶~

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传