<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>jojoma (daorren)</title>
    <link>https://ruby-china.org/jojoma</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>网络协议——写给每个懂点编程的同学</title>
      <description>&lt;h2 id="网络协议——写给每个懂点编程的同学"&gt;网络协议——写给每个懂点编程的同学&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;缘起是我想了解一下底层网络的原理，看了几天《TCP/IP 详解 卷一》，但是这部书读起来十分吃力。这时候正好看到 &lt;a href="https://news.ycombinator.com/item?id=14468471" rel="nofollow" target="_blank" title=""&gt;hn 谈到&lt;/a&gt;这篇 &lt;a href="https://www.destroyallsoftware.com/compendium/network-protocols?share_key=97d3ba4c24d21147" rel="nofollow" target="_blank" title=""&gt;Network protocols&lt;/a&gt;。所以特地翻译过来，希望也能有人从中受益。本人知识有限，在一些译文处补充了原文用词来辅助理解，翻译不对之处欢迎指正。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;网络栈技术，完成了几件看起来不可能的任务：它在不可靠网络基础上，实现了可靠数据传输，传输过程中鲜有可察觉的问题出现。它在网络拥塞时能够平滑适应。它给网络中上十亿的活动节点提供地址。它能在受损的网络基础设施中，往正确的路线发送数据包，即使是乱序到达在接收端，也能将数据包重新装配成正确的顺序。它适应了深奥的模拟 analog 硬件需求，比如以太网电缆两端的电荷平衡。网络技术工作得如此之好，以至于网络用户们从没听说过它们，甚至大部分编写程序的工程师们也不知道底层究竟是如何工作的。&lt;/p&gt;
&lt;h2 id="网络路由"&gt;网络路由&lt;/h2&gt;
&lt;p&gt;在古老的模拟电话年代，打电话意味着建立一个连接你和你朋友的电话的、持续的、电子连接。仿佛真的有根电话线，直接在你和朋友之间工作。当然实际没有这根线——电话连接经过了复杂的交换系统——但是这个连接电子上等效于一根线。&lt;/p&gt;

&lt;p&gt;互联网的节点数太多了，不能套用这个方式。我们不可能给每一台机器与另一台机器都建立一个直接连接的、不被打断的路线用于通信。&lt;/p&gt;

&lt;p&gt;相应地，数据是由一个路由器发送给下一个路由器，每次传输都使数据离目的地更近一步，整个链式传递过程像&lt;a href="https://www.wikiwand.com/en/Bucket_brigade" rel="nofollow" target="_blank" title=""&gt;传桶队列 Bucket-brigade&lt;/a&gt;。举个例子，从我的笔记本到 google.com 之间，途径的每个路由器都连接着许多其他路由器，各自维护着一组不精确的路由表，路由表表示出哪些路由器更靠近互联网的哪一部分。当一组目的地是 google.com 的数据包到达时，路由器在路由表上进行快速查找，并发送数据包到更靠近谷歌的地方。数据包很小，所以传输链上路由器之间的数据传递耗时也极短。&lt;/p&gt;

&lt;p&gt;路由可以拆分成两个子问题。第一个是，地址：数据的目的地用什么表示？这个由 &lt;a href="https://www.wikiwand.com/en/Internet_Protocol" rel="nofollow" target="_blank" title=""&gt;IP 协议&lt;/a&gt;，其中的 IP 地址来处理。IPv4，作为最广泛使用的 IP 版本，提供的地址空间只有 32 位，已经&lt;a href="https://blogs.igalia.com/dpino/2017/05/25/ipv4-exhaustion/" rel="nofollow" target="_blank" title=""&gt;全部被分配&lt;/a&gt;，所以添加一个新的节点到公开的互联网只能重用已存在的 IP 地址。IPv6，允许使用 2 ^ 128 个地址（大约 10 ^ 38），在 2017 年只有 20% 左右的采用率。&lt;/p&gt;

&lt;p&gt;既然已经解决了地址的问题，我们现在需要知道如何在互联网上路由数据包到其目的地。路由是非常快的，没有时间在远端数据库中查询路由信息（所以只能在本地）。这速度有多快呢？举个例子，Cisco ASR 9922 路由器拥有着每秒最大 160TB 的处理能力。假设数据包是完整的 1.5KB（12000 位 bit），那么每秒有 133 亿个数据包流经这个 19 英寸小机器。&lt;/p&gt;

