Julia Evans 发布的文章

当我第一次知道 DNS 时,我想它应该不会很复杂。不就是一些存储在服务器上的 DNS 记录罢了。有什么大不了的?

但是教科书上只是介绍了 DNS 的原理,并没有告诉你实际使用中 DNS 可能会以多少种方式破坏你的系统。这可不仅仅是缓存问题!

所以我 在 Twitter 上发起了一个提问,征集人们遇到的 DNS 问题,尤其是那些一开始看起来与 DNS 没什么关系的问题。(“总是 DNS 问题”这个梗)

我不打算在这篇文章中讨论如何解决或避免这些问题,但我会放一些讨论这些问题的链接,在那里可以找到解决问题的方法。

问题:网络请求缓慢

如果你的网络比预期的要慢,这是因为某些原因导致 DNS 解析器变慢了。这可能是解析器负载过大或者存在内存泄漏等原因导致的。

我的路由器的 DNS 转发器曾遇到过这个问题,导致我的所有 DNS 请求很慢。我通过重启路由器解决了这个问题。

问题:DNS 超时

一些网友提到由于 DNS 查询超时,他们的网络请求需要耗时 2 秒多甚至 30 秒。这跟“网络请求缓慢”问题类似,但情况要更糟糕,因为 DNS 请求就会消耗掉几秒钟时间。

Sophie Haskins 有一篇关于 Kubernete DNS 超时的博客文章 一次 Kube DNS 踩坑经历

问题:ndots 设置

一些网友提到在 /etc/resolv.conf 中设置 ndots:5 时会出现问题。

下面是从 这篇《Kubernetes 容器荚中 /etc/resolv.conf 里设置 ndots:5 为什么会拖慢你的程序性能》中引用的 /etc/resolv.conf文件。

    nameserver 100.64.0.10
    search namespace.svc.cluster.local svc.cluster.local cluster.local eu-west-1.compute.internal
    options ndots:5

如果你用上面的配置文件,想要查询得域名是 google.com,那么你的程序会调用 getaddrinfo 函数,而它会依次查询以下域名:

  1. google.com.namespace.svc.cluster.local.
  2. google.com.svc.cluster.local.
  3. google.com.cluster.local.
  4. google.com.eu-west-1.compute.internal.
  5. google.com.

简单来说,它会检查 google.com 是不是 search 这一行中的某个子域名。

所以每发起一次 DNS 查询,你都得先等待前 4 次查询失败后才能获取到最终查询结果。

问题:难以判断系统使用的 DNS 解析器

这本身并不是一个问题,但当你遇到 DNS 问题时,一般都会跟 DNS 解析器有关。我没有一种判断 DNS 解析器的万能方法。

下面是我知道的方法:

  • 在 Linux 系统上,最常见的是通过 /etc/resolv.conf 来选择 DNS 解析器。但是也有例外,比如浏览器可能会忽略 /etc/resolv.conf,而是使用 基于 HTTPS 的 DNS DNS-over-HTTPS 服务。
  • 如果你使用的是 UDP DNS,你可以通过 sudo tcpdump port 53 来查看 DNS 请求被发送到了哪里。但如果你使用的是基于 HTTPS 的 DNS 或 基于 TLS 的 DNS DNS over TLS ,这个方法就不行了。

我依稀记得这在 MacOS 系统上会更加令人迷惑,我也不清楚原因。

问题:DNS 服务器返回 NXDOMAIN 而不是 NOERROR

这是我曾经遇到过的一个 Nginx 不能解析域名的问题。

  • 我设置 Nginx 使用一个特定的 DNS 服务器来解析 DNS 查询
  • 当访问这个域名时,Nginx 做了两次查询,第一次是对 A 的,第二次是对 AAAA
  • 对于 A 的查询,DNS 服务器返回 NXDOMAIN
  • Nginx 认为这个域名不存在,然后放弃查询
  • 对于 AAAA 的查询 DNS 服务器返回了成功
  • 但 Nginx 忽略了对 AAAA 返回的查询结果,因为它前面已经放弃查询了

问题出在 DNS 服务器本应该返回 NOERROR 的——那个域名确实存在,只是没有关于 A 的记录罢了。我报告了这个问题,然后他们修复了这个问题。

我自己也写出过这个问题,所以我理解为什么会发生这种情况——很容易想当然地认为“没有要查询的记录,就应该返回 NXDOMAIN 错误码”。

问题:自动生效的 DNS 缓存

如果你在生成一个域名的 DNS 记录之前就访问这个域名,那么这个记录的缺失会被缓存起来。当你第一次遇到这个问题时一定会非常吃惊——我也是去年才知道有这个问题。

缓存的 TTL 就是域名的 起始权限记录 Start of Authority (SOA) 记录的 TTL ——比如对于 jvns.ca ,这个值是一个小时。

问题:Nginx 永久缓存 DNS 记录

如果你在 Nginx 中使用下面的配置:

    location / {
        proxy_pass https://some.domain.com;
    }

Nginx 只会在启动的时候解析一次 some.domain.com,以后不会再对其进行解析。这是非常危险的操作,尤其是对于那些 IP 地址经常变动的域名。它可能平安无事地运行几个月,然后突然在某个凌晨两点把你从床上纠起来。

针对这个问题已经有很多众所周知的方法了,但由于本文不是关于 Nginx 的,所以我不打算深入探讨它。但你第一次遇到它时一定会很惊讶。

这是一篇关于这个问题发生在 AWS 负载均衡器上的 博客文章

问题:Java 永久缓存 DNS 记录

跟上面类似的问题,只是出现在 Java 上:据说 这与你 Java 的配置有关。“JVM 的默认 TTL 设置可能会导致只有 JVM 重启时才会刷新 DNS 记录。”

我还没有遇到过这个问题,不过我那些经常写 Java 的朋友遇到过这个问题。

当然,任何软件都可能存在永久缓存 DNS 的问题,但据我所知它经常出现在 Nginx 和 Java 上。

问题:被遗忘的 /etc/hosts 记录

这是另一种缓存问题:/etc/hosts 中的记录会覆盖你的常规 DNS 设置!

让人迷惑的是 dig 命令会忽略 /etc/hosts 文件。所以当你使用 dig whatever.com 来查询 DNS 信息时,它会告诉你一切正常。

问题:电子邮件未发送 / 将成为垃圾邮件

电子邮件是通过 DNS(MX 记录, SPF 记录, DKIM 记录)来发送和验证的,所以有些电子邮件问题其实是 DNS 问题。

问题:对国际化域名无效

你可以使用非 ASCII 字符甚至是表情符来注册域名,比如 拉屎网 https://?.la

DNS 能够处理国际化域名是因为 ?.la 会被用 punycode 编码将转换为 xn--ls8h.la

尽管已经有了 DNS 处理国际化域名的标准,很多软件并不能很好地处理国际化域名。Julian Squires 的 干掉 Chrome 浏览器的表情符!! 就是一个非常有趣的例子。

问题:TCP DNS 被防火墙拦截

有人提到一些防火墙会允许在 53 端口上使用 UDP 协议,但是禁止 TCP 协议。然而很多 DNS 查询需要在 53 端口上使用 TCP,这可能会导致很难排查的间歇性的问题。

问题:musl 不支持 TCP DNS

