百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程文章 > 正文

Node.js 中遇到大数处理精度丢失如何解决?前端也适用

qiyuwang 2024-10-31 15:50 25 浏览 0 评论

作者:五月君来源: Nodejs技术栈

在 JavaScript 中浮点数运算时经常出现 0.1+0.2=0.30000000000000004 这样的问题,除此之外还有一个不容忽视的大数危机(大数处理精度丢失)问题。

JavaScript 最大安全整数

在开始本节之前,希望你能事先了解一些 JavaScript 浮点数的相关知识,在上篇文章 JavaScript 浮点数之迷:0.1 + 0.2 为什么不等于 0.3? 中很好的介绍了浮点数的存储原理、为什么会产生精度丢失(建议事先阅读下)。

IEEE 754 双精确度浮点数(Double 64 Bits)中尾数部分是用来存储整数的有效位数,为 52 位,加上省略的一位 1 可以保存的实际数值为 。

Math.pow(2, 53) // 9007199254740992
Number.MAX_SAFE_INTEGER // 最大安全整数 9007199254740991  
Number.MIN_SAFE_INTEGER // 最小安全整数 -9007199254740991  

只要不超过 JavaScript 中最大安全整数和最小安全整数范围都是安全的。

大数处理精度丢失问题复现

例一

当你在 Chrome 的控制台或者 Node.js 运行环境里执行以下代码后会出现以下结果,What?为什么我定义的 200000436035958034 却被转义为了 200000436035958050,在了解了 JavaScript 浮点数存储原理之后,应该明白此时已经触发了 JavaScript 的最大安全整数范围。

const num = 200000436035958034;
console.log(num); // 200000436035958050 

例二

以下示例通过流读取传递的数据,保存在一个字符串 data 中,因为传递的是一个 application/json 协议的数据,我们需要对 data 反序列化为一个 obj 做业务处理。

const http = require('http'); 
http.createServer((req, res) => { 
  if (req.method === 'POST') { 
    let data = ''; 
    req.on('data', chunk => {
      data += chunk;         });
    req.on('end', () => {
      console.log('未 JSON 反序列化情况:', data); 
      try {
        // 反序列化为 obj 对象,用来处理业务
        const obj = JSON.parse(data); 
        console.log('经过 JSON 反序列化之后:', obj); 
        res.setHeader("Content-Type", "application/json"); 
        res.end(data);
      } catch(e) { 
        console.error(e);
        res.statusCode = 400; 
        res.end("Invalid JSON"); 
      } 
    }); 
  } else { 
    res.end('OK');
  } 
}).listen(3000) 

运行上述程序之后在 POSTMAN 调用,200000436035958034 这个是一个大数值。

以下为输出结果,发现没有经过 JSON 序列化的一切正常,当程序执行 JSON.parse() 之后,又发生了精度问题,这又是为什么呢?JSON 转换和大数值精度之间又有什么猫腻呢?

未 JSON 反序列化情况: {  "id": 200000436035958034 }
经过 JSON 反序列化之后: { id: 200000436035958050 } 

经过 JSON 反序列化之后: { id: 200000436035958050 }

这个问题也实际遇到过,发生的方式是调用第三方接口拿到的是一个大数值的参数,结果 JSON 之后就出现了类似问题,下面做下分析。

JSON 序列化对大数值解析有什么猫腻?

先了解下 JSON 的数据格式标准,Internet Engineering Task Force 7159,简称(IETF 7159),是一种轻量级的、基于文本与语言无关的数据交互格式,源自 ECMAScript 编程语言标准.

https://www.rfc-editor.org/rfc/rfc7159.txt 访问这个地址查看协议的相关内容。

我们本节需要关注的是 “一个 JSON 的 Value 是什么呢?” 上述协议中有规定必须为 object, array, number, or string 四个数据类型,也可以是 false, null, true 这三个值。

到此,也就揭开了这个谜底,JSON 在解析时对于其它类型的编码都会被默认转换掉。对应我们这个例子中的大数值会默认编码为 number 类型,这也是造成精度丢失的真正原因。