&lt;p&gt;为了快速路由，路由器维护着指示着到达其他 IP 地址组路径的路由表。当一个新的数据包到达时，路由器查询路由表，告知这个数据包最接近目的地的路由器。这个路由器会把数据包发送到下一个路由器，然后再往下一个发送。&lt;a href="https://en.wikipedia.org/wiki/Border_Gateway_Protocol" rel="nofollow" target="_blank" title=""&gt;BGP 协议&lt;/a&gt;的工作就是，在不同路由器之间沟通，保证路由表是最新的。&lt;/p&gt;
&lt;h2 id="转换成数据包packet switching"&gt;转换成数据包 packet switching&lt;/h2&gt;
&lt;p&gt;如果互联网的工作方式是，路由器互相之间沿着线路传递数据，那如果数据很大会发生什么？比如说，如果我们请求一个 88.5MB 的视频&lt;a href="https://www.destroyallsoftware.com/talks/the-birth-and-death-of-javascript" rel="nofollow" target="_blank" title=""&gt;The Birth &amp;amp; Death of JavaScript&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我们可以试试设计一个网络，在这当中 88.5MB 的文件直接由网络服务器发送给第一个路由器，然后第二个，如此下去。不幸的是，这样的网络不可能在互联网级别的规模下工作，甚至内网规模下都很难。&lt;/p&gt;

&lt;p&gt;首先，计算机的存储量是有限的。如果一个给定的路由只有 88.4MB 的可用缓存，那它就不能存储这个 88.5MB 的视频文件。这个数据会被直接丢弃，甚至更糟，我完全不知道这件事的发生。如果路由器是如此忙碌以至于丢弃了数据之后，都没时间告诉我它丢弃了数据。&lt;/p&gt;

&lt;p&gt;其次，计算机都是不可靠的。有时，路由节点崩溃。有时，船只的⚓️意外损坏水下光缆，&lt;a href="https://www.wikiwand.com/en/2008_submarine_cable_disruption" rel="nofollow" target="_blank" title=""&gt;导致互联网一大部分不可访问&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;基于这些提到的以及更多原因，我们不会在互联网中传递 88.5MB 大小的消息。相反，我们把数据拆分成许多数据包，大小通常在 1.4KB 左右。我们的视频文件将被拆分成 63214 左右个分隔的数据包用于传输。&lt;/p&gt;
&lt;h2 id="乱序数据包"&gt;乱序数据包&lt;/h2&gt;
&lt;p&gt;使用抓包工具&lt;a href="https://www.wireshark.org/" rel="nofollow" target="_blank" title=""&gt;Wireshark&lt;/a&gt;观察&lt;a href="https://www.destroyallsoftware.com/talks/the-birth-and-death-of-javascript" rel="nofollow" target="_blank" title=""&gt;The Birth &amp;amp; Death of JavaScript&lt;/a&gt;的一次真实传输，我能看到接收了一共 61807 个数据包，每个 1432 字节。两者相乘，我们得到 88.5MB，这正是视频文件的大小。（这不包括其他协议的开支，如果包含的话，数字会更大些）&lt;/p&gt;

&lt;p&gt;这次传输是基于 HTTP，一种基于&lt;a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol" rel="nofollow" target="_blank" title=""&gt;TCP&lt;/a&gt;的协议。传输花了 14 秒，所以平均每秒有 4400 个数据包到达，或者说每个数据包花了 250 毫秒到达。在这 14 秒中，我的计算机接收了所有 61807 个数据包，也许不是按顺序接收，在接收过程中进行重新装配成完整文件。&lt;/p&gt;

&lt;p&gt;TCP 数据包重新组装使用的是一种可想象的最简单的机制：计时器。每个数据包在发送时都被赋予一个序列号。在接收端，数据包按序列号排列。一旦他们全部排好顺序，没有间隔，我们就知道整个文件都接收到了没有丢失。&lt;/p&gt;

&lt;p&gt;（真实情况下，TCP 序列号并非是每次增加一的整数，但这个细节在本文中并不重要。）&lt;/p&gt;

&lt;p&gt;就算如此，那我们怎么知道什么时候文件接收完成呢？TCP 对此一无所知，这个是更高级别协议的职责。举个例子，HTTP 响应 response 中包含一个叫做 &lt;code&gt;Content-Length&lt;/code&gt; 的头部，说明了返回响应的总长度。客户端读取这个头字段，然后一直读取 TCP 数据包，重新装配它们，直到达到了此头字段指定的数据大小。这是为什么 HTTP 头部（以及其他大多数协议的头部）比响应载荷 payload 率先到达的原因之一，否则我们都不能知晓载荷的大小。&lt;/p&gt;

&lt;p&gt;当我们在说客户端的时候，我们实际在说整个接收数据的计算机。TCP 组装是在内核中完成的，所以浏览器、curl 和 wget 这样的应用不需要手动重新装配 TCP 数据包。但是内核不处理 HTTP，所以应用需要理解 &lt;code&gt;Content-Length&lt;/code&gt; 头字段并知晓需要读取多少字节。&lt;/p&gt;

&lt;p&gt;有了序列号和数据包重新排序，我们能传输大量数据，即使数据包是乱序的。但如果一个数据包在传输中丢失了，在 HTTP 响应中留下一个空洞怎么办？&lt;/p&gt;
&lt;h2 id="传输窗口transmission winsow与慢启动slow start"&gt;传输窗口 transmission winsow 与慢启动 slow start&lt;/h2&gt;
&lt;p&gt;我开着&lt;a href="https://www.wireshark.org/" rel="nofollow" target="_blank" title=""&gt;Wireshark&lt;/a&gt;下载了&lt;a href="https://www.destroyallsoftware.com/talks/the-birth-and-death-of-javascript" rel="nofollow" target="_blank" title=""&gt;The Birth &amp;amp; Death of JavaScript&lt;/a&gt;。查看抓包记录，我能看到数据包一个接一个被成功接收。&lt;/p&gt;

