Ramon's Blog

记录平时工作学习中的知识


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

IO多路复用整理

发表于 2020-03-22 | 更新于: 2020-04-09 | 分类于 linux
字数统计: 2.4k 字 | 阅读时长 ≈ 8 分钟

介绍

大学时候写在csdn上的一篇博客,最近在看异步IO,就准备先介绍下IO方面内容,给后续的Node异步IO介绍奠定一个基础,同时也把当时的文章改进下搬过来。

基础介绍

首先最基础的,我们先来理解什么是IO。

什么是IO?

我们都知道unix世界里,一切皆文件(不知道的现在也知道了),而文件是什么呢?文件就是一串二进制流,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。

在信息交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output),例如读出数据使用系统调用read,写入数据使用系统调用write。

文件描述符

计算机中这么多流,我们是如何知道要操作哪个流呢?这时候我们需要有一个能够对文件进行定位的标识符,文件描述符应运而生。

概念

文件描述符是内核为了高效管理已经被打开的文件所创建的索引,它是一个从0开始的整数(对每个进程而言),程序所有执行的I/O操作都是通过文件描述符进行的。

分配规则

在程序刚刚启动时,0,1,2三个文件描述符已经被占用了:

  • 0代表标准输入设备stdin
  • 1代表标准输出设备stdout
  • 2代表标准错误stderr

POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此再打开一个文件,它的文件描述符会是3。

文件描述符作为文件的索引,也有它的最大限制。作为系统的一个重要的资源,实际中最大打开的文件数是系统内存的10%,这个是系统级限制,有系统就会有用户,用户级限制是单个进程最大打开的文件数,一般是1024,可以使用ulimit -n命令查看(我这里是256🌚)。

系统是通过文件描述符定位到文件的流程如下:

系统为每一个进程维护了一个文件描述符表,这是一个进程级的文件描述符表,它通过文件描述符所对应的文件指针指向系统级的打开文件描述符表中的一个打开文件句柄,句柄中存储了打开文件相应的全部信息,包括文件偏移量、状态标示、访问模式文件类型以及文件属性等等,其中有一个inode指针,它指向了inode表中该文件的表项,具体想要了解inode可以看我之前的文章。

阻塞与非阻塞

阻塞:进程调用了不能立即完成的任务而进行等待

非阻塞:进程调用了立即返回的任务,无需长时间等待

说的很简单,但从进程角度看可以这么理解:

首先看下进程状态转换图:

进程状态转换图

(偷了个懒,从网上找的,原文地址)

进程阻塞可以看做执行了系统调用后,因为有长时间的任务,从而转换成waittinig状态,(因为cpu资源是宝贵的,同一时间一个CPU核只能处理一个running的任务)。

非阻塞就是相反的,一直是running状态(进程是并发的,所以并不会一直是running,不过因为切片很小,我们从宏观认为他是没变的)。

这里聊一下非阻塞与异步IO的区别,有文章说是维度上的不同(不是一类概念),也有人说是一回事。我参考一些文章后得出如下结论:

  • 非阻塞是直接得到结果,这个结果可以是空值、完整的结果或者不完整的
  • 异步IO是必定会返回一个完整的结果,只是可能会需要等待一段时间后返回

IO多路复用

好了,我们讲了这么多,那么,到底什么是I/O多路复用呢?

概念

IO复用就是一种机制,可以同时监控多个阻塞的IO,一旦发现进程指定的一个或者多个IO准备读取,它就通知该进程。

多进程多线程当然也可以处理,但是开销增加的太大了,不只是空间的浪费,同时进程线程间的切换开销也很大(时间成本)。IO复用则是执行系统调用对IO端口进行监控,返回执行情况,显然要比我们在用户空间的操作要快得多。

实现

select, poll, epoll 都是I/O多路复用的具体的实现,下面我们按顺序介绍。

select

函数介绍

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

select的做法是将监听端口加到集合fs_set中,轮训监听端口状态,一旦发现了有IO事件,则返回通知进程,过程如下图:

select流程

问题

select 被实现以后,很快就暴露出了很多问题。

  1. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。

  2. select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找。

  3. select 只能监视1024个链接, linux 定义在头文件中的,参见FD_SETSIZE。

  4. select执行时会将fs_set整个从用户空间copy到内核空间,如果很大时复制操作消耗资源会比较多。

  5. select 不是线程安全的。如果你把一个sock加入到select,然后突然另外一个线程发现,这个sock不用,要收回。这个在这个select 不支持的,如果你强行关掉这个sock,select的描述是不可预测的,以下为官网介绍:

    “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”

poll

针对select的问题,14年以后(1997年)poll被实现出来。

poll与select最大的不同为存储监听端口的不再是数组,而是链表(具体实现看平台),于是也就没有了1024的限制,但是一些问题依然没有解决,比如线程安全、拷贝到内核空间以及轮训检查等等。

epoll

针对以上问题,在5年后的2002,大神 Davide Libenzi 实现了epoll。

实现

epoll的内部实现分为两部分:

  • 用于存储监听端口的红黑树
  • 用于存储有IO事件端口的双端队列

参考图如下(图片来源:《深入理解 Nginx:模块开发与架构解析(第二版)》,陶辉):

epoll

我们添加的监听端口存放在红黑树中,当端口有IO事件时,epoll会调用回调函数将事件放到队列中,进程只需要监听队列即可。

至于监听的端口存放在红黑树中,显然是为了支持快速的插入、删除以及取出操作(红黑树建议了解一下)。

优点

epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题,比如:

  1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
  2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。(共享内存的方式实现)

epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

扩展

epoll支持水平触发和边缘触发,边缘触发即它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次,这种效率要比水平触发一直占着队列的坑位每次都要处理要好一些,不过边缘触发既然发生后不会在提示,记得一次读完。

总结

本文是一个偏科普的,比较基础,也是自己大学时候写的(貌似很多参考别人的=-=),做了一点改进,不过很多内容都没有细说,epoll后面会专门出一篇文章介绍,毕竟实在是太重要了,红黑树不了解的同学也希望去深入了解下,基本逢考必问。

参考文章

原文链接

同步/异步,阻塞/非阻塞概念深度解析

IO复用原理剖析

Epoll原理解析

注:此文章改自以前的文章,加了些改动,之前没写明引用文章出处,现在也很难找,如果有引用您的原创内容,请及时联系,在此处添加您的文章信息。

Node模块详解(二)

发表于 2020-03-07 | 更新于: 2020-03-17 | 分类于 Node.js
字数统计: 2.1k 字 | 阅读时长 ≈ 8 分钟

介绍

上篇文章介绍了模块机制中的文件模块(直通链接),本篇文章继续接着介绍node模块机制中剩余的模块类型:

  • 核心模块
  • 扩展模块

注:本文是学习《深入浅出Node.js》一书的学习笔记,所以参考源码与书上版本一致。(版本:v0.10.13-release)

核心模块

什么是核心模块

区别于文件模块等第三方导入模块,核心模块就是直接包含在node源码中,为用户提供服务的模块。

当然这些模块也需要导入的,想了解都有哪些模块,直接进入官网,找到Docs文档,里面介绍的模块都是核心模块(链接:核心模块介绍)。

简单列举几个比较常用的核心模块:

  • fs:提供对文件的读写等操作
  • http:提供了搭建本地服务器的API
  • path:提供文件路径操作的API
  • os:操作系统相关信息的API
  • url:提供了操作URL信息的API

核心模块导入流程

核心模块的使用官网已经非常详细了,我也没什么好介绍的,今天的重点是核心模块在Node源码中是以什么形式存在的,以及核心模块的导入流程。