大数运算的解决方案

1. 常用方法转字符串

在前后端交互中这是通常的一种方案,例如,对订单号的存储采用数值类型 Java 中的 long 类型表示的最大值为 2 的 64 次方,而 JS 中为 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),显然超过 JS 中能表示的最大安全值之外就要丢失精度了,最好的解法就是将订单号由数值型转为字符串返回给前端处理,这是在工作对接过程中实实在在遇到的一个坑。

2. 新的希望 BigInt

Bigint 是 JavaScript 中一个新的数据类型,可以用来操作超出 Number 最大安全范围的整数。

创建 BigInt 方法一

一种方法是在数字后面加上数字 n

200000436035958034n; // 200000436035958034n 

创建 BigInt 方法二

另一种方法是使用构造函数 BigInt(),还需要注意的是使用 BigInt 时最好还是使用字符串,否则还是会出现精度问题,看官方文档也提到了这块 github.com/tc39/proposal-bigint#gotchas--exceptions 称为疑难杂症

BigInt('200000436035958034') // 200000436035958034n
// 注意要使用字符串否则还是会被转义
BigInt(200000436035958034) // 200000436035958048n 这不是一个正确的结果 

检测类型

BigInt 是一个新的数据类型,因此它与 Number 并不是完全相等的,例如 1n 将不会全等于 1。

typeof 200000436035958034n // bigint
1n === 1 // false 

运算

BitInt 支持常见的运算符,但是永远不要与 Number 混合使用,请始终保持一致。

// 正确
200000436035958034n + 1n // 200000436035958035n
// 错误
200000436035958034n + 1
^  TypeError: Cannot mix BigInt and other types, use explicit conversions 

BigInt 转为字符串

String(200000436035958034n) // 200000436035958034
// 或者以下方式
(200000436035958034n).toString() // 200000436035958034 

与 JSON 的冲突

使用 JSON.parse('{"id": 200000436035958034}') 来解析会造成精度丢失问题,既然现在有了一个 BigInt 出现,是否使用以下方式就可以正常解析呢?

JSON.parse('{"id": 200000436035958034n}'); 

运行以上程序之后,会得到一个 SyntaxError: Unexpected token n in JSON at position 25 错误,最麻烦的就在这里,因为 JSON 是一个更为广泛的数据协议类型,影响面非常广泛,不是轻易能够变动的。

在 TC39 proposal-bigint 仓库中也有人提过这个问题 github.comtc39/proposal-bigint/issues/24 截至目前,该提案并未被添加到 JSON 中,因为这将破坏 JSON 的格式,很可能导致无法解析。

BigInt 的支持

BigInt 提案目前已进入 Stage 4,已经在 Chrome,Node,Firefox,Babel 中发布,在 Node.js 中支持的版本为 12+。

BigInt 总结

我们使用 BigInt 做一些运算是没有问题的,但是和第三方接口交互,如果对 JSON 字符串做序列化遇到一些大数问题还是会出现精度丢失,显然这是由于与 JSON 的冲突导致的,下面给出第三种方案。

3. 第三方库

通过一些第三方库也可以解决,但是你可能会想为什么要这么曲折呢?转成字符串大家不都开开心心的吗,但是呢,有的时候你需要对接第三方接口,取到的数据就包含这种大数的情况,且遇到那种拒不改的,业务总归要完成吧!这里介绍第三种实现方案。

还拿我们上面 大数处理精度丢失问题复现 的第二个例子进行讲解,通过 json-bigint 这个库来解决。

知道了 JSON 规范与 JavaScript 之间的冲突问题之后,就不要直接使用 JSON.parse() 了,在接收数据流之后,先通过字符串方式进行解析,利用 json-bigint 这个库,会自动的将超过 2 的 53 次方类型的数值转为一个 BigInt 类型,再设置一个参数 storeAsString: true 会将 BigInt 自动转为字符串。