&lt;p&gt;举个例子，一个序列号为 563321 的数据包到达了。像所有 TCP 数据包一样，它包含了一个“下一个包序号”，指示着接下来一个数据包的序列号。这个包的“下一个包序号”是 564753。传输过程中下一个数据包，的序列号确实是 564753，所以一切正常。这发生了数千次，随着连接开始加速传输数据。&lt;/p&gt;

&lt;p&gt;有时候，我的计算机发出一条消息给服务器说，打个比方，“我已经接收了包序号小于或等于 564753 的所有数据包。”这称为 ACK，确认 acknowledgement 的简写，我的计算机确认接收服务器的数据包。在一个新的连接中，Linux 内核每接收 10 个数据包后，就发出一个 ACK。数字 10 由常数 &lt;code&gt;TCP_INIT_CWND&lt;/code&gt; 控制，常数在内核源码中被&lt;a href="http://elixir.free-electrons.com/linux/v4.5/source/include/net/tcp.h#L220" rel="nofollow" target="_blank" title=""&gt;定义&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;（&lt;code&gt;TCP_INIT_CWND&lt;/code&gt; 里的 CWND 表示 拥塞窗口 congestion window：同一时刻可以传输的数据总大小。）如果网络变得拥塞（超负荷），窗口大小减小，从而减慢数据包的传输。&lt;/p&gt;

&lt;p&gt;十个数据包是大约 14KB，所以一开始的速度限制是 14KB。这是 TCP 慢启动的部分：连接建立时拥塞窗口很小。如果没有数据包丢失，接受者将持续增加拥塞窗口，允许同时传输更多数据包。&lt;/p&gt;

&lt;p&gt;最终，将会有数据包丢失，所以接收窗口会减小，减慢传输。像这样自动调整拥塞窗口，以及其他参数，数据发送者和接收者让数据传输最大化利用网络带宽。&lt;/p&gt;

&lt;p&gt;这发生在连接的两端：每端都发出 ACK 确认消息，也维护各自的拥塞窗口。不对称窗口允许协议用不对称的上下行带宽，最大化利用网络连接，就像大多数住宅区和移动网络连接一样。&lt;/p&gt;
&lt;h2 id="可靠传输"&gt;可靠传输&lt;/h2&gt;
&lt;p&gt;计算机是不可靠的，由计算机组成的网络更加不可靠。在像互联网这样的大规模网络中，失败是操作中常见的一部分，并且必须得到良好处理。在一个数据包网络中，这意味着重传：如果客户端接收了序号 1 和 3 的数据包，但没有接收到 2，那么它需要要求服务器重新发出丢失的数据包。&lt;/p&gt;

&lt;p&gt;当每秒接收上千数据包时，比如下载我们的 88.5MB 视频时，错误几乎百分之百会产生。为了给大家展示，让我们打开 Wireshark。很多数据包接收，一切看起来很正常。每个数据包都有一个“下一个包序号”，紧接着一个带着这个序号的数据包。&lt;/p&gt;

&lt;p&gt;突然问题出现了。第 6269 个数据包的“下一个包序号”是 7208745，但那个数据包并没有到达。相反，序列号为 7211609 的数据包到达了。这是一个乱序数据包：有东西丢失了。&lt;/p&gt;

&lt;p&gt;我们很难说出究竟什么出了问题。也许互联网中的一个中间路由器超负荷了，也许是我的本地路由器超负荷了。也许有人打开了微波炉，产生了电磁干扰，减慢了我的无线连接。无论如何，这个数据包丢失了，唯一的迹象是意外接收到的数据包。&lt;/p&gt;

&lt;p&gt;TCP 并没有特别的“我丢失了一个数据包”消息。相反，ACK 消息会被巧妙地复用来表明数据丢失。任何乱序的数据包，会导致接收者重复确认最后的“正确的”数据包——正确顺序的最后一个。实际上，接收者说的是：“我确认接收到了数据包 5。在那之后我也接收到了别的数据，但我知道那不是数据包 6，因为它并不匹配数据包 5 的下一个包序号。”&lt;/p&gt;

&lt;p&gt;如果只是两个数据包在传输时调换了顺序，这会导致一次额外的 ACK，等到乱序数据包接收到之后一切就能正常继续下去。但是如果有个数据包真的丢失了，意外数据包将会一直到达，因而接收者会持续发出重复的、最后一个正常数据包的 ACK 消息。这会导致上百个重复的 ACK 消息。&lt;/p&gt;