Node源码中的核心模块

核心模块一般分为两部分:

  • C++编写的内建模块(核心实现)
  • JavaScript编写的封装模块

其中C++部分的代码存放在src/目录下,而JavaScript的代码存放在lib/下。

一般我们调用的都是JS编写的封装模块,在对Node源码不是特别了解的情况下,不建议直接调用C++编写的内建模块(调用方法也就不告诉你了)。

导入流程

导入流程主要分为如下几个步骤:

  1. js代码转存
  2. 运行时从内存中取出内容
  3. 编译执行
  4. 对象缓存和返回

看不懂没关系,下面详细介绍。

js代码转存

node编译过程中,并不会对js部分的代码进行编译操作,而是调用tools/js2c.py工具将js代码转换成C++中的字符串数组,存放在node_natives.h文件中,手动执行后的情况如下:

js2c

看代码就知道,这一步其实做了个很简单的事情,把js文件内容console.log("hello");变成ASCII码放在$filename_native数组中,这样node源码编译时,js文件的内容也就以字符串的形式编译进了可执行文件中。

取出文件

知道内容被存到node_natives.h文件中,那么取出的代码就很好找了,首先找到调用文件的地方,运气很好,只有一个文件node_javascript.cc,而且确实是获取js文件的代码,函数很简单,直接贴源码了

1
2
3
4
5
6
7
8
9
10
11
void DefineJavaScript(v8::Handle<v8::Object> target) {
HandleScope scope;

for (int i = 0; natives[i].name; i++) {
if (natives[i].source != node_native) {
Local<String> name = String::New(natives[i].name);
Handle<String> source = BUILTIN_ASCII_ARRAY(natives[i].source, natives[i].source_len);
target->Set(name, source);
}
}
}

一共几行代码,功能就是把想要的js文件内容取出,这里有个条件,是除了node_native模块,这里其实就是node.js文件,与其他js文件不同的是它在src/下,这个文件很重要,这里先不说明,我们继续往下走。

以前版本的代码就是简单,这里我们查使用函数DefineJavaScript()的地方,发现也只有一个地方,就是我们的node.cc文件(题外话,这个文件就是定义着我们的全局变量process的地方),而使用的函数为Binding(),以下为源码内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static Handle<Value> Binding(const Arguments& args) {
Local<String> module = args[0]->ToString();
String::Utf8Value module_v(module);
...
if ((modp = get_builtin_module(*module_v)) != NULL) {
...
} else if (!strcmp(*module_v, "constants")) {
...
} else if (!strcmp(*module_v, "natives")) {
// 重点关注内容
exports = Object::New();
DefineJavaScript(exports);
binding_cache->Set(module, exports);
} else {
return ThrowException(Exception::Error(String::New("No such module")));
}
return scope.Close(exports);
}

去掉无关的代码,留下我们的重要关注内容,从这部分代码可以得出,调用process.Binding('natives')就可以取出内存中的js代码,而这个引用就在我们刚刚谈到的node.js中,

1
NativeModule._source = process.binding('natives');

重点出来了,之前我们讲过,文件模块操作的对象为Module,那么我们核心模块的对象就是NativeModule,

编译&缓存

当我们对一个核心模块引用时,实际上就是运行了下列代码:

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
NativeModule.require = function(id) {
if (id == 'native_module') {
return NativeModule;
}

var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}

if (!NativeModule.exists(id)) {
throw new Error('No such native module ' + id);
}

process.moduleLoadList.push('NativeModule ' + id);

var nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;
};
// 从缓存中取出
NativeModule.getCached = function(id) {
return NativeModule._cache[id];
}
// 判断是否存在
NativeModule.exists = function(id) {
return NativeModule._source.hasOwnProperty(id);
}
// 从内存中得到js代码
NativeModule.getSource = function(id) {
return NativeModule._source[id];
}
// 封装
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
// 编译
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

var fn = runInThisContext(source, this.filename, true);
fn(this.exports, NativeModule.require, this, this.filename);

this.loaded = true;
};
// 保存到缓存中
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};

一下子放这么多代码实在是不应该,但是这部分代码简单而道出了核心模块导入的本质,实在不忍心割掉一点点,每个函数都对功能添加了注释,请务必仔细研读。

另外谈一点,核心模块的js代码因为是从内存中取出的,相比于从磁盘取出的文件模块加载速度肯定要快上很多。

核心模块至此就结束了,流程简单来说就是:编译node时将核心js代码变成c++中的字符串数组,引用的时候从内存中取出并进行编译,然后缓存放到NativeModule._cache中并返回编译后的对象,如果你看过文件模块的介绍,应该并不难掌握。

扩展模块

终于讲完了核心模块,希望你已经掌握,下面来讲一种特殊的文件模块,扩展模块。

什么是扩展模块

亘古不变,我们先来说下是什么。

扩展模块是用C/C++代码预编译后得到的.node文件,代码中调用process.dlopen()进行加载执行。显然这种方式性能要高于js编写的文件模块,而且在需要大量的位运算操作的场景中,C/C++的性能要优于JS。

JavaScript的位运算参照Java的位运算实现,但是Java位运算是在int型数字的基础上进行的,而JavaScript中只有double型的数据类型,在进行位运算的过程中,需要将double型转换为int型,然后再进行。所以,在JavaScript层面上做位运算的效率不高。(节选自书中)

这里你可能要说,那大家都去写扩展模块算了。世界上没有完美的东西,扩展模块也有他的缺点,当然,C/C++语言的学习和开发难度肯定是第一点,不然脚本语言也不会盛行于世,其次就是不再支持跨平台,对于*nix和windows平台的差异如下:

区别

这里也为我们展现了扩展模块的编译和导入流程。

所以在需要跨平台的应用上,应该对扩展模块的使用要小心,

实现一个自己的扩展模块

是的,这里应该有一个实现,不过这部分我并没有掌握清楚,书中的例子因为版本古老,编译失败了,所以这里也就不展示了,给自己留个坑,也希望感兴趣的小伙伴可以研究实现一个自己的扩展模块,加油,你们是最胖的!!!

总结

模块深入学习打开了我对node源码解读的开端,尽管代码是老的版本,但依旧能从中学到很多,这一系列文章是我读《深入浅出Node.js》一书的学习笔记,也希望自己能够成功读完,并将学习后的知识分享给大家。

Node模块详解(一)

发表于 2020-03-01 | 更新于: 2020-03-04 | 分类于 Node.js
字数统计: 2.5k 字 | 阅读时长 ≈ 9 分钟

介绍

本文是阅读《深入浅出Node.js》后对自己所学和理解的内容进行整理后完成的,结合Node源码为读者解读模块这一基础而又非常重要的一个机制是怎么实现和运转的。

此文为Node模块详解系列第一篇,主要介绍模块以及实现。

CommonJS规范

说到模块就先介绍下CommonJS规范,因为模块是它提出的,而Node也对规范有了很好的实现(尽管Node的npm管理器作者在13年宣称废弃它)。

引用维基百科的介绍:

CommonJS是一个项目,其目标是为JavaScript在网页浏览器之外创建模块约定。

说白话,就是希望JS摆脱前端束缚,能够在任何地方使用,比如:

  • 服务端
  • 命令行工具
  • 桌面应用程序
  • 混合应用(Titanium和Adobe AIR等形式的应用(我也没懂zzz~))

详细了解去看CommonJS官网,这里不做详细介绍,我们重点关注模块。

CommonJS的模块规范

