服务器设计散记

http://blog.csdn.net/madaoheng/article/details/17463617

window/linux
-> 
网络IO模型(IOCP/epool) -> 网络协议TCP/UDP -> 消息包格式(包长|消息码|包体)

 

关于服务器上port是监听用的,对指定端口进行监听,多个连接到来时系统为它们各自分配不同的socket句柄,服务器还是在这个端口上与它们通信,所以socket句柄才是描述每个连接的唯一标识,与端口无关;

登陆服务器

1,验证用户的登陆信息

a,密码信息以明文传输,然后进行校验,毫无安全性可言;

b,密码信息已可逆加密的字符串进行传输,可以被逆推,安全性低;

c,密码信息以散列(如md5算法)后获得不可逆的字符串进行传输,这样同一个密码信息得到的字符串是相同的,只要截取到这段字符串就可以进行登陆,安全性不高;

d,密码信息以动态的算法进行散列(SRP算法,安全远程密码);

2,从大区服务器获取游戏世界列表发给已经通过验证的客户端;

 

主循环

拥有唯一的一个线程,也是它的主循环线程,根据监听的socket的不同情况做select操作,更高级的选择是可以选择win下IOCP或linux下epoll;

 

在服务器的实现上,网络IO与逻辑处理一般会放在不同的线程中,以避免耗时长的IO过程阻塞了需要立即反应的游戏逻辑,当然还有数据库IO;

 

如上所说为了游戏主逻辑循环顺畅运行,所有比较耗时的IO操作如网络IO、数据库IO和日志IO等等都会放到独立的线程中去做,而主循环需要这部分IO结束后的数据来处理,所以就需要提供一个支持多线程互斥访问安全的消息队列,各个IO线程把整理好的消息包都加入到主循环的消息队列中,那么主循环要做的事情就很简单了:从消息队列中获取消息->处理消息->获取下一条消息;

 

优化技术:

1,消息队列

如果只是简单的使用list和一个互斥锁来实现消息队列,那么性能上就会比较不如人意,瓶颈在于频繁的锁竞争上:

a,队列容器:提供一个队列容器,里面有多个队列,每个队列可以固定存放一定数量的消息,IO线程从中获取一个空队列,往里面写数据,写满之后放回容器队列,而逻辑线程则从中获取有消息的队列来读取,处理后清空再放回容器队列,这样锁竞争仅发生在获取容器队列时,而平时对已经获取到的消息队列的读取/写入不需要加锁;

问题在于如果数据很少,那么IO线程一直没有写满消息队列,所以没有放回容器,那么可能会出现逻辑线程无数据可处理的情况(获取增加一个最长持有时间?);

b,双队列:只使用两个队列,分别用于逻辑线程读取和IO线程写入,当逻辑线程读取完队列之后就会IO线程的队列调换,这种方式加锁次数会比较多,IO线程在写入的时候需要加锁防止对了被调换,逻辑线程在调换时也需要加锁防止被IO线程来写入;

虽然锁的调用次数比较多,但是除了在逻辑线程调换队列的时候,其他情况下都不会引起阻塞,问题在于当有多个IO线程的时候对写入队列会存在竞争;

2,环形缓冲区

在网络IO线程中,会为每一个链接都准备一个环形缓冲区(类似使用循环队列一般,优点是只用固定的内存块,而不用频繁分配内存,且重复利用规则可以用更少的内存块做更多的事情),用于临时存放接收到的数据,以应付半包及粘包的情况;

 

3,打包方式

逻辑线程给客户端发送数据时,有时候可能会存在一些问题,比如遇到系统发送缓冲区满而阻塞,或者只发送了一部分数据,这就意味着需要最好先将数据缓存一下,即使遇到未发送完的,那么在下一次处理时可以继续发送;

缓存的话:

a,为每个socket链接准备一个缓冲区

b,设置一个全局缓冲区,把要发送的数据和socket加入到全局缓冲区中,再进一步可以用独立线程来处理,内部维护一个消息队列,逻辑线程把要发送的数据加入到队列中,独立发送线程循环去发送,这样哪怕有阻塞也不会影响逻辑线程;

如果出现广播的情况,那么a方法需要为每个socket都保存一份相同的数据,而b方法这只需要将消息加入队列一次,并指明发送对象为all即可;

猜你喜欢

转载自blog.csdn.net/q8547957/article/details/51850424