C#格式化时间,货币

1、格式化货币(跟系统的环境有关,中文系统默认格式化人民币,英文系统格式化美元)

string.Format(“{0:C}”,0.2) 结果为:¥0.20 (英文操作系统结果:$0.20)

默认格式化小数点后面保留两位小数,如果需要保留一位或者更多,可以指定位数
string.Format(“{0:C1}”,23.15) 结果为:¥23.2 (截取会自动四舍五入)

格式化多个Object实例
string.Format(“市场价:{0:C},优惠价{1:C}”,23.15,19.82)

2、格式化十进制的数字(格式化成固定的位数,位数不能少于未格式化前,只支持整形)

string.Format(“{0:D3}”,23) 结果为:023

string.Format(“{0:D2}”,1223) 结果为:1223,(精度说明符指示结果字符串中所需的最少数字个数。)

3、用分号隔开的数字,并指定小数点后的位数

string.Format(“{0:N}”, 14200) 结果为:14,200.00 (默认为小数点后面两位)

string.Format(“{0:N3}”, 14200.2458) 结果为:14,200.246 (自动四舍五入)

4、格式化百分比

string.Format(“{0:P}”, 0.24583) 结果为:24.58% (默认保留百分的两位小数)

string.Format(“{0:P1}”, 0.24583) 结果为:24.6% (自动四舍五入)

5、零占位符和数字占位符

string.Format(“{0:0000.00}”, 12394.039) 结果为:12394.04

string.Format(“{0:0000.00}”, 194.039) 结果为:0194.04

