Riff

每天晚上、辣个男人、打开Visu⚪⚪⚪...


  • 首页

  • 归档

angular6+koa+pm2部署前端服务器

发表于 2018-08-11 | 更新于 2018-11-24

不可避免地会出现以下概念,希望在阅读前能理解:

  • angular6 的打包和初始化
  • 基础的nodejs编程

前言

为何要部署前端服务器?

分离前后端

要知晓这个概念,首先就要区别出前后端的概念。
在传统的web应用开发中,大多数会将浏览器作为区别前后端的标准:为用户展示界面的部分称为前端,为界面提供数据或提供业务逻辑服务的称为后端。

而在 传统单体应用 开发模式中,所有的页面都和后端代码放在一个项目中,部署时也是整个项目部署,前端开发人员甚至还要掌握一些后端开发的知识才能输出代码。
为了让前后端分工更加明确,更好的各司其职,从长远来看,前后端分离也是一个必然的趋势和结果。

解耦代码

code-repo
在传统的 单体应用 开发模式中,所有的代码都放在同一个项目中,不能单独运行,这个时候的项目是 前后端未分离的。

  • 半分离
    前后端共在一个项目,共用一个代码库,但是分别建立前端和后端的工程;
    前端不用关心后端代码是怎么写的,后端也不用担心前端代码是什么样的,但整个项目不能单独运行,需要后端和前端进行交互。

  • 完全分离
    前后端分别建立项目,代码库相互独立;
    前端能独立开发,依靠 mock程序或测试构造一个伪后端 运行
    后端也能独立开发,编写有详细的测试用例,保证API的可用性,并做好详细的接口文档,使其跟前端能够顺利集成。

解耦部署

在部署单体应用中,整个项目打包上服务器即可运行,这个时候的项目是前后端未分离的。

部署半分离的前后端项目也是一样,发布整个项目即可。

完全分离前后端后,前后端可单独部署,本章结即是讲解部署前端项目部分。

⚠️ 前后端分离总体提升了部署和开发的复杂程度,对于小项目来说是过度设计,导致开发效率更低。

分离提升性能瓶颈

有时候前端的流量和后端不在一个量级上,分离前后端可以使前后端采用不同的架构设计,使系统更加强大。

一些大厂的解决方案比如阿里云的控制台管理系统,部署时就采用反向代理服务器,把获取页面的请求路由到前端服务器,接口请求路由到后端服务器。

你甚至可以在http response 的 header 里看到所使用的反向代理服务器的名称。

为何要选择koa作为前端服务器?

Koa 是一个基于 nodejs 平台的下一代 web 开发框架,相对最初火热的 Express 框架来说充分利用async 等一套优雅的方法,摒弃了”难看”的多层callback嵌套,能够快速而愉快地编写服务端应用程序。

而nodejs平台本身就具有着独特的异步、非阻塞I/O的特点,这也就意味着他特别适合I/O密集型操作,在处理并发量比较大的请求上能力比较强。
此外,现代web开发框架(angular,vue,react)都基本上利用nodejs平台搭建前端项目,使用nodejs能无缝集成前端项目;
前端项目打包生成静态文件后,直接运行nodejs程序,利用其高I/O向客户端提供静态文件,nodejs确实是一个不错的选择。

部署前端服务器

运行环境介绍

服务器系统:centerOS 7 (64位)
本地环境: windows 10 (64位)
连接工具:MobaXterm

我的服务器是在阿里云买的ECS,购买的时候直接选了centerOS 7,服务器上必须安装nodejs运行环境,如何安装在这里不做赘述。
安装完 nodejs 后用 npm 全局安装 pm2(一个nodejs程序托管系统),输入命令行npm install -g pm2即可。

前端项目介绍