&lt;p&gt;当数据发送者一下看到三个重复 ACK 消息，它就假定紧接着的数据包丢失了，并进行重新传输。这被称为 TCP 快速重传，因为它比以前的基于超时的做法要快一些。有趣的是，协议自身不会显式地去说“请立即重传这个消息！”相反，多个 ACK 消息从协议自然产生，作为重传的触发器。&lt;/p&gt;

&lt;p&gt;（一个有意思的思维实验：如果一部分重复的 ACK 消息也丢失了，没能到达数据发送者，会发生什么？）&lt;/p&gt;

&lt;p&gt;重传甚至在网络正常工作时都十分常见。在对下载 88.5MB 视频进行抓包的过程中，我看到了&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;因为持续性成功传输，拥塞窗口迅速增大到了将近 1MB。&lt;/li&gt;
&lt;li&gt;数千数据包按顺序出现，一切正常。&lt;/li&gt;
&lt;li&gt;一个数据包到达顺序不正确。&lt;/li&gt;
&lt;li&gt;数据继续以每秒几 MB 的速度涌入，但丢失的数据包依旧没出现。&lt;/li&gt;
&lt;li&gt;我的计算机发出了不少重复的最后正常数据包的 ACK 消息，但内核也存下待处理的乱序数据包，以备后续的重新组装。&lt;/li&gt;
&lt;li&gt;服务器接收到了重复的 ACK，并重新发送了丢失的数据包&lt;/li&gt;
&lt;li&gt;我的客户端发出之前丢失的数据包，以及后续数据包的确认接收的消息。简单确认最近的数据包即可，它会隐式地确认之前所有的数据包都被接收。&lt;/li&gt;
&lt;li&gt;传输继续，但由于丢失的数据包，拥塞窗口变小了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这就是正常情况，这些在每次我对完整下载进行抓包时都会产生。TCP 在自己的职责上做得是如此出色，以至于我们在日常使用中从没考虑过网络是不可靠的，尽管在正常情况下网络都会例行性地失败。&lt;/p&gt;
&lt;h2 id="物理网络"&gt;物理网络&lt;/h2&gt;
&lt;p&gt;所有这些网络数据，都必须通过像铜线、光缆、无线电这样的物理媒介进行传输。而在物理层协议之中，以太网最为著名。它在互联网兴起之初的流行，导致了我们在设计其他协议的时候必须适应它的局限。&lt;/p&gt;

&lt;p&gt;首先，让我们把物理细节弄清楚。以太网与 RJ45 接头关系最紧密，后者看起来像更大的八针 eight-pin 版本的四针手机插孔 four-pin phone jacks。以太网也连接着 cat5（或 cat5e，或 cat6，或 cat7）电缆，该电缆包含了拧成 4 对的 8 根电线。其他媒介也存在，但我们在家中最有可能遇到的就是这些：裹在保护套下的 8 根电线，以及与之相连的 8 针插头。&lt;/p&gt;

&lt;p&gt;以太网是一个物理层协议：描述了位信息如何转换成电线中的数字信号。它也是一个链路 link 层协议：描述了两个节点之间的直接连接。然而，这是单纯的点对点，对网络中数据是如何路由的并不关心。以太网这里没有 TCP 连接中的连接概念，也没有 IP 地址中的可重新分配的地址概念。&lt;/p&gt;

&lt;p&gt;作为一个协议，以太网有两个主要的工作。第一，每个设备需要意识到它连接着一些东西，并且连接速度这样的参数需要协商。&lt;/p&gt;

&lt;p&gt;第二，一旦链路 link 建立，以太网需要携带信息。像更高层次的 TCP 和 IP 协议一样，以太网的数据也拆分成数据包。数据包的核心是数据帧，帧有 1.5KB 的载荷，外加 22 字节的头部信息。头部信息中包含源 MAC 地址和目的地 MAC 地址，载荷长度，以及校验和 checksum 这样的信息。这些字段令人熟悉：工程师常常需要处理地址、长度以及校验和，我们也知道为什么它们是必须的。&lt;/p&gt;

&lt;p&gt;数据帧接着被其他层的头数据包裹起来，构造出完整的数据包。这些头部数据很...奇怪。它们已经开始和模拟电路系统的底层现实发生碰撞了，所以我们绝不想把这些数据放到软件协议中去。一个完整的以太网数据包包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;序言 preamble，由 56 位交替的 0 和 1 构成（7 字节）。设备使用这个来同步时钟，有点像人们数数发令“1-2-3-开始！”计算机不能数数超过 1，所以他们通过说“10101010101010101010101010101010101010101010101010101010”来同步&lt;/li&gt;
&lt;li&gt;一个 8 位（1 字节）起始帧分隔符，通常是十进制数字 171（二进制表示是 10101011）。它标识了序言的结尾，注意分隔符中开始还在重复“10”，直到末尾有个“11”。&lt;/li&gt;
&lt;li&gt;核心数据帧，包含了源地址、目标地址、载荷等等，如前所述。&lt;/li&gt;
&lt;li&gt;一个 96 位（12 字节）的数据包间隔，其中的行是留空的。大胆猜测一下，这是留给设备休息的，因为它们很累了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总结一下上面：我们想要传输 1.5KB 数据。我们添加 22 字节的包含源地址、目标地址、数据大小以及校验和的头信息以创建数据帧。我们再添加额外的 22 字节的数据，为了适应硬件需求，这些构成了完整的以太网数据包。&lt;/p&gt;