很多应用程序使用 libcgetaddrinfo 来做 DNS 查询。musl 是用在 Alpine Docker 容器上的 glibc 替代品。而它不支持 TCP DNS。如果你的 DNS 查询的响应数据超过 DNS UDP 数据包的大小(512 字节)就会出现问题。

我对此仍然不太清楚,我下面我的理解也可能是错的:

  1. muslgetaddrinfo 发起一个 DNS 请求
  2. DNS 服务器发现请求的响应数据太大了,没法放入一个 DNS 数据包中
  3. DNS 服务器返回一个 空截断响应 empty truncated response ,并期望客户端通过 TCP DNS 重新用发起查询
  4. musl 不支持 TCP DNS,所以根本不会重试

关于这个问题的文章:在 Alpine Linux 上的 DNS 解析问题

问题:getaddrinfo 不支持轮询 DNS

轮询 round robin DNS 是一种 负载均衡 load balancing 技术,每次 DNS 查询都会获得一个不同的 IP 地址。显然如果你使用 gethostbyname 做 DNS 查询不会有任何问题,但是用 getaddrinfo 就不行了。因为 getaddrinfo 会对获得的 IP 地址进行排序。

在你从 gethostbyname 切换到 getaddrinfo 时可能完全不会意识到这可能会引起负载均衡问题。

这个问题可能会非常隐蔽,如果你不是用 C 语言编程的话,这些函数调用被隐藏在各种调用库背后,你可能完全意识不到发生了这种改变。所以某次看似人畜无害的升级就可能导致你的 DNS 负载均衡失效。

下面是讨论这个的一些文章:

问题:启动服务时的竞争条件

有人 提到 使用 Kubernete DNS 时遇到的问题:他们有两个同时启动的容器,一旦启动就会立即尝试解析对方的地址。由于 Kubernete DNS 还没有改变,所以 DNS 查询会失败。这个失败会被缓存起来,所以后续的查询会一直失败。

写在最后

我所列举的不过是 DNS 问题的冰山一角,期待大家告诉我那些我没有提到的问题和相关链接。我希望了解这些问题在实际中是如何发生的以及如何被解决的。

(题图:MJ/f512f18e-2e1d-4614-bed1-b0a0c373e14d)


via: https://jvns.ca/blog/2022/01/15/some-ways-dns-can-break/

作者:Julia Evans 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我正在制作一份有关计算机以二进制表示事物的小册子,有人问我一个问题 - 为什么 x86 架构使用 8 位字节?为什么不能是其他大小呢?

对于类似这样的问题,我认为有两种可能性:

  • 这是历史原因造成的,其他尺寸(如 4、6 或 16 位)同样有效。
  • 8 位是客观上的最佳选择,即使历史发展不同,我们仍然会使用 8 位字节。
  • 一些混合 1 和 2 的因素。

我对计算机历史并不是非常着迷(与阅读计算机文献相比,我更喜欢使用计算机),但我总是很好奇计算机事物今天的方式是否存在本质原因,或者它们大多是历史偶然的结果。因此,我们将谈论一些计算机历史。

作为历史偶然性的一个例子:DNS 有一个 class 字段,它有 5 种可能的值(internetchaoshesiodnoneany)。 对我来说,这是一个明显的历史意外的例子 - 如果我们今天重新设计 DNS 而不必担心向后兼容性,我无法想象我们会以相同的方式定义类字段。我不确定我们是否会使用 class 字段!

这篇文章没有明确的答案,但我在 Mastodon 上提问,并找到了一些潜在的 8 位字节原因。我认为答案是这些原因的某种组合。

字节和字有什么区别?

首先,本文中经常提到 “ 字节 byte ” 和 “ word ”。它们有什么区别?我的理解是:

  • 字节的大小 是你可以寻址的最小单元。例如,在我的计算机上,程序中的 0x20aa87c68 可能是一个字节的地址,然后 0x20aa87c69 是下一个字节的地址。
  • 字的大小 是字节大小的某个倍数。我对此困惑了多年,维基百科的定义非常模糊(“字是特定处理器设计使用的自然数据单元”)。我最初认为字大小与寄存器大小相同(在 x86-64 上为 64 位)。但是根据 英特尔架构手册 的第 4.1 节(“基本数据类型”),在 x86 上,虽然寄存器是 64 位的,但一个字是 16 位的。因此我困惑了 —— 在 x86 上,一个字是 16 位还是 64 位?它可以根据上下文而有不同的含义吗?这是怎么回事?

现在让我们来讨论一些使用 8 位字节的可能原因!

原因 1:将英文字母适配到 1 字节中

维基百科文章 表示 IBM System/360 于 1964 年引入了 8 位字节。

在管理该项目的 Fred Brooks 的一段 视频采访 中,他讲述了原因。以下是我转录的一些内容:

…… 6 位字节在科学计算中确实更好,而 8 位字节则更适合商业计算,每个字节都可以针对另一个字节进行调整,以使两种字节互相使用。

因此,这变成了一个高管决策,我决定根据 Jerry 的建议采用 8 位字节。

……

我在我的 IBM 职业生涯中做出的最重要的技术决策是为 360 选择 8 位字节。

我相信字符处理将变得重要,而不是十进制数字。

使用 8 位字节处理文本很有道理:2 6 为 64,因此 6 位不足以表示小写字母、大写字母和符号。

为了使用 8 位字节,System/360 还引入了 EBCDIC 编码,这是一种 8 位字符编码。

接下来在 8 位字节历史上重要的机器似乎是 英特尔 8008,它设计用于计算机终端(Datapoint 2200)。终端需要能够表示字母以及终端控制代码,因此使用 8 位字节对其来说很有意义。计算机历史博物馆上的 Datapoint 2200 手册 在第 7 页上说 Datapoint 2200 支持 ASCII(7 位)和 EBCDIC(8 位)。

为什么 6 位字节在科学计算中更好?

我对这条 “6 位字节在科学计算中更好” 的评论很好奇。以下是 Gene Amdahl 的一段采访摘录

我原本希望采用 24 和 48 而非 32 和 64,因为这将为我提供一个更合理的浮点系统。因为在浮点运算中,使用 32 位字大小时,你必须将指数保持在 8 位中用于指数符号,并且要使其在数字范围上合理,你必须每次调整 4 个位而不是单个位。因此,这将导致你比使用二进制移位更快地失去一些信息。

我完全不理解这条评论 - 如果你使用 32 位字大小,为什么指数必须是 8 位?如果你想要,为什么不能使用 9 位或 10 位?但这是我在快速搜索中找到的全部内容。

为什么大型机使用 36 位?

与 6 位字节相关的问题是:许多大型机使用 36 位字大小。为什么?在维基百科的 36 位计算 文章中有一个很好的解释:

在计算机问世之前,即需要高精度科学和工程运算的领域,使用的是十位数码电动机械计算器……这些计算器每位数码均有一个专用按键,操作人员在输入数字时需要用到所有手指,因此,虽然有些专业计算器有更多位数码,但这种情况是个实际的限制。

因此,早期针对相同市场的二进制计算机通常使用 36 位字长度。这足以表示正负整数最高精度到十位数字(最小应为 35 位)。

因此,这种 36 位大小似乎是基于

的,它等于 34.2。嗯。

我猜这个原因是在 50 年代,计算机非常昂贵。因此,如果您想要你的计算机支持十位十进制数字,你将设计它恰好具有足够的位来执行此操作,而不会更多。

现在计算机更快更便宜,因此,如果您想要出于某种原因表示十位十进制数字,你只需使用 64 位即可 - 浪费一点空间通常并不会有太大问题。

