# KISS - Keep It Simple & Stupid
[![MIT licensed][1]][2]
[![Go Report Card][3]][4]
[1]: https://img.shields.io/badge/license-MIT-blue.svg
[2]: LICENSE.md
[3]: https://goreportcard.com/badge/github.com/nothollyhigh/kiss
[4]: https://goreportcard.com/report/github.com/nothollyhigh/kiss
- [KISS原则](https://zh.wikipedia.org/wiki/KISS%E5%8E%9F%E5%88%99) 是指在设计当中应当注重简约的原则
- 作者水平有限,欢迎交流和指点,qq群: 817937655
- [https://github.com/nothollyhigh/kiss](https://github.com/nothollyhigh/kiss)
## 安装
* go get github.com/nothollyhigh/kiss/...
## KISS可以用来做什么?
- 有的人喜欢"框架"这个词,KISS的定位是提供一些基础组件方便搭积木实现架构方案,组件不限于项目类型
- 作者主要从事游戏和web服务器开发,常用来构建游戏服务器,一些示例:
> 1. [单进程服务器示例](https://github.com/nothollyhigh/hellokiss)
> 2. [kissgate网关](https://github.com/nothollyhigh/kissgate),支持kiss格式的tcp/websocket连接反向代理到tcp服务,支持线路检测、负载均衡、realip等,常用来做游戏集群的网关,kiss协议格式详见 [net包](https://github.com/nothollyhigh/kiss/blob/master/net/README.md)
> 3. 集群是不同功能服务的拆分和实现,每个游戏的需求都可能不一样,请根据实际需求自行设计和实现
## KISS组件包简介
### 一、[net,网络包](https://github.com/nothollyhigh/kiss/blob/master/net/README.md)
1. Tcp
可以用做游戏服务器,支持自定义协议格式、压缩、加密等
2. Websocket
可以用做游戏服务器,支持自定义协议格式、压缩、加密等
3. Rpc
可以灵活使用任意序列化、反序列化,给用户更多自由,如protobuf、json、msgpack、gob等
支持服务端异步处理,服务端不必须在方法中处理完调用结果,可以异步处理结束后再发送结果
不像GRPC等需要生成协议、按格式写那么多额外的代码,用法上像写 net/http 包的路由一样简单
4. Http
支持优雅退出、pprof等
- 详见 [net](https://github.com/nothollyhigh/kiss/blob/master/net/README.md)
### 二、[log,日志包](https://github.com/nothollyhigh/kiss/blob/master/log/README.md)
- 自己实现 log 包之前,我简单尝试过标准库的 log 和一些三方的日志包,但是对日志文件落地不太友好,
比如日志文件按目录拆分、文件按时间和size切分、日志位置信息等
- KISS 的 log 包日志支持:
1. 日志位置信息,包括文件、行数
2. 支持文件日志,支持bufio
3. 文件日志按时间拆分目录
4. 文件日志按时间格式切分
5. 文件日志按size切分
6. 支持钩子对日志做结构化或其他自定义处理
- 详见 [log](https://github.com/nothollyhigh/kiss/blob/master/log/README.md)
### 三、[sync包](https://github.com/nothollyhigh/kiss/blob/master/sync/README.md)
- 开启debug支持死锁告警
web相关无状态服务通常不需要锁,游戏逻辑多耦合,不小心可能导致死锁,可以用这个包来排查
- WaitSession用法类似标准库的WaitGroup,但是可以指定session进行等待,支持超时
- 详见 [sync](https://github.com/nothollyhigh/kiss/blob/master/sync/README.md)
### 四、[timer,定时器](https://github.com/nothollyhigh/kiss/blob/master/timer/README.md)
- 标准库的 time.AfterFunc 触发时会创建一个协程来调用回调函数,大量定时器短时间内集中触发时开销较大
- KISS 的 timer 是小堆实现的,一个实例只需要一个协程就可以管理大量定时器,支持同步异步接口
主要用于优化标准库 time.AfterFunc 的协程开销,但要注意线头阻塞的问题
耗时较长的定时器回调建议仍使用 time.AfterFunc
- 详见 [timer](https://github.com/nothollyhigh/kiss/blob/master/timer/README.md)
### 五、[event,事件包](https://github.com/nothollyhigh/kiss/blob/master/event/README.md)
- 进程内的发布订阅组件,观察者模式,可以用于模块间解耦
- 详见 [event](https://github.com/nothollyhigh/kiss/blob/master/event/README.md)
### 六、[util,杂货铺](https://github.com/nothollyhigh/kiss/blob/master/util/README.md)
- 最常用的 Go,HandlePanic,Safe,处理panic,打印异常调用栈信息
- Workers,任务池,用于控制一定数量的协程异步处理任务
WorkersLink,也是任务池,但在Workers基础上做了一点扩展,支持异步处理的顺序组装
- Qps,方便统计、打印一些qps功能
- 详见 [util](https://github.com/nothollyhigh/kiss/blob/master/util/README.md)
### 日志
#### 一、日志文件切割
```golang
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
```golang
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")
}
```
#### 三、自定义日志处理
```golang
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)
}
}
```
#5
更多评论
### tcp echo server
```golang
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
```golang
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)
}
}
```
#1
### websocket echo server
```golang
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
```golang
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)
}
}
```
#2