const http = require('http');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
http.createServer((req, res) => {
  if (req.method === 'POST') {
    let data = '';
    req.on('data', chunk => {
      data += chunk;     });
    req.on('end', () => {
      try {
        // 使用第三方库进行 JSON 序列化
        const obj = JSONbig.parse(data)
        console.log('经过 JSON 反序列化之后:', obj);
        res.setHeader("Content-Type", "application/json");
        res.end(data); 
      } catch(e) {
        console.error(e);
        res.statusCode = 400;
        res.end("Invalid JSON");
      } 
    });
  } else {
    res.end('OK');
  }
}).listen(3000) 

再次验证会看到以下结果,这次是正确的,问题也已经完美解决了!

JSON 反序列化之后 id 值: { id: '200000436035958034' } 

json-bigint 结合 Request client

介绍下 axios、node-fetch、undici、undici-fetch 这些请求客户端如何结合 json-bigint 处理大数。

模拟服务端

使用 BigInt 创建一个大数模拟服务端返回数据,此时,若请求的客户端不处理是会造成精度丢失的。

const http = require('http');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
http.createServer((req, res) => { 
  res.end(JSONbig.stringify({ 
    num: BigInt('200000436035958034')
  }))
}).listen(3000) 

axios

创建一个 axios 请求实例 request,其中的 transformResponse 属性我们对原始的响应数据做一些自定义处理。

const axios = require('axios').default;
const JSONbig = require('json-bigint')({ 'storeAsString': true});
const request = axios.create({
  baseURL: 'http://localhost:3000',   transformResponse: [function (data) {
    return JSONbig.parse(data)   }], });
request({   url: '/api/test' }).then(response => { 
  // 200000436035958034 
  console.log(response.data.num); }); 

node-fetch

node-fetch 在 Node.js 里用的也不少,一种方法是对返回的 text 数据做处理,其它更便捷的方法没有深入研究。

const fetch = require('node-fetch');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
fetch('http://localhost:3000/api/data')  
 .then(async res => JSONbig.parse(await res.text()))   
 .then(data => console.log(data.num)); 

undici

request 这个已标记为废弃的客户端就不介绍了,再推荐一个值得关注的 Node.js 请求客户端 undici,前一段也写过一篇文章介绍 request 已废弃 - 推荐一个超快的 Node.js HTTP Client undici。

const undici = require('undici'); 
const JSONbig = require('json-bigint')({ 'storeAsString': true}); 
const client = new undici.Client('http://localhost:3000'); 
(async () => {   const { body } = await client.request({     path: '/api',     method: 'GET',   });      
 body.setEncoding('utf8');   
              let str = '';   
              for await (const chunk of body) {     str += chunk;   }
              console.log(JSONbig.parse(str)); // 200000436035958034
              console.log(JSON.parse(str)); // 200000436035958050 精度丢失 
             })(); 

undici-fetch

undici-fetch 是一个构建在 undici 之上的 WHATWG fetch 实现,使用和 node-fetch 差不多。

const fetch = require('undici-fetch');
const JSONbig = require('json-bigint')({ 'storeAsString': true});
(async () => {   const res = await fetch('http://localhost:3000'); 
const json = JSONbig.parse(await res.text());   
 console.log(json.num); // 200000436035958034
    })(); 

总结

本文提出了一些产生大数精度丢失的原因,同时又给出了几种解决方案,如遇到类似问题,都可参考。还是建议大家在系统设计时去遵循双精度浮点数的规范来做,在查找问题的过程中,有看到有些使用正则来匹配,个人角度还是不推荐的,一是正则本身就是一个耗时的操作,二操作起来还要查找一些匹配规律,一不小心可能会把返回结果中的所有数值都转为字符串,也是不可行的。【编辑推荐】

相关推荐

windows开启telnet服务,检测远程服务端口是否可以连通

本文介绍windwos开启telnet服务,telnet服务一般可以用于检测远程主机的某个端口服务是否可以连通,在日常的工作中,我们经常会遇到在本地的windows检测远程服务端口是否可以连通。win...

仅在Web登录新华三交换机条件下启用设备Telnet登录方式