string.Format(“{0:###.##}”, 12394.039) 结果为:12394.04

string.Format(“{0:####.#}”, 194.039) 结果为:194

下面的这段说明比较难理解,多测试一下实际的应用就可以明白了。
零占位符:
如果格式化的值在格式字符串中出现“0”的位置有一个数字,则此数字被复制到结果字符串中。小数点前最左边的“0”的位置和小数点后最右边的“0”的位置确定总在结果字符串中出现的数字范围。
“00”说明符使得值被舍入到小数点前最近的数字,其中零位总被舍去。

数字占位符:
如果格式化的值在格式字符串中出现“#”的位置有一个数字,则此数字被复制到结果字符串中。否则,结果字符串中的此位置不存储任何值。
请注意,如果“0”不是有效数字,此说明符永不显示“0”字符,即使“0”是字符串中唯一的数字。如果“0”是所显示的数字中的有效数字,则显示“0”字符。
“##”格式字符串使得值被舍入到小数点前最近的数字,其中零总被舍去。

6、日期格式化

string.Format(“{0:d}”,System.DateTime.Now) 结果为:2009-3-20 (月份位置不是03)

string.Format(“{0:D}”,System.DateTime.Now) 结果为:2009年3月20日

string.Format(“{0:f}”,System.DateTime.Now) 结果为:2009年3月20日 15:37

string.Format(“{0:F}”,System.DateTime.Now) 结果为:2009年3月20日 15:37:52

string.Format(“{0:g}”,System.DateTime.Now) 结果为:2009-3-20 15:38

string.Format(“{0:G}”,System.DateTime.Now) 结果为:2009-3-20 15:39:27

string.Format(“{0:m}”,System.DateTime.Now) 结果为:3月20日

string.Format(“{0:t}”,System.DateTime.Now) 结果为:15:41

string.Format(“{0:T}”,System.DateTime.Now) 结果为:15:41:50

更详细的说明请下面微软对此的说明或者上msdn上查询。

微软MSDN对string.format的方法说明:

名称 说明
String.Format (String, Object) 将指定的 String 中的格式项替换为指定的 Object 实例的值的文本等效项。
String.Format (String, Object[]) 将指定 String 中的格式项替换为指定数组中相应 Object 实例的值的文本等效项。
String.Format (IFormatProvider, String, Object[]) 将指定 String 中的格式项替换为指定数组中相应 Object 实例的值的文本等效项。指定的参数提供区域性特定的格式设置信息。
String.Format (String, Object, Object) 将指定的 String 中的格式项替换为两个指定的 Object 实例的值的文本等效项。
String.Format (String, Object, Object, Object) 将指定的 String 中的格式项替换为三个指定的 Object 实例的值的文本等效项。

标准数字格式字符串

格式说明符 名称 说明
C 或 c
货币
数字转换为表示货币金额的字符串。转换由当前 NumberFormatInfo 对象的货币格式信息控制。

精度说明符指示所需的小数位数。如果省略精度说明符,则使用当前 NumberFormatInfo 对象给定的默认货币精度。

D 或 d
十进制数
只有整型才支持此格式。数字转换为十进制数字 (0-9) 的字符串,如果数字为负,则前面加负号。

精度说明符指示结果字符串中所需的最少数字个数。如果需要的话,则用零填充该数字的左侧,以产生精度说明符给定的数字个数。

E 或 e
科学记数法(指数)
数字转换为“-d.ddd…E+ddd”或“-d.ddd…e+ddd”形式的字符串,其中每个“d”表示一个数字 (0-9)。如果该数字为负,则该字符串以减号开头。小数点前总有一个数字。

精度说明符指示小数点后所需的位数。如果省略精度说明符,则使用默认值,即小数点后六位数字。

格式说明符的大小写指示在指数前加前缀“E”还是“e”。指数总是由正号或负号以及最少三位数字组成。如果需要,用零填充指数以满足最少三位数字的要求。

F 或 f
定点
数字转换为“-ddd.ddd…”形式的字符串,其中每个“d”表示一个数字 (0-9)。如果该数字为负,则该字符串以减号开头。

精度说明符指示所需的小数位数。如果忽略精度说明符,则使用当前 NumberFormatInfo 对象给定的默认数值精度。

G 或 g
常规
根据数字类型以及是否存在精度说明符,数字会转换为定点或科学记数法的最紧凑形式。如果精度说明符被省略或为零,则数字的类型决定默认精度,如下表所示。

Byte 或 SByte:3

Int16 或 UInt16:5

Int32 或 UInt32:10

Int64 或 UInt64:19

Single:7

Double:15

Decimal:29

如果用科学记数法表示数字时指数大于 -5 而且小于精度说明符,则使用定点表示法;否则使用科学记数法。如果要求有小数点,并且忽略尾部零,则结果包含小数点。如果精度说明符存在,并且结果的有效数字位数超过指定精度,则通过舍入删除多余的尾部数字。

上述规则有一个例外:如果数字是 Decimal 而且省略精度说明符时。在这种情况下总使用定点表示法并保留尾部零。

使用科学记数法时,如果格式说明符是“G”,结果的指数带前缀“E”;如果格式说明符是“g”,结果的指数带前缀“e”。

N 或 n
数字
数字转换为“-d,ddd,ddd.ddd…”形式的字符串,其中“-”表示负数符号(如果需要),“d”表示数字 (0-9),“,”表示数字组之间的千位分隔符,“.”表示小数点符号。实际的负数模式、数字组大小、千位分隔符以及十进制分隔符由当前 NumberFormatInfo 对象指定。

精度说明符指示所需的小数位数。如果忽略精度说明符,则使用当前 NumberFormatInfo 对象给定的默认数值精度。

P 或 p
百分比
数字转换为由 NumberFormatInfo.PercentNegativePattern 或 NumberFormatInfo.PercentPositivePattern 属性定义的、表示百分比的字符串,前者用于数字为负的情况,后者用于数字为正的情况。已转换的数字乘以 100 以表示为百分比。

精度说明符指示所需的小数位数。如果忽略精度说明符,则使用当前 NumberFormatInfo 对象给定的默认数值精度。

R 或 r
往返过程
只有 Single 和 Double 类型支持此格式。往返过程说明符保证转换为字符串的数值再次被分析为相同的数值。使用此说明符格式化数值时,首先使用常规格式对其进行测试:Double 使用 15 位精度,Single 使用 7 位精度。如果此值被成功地分析回相同的数值,则使用常规格式说明符对其进行格式化。但是,如果此值未被成功地分析为相同数值,则它这样格式化:Double 使用 17 位精度,Single 使用 9 位精度。

虽然此处可以存在精度说明符,但它将被忽略。使用此说明符时,往返过程优先于精度。

X 或 x
十六进制数
只有整型才支持此格式。数字转换为十六进制数字的字符串。格式说明符的大小写指示对大于 9 的十六进制数字使用大写字符还是小写字符。例如,使用“X”产生“ABCDEF”,使用“x”产生“abcdef”。

精度说明符指示结果字符串中所需的最

Math.Round(3.45, 2, MidpointRounding.AwayFromZero);

Git:Access denied Authentication failed

电脑系统重装后,今天上传项目版本记录,发现被拒绝访问:“ HTTP Basic: Access denied fatal: Authentication failed for。。”
搜了下网上给的解决方案:“git config –system –unset credential.helper”。但在我git上运行此命令会报错:
error: could not lock config file C:/Program Files/Git/mingw64/etc/gitconfig: Permission denied
下搜这个报错,找到了解决方案:

原来登录凭据已被Windows记录,每次更新SSH密钥,除了要在git托管平台上更新公钥外还得在本地找到这里清除历史,再运行上述命令后,重新输用户名密码登录远程库。

SecureCRT通过密钥登录[转]

说明:
一般的密码方式登录容易被密码暴力破解。所以一般我们会将 SSH 的端口设置为默认22以外的端口,或者禁用root账户登录。其实可以通过密钥登录这种方式来更好地保证安全。

密钥形式登录的原理是:利用密钥生成器制作一对密钥——一只公钥和一只私钥。将公钥添加到服务器的某个账户上,然后在客户端利用私钥即可完成认证并登录。这样一来,没有私钥,任何人都无法通过 SSH 暴力破解你的密码来远程登录到系统。此外,如果将公钥复制到其他账户甚至主机,利用私钥也可以登录。

下面来讲解如何在 Linux 服务器上制作密钥对,将公钥添加给账户,设置 SSH,最后通过客户端登录。

实现:
1. 制作密钥对
首先在服务器上制作密钥对。首先用密码登录到你打算使用密钥登录的账户,然后执行以下命令:

[root@host ~]$ ssh-keygen  <== 建立密钥对
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): <== 按 Enter
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase): <== 输入密钥锁码,或直接按 Enter 留空
Enter same passphrase again: <== 再输入一遍密钥锁码
Your identification has been saved in /root/.ssh/id_rsa. <== 私钥
Your public key has been saved in /root/.ssh/id_rsa.pub. <== 公钥
The key fingerprint is:
0f:d3:e7:1a:1c:bd:5c:03:f1:19:f1:22:df:9b:cc:08 root@host

密钥锁码在使用私钥时必须输入,这样就可以保护私钥不被盗用。当然,也可以留空,实现无密码登录。

现在,在 root 用户的家目录中生成了一个 .ssh 的隐藏目录,内含两个密钥文件。id_rsa 为私钥,id_rsa.pub 为公钥。

2. 在服务器上安装公钥
键入以下命令,在服务器上安装公钥:

[root@host ~]$ cd .ssh
[root@host .ssh]$ cat id_rsa.pub >> authorized_keys
如此便完成了公钥的安装。为了确保连接成功,请保证以下文件权限正确:
[root@host .ssh]$ chmod 600 authorized_keys
[root@host .ssh]$ chmod 700 ~/.ssh

3. 设置 SSH,打开密钥登录功能
编辑 /etc/ssh/sshd_config 文件,进行如下设置:

RSAAuthentication yes
PubkeyAuthentication yes

另外,请留意 root 用户能否通过 SSH 登录,默认为yes:
PermitRootLogin yes

当我们完成全部设置并以密钥方式登录成功后,可以禁用密码登录。这里我们先不禁用,先允许密码登陆
PasswordAuthentication yes

最后,重启 SSH 服务:
[root@host .ssh]$ service sshd restart

4. 将私钥下载到客户端,如这里的SecureCRT
输入Hostname,Username,选择认证方式为PublicKey公钥认证,选择刚刚下载下来的id_rsa私钥文件即可!

配置SSH隧道访问Ubuntu服务器上的MongoDB[转]

为了数据安全,在MongoDB的配置文件里,一般会把默认的27017端口port改为自定义的端口号,然后把允许访问的IP设为127.0.0.1(即主机本身)。但是这样就会在开发的过程查看数据时带来麻烦,必须去服务器端或调用部署在服务器的API接口才能很好地可视化地查询数据。为了能在客户端也能使用可视化工具,可以建立一条SSH隧道,直接在客户端访问远程服务器的数据库。

本文例子基于以下开发环境:

服务器端:Ubuntu 16.04 x86_64,MongoDB 3.4.6

客户端:Windows10 x86_64,Robomongo 1.0.0-RC1,PuTTY或XShell

=========================================================================

有两种思路可以建立SSH隧道以实现客户端Robomongo访问服务器MongoDB,一是使用PuTTY或XShell连接服务器时建立隧道,将服务器的MongoDB监听端口映射到客户端的指定端口,这样在客户端使用Robomongo连接本地的指定端口即可访问到服务器的MongoDB端口,缺点是连接数据库前需要保持PuTTY或者XShell处于与数据库的连接状态;二是使用Robomongo自带的建立SSH隧道方式,只需要在Robomongo新建连接,输入建立SSH会话所需的用户名密码或者密钥,相当于使用Robomongo登录到服务器,然后在服务器访问本地的MongoDB端口即可,优势在于不需要单独建立SSH连接(没错这种方法才是推荐使用的,但是第一种就可以不局限于Robomongo的连接)。

=========================================================================

注意:因为SSH隧道是基于TCP Forward建立的,因此,在sshd配置文件里一定要允许TCPForwarding,我开始配置sshd的时候将这个禁用了,所以爬了好久才爬出这个坑。

$ sudo vim /etc/ssh/sshd_config

修改如下字段:

AllowTCPForwarding yes

 

重启sshd

$ sudo systemctl restart sshd

 

 

另外,要把MongoDB的配置文件进行修改,使其仅能通过本地访问:

$ sudo vim /etc/mongod.conf

修改如下字段:

net:

port: 27017

bindIP: 127.0.0.1

 

=========================================================================

一、使用PuTTY或XShell建立客户端与服务器的SSH隧道

PuTTY: 设置好登录信息(包括服务器IP端口用户密码密钥等)后,在左侧栏找到Connection/SSH/Tunnels,Add new forwarded port下,Source Port 填写需要映射到客户端的端口号,Destination填写服务器的主机和端口号(服务器的主机当然是localhost了)下面选Local和Auto即可,然后点击Add,保存以便下次登录方便,点击Open即可建立SSH隧道连接。

然后,打开Robomongo,新建连接到localhost:27018即可连接到服务器的MongoDB了。

或者说,可以通过监听本地的27018端口即可监听到服务器的27017端口。

类似地,使用XShell时,填写好登录主机端口用户名密码密钥等信息后,在左侧栏找到SSH/隧道,添加TCP/IP转移规则类型为Local, 源主机即使客户端的主机及侦听端口,目标则是服务器的主机及端口,确定后连接,即可建立SSH隧道。

=========================================================================

二、使用Robomongo自带的SSH隧道

打开Robomongo,在连接设置里的SSH选项卡中启用SSH tunnel并设置登录SSH服务器的信息,回到Connection选项卡,填入主机(localhost)与服务器上MongoDB对应的端口。保存连接即可。

原文地址:https://www.cnblogs.com/gyjerry/p/7291098.html

经典游戏服务器端架构概述 (二)(转)

全服分线模型
一. 模型描述
由于多进程服务器模型的发展,游戏开发者们首先发现,由于游戏业务的特点,那些需要持久化的数据,一般都是玩家的存档,以及一些游戏本身需要用的,在运行期只读的数据。这对于存储进程的分布,提供了非常有利的条件。于是玩家数据可以存放于同一个集群中,可以不再和游戏服务器绑定在一起,因为登录的时候便可根据玩家的ID去存储集群中定位想要存取的存储进程。

[图-全区分线模型]
二. 存储的挑战
需求:扩容和容灾在全区分线模型下,游戏玩家可以随便选择任何一个服务器登录,自己的帐号数据都可以提取出来玩。这种显然比每个服务器重新“练”一个号要省事的多。而且这样也可以和朋友们约定去一个负载较低的服务器一起玩,而不用苦苦等待某一个特定的服务器变得空闲。然而,这些好处所需要付出的代价,是在存储层的分布式设计。这种设计有一个最需要解决的问题,就是游戏服务器系统的扩容和容灾。
从模型上说,扩容是加入新的服务器,容灾是减掉失效的服务器。这两个操作在无状态的服务器进程上操作,都只是更新一下连接配置表,然后重启一下即可。但是,由于游戏存在大量的状态,包括运行时内存中的状态,以及持久化的存储状态,这就让扩容和容灾需要更多的处理才能成功。
最普通的情况下,在扩容和容灾的时候,首先需要通知所有玩家下线,把内存中的状态数据写入持久化数据进程;然后根据需要的配置,把持久化数据重新“搬迁”到新的变化后的服务器上。——如果一个游戏有几千万用户,这样的数据搬迁将会耗时非常长,玩家也被迫等待很长的时间才能重新登录游戏。所以在这种模型下,对于数据存储的设计是最关键的地方。

分区分服的关系型数据库我们常常会使用MySQL这种关系型数据库来存放游戏数据。由于SQL能够表述非常复杂的数据操作,这对于游戏数据的一些后期处理有非常好的支持:如客服需要发奖励,需要撤销某些错误的运营数据,需要封停某些特征的玩家……但是,分布式数据库也是最难做分布的。一般来说我们都需要通过某一主键字段做分库和分表;而另外一些如唯一关键字等数据,就需要一些技巧来处理。

[图-分表分库]
以玩家ID作为分表分库是一个非常自然的选择,但是这种方案,往往需要在逻辑代码中,对玩家数据按照自定义的规则,做存储进程的选择。但是如果发现这个分表分库的算法(原则)不符合需求,就需要把大量的数据做搬迁。如上图是按玩家ID做奇偶规则分布到两个表中,一旦需要增加第三台服务器,数据存储的目的服务器编号就变成了id%3,这样就需要把好多数据需要从原来的第一、二台数据库中拷贝出来,非常麻烦。
有的开发者会预先建立几十个表(如120个表=2x3x4x5),一开始是全部都放在一个服务器上,然后在增加数据库服务器的时候,把对应的整个表搬迁出来。这样能减轻在搬迁数据的时候造成的复杂度,但还是需要搬迁数据的。最后如果与建立的表还是放不下了,依然还是需要很复杂和耗时的重新拷贝数据。NoSQL在很多开发者绞尽脑汁折腾mysql的时候,NoSQL横空出世了。实际上在很早,目录型存储进程就在DNS等特定领域默默工作了。NoSQL系统最大的好处正是关系型数据库最大的弱点——分布。
由于主键只有一个,因此内置的分布功能使用起来非常简便。而且游戏玩家数据,绝大多数的操作都是根据主键来读写的。“自古以来”游戏就有“SL大法”之称,其本质就是对存档数据的简单读、写。在网游的早期版本MUD游戏时代,玩家存档只是简单的放在硬盘的文件上,文件名就是玩家的ID。这些,都说明了游戏中的玩家数据,其读写都是有明显约束的——玩家ID。这和NoSQL简直是天作之合。

[图-NoSQL]
NoSQL的确是非常适合用来存储游戏数据。特别是有些服务器如Redis还带有丰富的字段值类型。但是,NoSQL本身往往不带很复杂的容灾热备机制,这是需要额外注意的。而且NoSQL的访问延迟虽然比关系型数据库快很多,但是毕竟要经过一层网络。这对于那些发展了很多年的ORM库来说,缺乏了一个本地缓存的功能。这就导致了NoSQL还不能简单的取代掉所有服务器上的“状态”。而这些正是分布式缓存所希望达成的目标。分布式缓存在业界用的比较多的缓存系统有memcached,开发者有时候也会使用诸如hibernate这样的ROM库提供的cache功能。但是这些缓存系统在使用上往往会有一些限制,最主要的限制是“无法分布式使用”,也就是说缓存系统本身成为性能瓶颈后,就没有办法扩容了。或者在容灾的情景下,缓存系统往往容易变成致命的单点。
Orcale公司有一款叫Coherence的产品,就是一种能很好解决以上问题的“能分布式使用”的产品。他利用局域网的组播功能来做节点间的状态同步,同时采用节点互相备份的方案来分布数据。这款产品还使用Map接口来提供功能。这让整个缓存系统既使用简单又功能强大。更重要的是,它能让用户对于数据的存取特性做配置,从而提供用户可接受的数据风险下的更高性能——本地缓存。
由于游戏的数据,真正变化频繁的,往往不是“关键”的需要安全保障数据,如玩家的位置、玩家在某次战斗中的HP、子弹怪物的位置等等。而那些非常重要的数据,如等级、装备,又变化的不频繁。这就给了开发者针对数据特性做优化以很大的空间。而且,大部分数据的读、写频率都有典型的不平衡状态。普遍游戏数据都是读多写少。少量的日志、上报数据是写多、几乎不读。
对于缓存系统来说,有三个重要的因数决定了在游戏开发中的地位。首先是其使用的便利性,因为游戏的数据结构变化非常频繁,如果要很繁琐的配置数据结构,则不会适合游戏开发;其次是要能提供近似本地内存的性能,由于游戏服务器逻辑基本上都是在频繁的读写某一特定数据块,如玩家位置、经验、HP等等,而且游戏对于处理延迟也有较高的需求(WEB应用在2秒以内都可以忍受,游戏则要求最好能在20ms以内完成)。要能同时满足这两点,是不太容易的。

[图-分布式缓存]集成缓存的NoSQL根据上面的描述,读者应该也会想到,如果数据库系统,或者叫持久化系统,自带了缓存,是否更好呢?这样确实是会更好的,而且特别是对于NOSQL系统来说,能以一些内部的算法策略,来降低前端逻辑开发的复杂程度。一般来说,我们需要对集成缓存的NOSQL系统有以下几方面的需求:首先是冷热数据自动交换,就是对于常用数据有算法来判别其冷热,然后换入到内存以提高存取性;其次是分布式扩容和容灾功能,由于NOSQL是可以知道数据的主关键字的,所以自然就可以自动的去划分数据所在的分段,从而可以自动化的寻找到目标存储位置来做操作;最后是数据导出功能,由于NOSQL支持的查询索引只能是主键,对于很多后台游戏操作来说是不够的,所以一定要能够到处到传统的SQL服务器上去。
在这方面,有很多产品都做过一定的尝试,比如在redis或者MangoDB上做插件修改,或者以ORM系统封装MySQL以试图构造这种系统等等。

[图-集成缓存的NOSQL]
三. 跳线和开房间
开房间型游戏模型在全区分线服务器模型中,最早出现在开房间类型的游戏中。因为海量玩家需要临时聚合到一个个小的在线服务单元上互动。比如一起下棋、打牌等。这类游戏玩法和MMORPG有很大的不同,在于其在线广播单元的不确定性和广播数量很小。
这一类游戏最重要的是其“游戏大厅”的承载量,每个“游戏房间”受逻辑所限,需要维持和广播的玩家数据是有限的,但是“游戏大厅”需要维持相当高的在线用户数,所以一般来说,这种游戏还是需要做“分服”的。典型的游戏就是《英雄联盟》《穿越火线》这一类游戏了。而“游戏大厅”里面最有挑战性的任务,就是“自动匹配”玩家进入一个“游戏房间”,这需要对所有在线玩家做搜索和过滤。

[图-开房间型游戏]
这类游戏服务器,玩家先登录“大厅服务器”,然后选择组队游戏的功能,服务器会通知参与的所有游戏客户端,新开一条连接到房间服务器上,这样所有参与的用户就能在房间服务器里进行游戏交互了。
由于“大厅服务器”只负责“组队”,所以其承载力会比具体的房间服务器更高一些,但这里仍然会是性能瓶颈。所以一般我们需要尽量减少大厅服务器的功能,比如把登录功能单独列出来、把玩家的购买物品商城功能也单独出来等等。最后,我们也可以直接想办法把“组队”功能也按组队逻辑做一定划分,比如不同的组队玩法、副本类型、组队用户等级等等。
虽然这种模型已经可以对很多游戏做很好的承载了,但是在大厅服务器这里依然无法做到平行扩展,原因是玩家的在线数据比较难分布到不同的服务进程上去,而且还带有大量复杂的数据查询逻辑。

专用聊天服务器不管是MMORPG还是开房间类游戏,聊天一直都是网络游戏中一个重要的功能。而这个功能在“在线人数”很多,“聊天频道”很多的情况下,会给性能带来非常大的挑战。在很多类型的页游和少部分手机游戏里面,在线聊天甚至是唯一的“带公共状态”的服务。
聊天服务处理点对点的聊天,还有群聊。用户可能会添加好友、建立好友群组等各种功能。这些功能,都是和一般的游戏逻辑有一定差别的功能。这些功能往往并不是非常容易实现。很多游戏都期望建立类似腾讯QQ的游戏聊天功能,但是QQ是一整个公司在做开发,要用仅仅一个游戏团队做成这么完整的功能,是有一定困难的。
因此游戏开发者们常常会专门的针对聊天功能来开发一系列的服务进程,以便能让游戏的聊天功能独立出来,做到负载分流和代码重用的逻辑。很多网游系统,其聊天系统从客户端来说就是和主游戏进程分开的。
聊天服务器的本质是对客户端数据做广播,从而让玩家可以交互,所以有很多游戏开发者也直接拿聊天服务器来做棋牌游戏的房间服务器,或者反过来用。由于在游戏“分服”里面单独部署了聊天服务器,这类服务器也往往被用来承担做“跨服玩法”的进程。比如跨服团队战、跨服副本等等。不管这些服务器最终叫什么名字,实际上他们承担的主要功能还是广播,而且是运行玩家“二次登录”的广播服务器。以至于后来,有部分游戏直接全部都用聊天服务器来代替原始的“游戏服务器”,这样还能实现一个叫“跳线”的功能,也就是玩家从一个“在线环境”跳到另外一个“在线环境”去。——这些都是对于“广播”功能的灵活运用。

 

[图-专用聊天服务器]
全服全线模型
尽管分服的游戏模型已经运营了很多年,但是有一些游戏运营商还是希望能让尽量多的玩家一起玩。因为网游的人气越活跃,产生的交互越多,游戏的乐趣也可能越多。这一点最突出表现在棋牌类网游上。如联众、QQ游戏这类产品,无不是希望更多玩家能同时在线接入一个“大”服务器,从而找到可以一起玩的伙伴。在手游时代,由于手机本身在线时间不稳定,所以想要和朋友一起玩本来就比较困难,如果再以“服务器”划分区域,交互的乐趣就更少了,所以同样也呼唤这一个“大”服务器,能容纳下所有此款游戏的玩家。因此,开发者们在以前积累的分服模型和分线模型基础上,开发出满足海量在线互动需求的一系列游戏服务器模型——全服全线模型。

[图-全服全线模型]
一. 服务进程的组织
静态配置全服全线模型的本质是一个各种不同功能进程组成的分布式系统,因此这些进程间的关系是在运维部署期间必须关注的信息。最简单的处理方法,就是预先规划出具体的进程数量、以及进程部署的物理位置,然后通过一套配置文件来描述这个规划的内容。对于每个进程,需要配置列明每个进程的pid文件位置;内部通讯用的地址,如IP+端口或者消息队列ID;启动和停止脚本路径;日志路径等等……由于有了一套这样的配置文件,我们还可以编写工具对所有的这些进程进行监控和操作批量启停。

[图-静态配置]
虽然我们可以以静态配置为基础做很丰富的管理工具,但是这种做法还是有可以改进的空间:每次扩容、更换故障服务器或者搬迁服务器(这在运营中很常见),我们都必须手工修改静态配置数据,由于是人工操作,就总会产生很多错误,根据个人经验,游戏运营事故中的70%以上,是跟运维操作有关;由于整个分布式系统被切分成大量的进程,对于新进入此项目的程序员来说,要完整的理解这个系统,需要在思想上跨越层层阻隔:每个进程的功能、它们部署的关联、每个进程间的协议报的含义、每个业务流程具体的跨进程过程……这要花费很多时间才能搞明白的。而且大部分游戏的这种架构并不统一,每个游戏都可能需要重新理解一次,知识无法重用;在开发测试上,由于分布式系统的复杂性,要多搭几个开发、测试环境也是很费时间的,以至于这项工作甚至要安排专人来负责,这对于小型游戏开发团队来说几乎是不可承担的成本。因此我们还需要一些更加自动化,更加容易理解的全服全线游戏服务器模型。 基于中心点的动态组织SOA架构模式是业界一个比较经典的分布式软件架构模式,这个架构的特点是能动态的组织一个非常复杂的分布式服务系统。这个系统可以包含提供各种各样供的服务程序,而这些服务程序都以同一个标准接口来使用,并且服务自己会注册自己到集群中,以便请求方能找到自己。这种架构使用Web Serivce来作为服务接口标准,通过发布WSDL来提供接口API,这极大的降低了开发者对这些服务的使用成本。在游戏领域,服务器端提供的功能程序,实际上也是非常多样的,如果要构建一个分布式的系统,在这个方面是非常适合SOA架构的思想的;然而,游戏却很少使用HTTP协议及其之上的Web Service做通讯层,因为这个协议性能太低。不过,类似SOA的,基于中心节点的动态组织的服务管理思路,却依然适用。

[图-基于中心点的动态组织]
一般来说我们会使用一组目录服务器来充当“中心点”,代表整个集群。开源产品中最好的产品就是ZooKeeper了。当然也有一些开发者自己编写这样的目录服务器。由于每个服务进程会自己上报负载和状态,所以每个进程只需要配置自己提供的服务即可:服务名字、服务接口。对于请求方来说,一般都可以预先编写目标服务接口的类库,用来编程,有些项目还使用RPC功能,使用IDL语言配置直接生成这些接口类库。当需要请求的时候,执行“名字查找”-“路由选择”-“发起请求”就可以完成整个过程。由于有“查找”-“路由”的过程,所以如果目标服务故障、或者新增了服务提供者,请求方就能自动获得这些信息,从而达到自动动态扩容或容灾的效果,这些都是无需专门去做配置的。

服务化与云尽管动态组织的架构有如此多优点,但是开发者还是需要自己部署和维护中心节点。对于一些常用的服务,如网络代理服务、数据存储服务,用户还是要自己去安装,以及想办法接入到这套体系中去。这对于开发、测试还是有一定的运维工作压力的。于是一些开发团队就把这类工作集中起来,预先部署一套大的集群中心系统,所有开发者都直接使用,而不是自己去安装部署,这就成为了服务化,或者云服务。

[图-服务化、云]
使用专人维护的服务化集群确实是一个轻松愉快的过程。但是游戏开发和运营过程中,往往需要多套环境,如各个不同版本的测试环境、给不同运营平台搭建的环境、海外运营的环境等等……这些环境会大大增加维护服务化集群的工作量,对于解决这个问题,建立高度自动化运维的私有云,成为一个需要解决的问题放上了桌面。提高集群的运维效率,降低工作复杂程度,需要一些特别的技术,而虚拟化技术正式解决这些问题的最新突破。

二. 提高开发效率所用的结构
使用RPC提高网络接口编写效率在分布式系统中,如果所有的接口都需要自己定义数据协议报来做交互,这个网络编程的工作量将会非常的大,因为对于一个普通的通信接口来说,至少包括了:一个请求包结构、一个响应包结构、四段代码,包括请求响应包的编码和解码、一个接收数据做分发的代码分支、一个发送回应的调用。由于分布式的游戏服务器进程非常多,一个类似登录这样的操作,可能需要历经三、四个进程的合作处理,这就导致了接近十个数据结构的定义和无数段类似的代码。而这些代码,如果在单进程的环境下,仅仅只是三、四个函数定义而已。
因此很多开发者投入很大精力,让网络通信的编写过程,尽量简化成类似函数的编写一样。这就是前文所述的远程调用的方法。在全区全线的游戏中,如果是比较重度的游戏,采用RPC方式做开发,会大大降低开发的复杂程度。当然也有一些比较轻度的游戏,还是采用传统的协议包编解码、分发逻辑调用的做法。

简化数据处理在分布式系统中,对于避免单点、容灾、扩容中最复杂的问题,就是在内存中的数据。由于内存中有游戏业务的数据,所以一般我们不敢随便停止进程,也难以把一个进程的服务替换为另外一个进程。然而,游戏数据对比其他业务,还是非常有特点的:
写入越不频繁的数据,价值越高。比如过关、升级、获得重要装备。
大量数据都是读非常频繁,而写非常不频繁的,如玩家的等级、经验。
大量写入频繁的数据,实际上是不太重要,可以有一定损失,比如玩家位置,在某个关卡内的HP/MP等……
因此,只要我们能按数据的特性,对游戏中需要处理的数据做一定分类,就能很好的解决分布式中的这些问题。
首先我们要对数据的分布做规划,一般来说采用按玩家ID做分布,这样能让服务进程中内存的数据缓存高度命中。常用的手法有用一致性哈希来选择路由,调用相关的服务进程。
其次对于读频繁而写不频繁的数据,我们采用读缓存而写不缓存的策略。每个服务进程都保留其读缓存数据,如果需要扩容和容灾,仅仅需要修改服务访问的路由即可。
再次对于读不频繁而写频繁的数据,我们采用写缓存和读不缓存的策略。由于这些数据丢失掉一些是不要紧的,所以容灾处理就直接忽略即可,对于扩容,只需要对所有服务进程都做一次回写即可。
最后,有一些数据是读和写都频繁的数据,比如玩家位置,HP/MP这类,我们采用读写都缓存,由于数据重要性不高,只要我们多分几个服务进程即可降低故障时影响的范围;在扩容的时候调用全节点清理读缓存和回写脏数据即可。

在和持久化设备打交道的时候,传统的ORM类库往往能帮我们把数据存入关系型数据库,然而,使用一个自带数据热备的NOSQL也是很好的选择。因为这样能节省大量的分库分表逻辑代码。

自动化部署集群环境最新的虚拟化技术给分布式系统提供能更好的部署手段,以Docker为标志的虚拟化平台,可以很好的提高服务化集群的管理。我们可以把每个服务进程打包成一个映像文件,放入docker虚拟机中运行,也可以把一组互相关联的服务进程打包运行。这些环境问题都由Docker处理了。但是,我们同时需要注意的是,如果我们的进程的资源是静态分配的(前文提到),在Docker的虚拟机中可能因为内存不足等原因直接无法启动。这就需要我们把完全静态分配资源的程序,修改为有资源限制,但是动态分配的程序。这样我们才能在任何可以部署Docker的机器上部署我们的游戏服务器。

三. 分布式难点:状态同步
分布式接入层一般来说,我们全线服务器系统碰到的第一个问题,就是大量并发的网络请求。特别是大量玩家都在一起交互,产生了大量由于状态同步而需要广播的数据包。这些网络请求的处理,显然应该独立出来成为单独的进程。同时这些网络接入进程,还应该是一个集群中的成员。这就诞生了分布式接入服务层。
这些网路接入进程的第一个功能,就是把并发的连接,代理成为后端一个串行的连接,这可以让后端服务进程的处理逻辑更简单,而且网络处理消耗变得更小。
其次,网络接入进程需要支持广播功能。如果只是普通的广播实现,很多人会需要拷贝很多次需要广播的内容,然后挨个对Socket做发送。这其实是一个消耗很高的操作。而单独的网络接入进程,可以善用“零拷贝”等技术,大大降低广播的性能开销。而且还可以通过多个进程一起做广播操作,以达到更大的在线同步区域。

最后,网络接入进程需要支持一些额外的有用功能,包括通讯的加密、压缩、流量控制、过载保护等等。有些团队还把用户的登录鉴权也加入网络接入功能中。

[图-分布式接入层]
使用P2P网络状态同步产生的广播请求中,绝大多数都是客户端之间的网络状态,因此我们在可以使用P2P的客户端之间,直接建立P2P的UDP数据连接,会比通过服务器转发降低非常多的负载。在一些如赛车、音乐、武打类型的著名游戏中,都有使用P2P技术。而接入进程天然的就是一个P2P撮合服务器。
有些游戏为了进一步降低延迟,还对所有的玩家状态,只同步输入动作,以及死亡、技能等重要状态,让怪物和一般状态通过计算获得,这样就更能节省玩家的带宽,提高及时性。加上一些动作预测技术,在客户端上能表现的非常流畅。

展望
一. 可重用的游戏业务模版
游戏服务端的各种架构中,以前往往比较关注那些非功能性的需求:容灾性、扩容、承载量,延迟。而在现在手游时代,开发效率越来越重要,有些团队甚至不设专门的服务器端程序员。因此游戏服务端架构应该更多的关注业务开发的效率。
现代游戏中,只要是带RPG元素的,角色系统、物品系统、技能系统、任务系统就都会具备,而且都有一批比较稳定的核心逻辑。只要是能在线交互的,就有好友系统、邮件系统、聊天系统、公会系统等。另外商城系统、活动系统、公告系统更是每个游戏都似乎要重复发明的轮子。
游戏的后端应用也有很多可重用的部分,比如客服系统、数据统计平台、官网数据接口等等。这些在游戏服务端框架中往往是最后再添加进去的。
如果把以上的问题都统一考虑起来,我们实际上是可以在一个稳定的底层架构上,构造出一整套常用的游戏业务逻辑模板,用来减少游戏领域的业务代码开发。所以这样一套可以运行各种业务逻辑模版的底层架构,正是游戏服务端架构发展的方向。
二. 动态资源调度的PaaS云
现在有的团队已经在搭建自己的Docker云,这可以让游戏服务器在虚拟云上动态的生长,从而达到真正的动态扩容和动态容灾。加上如果游戏服务器不再是一个个服务进程,而是真正意义上的一个个服务,可以动态的加入或者离开云环境,那么这就是一个游戏领域的PaaS系统。我热切的希望能看到,可以用一套SDK,开发或重用那些成型的业务模版,然后动态注册到服务云中就能运行,这样一种游戏服务器架构。

作者:JumboWu
链接:https://www.jianshu.com/p/4d769767c1ac