网络游戏核心技术与实战-偏重网络部分的笔记

游戏部分下次再来,网络服务器部分太薄弱了先恶补一下

正片开始:

OSI模型
1.应用层:提供具体的通信服务,比如文件和邮件的传送,访问远程数据库等
      HTTP、FTP(文件传输协议)
2.表示层:规定数据的表现形式,比如将EBCDIC表示的文本文件转换为ASCII码
3.会话层:规定应用程序之间的通信从开始到结束之间的顺序(连接中断时的恢复连接)
4.传输层:应用程序进程之间端到端的通信管理,如差错恢复、重发控制等
      TCP、UDP、ICMP
5.网络层:对网络中的通信链路进行选择(路由选择)、中继
      IP
6.数据链路层:通信设备间的信号收发
7.物理层:物理连接

套接字API中包括直接控制IP协议的第3层API和使用TCP等协议的第4层API,游戏编程只用到第4层的。
由于第三层IP协议并不会重发数据包,也无法保证到达顺序,因此第三层是不可靠的
面向连接(流式,STREAM)
数据报
总结:
BLCA(bind\listen\connect\accept)(这个问题之前在bigo二面时有遇到过,然而并没有答对,面试官很耐心的跟我说了connect是属于客户端的,然而当时忘了,这几个函数一定要烂熟于心,当时还问的问题有epoll\select\poll,这些应该算是基础了吧。回想自己的大一大二真的是浪得一批啊,春招的确是目前让人进步最快的时候了。华为有问到分布式如何取数据相关的问题,其实答案很简单就是哈希,数据量、数据范围,元数据服务器这几个,还有问到linux的了解,过几天还要恶补linux,还有c的库函数)
只有C在客户端
ECHO服务器端:
int sock=socket(PF_INET,SOCK_STREAM);
bind(sock,addr);
listen(sock);
int new_sock=accept(sock,&addr);
size_t size=read(new_sock,buf,100);
if(size==0){close(new_sock);}
else{write(new_sock,buf,size);}

ECHO客户端
int sock=socket(PF_INET,SOCK_STREAM);
connet(sock,addr);
write(sock,"ping");
char buf[100];
read(sock,buf,100); 


select版本其中会生成一种数组,里面用来存储sock和new_sock,然后使用select命令去轮询
select由于是单个进程有fd数量的限制,需要维护一个用来存放大量fd的数据结构(这样会使得用户空间和内核空间在传递该结构时复制开销大),对socket进行扫描时是线性扫描
poll本质上和select没有区别,将用户传入的数组拷贝到内核空间,然后查询每个fd对应的状态,如果设备准备就绪则在设备等待队列中加入一项并继续遍历,如果没有就绪设备则挂起当前进程,直到设备就绪或主动超时,被唤醒后继续遍历。由于是基于链表没有最大连接数的限制。但缺点还是大量的fd的数组被整体复制于用户态和内核地址空间中。并且是水平触发,报告了没有处理之后下次继续处理
epoll边缘触发,只告诉进程哪些fd刚刚变成就绪态,并且只会通知一次,使用mmap内存映射减少复制开销,通过epoll_ctl注册fd,一旦fd就绪,内核就会采用类似callback的回调机制来激活fd,epoll_wait可以收到通知。

mmap、红黑树、链表
epoll是通过内核与用户空间mmap同一块内存实现的,对内核和用户均可见,减少用户态和内核态之间的数据交换,内核可以直接看到epoll监听的句柄,效率高。
红黑树用于存储epoll所监听的套接字(通过epoll_ctl函数添加进来),红黑树处理增删的复杂度为log(n)
添加进来后需要与相应的设备驱动程序建立回调关系,回调函数为ep_poll_callback,该回调函数将事件添加到rdllist这个双向链表中,一旦有事件发生epoll就将事件添加到双向链表中。调用epoll_wait时只需要检查rdllist中是否有存在注册的事件。效率很可观。
epoll_wait调用ep_poll,当rdllist为空时挂起当前线程。
状态改变后ep_poll_callback()被调用,将相应fd对应的epitem加入rdllist,挂起进程被唤醒,epoll_wait继续执行。
ep_event_transfer函数将rdllist中的epitem拷贝到txlist,并清空rdllist。
ep_send_events函数扫描txlist中的每个epitem,调用其关联fd对应的poll方法。此时对poll的调用只是取得fd上较新的events,之后封装到epoll_event中从epoll_wait返回到用户空间。

RPC远程调用:
如何做到透明化远程调用:对于Java有代理和字节码生成
如何对消息进行编解码:
1.确定消息数据结构
客户端(接口名称,方法名,参数类型&参数值,超时时间,requestID)
服务器端(返回值,状态code,requestID)
2.序列化
通信:BIO/NIO
NIO、mina、netty(如阿里的HSF、dubbo、twitter的finagle)
为什么要有requestID:
使用netty的话一般是channel.writeAndFlush()来发送消息二进制串,对于线程来说是异步的
client每次通过socket调用一次远程接口前会生成一个唯一的ID也就是requestID,这个ID在一个socket连接中是唯一的。处理的结果callback放入全局ConcurrentHashMap里面put(requestID,callback);
get的时候使用synchroninzed获取callback上的锁,检测是否已有结果,没有调用wait等待结果。
服务器处理后产生了reponse后发送给客户端,客户端socket连接上坚挺的县城收到消息分析结果将方法调用返回到callback对象里使用callback.notifyAll()唤醒前面等待的对象即可。
如何发布自己的服务:使用zooKeeper自动发布(自动注册与发现功能)
是一个服务注册表,让多个服务提供者形成一个集群,让服务消费者通过服务注册表去获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。