CommonJS对模块的定义为模块引用、模块定义和模块标识3个部分。

  1. 模块引用

    通过require()方法导入一个模块的API到当前上下文中。

    示例代码:

    1
    var http = require("http");
  2. 模块定义

    对应引入的功能,上下文提供了exports对象。在模块中,存在一个module对象,它代表模块自身,exports是module的属性,用于导出当前模块的方法或者变量,并且它是唯一导出的出口,module信息如下图:

    module信息

    在Node中,一个文件就是一个模块,将值传给exports对象即可导出。

  3. 模块标识

    模块标识其实就是传递给require()方法的参数,即导入模块的name。

模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。

Node模块实现

Node参考CommonJS规范进行模块设计,同时也增加了一些自身的特性,本文我们主要研究Node内部如何实现模块机制。

在Node中,模块主要分为两类

  • 核心模块:Node提供的模块(Node启动时直接加载)
  • 文件模块:用户编写的模块(运行时动态加载)

本文主要主要研究的是文件模块的加载编译流程,主要分为三个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

路径分析和文件定位

顾名思义:找到要导入的模块。因为模块标识符情况比较多,所以在这里讲一下。标识符主要分为以下几类:

  • 核心模块:如http、fs等直接写
  • 相对路径文件模块:.或者..开头的
  • 绝对路径模块:以/开头的
  • 自定义模块:在node_modules目录中的模块

核心模块直接编译在node内部,导入最快,带路径的则要进行解析,解析流程如下:

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理。

在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。

而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.node、index.json。

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

内容引用书里的,本来想画个流程图,结果发现还不如直接看原文明白,毕竟这里不是我们的重点关注,也就不细讲了。

前面讲了这么多,终于讲到我们今天的重点:模块编译。

模块编译

上述步骤后,我们已经定位到文件,进入后续编译和执行环节。

注:本章主要结合源码进行分析,有兴趣可以下载源码学习(版本:v0.10.13-release)。

之前讲到过,一个文件就是一个模块,而一个(文件)模块也是一个对象,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}

this.filename = null;
this.loaded = false;
this.children = [];
}

模块的载入以文件类型进行区分,不同的扩展名,载入方式也不同,分为以下四种情况:

  • js文件:通过fs模块同步读取文件后编译执行
  • node文件:用c/c++编写的扩展文件,通过dlopen()加载编译生成的文件
  • json文件:通过fs模块读取,使用JSON.parse()解析返回
  • 其他扩展名文件:当做.js文件载入

解析扩展名的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Module.prototype.load = function(filename) {
debug('load ' + JSON.stringify(filename) +
' for module ' + JSON.stringify(this.id));

assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));

var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js'; // 这里对其他扩展名以js文件对待
Module._extensions[extension](this, filename);
this.loaded = true;
};

接下来对这三种文件的载入以及编译进行详细讲解

.js文件

.js文件是我们主要关注的重点,它要比其他两种情况多一个包装的步骤。

使用过node的朋友都知道,每个模块都有__filename、__dirname这两个变量的存在,并且模块也有自己单独的作用域,不同模块之间参数不会被相互污染,这个其实就是Node在编译前对模块文件进行了封装,代码如下:

1
2
3
4
5
6
7
8
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

非常简单的代码,却实现了每个模块文件之间作用域的隔离,为模块注入了灵魂。

包装之后的代码会通过vm原生模块的runInThisContext()方法执行,返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行,代码如下:

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
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};

// Returns exception if any
Module.prototype._compile = function(content, filename) {
var self = this;
// remove shebang
content = content.replace(/^\#\!.*/, '');
...
if (Module._contextLoad) {
// create wrapper function
var wrapper = Module.wrap(content); //这里进行的包装

var compiledWrapper = runInThisContext(wrapper, filename, true);
if (global.v8debug) {
if (!resolvedArgv) {
// we enter the repl if we're not given a filename argument.
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null);
} else {
resolvedArgv = 'repl';
}
}
// Set breakpoint on module start
if (filename === resolvedArgv) {
global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0);
}
}
var args = [self.exports, require, self, filename, dirname]; //注意这里
return compiledWrapper.apply(self.exports, args);
};

这样一个js文件就被导入执行了,这里有一点要注意的是(上面注释有标明),exports原来是module的exports参数,为什么这么做,不直接传一个module对象或者将exports单独拿出来呢?原因主要有如下两点:

  • 达到require只引入一个类的效果
  • exports是形参引用,修改无效

.node文件

.node的模块文件实际上是编写C/C++模块之后编译生成的,所以并不需要编译操作,加载和运行通过调用process.dlopen()方法来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
if (manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};

dlopen()在文件src/node.cc中有着相关实现,主要是依托于libuv库进行的封装,执行后.node中的对象传给exports返回给调用者。

.node文件模块因为是C/C++编译生成的,所以执行效率远高于.js文件模块,不过因为学习门槛高,流程复杂,大都更倾向于.js文件模块。

.json文件

最后说的是最简单的.json文件模块,调用fs模块同步读取文件内容后,通过调用内部方法JSON.parse()解析得到对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');

if (manifest) {
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}

try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};

因为导入的模块会进行缓存,二次导入的时候直接读取缓存中的对象,所以这种文件模块导入的方式要优于在代码中进行导入的实现。

总结

本文主要是我在学习《深入浅出Node.js》一书中的学习笔记,是模块系列的第一篇,这一部分代码与最新的没什么区别,后续的源码也会继续沿用书中的老版本,毕竟没有人带路去读最新的node源码实在头疼,理解了书中的思想后续在对升级后的代码进行理解相比会舒服很多(自以为🌚)。

七天学会nodejs整理

发表于 2020-02-23 | 更新于: 2020-02-24 | 分类于 Node.js
字数统计: 5.4k 字 | 阅读时长 ≈ 20 分钟

介绍

工作原因接触了NodeJS,开始学习它,不过以前对这门语言不了解,学习了基本语法可以工作后就没有加深学习了。时间长了后发现这门语言比我想象中强大了很多,于是开始准备全面了解,深入学习,本文是学习了七天学会NodeJS之后的一个整理总结,入门向的一篇文章,旨在让我们可以全面了解这门语言,有兴趣的可以参考。

NodeJS基础

学习NodeJS,首先了解它是什么。

JS我们都知道–JavaScript,一门前端都在使用的脚本语言,而NodeJS就是运行在后端的js语言。换句话说,前端使用的JS是由浏览器进行解析运行的,而NodeJS就是一个独立的js解析器,随时随地,有node环境就可以解析运行。

NodeJS诞生的目的就是为了实现高性能的Web服务器(作者自己说的),至于为什么使用JS这门语言,主要有如下几点:

  1. JS这门语言自带的事件机制和异步IO模型(契合作者的需求)
  2. 同时JS没有自带的IO功能,不会有历史包袱(我理解是作者可以随意发挥)
  3. 有一大批前端程序员的拥护(用户众多)
  4. chrome浏览器的v8高性能JS引擎的出现(可移植,有大哥维护)

在这么多的优势的加护下,NodeJS也如作者期望的那样发展的欣欣向荣。

入门向的文章,安装运行少不了,不过原文已经很详细了,这里就不赘述了。

nodejs基础

(为什么思维导图放在中间了,因为以下就开始将知识点了=-=)

模块是nodejs中很重要的一个概念,简单来说一个文件就是一个模块,文件名就是模块名。

模块中有三个预先定义好的变量:

  • require: 加载其他模块
  • exports: 当前模块的导出对象
  • module: 当前模块的信息

关于模块的内容后面会专门写一篇文章详细介绍,此处略过。

最后注意一下,Node有个缓存机制,会将require的模块中编译执行后的对象进行缓存,二次加载时使用缓存的对象。

代码的组织和部署