&lt;p&gt;你也许会以为以太网已经是网络技术栈的最底层了。不是这样，但事情确实变得更奇怪了，因为模拟世界的对技术的影响更甚 pokes through even more。&lt;/p&gt;
&lt;h2 id="现实世界中的网络"&gt;现实世界中的网络&lt;/h2&gt;
&lt;p&gt;数字系统并不存在，一切都是模拟的。&lt;/p&gt;

&lt;p&gt;假设我们有一个 5 伏特 CMOS 系统，（CMOS 是一种数字系统，不熟悉也没关系。）这意味着，完全开启 fully-on 的信号将是 5 伏特，完全关闭的信号是 0 伏特。但是没有信号是完全开闭的，物理世界不这样工作。实际上，我们的 5 伏特 CMOS 系统，会把任何高于 1.67 伏特的信号看做 1，低于 1.67 伏特的信号看做 0。&lt;/p&gt;

&lt;p&gt;（1.67 是 5 的 1/3。我们不用关心为什么分界线在 1/3。当然如果你想深究，这里有&lt;a href="https://www.wikiwand.com/en/Logic_level" rel="nofollow" target="_blank" title=""&gt;维基百科说明&lt;/a&gt;。另外，以太网不是 CMOS，甚至跟 CMOS 都没有关系，但 CMOS 和它的 1/3 分界线能用来做一个简单说明 make for a simple illustration）&lt;/p&gt;

&lt;p&gt;我们的以太网数据包必须经由一条物理线，也就是改变电线中的电压。以太网是一个 5 伏特的系统，所以我们会天真地以为，以太网协议中的 1 位 bit 是电线中的 5 伏特，0 位是 0 伏特。但是有两个问题：首先，电压范围是 -2.5 伏特到 +2.5 伏特。其次，更奇怪的是，每组 8 位信息在到达电线之前，都会被拓展成 10 位。&lt;/p&gt;

&lt;p&gt;8 位可以有 256 种取值，10 位有 1024 种取值，所以可以想象有张表在它们之间映射。每个 8 位的字节能被映射成 4 种 10 字节的信息，后者到达接收终点之后会被还原成同一个 8 位字节。举个例子，10 位的值 00.0000.0000 也许映射到 8 位 0000.0000。但是也许 10 位值 10.1010.1010 也能映射到同一个 8 位字节。当以太网设备不管看到 00.0000.0000 还是 10.1010.1010，它都能理解这是字节 0（二进制 0000.0000）。&lt;/p&gt;

&lt;p&gt;（警告：下面可能需要一些电子电路知识）&lt;/p&gt;

&lt;p&gt;上面这种映射的存在，是为了服务一个极其模拟的需求 extremely analog need：平衡设备中的电压。假设这种 8 位到 10 位的编码不存在，并且我们需要发送的数据恰好都是二进制 1。以太网的电压范围是 -2.5 伏特到 +2.5 伏特，所以我们会使以太网线的电压维持在 +2.5 伏特，继而一直从线的另一端吸引电子过来 pulling electrons。&lt;/p&gt;

&lt;p&gt;为什么我们要关心一端从另一端获取电子呢？因为模拟世界是混乱的，可能会产生各种各样意外的影响。举个例子，这样会给低通滤波器中使用的电容器充电，使得信号电平中产生偏移，最终导致位错误。这些错误需要时间积累，但我们显然不希望，仅仅因为我们传输的二进制 1 比 0 多，两年之后网络设备中突然开始产生数据错误。&lt;/p&gt;

&lt;p&gt;（有关电子电路的说到这里）&lt;/p&gt;

&lt;p&gt;通过使用 &lt;a href="https://www.wikiwand.com/en/8b/10b_encoding" rel="nofollow" target="_blank" title=""&gt;8 位 -10 位 编码&lt;/a&gt;，以太网能保持电线中的 0 和 1 的平衡，即使我们要发送的数据都是 1 或者都是 0。硬件会检测 0 和 1 的比例，映射要发送的 8 位字节到不同的 10 位信号，以达到维持电荷平衡。（新的以太网标准，如 10GB 以太网，使用不同的更复杂的编码系统）&lt;/p&gt;