Unix/Linux使用的是Reactor(epoll)epoll适用于连接多,活动连接少。
Windows使用的是Proactor(IOCP)
Reactor和Proactor是高性能并发服务器经常用到的两个设计模式
Reactor用于同步I/O,在I/O就绪的情况下通知用户,用户再采取实际的IO操作
Proactor是在I/O操作完成后通知用户。
epoll实现的是Reactor模式,
然而asio使用epoll机制实现了Proactor模式,asio内部有某个循环调用epoll_wait,当有I/O事件就绪时帮用户做一些操作,然后在操作完成时调用handler。
Reactor(适用于耗时短的情况)属于被动分离和分发模型,一定要等待到指示事件的到来才能做出反应,有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时才进行操作。
Proactor(适用于耗时长的情况)属于主动分离,允许多任务并发的执行,提高吞吐量

游戏数据一般存放在内存中,而不是RDBMS,RDBMS单单判断与法是否正确就需要几百个CPU周期
一帧内只有通过CPU的原始指令组合来完成
游戏数据需要放在CPU所在的机器上

通信延迟
带宽
服务器:
作弊行为
1.内存破解(存储的游戏过程数据)
2.数据包破解(收发的数据包)
3.数据文件破解(游戏程序所读取的文件内容)
4.DDL破解(启动时读取的动态链接二进制文件)
5.时钟破解
6.UI工具破解
7.服务器攻击(非法侵入服务器,偷看、篡改服务器端数据)
Dos拒绝服务攻击命令(使服务器超负荷不稳定)
对加密文件进行解密后从内存中取出
同步方式:能够简单的存储数据,各个终端的游戏数据一致
异步方式:不用保持一致

同步方式下收发的是玩家输入的信息
浏览器方式下只向服务器发送玩家的操作信息,服务器只向浏览器发送游戏过程中的结果
王者荣耀属于帧同步
同步异步共享游戏过程的所有终端都共享游戏过程的所有主数据
浏览器管理游戏数据的只有服务器,各个终端只是将当前的游戏情况可视化展现给玩家。
在MMO中,游戏逻辑全在服务器上实现,客户端并不包含用于是游戏发展下去的程序,而只包含渲染、音效等。
通常客户端是windows,服务器端是linux。
限制远程shell登录,防止SQL注入,缓存溢出程序的非法运行,可防作弊

3D渲染、物理计算、游戏逻辑,误差度为通信处理

物理结构:P2P、C/S
逻辑结构:MO、MMO(浏览器方式)
常用:C\S MMO、P2P MO


服务器端知识

消息校验是为了防止外挂添加的检查:
1.长度校验
2.消息标识检查
3.校验码校验
4.消息编号校验
struct消息头
需要记录长度、消息头、校验码、消息编号、一级二级指令等。

服务器的逻辑网关:
1.网关接收连接线程:循环检查连接并建立新会话,select模型
2.网关的数据发送线程:遍历发送缓冲区中的数据并进行发送
3.网关的数据接收线程:检查会话的合法性把合法的socket注册到读文件描述符集,然后检查描述符的状态。网络接收数据到数据包接受队列
4.网关的数据处理线程:处理连包,校验,派送到账号线程(登录消息)或逻辑线程(系统消息)的消息处理队列

把数据从网关的接收数据消息队列拷贝到会话的接受处理缓冲区

游戏服务器的核心是逻辑服务器,也就是对用户的输入数据进行处理,产出输出数据返回给用户。
服务器需要一个稳定的硬盘存储服务替代逻辑服务器后面的服务器硬盘。
由于MySQL的速度不够快,又加入了内存数据库作为快速读取的缓存服务器。
为了解决逻辑服务器不稳定丢失数据的问题,引入了缓存服务器和数据存储服务器。

逻辑服务器的崩溃主要原因是内存指针的泄露,产生了各种脚本语言替代指针性语言。但随着功能的逐渐复杂,内存泄露难以避免。
one object do something
可重入函数:所有函数都应该只处理本函数内的数据,而不要去操作共享数据,也不要通过地址去操作其他函数内部的数据
任何系统都是不可靠的,因此需要在数据逻辑上做到互相检查互相校验,实现操作数据逻辑的可重入可检查。
我们需要保证任务是可重入的,也即任务的相关数据是记录式的。如果任何情况导致任务中断,这个交易也会记录在册,显示交易失败,发起方可以重新手动进行交易直到成功。