搭建房子首先要规划好房子结构,而稍微大一点的项目免不了要准备好代码的目录结构和部署方式,这一章主要是组织结构的介绍。

代码的组织和部署

模块解析规则

模块解析规则也就是模块导入的规则,主要分为三种

  • 内置模块
  • node_moudle模块
  • NODE_PATH环境变量

原文说的很清楚了,这里补充的一点是查看当前的node_module目录可以参考如下方法:

包

包(PACKAGE)可以理解是多个模块的组合。复杂的模块往往由多个子模块组成,包就是多个子模块的集合。包中有两个重点:

  • 入口模块
  • package.json

入口模块的导出对象是包的导出对象,因此我们可以直接导入入口对象就算是导入了整个包。

不过这样做并不够灵活,如果入口模块名字改了,所有导入它的代码都要跟着修改,因为我们引入了package.json。

最简单的package.json文件我们可以这样写:

1
2
3
4
{
"name": "cat",
"main": "./lib/main.js"
}

这样,加再模块时,只需要使用require('/home/user/lib/cat')的方式就可以了,Node会自己根据package.json的配置找到入口模块。

package.json还有其他的成员,一个完整的package.json可以如下内容:

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
{
"name": "Hello World",
"version": "0.0.1",
"author": "张三",
"description": "第一个node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": {
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}

具体每个字段介绍可以参考JavaScript标准参考教程。

命令行程序

NodeJS可以编写命令行程序,只需要在程序前面加下如下内容

1
#! /usr/bin/env node

然后在/usr/local/bin/下添加一个软链接就可以实现了,下面是一个很简单的实例:

node-hello

Windows会有些不一样,我没用windows测试,这里也不介绍了,感兴趣自己看原文。

工程目录

代码的组织,可以参考如下布局:

1
2
3
4
5
6
7
8
9
10
11
- /home/user/workspace/node-echo/   # 工程目录
- bin/ # 存放命令行相关代码
node-echo
+ doc/ # 存放文档
- lib/ # 存放API相关代码
echo.js
- node_modules/ # 存放三方包
+ argv/
+ tests/ # 存放测试用例
package.json # 元数据文件
README.md # 说明文件

没什么好聊的,只是个参考。

NPM

NPM是NodeJS的包管理工具,常见的使用场景如下:

  • 允许用户从NPM服务器下载别人编写的三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

NPM搭建了NodeJS的生态圈。

这里有一点注意的,我们可以在之前的package.json文件中描述依赖的第三方包,直接通过npm install进行安装,避免了环境搭建的烦恼(很棒的功能)。

npm -l可以看到都有哪些命令,想看命令的详细介绍也可以使用npm help <command>,文档非常全,这里就不赘述了。

文件操作

一个后端程序怎么能少得了文件操作。

文件操作

文章有个copy文件的小程序很不错,可以引出一个知识点,程序如下:

1
2
3
4
5
6
7
8
9
var fs = require('fs');

function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));

程序很简单,这里说他主要是下面这个小知识点:

process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。

API

和文件相关的API有很多,这里拿出几个来对比

  • Buffer
  • Stream
  • File System

Buffer

Buffer类的官方解释:

Buffer 类的实例类似于从 0 到 255 之间的整数数组(其他整数会通过 & 255 操作强制转换到此范围),但对应于 V8 堆外部的固定大小的原始内存分配。 Buffer 的大小在创建时确定,且无法更改。

在我看来,Buffer底层就是一个unsigned char类型的数组,申请所需的堆空间并存储二进制数据。

使用场景;

多用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。

相关api使用去官网查看文档。

Stream

照例来段官方解释:

流(stream)是 Node.js 中处理流式数据的抽象接口。

当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。

流主要分为以下几种

  • 可读流
  • 可写流
  • 读写流
  • 转换流 Transform

在node中,这四种流都是EventEmitter的实例,它们都有close、error事件,可读流具有监听数据到来的data事件等,可写流则具有监听数据已传给低层系统的finish事件等,Duplex 和 Transform 都同时实现了 Readable 和 Writable 的事件和接口 。

当然,流也不是完美的,虽然消耗很少的内存,但那是比较理想的情况,如果读方没有尽快处理,会导致大量的数据被积压,而如何处理积压问题可以观看此文章数据流中的积压问题,官方文档,童叟无欺。

File System

正经的文件操作模块。

fs模块提供的API基本操作分为三类:

  • 文件属性读写
  • 文件内容读写
  • 底层文件操作

如下为一个简单而又典型的异步IO模型读取文件的示例代码:

1
2
3
4
5
6
7
fs.readFile(pathname, function (err, data) {
if (err) {
// Deal with error.
} else {
// Deal with data.
}
});

网络操作

千呼万唤始出来,终于到了网络模块。

先来个官方文档的例子:

1
2
3
4
5
6
var http = require('http');

http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('Hello World\n');
}).listen(8124);

这个是一个简单的HTTP服务器,打开浏览器访问该端口http://127.0.0.1:8124/就能够看到效果。

网络操作

API

与网络操作相关的API主要有如下几个:

  • HTTP:网络服务核心模块
  • HTTP/2:h2版本
  • HTTPS:加密版本
  • URL:url解析
  • Zlib:提供了数据压缩和解压的功能
  • net:用于创建Socket服务器或Socket客户端

下面介绍重点api:

HTTP

http模块是NodeJS中的核心模块,主要提供两种使用:

  • 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。
  • 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。

服务端

本章刚开始的小例子就是一个服务端的实现,使用.createServer方法创建一个服务器,然后调用.listen方法监听端口,当有客户端请求过来时,就会有request事件被触发,调用回调函数进行返回。

HTTP请求本质是一个数据流,由请求头和请求体组成,默认大家都了解,这里不再赘述。

服务端也是继承自EventEmitter的,而它所提供的事件如下:

  • request:当客户端请求到来时,该事件被触发,提供两个参数req和res,表示请求和响应信息。

  • connection:当TCP连接建立时,该事件被触发,提供一个参数socket,是net.Socket的实例。

  • close:当服务器关闭时,触发事件(注意不是在用户断开连接时)。

注意:request事件的参数req和res分别是http.IncomingMessage和http.ServerResponse的实例。

http.IncomingMessage是HTTP请求的信,其提供了三个事件

  • data:当请求体数据到来时,该事件被触发,该事件提供一个参数chunk,表示接受的数据,如果该事件没有被监听,则请求体会被抛弃,该事件可能会被调用多次(这与nodejs是异步的有关系)
  • end:当请求体数据传输完毕时,该事件会被触发,此后不会再有数据
  • close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close

而http.ServerResponse是返回给客户端的信息,决定了用户最终看到的内容,类似于上面,他有三个重要成员函数,用于返回响应头、内容以及结束请求:

  • res.writeHead():向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头

  • res.write():向请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用

  • res.end()):结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。

客户端