项目采用angular+koa搭建的一个前端项目:
directory-tree

  • dist/spa-server是我们前端项目打包后的静态文件,展示网站主要就是输出这些文件。
  • src 是前端项目的源代码
  • koa-app.js 是运行前端服务器的脚本
  1. 先使用的angular cli生成前端项目脚手架,再使用npm i koa koa-static koa-router安装运行koa的依赖库。
  2. 在项目根目录添加运行服务器的脚本 koa-app.js
    添加以下基本代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    const koa = require("koa");
    const router = require("koa-router")();
    const static = require('koa-static');
    const app = new koa();

    const path = require('path');
    const fs = require('fs');

    var staticFileFolder = path.join(__dirname,"dist","spa-server");
    var indexPath = path.join(staticFileFolder,"index.html");
    /******************************
    * Set Static File
    * 设置静态文件后访问该文件路径 会直接返回不会走下一个中间件
    *****************************/
    app.use(static(
    staticFileFolder
    ));

    /******************************
    * logger
    *
    *****************************/
    app.use(async (ctx, next) => {
    await next();
    console.log(`${ctx.method} : ${ctx.url} - ${hResponse}`);
    });

    /******************************
    * Routes
    *
    *****************************/
    // 读取 index.html 的文件内容
    const indexPage = fs.readFileSync(indexPath).toString();

    router.get('*', async (ctx, next) => {
    ctx.response.body = indexPage;
    });
    app.use(router.middleware())



    // 监听端口,启动服务器
    app.listen(80);

上面的代码在路由部分做了特殊的处理,因为使用angular6搭建的网页为单页面应用程序(SPA),路由部分是由前端来处理的。
这种特性使得服务器要对请求的路径做出特殊响应:所有访问页面的路径都默认返回index.html的内容,然后交由前端的js来解析路径生成页面。可参考angular官网说明

首先要在本地能够运行服务器,在项目根目录输入node koa-app.js运行服务器脚本。访问浏览器,页面如下:
page
本地运行可以正常访问的话,接下来就可以正式部署到服务器上了。

部署到服务器

精简上传文件

传输到服务器上运行的文件不必像本地开发一样,只需保留必须的文件即可,最终精简如下:
page

  • www 是我们前端项目打包后的静态文件
  • 由于静态文件的文件夹的名称和路径都变了, koa-app.js 中需修改相应代码

    1
    2
    3
    var staticFileFolder = path.join(__dirname,"dist","spa-server"); 
    // 改为如下:
    var staticFileFolder = path.join(__dirname,"www");
  • deploys.json 这个新增的文件用来引导 pm2的运行,基本内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "apps": [
    {
    "name": "deploy-test1",
    "script": "./koa-app.js",
    "env": {
    "COMMON_VARIABLE": "true"
    },
    "env_production": {
    "NODE_ENV": "production"
    }
    }
    ]
    }

name 是 托管到pm2上的程序名称
script 是 程序的路径

  • package.json 是nodejs程序的工程信息,随着项目初始化新建。包括我们程序的依赖项都在该处配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "name": "publish",
    "version": "1.0.0",
    "description": "",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "koa": "^2.5.2",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0"
    }
    }

dependencies 即是程序的依赖项,必须保留 koa,koa-router,koa-static

上传到服务器

这里我使用 MobaXterm 作为远程工具:
保持目录结构,上传到服务器上,定位到项目的根目录位置下,输入npm install安装所有的依赖包,如下
linux-server

运行程序

安装完所有的依赖包程序就可以成功运行了,
还是定位到项目的根目录下,输入pm2 start deploy.json

linux-server

如果看到下面类似仪表盘的东西,恭喜你已经成功部署了前端服务器。

pm2会帮你维持程序的运行防止意外挂掉,可以使用一些命令行查看程序的运行状态:
pm2 list查看所有正在运行程序的状态快照
pm2 monit打开监控仪表盘,监控所有运行的程序

这个时候就可以访问我们部署在服务器上的网站了:
linux-server

  • 如果不能访问网站可以搜索防火墙设置与端口开放的方法开放端口
  • 阿里云等一些云服务器需配置安全策略,把一些端口设置白名单才可访问网站

后话

前端单独部署,如果跟后端服务器不在一个域名或端口的话还会有跨域的问题。
为了解决这个问题,可以部署一个反向代理服务器,把前端的请求路由到前端服务器上,后端的请求路由到后端服务器上,对外都是只访问这个反向代理服务器,跨域问题也就迎刃而解。

【C#】使用async&await异步编程

发表于 2018-08-07 | 更新于 2018-11-24

内网访问开发服务器

发表于 2018-08-03 | 更新于 2018-11-24

前言

在平时的开发中,除了使用本机的浏览器测试页面,有时也要使用其他设备测试。
但是开发调试,运行的是本地的开发服务器,在局域网中是访问不到的,这是因为开发服务器地址是localhost,ipv4是127.0.0.1。
这个地址相当于仅本机的局域网,所以外部网络无法访问。