&lt;p&gt;到此打住，因为我们谈论的已经超出编程的范围了，但是必须要说明的是，还有更多协议相关问题是为了适应物理层。在许多情况下，解决硬件问题的方法，都在软件中实现，比如上文使用 8 位 -10 位编码来修正直流偏移 DC offset。这对我们这样的工程师来说可能有点尴尬：我们习惯于假装软件生活在一个完美的柏拉图式的世界中，没有物理上庸俗的缺陷 devoid of the vulgar imperfections of physicality。事实上，一切都是模拟的，适应这种复杂性是每个人的工作，当然也包括软件。&lt;/p&gt;
&lt;h2 id="相互联接的网络栈"&gt;相互联接的网络栈&lt;/h2&gt;
&lt;p&gt;互联网协议族最好理解为一组层的集合。以太网提供物理数据传输以及两个点对点设备之间的链路。IP 提供了地址层，允许路由器和大规模网络的存在，但是是无连接的，数据包双向传输却无从判断是否到达。TCP 通过使用序列号、确认以及重传，添加了可靠的传输层。&lt;/p&gt;

&lt;p&gt;最终，应用层协议如 HTTP 建立在 TCP 之上。在这一层，我们已经有了地址，以及可靠传输和持续连接的幻觉 illusion。IP 和 TCP 将应用开发者，从重复实现数据包重传、地址处理等等的地狱中拯救出来。&lt;/p&gt;

&lt;p&gt;这些层的独立性是十分重要的。举个例子，当传输 88.5MB 视频有数据包丢失的时候，互联网的网络中枢路由器并不知道；只有我的计算机和网络服务器知道。这个弄丢了原始数据包的路由基础设施，还在尽职地将我计算机发出的许多重复的 ACK 消息路由到目的地去。有可能就是同一个路由器，弄丢了数据包，几毫秒之后又带着重发的数据包来了。这是理解互联网的一个重点：路由基础设施对 TCP 一无所知；它做的仅仅是路由。（当然这也有例外，不过大多数情况下就是这样）&lt;/p&gt;

&lt;p&gt;不同层的协议独立工作，但它们不是分开独立设计的。高层次协议通常建立在低层次协议基础上，HTTP 建立在 TCP 上，TCP 建立在 IP 上，IP 建立在以太网上。更底层的设计决策，即使在几十年之后，也会影响到更高层次的决策。&lt;/p&gt;

&lt;p&gt;以太网是古老的，且涉及物理层，所以它的需求设置了基本参数。一个以太网载荷最大是 1.5KB。&lt;/p&gt;

&lt;p&gt;IP 数据包需要包含于以太网数据帧中。IP 的最小头部大小是 20 字节，所以 IP 数据包的最大载荷是 1500 - 20 = 1480 字节。&lt;/p&gt;

&lt;p&gt;同样，TCP 数据包需要包含于 IP 数据包中。TCP 的最小头部大小也是 20 字节，所以 TCP 的最大载荷是 1480 - 20 = 1460 字节。在现实中，其他头部和协议会占据更多空间，保守估计 TCP 的载荷大小是 1400 字节。&lt;/p&gt;

&lt;p&gt;1400 字节的限制影响了现代协议的设计。举个例子，HTTP 请求通常很小。如果我们把它们塞进一个数据包而不是两个，就能减小丢失请求某部分的可能，从而减少需要 TCP 重传的可能。为了从小请求中挤出每个字节，&lt;a href="http://httpwg.org/specs/rfc7540.html" rel="nofollow" target="_blank" title=""&gt;HTTP/2&lt;/a&gt; 指定了头部压缩，头部通常很小。没有 TCP、IP 和以太网的情境的话，这看起来很不明智：为什么要压缩一个协议的头部，仅仅为了节约几字节大小的空间？因为，正如 HTTP/2 规范在第 2 节的介绍中所说，压缩允许“多个请求被压缩成一个数据包”。&lt;/p&gt;

&lt;p&gt;HTTP/2 的头部压缩是为了适应 TCP 的限制，这个限制来自 IP 的限制，再往上来自以太网的限制。而以太网在上世纪 70 年代发展起来的，1980 年投入商用，并在 1983 年标准化。&lt;/p&gt;

&lt;p&gt;最后一个问题：为什么以太网的载荷大小设置在 1500 字节（1.5KB）呢？其实没有深层次的原因：只是一个很好的权衡考量。每个数据帧中有 42 字节大小的非载荷数据。如果载荷的最大值只有 100 字节，那么只有 70%（100/142）的时间花在发送载荷上。而 1500 字节大小的载荷，意味着大约 97%（1500/1542）的时间用于发送载荷，这样的效率是可观的。再增加数据包的大小的话，会需要设备拥有更大的缓冲区，这使得再提高一两个百分比的效率变得十分困难。简而言之，20 世纪 70 年代末网络设备的 RAM 限制，导致 HTTP/2 引入了头部压缩。&lt;/p&gt;</description>
      <author>jojoma</author>
      <pubDate>Tue, 06 Jun 2017 11:25:55 +0800</pubDate>
      <link>https://ruby-china.org/topics/33160</link>
      <guid>https://ruby-china.org/topics/33160</guid>
    </item>
    <item>
      <title>客户端订阅 ActionCable 的 Channel，JSON 解析错误</title>
      <description>&lt;p&gt;我用 ActionCable（5.0.0 版本）用作 websocket 服务端，全是 setup 基本操作。