还有人提到,一些具有 36 位字大小的计算机可以让你选择字节大小 - 根据上下文,你可以使用 5 或 6 或 7 或 8 位字节。

原因 2:与二进制编码的十进制一起工作

20 世纪 60 年代,有一种流行的整数编码叫做 二进制编码的十进制 binary-coded decimal (缩写为 BCD),它将每个十进制数字编码为 4 位。

例如,如果你想要编码数字 1234,在 BCD 中,它会是这样的:

0001 0010 0011 0100

因此,如果你想要能够轻松地与二进制编码的十进制一起工作,你的字节大小应该是 4 位的倍数,比如 8 位!

为什么 BCD 很流行?

这个整数表示方法对我来说真的很奇怪 —— 为什么不用更有效率的二进制来存储整数呢?在早期的计算机中,效率非常重要!

我最好的猜测是,早期的计算机没有像我们现在这样的显示器,所以一个字节的内容被直接映射到开关灯上。

这是来自维基百科一个带有一些亮灯的 IBM 650 显示器的图片(CC BY-SA 3.0 许可):

因此,如果你想让人们能够相对容易地从二进制表示中读取十进制数,这样做就更有意义了。我认为,今天 BCD 已经过时了,因为我们拥有显示器,并且我们的计算机可以将用二进制表示的数字转换为十进制,并显示它们。

此外,我想知道,“ 四位 nibble ”(意为 “4 位”)这个词是不是来自 BCD 的。在 BCD 的上下文中,你经常会引用半个字节(因为每个十进制数字是 4 位)。所以有一个 “4 位” 的词语是有意义的,人们称 4 个位为 “ 四位 nibble ”。今天,“四位” 对我来说感觉像是一个古老的词汇,除了作为一个趣闻我肯定从未使用过它(它是一个很有趣的词!)。维基百科关于 “四位” 的文章支持了这个理论:

“四位” 用来描述存储在 IBM 大型计算机中打包的十进制格式(BCD)中数字的位数。

还有一个人提到 BCD 的另一个原因是 金融计算。今天,如果你想存储美元金额,你通常只需使用整数的分数,然后在需要美元部分时除以 100。这没什么大不了的,除法很快。但显然,在 70 年代,将一个用二进制表示的整数除以一个 100 是非常慢的,所以重新设计如何表示整数,以避免除以 100 是值得的。

好了,关于 BCD 就说这么多。

原因 3:8 是 2 的幂?

许多人说,CPU 的字节大小是 2 的幂次方很重要。我无法确定这是真的还是假的,而且我对 “计算机使用二进制,所以 2 的幂次方很好” 这种解释感到不满意。这似乎非常合理,但我想深入探讨一下。而且从历史上看,肯定有很多使用字节大小不是 2 的幂次方的机器,例如(来自这个来自 Stack Exchange 上复古计算版块的 帖子):

  • Cyber 180 大型机使用 6 位字节
  • Univac 1100/2200 系列使用 36 位字长
  • PDP-8 是一台 12 位计算机

一些我听到的关于 2 的幂次方很好的原因我还没有理解:

  • 一个单词中的每个位都需要一个总线,而你希望总线数量是 2 的幂次方(为什么?)
  • 很多电路逻辑容易针对分而治之的技术(我需要一个例子来理解这个)

对我更有意义的原因是:

  • 它使设计“时钟分频器”更容易,这些分频器可以测量“在这条线路上发送了 8 位”,分别基于减半进行操作 - 你可以将 3 个减半时钟分频器串联起来。Graham Sutherland 告诉我这个,他制作了这个非常酷的 分频器模拟器,展示了这些分频器的工作原理。该网站(Falstad)还有很多其他示例电路,似乎是制作电路模拟器的一个非常酷的方式。
  • 如果你有一个指令可以将字节中的特定位清零,则如果你的字节大小为 8(2 的 3 次方),你可以只使用 3 位指令来指示哪一位。x86 似乎没有这样做,但 Z80 的位测试指令 是这样做的。
  • 有人提到一些处理器使用 进位前瞻加法器,它们按 4 位分组。经过一些快速的谷歌搜索,似乎有各种各样的加法器电路。
  • 位图:你计算机的内存被组织成页(通常大小为 2 的 n 次方)。它需要跟踪每一页是否空闲。操作系统使用位图来完成这项工作,其中每个位对应一页,并且根据页面是空闲还是占用,值为 0 或 1。如果你有一个 9 位的字节,你需要除以 9 来在位图中找到你要查找的页面。除以 9 的速度比除以 8 慢,因为除以 2 的幂次方总是最快的。

我可能很糟糕地扭曲了其中一些解释:在这里,我非常超出了自己的知识领域。我们继续前进吧。

原因 4:小字节大小很好

你可能会想:好吧,如果 8 位字节比 4 位字节更好,为什么不继续增加字节大小呢?我们可以有 16 位字节啊!

有几个保持字节大小较小的理由:

  • 它是一种空间浪费 —— 字节是你可以寻址的最小单位,如果你的计算机存储了大量的 ASCII 文本(只需要 7 位),那么每个字符分配 12 或 16 个位相当浪费,而你可以使用 8 个位代替。
  • 随着字节变得越来越大,你的 CPU 需要变得更复杂。例如,你需要每个位线路一条总线线路。因此,我想简单总是更好。

我对 CPU 架构的理解非常薄弱,所以就说到这里吧。对我来说,“这是一种空间浪费” 的理由似乎相当有说服力。

原因 5:兼容性

英特尔 8008(1972 年)是 8080(1974 年)的前身,8080 是第一款 x86 处理器 8086(1976 年)的前身。似乎 8080 和 8086 很受欢迎,这就是我们现代 x86 计算机的来源。

我认为这里有一个 “如果它好好的就不要动它” 的问题 - 我假设 8 位字节功能良好,因此英特尔看不到需要更改设计的必要性。如果你保持相同的 8 位字节,那么你可以重复使用更多指令集。

此外,80 年代左右我们开始出现像 TCP 这样的网络协议,它们使用 8 位字节(通常称为“ 八位组 octet ”),如果你要实现网络协议,你可能希望使用 8 位字节。

就这些!

在我看来,8 位字节的主要原因是:

  • 很多早期的电脑公司都是美国的,美国使用最广泛的语言是英语
  • 这些人希望计算机擅长文本处理
  • 较小的字节大小通常更好
  • 7 位是你可以用来容纳所有英文字母和标点符号的最小尺寸
  • 8 比 7 更好(因为它是 2 的幂次方)
  • 一旦有得到成功应用的受欢迎的 8 位计算机,你希望保持相同的设计以实现兼容性。

有人指出 这本 1962 年的书 第 65 页谈到了 IBM 选择 8 位字节的原因,基本上说了相同的内容:

  1. 其完整的 256 个字符的容量被认为足以满足绝大多数应用程序的需要。
  2. 在该容量范围内,单个字符由单个字节表示,因此任何特定记录的长度并不因该记录中字符而异。
  3. 8 位字节在存储空间上是相当经济的。
  4. 对于纯数字工作,一个十进制数字只需要 4 个比特表示,两个这样的 4 位字节可以打包成一个 8 位字节。尽管这种数字数据包装不是必需的,但为了提高速度和存储效率,它是一种常见做法。严格来说,4 位字节属于不同的代码,但与 4 位及 8 位方案相比,它们的简单性导致了更简单的机器设计和更清晰的寻址逻辑。
  5. 4 位和 8 位的字节大小,作为 2 的幂次方,允许计算机设计师利用二进制寻址和位级索引的强大功能(见第 4 章和第 5 章)。