根据这个原因,网站在局域网中访问的方法有:

  • 部署到IIS
    (更改后需发布,对于日常开发还是略显繁琐)

本文主要介绍另外的方法:

  • 更改开发服务器的地址 为内网地址
  • 使用 nginx 代理请求 转到开发服务器

以上方法均利用了 设置服务器主机ip 为 内网ip 的方式实现内网访问

更改开发服务器的主机ip 为内网ip

这里仅介绍visual studio下的IIS Express配置:

  1. 打开项目目录找到.sln文件 , 同目录下找到 .vs 的隐藏文件夹
  2. 找到路径 .vs/config 下的文件 applicationhost.config 打开
    • 运行网站时也可以右键IIS Express –> “显示所有应用程序” –> 选择你运行的网站 –> 点击下边的配置打开文件
  3. 编辑applicationhost.config ,如下图:
    • 找到 sites/site/bindings/binding 的节点
    • 编辑bindingInfosrmation属性 –> 替换 localhost 为 本机局域网ip
      (或者新增binding节点 –> 编辑bindingInfosrmation属性 替换 localhost 为本机局域网ip)

edit

  1. 重新运行项目即可

使用 nginx 代理请求 转到开发服务器

nginx 是一款强大的web服务器和反向代理服务器。
nginx 的安装非常简单,使用也很简单:启动程序时根据配置文件实现相应的功能。
利用它可以代理我们开发服务器的端口,从而可以在映射的地址上访问我们的开发服务器。

更多概念及原理可以去官网上了解,这里只做windows下使用介绍

1. 安装 nginx

从 官方网站 下载windows发布版压缩包(推荐下载stable version的稳定版本);
解压后的目录即是nginx程序的运行目录,运行nginx.exe即可。

但实际可能不止运行一个服务器,或单独一种配置。这时候就需配置环境变量,从命令行运行程序

  • 右键 “这台电脑” –> “高级系统设置” –> “环境变量” 找到 “系统变量”
  • 编辑 “Path” 变量 –> 把解压后nginx.exe所在目录路径 完整的复制到最后面,注意目录之间要使用”;” 隔开
    path-edit
  • 运行命令行, 输入 nginx -v,若跟下载的版本匹配则表示环境配置成功
    path-edit2

2. 配置 nginx

一个能供nginx运行的基本文件目录结构为:
nginxFolder

  • conf 配置文件的目录,运行时未指定配置文件会默认使用 conf/nginx.conf 路径的文件.
  • logs 项目启动时记录文件的目录,程序运行的pid,errorLog就记录在这里,可为空目录.
  • temp 其他临时文件的目录,可为空目录.

编辑好配置文件,运行nginx即可。

配置文件可参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#运行用户
#user somebody;

#启动进程,通常设置成和cpu的数量相等
worker_processes 1;

#全局错误日志
# error_log C:/Users/Administrator/Desktop/nginxSetting/logs/error.log;
# error_log D:/Tools/nginx-1.10.1/logs/notice.log notice;
# error_log D:/Tools/nginx-1.10.1/logs/info.log info;

#PID文件,记录当前启动的nginx的进程ID
# pid C:/Users/Administrator/Downloads/======Installer/soft/nginx-1.13.10/logs/nginx.pid;

#工作模式及连接数上限
events {
worker_connections 1024; #单个后台worker process进程的最大并发链接数
}

#设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
#设定mime类型(邮件支持类型),类型由mime.types文件定义
# include C:/Users/Administrator/Downloads/======Installer/soft/nginx-1.13.10/conf/mime.types;
default_type application/octet-stream;

#设定日志
log_format main '[$remote_addr] - [$remote_user] [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

# access_log C:/Users/Administrator/Downloads/======Installer/soft/nginx-1.13.10/logs/access.log main;
rewrite_log on;

#sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
#必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度,降低系统的uptime.
sendfile on;
#tcp_nopush on;

#连接超时时间
keepalive_timeout 35;
tcp_nodelay on;

#gzip压缩开关
#gzip on;

# #设定实际的服务器列表
# upstream zp_server1{
# # server localhost:4079;
# }

# #HTTP服务器
# server {

# listen 192.168.3.104:8001;

# #定义使用www.xx.com访问
# # server_name 192.168.3.104;

# #首页
# # index index.html

# #指向webapp的目录
# # root D:\01_Workspace\Project\github\zp\SpringNotes\spring-security\spring-shiro\src\main\webapp;

