This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
<audio id="audio" title="第22讲 | 如何选择合适的开发语言?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e1/f6/e1216f72adbe0e7fa57338dc3cfb0cf6.mp3"></audio>
有许多编程语言可以用来开发服务器端。一些语言对于网络开发有先天优势,一些语言没有先天优势,但是能完成任务,而有一些语言,则不太适合。今天,我就来具体讲一讲这些语言来开发网络服务的优劣势。
## 你了解这些编程语言吗?
**C/C++** 是最标准的开发语言速度快并发性能高能最大程度利用机器资源完成任务。现在C/C++层面拥有无数网络开发库和SDK知名的有ACE、Boost/Asio、ICE等等。但是缺点是开发效率不比别的语言来得高出错后常常只有熟练的程序员才能定位问题并且作出修复。
**Go**是2009年新出现的语言。Go天生适合编写网络程序。它也是一种系统级的语言可以直接编译为执行文件当然由于封装了大量的语法糖所以编译出来的文件会更大它天生支持并发操作所以很多时候你不需要像C/C++一样手工去处理。缺点是Go语言仍然存在许多语法方面的坑你可以去 [https://studygolang.com/](https://studygolang.com/) 学习最新的资料。
**Java**是公认的编写网络服务的第一大语言。在运行性能和开发效率上有很好的折中效果。Java拥有众多的中间件和开发库调试方便一般的运维人员也有极为广泛可用的第三方维护工具可以使用。缺点是Java的运行效率虽然有了质的飞跃但因为中间隔了一层虚拟机所以仍然比不上系统开发语言编写的软件。另外Java的发布和部署需要众多的依赖包和库软件体积庞大也是其重要弊病。
如果深入理解,**Python、Ruby**这两种语言的相似程度以及对系统的支持程度可以用C和C++的相似程度来相比。你或许会很疑惑毕竟Python和Ruby的语法几乎不一样Python需要格式化源代码而Ruby并不需要Python更严谨Ruby更开放Python用户更多Ruby用户更少。
不可否认的是两种语言编写网络程序都非常方便也非常高效。两种语言都可以在100行内编写出一个简单的、完全可以直接使用的网络服务器程序。但是这两种语言的弊病也很明显那就是速度不够快。
比之Java或许运行效率更慢一点但由于目前机器硬件水平的提升软件效率不足的缺点一部分已经被硬件所弥补但是仍然不能否认Python、Ruby语言适合IO密集型开发而不适合计算密集型的开发。
Python的书籍比Ruby多好几倍然而你如果仔细去看的话就会发现Ruby的书籍质量明显比Python高几个等级所以如果要看好的脚本语言的书籍Ruby相关的书籍是首选我这里推荐一本[Programming in Ruby](https://book.douban.com/subject/2032343/),有兴趣的话可以找来看看。
**Node.js**从前端语言变成后端语言让编程界眼前为之一亮。随后的发展大家也有目共睹Node.js由于使用JavaScript语言语法所以我们一般采用事件驱动的形式以及非阻塞的模型来进行网络开发。因为这些特点它非常适合做分布式的、数据密集型的工作。但是缺点也很明显Node.js是单线程无法很好地使用多核CPU这个问题在Python、Ruby语言中也很明显。
或许你没有听说过**Erlang**这种语言这种语言最初是由爱立信开发的。它的初衷是让程序员应对大规模并发、分布式、软件实时并行系统进行编程。最早期的版本在80年代就出现了但是一直到1998年才开源。
Erlang也不是系统语言它有自己的虚拟机和解释器。当然和Java一样Erlang也支持将代码编译为Native Code来运行。Erlang的缺点就是类型问题它并非强类型语言。由于是事件编程所以导致会在运行时无法匹配类型而出错不过这些问题可以使用规范的编程方法来规避。
这么多种编程语言,整合起来看,大致可以把他们分为三类。
**系统级编程语言**诸如汇编、C、C++。这种编程语言执行效率快,并发量也比较高,作为编写网络服务的第一语言,一台服务器就能支撑许多人。缺点是开发效率不够高,需要几年以上经验的程序员才能搞定。
**专门为网络服务器开发的语言**诸如Go、Erlang。这种语言编写高并发和开发效率都不是问题有很好的折中效果。缺点就是语言比较新有许多的坑等着后来的程序员去填而且语言、语法等系统机制要随着进一步的发展才能稳定下来。
**解释型脚本语言**诸如Python、Ruby。 这类语言的开发效率非常高效,在现在的服务器硬件上,也能支撑不少用户,但是唯一的缺点是,运行效率低下。虽然也有解决方案,但仍然不能对抗高性能的系统编程语言和专业网络开发的语言。
## 如何选择一种合适的语言来编写网络服务?
### Web服务
现在有一种流行的说法叫前后端分离。对于编写C/S结构的程序员听到这种说法应该会比较蒙客户端和服务器端难道不是本来就分离的吗
很长的一段时间里在Web的世界中前后端都是混合在一起编写的比如PHP的方式只有用到Ajax交互的时候才需要用到后端的代码。但是前后端一分离后台就需要做更多的工作了当然前端的工作也不会变少。
编写Web服务需要HTTP和HTTPS的服务体系那么在这种情况下使用nginx、Apache作为静态页面路由Java、Tomcat、Python、Ruby等脚本语言就有了用武之地。因为页面只需要使用JSON交互即可。
所以编写Web服务我们可以选择Java、Python、Ruby。但是如果公司财力物力有限再考虑到招人成本的问题次选也可以是Java语言第一是写Java的人够多第二是Java成熟的类库够多因此一旦出问题有解决经验的人也比较多。
### Socket服务
传统TCP/IP和UDP服务或者最近的WebSocket等都需要快速响应和并发操作在这种情况下系统级编程语言和网络编程语言就可以派上用场了。
如果公司的项目需要更快更高效并且财力也允许那么选择C、C++、Go、Erlang等编程语言未尝不是一种选择。当然Java也能很好地提供服务但是从业务上来讲既然选择了Socket服务模式那么就必然是对并发量有一定的要求所以选择上述这些语言更合适。
### 混合模式
这类业务既有HTTP/HTTPS的服务也有Socket服务那么如何平衡两者之间的语言成本如何平衡程序员之间技术栈的问题呢
如果要做一款短期内必须上线的产品我建议选择成熟的、有大量解决方案的开发人员不短缺的语言比如Java或者能快速做出原型的语言比如服务器专有语言Go。如果是长期发展的产品并不那么着急成型那么选择稳定成熟的人员素质高的语言比如Python、Java等。
至于平衡技术栈的问题首先要选择网上有众多解决方案的语言其次是找成熟的语言比如Python、Java、Ruby。如果针对某种特殊的产品比如并发要求特别高的那么只有选择系统语言或者专门的语言比如Go、C++等。
看到这里你是不是觉得Java语言是一种万能药或者是银弹错了这个世界上没有银弹。Java虽然有其独特的优势但是其被人诟病的地方也是有不少的。
第一点莫过于速度。就算拥有JIT编译总体速度仍然比不上C/C++,但是事实上这些因素综合考虑并不算特别大的弊病,因为硬件资源提升后,速度这些问题已经可以“得过且过”了。
那么从语言本身来看如果说C/C++语言本身的弊病是因为系统平台导致的那么Java语言的弊病就是因为继承自C++,却没有做更彻底的改革而导致的。
我随便举一个例子比如说switch case判断语句硬生生地从C/C++处直接继承了下来因为C/C++只允许使用int、enum其实是int、char提升为int作为判断类型而Java也是直接将这套规范继承了下来。
再比如在Java里面异常检查也是一个痛苦的根源程序员不得不写下无数try catch语句以使得将捕获的异常转变为运行时的异常然后再将之抛出去这样一来使用Java编写的API将缺少灵活和扩展性。
那如果选择了Python或者Ruby等脚本语言进行开发却需要大量高并发的操作该怎么办呢我们可以选择多进程不是多线程编程的方式进行开发代码尽量简洁、高效一个进程兼顾一个任务进程之间的通信方式要尽量高效、简洁比如可以使用自定义的队列等方式。
## 小结
学完这一节,你应该对使用各种编程语言来编写网络服务有了一个更深的了解。我主要讲了以下几个内容。
<li>
编程语言可以大致分为三类,系统级编程语言、专为网络服务器开发的编程语言和解释型脚本语言。
</li>
<li>
在编写网络服务的时候可以根据要编写的是Web服务、Socket服务还是混合模式来选择合适的编程语言。
</li>
给你留一个小问题吧。
如果让你来使用C/C++粘合Lua脚本来编写网络服务器你会怎么设计这个程序框架
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="第23讲 | 如何定制合适的开发协议?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/58/4d7926809b5247420e66ede7f381cf58.mp3"></audio>
什么是开发协议说得简单一点就是一种客户端和服务器端的网络沟通协议Protocol
广义上说协议是计算机各种设备之间沟通的桥梁。比如网络之间需要协议ping一个网站是否通顺也需要协议广播地址也需要协议。我们甚至可以说键盘鼠标操作事件也需要协议Dubbo架构也需要协议沟通等等。
从狭义上说协议指的就是网络协议。比如在网络游戏中客户端和服务器端之间的内容交互就需要网络协议在Web网站中前端和后端的交互也需要协议再比如邮件服务的网络交互也需要协议的交互等等。可以说任何与网络相关的服务都少不了协议的支撑。
在游戏开发中,我们可以自定义一套自己的开发协议,也可以把现成的开发协议拿来使用。具体怎么做呢?我们先来看现在网上用得比较多的几种协议。
## 三种最流行的开发协议
### XML
XML几乎是网络上最早出现的传输协议之一。在最早的Web开发中XML可以作为网络协议也可以用作配置文件。比如某些游戏或者某些应用的配置文件都可以使用XML来编写。
从人类的角度讲,它的可读性比较强,解析也比较方便。我们先来看几种解析方式。
解析方式是这些协议被程序理解的一种方式,按照这种方式解析,和我后面要说的自定义协议的解析和剖析结合起来,乃前后呼应之奇效。
<li>
PULL方式PULL解析是一种专门为安卓设备解析XML文件设计的解析方式。这种解析方式更适用于移动设备。PULL解析和我们下面要说的SAX解析比较类似不一样的地方是PULL读取XML文件后触发相应的事件调用方法返回的是number。另外PULL方式可以控制程序的解析停止位置不需要全部解析可以随时停止。
</li>
<li>
<p>SAX方式SAXSimple API for XML采用事件驱动型方式。语法解析从发现元素开始、元素结束、文本、文档的开始或结束等就开始事件的发送程序员编写响应这些事件的代码就可以直接将数据保存下来。所以优点是不需要载入整个文档资源占用比较少。<br>
SAX解析器的代码比DOM解析器的代码更精简一般应用于Applet技术。缺点就是这种解析方式并不持久等事件消息过去后如果你没有保存数据那么数据就丢失了从事件的消息中我们只能得到文本数据但不知道这个文本属于哪个元素。但是我们只需XML文档的少量内容就可以解析出我们所需的数据所以耗费的内存更少。</p>
</li>
<li>
DOM方式DOMDocument Object Model是最传统的解析方式。解析器读入整个文档然后在内容中构建一个XML的树结构使用代码就可以操作这个树结构。优点是整个文档树在内存中便于操作而且支持删除、修改、重排等多种功能。缺点是将整个文档调入内存比较浪费计算机的时间和空间但是如果一旦解析了文档还需多次访问这些数据的话这种方式就可以起到作用了。
</li>
### JSON
其实目前XML已经不太流行取而代之的是JSON。JSON是一种轻量级的数据交换格式。它用完全独立于编程语言的文本格式来存储和表示数据。
比之XML它看起来更加简洁和清晰层次结构分明JSON易于阅读和编写在程序方面也易于机器解析和生成同时也提升了网络传输效率。这些优点使得JSON很快在程序员中流行起来成为理想的数据交换语言。
它也是移动端比较常见的网络传输协议。相对于前面所说的XML格式它更为简单体积更小加之对网络流量和内存的需求更小所以JSON比XML更适合移动端的使用。
我们来看一下JSON的几种流行的解析程序库。
<li>
Gson是谷歌开源的一种解析方法使用Java编写你可以通过提供的JAR文件包使用静态方法直接将JSON字符串解析成Java对象这样使用起来简单方便。
</li>
<li>
FastJSON是阿里开源的一个解析JSON数据的类库。
</li>
<li>
JSONObject也是一个解析JSON字符串的Java类。第二、第三这两种用的人都比较少我就不多介绍了。
</li>
当然支持别的语言的库也有很多由于JSON比较流行所以各种语言都有其支持的类库版本比如Python、C++、Ruby等等。
### ProtoBuf
ProtoBuf全称Google Protocol Buffer 是谷歌公司开发的内部混合语言数据标准。目前正在使用的有接近五万种报文格式定义和超过一万两千多个.proto文件。它们都用于RPC系统和持续数据存储的系统。
这是一种轻便、高效的结构化数据存储格式可以用于结构化数据的序列化操作。它很适合用作数据存储或RPC数据交换格式。可以用于通讯协议、数据存储等领域。由于是独立的协议系统所以它和开发语言、运行平台都没有关系可以用在扩展的序列化结构数据格式。目前提供了 C++、Java、Python 、Ruby、Go等语言的开发接口API。
ProtoBuf方便的地方在于它有一款编译器可以将.proto后缀的协议文件编译成C++、Java、Python等语言的源代码。你可以直接看到和利用这些源代码且不需要自己去做解析所以不同语言之间使用ProtoBuf的协议规范都是一样的但是有一个问题是ProtoBuf存储的文件格式是二进制的由于是二进制的所以程序员需要调试其保存的内容就有点麻烦当然这可能只是对于某些人来说的瑕疵吧对于大部分人来讲方便性还是大于瑕疵的。
ProtoBuf的编码风格是这样的花括号的使用类似C/C++、Java。数据类型的命名方式使用驼峰命名比如DataType、NewObject。字段的变量小写并使用下划线连接类似GNU规范比如proto_buf、user_name。枚举类型使用大写并使用下划线连接比如MY_HOMEBEST_FRIEND。
Protobuf并不是针对大型数据设计的Protobuf对于1M以下的message有很高的效率但是当message大于1M的时候Protobuf的效率会逐步降低。
## 如何自己定义协议包?
我们讲完了三种目前最流行的开发协议,接下来我们要讲讲如何自己定义协议包。
我们所说的**协议包**,是**在TCP和UDP传输之上的协议包也就是通过字符串的形式发送的协议包**。这些协议包在客户端和服务器之间做了约定,也就是说,客户端和服务器都能通过拿到协议包来进行解包操作,并且进行一系列的逻辑运算并返回结果,当然结果也是协议包的形式发送出去。
一个好的协议不仅能节约网络带宽,也能让接收端快速拿到和解析需要的内容。设计协议包,必须保证**安全性**和**完整性**。
为了保证完整性,接收方需要知道协议的长度,或者知道协议的尾部在哪里。
我们可以给协议最末尾添加分隔符,该分隔符需要特殊字符。不能被传输的内容所混淆,又要能达到方便接收方辨认,因此,该特殊字符需要具有唯一性。比如我们可以将“!@#$”这四个字符做为分隔符,那么协议看起来可能是这样:
```
[协议头][协议体][协议结尾分隔符]
```
你可能要问了,在传输的过程中,我知道了协议长度,不需要协议头,只需要协议长度就可以?是的。因为有了协议长度,协议尾部有没有分隔符就不重要了。如果我们固定好输出协议长度的字节数,就可以忽略协议头。在这种情况下,协议看起来像是这样:
```
[协议长度2字节][协议体]
```
这样简单地就能定义整个协议的内容。
在读取的时候我们只需要读取开头的两个字节转换为一个short的长度或者四个字节一个int的长度在第三个字节开始就是协议体。让程序开始计算长度如果长度少于协议长度所定义的长度那就继续接收如果接收长度超过协议所定义的长度切割协议体并将下一段开始的协议存储到内存中留待下一次取出。这种方式是最方便的。
我们在保证协议完整性的同时也要保证协议不被破坏和篡改也就是所说的安全性。在这种情况下最直接的方式就可以将协议内容进行加密。比如SHA-256或者AES等等加密方式将内容加密随后传输过去最简单的做法就是将密码在客户端和服务器端协商好就可以了。
看起来可能是这个样子:
```
[协议长度2字节][加密协议体]
```
## 小结
这节内容差不多了,我们总结一下。我和你介绍了这几个内容。
<li>
我介绍了三种的开发协议XML、JSON和ProtoBuf以及它们对应的解析方式。XML是网络上最早出现的传输协议之一。
</li>
<li>
游戏或应用的配置文件都可以使用XML来编写但是目前XML已经不太流行取而代之的是JSON。
</li>
<li>
ProtoBuf适合用作数据存储或RPC数据交换格式缺点是保存比较麻烦但是总体来讲还是比较方便的。
</li>
<li>
自己定义协议包需要考虑完整性和安全性。接收方需要知道协议的长度,或者知道协议的尾部在哪里,就可以保证协议包的完整性。而最直接的给协议包加密,就可以保证安全性。
</li>
最后,给你留一个小问题吧。
在自定义协议中,如果使用添加协议结尾的方式来做协议,如何才能保证协议结尾分割字符串不和协议本身的二进制内容重复?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,143 @@
<audio id="audio" title="第24讲 | 不可忽视的多线程及并发问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/db/f0ed6005d2e4957c32774f5f3d393edb.mp3"></audio>
既然我们说到了服务器端的开发我们就不得不提起多线程和并发的问题因为如果没有多线程和并发是不可能做网络服务器端的除非你的项目是base在Nginx或者Apache之上的。
## 多线程和并发究竟有什么区别和联系?
提到并发,不得不提到并行,所以我就讲这三个概念:并发、并行,以及多线程。作为初学者,你或许不太明白,多线程和并发究竟有什么区别和联系?下面我们就分别来看看。
**并发**出现在电脑只有一个CPU的情况下那如果有多个线程需要操作该怎么办呢CPU不可能一次只运行一个程序运行完一个再运行第二个这个效率任谁都忍受不了啊所以就想了个办法。
CPU将运行的线程分成若干个CPU时间片来运行。不运行的那个线程就挂起运行的时候那个线程就活过来切换地特别快就好像是在同时运行一样。
你可以想象这个场景有一个象棋大师一个人对十个对手下棋那十个人轮流和他下。大师从1号棋手这里开始下下完1号走到2号的棋手面前下2号棋手的棋一直轮流走下去直到再走回1号棋手这里再下一步。只要象棋大师下象棋下得足够快然后他移动到下一位棋手这里又移动得足够快大家都会觉得好像有十位象棋大师在和十个对手下棋。事实上只有一位象棋大师在下棋只是他移动得很快而已。
**并行**和并发不同并行是出现在多个物理CPU的情况下。在这种情况下并行是真正的并发状态是在物理状态下的并发运行。所以并行是真的有几位象棋大师在应对几个对手。当然在并行的同时CPU也会进行并发运算。
而**多线程**是单个进程的切片,单个进程中的线程中的内存和资源都是共享的,所以线程之间进行沟通是很方便的。
多线程的意义,就好比一个厨师,他掌管了三个锅,一个锅在煮排骨,一个锅在烧鱼,另一个锅在煮面,这三个锅内容不同,火候不同,但是所有的调料和资源,包括菜、油、水、盐、味精、糖、酱油等等,都来自同一个地方(也就是资源共享),而厨师自己是一个进程,他分配了三个线程(也就是三个锅),这三个锅烧着不同的东西,三个食物或许不是同时出锅的,但是厨师心里有数,什么时候这个菜可以出锅,什么时候这个菜还需要煮。这就是多线程的一个比喻。
我们在编写网络服务器的时候多线程和并发的问题是一定会考虑的。我们说的网络并发和CPU的并发可以说是异曲同工也就是说**网络并发的意义是,这个网络服务器可以同时支撑多少个用户同时登陆,或者同时在线操作**。
## 为什么Python用多个CPU的时候会出现问题
那么我们又回头来看为什么Python、Ruby或者Node.js在利用多个CPU的时候会出现问题呢这是因为它们是使用C/C++语言编写的。是的,问题就在这里。
我们后续的内容还是会用Python来写所以我们先来看看Python的多线程问题。Python有个**GIL**Global Interpreter Lock全局解释锁问题就出在GIL上。
使用C语言编写的Python版本后面简写为C-Python的线程是操作系统的原生线程。在Linux上为pthread在Windows上为Win thread完全由操作系统调度线程的执行。
一个Python解释器进程内有一条主线程以及多条用户程序的执行线程。即使在多核CPU平台上。由于GIL的存在所以会禁止多线程的并行执行。这是为什么呢?
因为Python解释器进程内的多线程是合作多任务方式执行的。当一个线程遇到I/O输入输出任务时将释放GIL锁。计算密集型以计算为主的逻辑代码的线程在执行大约100次解释器的计步时将释放GIL锁。你可以将计步看作是Python虚拟机的指令。计步实际上与CPU的时间片长度无关。我们可以通过Python的库sys.setcheckinterval()设置计步长度来控制GIL的释放事件。
在单核的CPU上数百次间隔检查才会导致一次线程切换。在多核CPU上就做不到这些了。从Python 3.2开始就使用新的GIL锁了。在新的GIL实现中用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁且其他线程请求这个锁的时候当前线程就会在五毫秒后被强制释放掉这个锁。
我们如果要实现并行利用Python的多线程效果不好所以我们可以创建独立的进程来实现并行化。Python 2.6以上版本引进了multiprocessing这个多进程包。
我们也可以把多线程的关键部分用C/C++写成Python扩展通过ctypes使Python程序直接调用C语言编译的动态库的导出函数来使用。
C-Python的GIL的问题存在于C-Python的编写语言原生语言C语言中由于GIL为了保证Python解释器的顺利运行所以事实上多线程只是模拟了切换线程而已。这么做的话如果你使用的是IO密集型任务的时候就会提高速度。为什么这么说
因为写文件读文件的时间完全可以将GIL锁给释放出来而如果是计算密集型的任务或许将会得到比单线程更慢的速度。为什么呢事实上GIL是一个全局的排他锁它并不能很好地利用CPU的多核相反地它会将多线程模拟成单线程进行上下文切换的形式进行运行。
我们来看一下,在计算密集型的代码中,单线程和多线程的比较。
单线程版本:
```
from threading import Thread
import time
def my_counter():
i = 0
for x in range(10000):
i = i + 1
return True
def run():
thread_array = {}
start_time = time.time()
for tt in range(2):
t = Thread(target=my_counter)
t.start()
t.join()
end_time = time.time()
print(&quot;count time: {}&quot;.format(end_time - start_time))
if __name__ == '__main__':
run()
```
多线程版本:
```
from threading import Thread
import time
def my_counter():
i = 0
for x in range(10000):
i = i + 1
return True
def run():
thread_array = {}
start_time = time.time()
for tt in range(2):
t = Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
end_time = time.time()
print(&quot;count time: {}&quot;.format(end_time - start_time))
if __name__ == '__main__':
run()
```
当然我们还可以把这个ranger的数字改得更大看到更大的差异。
当计步完成后将会达到一个释放锁的阀值释放完后立刻又取得锁然而这在单CPU环境下毫无问题但是多CPU的时候第二块CPU正要被唤醒线程的时候第一块CPU的主线程又直接取得了主线程锁这时候就出现了第二块CPU不停地被唤醒第一块CPU拿到了主线程锁继续执行内容第二块继续等待锁唤醒、等待唤醒、等待。这样事实上只有一块CPU在执行指令浪费了其他CPU的时间。这就是问题所在。
这也就是C语言开发的Python语言的问题。当然如果是使用Java写成的PythonJython和.NET下的PythonIron Python并没有GIL的问题。事实上它们其实连GIL锁都不存在。我们也可以使用新的Python实作项目PyPy。所以这些问题事实上是由于实现语言的差异造成的。
## 如何尽可能利用多线程和并发的优势?
我们来尝试另一种解决思路我们仍然用的是C-Python但是我们要尽可能使之能利用多线程和并发的优势这该怎么做呢
multiprocess是在Python 2.6以上版本的提供是为了弥补GIL的效率问题而出现的不同的是它使用了多进程而不是多线程。每个进程有自己的独立的GIL锁因此也不会出现进程之间CPU进行GIL锁的争抢问题因为都是独立的进程。
当然multiprocessing也有不少问题。首先它会增加程序实现时线程间数据通信和同步的困难。
就拿计数器来举例子。如果我们要多个线程累加同一个变量对于thread来说申明一个global变量用thread.Lock的context就可以了。而multiprocessing由于进程之间无法看到对方的数据只能通过在主线程申明一个Queueput再get或者用共享内存、共享文件、管道等等方法。
我们可以来看一下multiprocess的共享内容数据的方案。
```
from multiprocessing import Process, Queue
def f(q):
q.put([4031, 1024, 'my data'])
if __name__ == '__main__':
q = Queue()
p = Process(target=f, args=(q,))
p.start()
print q.get()
p.join()
```
这样的方案虽说可行,但是编码效率变得比较低下,但是也是一种权宜之计吧。
## 小结
我们来总结一下今天的内容。
<li>
我首先介绍了几个概念。并发是单个CPU之间切换多线程任务的操作。并行是多个CPU同时分配和运行多线程任务的操作。线程是进程内的独立任务单元但是共享这个进程的所有资源。网络的并发指的是服务器同时可以承载多少数量的人数和任务。
</li>
<li>
而C语言编写的Python有GIL锁的问题会让其多线程计算密集型的任务效率更低解决方案有利用多进程解决问题 或者 更换Python语言的实现版本比如PyPy或者JPython等等。
</li>
给你留一个小问题如果Python以多进程方式进行操作那么如果我们网络服务器是用Python编写的其中一个Python进程崩溃或者报错了有什么办法可以让其复活
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="第25讲 | 如何判断心跳包是否离线?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bf/97/bf7f27a06e2bd28ef543a7c5b74cbb97.mp3"></audio>
在初学网络,编写过阻塞和非阻塞网络代码的时候,有一个问题,那就是在非阻塞的情况下,不知道对方的网络何时断开。
因为在非阻塞的情况下如果没有接收到消息recv的数值一直会是0。如果以这个来判断显然是错误的。而在阻塞情况下只要对方一断开接收到0就说明断开了那么我们怎么才能在非阻塞的情况下确定连接是断开还是没断开呢
我们可以采用离线超时的方案来判断对方连接是否断开。那什么是离线超时呢?
我们都知道,人累了就要休息。你在休息的时候,有没有注意过这么一个现象,那就是你在快要睡着的时候,忽然脚会蹬一下,或者人会抽一下,这是为什么呢?
有一种说法流传很广,说,其实大脑是在不停地检测人有没有“死”,所以发送神经信号给手和腿。抽动一下,检验其是否死亡。这个就有点儿像我们检测超时,看看有没有反应。
现在我们先看一段Python代码让它运行起来。
```
import socket
import time
def server_run():
clients = []
my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
my_server.bind((&quot;&quot;, 1024))
my_server.listen(256)
my_server.setblocking(False)
```
这是我节选的一部分代码。其中在函数server_run里面我们先定义了一个clients这是一个列表用于后面保存客户端连接用。my_server获得socket句柄并且将之设置为TCP模式随后我们绑定地址为本地bind函数端口号为1024并且开始侦听随后我们看到 setblocking函数将之设置为非阻塞模式。
```
while True:
time.sleep(1)
try:
client, addr = my_server.accept()
print client
client.setblocking(False)
clients.append(client)
except Exception as e:
print &quot;no client incoming&quot;
for cli in clients:
try:
data = cli.recv(1024)
if data:
print data
else:
cli.close()
clients.remove(cli )
except Exception as e:
print &quot;no data from &quot;, cli
my_server.close()
```
在一个大循环内我们做了如下几件事情第一个是accept只要有客户端进来我们就accept如果没有客户端进来一直等待状态下就打印 no client incoming字符串如果有客户端进入的话就直接将新客户端放入列表。
我们在启动函数的时候,如果没有客户端连接,就会出现这样的字样:
然后我们使用Windows下的telnet命令来模拟客户端。输入telnet 127.0.0.1 1024服务器端代码会出现这样的字符串
我们打印新的客户端连接的对象地址并且将新的客户端连接句柄放入列表里面。随后循环进入到了取出新客户端列表并且做出判断每次接收1024字节。如果没有则显示 no data from &lt;xxxx地址&gt;;如果有,那就显示输入的字符串。
好了现在我们打开Windows任务管理器找到拥有telnet的程序并且“杀死”它。
随后,我们会发现,命令行提示符出现了如下内容的字符串:
按照道理,服务器不是应该断开连接了吗?它应该能知道客户端断开了不是吗?
服务器端根本不知道对方已经被“杀死”了所以它的状态仍然在接收中。由于是TCP握手除非你正常将telnet程序关闭才会让服务器端正常接收到客户端关闭的消息否则你永远不知道对方已经退出连接了。
所以**心跳包的作用**就在这里,心跳包**允许你每隔多少毫秒发送数据给服务器端,告诉服务器我还活着,否则服务器就当它已经死了,确认超时,并且退出**。
事实上在TCP/IP的协议层中本身是存在心跳包的设计的就是TCP协议中的SO_KEEPALIVE。
系统默认是设置2小时的心跳频率。需要用setsockopt选项将SOL_SOCKET.SO_KEEPALIVE设置为1打开并且可以设置三个参数tcp_keepalive_timetcp_keepalive_probestcp_keepalive_intvl分别表示连接闲置多久开始发keepalive的ACK包、发几个ACK包不回复就当连接端“死”了。
这种心跳检测包是属于TCP协议底层的检测机制上层软件只是解析显示网口的有用数据包收到心跳包报文属于TCP协议层的数据一般软件不会将它直接在应用层显示出来所以用户是看不到的。以太网中的心跳包可以通过以太网抓包软件分析TCP/IP协议层的数据流看到。报文名称是TCP Keep-Alive。
当然我们也可以做应用层的心跳包检测我们在编写游戏服务器的时候就可以自定义心跳服务TCP层的心跳服务是为了保持存活的但是应用层的心跳则是拥有更明确或者其他的目的比如对方是否还活着
我们专门独立一台服务器做心跳服务器,连接客户端和真正的游戏逻辑服务器,那么我们希望逻辑服务器的同步率和心跳服务器统一,也就是说,**心跳服务器负责的就是发送心跳包和客户端数据给逻辑服务器**,逻辑服务器每一次获取数据,也是从心跳服务器获得的,那么心跳服务器能做的事情就会变得很多。
为了调试方便,我们可以利用心跳服务器,将客户端传送过去的数据包存储在本地磁盘上。如果应用或者游戏在测试的时候,就可以看到那些发送的内容,甚至可以回滚任意时段的数据内容,这样调试起来就相对方便,而不需要客户端大费周章地不停演练重现出现的错误。代码看起来是这样:
```
def SendToServer(is_save = 0):
package = socket.recv(recv_len)
ticktock()
if is_save:
SaveToDisk(package)
server_socket.send(package)
```
在逻辑服务器内部,每一次接收数据,都根据心跳服务包的心跳来接收,这样做的好处就是,可以随时调整心跳的频率,而不需要调整逻辑服务器的代码。
在应用层的心跳模式下,我们会有两种策略需要进行选择。
我们假定把逻辑运算设为A心跳时间比如代码的Sleep或者挂起设为B。
第一种是运算时间A和心跳时间B相对固定。也就是说不管A运算多久B一定是固定挂起多久。
第二种策略是运算时间A和心跳时间B是实时调整。A运算时间长挂起时间就短如果A运算时间加上B挂起时间超过约定心跳总时间那B就不挂起直接进行另一个A运算。这两种策略究竟哪种好呢
在CPU负载并不是那么严重的情况下策略二是比较好的选择。
假设心跳Sleep时间是1000ms运行时间规定为2000ms。如果运行时间小于等于2000ms的话Sleep时间不变如果运行时间超过2000ms的话那么Sleep时间就等于Sleep时间 - (运行时间 - 2000ms)。
这样一来平均心跳有了保障但是在运算量加大的时候Sleep时间已经完全被运行时间所占据那么心跳Sleep时间就会减少到最少甚至不存在CPU的负载就会变得很高这种时候就需要用到策略一。
你可以这么理解。策略一是说不管我们的运行时间多久Sleep时间始终是一致的1000ms这种方式保证了服务器一定会进行心跳而不会导致负载过高等情况。
当然这只是一种简单的模型在进行大规模运算或者有多台服务器的时候我们可以将两种方式合并起来进行策略交互。任务不繁重的时候采用策略二当服务器发现任务一直很多且超过Sleep时间几次就切换到策略一这样可以保证心跳时间基本一致。
我们可以将心跳服务和逻辑服务分开运行,而是否放在同一台物理机并不是首要的问题,这样心跳服务器只提供心跳包,而逻辑服务通过心跳包自动判断并且调整运行频率。
## 小结
好了,我给今天的内容做一个总结。
<li>
判断非阻塞模型的网络是否断开可以使用心跳包和计算超时的方式进行断开操作比如30秒没收到心跳包则可以强制关闭Socket句柄断开。
</li>
<li>
心跳包是一种服务器之间交互的方法也可以用作服务器数据调试和回滚的策略方案。心跳包有两种策略第一种就是运算时间A和心跳时间B相对固定第二种策略是运算时间A和心跳时间B是实时调整。CPU的负载很高的时候用策略一CPU负载并不是那么严重的情况下策略二是比较好的选择。
</li>
最后,给你留一个思考题吧。
如果编写的是阻塞方式的服务器代码,心跳包还有存在的意义吗?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,86 @@
<audio id="audio" title="第26讲 | 如何用网关服务器进行负载均衡?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/6e/19ec637ff46fb9455604393742440b6e.mp3"></audio>
我们费劲心思做了一款游戏,那肯定希望有很多人来玩。一旦玩家数量开始多起来,服务器需要承担的压力就会变大,我们就需要做负载均衡了。
所谓的负载均衡,就是对外有一个公共地址,在请求过来的时候,通过轮询、随机分配等方式,定点到不同的服务器,以分摊服务器的压力。
## 两种常用的负载均衡技术
常用的负载均衡技术有**软件模式**和**硬件模式。**
其中硬件模式用得比较多的是F5。F5是F5 Networks公司提供的一个负载均衡器专用设备 F5 BIG-IP LTM 的官方名称叫本地流量管理器可以做47层负载均衡具有负载均衡、应用交换、会话交换、状态监控等等全备的强大网络功能。
F5硬件负载均衡的优点是负载均衡能直接通过智能交换机实现,处理能力更强,与操作系统无关,负载性能强,适用于一大堆设备、大访问量,其缺点也是很明显的,那就是购买成本高,设备的配置冗余,有些用不上的都给默认配置了。另外,由于设备只有单个,所以单个负载均衡配置无法有效掌握所有服务器及应用状态。
硬件的负载均衡,是从网络层来判断负载规则,所以有时候软件的系统处理能力出现问题,网络硬件还可以作出负载的动作。
软件模式的话比较常用的软件的有LVS、Nginx、HAProxy。
LVS是四层负载均衡根据目标地址和端口选择内部服务器。Nginx这种负载均衡工具是七层负载均衡。而HAProxy同时支持四层、七层负载均衡还可以根据报文内容选择内部服务器。
因此LVS分发路径优于Nginx和HAProxy性能上也要高些。但Nginx和HAProxy则更具配置性比如说可以用来做动静分离。所谓动静分离就是根据请求协议的特征去选择静态资源服务器还是应用服务器。
很久以前,游戏服务器只是简单的对应客户端的服务,就像使用编程语言写了一个多线程的程序,每个线程接收一个客户端,然后把该存储的数据放到数据库去保存。到了后期,大量的网游开始兴起,玩的人越来越多,所以一些老式游戏服务器框架已经无法满足更大更现代化的网络游戏的需求了。
到了2005年左右这种情况愈演愈烈不改变现状不行了。于是程序员和游戏开发厂商设计出了新的一种服务器的框架模型。这种模型几乎是延用到今天这种模型甚至延伸到各行各业的服务框架。
我们甚至可以说Nginx反向代理的想法也是类似这种模型的一种表现形式。尽管我们不能说Nginx学的就是这种模型但是与这种反向代理的模型的做法实在太类似了。
这种服务器模型的最大改变就是加了一个gateway可以称作网关。这当然不是传统意义上的网关路由器只是在服务器的应用层面做的事情类似网关路由器所以我们仍然把它称为网关。
我们可以在Web端称它为**会话****Session**),也可以称它为**Link Server**,总之道理是一样的。
这个网关服务器所做的工作可以分为两种,对应网关服务器实现不同功能的服务。每一种功能不同,后台逻辑服务器的传输数据内容也会不同,不能相互混合使用。
## 网关服务器有哪些功能?
### 1.中转功能
网关服务器作为一种代理所有玩家从客户端传输到真正的游戏逻辑服务器的内容都需要通过网关服务器用该服务器作为中转。也就是说假设有A客户端到B服务器网关为G的话就是A到G到B然后B服务器完成逻辑计算后返回给G网关网关再一次返回给A、B到G到A。
这样做的好处是,网关可以随时询问它底下的真实逻辑服务器到底哪一台趋于饱和,可以将玩家移动到不饱和的游戏服务器,但是缺点也是很明显的,那就是玩家和服务器之间隔了一层网关,需要消耗更长的时间,传输速率相对低。
### 2.负载均衡
网关服务器作为查询网关,也就是说,网关服务器会和底下所有服务器做一个长连接,或者随时询问的连接,这个连接所询问的内容,可以放到一个缓存里面,所查询的内容就是它底下所有服务器哪一台有空,在这种功能模式下,网关服务器只做了负载均衡的工作。
那么当客户端A要连接到游戏服务器的时候需要先询问网关服务器G模型看起来会是这样
A-询问GG通过查询缓存表告知A客户端C服务器有空于是通知A你去连C服务器IP地址和端口号是多少多少于是A从网关G关闭连接去连接C服务器。如果连接失败因为是缓存查询从逻辑上讲有可能滞后那么再次询问网关直到成功连接某一台服务器为止。
这个模型,网关服务器只做了负载均衡的动作,客户端和网关之间不会保持一个长连接,在这个基础上,一台网关服务器支撑同时七千人以上都不是什么太大的问题。但是它的缺点也很明显,那就是一台游戏逻辑服务器只能负责一个游戏世界,不能进行分块。如果要进行分块,则需要其他模型的服务器模块,这个我一会儿会说。
Nginx的反向代理也是类似这种负载均衡的网关模型这种模型大量运用在很多应用服务器、HTTP连接的网络服务器上。但是这项技术到了上升时期开始遇到了瓶颈人们发现就算加上网关也无法负担体量更大的游戏地图。于是我们需要对这样的模型进行修改。
## 如何优化负载均衡的网关模型?
首先,需要将网关服务器增加为几个网关服务器。每个网关服务器都做相同的工作,也就是管理它所下属的所有逻辑服务器。客户端在启动的时候,随机抽取某一个网关服务器,进行询问,使用网关服务器做代理进行中转。
如果游戏地图特别大这样的模型可以将游戏地图分割成几块分割好的地图放到下属的各个逻辑服务器中网关做中转服务比如服务器A负责浙江省服务器B负责安徽省等等。
客户端在连接到网关服务器后,随着游戏进度的走向,网关服务器可以选择连接负责哪一块地图的逻辑服务器,这样在玩家看来就像是连接了一台服务器,而客户端并不用考虑切换服务器的工作。
当然为了减轻服务器的压力,增加更多的人流量,后期这样的模型被逐步细分。比如可以将聊天服务放到一台独立的服务器进行存放,把用户数据独立到一台数据服务器存放,把商品交易放到另一个独立的服务器,或者把私信等等这些和主游戏逻辑无关的内容都放到一个独立的服务器上。
这样一来,主游戏逻辑的服务器的负载就会减轻,然而客户端就不得不多连接几台服务器,要不停获取用户数据或者聊天信息等等,某些负载就转嫁到客户端上了。
这样的游戏逻辑服务器的模型一直沿用到现在。某一些稍微轻量级的,只是使用网关当成负载均衡使用,有一些重量级的,加上地图分割,就会增加网关服务器,但是付出的代价就是,如果要加一台新的游戏逻辑服务器的话,势必会增加部署难度。
不仅网关服务器的配置文件要重新部署,每个游戏节点服务器和被分割的诸如聊天等服务都需要进行重新配置,这样付出的代价也是巨大的,当然很多游戏公司靠着这样的服务器框架使用了好多年,其思想也被延伸到各个行业领域的服务器架构中。
## 小结
这节内容差不多了,我来总结一下。
<li>
我首先讲了两种常用的负载均衡技术软件模式和硬件模式。硬件模式用得比较多的是F5。软件模式的话比较常用的软件的有LVS、Nginx、HAProxy。
</li>
<li>
网关服务器有中转功能和负载功能。Nginx的反向代理用的是负载均衡的网关模型但是这种模型无法负担更大体量的内容。为了减轻服务器的压力也为了增加更多的人流量可以通过增加网关分割业务逻辑到独立的服务器分摊服务器压力这种经典类型的服务器模型被大量沿用并使用至今。
</li>
现在给你留一个小问题吧。
我们使用网关服务器这样的模型,如果网关服务器宕机了,或者网关服务器很久没有响应的情况下,有什么办法让客户端能顺利连上网关服务器之下的逻辑服务器呢?
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="第27讲 | 如何制作游戏内容保存和缓存处理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/ec/276f423f97325bfb9531878a2b5624ec.mp3"></audio>
我们在打完游戏的关卡之后,需要保存游戏进度。单机游戏的进度都保存在本地磁盘上,如果是网络游戏的话该怎么办呢?这一节,我就来讲这个内容。
首先,我们要了解游戏内容的保存,需要先了解缓存处理。
为什么要了解缓存的处理呢?那是因为在大量用户的情况下,我们所保存的内容都是为了下次读取,如果每一次都从硬盘或者数据库读取,会导致用户量巨大数据库死锁,或者造成读取速度变慢,所以在服务器端,缓存的功能是一定要加上的。
## Redis不仅是内存缓存
缓存机制里有个叫Redis的软件。它是一种内存数据库很多开发者把Redis当作单纯的内存缓存来使用事实上这种说法并不准确Redis完全可以当作一般数据库来使用。
Redis是一种key-value型的存储系统。它支持存储的value类型很多包括字符串、链表、集合、有序集合和哈希类型。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作而且这些操作都具有原子性。
Redis还支持各种不同方式的排序。为了保证效率数据一般都会缓存在内存中而Redis会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件并且在此基础上实现master-slave主从的同步。
说到Redis就不得不说缓存机制的老前辈Memcached。同样是缓存机制Memcached的做法是多线程非阻塞的IO复用的网络模型。
多线程分监听线程和工作子线程。监听线程监听网络连接接受请求了之后将连接描述字使用管道传递给工作线程进行读写。网络层的事件使用libevent封装。多线程模型可以发挥多核的作用。Memcached所有操作都要对全局变量加锁进行计数等工作所以会有性能损耗。
而Redis使用单线程IO复用模型自己封装了一个简单的事件处理框架对于单纯只有IO操作的模型来说单线程可以将速度优势发挥到最大但是Redis也提供了一些简单的计算功能比如排序、聚合等。
Redis还可以在某些场景下对关系数据库比如MySQL起到较好的补充作用。它提供了多种编程语言的接口开发人员调用起来也很方便。
Redis支持主从同步。通过配置文件可以将主服务器上的数据往任意数量的从服务器上同步从服务器A1也是主服务器BB是关联到其他从服务器B1B2的主服务器同时又是主服务器A的从服务器A1
这种做法就使得Redis可以执行单层的树结构的复制。Redis实现了发布/订阅publisher/subscriber的机制。所谓发布和订阅就是订阅者接收发布者的消息的时候发布者和订阅者都不用去管对方是什么状态只管各司其职就好了在这种状态下可以订阅一个频道并接收主服务器完整的消息发布记录。
## 编写Redis接口代码
我们尝试使用Python编写Redis接口的代码。
要使Python支持Redis编程必须安装一个包“redis”在使用的时候import一下。
```
import redis
```
然后我们开启Redis服务在Windows下可以运行redis-server.exe使用默认配置即可。
现在,我们尝试使用代码连接一下数据库服务,并且往数据库存放并取出、删除内容。
```
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
r.set('foo', 'my_redis')
print r.get('foo')
r.delete('foo')
print r.dbsize(
```
运行结果为输出 my_redis 和 0。
当然如果我们没有运行Redis则会抛出一个异常
<img src="https://static001.geekbang.org/resource/image/44/a3/44d41a80ab192dfa5514a25bb66c80a3.jpg" alt="">
r对象为连接Redis服务器的对象其中db=0表示使用 redis 的0号数据库可以随你喜欢切换为1号、2号等等。如果Redis设置了密码还可以在初始化的时候输入密码。
Redis的初始函数是这样定义的
```
__init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, connection_pool=None, charset='utf-8', errors='strict', decode_responses=False, unix_socket_path=None)
```
在之后的代码中r.set 表明将 key 为 foovalue为 my_redis的内容写入数据库。
最后输出 0 号数据库的内容长度。
值得一提的是Redis对于存储的内容是来者不拒有什么扔什么所以你如果往Redis里插入二进制、UTF-8编码、图片等等任何东西都可以。理论上只要不超过内存大小的数据都可以往里面扔。
最后,我们可以这么写:
```
r.save()
```
强制Redis往硬盘里写入数据这样我们就能保证数据不会因为电脑发生异常而丢失。这样就将内存的数据同步了下来。
我们常说的木桶理论其实在这里也适用。比如电脑的速度取决于电脑设备中最慢的那个设备就像水在桶中的高度始终取决于水桶里面最下方的那个漏水处。而磁盘I/O始终是拖慢电脑速度的重要力量。
前面我们介绍了Redis所以我们可以使用Redis对文件进行缓存。Redis可以当作普通缓存也可以当作文件缓存在Redis中放入任何东西当然也包括放入二进制文件Redis也不会有任何异常出现从Redis缓存中取出二进制文件的速度也非常快因为是直接从内存中取出数据。
我们假设网络游戏保存下来的数据很大因为有人物属性、人物装备、地图NPC位置和怪兽等等。这些玩家退出后游戏保存的数据文件被保存在关系型数据库中或者保存在服务器硬盘的文件中。我们不可能每次都去读取关系数据库中的游戏内容或者硬盘文件内容所以可以用一种方案来存放游戏保存的文件和缓存。
## 如何存放文件和缓存?
这套机制并不局限于读取保存文件,某些大文件,或者数据文件的读取和缓存上,都可以使用这种思路去做。
首先我们假定文件存放在某一个目录所有的负载均衡服务器都存放有这个目录的副本其他分布式服务器存放其他文件和目录我们先暂定A服务器存放文件A1、A2、A3。
这些都是游戏的保存文件在服务器初始启动的时候Redis并不读取任何文件当有请求过来的时候服务器程序通知Redis读取某个文件。
这时我们需要一个机制为了保证服务器的内存开销也为了保证缓存速度我们必须保证被读取量最大的文件被缓存而不是所有文件这时候Python程序可以另开一个线程或者进程暂且命名为 T 线程,记录某文件被缓存。
服务器程序每次得到请求的时候都会将需要递交的被读取文件告诉Python线程T说文件 A1 被缓存了 N 次,文件 A2 被缓存了 N 次在这种策略下T线程通过几个小时或者几天的计数就能明确知道 比如A2 文件被递交次数最多于是它始终通知Redis将A2文件进行缓存而A1由于到了某一天递交次数下降在某一个时间节点上线程T就告知Redis A1文件可以从缓存文件中撤出来节省内存开销让位给读取频次更高更高的文件。
这样,一套完整的缓存计数和缓存的解决方案就出现了。
当然并不是说MySQL等关系型数据库不能做这些工作但从效率和开发成本来讲Redis缓存的开发成本和效率显然更胜一筹。因为在几十万几百万甚至上亿等级用户量的时候就算是Redis在这种量级的情况下也是吃不消的所以如果不在上层做更多层的缓存底层数据库一定是会死锁或者出现各种各样的问题。
那么你可能会说,我可以做索引啊,要知道在连接数足够多的时候,做索引、读写分离,主从数据库等方案,也只是救急只用,无法真正实现稳固的架构体系。
## 小结
我来总结一下今天的内容。
<li>
Redis不仅仅可以用作普通的缓存机制使用也可以当作正常的数据库使用Redis也支持主从同步要按照应用场景不同来配置不同的Redis使用场景。
</li>
<li>
缓存机制不仅仅针对读取游戏保存文件这么一种方案,也可以用作各种数据文件的读取和写入操作。
</li>
<li>
使用现成的Redis等缓存数据软件是一个好的方案。而设计好的框架、好的缓存机制、好的网络模型是一款好网游必不可少的条件。
</li>
现在给你留一个小问题吧。
有没有可能将网络游戏的内容保存在客户端本地的电脑上,如果可以的话,请问如果玩家换了一台电脑,怎么同步内容呢?保存在客户端本地的意义是什么?
欢迎留言说出你的看法。