<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>系统设计 on codedump notes</title>
    <link>https://www.codedump.info/zh/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link>
    <description>Recent content in 系统设计 on codedump notes</description>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Sat, 14 Dec 2019 22:41:22 +0800</lastBuildDate>
    <atom:link href="https://www.codedump.info/zh/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>C&#43;&#43;11中的内存模型下篇 - C&#43;&#43;11支持的几种内存模型</title>
      <link>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-2/</link>
      <pubDate>Sat, 14 Dec 2019 22:41:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-2/</guid>
      <description>&lt;p&gt;在本系列的上篇，介绍了内存模型的基本概念，接下来看C++11中支持的几种内存模型。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;几种关系术语&#34;&gt;&#xA;  几种关系术语&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%87%a0%e7%a7%8d%e5%85%b3%e7%b3%bb%e6%9c%af%e8%af%ad&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在接着继续解释之前，先了解一下几种关系术语。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;sequenced-before&#34;&gt;&#xA;  sequenced-before&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sequenced-before&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sequenced-before用于表示&lt;strong&gt;单线程&lt;/strong&gt;之间，两个操作上的先后顺序，这个顺序是非对称、可以进行传递的关系。&lt;/p&gt;&#xA;&lt;p&gt;它不仅仅表示两个操作之间的先后顺序，还表示了操作结果之间的可见性关系。两个操作A和操作B，如果有A sequenced-before B，除了表示操作A的顺序在B之前，还表示了操作A的结果操作B可见。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;happens-before&#34;&gt;&#xA;  happens-before&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#happens-before&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;与sequenced-before不同的是，happens-before关系表示的&lt;strong&gt;不同线程&lt;/strong&gt;之间的操作先后顺序，同样的也是非对称、可传递的关系。&lt;/p&gt;&#xA;&lt;p&gt;如果A happens-before B，则A的内存状态将在B操作执行之前就可见。在上一篇文章中，某些情况下一个写操作只是简单的写入内存就返回了，其他核心上的操作不一定能马上见到操作的结果，这样的关系是不满足happens-before的。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;synchronizes-with&#34;&gt;&#xA;  synchronizes-with&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#synchronizes-with&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;synchronizes-with关系强调的是变量被修改之后的传播关系（propagate），即如果一个线程修改某变量的之后的结果能被其它线程可见，那么就是满足synchronizes-with关系的。&lt;/p&gt;&#xA;&lt;p&gt;显然，满足synchronizes-with关系的操作一定满足happens-before关系了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;c11中支持的内存模型&#34;&gt;&#xA;  C++11中支持的内存模型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#c11%e4%b8%ad%e6%94%af%e6%8c%81%e7%9a%84%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从C++11开始，就支持以下几种内存模型：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;enum memory_order {&#xA;    memory_order_relaxed,&#xA;    memory_order_consume,&#xA;    memory_order_acquire,&#xA;    memory_order_release,&#xA;    memory_order_acq_rel,&#xA;    memory_order_seq_cst&#xA;};&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;与内存模型相关的枚举类型有以上六种，但是其实分为四类，如下图所示，其中对一致性的要求逐渐减弱，以下来分别讲解。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;c&amp;#43;&amp;#43;model&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-2/c&amp;#43;&amp;#43;model.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; c++model &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;memory_order_seq_cst&#34;&gt;&#xA;  memory_order_seq_cst&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#memory_order_seq_cst&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;这是默认的内存模型，即上篇文章中分析过的顺序一致性内存模型，由于在上篇中的相关概念已经做过详细的介绍，这里就不再阐述了。仅列出引用自《C++  Concurrency In Action》的示例代码。&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;&#xA;#include &amp;lt;thread&amp;gt;&#xA;#include &amp;lt;assert.h&amp;gt;&#xA;&#xA;std::atomic&amp;lt;bool&amp;gt; x,y;&#xA;std::atomic&amp;lt;int&amp;gt; z;&#xA;&#xA;void write_x()&#xA;{&#xA;    x.store(true,std::memory_order_seq_cst);&#xA;}&#xA;&#xA;void write_y()&#xA;{&#xA;    y.store(true,std::memory_order_seq_cst);&#xA;}&#xA;&#xA;void read_x_then_y()&#xA;{&#xA;    while(!x.load(std::memory_order_seq_cst));&#xA;    if(y.load(std::memory_order_seq_cst))&#xA;        ++z;&#xA;}&#xA;&#xA;void read_y_then_x()&#xA;{&#xA;    while(!y.load(std::memory_order_seq_cst));&#xA;    if(x.load(std::memory_order_seq_cst))&#xA;        ++z;&#xA;}&#xA;&#xA;int main()&#xA;{&#xA;    x=false;&#xA;    y=false;&#xA;    z=0;&#xA;    std::thread a(write_x);&#xA;    std::thread b(write_y);&#xA;    std::thread c(read_x_then_y);&#xA;    std::thread d(read_y_then_x);&#xA;    a.join();&#xA;    b.join();&#xA;    c.join();&#xA;    d.join();&#xA;    assert(z.load()!=0);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于采用了顺序一致性模型，因此最后的断言不可能发生，即在程序结束时不可能出现z为0的情况。&lt;/p&gt;</description>
    </item>
    <item>
      <title>C&#43;&#43;11中的内存模型上篇 - 内存模型基础</title>
      <link>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-1/</link>
      <pubDate>Sat, 14 Dec 2019 10:10:15 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-1/</guid>
      <description>&lt;p&gt;前段时间花了些精力研究C++11引入的内存模型相关的操作，于是把相关的知识都学习了一下，将这个学习过程整理为两篇文档，这是第一篇，主要分析内存模型的一些基础概念，第二篇展开讨论C++11相关的操作。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;cpu架构的演进&#34;&gt;&#xA;  CPU架构的演进&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cpu%e6%9e%b6%e6%9e%84%e7%9a%84%e6%bc%94%e8%bf%9b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;早期的CPU，CPU之间能共享访问的只有内存，此时的结构大体如图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;memory&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/memory.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; memory &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;随着硬件技术的发展，内存的访问已经跟不上CPU的执行速度，此时内存反而变成了瓶颈。为了加速读写速度，每个CPU也都有自己内部才能访问的缓存，结构变成了这样：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;multicore&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/multicore.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; multicore &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;有多个CPU处理器，每个CPU处理器内部又有多个核心。&lt;/li&gt;&#xA;&lt;li&gt;存在只能被一个CPU核心访问的L1 cache。&lt;/li&gt;&#xA;&lt;li&gt;存在只能被一个CPU处理器的多个核心访问的L2 cache。&lt;/li&gt;&#xA;&lt;li&gt;存在能被所有CPU处理器都能访问到的L3 cache以及内存。&lt;/li&gt;&#xA;&lt;li&gt;L1 cache、L2 cache、L3 cache的容量空间依次变大，但是访问速度依次变慢。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当CPU结构发生变化，增加了只能由内部才能访问的缓存之后，一些在旧架构上不会出现的问题，在新的架构上就会出现。而本篇的主角内存模型（memory model），其作用就是规定了各种不同的访问共享内存的方式，不同的内存模型，既需要编译器的支持，也需要硬件CPU的支持。&lt;/p&gt;&#xA;&lt;p&gt;我们从一个最简单的多线程访问变量问题谈起。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;简单的多线程访问数据问题&#34;&gt;&#xA;  简单的多线程访问数据问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%80%e5%8d%95%e7%9a%84%e5%a4%9a%e7%ba%bf%e7%a8%8b%e8%ae%bf%e9%97%ae%e6%95%b0%e6%8d%ae%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;假设在程序执行之前，A=B=0，有两个线程同时分别执行如下的代码：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;线程1&lt;/th&gt;&#xA;          &lt;th&gt;线程2&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;1. A=1&lt;/td&gt;&#xA;          &lt;td&gt;3. B=2&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2. print(B)&lt;/td&gt;&#xA;          &lt;td&gt;4. print(A)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;问上述程序的执行结果如何？&lt;/p&gt;&#xA;&lt;p&gt;这个问题是一个简单的排列组合问题，其结果有：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;2（先选择A或B输出）* 2（输出修改前还是之后的结果）* 1（前面第一步选择了一个变量之后，现在只能选剩下的变量）* 2（输出修改前还是之后的结果） = 8&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;其可能的结果包括：(0,0)、(1,0)、(0,2)、(1,2)、(0,1)、(2,0)、(2,1)。（这里只有7个结果，是因为有两个(0,0)，所以少了一个）。&lt;/p&gt;&#xA;&lt;p&gt;由于多个线程交替执行，可能有以下几种结果，下面来分别解析。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个线程依次执行&#34;&gt;&#xA;  两个线程依次执行&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e7%ba%bf%e7%a8%8b%e4%be%9d%e6%ac%a1%e6%89%a7%e8%a1%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;最简单的情况，就是这两个线程依次执行，即一个线程执行完毕之后再执行另一个线程的指令，这种情况下有两种可能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;2-&amp;gt;3-&amp;gt;4&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况先执行完毕线程1，再执行线程2，最后输出的结果是(0,1)。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc1&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc1.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;3-&amp;gt;4-&amp;gt;1-&amp;gt;2&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况先执行完毕线程2，再执行线程1，最后输出的结果是(0,2)。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc2&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个线程交替执行&#34;&gt;&#xA;  两个线程交替执行&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e7%ba%bf%e7%a8%8b%e4%ba%a4%e6%9b%bf%e6%89%a7%e8%a1%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;这样情况下，先执行的可能是线程1或者线程2，来看线程1先执行的情况。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;3-&amp;gt;2-&amp;gt;4&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况下的输出是（2,1）。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc3&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc3.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;3-&amp;gt;4-&amp;gt;2&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况下的输出是（1,2）。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc4&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc4.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上是第一条指令先执行线程1执行的情况，同样地也有先执行线程2指令的情况（3-1-&amp;gt;4-&amp;gt;2和3-&amp;gt;1-&amp;gt;2-4），这里不再列出，有兴趣的读者可以自行画图理解。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;不可能出现的情况&#34;&gt;&#xA;  不可能出现的情况&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%8d%e5%8f%af%e8%83%bd%e5%87%ba%e7%8e%b0%e7%9a%84%e6%83%85%e5%86%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;除了以上的情况之外，还有一种可能是输出(0,0)，但是这种输出在一般情况下不可能出现（我们接下来会解释什么情况下可能出现），下面来做解释。&lt;/p&gt;</description>
    </item>
    <item>
      <title>对比脚本型和编译型游戏服务器的热更新方案</title>
      <link>https://www.codedump.info/zh/post/20191206-gameserver-hot-refresh/</link>
      <pubDate>Fri, 06 Dec 2019 22:40:49 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191206-gameserver-hot-refresh/</guid>
      <description>&lt;p&gt;本文对比游戏服务器中C++搭配脚本语言（Lua、Python）以及纯编译型语言（C++、Golang）来进行开发时，进行线上服务器热更新的方案。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;游戏开发模式&#34;&gt;&#xA;  游戏开发模式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b8%b8%e6%88%8f%e5%bc%80%e5%8f%91%e6%a8%a1%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在开始下文之前，有必要简单描述一下游戏服务与web服务的区别。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;长连接-vs-短连接&#34;&gt;&#xA;  长连接 VS 短连接&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%95%bf%e8%bf%9e%e6%8e%a5-vs-%e7%9f%ad%e8%bf%9e%e6%8e%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务对外与客户端之间的链接多是长连接形式，而web服务多是短连接。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;有状态服务-vs-无状态服务&#34;&gt;&#xA;  有状态服务 VS 无状态服务&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%89%e7%8a%b6%e6%80%81%e6%9c%8d%e5%8a%a1-vs-%e6%97%a0%e7%8a%b6%e6%80%81%e6%9c%8d%e5%8a%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务内，需要维持着玩家的状态数据，如玩家属性、位置等，web请求多是无状态服务。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;启动时间&#34;&gt;&#xA;  启动时间&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%af%e5%8a%a8%e6%97%b6%e9%97%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;由于前面提到的游戏服务是有状态服务，因此游戏服务器启动的时候，需要从持久化存储中将数据加载到内存中，这意味着游戏服务器的启动时间会很长，一般一次需要几分钟，web服务器相对轻量很多，因为需要访问的持久化数据在另外的存储服务器上。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;开发周期&#34;&gt;&#xA;  开发周期&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bc%80%e5%8f%91%e5%91%a8%e6%9c%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务的开发周期短，有一些游戏一周就需要进行一次维护，这意味着在这一周内策划（对应互联网中的产品经理）提出的需求都要完成上线。&lt;/p&gt;&#xA;&lt;p&gt;从以上对比可以看到，游戏业务的特点是更新频繁，而启动一个服务器的时间又比较长。在进行开发的过程中，如果使用纯编译型语言进行开发，那么流程就是如下所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cpp-dev&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/cpp-dev.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cpp-dev &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，上面是一个比较长的开发功能流程，而如果还考虑到开发周期短这个特点，显然是不能匹配游戏开发这种业务的特征的，此时就需要“热更新”功能才能提高开发效率。&lt;/p&gt;&#xA;&lt;p&gt;以下就脚本语言与编译型语言如何实现“热更新”展开讨论。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;c搭配脚本语言&#34;&gt;&#xA;  C++搭配脚本语言&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#c%e6%90%ad%e9%85%8d%e8%84%9a%e6%9c%ac%e8%af%ad%e8%a8%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;这种方案是笔者见过的方案，其一般的做法是：C++来实现底层的框架（网络、与数据库通信等），接收到数据包之后，将数据传递给脚本层，由脚本来处理具体的业务逻辑。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;script-level&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/script-level.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; script-level &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这种也是软件设计中常见的分层方案：底层的模块为上层的模块服务，同时底层模块也变动的较少。&lt;/p&gt;&#xA;&lt;p&gt;由于嵌入到进程里面的脚本语言引擎，本质上是将脚本语言代码翻译成内存中的Opcode来执行，因此这类型游戏服务器实现“热更新”方案很简单：将新的脚本同步到服务器上，然后给服务器发出一个信号，重新读取脚本代码到内存中即可。&lt;/p&gt;&#xA;&lt;p&gt;有了这个架构之后，原先的开发模式就变成了下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;script-dev&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/script-dev.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; script-dev &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，前面编译型语言中编译和重启服务器这两部最消耗时间的步骤，变成了热更新脚本，这样就不需要重启服务器来验证功能，开发效率提高了很多。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;编译型语言实现热更新&#34;&gt;&#xA;  编译型语言实现热更新&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bc%96%e8%af%91%e5%9e%8b%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0%e7%83%ad%e6%9b%b4%e6%96%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从上面的分析可以看到，因为编译型语言存在需要重启服务器的步骤，导致了以下两个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端连接需要断开，因为游戏服务是长连接。&lt;/li&gt;&#xA;&lt;li&gt;重启服务器时需要耗费大量的时间将持久化存储的数据加载到内存中，这样启停过程中的客户端请求就会丢失。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面依次看看如何解决这两个问题。&lt;/p&gt;&#xA;&lt;p&gt;维护客户端连接，可以再引入一个网关组件，由网关来维护连接，这样服务器重启流程中客户端对内部游戏服务器的启停并无感知。&lt;/p&gt;&#xA;&lt;p&gt;为了在启动新版本服务器的过程中继续服务客户端请求，并且新版本服务器上线之后能接着当前的玩家属性继续操作，可以考虑将数据存入共享内存中，这样即便进程退出共享内存还存在。这样做的思路是“代码与数据分离”。&lt;/p&gt;&#xA;&lt;p&gt;如果要实现这个方案，又要做到以下两点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;设计一套面向共享内存的数据结构，至少应该能支持常见的链表、数组、字典等类型。&lt;/li&gt;&#xA;&lt;li&gt;数据结构的设计需要考虑可扩展性以及前后兼容性，因为可能出现两个前后版本中，有一些字段不存在或者有一些字段新增的情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了以上的介绍，下图中就是为了支持热更新的编译型语言的架构方案：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cpp-hotrefresh&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/cpp-hotrefresh.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cpp-hotrefresh &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关负责维护与客户端的连接，同时也知道当前访问的是哪个游戏服务器。当新版本服务器启动完毕之后，向网关发送一个指令，让网关在收到这个指令之后的所有客户端请求，都转发到新的游戏服务器上，这样就完成了一个看似没有重启的“热更新”。&lt;/li&gt;&#xA;&lt;li&gt;数据保存在共享内存中，这样即使在启动新版本服务器的时候也能继续服务客户端的请求。另外需要注意的是，启动的时候服务器需要判断一下是否已经有一个进程存在，如果存在进程且有共享内存数据的情况下，不需要再从持久化存储中加载数据到内存中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;方案对比&#34;&gt;&#xA;  方案对比&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b9%e6%a1%88%e5%af%b9%e6%af%94&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以下来对比一下两种技术方案的优缺点。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;特性&lt;/th&gt;&#xA;          &lt;th&gt;脚本型游戏服务器&lt;/th&gt;&#xA;          &lt;th&gt;编译型游戏服务器&lt;/th&gt;&#xA;          &lt;th&gt;备注&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;开发效率&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;脚本语言没有编译步骤&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;性能&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;脚本语言执行性能不如编译型语言&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;架构难度&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;编译型语言为了实现热更新，需要解决：网关维护连接，代码数据分离，数据存入共享内存等，而脚本型语言只需要实现热更新脚本即可&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;重构难度&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;“脚本语言一时爽，代码重构火葬场”&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-如何解决消息的乱序</title>
      <link>https://www.codedump.info/zh/post/20191013-im-msg-out-of-order/</link>
      <pubDate>Sun, 13 Oct 2019 10:59:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191013-im-msg-out-of-order/</guid>
      <description>&lt;p&gt;IM消息需要面对的另一个难题：如何保证收到的消息不乱序。下面先展开看看要解决这个难题有哪些障碍。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;消息乱序的原因&#34;&gt;&#xA;  消息乱序的原因&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b6%88%e6%81%af%e4%b9%b1%e5%ba%8f%e7%9a%84%e5%8e%9f%e5%9b%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;时间难以保证&#34;&gt;&#xA;  时间难以保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%97%b4%e9%9a%be%e4%bb%a5%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然谈到“顺序”，就必然有一个衡量的标准，然而无论是使用客户端时间还是服务器时间都难以作为这个标准来衡量消息的先后顺序。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;msg&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，一个IM系统在多个客户端，在不同的接入网关进行接入，进而又在不同的逻辑处理服务器上进行处理，不论是客户端本身，还是服务器（网络、逻辑服务器），各自机器上的时间都不相同，因此无法以机器本地的时间来作为衡量消息顺序的标准。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;网络顺序无法保证&#34;&gt;&#xA;  网络顺序无法保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bd%91%e7%bb%9c%e9%a1%ba%e5%ba%8f%e6%97%a0%e6%b3%95%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;考虑到只有一个客户端连接上一个网关的场景，即使在这样的场景中，消息的先后顺序也因为网络的因素难以得到保证。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;msg-network&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/msg-network.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; msg-network &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，网关试图向客户端依次发送消息1、2这两条消息，可能出现下面的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关向客户端发送消息1，此时客户端的网络状况不好，导致该消息可能会丢失或者重传。&lt;/li&gt;&#xA;&lt;li&gt;网关没有等待消息1的发送结果，继续发送了消息2，而此时客户端的网络状况变好，这条消息比消息1更快的被客户端收到。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上的场景，可能会有人想到一种处理模式：网关只有在客户端应答收到了消息1之后再继续发送消息2，这样就不会出现网络原因导致的消息乱序问题了。然而这样的话，消息相当于串行发送了，效率并不高。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;多线程因素导致的乱序&#34;&gt;&#xA;  多线程因素导致的乱序&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%9a%e7%ba%bf%e7%a8%8b%e5%9b%a0%e7%b4%a0%e5%af%bc%e8%87%b4%e7%9a%84%e4%b9%b1%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端、服务器都可能存在多个发送、接收线程，这也是导致消息乱序的原因之一。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;解决策略&#34;&gt;&#xA;  解决策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a7%a3%e5%86%b3%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面分析了消息乱序的几个成因，下面就逐个分析都应该怎么解决。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;消息序列号&#34;&gt;&#xA;  消息序列号&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b6%88%e6%81%af%e5%ba%8f%e5%88%97%e5%8f%b7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;前面提过的第一个问题：消息的时序标准问题，无法以客户端或者服务器本地的时间来作为衡量的标准，此时可以引入一个产生递增ID的组件，由这一组件来统一生成递增、不回退的消息序列号用于衡量消息的先后顺序。&lt;/p&gt;&#xA;&lt;p&gt;然而这里还有可以细化讨论的部分：这个组件生成的ID，是否需要全局唯一？即不论单聊、群聊都需要保证生成出来的序列号唯一。&lt;/p&gt;&#xA;&lt;p&gt;这个全局唯一性不是必要的，原因在于不同的聊天，能保证消息在自己的频道唯一、递增即可。有了这个前提，这个组件生成ID的流程大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;处理该聊天的逻辑服务器ID。&lt;/li&gt;&#xA;&lt;li&gt;每个聊天频道（单聊、群聊）有自己一个独立的频道ID。&lt;/li&gt;&#xA;&lt;li&gt;每个频道内部，保证能够产生一个递增、不回退的序列号。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样，消息序列号实际上由三部分部分组成：逻辑服务器ID-频道ID-频道内的消息序列号。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;群聊消息的处理&#34;&gt;&#xA;  群聊消息的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%be%a4%e8%81%8a%e6%b6%88%e6%81%af%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;有了前面的消息序列号，已经解决了第一个问题：消息的时序标准问题。然而这样还不足够，考虑到下图中的群聊场景：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;group-msg&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/group-msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; group-msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;两个客户端依次发出消息A和消息B。&lt;/li&gt;&#xA;&lt;li&gt;在两个不同的处理群聊消息的服务器中，由于种种原因，反倒是消息B比消息A先到。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面可以看出，群聊消息乱序的原因在于：同一个群聊的消息，最后被分派到了两个不同的逻辑服务器上处理。&lt;/p&gt;&#xA;&lt;p&gt;还是继续沿用上面生成消息序列号的思路：如果是同一个聊天频道的消息，就放在一起处理。因此可以变成下图中的处理方式：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;group-msg-2&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/group-msg-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; group-msg-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，根据群聊消息的群ID来选择逻辑服务器，这样同一个群的消息都能落在同一个服务器中来处理了。&lt;/p&gt;&#xA;&lt;p&gt;可以看到，这里并不需要使用一个“分布式唯一递增ID”这样的组件来产生ID，因为这里的问题简化成了：只需要该消息序列号在所在的逻辑服务器处理的聊天频道中唯一且递增就可以了。问题的重新分析和定义，让这个处理变得简单了很多。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;网络乱序的处理&#34;&gt;&#xA;  网络乱序的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bd%91%e7%bb%9c%e4%b9%b1%e5%ba%8f%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;接着处理由于网络原因导致的乱序，TCP协议中也有类似处理网络乱序的手段，简单来说：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TCP协议栈中有缓冲区缓存收到的数据。&lt;/li&gt;&#xA;&lt;li&gt;发送端使用序列号ACK来确认接收端收到的数据，比如1、2、3三个序列号的数据，如果先接收到1，此时发送端会收到ACK 1的消息，但是在这之后如果消息3先于消息2被接收端收到，此时发送端仍然会ACK消息1，表示消息3这条消息是乱序的。&lt;/li&gt;&#xA;&lt;li&gt;有了缓冲区和确认序列号，就知道哪些数据可以由协议栈提供给应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;tcp-stack&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/tcp-stack.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; tcp-stack &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;接收方TCP协议栈中依次存有消息1和3，而消息2还未接收到。&lt;/li&gt;&#xA;&lt;li&gt;消息1被发送方确认，此时消息1可以提供给应用层。&lt;/li&gt;&#xA;&lt;li&gt;由于消息2没有接收到，因此消息3是乱序消息，不能提供给应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从中得到的启发是：收发队列是可以有发送者来掌控的，发送者知道消息的顺序，虽然不能保证消息收发的前后顺序，但是由于引入了缓冲区，只有被确认的消息才可以被消费，这样可以通过发送者的ACK确认，来保证消息的顺序消费。&lt;/p&gt;&#xA;&lt;p&gt;以上的思路，可以沿用到网络乱序消息的处理中。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;最终方案&#34;&gt;&#xA;  最终方案&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%80%e7%bb%88%e6%96%b9%e6%a1%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;综合以上的分析，消息乱序问题可以使用下面的方式来解决。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端消息缓存队列&#34;&gt;&#xA;  客户端消息缓存队列&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e6%b6%88%e6%81%af%e7%bc%93%e5%ad%98%e9%98%9f%e5%88%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端内部，维持一个缓存消息的队列，每个消息都有对应的消息序列号，收到消息之后需要与网关进行确认，以此确认这条消息是否是按序接收的消息，只有这样的消息才能提供给应用层消费。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-网关接入层</title>
      <link>https://www.codedump.info/zh/post/20190818-im-msg-gate/</link>
      <pubDate>Sun, 18 Aug 2019 16:55:17 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190818-im-msg-gate/</guid>
      <description>&lt;p&gt;IM服务系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20190608-im-design-base/&#34;&gt;IM服务器设计-基础&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20190608-im-msg-storage/&#34;&gt;IM服务器设计-消息存储&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;网关接入层负责维护与客户端之间的长连接，由于它是唯一一个与客户端进行直接通信的服务入口，维护着大量的客户端连接，其设计原则应该满足：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;安全&lt;/li&gt;&#xA;&lt;li&gt;稳定&lt;/li&gt;&#xA;&lt;li&gt;快速&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;具体来说，需要考虑不少的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用什么数据结构保存与客户端的连接？&lt;/li&gt;&#xA;&lt;li&gt;如何清除死链？&lt;/li&gt;&#xA;&lt;li&gt;在网关宕机的情况下如何容错？&lt;/li&gt;&#xA;&lt;li&gt;服务如何降级？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下具体展开。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基础设计&#34;&gt;&#xA;  基础设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e7%a1%80%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;简而言之，网关内部维护着一个map，其中保存着客户端相关的ID与对应连接的映射关系。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;map&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/map.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; map &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;内部服务需要应答客户端时，经历如下步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;到redis中查询路由信息，即客户端连接到了哪个网关，将消息发送给该网关。&lt;/li&gt;&#xA;&lt;li&gt;网关服务在上面的map中找到对应的客户端连接，将消息发送给客户端。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;死链的处理&#34;&gt;&#xA;  死链的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%ad%bb%e9%93%be%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;由于网关上维护着大量的客户端连接，需要通过收发心跳报的方式检查死链，具体做法是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关针对每个连上的连接，都创建一个定时器。&lt;/li&gt;&#xA;&lt;li&gt;网关跟客户端的每次交互之后，网关都对应的更新一下该客户端的心跳时间为当前时间。&lt;/li&gt;&#xA;&lt;li&gt;客户端内部同样也维护一个定时器，每次定时器超时时，判断当前是否已经有一段时间没有跟网关通信了，此时将发出心跳消息进行保活。&lt;/li&gt;&#xA;&lt;li&gt;当该每个定时器到期时，检查客户端的心跳时间距离当前时间已经超过一个阈值了，那么将认为该客户端已经失连，将清除掉该连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要注意的是，客户端的定时器应该小于网关层给每个连接加上的定时器。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;keepalive&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/keepalive.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; keepalive &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;容错设计&#34;&gt;&#xA;  容错设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%b9%e9%94%99%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;网关有可能宕机，此时要考虑到这种情况下的容错处理。&lt;/p&gt;&#xA;&lt;p&gt;这里的原则有两条：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端一旦发现前面连接的网关宕机，将尝试重连。&lt;/li&gt;&#xA;&lt;li&gt;内部服务要通过网关层应答给客户端的消息，一旦发现由于网关宕机而无法发出，将直接丢弃，由客户端重新尝试重连。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下来详细解释一下这两个原则。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端重连&#34;&gt;&#xA;  客户端重连&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e9%87%8d%e8%bf%9e&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端内部维护着一个发出消息的消息队列，仅在收到服务器的处理应答之后才可以从其中清除相应的消息。注意，这里每个客户端的消息ID需要做到严格递增。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;messagequeue&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/messagequeue.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; messagequeue &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;比如，上图中发出但是未收到应答的消息有三条，消息ID依次递增，分别是100、101、102。此时如果收到服务器应答消息101已经被确认处理，那么在这个序号之前的消息100以及101都可以被认为已经被服务器正常接收并且处理完毕，此时可以从消息队列中删除掉序号101之前的消息了。&lt;/p&gt;&#xA;&lt;p&gt;反之，客户端同时还维护另外一个定时器，一段时间没有收到连接的网关消息时，将向网关发出心跳消息，如果仍然没有回复则认为网关出现异常，将重新走正常的登录流程尝试选择另外一台网关登录。重连之后，将重新发送消息队列中已经存在的消息。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;重连策略&#34;&gt;&#xA;  重连策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%bf%9e%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;当一台网关出现问题需要客户端进行重连时，还需要考虑到不要因为重连问题导致了其他网关服务器也受影响，产生雪崩效应，此时还需要考虑以下几点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;打散重连时间：需要进行重连的客户端，在一个时间范围内选择一个随机的时间，这样将这些客户端的重连时间打散，不至于一下子都连接上来。&lt;/li&gt;&#xA;&lt;li&gt;指数退避：一次重连不上时，客户端还需要再次尝试进行多次重连，然而重连的时间需要像TCP协议那样在阻塞恢复时做指数退避，即第一次重连时间是1秒后，第二次2秒后，第三次4秒后，等等。这个策略也是为了避免由于重连导致的服务雪崩。&lt;/li&gt;&#xA;&lt;li&gt;服务器保护：上面两条是客户端的重连策略，然而服务器自身也需要进行保护，当服务器判断自己当前的负载到一定程度时，将拒绝客户端的连接请求。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;内部服务丢弃应答消息&#34;&gt;&#xA;  内部服务丢弃应答消息&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%85%e9%83%a8%e6%9c%8d%e5%8a%a1%e4%b8%a2%e5%bc%83%e5%ba%94%e7%ad%94%e6%b6%88%e6%81%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;同样的，内部服务也只是通过网关层与客户端进行通信，当处理了一些消息之后需要应答客户端，此时发现对应的网关已经宕机，那么应该丢弃掉这些应答消息，等待客户端重连之后重新将前面没有收到应答的消息发出来。&lt;/p&gt;&#xA;&lt;p&gt;如果是这个处理原则的话，对应的就需要服务器的逻辑中做到“幂等性（idempotent）”了，即同一个操作，一次请求与多次请求的结果是一样的。比如，逻辑服务器可以通过客户端的消息ID来判断这条消息之前是否已经被处理过，如果是的话可以直接忽略处理应答处理即可。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务保证&#34;&gt;&#xA;  服务保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;每个网关服务器可以容纳的长连接总数是固定的，到了一定程度系统资源就消耗的差不多了，应答的延迟也提高了。所以，网关层还需要考虑到服务的可用性。&lt;/p&gt;&#xA;&lt;p&gt;比如，可以向管理网关的服务器上报如下数据：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;当前维护的连接数量。&lt;/li&gt;&#xA;&lt;li&gt;当前应答延迟指标，90%的延迟到多少，99%的应答延迟到多少，等等。&lt;/li&gt;&#xA;&lt;li&gt;当前系统资源的消耗情况，比如CPU占用、内存占用等等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样，可以有依据来判断该网关是否还能继续接收新的连接，如果不能接收连接可以返回一批当前可用的其他网关服务列表给客户端重新发起连接，同时将当前不可用的网关从返回给客户端的网关列表中删除，这样下次就不会再来这个网关进行连接。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;qos&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/qos.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; qos &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，有如下步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关都向网关管理服务上报自己当前的服务状态，管理服务发现网关A已经接近服务极限，此时将通知网关A此时不能再接收新的连接，同时还告知当前可用的网关B和C地址。&lt;/li&gt;&#xA;&lt;li&gt;客户端向网关A发起请求，此时网关A拒绝该连接请求，并且返回网关B和C的服务列表给客户端。&lt;/li&gt;&#xA;&lt;li&gt;客户端选择网关C进行连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，这实际上是“服务降级”的一种做法。&lt;/p&gt;</description>
    </item>
    <item>
      <title>服务调用的演进历史</title>
      <link>https://www.codedump.info/zh/post/20190629-service-history/</link>
      <pubDate>Sat, 29 Jun 2019 12:47:07 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190629-service-history/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这是2019年给组内分享时整理的一篇服务调用演进历史的科普文。写作本文的时候，我自己最大的感受是：如果能清楚理解演化历史中的一些原则和思路，就会发现现在的变化并不新鲜。它们不是今天才有，也不会止于今天的演化。在技术大发展的今天，更多的关注本质才能让我们不至于在变化中失去方向。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;这个题目稍微有点大，纯粹是一篇科普文，将我所了解到的解决“服务调用”相关的技术演进历史简述一下，本文专注于演化过程中每一步的为什么（Why）和是什么（What）上面，尽量不在技术细节（How）上面做太多深入。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务的三要素&#34;&gt;&#xA;  服务的三要素&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e7%9a%84%e4%b8%89%e8%a6%81%e7%b4%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;一般而言，一个网络服务包括以下的三个要素：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地址：调用方根据地址访问到网络接口。地址包括以下要素：IP地址、服务端口、服务协议（TCP、UDP，etc）。&lt;/li&gt;&#xA;&lt;li&gt;协议格式：协议格式指的是该协议都有哪些字段，由接口提供者与协议调用者协商之后确定下来。&lt;/li&gt;&#xA;&lt;li&gt;协议名称：或者叫协议类型，因为在同一个服务监听端口上面，可能同时提供多种接口服务于调用方，这时候需要协议类型（名称）来区分不同的网络接口。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明在服务地址中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IP地址提供了在互联网上找到这台机器的凭证。&lt;/li&gt;&#xA;&lt;li&gt;协议以及服务端口提供了在这台机器上找到提供服务的进程的凭证。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;service-address&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/service-address.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; service address &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这都属于TCPIP协议栈的知识点，不在这里深入详述。&lt;/p&gt;&#xA;&lt;p&gt;下图中，以最简单的一个HTTP请求，来拆解请求URL中的服务要素：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;http-request&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/http-request.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; http-request &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;http：指明使用的是哪种应用层协议，同类型的还有“https”、“ftp”等。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.abc.com&#34;&gt;www.abc.com&lt;/a&gt;：域名地址，最终会由DNS域名解析服务器解析成数字的IP地址。&lt;/li&gt;&#xA;&lt;li&gt;8080：前面解析成数字化的IP地址之后，就可以访问到具体提供服务的机器上，但是上面提供服务的进程可能有很多，这时候就需要端口号来告诉协议栈到底是访问哪个进程提供的服务了。&lt;/li&gt;&#xA;&lt;li&gt;hello：该服务进程中，可能提供多个接口供访问，所以需要接口名+协议（即前面的http）告诉进程访问哪个协议的哪个接口。&lt;/li&gt;&#xA;&lt;li&gt;msg=world：不同的接口，需要的参数不同，最后跟上的查询参数（query param）告诉服务请求该接口服务时传入的参数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上，简单的把网络服务的几个要素进行了描述。&lt;/p&gt;&#xA;&lt;p&gt;这里还需要对涉及到服务相关的一些名词做解释。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;服务实例：服务对应的IP地址加端口的简称。需要访问服务的时候，需要先寻址知道该服务每个运行实例的地址加端口，然后才能建立连接进行访问。&lt;/li&gt;&#xA;&lt;li&gt;服务注册：某个服务实例宣称自己提供了哪些服务，即某个IP地址+端口都提供了哪些服务接口。&lt;/li&gt;&#xA;&lt;li&gt;服务发现：调用方通过某种方式找到服务提供方，即知道服务运行的IP地址加端口。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基于ip地址的调用&#34;&gt;&#xA;  基于IP地址的调用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8eip%e5%9c%b0%e5%9d%80%e7%9a%84%e8%b0%83%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最初的网络服务，通过原始的IP地址暴露给调用者。这种方式有以下的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IP地址是难于记忆并且无意义的。&lt;/li&gt;&#xA;&lt;li&gt;另外，从上面的服务三要素可以看到，IP地址其实是一个很底层的概念，直接对应了一台机器上的一个网络接口，如果直接使用IP地址进行寻址，更换机器就变的很麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;“尽量不使用过于底层的概念来提供服务”，是这个演化流程中的重要原则，好比在今天已经很少能够看到直接用汇编语言编写代码的场景了，取而代之的，就是越来越多的抽象，本文中就展现了服务调用这一领域在这个过程中的演进流程。&lt;/p&gt;&#xA;&lt;p&gt;在现在除非是测试阶段，否则已经不能直接以IP地址的形式将服务提供出去了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;域名系统&#34;&gt;&#xA;  域名系统&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%9f%e5%90%8d%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面的IP地址是给主机做为路由器寻址的数字型标识，并不好记忆。此时产生了域名系统，与单纯提供IP地址相比，域名系统由于使用有意义的域名来标识服务，所以更容易记忆。另外，还可以更改域名所对应的IP地址，这为变换机器提供了便利。有了域名之后，调用方需要访问某个网络服务时，首先到域名地址服务中，根据DNS协议将域名解析为相应的IP地址，再根据返回的IP地址来访问服务。&lt;/p&gt;&#xA;&lt;p&gt;从这里可以看到，由于多了一步到域名地址服务查询映射IP地址的流程，所以多了一步解析，为了减少这一步带来的影响，调用方会缓存解析之后的结果，在一段时间内不过期，这样就省去了这一步查询的代价。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;协议的接收与解析&#34;&gt;&#xA;  协议的接收与解析&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%8e%a5%e6%94%b6%e4%b8%8e%e8%a7%a3%e6%9e%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以上通过域名系统，已经解决了服务IP地址难以记忆的问题，下面来看协议格式解析方面的演进。&lt;/p&gt;&#xA;&lt;p&gt;一般而言，一个网络协议包括两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;协议包头：这里存储协议的元信息（meta infomation），其中可能会包括协议类型、报体长度、协议格式等。需要说明的是，包头一般为固定大小，或者有明确的边界（如HTTP协议中的\r\n结束符），否则无法知道包头何时结束。&lt;/li&gt;&#xA;&lt;li&gt;协议包体：具体的协议内容。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;无论是HTTP协议，又或者是自定义的二进制网络协议，大体都由这两部分组成。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;protocol-format&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-format.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol format &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于很多时候不能一口气接收完毕客户端的协议数据，因此在接收协议数据时，一般采用状态机来做协议数据的接收：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;protocol-statemachine&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-statemachine.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol statemachine &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;接收完毕了网络数据，在协议解析方面却长期停滞不前。一个协议，有多个字段（field），而这些不同的字段有不同的类型，简单的raw类型（如整型、字符串）还好说，但是遇到复杂的类型如字典、数组等就比较麻烦。&lt;/p&gt;&#xA;&lt;p&gt;当时常见的手段有以下几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用json或者xml这样的数据格式。好处是可视性强，表达起上面的复杂类型也方便，缺陷是容易被破解，传输过去的数据较大。&lt;/li&gt;&#xA;&lt;li&gt;自定义二进制协议。每个公司做大了，在这一块难免有几个类似的轮子。笔者见过比较典型的是所谓的TLV格式（Type-Length-Value），自定义二进制格式最大的问题出现在协议联调与协商的时候，由于可视性比较弱，有可能这边少了一个字段那边多了一个字段，给联调流程带来麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上面的问题一直到Google的Protocol Buffer（以下简称PB）出现之后才得到很大的改善。PB出现之后，也有很多类似的技术出现，如Thrift、MsgPack等，不在这里阐述，将这一类技术都以PB来描述。&lt;/p&gt;&#xA;&lt;p&gt;与前面的两种手段相比，PB具有以下的优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用proto格式文件来定义协议格式，proto文件是一个典型的DSL（domain-specific language）文件，文件中描述了协议的具体格式，每个字段都是什么类型，哪些是可选字段哪些是必选字段。有了proto文件之后，C\S两端是通过这个文件来进行协议的沟通交流的，而不是具体的技术细节。&lt;/li&gt;&#xA;&lt;li&gt;PB能通过proto文件生成各种语言对应的序列化反序列化代码，给跨语言调用提供了方便。&lt;/li&gt;&#xA;&lt;li&gt;PB自己能够对特定类型进行数据压缩，减少数据大小。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;pb&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/pb.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; pb &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务网关&#34;&gt;&#xA;  服务网关&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e7%bd%91%e5%85%b3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;有了前面的演化之后，写一个简单的单机服务器已经不难。然而，当随着访问量的增大，一台机器已经不足以支撑所有的请求，此时就需要横向扩展多加一些业务服务器。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-消息存储</title>
      <link>https://www.codedump.info/zh/post/20190608-im-msg-storage/</link>
      <pubDate>Sat, 08 Jun 2019 20:18:47 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190608-im-msg-storage/</guid>
      <description>&lt;p&gt;这部分专门讲述IM消息存储的设计。消息存储的难度在于，要考虑以下的场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;离线消息存储。即发送消息时对方不在线该怎么处理。&lt;/li&gt;&#xA;&lt;li&gt;单聊、群聊消息。&lt;/li&gt;&#xA;&lt;li&gt;随着用户量越来越大，应该以后如何扩展。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;读扩散-vs-写扩散&#34;&gt;&#xA;  读扩散 VS 写扩散&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%89%a9%e6%95%a3-vs-%e5%86%99%e6%89%a9%e6%95%a3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;消息同步模型中，有写扩散和读扩散这两种模型。在开始讨论之前需要先了解两个相关的概念：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;收件箱（inbox）：该用户收到的消息。&lt;/li&gt;&#xA;&lt;li&gt;发件箱（outbox）：该用户发出的消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;写扩散push&#34;&gt;&#xA;  写扩散（push）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%99%e6%89%a9%e6%95%a3push&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;写扩散就是经常说的push模式，即每个消息都直接发送到该用户的收件箱中。其优缺点如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：读优化，用户每次只需要去读取自己收件箱中的消息即可。&lt;/li&gt;&#xA;&lt;li&gt;缺点：写很重，如果这个消息是一条群消息，那么一个群成员发送出去的消息将拷贝到所有其余群成员的收件箱中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-push&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-push.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg push &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;读扩散pull&#34;&gt;&#xA;  读扩散（pull）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%89%a9%e6%95%a3pull&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;读扩散就是pull模式，用户每次到消息发送者的发件箱去拉取消息，优缺点如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：写优化，每次发送的消息只需要写到一个地方，由收件者自己去拉取消息即可。&lt;/li&gt;&#xA;&lt;li&gt;缺点：读操作很重，假设一个用户有一千个好友，重新登录时需要拉取这些好友所有的离线消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-pull&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-pull.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg pull &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;最终选择的是以pull模式为主的模式，理由在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IM业务属于『写多读少』类型的业务，如果使用push模式，将造成消息的大量冗余。&lt;/li&gt;&#xA;&lt;li&gt;pull模式读操作较重的缺陷可以通过其他方式来优化解决。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面来看具体的设计。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;表设计&#34;&gt;&#xA;  表设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a1%a8%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在数据库设计中，仅使用一个发送消息表来存储消息的具体内容，而另外有一个消息接收表用来存储消息的ID信息而不是具体内容，这样用户查询消息时，大体流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先拉取接收消息表中的信息。&lt;/li&gt;&#xA;&lt;li&gt;根据接收消息表中的ID以及发送者ID信息到发送信息表来具体查询消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;用户发送消息表&#34;&gt;&#xA;  用户发送消息表&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%94%a8%e6%88%b7%e5%8f%91%e9%80%81%e6%b6%88%e6%81%af%e8%a1%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;无论是单聊还是群聊消息，都使用这个表来存储发送出去的消息。&lt;/p&gt;&#xA;&lt;p&gt;im_message_send（msg_id,msg_from,msg_to,msg_seq,msg_content,send_time,msg_type）&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;msg_id：消息ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_from：消息发送者UID。&lt;/li&gt;&#xA;&lt;li&gt;msg_to：消息接收者。如果是单聊消息那么就是用户UID，如果是群聊消息就是群ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_seq：客户端发送消息时带上的序列号，主要用于消息排重以及通知客户端消息发送成功之用。&lt;/li&gt;&#xA;&lt;li&gt;msg_content：消息内容。&lt;/li&gt;&#xA;&lt;li&gt;send_time：消息发送时间。&lt;/li&gt;&#xA;&lt;li&gt;msg_type：消息类型，如单聊、群聊消息等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;用户接收消息表&#34;&gt;&#xA;  用户接收消息表&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%94%a8%e6%88%b7%e6%8e%a5%e6%94%b6%e6%b6%88%e6%81%af%e8%a1%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;im_message_recieve（id,msg_from,msg_to,msg_id,flag）&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;id：这个表的ID，自增。&lt;/li&gt;&#xA;&lt;li&gt;msg_from：消息发送者ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_to：消息接收者ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_id：消息ID，对应发送消息表中的ID。&lt;/li&gt;&#xA;&lt;li&gt;flag：标志位，表示该消息是否已读。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;接收消息表的信息并没有很多，因为主体部分如消息内容、发送消息时间等都在发送消息表中。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-table&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-table.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg table &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分库分表及访问策略&#34;&gt;&#xA;  分库分表及访问策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e5%ba%93%e5%88%86%e8%a1%a8%e5%8f%8a%e8%ae%bf%e9%97%ae%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;发送消息表，根据msg_from字段做为分库分表的依据，而接收消息表则使用msg_to字段做为分库分表的依据。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-基础</title>
      <link>https://www.codedump.info/zh/post/20190608-im-design-base/</link>
      <pubDate>Sat, 08 Jun 2019 11:09:10 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190608-im-design-base/</guid>
      <description>&lt;p&gt;IM做为非常经典的服务器系统，其设计时候的考量具备代表性，所以这一次花几个篇幅讨论其相关设计。&lt;/p&gt;&#xA;&lt;p&gt;主要内容相当部分参考了 &lt;a href=&#34;http://www.52im.net/thread-812-1-1.html&#34;&gt;一套海量在线用户的移动端IM架构设计实践分享&lt;/a&gt;一文，在此之上补充了更好的消息存储设计以及集群设计。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;整体架构&#34;&gt;&#xA;  整体架构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b4%e4%bd%93%e6%9e%b6%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-arch&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im arch &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上架构图中，分为几个部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端：支持IOS、Android系统。&lt;/li&gt;&#xA;&lt;li&gt;接入层：负责维护与客户端之间的长连接。&lt;/li&gt;&#xA;&lt;li&gt;逻辑层：负责IM系统中各逻辑功能的实现。&lt;/li&gt;&#xA;&lt;li&gt;存储层：存储IM系统相关的数据，主要包括Redis缓存系统（用于保存用户状态及路由数据）、消息数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上图中几部分的交互如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端通过gate接入IM服务器。在这里，客户端与gate之间保持TCP长连接，客户端使用DNS查询域名返回最近的gate地址进行连接。&lt;/li&gt;&#xA;&lt;li&gt;Gate的作用：保持与客户端之间的长连接，将请求数据转发给后面的逻辑服务LogicServer。LogicServer最上面是一个消息路由服务Router，根据请求的类型转发到后面具体的逻辑服务器。其中c代表客户端，s代表服务器，g代表群组，因此比如c2c服务就是处理客户端之间消息的服务器，而auth服务是处理客户端登录请求的服务器。&lt;/li&gt;&#xA;&lt;li&gt;逻辑类服务器与存储层服务打交道，其中：redis用于存储用户在线状态、用户路由数据（用户路由数据就是指用户在哪个gate服务上维护长连接），而DB用于存储用户的消息数据，这部分留待下一部分讲解。&lt;/li&gt;&#xA;&lt;li&gt;以上的接入层、逻辑层由于本身不存储状态，因此都可以进行横向扩展。看似Gate维护着长连接，但是即使一个Gate宕机，客户端检测到之后可以重新发起请求接入另一台Gate服务器。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;数据存储&#34;&gt;&#xA;  数据存储&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e5%ad%98%e5%82%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;路由数据：存放在Redis中，格式为（UID,客户端在哪个gate登录）。&lt;/li&gt;&#xA;&lt;li&gt;消息数据：存储在DB中，部分也会缓存在缓存中方便查询，这部分做为下一部分文章的重点来讲解，不在这部分展开讨论。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;核心交互流程&#34;&gt;&#xA;  核心交互流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a0%b8%e5%bf%83%e4%ba%a4%e4%ba%92%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;统一登录系统&#34;&gt;&#xA;  统一登录系统&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bb%9f%e4%b8%80%e7%99%bb%e5%bd%95%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;登录授权auth&#34;&gt;&#xA;  登录授权（auth）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%99%bb%e5%bd%95%e6%8e%88%e6%9d%83auth&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-login&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-login.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im login &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;客户端通过统一登录系统验证登录密码等。&lt;/li&gt;&#xA;&lt;li&gt;SSO验证客户端用户名密码之后，生成登录token并返回给客户端。&lt;/li&gt;&#xA;&lt;li&gt;客户端使用UID和返回的token向gate发起授权验证请求。&lt;/li&gt;&#xA;&lt;li&gt;gate同步调用logic server的验证接口。&lt;/li&gt;&#xA;&lt;li&gt;logic server请求SSO系统验证token合法性。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;SSO向auth系统返回验证token结果。&lt;/li&gt;&#xA;&lt;li&gt;如果验证成功，auth系统在redis中存储客户端的路由信息，即客户端在哪个gate上登录。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;ol start=&#34;6&#34;&gt;&#xA;&lt;li&gt;auth系统向gate返回验证登录结果。&lt;/li&gt;&#xA;&lt;li&gt;gate向客户端返回授权结果。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;登出logout&#34;&gt;&#xA;  登出（logout）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%99%bb%e5%87%balogout&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-logout&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-logout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im logout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;客户端向gate发出logout请求。&lt;/li&gt;&#xA;&lt;li&gt;gate设置客户端UID对应的peer无效，然后应答客户端登出成功。&lt;/li&gt;&#xA;&lt;li&gt;gate向logic server发出登录请求。&lt;/li&gt;&#xA;&lt;li&gt;处理该类请求的c2s服务器，清除redis中的客户端路由信息。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;踢人kickout&#34;&gt;&#xA;  踢人（kickout）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b8%a2%e4%ba%bakickout&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;用户请求授权时，可能在另一个设备（同类型设备，比如一台苹果手机登录时发现一台安卓手机也在登录这个账号）开着软件处于登录状态。这种情况需要系统将那个设备踢下线。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-kickout&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-kickout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im kickout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;新的客户端登陆流程同上面的登陆认证流程，只不过在auth模块完成认证之后，会做如下的操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;根据UID到redis中查询路由数据，如果不存在说明前面没有登陆过，那么就像登陆流程一样返回即可。&lt;/li&gt;&#xA;&lt;li&gt;否则说明前面已经有其他设备登陆了，将向前面的gate发送踢人请求，然后保存新的路由信息到redis中。&lt;/li&gt;&#xA;&lt;li&gt;gate接收到踢人请求，踢掉客户端之后断掉与客户端的连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端上报消息c2s消息&#34;&gt;&#xA;  客户端上报消息（c2s消息）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e4%b8%8a%e6%8a%a5%e6%b6%88%e6%81%afc2s%e6%b6%88%e6%81%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-c2smsg&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-c2smsg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im c2s msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