# #编码格式
# charset utf-8;

# #代理配置参数
# proxy_connect_timeout 180;
# proxy_send_timeout 180;
# proxy_read_timeout 180;
# proxy_set_header Host $host;
# proxy_set_header X-Forwarder-For $remote_addr;

# #反向代理的路径(和upstream绑定),location 后面设置映射的路径
# location / {
# proxy_pass http://localhost:4079;
# }

# #静态文件,nginx自己处理
# # location ~ ^/(images|javascript|js|css|flash|media|static)/ {
# # root D:\01_Workspace\Project\github\zp\SpringNotes\spring-security\spring-shiro\src\main\webapp\views;
# # #过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。
# # expires 30d;
# # }

# #设定查看Nginx状态的地址
# location /NginxStatus {
# stub_status on;
# access_log on;
# auth_basic "NginxStatus";
# auth_basic_user_file conf/htpasswd;
# }

# #禁止访问 .htxxx 文件
# location ~ /\.ht {
# deny all;
# }

# #错误处理页面(可选择性配置)
# #error_page 404 /404.html;
# #error_page 500 502 503 504 /50x.html;
# #location = /50x.html {
# # root html;
# #}
# }

server {
listen 192.168.3.104:10086;

location / {
# proxy_pass http://localhost:49234;
# proxy_pass http://localhost:55773;
# proxy_pass http://localhost:4079;
proxy_pass http://localhost:4200;
}


}
}

修改下列参数直接使用:

该配置文件无法代理除http以外的协议!! 如https,websocket等

详细的http代理配置可参考 官方文档 - ngx_http_proxy_module 这块

3. 运行 nginx

直接在nginx的配置目录下运行命令行:

  • 输入 start nginx可运行程序,如图:
    step1

    运行的第二个nginx进程为守护进程,在第一个进程挂掉的时候负责重启

  • 输入 nginx -s stop 可停止程序

  • 输入 nginx -s reload 可重新载入配置文件

nginx -h 可以查看其他命令参数,比如其他经常使用的:

  • -v 查看版本号
  • -t 检查配置文件是否正确
  • -c 路径 指定使用配置文件的相对路径(一般和其他参数组合使用)

【C#】使用闭包

发表于 2018-08-01 | 更新于 2018-11-24

不可避免地会出现以下概念,希望在阅读前能理解:

  • 闭包(Closure)
  • C# 中的委托,lambda表达式 ,匿名方法

前言

javascript中的闭包

这个概念经常在javascript中见到,因为闭包几乎是支撑起javascript生态不可或缺的重要特性!在你熟悉的任何库或框架中(如jquery,vue等)都使用了闭包。

在其他强类型的语言如java, C#,私有变量可以通过private修饰一个成员变量声明。
但是在js中没有这种机制,但是同样可通过闭包封装私有变量:利用了闭包保存了内部函数变量的引用,而外部函数无法直接获取变量的引用。

比如,在浏览器中原生只有 “click” 事件可供使用,并不提供 “双击事件”,利用闭包,我们可以轻松实现双击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 双击包装函数
// 闭包保存了如下变量的引用,形成"私有变量"
function dblclick(handler){
var delay = 300; //双击间隔最大300ms
var first = Date.now();

return function(){
var second = Date.now();
var isDblclick = second - first <= delay;
first = second;
if(isDblclick) handler();
}
}
// 绑定点击事件
document
.querySelector("body")
.addEventListener("click",dblclick(function(){
alert("dblclick!");
}))

C#中的闭包

C#中的闭包也是同样的表现:通过闭包保存了内部函数对变量的引用。

一个经典的案例

声明一个集合,保存我们循环5次期间所创建的函数,每个函数打印出当前循环的索引值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(
() => Console.WriteLine(i)
);
}

actions.ForEach(func => func());
// 输出:
// 5
// 5
// 5
// 5
// 5

所有的变量都打印为最终的索引,因为打印的变量 i 的值指向了最终的索引值。

很显然这不是我们想要的结果,所以在每次循环的时候,我们需要保存 i 的值,这个时候就轮到 “闭包” 发挥作用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建一个函数,接收一个值
// 传入的索引值和返回函数就形成了闭包
Action ActionFactory(int index)
{
return () => Console.WriteLine(index);
}


