node服务中遇到的后端问题

项目中使用 nodeJS 写 web 业务时,碰上了一些问题,花费了一些时间解决,记录一下以防再掉坑里。

我们使用 nodeJS 实现了一个 web 服务,对接了后端服务的网关,并且向前端返回静态资源。

JS 中的整数

首先是大整数的问题。对接用户中心时,我们的 node 服务需要给前端返回用户 id。然而,在使用此用户 id 去查询时,却发现查不到此用户的内容。在定位过程中,发现此用户的 id 甚至不在数据库中。最后发现是我们的 node 服务在返回真实 id 时,直接返回了整型,使 JS 丢失了精度。

1
2
> 409119721175122920
< 409119721175122940 // 大整数丢失精度

首先复习一下 10 进制转换为 2 进制。

整数部分,除 2 取余,直至商数为 0,从下到上读余数,即是二进制的整数部分。小数部分,用其小数部分乘 2,取其整数部分的结果,再用计算后的小数部分依此重复计算,算到小数部分全为 0 为止,从上到下读所有计算后整数部分的数字,即是二进制的小数部分。

二进制科学计数法:59.25(10) = +111011.01(2) = +11.101101(2) x 2^4 = +1.1101101(2) x 2^5

JS 中的 Number 对象是采用双精度IEEE 754 64位浮点类型来存储的,也是用二进制的科学计数法表示一个数的。对整数的表示等同于双精度浮点数(64-bit)对整数的表示。它的规则如下:

Sign(符号位) Exponent(指数位偏移) Fraction(有效数字)
1 11 52
+-1 规定指数偏移量为 2^(e - 1) - 1 位,64 位需要偏移 1023 1.f * 2 ^ p

指数部分为什么引入偏移量?

方便运算。如果没有偏移量的存在,指数需引入符号位, 则需引入补码,比较计算更加复杂,为了简化操作,才使用无符号的阶码,引入偏移量的概念。

为什么指数需要11位?Why do higher-precision floating point formats have so many exponent bits?

表达式 约束 说明
(−1)^s × 1.f × 2^(e−1023) 0 < e < 2047, e 值域[1, 2046],指数值域 [−1022, +1023] 规约化数值
(−1)^s × 0.f × 2^(-1022) e = 0, f > 0。 指数部分的偏移量比规约形式少 1,对双精度浮点格式来说即 1022,所以非规约形式的指数 e 总是-1022。 非规约化数值,用来表示无限接近 0 的数
(−1)^s × 0 e = 0, f = 0 正负 0,js 中它们相等
NaN e = 2047, f > 0 指数位全为 1,有效数字大于 0,不是 number
(−1)^s × ∞ (infinity) e = 2047, f = 0 正负无穷

为什么 52 位有效数字能表示 53 位的精度呢?IEEE754 规定规约数的尾数第一位隐含为 1(二进制的科学计数法,第一位总是 1,规定省略),不存入比特位中。那么,1.xxxx(52 个 x)最多共有 53 位精确位,拿来做整数表示,这 53 个二进制 1 转换为 10 进制,值为 2^53 - 1,科学计数法表示为1.111…111 * 2^52 = 2^53 - 1,指数计算结果为52(e=1075)。

BigInt:BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这是 Javascript 中可以用 Number 表示的最大安全整数。BigInt 可以表示任意大的整数。可以用在一个整数字面量后面加n的方式定义一个 BigInt。

1
2
3
4
5
6
// 最大安全整数
// https://www.zhihu.com/question/29010688
> 2**53 - 1
< 9007199254740991

typeof 1n === 'bigint'; // true

综上,前后端应当约定好,处理此类可能存在大整数的情况下,使用字符串来进行交互。这里我们使用了json-big-int来处理 json 中的大整数。

1
2
3
4
5
import { parse } from "json-bigint";

fetch("/url")
.then((res) => res.text())
.then((res) => parse(res)); // 其中的大整数将被bignumber库处理

怎么来判断大整数呢?

  • 字符串位数
  • ECMAScript 6 中 MAX_SAFE_INTEGER 和 MIN_SAFE_INTEGER
  • bignumber中有更为严谨的判断方法
1
2
3
4
5
6
7
8
Number.isSafeInteger = function (n) {
return (
typeof n === "number" &&
Math.round(n) === n &&
Number.MIN_SAFE_INTEGER <= n &&
n <= Number.MAX_SAFE_INTEGER
);
};

为什么0.1 + 0.2 != 0.3?

首先,IEEE 754 舍入规则是:

Round to nearest, ties to even
向最接近的整数舍入;当到两边整数的距离相等时,向最接近的偶数舍入。(四舍六入五成双)
对于二进制而言,舍弃的位为1则进位,0则舍去。

0.1的二进制表示为 1.(1001*) * 2 ^ -4,存储为双精度浮点为
理论存储
0 01111111011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001 无限循环)
超过部分进一舍零,实际存储为
0 01111111011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

0.2的二进制表示为 1.(1001*) * 2 ^ -3,存储为双精度浮点为
理论存储
0 01111111100 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001 无限循环)
超过部分进一舍零,实际存储为
0 01111111100 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

相加时,指数需要统一为-3,0.1左移一位
0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 0
1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

得到
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
科学计数表示为 1.(0011*) * 2
1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1 (53位将被舍弃,向前进1位)
有效部分实际存储为
0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100

而0.3实际存储为
0 01111111101 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011

转换浮点数可在这查看

签名按字典序

在数字签名鉴权时,通常会要求按一个字典序来排列参数。字典排序(lexicographical order)是一种对于随机变量形成序列的排序方法。其方法是,按照字母顺序,或者数字小大顺序,由小到大的形成序列(0-9A-Za-z)。而 Js 的 sort 默认采用字典序。

1
[1, 2, 5, 10, "A", "", "ba", "ab", "aa"].sort(); // ["", 1, 10, 2, 5, "A", "aa", "ab", "ba"]
  • 多个字符串按字典序排列
  • 一个字符串按字典序排列
  • 全排列

nginx 开启 gzip

nginx 默认是关闭 gizp 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name .example.com
# max upload size
client_max_body_size 75M;
# 启用 gzip 压缩功能
gzip on;
# nginx做前端代理时启用该选项,表示无论后端服务器的headers头返回什么信息,都无条件启用压缩
gzip_proxied any;
gzip_vary on;
gzip_http_version 1.1;
# 哪些种类开启 gzip_types *; 是没效果的
gzip_types application/javascript application/json text/css text/xml image/png;
gzip_comp_level 4;

location /example {
alias /usr/local/etc/files/mysite/media_root;
}

}

JWT 退出登陆

当点了退出登陆后,怎么清除客户端侧的登陆信息呢:设置空的 session cookie。

1
2
3
4
'get /api/logout': (req, res) => {
res.clearCookie('session_usr');
return res.send('ok');
}

如果是多系统共用用户中心,在别的系统中退出,如何通知我们的系统清空用户状态呢?有一种方式是使用<img>向我们的系统发送一个上述请求,清空服务端的 session,同时向浏览器种下空 session。