作为客户端就要简单很多了,先来个小栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var options = {
hostname: 'www.example.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};

var request = http.request(options, function (response) {});

request.write('Hello World');
request.end();

这里用到了http.request,另一个函数为http.get,功能是作为客户端向http服务器发起请求。

因为GET请求不需要请求体,所以请求不太一样,再来个小栗子:

1
http.get('http://www.example.com/', function (response) {});

也没什么好介绍的了,详细介绍看官方文档吧。

HTTPS

https模块与http模块极为类似,区别在于https模块需要额外处理SSL证书。

在服务端模式下,创建一个HTTPS服务器的示例如下:

1
2
3
4
5
6
7
8
var options = {
key: fs.readFileSync('./ssl/default.key'),
cert: fs.readFileSync('./ssl/default.cer')
};

var server = https.createServer(options, function (request, response) {
// ...
});

可以看到,与创建HTTP服务器相比,多了一个options对象,通过key和cert字段指定了HTTPS服务器使用的私钥和公钥。

HTTPS与HTTP的区别这里就不介绍了,后面会专门出一篇文章,感兴趣的小伙伴也可以自己去查,网上有很多相关文章。

进程管理

NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。

进程管理

API

进程管理有关的API如下:

  • process: 当前进程管理
  • child_process: 创建和控制子进程
  • cluster: 对child_process模块的封装,充分利用多核CPU

process

process是一个全局对象,提供了当前NodeJS进程的信息并可以对其进行控制,以下为process所提供的一系列属性:

  • process.argv:返回一个数组,成员是当前进程的所有命令行参数。
  • process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME。
  • process.installPrefix:返回一个字符串,表示 Node 安装路径的前缀,比如/usr/local。相应地,Node 的执行文件目录为/usr/local/bin/node。
  • process.pid:返回一个数字,表示当前进程的进程号。
  • process.platform:返回一个字符串,表示当前的操作系统,比如Linux。
  • process.title:返回一个字符串,默认值为node,可以自定义该值。
  • process.version:返回一个字符串,表示当前使用的 Node 版本,比如v7.10.0。

当然,还有各种事件以及成员方法,方法简要说下吧,事件自己去看。

  • process.chdir():切换工作目录到指定目录。
  • process.cwd():返回运行当前脚本的工作目录的路径。
  • process.exit():退出当前进程。
  • process.getgid():返回当前进程的组ID(数值)。
  • process.getuid():返回当前进程的用户ID(数值)。
  • process.nextTick():指定回调函数在当前执行栈的尾部、下一次Event Loop之前执行。
  • process.on():监听事件。
  • process.setgid():指定当前进程的组,可以使用数字ID,也可以使用字符串ID。
  • process.setuid():指定当前进程的用户,可以使用数字ID,也可以使用字符串ID。

child_process

child_process模块用来创建和控制子进程,其核心方法为child_process.spawn(),常用的四个方法如下:

  • exec:用于执行bash命令,它的参数是一个命令字符串。
  • execFile:直接执行特定的程序,参数作为数组传入,不会被bash解释
  • fork:创建一个子进程,执行Node脚本
  • spawn:创建一个子进程来执行特定命令,没有回调函数

每个都有自己的用途,一个简单的实例如下:

1
2
3
4
5
6
7
8
9
var exec = require('child_process').exec;

var ls = exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log(error.stack);
console.log('Error code: ' + error.code);
}
console.log('Child Process STDOUT: ' + stdout);
});

具体介绍和使用官方写的很详细了,不过有一段话这里可以介绍下

child_process.execFile(): 类似于 child_process.exec(),但是默认情况下它会直接衍生命令而不先衍生 shell。

这是一段对execFile的介绍,看到这里后对这段话很不理解,于是去查看了源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function normalizeExecArgs(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}

// Make a shallow copy so we don't clobber the user's options object.
options = { ...options };
options.shell = typeof options.shell === 'string' ? options.shell : true;

return {
file: command,
options: options,
callback: callback
};
}
function exec(command, options, callback) {
const opts = normalizeExecArgs(command, options, callback);
return module.exports.execFile(opts.file,
opts.options,
opts.callback);
}

上面这段是exec()方法的实现,可以看到是调用了execFile();

execFile

官网中对shell参数有如上图描述,在上述代码中我们可以看到exec()传进来的默认为true,所以会启动shell,也就是可以执行一些shell命令和脚本,而execFile()默认为false,则不会启动shell,可以执行一些可运行的程序,效率会高过exec()。

cluster

cluster意为集群,表示多个Node进程构成的服务。cluster封装了child_process.fork方法创建node子进程.。利用cluster模块,我们可以创建多进程的Web服务器,充分利用多核处理器的计算资源。下面给出一个具体示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是一个 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);

console.log(`工作进程 ${process.pid} 已启动`);
}

我们将master称为主进程,worker进程称为工作进程,利用cluster模块,使用Node的cluster模块封装好的API、IPC通道和调度机制可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构。

我们可以看到多个子进程共同监听了同一端口,这是违背规则的,那么Node是如何实现的呢?
这是因为,子进程并没有创建ServerSocket作监听,而是交由父进程创建ServerSocket监听指定端口,接收到连接后创建Socket,而得到的请求会根据“指定的分发规则”通过IPC发送给子进程,子进程处理后的结果再通过IPC由父进程转发给请求方。

最后,cluster的请求分发策略有两种:

  • Round-Robin法:即轮询,依次循环将请求分配给子线程。
  • 共享服务端socket方式:由操作系统进行调度。

cluster的底层对master和child有着不同的实现:

cluster

上图为cluster实现的源码,具体原理还需要对其进行源码解读。

异步编程

先来说异步是什么,我的理解是:

异步就是有事件发生找个人去处理,自己继续忙自己的任务,等那个人处理完了通知到你,你再验收。

我们都知道,Node是单线程的,可是如果是单线程,又如何完成异步机制和事件机制呢?

实际上,NodeJS的单线程是指的只有一个主线程,其他线程包括而不限于如下几种:

  • js引擎执行线程
  • 定时器线程
  • 异步IO线程

这些线程可以称之为工作线程,这种机制避免了node因为线程切换造成的损耗,使得Node非常适合IO密集型应用。而本章的异步编程主要是创建工作线程与主线程并发执行,但需要等主线程空闲时才能执行回调函数。

异步编程

回调

在代码中,异步编程的直接体现就是回调。异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了,比如如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function heavyCompute(n, callback) {
var count = 0,
var i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
callback(count);
}
heavyCompute(10000, function (count) {
console.log(count);
});
console.log('hello');

-- Console ------------------------------
100000000
hello

显然,并没有异步执行,那如果想写一个异步的程序需要怎么写呢,学到的一个很简单的实例:

1
2
3
4
5
6
7
8
setTimeout(function () {
console.log('world');
}, 1000);
console.log('hello');

-- Console ------------------------------
hello
world

通过调用setTimeout()这个原生的异步函数来实现异步。

你说这耍赖,不是你自己实现的,那再来个异步遍历数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function (i, len, count, callback) {
for (; i < len; ++i) {
(function (i) {
async(arr[i], function (value) {
arr[i] = value;
if (++count === len) {
callback();
}
});
}(i));
}
}(0, arr.length, 0, function () {
// All array items have processed.
}));

看到这里就结束了,有人可能好奇,不是七天么,这不是才六章吗?

第七天是个综合示例,自己照着敲学习吧🤪。

引用文献

七天学会NodeJS

官方文档(中文版)

node.js模块简明详解

JavaScript标准参考教程

认识node核心模块–从Buffer、Stream到fs

Node.js中的流(Streams):你需要知道的一切

浅析nodejs的http模块

Node的进程详解

缓存相关问题

发表于 2019-12-26 | 更新于: 2019-12-26 | 分类于 other
字数统计: 294 字 | 阅读时长 ≈ 1 分钟

缓存穿透

原因

查到DB和缓存都没有的数据

带来的问题

大量DB和缓存都不存在的数据请求打到数据库,会加大数据库压力,严重发生宕机。

解决

缓存空值

如果数据库不存在则在缓存中加入key与null的关系,后续由缓存将null返回给用户

BloomFilter

Bloom Filter是一个很长的二进制向量和一系列随机映射函数,它可以判断数据是否存在。当前场景我们可以用它来判断查询的数据是否在集合中。

bloomfilter

