准备阶段

总体思考:

聊天室服务器决定采用网络层和业务层分离的思想,业务层向网络层提供一个数据流入的入口,网络层向业务层提供一个数据发送的入口,整个业务层使用单协程处理,当遇到IO操作等可能阻塞业务处理协程的情况时,用另外的协程进行那些IO操作。

网络层思考:

程序启动后的主协程,负责socket的创建和监听以及Accept客户端的连接,每当一个客户端连接,创建一个connection对象保存该连接的socket,并且该connection启动一个网络IO发送协程和一个网络IO接收协程,就算有大量连接,但在没有数据交互式该连接的两个协程会处于阻塞状态,减少CPU调度资源消耗。
服务器客户端通信使用简单的TLV格式,每个connection的接收协程循环从socket中读取数据,先读取包头(T和L)的数据,然后通过L的长度读取V的数据。业务处理协程提供一个channel,接收协程将对接受到的数据进行的行为封装为一个function打入该channel,然后继续循环。业务处理协程通过循环select从channel取得该function并执行。
connection拥有一个发送channel,它的网络IO发送协程循环用select从该channel读取数据并发送,connection还提供一个给业务协程调用的接口函数,当业务处理协程处理完后,把要发送的数据打入connection的发送channel,使得发送协程能将数据发出。

业务层思考:

业务协程进行密集的逻辑计算,不应该有阻塞的调用,使用单协程就可以承担运算任务,当有需要IO操作时,用其它协程去进行。
只提供一个入口函数给网络层,在该函数分发不同类型的消息进入不同的业务处理函数。
业务处理函数和玩家不同部分的数据结构分离,新的协议添加对应的业务处理函数。

实施阶段

网络层:

创建了一个hnet包,网络层相关代码放在其中,server.go编写了socket的创建监听和接收连接逻辑。
每接收一个连接创建一个connection对象,connection对象定义在connection.go中,然后调用start分别启动读写协程。在thread_work.go中定义了两个结构体,一个用来创建运行业务协程,另外一个创建了本机cpu数量的协程池用于处理业务层的IO任务。
proto.go定义了两个结构体,一个用于业务和网络间消息传递的封装,另一个用于拆包封包操作。
网络层还创建了一个hiface包,提供了server和connection以及proto.go中封装的消息的接口。

业务层:

创建了一个server包,其中the_world.go提供一个msghandle函数供网络层绑定,网络层的消息从这个函数进入业务层,the_world.go定义了一个world结构体,保存了当前的连接map。player.go则是每个player的相关信息
server包中还有msgwork包,里面分别定义了不同消息类型的结构体,这些结构体只有以On开头的协议处理方法,On后面的命名对应protobuf文件中定义的MsgID。程序会在开始运行时,调用theworld中的初始化函数,会将主动添加的msgwork的结构体的方法通过反射遍历,然后判断protobuf中是否定义了同名的协议,然后绑定到theworld的handlemap成员中,当消息进入msghandle时,会通过handlemap进入相应的协议处理方法中。
消息通过msghandle调用不同的OnXXX方法后,在其中使用protobuf生成的结构体解包,处理完后封包,调用sendmessage将消息发给网络层,网络层将其打入sendChannel然后又write协程进行发送。

功能:

在msgwork下添加了chat_message和player_message,当是player操作相关协议时,消息进入player_message结构体的各方法,当是聊天时,消息进入chat_message中的结构体的各方法。
player_message提供了玩家进入世界,创建角色和获取全部角色三个协议处理方法,chat_message提供历史消息,广播聊天,私密聊天三个协议处理方法。这些方法都有两个参数,一个是进行处理的player对象,一个是消息对象,当需要使用player的成员时,可以进行调用。
玩家需要先进入世界,然后status改变,可以继续进行取名字,玩家需要进行取名消息的发送,然后状态改变才能进行消息相关功能。
玩家取名后,客户端自动发送历史消息协议,获取最近50条世界聊天的记录,获取记录将通过协程池异步调用。然后玩家通过客户端不同的指令进行广播或者私密聊天的发送,其中广播聊天会通过异步调用存入数据库。也可以获取当前在线的已经取名的玩家列表。

机器人:

机器人定义了一个消息表,存储了不同的对话内容。首先创建N个机器人,每个机器人按照进入世界->取名->获取历史消息的顺序进行协议收发,然后每秒随机选择两种行为进行操作,一种是先获取当前在线列表,然后从在线列表随机取一名玩家,从消息表随机取一条消息进行发送,还有一种则是广播随机发送一条消息表的消息。

期间遇到的问题

多个机器人退出后,服务器向已经关闭的机器write,发生reset错误引发panic,因此在write协程添加一个退出channel,read协程遇到EOF错误将该channel打入数据,write协程随之唤醒然后进行退出流程。
后来又遇到业务处理协程channel满后没有继续处理的情况,后面发现在write时若对方关闭会返回错误,但是并没有处理而是直接退出协程,导致某次业务处理将要发送消息打入发送channel,但是write协程已经退出,channel一直未被处理容量变满,业务处理协程尝试向sendChannel中打入数据但是已满所以一直阻塞住,在write错误后添加退出流程代码,解决了这个问题。
业务处理协程中的IO操作将放到hnet中的协程池中异步处理,刚开始使用随机数负载均衡,后面听取建议使用轮询。

心得

本次的聊天室练习,练习了golang相关的语言特性,比如goroutine、channel、反射等,也练习了socket编程,也练习了日志库、mysql操作、配置表读取,也对golang的熟练度有所增加。得到了清咖老师的许多建议,比如使用log可以更快定位问题,结构体命名和成员及方法的命名风格,对反射的使用应该有所权衡等等。