List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(
ActionFactory(i)
);

}
actions.ForEach(func => func());
// 输出:
// 0
// 1
// 2
// 3
// 4

传入ActionFactory的index参数被内部函数所引用,形成闭包,结果就是我们想要的。

创建闭包

以上演示可以看出,闭包实质上就是保存一个变量在内部函数的引用,而外部函数无法访问。
所以在C#中可创建函数形成闭包的方式有:

  • 委托
  • 表达式树

举一个简单的例子:
创建一个函数,根据传入参数值,返回一个附加该值的函数

使用 委托 创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通过Lambda
Func<int,int> SetAdditional(int additional)
{
return v => (additional + v);
}

// 通过匿名方法
Func<int,int> SetAdditional(int additional)
{
return delegate (int v)
{
return additional + v;
};
}


// 获取闭包后的函数
var addition_10 = SetAdditional(10);
var addition_1 = SetAdditional(1);

var num = 1;
addition_1(num); // 2
addition_10(num); // 11

使用 表达式树 创建

表达式树也是通过创建委托的方式形成闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Func<int, int> SetAdditional(int additional)
{
var parameter = Expression.Parameter(typeof(int), "v");
var addition = Expression.Constant(additional,typeof(int));

var lambda = Expression.Lambda(
Expression.Add(
addition,
parameter),
parameter);

return lambda.Compile() as Func<int,int>;
}

// 获取闭包后的函数
var addition_10 = SetAdditional(10);
var addition_1 = SetAdditional(1);

var num = 1;
addition_1(num); // 2
addition_10(num); // 11

⚠️ 一般函数执行完毕后,内部的变量会被回收。但是由于闭包保存了变量的引用,导致该变量无法被释放,始终占用物理内存。
当闭包的函数越来越多的时候,就造成了程序的性能下降


闭包是委托的一种使用技巧,而委托能使得代码更具抽象能力。正确的理解闭包无疑可以更好的使用委托,使代码更”优雅”。

程序中计算小数,0.1+0.2为何不等于0.3

发表于 2018-07-28 | 更新于 2018-11-24

参考的文章:

  • 双精度,单精度和半精度
  • 为什么0.1+0.2不等于0.3
  • 该死的IEEE-754浮点数……

问题

起初问题是来自项目里,一段数字莫名奇妙得显示成了 “乱码” :

1
6.93889390390723e-18

这种问题是不是似曾相识呢?

相信你肯定知道javascript里面一个”bug”:

1
2
3
0.1 + 0.2 = 0.30000000000000004 ; // 0.1+ 0.2 > 0.3 

0.6 - 0.5 - 0.1 = -2.7755575615628914e-17;// 0.6 - 0.5 - 0.1 < 0

得出的这个数是不是就像 “乱码” ?

实际这段 “乱码” 是程序语言中的科学计数的表示方法:
e作为分隔符,所以这段数字折合成十进制就是 -2.7755575615628914 * 10^-17

计算出来的结果是一个非常小的数字。
而且只要使用双精度浮点型类型计算都会出现该问题。

原因

想要回答这个问题,就得从计算机存储数字的原理说起。

计算机如何存储数字

我们都知道,计算机只能认得出0和1,所以数据在计算机中都是二进制的方式存储,最小的单位就是位(bit)。
程序中所使用的数字存储到计算机中,也都是以二进制的方式存储。

但是计算机怎么就凭0和1知道我们要存储的数字了呢?
答案是 指定存储数据的数据类型

以C#的数据类型举例:常规的 Byte,float,double,Int16,Int,Int64,UInt16…都是用来指定 “0和1” 的数据类型。

计算机如何读取数字

计算机在存储的时候指定了数据类型,在读取的时候就可以根据存储的数据类型计算出原来的数字(计算机如何识别数据类型这里不展开)。

在C#中:

1
2
3
4
5
byte num = 15;
// 在计算机存储为 (byte长度为8位)
00001111
// 这段二进制表示的数据解析出值为(按位计数法)
0*2^7 + 0*2^6 + 0*2^5 + 0*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0 = 15

在 javascript 中 number 都是以双精度浮点型来存储的,在C#中对应的类型就是double

浮点数

浮点数。顾名思义,用来保存有小数点的数。也使用二进制存储,但是保存规则和解析规则不同(参考IEEE 754)

  • 双精度浮点数
    float1

  • 单精度浮点数
    float2

  • 半精度浮点数
    float3