总的来说,如果你在英语国家设计二进制计算机,选择 8 位字节似乎是一个非常自然的选择。

(题图:MJ/3526a0d5-bee5-4678-8637-e96e9843b53c)


via: https://jvns.ca/blog/2023/03/06/possible-reasons-8-bit-bytes/

作者:Julia Evans 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

嗨!昨天我试着写点关于浮点数的东西,我发现自己对这个 64 位浮点数的计算方法很好奇:

>>> 0.1 + 0.2
0.30000000000000004

我意识到我并没有完全理解它是如何计算的。我的意思是,我知道浮点计算是不精确的,你不能精确地用二进制表示 0.1,但是:肯定有一个浮点数比 0.30000000000000004 更接近 0.3!那为什么答案是 0.30000000000000004 呢?

如果你不想阅读一大堆计算过程,那么简短的答案是: 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 正好位于两个浮点数之间,即 0.299999999999999988897769753748434595763683319091796875 (通常打印为 0.3) 和 0.3000000000000000444089209850062616169452667236328125(通常打印为 0.30000000000000004)。答案是 0.30000000000000004,因为它的尾数是偶数。

浮点加法是如何计算的

以下是浮点加法的简要计算原理:

  • 把它们精确的数字加在一起
  • 将结果四舍五入到最接近的浮点数

让我们用这些规则来计算 0.1 + 0.2。我昨天才刚了解浮点加法的计算原理,所以在这篇文章中我可能犯了一些错误,但最终我得到了期望的答案。

第一步:0.1 和 0.2 到底是多少

首先,让我们用 Python 计算 0.10.2 的 64 位浮点值。

>>> f"{0.1:.80f}"
'0.10000000000000000555111512312578270211815834045410156250000000000000000000000000'
>>> f"{0.2:.80f}"
'0.20000000000000001110223024625156540423631668090820312500000000000000000000000000'

这确实很精确:因为浮点数是二进制的,你也可以使用十进制来精确的表示。但有时你只是需要一大堆数字:)

第二步:相加

接下来,把它们加起来。我们可以将小数部分作为整数加起来得到确切的答案:

>>> 1000000000000000055511151231257827021181583404541015625 + 2000000000000000111022302462515654042363166809082031250
3000000000000000166533453693773481063544750213623046875

所以这两个浮点数的和是 0.3000000000000000166533453693773481063544750213623046875

但这并不是最终答案,因为它不是一个 64 位浮点数。

第三步:查找最接近的浮点数

现在,让我们看看接近 0.3 的浮点数。下面是最接近 0.3 的浮点数(它通常写为 0.3,尽管它不是确切值):

>>> f"{0.3:.80f}"
'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

我们可以通过 struct.pack0.3 序列化为 8 字节来计算出它之后的下一个浮点数,加上 1,然后使用 struct.unpack

>>> struct.pack("!d", 0.3)
b'?\xd3333333'
# 手动加 1
>>> next_float = struct.unpack("!d", b'?\xd3333334')[0]
>>> next_float
0.30000000000000004
>>> f"{next_float:.80f}"
'0.30000000000000004440892098500626161694526672363281250000000000000000000000000000'

当然,你也可以用 math.nextafter

>>> math.nextafter(0.3, math.inf)
0.30000000000000004

所以 0.3 附近的两个 64 位浮点数是 0.2999999999999999888977697537484345957636833190917968750.3000000000000000444089209850062616169452667236328125

第四步:找出哪一个最接近

结果证明 0.3000000000000000166533453693773481063544750213623046875 正好在 0.2999999999999999888977697537484345957636833190917968750.3000000000000000444089209850062616169452667236328125 的中间。

你可以通过以下计算看到:

>>> (3000000000000000444089209850062616169452667236328125000 + 2999999999999999888977697537484345957636833190917968750) // 2 == 3000000000000000166533453693773481063544750213623046875
True

所以它们都不是最接近的。

如何知道四舍五入到哪一个?

在浮点数的二进制表示中,有一个数字称为“尾数”。这种情况下(结果正好在两个连续的浮点数之间),它将四舍五入到偶数尾数的那个。

在本例中为 0.300000000000000044408920985006261616945266723632812500

我们之前就见到了这个数字的尾数:

  • 0.30000000000000004 是 struct.unpack('!d', b'?\xd3333334') 的结果
  • 0.3 是 struct.unpack('!d', b'?\xd3333333') 的结果

0.30000000000000004 的大端十六进制表示的最后一位数字是 4,它的尾数是偶数(因为尾数在末尾)。

我们用二进制来算一下

之前我们都是使用十进制来计算的,这样读起来更直观。但是计算机并不会使用十进制,而是用 2 进制,所以我想知道它是如何计算的。

我不认为本文的二进制计算部分特别清晰,但它写出来对我很有帮助。有很多数字,读起来可能很糟糕。

64 位浮点数如何计算:指数和尾数

64 位浮点数由 2 部分整数构成:指数尾数,还有 1 比特 符号位.

以下是指数和尾数对应于实际数字的方程:

例如,如果指数是 1,尾数是 2**51,符号位是正的,那么就可以得到:

它等于 2 * (1 + 0.5),即 3。

步骤 1:获取 0.1 和 0.2 的指数和尾数

我用 Python 编写了一些低效的函数来获取正浮点数的指数和尾数:

def get_exponent(f):
    # 获取前 52 个字节
    bytestring = struct.pack('!d', f)
    return int.from_bytes(bytestring, byteorder='big') >> 52

def get_significand(f):
    # 获取后 52 个字节
    bytestring = struct.pack('!d', f)
    x = int.from_bytes(bytestring, byteorder='big')
    exponent = get_exponent(f)
    return x ^ (exponent << 52)

我忽略了符号位(第一位),因为我们只需要处理 0.1 和 0.2,它们都是正数。

首先,让我们获取 0.1 的指数和尾数。我们需要减去 1023 来得到实际的指数,因为浮点运算就是这么计算的。

>>> get_exponent(0.1) - 1023
-4
>>> get_significand(0.1)
2702159776422298

它们根据 2**指数 + 尾数 / 2**(52 - 指数) 这个公式得到 0.1

下面是 Python 中的计算:

>>> 2**-4 + 2702159776422298 / 2**(52 + 4)
0.1

(你可能会担心这种计算的浮点精度问题,但在本例中,我很确定它没问题。因为根据定义,这些数字没有精度问题 -- 从 2**-4 开始的浮点数以 1/2**(52 + 4) 步长递增。)

0.2 也一样:

>>> get_exponent(0.2) - 1023
-3
>>> get_significand(0.2)
2702159776422298

它们共同工作得到 0.2:

>>> 2**-3 + 2702159776422298 / 2**(52 + 3)
0.2

(顺便说一下,0.1 和 0.2 具有相同的尾数并不是巧合 —— 因为 x2*x 总是有相同的尾数。)

步骤 2:重新计算 0.1 以获得更大的指数

0.2 的指数比 0.1 大 -- -3 大于 -4。

所以我们需要重新计算:

2**-4 + 2702159776422298 / 2**(52 + 4)

等于 X / 2**(52 + 3)

如果我们解出 2**-4 + 2702159776422298 / 2**(52 + 4) = X / 2**(52 + 3),我们能得到:

X = 2**51 + 2702159776422298 / 2

在 Python 中,我们很容易得到:

>>> 2**51 + 2702159776422298 //2
3602879701896397

步骤 3:添加符号位

现在我们试着做加法:

2**-3 + 2702159776422298 / 2**(52 + 3) + 3602879701896397 / 2**(52 + 3)

我们需要将 27021597764222983602879701896397 相加:

>>> 2702159776422298  + 3602879701896397
6305039478318695

棒。但是 63050394783186952**52-1(尾数的最大值)大,问题来了:

>>> 6305039478318695 > 2**52
True

第四步:增加指数

目前结果是:

2**-3 + 6305039478318695 / 2**(52 + 3)

首先,它减去 2**52:

2**-2 + 1801439850948199 / 2**(52 + 3)

完美,但最后的 2**(52 + 3) 需要改为 2**(52 + 2)

我们需要将 1801439850948199 除以 2。这就是难题的地方 -- 1801439850948199 是一个奇数!

>>> 1801439850948199  / 2
900719925474099.5

它正好在两个整数之间,所以我们四舍五入到最接近它的偶数(这是浮点运算规范要求的),所以最终的浮点结果是:

>>> 2**-2 + 900719925474100 / 2**(52 + 2)
0.30000000000000004

它就是我们预期的结果:

>>> 0.1 + 0.2
0.30000000000000004

在硬件中它可能并不是这样工作的

在硬件中做浮点数加法,以上操作方式可能并不完全一模一样(例如,它并不是求解 “X”),我相信有很多有效的技巧,但我认为思想是类似的。

打印浮点数是非常奇怪的

我们之前说过,浮点数 0.3 不等于 0.3。它实际上是:

>>> f"{0.3:.80f}"
'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

但是当你打印它时,为什么会显示 0.3

计算机实际上并没有打印出数字的精确值,而是打印出了最短的十进制数 d,其中 f 是最接近 d 的浮点数。

事实证明,有效做到这一点很不简单,有很多关于它的学术论文,比如 快速且准确地打印浮点数如何准确打印浮点数 等。

如果计算机打印出浮点数的精确值,会不会更直观一些?

四舍五入到一个干净的十进制值很好,但在某种程度上,我觉得如果计算机只打印一个浮点数的精确值可能会更直观 -- 当你得到一个奇怪的结果时,它可能会让你看起来不那么惊讶。

对我来说,0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.30000000000000004440892098500626161694526672363281250.1 + 0.2 = 0.30000000000000000004 惊讶少一点。

这也许是一个坏主意,因为它肯定会占用大量的屏幕空间。

PHP 快速说明

有人在评论中指出在 PHP 中 <?php echo (0.1 + 0.2 );?> 会输出 0.3,这是否说明在 PHP 中浮点运算不一样?

非也 —— 我在 这里 运行:

<?php echo (0.1 + 0.2 )- 0.3);?>,得到了与 Python 完全相同的答案:5.5511151231258E-17。因此,浮点运算的基本原理是一样的。

我认为在 PHP 中 0.1 + 0.2 输出 0.3 的原因是 PHP 显示浮点数的算法没有 Python 精确 —— 即使这个数字不是最接近 0.3 的浮点数,它也会显示 0.3

总结

我有点怀疑是否有人能耐心完成以上所有些算术,但它写出来对我很有帮助,所以我还是发表了这篇文章,希望它能有所帮助。

(题图:MJ/53e9a241-14c6-4dc7-87d0-f9801cd2d7ab)


via: https://jvns.ca/blog/2023/02/08/why-does-0-1-plus-0-2-equal-0-30000000000000004/

作者:Julia Evans 选题:lkxed 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

哈喽!昨天我见识到了一种我以前从没见过的从服务器推送事件的炫酷方法: 服务器推送事件 server-sent events !如果你只需要让服务器发送事件,相较于 Websockets,它们或许是一个更简便的选择。

我会聊一聊它们的用途、运作原理,以及我昨日在试着运行它们的过程中遇到的几个错误。

问题:从服务器流式推送更新

现在,我有一个启动虚拟机的 Web 服务,客户端轮询服务器,直到虚拟机启动。但我并不想使用轮询方式。

相反,我想让服务器流式推送更新。我跟 Kamal 说我要用 Websockets 来实现它,而他建议使用服务器推送事件不失为一个更简便的选择!

我登时就愣住了——那什么玩意???听起来像是些我从来没见过的稀罕玩意儿。于是乎我就查了查。

服务器推送事件就是个 HTTP 请求协议

下文便是服务器推送事件的运作流程。我-很-高-兴-地了解到它们就是个 HTTP 请求协议。

1.客户端提出一个 GET 请求(举个例子)https://yoursite.com/events 2.客户端设置 Connection: keep-alive,这样我们就能有一个长连接 3.服务器设置设置一个 Content-Type: text/event-stream 响应头 4.服务器开始推送事件,就比如下文这样:

event: status
data: one

举个例子,这里是当我借助 curl 发送请求时,一些服务器推送事件的样子:

$ curl -N 'http://localhost:3000/sessions/15/stream'
event: panda
data: one

event: panda
data: two

event: panda
data: three

event: elephant
data: four

服务器可以根据时间推移缓慢推送事件,并且客户端也能够在它们到来时读取它们。你也可以将 JSON 或任何你想要的东西放在事件当中,就比如 data: {'name': 'ahmed'}

线路协议真的很简单(只需要设置 event:data:,或者如果你愿意,可设置为 id:retry:),所以你并不需要任何花里胡哨的服务器库来实现服务器推送事件。

JavaScript 的代码也超级简单(仅使用 EventSource)

以下是用于流式服务器推送事件的浏览器 JavaScript 的代码。(我从 服务器推送事件的 MND 页面 得到的这个范例)

你可以订阅所有事件,也可以为不同类型的事件使用不同的处理程序。这里我有一个只接受类型为 panda 的事件的处理程序(就像我们的服务器在上一节中推送的那样)。

const evtSource = new EventSource("/sessions/15/stream", { withCredentials: true })
evtSource.addEventListener("panda", function(event) {
  console.log("status", event)
});

客户端在中途不能推送更新

不同于 Websockets,服务器推送事件不允许大量的来回事件通讯。(这体现在它的字眼中 —— 服务器 推送所有事件)。初始的时候客户端发出一个请求,然后服务器发出一连串响应。

如果 HTTP 连接结束,它会自动重连

使用 EventSource 发出的 HTTP 请求和常规 HTTP 请求有一个很大的区别,MDN 文档中对此有所说明:

默认情况下,如果客户端和服务器之间的连接断开,则连接会重启。请使用 .close() 方法来终止连接。

很奇怪,一开始我真的被它吓到了:我打开了一个连接,然后在服务器端将其关闭,然后两秒过后客户端向我的传送终端发送了另一条请求!

我觉得这里可能是因为连接在完成之前意外断开了,所以客户端自动重新打开了它以防止类似情况再发生。

所以如果你不想让客户端继续重试,你就得通过调用 .close() 直截了当地关闭连接。

这里还有些其它特性

你还能在服务器推送事件中设置 id:retry: 字段。似乎,如果你在服务器推送事件上设置,那么当重新连接时,客户端将发送一个 Last-Event-ID 响应头,带有它收到的最后一个 ID。酷!

我发现 W3C 的服务器推送事件页面 令人惊讶地容易理解。

在设置服务器推送事件的时候我遇到了两个错误

我在 Rails 中使用服务器推送事件时遇到了几个问题,我认为这些问题挺有趣的。其中一个缘于 Nginx,另一个是由 Rails 引起的。

问题一:我不能在事件推送的过程中暂停

这个奇怪的错误是在我做以下操作时出现的:

def handler
    # SSE is Rails' built in server-sent events thing
    sse = SSE.new(response.stream, event: "status")
    sse.write('event')
    sleep 1
    sse.write('another event')
end

它会写入第一个事件,但不能写入第二个事件。我对此-非-常-困-惑,然后放开脑洞,试着理解 Ruby 中的 sleep 是如何运作的。但是 Cass 将我引领到一个与我有着相同困惑的 Stack Overflow 问答帖,而这里包含了让我为之震惊的回答!

事实证明,问题出在我的 Rails 服务器位于 Nginx 之后,似乎 Nginx 默认使用 HTTP/1.0 向上游服务器发起请求(为啥?都 2021 年了,还这么干?我相信这其中一定有合乎情理的解释,也许是为了向下兼容之类的)。

所以客户端(Nginx)会在服务器推送第一个事件之后直接关闭连接。我觉得如果在我推送第二个事件的过程中 没有 暂停,它继续正常工作,基本上就是服务器在连接关闭之前和客户端在争速度,争着推送第二部分响应,如果我这边推送速度足够快,那么服务器就会赢得比赛。

我不确定为什么使用 HTTP/1.0 会使客户端的连接关闭(可能是因为服务器在每个事件结尾写入了两个换行符?),但因为服务器推送事件是一个比较新的玩意儿,HTTP/1.0 (这种老旧协议)不支持它一点都会不意外。

设置 proxy_http_version 1.1 从而解决那个麻烦。好欸!

问题二:事件被缓冲

这个事情解决完,第二个麻烦接踵而至。不过这个问题实际上非常好解决,因为 Cass 已经建议将 stackoverflow 里另一篇帖的回答 作为前一个问题的解决方案,虽然它并没有是导致问题一出现的源头,但它-确-实-解-释-了问题二。

问题在这个示例代码中:

def handler
    response.headers['Content-Type'] = 'text/event-stream'
    # Turn off buffering in nginx
    response.headers['X-Accel-Buffering'] = 'no'
    sse = SSE.new(response.stream, event: "status")
    10.times do
        sse.write('event')
        sleep 1
    end
end

我本来期望它每秒返回 1 个事件,持续 10 秒,但实际上它等了 10 秒才把 10 个事件一起返回。这不是我们想要的流式传输方式!

原来这是因为 Rack ETag 中间件想要计算 ETag(响应的哈希值),为此它需要整个响应为它服务。因此,我需要禁用 ETag 生成。

Stack Overflow 的回答建议完全禁用 Rack ETag 中间件,但我不想这样做,于是我去看了 链接至 GitHub 上的议题

那个 GitHub 议题建议我可以针对仅流式传输终端应用一个解决方法,即 Last-Modified 响应头,显然,这么做可以绕过 ETag 中间件。

所以我设置为:

headers['Last-Modified'] = Time.now.httpdate

然后它起作用了!!!

我还通过设置响应头 X-Accel-Buffering: no 关闭了位于 Nginx 中的缓冲区。我并没有百分百确定我要那样做,但这么做似乎更安全。

Stack Overflow 很棒

起初,我全身心致力于从头开始调试这两个错误。Cass 为我指向了那两个 Stack Overflow 帖子,一开始我对那些帖下提出的解决方案持怀疑态度(我想:“我没有使用 HTTP/1.0 啊!ETag 响应头什么玩意,跟这一切有关系吗??”)。

但结果证明,我确实无意中使用 HTTP/1.0,并且 Rack ETag 中间件确实给我带来了问题。

因此,也许这个故事告诉我,有时候计算机就是会以奇怪的方式相互作用,其它人在过去也遇到过计算机以完全相同的奇怪方式相互作用的问题,而 Stack Overflow 有时会提供关于为什么会发生这些情况的答案 : )

我认为重要的是不要随意从 Stack Overflow 中尝试各种解决方案(当然,在这种情况下不会有人建议这样做!)。对于这两个问题,我确实需要去仔细思考,了解发生了什么,还有为什么更改这些设置会起作用。

就是这样!

今天我要继续着手实现服务器推送事件,因为昨天一整天我都沉浸在上述这些错误里。好在我学到了一个以前从未听说过的易学易用的网络技术,心里还是很高兴的。

(题图:MJ/4c08a193-086e-4efe-a662-00401c928c41)


via: https://jvns.ca/blog/2021/01/12/day-36--server-sent-events-are-cool--and-a-fun-bug/

作者:Julia Evans 选题:lujun9972 译者:Drwhooooo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

嗨!这周我一直在写一些 Javascript,和往常一样,当我开始一个新的前端项目时,我面临的问题是:我是否应该使用构建系统?

我想谈谈构建系统对我有什么吸引力,为什么我(通常)仍然不使用它们,以及一些前端 Javascript 库要求你使用构建系统时,为什么我觉得这让我感到沮丧。

我写这篇文章是因为我看到的大多数关于 JS 的文章都假定你正在使用构建系统,而对于像我这样的人来说,编写非常简单的、不需要构建系统的小型 Javascript 项目时,构建系统可能反而添加了很多麻烦。

什么是构建系统?

构建系统的思路是,你有一堆 Javascript 或 Typescript 代码,你想在把它放到你的网站上之前把它翻译成不同的 Javascript 代码。

构建系统可以做很多有用的事情,比如:

  • (出于效率的考虑)将 100 多个 JS 文件合并成一个大的捆绑文件
  • 将 Typescript 翻译成 Javascript
  • 对 Typescript 进行类型检查
  • 精简化
  • 添加 Polyfills 以支持旧的浏览器
  • 编译 JSX
  • 摇树优化 Tree Shaking (删除不使用的 JS 代码以减少文件大小)
  • 构建 CSS(像 tailwind 那样)
  • 可能还有很多其他重要的事情

正因为如此,如果你今天正在构建一个复杂的前端项目,你可能会使用 Webpack、Rollup、Esbuild、Parcel 或 Vite 等构建系统。

很多这些功能对我很有吸引力,我过去使用构建系统也是出于这样一些原因: 例如,Mess With DNS 使用 Esbuild 来翻译 Typescript,并将许多文件合并成一个大文件。

目标:轻松地对旧的小网站进行修改

我做了很多简单的小网站(之一之二之三之四),我对它们的维护精力大约为 0,而且我改变它们的频率很低。

我的目标是,如果我有一个 3、5 年前做的网站,我希望能在 20 分钟内,

  • 在一台新的电脑上从 GitHub 获取源代码
  • 做一些修改
  • 把它放到互联网上

但我对构建系统(不仅仅是 Javascript 构建系统!)的经验是,如果你有一个 5 年历史的网站,要重新构建这个网站会非常痛苦。

因为我的大多数网站都很小,所以使用构建系统的 优势 很小 —— 我并不真的需要 Typescript 或 JSX。我只要有一个 400 行的 script.js 文件就可以了。

示例:尝试构建 SQL 实验场