Key:任务号,value:顺序id
Key:顺序id,value:加减值
Key:顺序id,value:加减之后的总值
任务号是客户端生成的,顺序id是服务器生成的由小到大的顺序号,方便查询最后的任务。
可重入的意思是可以拿客户端的任务号重新检查当前任务是否已经产生顺序id,是否已经写入正确的加减值,是否已经产生正确的总值,如果没有就重新产生记录。只要客户端的任务号不变,同一个任务就不会产生多次重复操作,保证数据的一致性与完整性。

消息队列:一个队列在一个消费组中只能被一个消费者占有并消费,其他没分配队列的消费者轮空。在消费者拉取消息和确认回调这段锁定时间有一个超时机制,超过这个时间队列会自动解锁,被别的消费者消费。

处理消息耗时的瓶颈主要是业务处理总时间和拉取消息总时间,通过调整进程数可减少业务总时间,使得总耗时接近拉取消息总时间,达到最佳,QPS也就达到峰值,需要根据业务处理时间确定进程池进程数。
进程池进程数=处理时间*单次拉取数/单次拉取时间。
因为线程切换很占用CPU资源,最好是线程数=CPU核数*2+2;

RPC:划重点!!!!

RPC通信是基于netty的
一些非CPU密集型的工作瓶颈一般都在后端数据库,本地CPU计算的时间很少,设置几十到几百个线程都是有可能的。
加解密、压缩解压缩、搜索排序等是CPU密集的业务
量化分析线程数需要了解工作线程的工作模式,典型的工作线程处理过程有:
1.从工作队列中拿出任务,进行一些本地化初始计算,例如http协议分析、参数解析、参数校验等。
2.访问cache拿一些数据。
3.拿到cache里的数据后再进行一些本地计算,计算与业务逻辑相关。
4.通过RPC调用下游service再拿一些数据,或者让下游service去处理一些相关的任务
5.RPC调用结束后,再进行一些本地计算,与业务逻辑相关。
6.访问DB进行一些数据操作
7.操作完数据库后进行一些收尾工作,这些收尾工作也是本地计算,业务逻辑相关。
因此1、3、5、7是占用cpu,2、4、6是不占用cpu,此时可以把2、4、6所用时间分配给其他线程。

同步调用特点:Result=Add(Obj1,Obj2);
异步调用:Add(Obj1,Obj2,callback);
callback(Result){//得到处理结果后会调用这个回调函数}
同步回调:
1.业务代码发起RPC调用,Result=Add(Obj1,Obj2)
2.序列化组件,将对象调用序列化成二进制字节流,可理解为一个待发送的包packet1
3.通过连接池组件拿到一个可用的连接connection
4.通过连接connection将包packet1发送给RPC-Server
5.发送包在网络传输,发送给RPC-Server
6.相应包在网络传输,发回给RPC-client
7.通过连接connection从RPC-server收取响应包packet2
8.通过连接池组件,将connection放回连接池
9.序列化组件,将packet2反序列化为Result对象返回给调用方
10.业务代码获取Result结果,工作线程继续往下走
RPC框架需要支持负载均衡、故障转移、发送超时,这些特性都是通过连接池组件去实现的。
经典的连接池的对外接口:
int ConnectionPool::init(...);//与下游RPC-Server建立N个TCP长连接,也就是连接池
Connection ConnectionPool::getConnection();//从连接池中拿一个连接,加锁返回给调用方
int ConnectionPool::putConnection(Connection t);//将一个分配出去的连接放回连接池中,解锁
连接池返回连接时需要实现负载均衡
连接池发现机器异常后需要先将机器排除掉,恢复后再加回,也就是故障转移
同步阻塞调用在拿到连接后使用带潮湿的send/recv即可实现发送超时
异步则是多了一个上下文管理器,将请求,回调,上下文存储起来
序列化后放入下游收发队列(待发送队列),收发线程将报文从收发队列取出,通过连接池组件拿到可用的连接connection
回来后下游收发线程将报文放入(已接受队列),connection放回连接池
下游收发队列中保温杯取出,开始回调。反序列化为Result对象。由上下文管理器将结果,回调,上下文取出,回调业务代码返回Result结果。
上下文管理器请求长时间不返回有一个超时管理器,拿到超时的上下文,通过timeout_cb回调业务代码。
异步:nio管理时延。
同步适合时延敏感,异步适合吞吐量敏感。

netty框架学习
管道:父子进程,单向
有名管道:允许非亲缘的进程间通信
消息队列:消息的链表,存放在内核中并由消息队列标识符标识
信号量是一种进程间的同步手段,常用做锁机制
信号用于通知进某个事件已经发生
共享内存通常与信号量配合使用,最快的IPC
套接字:不同机器间

IPC的四种技术:
消息传递(管道,FIFO,posix和system v消息队列)
同步(互斥锁,条件变量,读写锁,文件和记录锁,posix和system v信号灯)
共享内存区
过程调用(solaries门,RPC)

解决数据库瓶颈:平行世界方式
解决服务器瓶颈:空间分割法、实例法

负载均衡数据链路层的解决方案,改MAC;还有重定向;DNS做load balance;IP层修改等等,目前LVS使用的是链路层方式。

 

猜你喜欢

转载自blog.csdn.net/parallel2333/article/details/81152705
今日推荐