A chat server is written by golang.
- 编译原代码:
go build main.go
- 将config目录及web目录复制到执行文件所在目录中。之后便可以直接执行文件
./main
- 若需要后台守护执行,在Linux下,可以使用如下指令:
nohup ./main > /dev/null &
- 在浏览器中输入
打开页面进行访问,需要模拟多个用户时,则多次打开即可。若IP不为127.0.0.1,则需要修改web/index.html中的相应IP。若需要修改端口,则需要同时修改web/index.html及main.go中的端口信息。
在WEB上,首先需要输入一个用户名并进行登录,登录成功后,这时可以见到房间列表,可以选择一个房间进入。如果房间列表为空,可以自行创建一个房间,创建后会自动进入该房间。
进入房间后,会自动下发近50条当前房间和消息。这时可以输入消息或GM指令在文本框中并点击“发送”。则可以发送消息或执行相应的GM操作。
api 该目录下存放相关的api处理逻辑,每个api方法对应一条请求指令,api按类型分组,每组一个文件
config 该目录用于存放一些配置文件,当前只有filter.txt这一个过滤词库
model 该目录主要存放数据结构
router 该目录用于处理指令与api的关系
service 该目录存放核心业务逻辑类
utils 该目录存放一些工具类
web 该目录下存放的是客户端
首先,系统中各个模块的大概的调用行为一般为:main -> router -> api -> service -> model
系统本身比较简单,采用websocket进行通讯。当客户端连接上服务端后,在accept方法中,会先接收用户名,接收到用户名后则会使用连接与用户名创建UserInfo对象。并将其添加到用户列表中,这样以后便可以根据用户名获取到用户对象了。
每个用户会有一个发送消息的协程、一个接收消息的协程以及一个逻辑协程。当用户相关数据创建完毕后,程序则会进入这三个协程的消息循环。直到出现定时消息(如心跳)、用户请求消息、数据下发消息,才会进行相应的处理。
接收协程每次只会接收一条消息,将其放入接收队列后便会被挂起,在这条消息被消费前不会再接收其它新的消息。而发送消息是异步执行发送任务的,在缓冲区未满的情况下是不需要等待发送完成的。接收到的消息会放入model.Conn的Received接收队列中,而需要发送的消息则会放入Sending发送队列中。
Received队列中的消息是靠Router.DoCmd中的消息循环来消费。Rounter.DoCmd会依据不同的cmd将逻辑分发给不同的API处理,API处理时,会调用相关的service。
在websocket的基础上,我们采用了json协议(除了一开始的获取用户名是字符串外,其它均是json)。而基本的消息结构为: { cmd: xxx, body: xxx } 其中body是可以为任意类型的,根据不同的指令而有不同的数据结构。比如,我们下发消息的JSON则为: { cmd:"msg", body: { from:"xxx", room_id: 1, msg: "hello,world!" } }
请求指令 { cmd: "newroom", body: null }
创建房间成功后,则会直接下发进入房间的指令及房间的历史消息推送。
请求指令 { cmd: "setroom", body: 房间ID }
进入房间后,系统会自动下发该房间的历史消息
通知指令 { cmd: "enterroom", body: 房间ID }
请求指令 { cmd: "leaveroom", body: null }
由于一个用户同一时间只能进入一个房间,所以这里是不需要携带房间ID的,当前所在房间ID会记录在UserInfo中。
通知指令 { cmd: "leaveroom", body: null }
请求指令 { cmd: "roomlist" body: null }
请求返回当前所有房间的列表
通知指令 { cmd: "roomlist", body: { list: [房间ID1,房间ID2,...] } }
返回房间列表,这里只有房间ID
请求指令 { cmd: "history", body: 房间ID }
请求获取最近的50条房间消息
请求指令 { cmd: "heartjump", body: null, }
一般会由系统每15秒下发一个心跳通知,这时客户端必须回复此指令,不然系统在尝试2次后将会关闭当前用户的连接
通知指令 { cmd: "heartjump", body: null, }
每15秒下发给客户端,必须及时回复
请求指令 { cmd: "sendmsg", body: "消息内容" }
请求在当前所在的房间里向房间里的所有人发送一条消息,这条消息会被Filter所过滤后转发给当前房间的所有人。被过滤的内容会被替换为*
通知指令 { cmd: "msg", body: { from: "发送者名称", room_id: 房间ID, msg: "消息内容", } }
此指令会用于下发消息使用,包括实时发送的消息及房间的历史消息。
通知指令 { cmd: "msg", body: { from: "SYSTEM", room_id: 0, msg: "消息内容", } }
此指令用于发送系统消息,当前主要是用于回复GM指令的结果使用。
- 词库存储格式
为了提高过滤的处理速度,词库需要以特定的格式在内存中进行存储。当前使用的多层Hash的结构。顶层Hash的KEY为词的第一字,第二层为第二字,依次类推直到存储完毕。
若现在需要存储 hello及 world这两个词,若用伪代码表示,则它们将会被存储如下:
map[h][e][l][l][o] 及 map[w][o][r][l][d]
最大层数与词库中最长单词的长度一致。
- 过滤处理
在过滤时,会遍历消息字符串,依次去匹配多层Hash的KEY,若匹配上,则过滤当前单词。在匹配时会尽量匹配最长可匹配单词。即是说,若我们有过滤词:abc 及 abcd,针对消息abcdef过滤时,结果为****ef
- 中文支持
词库使用rune进行存储,支持中文及其它文字的过滤
由于时间原因,当前架构为单机架构,若需要更多的IO处理,可以进行分服处理(需要调整代码),采用微服务架构。主要**为,将用户管理、房间管理、过滤服务进行分离。每个房间(所在哪个服务器)均需要在房间管理服务中注册,进入房间前由房间管理服务返回IP地址。若不使用网关,则可以直接将房间服务器IP及端口返回给客户端,由客户端直连。用户管理主要管理当前用户的信息,以防止重复登录的情况发生。同时记录当前用户所在的房间服务器及房间号。采用这类架构后,吞吐量可以提高,延迟会有所增加。