<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Netty on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/netty/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 20 Feb 2021 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/netty/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《Netty 源码剖析与应用》详细总结：写给从未接触过 Netty 的人</title>
      <link>https://ftmao.com/blog/bookstore/netty-source-code-analysis-notes/</link>
      <pubDate>Sat, 20 Feb 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/netty-source-code-analysis-notes/</guid>
      <description>&lt;h1 id=&#34;netty-源码剖析与应用详细总结写给从未接触过-netty-的人&#34;&gt;《Netty 源码剖析与应用》详细总结：写给从未接触过 Netty 的人&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;参考书籍：刘耀林《Netty 源码剖析与应用》（电子工业出版社，2020 年 11 月）
目标读者：会 Java、听说过 Netty 但没系统学过，想一篇文章建立 Netty 核心知识框架的人。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;这本书讲什么为什么学-netty&#34;&gt;〇、这本书讲什么？为什么学 Netty？&lt;/h2&gt;
&lt;p&gt;普通开发在工作中很少直接碰 Netty，但只要翻开任何一个主流分布式框架的底层源码，通信模块几乎都是它：&lt;strong&gt;Dubbo、gRPC、Spark、Flink、Elasticsearch、HBase&lt;/strong&gt;（HBase 从 2.0 开始默认用 Netty RPC Server 替代原生实现）。不管是做 Java Web 后台还是大数据，Netty 都是绕不过去的地基。&lt;/p&gt;
&lt;p&gt;为什么不直接用 JDK 的 NIO？作者的回答很直白：&lt;strong&gt;用原生 NIO 写一套高效稳定的通信模块非常难&lt;/strong&gt;——要处理多路复用、粘包拆包、线程模型、内存管理、断线重连等一堆坑。Netty 把这些全部封装好，让普通人也能快速开发高性能、面向协议的服务端和客户端。&lt;/p&gt;
&lt;p&gt;全书的结构是循序渐进的四部分：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;第一部分：实战入门&amp;lt;br/&amp;gt;第1章 Netty基础篇&amp;lt;br/&amp;gt;第2章 原理部分&amp;lt;br/&amp;gt;第3章 分布式RPC&amp;#34;] --&amp;gt; B[&amp;#34;第二部分：核心源码剖析&amp;lt;br/&amp;gt;第4章 核心组件&amp;lt;br/&amp;gt;（NioEventLoop/Channel/ByteBuf）&amp;lt;br/&amp;gt;第5章 读/写请求源码&amp;#34;]
    B --&amp;gt; C[&amp;#34;第三部分：内存管理&amp;lt;br/&amp;gt;第6章 jemalloc内存池&amp;lt;br/&amp;gt;（全书最难）&amp;#34;]
    C --&amp;gt; D[&amp;#34;第四部分：高级应用与调优&amp;lt;br/&amp;gt;第7章 时间轮高级应用&amp;lt;br/&amp;gt;第8章 问题分析与性能调优&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两个贯穿全书的高级项目：&lt;strong&gt;一套类似 Dubbo 的分布式 RPC 框架&lt;/strong&gt;（第 3 章写、第 8 章压测调优），以及&lt;strong&gt;一套基于时间轮的 10 亿级任务调度监控引擎&lt;/strong&gt;（第 7 章）。&lt;/p&gt;
&lt;h2 id=&#34;一netty-基础篇先看长连接通信长什么样第-1-章&#34;&gt;一、Netty 基础篇：先看长连接通信长什么样（第 1 章）&lt;/h2&gt;
&lt;h3 id=&#34;11-服务端五步法&#34;&gt;1.1 服务端五步法&lt;/h3&gt;
&lt;p&gt;Netty 服务端程序的固定套路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建两个线程组：&lt;strong&gt;Boss 线程组&lt;/strong&gt;专门接收客户端连接；&lt;strong&gt;Worker 线程组&lt;/strong&gt;处理已接收连接的 I/O 读写；&lt;/li&gt;
&lt;li&gt;用启动辅助类 &lt;code&gt;ServerBootstrap&lt;/code&gt; 配置启动参数（如 &lt;code&gt;ChannelOption.SO_RCVBUF&lt;/code&gt; / &lt;code&gt;SO_SNDBUF&lt;/code&gt; 设置接收/发送缓冲区大小）；&lt;/li&gt;
&lt;li&gt;Boss 把接收到的连接注册到 Worker 后，交给 &lt;code&gt;ChannelInitializer&lt;/code&gt; 初始化 Handler 链；&lt;/li&gt;
&lt;li&gt;编写业务 Handler（继承 &lt;code&gt;ChannelInboundHandlerAdapter&lt;/code&gt;，重写 &lt;code&gt;channelRead&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;绑定端口——绑定由 Boss 线程完成，所以主线程要执行同步阻塞方法等待绑定完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是经典的&lt;strong&gt;主从 Reactor 模型&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph BossGroup[&amp;#34;Boss 线程组（接收连接）&amp;#34;]
        B1[&amp;#34;Boss NioEventLoop&amp;lt;br/&amp;gt;监听 OP_ACCEPT&amp;#34;]
    end
    subgraph WorkerGroup[&amp;#34;Worker 线程组（处理 I/O）&amp;#34;]
        W1[&amp;#34;Worker 线程 1&amp;lt;br/&amp;gt;管理多个 Channel&amp;#34;]
        W2[&amp;#34;Worker 线程 2&amp;lt;br/&amp;gt;管理多个 Channel&amp;#34;]
    end
    C1[&amp;#34;客户端连接 1&amp;#34;] --&amp;gt; B1
    C2[&amp;#34;客户端连接 2&amp;#34;] --&amp;gt; B1
    C3[&amp;#34;客户端连接 3&amp;#34;] --&amp;gt; B1
    B1 --&amp;gt;|&amp;#34;注册 SocketChannel&amp;#34;| W1
    B1 --&amp;gt;|&amp;#34;注册 SocketChannel&amp;#34;| W2
    W1 --&amp;gt; P1[&amp;#34;ChannelPipeline&amp;lt;br/&amp;gt;Decoder → 业务Handler → Encoder&amp;#34;]
    W2 --&amp;gt; P2[&amp;#34;ChannelPipeline&amp;lt;br/&amp;gt;Decoder → 业务Handler → Encoder&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;12-一个值得琢磨的示例客户端怎么同步等异步响应&#34;&gt;1.2 一个值得琢磨的示例：客户端怎么&amp;quot;同步等&amp;quot;异步响应&lt;/h3&gt;
&lt;p&gt;第 1 章的客户端示例藏着一个 Netty 编程的核心范式。客户端连续发 100 个请求，服务端异步返回。问题是：&lt;strong&gt;&lt;code&gt;channel.writeAndFlush()&lt;/code&gt; 是异步的，主线程怎么拿到响应结果？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;书里的解法是自实现一套 &lt;code&gt;RequestFuture&lt;/code&gt; 机制（这正是 Dubbo 等 RPC 框架的原型）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个请求带唯一 id，发送前把 &lt;code&gt;RequestFuture&lt;/code&gt; 存入以 id 为 key 的 Map，主线程在 &lt;code&gt;future.get()&lt;/code&gt; 上阻塞等待；&lt;/li&gt;
&lt;li&gt;服务端 Handler 处理完后，把结果&lt;strong&gt;带上请求 id&lt;/strong&gt; 写回；&lt;/li&gt;
&lt;li&gt;客户端 &lt;code&gt;channelRead&lt;/code&gt; 收到响应，按 id 找到对应的 Future，设置结果并唤醒主线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;书中的思考题也很有意思：示例里客户端输出是有序的（按 id 逐个 get），&lt;strong&gt;如何修改代码让输出无序？&lt;/strong&gt;——答案是不再按顺序 get，而是在响应到达的回调里直接输出。这个小实验能帮你真正理解&amp;quot;异步 + 回调&amp;quot;与&amp;quot;同步等待&amp;quot;的差别。&lt;/p&gt;
&lt;h2 id=&#34;二原理部分绕不开的六块基石第-2-章&#34;&gt;二、原理部分：绕不开的六块基石（第 2 章）&lt;/h2&gt;
&lt;h3 id=&#34;21-多路复用器selectpollepoll&#34;&gt;2.1 多路复用器：select、poll、epoll&lt;/h3&gt;
&lt;p&gt;BIO 时代每个 Socket 连接要一条线程同步阻塞，1 万连接就要 1 万条线程，根本扛不住。NIO 的**多路复用器（Selector）**让一条线程可以管理成千上万个连接的接入和读写事件。&lt;/p&gt;
&lt;p&gt;三种经典模型的区别（面试高频）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模型&lt;/th&gt;
					&lt;th&gt;遍历方式&lt;/th&gt;
					&lt;th&gt;连接数上限&lt;/th&gt;
					&lt;th&gt;触发方式&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;select&lt;/td&gt;
					&lt;td&gt;轮询全部 fd&lt;/td&gt;
					&lt;td&gt;1024 左右&lt;/td&gt;
					&lt;td&gt;水平触发&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;poll&lt;/td&gt;
					&lt;td&gt;轮询全部 fd&lt;/td&gt;
					&lt;td&gt;无硬上限&lt;/td&gt;
					&lt;td&gt;水平触发&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;epoll&lt;/td&gt;
					&lt;td&gt;回调就绪列表，只处理活跃连接&lt;/td&gt;
					&lt;td&gt;无硬上限&lt;/td&gt;
					&lt;td&gt;支持边缘触发&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个容易混淆的点：&lt;strong&gt;JDK 自带的 epoll 封装是水平触发（LT）的，而 Netty 通过 JNI 自己重写的 epoll 是边缘触发（ET）的&lt;/strong&gt;，这也是 Netty 在 Linux 上推荐 EpollEventLoop 的原因之一。&lt;/p&gt;
&lt;h3 id=&#34;22-netty-的线程模型&#34;&gt;2.2 Netty 的线程模型&lt;/h3&gt;
&lt;p&gt;Netty 的 Boss/Worker 默认线程数与 CPU 核数挂钩（默认 核数 × 2）。Boss 只关心 &lt;code&gt;OP_ACCEPT&lt;/code&gt;，接收连接后把 &lt;code&gt;SocketChannel&lt;/code&gt; 按策略轮询注册到某个 Worker 的 Selector 上。关键约束：&lt;strong&gt;一个 Channel 在其生命周期内只绑定一个 EventLoop 线程&lt;/strong&gt;，所以处理 Channel 的所有事件都不需要加锁——这是 Netty 高并发能力的重要来源。&lt;/p&gt;
&lt;h3 id=&#34;23-编码和解码tcp-粘包半包问题&#34;&gt;2.3 编码和解码：TCP 粘包/半包问题&lt;/h3&gt;
&lt;p&gt;TCP 是面向字节流的协议，它把数据看成无结构的字节流，根本不知道&amp;quot;一条消息&amp;quot;的边界在哪。于是产生了经典问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;粘包&lt;/strong&gt;：两条消息被合并成一次发送/接收；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;半包（拆包）&lt;/strong&gt;：一条消息被拆成多次接收。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 发送方
        A[&amp;#34;业务对象&amp;#34;] --&amp;gt; B[&amp;#34;编码器 Encoder&amp;lt;br/&amp;gt;对象 → 字节&amp;#34;]
        B --&amp;gt; C[&amp;#34;TCP 输出缓冲区&amp;lt;br/&amp;gt;SO_SNDBUF&amp;#34;]
    end
    C --&amp;gt;|&amp;#34;② TCP 网络传输&amp;lt;br/&amp;gt;（受滑动窗口/拥塞控制影响）&amp;#34;| D[&amp;#34;TCP 输入缓冲区&amp;lt;br/&amp;gt;SO_RCVBUF&amp;#34;]
    subgraph 接收方
        D --&amp;gt; E[&amp;#34;解码器 Decoder&amp;lt;br/&amp;gt;处理粘包/半包&amp;#34;]
        E --&amp;gt; F[&amp;#34;业务对象&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Netty 的解法是在 Pipeline 里插&lt;strong&gt;解码器&lt;/strong&gt;：固定长度解码器、分隔符解码器、&lt;strong&gt;长度字段解码器（LengthFieldBasedFrameDecoder，最常用）&lt;/strong&gt;——协议头里放一个长度字段，解码器攒够一条完整消息才往下游传。书中第 3 章手写 RPC 时就是用自定义协议（协议头 + 长度 + 内容）解决的。&lt;/p&gt;
&lt;h3 id=&#34;24-序列化protobuf-与-kryo&#34;&gt;2.4 序列化：Protobuf 与 Kryo&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Protobuf&lt;/strong&gt;（Google）：跨语言、压缩率高（相同数据比 JSON 小 3~5 倍）、需要预定义 schema，适合 RPC 服务间通信；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kryo&lt;/strong&gt;：专注 Java，使用简单、速度快，但不跨语言，适合 Java 生态内部。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;25-零拷贝少搬一次数据是一次&#34;&gt;2.5 零拷贝：少搬一次数据是一次&lt;/h3&gt;
&lt;p&gt;零拷贝指操作数据时无须把 Buffer 从一个内存区域拷贝到另一个区域。Netty 里主要有三种场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;堆外直接内存收发&lt;/strong&gt;：用堆内存做 Socket 读写时，JVM 会先把堆内数据拷贝到堆外再写入 Socket；Netty 收发直接使用堆外内存，省掉这次拷贝；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CompositeByteBuf 组合视图&lt;/strong&gt;：一条消息被拆成多个数据包时，把多个 Buffer 逻辑上组合成一个（类似数据库视图），而不是物理拷贝拼接；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FileChannel.transferTo()&lt;/strong&gt;：文件传输直接在内核空间完成，不经用户态缓冲区。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;26-背压backpressure让快生产者等一等慢消费者&#34;&gt;2.6 背压（Backpressure）：让快生产者等一等慢消费者&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 生产者
        P1[&amp;#34;消息生产者 A&amp;lt;br/&amp;gt;写入缓存队列&amp;#34;]
        P2[&amp;#34;消息生产者 B&amp;lt;br/&amp;gt;写入缓存队列&amp;#34;]
    end
    P1 &amp;amp; P2 --&amp;gt; Q[&amp;#34;TCP 链路&amp;#34;]
    Q --&amp;gt;|&amp;#34;背压&amp;#34;| C[&amp;#34;消费者&amp;lt;br/&amp;gt;I/O 读取放入缓存队列&amp;#34;]
    C -.-&amp;gt;|&amp;#34;消费速率 &amp;lt; 生产速率时&amp;lt;br/&amp;gt;TCP 窗口缩小 → 通道阻塞&amp;lt;br/&amp;gt;→ 生产者降速&amp;#34;| P1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;机制链条：消费者消费变慢 → TCP 接收缓冲区满 → &lt;strong&gt;TCP 滑动窗口&lt;/strong&gt;变小 → 生产者发送缓冲区写不进去 → 生产者降速。书中还以 &lt;strong&gt;Flink 为例&lt;/strong&gt;讲了它的应用：Flink 基于 Netty 实现了 credit-based（基于信用额度）的反压机制，下游告诉上游&amp;quot;我还能收多少&amp;quot;，从根本上防止快生产慢消费把系统压垮。&lt;/p&gt;
&lt;h2 id=&#34;三分布式-rpc把-netty-组装成一个-dubbo第-3-章&#34;&gt;三、分布式 RPC：把 Netty 组装成一个 Dubbo（第 3 章）&lt;/h2&gt;
&lt;p&gt;这一章是全书第一个大项目：手写一套分布式 RPC。目标是业务方用起来像 SpringMVC/Dubbo 一样——引个 jar 包就行，不用关心 Netty 底层；服务端可动态扩容，不用写死 IP 端口。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 客户端
        A[&amp;#34;业务代码&amp;lt;br/&amp;gt;像调用本地方法一样调用&amp;#34;] --&amp;gt; B[&amp;#34;动态代理&amp;lt;br/&amp;gt;拦截方法调用&amp;#34;]
        B --&amp;gt; C[&amp;#34;协议封装&amp;lt;br/&amp;gt;类名/方法名/参数 + 请求id&amp;#34;]
    end
    subgraph 网络层
        C --&amp;gt; D[&amp;#34;Netty 长连接&amp;lt;br/&amp;gt;自定义协议 + Kryo 序列化&amp;#34;]
    end
    subgraph 服务端
        D --&amp;gt; E[&amp;#34;Netty Server Handler&amp;lt;br/&amp;gt;解码出调用信息&amp;#34;]
        E --&amp;gt; F[&amp;#34;Java 反射&amp;lt;br/&amp;gt;调用 Spring 容器中的实现类&amp;#34;]
    end
    subgraph 注册中心[&amp;#34;Zookeeper 注册中心&amp;#34;]
        G[&amp;#34;服务启动时注册&amp;lt;br/&amp;gt;IP/端口/权重&amp;#34;]
        H[&amp;#34;客户端订阅服务列表&amp;lt;br/&amp;gt;负载均衡选择节点&amp;#34;]
    end
    F --&amp;gt; G
    C --&amp;gt; H&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个关键技术点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议设计&lt;/strong&gt;：自定义协议头（魔数 + 长度 + 请求 id）+ 序列化内容，配合 LengthField 解码器解决粘包；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java 反射解耦&lt;/strong&gt;：服务端 Handler 解析出&amp;quot;类名 + 方法名 + 参数类型 + 参数值&amp;quot;，从 Spring 容器找到实现类反射调用——业务代码和 Netty 的 Handler 逻辑完全解耦；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务注册与发现&lt;/strong&gt;：引入 Zookeeper（对比了 Consul/ETCD/Eureka），服务端启动注册临时节点，客户端监听节点变化做动态感知和负载均衡（可按权重）。这一层替代了传统 Nginx/LVS 的硬负载方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;四核心组件源码剖析第-4-章全书的硬核部分&#34;&gt;四、核心组件源码剖析（第 4 章，全书的硬核部分）&lt;/h2&gt;
&lt;h3 id=&#34;41-nioeventloopgroup-与-nioeventloopnetty-的发动机&#34;&gt;4.1 NioEventLoopGroup 与 NioEventLoop：Netty 的发动机&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;NioEventLoopGroup&lt;/code&gt; 就是线程池，&lt;code&gt;NioEventLoop&lt;/code&gt; 是里面的每条线程，&lt;strong&gt;每条线程一个 Selector，绑定一堆 Channel&lt;/strong&gt;。核心在它的 &lt;code&gt;run()&lt;/code&gt; 方法——一个永不退出的循环：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;run() 死循环开始&amp;#34;] --&amp;gt; B[&amp;#34;select() 轮询 I/O 事件&amp;lt;br/&amp;gt;（带超时，防止空轮询 bug）&amp;#34;]
    B --&amp;gt; C{&amp;#34;有就绪事件？&amp;#34;}
    C -- 是 --&amp;gt; D[&amp;#34;处理 SelectionKey&amp;lt;br/&amp;gt;OP_ACCEPT / OP_READ / OP_WRITE&amp;#34;]
    C -- 否 --&amp;gt; E[&amp;#34;跳过 I/O 处理&amp;#34;]
    D --&amp;gt; F[&amp;#34;执行异步任务队列&amp;lt;br/&amp;gt;（用户提交的任务 + 定时任务）&amp;#34;]
    E --&amp;gt; F
    F --&amp;gt;|&amp;#34;ioRatio 控制 I/O 与任务的时间配比&amp;lt;br/&amp;gt;默认各 50%&amp;#34;| A
    B -.-&amp;gt;|&amp;#34;连续空轮询超过阈值&amp;lt;br/&amp;gt;→ rebuildSelector() 重建 Selector&amp;lt;br/&amp;gt;（规避 JDK epoll 空轮询 bug）&amp;#34;| B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;源码里的几个精妙优化，作者都点了出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SelectedSelectionKeySet&lt;/strong&gt;：用数组替换 JDK Selector 内部的 HashSet，遍历就绪事件从哈希遍历变成数组遍历，更快；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selector 重建&lt;/strong&gt;：JDK epoll 有著名的&amp;quot;空轮询 bug&amp;quot;（select 立刻返回但无事件，CPU 100%），Netty 的解法是统计空轮询次数，超阈值就新建一个 Selector 把所有 Channel 迁过去；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ioRatio&lt;/strong&gt;：I/O 处理与异步任务执行的时间比例可调，默认 50:50。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-channel-的继承体系每层抽象都有明确职责&#34;&gt;4.2 Channel 的继承体系：每层抽象都有明确职责&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Channel 接口&amp;lt;br/&amp;gt;id/eventLoop/pipeline/config&amp;#34;] --&amp;gt; B[&amp;#34;AbstractChannel&amp;lt;br/&amp;gt;模板方法：bind/connect/read/write&amp;lt;br/&amp;gt;持有 Unsafe 和 Pipeline&amp;#34;]
    B --&amp;gt; C[&amp;#34;AbstractNioChannel&amp;lt;br/&amp;gt;对接 NIO：SelectionKey&amp;lt;br/&amp;gt;interestOps/readInterestOp&amp;#34;]
    C --&amp;gt; D[&amp;#34;AbstractNioByteChannel&amp;lt;br/&amp;gt;字节流读写（TCP 数据通道）&amp;#34;]
    C --&amp;gt; E[&amp;#34;AbstractNioMessageChannel&amp;lt;br/&amp;gt;消息读写（接收连接用）&amp;#34;]
    D --&amp;gt; F[&amp;#34;NioSocketChannel&amp;lt;br/&amp;gt;客户端连接通道&amp;lt;br/&amp;gt;doRead/doWrite 具体实现&amp;#34;]
    E --&amp;gt; G[&amp;#34;NioServerSocketChannel&amp;lt;br/&amp;gt;服务端监听通道&amp;lt;br/&amp;gt;doReadMessages = accept 新连接&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;读源码时最值得体会的设计：I/O 操作全部收敛到内部类 &lt;strong&gt;Unsafe&lt;/strong&gt; 里，把&amp;quot;Netty 的 Channel API&amp;quot;和&amp;quot;JDK 底层的 Socket 操作&amp;quot;隔离开；&lt;code&gt;doXxx()&lt;/code&gt; 抽象方法留给子类实现，&lt;code&gt;AbstractChannel&lt;/code&gt; 用模板方法串起流程。书中还点名了 Netty 的性能优化手段：JDK 的 &lt;code&gt;Unsafe&lt;/code&gt;、乐观锁、对象池等。&lt;/p&gt;
&lt;h3 id=&#34;43-bytebuf比-nio-bytebuffer-好用得多的缓冲区&#34;&gt;4.3 ByteBuf：比 NIO ByteBuffer 好用得多的缓冲区&lt;/h3&gt;
&lt;p&gt;JDK 的 &lt;code&gt;ByteBuffer&lt;/code&gt; 只有一个 position 指针，读写模式要 &lt;code&gt;flip()&lt;/code&gt; 来回切换，极易出错。Netty 的 &lt;code&gt;ByteBuf&lt;/code&gt; 用&lt;strong&gt;读写分离的双指针&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph ByteBuf[&amp;#34;ByteBuf 内部结构&amp;#34;]
        A[&amp;#34;[0, readerIndex)&amp;lt;br/&amp;gt;已读区域（可丢弃）&amp;#34;] --&amp;gt; B[&amp;#34;[readerIndex, writerIndex)&amp;lt;br/&amp;gt;可读区域 readableBytes&amp;#34;]
        B --&amp;gt; C[&amp;#34;[writerIndex, capacity)&amp;lt;br/&amp;gt;可写区域 writableBytes&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;继承体系（每层干一件事）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AbstractByteBuf&lt;/code&gt;：读写索引维护、边界检查；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AbstractReferenceCountedByteBuf&lt;/code&gt;：&lt;strong&gt;引用计数&lt;/strong&gt;（&lt;code&gt;retain()&lt;/code&gt;/&lt;code&gt;release()&lt;/code&gt;），配合 &lt;code&gt;ReferenceCountUpdater&lt;/code&gt; 用 CAS 乐观锁更新计数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CompositeByteBuf&lt;/code&gt;：多个 Buffer 的逻辑组合（零拷贝场景 2）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PooledByteBuf&lt;/code&gt;：池化缓冲区，用完归还对象池复用，是第 6 章内存管理的入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-内存泄漏检测弱引用--采样的巧思&#34;&gt;4.4 内存泄漏检测：弱引用 + 采样的巧思&lt;/h3&gt;
&lt;p&gt;堆外内存不归 GC 管，忘了 &lt;code&gt;release()&lt;/code&gt; 就泄漏。Netty 的 &lt;code&gt;ResourceLeakDetector&lt;/code&gt; 机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个 ByteBuf 创建时（按级别&lt;strong&gt;采样&lt;/strong&gt;，不是全量——开销可控）包装成弱引用/虚引用，记录创建时的调用栈；&lt;/li&gt;
&lt;li&gt;ByteBuf 被 GC 时，若引用计数没归零，说明泄漏了；&lt;/li&gt;
&lt;li&gt;从引用队列中捞出泄漏记录，打印带调用栈的告警日志。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个机制本身就是一节生动的 JVM 引用类型教学课——书中还配了弱引用的 JUnit 测试用例帮助理解。&lt;/p&gt;
&lt;h2 id=&#34;五读写请求源码剖析一次-io-的完整旅程第-5-章&#34;&gt;五、读/写请求源码剖析：一次 I/O 的完整旅程（第 5 章）&lt;/h2&gt;
&lt;h3 id=&#34;51-serverbootstrap-启动过程&#34;&gt;5.1 ServerBootstrap 启动过程&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bind()&lt;/code&gt; 背后是：反射创建 &lt;code&gt;NioServerSocketChannel&lt;/code&gt; → 初始化 Pipeline（插入 &lt;code&gt;ServerBootstrapAcceptor&lt;/code&gt;）→ 注册到 Boss EventLoop → 绑定端口 → 监听 &lt;code&gt;OP_ACCEPT&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-接入与读写两个-unsafe-的分工&#34;&gt;5.2 接入与读写：两个 Unsafe 的分工&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant Client as 客户端
    participant Boss as Boss线程(NioMessageUnsafe)
    participant Acceptor as ServerBootstrapAcceptor
    participant Worker as Worker线程(NioByteUnsafe)
    participant Handler as 业务Handler链

    Client-&amp;gt;&amp;gt;Boss: 发起 TCP 连接
    Boss-&amp;gt;&amp;gt;Boss: 监听到 OP_ACCEPT&amp;lt;br/&amp;gt;accept 出 SocketChannel
    Boss-&amp;gt;&amp;gt;Acceptor: fireChannelRead(新连接)
    Acceptor-&amp;gt;&amp;gt;Worker: 设置 options/attrs&amp;lt;br/&amp;gt;注册到 Worker 的 Selector
    Client-&amp;gt;&amp;gt;Worker: 发送数据
    Worker-&amp;gt;&amp;gt;Worker: 监听到 OP_READ&amp;lt;br/&amp;gt;分配 ByteBuf 循环读
    Worker-&amp;gt;&amp;gt;Handler: fireChannelRead(msg)&amp;lt;br/&amp;gt;沿 Pipeline 传播&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Boss 侧用 &lt;strong&gt;NioMessageUnsafe&lt;/strong&gt;（读出来的是&amp;quot;一条消息&amp;quot;= 一个新连接）；Worker 侧用 &lt;strong&gt;NioByteUnsafe&lt;/strong&gt;（读出来的是字节流）——这就是第 4 章两个 AbstractNio 子类分工的实际体现；&lt;/li&gt;
&lt;li&gt;OP_READ 是&lt;strong&gt;循环读&lt;/strong&gt;：一次事件里反复读直到读空，每轮用 &lt;code&gt;RecvByteBufAllocator&lt;/code&gt; 预测该分配多大的缓冲区（第 6 章细讲）；&lt;/li&gt;
&lt;li&gt;写操作先进 &lt;strong&gt;ChannelOutboundBuffer&lt;/strong&gt; 链表（Entry 节点用完回收进对象池），&lt;code&gt;flush0()&lt;/code&gt; 时交给 &lt;code&gt;NioSocketChannel.doWrite()&lt;/code&gt; 批量写（合并成 &lt;code&gt;NIO_BUFFERS&lt;/code&gt; 数组一次 writev），写不完（TCP 缓冲区满）则注册 &lt;code&gt;OP_WRITE&lt;/code&gt; 等可写时再续写——这就是&lt;strong&gt;写半包&lt;/strong&gt;的处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;六内存管理jemalloc-思想在-java-的落地第-6-章最难也最值&#34;&gt;六、内存管理：jemalloc 思想在 Java 的落地（第 6 章，最难也最值）&lt;/h2&gt;
&lt;p&gt;为什么 Netty 要自己管内存？堆外内存的 &lt;code&gt;malloc/free&lt;/code&gt; 系统调用很贵，频繁申请释放是性能杀手。Netty 把 &lt;strong&gt;jemalloc&lt;/strong&gt; 的分配思想搬到了 Java 里，三层结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 三层结构[&amp;#34;内存分配的三层结构&amp;#34;]
        A[&amp;#34;① PoolThreadCache&amp;lt;br/&amp;gt;本地线程缓存&amp;lt;br/&amp;gt;每个 I/O 线程一个，免锁&amp;#34;]
        B[&amp;#34;② PoolArena&amp;lt;br/&amp;gt;分配区（arena）&amp;lt;br/&amp;gt;管理一组 PoolChunk&amp;#34;]
        C[&amp;#34;③ 系统内存&amp;lt;br/&amp;gt;堆外/堆内直接申请&amp;#34;]
    end
    A --&amp;gt;|&amp;#34;缓存命中就直接用&amp;lt;br/&amp;gt;不命中向下申请&amp;#34;| B
    B --&amp;gt;|&amp;#34;没有可用 chunk 才申请&amp;#34;| C
    D[&amp;#34;PoolChunk（默认 16MB）&amp;lt;br/&amp;gt;= 2048 个 page（8KB）&amp;#34;] -.属于.- B
    E[&amp;#34;PoolSubpage&amp;lt;br/&amp;gt;把 page 再切成 tiny/small 小块&amp;#34;] -.切分.- D&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;分配策略（申请一块内存时）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先查当前线程的 &lt;code&gt;PoolThreadCache&lt;/code&gt;，命中直接返回——&lt;strong&gt;无线程竞争，无锁&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;不命中则找 &lt;code&gt;PoolArena&lt;/code&gt; 分配：大于等于 8KB 走 &lt;code&gt;PoolChunk&lt;/code&gt; 按 page 分配；小于 8KB 走 &lt;code&gt;PoolSubpage&lt;/code&gt; 把一个 page 切成更小的块（tiny/small 规格）；&lt;/li&gt;
&lt;li&gt;Arena 也分不到，才向系统申请新 chunk（默认 16MB），用完放回 Arena 重复利用；&lt;/li&gt;
&lt;li&gt;超大内存（超过 chunk 大小）不池化，直接申请、用完直接回收。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;PoolChunk&lt;/code&gt; 内部用&lt;strong&gt;完全二叉树&lt;/strong&gt;管理 2048 个 page：分配 ≥8KB 时在树上找深度合适的连续节点；&lt;code&gt;PoolSubpage&lt;/code&gt; 用位图（bitmap）记录小块占用。&lt;code&gt;PoolArena&lt;/code&gt; 里维护按使用率分档的 chunk 链表（qInit/q000/q025/q050/q075/q100），让分配尽量集中在少数 chunk 上，提高内存利用率。&lt;/p&gt;
&lt;p&gt;最后一块拼图是 &lt;code&gt;RecvByteBufAllocator&lt;/code&gt;（配合第 5 章的 OP_READ 循环读）：&lt;strong&gt;AdaptiveRecvByteBufAllocator 会自适应预测&lt;/strong&gt;——这次实际读了多少，下次就按这个量级分配缓冲区（默认从 1KB 起步，在 64B ~ 64KB 之间动态调整），避免&amp;quot;每次固定分配 64KB 却只读到 16B&amp;quot;的浪费。&lt;/p&gt;
&lt;p&gt;学习建议（书中原话）：这部分涉及大量多线程并发和位移与或非操作，&lt;strong&gt;把难以理解的代码贴到测试类里单独调试&lt;/strong&gt;是最有效的办法。&lt;/p&gt;
&lt;h2 id=&#34;七时间轮高级应用从心跳检测到-10-亿级任务监控第-7-章&#34;&gt;七、时间轮高级应用：从心跳检测到 10 亿级任务监控（第 7 章）&lt;/h2&gt;
&lt;h3 id=&#34;71-hashedwheeltimer-的原理&#34;&gt;7.1 HashedWheelTimer 的原理&lt;/h3&gt;
&lt;p&gt;IM 系统里有成千上万甚至百万条长连接链路，Netty 自带的心跳检测是给每条链路创建 &lt;code&gt;ScheduledFutureTask&lt;/code&gt; 定时任务——链路多了定时任务就爆炸。时间轮（HashedWheelTimer）是另一种方案：&lt;strong&gt;只用一条额外的 Worker 线程&lt;/strong&gt;，像钟表一样每隔一个 tick 走一格，只把当前格子的任务捞出来执行，没到的根本不碰。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 时间轮[&amp;#34;时间轮（wheelSize 个刻度）&amp;#34;]
        direction LR
        T0[&amp;#34;刻度 0&amp;lt;br/&amp;gt;task 链表&amp;#34;] --- T1[&amp;#34;刻度 1&amp;lt;br/&amp;gt;task 链表&amp;#34;] --- T2[&amp;#34;...&amp;#34;] --- TN[&amp;#34;刻度 N-1&amp;lt;br/&amp;gt;task 链表&amp;#34;]
    end
    P[&amp;#34;指针（tick）&amp;lt;br/&amp;gt;每 tickDuration 走一格&amp;#34;] --&amp;gt; T0
    W[&amp;#34;Worker 线程&amp;lt;br/&amp;gt;tick++&amp;lt;br/&amp;gt;落格 = tick &amp;amp; (wheelSize-1)&amp;#34;] -.驱动.- P&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;先思考三个问题（书中的引导方式很好）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指针走一轮是多久？&lt;/strong&gt; tickDuration × wheelSize；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用什么容器存任务？&lt;/strong&gt; 每个刻度（bucket）一条链表；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务时间晚于一轮终点放哪？&lt;/strong&gt; 照样按落格公式放进去，任务上记录 &lt;strong&gt;round 数（还要等几圈）&lt;/strong&gt;，指针扫到它时 round 减 1，减到 0 才真正执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;7274-实战外卖订单超时监控系统&#34;&gt;7.2~7.4 实战：外卖订单超时监控系统&lt;/h3&gt;
&lt;p&gt;书中设计的应用场景非常接地气：外卖平台每天上千万订单，用户下单后在规定时间内无人处理就要告警（超时要被投诉、要赔偿）。类似的还有优惠券过期提醒、医院预约提醒——&lt;strong&gt;凡是&amp;quot;与未来一段时间相关&amp;quot;的业务都能用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但 10 亿级任务放内存里必然溢出，于是引出改造方案：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;业务系统&amp;lt;br/&amp;gt;（外卖订单等）&amp;#34;] --&amp;gt;|&amp;#34;上报监控数据&amp;#34;| K[&amp;#34;Kafka 数据源&amp;lt;br/&amp;gt;（处理能力弱时数据留在 Kafka，削峰）&amp;#34;]
    K --&amp;gt; B[&amp;#34;时间轮计算服务&amp;lt;br/&amp;gt;（分布式部署，每分钟百万级吞吐）&amp;#34;]
    B &amp;lt;--&amp;gt; R[&amp;#34;Redis&amp;lt;br/&amp;gt;任务数据按 时间轮节点wheel + 刻度idx 存储&amp;#34;]
    B --&amp;gt;|&amp;#34;到期且未处理（isOver=0）&amp;#34;| C[&amp;#34;告警输出&amp;lt;br/&amp;gt;回调 callBackUrl / 通知相关人员&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;设计要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据源选 Kafka&lt;/strong&gt;：数据量大、要监控&amp;quot;是否被及时处理&amp;quot;，Kafka 天然削峰；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务数据外置 Redis&lt;/strong&gt;：内存里只放时间轮的调度结构，任务内容放 Redis，用&amp;quot;时间轮节点名称 wheel + 刻度编号 idx&amp;quot;做 key 映射，还能直接从 Redis 捞数据做问题定位；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控数据通用化设计&lt;/strong&gt;：&lt;code&gt;dataId&lt;/code&gt;（唯一标识）、&lt;code&gt;sysCode&lt;/code&gt;（系统来源）、&lt;code&gt;detectionTime&lt;/code&gt;（检测时间）、&lt;code&gt;isOver&lt;/code&gt;（是否结束，相当于任务取消）、&lt;code&gt;isCycle&lt;/code&gt;/&lt;code&gt;cycleTime&lt;/code&gt;（支持周期性调度监控）、&lt;code&gt;callBackUrl&lt;/code&gt;（到期回调）——这套字段设计可以直接抄到自己的项目里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;八问题分析与性能调优第-8-章&#34;&gt;八、问题分析与性能调优（第 8 章）&lt;/h2&gt;
&lt;h3 id=&#34;81-linux-部署&#34;&gt;8.1 Linux 部署&lt;/h3&gt;
&lt;p&gt;把第 3 章的 RPC 服务部署到 Linux：分环境配置文件（&lt;code&gt;server.properties&lt;/code&gt;：服务名、端口、Zookeeper 地址、权重，开发/测试/生产各一份）+ &lt;code&gt;PropertyUtil&lt;/code&gt; 工具类加载 + shell 启动脚本。&lt;/p&gt;
&lt;h3 id=&#34;82-jmeter-秒杀压测&#34;&gt;8.2 JMeter 秒杀压测&lt;/h3&gt;
&lt;p&gt;普通 JMeter TCP 脚本只能压一台服务器，也不用为此加 Nginx/LVS——书中选择的方式是&lt;strong&gt;把 RPC 客户端打成 jar 包嵌入 JMeter&lt;/strong&gt;（实现 &lt;code&gt;AbstractJavaSamplerClient&lt;/code&gt;），直接模拟长连接高并发调用分布式服务，观察 TPS、并发用户数、平均响应耗时。&lt;/p&gt;
&lt;h3 id=&#34;83-常见生产问题排查实战中真能救命的套路&#34;&gt;8.3 常见生产问题排查（实战中真能救命的套路）&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;症状&lt;/th&gt;
					&lt;th&gt;排查方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;内存溢出&lt;strong&gt;一定会出现&lt;/strong&gt;（跑一段时间就炸）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;jmap -histo&lt;/code&gt; 排序观察，看哪些对象增长快且不释放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;内存溢出&lt;strong&gt;偶尔出现&lt;/strong&gt;（很久才发生）&lt;/td&gt;
					&lt;td&gt;启动参数加 &lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/xxx.hprof&lt;/code&gt;，溢出时自动快照，用 Eclipse MAT 分析&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无溢出但响应慢、TPS 极低&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;top -H -p pid&lt;/code&gt; 找到 CPU 最高的线程 id → 转 16 进制小写 → &lt;code&gt;jstack -l pid | grep &#39;线程id&#39;&lt;/code&gt; 定位运行栈&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;84-性能调优三大方向&#34;&gt;8.4 性能调优三大方向&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Linux 系统参数&lt;/strong&gt;：压测常见的&amp;quot;文件句柄数过多&amp;quot;——全局改 &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; 加 &lt;code&gt;fs.file-max=1000000&lt;/code&gt;（支持百万连接），&lt;code&gt;sysctl -p&lt;/code&gt; 生效；局部改 &lt;code&gt;/etc/security/limits.conf&lt;/code&gt; 加 &lt;code&gt;* hard/soft nofile 1000000&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP 参数&lt;/strong&gt;：缓冲区大小、 backlog 队列等内核参数调整；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Netty 应用层&lt;/strong&gt;：理解第 6 章后大胆用内存池减少堆外分配次数；用第 7 章的时间轮替换默认心跳检测，减轻 I/O 线程负担。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;书中一个坦诚的结论值得记住：&lt;strong&gt;无复杂业务逻辑时，Netty 不调优性能也已经很好&lt;/strong&gt;——调优的价值在于真实业务（数据库操作、远程调用混入 Service 层）出现瓶颈时，你知道从哪里下手。&lt;/p&gt;
&lt;h2 id=&#34;九全书知识地图与结语&#34;&gt;九、全书知识地图与结语&lt;/h2&gt;
&lt;p&gt;把全书浓缩成一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((Netty))
    应用层
      长连接通信（第1章）
      分布式RPC（第3章 协议+反射+ZK）
      时间轮（第7章 心跳/10亿级任务监控）
    原理层
      多路复用器 epoll/select
      Reactor线程模型 Boss/Worker
      编解码与粘包拆包
      序列化 Protobuf/Kryo
      零拷贝
      背压 TCP窗口/Flink
    源码层
      NioEventLoop run（）循环
      Channel继承体系/Unsafe
      ByteBuf 双指针/引用计数
      内存泄漏检测 弱引用+采样
      读写流程 accept/read/write半包
    内存层
      PoolThreadCache 线程缓存
      PoolArena/PoolChunk 16MB
      PoolSubpage 小块位图
      AdaptiveRecvByteBuf 自适应预测
    运维层
      Linux部署与句柄调优
      JMeter长连接压测
      jmap/MAT/jstack排查三板斧&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;最后给新人的学习路径建议（也是作者的阅读建议）：先跑通第 1 章的长连接示例和第 3 章的 RPC，建立&amp;quot;会用&amp;quot;的信心；再反复啃第 4~6 章源码（作者建议源码至少读 3 遍，直到对 &lt;code&gt;Channel&lt;/code&gt;、&lt;code&gt;Handler&lt;/code&gt;、&lt;code&gt;NioEventLoop&lt;/code&gt;、&lt;code&gt;ByteBuf&lt;/code&gt; 的各个方法都了如指掌）；最后用第 7、8 章的实战项目把知识串起来。Netty 涉及多线程、复杂数据结构与内存管理模型，读完它对并发编程能力的提升是肉眼可见的。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>教你打麻将</title>
      <link>https://ftmao.com/blog/posts/beimi-chess-card-game-summary/</link>
      <pubDate>Wed, 24 Jun 2020 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/beimi-chess-card-game-summary/</guid>
      <description>&lt;h1 id=&#34;教你打麻将&#34;&gt;教你打麻将&lt;/h1&gt;
&lt;p&gt;文章面向&lt;strong&gt;从未接触过联网游戏开发&lt;/strong&gt;的读者，所以前半部分会把&amp;quot;联网棋牌游戏到底是怎么跑起来的&amp;quot;讲清楚，中间穿插真实代码层面的例子；后半部分专门开一章讲四川麻将的新手实战策略——毕竟做麻将游戏的人，自己先得会打麻将。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端：Java + Spring Boot + Netty（netty-socketio）+ Hazelcast + MySQL&lt;/li&gt;
&lt;li&gt;客户端：Cocos Creator / JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞明白联网棋牌游戏到底是怎么回事&#34;&gt;一、先搞明白：联网棋牌游戏到底是怎么回事&lt;/h2&gt;
&lt;h3 id=&#34;11-单机游戏和联网游戏的本质区别&#34;&gt;1.1 单机游戏和联网游戏的本质区别&lt;/h3&gt;
&lt;p&gt;单机游戏里，所有逻辑都在你自己的设备上运行，规则由你的设备说了算。联网游戏不行——四个人打麻将，&lt;strong&gt;谁摸了什么牌、谁点了炮，必须有一个&amp;quot;裁判&amp;quot;说了算&lt;/strong&gt;，否则每个人的手机都各算各的，立刻就乱套了。&lt;/p&gt;
&lt;p&gt;这个&amp;quot;裁判&amp;quot;就是&lt;strong&gt;服务器&lt;/strong&gt;。联网棋牌游戏的基本模型是：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 玩家设备
        C1[玩家A的手机&amp;lt;br/&amp;gt;Cocos Creator 客户端]
        C2[玩家B的手机]
        C3[玩家C的手机]
        C4[玩家D的手机]
    end
    S[游戏服务器&amp;lt;br/&amp;gt;贝密：Spring Boot + Netty]
    DB[(MySQL&amp;lt;br/&amp;gt;账号/战绩/房卡)]
    CACHE[(内存缓存 Hazelcast&amp;lt;br/&amp;gt;房间/牌局实时状态)]

    C1 &amp;lt;--&amp;gt;|WebSocket 长连接| S
    C2 &amp;lt;--&amp;gt; S
    C3 &amp;lt;--&amp;gt; S
    C4 &amp;lt;--&amp;gt; S
    S --&amp;gt; CACHE
    S -.异步落盘.-&amp;gt; DB&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;牌在服务器手里&lt;/strong&gt;。洗牌、发牌都在服务器上做，客户端只负责&amp;quot;显示&amp;quot;和&amp;quot;发指令&amp;quot;。你在手机上点&amp;quot;打出三万&amp;quot;，本质是给服务器发了一条消息：&lt;code&gt;doplaycards: &amp;quot;12&amp;quot;&lt;/code&gt;，服务器校验&amp;quot;现在确实轮到你、你手里确实有三万&amp;quot;之后，才广播给全桌人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个人看到的信息不同&lt;/strong&gt;。服务器把同一局牌的状态，按&amp;quot;你能看到什么&amp;quot;过滤后分别发给四个人——你只能看到自己的手牌和别人打出来的牌。这叫&lt;strong&gt;信息不对称&lt;/strong&gt;，也是棋牌游戏和射击游戏最大的不同：状态同步必须分人定制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防作弊的第一道防线就是&amp;quot;服务器权威&amp;quot;&lt;/strong&gt;。客户端想赖牌（打出一张自己没有的牌），服务器直接拒绝。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;12-为什么用-websocket-而不是-http&#34;&gt;1.2 为什么用 WebSocket 而不是 HTTP&lt;/h3&gt;
&lt;p&gt;没接触过联网游戏的人容易问：发消息不就是调接口吗，用 HTTP 不行吗？&lt;/p&gt;
&lt;p&gt;不行，因为 HTTP 是&amp;quot;问一句答一句&amp;quot;，服务器没法&lt;strong&gt;主动&lt;/strong&gt;找你。而棋牌游戏里大量事件是服务器主动推的：轮到你出牌了、有人碰了你的牌、房间人齐了……这些都需要服务器&lt;strong&gt;随时&lt;/strong&gt;推消息给客户端。WebSocket 是一条建立后一直保持的双向通道，双方随时可以说话，延迟低、开销小。贝密用的 &lt;code&gt;netty-socketio&lt;/code&gt; 就是在 Netty 上实现了 Socket.IO 协议（跑在 WebSocket 上），客户端 Cocos Creator 里直接用 socket.io 的 JS 库连。&lt;/p&gt;
&lt;h3 id=&#34;13-房卡模式一张虚拟门票&#34;&gt;1.3 房卡模式：一张&amp;quot;虚拟门票&amp;quot;&lt;/h3&gt;
&lt;p&gt;贝密 0.8.0 主打&lt;strong&gt;房卡模式&lt;/strong&gt;，这也是 2016-2018 年棋牌游戏爆发的典型商业模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你在商城购买&amp;quot;房卡&amp;quot;（虚拟道具，存在 MySQL 里）；&lt;/li&gt;
&lt;li&gt;你想和朋友打牌，就&amp;quot;开房&amp;quot;——消耗房卡，服务器生成一个 &lt;strong&gt;6 位数字房号&lt;/strong&gt;（贝密里由 &lt;code&gt;GameEngine.creatGameRoom&lt;/code&gt; 生成）；&lt;/li&gt;
&lt;li&gt;朋友在大厅输入房号进房（客户端发 &lt;code&gt;searchroom&lt;/code&gt; 事件），人齐开局；&lt;/li&gt;
&lt;li&gt;打完约定的局数，服务器出结算单，房间销毁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;和陌生人随机撮合的&amp;quot;大厅模式&amp;quot;相比，房卡模式本质上卖的是&amp;quot;熟人社交的场地&amp;quot;，技术上则多了一个&amp;quot;按房号精确找房&amp;quot;的入口。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二贝密的整体架构拆解&#34;&gt;二、贝密的整体架构拆解&lt;/h2&gt;
&lt;h3 id=&#34;21-架构总览&#34;&gt;2.1 架构总览&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    subgraph 客户端
        A[Cocos Creator&amp;lt;br/&amp;gt;dizhu / majiang 模块]
    end
    subgraph 服务端 com.beimi
        B[GameEventHandler&amp;lt;br/&amp;gt;Socket 事件入口]
        C[GameEngine&amp;lt;br/&amp;gt;游戏总调度]
        D[自研状态机&amp;lt;br/&amp;gt;BeiMiStateMachine]
        E[Board 牌局内存模型&amp;lt;br/&amp;gt;MaJiangBoard / DuZhuBoard]
        F[Action/Task&amp;lt;br/&amp;gt;出牌·结算·计时任务]
    end
    subgraph 存储层
        G[(Hazelcast&amp;lt;br/&amp;gt;房间/牌局/玩家/撮合队列)]
        H[(cache2k&amp;lt;br/&amp;gt;过期回调当计时器)]
        I[Disruptor RingBuffer&amp;lt;br/&amp;gt;异步事件]
        J[(MySQL JPA)]
        K[(Elasticsearch)]
    end

    A &amp;lt;--&amp;gt;|WebSocket /game namespace| B
    B --&amp;gt; C --&amp;gt; D --&amp;gt; E
    D --&amp;gt; F
    E --&amp;gt; G
    F --&amp;gt; H
    C --&amp;gt; I
    I --&amp;gt; J
    I --&amp;gt; K&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;各层职责一句话概括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GameEventHandler&lt;/strong&gt;：所有 &lt;code&gt;@OnEvent&lt;/code&gt; 端点都在这里，负责把客户端消息翻译成人话（比如把 &lt;code&gt;&amp;quot;12,13&amp;quot;&lt;/code&gt; 解析成字节数组），然后交给 GameEngine；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GameEngine&lt;/strong&gt;：总调度，加房、开局、出牌、杠碰胡、离房、解散房间都从这儿过；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自研状态机&lt;/strong&gt;：驱动&amp;quot;房间&amp;quot;这个对象的生命周期（后面细讲）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Board 内存模型&lt;/strong&gt;：一局牌的完整状态（四家的手牌、桌面牌、轮到谁、倍率），只活在 Hazelcast 内存缓存里；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储分层&lt;/strong&gt;：实时状态全在 Hazelcast，MySQL 只存账号/战绩/房卡流水，而且是通过 Disruptor &lt;strong&gt;异步&lt;/strong&gt;写入——牌局进行中的一秒钟都不等数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-一次出牌的完整旅程以麻将为例&#34;&gt;2.2 一次出牌的完整旅程（以麻将为例）&lt;/h3&gt;
&lt;p&gt;这是最核心的一条链路，用实际代码里的事件名走一遍：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant P1 as 玩家A客户端
    participant H as GameEventHandler
    participant E as GameEngine
    participant B as MaJiangBoard
    participant T as 计时器(cache2k)
    participant P2 as 其他玩家客户端

    P1-&amp;gt;&amp;gt;H: emit(&amp;#34;doplaycards&amp;#34;, &amp;#34;12&amp;#34;)
    H-&amp;gt;&amp;gt;E: takeCardsRequest(token, card)
    E-&amp;gt;&amp;gt;B: 出牌校验 + 从手牌移除
    E-&amp;gt;&amp;gt;T: remove(roomid) 取消A的出牌倒计时
    E--&amp;gt;&amp;gt;P2: 广播 &amp;#34;command&amp;#34;: takecards
    E-&amp;gt;&amp;gt;E: checkMJCard 遍历其他家&amp;lt;br/&amp;gt;查 杠/碰/吃/胡
    alt 有人能杠碰胡
        E-&amp;gt;&amp;gt;T: 挂5秒等待任务(DEAL事件)
        P2-&amp;gt;&amp;gt;H: emit(&amp;#34;selectaction&amp;#34;, &amp;#34;peng&amp;#34;)
        H-&amp;gt;&amp;gt;E: actionEventRequest 执行碰
    else 无人响应
        E-&amp;gt;&amp;gt;B: dealRequest 下家抓牌
        E--&amp;gt;&amp;gt;P2: 广播 &amp;#34;command&amp;#34;: dealcard
        E-&amp;gt;&amp;gt;T: 挂8秒出牌任务&amp;lt;br/&amp;gt;(超时自动打)
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;举例说明&lt;/strong&gt;：假设你打出一张&amp;quot;五筒&amp;quot;，服务器干的事依次是——&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;校验：轮到你、你有五筒、这张牌合法；&lt;/li&gt;
&lt;li&gt;把你 8 秒的出牌倒计时从计时器缓存里删掉（你不主动出，超时会由 &lt;code&gt;CreateMJPlayCardsTask&lt;/code&gt; 强制替你出）；&lt;/li&gt;
&lt;li&gt;广播给全桌：&amp;ldquo;A 打了五筒&amp;rdquo;；&lt;/li&gt;
&lt;li&gt;挨个问另外三家：你能碰吗？能杠吗？能胡吗？（定缺了筒子的那家直接跳过——这就是四川麻将的规则在代码里的体现，&lt;code&gt;GameUtils.processMJCard&lt;/code&gt; 里定缺花色不参与判断）；&lt;/li&gt;
&lt;li&gt;如果 B 能碰，给 B 弹&amp;quot;碰/过&amp;quot;按钮，牌局挂起 5 秒等他决定；B 点了&amp;quot;碰&amp;quot;，就执行碰、轮到 B 出牌；&lt;/li&gt;
&lt;li&gt;如果三家都&amp;quot;过&amp;quot;，下家自动摸一张牌，服务器先替他检查一次&amp;quot;摸上来的牌能不能暗杠/自摸&amp;quot;，然后通知他出牌，重新挂上 8 秒倒计时。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程中，客户端只做两件事：把服务器的消息渲染成动画，把玩家的点击发给服务器。&lt;/p&gt;
&lt;h3 id=&#34;23-房间的状态机流转&#34;&gt;2.3 房间的状态机流转&lt;/h3&gt;
&lt;p&gt;房间是有生命的：创建、等人、人齐、开局、结算、销毁。贝密没有用 Spring StateMachine，而是&lt;strong&gt;自研了一个极简状态机&lt;/strong&gt;（&lt;code&gt;core/statemachine&lt;/code&gt; 包），每种游戏一个 Bean：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; NONE
    NONE --&amp;gt; CRERATED: ENTER 创建房间
    CRERATED --&amp;gt; WAITTING: JOIN 玩家陆续进房
    WAITTING --&amp;gt; READY: ENOUGH 人齐
    WAITTING --&amp;gt; WAITTING: AUTO 超时补AI机器人
    READY --&amp;gt; BEGIN: RAISEHANDS 全员准备
    BEGIN --&amp;gt; LASTHANDS: 发牌完成
    LASTHANDS --&amp;gt; PLAY: 庄家打出首张
    PLAY --&amp;gt; PLAY: DEAL/PLAYCARDS&amp;lt;br/&amp;gt;摸牌出牌循环
    PLAY --&amp;gt; END: ALLCARDS 有人胡牌/牌摸完
    END --&amp;gt; [*]: 结算发战报&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;状态机里每个&amp;quot;事件&amp;quot;对应一串 Action（比如麻将的链路是 &lt;code&gt;EnterAction → JoinAction → EnoughAction → SelectAction(定缺) → MJRaiseHandsAction → DealMJCardAction → PlayMJCardsAction → AllCardsAction&lt;/code&gt;），业务代码只需要一句 &lt;code&gt;game.change(room, event, intervalSeconds)&lt;/code&gt;，状态机负责找到对应 Action 执行，并由 Action 顺手创建下一个延时任务，形成闭环。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三断线重连联网游戏逃不掉的工程问题&#34;&gt;三、断线重连：联网游戏逃不掉的工程问题&lt;/h2&gt;
&lt;p&gt;新手最容易低估的就是这块。手机上打牌，来个电话、进个电梯，连接说断就断。贝密的处理方式是&amp;quot;托管 + 快照恢复&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[玩家网络断开] --&amp;gt; B{服务器收到&amp;lt;br/&amp;gt;OnDisconnect}
    B --&amp;gt;|玩家正在对局中| C[标记 OFFLINE&amp;lt;br/&amp;gt;进入托管状态]
    C --&amp;gt; D[计时器超时后&amp;lt;br/&amp;gt;auto=true 自动出牌]
    D --&amp;gt; E{玩家重连?}
    E --&amp;gt;|emit recovery| F[GameEngine 查缓存&amp;lt;br/&amp;gt;发现该房间有进行中的 Board]
    F --&amp;gt; G[下发 RecoveryData 全量快照:&amp;lt;br/&amp;gt;自己的手牌/桌面牌/轮到谁/剩余倒计时25s]
    G --&amp;gt; H[客户端按快照重建牌桌画面]
    E --&amp;gt;|一直没回来| I[AI托管打到本局结束]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：你出牌出了半局断网了，服务器不会暂停牌局等你（其他三个人不答应），而是把你标记为 OFFLINE，由超时任务替你&amp;quot;摸什么打什么&amp;quot;。30 秒后你连回来了，客户端发 &lt;code&gt;recovery&lt;/code&gt;，服务器把当前牌局的完整快照发给你，你的手机根据快照把桌面重新画出来，无缝接上。注意重连期间你是被托管的——所以掉线久了回来，往往牌已经很难看了，这是规则的一部分。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四四川麻将新手致胜策略一个结构化的决策框架&#34;&gt;四、四川麻将新手致胜策略：一个结构化的决策框架&lt;/h2&gt;
&lt;p&gt;做了这么久的麻将游戏，经常被问&amp;quot;新手怎么提高胜率&amp;quot;。零散的口诀（&amp;ldquo;记牌&amp;quot;&amp;ldquo;防点炮&amp;rdquo;）新手记不住也用不上，所以这一章我把四川麻将的策略整理成一个&lt;strong&gt;结构化框架&lt;/strong&gt;：先确立唯一的目标函数，再按&amp;quot;战略层 → 信息层 → 战术层 → 概率层&amp;quot;四个层级组织所有决策，最后给出血战到底的特有推论和新手错误清单。逻辑主线只有一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;麻将的每一步都是在信息不完备下做期望收益最大化的决策。四个层级分别回答：往哪走（战略）、知道什么（信息）、这巡打哪张（战术）、什么时候切换路线（概率）。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;40-规则即策略先立规则再推结论&#34;&gt;4.0 规则即策略：先立规则，再推结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定缺&lt;/strong&gt;：开局后每人必须选一门花色（万/筒/条）作为&amp;quot;缺门&amp;rdquo;，胡牌时手里不能有这门牌，摸到缺门的牌必须打掉。牌墙只有 108 张（三门花色，无风牌）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血战到底&lt;/strong&gt;：一家胡牌不结束牌局，剩下的人继续打，直到牌摸完或只剩一家没胡。点炮只有点炮者付分，最后没胡的人还要给已听牌的人&amp;quot;查叫&amp;quot;赔分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从这两条规则可以直接推出三条战略推论，整章都是它们的展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;信息压缩&lt;/strong&gt;：定缺把每家的&amp;quot;有效牌域&amp;quot;从三门砍到两门，而且是&lt;strong&gt;公开信息&lt;/strong&gt;。读牌成本比别的麻将低一个量级——新手也能读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间价值&lt;/strong&gt;：牌局不因一人胡牌而结束，每张牌的价值随牌墙减少而衰减。&lt;strong&gt;&amp;ldquo;还剩几巡&amp;quot;是所有决策的隐含变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;止损优先&lt;/strong&gt;：点炮只由点炮者一人付分、没胡要赔查叫——&lt;strong&gt;落后的代价是确定的，追上的收益是概率性的&lt;/strong&gt;。这决定了&amp;quot;不跟随到最后&amp;quot;在数学上往往是对的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三条推论的推导关系：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    R1[规则一: 定缺&amp;lt;br/&amp;gt;缺门公开] --&amp;gt; C1[推论一: 信息压缩&amp;lt;br/&amp;gt;读牌成本低]
    R2[规则二: 血战到底&amp;lt;br/&amp;gt;胡牌不终局] --&amp;gt; C2[推论二: 时间价值&amp;lt;br/&amp;gt;剩几巡是隐含变量]
    R2 --&amp;gt; C3[推论三: 止损优先&amp;lt;br/&amp;gt;点炮独赔+查叫保底]
    C1 --&amp;gt; S[决定 4.2 信息层怎么读]
    C2 --&amp;gt; S2[决定 4.4 何时切换路线]
    C3 --&amp;gt; S3[决定 4.5 防守与保底策略]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;41-战略层起手评估与路线选择&#34;&gt;4.1 战略层：起手评估与路线选择&lt;/h3&gt;
&lt;p&gt;拿到起手牌、定缺之前，先给牌打分。不需要精确，三个数就够：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向听数&lt;/strong&gt;：还差几张牌能听牌。数一下你手里有几个成型搭子/刻子/对子，13 张牌需要 4 副 + 1 将，差几副就是几向听。新手只要会区分&amp;quot;两向听以内（好牌）&amp;ldquo;和&amp;quot;三向听以上（差牌）&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对子数&lt;/strong&gt;：4 对以上 → 七对/大对子路线候选；0~1 对 → 别惦记对子路线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同花色集中度&lt;/strong&gt;：9 张以上同花色 → 清一色路线候选。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;定缺选择&lt;/strong&gt;是第一个战略决策，原则按优先级排：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;缺张数最少的那门&lt;/strong&gt;——缺门每张都是必打的废牌，张数越少浪费的巡数越少；&lt;/li&gt;
&lt;li&gt;张数相近时，&lt;strong&gt;缺结构最差的那门&lt;/strong&gt;（孤张多、没搭子的）；&lt;/li&gt;
&lt;li&gt;还在纠结时，&lt;strong&gt;缺端张多的那门&lt;/strong&gt;（1、9 多），因为端张成搭能力弱。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：起手万子 2 张（三万、八万，无关联）、筒子 5 张（两个搭子）、条子 6 张（一个刻子一个边搭）。毫无疑问题缺万——但如果万子是 2 张而筒子是 3 张但全是散牌，就该缺筒子：定缺看的是&amp;quot;废牌数量&amp;rdquo;，不是&amp;quot;张数&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;定缺公布后立即做第二步战略判断：&lt;strong&gt;看三家的缺门，决定做大还是快跑&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三家中有两家缺你这门 → 你要做的花色没人抢，牌墙里全剩给你，&lt;strong&gt;做大的绿灯&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;三家都不缺你这门 → 四个人抢一门牌，你的关键张会被飞速消耗，&lt;strong&gt;果断转快跑&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此，三条路线已经清晰：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;触发条件&lt;/th&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;核心动作&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;快跑&lt;/td&gt;
					&lt;td&gt;牌型一般、向听小、花色被抢&lt;/td&gt;
					&lt;td&gt;最速听牌、先胡锁定收益&lt;/td&gt;
					&lt;td&gt;牌效优先，不留废搭&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;做大&lt;/td&gt;
					&lt;td&gt;起手 9 张+同花色/4 对+，且无竞争&lt;/td&gt;
					&lt;td&gt;清一色/大对子/七对&lt;/td&gt;
					&lt;td&gt;忍受慢听牌，屯关键张&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;防守&lt;/td&gt;
					&lt;td&gt;有人已听牌且自己手牌差&lt;/td&gt;
					&lt;td&gt;不点炮、保查叫底线&lt;/td&gt;
					&lt;td&gt;只打熟张，听牌优先于胡牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;把起手到定路线的全过程画成一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[拿到13张起手牌] --&amp;gt; B[三个数打分:&amp;lt;br/&amp;gt;向听数 / 对子数 / 同花色集中度]
    B --&amp;gt; C{定缺: 缺哪门?}
    C --&amp;gt;|第一优先| C1[缺张数最少的门]
    C --&amp;gt;|张数相近| C2[缺结构最差的门&amp;lt;br/&amp;gt;孤张多没搭子]
    C --&amp;gt;|还纠结| C3[缺端张多的门&amp;lt;br/&amp;gt;1和9成搭弱]
    C1 &amp;amp; C2 &amp;amp; C3 --&amp;gt; D[公布定缺&amp;lt;br/&amp;gt;观察三家缺门]
    D --&amp;gt; E{有人抢我要做的花色?}
    E --&amp;gt;|两家以上缺我这门&amp;lt;br/&amp;gt;绿灯| F{起手支持做大?&amp;lt;br/&amp;gt;9张+同花色 或 4对+}
    F --&amp;gt;|是| G[做大路线&amp;lt;br/&amp;gt;清一色/大对子/七对]
    F --&amp;gt;|否| H[快跑路线&amp;lt;br/&amp;gt;最速听牌]
    E --&amp;gt;|三家都抢我这门| H
    E --&amp;gt;|牌型差 三向听以上| I[保守推进&amp;lt;br/&amp;gt;随时准备转防守]
    style G fill:#ffe6cc
    style H fill:#d4edda
    style I fill:#f8d7da&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;路线不是选定就不变——&lt;strong&gt;每一巡都可以用新信息重估&lt;/strong&gt;，4.4 会给出切换的量化标准。&lt;/p&gt;
&lt;h3 id=&#34;42-信息层记牌与读牌&#34;&gt;4.2 信息层：记牌与读牌&lt;/h3&gt;
&lt;p&gt;信息层回答&amp;quot;我知道什么&amp;quot;。它分两半：&lt;strong&gt;记牌&lt;/strong&gt;是对已发生事实的压缩存储，&lt;strong&gt;读牌&lt;/strong&gt;是从事实反推未知信息。&lt;/p&gt;
&lt;h4 id=&#34;421-记牌分层记忆法&#34;&gt;4.2.1 记牌：分层记忆法&lt;/h4&gt;
&lt;p&gt;让新手背下三家打过的每张牌不现实。按重要性只记三类：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一层：记&amp;quot;张数&amp;quot;不记&amp;quot;顺序&amp;quot;。&lt;/strong&gt; 每门花色每张牌只有 4 张。你不需要记住&amp;quot;上家第 3 巡打了七万&amp;quot;，只需在脑子里维护计数：&amp;ldquo;七万已见 3 张&amp;rdquo;。重点盯的是&lt;strong&gt;你自己将牌和搭子两端的关键牌&lt;/strong&gt;——你手里 6、8 万等卡 7 万，&amp;ldquo;7 万见了几张&amp;quot;直接决定这个搭子的死活：见 3 张以上，搭子基本死了，趁早拆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二层：记&amp;quot;缺门&amp;quot;和&amp;quot;花色倾向&amp;rdquo;。&lt;/strong&gt; 定缺后，对手打出的&lt;strong&gt;非缺门&lt;/strong&gt;花色才是有效信息。下家缺条，从第 2 巡起一直打筒子、摸到万子全留——一句话记住&amp;quot;下家只留万&amp;quot;即可，不用记他打的每一张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三层：记&amp;quot;序盘的反常中张&amp;quot;。&lt;/strong&gt; 序盘（前 4~5 巡）正常人先打孤张边张（1、9）。有人序盘就打 4、5、6 中张，说明这门要么极好（搭子齐了中张多余）要么整门不要，结合缺门大概率能分辨。&lt;/p&gt;
&lt;p&gt;口诀：&lt;strong&gt;数自己的关键张，记别人的花色倾向，盯序盘的反常中张。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三层记忆的工作量递减、收益递增，像一座金字塔——新手从塔底练起：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart BT
    L1[第三层: 序盘反常中张&amp;lt;br/&amp;gt;一两个反常信号即可&amp;lt;br/&amp;gt;精力占比 ~10%] --&amp;gt; L2[第二层: 缺门与花色倾向&amp;lt;br/&amp;gt;每家一句话: 他只留X&amp;lt;br/&amp;gt;精力占比 ~30%]
    L2 --&amp;gt; L3[第一层: 关键张计数&amp;lt;br/&amp;gt;自己搭子两端的牌各见几张&amp;lt;br/&amp;gt;精力占比 ~60%]
    L3 --&amp;gt; R[收益: 搭子死活判断&amp;lt;br/&amp;gt;直接决定拆不拆]
    L2 --&amp;gt; R2[收益: 知道谁在收哪门&amp;lt;br/&amp;gt;直接决定防什么]
    L1 --&amp;gt; R3[收益: 序盘读出牌型方向]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;422-读牌他要什么不要什么打什么危险&#34;&gt;4.2.2 读牌：他要什么、不要什么、打什么危险&lt;/h4&gt;
&lt;p&gt;读牌的本质是&lt;strong&gt;用别人的&amp;quot;打出&amp;quot;反推他的&amp;quot;留下&amp;quot;&lt;/strong&gt;——人留下什么牌，才会打出什么牌。按牌局阶段读不同的东西：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;序盘读&amp;quot;结构&amp;quot;&lt;/strong&gt;：缺门 + 反常中张 + 第一碰，拼出每家的大致牌型方向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中盘读&amp;quot;进度&amp;quot;&lt;/strong&gt;：看连续动作。上家碰了&amp;quot;二筒&amp;quot;后摸打筒子以外的牌，说明他筒子上还有货；碰完后打的那张牌的&lt;strong&gt;邻张&lt;/strong&gt;往往是他不需要的（碰二筒后打五万，他大概率没有 3-4 万、4-6 万的搭子）；碰了二筒后打的还是筒子，几乎可断定他在做筒子清一色或已听牌。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尾盘读&amp;quot;节奏&amp;quot;&lt;/strong&gt;：一个人&lt;strong&gt;连续摸牌就打（摸打）&lt;/strong&gt;，说明手牌已成型，打出的就是刚摸的；反过来&lt;strong&gt;想了很久才打出一张&lt;/strong&gt;，说明这张是从手里&amp;quot;拆&amp;quot;出来的，他在这张牌附近有搭子变动，其邻张不再安全。&lt;/p&gt;
&lt;p&gt;由此得到防点炮 checklist，中后盘（牌墙剩 1/3 以内）每打一张前过一遍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;这张牌见过几张？&lt;/strong&gt; 见 2~3 张的熟张相对安全；一张没见的生张风险最高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有没有人明显在收这门？&lt;/strong&gt; 对家在做万子，你手里的生张五万就是高危，宁可拆搭子打熟张。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;他是不是单钓/卡张？&lt;/strong&gt; 碰了三副、手牌只剩一两张的人，听的牌范围很窄，往往就在他碰的那几副的邻张上——那一片中张全是雷区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;完整例子&lt;/strong&gt;：对家定缺条子，碰了&amp;quot;三筒&amp;quot;，摸打两巡后思考良久切出&amp;quot;八万&amp;quot;。推断：他在做筒子（碰三筒+留筒），八万是万子里的多余搭子；&lt;strong&gt;你手里五、六、七万从此相对安全&lt;/strong&gt;（他不要万子），但&lt;strong&gt;任何生张筒子都是高危&lt;/strong&gt;，尤其一、二、四筒（三筒邻张）。&lt;/p&gt;
&lt;p&gt;读牌的推理链条固定是&amp;quot;观察 → 反推 → 行动&amp;quot;三段，常见信号对照如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 观察[他打出/做了什么]
        O1[缺门公布后&amp;lt;br/&amp;gt;一直打筒留万]
        O2[碰二筒后&amp;lt;br/&amp;gt;又打出筒子]
        O3[碰二筒后&amp;lt;br/&amp;gt;打出五万]
        O4[连续摸打]
        O5[长考后拆出八万]
    end
    subgraph 反推[他留下/在想什么]
        I1[在做万子清一色]
        I2[筒子听牌或做筒]
        I3[万子3-4/4-6搭子不存在&amp;lt;br/&amp;gt;→ 万字邻张变安全]
        I4[手牌已成型&amp;lt;br/&amp;gt;打的就是刚摸的]
        I5[八万附近有搭子变动&amp;lt;br/&amp;gt;→ 邻张不安全]
    end
    subgraph 行动[我该怎么办]
        A1[生张万子列为高危]
        A2[生张筒子绝不打]
        A3[手里4万6万可放心打]
        A4[按他已听牌对待&amp;lt;br/&amp;gt;进入防守]
        A5[七万九万扣住观察]
    end
    O1 --&amp;gt; I1 --&amp;gt; A1
    O2 --&amp;gt; I2 --&amp;gt; A2
    O3 --&amp;gt; I3 --&amp;gt; A3
    O4 --&amp;gt; I4 --&amp;gt; A4
    O5 --&amp;gt; I5 --&amp;gt; A5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-战术层牌效这一巡到底打哪张&#34;&gt;4.3 战术层：牌效——这一巡到底打哪张&lt;/h3&gt;
&lt;p&gt;战术层是执行：在路线已定、信息已有的前提下，每张出牌都按同一套优先级评估。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;出牌优先级（无安全压力时）&lt;/strong&gt;：缺门牌（必打）→ 孤立端张（无邻张的 1、9）→ 孤立中张 → 多余搭子中进张最少的那张 → 拆搭子。&lt;strong&gt;核心原则是&amp;quot;每一巡都最大化有效进张总数&amp;quot;&lt;/strong&gt;：假设搭子 A（4-5 万）有 8 张潜在进张、搭子 B（1-2 筒）只有 3 张潜在进张，要拆就先拆 B——哪怕 B 看起来更&amp;quot;连得顺&amp;quot;。注意&amp;quot;潜在进张&amp;quot;还要减去已见张数，见 3 张的两面搭子不如没见过的卡张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;碰/杠决策&lt;/strong&gt;是新手最容易犯错的地方，默认答案是&amp;quot;&lt;strong&gt;不碰&lt;/strong&gt;&amp;quot;，除非满足以下至少一条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;碰了立刻听牌，或碰的是你的将牌方向（对子路线）；&lt;/li&gt;
&lt;li&gt;你在做清一色/大对子，碰能加速成型；&lt;/li&gt;
&lt;li&gt;这张牌不碰就被下家吃碰走、你再也等不到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为碰有隐性成本：&lt;strong&gt;暴露信息&lt;/strong&gt;（全桌知道你在收这门）、&lt;strong&gt;破坏门清弹性&lt;/strong&gt;（碰完之后手牌变窄，改路线更难）、&lt;strong&gt;改变摸牌顺序&lt;/strong&gt;（可能把本该你摸的关键张让给下家）。杠同理：明杠多摸一张且血战里&amp;quot;杠上花&amp;quot;有额外收益，但同样暴露信息，尾盘还可能给别人开&amp;quot;杠上炮&amp;quot;。&lt;strong&gt;起手做大时积极碰杠，快跑路线时能不碰就不碰。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;听口选择&lt;/strong&gt;遵循两个数字：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;听口宽度&lt;/strong&gt;：两面听（8 张潜在）&amp;gt; 卡张/边张（4 张）&amp;gt; 对倒/单钓（2~4 张）。能选宽不选窄；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;剩余张数&lt;/strong&gt;：减去已见的。听见过 2 张的两面（剩 4~6 张），通常不如听没见过的卡张（剩 4 张但还有别人扣牌的空间）。尾盘再加一条修正：&lt;strong&gt;听熟张&lt;/strong&gt;——别人敢打的牌，你才能吃到点炮；听生张只能指望自摸。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把出牌优先级和碰杠决策各画成一张图，实战时按图索骥即可：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[这巡要出牌] --&amp;gt; B{手里有缺门牌?}
    B --&amp;gt;|有| C[打缺门牌]
    B --&amp;gt;|没有| D{有孤立端张?&amp;lt;br/&amp;gt;无邻张的1/9}
    D --&amp;gt;|有| E[打孤立端张]
    D --&amp;gt;|没有| F{有孤立中张?}
    F --&amp;gt;|有| G[打孤立中张]
    F --&amp;gt;|没有| H{搭子进张数比较}
    H --&amp;gt;|拆进张最少的搭子| I[打出该搭子中&amp;lt;br/&amp;gt;价值更低的一张]
    C &amp;amp; E &amp;amp; G &amp;amp; I --&amp;gt; J[打完前最后过一遍&amp;lt;br/&amp;gt;4.2.2 防点炮checklist]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    P[别人打出一张牌&amp;lt;br/&amp;gt;我可以碰/杠] --&amp;gt; Q{碰了立刻听牌&amp;lt;br/&amp;gt;或是对子路线?}
    Q --&amp;gt;|是| R[碰]
    Q --&amp;gt;|否| S{我在做大&amp;lt;br/&amp;gt;且碰能加速成型?}
    S --&amp;gt;|是| R
    S --&amp;gt;|否| T{不碰就永远&amp;lt;br/&amp;gt;等不到这张?}
    T --&amp;gt;|是| R
    T --&amp;gt;|否| U[不碰, 默认答案]
    U -.成本.-&amp;gt; V1[暴露信息: 全桌知道你收这门]
    U -.成本.-&amp;gt; V2[破坏门清弹性: 手牌变窄]
    U -.成本.-&amp;gt; V3[改变摸牌顺序: 可能放走关键张]
    style U fill:#d4edda
    style R fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-概率层期望值决策做大快跑防守的切换点&#34;&gt;4.4 概率层：期望值决策——做大、快跑、防守的切换点&lt;/h3&gt;
&lt;p&gt;这是整个框架的&amp;quot;调度器&amp;quot;，回答&amp;quot;什么时候换路线&amp;quot;。先把 4.3 的直觉量化成一个可心算的模型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本概率感觉&lt;/strong&gt;：任一时刻，你某方向的&amp;quot;期望进展&amp;quot; ≈ 剩余进张数 × 剩余可摸次数。你手里 4-5 万等 3/6 万，若都少见，剩余进张约 6-8 张；牌墙剩 40 张、四家轮流摸，你大约还能摸 8-10 次，摸中概率相当可观。但如果 3/6 万已见 3 张，进张只剩 1~2 张，期望直接砍到四分之一以下——&lt;strong&gt;搭子的价值不看牌面，看剩余进张数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切换到&amp;quot;做大&amp;quot;的条件&lt;/strong&gt;（需同时满足）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起手结构支持：9 张以上同花色，或 4 对以上；&lt;/li&gt;
&lt;li&gt;进张供给充足：三家缺门不与你抢这门（4.1 的绿灯）；&lt;/li&gt;
&lt;li&gt;时间充足：牌墙过半之前。做牌是&amp;quot;用听牌速度换番数&amp;quot;，起手离目标越近、剩余巡数越多，换得越划算；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血战规则加成&lt;/strong&gt;：一家胡牌不结束牌局，早胡者只是先锁定小收益，晚胡的大牌仍能兑现——大牌的&amp;quot;被截胡损失&amp;quot;比推倒胡小得多，所以四川麻将天然鼓励做牌。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;切换到&amp;quot;快跑/止损&amp;quot;的条件&lt;/strong&gt;（满足任一即执行）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进张枯竭&lt;/strong&gt;：关键牌期望进张 &amp;lt; 2，每一巡等待都是负期望——马上换听、拆死搭子；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有人已听牌且你生张密集&lt;/strong&gt;：此时你的期望收益为负（点炮赔分 + 被查叫），最优解不是&amp;quot;我也赶紧听&amp;quot;，而是&lt;strong&gt;只打熟张把点炮概率压到最低&lt;/strong&gt;，保住查叫底线。这就是&amp;quot;不跟随到最后&amp;quot;——追不上就不再投入，及时止损；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;牌墙见底（剩 15 张以内）还没听牌&lt;/strong&gt;：做牌的时间价值归零，唯一目标是&lt;strong&gt;听牌防查叫&lt;/strong&gt;——往剩余张数最多的熟张方向听，只听一张也比&amp;quot;做了一半的大牌&amp;quot;值钱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个简化的期望算账例子&lt;/strong&gt;（假设平胡 1 番 = 2 分，清一色 4 番 = 16 分）：你听牌了，可以选听口 A——平胡两面听，剩余进张 6 张；或改听口 B——清一色单钓，剩余进张 2 张。若剩余可摸次数约 6 次、点炮胡与自摸混合，A 的期望收益约是正 6~8 分；B 做成的概率约一半不到，做成了 16 分、做不成还要承担被查叫赔 2 分——期望约 6 分，且方差巨大。&lt;strong&gt;结论：番数翻倍抵不过进张减半，除非时间足够早或你已经落后必须搏。&lt;/strong&gt; 新手默认选宽听口，永远是长期正期望。&lt;/p&gt;
&lt;p&gt;把&amp;quot;什么时候切换路线&amp;quot;画成一张象限图——横轴是你的剩余进张，纵轴是牌局剩余时间，每巡定位一次自己落在哪个象限：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    UP[纵轴 ↑ 剩余时间多] --- TOP
    subgraph TOP[ ]
        direction LR
        LEFT2[进张少] ~~~ A[换听调整&amp;lt;br/&amp;gt;时间够但方向死了&amp;lt;br/&amp;gt;例: 卡七万已见三张]
        A ~~~ B[做大&amp;lt;br/&amp;gt;时间够且进张足&amp;lt;br/&amp;gt;例: 起手清一色九张]
        B ~~~ RIGHT2[进张多]
    end
    TOP --- BOT
    subgraph BOT[ ]
        direction LR
        LEFT[进张少] ~~~ C[防守保底&amp;lt;br/&amp;gt;先听牌防查叫&amp;lt;br/&amp;gt;例: 剩十二张没听牌]
        C ~~~ D[快跑&amp;lt;br/&amp;gt;进张还在抢时间&amp;lt;br/&amp;gt;例: 两面听六张剩半局]
        D ~~~ RIGHT[进张多]
    end
    BOT --- DOWN[纵轴 ↓ 剩余时间少]
    style A fill:#fff3cd
    style B fill:#ffe6cc
    style C fill:#f8d7da
    style D fill:#d4edda&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两条边界记死：&lt;strong&gt;进张 &amp;lt; 2 立刻换方向（左半区行动），剩 15 张只保听牌（下半区行动）&lt;/strong&gt;。右下象限是血战到底最常见的好局面——进张还在但时间不多，别犹豫，最快胡法锁收益。&lt;/p&gt;
&lt;h3 id=&#34;45-血战到底的三条特有推论&#34;&gt;4.5 血战到底的三条特有推论&lt;/h3&gt;
&lt;p&gt;规则特殊，策略就有别处用不上的推论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先胡是&amp;quot;锁仓&amp;quot;，不是&amp;quot;胜利&amp;quot;&lt;/strong&gt;。你第一个平胡跑了，只是锁定 2 分；剩下三人继续打，大牌仍在你走后兑现。所以领先时&lt;strong&gt;快跑锁收益&lt;/strong&gt;，落后时反而要&lt;strong&gt;做大搏翻盘&lt;/strong&gt;——这和推倒胡&amp;quot;先胡为王&amp;quot;的直觉相反。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查叫把尾盘变成了保底竞赛&lt;/strong&gt;。没胡的人要给听牌者赔分，所以尾盘的及格线是&amp;quot;听牌&amp;quot;而不是&amp;quot;胡牌&amp;quot;。最后 15 张，一切决策让位于&amp;quot;听上&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点炮一人付，防守成本低&lt;/strong&gt;。别人自摸三家分摊、你点炮你独赔——你打熟张&amp;quot;放生&amp;quot;别人自摸，损失远小于你点炮。所以防守期打熟张不仅是避险，还是&lt;strong&gt;把损失转移给全桌分摊&lt;/strong&gt;的理性选择。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三种结算方式的赔付流向对比，一看就懂为什么&amp;quot;防守打熟张&amp;quot;是理性选择：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 点炮[你点炮: 你独赔]
        Y1[你 -8分] --&amp;gt; H1[胡牌者 +8分]
        O1[另两家 0分]
    end
    subgraph 自摸[对手自摸: 三家分摊]
        Y2[你 -3分] --&amp;gt; H2[胡牌者 +9分]
        O2[另两家各 -3分] --&amp;gt; H2
    end
    subgraph 查叫[流局查叫: 没听赔听牌]
        Y3[你没听 -2分] --&amp;gt; H3[听牌者 +2分]
    end
    style Y1 fill:#f8d7da
    style Y2 fill:#fff3cd
    style Y3 fill:#d4edda&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;同样是对手胡牌，你点炮亏 8 分、放生他自摸只亏 3 分——&lt;strong&gt;防守期每打一张熟张，赚的就是这个差价。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;46-新手常见错误清单&#34;&gt;4.6 新手常见错误清单&lt;/h3&gt;
&lt;p&gt;对照检查，改掉一条涨一截胜率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只记自己牌，不看池子&lt;/strong&gt;——搭子死活取决于关键张已见几张，不看池子等于闭眼打牌；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;见碰就碰&lt;/strong&gt;——暴露信息、锁死手牌结构，快跑路线尤其要忍住；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死抱死搭子&lt;/strong&gt;——7 万已见 3 张还抱着 6、8 万不拆，每巡都在付租金；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做大不看缺门&lt;/strong&gt;——三家都抢你这门还硬做清一色，供给断了；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尾盘还想着翻盘&lt;/strong&gt;——剩 15 张没听牌，唯一 KPI 是听牌防查叫；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不跟随到最后反着用&lt;/strong&gt;——该止损时硬追生张点炮，该搏的时候（落后、时间早）却提前保守。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;47-一页纸决策框架&#34;&gt;4.7 一页纸决策框架&lt;/h3&gt;
&lt;p&gt;把整章压缩成一张每巡都走的决策图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[每一巡] --&amp;gt; B{信息更新:&amp;lt;br/&amp;gt;关键张已见数/&amp;lt;br/&amp;gt;三家缺门/谁听牌了}
    B --&amp;gt; C{阶段判断}
    C --&amp;gt;|序盘: 定缺+定路线| D{起手支持做大?&amp;lt;br/&amp;gt;9张+同花色/4对+&amp;lt;br/&amp;gt;且三家不抢这门}
    D --&amp;gt;|是| E[做大路线:&amp;lt;br/&amp;gt;积极碰杠, 屯关键张]
    D --&amp;gt;|否| F[快跑路线:&amp;lt;br/&amp;gt;牌效优先, 能不碰不碰]
    C --&amp;gt;|中盘| G{有人已听牌&amp;lt;br/&amp;gt;且我生张密集?}
    G --&amp;gt;|是| H[防守: 只打熟张&amp;lt;br/&amp;gt;放生自摸, 不点炮]
    G --&amp;gt;|否| I{我的关键进张&amp;lt;br/&amp;gt;剩余&amp;gt;=4?}
    I --&amp;gt;|是| J[继续推进&amp;lt;br/&amp;gt;选进张最多的打]
    I --&amp;gt;|否| K[拆死搭子&amp;lt;br/&amp;gt;换听熟张方向]
    C --&amp;gt;|尾盘: 剩15张内| L{已听牌?}
    L --&amp;gt;|否| M[唯一目标: 听牌防查叫&amp;lt;br/&amp;gt;听剩张最多的熟张]
    L --&amp;gt;|是| N[宽听口优先&amp;lt;br/&amp;gt;不为一番改窄]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;四个层级的关系：&lt;strong&gt;战略层定方向，信息层供弹药，战术层管执行，概率层负责喊停。&lt;/strong&gt; 新手先把战术层的&amp;quot;进张最大化&amp;quot;练成肌肉记忆，再逐步叠加记牌和读牌，最后学会用期望值喊停——这条学习路径本身也是按依赖关系排的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五贝密的技术亮点&#34;&gt;五、贝密的技术亮点&lt;/h2&gt;
&lt;p&gt;最后回到工程，挑几个我认为值得讲的点。&lt;/p&gt;
&lt;h3 id=&#34;51-用-byte-编码整副牌一副麻将只要几十个字节&#34;&gt;5.1 用 byte 编码整副牌：一副麻将只要几十个字节&lt;/h3&gt;
&lt;p&gt;贝密把每张麻将牌编码成一个 &lt;code&gt;byte&lt;/code&gt;：万/筒/条各占 0~107（每门 36 个编号 = 9 个面值 × 4 张），牌面值 = &lt;code&gt;(card % 36) / 4&lt;/code&gt;，花色 = &lt;code&gt;card / 36&lt;/code&gt;；风牌用负数。这样 108 张牌的牌墙就是一个 &lt;code&gt;byte[108]&lt;/code&gt;，序列化后不到 80 字节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：编号 12 这张牌，&lt;code&gt;12 / 36 = 0&lt;/code&gt;（万子），&lt;code&gt;(12 % 36) / 4 = 3&lt;/code&gt;（第 4 个面值）——就是&amp;quot;四万&amp;quot;。一个除法一个取模就解出了花色和面值，不用查表、不用对象，广播出牌消息时网络上跑的就是 &lt;code&gt;&amp;quot;12,13&amp;quot;&lt;/code&gt; 这样的字符串。&lt;/p&gt;
&lt;h3 id=&#34;52-胡牌判断不用-3n2-递归用分堆--贪心摘顺子&#34;&gt;5.2 胡牌判断：不用 3N+2 递归，用&amp;quot;分堆 + 贪心摘顺子&amp;quot;&lt;/h3&gt;
&lt;p&gt;经典麻将胡牌算法是 3N+2 递归枚举。贝密的 &lt;code&gt;GameUtils.processMJCard&lt;/code&gt; 走了一条不同的路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 14 张手牌按 &lt;code&gt;key = 面值 + 9 × 花色&lt;/code&gt; 放进字典计数，&lt;strong&gt;先查定缺&lt;/strong&gt;——缺门花色还在手里，直接不能胡（四川规则前置拦截）；&lt;/li&gt;
&lt;li&gt;按计数把牌分成三堆：&lt;code&gt;others&lt;/code&gt;（单张）、&lt;code&gt;pairs&lt;/code&gt;（对子）、&lt;code&gt;kezi&lt;/code&gt;（刻子）；&lt;/li&gt;
&lt;li&gt;对单张堆排序后&lt;strong&gt;贪心摘顺子&lt;/strong&gt;：同花色且 key 连续的 k/k+1/k+2 三张直接移除；&lt;/li&gt;
&lt;li&gt;终止条件：单张摘空且恰好剩 1 对将（或 7 对——七对牌型天然被&amp;quot;pairs 堆 = 14 张&amp;quot;覆盖）；&lt;/li&gt;
&lt;li&gt;对子多于 1 对时，把多余对子拆开当顺子候选再摘；全无刻子时枚举刻子拆开试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;源码 &lt;code&gt;main()&lt;/code&gt; 里作者还放了性能测试：&lt;strong&gt;500 万次胡牌判断&lt;/strong&gt;直接计时。要说明的是，这是个简化实现——作者自己在注释里承认某些&amp;quot;三连对&amp;quot;式的特殊牌型没完整处理。对在线棋牌的实际负载来说，这种&amp;quot;足够快、覆盖主流牌型&amp;quot;的取舍是合理的。&lt;/p&gt;
&lt;h3 id=&#34;53-斗地主牌型识别频次直方图一把梭&#34;&gt;5.3 斗地主牌型识别：频次直方图一把梭&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ActionTaskUtils.identification(byte[])&lt;/code&gt; 的做法很干净：先统计同点数张数的分布（几个单张、几个对子、几个三张、几个炸弹），再用 &lt;code&gt;switch(种类数, 最大点数)&lt;/code&gt; 一把识别全部牌型——单张、对子、三带一/二、顺子、连对、飞机带翅、四带二、炸弹、王炸。比较两手牌大小的 &lt;code&gt;allow()&lt;/code&gt; 则只有三条规则：王炸最大、炸弹管一切非炸弹、同牌型比最大点数。AI 托管出牌也复用这套识别，找&amp;quot;能压住上家的最小牌&amp;quot;。&lt;/p&gt;
&lt;h3 id=&#34;54-自研极简状态机统一驱动两种游戏&#34;&gt;5.4 自研极简状态机统一驱动两种游戏&lt;/h3&gt;
&lt;p&gt;斗地主和麻将的流程差异巨大（抢地主 vs 定缺、出牌规则完全不同），但贝密用一个抽象统一了：&lt;code&gt;BeiMiStateMachine&lt;/code&gt; + 按事件名查 transition 表 + Action 链。新增一种游戏 = 写一组 Action + 一份状态配置，调度、计时、广播的基础设施全部复用。&lt;/p&gt;
&lt;h3 id=&#34;55-把缓存过期回调当游戏计时器用&#34;&gt;5.5 把缓存过期回调当游戏计时器用&lt;/h3&gt;
&lt;p&gt;这是我觉得最&amp;quot;巧&amp;quot;的设计。棋牌游戏里到处是倒计时：出牌 8 秒、杠碰胡决策 5 秒、抢地主超时自动放弃、房间超时补机器人。贝密没用 Quartz、没开线程池，而是用一个 &lt;code&gt;sharpExpiry&lt;/code&gt; 的 &lt;strong&gt;cache2k 缓存&lt;/strong&gt;当计时器：key 是 roomid，value 是任务对象，&lt;strong&gt;到期自动触发过期监听器执行任务&lt;/strong&gt;。玩家主动操作了？&lt;code&gt;remove(roomid)&lt;/code&gt; 就把倒计时取消了；要重置？put 一个新任务进去。零额外线程，语义严丝合缝。&lt;/p&gt;
&lt;h3 id=&#34;56-实时状态与持久化彻底解耦&#34;&gt;5.6 实时状态与持久化彻底解耦&lt;/h3&gt;
&lt;p&gt;牌局进行中的所有状态（房间、Board、玩家手牌、撮合队列）全在 &lt;strong&gt;Hazelcast&lt;/strong&gt; 分布式内存里，MySQL 只在结算时被&lt;strong&gt;异步&lt;/strong&gt;写入——通过 &lt;strong&gt;Disruptor&lt;/strong&gt; 的 RingBuffer 把变更事件丢给消费者，JPA 和 Elasticsearch 双写（ES 用于运营后台的战绩检索）。注释里还有一条重要的工程纪律：&amp;ldquo;只有一个地方对 GameRoom 二次写入，避免分布式锁&amp;rdquo;——用设计约束换掉了锁的开销。另外，房卡模式建房时的局数等规则赋值用了 &lt;strong&gt;Drools&lt;/strong&gt; 规则引擎，规则写在 &lt;code&gt;.drl&lt;/code&gt; 文件里，运营改规则不用改代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六总结与反思&#34;&gt;六、总结与反思&lt;/h2&gt;
&lt;p&gt;回顾这个项目，对新手最有价值的三个认知是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;联网游戏 = 服务器权威 + 按人过滤的状态分发&lt;/strong&gt;。理解了&amp;quot;客户端只是显示器和遥控器&amp;quot;，就看懂了所有联网游戏的骨架。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;棋牌游戏的复杂度不在算法，在工程&lt;/strong&gt;。胡牌判断几百行就写完了，真正难的是断线重连、超时托管、并发房间这些&amp;quot;无聊&amp;quot;的部分——它们决定了玩家半夜打牌掉线时骂不骂人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做游戏的人必须会玩游戏&lt;/strong&gt;。第四章的四川麻将策略——分层记牌、由&amp;quot;打出&amp;quot;反推&amp;quot;留下&amp;quot;、用剩余进张 × 剩余巡数做期望决策——不只是给玩家的攻略，也是当初我们校验 AI 托管和出牌提示是否合理的标尺。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然也有粗糙之处，如实记录：README 宣称的德州扑克在 0.8.0 里只有枚举占位并未实现；胡牌算法是简化版；源码里散落的 &lt;code&gt;System.out.println&lt;/code&gt; 和作者自注的 bug 注释（比如多人同时可碰时先碰者胜出的竞态问题）都还在。但作为一个学习联网游戏开发的开源样本，它把一个房卡棋牌系统该有的零件——长连接、房间状态机、计时器、托管、异步落盘——都真实地摆在了那里，这比完美更有价值。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