自定义的东西也就是在哪里 broadcast&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;after_create_commit&lt;/span&gt; &lt;span class="ss"&gt;:message_broadcast&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_screen_broadcast&lt;/span&gt;
  &lt;span class="no"&gt;ActionCable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt; &lt;span class="s1"&gt;'message_channel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在浏览器，尝试去连接 ws&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://localhost:3000/cable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发日志返回&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Started GET "/cable" for ::1 at 2016-12-25 13:04:05 +0800
Started GET "/cable/" [WebSocket] for ::1 at 2016-12-25 13:04:05 +0800
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能在浏览器的 network 看到 ping 在跳，理论上已经连接上了 ws。&lt;/p&gt;

&lt;p&gt;但是想要订阅 channel 的时候，问题就来了&lt;/p&gt;

&lt;p&gt;搜过&lt;a href="https://github.com/rails/rails/issues/22675" rel="nofollow" target="_blank" title=""&gt;这个问题&lt;/a&gt;，所以知道需要使用 &lt;code&gt;{"command":"subscribe","identifier":"{\"channel\":\"MessageChannel\"}"}&lt;/code&gt;这样的 json&lt;/p&gt;

&lt;p&gt;但是我在浏览器 &lt;code&gt;ws.send({"command":"subscribe","identifier":"{\"channel\":\"MessageChannel\"}"})&lt;/code&gt;
日志里面却返回错误&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There was an exception - JSON::ParserError(419: unexpected token at 'object Object]')
/Users/jojoma/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `parse'
/Users/jojoma/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `parse'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/json/decoding.rb:21:in `decode'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/connection/base.rb:164:in `decode'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/connection/base.rb:90:in `dispatch_websocket_message'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker.rb:58:in `block in invoke'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker.rb:39:in `block in work'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:126:in `call'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:506:in `block (2 levels) in compile'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:455:in `call'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:448:in `block (2 levels) in around'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:286:in `block (2 levels) in halting'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker/active_record_connection_management.rb:14:in `block in with_database_connections'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/connection/tagged_logger_proxy.rb:22:in `block in tag'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/tagged_logging.rb:70:in `block in tagged'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/tagged_logging.rb:26:in `tagged'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/tagged_logging.rb:70:in `tagged'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/connection/tagged_logger_proxy.rb:22:in `tag'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker/active_record_connection_management.rb:14:in `with_database_connections'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:382:in `block in make_lambda'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:285:in `block in halting'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:447:in `block in around'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:455:in `call'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:448:in `block (2 levels) in around'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:286:in `block (2 levels) in halting'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/engine.rb:60:in `block (4 levels) in &amp;lt;class:Engine&amp;gt;'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/execution_wrapper.rb:76:in `wrap'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/engine.rb:55:in `block (3 levels) in &amp;lt;class:Engine&amp;gt;'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:391:in `instance_exec'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:391:in `block in make_lambda'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:285:in `block in halting'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:447:in `block in around'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:455:in `call'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:101:in `__run_callbacks__'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:750:in `_run_work_callbacks'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/callbacks.rb:90:in `run_callbacks'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker.rb:38:in `work'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker.rb:56:in `invoke'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/actioncable-5.0.0/lib/action_cable/server/worker.rb:51:in `block in async_invoke'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:348:in `run_task'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:337:in `block (3 levels) in create_worker'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:320:in `loop'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:320:in `block (2 levels) in create_worker'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:319:in `catch'
/Users/jojoma/.rvm/gems/ruby-2.3.1/gems/concurrent-ruby-1.0.2/lib/concurrent/executor/ruby_thread_pool_executor.rb:319:in `block in create_worker'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在&lt;code&gt;action_cable/connection/base.rb:90:in dispatch_websocket_message&lt;/code&gt;这里打印了一下&lt;/p&gt;

&lt;p&gt;发现，待处理的 websocket_message 居然变成了 &lt;code&gt;"[object Object]"&lt;/code&gt;
而页面上，rails 自己的 js 产生的客户端订阅 channel，传过去的 websocket_message 是没有问题的，打印出来是&lt;code&gt;{"command":"subscribe","identifier":"{\"channel\":\"MessageChannel\"}"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;不知道有人遇到过这样的问题吗？怎么跟最初 github issue 里面描述的不一样呢？&lt;/p&gt;</description>
      <author>jojoma</author>
      <pubDate>Sun, 25 Dec 2016 13:29:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/31999</link>
      <guid>https://ruby-china.org/topics/31999</guid>
    </item>
    <item>
      <title>关于 Object 和 BasicObject</title>
      <description>&lt;p&gt;在 RailsCasts 看到个类，分别继承于 Object 和 BasicObject 时，有不同的行为&lt;/p&gt;

&lt;p&gt;第一种，继承于 Object&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DynamicDelegator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;method_missing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@target.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind_of?&lt;/span&gt; &lt;span class="vi"&gt;@target.class&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# ---------开始测试-----------&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DynamicDelegator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Patient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;DynamicDelegator:0x007fbfec857e98 @target=#&amp;lt;Patient id: 4, first_name: "JO", middle_name: nil, last_name: "JO", birth_date: nil, gender: "Male", status: "Initial", location_id: 1, view_count: 0, is_valid: true, created_at: "2016-09-29 07:36:28", updated_at: "2016-09-29 07:36:28"&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DynamicDelegator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;DynamicDelegator:0x007fbfeb8c9da0 @target=12&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种，继承于 BasicObject&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DynamicDelegator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BasicObject&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;method_missing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@target.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind_of?&lt;/span&gt; &lt;span class="vi"&gt;@target.class&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# ---------开始测试-----------&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DynamicDelegator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Patient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;Patient id: 4, first_name: "JO", middle_name: nil, last_name: "JO", birth_date: nil, gender: "Male", status: "Initial", location_id: 1, view_count: 0, is_valid: true, created_at: "2016-09-29 07:36:28", updated_at: "2016-09-29 07:36:28"&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DynamicDelegator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="c1"&gt;# =&amp;gt; 123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一种情况下，返回一个 DynamicDelegator 类实例，参数保存在其实例变量中，是完全可以理解的。
但是，一旦修改继承，返回的是创建时赋的参数。求问这是为啥呢？&lt;/p&gt;

&lt;p&gt;可能是 new 的时候，执行了一些不知道的东西，导致这样的结果。可是我记得 new = allocate + initialize，不知道在哪里有了变化？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ryan 在视频里说，是因为 Object 有较多实例方法，而 BasicObject 没有。所以继承于干净的 BasicObject，能够更好的 delegate。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我看了 Object 和 BasicObject 之间的方法，也没有找到答案...&lt;/p&gt;</description>
      <author>jojoma</author>
      <pubDate>Wed, 12 Oct 2016 17:46:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/31301</link>
      <guid>https://ruby-china.org/topics/31301</guid>
    </item>
    <item>
      <title>又一个新的 RubyGems 镜像源选择</title>
      <description>&lt;p&gt;清华 TUNA 镜像源，新增 Ruby Gems 镜像。&lt;/p&gt;

&lt;p&gt;使用以下命令替换 gems 默认源&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 添加 TUNA 源并移除默认源
gems sources --add https://mirrors.tuna.tsinghua.edu.cn/rubygems/ --remove https://rubygems.org/
# 列出已有源
gem sources -l
# 应该只有 TUNA 一个
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，编辑 &lt;code&gt;~/.gemrc&lt;/code&gt;，将 &lt;code&gt;https://mirrors.tuna.tsinghua.edu.cn/rubygems/&lt;/code&gt; 加到 &lt;code&gt;sources&lt;/code&gt; 字段。&lt;/p&gt;</description>
      <author>jojoma</author>
      <pubDate>Thu, 18 Aug 2016 23:14:26 +0800</pubDate>
      <link>https://ruby-china.org/topics/30850</link>
      <guid>https://ruby-china.org/topics/30850</guid>
    </item>
    <item>
      <title>新手做 Blog，index 页面底部出现问题</title>
      <description>&lt;h4 id="我跟随教程做blog，其中views/posts/index.html.erb文件内容如下"&gt;我跟随教程做 blog，其中 views/posts/index.html.erb 文件内容如下&lt;/h4&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @posts.each do |post| %&amp;gt;
  &amp;lt;div class=&lt;/span&gt;&lt;span class="s2"&gt;"post_wrapper"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= link_to post.title, post %&amp;gt;&amp;lt;/h2&amp;gt;
    &amp;lt;p class=&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= post.created_at.strftime("%B, %d, %Y") %&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="但是打开浏览器，在index页面底部（上面正常显示title和date），出现下面这样的语句，相当于把数据库内容列出来了："&gt;但是打开浏览器，在 index 页面底部（上面正常显示 title 和 date），出现下面这样的语句，相当于把数据库内容列出来了：&lt;/h4&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
[#&amp;lt;Post id:2, title:"this is a title", body:"body container", created_at:"2015-11-17 13:51:06", updated_at:"2015-11-17 13:51:06"&amp;gt;, #&amp;lt;Post id:1, title:"new title", body:"some words", created_at:"2015-11-17 13:42:31", updated_at:"2015-11-17 13:42:31"&amp;gt;]
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可是我的 index 文件中，没有语句跟页面中以&lt;code&gt;#&amp;lt;&amp;gt;&lt;/code&gt;形式列出数据内容的行为相关。我百思不得其解，搜索也不好搜，所以来请教一下&lt;/p&gt;</description>
      <author>jojoma</author>
      <pubDate>Tue, 17 Nov 2015 22:39:40 +0800</pubDate>
      <link>https://ruby-china.org/topics/28091</link>
      <guid>https://ruby-china.org/topics/28091</guid>
    </item>
  </channel>
</rss>