它们都分成3部分,符号位,指数(阶码)和尾数。不同精度只不过是指数位和尾数位的长度不一样。

解析一个浮点数就5条规则 (下例为单精度)

  • 如果指数位全零,尾数位是全零,那就表示0
  • 如果指数位全零,尾数位是非零,就表示一个很小的数(subnormal),计算方式 (−1)^sign_bit × 2^−126 × 0.fraction_bits
  • 如果指数位全是1,尾数位是全零,表示正负无穷
  • 如果指数位全是1,尾数位是非零,表示不是一个数NAN
  • 剩下的计算方式为 (−1)^sign_bit × 2^(exponent_bits−127) × 1.fraction_bits
  1. 浮点数指数部分为了表示正数和负数,有偏移量,规则为: 指数的值 = 指数部分按位计数的值 - 偏移量。
  2. 这里的 0.fraction_bits,1.fraction_bits 都表示二进制小数(关于如何计算二进制小数不展开)。
    ⚠️ 这些都不是我制定的,都来自国际标准 IEEE 754

我们就拿上图单精度浮点数来说,保存一个单精度浮点数

1
2
3
4
5
6
// 总共 32bit
SIGN EXPONENT FRACTION
0 01111100 0100 0000 0000 0000 0000 000

// 表示的值为 :
(−1)^0 * 2^124-127 * 1.01 = 2^-3 * (1*2^0 + 0 + 1*2^-2) = 2^-3 * 1.25 = 0.15625

如何用二进制表示0.1?

上面介绍了小数在计算机中是如何存储的,这里我们直接看0.1是如何表示的:

关于十进制的转换这里就直接说结论:
十进制整数转二进制方法:除2取余;十进制小数转二进制方法:乘2除整

十进制0.1表示成二进制小数,乘2取整过程:

1
2
3
4
5
6
7
8
9
10
11
12
0.1 * 2 = 0.2 # 0
0.2 * 2 = 0.4 # 0
0.4 * 2 = 0.8 # 0
0.8 * 2 = 1.6 # 1
0.6 * 2 = 1.2 # 1
0.2 * 2 = 0.4 # 0
.....
// 转换成二进制大约是0.000110001100011...这样的数

// 转换成双精度浮点类型的计算方式为:
SIGN EXPONENT FRACTION
(-1)^0 * 2^-3 * 1.10001100011...

从上面可以看出,转换出来的二进制小数是一个无限循环的,保存到浮点数尾数也是无限的。

但是数据存储的位数有限,我们不能保存所有的尾数,这时候该怎么保存进去呢?
答案是: 在某个精度点直接舍入
当然造成的后果就是0.1并不是精确表示的,是有舍入误差的0.1。

相等运算就是比较位数,舍入误差会引起如下场景:

1
2
3
0.100000000000000002 == 0.1 //true
0.100000000000000002 == 0.100000000000000010 //true
0.100000000000000002 == 0.100000000000000020 //false

可以使用工具查看转换出的二进制来分析原因

精度是在哪里丢失的?

保存数值时

由于我们的十进制转换为二进制,而二进制不能精确得表示出来,这个时候就有了舍入误差。

IEEE 754规定了几种舍入规则,但是默认的是舍入到最接近的值,如果“舍”和“入”一样接近,那么取结果为偶数的选择。

进行运算时

在浮点数参与计算时,有一个步骤叫对阶,以加法为例,要把小的指数域转化为大的指数域,也就是左移小指数浮点数的小数点。一但指数左移,必然要把尾数最右边的 “挤出去”,这个时候挤出去的部分也会发生舍入,再次发生舍入误差。

当这个误差大到编译不能忽略的时候,自然就被表示出来了
理解计算机这么做的”苦衷”,也就理解0.1+0.2不等于0.3了 😊

解决

  • 计算时转换成整数,把小数部分抵消掉,计算结果再还原位数

    1
    2
    3
    ✗ 0.1 + 0.2 
    ✓ (0.1*10 + 0.2*10)/10
    // 可以封装,使用截取小数位等方式实现
  • 在javascript中可使用专门的库,如 math.js, bignumber.js

  • 使用专门为解决这类问题封装的类型,在c#中有decimal,java中BigDecimal等

Gkeeno

Gkeeno

5 日志
5 标签
© 2018 Gkeeno
由 Hexo 强力驱动 v3.7.1
|
主题 — NexT.Muse v6.3.0