如上图,在缓存上增加一层BloomFilter,进行筛选,不存在直接返回null

缓存击穿

原因

大量的请求到缓存没有的数据(DB有)

带来的问题

加大数据库压力,严重发生宕机

解决

加上锁,第一个取完后放入缓存,后面的去缓存取数据

缓存雪崩

原因

缓存服务崩溃

带来的问题

加大数据库压力,严重发生宕机

解决

  1. 增加高可用(事后)
  2. 本地缓存
  3. 限流
  4. 服务降级(返回默认值)

JAVA基础知识(持续更新)

发表于 2019-12-25 | 更新于: 2019-12-25 | 分类于 Java
字数统计: 742 字 | 阅读时长 ≈ 2 分钟

语法基础

编程思想

类与对象

###Object

概念

超父类,所有类的源头,如果不写继承则默认继承Object

方法

registNatives(): native修饰,

getClass():

hashCode():

equals():

clone():

toString():

notify():

notifyAll():

wait(…):

finalize():

this

概念

当前类的指针(是个对象吧?)

原理

?

super

概念

表示父类的引用

原理

?

抽象

概念

类为抽象的概念

分类

抽象类:由abstract修饰的类,不能被实例化

抽象方法:用abstract修饰的方法,只能由子类进行实现

native

概念

A native method is a Java method whose implementation is provided by non-java code.

一般为c/c++实现,使用JNI(java native interface)进行通信

使用规则

  1. native标识符除不能与abstract联用外,可以与其它标识符联用;
  2. native method方法可以返回任何java类型,包括非基本类型,也可以进行异常控制;
  3. 如果含有native method方法的类被继承,子类会继承这个native method方法,也可以使用java语言重写 这个方法;
  4. 如果一个native method方法被fianl标识,它被继承后不能被重写。

实现步骤

  1. 在Java中声明native()方法,然后编译;

  2. 使用javah命令生成.h文件;

  3. 编写.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中需包含JDK带的jni.h文件);

  4. 将本地代码编译成动态库(Windows:.dll,linux/unix:.so,mac os x:*.jnilib);

  5. Java中使用System.loadLibrary()方法加载第四步产生的动态链接库文件,至此,整个过程结束。

封装

概念

隐藏细节,开放给用户安全的接口

权限访问控制

权限关键字:public、protected、default(不填一样)、private

权限依次降低

作用域 当前类 同一package普通类 其他package普通类 同一package子孙类 其他package子孙类
public √ √ √ √ √
protected √ √ × √ √
默认 √ √ × √ ×
private √ × × × ×

原理

?

继承

概念

子类继承父类的属性和方法、减少了重复代码、增加了类结构关系

注意点

  1. 继承的抽象方法必须实现
  2. 只能继承父类非private权限的,具体看上面的权限表格

原理

?

多态

概念

子类对父类方法的不同实现

条件

  1. 父类抽象方法
  2. 多个子类不同实现
  3. 使用父类类型接收子类对象,并进行调用

原理

?

函数指针指向不同的方法实现

接口

概念

规范使用,规定一套标准的用法

详解

  1. 接口所有方法为抽象类
  2. 接口中的属性为public final类型的(不写也是)
  3. 接口是制定适用规范,如对虚拟文件系统对外的增删改查标准接口

Java容器

  • Collection
    • List
      • LinkedList
      • ArrayList
      • Vector
    • Stack
    • Set
      • HashSet
    • LinkedHashSet
      • TreeSet
    • Queue
      • PriorityQueue
  • Map
    • TreeMap
    • HashMap
      • LinkedHashMap

如下为java容器分类图(粗体为重点容器)

java容器分类图

异常

泛型

I/O

  • 文件操作

  • 标准输入输出

  • linux IO模式

    linux IO模式

注解与反射

图形化界面

Git内部原理介绍

发表于 2019-12-25 | 更新于: 2019-12-26 | 分类于 git
字数统计: 2.3k 字 | 阅读时长 ≈ 9 分钟

前言

本文主要是介绍Git内部原理,包含底层命令与文件系统两部分,旨在让我们从了解Git底层原理的运作,从而更好的使用。本文适用于有一定Git基础,并且对Git实现原理比较好奇的同学。

Git 文件系统简介

首先,先说个概念:

Git文件系统是内容寻址文件系统。

内容寻址文件系统是什么?

简单来说就是一个键值对数据库,你可以向数据库中插入任意类型的内容,它会返回一个键值,你可以通过这个键值再次检索插入的内容。

有没有点感觉了,没有也没关系,我们先来分析研究Git文件系统的内容,找到我们想要的答案。

如何得到Git文件系统?

首先,新建个文件夹,如mkdir mygit,进入文件夹后使用git init命令,初始化Git后得到.git文件夹,这个就是我们的文件系统实体,具体操作如下图:

git文件系统

可以看到.git文件夹下已经有很多内容,相关介绍如下图所示:

git_file

可以看到每个文件或者目录对应的介绍,这里面我们重点关注的主要有HEAD、objects以及refs等三个内容,在介绍他们前,我们先了解底层命令,用它来打开我们走进Git内部的大门。

Git底层命令

通过git help可以查看高级命令及相关介绍,而如果我们想看底层命令,可以通过命令git help -g,下面是一些常用底层命令:

git底层命令

了解底层命令可以让我们更直观的理解git的工作原理,比如我们平时操作的git add & git commit其实可以用底层命令实现,如下:

git commit

上图为一个新建的git仓库,通过使用内部命令hash-object、update-index、write-tree、commit-tree等实现了我们的添加和提交的功能,这几部的作用依次是:

  1. hash-object 得到test1文件对象的键值
  2. updata-index将test1添加到暂存区(实现了add操作)
  3. write-tree命令将暂存区内容写入到一个树对象中(这里提到了对象,下面来介绍对象)
  4. 最后通过commit-tree命令将树对象提交得到一个提交对象(实现了commit操作)

用底层命令需要四部才能完成我们的工作(其实还有第五步,后面讲文件系统在介绍),而我们平时用的高级命令add、commit只需要两步,这么一比确实高级了很多,但通过这四步,我们可以理解下之前我说的Git就是个键值对数据库这个概念是不是理解了很多,Git所有的文件都可以由生成的键值取出,如下图:

1
2
3
4
➜ /home/gewuang/profile/mygit git:(master) ✗ > git cat-file -p \
ce013625030ba8dba906f756967f9e9ca394464a
hello
➜ /home/gewuang/profile/mygit git:(master) ✗ >

这个键值ce013625030ba8dba906f756967f9e9ca394464a就是我们上面由hash-object生成的。

Git对象

对象是Git文件系统的基础,可分为树对象、数据对象、提交对象、标签对象等四种,如下图所示:

git对象

数据对象就是linux系统中的inode节点(详情看上篇inode介绍),只存储文件中的内容,数据对象的名字就是它的”inode节点”,也就是索引节点,而文件名等信息则存放在树对象中,对比linux系统就是目录文件,提交对象当然就是我们的commit_id了,这也是我们平时接触最多的,下图为对象中的内容:

git object

上图主要使用底层命令cat-file获取对象信息,首先用-p选项打印对象中文件的内容,然后用-t打印对象类型。可以看到首先打印的是blob类型,也就是数据对象的内容,然后是树对象内容,最后是提交对象的内容,这里可以看到tree就是我们当时提交时使用的树对象的键值。下面为具体格式介绍:

git object2

这里为什么没有tag对象,因为tag对象内容就是我们使用git tag *时标签的commit id,如下图:

git tag

Git文件系统原理

介绍完了git底层命令和git对象,我们可以回头再来看Git文件系统原理了。