概述Web登录新华三交换机可以在“网络-服务”页面中启用设备Telnet服务或SSH服务,也可以在“设备-管理员”设置管理员用户的可用服务,然而,在设备Web页面中,无法设置lineVTY用户线【l...

思科交换机,路由器如何关闭telnet 开启ssh服务

SSH为建立在应用层基础上的安全协议。SSH是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用SSH协议可以有效防止远程管理过程中的信息泄露问题。今天我们就来说说思科交换机,路...

智能化弱电行业常用的DOS命令,掌握了你也能成为...

前言在做智能化弱电项目时,前端摄像头设备安装结束后,我们会对网络摄像头进行调试,调试过程中会遇到前端摄像头没有图像或者图像出来了画面卡顿的现象。我们会采用ping命令来测试网络的连通性和网络承载能力。...

「干货」eNSP模拟器之配置Telnet登录

配置说明:配置Telnet,使R2(模拟PC)通过SW1登录到R1进行管理和配置。操作步骤:system-view##进入系统视图[Huawei]sysnameR1##改名为R1[R1]int...

win11开启telnet服务怎么操作 win11打开telent指令是什么

telnet服务是我们在进行远程连接的时候,必须要打开的一项功能。但是有不少用户们不清楚在windows11系统中怎么开启telnet服务。今天小编就使用详细的图文教程,来给大家说明一下打开telen...

华三(H3C)交换机Telnet的远程登陆

一,配置交换机管理IP[SW1]vlan20//创建管理vlan[SW1]interfacevlan20//进入vlan接口[SW1-Vlanif20]ipaddress192.168....

win10 telnet命令怎么查看端口是否打开

可能大家也会遇到这个问题,win10telnet命令查看端口是否打开的步骤是什么?具体方法如下:1、键盘输入快捷键WIN+R,打开运行窗口。2、输入cmd,点击确定按钮。3、弹出cmd命令行窗...

Windows 7如何打开Telnet功能(win7系统打开telnet)

Windows7默认安装后是没有开启telnet客户端功能的,例如,我们在开始菜单中输入cmd,然后使用telnet命令,会弹出下图提示:‘telnet’不是内部或外部命令,也不是可运行程序或批处理文...

为锐捷路由器交换机开启web和telnet,实现轻松管理

笔者上一篇文章写了关于锐捷二层交换机配置教程,那么接下来讲一下锐捷的路由交换设备配置web、telnet技巧。同样,今天的教程也是基于命令行,比较简单,适合新手小白进行学习。准备工作配置前准备:con...

一文学会telnet命令的用途和使用方法

Telnet是一个古老的远程登录协议,可以让本地计算机获得远程计算机的工作能力。它采用了TCP的可靠连接方式,可以连接任何网络互通的远程计算机。不过由于它采用了明文传输方式,存在安全风险,目前已经很少...

Telnet命令是什么?如何使用?(telnet命令在哪里开启)

telnet命令是一个常用的远程登陆工具,使用它,我们可以快捷地登陆远程服务器进行操作。那么如何使用telnet命令呢?首先,我们需要打开telnet功能,任何电脑默认是关闭此功能的,开启方式如下:打...

win11系统如何开启telnet服务(拷贝版本)

  我们要知道,Telnet协议是Internet远程登陆服务的标准协议,可以使用户在本地计算机上完成远程主机的工作,不过对于一些刚接触win11中文版系统的用户来说,可能还不知道telnet服务在哪...

如何开启telnet客户端(如何开启telnet服务)

Telnet协议是TCP/IP协议家族中的一员,是Internet远程登陆服务的标准协议和主要方式,Telnet是常用的远程控制Web服务器的方法。工作中经常用到telnet客户端,但在windows...

Telnet 是什么,如何启用它?(telnet有什么用)

对于Internet等TCP/IP网络,Telnet是一个终端仿真程序。Telnet软件在您的系统上运行并将您的个人计算机链接到网络服务器。它将所有数据转换为纯文本这一事实被认为是易受...

取消回复欢迎 发表评论: