T(N) = O(f(N))
表示T(N)的增长率小于等于f(N)T(N) = Ω(g(N))
表示T(N)的增长率大于等于f(N)T(N) = θ(h(N))
表示T(N)的增长率等于h(N)T(N) = o(p(N))
表示T(N)的增长率等于p(N)
当且仅当T(N) = O(f(N))
且T(N) = Ω(f(N))
时,T(N) = θ(h(N))
。
T(N) = O(f(N))
表示T(N)的增长率小于等于f(N)T(N) = Ω(g(N))
表示T(N)的增长率大于等于f(N)T(N) = θ(h(N))
表示T(N)的增长率等于h(N)T(N) = o(p(N))
表示T(N)的增长率等于p(N)当且仅当T(N) = O(f(N))
且T(N) = Ω(f(N))
时,T(N) = θ(h(N))
。
用了node.js这么长时间,对node.js擅长和不擅长的领域算是比较清晰了。
进程是计算机中已运行程序的实体,一个进程可以包含多个线程,多个线程共同某个进程的工作。一个CPU同一时间只能处理某个进程的任务。所有的线程共享着计算机的内存,没有做任何的隔离。但是,为了保证某些数据的安全,会在内存中使用互斥锁(Mutual exclusion,缩写 Mutex)
及信号量(Semaphore)
。互斥锁允许且只允许同一时间只有一个线程操作共享内存,其它线程必须等待该线程处理完成后再对该内存块进行处理;信号量的逻辑与互斥锁相似,只是同一时间允许N个线程对共享内存进行操作,当名额满了之后其他线程必须等待。
这篇文章很好的解释了进程与线程的关系,以及他们对CPU和内存的使用方式。
写程序,操作文件、读写数据库和请求第三方服务等都是常有的事,这些都是I/O,I/O是非常耗时间的。
当程序采用阻塞I/O的策略时,线程遇到I/O操作就会等待,直到I/O操作结束,再继续执行,例如PHP语言,就是采用这种机制。那么PHP采用这种机制,如果一个请求由于I/O操作导致线程被阻塞,是否会影响其它请求的响应时间呢,答案是不会的,看多线程与单线程
里面的解释。
当程序采用非阻塞I/O的策略时,线程遇到I/O操作时,会通知另外的线程去处理,然后主线程挂起一个回调函数,并继续执行其他的任务,不会做任何的等待,直到这个I/O操作完成,另外的线程将结果返回给主线程,主线程调用之前挂起的回到函数,继续执行接下来的操作。
node.js就是采用非阻塞I/O和事件轮询的方式。我们知道Javascript单进程单线程的语言,但是所谓的单线程只是说他执行任务时用的是一个线程,我们称之为主线程,既然有主线程,那就说明肯定还有其他线程,是的,就是任务队列,专门负责处理I/O操作,当node.js的主线程发现了一个异步行为,他会将该任务挂起,将他提交给任务队列去处理,直到任务队列处理完该任务并返回处理结果时,主线程再调用处理该结果的回调函数继续执行。通过这样的方式,主线程不需要等待任何I/O操作,使主线程的资源利用率成倍提升。
上面的内容中提到,PHP采用了阻塞I/O的方案,该方案会等待I/O操作结束再继续执行接下来的代码。那么这样的方式,如果同时多个请求进来了,由于I/O阻塞的原因会不会出现某个请求执行了很久,导致后面的请求响应慢的问题,答案是不会的。因为PHP采用了多线程的处理方案,当有一个请求进来的时候,PHP就会开启一个线程来处理一个请求,一个请求对应一个线程,那么一个请求所出现的问题是不会影响到另一个请求的(CPU和内存富足的情况下)。PHP的这种方案使得各线程之间相对安全,不会互相影响,但是弊端也是明显的,当I/O出现时,PHP始终处于等待状态,这导致CPU的利用率低,并且,一个请求进来就创建一个线程,这对内存的要求较高,因此,在高并发的情况下,需要横向扩展PHP服务器以满足高并发的需求,很多时候都是因为线程过多导致内存不足的问题。
node.js的非阻塞I/O和事件轮询机制很好的优化了这个问题,通过非阻塞I/O提高了CPU的利用率,并且单个线程对内存的需求肯定比PHP少得多,因此node.js更能扛得住高并发。但任何事情都是双面的,有优点就必然有确定,单线程的javascript,如果某个请求出现了异常,就会导致整个进程不用了,当然,我们有解决方案可以让程序在出现异常的情况下自动重启。还有就是如果你在node.js的主线程执行了高CPU运算的操作,那将是灾难。因为在主线程中,除了I/O是异步的,其他运算都是同步的,一个高CPU运算的操作会导致后续的请求全部被卡住。
因此,node.js虽然被很多人追捧,但是,如果用的不好,还不如写PHP。node.js更适合做任务的分发者而不是执行者!!!
如果我有一个团队,我更愿意这样去搭建一个后端的服务,node.js不太适合出现在数据层,除非真的只是增删查改而没有其他高耗CPU的操作,node.js更适合作为前端的路由去分发请求,他自身不应该出现过多的CPU操作,如果有,请用fork。
1.SSH是我们常用的连接服务器的工具,频繁的输入账号密码会很麻烦,通过RSA的公私钥机制,可以实现免密登录。方法是将客户端的公钥放到服务器的~/.ssh目录下,并重命名为authorized_keys
;
2.SSH工具采用的是TCP连接,在网络断开的情况下SSH就会自动断开,于是终端会变成假死状态,必须关了重开,很麻烦。mosh使用UDP连接,不用担心断网重连的问题。mosh的使用,必须拥有服务端,即除了客户端需要安装mosh外,服务端也需要装mosh,之后通过命令mosh root@<your host>
即可完成连接。
抽象数据类型(abstract data type, ADT)是一些操作的集合。对诸如表、集合、图和它们的操作一起可以看作是抽象数据类型。
使用数组实现表结构时,在元素的增删比较麻烦,例如在0位增加一个元素,需要将所有元素后移一位,删除一个元素,需要讲所有元素前移一位,时间复杂度位O(N)。
链表实现表结构时,增删操作的时间复杂度位O(1)。假设链表有5个节点,当需要在2和3之间增加一个节点时,只要创建一个新的节点空间,让2的next指针指向这个新创建的节点,再让新创建节点的指针指向3这个节点,则新增操作完成。删除某个节点时,讲节点的前一个节点的next指针指向当前节点的下个节点,并删除当前节点即可。
双向链表与链表的差别在于,双向链表出了有next指针指向下一个元素,还有prev指针指向前一个元素,形成了双向链。
循环链表的特别在于,最后一个元素的next指向第一个元素,而第一个元素的prev指向了最后一个元素。在双向链表中,这两个指针指向的都是null。
1 | 'use strict'; |
C语言中,正数的原码、反码和补码都是一样的,但是负数的的表示是其补码,补码是反码+1。
int类型是4字节,表示-2^31 ~ 2^31-1
我们设定一个变量60,看它各运算后的结果
60的二进制表示为00000000 00000000 00000000 00111100
~60,我们对60取反,得到这样的二进制11111111 11111111 11111111 11000011
首位的比特位是1,表示这是一个负数,C语言中的负数是补码表示,我们需要讲补码转为原码才知道该二进制在C语言中表示什么编码
补码是反码+1,因此反码为11111111 11111111 11111111 11000010
原码为反码除符号位外其他位求反,因此,原码为10000000 00000000 00000000 00111101,即-61
因此~60 == -61
60 << 2,我们对60左移两位,得到这样的二进制00000000 00000000 00000000 11110000
这是一个符号位为0的二进制,表示的结果为正数,240
在使用异步回调的时候,我们经常会接住async等模块的帮助,但是有时候我们仅仅只需要一个异步调用同步执行的方法,似乎没必要使用那么多的代码,那么,异步调用顺序执行的方法如果自己写要怎么写呢?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(function() {
var fns = [].slice.call(arguments);
fns.map(function(fn, i, fns){
fns[i] = function () {
var args = [].slice.call(arguments);
fn.apply(null, i + 1 === fns.length ? args : [fns[i + 1]].concat(args));
}
});
fns[0].call(null);
}(function(next) {
setTimeout(function() {
console.log('method 1');
next('method 1', 'complete!');
}, 200);
}, function(next, a, b) {
setTimeout(function() {
console.log(a, b);
console.log('method 2');
next('method 2', 'complete!');
}, 100);
}, function(a, b) {
console.log(a, b);
console.log('end');
}));
// method 1
// method 1 complete!
// method 2
// method 2 complete!
// end
一开始我一直处理不好next和data混合在一起的处理方法,直到我看到了一段类似的代码才醒悟。当前函数执行完之后调用next函数时,只有实际数据部分,但是下一个函数的定义却有一个next的第一参数,因此,我们需要讲原有的函数做替换,让他可以正常的接受当前函数传递过来的参数,并且执行的时候加上next参数。
最近有空的时候一直在整理自己的后端代码,希望整理出一套较为通用的代码,可以运用到大多数的项目中去。现在的项目基本都是前后端分离的,后端只负责提供restful api接口,根据不同的项目,和前端会有不同的集成方式,例如app的后端,基本就是一个独立的项目,我个人的博客后端,我会考虑使用nginx做一个转发,当接收到/
到请求时,转发给前端页面处理;当接受到/service
请求时,转发给后端处理,这些请求基本上都是前端的ajax请求,避免了跨域的麻烦。后端的代码中我们只要用一个简单的use方法做转发可以将/service
的请求转回原地址了。
我的项目目录:1
controller // 存储控制器的文件夹
library // 存储通用代码的文件夹
- error.js // 通用错误定义
- helper.js // 系统级通用函数定义,例如jwt令牌生产和校验
- middleware.js // 中间件定义
- util.js // 常规通用函数定义,一般以lodash或系统内置的util模块为基础,md5等自己常用的方法
model // 存储mysql、mongo及redis数据模型的文件夹
- mysql // mysql
- mongo // mongodb
- redis // redis
- index.js
node_modules // node.js的模块
router // 系统路由
- v1.js // 版本1的路由
- v1_1.js // 版本1.1的路由
- index.js // 通过use方法调用各版本路由
service // controller和model之间的业务逻辑代码
test // 单元测试文件夹
.gitignore
app.js // 启动文件
config.js // 配置文件
package.json // 模块配置文件
readme.md
所有的请求都会经过路由再到达控制器,路由是所有代码内容的第一关,特别是在app项目里面,url一旦发布就难以修改(客户端也可以通过默写热修复的方法处理),url地址的设计好坏对于项目维护很重要。我在定义路由的时候,会有一个总的index.js文件,app.js只要引入这个文件即可,项目就可以访问到所有版本的api。
index.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23'use strict';
/* jshint node: true */
const Mdl = require('../library/middleware');
const Ctr = require('../controller');
const Router = require('koa-router');
const v_1 = require('./v1');
const v_1_1 = require('./v1_1');
const v_admin = require('./admin');
const router = new Router();
// restful api v1
router.use('/1', v_1.routes(), v_1.allowedMethods());
// restful api v1.1
rotuer.use('/1.1', v_1_1.routes(), v_1_1.allowedMethods());
// manage api
router.use('/admin', v_admin.routes(), v_admin.allowedMethods());
router.all('*', function*() {
this.status = 404;
});
module.exports = router;
v1.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16'use strict';
/* jshint node: true */
const Ctr = require('../controller').v1;
const Router = require('koa-router');
const v1 = new Router();
// 增加用户
v1.post('/user', Ctr.User.create);
// 删除用户
v1.delete('/user/:user_id', Ctr.User.delete);
// 查询用户信息
v1.get('/user/:user_id', Ctr.User.read);
v1.get('/user', Ctr.User.read);
// 修改用户信息
v1.put('/user/:user_id', Ctr.User.update);
在我的controller
service
model
三个文件夹下,都会有一个index.js
的文件,controller
会被路由引入,controller
又会引入service
,service
又引入model
,最终到达数据库。我比较习惯用一个单独的大写字母来标示一些特殊的集合。
1 | const U = require('../library/util'); |
一开始用这种方法的时候,总会担心别人的批评,因为这种看不出是什么鬼的定义很容易被喷,有点过不去心里的坎,不过后来想想,规则就是人定的,为什么一定要遵守别人的规则,自己觉得好久即可。以前做PHP用think PHP框架也有类似的语法。用一个字母表示一个集合,确实是蛮实用的。
service
作为服务层,承载着所有增删改查的业务逻辑处理,虽然有模型层把控数据格式,但是模型层很多时候只会返回诸如Validation error
之类的单一错误提示,这样的提示很难判定是哪个地方出了异常,所以在数据进入模型层之前,我会对参数做断言判断,而断言的异常情况,会统一定义在library/error.js
文件里。
登录验证的服务层1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22exports.verify = function*(account, password) {
assert(H.isAccount(account), 'ACCOUNT_INVALID');
assert(U.isPassword(password), 'PASSWORD_INVALID');
let rs = yield M.Mysql.User.findOne({
where: { $or: [{ mobile: account }, { email: account }] },
include: [{
model: M.Mysql.UserProfile,
through: {
attributes: ['gender']
}
}]
});
assert(rs, 'USER_GONE');
assert(rs.password == H.generatePassword(password, rs.password_salt), 'PASSWORD_ERROR');
let access_token = yield H.signToken(U.extend(rs, { password_salt: null }, U.extend.RN));
let refresh_token = yield H.signToken({ user_id: rs._id }, refresh_token_expires);
return { access_token: access_token, refresh_token: refresh_token };
};
error.js1
2
3
4
5
6
7
8
9
10
11
12
13
14'use strict';
/* jshint node: true */
exports.ACCOUNT_INVALID = {message: '账号无效', code: 400};
exports.PASSWORD_INVALID = {message: '密码无效', code: 400};
exports.PASSWORD_ERROR = {message: '密码错误', code: 400};
exports.USER_GONE = {message: '用户不存在', code: 410};
exports.PARAM_INVALID = {message: '参数错误', code: 400};
exports.MOBILE_EXISTS = {message: '手机号码已存在', code: 409};
exports.EMAIL_EXISTS = {message: '电子邮箱已存在', code: 409};
exports.OBJECT_ID_INVALID = {message: '无效的查询ID', code: 400};
exports.EMAIL_INVALID = {message: '电子邮箱格式错误', code: 400};
exports.MOBILE_INVALID = {message: '手机号码格式错误', code: 400};
exports.BOOK_GONE = {message: '书籍不存在', code: 400};
有了assert
,于是代码里必然要出现try {} catch(e) {}
的代码。每个接口都写一遍try catch
会很麻烦,既然error可以统一由error.js定义,那么统一处理也是可以的
登录验证的controller1
2
3
4
5
6
7
8exports.verify = H.f(function*() {
let account = this.request.body.account;
let password = this.request.body.password;
let ret = yield S.Auth.verify.call(this, account, password);
this.send(200, ret);
});
正常来讲,verify这个方法只要返回一个Generator function
即可,为了把try catch
的代码提取出来,我包多了一层函数用来处理异常。这个函数定义在我的library/helper.js
文件里面
1 | exports.f = function(func, error) { |
函数的名字主要是为了简短,又想不出太好的命名,就直接给了个f
,函数的两个参数,第一个就是传入进来的generator function
,第二个是自定义的错误释义,因为有时候我们需要返回的错误解释也许不是error.js里面定义的那个,或者说有时候同样是401的状态码,但是可能是不同情况导致的,例如有可能是令牌过期,也有可能是用户修改了密码,要求重新登录,于是你必须给出不同的释义。
模型层基本用的都是orm框架,较好的把控了数据问题,关系数据都存在mysql数据库,较少的数据存mongo,例如书籍这类树形的数据。