首先填前面的第一个坑,.git目录下我们关注的三个内容:HEAD、objects/以及refs/。

HEAD

HEAD存放的是我们当前的分支信息

1
2
3
➜ /home/gewuang/profile/mygit git:(master) > ls
➜ /home/gewuang/profile/mygit git:(master) > cat .git/HEAD
ref: refs/heads/master

当我们新建一个分支后,切换当前分支可以发现HEAD内容已经被改变

1
2
3
4
5
➜ /home/gewuang/profile/mygit git:(master) > git branch branch1
➜ /home/gewuang/profile/mygit git:(master) > git co branch1
Switched to branch 'branch1'
➜ /home/gewuang/profile/mygit git:(branch1) > cat .git/HEAD
ref: refs/heads/branch1

当然我们可以直接修改HEAD文件来更改分支,不过这样是不推荐的,你可以骚一点,用底层命令symbolic-ref来进行修改,如下:

1
2
3
4
5
➜ /home/gewuang/profile/mygit git:(branch1) > cat .git/HEAD
ref: refs/heads/branch1
➜ /home/gewuang/profile/mygit git:(branch1) > git symbolic-ref HEAD refs/heads/master
➜ /home/gewuang/profile/mygit git:(master) > cat .git/HEAD
ref: refs/heads/master

objects/

objects/目录下存放着我们的数据对象、树对象以及提交对象等等

1
2
3
4
5
➜ /home/gewuang/profile/mygit git:(master) > find .git/objects -type f
.git/objects/2e/4f59e4b1040fca028e89fe687909373463607a
.git/objects/74/9b9d6b710c728b0296098c2f4c3d883566af08
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
➜ /home/gewuang/profile/mygit git:(master) >

可以看到,我们之前操作的对象,都在这里存着,存放的格式为前两位作为目录名,后续内容作为文件名,这个文件是由zlib压缩过的,我们一般通过cat-file命令进行查看。

refs/

refs/目录放置的是引用文件,先收下我对引用的理解:存储指向数据(提交)对象的指针。

对,我理解的引用文件就是指针,里面存放的内容就是键值,可以直接找到对应的对象文件,拿到想要的内容,引用的分类和介绍如下图:

refs

HEAD我认为也是一种引用,不过它比较特殊,是符号引用,因为它存放的内容为就是文件路径,如master的refs/heads/master,像极了软链接的样子。

这里的refs/head/master的内容就是我们当前分支的键值了。

1
2
3
4
5
➜ /home/gewuang/profile/mygit git:(master) > cat .git/refs/heads/master
749b9d6b710c728b0296098c2f4c3d883566af08
➜ /home/gewuang/profile/mygit git:(master) > git gl
749b9d6 [2019-12-25 22:16:45 +0800] <gewuang> (HEAD -> master, branch1) first commit
➜ /home/gewuang/profile/mygit git:(master) >

remote/主要是远程引用相关的,这里不多介绍。

包文件

git存放数据对象的时候会把整个文件都存到数据对象中,即使有zlib压缩,你可能也会疑惑,我项目这么大,这么多提交,.git目录不是早就爆掉了么,你能想到的设计的人肯定也想到的,所以就有了包文件,他存在的意义就是将对象进行打包,节省空间。

packet

上图介绍的很清楚了(请自动把引用两个字替换成packet,笔误),不过还有一点注意的是相同的文件修改后的数据对象打包后,前一个对象保存的是差异,而文件的全部内容存放在包中此文件最新的数据对象中,因为git认为最新的是人们访问几率最大的,这个设计是为了节省计算的时间,提高效率。

Git组织结构

讲了这么多,那么git组织结构是什么样的呢?看下图:

git_fs

文章很长了,不详细介绍了,看不懂就去翻上面,动动脑子去理解才能吸收。

看到这里的都是好孩子,给你个奖励,我自己的一套git别名,用起来会方便很多,如下:

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
[alias]
glf = log -n 10 --name-only --format=\"%Cgreen%h %Cred[%ci] %Creset<%an> %Creset %Cgreen%s %Creset
gl = log -n 30 --date-order --format=\"%Cgreen%h %Cred[%ci] %Creset <%an>%C(yellow)%d%Creset %Creseset \"
gll = log -n 30 --format=\"%Cgreen%H %Cred[%ci] %Creset<%an> %Creset %Cgreen%s %Creset \"
gl3 = log -n 20 --format=\"%Cgreen%h %Cred[%ci] %Creset<%an> %Creset %Cgreen%s %Creset \" --graph
gl2 = log --format=\"%Cgreen%h %Cred[%ci] %Creset<%an> %Creset %Cgreen%s %Creset \"
glc = log --format=\"%Cgreen%h %Cred[%cd] %Creset<committer:%cn> : %Cred[%ad] %Creset<author:%cn> %C
glc2 = log --format=\"%Cgreen%h %Cred[%ci] %Creset<committer:%cn> : %Cred[%ai] %Creset<author:%cn> %
glc3 = log --format=\"%Cgreen%h %Cred[%ci] %Creset<committer:%cn> : %Cred[%ai] %Creset<author:%cn> %
glw = log -n 20 --format=\"%Cgreen%h %Cred[%ci] %Creset<%an> %Creset %n%Cgreen%s%Creset%n%b \"
#| grep \"+0800]\"
gldetail = log --format=\"%h `[%cd] `<committer:%cn> `[%ad] `<author:%an> ` %s \"
hist = log --pretty=format:\"%C(yellow)%h %C(red)%d %C(reset)%s %C(green)[%an] %C(blue)%ad\" --topo-order --graph latest = for-each-ref --sort=-committerdate --format=\"%(committername)@%(refname:short) [%(committerdate:short)] %(contents)\"
#gldetail = log --format=\"%h [%cd] <committer:%cn> :[%ad] <author:%an> %s \"

st = status -uno -s
st2 = status -s
co = checkout
bl = blame --date=short
#gll = log --format=\"%Cgreen%h %Cred[%ad] %Creset<%an> %Creset %Cgreen%s %Creset \"
ci = commit
dt = difftool
dif = diff --word-diff
#di = diff --color-words
di = diff --no-ext-diff

linux文件系统-inode学习整理

发表于 2019-12-21 | 更新于: 2019-12-26 | 分类于 linux
字数统计: 1.6k 字 | 阅读时长 ≈ 5 分钟

linux文件系统-inode学习整理

介绍

linux文件系统可讲的模块有很多,包括文件系统整体架构、文件系统分类、虚拟文件系统以及文件系统存储结构等等,本文主要介绍的是文件系统的存储结构,也就是本文的重点-inode。

文件存储结构

首先从开天辟地开始介绍,我们知道数据是保存在磁盘中的,磁盘具体存贮原理细节不在这里进行说明,而磁盘中的存储空间是如何进行管理的?这里就说到了磁盘块的划分:

  1. 超级快:文件系统中第一个块,存放的是文件系统本身的结构信息,包括每个区域的大小以及未被使用的磁盘块等等信息
  2. inode节点表:超级块的下部分就是inode节点表了,也就是我们上面的inode table。每个inode节点对应一个文件(或目录)的结构,包括了文件的创建时间、权限等信息,下面有详细的介绍。
  3. 数据区:显然它就是用来保存文件内容的区域,这里要介绍下,磁盘上的块大小一样,一般来说为4kb,即连续的八个扇区(512字节),块手是文件存取的最小单位,超过块大小的文件会放到下一个块中。

就像大家知道的,linux一切皆是文件,所以目录项也是文件,不过这个文件中存储的是目录下的文件及子目录组织结构,相应的文件指向了inode的节点,这里需要说明每个文件对应一个inode节点,之后通过inode节点中有关数据区块的信息找到对应的数据。

