博客

  • koa-send源码分析

    通常我们在做静态文件服务的时候,首选CDN。当文件内容需要经常变动时,则可以采用nginx代理的方式。node本身也可以搭建静态服务,用koa static可以很容易实现这个功能。
    koa static是一个koa中间件,内部是对koa send的封装。koa static本身只做了一层简单的逻辑,所以这篇文章主要分析一下koa send的实现方式。
    如果让我们自己实现这个功能,也很简单,逻辑就是根据用户请求路径,找到文件,然后做一个文件流的响应。
    koa send的实现也大概是这个思路,另外多了一些基于http协议的处理,当然,阅读koa send的源码,还是有一些意外的收获。
    koa send源码很简洁,唯一暴露了一个工具函数send,send函数大致结构如下:
    async function send(ctx, path, opts = {}) {
    // 1、参数path校验
    // 2、配置opts初始化
    // 3、accept encoding处理
    // 4、404、500处理
    // 5、缓存头处理
    // 6、流响应
    }

    第1步和第2步是koa send本身的一些配置的处理,代码比较啰嗦,我们可以忽略。
    第3步,主要是根据请求头Accept-Encoding进行处理,如果用户浏览器支持br或者gzip的压缩方式,koa send会判断是否存在br或者gz格式文件,如果存在会优先响应br或者gz文件。

    第4步,会做文件查找,如果不存在文件,或者文件查找异常,则进行404或者500的响应。具体代码如下:
    try {
    stats = await fs.stat(path)
    // 默认文件index
    if (stats.isDirectory()) {
    if (format && index) {
    path += ‘/’ + index
    stats = await fs.stat(path)
    } else {
    return
    }
    }
    } catch (err) {
    const notfound = [‘ENOENT’, ‘ENAMETOOLONG’, ‘ENOTDIR’]
    if (notfound.includes(err.code)) {
    throw createError(404, err)
    }
    err.status = 500
    throw err
    }

    第5步,会设置协商缓存Last-Modified和强制缓存Cache-Control,代码很简单不解释,不过这里面有一个之前没遇到的知识点,koa send设置的Cache-Control会有类似max-age=10000,immutable的值,immutable表示永不改变,浏览器永不需要请求资源,这个感觉可以配合带hash或者版本号的资源使用。

    第6步最有意思,代码很简单,只有一行:
    ctx.body = fs.createReadStream(path)
    熟悉node文件流的同学都会知道,fs.createReadStream创建了一个自path的文件流,调用.pipe即可通过管道做文件流传输。比如使用node的http模块实现的http服务,如果要对res做文件流响应,那么只要调用strame.pipe(res)即可。
    但是这里有意思的是,koa send把stream赋值给ctx.body就完了,没有看到任何流处理。另外平时我们做json格式的响应,也是类似的调用ctx.body={a:1}。也就是说,ctx.body可以做不同类型的赋值操作。要解释这个操作方式,就要回到koa本身对koa.body的实现。
    先贴上koa body的实现代码:
    set body(val) {

    // no content
    if (null == val) {
    if (!statuses.empty[this.status]) this.status = 204;
    this.remove(‘Content-Type’);
    this.remove(‘Content-Length’);
    this.remove(‘Transfer-Encoding’);
    return;
    }

    // string
    if (‘string’ == typeof val) {
    if (setType) this.type = /^s*</.test(val) ? ‘html’ : ‘text’;
    this.length = Buffer.byteLength(val);
    return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
    if (setType) this.type = ‘bin’;
    this.length = val.length;
    return;
    }

    // stream
    if (‘function’ == typeof val.pipe) {
    // 结束关闭流 stream.destroy stream.close
    // https://www.npmjs.com/package/on-finished
    // https://www.npmjs.com/package/destroy
    onFinish(this.res, destroy.bind(null, val));
    ensureErrorHandler(val, err => this.ctx.onerror(err));
    // overwriting
    if (null != original && original != val) this.remove(‘Content-Length’);
    if (setType) this.type = ‘bin’;
    return;
    }

    // json
    this.remove(‘Content-Length’);
    this.type = ‘json’;
    }
    从截取的源码可以看到,原来是ctx.body做了一层setter拦截,当我们赋值的时候,koa对于不同格式的body,统一在setter中做了分类处理。从源码上看,body依次支持了空值、字符串、字节、文件流和json几种响应类型。对于是否流的判断,就是通过是否对象存在pipe函数确定的。
    这个实现方式挺有意思,以后对于一些不同类型的复制操作,可以把类型判断和一些逻辑放到setter中来做,代码会清晰很多。

  • vue 单文件探索

    原文地址: vue 单文件探索

    以 vue 作为开发技术栈的前端开发者,往往会配合前端构建工具,进行项目的工程化管理。比如,大家常用的 vue 全家桶 + webpack 的方案进行一些中大型前端项目的开发。配合 webpack 后,vue 的组件化优势更加明显,我们可以通过单文件的组件化开发方式,在工作实践中搭建前端页面,从而提高开发效率。有这样一个问题:“当我们在写 vue 单文件时,我们在写什么?” 很多人可能会这样回答:template 负责模板,javascript 负责逻辑,style 负责样式。当回答到这里时,一个 vue 开发者的世界观基本上算是很明确了。我们要做的就是在一个单文件组件中写 template、javascript、style。如果仅仅局限于此,显然我们无法从更好的利用的单文件组件服务我们的整个开发流程。接下来我将和大家讨论在 vue 单文件开发中的一些方法论的问题。
    vue 单文件本质
    vue单文件是以特定文件扩展名 .vue 命名的文件。如下所示的代码:
    ListDemo.vue
    <template>
    <div class=”list-demo”>
    <ul>
    <li v-for=”item in list” :key=”item.key”>{{item.value}}</li>
    </ul>
    </div>
    </template>

    <script>
    export default {
    name: ‘ListNav’,
    data() {
    return {
    list: [
    { key: ‘home’, value: ‘首页’ },
    { key: ‘category’, value: ‘文章分类’ },
    { key: ‘tags’, value: ‘标签’ },
    { key: ‘about’, value: ‘关于我’ },
    { key: ‘links’, value: ‘友情链接’},
    ],
    };
    },
    };
    </script>

    <style>
    .list-demo {
    font-size: 14px;
    }
    </style>

    代码中含有 template,script,style。三者的作用此处就不在赘述,如上的结构展示了一个 vue 单文件基本的文件结构。其背后的理念就是一个单文件组件对应了一个功能性组件,该组件的模板,样式,业务逻辑都采用就近维护的思想。从组件的复用性,后期可维护性的角度上来说,这样的理念都大大的提高了组件化的开发效率。vue 的单文件,既不是 js,也不是 html,也不是 css 文件,这样的文件如何被应用到页面上,这也就是下面将会说到的一个问题,vue 单文件是如何被处理成页面中可用的资源。
    vue 单文件被处理的流程
    vue 单文件配合 webpack 构建工具,在 webpack 中会交由 vue-loader 来处理。如下所示:
    {
    test: /.vue$/,
    loader: ‘vue-loader’,
    }
    项目中通过 import 或者 require 引入的 vue 单文件,都会经过 vue-loader 处理,vue-loader 在这个过程中会将模板按照 template、script、style 解析并将处理结果返回,三种不同类型的文件交由接下来的loader 进行处理。如果该单文件组件在父组件中的 components 声明,则 components 中对应的该项会被插入解析后 script 代码。这个过程从入口文件 main.js 开始,所有涉及的被依赖单文件组件依次经历这样的处理过程。之后所有的组件的实例化将根据业务逻辑中的依赖关系进行,这个过程也是我们平时在开发中经常用到的一种方式。(这里可以单拉一篇文章详细讲述 vue-loader 的处理流程)
    单文件的常用姿势
    模板中的组件引用
    一、使用方式
    组件的拆分和嵌套:

    将具体的业务按照功能以及后期复用性方面的考虑划分成更小的组件
    通过一个容器组件(父组件)将小的功能组件(子组件)进行整合

    操作手法:父组件中引入子组件,components 中注册,template 中添加相应的组件引用模板
    这种方式也是我们在进行单文件的开发中常用的一种方式,所有组件的实例化,都被隐含在组件的嵌套关系和业务逻辑中。开发者只需要关心组件的引入,在父组件逻辑中注册该组件,并在父组件的模板中以标签的方式引入组件。这个过程中待引入的组件的实例化时机也可以通过 v-if 指令在业务逻辑中进行控制。
    二、适用场景
    大部分场景下我们都可以通过这样的方式进行组件化的开发。这种模式的有一个特点: 组件的引入通过组件注册和模板中写入对应的组件的标签来完成。模板中通过标签来引入组件这一步必不可少,这个特点在某些业务场景下可能给开发者带来了一定的重复工作量。
    API 式的调用
    API 式的调用指的是手动创建子组件的实例,业务逻辑中无需引入组件和模板标签占位,在暴露的 API 中控制组件的实例化与显示。
    一、使用方式

    功能模块提供一个入口 js 来控制该功能模块下单文件实例的所有功能逻辑
    其他组件中使用该功能模块时,调用功能模块下的 js,传入部分参数

    操作手法:
    Confirm.vue
    <template>
    <el-dialg
    title=”test”
    :visible.sync=”visible”>
    {{content}}
    <el-button @click=”handleCancelClick”>cancel</el-button>
    <el-button @click=”handleOkClick”>ok</el-button>
    </el-dialg>
    </template>

    <script>
    export default {
    name: ‘Confirm’,
    data() {
    return {
    visible: false,
    content: ‘这是一个confirm dialog’,
    callback: null,
    };
    },
    methods: {
    handleCancelClick() {
    this.callback(‘cancel’);
    },
    handleOkClick() {
    this.callback(‘confirm’);
    },
    },
    };
    </script>

    confirm.js
    import Vue from ‘vue’;
    import Confirm from ‘./confirm’;

    const ConfirmConstructor = Vue.extend(Confirm);

    const confirm = (content) => {
    let confirmInstance = new ConfirmConstructor({
    data: {
    content,
    },
    });
    confirmInstance.vm = confirmInstance.$mount();
    confirmInstance.vm.visible = true;
    // 手动插入目的 dom
    document.body.appendChild(confirmInstance.vm.$el);
    confirmInstance.vm.callback = action => {
    return new Promise((resolve, reject) => {
    resolve(action);
    });
    };
    return confirmInstance.vm;
    };

    如上所示,给出的是一个确认弹框的场景实现。确认弹框在很多用户交互中是一个必须的交互形式。很多组件库也采用上面这种 API 式的组件调用。调用方仅仅通过 api 的调用,就能实现该功能模块的引用。这样就避免了在 template 中通过标签占位的方式引用。实现原理就是手动接管单文件组件的实例化,通过 Vue.extend 获得该组件对应的 Vue 的子类,在暴露给调用的 api 中去实例化这个组件。这个过程中我们可能还要完成一些组件数据的注入,逻辑相关以及手动将该组件插入到目的 dom 中。手动的注入 dom 是该种方式的一个很大特点,通过在 api 中动态的注入目的 dom,避免我们在各个业务组件中调用该功能模块时重复性的在业务组件 template 中手写组件标签。
    二、适用场景

    功能聚合度高,组件内逻辑简单,输入输出较为单一,比如一些功能较为独立的弹框
    一些特殊的自定义指令开发,比如在一些特殊场景的指令,可以复用一些单文件组件,通过在指令的钩子中实例化组件对应的 vue 子类,按照特定的逻辑插入到目的 dom 中(例如:element-ui的v-loading)

    区别和共性
    共性:通过实例化对应组件完成组件的功能逻辑
    区别:实例化的时机和方式不同。模板式的引入通过组件注册和标签引入的方式来使用单文件组件。标签引入解决了子组件插入的 dom 位置问题,开发者无需关心。API 式的单文件组件使用,在 API 调用时手动实例化组件,需要手动控制插入到目的 dom。
    总结
    vue 的单文件组件提供了 vue 的组件化开发思路,其本质在导出 vue 的一些关键属性,比如生命周期函数,methods,computed, watch,props等。我们可以通过上述两种方式来使用单文件组件,目的在于工程内部尽量减少重复的模板代码,组件解耦。

  • 【前端芝士树】Vue.js面试题整理 / 知识点梳理

    【前端芝士树】 Vue.js 面试题整理
    MVVM是什么?
    MVVM 是 Model-View-ViewModel 的缩写。

    Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。

    View 代表UI 组件,它负责将数据模型转化成UI 展现出来。

    ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。

    在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
    ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

    注意, MVVM模型中, Model和View是不会直接连接的,而ViewModel则会以双向连接的形式连接Model和View。
    0. Vue的特性(优缺点)

    轻量级的框架
    双向数据绑定
    指令
    插件化

    1. Vue的生命周期
    具体可以参照官网的这张图,左侧以红色框表示的都是阶段

    beforeCreate
    created
    beforeMount
    mounted
    beforeUpdated
    updated
    beforeDestroy
    destroyed

    大致过程就是

    数据初始化(1~2)完成数据观测、属性和方法的运算加载,event/wather时间回调。
    dom挂载阶段(3~4)el被新创建的vm.$el替换并挂载到实例上去,之后调用钩子函数。
    数据更新阶段(5~6)数据更新,虚拟dom重渲染
    组件卸载阶段(7~8)销毁实例及子实例

    2. Vue实现数据双向绑定的原理
    vue实现数据双向绑定主要是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
    vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令,最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。
    3. Vue的路由实现
    路由的实现有两种:hash和history interface来实现前端路由,hash在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;特点:
    (1)hash虽然在URL中,但不被包括在HTTP请求中
    (2)用来指导浏览器动作,对服务端安全无用,hash不会重加载页面
    history采用h5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更,不过history有个问题是:如果用户直接在地址栏中输入并回车,浏览器重启或重新加载时,history模式会将url修改的和正常请求后端一样,此情况下,重新向后端发送请求,后端如果没有配置对应路由处理,则返回404,解决方法是后端配置一下。
    4. Vue路由传参
    <router-link :to=”{path:’/index’,params:{id:num}}”>
    <router-link :to=”{ path:’/index’ , query:{id:num}}”>

    然后通过$route.params来读取数据,但路由传递参数值是对象的话就不行了会报错,传递前用base64转译一下就可以了。
    未完待续 – – – – – –
    参考链接 MVVM – 廖雪峰的官方网站MVC,MVP 和 MVVM 的图示 – 阮一峰的网络日志vue.js学习笔记(一):什么是mvvm框架,vue.js的核心思想 – _林冲 – 博客园

  • Windows下PHP服务nginx不能使用file_get_contents的原因

    注意:本文为转载,原文链接:Windows下PHP服务nginx不能使用file_get_contents/curl/fopen的原因!

    一、问题说明
    在Windows环境下搭建了一个本地开发服务环境,使用Nginx做服务,但是在使用file_get_contents()获取本地的链接时http://127.0.0.1/index.php,出现了这样的错误:
    file_get_contents(http://127.0.0.1/index.php) [<a href=’function.file-get-contents’>function.file-get-contents</a>]: failed to open stream: HTTP request failed!
    本地电脑php环境为:nginx+php+mysql;于是找到这篇文章做个笔记,记录下!
    这两天一直在搞windows下nginx+fastcgi的file_get_contents请求。我想,很多同学都遇到当file_get_contents请求外网的http/https的php文件时毫无压力,比如echo file_get_contents(‘http://www.baidu.com’) ,它会显示百度的页面。但当你请求localhost/127.0.0.1本地网络的php服务时却一直是timeout,无论你将请求时间和脚本运行时间多长都无法返回数据,如file_get_contents(‘http://localhost/phpinfo.php’) 。然而当你尝试请求html这样的静态文件时却完全没有问题。是什么原因呢?!
    首先,我们知道file_get_contents/curl/fopen打开一个基于tcp/ip的http请求时,请求数据发送到nginx,而nginx则委托给php-cgi(fastcgi)处理php文件,一般情况fastcgi处理完一个php请求后会马上释放结束信号,等待下一个处理请求(当然也有程序假死,一直占用资源的情况)。打开nginx.conf,我们看到下面这一行:
    location ~ .php {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME d:/www/htdocs$fastcgi_script_name;
    include fastcgi_params;
    }

    上面已经清楚地看到,所有使用php结尾的文件都经过fastcgi处理,而在php.ini的配置文件中也有一句:
    cgi.force_redirect = 1
    表明,所有php程序安全地强制转向交给cgi处理。
    但在windows中,本地127.0.0.1:9000怎样与php-cgi联系的呢?!答案是增加一个php-cgi进程,用它来监听127.0.0.1:9000。通过控制器命令:
    RunHiddenConsole.exe D:/www/php/php-cgi.exe -b 127.0.0.1:9000 -c C:/WINDOWS/php.ini
    我们就可以在启动windows时,开启一个php-cgi.exe进程监听来自127.0.0.1:9000 的请求。在dos命令下打开netstat –a就可以看到本地计算机下的9000端口处于listening状态(也就是空置,如果没有发送任何请求的话)。
    好了,该说说在php中使用file_get_contents()、curl()、fopen()函数访问localhost时为什么不能返回结果。我们再来试验在index.php中加入file_get_contents(‘http://127.0.0.1/phpinfo.php’) 语句向phpinfo.php发送一个请求,这时浏览器中的状态指示一直在打转,表示它一直在工作中。打开Dos中的netstat命令,可以看到本地的9000端口的状态为:ESTABLISHED,表示该进程在联机处理中。实际上,这里我们已经同时向nginx发送了两个基于http的php请求,一个是解析index.php,而另一个是phpinfo.php,这样矛盾就出来了,因为我们的windows系统只加载了一个http进程,因此,它无法同时处理两个php请求,它只能先处理第一个请求(index.php),而index.php却又在等待phpinfo.php处理结果,phpinfo.php没人帮它处理请求,因为它一直在等待index.php释放结束信号,因此,造成了程序的阻塞状态,陷入了死循环。所以我们就看到了浏览器的状态指示一直在打转。Curl()与fopen函数的原因也相同。
    二、解决方法
    找到了原因,我们也就有了解决办法。
    一是,向系统增加一个http请求,当一个php-cig内要加载另一个请求时,它能够分配其它http处理额外的php请求。这时需给另一个http sever分配不同的端口,比如8080。nginx的案例如下:
    http {
    server {
    listen 80;
    server_name 127.0.0.1;
    location / {
    index index.php;
    root /web/www/htdocs;
    }
    }
    server {
    listen 8080;
    server_name 127.0.0.1;
    location / {
    index index.html;
    root /web/www/htdocs;
    }
    }
    include /opt/nginx/conf/vhosts/php.conf;
    }

    这样,端口80与8080可以分别处理不同的程序,比如:test.php
    echo file_get_contents(‘http://localhost:8080/phpinfo.php’);
    当然,在*unix下有更多选择,比如fork。
    另外提醒下,网上有人说,通过去掉地址中的http://协议标记,而使用相对地址就规避函数的检查,实际情况是不是这样呢?!当在index.php中使用file_get_contents(‘phpinfo.php’); 时,我们可以看到函数输出了phpinfo.php的源代码,相当于file_get_contents(‘file:c:wwwphpinfo.php’); ,它实际上只是读取你的文本内容,因为file_get_contents()函数首先是处理file协议的,而curl则直接报错无法解析。因此这些人纯粹是不学无术的骗子。
    还有人提出修改hosts文件,增加localhost www.xxx.com影射关系,函数通过www.xxx.com访问本地php,这其实也是不治本的偏方,因为这只是方便计算机的dns解析,最终www.xxx.com交给127.0.0.1,而后者交给唯一http,还是阻塞。

  • 构建自己的博客

    一、前言
    看过很多人,用github创建个人博客,最近抽空也实现的自己的博客,下面就把摸索过程记录下。
    二、准备
    安装Node.js
    Node.js下载地址:https://nodejs.org/en/download/
    安装过程一路默认安装即可。
    详细安装文档参看:http://www.runoob.com/nodejs/…
    安装Git软件
    Git软件下载地址:https://git-scm.com/download
    安装过程一路默认安装即可。
    关于更多的Git讲解参看:
    https://www.liaoxuefeng.com/w…
    安装Hexo
    什么是 Hexo?Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。
    Hexo官方网站:https://hexo.io/zh-cn/ ,我用的当前版本是v6.4.0,基本步骤:

    新建一个blog空文件夹,cmd窗口或vscode终端,输入命令npm install -g hexo-cli全局安装hexo;
    安装完成后输入hexo -v显示hexo的相关信息说明安装成功;
    输入命令hexo init初始化hexo项目,安装相关依赖:
    上一步安装完成,输入命令hexo s或hexo server,开启服务,成功后,在浏览器访问http://localhost:4000生成的默认主题博客。PS:在这里可以npm install hexo-browsersync –save实现热更新,不必每次更改都去刷新浏览器。

    安装完成后的目录结构
    – node_modules // 依赖包
    – public // 存放生成的页面
    – scaffolds // 生成页模板
    – source // 创建的源文章
    – themes // 主题
    – _config.yml // 博客配置(站点配置)
    – db.json // 解析source得到的库文件
    – package.json // 项目依赖配置

    三、修改站点配置
    _config.yml文件是对整个站点基本信息的配置,比如:
    # Site
    title: // 博客名称
    subtitle: // 副标题
    description: // 描述 用于seo
    keywords: // 关键字 用于seo
    author: // 作者 用于seo
    language: // 语言
    timezone: // 时区
    四、Github创建一个hexo的代码库
    和创建其它git仓库一样,只不过名称必须为yourname.github.io这种形式,其中yourname是你github个人账号名,创建好后,找到站点配置文件(blog下的_config.yml文件),找到:
    # Deployment
    ## Docs: https://hexo.io/docs/deployment.html
    deploy:
    type:
    改成你的 博客git仓库地址和分支:
    deploy:
    type: git
    repo: https://github.com/YourgithubName/YourgithubName.github.io.git
    branch: master
    这样,远程仓库配置完成。接下来

    输入命令hexo generate或hexo g将原markedown文章生成静态页面,放置在生成的public目录下;

    npm install hexo-deployer-git –save安装hexo的git插件;
    再输入命令hexo deploy或hexo d将生成的静态页面推送到远程仓库;
    完成后,在浏览器访问https://yourname.github.io/,就能看到你构建好的个人博客啦!

    五、写文章
    hexo支持用markdown写作,在目录blog/source/_posts新建markdown文件,或者使用命令hexo new posts 你的文章标题。
    小坑:{{}}符号编译出错
    markdown生成静态页面,{{}}是swig模板符号,属于特殊字符,在编译时解析不了双大括号中间字符串就会报错
    FATAL Something’s wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.html
    Template render error: (unknown path) [Line 3, Column 8]
    unexpected token: }}
    解决方案:用转义字符代替
    { -> &#123; — 大括号左边部分Left curly brace
    } -> &#125; — 大括号右边部分Right curly brace
    六、配置主题
    hexo默认主题是landscape,样式可能不是每个人都喜爱的,官方页提供了一些主题,可以按自己的风格安装,只需在站点配置文件_config.yml更改主题名称。
    Next主题是目前比较流行的主题,文档相对比较成熟。next主题文档
    安装
    cd blog
    git clone https://github.com/theme-next/hexo-theme-next themes/next
    更换主题
    # Extensions
    ## Plugins: https://hexo.io/plugins/
    ## Themes: https://hexo.io/themes/
    theme: next
    修改Next主题配置文件
    目录blog/themes/next找到_config.yml文件,其中有很多配置项,下面列举几个常用的。
    更换头像
    # Sidebar Avatar
    avatar:
    # in theme directory(source/images): /images/avatar.gif
    # in site directory(source/uploads): /uploads/avatar.gif
    # You can also use other linking images.
    url: /images/avatar.png
    # If true, the avatar would be dispalyed in circle.
    rounded: true
    # The value of opacity should be choose from 0 to 1 to set the opacity of the avatar.
    opacity: 1
    # If true, the avatar would be rotated with the cursor.
    rotated: false
    只需将头像的url换成你自己头像的url或者直接覆盖头像图片即可。
    注意这里的根/的绝对路径是blog/themes/next/source/,以后写文章引用本地图片的话,注意放到这个目录下!
    代码高亮
    NexT使用Tomorrow Theme作为代码高亮,共有5款主题供你选择。 NexT默认使用的是白色的normal主题,可选的值有normal,night,night blue, night bright,night eighties。
    # Code Highlight theme
    # Available values: normal | night | night eighties | night blue | night bright
    # https://github.com/chriskempson/tomorrow-theme
    highlight_theme: normal

    添加分类页
    文章可能需要分类,添加分类页
    cd blog
    hexo new page categories
    此时在blog/source目录下就生成了categories/index.md文件:

    title: 分类
    date: 2018-08-28 14:59:48
    type: categories
    comments: false // 分类页不需要添加评论

    然后,放开主题配置文件_config.yml中menu setting对categories注释
    menu:
    home: / || home
    categories: /categories/ || th
    以后文章的内容头声明的分类就会在分类页有索引了。

    添加标签页
    跟添加分类页一样,文章也需要标签
    cd blog
    hexo new page tags
    此时在blog/source目录下就生成了tags/index.md文件:

    title: 标签
    date: 2018-08-28 14:34:22
    type: tags
    comments: false // 标签页不需要评论

    然后,放开主题配置文件_config.yml中menu setting对tags注释
    menu:
    home: / || home
    tags: /tags/ || tags
    以后文章的内容头声明的分类就会在分类页有分类了。

    404页
    当访问当前站点,页面找不到,跳转到404页,推荐用腾讯公益404页面,寻找丢失儿童,让大家一起关注此项公益事业!使用方法,新建404.html页面,放到主题的source目录下,内容如下:
    <!DOCTYPE HTML>
    <html>
    <head>
    <meta http-equiv=”content-type” content=”text/html;charset=utf-8;”/>
    <meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″ />
    <meta name=”robots” content=”all” />
    <meta name=”robots” content=”index,follow”/>
    <link rel=”stylesheet” type=”text/css” href=”https://qzone.qq.com/gy/404/style/404style.css”>
    </head>
    <body>
    <script type=”text/plain” src=”http://www.qq.com/404/search_children.js”
    charset=”utf-8″ homePageUrl=”/”
    homePageName=”回到我的主页”>
    </script>
    <script src=”https://qzone.qq.com/gy/404/data.js” charset=”utf-8″></script>
    <script src=”https://qzone.qq.com/gy/404/page.js” charset=”utf-8″></script>
    </body>
    </html>
    站点分析统计
    对于个人站点,我们需要统计用户访问量,用户分布,跳转率等。Next主题推荐使用的有百度统计、Google分析、腾讯分析了,使用均一样,注册获取站点统计id。
    百度统计
    我一直用的百度统计,注册百度统计,管理 > 网站列表 > 新增网站完成后,代码管理 > 代码获取,就能得到统计id。
    # Baidu Analytics ID
    baidu_analytics: 统计id

    不蒜子统计
    不蒜子统计可以统计站点以及每个页面的PV、UV和站点总的访问数,以小眼睛的形式展现。
    编辑主题配置文件中的busuanzi_count的配置项。当enable: true时,代表开启全局开关。若total_visitors、total_views、post_views的值均为false时,不蒜子仅作记录而不会在页面上显示。
    内容分享服务
    Next主题还提供了对外提供分享接口,包括百度分享、addthis Share和NeedMoreShare2,要用到哪个,只需把相应enable: true,注册账号获取id即可。
    评论功能
    当前版本配置,支持畅言,Disqus,valine,gitment。

    畅言 – 搜狐提供的评论组件,功能丰富,体验优异,防止注水;但必须进行域名备案。只要域名备过案就可以通过审核。
    Disqus – 国外使用较多的评论组件。万里长城永不倒,一枝红杏出墙来,你懂的。
    valine – LeanCloud提供的后端云服务,可用于统计网址访问数据,分为开发版和商用版,只需要注册生成应用App ID和App Key即可使用。
    Ditment – Gitment 是一款基于GitHub Issues的评论系统。支持在前端直接引入,不需要任何后端代码。可以在页面进行登录、查看、评论、点赞等操作,同时有完整的Markdown / GFM和代码高亮支持。尤为适合各种基于GitHub Pages的静态博客或项目页面。

    畅言要备案,对于对于挂靠在GitHub的博客非常的不友好,放弃!Disqus,目前国内网络环境访问不了,放弃!valine在用户没有认证登录可以评论,不能防止恶意注水,放弃!我选择用Gitment,让用户用自己的GitHub账号才能评论,用git的一个仓库来存储评论(评论以该仓库的issue形式展现)。
    gitment配置
    Gitment的全部配置项如下,
    # Gitment
    # Introduction: https://imsun.net/posts/gitment-introduction/
    gitment:
    enable: true
    mint: true # RECOMMEND, A mint on Gitment, to support count, language and proxy_gateway
    count: true # Show comments count in post meta area
    lazy: false # Comments lazy loading with a button
    cleanly: true # Hide ‘Powered by …’ on footer, and more
    language: zh-CN # Force language, or auto switch by theme
    github_user: xxx # MUST HAVE, Your Github Username
    github_repo: xxx # MUST HAVE, The name of the repo you use to store Gitment comments
    client_id: xxx # MUST HAVE, Github client id for the Gitment
    client_secret: xxx # EITHER this or proxy_gateway, Github access secret token for the Gitment
    proxy_gateway: # Address of api proxy, See: https://github.com/aimingoo/intersect
    redirect_protocol: # Protocol of redirect_uri with force_redirect_protocol when mint enabled
    开启enable为true就显示评论框了,不过要真正实现评论可用,需要用自己的github账号注册一个应用许可证书,即OAuth application,注册成功就生成了client_id和client_secret。
    步骤:你的github首页 > settings > Developer settings > OAuth Apps > New oAuth App。填写好相关信息,其中,Homepage URL和Authorization callback URL都写上你的github博客首页地址,比如https://xxx.github.io/,点击Register application即可完成注册,生成Client ID和Client Secret。

    这样,用户点击评论框右边登入跳转到github授权,允许授权跳转回来就可以评论啦!

    小坑:有些文章评论初始化弹出validation failed错误

    因为GitHub的每个issues有两个lables,一个是gitment,另一个是id,id取的是页面pathname,标签长度限定不超过50个字符,而像一般中文标题文章,转义后字符很容易超过50个字符。
    目录blog/themes/next/layout/_third-party/comments找到文件gitment.swig,

    在这里我用文章创建时间戳来当作id,这样长度就不会超过50个字符,成功解决!
    七、总结
    构建自己的博客并不难,也不需要什么专业代码基础,需要的是耐心而已(┭┮﹏┭┮都是配置)。PS:我的GitHub博客https://wuwhs.github.io 大佬拍轻点

  • 高效的Mobx模式 – (Part 1)

    起因
    很早之前看到的一篇关于mobx的文章,之前记得是有人翻译过的,但是怎么找都找不到,故花了点时间通过自己那半桶水的英文水平,加上Google翻译一下,对于初学者,以及mobx的开发者提供些许帮助。
    这里针对已经了解mobx,且有过一些简单的开发的同学,其中对文章有一些删减,还有翻译的不对的地方欢迎大家指出。
    高效的Mobx模式 – (Part 1)
    MobX提供了一种简单而强大的方法来管理客户端状态。 它使用一种称为(TFRP-Transparent Functional Reactive Programming)的技术,其中如果任何相关值发生变化,它会自动计算派生值。 也就是通过设置跟踪值更改的依赖关系图来完成的。
    MobX导致思维方式的转变(For the better),并改变您的心理模型围绕管理客户端状态。
    Part 1 – 构建可观察者
    当我们使用Mobx时,建立客户端状态模型是第一步, 这是最有可能被客户端上呈现你的域模型的直接体现。实际上客户端状态也就是我们通常说的Store,了解redux的对此都很熟悉,虽然你只有一个Store,但是它是由多个子Store组件的,每一个子Store用来处理应用程序的各种功能。
    最简单的入门方法是注释Store的属性,这些属性将随着@observable而不断变化。 请注意,我使用的是装饰器语法,但使用简单的ES5函数包装器可以实现相同的功能。
    import { observable } from ‘mobx’
    class MyStore {
    @observable name
    @observable description
    @observable author

    @observable photos = []
    }
    修剪可观察属性
    通过将对象标记为@observable,您将自动观察其所有嵌套属性。 现在这可能是你想要的东西,但很多时候它更能限制可观察性。 你可以使用一些MobX修饰符来做到这一点:
    asReference
    当某些属性永远不会改变值时,这是非常有用的。 请注意,如果您确实更改了引用本身,它将触发更改。
    let address = new Address();
    let contact = observable({
    person: new Person(),
    address: asReference(address)
    });

    address.city = ‘New York’; // 不会触发通知任何

    // 将触发通知,因为这是属性引用更改
    contact.address = new Address();
    在上面的示例中,address属性将不可观察。 如果您更改地址详细信息,则不会收到通知。 但是,如果您更改地址引用本身,您将收到通知。
    一个有趣的消息是一个可观察对象的属性,其值具有原型(类实例)将自动使用asReference()注释。 此外,这些属性不会被进一步递归。

    asFlat
    这比asReference略宽一些。 asFlat允许属性本身可观察,但不允许其任何子节点。 典型用法适用于您只想观察数组实例而不是其项目的数组。 请注意,对于数组,length属性仍然是可观察的,因为它在数组实例上。 但是,对子属性的任何更改都不会被观察到。

    首先创建@observable所有内容,然后应用asReference和asFlat修饰符来修剪可观察属性。
    当你深入实现应用程序的各种功能时,你会发现这种修剪的好处。且当你开始时可能并不明显,这完全很正常。当你识别出不需要深度可观察性的属性时,请确保重新检查你的Store, 它可以对您的应用程序的性能产生积极影响。
    import {observable} from ‘mobx’;

    class AlbumStore {
    @observable name;

    // 这里不需要观察
    @observable createdDate = asReference(new Date());

    @observable description;
    @observable author;

    // 只观察照片数组,而不是单独的照片
    @observable photos = asFlat([]);
    }

    扩展可观察属性
    和修剪可观察属性相反,你可以扩展对象上可观察性的范围/行为,而不是删除可观察性。 这里有三个可以控制它的修饰符:
    asStructure
    这会修改将新值分配给observable时完成相等性检查的方式。 默认情况下,仅将引用更改视为更改。 如果您希望基于内部结构进行比较,则可以使用此修饰符。 这主要是针对值类型(也称为结构),只有在它们的值匹配时才相等。如下图:
    const { asStructure, observable } = require(‘mobx’);

    let address1 = {
    zip: 12345,
    city: ‘New York’
    };

    let address2 = {
    zip: 12345,
    city: ‘New York’
    };

    let contact = {
    address: observable(address1)
    };

    // 将被视为一种变化,因为它是一个新的引用
    contact.address = address2;

    // 使用 asStructure() 修饰
    let contact2 = {
    address: observable(asStructure(address1))
    };

    // 不会被视为一种变化,因为它具有相同的价值
    contact.address = address2;

    asMap
    默认情况下,将对象标记为可观察对象时,它只能跟踪最初在对象上定义的属性。 如果添加新属性,则不会跟踪这些属性。 使用asMap,您甚至可以使新添加的属性可观察。 在内部,MobX将创建一个类似ES6的Map,它具有与原生Map类似的API。
    除了使用此修饰符,您还可以通过从常规可观察对象开始来实现类似的效果。 然后,您可以使用extendObservable()API添加更多可观察的属性。 当您想要延迟添加可观察属性时,此API非常有用。

    computed
    这是一个如此强大的概念,其重要性无法得到足够的重视。 计算属性不是域的真实属性,而是使用实际属性派生(也称为计算)。 一个典型的例子是person实例的fullName属性。 它派生自firstName和lastName属性。 通过创建简单的计算属性,您可以简化域逻辑。 例如,您可以只创建一个计算的hasLastName属性,而不是检查一个人是否在任何地方都有lastName
    class Person {
    @observable firstName;
    @observable lastName;

    @computed get fullName() {
    return `${this.firstName}, ${this.lastName}`;
    }

    @computed get hasLastName() {
    return !!this.lastName;
    }
    }

    构建可观察树是使用MobX的一个重要方面,这使MobX开始跟踪您的store中有趣且值得改变的部分!
    Part 2 稍后发布

  • 细数那些不懂Spring底层原理带来的伤与痛

    1. 什么是spring?
    Spring 是个Java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。
    2. 使用Spring框架的好处是什么?

    轻量:Spring 是轻量的,基本的版本大约2MB。
    控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
    面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
    容器:Spring 包含并管理应用中对象的生命周期和配置。
    MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
    事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
    异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。

    3. Spring由哪些模块组成?
    以下是Spring 框架的基本模块:

    Core module
    Bean module
    Context module
    Expression Language module
    JDBC module
    ORM module
    OXM module
    Java Messaging Service(JMS) module
    Transaction module
    Web module
    Web-Servlet module
    Web-Struts module
    Web-Portlet module

    4. 核心容器(应用上下文) 模块。
    这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。
    5. BeanFactory – BeanFactory 实现举例。
    Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离。
    最常用的BeanFactory 实现是XmlBeanFactory 类。
    6. XMLBeanFactory
    最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
    7. 解释AOP模块
    AOP模块用于发给我们的Spring应用做面向切面的开发, 很多支持由AOP联盟提供,这样就确保了Spring和其他AOP框架的共通性。这个模块将元数据编程引入Spring。
    8. 解释JDBC抽象和DAO模块。
    通过使用JDBC抽象和DAO模块,保证数据库代码的简洁,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层。它还利用Spring的AOP 模块给Spring应用中的对象提供事务管理服务。
    9. 解释对象/关系映射集成模块。
    Spring 通过提供ORM模块,支持我们在直接JDBC之上使用一个对象/关系映射映射(ORM)工具,Spring 支持集成主流的ORM框架,如Hiberate,JDO和 iBATIS SQL Maps。Spring的事务管理同样支持以上所有ORM框架及JDBC。
    10. 解释WEB 模块。
    Spring的WEB模块是构建在application context 模块基础之上,提供一个适合web应用的上下文。这个模块也包括支持多种面向web的任务,如透明地处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。它也有对Jakarta Struts的支持。
    11. Spring配置文件
    Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。
    12. 什么是Spring IOC 容器?
    Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
    13. IOC的优点是什么?
    IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载。
    15. ApplicationContext通常的实现是什么?

    FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
    ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。
    WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。

    16. Bean 工厂和 Application contexts 有什么区别?
    Application contexts提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的bean发布事件。另外,在容器或容器内的对象上执行的那些不得不由bean工厂以程序化方式处理的操作,可以在Application contexts中以声明的方式处理。Application contexts实现了MessageSource接口,该接口的实现以可插拔的方式提供获取本地化消息的方法。
    17. 一个Spring的应用看起来象什么?

    一个定义了一些功能的接口。
    这实现包括属性,它的Setter , getter 方法和函数等。
    Spring AOP。
    Spring 的XML 配置文件。
    使用以上功能的客户端程序。
    依赖注入

    18. 什么是Spring的依赖注入?
    依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。
    19. 有哪些不同类型的IOC(依赖注入)方式?

    构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
    Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。

    20. 哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?
    你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。
    Spring Beans
    21.什么是Spring beans?
    Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中<bean/> 的形式定义。
    Spring 框架定义的beans都是单件beans。在bean tag中有个属性”singleton”,如果它被赋为TRUE,bean 就是单件,否则就是一个 prototype bean。默认是TRUE,所以所有在Spring框架中的beans 缺省都是单件。
    22. 一个 Spring Bean 定义 包含什么?
    一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。
    23. 如何给Spring 容器提供配置元数据?
    这里有三种重要的方法给Spring 容器提供配置元数据。

    XML配置文件。
    基于注解的配置。
    基于java的配置。

    24. 你怎样定义类的作用域?
    当定义一个<bean> 在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如,当Spring要在需要的时候每次生产一个新的bean实例,bean的scope属性被指定为prototype。另一方面,一个bean每次使用的时候必须返回同一个实例,这个bean的scope 属性 必须设为 singleton。
    25. 解释Spring支持的几种bean的作用域。
    Spring框架支持以下五种bean的作用域:

    singleton : bean在每个Spring ioc 容器中只有一个实例。
    prototype:一个bean的定义可以有多个实例。
    request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
    session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
    global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

    缺省的Spring bean 的作用域是Singleton.
    26. Spring框架中的单例bean是线程安全的吗?
    不,Spring框架中的单例bean不是线程安全的。
    27. 解释Spring框架中bean的生命周期。

    Spring容器 从XML 文件中读取bean的定义,并实例化bean。
    Spring根据bean的定义填充所有的属性。
    如果bean实现了BeanNameAware 接口,Spring 传递bean 的ID 到 setBeanName方法。
    如果Bean 实现了 BeanFactoryAware 接口, Spring传递beanfactory 给setBeanFactory 方法。
    如果有任何与bean相关联的BeanPostProcessors,Spring会在postProcesserBeforeInitialization()方法内调用它们。
    如果bean实现IntializingBean了,调用它的afterPropertySet方法,如果bean声明了初始化方法,调用此初始化方法。
    如果有BeanPostProcessors 和bean 关联,这些bean的postProcessAfterInitialization() 方法将被调用。
    如果bean实现了 DisposableBean,它将调用destroy()方法。

    28. 哪些是重要的bean生命周期方法? 你能重载它们吗?
    有两个重要的bean 生命周期方法,第一个是setup , 它是在容器加载bean的时候被调用。第二个方法是 teardown 它是在容器卸载类的时候被调用。
    The bean 标签有两个重要的属性(init-method和destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct和@PreDestroy)。
    29. 什么是Spring的内部bean?
    当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean,为了定义inner bean,在Spring 的 基于XML的 配置元数据中,可以在 <property/>或 <constructor-arg/> 元素内使用<bean/> 元素,内部bean通常是匿名的,它们的Scope一般是prototype。
    30. 在 Spring中如何注入一个java集合?
    Spring提供以下几种集合的配置元素:

    <list>类型用于注入一列值,允许有相同的值。
    <set> 类型用于注入一组值,不允许有相同的值。
    <map> 类型用于注入一组键值对,键和值都可以为任意类型。
    <props>类型用于注入一组键值对,键和值都只能为String类型。

    31. 什么是bean装配?
    装配,或bean 装配是指在Spring 容器中把bean组装到一起,前提是容器需要知道bean的依赖关系,如何通过依赖注入来把它们装配到一起。
    32. 什么是bean的自动装配?
    Spring 容器能够自动装配相互合作的bean,这意味着容器不需要<constructor-arg>和<property>配置,能通过Bean工厂自动处理bean之间的协作。
    33. 解释不同方式的自动装配 。
    有五种自动装配的方式,可以用来指导Spring容器用自动装配方式来进行依赖注入。

    no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。
    byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
    byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
    constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
    autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。

    34.自动装配有哪些局限性 ?
    自动装配的局限性是:

    重写: 你仍需用 <constructor-arg>和 <property> 配置来定义依赖,意味着总要重写自动装配。
    基本数据类型:你不能自动装配简单的属性,如基本数据类型,String字符串,和类。
    模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。

    35. 你可以在Spring中注入一个null 和一个空字符串吗?
    可以。
    Spring注解
    36. 什么是基于Java的Spring注解配置? 给一些注解的例子.
    基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。
    以@Configuration 注解为例,它用来标记类可以当做一个bean的定义,被Spring IOC容器使用。另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。
    37. 什么是基于注解的容器配置?
    相对于XML文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明。
    开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不是使用xml表述bean的装配关系。
    38. 怎样开启注解装配?
    注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 <context:annotation-config/>元素。
    39. @Required 注解
    这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显式的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializationException。
    40. @Autowired 注解
    @Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它的用法和@Required一样,修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。
    41. @Qualifier 注解
    当有多个相同类型的bean却只有一个需要自动装配时,将@Qualifier 注解和@Autowire 注解结合使用以消除这种混淆,指定需要装配的确切的bean。
    Spring数据访问
    42.在Spring框架中如何更有效地使用JDBC?
    使用SpringJDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate (例子见这里here)
    43. JdbcTemplate
    JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理。
    44. Spring对DAO的支持
    Spring对数据访问对象(DAO)的支持旨在简化它和数据访问技术如JDBC,Hibernate or JDO 结合使用。这使我们可以方便切换持久层。编码时也不用担心会捕获每种技术特有的异常。
    45. 使用Spring通过什么方式访问Hibernate?
    在Spring中有两种方式访问Hibernate:

    控制反转 Hibernate Template和 Callback。
    继承 HibernateDAOSupport提供一个AOP 拦截器。

    46. Spring支持的ORM
    Spring支持以下ORM:

    Hibernate
    iBatis
    JPA (Java Persistence API)
    TopLink
    JDO (Java Data Objects)

    47.如何通过HibernateDaoSupport将Spring和Hibernate结合起来?
    用Spring的 SessionFactory 调用 LocalSessionFactory。集成过程分三步:

    配置the Hibernate SessionFactory。
    继承HibernateDaoSupport实现一个DAO。
    在AOP支持的事务中装配。

    48. Spring支持的事务管理类型
    Spring支持两种类型的事务管理:

    编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
    声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。

    49. Spring框架的事务管理有哪些优点?

    它为不同的事务API 如 JTA,JDBC,Hibernate,JPA 和JDO,提供一个不变的编程模式。
    它为编程式事务管理提供了一套简单的API而不是一些复杂的事务API如
    它支持声明式事务管理。
    它和Spring各种数据访问抽象层很好得集成。

    50. 你更倾向用那种事务管理类型?
    大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。
    Spring面向切面编程(AOP)
    51. 解释AOP
    面向切面的编程,或AOP, 是一种编程技术,允许程序模块化横向切割关注点,或横切典型的责任划分,如日志和事务管理。
    52. Aspect 切面
    AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。
    53. 在Spring AOP 中,关注点和横切关注的区别是什么?
    关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
    横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。
    54. 连接点
    连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个应用程序执行Spring AOP的位置。
    55. 通知
    通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段。
    Spring切面可以应用五种类型的通知:

    before:前置通知,在一个方法执行前被调用。
    after: 在方法执行之后调用的通知,无论方法执行是否成功。
    after-returning: 仅当方法成功完成后执行的通知。
    after-throwing: 在方法抛出异常退出时执行的通知。
    around: 在方法执行之前和之后调用的通知。

    56. 切点
    切入点是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。
    57. 什么是引入?
    引入允许我们在已存在的类中增加新的方法和属性。
    58. 什么是目标对象?
    被一个或者多个切面所通知的对象。它通常是一个代理对象。也指被通知(advised)对象。
    59. 什么是代理?
    代理是通知目标对象后创建的对象。从客户端的角度看,代理对象和目标对象是一样的。
    60. 有几种不同类型的自动代理?

    BeanNameAutoProxyCreator
    DefaultAdvisorAutoProxyCreator
    Metadata autoproxying

    61. 什么是织入。什么是织入应用的不同点?
    织入是将切面和到其他应用类型或对象连接或创建一个被通知对象的过程。
    织入可以在编译时,加载时,或运行时完成。
    62. 解释基于XML Schema方式的切面实现。
    在这种情况下,切面由常规类以及基于XML的配置实现。
    63. 解释基于注解的切面实现
    在这种情况下(基于@AspectJ的实现),涉及到的切面声明的风格与带有java5标注的普通java类一致。
    Spring 的MVC
    64. 什么是Spring的MVC框架?
    Spring 配备构建Web 应用的全功能MVC框架。Spring可以很便捷地和其他MVC框架集成,如Struts,Spring 的MVC框架用控制反转把业务对象和控制逻辑清晰地隔离。它也允许以声明的方式把请求参数和业务对象绑定。
    65. DispatcherServlet
    Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。
    66. WebApplicationContext
    WebApplicationContext 继承了ApplicationContext 并增加了一些WEB应用必备的特有功能,它不同于一般的ApplicationContext ,因为它能处理主题,并找到被关联的servlet。
    67. 什么是Spring MVC框架的控制器?
    控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层,允许用户创建多种用途的控制器。
    说到这里顺便给大家推荐一个Java架构方面的交流学习社群:650385180,里面不仅可以交流讨论,还有面试经验分享以及免费的资料下载,包括Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。
    68. @Controller 注解
    该注解表明该类扮演控制器的角色,Spring不需要你继承任何其他控制器基类或引用Servlet API。
    69. @RequestMapping 注解
    该注解是用来映射一个URL到一个类或一个特定的方处理法上。
    by:老李

  • Web通信协议,你还需要知道: SPDY 和 QUIC

    上一篇文章前端阶段性总结(一): HTTP 协议,从1.0到2.0梳理了一下HTTP协议。本文将重点介绍,在前端通信协议中HTTP2的前身,具有开拓性意义的SPDY,以及具有颠覆性的QUIC协议。
    一、开拓者:SPDY
    1. 简介:
    spdy 是由google推行的,改进版本的HTTP1.1 (那时候还没有HTTP2)。它基于TCP协议,在HTTP的基础上,结合HTTP1.X的多个痛点进行改进和升级的产物。它的出现使web的加载速度有极大的提高。HTTP2也借鉴了很多spdy的特性。
    2. 特性:
    上一篇文章中有介绍,基本和HTTP2差不多,这里就不赘述了:

    多路复用
    头部压缩
    服务器推送
    请求优先级

    spdy的架构图:

    3. 现状:
    在HTTP2未推出之前,spdy在业界内已经有一定的市场占用量,并且它的成绩也是不容忽视的,因此在很长一段时间,市场上可能会见到spdy和h2同时使用的场景。
    二、颠覆者:QUIC
    1. 前置知识:
    TCP 与 UDP
    TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
    1)提供IP环境下的数据可靠传输(一台计算机发出的字节流会无差错的发往网络上的其他计算机,而且计算机A接收数据包的时候,也会向计算机B回发数据包,这也会产生部分通信量),有效流控,全双工操作(数据在两个方向上能同时传递),多路复用服务,是面向连接,端到端的传输;
    2)面向连接:正式通信前必须要与对方建立连接。事先为所发送的数据开辟出连接好的通道,然后再进行数据发送,像打电话。
    3)TCP支持的应用协议:Telnet(远程登录)、FTP(文件传输协议)、SMTP(简单邮件传输协议)。TCP用于传输数据量大,可靠性要求高的应用。
    UDP(User Datagram Protocol用户数据报协议)是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
    1) 面向非连接的(正式通信前不必与对方建立连接,不管对方状态就直接发送,像短信,QQ),不能提供可靠性、流控、差错恢复功能。UDP用于一次只传送少量数据,可靠性要求低、传输经济等应用。2) UDP支持的应用协议:NFS(网络文件系统)、SNMP(简单网络管理系统)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。
    总的来说:TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。
    Diffie-Hellman 算法

    D-H算法的数学基础是离散对数的数学难题,其加密过程如下:

    (1)客户端与服务端确定两个大素数 n和 g,这两个数不用保密
    (2)客户端选择另一个大随机数x,并计算A如下:A = g^x mod n
    (3)客户端将 A 发给服务端
    (4)服务端选择另一个大随机数y,并计算B如下:B = g^y mod n
    (5)服务端将B发给客户端
    (7)计算秘密密钥K1如下:K1=B^2 mod n , 计算秘密密钥K2如下:K2=A^y mod n , K1=K2,因此服务端和客户端可以用其进行加解密

    攻击者知道n和g,并且截获了A和B,但是当它们都是非常大的数的时候,依靠这四个数来计算k1和k2非常困难,这就是离散对数数学难题。
    2. 什么是QUIC?
    quic 是google推出的,基于UDP的高效可靠协议。quic在UDP的基础上实现了TCP的一些特性,而由于底层使用的是UDP,所以传输效率比TCP高效。
    3. 特性
    a. 基于UDP建立的连接:
    我们知道,基于TCP的协议,如http2,在首次建立连接的时候需要进行三次握手,即至少需要3个ntt,而考虑安全HTTPS的TLS层,又需要至少次的通信才能协商出密钥。这在短连接的场景中极大的增加了网络延迟,而这种延迟是无法避免的。而基于UDP的quic协议,则不需要3次握手的过程,甚至在安全协商阶段只需要进行1~2次的协商通信,即可建立安全稳定的连接,极大的减少了网络延迟。

    b. 基于Diffie-Hellman的加密算法:
    HTTPS 使用的是 TLS + SSL 的加密手段,在交换证书、协商密钥的过程中,至少需要2次ntt进行协商通信。而quic使用了Diffie-Hellman算法,算法的原理使得客户端和浏览器之间只需要1次的协商就能获得通信密钥,quic建立安全链接的详细过程:

    客户端发起Inchoate client hello
    服务器返回Rejection,包括密钥交换算法的公钥信息,算法信息,证书信息等被放到server config中传给客户端
    客户端发起client hello,包括客户端公钥信息

    后续发起连接的过程中,一旦客户端缓存或持久化了server config,就可以复用并结合本地生成的私钥进行加密数据传输了,不需要再次握手,从而实现0RTT建立连接。
    c. 改进的多路复用
    我们知道,无论是HTTP2还是SPDY,基于TCP的协议尽管实现了多路复用,但仍然没有避免队头阻塞的问题,这个问题是由于TCP底层的实现造成的,即TCP的包有严格的顺序控制,前序包如果丢失,则后续包即使返回了也不会通知到应用层协议,从而导致了前序包阻塞。而quic基于UDP则天然的避免了这个问题,由于UDP没有严格的包顺序,一个包的阻塞只会影响它自身,并不会影响到其他资源,且quic也实现了类似HTTP2的多路复用,这种没有队头阻塞的多路复用对延迟的降低是显而易见的。
    d. 连接的迁移
    在以往的基于TCP的协议中,往往使用四元组(源IP,源端口,目的IP,目的端口)来标识一条连接,当四元组中的IP或端口任一个发生变化了连接就需要重新建立,从而不具备连接迁移的能力。而QUIC使用了connection id对连接进行唯一标识。即使网络从4G变成了wifi,只要两次连接中的connection id不变,并且客户端或者服务器能通过校验,就不需要重新建立连接,连接迁移就能成功。这在移动端场景的优势极为明显,因为手机经常会在wifi和4g中切换,使用quic协议降低了重建连接的成本。
    e. 协商的升级
    在chorme浏览器中,发起一个TCP请求,这个请求会同时与服务器开始建立tcp 和 quic 的连接(前提是服务器支持),如果quic连接先建立成功,则使用quic建立的连接通信,反之,则使用tcp建立的连接进行通信。具体步骤如下:

    1、客户端发出tcp请求
    2、服务端如果支持quic可以通过响应头alt-svc告知客户端
    3、客户端同时发起tcp连接和quic连接竞赛
    4、一旦quic建立连接获胜则采用quic协议发送请求
    5、如遇网络或服务器不支持quic/udp,客户端标记quic为broken
    6、传输中的请求通过tcp重发
    7、5min后尝试重试quic,下一次尝试增大到10min
    8、一旦再次成功采用quic并把broken标记取消

    其中,支持quic的alt-svc头部信息如下图示:
    d. 其他特性:

    改进的拥塞控制
    丢包恢复
    底层的连接持久化
    head stream 保证包顺序
    双级别流量控制

    三、总结与思考
    在web通信协议的演进中,SPDY是不可或缺的角色,在没有HTTP2的时代,它的出现极大的提升了网页加载速度,甚至HTTP2也是吸取了它很多的特性而制定的,是当之无愧的开拓者。而在有HTTP2的今天,quic协议的出现无异于对TCP的颠覆,它在底层抛弃了传统的TCP,而使用高效的UDP,并实现了TCP的特性,并且没有修改到应用层协议,我们可以无缝的基于原有的规范去开发。最后,这两东西居然都是google提出并推行的。只能说google真的牛叉~
    参考文章https://www.jianshu.com/p/2d8…https://wetest.qq.com/lab/vie…https://cloud.tencent.com/dev…https://www.zhihu.com/questio…

  • 绝对干货!初学者也能看懂的DPDK解析

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~
    本文由Willko发表于云+社区专栏

    一、网络IO的处境和趋势
    从我们用户的使用就可以感受到网速一直在提升,而网络技术的发展也从1GE/10GE/25GE/40GE/100GE的演变,从中可以得出单机的网络IO能力必须跟上时代的发展。
    1. 传统的电信领域
    IP层及以下,例如路由器、交换机、防火墙、基站等设备都是采用硬件解决方案。基于专用网络处理器(NP),有基于FPGA,更有基于ASIC的。但是基于硬件的劣势非常明显,发生Bug不易修复,不易调试维护,并且网络技术一直在发展,例如2G/3G/4G/5G等移动技术的革新,这些属于业务的逻辑基于硬件实现太痛苦,不能快速迭代。传统领域面临的挑战是急需一套软件架构的高性能网络IO开发框架。
    2. 云的发展
    私有云的出现通过网络功能虚拟化(NFV)共享硬件成为趋势,NFV的定义是通过标准的服务器、标准交换机实现各种传统的或新的网络功能。急需一套基于常用系统和标准服务器的高性能网络IO开发框架。
    3. 单机性能的飙升
    网卡从1G到100G的发展,CPU从单核到多核到多CPU的发展,服务器的单机能力通过横行扩展达到新的高点。但是软件开发却无法跟上节奏,单机处理能力没能和硬件门当户对,如何开发出与时并进高吞吐量的服务,单机百万千万并发能力。即使有业务对QPS要求不高,主要是CPU密集型,但是现在大数据分析、人工智能等应用都需要在分布式服务器之间传输大量数据完成作业。这点应该是我们互联网后台开发最应关注,也最关联的。
    二、Linux + x86网络IO瓶颈
    在数年前曾经写过《网卡工作原理及高并发下的调优》一文,描述了Linux的收发报文流程。根据经验,在C1(8核)上跑应用每1W包处理需要消耗1%软中断CPU,这意味着单机的上限是100万PPS(Packet Per Second)。从TGW(Netfilter版)的性能100万PPS,AliLVS优化了也只到150万PPS,并且他们使用的服务器的配置还是比较好的。假设,我们要跑满10GE网卡,每个包64字节,这就需要2000万PPS(注:以太网万兆网卡速度上限是1488万PPS,因为最小帧大小为84B《Bandwidth, Packets Per Second, and Other Network Performance Metrics》),100G是2亿PPS,即每个包的处理耗时不能超过50纳秒。而一次Cache Miss,不管是TLB、数据Cache、指令Cache发生Miss,回内存读取大约65纳秒,NUMA体系下跨Node通讯大约40纳秒。所以,即使不加上业务逻辑,即使纯收发包都如此艰难。我们要控制Cache的命中率,我们要了解计算机体系结构,不能发生跨Node通讯。
    从这些数据,我希望可以直接感受一下这里的挑战有多大,理想和现实,我们需要从中平衡。问题都有这些
    1.传统的收发报文方式都必须采用硬中断来做通讯,每次硬中断大约消耗100微秒,这还不算因为终止上下文所带来的Cache Miss。
    2.数据必须从内核态用户态之间切换拷贝带来大量CPU消耗,全局锁竞争。
    3.收发包都有系统调用的开销。
    4.内核工作在多核上,为可全局一致,即使采用Lock Free,也避免不了锁总线、内存屏障带来的性能损耗。
    5.从网卡到业务进程,经过的路径太长,有些其实未必要的,例如netfilter框架,这些都带来一定的消耗,而且容易Cache Miss。
    三、DPDK的基本原理
    从前面的分析可以得知IO实现的方式、内核的瓶颈,以及数据流过内核存在不可控因素,这些都是在内核中实现,内核是导致瓶颈的原因所在,要解决问题需要绕过内核。所以主流解决方案都是旁路网卡IO,绕过内核直接在用户态收发包来解决内核的瓶颈。
    Linux社区也提供了旁路机制Netmap,官方数据10G网卡1400万PPS,但是Netmap没广泛使用。其原因有几个:
    1.Netmap需要驱动的支持,即需要网卡厂商认可这个方案。
    2.Netmap仍然依赖中断通知机制,没完全解决瓶颈。
    3.Netmap更像是几个系统调用,实现用户态直接收发包,功能太过原始,没形成依赖的网络开发框架,社区不完善。
    那么,我们来看看发展了十几年的DPDK,从Intel主导开发,到华为、思科、AWS等大厂商的加入,核心玩家都在该圈子里,拥有完善的社区,生态形成闭环。早期,主要是传统电信领域3层以下的应用,如华为、中国电信、中国移动都是其早期使用者,交换机、路由器、网关是主要应用场景。但是,随着上层业务的需求以及DPDK的完善,在更高的应用也在逐步出现。
    DPDK旁路原理:
    图片引自Jingjing Wu的文档《Flow Bifurcation on Intel® Ethernet Controller X710/XL710》
    左边是原来的方式数据从 网卡 -> 驱动 -> 协议栈 -> Socket接口 -> 业务
    右边是DPDK的方式,基于UIO(Userspace I/O)旁路数据。数据从 网卡 -> DPDK轮询模式-> DPDK基础库 -> 业务
    用户态的好处是易用开发和维护,灵活性好。并且Crash也不影响内核运行,鲁棒性强。
    DPDK支持的CPU体系架构:x86、ARM、PowerPC(PPC)
    DPDK支持的网卡列表:https://core.dpdk.org/supported/,我们主流使用Intel 82599(光口)、Intel x540(电口)
    四、DPDK的基石UIO
    为了让驱动运行在用户态,Linux提供UIO机制。使用UIO可以通过read感知中断,通过mmap实现和网卡的通讯。
    UIO原理:

    要开发用户态驱动有几个步骤:
    1.开发运行在内核的UIO模块,因为硬中断只能在内核处理
    2.通过/dev/uioX读取中断
    3.通过mmap和外设共享内存
    五、DPDK核心优化:PMD
    DPDK的UIO驱动屏蔽了硬件发出中断,然后在用户态采用主动轮询的方式,这种模式被称为PMD(Poll Mode Driver)。
    UIO旁路了内核,主动轮询去掉硬中断,DPDK从而可以在用户态做收发包处理。带来Zero Copy、无系统调用的好处,同步处理减少上下文切换带来的Cache Miss。
    运行在PMD的Core会处于用户态CPU100%的状态

    网络空闲时CPU长期空转,会带来能耗问题。所以,DPDK推出Interrupt DPDK模式。
    Interrupt DPDK:
    图片引自David Su/Yunhong Jiang/Wei Wang的文档《Towards Low Latency Interrupt Mode DPDK》
    它的原理和NAPI很像,就是没包可处理时进入睡眠,改为中断通知。并且可以和其他进程共享同个CPU Core,但是DPDK进程会有更高调度优先级。
    六、DPDK的高性能代码实现
    1. 采用HugePage减少TLB Miss
    默认下Linux采用4KB为一页,页越小内存越大,页表的开销越大,页表的内存占用也越大。CPU有TLB(Translation Lookaside Buffer)成本高所以一般就只能存放几百到上千个页表项。如果进程要使用64G内存,则64G/4KB=16000000(一千六百万)页,每页在页表项中占用16000000 * 4B=62MB。如果用HugePage采用2MB作为一页,只需64G/2MB=2000,数量不在同个级别。
    而DPDK采用HugePage,在x86-64下支持2MB、1GB的页大小,几何级的降低了页表项的大小,从而减少TLB-Miss。并提供了内存池(Mempool)、MBuf、无锁环(Ring)、Bitmap等基础库。根据我们的实践,在数据平面(Data Plane)频繁的内存分配释放,必须使用内存池,不能直接使用rte_malloc,DPDK的内存分配实现非常简陋,不如ptmalloc。
    2. SNA(Shared-nothing Architecture)
    软件架构去中心化,尽量避免全局共享,带来全局竞争,失去横向扩展的能力。NUMA体系下不跨Node远程使用内存。
    3. SIMD(Single Instruction Multiple Data)
    从最早的mmx/sse到最新的avx2,SIMD的能力一直在增强。DPDK采用批量同时处理多个包,再用向量编程,一个周期内对所有包进行处理。比如,memcpy就使用SIMD来提高速度。
    SIMD在游戏后台比较常见,但是其他业务如果有类似批量处理的场景,要提高性能,也可看看能否满足。
    4. 不使用慢速API
    这里需要重新定义一下慢速API,比如说gettimeofday,虽然在64位下通过vDSO已经不需要陷入内核态,只是一个纯内存访问,每秒也能达到几千万的级别。但是,不要忘记了我们在10GE下,每秒的处理能力就要达到几千万。所以即使是gettimeofday也属于慢速API。DPDK提供Cycles接口,例如rte_get_tsc_cycles接口,基于HPET或TSC实现。
    在x86-64下使用RDTSC指令,直接从寄存器读取,需要输入2个参数,比较常见的实现:
    static inline uint64_t
    rte_rdtsc(void)
    {
    uint32_t lo, hi;

    __asm__ __volatile__ (
    “rdtsc” : “=a”(lo), “=d”(hi)
    );

    return ((unsigned long long)lo) | (((unsigned long long)hi) << 32);
    }
    这么写逻辑没错,但是还不够极致,还涉及到2次位运算才能得到结果,我们看看DPDK是怎么实现:
    static inline uint64_t
    rte_rdtsc(void)
    {
    union {
    uint64_t tsc_64;
    struct {
    uint32_t lo_32;
    uint32_t hi_32;
    };
    } tsc;

    asm volatile(“rdtsc” :
    “=a” (tsc.lo_32),
    “=d” (tsc.hi_32));
    return tsc.tsc_64;
    }
    巧妙的利用C的union共享内存,直接赋值,减少了不必要的运算。但是使用tsc有些问题需要面对和解决
    1) CPU亲和性,解决多核跳动不精确的问题
    2) 内存屏障,解决乱序执行不精确的问题
    3) 禁止降频和禁止Intel Turbo Boost,固定CPU频率,解决频率变化带来的失准问题
    5. 编译执行优化
    1) 分支预测
    现代CPU通过pipeline、superscalar提高并行处理能力,为了进一步发挥并行能力会做分支预测,提升CPU的并行能力。遇到分支时判断可能进入哪个分支,提前处理该分支的代码,预先做指令读取编码读取寄存器等,预测失败则预处理全部丢弃。我们开发业务有时候会非常清楚这个分支是true还是false,那就可以通过人工干预生成更紧凑的代码提示CPU分支预测成功率。
    #pragma once

    #if !__GLIBC_PREREQ(2, 3)
    # if !define __builtin_expect
    # define __builtin_expect(x, expected_value) (x)
    # endif
    #endif

    #if !defined(likely)
    #define likely(x) (__builtin_expect(!!(x), 1))
    #endif

    #if !defined(unlikely)
    #define unlikely(x) (__builtin_expect(!!(x), 0))
    #endif
    2) CPU Cache预取
    Cache Miss的代价非常高,回内存读需要65纳秒,可以将即将访问的数据主动推送的CPU Cache进行优化。比较典型的场景是链表的遍历,链表的下一节点都是随机内存地址,所以CPU肯定是无法自动预加载的。但是我们在处理本节点时,可以通过CPU指令将下一个节点推送到Cache里。
    API文档:https://doc.dpdk.org/api/rte_…
    static inline void rte_prefetch0(const volatile void *p)
    {
    asm volatile (“prefetcht0 %[p]” : : [p] “m” (*(const volatile char *)p));
    }
    #if !defined(prefetch)
    #define prefetch(x) __builtin_prefetch(x)
    #endif
    …等等
    3) 内存对齐
    内存对齐有2个好处:
    l 避免结构体成员跨Cache Line,需2次读取才能合并到寄存器中,降低性能。结构体成员需从大到小排序和以及强制对齐。参考《Data alignment: Straighten up and fly right》
    #define __rte_packed __attribute__((__packed__))
    l 多线程场景下写产生False sharing,造成Cache Miss,结构体按Cache Line对齐
    #ifndef CACHE_LINE_SIZE
    #define CACHE_LINE_SIZE 64
    #endif

    #ifndef aligined
    #define aligined(a) __attribute__((__aligned__(a)))
    #endif
    4) 常量优化
    常量相关的运算的编译阶段完成。比如C++11引入了constexp,比如可以使用GCC的__builtin_constant_p来判断值是否常量,然后对常量进行编译时得出结果。举例网络序主机序转换
    #define rte_bswap32(x) ((uint32_t)(__builtin_constant_p(x) ?
    rte_constant_bswap32(x) :
    rte_arch_bswap32(x)))
    其中rte_constant_bswap32的实现
    #define RTE_STATIC_BSWAP32(v)
    ((((uint32_t)(v) & UINT32_C(0x000000ff)) << 24) |
    (((uint32_t)(v) & UINT32_C(0x0000ff00)) << 8) |
    (((uint32_t)(v) & UINT32_C(0x00ff0000)) >> 8) |
    (((uint32_t)(v) & UINT32_C(0xff000000)) >> 24))
    5)使用CPU指令
    现代CPU提供很多指令可直接完成常见功能,比如大小端转换,x86有bswap指令直接支持了。
    static inline uint64_t rte_arch_bswap64(uint64_t _x)
    {
    register uint64_t x = _x;
    asm volatile (“bswap %[x]”
    : [x] “+r” (x)
    );
    return x;
    }
    这个实现,也是GLIBC的实现,先常量优化、CPU指令优化、最后才用裸代码实现。毕竟都是顶端程序员,对语言、编译器,对实现的追求不一样,所以造轮子前一定要先了解好轮子。
    Google开源的cpu_features可以获取当前CPU支持什么特性,从而对特定CPU进行执行优化。高性能编程永无止境,对硬件、内核、编译器、开发语言的理解要深入且与时俱进。
    七、DPDK生态
    对我们互联网后台开发来说DPDK框架本身提供的能力还是比较裸的,比如要使用DPDK就必须实现ARP、IP层这些基础功能,有一定上手难度。如果要更高层的业务使用,还需要用户态的传输协议支持。不建议直接使用DPDK。
    目前生态完善,社区强大(一线大厂支持)的应用层开发项目是FD.io(The Fast Data Project),有思科开源支持的VPP,比较完善的协议支持,ARP、VLAN、Multipath、IPv4/v6、MPLS等。用户态传输协议UDP/TCP有TLDK。从项目定位到社区支持力度算比较靠谱的框架。
    腾讯云开源的F-Stack也值得关注一下,开发更简单,直接提供了POSIX接口。
    Seastar也很强大和灵活,内核态和DPDK都随意切换,也有自己的传输协议Seastar Native TCP/IP Stack支持,但是目前还未看到有大型项目在使用Seastar,可能需要填的坑比较多。
    我们GBN Gateway项目需要支持L3/IP层接入做Wan网关,单机20GE,基于DPDK开发。

    问答如何检查网络连接?相关阅读把报文再扔回内核,DPDK这样做用DPDK rte_ring实现多进程间通信低于0.01%的极致Crash率是怎么做到的? 【每日课程推荐】新加坡南洋理工大学博士,带你深度学习NLP技术

  • 有赞搜索系统的架构演进

    有赞搜索平台是一个面向公司内部各项搜索应用以及部分 NoSQL 存储应用的 PaaS 产品,帮助应用合理高效的支持检索和多维过滤功能,有赞搜索平台目前支持了大大小小一百多个检索业务,服务于近百亿数据。
    在为传统的搜索应用提供高级检索和大数据交互能力的同时,有赞搜索平台还需要为其他比如商品管理、订单检索、粉丝筛选等海量数据过滤提供支持,从工程的角度看,如何扩展平台以支持多样的检索需求是一个巨大的挑战。
    我是有赞搜索团队的第一位员工,也有幸负责设计开发了有赞搜索平台到目前为止的大部分功能特性,我们搜索团队目前主要负责平台的性能、可扩展性和可靠性方面的问题,并尽可能降低平台的运维成本以及业务的开发成本。
    Elasticsearch
    Elasticsearch 是一个高可用分布式搜索引擎,一方面技术相对成熟稳定,另一方面社区也比较活跃,因此我们在搭建搜索系统过程中也是选择了 Elasticsearch 作为我们的基础引擎。
    架构1.0
    时间回到 2015 年,彼时运行在生产环境的有赞搜索系统是一个由几台高配虚拟机组成的 Elasticsearch 集群,主要运行商品和粉丝索引,数据通过 Canal 从 DB 同步到 Elasticsearch,大致架构如下:

    通过这种方式,在业务量较小时,可以低成本的快速为不同业务索引创建同步应用,适合业务快速发展时期,但相对的每个同步程序都是单体应用,不仅与业务库地址耦合,需要适应业务库快速的变化,如迁库、分库分表等,而且多个 canal 同时订阅同一个库,也会造成数据库性能的下降。另外 Elasticsearch 集群也没有做物理隔离,有一次促销活动就因为粉丝数据量过于庞大导致 Elasticsearch 进程 heap 内存耗尽而 OOM,使得集群内全部索引都无法正常工作,这给我上了深深的一课。
    架构 2.0
    我们在解决以上问题的过程中,也自然的沉淀出了有赞搜索的 2.0 版架构,大致架构如下:

    首先数据总线将数据变更消息同步到 mq,同步应用通过消费 mq 消息来同步业务库数据,借数据总线实现与业务库的解耦,引入数据总线也可以避免多个 canal 监听消费同一张表 binlog 的虚耗。
    高级搜索(Advanced Search)
    随着业务发展,我们也逐渐出现了一些比较中心化的流量入口,比如分销、精选等,这时普通的 bool 查询并不能满足我们对搜索结果的细粒率排序控制需求,将复杂的 function_score 之类专业性较强的高级查询编写和优化工作交给业务开发负责显然是个不可取的选项,这里我们考虑的是通过一个高级查询中间件拦截业务查询请求,在解析出必要的条件后重新组装为高级查询交给引擎执行,大致架构如下:

    这里另外做的一点优化是加入了搜索结果缓存,常规的文本检索查询 match 每次执行都需要实时计算,在实际的应用场景中这并不是必须的,用户在一定时间段内(比如 15 或 30 分钟)通过同样的请求访问到同样的搜索结果是完全可以接受的,在中间件做一次结果缓存可以避免重复查询反复执行的虚耗,同时提升中间件响应速度,对高级搜索比较感兴趣的同学可以阅读另外一篇文章《有赞搜索引擎实践(工程篇)》,这里不再细述。
    大数据集成
    搜索应用和大数据密不可分,除了通过日志分析来挖掘用户行为的更多价值之外,离线计算排序综合得分也是优化搜索应用体验不可缺少的一环,在 2.0 阶段我们通过开源的 es-hadoop 组件搭建 hive 与 Elasticsearch 之间的交互通道,大致架构如下:

    通过 flume 收集搜索日志存储到 hdfs 供后续分析,也可以在通过 hive 分析后导出作为搜索提示词,当然大数据为搜索业务提供的远不止于此,这里只是简单列举了几项功能。
    问题
    这样的架构支撑了搜索系统一年多的运行,但是也暴露出了许多问题,首当其冲的是越发高昂的维护成本,除去 Elasticsearch 集群维护和索引本身的配置、字段变更,虽然已经通过数据总线与业务库解耦,但是耦合在同步程序中的业务代码依旧为团队带来了极大的维护负担。消息队列虽然一定程序上减轻了我们与业务的耦合,但是带来的消息顺序问题也让不熟悉业务数据状态的我们很难处理。这些问题我总结在之前写过的一篇文章。除此之外,流经 Elasticsearch 集群的业务流量对我们来说呈半黑盒状态,可以感知,但不可预测,也因此出现过线上集群被内部大流量错误调用压到CPU占满不可服务的故障。
    目前的架构 3.0
    针对 2.0 时代的问题,我们在 3.0 架构中做了一些针对性调整,列举主要的几点:

    通过开放接口接收用户调用,与业务代码完全解耦;
    增加 proxy 用来对外服务,预处理用户请求并执行必要的流控、缓存等操作;
    提供管理平台简化索引变更和集群管理

    这样的演变让有赞搜索系统逐渐的平台化,已经初具了一个搜索平台的架构:

    Proxy
    作为对外服务的出入口,proxy 除了通过 ESLoader 提供兼容不同版本 Elasticsearch 调用的标准化接口之外,也内嵌了请求校验、缓存、模板查询等功能模块。请求校验主要是对用户的写入、查询请求进行预处理,如果发现字段不符、类型错误、查询语法错误、疑似慢查询等操作后以 fast fail 的方式拒绝请求或者以较低的流控水平执行,避免无效或低效能操作对整个 Elasticsearch 集群的影响。缓存和 ESLoader 主要是将原先高级搜索中的通用功能集成进来,使得高级搜索可以专注于搜索自身的查询分析和重写排序功能,更加内聚。我们在缓存上做了一点小小的优化,由于查询结果缓存通常来说带有源文档内容会比较大,为了避免流量高峰频繁访问导致 codis 集群网络拥堵,我们在 proxy 上实现了一个简单的本地缓存,在流量高峰时自动降级。
    这里提一下模板查询,在查询结构(DSL)相对固定又比较冗长的情况下,比如商品类目筛选、订单筛选等,可以通过模板查询(search template)来实现,一方面简化业务编排DSL的负担,另一方面还可以通过编辑查询模板 template,利用默认值、可选条件等手段在服务端进行线上查询性能调优。
    管理平台
    为了降低日常的索引增删、字段修改、配置同步上的维护成本,我们基于 Django 实现了最初版本的搜索管理平台,主要提供一套索引变更的审批流以及向不同集群同步索引配置的功能,以可视化的方式实现索引元数据的管理,减少我们在平台日常维护上的时间成本。由于开源 head 插件在效果展示上的不友好,以及暴露了部分粗暴功能:

    如图,可以通过点按字段使得索引按指定字段排序展示结果,在早期版本 Elasticsearch 会通过 fielddata 加载需要排序的字段内容,如果字段数据量比较大,很容易导致 heap 内存占满引发 full gc 甚至 OOM,为了避免重复出现此类问题,我们也提供了定制的可视化查询组件以支持用户浏览数据的需求。
    ESWriter
    由于 es-hadoop 仅能通过控制 map-reduce 个数来调整读写流量,实际上 es-hadoop 是以 Elasticsearch 是否拒绝请求来调整自身行为,对线上工作的集群相当不友好。为了解决这种离线读写流量上的不可控,我们在现有的 DataX 基础上开发了一个 ESWriter 插件,能够实现记录条数或者流量大小的秒级控制。
    挑战
    平台化以及配套的文档体系完善降低了用户的接入门槛,随着业务的快速增长,Elasticsearch 集群本身的运维成本也让我们逐渐不堪,虽然有物理隔离的多个集群,但不可避免的会有多个业务索引共享同一个物理集群,在不同业务间各有出入的生产标准上支持不佳,在同一个集群内部署过多的索引也是生产环境稳定运行的一个隐患。另外集群服务能力的弹性伸缩相对困难,水平扩容一个节点都需要经历机器申请、环境初始化、软件安装等步骤,如果是物理机还需要更长时间的机器采购过程,不能及时响应服务能力的不足。
    未来的架构 4.0
    当前架构通过开放接口接受用户的数据同步需求,虽然实现了与业务解耦,降低了我们团队自身的开发成本,但是相对的用户开发成本也变高了,数据从数据库到索引需要经历从数据总线获取数据、同步应用处理数据、调用搜索平台开放接口写入数据三个步骤,其中从数据总线获取数据与写入搜索平台这两个步骤在多个业务的同步程序中都会被重复开发,造成资源浪费。这里我们目前也准备与 PaaS 团队内自研的DTS(Data Transporter,数据同步服务)进行集成,通过配置化的方式实现 Elasticsearch 与多种数据源之间的自动化数据同步。
    要解决共享集群应对不同生产标准应用的问题,我们希望进一步将平台化的搜索服务提升为云化的服务申请机制,配合对业务的等级划分,将核心应用独立部署为相互隔离的物理集群,而非核心应用通过不同的应用模板申请基于 k8s 运行的 Elasticsearch 云服务。应用模板中会定义不同应用场景下的服务配置,从而解决不同应用的生产标准差异问题,而且云服务可以根据应用运行状况及时进行服务的伸缩容。
    小结
    本文从架构上介绍了有赞搜索系统演进产生的背景以及希望解决的问题,涉及具体技术细节的内容我们将会在本系列的下一篇文章中更新。