我的一个网站(SQL 试验场)使用了一个构建系统(它使用 Vue)。我最后一次编辑该项目是在 2 年前,是在另一台机器上。

让我们看看我今天是否还能在我的机器上轻松地构建它。首先,我们要运行 npm install。下面是我得到的输出:

$ npm install
[lots of output redacted]
npm ERR! code 1
npm ERR! path /Users/bork/work/sql-playground.wizardzines.com/node_modules/grpc
npm ERR! command failed
npm ERR! command sh /var/folders/3z/g3qrs9s96mg6r4dmzryjn3mm0000gn/T/install-b52c96ad.sh
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/surface/init.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/avl/avl.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/backoff/backoff.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_args.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_stack.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_stack_builder.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_trace.o
npm ERR!   CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channelz.o

在构建 grpc 时出现了某种错误。没问题。反正我也不需要这个依赖关系,所以我可以花 5 分钟把它拆下来重建。现在我可以 npm install 了,一切正常。

现在让我们试着构建这个项目:

$ npm run build
  ?  Building for production...Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:71:19)
    at Object.createHash (node:crypto:130:10)
    at module.exports (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/util/createHash.js:135:53)
    at NormalModule._initBuildHash (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:414:16)
    at handleParseError (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:467:10)
    at /Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:499:5
    at /Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:356:12
    at /Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
    at /Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:236:3
    at runSyncOrAsync (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:130:11)
    at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
    at Array.<anonymous> (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
    at Storage.finished (/Users/bork/work/sql-playground.wizardzines.com/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:43:16)
    at /Users/bork/work/sql-playground.wizardzines.com/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:79:9

这个 Stack Overflow 的答案 建议运行 export NODE_OPTIONS=--openssl-legacy-provider 来解决这个错误。

这很有效,最后我得以 npm run build 来构建这个项目。

这其实并不坏(我只需要删除一个依赖关系和传递一个略显神秘的 Node 选项!),但我宁愿不被那些构建错误破坏。

对我来说,对于小项目来说,构建系统并不值得

对我来说,一个复杂的 Javascript 构建系统对于 500 行的小项目来说似乎并不值得 —— 它意味着放弃了在未来能够轻松更新项目的能力,以换取一些相当微小的好处。

Esbuild 似乎更稳定一些

我想为 Esbuild 大声叫好: 我 在 2021 年了解到 Esbuild,并用于一个项目,到目前为止,它确实是一种更可靠的构建 JS 项目的方式。

我刚刚尝试在一台新电脑上构建一个我最后一次改动在 8 个月前的 Esbuild 项目,结果成功了。但我不能肯定的说,两年后我是否还能轻松的建立那个项目。也许会的,我希望如此!

不使用构建系统通常是很容易的

下面是 Nginx 实验场 代码中导入所有库的部分的样子:

<script src="js/vue.global.prod.js"></script>
<script src="codemirror-5.63.0/lib/codemirror.js"></script>
<script src="codemirror-5.63.0/mode/nginx/nginx.js"></script>
<script src="codemirror-5.63.0/mode/shell/shell.js"></script>
<script src="codemirror-5.63.0/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="codemirror-5.63.0/lib/codemirror.css">
<script src="script.js "></script>

这个项目也在使用 Vue,但它只是用 <script src 来加载 Vue —— 前端没有构建过程。

一个使用 Vue 的无构建系统模板

有几个人问如何在没有构建系统的情况下开始编写 Javascript。当然,如果你想的话,你可以写原味的 JS,但我常用的框架是 Vue 3。

这是我做的一个小模板,用于启动一个没有构建系统的 Vue 3 项目。它只有 2 个文件和大约 30 行的 HTML/JS。

有些库需要你使用构建系统

构建系统这些事情最近盘旋在我的脑海里,因为这周我正在用 CodeMirror 5 做一个新项目,我看到有一个新版本,CodeMirror 6。

所以我想 —— 很酷,也许我应该使用 CodeMirror 6 而不是 CodeMirror 5。但是 —— 似乎没有构建系统你就不能使用 CodeMirror 6(根据 迁移指南),所以我打算坚持使用 CodeMirror 5。

同样地,你以前可以把 Tailwind 作为一个巨大的 CSS 文件下载,但是 Tailwind 3 似乎根本不能作为一个大的 CSS 文件使用,你需要运行 Javascript 来构建它。所以我现在要继续使用 Tailwind 2。(我知道,我知道,你不应该使用大的 CSS 文件,但是它验收只有 300KB,而且我真的不希望有构建步骤)

(更正:看起来 Tailwind 在 2021 年发布了一个 独立的命令行工具,这似乎是一个不错的选择)

我不完全确定为什么有些库不提供无构建系统版本 —— 也许发布无构建系统版本会给库增加很多额外的复杂性,而维护者认为这不值得。或者,库的设计意味着由于某种原因,不可能发布无构建系统版本。

我希望有更多的无构建系统的 Javascript 技巧

到目前为止,我的主要策略是:

  • 在某个库的网站上搜索 “CDN”,找到一个单独的 Javascript 文件
  • 使用 https://unpkg.com 来查看该库是否有一个我可以使用的内置版本
  • 托管我自己的库的版本,而不是依赖可能崩溃的 CDN
  • 编写我自己的简单集成方案,而不是拉入另一个依赖关系(例如,前几天我为 Vue 编写了自己的 CodeMirror 组件)。
  • 如果我想要一个构建系统,就使用 Esbuild

还有一些看起来很有趣但我还没有研究过的东西:


via: https://jvns.ca/blog/2023/02/16/writing-javascript-without-a-build-system/

作者:Julia Evans 选题:lkxed 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

大家好!今年早些时候,我在写《DNS 是如何工作的》 时,有人问我——为什么人们有时在域名的末尾加一个点?例如,如果你通过运行 dig example.com 查询 example.com 的 IP,你会看到一下内容:

$ dig example.com
example.com.        5678    IN  A   93.184.216.34

执行完 dig 命令后,example.com 有一个 . ——变成了 example.com.!发生了什么?

有些 DNS 工具也要求传给它的域名后加一个 .:如果你在使用 miekg/dns 时传给它 example.com,它会报错:

// trying to send this message will return an error
m := new(dns.Msg)
m.SetQuestion("example.com", dns.TypeA)

最初我以为我知道这个问题的答案(“呃,末尾的点意味着域名是完全限定的?”)。这是对的 —— 一个 完全限定域名 fully qualified domain name (FQDN)是一个末尾有 . 的域名!

但是为什么末尾的点是有用且重要的呢?

在 DNS 的请求/响应中,域名的末尾并没有 “.”

我曾经(错误地)认为 “为什么末尾有一个点?”的答案可能是 “在 DNS 请求/响应中,域名末尾有一个 .,所以我们把它放进去,以匹配你的计算机实际发送/接收的内容”。但事实并不是这样!

当计算机发送 DNS 请求/响应时,域名的末尾并没有点。实际上,域名中没有点。

域名会被编码成一系列的长度/字符串对。例如,域名 example.com 被编码为这 13 个字节。

7example3com0

编码后的内容一个点也没有。一个 ASCII 域名(如 example.com)被转成了各种 DNS 软件的 DNS 请求/响应中使用的格式。

今天我们来讨论域名被转成 DNS 响应的一个地方:区域文件。

区域文件中域名末尾的 “.”

一些人管理域名的 DNS 记录的方法是创建一个被称为 “区域文件” 的文本文件,然后配置一些 DNS 服务器软件(如 nsdbind)来为该区域文件中指定的 DNS 记录提供服务。

下面是一个对应 example.com 的示例区域文件:

orange  300   IN    A     1.2.3.4
fruit   300   IN    CNAME orange
grape   3000  IN    CNAME example.com.

在这个文件中,任何不以 . 结尾的域名(比如 orange)后都会自动加上 .example.com。所以 orange 成了 orange.example.com 的简称。DNS 服务器从它的配置中得知这是一个 example.com 的区域文件,所以它知道在所有不以点结尾的名字后面自动添加 example.com

我想这里的想法只是为了少打几个字符——如果要打出全称,区域文件会是这样:

    orange.example.com.  300   IN    A     1.2.3.4
    fruit.example.com.   300   IN    CNAME orange.example.com.
    grape.example.com.   3000  IN    CNAME example.com.

确实多了很多字符。

你也可以不通过区域文件来使用 DNS

尽管官方的 DNS RFC(RFC 1035)中定义了区域文件格式,但你也可以不通过区域文件来使用 DNS。例如,AWS Route 53 就不用区域文件来存储 DNS 记录!你可以通过 Web 界面或 API 来创建记录,我猜他们是用某种数据库而不是一堆文本文件来存储记录。

不过,Route 53(像许多其他 DNS 工具一样)确实支持导入和导出区域文件,这个功能或许在你更换 DNS 提供商时很有用。

dig 命令输出中末尾的 “.”

现在我们来讨论下 dig 命令的输出:

$ dig example.com
; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> +all example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10712
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.com.           IN  A

;; ANSWER SECTION:
example.com.        81239   IN  A   93.184.216.34

有一件奇怪的事是,几乎每一行都以 ;; 开头,这是怎么回事?; 是区域文件中的注释字符!

我想 dig 以这种奇怪的方式输出的原因可能是为了方便你粘贴这些内容到区域文件时,不用修改就可以直接用。

这也是 example.com 末尾有个 . 的原因 —— 区域文件要求域名末尾必须有点(否则它们会被解释为是相对于该区域的)。因此 dig 也这么处理了。

我真的希望 dig 有一个 +human 选项,以更人性化的方式打印出这些信息,但现在我太懒了,懒得花工夫去实际贡献代码来做这件事(而且我并不擅长 C),所以我只能在我的博客上抱怨一下 :smiley:

curl 命令输出中末尾的 “.”

我们来看下另一个末尾有 . 的例子:curl

我家里有台计算机名为 grapefruit,其上运行着 Web 服务器。当我执行 curl grapefruit 时,会输出:

$ curl grapefruit
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<html>
<head>
......

这样运行没问题!但是如果我在域名后加一个 . 会怎样呢?它报错了:

$ curl grapefruit.
curl: (6) Could not resolve host: grapefruit.

发生了什么?为了搞清楚,我们需要先来学习下搜索域:

初识搜索域

当我执行 curl grapefrult 时,它是怎么被转成一个 DNS 请求的?你可能会认为我的计算机会向域名 grapefruit 发送一个请求,对吗?但事实并不是这样。

让我们用 tcpdump 来看看到底是什么域名在被查询。

$ sudo tcpdump -i any port 53
[...] A? grapefruit.lan. (32)

实际上是向 grapefruit.lan. 发送的请求。为什么呢?

解释一下:

  1. curl 调用函数 getaddrinfo 来查询 grapefruit
  2. getaddrinfo 查询了我计算机上的文件 /etc/resolv.conf
  3. /etc/resolv.conf 包含两行内容:
nameserver 127.0.0.53
search lan
  1. 因为有 search lan 这行内容,所以 getaddrinfograpefruit 的末尾添加了一个 lan,去查询 grapefruit.lan

什么时候搜索域被使用?

现在我们知道了一些奇怪的事情:当我们查询一个域名时,有时会有一个额外的东西(如 lan)被加到最后。但是什么时候会发生这种情况呢?

  1. 如果我们在域名末尾添加一个 .,那么这时不会用到搜索域
  2. 如果域名中间包含一个 .(如 example.com),那么默认也不会用到搜索域。但是可以通过修改配置来改变处理逻辑(在 ndots 里有更详细的说明)

我们现在知道了 curl grapefruit.curl grapefruit 结果不一样的原因——因为一个查询的是 grapefruit.,而另一个查询的是 grapefruit.lan.

我的计算机怎么知道使用哪个搜索域呢?

当我连接路由时,它会通过 DHCP 告诉我它的搜索域是 lan —— 它也是通过这个方式给我的计算机分配 IP。

所以为什么要在域名末尾加一个点呢?

现在我们已经了解了区域文件和搜索域,下面是我认为的人们要在域名末尾加点的原因:

有两种情况下,域名会被修改,并在末尾添加其他东西。

  • example.com 的区域文件中,grapefruit 会被转为 grapefruit.example.com
  • 在我的本地网络(我的计算机已经配置了使用搜索域 lan),grapefruit 被转为 grapefruit.lan

因此,由于域名在某些情况下实际上可能被转成其他名字,人们就在结尾处加一个 .,以此来表示 “这是域名,末尾不需要添加任何东西,这就是全部内容”。否则会引起混乱。

“这就是全部内容”的技术术语是**“完全限定域名”,简称为“FQDN”**。所以 google.com. 是一个完全限定域名,而 google.com 不是。

我总是要提醒自己这样做的原因,因为我很少使用区域文件和搜索域,所以我经常觉得——“我当然是指 google.com 而不是 google.com.something.else! 我为什么要指其他东西?那太傻了!”

但是有些人确实在使用区域文件和搜索域(例如 Kubernetes 中使用了搜索域!),所以结尾的 . 很有用,可以让人确切的知道,不应该再添加其他东西。

什么时候在末尾添加 “.”?

以下是关于何时在域名末尾加 ". " 的几个简单说明:

需要添加:配置 DNS 时

在配置 DNS 时,使用完全限定域名从来都不是坏事。你不一定要这样做:非完全限定域名通常也能正常工作,但我从来没有遇到过不接受完全限定域名的 DNS 软件。

有些 DNS 软件需要这样做:现在我为 jvns.ca 使用的 DNS 服务器让我在域名的末尾加上 .(例如在 CNAME 记录中),并提示如果我不添加,它将在我输入的内容末尾加上 .jvns.ca。我不同意这个设计决定,但这不是什么大问题,我只是在最后加一个 .

不需要加:在浏览器中

令人困惑的是,在浏览器中,在域名结尾处加一个 . 不能正常运行。例如,如果我在浏览器中输入 https://twitter.com.,它就会报错。它会返回 404。

我认为这里发生的事情是,它将 HTTP Host 标头设置为 Host:twitter.com.,而对端的 Web 服务器则期望 Host:twitter.com

同样地,https://jvns.ca. 由于某种原因,返回了一个 SSL 错误。

我认为相对域名在过去是比较常见的

最后一件事:我认为“相对”域名(比如我用 grapefruit 来指代我家的另一台计算机 grapefruit.lan)在过去更常用,因为 DNS 是在大学或其他有大型内部网络的大机构中开发的。

在今天的互联网上,使用“绝对”域名(如 example.com)似乎更为普遍。


via: https://jvns.ca/blog/2022/09/12/why-do-domain-names-end-with-a-dot-/

作者:Julia Evans 选题:lujun9972 译者:lxbwolf 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出