文件存储结构的整体架构,如下图所示:

文件存储架构

inode节点

inode节点详解

inode节点就是文件元数据的存储区,包括了文件如下内容

1
2
3
4
5
6
7
8
- 文件的字节数
- 文件拥有者的User ID
- 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,
mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
- 链接数,即有多少文件名指向这个inode
- 文件数据block的位置

可以使用stat filename 命令查看:

inode节点信息

基本除了文件内容外的信息都存储在inode节点中。

inode节点的大小一般来说为128或者256个字节,inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

如果想要查看inode大小,可以使用dump2fs -h /dev/sda1 | grep "Inode size"查看:

inode大小

如果要查看每个磁盘的inode使用情况,可以使用df -i命令查看:

inode使用情况

每个文件都有自己的对应的inode号,这里要说明的是unix/linux系统中主要根据inode号来识别文件,文件名只是我们用来整理和分辨文件的别称,而文件名主要存储在目录项中。

目录项

目录项的结构

目录项是linux文件系统的重要组成部分,在linux中目录项也是一种文件,不过他内部存储的信息由两部分组成

  • 文件名
  • inode编号

我们可以通过ls -ai dirname 查看目录结构:

当我们创建目录时,一定会有的两个内容就是.和..,.表示的是当前目录文件所对应的inode号,..对应的是当前目录父目录的inode号,其他的就是我们目录下的文件和对应的inode号。

介绍完上面这些信息我们再来看一开始的流程就很清楚了:

首先从目录文件中拿到我们所需文件对应的inode号,通过inode号拿到文件的元数据,通过其中所指向的数据块号取出文件内容。

创建流程

通过创建流程串通知识点

文件创建流程

通过前面的内容我们了解到了文件取出的流程,那创建一个文件的流程是什么样的呢?下面我们来介绍下创建文件的流程。

  1. 存储inode节点信息:内核首先找到一块空的inode节点,将文件的信息存在节点中。
  2. 存储数据信息:数据信息即文件信息,内核从未使用的块列表中找到几个数据块(一般是不连续的),如300、230、540等,内核将缓存区中的数据存储到对应的数据块中。
  3. 记录分配情况:存储完信息后,数据块的分配情况记录在inode节点信息中
  4. 添加文件名到目录:最后内核将文件名和对应的inode节点放到目录文件中。

inode应用扩展

硬连接

一般情况下,linux中的文件名和inode号码是一一对应的,不过也可以多个文件名指向同一个inode节点,也就是我们要介绍的硬链接。

创建硬链接的命令为ln 源文件目标文件,硬链接与正常的文件相同,只是与其他文件共享同一个inode节点,前面介绍的inode节点信息中Links数就是文件名指向的数量,当对其进行删除的时候只会对inode节点中的links数减少1,当为0的时候文件才会真正被删除。

硬链接

这里说明下,目录项中的.和..也是一种硬链接。

软链接

介绍完硬链接,再介绍一种我们平常使用比较多的一种方式:软链接。

ln -s 源文件 目标文件是软链接的创建方式,虽然看起来只是多了个选项s,当时内部原理完全不同。

软链接是单独生成一个链接文件,有自己的inode号,是一个单独的文件,这个文件中的信息是链接的文件的信息。

软链接文件

如上图,可以把软链接看做是一个指针,只不过指针里面的内容为所指向文件的路径,这个指针有自己单独的内存空间。

参考文章

理解inode

Linux文件系统详解

libuv介绍

发表于 2019-08-25 | 更新于: 2020-01-20 | 分类于 Node.js
字数统计: 64 字 | 阅读时长 ≈ 1 分钟

是什么

先来看官网对他的介绍:

libuv is a multi-platform support library with a focus on asynchronous I/O.

直译过来就是:libuv是一个关注于异步IO的多平台库

为什么用它

底层原理

参考链接

libuv design

wiki/libuv

最长回文子序列

发表于 2019-08-25 | 更新于: 2019-08-25 | 分类于 leetcode
字数统计: 595 字 | 阅读时长 ≈ 3 分钟

题目

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:

1
2
3
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例2:

1
2
输入: "cbbd"
输出: "bb"

anwser

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
class Solution {
public:
bool isPalindromic(string sub) {
bool ret = true;
stack<char> c;
int len = sub.length();
int i = 0;
char a;

for (; i<len; i++) {
c.push(sub[i]);
}
for(i=0; i<len; i++) {
a = c.top();
c.pop();
if(sub[i] != a) {
ret = false;
break;
}
}

return ret;
}
string longestPalindrome(string s) {
string ret = s.substr(0,1);
int len = s.length();
int i = 0;
int j = 0;
int max = 0;

for(; i<len-1; i++) {
for (j=len-1; j>0; j--) {
if(isPalindromic(s.substr(i, j-i+1))) {
if(max < (j-i)) {
ret = s.substr(i, j-i+1);
max = j-i;
}
break;
}
if(max!=0 && max>(j-i)) {
break;
}
}
if(max!=0 && max>(len-1-i)) {
break;
}
}
return ret;
}
};

谨记大神的说法,想不出高级解法就先用暴力法,上面就是暴力法,不过超时了?题目没有说时间啊、、、不懂了,但是毕竟也是自己写的,所以还是贴出来吧,优秀解法如下:
动态规划法(Time:O(n^2))

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
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
int start = 0, maxLen = 0;

for(int i=0; i<n;i++){
dp[i][i] = true;
if(maxLen < 1) { start = i; maxLen = 1;}
if(i<n-1 && s.charAt(i) == s.charAt(i+1)) {
dp[i][i+1] = true;
start = i;
maxLen = 2;
}
}

for(int numChars=3; numChars<=n;numChars++){
for(int j=0; j<n-numChars+1; j++){
int pos = j+numChars-1;
if(dp[j+1][pos-1] && s.charAt(j) == s.charAt(pos)) {
dp[j][pos] = true;
start = j;
maxLen = numChars;
}
}
}
return s.substring(start, start+maxLen);
}
}

Best solution:

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
class Solution {

private static class LongestPalindromeNode {

String longestPalindrome;
int length;

LongestPalindromeNode() {
this.longestPalindrome = "";
this.length = 0;
}
}

private static LongestPalindromeNode helper(String word, int left, int right, LongestPalindromeNode longestPalindromeNode) {

int curr_max_length = 0;

while(left >= 0 && right < word.length() && word.charAt(left) == word.charAt(right)) {
left--;
right++;
}
curr_max_length = right - left -1;
if(longestPalindromeNode.length < curr_max_length) {
longestPalindromeNode.length = curr_max_length;
longestPalindromeNode.longestPalindrome = word.substring(left+1, right);
}

return longestPalindromeNode;
}

public String longestPalindrome(String word) {

int length = word.length();
if(length == 0 || length == 1)
return word;

LongestPalindromeNode longestPalindromeNode = new LongestPalindromeNode();

for(int idx = 0; idx < word.length(); idx++) {
int remaining_length = length - idx;
int max_palindrome_length_can_be_formed = (remaining_length) + (remaining_length-1);
if(max_palindrome_length_can_be_formed < longestPalindromeNode.length)
break;

longestPalindromeNode = helper(word, idx, idx, longestPalindromeNode);
longestPalindromeNode = helper(word, idx, idx+1, longestPalindromeNode);

}

return longestPalindromeNode.longestPalindrome;

}
}
12

Ramon

当你觉得晚的时候,恰恰是最早的时候

20 日志
8 分类
8 标签
© 2020 Ramon
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4
访问人数 总访问量 次