-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 277 KB
/
Copy pathcontent.json
File metadata and controls
1 lines (1 loc) · 277 KB
1
{"meta":{"title":"coderPreacher's Blog","subtitle":"Love life, love technology, positive, optimistic, beyond the self","description":"Love life, love technology, positive, optimistic, beyond the self","author":"谢晖","url":"http://coderpreacher.top"},"pages":[{"title":"","date":"2017-04-12T15:33:09.000Z","updated":"2018-03-03T00:13:41.000Z","comments":false,"path":"about/index.html","permalink":"http://coderpreacher.top/about/index.html","excerpt":"","text":"姓名:谢晖 邮箱:coderpreacher@aliyun.com 职业:软件开发工程师、软件研发工程师、技术架构师"},{"title":"","date":"2017-08-20T02:54:07.000Z","updated":"2017-07-30T13:21:51.000Z","comments":false,"path":"categories/index.html","permalink":"http://coderpreacher.top/categories/index.html","excerpt":"","text":""},{"title":"","date":"2017-08-20T02:54:07.000Z","updated":"2017-07-30T13:59:04.000Z","comments":false,"path":"tags/index.html","permalink":"http://coderpreacher.top/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"了解Node.js事件驱动架构","slug":"了解Node-js事件驱动架构","date":"2017-09-10T05:02:05.000Z","updated":"2017-09-10T15:18:16.000Z","comments":true,"path":"2017/09/10/了解Node-js事件驱动架构/","link":"","permalink":"http://coderpreacher.top/2017/09/10/了解Node-js事件驱动架构/","excerpt":"","text":"Node的大部分对象(如HTTP请求,响应和流)都实现了EventEmitter模块,从而可以提供发送和监听事件的方法。 事件驱动性质的最简单形式是一些流行的Node.js函数的回调方式,例如fs.readFile。在这个类比中,事件将被触发一次(当Node准备好调用回调函数时),回调作为事件处理程序。 我们先来探讨这个基本形式。 当你准备好的时候给我打电话,Node!Node处理异步事件的原始方式是回调。这是很久以前,在JavaScript有本机(Promise)承诺支持和async/await功能之前。 回调基本上只是你传递给其他功能的函数。这在JavaScript中是可能的,因为函数是第一类对象。 重要的是要明白回调在代码中不表示异步调用。函数可以同步和异步地调用回调。 例如,这里是一个主机函数fileSize,它接受一个回调函数cb,并且可以基于一个条件同步和异步地调用该回调函数: 123456789function fileSize (fileName, cb) { if (typeof fileName !== 'string') { return cb(new TypeError('argument should be string')); // Sync } fs.stat(fileName, (err, stats) => { if (err) { return cb(err); } // Async cb(null, stats.size); // Async });} 请注意,这是导致意外错误的不良做法。设计主机函数以始终同步或始终异步地使用回调。 我们来探讨一个典型的异步Node函数的简单示例,它使用回调样式: 123456789const readFileAsArray = function(file, cb) { fs.readFile(file, function(err, data) { if (err) { return cb(err); } const lines = data.toString().trim().split('\\n'); cb(null, lines); });}; readFileAsArray需要一个文件路径和一个回调函数。它读取文件内容,将其拆分成行数组,并使用该数组调用回调函数。 这是一个用例。假设我们在同一目录中的文件numbers.txt包含如下内容: 123456101112131415 如果我们有一个任务来计算该文件中的奇数,我们可以使用readFileAsArray来简化代码: 123456readFileAsArray('./numbers.txt', (err, lines) => { if (err) throw err; const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log('Odd numbers count:', oddNumbers.length);}); 代码将数字内容读入字符串数组,将其解析为数字,并计数奇数。 Node的回调风格纯粹在这里使用。回调有一个第一个错误的参数err为空,我们将回调作为主机函数的最后一个参数传递。您应该始终在您的功能中执行此操作,因为用户可能会假设。使主机函数接收回调作为其最后一个参数,并使回调期望一个错误对象作为其第一个参数。 回调的现代JavaScript替代方案在现代JavaScript中,我们有承诺(Promise)的对象。 Promises可以替代异步API的回调。而不是将回调作为参数传递并在相同的地方处理错误,承诺对象允许我们单独处理成功和错误的情况,并且还允许我们链接多个异步调用而不是嵌套它们。 如果readFileAsArray函数支持promises,我们可以使用它,如下所示: 1234567readFileAsArray('./numbers.txt') .then(lines => { const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log('Odd numbers count:', oddNumbers.length); }) .catch(console.error); 而不是传入回调函数,我们在主机函数的返回值上调用了.then函数。这个.then函数通常给我们访问我们在回调版本中获得的相同的行数组,我们可以像以前一样对它进行处理。为了处理错误,我们在结果上添加一个.catch调用,当我们发生错误时,我们可以访问一个错误。 由于新的Promise对象,使现代JavaScript中的主机功能支持承诺界面更容易。这里的readFileAsArray函数修改为支持promise界面,除了它已经支持的回调接口: 12345678910111213const readFileAsArray = function(file, cb = () => {}) { return new Promise((resolve, reject) => { fs.readFile(file, function(err, data) { if (err) { reject(err); return cb(err); } const lines = data.toString().trim().split('\\n'); resolve(lines); cb(null, lines); }); });}; 所以我们使函数返回一个Promise对象,它包裹fs.readFile异步调用。 promise对象暴露两个参数,一个resolve函数和一个reject函数。 每当我们想使用错误调用回调时,我们也使用promise reject函数,当我们想要使用数据调用回调函数时,我们也使用promise resolve函数。 在这种情况下,我们需要做的唯一其他事情是为这个回调参数设置默认值,以防代码与promise接口一起使用。我们可以在这个例子的参数中使用一个简单的默认空函数:()=> {}。 消费承诺与async/await当需要循环异步功能时,添加承诺界面可使您的代码更容易处理。随着回调,事情变得凌乱。 承诺改善了一点,功能发生器改善了一点点。这就是说,使用异步代码的最新替代方案是使用异步功能,它允许我们将异步代码视为同步代码,使其整体上更加可读。 以下是使用async / await的readFileAsArray函数的方法: 1234567891011async function countOdd () { try { const lines = await readFileAsArray('./numbers'); const numbers = lines.map(Number); const oddCount = numbers.filter(n => n%2 === 1).length; console.log('Odd numbers count:', oddCount); } catch(err) { console.error(err); }}countOdd(); 我们首先创建一个异步功能,这是一个正常的功能,它之前的字异步。在异步函数内部,我们调用readFileAsArray函数,就像它返回的是行变量一样,为了做这个工作,我们使用关键字await。之后,我们继续执行代码,就好像readFileAsArray调用是同步的。 要使事情运行,我们执行异步功能。这很简单,可读性更高。要处理错误,我们需要将异步调用包装在一个try / catch语句中。 使用此异步/等待功能,我们不必使用任何特殊的API(如.then和.catch)。我们只是标记功能不同,并使用纯JavaScript代码。 我们可以使用async / await功能与任何支持承诺接口的功能。但是,我们无法使用回调式异步函数(例如setTimeout)。 EventEmitter模块EventEmitter是一个促进Node中对象之间的通信的模块。 EventEmitter是Node异步事件驱动架构的核心。 Node的许多内置模块都继承自EventEmitter。 这个概念很简单:发射体对象会发出命名事件,这些事件会导致以前注册的侦听器被调用。所以,发射器对象基本上有两个主要特征: 发出名称事件。 注册和注销侦听器功能。 要使用EventEmitter,我们只需创建一个扩展EventEmitter的类。 123class MyEmitter extends EventEmitter {} Emitter对象是我们从基于EventEmitter的类实例化的对象: 1const myEmitter = new MyEmitter(); 在这些发射器对象的生命周期的任何时刻,我们可以使用emit函数发出我们想要的任何命名事件。 12myEmitter.emit('something-happened'); 发出事件是发生某种情况的信号。这种情况通常是关于发光物体的状态变化。 我们可以使用on方法添加监听器函数,并且这些监听器函数将在每次发射器对象发出关联名称事件时执行。 事件!==异步我们来看一个例子: 123456789101112131415161718const EventEmitter = require('events');class WithLog extends EventEmitter { execute(taskFunc) { console.log('Before executing'); this.emit('begin'); taskFunc(); this.emit('end'); console.log('After executing'); }}const withLog = new WithLog();withLog.on('begin', () => console.log('About to execute'));withLog.on('end', () => console.log('Done with execute'));withLog.execute(() => console.log('*** Executing task ***')); Class WithLog 是事件发射器。它定义一个实例函数execute。此执行函数接收一个参数,任务函数,并使用log语句包装其执行。它在执行之前和之后触发事件。 要查看这里会发生什么的顺序,我们在两个命名事件上注册侦听器,最后执行一个示例任务来触发事件。 这是以下的输出: 12345Before executingAbout to execute*** Executing task ***Done with executeAfter executing 我想让你注意到上面的输出是一切都是同步发生的。这段代码没有异步。我们先得到“执行前”行。begin命名事件导致“关于执行”行。实际执行行然后输出“执行任务”行。结束命名事件然后导致“完成与执行”行我们得到最后执行“行”。就像普通的回调一样,不要以为事件意味着同步或异步代码。这很重要,因为如果我们传递异步taskFunc来执行,那么发出的事件将不再准确。","categories":[{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/categories/Node-js/"}],"tags":[{"name":"Nodejs","slug":"Nodejs","permalink":"http://coderpreacher.top/tags/Nodejs/"},{"name":"Node.js Event-Driven","slug":"Node-js-Event-Driven","permalink":"http://coderpreacher.top/tags/Node-js-Event-Driven/"},{"name":"Event-Driven Architecture","slug":"Event-Driven-Architecture","permalink":"http://coderpreacher.top/tags/Event-Driven-Architecture/"},{"name":"Node.js Architecture","slug":"Node-js-Architecture","permalink":"http://coderpreacher.top/tags/Node-js-Architecture/"}]},{"title":"可扩展的Node.js应用","slug":"可扩展的Node-js应用","date":"2017-09-10T02:14:58.000Z","updated":"2017-09-10T02:25:12.000Z","comments":true,"path":"2017/09/10/可扩展的Node-js应用/","link":"","permalink":"http://coderpreacher.top/2017/09/10/可扩展的Node-js应用/","excerpt":"","text":"介绍Node.js中的可扩展性不是事后的想法。这是一个被烘焙成运行时核心的东西。 Node被命名为Node,以强调Node应用程序应该包含相互通信的多个小型分布式节点的想法。 您是否为Node应用程序运行多个节点?您是否在生产机器的每个CPU核心上运行一个Node进程,并负载平衡其中的所有请求?你知道Node有一个内置的模块来帮助吗? 节点的集群模块不仅提供了一个开箱即用的解决方案来利用机器的全部CPU功能,而且还有助于提高节点进程的可用性,并提供一个可以在零停机时重新启动整个应用程序的选项。本文涵盖了所有的善良和更多。 可扩展性策略工作量是我们扩展应用程序最受欢迎的原因,但并不是唯一的原因。我们还扩展了应用程序,以增加其可用性和容错性。 扩展应用程序主要有三种不同的事情: 1 - 克隆扩展大型应用程序最简单的方法是克隆多次,并将每个克隆的实例处理部分工作负载(例如使用负载平衡器)。这在开发时间方面并不花费很多,而且非常有效。此策略是您应该做的最小的工作,Node.js具有内置模块,集群,使您更容易在单个服务器上实施克隆策略。 2 - 分解我们还可以根据功能和服务进行分解来扩展应用程序。这意味着具有多个不同的应用程序,具有不同的代码库,有时具有自己的专用数据库和用户界面。这个策略通常与Microservice这个术语有关,其中micro表示这些服务应该尽可能的小,但实际上服务的大小不是什么重要的,而是执行松耦合和服务之间的高度凝聚力。这个战略的实施往往并不容易,可能会导致长期的意想不到的问题,但是当做好的时候,优势是巨大的。 3 - 分割我们还可以将应用程序拆分成多个实例,每个实例只负责应用程序数据的一部分。这种策略通常被称为数据库中的水平分区或分片。数据分区需要在每个操作之前查找步骤,以确定要使用的应用程序的哪个实例。例如,也许我们希望根据他们的国家/地区或语言对用户进行分区。我们首先需要查找该信息。成功扩大应用程序应该最终实现所有三种策略。 Node.js可以轻松实现,但我将重点介绍本文中的克隆策略,并探讨Node.js中可用的内置工具来实现。请注意,在阅读本文之前,您需要对Node.js子进程了解很多。如果你还没有,我建议你先阅读另一篇文章:","categories":[{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/categories/Node-js/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"http://coderpreacher.top/tags/JavaScript/"},{"name":"Nodejs","slug":"Nodejs","permalink":"http://coderpreacher.top/tags/Nodejs/"},{"name":"Node.js Scaling","slug":"Node-js-Scaling","permalink":"http://coderpreacher.top/tags/Node-js-Scaling/"},{"name":"Distributed Systems","slug":"Distributed-Systems","permalink":"http://coderpreacher.top/tags/Distributed-Systems/"}]},{"title":"你必须知道的Node.js Stream","slug":"你必须知道的Node-js-Stream","date":"2017-09-09T14:11:50.000Z","updated":"2017-09-10T01:49:48.000Z","comments":true,"path":"2017/09/09/你必须知道的Node-js-Stream/","link":"","permalink":"http://coderpreacher.top/2017/09/09/你必须知道的Node-js-Stream/","excerpt":"","text":"介绍Node.js Streams具有很强大的优势:您可以使用异步的方式处理输入和输出,您可以独立的步骤转换数据。在本教程中,我将引导您完成理论,并教您如何使用对象流变换,就像Gulp一样。 什么是流?流是数据集合,就像数组或字符串一样。 不同之处在于流可能并非全部可用,并且它们不必适应内存。 这使得当处理大量数据时,流非常强大,或者一次来自外部源的数据。然而,流不仅仅是关于使用大数据。 他们也给我们在我们的代码中组合的力量。 就像我们可以通过管道其他较小的Linux命令组成强大的linux命令一样,我们可以在Node中使用流完全一样。 123const grep = ... // A stream for the grep outputconst wc = ... // A stream for the wc inputgrep.pipe(wc) Node中的许多内置模块实现流式接口: 上面的列表有一些本机Node.js对象的示例,这些对象也是可读写的流。 这些对象中的一些是可读写的流,如TCP套接字,zlib和crypto流。注意对象也是密切相关的。 虽然HTTP响应是客户端上的可读流,但它是服务器上的可写入流。 这是因为在HTTP情况下,我们基本上从一个对象(http.IncomingMessage)读取并写入另一个对象(http.ServerResponse)。还要注意,当涉及到子进程时,stdio stream(stdin,stdout,stderr)如何具有逆流类型。 这允许一种非常简单的方式来管理来自主进程stdio流的这些流。 您的第一流应用程序我们来仔细看看流。 为此,我们将构建一个简单的文件上传应用程序。 首先,我们需要构建一个使用可读流读取文件并将数据管理到特定目标的客户端。 在管道的另一端,我们将实现一个使用可写入流保存上传数据的服务器。 我们从客户端开始吧。 我们从导入HTTP和文件系统模块开始。\u0010 客户端12var http = require('http') , fs = require('fs'); 然后,我们定义我们的HTTP请求。 123456789var options = { host: 'localhost' , port: 8000 , path: '/' , method: 'POST'};var req = http.request(options, function(res) { console.log(res.statusCode);}); 现在我们有了我们的请求,我们创建一个读取文件并将内容管理到请求对象的可读流。 12var readStream = fs.ReadStream(__dirname + &quot;/in.txt&quot;);readStream.pipe(req); 一旦流完成读取所有数据,我们关闭与服务器的连接,调用我们的请求的end()方法。 1234readStream.on('close', function () { req.end(); console.log(&quot;I finished.&quot;);}); 服务端就像我们为客户端所做的那样,我们从导入Node.js模块开始。然后,我们创建一个新的可写流,将数据保存到文本文件中。 1234var http = require('http') , fs = require('fs');var writeStream = fs.createWriteStream(__dirname + &quot;/out.txt&quot;); 为了让我们的客户端应用上传文件,我们必须创建一个新的Web服务器对象。当数据来自请求对象时,服务器调用我们的流并将缓冲区刷新到输出文件。 1234567891011var server = http.createServer(function (req, res) { req.on('data', function (data) { writeStream.write(data); }); req.on('end', function() { writeStream.end(); res.statusCode = 200; res.end(&quot;OK&quot;); });});server.listen(8000); 请注意,createServer()返回的req和res对象分别是可读流和可写入流。我们可以监听数据事件,并且一旦处理结束,就将结果回送给客户端。 Node.js中的StreamNode.js中有四种基本流类型:Readable(可读),Writable(可写),Duplex(双工)和Transform(转换)。 Readable可读流是可以从其中消耗数据的源的抽象。一个例子是fs.createReadStream方法。 Writable可写流是可以写入数据的目的地的抽象。一个例子是fs.createWriteStream方法。 Duplex双工流既可读又可写。一个例子是TCP套接字。 Transform变换流基本上是一个双工流,可用于在写入和读取数据时修改或转换数据。一个例子是使用gzip压缩数据的zlib.createGzip流。您可以将变换流视为函数,其中输入是可写入流部分,输出是可读流部分。您也可能会听到称为“通过流”的转换流。 所有流都是EventEmitter的实例。它们发出可用于读取和写入数据的事件。但是,我们可以使用管道方法以更简单的方式使用流数据。 pipe(管道) 方法这是您需要记住的魔术线: 1readableSrc.pipe(writableDest) 在这个简单的一行中,我们将可读流的输出(数据源)作为可写流的输入 - 目标管道。源必须是可读流,目的地必须是可写的。当然,它们也可以是双工/转换流。实际上,如果我们正在进行双工流,我们可以像在Linux中一样链接管道呼叫: 1234readableSrc .pipe(transformStream1) .pipe(transformStream2) .pipe(finalWrtitableDest) pipe方法返回目标流,这使我们能够在上面进行链接。对于流(可读),b和c(双工)和d(可写),我们可以: 12345678910a.pipe(b).pipe(c).pipe(d)# Which is equivalent to:a.pipe(b)b.pipe(c)c.pipe(d)# Which, in Linux, is equivalent to:$ a | b | c | d pipe方法是消耗流的最简单方法。通常建议使用管道方法或使用事件消耗流,但不要混合这两个。通常当您使用管道方法时,您不需要使用事件,但是如果您需要以更自定义的方式使用流,那么事件就是要走的路。 流事件(Stream events)除了从可读流源读取和写入可写目的地之外,管道方法还会自动管理一些事情。例如,它处理错误,文件结尾以及一个流比另一个流更慢或更快的情况。 但是,流也可以直接与事件一起使用。以下是管道方法主要用于读取和写入数据的简化事件等效代码: 12345678# readable.pipe(writable)readable.on('data', (chunk) => { writable.write(chunk);});readable.on('end', () => { writable.end();}); 以下是可读写可用流的重要事件和功能的列表: 事件和功能在某种程度上是相关的,因为它们通常被一起使用。 Readable可读流最重要的事件是: data事件,当流将一大块数据传递给消费者时,该事件被发出。 end事件,当没有更多数据要从流中消耗时发出。 Writable可写的流中最重要的事件是: drain事件是可写入流可以接收更多数据的信号。 finish事件,当所有数据已刷新到底层系统时都会发出。 事件和功能可以组合起来,以便定制和优化流的使用。要使用可读流,我们可以使用pipe/unpipe方法,或者read/unshift/resume方法。要使用可写入流,我们可以使用pipe/unpipe方法,或者只是使用write方法写入它,并在完成后调用end方法。 可读流的暂停和流动模式可读流具有两种主要模式,影响我们消费的方式: 它们可以处于暂停模式 或者在流动模式 这些模式有时被称为拉和推模式。 默认情况下,所有可读流可在暂停模式下启动,但在需要时可以轻松切换到流动状态并返回到暂停状态。有时会自动切换。 当可读流处于暂停模式时,我们可以使用read方法从流中读取流,但是,对于流模式中的可读流,数据持续流动,我们必须收听事件消耗它 在流动模式下,如果没有消费者可以处理它,数据实际上可能会丢失。这就是为什么当我们在流模式下有可读流时,我们需要一个数据事件处理程序。事实上,只需添加一个数据事件处理程序即可将暂停的流转换为流模式,并删除数据事件处理程序将流切换回暂停模式。其中一些是为了与旧的Node Stream接口的向后兼容而完成的。 要手动切换这两种流模式,可以使用resume和pause方法。 当使用管道方法消耗可读流时,我们不必担心管道自动管理这些模式。 流实现当谈到Node.js中的流时,主要有两个不同的任务: 实现流的任务。 消费它们的任务。 到目前为止,我们一直在谈论只消耗流。让我们实现一些! 实现可写流(Writable)要实现可写流,我们需要使用Stream模块中的Writable构造函数。 1const { Writable } = require('stream'); 我们可以在许多方面实现一个可写的流。例如,我们可以扩展Writable构造函数 12class myWritableStream extends Writable {} 但是,我更喜欢更简单的构造方法。我们只是从Writable构造函数创建一个对象,并传递一些选项。唯一需要的选项是write函数,它暴露要写入的数据块。 12345678910const { Writable } = require('stream');const outStream = new Writable({ write(chunk, encoding, callback) { console.log(chunk.toString()); callback(); }});process.stdin.pipe(outStream); 这个写入方法有三个参数。 除非我们以不同的方式配置流,否则chunk(块)通常是缓冲区。 在这种情况下需要encoding(编码参数),但通常我们可以忽略它。 callback回调函数是处理数据块后我们需要调用的函数。这是写信是否成功的信号。要发出失败信号,请使用错误对象调用回调。 在outStream中,我们只是将该块作为一个字符串进行console.log,然后在没有错误的情况下调用回调来表示成功。这是一个非常简单的,可能不是很有用的回声流。它将回传任何收到的东西。 要消耗这个流,我们可以简单地使用process.stdin,这是一个可读的流,所以我们可以将process.stdin管道传入我们的outstream 当我们运行上面的代码时,我们输入到process.stdin的任何东西都将使用outStream console.log行回显。 这不是一个非常有用的实现流,因为它实际上已经被实现和内置。这非常等同于process.stdout。我们可以将stdin管道输入stdout,我们可以用这条单行得到完全相同的回声功能: 1process.stdin.pipe(process.stdout); 实现可读流(Readable)要实现可读流,我们需要可读接口并从中构造一个对象: 12const { Readable } = require('stream');const inStream = new Readable({}); 有一种实现可读流的简单方法。我们可以直接push(推送)我们希望消费者消费的数据。 123456const { Readable } = require('stream'); const inStream = new Readable();inStream.push('ABCDEFGHIJKLM');inStream.push('NOPQRSTUVWXYZ');inStream.push(null); // No more datainStream.pipe(process.stdout); 当我们push一个空对象时,这意味着我们想要表示流没有任何更多的数据。 为了消耗这个简单的可读流,我们可以简单地将它导入到可写入流process.stdout中。 当我们运行上面的代码时,我们将从inStream读取所有数据,并将其回显到标准。很简单,也不是很有效率。 我们基本上推送流中的所有数据,然后将其管理到process.stdout。更好的方法是在消费者要求时按需推送数据。我们可以通过在可读流配置中实现read方法来实现: 12345const inStream = new Readable({ read(size) { // there is a demand on the data... Someone wants to read it. }}); 当读取方法在可读流上被调用时,该实现可以将部分数据推送到队列。例如,我们可以一次推一个字母,从字符代码65(表示A)开始,并在每次推送时递增: 12345678910const inStream = new Readable({ read(size) { this.push(String.fromCharCode(this.currentCharCode++)); if (this.currentCharCode > 90) { this.push(null); } }});inStream.currentCharCode = 65;inStream.pipe(process.stdout); 当消费者读取可读流时,读取方法将继续触发,我们会推送更多的信件。我们需要在某个地方停止这个循环,这就是为什么当currentCharCode大于90(表示Z)时,if语句推空。 这个代码相当于我们开始使用的更简单的代码,但是现在我们正在按消费者要求的数据推送数据。你应该永远这样做. 实现双工(Duplex)/转换流(Transfer)使用双工流,我们可以使用相同的对象来实现可读和可写的流。就好像我们从两个接口继承。 这是一个实例双工流,它结合了上面实现的两个可写和可读的例子: 12345678910111213141516171819const { Duplex } = require('stream');const inoutStream = new Duplex({ write(chunk, encoding, callback) { console.log(chunk.toString()); callback(); }, read(size) { this.push(String.fromCharCode(this.currentCharCode++)); if (this.currentCharCode > 90) { this.push(null); } }});inoutStream.currentCharCode = 65;process.stdin.pipe(inoutStream).pipe(process.stdout); 通过组合这些方法,我们可以使用这个双工流来读取从A到Z的字母,我们也可以用它的回波特征。我们将可读的stdin流管道到这个双工流中以使用回声功能,我们将双工流本身管道写入可写入stdout流,以查看字母A到Z。 重要的是要了解双工流的可读写双方完全独立运行。这只是将两个特征组合成一个对象。 转换流是更有趣的双工流,因为它的输出是从其输入计算的。 对于一个转换流,我们不需要实现读取或者写入方法,我们只需要实现一个转换方法,它将两者结合起来。它具有写入方式的签名,我们也可以使用它来推送数据。 这是一个简单的转换流,它将您将其转换为大写格式后,再次输入任何内容: 12345678910const { Transform } = require('stream');const upperCaseTr = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()); callback(); }});process.stdin.pipe(upperCaseTr).pipe(process.stdout); 在这个转换流中,我们正在消费与之前的双工流示例一样,我们只实现了一个transform方法。在该方法中,我们将块转换为大写版本,然后将该版本推送为可读部分。 流对象模式(Stream Object Mode)默认情况下,流期望缓冲区(Buffer)/字符串(String)值。有一个objectMode标志,我们可以设置为让流接受任何JavaScript对象。 这是一个简单的例子来证明这一点。以下变换流的组合使得将逗号分隔值的字符串映射为JavaScript对象的功能。所以“a,b,c,d”成为{a:b,c:d}。 1234567891011121314151617181920212223242526272829303132const { Transform } = require('stream');const commaSplitter = new Transform({ readableObjectMode: true, transform(chunk, encoding, callback) { this.push(chunk.toString().trim().split(',')); callback(); }});const arrayToObject = new Transform({ readableObjectMode: true, writableObjectMode: true, transform(chunk, encoding, callback) { const obj = {}; for(let i=0; i < chunk.length; i+=2) { obj[chunk[i]] = chunk[i+1]; } this.push(obj); callback(); }});const objectToString = new Transform({ writableObjectMode: true, transform(chunk, encoding, callback) { this.push(JSON.stringify(chunk) + '\\n'); callback(); }});process.stdin .pipe(commaSplitter) .pipe(arrayToObject) .pipe(objectToString) .pipe(process.stdout) 我们通过commasplitter传递输入字符串(例如“a,b,c,d”),该数组将数组作为其可读取数据([“a”,“b”,“c”,“d”]))。在该流上添加可读ObjectMode标志是必要的,因为我们正在将对象推送到其上,而不是字符串。 然后我们将数组并且将其导入到arrayToObject流中。我们需要一个writableObjectMode标志来使该流接受一个对象。它还将推送一个对象(输入数组映射到一个对象),这就是为什么我们也需要可读ObjectMode标志的原因。最后一个objectToString流接受一个对象,但是推出一个字符串,这就是为什么我们只需要一个writableObjectMode标志的原因。可读部分是普通字符串(字符串对象)。 Node.js内置的转换流Node有一些非常有用的内置变换流。即,zlib和crypto stream。 这是一个使用zlib.createGzip()流结合fs readable/writable 流创建文件压缩脚本的示例: 12345678const fs = require('fs');const zlib = require('zlib');const file = process.argv[2];fs.createReadStream(file) .pipe(zlib.createGzip()) .pipe(fs.createWriteStream(file + '.gz')); 您可以使用此脚本将您传递的任何文件gzip作为参数。我们将该文件的可读流管道转换为zlib内置的转换流,然后转换为新的gzip-压缩文件的可写流。简单。 使用管道的很酷的事情是,如果我们需要,我们可以将它们与事件结合起来。例如,我希望用户在脚本运行时看到一个进度指示符,当脚本完成时,我想要一个“完成”消息。由于管道方法返回目标流,因此我们可以链接注册事件处理程序: 123456789const fs = require('fs');const zlib = require('zlib');const file = process.argv[2];fs.createReadStream(file) .pipe(zlib.createGzip()) .on('data', () => process.stdout.write('.')) .pipe(fs.createWriteStream(file + '.zz')) .on('finish', () => console.log('Done')); 所以使用管道方法,我们可以轻松地消耗流,但是我们还可以使用需要的事件进一步定制与这些流的交互。 管道方法有什么好处,但是我们可以使用它以一种可读的方式逐个构成我们的程序。例如,我们可以简单地创建一个变换流来报告进度,而不用监听上面的数据事件,并用另一个.pipe()调用替换.on()调用: 123456789101112131415161718const fs = require('fs');const zlib = require('zlib');const file = process.argv[2];const { Transform } = require('stream');const reportProgress = new Transform({ transform(chunk, encoding, callback) { process.stdout.write('.'); callback(null, chunk); }});fs.createReadStream(file) .pipe(zlib.createGzip()) .pipe(reportProgress) .pipe(fs.createWriteStream(file + '.zz')) .on('finish', () => console.log('Done')); 该reportProgress流是一个简单的直通流,但它将进度标准化。注意我如何使用callback()函数中的第二个参数来推送transform()方法中的数据。这相当于推动数据。 结合流的应用是无止境的。例如,如果我们需要在我们gzip之前或之后加密文件,我们需要做的就是按照我们需要的确切顺序来管理另一个转换流。我们可以使用Node的加密模块: 12345678const crypto = require('crypto');// ...fs.createReadStream(file) .pipe(zlib.createGzip()) .pipe(crypto.createCipher('aes192', 'a_secret')) .pipe(reportProgress) .pipe(fs.createWriteStream(file + '.zz')) .on('finish', () => console.log('Done')); 上面的脚本压缩然后加密传递的文件,只有具有秘密的人可以使用输出的文件。我们无法使用正常的解压缩实用程序解压缩此文件,因为它已加密。 为了能够解压缩上面脚本压缩的任何东西,我们需要以相反的顺序使用相反的加密和zlib流,这很简单: 123456fs.createReadStream(file) .pipe(crypto.createDecipher('aes192', 'a_secret')) .pipe(zlib.createGunzip()) .pipe(reportProgress) .pipe(fs.createWriteStream(file.slice(0, -3))) .on('finish', () => console.log('Done')); 假设传递的文件是压缩版本,上面的代码将创建一个从它的读取流,将其传输到crypto createDecipher()流(使用相同的密钥),将其输出管道输入到zlib createGunzip()流中,然后将文件写回到没有扩展部分的文件中。 小结了解了这些 Stream 的内部机制,对我们后续深入理解上层代码有很大的促进作用,特别希望初学 Node.js 的同学花点时间进来看看。","categories":[{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/categories/Node-js/"}],"tags":[{"name":"Stream","slug":"Stream","permalink":"http://coderpreacher.top/tags/Stream/"},{"name":"Node.js Stream","slug":"Node-js-Stream","permalink":"http://coderpreacher.top/tags/Node-js-Stream/"},{"name":"Node","slug":"Node","permalink":"http://coderpreacher.top/tags/Node/"}]},{"title":"了解Node.js事件循环","slug":"了解Node-js事件循环","date":"2017-07-31T13:13:18.000Z","updated":"2017-07-31T14:12:27.000Z","comments":true,"path":"2017/07/31/了解Node-js事件循环/","link":"","permalink":"http://coderpreacher.top/2017/07/31/了解Node-js事件循环/","excerpt":"","text":"Node的“事件循环”是能够处理高吞吐量场景的核心。 它是一个充满独角兽和彩虹的神奇的地方,是Node本质上可以“单线程”的原因,同时还允许在后台处理任意数量的操作。 这个帖子将揭示事件循环如何运行,这样你也可以享受魔术。 事件驱动编程为了了解”事件循环”所需要的第一件事当然是理解事件驱动编程。事件驱动编程主要用于UI应用程序。 JavaScript的主要用法是与DOM进行交互,因此使用基于事件的API是自然的。 定义简单:事件驱动编程是由事件或状态变化决定的应用程序流控制。 一般的实现是具有侦听事件的中心机制,并且一旦检测到事件(即状态已经改变)就调用回调函数。 听起来很熟悉 这应该就是Node的事件循环的基本原理。 对于熟悉客户端JavaScript开发的人员,请考虑与DOM Elements结合使用的所有.on *()方法,如element.onclick(),以传达用户交互。 当单个项目可以发出许多可能的事件时,此模式运行良好。 Node以EventEmitter的形式使用此模式,并且位于诸如Server,Socket和“http”模块之类的位置。 当我们需要从单个实例发出多种类型的状态更改时,这很有用。 另一种常见的模式是成功或失败。 今天有两个常见的实现。 首先是“回退错误”回调风格,其中调用的错误是传递给回调的第一个参数。 ES6已经出现了第二个,使用Promises。 ‘fs’模块主要使用回退回调风格。 在技术上可能会为某些调用发出额外的事件,例如fs.readFile(),但是API只是为了提醒用户,如果所需的操作成功或者某些失败。 此API选择是一种体系结构决策,而不是由于技术限制。 一个常见的误解是,事件发射器本质上是异步的,但这是不正确的。 以下是一个简单的代码片段来演示这一点。 123456789101112131415161718192021function MyEmitter() { EventEmitter.call(this);}util.inherits(MyEmitter, EventEmitter);MyEmitter.prototype.doStuff = function doStuff() { console.log('before') emitter.emit('fire') console.log('after')}};var me = new MyEmitter();me.on('fire', function() { console.log('emit fired');});me.doStuff();// Output:// before// emit fired// after EventEmitter经常出现异步,因为它经常用于表示异步操作的完成,但EventEmitter API完全是同步的。 发射功能可以异步调用,但请注意,所有侦听器函数将按照添加的顺序同步执行,之后任何执行可以在调用发出后的语句中继续执行。 机制概述Node本身取决于多个库。 其中之一是libuv,它是处理异步事件排队和处理的魔法库。 对于这篇文章的其余部分,请记住,我不会区分一个点是否直接与Node或libuv相关。 Node尽可能利用操作系统的内核可用的内容。 因此,将制作写请求,保持连接等更多的责任由系统委托并处理。 例如,传入连接由系统排队,直到它们可以被Node处理。 您可能已经听说Node有一个线程池,可能会想知道“如果Node将所有这些责任推下来,为什么需要一个线程池? 这是因为内核不支持异步处理。 在这些情况下,Node必须在操作期间锁定线程,以便可以继续执行事件循环而不阻止。 这是一个简化的图表,用于解释何时运行的机制概览: 关于事件循环的内部工作的几个重要注意事项,将难以包含在图中: 通过process.nextTick()调度的所有回调都在转换到下一阶段之前在事件循环阶段(例如定时器)结束时运行。 这会产生潜在的无意中使用循环调用process.nextTick()的事件循环。 “Pending callbacks”是回调排队运行,不会被任何其他阶段处理(例如,传回给fs.write()的回调)。 事件发射器(Event Emitter)和事件循环(Event Loop)为了简化与事件循环的交互,创建了EventEmitter。 它是一个通用的包装器,可以更容易地创建基于事件的API。 由于这两种互动之间的一些困惑,我们现在将解决常常会让开发者陷入困境的问题。 以下示例显示如何忘记发生事件同步发生可能会导致用户错过事件。1234567891011121314151617// Post v0.10, require('events').EventEmitter is not necessary.var EventEmitter = require('events');var util = require('util');function MyThing() { EventEmitter.call(this); doFirstThing(); this.emit('thing1');}util.inherits(MyThing, EventEmitter);var mt = new MyThing();mt.on('thing1', function onThing1() { // Sorry, never going to happen.}); 上述的缺点是,’thing1’永远不能被用户捕获,因为MyThing()必须在侦听任何事件之前完成实例化。 这是一个简单的解决方案,也不需要任何额外的关闭: 1234567891011121314151617181920var EventEmitter = require('events');var util = require('util');function MyThing() { EventEmitter.call(this); doFirstThing(); setImmediate(emitThing1, this);}util.inherits(MyThing, EventEmitter);function emitThing1(self) { self.emit('thing1');}var mt = new MyThing();mt.on('thing1', function onThing1() { // Whoot!}); 以下也可以工作,但是性能成本很高: 12345678function MyThing() { EventEmitter.call(this); doFirstThing(); // Using Function#bind() makes the world much slower. setImmediate(this.emit.bind(this, 'thing1'));}util.inherits(MyThing, EventEmitter); 另一个问题是发生错误。 解决您的应用程序的问题可能很难,但丢失调用堆栈可能会使它不可能。 当错误在异步请求的远端被实例化时,调用堆栈丢失。 解决这个问题的两个最合理的解决方案是同步发射或确保其他重要信息与错误一起传播。 以下示例显示正在使用的每个: 1234567891011121314151617MyThing.prototype.foo = function foo() { // This error will be emitted asynchronously. var er = doFirstThing(); if (er) { // The error needs to be created immediately to preserve // the call stack. setImmediate(emitError, this, new Error('Bad stuff')); return; } // Emit the error immediately so it can be handled. var er = doSecondThing(); if (er) { this.emit('error', 'More bad stuff'); return; }} 考虑这个情况 在应用程序执行之前,可能会立即处理正在发出的错误。 或者它可能是一个微不足道的问题,需要报告,可以很容易地处理。 此外,由于对象实例的构造可能非常不完整,因此有一个发出错误的构造函数不是一个好主意。 在这种情况下抛出异常。 -","categories":[{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/categories/Node-js/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"http://coderpreacher.top/tags/JavaScript/"},{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/tags/Node-js/"},{"name":"事件循环","slug":"事件循环","permalink":"http://coderpreacher.top/tags/事件循环/"},{"name":"Event Loop","slug":"Event-Loop","permalink":"http://coderpreacher.top/tags/Event-Loop/"}]},{"title":"IdentityServer介绍","slug":"IdentityServer介绍","date":"2017-05-15T13:59:23.000Z","updated":"2017-05-15T15:24:37.000Z","comments":true,"path":"2017/05/15/IdentityServer介绍/","link":"","permalink":"http://coderpreacher.top/2017/05/15/IdentityServer介绍/","excerpt":"","text":"IdentityServer是什么?IdentityServer是一个基于.NET / Katana的框架和可托管组件,允许使用OpenID Connect和OAuth2等协议实现对现代Web应用程序和API的单点登录和访问控制。 它支持广泛的客户端,如移动,Web,SPA和桌面应用程序,并且是可扩展的,可以集成到新的和现有的体系结构中。 入门(Getting started)大多数现代应用看起来或多或少是这样的: 最常见的互动是: 浏览器与Web应用程序通信 Web应用程序与Web API进行通信(有时是自己的,有时代表用户) 基于浏览器的应用程序与Web API通信 本地应用程序与Web API进行通信 基于服务器的应用程序与Web API通信 Web API与Web API进行通信(有时是自己的,有时代表用户) 通常,每个层(前端,中间层和后端)都必须保护资源并实施身份验证和/或授权(通常针对同一用户存储)。 将这些基本安全功能外包给安全令牌服务可以防止在这些应用程序和端点之间复制该功能。 重组应用程序以支持安全令牌服务导致以下体系结构和协议: 这样的设计将安全问题分为两部分: 认证当应用程序需要知道当前用户的身份时,需要进行身份验证。 通常,这些应用程序代表该用户管理数据,并且需要确保该用户只能访问他被允许的数据。 最常见的例子是(经典)Web应用程序 - 但是基于本机和基于JS的应用程序也需要进行身份验证。 最常见的认证协议是SAML2p,WS-Federation和OpenID Connect - SAML2p是最流行和最广泛部署的。 OpenID Connect是三款中最新的,但被认为是未来,因为它具有现代应用的最大潜力。 它是从一开始就构建用于移动应用场景的,并且被设计为API友好。 API访问应用程序有两种基本的方式与API通信 - 使用应用程序标识或委派用户身份。 有时候两种方法都需要组合。 OAuth2是允许应用程序从安全令牌服务请求访问令牌并使用它们与API进行通信的协议。 该代理可以降低客户端应用程序和API的复杂性,因为可以集中验证和授权。 OpenID Connect和OAuth 2.0 - 一起更好OpenID Connect和OAuth 2.0非常相似 - 实际上OpenID Connect是OAuth 2.0之上的扩展。 两个基本的安全考虑,身份验证和API访问,被组合成一个单一的协议 - 通常与安全令牌服务的单一往返。 我们认为OpenID Connect和OAuth 2.0的组合是在可预见的未来保护现代应用程序的最佳方法。 IdentityServer是这两个协议的实现,并且经过高度优化,可以解决当今移动,本机和Web应用程序的典型安全问题。 术语规范,文档和对象模型使用您应该注意的某些术语。 IdentityServerIdentityServer是一个OpenID Connect提供程序 - 它实现了OpenID Connect和OAuth 2.0协议。 不同的文献对于相同的角色使用不同的术语 - 您可能还会发现安全令牌服务,身份提供者,授权服务器,IP-STS等等。 但总而言之,它们都是一样的:一种向客户发出安全令牌的软件。 IdentityServer有许多工作和功能 - 包括: 保护您的资源 使用本地帐户商店或通过外部身份提供商验证用户 提供会话管理和单点登录 管理和验证客户端 发出身份和访问令牌给客户 验证令牌 用户 (User)用户是使用注册的客户端访问资源的人。 客户端(Client)客户端是从IdentityServer请求令牌的软件,用于验证用户(请求身份令牌)或访问资源(请求访问令牌)。 必须首先向IdentityServer注册客户端才能请求令牌。 客户端的示例是Web应用程序,本地移动或桌面应用程序,SPA,服务器进程等。 资源(Resources)资源是您希望使用IdentityServer保护的 - 用户的身份数据或API。 每个资源都有唯一的名称 - 客户端使用此名称来指定他们想要访问的资源。 身份信息关于用户的身份信息(也称为权利要求),例如。 名称或电子邮件地址。 API资源表示客户端想要调用的功能 - 通常被建模为Web API,但不一定。 身份令牌(Identity Token)身份令牌表示身份验证过程的结果。 它至少包含用户的标识符(称为副主题主题),以及用户如何以及何时进行身份验证的信息。 它可以包含其他身份数据。 访问令牌(Access Token)访问令牌允许访问API资源。 客户端请求访问令牌并将其转发到API。 访问令牌包含有关客户端和用户的信息(如果存在)。 API使用该信息来授权访问其数据。","categories":[{"name":"Authentication","slug":"Authentication","permalink":"http://coderpreacher.top/categories/Authentication/"}],"tags":[{"name":"Authentication","slug":"Authentication","permalink":"http://coderpreacher.top/tags/Authentication/"},{"name":"OpenID Connect and OAuth 2.0","slug":"OpenID-Connect-and-OAuth-2-0","permalink":"http://coderpreacher.top/tags/OpenID-Connect-and-OAuth-2-0/"},{"name":"API Access","slug":"API-Access","permalink":"http://coderpreacher.top/tags/API-Access/"},{"name":"IdentityServer","slug":"IdentityServer","permalink":"http://coderpreacher.top/tags/IdentityServer/"},{"name":"OpenID","slug":"OpenID","permalink":"http://coderpreacher.top/tags/OpenID/"},{"name":"OAuth 2.0","slug":"OAuth-2-0","permalink":"http://coderpreacher.top/tags/OAuth-2-0/"},{"name":"SSO","slug":"SSO","permalink":"http://coderpreacher.top/tags/SSO/"},{"name":"单点登录","slug":"单点登录","permalink":"http://coderpreacher.top/tags/单点登录/"},{"name":"访问控制","slug":"访问控制","permalink":"http://coderpreacher.top/tags/访问控制/"}]},{"title":"ASP.NET Boilerplate(ABP)介绍","slug":"ASP-NET-Boilerplate(ABP)介绍","date":"2017-05-10T15:11:12.000Z","updated":"2017-05-15T13:50:13.000Z","comments":true,"path":"2017/05/10/ASP-NET-Boilerplate(ABP)介绍/","link":"","permalink":"http://coderpreacher.top/2017/05/10/ASP-NET-Boilerplate(ABP)介绍/","excerpt":"","text":"背景我们正在根据不同需求创建不同的应用程序。但是,至少在某种程度上一遍又一遍地实施共同和类似的结构。授权,验证,异常处理,日志记录,本地化,数据库连接管理,设置管理,审核日志记录是其中一些常见的结构。此外,我们正在建立架构结构和最佳实践,如分层和模块化架构,域驱动设计,依赖注入等。并试图根据一些惯例开发应用程序。 由于所有这些都是非常耗时且难以为每个项目建立单独的,许多公司创建私有框架。他们正在使用这些框架更少的错误来更快地开发新的应用程序。当然,并不是所有的公司都很幸运。他们大多数没有时间,预算和团队来发展这样的框架。即使他们有可能创建一个框架,很难记录,培训开发人员并维护它。 ASP.NET Boilerplate(ABP)是一个开放源码和文档良好的应用程序框架,开始了“为所有公司和所有开发人员制定通用框架”的想法。这不仅仅是一个框架,而且还提供了一个基于领驱动设计(DDD)和最佳实践的强大的架构模型。 “DRY——避免重复造轮子”,这也是ASP.NET Boilerplate(ABP)的重要思想,作者之所以把项目命名为“ASP.NET Boilerplate”,就是希望它能成为开发一般企业WEB应用的新起点,直接把ABP作为项目模板,避免做重复的工作,让开发人员只关注具体业务逻辑的实现,这样也大大提高了开发效率。 使用框架当然有代价,你必须受到框架强API的侵入,抑或要使用他的方言。而且这个框架想要吃透,也要付出很大的学习成本。但是好处也是显而易见的。业界顶尖的架构师已经为你搭建好了一套基础架构,很好的回应了关于一个软件系统应该如何设计,如何规划的问题,并且提供了一套最佳实践和范例。 学习虽然要付出成本,但是经过漫长的跋涉,我们从一无所知已经站到了工业级开发的门槛上。基于这个框架,我们可以很好的来划分任务,进行单元测试等。大大降低了软件出现BUG的几率。 什么是ASP.NET Boilerplate(ABP)?ASP.NET Boilerplate是使用最佳实践和最受欢迎工具的新型现代Web应用程序的起点。 它的目标是成为一个坚实的模型,通用应用程序框架和项目模板。 它能做什么? 服务端(Server side) 基于最新的.NET技术 (目前是ASP.NET MVC 5、Web API 2、C# 5.0,在ASP.NET 5正式发布后会升级) 实现领域驱动设计(实体、仓储、领域服务、领域事件、应用服务、数据传输对象,工作单元等等) 实现分层体系结构(领域层,应用层,展现层和基础设施层) 提供了一个基础架构来开发可重用可配置的模块 集成一些最流行的开源框架/库,也许有些是你正在使用的。 提供了一个基础架构让我们很方便地使用依赖注入(使用Castle Windsor作为依赖注入的容器) 提供Repository仓储模式支持不同的ORM(已实现Entity Framework 、NHibernate、MangoDb和内存数据库) 支持并实现数据库迁移(EF 的 Code first) 模块化开发(每个模块有独立的EF DbContext,可单独指定数据库) 包括一个简单的和灵活的多语言/本地化系统 包括一个 EventBus来实现服务器端全局的领域事件 统一的异常处理(应用层几乎不需要处理自己写异常处理代码) 数据有效性验证(Asp.NET MVC只能做到Action方法的参数验证,ABP实现了Application层方法的参数有效性验证) 通过Application Services自动创建Web Api层(不需要写ApiController层了) 提供基类和帮助类让我们方便地实现一些常见的任务 使用“约定优于配置原则” 客户端(Client side) 提供单页面应用程序(使用AngularJs和Durandaljs)和多页面应用程序的项目模板。 模板基于Twitter Bootstrap。 大多数使用的JavaScript库都包含在默认情况下配置的。 创建动态JavaScript代理来轻松调用应用程序服务(使用动态Web API层)。 封装一些Javascript 函数:显示警报和通知,阻止UI,制作AJAX请求… 除了这些共同的基础设施,正在开发一个名为“Zero”的模块。 它将提供基于角色和权限的授权系统(使用最新的ASP.NET身份框架),设置系统,多租户,审计日志等。 ABP不是什么?ASP.NET Boilerplate提供了具有最佳实践的应用程序开发模型。 它具有基础类,接口和工具,使得易于构建可维护的大规模应用程序。 但它不是RAD(快速应用程序开发)工具之一,它们尝试为无需编码的应用程序提供基础架构。 相反,它提供了一个基础设施来编写最佳实践。 它不是代码生成工具。 虽然它具有在运行时构建动态代码的几个功能,但它不生成代码。 这不是一个一体化的框架。 相反,它为特定任务使用了众所周知的工具/库(如用于O / RM的NHibernate和EntityFramework,用于日志记录的Log4Net,作为DI容器的Castle Windsor,SPA框架的AngularJS)。 入门(Getting started)在本文中,我将展示如何使用ASP.NET Boilerplate去除单页面和响应性Web应用程序(我现在称之为ABP)。 我将在这里使用DurandalJs作为SPA框架和NHibernate作为ORM框架。 我准备了另一篇用AngularJs和EntityFramework实现相同应用的文章。 此示例应用程序被命名为“简单任务系统”,它由两个页面组成:一个用于列出任务,另一个是添加新任务。 一个任务可以与一个人相关,可以是活动的或完成的。 应用程序以两种语言进行本地化。 应用程序中任务列表的屏幕截图如下所示: 从模板创建空的web应用程序ABP为新项目提供启动模板(即使您可以手动创建项目并从nuget获取ABP包,模板方式更容易)。 请访问www.aspnetboilerplate.com/Templates以从模板创建应用程序。 您可以选择具有可选AngularJs或DurandalJs的SPA(单页面应用)项目。 或者您可以选择MPA(经典的,多页面应用程序)项目。 那么你可以选择EntityFramework或NHibernate作为ORM框架。 我将我的项目命名为SimpleTaskSystem,并与Durandal和NHibernate建立了一个SPA项目。 下载项目为zip文件。 当我打开zip文件时,我看到一个解决方案已经准备好,包含每个域驱动设计层的程序集(项目): 就这样,你的项目已经准备好运行了! 在VS2013中打开它,然后按F5: 领域层(Domain layer)“负责代表业务概念,有关业务情况的信息和业务规则”(Eric Evans)。 在领域驱动设计(DDD)中,核心层是领域层。 领域层定义您的实体,实现您的业务规则等。 实体(Entities)实体是DDD的核心概念之一。 埃里克·埃文斯(Eric Evans)将其描述为“一个不是由其属性从根本上定义的对象,而是一个连贯性和身份的线索”。 因此,实体具有Id并存储在数据库中。 任务(Task)实体类定义如下:12345678910111213141516public class Task : Entity<long>{ public virtual Person AssignedPerson { get; set; } public virtual string Description { get; set; } public virtual DateTime CreationTime { get; set; } public virtual TaskState State { get; set; } public Task() { CreationTime = DateTime.Now; State = TaskState.Active; }} 它是从具有长主键类型的实体基类派生的简单类。 TaskState是一个具有“Active”和“Completed”的枚举。 人(Person)实体类定义如下:1234public class Person : Entity{ public virtual string Name { get; set; }} 任务与一个人有关系,这就是这个简单的应用。 实体在ABP中必须实现IEntity 接口。 因此,如果主键的类型是Long,则必须实现IEntity 。 如果您的Entity的主键是int,您无需定义主键类型并直接实现IEntity接口。 实际上,您可以轻松地从Entity或Entity 派生(如上图所示)(Task和Person)。 IEntity定义了Entity的Id属性。 仓储(Repositories)“使用类收集接口访问领域对象,在领域和数据映射层之间进行中介”(Martin Fowler)。 实际上,仓储用于对域对象(实体或值类型)执行数据库操作。 通常,每个实体(或聚合根)使用分隔的仓储。 ASP.NET Boilerplate为每个实体提供默认仓储(我们将看到如何使用默认仓储)。 如果我们需要定义其他方法,我们可以扩展IRepository接口。 我将其扩展到了Task库:1234public interface ITaskRepository : IRepository<Task, long>{ List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);} 为每个Repository定义一个接口是很好的。 因此,我们可以将接口与实现分开。 仓储接口定义了仓储的常见方法: 它定义了基本的CRUD方法。 所以,所有仓储都会自动实现所有这些方法。 除了标准的基础方法之外,还可以添加特定于该仓储的方法,就像我定义了GetAllWithPeople方法一样。 基础设施层(Infrastructure layer)“提供支持更高层次的通用技术功能”(Eric Evans)。 它用于使用第三方库和框架(如对象关系映射)实现应用程序的抽象。 在这个应用程序中,我将使用基础设施层: 使用FluentMigrator创建数据库迁移系统。 实现存储库并使用NHibernate和FluentNHibernate映射实体。 数据库迁移(Database Migrations)“进化数据库设计:在过去几年中,我们开发了许多技术,允许数据库设计随着应用程序的发展而发展,这对于敏捷方法来说是非常重要的。” Martin Fowler在他的网站中说。 数据库迁移是支持这一想法的重要技术。 在没有这种技术的情况下,很难在多个生产环境中维护应用程序的数据库。 即使你只有一个在线系统,这是至关重要的。 FluentMigrator 是数据库迁移的好工具。 它支持大多数常见的数据库系统。 在这里,我的迁移代码为Person和Task表。123456789101112131415161718192021222324252627282930[Migration(2014041001)]public class _01_CreatePersonTable : AutoReversingMigration{ public override void Up() { Create.Table(\"StsPeople\") .WithColumn(\"Id\").AsInt32().Identity().PrimaryKey().NotNullable() .WithColumn(\"Name\").AsString(32).NotNullable(); Insert.IntoTable(\"StsPeople\") .Row(new { Name = \"Douglas Adams\" }) .Row(new { Name = \"Isaac Asimov\" }) .Row(new { Name = \"George Orwell\" }) .Row(new { Name = \"Thomas More\" }); }}[Migration(2014041002)]public class _02_CreateTasksTable : AutoReversingMigration{ public override void Up() { Create.Table(\"StsTasks\") .WithColumn(\"Id\").AsInt64().Identity().PrimaryKey().NotNullable() .WithColumn(\"AssignedPersonId\").AsInt32().ForeignKey(\"TsPeople\", \"Id\").Nullable() .WithColumn(\"Description\").AsString(256).NotNullable() .WithColumn(\"State\").AsByte().NotNullable().WithDefaultValue(1) //1: TaskState.New .WithColumn(\"CreationTime\").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); }} 在FluentMigrator中,迁移在从Migration派生的类中定义。 如果您的迁移可以自动回滚,AutoReversingMigration是一个快捷方式。 迁移类应该具有MigrationAttribute。 它定义了迁移类的版本号。 所有迁移都按此版本号排序。 它可以是任何长的数字。 我使用一个标识迁移类创建日期的数字加上同一天的增量值(例如:2014年4月24日第二个迁移类,版本为“2014042402”)。 这完全取决于你 只有重要的是他们的相对秩序。 FluentMigrator将最新的应用版本号存储在数据库的表中。 因此,它仅适用于那些大于数据库版本的迁移。 默认情况下,它使用’VersionInfo’表。 如果要更改表名,可以创建一个类:1234567891011[VersionTableMetaData]public class VersionTable : DefaultVersionTableMetaData{ public override string TableName { get { return \"StsVersionInfo\"; } }} 如你所见,我为所有表写了一个前缀Sts(简单任务系统)。 这对于模块化应用程序很重要,所以所有模块都可以使用其特定的前缀来标识模块特定的表。 要在数据库中创建表,我使用这种“命令行”命令使用FluentMigrator的Migrate.exe工具:1Migrate.exe /connection "Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" /db sqlserver /target "SimpleTaskSystem.Infrastructure.NHibernate.dll" 对于快捷方式,ABP模板包含RunMigrations.bat文件。 在Debug模式下编译项目后,我运行“RunMigrations.bat”: 正如你看到的,两种迁移文件执行,表创建: 有关FluentMigrator的更多信息,请参阅它的网站。 实体映射(Entity mappings)为了获取/存储实体到数据库中,我们应该使用数据库表映射实体。 NHibernate有几个选择可以实现。 在这里,我将使用Fluent Mapping手册(您可以使用传统的自动映射,请参阅FluentNHibernate的网站):1234567891011121314151617181920public class PersonMap : EntityMap<Person>{ public PersonMap() : base("StsPeople") { Map(x => x.Name); }}public class TaskMap : EntityMap<Task, long>{ public TaskMap() : base("StsTasks") { Map(x => x.Description); Map(x => x.CreationTime); Map(x => x.State).CustomType<TaskState>(); References(x => x.AssignedPerson).Column("AssignedPersonId").LazyLoad(); }} EntityMap是一类ABP的自动映射Id属性,并在构造函数中获取表名。所以,我从它派生和映射等性能。 仓储实现我定义的接口为域中层任务储存库(ITaskRepository)。在这里,我将在这里NHibernate的实现它:123456789101112131415161718192021222324252627public class TaskRepository : NhRepositoryBase<Task, long>, ITaskRepository{ public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { //In repository methods, we do not deal with create/dispose DB connections (Session) and transactions. ABP handles it. var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it. //var query = Session.Query<Task>(); //Alternatively, we can directly use NHibernate's Session //Add some Where conditions... if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Fetch(task => task.AssignedPerson) //Fetch assigned person in a single query .ToList(); }} NhRepositoryBase实现了在IRepository界面中定义的所有方法。 所以,你必须实现你的自定义方法,因为我实现了GetAllWithPeople。 GetAll()方法返回IQueryable ,所以你可以写额外的条件,直到调用ToList()。 如果标准仓储方法足够用于该实体,则无需为实体定义或实现仓储。 所以,我没有为Person实体实现仓储。 应用层(Application layer)“定义软件应该做的工作,并指导表达领域的对象解决问题”(Eric Evans)。 应用层在理想应用中不包括域信息和业务规则(这在现实生活中可能是不可能的,但我们应该将其最小化)。 它介于表示层和域层之间。 应用服务和数据传输对象(DTO)应用服务提供应用层的功能。 应用程序服务方法将数据传输对象作为参数,并返回数据传输对象。 直接返回的实体(或其他领域对象)有许多问题(如数据隐藏,序列化和延迟加载问题)。 我强烈建议不要从应用程序服务获取/返回实体或任何其他域对象。 他们应该只是返回DTO。 因此,显示(Presentation)层与领域(Domain)层完全隔离。 所以,让我们从简单的一,个人应用服务入手:1234public interface IPersonAppService : IApplicationService{ GetAllPeopleOutput GetAllPeople();} 所有应用程序服务按惯例执行IApplicationService。 它确保依赖注入,并提供一些ABP的内置功能(如验证,审核日志和授权)。 我只定义了一个名为GetAllPeople()的方法,并返回一个名为GetAllPeopleOutput的DTO。 我将DTO命名为:方法名称加输入或输出后缀。 参见GetAllPeopleOutput类:1234public class GetAllPeopleOutput{ public List<PersonDto> People { get; set; }} PersonDto是将Person信息传递给表示层的另一个DTO类:12345[AutoMapFrom(typeof(Person))] //AutoMapFrom attribute maps Person -> PersonDtopublic class PersonDto : EntityDto{ public string Name { get; set; }} EntityDto是另一个帮助类ABP,它定义了Id属性。 AutoMapFrom属性为AutoMapper创建Person到PersonDto的自动映射配置。 IPersonAppService的实现如下所示:12345678910111213141516171819public class PersonAppService : IPersonAppService //Optionally, you can derive from ApplicationService as we did for TaskAppService class.{ private readonly IRepository<Person> _personRepository; //ABP provides that we can directly inject IRepository<Person> (without creating any repository class) public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public GetAllPeopleOutput GetAllPeople() { var people = await _personRepository.GetAllListAsync(); return new GetAllPeopleOutput { People = people.MapTo<List<PersonDto>>() }; }} PersonAppService在其构造函数中获取IRepository 作为参数。 ABP的内置依赖注入系统使用Castle Windsor处理它。 所有存储库和应用程序服务都会自动注册到IOC(控制反转)容器作为临时对象。 所以,你没有想到DI细节。 此外,ABP可以在不定义或实现存储库的情况下为实体创建标准存储库。 GetAllPeople()方法只是从数据库中获取所有人(使用ABP的开箱即用)列表,并使用AutoMapper [6]库将其转换为PersonDto对象列表。 AutoMapper使得使用约定(如果需要的话)将一个类映射到另一个类是非常容易的。 ABP的MapTo扩展方法在内部使用AutoMapper进行转换。1Mapper.CreateMap<Person, PersonDto>(); 要获取有关AutoMapper的更多信息,请参阅它的网站。 其他应用程序服务是TaskAppService实现如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071public class TaskAppService : ApplicationService, ITaskAppService{ //These members set in constructor using constructor injection. private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; /// <summary> ///In constructor, we can get needed classes/interfaces. ///They are sent here by dependency injection system automatically. /// </summary> public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public GetTasksOutput GetTasks(GetTasksInput input) { //Called specific GetAllWithPeople method of task repository. var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); //Used AutoMapper to automatically convert List<Task> to List<TaskDto>. return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; } public void UpdateTask(UpdateTaskInput input) { //We can use Logger, it's defined in ApplicationService base class. Logger.Info("Updating a task for input: " + input); //Retrieving a task entity with given id using standard Get method of repositories. var task = _taskRepository.Get(input.TaskId); //Updating changed properties of the retrieved task entity. if (input.State.HasValue) { task.State = input.State.Value; } if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } //We even do not call Update method of the repository. //Because an application service method is a 'unit of work' scope as default. //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception). } public void CreateTask(CreateTaskInput input) { //We can use Logger, it's defined in ApplicationService class. Logger.Info("Creating a task for input: " + input); //Creating a new Task entity with given input's properties var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; } //Saving entity with standard Insert method of repositories. _taskRepository.Insert(task); }} 在UpdateTask方法中,我从任务存储库获取任务实体,并设置更改的属性。 状态或/和AssignedPersonId可能会更改。 请注意,我没有调用_taskRepository.Update或任何其他方法来保存对数据库的更改。 因为在ASP.NET Boilerplate中,应用程序服务方法是默认的工作单元。 对于单位工作方法,它基本上打开数据库连接,并在方法开始时开始事务,并在方法结束时将所有更改(提交事务)保存到数据库。 如果在方法的执行中抛出异常,它将回滚事务。 如果一个工作单元的方法调用另一个工作单元方法,则它们使用相同的事务。 第一个称为工作单位的方法自动处理连接和事务管理。 要了解有关ASP.NET Boilerplate中工作单元的更多信息,请参阅文档。 数据传输对象验证(DTO Validation)验证在应用程序开发中是一个重要且关键但有点乏味的概念。 ABP提供基础设施,使验证更容易和更好。 验证用户输入是一个应用层任务。 如果给定输入无效,则应用程序服务方法应验证输入并抛出异常。 ASP.NET MVC和Web API具有内置的验证系统,可以使用数据注释(如Required)来实现。 但是应用程序服务是一个简单的类,不是从Controller派生的。 幸运的是,ABP为普通应用服务方法提供了类似的机制(使用Castle Dynamic代理和拦截): 1234567public class CreateTaskInput{ public int? AssignedPersonId { get; set; } [Required] public string Description { get; set; }} 在此输入DTO中,只需要“描述”属性。 在调用应用程序服务方法之前,ABP会自动检查它,如果它为空或为空则抛出异常。 System.ComponentModel.DataAnnotations命名空间中的所有验证属性都可以在这里使用。 如果这些标准属性对你来说还不够,可以实现ICustomValidate:1234567891011121314151617public class CreateTaskInput : IInputDto, ICustomValidate{ public int? AssignedPersonId { get; set; } public bool SendEmailToAssignedPerson { get; set; } [Required] public string Description { get; set; } public void AddValidationErrors(List<ValidationResult> results) { if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0)) { results.Add(new ValidationResult("AssignedPersonId must be set if SendEmailToAssignedPerson is true!")); } }} 还有一件事:ABP检查服务方法的输入参数是否为空。 所以,你不需要写保护条款。 我建议为每个应用程序服务方法创建分离的输入和输出类,即使它只有一个输入参数。 当通过向该方法添加其他参数来扩展应用程序时,这是很好的。 它提供了一种在不破坏现有客户端的情况下向应用程序服务方法添加参数的方法。 动态Web API控制器(Dynamic Web API Controllers)应用程序服务由表示层消耗。 在单页面应用程序中,所有的数据都是使用AJAX在javascript和服务器之间发送/接收的。 ABP极大地简化了从javascript调用应用程序服务方法。 这是怎么做到的 让我来解释一下 一个应用程序服务不能直接通过javascript调用。 我们可以使用ASP.NET Web API来向客户端公开服务(还有许多其他框架,如Web服务,WCF,SignalR等)。 所以,可能会有这样的一个流程: Javascript通过AJAX调用Web API控件的操作,Web API控制器的操作然后调用相应的应用程序服务的方法,获取结果并返回给客户端。 这很漂亮的机器人。 ABP自动执行此操作,并可为应用程序服务动态创建Web API控制器。 这里是为我的应用服务创建Web API控制器的所有代码:任务服务和个人服务:123DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem") .Build(); 因此,使用ASP.NET Web API(ABP流畅的动态控制器创建API支持从Web API隐藏方法或选择特定应用程序服务,自己尝试),任务和个人应用程序服务的所有方法都暴露给客户端。 在演示层部分,我们将看到如何使用ABP的动态JavaScript代理调用这些Web API控制器。 显示层(Presentation layer)“负责向用户显示信息并解释用户的命令”(Eric Evans)。 DDD中最明显的一层是Presentation Layer,因为我们可以看到它,我们可以点击它:)。 单页应用(Single page applications) 维基百科说SPA: 单页面应用程序(SPA)也称为单页面接口(SPI),是一个适用于单个网页的Web应用程序或网站,目标是提供类似桌面应用程序的更流畅的用户体验。 在SPA中,通过单个页面加载检索所有必需的代码(HTML,JavaScript和CSS),或者根据需要动态加载适当的资源并将其添加到页面,通常是响应用户操作。 尽管现代网络技术(如HTML5中包含的)技术可以在应用程序中提供单独逻辑页面的感知和导航性,但该页面在此过程中的任何时间点都不会重新加载,也不会将其转移到其他页面。 与单页应用程序的交互通常涉及与Web服务器的幕后动态通信。 有许多框架和库提供了构建SPA的基础设施。 ASP.NET Boilerplate可以与任何SPA框架一起使用,但可以提供简单的基础设施,以便与DurandalJs和AngularJs配合使用(请参阅AngularJs开发的相同应用程序)。 Durandal [7]是这些框架之一,我认为这是一个非常成功的开源项目。 它基于成功和大多数使用的项目:jQuery(用于DOM操作和AJAX),knockout.js(用于MVVM,使用HTML绑定JavaScript模型)和require.js(用于管理javascript依赖关系并从服务器动态加载javascript)。 有关更多信息和丰富的文档,请访问Durandal的网站。 本地化ABP提供了一个强大而灵活的本地化系统。你可以存储在资源文件,XML文件,甚至在自定义源您的本地化的文本。在本节中,我将展示使用XML文件。简单的任务系统项目包括本地化文件夹中的XML文件: 在这里,SimpleTaskSystem.xml的内容:12345678910111213141516171819<?xml version="1.0" encoding="utf-8" ?><localizationDictionary culture="en"> <texts> <text name="TaskSystem" value="Task System" /> <text name="TaskList" value="Task List" /> <text name="NewTask" value="New Task" /> <text name="Xtasks" value="{0} tasks" /> <text name="AllTasks" value="All tasks" /> <text name="ActiveTasks" value="Active tasks" /> <text name="CompletedTasks" value="Completed tasks" /> <text name="TaskDescription" value="Task description" /> <text name="EnterDescriptionHere" value="Task description" /> <text name="AssignTo" value="Assign to" /> <text name="SelectPerson" value="Select person" /> <text name="CreateTheTask" value="Create the task" /> <text name="TaskUpdatedMessage" value="Task has been successfully updated." /> <text name="TaskCreatedMessage" value="Task {0} has been created successfully." /> </texts></localizationDictionary> 这是一个简单的XML文件,包括所有可本地化文本的名称 - 值对。 文化属性定义文件的文化。 在解决方案中还有一个土耳其语(tr)本地化的XML文件。 本地化文件应该注册到ABP,以便可以在C#和javascript中使用:123456Configuration.Localization.Sources.Add( new XmlLocalizationSource( "SimpleTaskSystem", HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem") ) ); 本地化源必须是唯一的名称(SimpleTaskSystem这里)。 因此,可以在应用程序中使用不同的源(以不同的格式和数据源存储)。 XmlLocalizationSource还需要一个文件夹(/ Localization / SimpleTaskSystem here)来读取本地化文件。 然后我们可以在需要时获得本地化的文本。 在C#中,我们有两个选项来获取本地化文本:123456//Use directlyvar s1 = LocalizationHelper.GetString("SimpleTaskSystem", "NewTask");//Use after get sourcevar source = LocalizationHelper.GetSource("SimpleTaskSystem");var s2 = source.GetString("NewTask"); 它(通过使用当前线程的CurrentUICulture)返回当前语言本地化的文本。还有覆盖在一个特定的文化得到文本。有一个在JavaScript中的类似的API,以获得本地化的文本:123456//Use directlyvar s1 = abp.localization.localize('NewTask', 'SimpleTaskSystem');//Use after get sourcevar source = abp.localization.getSource('SimpleTaskSystem');var s2 = source('NewTask'); 这些方法还获得当前语言的本地化的文本。 Javascript API在javascript中,客户端每个应用程序都需要一些常见的功能。 例如:显示成功通知,阻止ui元素,显示消息框等。 存在很多库(jQuery插件)。 但是他们都有不同的API。 ASP.NET Boilerplate为这些任务定义了一些常见的API。 因此,如果您以后要更改通知插件,则只能实现一个简单的API。 此外,jQuery插件可以直接实现ABP API。 您可以调用ABP的通知API而不是直接调用插件的通知API。 在这里,我将解释一些API。 日志记录API当你想要写一些简单的日志中的客户端,你可以如你所知使用的console.log(“…”)API。但它不是所有的浏览器都支持,你的脚本可能被打破。所以,你应该先检查一下。此外,您可能想要写日志别处。 ABP定义安全日志记录功能:12345abp.log.debug('...');abp.log.info('...');abp.log.warn('...');abp.log.error('...');abp.log.fatal('...'); 此外,您还可以通过abp.log.level设置abp.log.levels的一个改变日志级别(例如:abp.log.levels.INFO到不写调试日志)。这些函数写日志,在默认情况下控制台。但是你可以很容易地覆盖这个行为。 通知API当事情发生时,我们喜欢显示一些奇特的自动消失通知,例如在保存项目或出现问题时。 ABP定义了API:1234abp.notify.success('a message text', 'optional title');abp.notify.info('a message text', 'optional title');abp.notify.warn('a message text', 'optional title');abp.notify.error('a message text', 'optional title'); 通知API由默认toastr库实现。您可以在自己喜欢的通知库实现它。 MessageBox APIMessageBox API用于显示一个消息给用户。用户点击OK,关闭消息窗口/对话框。例子:123abp.message.info('some info message', 'some optional title');abp.message.warn('some warning message', 'some optional title');abp.message.error('some error message', 'some optional title'); 它是目前不能实现。您可以实现它显示一个对话框或消息框。 UI Block API此API用于阻止整个页面或页面上的元素。因此,用户不能点击它。 ABP API的是:12345abp.ui.block(); //Block all pageabp.ui.block($('#MyDivElement')); //You can use any jQuery selection..abp.ui.block('#MyDivElement'); //..or directly selectorabp.ui.unblock(); //Unblock all pageabp.ui.unblock('#MyDivElement'); //Unblock specific element UI Busy API有时你可能需要做一些页面/元素忙。例如,你可能想阻止一个表格,然后让一个忙碌的指标,同时提交表单到服务器。 ABP提供API为:12abp.ui.setBusy('#MyRegisterForm');abp.ui.clearBusy('#MyRegisterForm'); setBusy可以采取承诺的第二个参数来自动调用clearBusy时承诺完成。请参见newtask视图模型示例项目(和文章)的使用。 模块系统Abp的设计是模块化的。它提供了基础设施来创建通用模块那些可以在不同的应用中使用。一个模块可以依赖于其他模块。一个应用程序是由模块组成。模块是包括从AbpModule衍生的模块类的组件。在示例应用程序在这篇文章中所解释的,所有层被定义为分隔模块。例如,应用层定义那样的模块:123456789101112131415/// <summary>/// 'Application layer module' for this project./// </summary>[DependsOn(typeof(SimpleTaskSystemCoreModule))]public class SimpleTaskSystemApplicationModule : AbpModule{ public override void Initialize() { //This code is used to register classes to dependency injection system for this assembly using conventions. IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); //We must declare mappings to be able to use AutoMapper DtoMappings.Map(); }} Abp分别在应用程序启动时调用模块PreInitialize,Initialize和PostInitialize方法。 如果模块A依赖于模块B,模块B在模块A之前初始化。所有方法的准确顺序:PreInitialize-B,PreInitialize-A,Initialize-B,Initialize-A,PostInitialize-B和PostInitialize-A。 所有依赖图都是这样的。 Initialize是应该放置依赖注入配置的方法。 在这里,您将看到该模块以常规方式注册其组装中的所有类(请参阅下一节)。 然后它使用AutoMapper库映射类(它是此应用程序的特定的)。 该模块还定义依赖关系(应用程序层仅依赖于应用程序的域(核心)层)。 依赖注入和惯例当你写遵循最佳实践和一些约定您的应用程序ASP.NET样板几乎使无形的使用依赖注入系统。它会自动注册所有存储库,域名服务,应用服务,自动MVC控制器和Web API控制器。例如,你可能有一个IPersonAppService接口和实现它PersonAppService类:123456789public interface IPersonAppService : IApplicationService{ //...}public class PersonAppService : IPersonAppService{ //...} ASP.NET Boilerplate自动注册它,因为它实现了IApplicationService接口(它只是一个空的接口)。 它被注册为transient(每个用法创建的实例)。 当您注册(使用构造函数注入)IPersonAppService接口到一个类时,将创建一个PersonAppService对象并自动传递到构造函数中。 请参阅有关依赖注入的详细文档,并在ASP.NET Boilerplate中实现。 总结Abp提供了一个基于DDD的一款优秀的asp.net框架,提供了基本基本常用的api,可以让开发者只关注具体的业务逻辑,省去了很多重复造轮子的工作,虽然abp为开发者提供了创建项目的一个好的模块、入口的基本框架。但abp也对开发者本身所掌握的知识和经验有一定的要求,如果你使用abp作为你项目的框架,同时意味着你必须熟悉DDD知识并且拥有一定的经验。","categories":[{"name":"ASP.NET","slug":"ASP-NET","permalink":"http://coderpreacher.top/categories/ASP-NET/"}],"tags":[{"name":"ASP.NET","slug":"ASP-NET","permalink":"http://coderpreacher.top/tags/ASP-NET/"},{"name":"DDD","slug":"DDD","permalink":"http://coderpreacher.top/tags/DDD/"},{"name":"ABP","slug":"ABP","permalink":"http://coderpreacher.top/tags/ABP/"},{"name":"领域驱动设计","slug":"领域驱动设计","permalink":"http://coderpreacher.top/tags/领域驱动设计/"},{"name":"ASP.NET Boilerplate","slug":"ASP-NET-Boilerplate","permalink":"http://coderpreacher.top/tags/ASP-NET-Boilerplate/"}]},{"title":"领域驱动设计(DDD:Domain-Driven Design)","slug":"领域驱动设计-DDD-Domain-Driven-Design","date":"2017-05-09T15:14:33.000Z","updated":"2017-05-09T16:27:26.000Z","comments":true,"path":"2017/05/09/领域驱动设计-DDD-Domain-Driven-Design/","link":"","permalink":"http://coderpreacher.top/2017/05/09/领域驱动设计-DDD-Domain-Driven-Design/","excerpt":"","text":"背景软件系统面向对象的设计思想可谓历史悠久,20世纪70年代的Smalltalk可以说是面向对象语言的经典,直到今天我们依然将这门语言视为面向对象语言的基础。随着编程语言和技术的发展,各种语言特性层出不穷,面向对象是大部分语言的一个基本特性,像C++、Java、C#这样的静态语言,Ruby、Python这样的动态语言都是面向对象的语言。 但是面向对象语言并不是银弹,如果开发人员认为使用面向对象语言写出来的程度本身就是面向对象的,那就大错特错了,实际开发中,大量的业务逻辑堆积在一个巨型类中的例子屡见不鲜,代码的复用性和扩展性无法得到保证。为了解决这样的问题,领域驱动设计提出了清晰的分层架构和领域对象的概念,让面向对象的分析和设计进入了一个新的阶段,对企业级软件开发起到了巨大的推动作用。 领域驱动设计(DDD)的中心内容是如何将业务领域概念映射到软件工件中。大部分关于此主题的著作和文章都以Eric Evans的书《领域驱动设计》为基础,主要从概念和设计的角度探讨领域建模和设计情况。这些著作讨论实体、值对象、服务等DDD的主要内容,或者谈论通用语言、界定的上下文(Bounded Context)和防护层(Anti-Corruption Layer)这些的概念。 什么是领域驱动设计(DDD)2004年著名建模专家Eric Evans发表了他最具影响力的书籍:《Domain-Driven Design: Tackling Complexity in the Heart of Software》(中文译名:领域驱动设计:软件核心复杂性应对之道),书中提出了领域驱动设计(简称 DDD)的概念。 领域驱动设计事实上是针对OOAD的一个扩展和延伸,DDD基于面向对象分析与设计技术,对技术架构进行了分层规划,同时对每个类进行了策略和类型的划分。 领域模型是领域驱动的核心。采用DDD的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象(类)组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。基于领域驱动的设计,保证了系统的可维护性、扩展性和复用性,在处理复杂业务逻辑方面有着先天的优势。 领域驱动设计的特点 领域驱动的核心应用场景就是解决复杂业务的设计问题,其特点与这一核心主题息息相关: 分层架构与职责划分:领域驱动设计很好的遵循了关注点分离的原则,提出了成熟、清晰的分层架构。同时对领域对象进行了明确的策略和职责划分,让领域对象和现实世界中的业务形成良好的映射关系,为领域专家与开发人员搭建了沟通的桥梁。 复用:在领域驱动设计中,领域对象是核心,每个领域对象都是一个相对完整的内聚的业务对象描述,所以可以形成直接的复用。同时设计过程是基于领域对象而不是基于数据库的Schema,所以整个设计也是可以复用的。 使用场景:适合具备复杂业务逻辑的软件系统,对软件的可维护性和扩展性要求比较高。不适用简单的增删改查业务。 如果不使用DDD?面对复杂的业务场景和需求,如果没有建立和实现领域模型,会导致应用架构出现胖服务层和贫血的领域模型,在这样的架构中,Service层开始积聚越来越多的业务逻辑,领域对象则成为只有getter和setter方法的数据载体。这种做法还会导致领域特定业务逻辑和规则散布于多个的Service类中,有些情况下还会出现重复的逻辑。我们曾经见过5000多行的Service类,上百个方法,代码基本上是不可读的。 在大多数情况下,贫血的领域模型没有成本效益。它们不会给公司带来超越其它公司的竞争优势,因为在这种架构里要实现业务需求变更,开发并部署到生产环境中去要花费太长的时间。 领域驱动设计的分层架构和构成要素下面我们简单介绍一下领域驱动设计的分层架构和构成要素,这部分内容在Eric Evans的书中有非常详尽的描述,想要详细了解的,最好去读原版书籍。 下面这张图是该书中著名的分层架构图,如下: 整个架构分为四层,其核心就是领域层(Domain),所有的业务逻辑应该在领域层实现,具体描述如下: 逻辑分层 层逻辑 用户界面/展现层 负责向用户展现信息以及解释用户命令。 应用层 很薄的一层,用来协调应用的活动。它不包含业务逻辑。它不保留业务对象的状态,但它保有应用任务的进度状态。 领域层 本层包含关于领域的信息。这是业务软件的核心所在。在这里保留业务对象的状态,对业务对象和它们状态的持久化被委托给了基础设施层。 基础设施层 本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支撑库等作用。 领域驱动设计除了对系统架构进行了分层描述,还对对象(Object)做了明确的职责和策略划分: 实体(Entities):具备唯一ID,能够被持久化,具备业务逻辑,对应现实世界业务对象。 值对象(Value objects):不具有唯一ID,由对象的属性描述,一般为内存中的临时对象,可以用来传递参数或对实体进行补充描述。 工厂(Factories):主要用来创建实体,目前架构实践中一般采用IOC容器来实现工厂的功能。 仓储(Repositories):用来管理实体的集合,封装持久化框架。 服务(Services):为上层建筑提供可操作的接口,负责对领域对象进行调度和封装,同时可以对外提供各种形式的服务。 事务脚本和领域模型Martin Fowler 2004年所著的企业应用架构模式(Patterns of Enterprise Application Architecture)中的第九章领域逻辑模式(Domain Logic Patterns)专门介绍了事务脚本(Transaction Script)和领域模型(Domain Model),理解这两种模式对设计和构建企业应用软件非常有帮助,所以有必要介绍一下。 事务脚本: 事务脚本的核心是过程,通过过程的调用来组织业务逻辑,每个过程处理来自表现层的单个请求。大部分业务应用都可以被看成一系列事务,从某种程度上来说,通过事务脚本处理业务,就像执行一条条SQL语句来实现数据库信息的处理。事务脚本把业务逻辑组织成单个过程,在过程中直接调用数据库,业务逻辑在服务(Service)层处理。 事务脚本模式可以简单的通过UML图表示成这样: 由Action层处理UI层的动作请求,将Request中的数据组装后传递给BusinessService,BS层做简单的逻辑处理后,调用数据访问对象进行数据持久化,其中VO充当了数据传输对象的作用,一般是贫血的POJO,只具备getter和setter方法,没有状态和行为。 事务脚本模式的特点是简单容易理解,面向过程设计。对于少量逻辑的业务应用来说,事务脚本模式简单自然,性能良好,容易理解,而且一个事务的处理不会影响其他事务。不过缺点也很明显,对于复杂的业务逻辑处理力不从心,难以保持良好的设计,事务之间的冗余代码不断增多,通过复制粘贴方式进行复用。可维护性和扩展性变差。 领域模型: 领域模型的特点也比较明显, 属于面向对象设计,领域模型具备自己的属性行为状态,并与现实世界的业务对象相映射。各类具备明确的职责划分,领域对象元素之间通过聚合和引用等关系配合解决实际业务应用和规则。可复用,可维护,易扩展,可以采用合适的设计模型进行详细设计。缺点是相对复杂,要求设计人员有良好的抽象能力。 在实际的设计中,我们需要根据具体的需求选择相应的设计模式。具备复杂业务逻辑的核心业务系统适合使用领域模型,简单的信息管理系统可以考虑采用事务脚本模式。 领域(Domain)所谓领域,即是一个组织的业务开展方式,业务价值便体现在其中。长久以来,我们程序员都是很好的技术型思考者,我们总是擅长从技术的角度来解决项目问题。但是,一个软件系统是否真正可用是通过它所提供的业务价值体现出来的。因此,与其每天钻在那些永远也学不完的技术中,何不将我们的关注点向软件系统所提供的业务价值方向思考思考,这也正是DDD所试图解决的问题。 既然是领域驱动设计,那么我们主要的关注点理所当然应该放在如何设计领域模型上,以及对领域模型的划分。 领域并不是多么高深的概念,比如,一个保险公司的领域中包含了保险单、理赔和再保险等概念;一个电商网站的领域包含了产品名录、订单、发票、库存和物流的概念。这里,我主要讲讲对领域的划分,即将一个大的领域划分成若干个子域。 在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。但是在DDD中,我们对系统的划分是基于领域的,也即是基于业务的。 于是,问题也来了:首先,哪些概念应该建模在哪些子系统里面?我们可能会发现一个领域概念建模在子系统A中是可以的,而建模在子系统B中似乎也合乎情理。第二个问题是,各个子系统之间的应该如何集成?有人可能会说,这不简单得就像客户端调用服务端那么简单吗?问题在于,两个系统之间的集成涉及到基础设施和不同领域概念在两个系统之间的翻译,稍不注意,这些概念就会对我们精心创建好的领域模型造成污染。 如何解决?答案是:限界上下文和上下文映射图。 限界上下文(Bounded Context)在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文和领域具有一对一的关系。 举个例子,同样是一本书,在出版阶段和出售阶段所表达的概念是不同的,出版阶段我们主要关注的是出版日期,字数,出版社和印刷厂等概念,而在出售阶段我们则主要关心价格,物流和发票等概念。我们应该怎么办呢,将所有这些概念放在单个Book对象中吗?这不是DDD的做法,DDD有限界上下文将这两个不同的概念区分开来。 从物理上讲,一个限界上下文最终可以是一个DLL(.NET)文件或者JAR(Java)文件,甚至可以是一个命名空间(比如Java的package)中的所有对象。但是,技术本身并不应该用来界分限界上下文。 将一个限界上下文中的所有概念,包括名词、动词和形容词全部集中在一起,我们便为该限界上下文创建了一套通用语言。通用语言是一个团队所有成员交流时所使用的语言,业务分析人员、编码人员和测试人员都应该直接通过通用语言进行交流。 对于上文中提到的各个子域之间的集成问题,其实也是限界上下文之间的集成问题。在集成时,我们主要关心的是领域模型和集成手段之间的关系。比如需要与一个REST资源集成,你需要提供基础设施(比如Spring 中的RestTemplate),但是这些设施并不是你核心领域模型的一部分,你应该怎么办呢?答案是防腐层,该层负责与外部服务提供方打交道,还负责将外部概念翻译成自己的核心领域能够理解的概念。当然,防腐层只是限界上下文之间众多集成方式的一种,另外还有共享内核、开放主机服务等,具体细节请参考《实现领域驱动设计》原书。限界上下文之间的集成关系也可以理解为是领域概念在不同上下文之间的映射关系,因此,限界上下文之间的集成也称为上下文映射图。 实体vs值对象(Entity vs Value Object) 在一个软件系统中,实体表示那些具有生命周期并且会在其生命周期中发生改变的东西;而值对象则表示起描述性作用的并且可以相互替换的概念。同一个概念,在一个软件系统中被建模成了实体,但是在另一个系统中则有可能是值对象。例如货币,在通常交易中,我们都将它建模成了一个值对象,因为我们花了20元买了一本书,我们只是关心货币的数量而已,而不是关心具体使用了哪一张20元的钞票,也即两张20元的钞票是可以互换的。但是,如果现在中国人民银行需要建立一个系统来管理所有发行的货币,并且希望对每一张货币进行跟踪,那么此时的货币便变成了一个实体,并且具有唯一标识(Identity)。在这个系统中,即便两张钞票都是20元,他们依然表示两个不同的实体。 具体到实现层面,值对象是没有唯一标识的,他的equals()方法(比如在Java语言中)可以用它所包含的描述性属性字段来实现。但是,对于实体而言,equals()方法便只能通过唯一标识来实现了,因为即便两个实体所拥有的状态是一样的,他们依然是不同的实体,就像两个人的名字都叫张三,但是他们却是两个不同的人的个体。 我们发现,多数领域概念都可以建模成值对象,而非实体。值对象就像软件系统中的过客一样,具有“创建后不管”的特征,因此,我们不需要像关心实体那样去关心诸如生命周期和持久化等问题。 聚合(Aggregate) 聚合可能是DDD中最难理解的概念 ,之所以称之为聚合,是因为聚合中所包含的对象之间具有密不可分的联系,他们是内聚在一起的。比如一辆汽车(Car)包含了引擎(Engine)、车轮(Wheel)和油箱(Tank)等组件,缺一不可。一个聚合中可以包含多个实体和值对象,因此聚合也被称为根实体。聚合是持久化的基本单位,它和资源库(请参考下文)具有一一对应的关系。 既然聚合可以容纳其他领域对象,那么聚合应该设计得多大呢?这也是设计聚合的难点之一。比如在一个博客(Blog)系统中,一个用户(User)可以创建多个Blog,而一个Blog又可以包含多篇博文(Post)。在建模时,我们通常的做法是在User对象中包含一个Blog的集合,然后在每个Blog中又包含了一个Post的集合。你真的需要这么做吗?如果你需要修改User的基本信息,在加载User时,所有的Blog和Post也需要加载,这将造成很大的性能损耗。诚然,我们可以通过延迟加载的方式解决问题,但是延迟加载只是技术上的实现方式而已。导致上述问题的深层原因其实在我们的设计上,我们发现,User更多的是和认证授权相关的概念,而与Blog关系并不大,因此完全没有必要在User中维护Blog的集合。在将User和Blog分离之后,Blog也和User一样成为了一个聚合,它拥有自己的资源库。问题又来了:既然User和Blog分离了,那么如果需要在Blog中引用User又该怎么办呢?在一个聚合中直接引用另外一个聚合并不是DDD所鼓励的,但是我们可以通过ID的方式引用另外的聚合,比如在Blog中可以维护一个userId的实例变量。User作为Blog的创建者,可以成为Blog的工厂。放到DDD中,创建Blog的功能也只能由User完成。 综上,对于“创建Blog”的用例,我们可以通过以下方法完成:1234567public class Customer { private String email; public void setEmail(String email) { this.email = email; }} 在上例中,业务用例通过BlogApplicationService应用服务完成,在用例方法createBlog()中,首先通过User的资源库得到一个User,然后调用User中的工厂方法createBlog()方法创建一个Blog,最后通过BlogRepository对Blog进行持久化。整个过程构成了一次事务,因此createBlog()方法标记有@Transactional作为事务边界。 使用聚合的首要原则为在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及到了对多个聚合状态的更改,那么应该采用发布领域事件(参考下文)的方式通知相应的聚合。此时的数据一致性便从事务一致性变成了最终一致性(Eventual Consistency)。 领域服务(Domain Service) 你是否遇到过这样的问题:想建模一个领域概念,把它放在实体上不合适,把它放在值对象上也不合适,然后你冥思苦想着自己的建模方式是不是出了问题。恭喜你,祝贺你,你的建模手法完全没有问题,只是你还没有接触到领域服务(Domain Service)这个概念,因为领域服务本来就是来处理这种场景的。比如,要对密码进行加密,我们便可以创建一个PasswordEncryptService来专门负责此事。 值得一提的是,领域服务和上文中提到的应用服务是不同的,领域服务是领域模型的一部分,而应用服务不是。应用服务是领域服务的客户,它将领域模型变成对外界可用的软件系统。 领域服务不能滥用,因为如果我们将太多的领域逻辑放在领域服务上,实体和值对象上将变成贫血对象。 仓储(Repository)仓储用于保存和获取聚合对象,在这一点上,仓储与DAO多少有些相似之处。但是,仓储和DAO是存在显著区别的。DAO只是对数据库的一层很薄的封装,而仓储则更加具有领域特征。另外,所有的实体都可以有相应的DAO,但并不是所有的实体都有仓储,只有聚合才有相应的仓储。 仓储分为两种,一种是基于集合的,一种是基于持久化的。顾名思义,基于集合的仓储具有编程语言中集合的特征。举个例子,Java中的List,我们从一个List中取出一个元素,在对该元素进行修改之后,我们并不用显式地将该元素重新保存到List里面。因此,面向集合的仓储并不存在save()方法。比如,对于上文中的User,其仓储可以设计为: 123456 public interface CollectionOrientedUserRepository { public void add(User user); public User userById(String userId); public List allUsers(); public void remove(User user); } 对于面向持久化的资源库来说,在对聚合进行修改之后,我们需要显式地调用sava()方法将其更新到资源库中。依然是User,此时的资源库如下: 123456public interface PersistenceOrientedUserRepository { public void save(User user); public User userById(String userId); public List<User> allUsers(); public void remove(User user); } 在以上两种方式所实现的资源库中,虽然只是将add()方法改成了save()方法,但是在使用的时候却是不一样的。在使用面向集合资源库时,add()方法只是用来将新的聚合加入资源库;而在面向持久化的资源库中,save()方法不仅用于添加新的聚合,还用于显式地更新既有聚合。 领域事件(Domain Event)在Eric的《领域驱动设计》中并没有提到领域事件,领域事件是最近几年才加入DDD生态系统的。 在传统的软件系统中,对数据一致性的处理都是通过事务完成的,其中包括本地事务和全局事务。但是,DDD的一个重要原则便是一次事务只能更新一个聚合实例。然而,的确存在需要修改多个聚合的业务用例,那么此时我们应该怎么办呢? 另外,在最近流行起来的微服务(Micro Service)的架构中,整个系统被分成了很多个轻量的程序模块,他们之间的数据一致性并不容易通过事务一致性完成,此时我们又该怎么办呢? 在DDD中,领域事件便可以用于处理上述问题,此时最终一致性取代了事务一致性,通过领域事件的方式达到各个组件之间的数据一致性。 领域事件的命名遵循英语中的“名词+动词过去分词”格式,即表示的是先前发生过的一件事情。比如,购买者提交商品订单之后发布OrderSubmitted事件,用户更改邮箱地址之后发布EmailAddressChanged事件。 需要注意的是,既然是领域事件,他们便应该从领域模型中发布。领域事件的最终接收者可以是本限界上下文中的组件,也可以是另一个限界上下文。 领域事件的额外好处在于它可以记录发生在软件系统中所有的重要修改,这样可以很好地支持程序调试和商业智能化。另外,在CQRS架构的软件系统中,领域事件还用于写模型和读模型之间的数据同步。再进一步发展,事件驱动架构可以演变成事件源(Event Sourcing),即对聚合的获取并不是通过加载数据库中的瞬时状态,而是通过重放发生在聚合生命周期中的所有领域事件完成。 写在最后在这篇文章中,我试图用现实世界的例子介绍域驱动设计的基本概念和术语。 目标是使您对DDD世界感到舒适。 但是真正开发使用DDD的应用程序是一个很大的挑战。 在设计对象模型时,您学习DDD的概念越多,您在设计中获得的准确度越高。 正如我之前说的最重要的是,你必须考虑域驱动的方式。 如果你不这样做,当你的应用程序是一个复杂的应用程序时,你会非常受苦。 参考文章: Domain Driven Design。 领域驱动设计实现之路。","categories":[{"name":"DDD","slug":"DDD","permalink":"http://coderpreacher.top/categories/DDD/"}],"tags":[{"name":"DDD","slug":"DDD","permalink":"http://coderpreacher.top/tags/DDD/"},{"name":"领域驱动设计","slug":"领域驱动设计","permalink":"http://coderpreacher.top/tags/领域驱动设计/"},{"name":"Domain-Driven Design","slug":"Domain-Driven-Design","permalink":"http://coderpreacher.top/tags/Domain-Driven-Design/"}]},{"title":"命令查询职责分离(CQRS)模式","slug":"命令查询职责分离-CQRS-模式","date":"2017-05-08T13:08:30.000Z","updated":"2017-05-09T01:05:45.000Z","comments":true,"path":"2017/05/08/命令查询职责分离-CQRS-模式/","link":"","permalink":"http://coderpreacher.top/2017/05/08/命令查询职责分离-CQRS-模式/","excerpt":"","text":"背景和问题在传统的数据管理系统中,两个命令(对数据的更新)和查询(对数据的请求)都是针对单个数据存储库中的同一组实体执行的。 这些实体可以是关系数据库(如SQL Server)中一个或多个表中的行的子集。 通常在这些系统中,所有创建,读取,更新和删除(CRUD)操作都应用于实体的相同表示形式。 例如,通过数据访问层(DAL)从数据存储器检索表示客户的数据传输对象(DTO)并显示在屏幕上。 用户更新DTO的某些字段(可能通过数据绑定),然后DTO将由DAL保存回数据存储。 同样的DTO用于读写操作。 该图说明了传统的CRUD架构。 当只有有限的业务逻辑应用于数据操作时,传统的CRUD设计才能正常运行。开发工具提供的支架机制可以非常快速地创建数据访问代码,然后可以根据需要进行自定义。然而,传统的CRUD方法有一些缺点: 这通常意味着数据的读取和写入表示之间存在不匹配的情况,例如即使不需要作为操作的一部分,必须正确更新的附加列或属性。 当记录被锁定在协作域中的数据存储中时,数据争用就会发生风险,其中多个角色在同一组数据上并行操作。或者当使用乐观锁定时更新由并发更新引起的冲突。随着系统的复杂性和吞吐量的增加,这些风险将增加。此外,由于数据存储和数据访问层的负载以及检索信息所需的查询的复杂性,传统的方法可能会对性能产生负面影响。 它可以使管理安全和权限更加复杂,因为每个实体都受到读写操作的限制,这可能会在错误的上下文中暴露数据。 同步的,直接与数据库进行交互在大数据量同时访问的情况下可能会影响性能和响应性,并且可能会产生性能瓶颈。 解决方案CQRSCQRS介绍CQRS表示命令查询责任分离。 许多人认为CQRS是整个架构,但它们是错误的。 CQRS只是一个小小的模式。 这种模式首先由Greg Young和Udi Dahan介绍。 他们从Bertrand Meyer在“面向对象软件构建”一书中定义的命令查询分离模式中获得灵感。 CQS背后的主要思想是:“一个方法应该改变一个对象的状态,或者返回一个结果,而不是两者。 换句话说,问问题不应该改变答案。 更正式地,如果方法透明,那么方法应该返回一个值,因此没有副作用。“(维基百科)因此,我们可以将方法分为两组: Commands:更改对象或整个系统的状态(有时称为修饰符或变体)。 Queries:返回结果,不要更改对象的状态。 命令和查询责任分离(CQRS)是一种模式,它通过使用单独的接口来隔离从更新数据(命令)的操作中读取数据(查询)的操作。 这意味着用于查询和更新的数据模型是不同的。 然后,可以隔离模型,如下图所示,尽管这不是绝对要求。 与基于CRUD的系统中使用的单一数据模型相比,在基于CQRS的系统中使用单独的查询和更新模型来简化设计和实现。 然而,一个缺点是与CRUD设计不同,CQRS代码不能使用脚手架机制自动生成。用于读取数据的查询模型和用于写入数据的更新模型可以访问相同的物理存储,也许通过使用SQL视图或通过快速生成投影。 然而,通常将数据分成不同的物理存储,以最大限度地提高性能,可扩展性和安全性,如下图所示。 读存储可以是写存储的只读副本,或者读写存储可以具有不同的结构。 使用读取存储器的多个只读副本可以大大提高查询性能和应用程序UI响应性,特别是在只读副本位于靠近应用程序实例的分布式场景中。 某些数据库系统(SQL Server)提供了其他功能,例如故障转移副本,以最大限度地提高可用性。读写存储器的分离还允许每个存储器被适当地缩放以匹配负载。 例如,读取存储器通常会遇到比写入存储器高得多的负载。 在一个真实的情况下,很简单的告诉哪个是哪个。查询将声明返回类型,命令将返回void。这种模式是广泛适用的,它使得关于对象的推理更容易。另一方面,CQRS仅适用于具体问题。 使用主流方法的许多应用程序都由读写方面常见的模型组成。拥有相同的读写方式可以导致更为复杂的模型,难以维护和优化。 这两种模式的真正实力就是你可以分开改变状态的方法。在处理性能和调优的情况下,这种分离可能非常方便。您可以从写入端分开优化系统的读取端。写方面被称为域。域包含所有行为。阅读方面专门针对报告需求。 这种模式的另一个好处是在大量应用的情况下。您可以将开发人员拆分为在系统不同方面工作的较小团队(读或写),而不了解对方。例如,在阅读方面工作的开发人员不需要了解域模型。 查询端(Query side)这些查询只会包含获取数据的方法。 从架构的角度来看,这些将是返回客户端在屏幕上显示的DTO的所有方法。 DTO通常是域对象的预测。 在某些情况下,这可能是一个非常痛苦的过程,特别是当需要复杂的DTO时。 使用CQRS可以避免这些预测。 相反,可以引入一种新的投资DTO的方法。 您可以绕过域模型,并使用读取层从数据存储中直接获取DTO。 当应用程序正在请求数据时,可以通过单次调用读取层来完成此操作,该层返回包含所有所需数据的单个DTO。 读取层可以直接连接到数据库(数据模型),而使用存储过程来读取数据并不是个好主意。 与数据源的直接连接通过维护和优化使查询变得非常简单。 非正规化数据是有道理的。 这样做的原因是数据通常被查询是执行域行为的多倍。 这种非规范化可能会提高应用程序的性能。 命令端(Command side)由于读取端已被分离,因此域仅专注于处理命令。 现在域对象不再需要暴露内部状态。 存储库除了GetById之外只有几种查询方法。 命令由客户端应用程序创建,然后发送到域层。 命令是指示特定实体执行某些操作的消息。 命令命名为DoSomething(例如ChangeName,DeleteOrder …)。 他们指示目标实体做某些可能导致不同结果或失败的事情。 命令由命令处理程序处理。 为什么要使用CQRS?从CQRS回退一段时间,将域分为DDD中的有界环境的好处之一是使您能够识别并集中于系统更复杂的部分(有界环境),受到不断变化的业务 规则或提供作为关键业务差异化的功能。只有在提供可识别的业务收益的情况下,才应考虑将CQRS模式应用于特定有限的上下文,而不是因为它是您考虑的默认模式。您可以通过应用CQRS模式获得的最常见的业务优势是增强的可扩展性,简化您的域的复杂方面,提高解决方案的灵活性,以及更好地适应不断变化的业务需求。 可扩展性在许多企业系统中,读取次数大大超过了写入次数,因此您的可扩展性要求在每一方面都会有所不同。通过将读取端和写入端分隔为有界环境中的单独模型,您现在可以独立地对每个模型进行扩展。例如,如果您在Microsoft Azure中托管应用程序,则可以为每一方使用不同的角色,然后通过向每个角色添加不同数量的角色实例来独立扩展它们。可扩展性不应该是您在特定有限上下文中选择实施CQRS模式的唯一原因:“在非协作域中,您可以在其中添加更多的数据库服务器来支持更多用户,请求和数据,同时添加Web服务器,但没有真正的可伸缩性问题(直到您的大小为Amazon,Google或Facebook)。如果您使用MySQL,SQL Server Express或其他数据库服务器,数据库服务器可以便宜。 降低复杂性在您的领域的复杂领域,设计和实现负责读取和写入数据的对象可能会加剧复杂性。在许多情况下,复杂的业务逻辑仅在系统处理更新和事务操作时应用;相比之下,读逻辑往往要简单得多。当业务逻辑和读逻辑在同一模型中混合在一起时,处理诸如多用户,共享数据,性能,事务,一致性和过时数据等困难问题变得更加困难。将读取的逻辑和业务逻辑分成单独的模型可以更容易地分离和解决这些复杂的问题。然而,在许多情况下,可能需要一些努力来解开和了解域中现有的模型。分离问题是Bertrand Meyer的命令查询分离原则背后的关键动机:“这个原则中真正有价值的想法是,如果您能够清楚地将状态与不改变状态的方法分开,这是非常有用的,这是因为您可以在许多情况下使用查询更有信心,在任何地方介绍它们,你必须更加小心修饰符。“-Martin Fowler,CommandQuerySeparation像许多模式一样,您可以将CQRS模式视为将您领域中固有的一些复杂性转化为众所周知的知识,并为解决某些类别问题提供了一种标准方法。通过分离读取逻辑和业务逻辑来简化有界环境的另一个潜在好处是它可以使测试更容易。 灵活性使用CQRS模式的解决方案的灵活性主要来自于分离到读取端和写入端模型。在读取方面进行更改变得更加容易,例如在您可以确信不会对业务逻辑的行为产生任何影响的情况下添加新的查询来支持UI中的新的报告屏幕。在写作方面,拥有一个仅关心域内核心业务逻辑的模型意味着您拥有一个比包含读取逻辑的模型更简单的模型来处理。从长远来看,一个准确描述您的核心域业务逻辑的良好有用的模型将成为宝贵的资产。面对不断变化的商业环境和对您的组织的竞争压力,这将使您更加敏捷。这种灵活性和敏捷性与DDD中持续集成的概念有关:“持续整合意味着在上下文中的所有工作正在被合并并且变得足够一致,当分裂发生时,它们被快速地捕获和纠正。在某些情况下,可能有不同的开发团队在写作方面和阅读方面工作,但实际上这可能取决于特定有界上下文的大小。 专注于业务如果您使用像CRUD这样的方法,那么该技术往往会塑造解决方案。 采用CQRS模式有助于您专注于业务和构建面向任务的UI。 将不同问题分解为读取端和写入端的结果是在面对不断变化的业务需求时更适应的解决方案。 这导致较长的开发和维护成本较低。 有利于构建基于任务的用户界面当您实现CQRS模式时,您可以使用命令(通常来自UI)来启动域中的操作。 这些命令通常与域操作和无处不在的语言密切相关。 例如,“为X会议预订两个座位” 您可以设计UI以将这些命令发送到域,而不是启动CRUD风格的操作。 这使得更容易设计直观的基于任务的UI。 什么时候可以考虑CQRSCQRS模式有一些优点: 分工明确,可以负责不同的部分 将业务上的命令和查询的职责分离能够提高系统的性能、可扩展性和安全性。并且在系统的演化中能够保持高度的灵活性,能够防止出现CRUD模式中,对查询或者修改中的某一方进行改动,导致另一方出现问题的情况。 逻辑清晰,能够看到系统中的那些行为或者操作导致了系统的状态变化。 可以从数据驱动(Data-Driven) 转到任务驱动(Task-Driven)以及事件驱动(Event-Driven). 在下场景中,可以考虑使用CQRS模式: 当在业务逻辑层有很多操作需要相同的实体或者对象进行操作的时候。CQRS使得我们可以对读和写定义不同的实体和方法,从而可以减少或者避免对某一方面的更改造成冲突 对于一些基于任务的用户交互系统,通常这类系统会引导用户通过一系列复杂的步骤和操作,通常会需要一些复杂的领域模型,并且整个团队已经熟悉领域驱动设计技术。写模型有很多和业务逻辑相关的命令操作的堆,输入验证,业务逻辑验证来保证数据的一致性。读模型没有业务逻辑以及验证堆,仅仅是返回DTO对象为视图模型提供数据。读模型最终和写模型相一致。 适用于一些需要对查询性能和写入性能分开进行优化的系统,尤其是读/写比非常高的系统,横向扩展是必须的。比如,在很多系统中读操作的请求时远大于写操作。为适应这种场景,可以考虑将写模型抽离出来单独扩展,而将写模型运行在一个或者少数几个实例上。少量的写模型实例能够减少合并冲突发生的情况 适用于一些团队中,一些有经验的开发者可以关注复杂的领域模型,这些用到写操作,而另一些经验较少的开发者可以关注用户界面上的读模型。 对于系统在将来会随着时间不段演化,有可能会包含不同版本的模型,或者业务规则经常变化的系统 需要和其他系统整合,特别是需要和事件溯源Event Sourcing进行整合的系统,这样子系统的临时异常不会影响整个系统的其他部分。但是在以下场景中,可能不适宜使用CQRS: 领域模型或者业务逻辑比较简单,这种情况下使用CQRS会把系统搞复杂。 对于简单的,CRUD模式的用户界面以及与之相关的数据访问操作已经足够的话,没必要使用CQRS,这些都是一个简单的对数据进行增删改查。 不适合在整个系统中到处使用该模式。在整个数据管理场景中的特定模块中CQRS可能比较有用。但是在有些地方使用CQRS会增加系统不必要的复杂性。 CQRS的简单实现CQRS模式在思想上比较简单,但是实现上还是有些复杂。它涉及到DDD,以及Event Sourcing,这里使用codeproject上的 Introduction to CQRS 这篇文章的例子来说明CQRS模式。这个例子是一个简单的在线记日志(Diary)系统,实现了日志的增删改查功能。整体结构如下: 上图很清晰的说明了CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。 例子代码可以到codeproject上下载,整体结构如下:由三个项目构成,Diary.CQRS包含了所有的Domain和消息对象。Configuration通过使用一个名为StructMap的IOC来初始化一些变量方便Web调用,Web是一个简单的MVC3项目,在Controller中有与CQRS交互的代码。 下面分别看Query和Command方面的实现: Query端的实现查询方面很简单,日志列表和明细获取就是简单的查询。下面先看列表查询部分的代码。1234567891011121314151617181920public ActionResult Index(){ ViewBag.Model = ServiceLocator.ReportDatabase.GetItems(); return View();}public ActionResult Edit(Guid id){ var item = ServiceLocator.ReportDatabase.GetById(id); var model = new DiaryItemDto() { Description = item.Description, From = item.From, Id = item.Id, Title = item.Title, To = item.To, Version = item.Version }; return View(model);} ReportDatabase的GetItems和GetById(id)方法就是简单的查询,从命名可以看出他是ReportDatabase。123456789101112131415161718192021222324public class ReportDatabase : IReportDatabase{ static List<DiaryItemDto> items = new List<DiaryItemDto>(); public DiaryItemDto GetById(Guid id) { return items.Where(a => a.Id == id).FirstOrDefault(); } public void Add(DiaryItemDto item) { items.Add(item); } public void Delete(Guid id) { items.RemoveAll(i => i.Id == id); } public List<DiaryItemDto> GetItems() { return items; }} ReportDataBase只是在内部维护了一个List的DiaryItemDto列表。在使用的时候,是通过IRepositoryDatabase对其进行操作的,这样便于mock代码。 Query端的代码很简单。在实际的应用中,这一块就是直接对DB进行查询,然后通过DTO对象返回,这个DB可能是应对特定场景的报表数据库,这样可以提升查询性能。 下面来看Command端的实现: Command端实现Command的实现比较复杂,下面以简单的创建一个新的日志来说明。 在MVC的Control中,可以看到Add的Controller中只调用了一句话:1234567[HttpPost]public ActionResult Add(DiaryItemDto item){ ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To)); return RedirectToAction("Index");} 首先声明了一个CreateItemCommand,这个Command只是保存了一些必要的信息。1234567891011121314151617public class CreateItemCommand:Command{ public string Title { get; internal set; } public string Description { get;internal set; } public DateTime From { get; internal set; } public DateTime To { get; internal set; } public CreateItemCommand(Guid aggregateId, string title, string description,int version,DateTime from, DateTime to) : base(aggregateId,version) { Title = title; Description = description; From = from; To = to; }} 然后将Command发送到了CommandBus上,其实就是让CommandBus来选择合适的CommandHandler来处理。12345678910111213141516171819202122public class CommandBus:ICommandBus{ private readonly ICommandHandlerFactory _commandHandlerFactory; public CommandBus(ICommandHandlerFactory commandHandlerFactory) { _commandHandlerFactory = commandHandlerFactory; } public void Send<T>(T command) where T : Command { var handler = _commandHandlerFactory.GetHandler<T>(); if (handler != null) { handler.Execute(command); } else { throw new UnregisteredDomainCommandException("no handler registered"); } }} 这个里面需要值得注意的是CommandHandlerFactory这个类型的GetHandler方法,他接受一个类型为T的泛型,这里就是我们之前传入的CreateItemCommand。来看他的GetHandler方法。12345678910111213141516171819202122232425public class StructureMapCommandHandlerFactory : ICommandHandlerFactory{ public ICommandHandler<T> GetHandler<T>() where T : Command { var handlers = GetHandlerTypes<T>().ToList(); var cmdHandler = handlers.Select(handler => (ICommandHandler<T>)ObjectFactory.GetInstance(handler)).FirstOrDefault(); return cmdHandler; } private IEnumerable<Type> GetHandlerTypes<T>() where T : Command { var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes() .Where(x => x.GetInterfaces() .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>) )) .Where(h=>h.GetInterfaces() .Any(ii=>ii.GetGenericArguments() .Any(aa=>aa==typeof(T)))).ToList(); return handlers; }} 这里可以看到,他首先查找当前的程序集中(ICommandHandler)所在的程序集中的所有的实现了ICommandHandler的接口的类型,然后在所有的类型找查找实现了该泛型接口并且泛型的类型参数类型为T类型的所有类型。以上面的代码为例,就是要找出实现了ICommandHandler接口的类型。可以看到就是CreateItemCommandHandler类型。123456789101112131415161718192021222324public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>{ private IRepository<DiaryItem> _repository; public CreateItemCommandHandler(IRepository<DiaryItem> repository) { _repository = repository; } public void Execute(CreateItemCommand command) { if (command == null) { throw new ArgumentNullException("command"); } if (_repository == null) { throw new InvalidOperationException("Repository is not initialized."); } var aggregate = new DiaryItem(command.Id, command.Title, command.Description, command.From, command.To); aggregate.Version = -1; _repository.Save(aggregate, aggregate.Version); }} 找到之后然后使用IOC实例化了该对象返回。 现在CommandBus中,找到了处理特定Command的Handler。然后执行该类型的Execute方法。 可以看到在该类型中实例化了一个名为aggregate的DiaryItem对象。这个和我们之前查询所用到的DiaryItemDto有所不同,这个一个领域对象,里面包含了一系列事件。1234567891011121314151617181920212223242526272829303132333435363738394041424344public class DiaryItem : AggregateRoot, IHandle<ItemCreatedEvent>, IHandle<ItemRenamedEvent>, IHandle<ItemFromChangedEvent>, IHandle<ItemToChangedEvent>, IHandle<ItemDescriptionChangedEvent>, IOriginator{ public string Title { get; set; } public DateTime From { get; set; } public DateTime To { get; set; } public string Description { get; set; } public DiaryItem() { } public DiaryItem(Guid id,string title, string description, DateTime from, DateTime to) { ApplyChange(new ItemCreatedEvent(id, title,description, from, to)); } public void ChangeTitle(string title) { ApplyChange(new ItemRenamedEvent(Id, title)); } public void Handle(ItemCreatedEvent e) { Title = e.Title; From = e.From; To = e.To; Id = e.AggregateId; Description = e.Description; Version = e.Version; } public void Handle(ItemRenamedEvent e) { Title = e.Title; } ...} ItemCreatedEvent 事件的定义如下,其实就是用来存储传输过程中需要用到的数据。1234567891011121314151617public class ItemCreatedEvent:Event{ public string Title { get; internal set; } public DateTime From { get; internal set; } public DateTime To { get; internal set; } public string Description { get;internal set; } public ItemCreatedEvent(Guid aggregateId, string title , string description, DateTime from, DateTime to) { AggregateId = aggregateId; Title = title; From = from; To = to; Description = description; }} 可以看到在Domain对象中,除了定义基本的字段外,还定义了一些相应的事件,比如在构造函数中,实际上是发起了一个名为ItemCreateEvent的事件,同时还定义了处理时间的逻辑,这些逻辑都放在名为Handle的接口方法发,例如ItemCerateEvent的处理方法为Handle(ItemCreateEvent)方法。 ApplyChange方法在AggregateRoot对象中,他是聚集根,这是DDD中的概念。通过这个根可以串起所有对象。 该类实现了IEventProvider接口,他保存了所有在_changes中的所有没有提交的变更,其中的ApplyChange的用来为特定的Event查找Eventhandler的方法:12345678910111213141516171819202122232425262728293031323334353637383940414243444546public abstract class AggregateRoot : IEventProvider{ private readonly List<Event> _changes; public Guid Id { get; internal set; } public int Version { get; internal set; } public int EventVersion { get; protected set; } protected AggregateRoot() { _changes = new List<Event>(); } public IEnumerable<Event> GetUncommittedChanges() { return _changes; } public void MarkChangesAsCommitted() { _changes.Clear(); } public void LoadsFromHistory(IEnumerable<Event> history) { foreach (var e in history) ApplyChange(e, false); Version = history.Last().Version; EventVersion = Version; } protected void ApplyChange(Event @event) { ApplyChange(@event, true); } private void ApplyChange(Event @event, bool isNew) { dynamic d = this; d.Handle(Converter.ChangeTo(@event, @event.GetType())); if (isNew) { _changes.Add(@event); } }} 在ApplyChange的实现中,this其实就是对应的实现了AggregateRoot的DiaryItem的Domain对象,调用的Handle方法就是我们之前在DiaryItem中定义的行为。然后将该event保存在内部的未提交的事件列表中。相关的信息及事件都保存在了定义的aggregate对象中并返回。 然后Command继续执行,然后调用了_repository.Save(aggregate, aggregate.Version);这个方法。先看这个Repository对象。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class Repository<T> : IRepository<T> where T : AggregateRoot, new(){ private readonly IEventStorage _storage; private static object _lockStorage = new object(); public Repository(IEventStorage storage) { _storage = storage; } public void Save(AggregateRoot aggregate, int expectedVersion) { if (aggregate.GetUncommittedChanges().Any()) { lock (_lockStorage) { var item = new T(); if (expectedVersion != -1) { item = GetById(aggregate.Id); if (item.Version != expectedVersion) { throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified", item.Id)); } } _storage.Save(aggregate); } } } public T GetById(Guid id) { IEnumerable<Event> events; var memento = _storage.GetMemento<BaseMemento>(id); if (memento != null) { events = _storage.GetEvents(id).Where(e=>e.Version>=memento.Version); } else { events = _storage.GetEvents(id); } var obj = new T(); if(memento!=null) ((IOriginator)obj).SetMemento(memento); obj.LoadsFromHistory(events); return obj; }} 这个方法主要是用来对事件进行持久化的。 所有的聚合的变动都会存在该Repository中,首先,检查当前的聚合是否和之前存储在storage中的聚合一致,如果不一致,则表示对象在其他地方被更改过,抛出ConcurrencyException,否则将该变动保存在Event Storage中。 IEventStorage用来存储所有的事件,其实现类型为InMemoryEventStorage。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364public class InMemoryEventStorage:IEventStorage{ private List<Event> _events; private List<BaseMemento> _mementos; private readonly IEventBus _eventBus; public InMemoryEventStorage(IEventBus eventBus) { _events = new List<Event>(); _mementos = new List<BaseMemento>(); _eventBus = eventBus; } public IEnumerable<Event> GetEvents(Guid aggregateId) { var events = _events.Where(p => p.AggregateId == aggregateId).Select(p => p); if (events.Count() == 0) { throw new AggregateNotFoundException(string.Format("Aggregate with Id: {0} was not found", aggregateId)); } return events; } public void Save(AggregateRoot aggregate) { var uncommittedChanges = aggregate.GetUncommittedChanges(); var version = aggregate.Version; foreach (var @event in uncommittedChanges) { version++; if (version > 2) { if (version % 3 == 0) { var originator = (IOriginator)aggregate; var memento = originator.GetMemento(); memento.Version = version; SaveMemento(memento); } } @event.Version=version; _events.Add(@event); } foreach (var @event in uncommittedChanges) { var desEvent = Converter.ChangeTo(@event, @event.GetType()); _eventBus.Publish(desEvent); } } public T GetMemento<T>(Guid aggregateId) where T : BaseMemento { var memento = _mementos.Where(m => m.Id == aggregateId).Select(m=>m).LastOrDefault(); if (memento != null) return (T) memento; return null; } public void SaveMemento(BaseMemento memento) { _mementos.Add(memento); }} 在GetEvent方法中,会找到所有的聚合根Id相关的事件。在Save方法中,将所有的事件保存在内存中,然后每隔三个事件建立一个快照。可以看到这里面使用了备忘录模式。 然后在foreach循环中,对于所有的没有提交的变更,EventBus将该事件发布出去。 现在,所有的发生变更的事件已经记录下来了。事件已经被发布到EventBus上,然后对应的EventHandler再处理对应的事件,然后与DB交互。现在来看EventBus的Publish方法。1234567891011121314151617public class EventBus:IEventBus{ private IEventHandlerFactory _eventHandlerFactory; public EventBus(IEventHandlerFactory eventHandlerFactory) { _eventHandlerFactory = eventHandlerFactory; } public void Publish<T>(T @event) where T : Event { var handlers = _eventHandlerFactory.GetHandlers<T>(); foreach (var eventHandler in handlers) { eventHandler.Handle(@event); } }} 可以看到EventBus的Publish和CommandBus中的Send方法很相似,都是首先通过EventHandlerFactory查找对应Event的Handler,然后调用其Handler方法。比如123456789101112131415161718192021public class StructureMapEventHandlerFactory : IEventHandlerFactory{ public IEnumerable<IEventHandler<T>> GetHandlers<T>() where T : Event { var handlers = GetHandlerType<T>(); var lstHandlers = handlers.Select(handler => (IEventHandler<T>) ObjectFactory.GetInstance(handler)).ToList(); return lstHandlers; } private static IEnumerable<Type> GetHandlerType<T>() where T : Event { var handlers = typeof(IEventHandler<>).Assembly.GetExportedTypes() .Where(x => x.GetInterfaces() .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(IEventHandler<>))) .Where(h => h.GetInterfaces() .Any(ii => ii.GetGenericArguments() .Any(aa => aa == typeof(T)))) .ToList(); return handlers; }} 然后返回并实例化了ItemCreatedEventHandler 对象,该对象的实现如下:12345678910111213141516171819202122public class ItemCreatedEventHandler : IEventHandler<ItemCreatedEvent>{ private readonly IReportDatabase _reportDatabase; public ItemCreatedEventHandler(IReportDatabase reportDatabase) { _reportDatabase = reportDatabase; } public void Handle(ItemCreatedEvent handle) { DiaryItemDto item = new DiaryItemDto() { Id = handle.AggregateId, Description = handle.Description, From = handle.From, Title = handle.Title, To=handle.To, Version = handle.Version }; _reportDatabase.Add(item); }} 可以看到在Handler方法中,从事件中获取参数,然后新建DTO对象,然后将该对象更新到DB中。 到此,整个Command执行完成。 总结CQRS是一种思想很简单清晰的设计模式,他通过在业务上分离操作和查询来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在CQRS中,所有的涉及到对DB的操作都是通过发送Command,然后特定的Command触发对应事件来完成操作,这个过程是异步的,并且所有涉及到对系统的变更行为都包含在具体的事件中,结合Eventing Source模式,可以记录下所有的事件,而不是以往的某一点的数据信息,这些信息可以作为系统的操作日志,可以来对系统进行回退或者重放。 参考文章: Introduction to CQRS。 浅谈命令查询职责分离(CQRS)模式。","categories":[{"name":"Design Patterns","slug":"Design-Patterns","permalink":"http://coderpreacher.top/categories/Design-Patterns/"}],"tags":[{"name":"CQRS","slug":"CQRS","permalink":"http://coderpreacher.top/tags/CQRS/"}]},{"title":"Nginx 基本配置与参数说明","slug":"Nginx-基本配置与参数说明","date":"2017-05-07T14:28:15.000Z","updated":"2017-05-07T16:01:13.000Z","comments":true,"path":"2017/05/07/Nginx-基本配置与参数说明/","link":"","permalink":"http://coderpreacher.top/2017/05/07/Nginx-基本配置与参数说明/","excerpt":"","text":"概述Nginx 是一个高性能的 Web 和反向代理服务器, 它具有有很多非常优越的特性: 作为 Web 服务器:相比 Apache,Nginx 使用更少的资源,支持更多的并发连接,体现更高的效率,这点使 Nginx 尤其受到虚拟主机提供商的欢迎。能够支持高达 50,000 个并发连接数的响应,感谢 Nginx 为我们选择了 epoll and kqueue 作为开发模型. 作为负载均衡服务器:Nginx 既可以在内部直接支持 Rails 和 PHP,也可以支持作为 HTTP代理服务器 对外进行服务。Nginx 用 C 编写, 不论是系统资源开销还是 CPU 使用效率都比 Perlbal 要好的多。 作为邮件代理服务器:Nginx 同时也是一个非常优秀的邮件代理服务器(最早开发这个产品的目的之一也是作为邮件代理服务器),Last.fm 描述了成功并且美妙的使用经验。 Nginx 安装非常的简单,配置文件 非常简洁(还能够支持perl语法),Bugs非常少的服务器: Nginx 启动特别容易,并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动。你还能够在 不间断服务的情况下进行软件版本的升级。 认识NginxNginx是由俄罗斯软件工程师Igor Sysoev开发的一个高性能的HTTP和反向代理服务器,具备IMAP/POP3和SMTP服务器功能。Nginx最大的特点是对高并发的支持和高效的负载均衡,在高并发的需求场景下,是Apache服务器不错的替代品。目前,包括新浪、腾讯等知名网站已经开始使用Nginx作为Web应用服务器。 正向代理与反向代理Nginx 最常的用途是提供反向代理服务,那么什么反向代理呢?正向代理相信很多大陆同胞都在这片神奇的土地上用过了,原理大致如下图:代理服务器作为客户端这边的中介接受请求,隐藏掉真实的客户,向服务器获取资源。如果代理服务器在长城外的话还能顺便帮助我们实现翻越长城的目的。而反向代理顾名思义就是反过来代理服务器作为服务器的中介,隐藏掉真实提供服务的服务器,原理大致如下图: 这么做当然不是为了实现翻越长城,而是为了实现安全和负载均衡等一系列的功能。所谓安全指客户端的请求不会直接落到内网的服务器上而是通过代理做了 一层转发,在这一层就可以实现安全过滤,流控,防 DDOS 等一系列策略。而负载均衡指我们可以水平扩展后端真正提供服务的服务器数量,代理按规则转发请求到各个服务器,使得各个服务器的负载接近均衡。 而 nginx 就是目前流行的这样一个反向代理服务。 安装nginx可以使用各平台的默认包来安装,本文是介绍使用源码编译安装,包括具体的编译参数信息。 正式开始前,编译环境gcc g++ 开发库之类的需要提前装好,这里默认你已经装好。 ububtu平台编译环境可以使用以下指令: 12apt-get install build-essentialapt-get install libtool centos平台编译环境使用如下指令 安装make:1yum -y install gcc automake autoconf libtool make 安装g++: 1yum install gcc gcc-c++ 下面正式开始 1. 选定源码目录:可以是任何目录,本文选定的是/usr/local/src1cd /usr/local/src 2. 安装PCRE库:ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/ 下载最新的 PCRE 源码包,使用下面命令下载编译和安装 PCRE 包: 1234567cd /usr/local/srcwget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.37.tar.gz tar -zxvf pcre-8.37.tar.gzcd pcre-8.37./configuremakemake install 3.安装zlib库http://zlib.net/zlib-1.2.8.tar.gz 下载最新的 zlib 源码包,使用下面命令下载编译和安装 zlib包:1234567cd /usr/local/srcwget http://zlib.net/zlib-1.2.8.tar.gztar -zxvf zlib-1.2.8.tar.gzcd zlib-1.2.8./configuremakemake install 4.安装ssl(某些vps默认没装ssl)123cd /usr/local/srcwget https://www.openssl.org/source/openssl-1.0.1t.tar.gztar -zxvf openssl-1.0.1t.tar.gz 5.安装nginxNginx 一般有两个版本,分别是稳定版和开发版,您可以根据您的目的来选择这两个版本的其中一个,下面是把 Nginx 安装到 /usr/local/nginx 目录下的详细步骤:123456789101112131415cd /usr/local/srcwget http://nginx.org/download/nginx-1.4.2.tar.gztar -zxvf nginx-1.4.2.tar.gzcd nginx-1.4.2 ./configure --sbin-path=/usr/local/nginx/nginx \\--conf-path=/usr/local/nginx/nginx.conf \\--pid-path=/usr/local/nginx/nginx.pid \\--with-http_ssl_module \\--with-pcre=/opt/app/openet/oetal1/chenhe/pcre-8.37 \\--with-zlib=/opt/app/openet/oetal1/chenhe/zlib-1.2.8 \\--with-openssl=/opt/app/openet/oetal1/chenhe/openssl-1.0.1t makemake install –with-pcre=/usr/src/pcre-8.34 指的是pcre-8.34 的源码路径。–with-zlib=/usr/src/zlib-1.2.7 指的是zlib-1.2.7 的源码路径。 安装成功后 /usr/local/nginx 目录下如下123456fastcgi.conf koi-win nginx.conf.defaultfastcgi.conf.default logs scgi_paramsfastcgi_params mime.types scgi_params.defaultfastcgi_params.default mime.types.default uwsgi_paramshtml nginx uwsgi_params.defaultkoi-utf nginx.conf win-utf 6.启动确保系统的 80 端口没被其他程序占用,运行/usr/local/nginx/nginx 命令来启动 Nginx,1netstat -ano|grep 80 如果查不到结果后执行,有结果则忽略此步骤(ubuntu下必须用sudo启动,不然只能在前台运行)1sudo /usr/local/nginx/nginx 打开浏览器访问此机器的 IP,如果浏览器出现 Welcome to nginx! 则表示 Nginx 已经安装并运行成功。 Nginx配置文件结构1. 全局块:配置影响nginx全局的指令。一般有运行nginx服务器的用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process数等。123456789101112#user nobody;worker_processes 1; #error_log logs/error.log;#error_log logs/error.log notice;#error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024;} 这些是配置文件开始的默认行。通常的环境下,你不需要修改这些选项。这一部分有几个方面需要我们注意: 所有以#号开的行是注释,nginx不会解析。默认的配置文件有许多说明解释的注释块 指令是以一个变量名开头(例如,worker_processes或pid),然后包含一个参数(例如,1或 logs/nginx.pid)或者多个参数(例如,”logs/error.log notice”) 所有指令以分号结尾 某些指令,像上面的events可以包含多个子指令作为参数。这些子指令以花括号包围。 虽然nginx不解析空白符(例如tab,空格,和换行符),但是良好的缩进能提高你维护长期运行配置文件的效率。良好的缩进使配置文件读起来更流畅,能让你很容易明白配置的策略,即使几个月前。 2. events块:配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。 3. http块:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type定义,日志自定义,是否使用sendfile传输文件,连接超时时间,单连接请求数等。1234567891011121314151617http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; “http { }”块的开头像配置文件的开头一样都是标准配置不需要修改。这里我们需要把注意力放在这些元素上: 这部分内容的开始”include”语句包含/usr/loca/nginx/mime.types文件到nginx.conf文件include语句所在位置。include对ningx.conf文件的可读性和组织性很有用。 不能过多使用include,如果太多递归地include文件会产生混乱,所以需要合理有限制地使用include来保证配置文件的清晰和可管理。 你可以去掉log_format指令前的注释并修改这几行设置的变量为你想记录的信息。 gzip指令告诉nginx使用gzip压缩的方式来降低带宽使用和加快传输速度。如果想使用gzip压缩,需要添加如下配置到配置文件的gzip位置。1234567gzip on; gzip_http_version 1.1; gzip_comp_level 2; gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript; 使用gizp压缩并不是没有代价的。在降低带宽的同时也增加了CPU的使用。gzip_cop_level的参数取值范围1-9,9代表最用CPU和1代表最少用CPU,其默认值是1. 另外,请注意上面的片段 “http { “ 是http的前半部分,其余部分解下面继续,直到匹配的”}”。 4. server块:配置虚拟主机的相关参数,一个http中可以有多个server。123456789101112 server { listen 80; server_name localhost; access_log logs/localhost.access.log main; location / { root html; index index.html index.htm; } }} server指令块,像上面例子中那个一样,是我们nginx用户主要配置自己虚拟主机的地方。在server块里有许多重要的指令。listen指令告诉nginx在一个特定的hostname,ip或者tcp端口监听连接。默认,http服务运行在80端口。一下这些listen指令都是有效的: 1234567891011121314151617listen 127.0.0.1:80;listen localhost:80; listen 127.0.0.1:8080;listen localhost:8080; listen 192.168.3.105:80;listen 192.168.3.105:8080; listen 80;listen *:80;listen 8080;listen *:8080; listen 12.34.56.77:80;listen 12.34.56.78:80;listen 12.34.56.79:80; 在这些例子中,我们可以看到很多不同表达方式: 第一组2个指令指明服务器监听在127.0.0.1或localhost的80端口,localhost通常定义在/etc/hosts指向127.0.0.1 第二组除了端口号监听在8080而不是80外,与第一组相同。 第三组例子定义服务器监听在192.168.3.105的80和8080端口 第四组例子是在所有地址上监听特定的端口。listen 80与listen :80相同,listen 8080与listen :80相同。 最后一组例子设置服务器只监听在12.34.56.77/78/79的80端口上的请求。 server_name指令可以设置基于域名的虚拟主机,根据请求头部的内容,一个ip的服务器可以配置多个域名。下面这些server_name的参数是有效的:12345678server_name nginx.cn;server_name nginx.cn www.nginx.cn;server_name *.nginx.cn;server_name .nginx.cn;server_name nginx.*;server_name nginx.cng bucknell.net brackley.org;server_name localhost litchfield bleddington;server_name ""; 多个域名之间以空格分隔。nginx允许一个虚拟主机有一个或多个名字,也可以使用通配符”*”来设置虚拟主机的名字。上面的例子我们看到了很多特殊的地方: 第一组例子,首先定义server_name为nginx.cn,那么来自http://nginx.cn 的请求就会发到该主机上。第二个例子配置了nginx.cn和www.nginx.cn,那么http://nginx.cn 和http://www.nginx.cn 的请求会发到这个主机上。*.nginx.cn和.nginx.cn是等同的配置,设置该主机处理所有来自nginx.cn的子域名,比如www.nginx.cn,blog.nginx.cn等 第二组server_name配置nginx.*,配置服务器处理所有以nginx.开头的请求。例如,nginx.com,nginx.cn,nginx.net,nginx.baidu.com 接下来一组第一个server_name配置,设置主机处理来自三个域名的请求。nginx允许设置不是有效域名的名字。比如接下来这个配置我们可以看到三个不是有效域名的例子,localhost,litchfiled和bledington。nginx只查找请求的HTTP头中的域名但并不判断域名是否有效,这个例子中这些主机名可以配制在/etc/hosts中。当你在本机调试时使用非域名的主机名有时候更适合些。 最后一组例子,server_name设置为空的双引号,它告诉nginx捕捉所有没有hostname的请求,或者hostname没有在其它server_name中指定的。 5. location块:配置请求的路由,以及各种页面的处理情况。 对于特定的请求,一旦nginx匹配一个location来处理。那么这个请求的响应内容就会由这个location块中的指令决定。我们先来看一个最基本的locaiton配置块。1234location / { root html; index index.html index.htm;} 在这个例子中文档根(doucument root)位于html/目录。根据nginx的安装目录/usr/local/nginx,这个location的完整路径是/usr/local/nginx/html。假设一个请求访问位于/blog/includes/styles.css文件同时没有别的location块匹配,那么nginx会用位于文件系统的/usr/local/nginx/html/blog/includes/styles.css响应。当然你也可以用绝对路径设置root指令。 index指令会告诉nginx使用哪个资源如果请求中没有文件名。因此,如果请求http://.ducklington.org/ 将会补全资源位置为/usr/local/nginx/html/index.html。如果index配置了多个文件,nginx会按顺序处理直到找到第一个存在的补全资源。如果index.html在相关目录中没有,那么将使用index.htm。如果两个都不存在,会返回404错误。 上面的配置只是将用户的 url 映射到本地的文件,并没有实现传说中的反向代理和负载均衡(当然 nginx 做静态文件的分发也是想到的厉害),下面我们就来进一步配置 location 看看怎么实现。 配置起来很简单比如我要将所有的请求到转移到真正提供服务的一台机器的 8080 端口,只要这样:123location / { proxy_pass 123.34.56.67:8080;} 这样所有的请求就都被反向代理到 123.34.56.67 去了。这样我们反向代理的功能是实现了,可是就能代理到一台服务器上哪有什么负载均衡呀?这就要用到 nginx 的 upstream 模块了。12345678910upstream backend { ip_hash; server backend1.example.com; server backend2.example.com; server backend3.example.com; server backend4.example.com;}location / { proxy_pass http://backend;} 我们在 upstream 中指定了一组机器,并将这个组命名为 backend,这样在 proxypass 中只要将请求转移到 backend 这个 upstream 中我们就实现了在四台机器的反向代理加负载均衡。其中的 iphash 指明了我们均衡的方式是按照用户的 ip 地址进行分配。 Nginx基本配置与参数说明123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122user nobody;#启动进程,通常设置成和cpu的数量相等worker_processes 1; #全局错误日志及PID文件#error_log logs/error.log;#error_log logs/error.log notice;#error_log logs/error.log info; #pid logs/nginx.pid; #工作模式及连接数上限events { #epoll是多路复用IO(I/O Multiplexing)中的一种方式, #仅用于linux2.6以上内核,可以大大提高nginx的性能 use epoll; #单个后台worker process进程的最大并发链接数 worker_connections 1024; # 并发总数是 worker_processes 和 worker_connections 的乘积 # 即 max_clients = worker_processes * worker_connections # 在设置了反向代理的情况下,max_clients = worker_processes * worker_connections / 4 为什么 # 为什么上面反向代理要除以4,应该说是一个经验值 # 根据以上条件,正常情况下的Nginx Server可以应付的最大连接数为:4 * 8000 = 32000 # worker_connections 值的设置跟物理内存大小有关 # 因为并发受IO约束,max_clients的值须小于系统可以打开的最大文件数 # 而系统可以打开的最大文件数和内存大小成正比,一般1GB内存的机器上可以打开的文件数大约是10万左右 # 我们来看看360M内存的VPS可以打开的文件句柄数是多少: # $ cat /proc/sys/fs/file-max # 输出 34336 # 32000 < 34336,即并发连接总数小于系统可以打开的文件句柄总数,这样就在操作系统可以承受的范围之内 # 所以,worker_connections 的值需根据 worker_processes 进程数目和系统可以打开的最大文件总数进行适当地进行设置 # 使得并发总数小于操作系统可以打开的最大文件数目 # 其实质也就是根据主机的物理CPU和内存进行配置 # 当然,理论上的并发总数可能会和实际有所偏差,因为主机还有其他的工作进程需要消耗系统资源。 # ulimit -SHn 65535 } http { #设定mime类型,类型由mime.type文件定义 include mime.types; default_type application/octet-stream; #设定日志格式 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log logs/access.log main; #sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件, #对于普通应用,必须设为 on, #如果用来进行下载等应用磁盘IO重负载应用,可设置为 off, #以平衡磁盘与网络I/O处理速度,降低系统的uptime. sendfile on; #tcp_nopush on; #连接超时时间 #keepalive_timeout 0; keepalive_timeout 65; tcp_nodelay on; #开启gzip压缩 gzip on; gzip_disable "MSIE [1-6]."; #设定请求缓冲 client_header_buffer_size 128k; large_client_header_buffers 4 128k; #设定虚拟主机配置 server { #侦听80端口 listen 80; #定义使用 www.nginx.cn访问 server_name www.nginx.cn; #定义服务器的默认网站根目录位置 root html; #设定本虚拟主机的访问日志 access_log logs/nginx.access.log main; #默认请求 location / { #定义首页索引文件的名称 index index.php index.html index.htm; } # 定义错误提示页面 error_page 500 502 503 504 /50x.html; location = /50x.html { } #静态文件,nginx自己处理 location ~ ^/(images|javascript|js|css|flash|media|static)/ { #过期30天,静态文件不怎么更新,过期可以设大一点, #如果频繁更新,则可以设置得小一点。 expires 30d; } #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置. location ~ .php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } #禁止访问 .htxxx 文件 location ~ /.ht { deny all; } }}","categories":[{"name":"Nginx","slug":"Nginx","permalink":"http://coderpreacher.top/categories/Nginx/"}],"tags":[{"name":"Nginx","slug":"Nginx","permalink":"http://coderpreacher.top/tags/Nginx/"},{"name":"反向代理","slug":"反向代理","permalink":"http://coderpreacher.top/tags/反向代理/"},{"name":"负载均衡","slug":"负载均衡","permalink":"http://coderpreacher.top/tags/负载均衡/"},{"name":"Nginx配置","slug":"Nginx配置","permalink":"http://coderpreacher.top/tags/Nginx配置/"}]},{"title":"SSH协议介绍","slug":"SSH协议介绍","date":"2017-04-27T14:53:41.000Z","updated":"2017-04-27T15:52:19.000Z","comments":true,"path":"2017/04/27/SSH协议介绍/","link":"","permalink":"http://coderpreacher.top/2017/04/27/SSH协议介绍/","excerpt":"","text":"SSH协议介绍安全Shell(SSH)协议是一种用于安全网络通信的协议,旨在实现相对简单和便宜。 初始版本,SSH1,专注于提供安全的远程登录功能,以取代Telnet和其他远程登录方案,而不提供安全性[4]。 SSH还提供了更一般的客户端 - 服务器功能,可用于保护文件传输和电子邮件等网络功能。 新版本的SSH2提供了SSH的标准化定义,并以许多方式改进了SSH1。 SSH2被记录为RFC 4250至4256中的建议标准。 SSH客户端和服务器应用程序广泛适用于大多数操作系统。 它已成为远程登录和X隧道技术的首选方法,并且正在迅速成为嵌入式系统以外的最普遍的加密技术应用之一。 SSH被组织为通常在TCP之上运行的三种协议(图1): 传输层协议:提供服务器身份验证,数据保密性和数据完整性,具有前瞻性的保密性(即,如果密钥在一个会话中受到威胁,则知识不会影响早期会话的安全性); 传输层可以可选地提供压缩。 用户认证协议:将用户验证到服务器。 连接协议:通过单个底层SSH连接复用多个逻辑通信通道。图1 SSH 协议分层 传输层协议服务器认证发生在传输层,基于具有公私属密钥对的服务器。 服务器可以具有使用多个不同的非对称加密算法的多个主机密钥。 多个主机可以共享相同的主机密钥。 无论如何,在密钥交换期间使用服务器主机密钥来验证主机的身份。 为了使认证成为可能,客户端必须具有服务器公共主机密钥的推定知识。 RFC 4251规定了可以使用的两种可选的信任模型: 客户端具有将每个主机名(由用户键入)与相应的公共主机密钥相关联的本地数据库。 该方法不需要集中管理的基础设施,也不需要第三方协调。 缺点是名称到密钥关联的数据库可能会变得难以维护。 主机名称到密钥关联由受信任的证书颁发机构(CA)进行认证。 客户端只知道CA根密钥,并且可以验证由接受的CA认证的所有主机密钥的有效性。 这种选择可以减轻维护问题,因为理想情况下只需要安全地将一个CA密钥存储在客户机上。 另一方面,每个主机密钥必须经过中央机构的认证才可以进行认证。图2:SSH传输层协议数据包交换 图2说明了SSH传输层协议中事件的顺序。 首先,客户端使用TCP协议与服务器建立TCP连接,而不是传输层协议的一部分。 当建立连接时,客户端和服务器在TCP段的数据字段中交换称为数据包的数据。 每个数据包采用以下格式(图3): 数据包长度:数据包长度是数据包的字节长度,不包括数据包长度和消息认证码(MAC)的字段。 填充长度:填充长度是随机填充字段的长度。 有效载荷:有效载荷构成数据包的有用内容。 在算法协商之前,此字段未压缩。 如果协商压缩,则在随后的数据包中,此字段被压缩。 随机填充:协商加密算法后,添加该字段。 它包含填充的随机字节,使得分组的总长度(不包括MAC字段)是密码块大小的倍数,或者是流密码的8字节。 消息认证码(MAC):如果已经协商消息认证,则该字段包含MAC值。 MAC数据在整个数据包上加上序列号,不包括MAC字段。 序列号是一个隐含的32位数据包序列,它为第一个数据包初始化为零,并为每个数据包递增。 序列号不包括在通过TCP连接发送的数据包中。图3 SSH传输层协议数据包形成 在协商加密算法之后,在计算MAC值之后,将整个数据包(不包括MAC字段)加密。 SSH传输层分组交换包括一系列步骤(图2)。第一步,识别字符串交换,以客户端发送带有以下形式的标识字符串的数据包开始: SSH-protoversion-softwareversion SP comments CR LF 其中SP,CR和LF分别为空格,回车和换行。一个有效字符串的例子是SSH-2.0-billsSSH_3.6.3q3 。服务器使用自己的标识符进行响应。这些字符串用于Diffie-Hellman密钥交换。 接下来是算法协商。每个端口发送一个包含受支持算法列表的SSH_MSG_KEXINIT,以发送者的优先顺序。每种类型的加密算法都有一个列表。算法包括密钥交换,加密,MAC算法和压缩算法。表1显示了加密,MAC和压缩的允许选项。对于每个类别,所选择的算法是服务器也支持的客户端¢â,¬“列表上的第一个算法。 表1:SSH传输层加密算法 Cipher 3des-cbc* Three-key Triple Digital Encryption Standard (3DES) in Cipher-Block-Chaining (CBC) mode blowfish-cbc Blowfish in CBC mode twofish256-cbc Twofish in CBC mode with a 256-bit key twofish256-cbc Twofish in CBC mode with a 256-bit key twofish192-cbc Twofish with a 192-bit key twofish128-cbc Twofish with a 128-bit key aes256-cbc Advanced Encryption Standard (AES) in CBC mode with a 256-bit key aes192-cbc AES with a 192-bit key aes128-cbc** AES with a 128-bit key Serpent256-cbc Serpent in CBC mode with a 256-bit key Serpent192-cbc Serpent with a 192-bit key Serpent128-cbc Serpent with a 128-bit key arcfour RC4 with a 128-bit key cast128-cbc CAST-128 in CBC mode Cipher hmac-sha1* HMAC-SHA1; Digest length = Key length = 20 hmac-sha1-96** First 96 bits of HMAC-SHA1; Digest length = 12; Key length = 20 hmac-md5 HMAC-SHA1; Digest length = Key length = 16 hmac-md5-96 First 96 bits of HMAC-SHA1; Digest length = 12; Key length = 16 Cipher none* No compression zlib Defined in RFCs 1950 and 1951 Note : “*” 代表必须 “**” 代表推荐 下一步是密钥交换。该规范允许用于密钥交换的替代方法,但目前只指定了两个版本的Diffie-Hellman密钥交换。两种版本都在RFC 2409中定义,每个方向只需要一个数据包。交换涉及以下步骤。在这里,C是客户; S是服务器; p是一个很大的安全素; g是GF(p)子群的发生器; q是子组的顺序; V_S是S识别字符串; V_C是C标识串; K_S是S公共主机密钥; I_C是C SSH_MSG_KEXINIT消息;而I_S是在此部分开始之前交换的S SSH_MSG_KEXINIT消息。作为算法选择协商的结果,客户端和服务器都知道p,g和q的值。散列函数hash()也是在算法协商过程中决定的。 C生成随机数x(1 <x <q)并计算e = gx mod p。 C发送e到S.S生成随机数y(0 <y <q)并计算f = gy mod p。 S收到e。它使用其专用主机密钥计算H = H mod H,H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K)和H上的签名。 S向C发送(K_S || f || s)。签名操作可以涉及第二散列操作。C验证K_S确实是S的主机密钥(例如,使用证书或本地数据库)。 C也被允许接受密钥,无需验证;然而,这样做将使协议不能抵抗主动攻击(但是在许多环境中短期内可能需要实际的原因)。 C然后计算K = fx mod p,H =哈希(V_C || V_S || I_C || I_S || K_S || e || f || K),并验证H上的签名s作为这些步骤的结果,双方现在共享主密钥K.此外,服务器已经被认证给客户端,因为服务器已经使用其私有密钥来签署Diffie-Hellman交换机的一半。最后,散列值H用作该连接的会话标识符。当计算时,会话标识符不改变,即使再次执行密钥交换以获得新密钥。 密钥交换的结束通过交换SSH_MSG_NEWKEYS数据包来表示。在这一点上,双方可以开始使用从K生成的密钥,如下所述。 最后一步是服务请求。客户端发送SSH_MSG_SERVICE_REQUEST数据包以请求用户认证或连接协议。在此请求之后,所有数据都作为SSH传输层数据包的有效负载进行交换,受加密和MAC保护。 用于加密和MAC(以及任何需要的IV)的密钥是从共享秘密密钥K生成的,密钥交换H的哈希值和会话标识符等于H,除非已经进行了随后的密钥交换初始密钥交换。值计算如下: 初始IV客户端到服务器:HASH(K || H ||“A”|| session_id)初始IV服务器到客户端:HASH(K || H ||“B”|| session_id)加密密钥客户端到服务器:HASH(K || H ||“C”|| session_id)加密密钥服务器到客户端:HASH(K || H ||“D”|| session_id)服务器的完整性密钥客户端:HASH(K || H ||“E”|| session_id)客户端的完整密钥服务器:HASH(K || H ||“F”|| session_id)其中HASH()是在算法协商期间确定的散列函数。 用户认证协议用户认证协议提供了客户端对服务器进行身份验证的方式。 用户认证协议中总是使用三种类型的消息。来自客户端的身份验证请求的格式如下: byte SSH_MSG_USERAUTH_REQUEST (50)string usernamestring service namestring method name…. method-specific fields 其中username是客户端声明的授权身份,服务名称是客户端请求访问的功能(通常为SSH连接协议),方法名称是此请求中使用的身份验证方法。第一个字节具有十进制值50,这被解释为SSH_MSG_USERAUTH_REQUEST。 如果服务器拒绝身份验证请求或接受请求,但需要一个或多个其他身份验证方法,则服务器将发送以下格式的消息: 字节SSH_MSG_USERAUTH_FAILURE(52)名称列表认证可以继续布尔部分成功 名称列表是可以有效地继续对话的方法的列表。如果服务器接受认证,它会发送一个单字节消息SSH_MSG_USERAUTH_SUCCESS(52)。 消息交换涉及以下步骤: 客户端发送一个请求方法为none的SSH_MSG_USERAUTH_REQUEST。 服务器检查以确定用户名是否有效。如果没有,则服务器返回部分成功值为false的SSH_MSG_USERAUTH_FAILURE。如果用户名有效,则服务器进入步骤3。 服务器返回SSH_MSG_USERAUTH_FAILURE,其中包含要使用的一种或多种身份验证方法的列表。 客户端选择可接受的认证方法之一,并发送一个SSH_MSG_USERAUTH_REQUEST,该方法名称和所需的方法特定字段。在这一点上,可能有一系列交换来执行该方法。 如果认证成功并需要更多认证方法,则服务器使用部分成功值为true进行到步骤3。如果认证失败,则服务器使用部分成功值为false进行到步骤3。 当所有需要的认证方法成功时,服务器发送一个SSH_MSG_USERAUTH_SUCCESS消息,认证协议结束。 服务器可能需要以下一种或多种认证方法: publickey:该方法的细节取决于所选择的公钥算法。实质上,客户端向包含客户端公钥的服务器发送消息,消息由客户端的私钥签名。当服务器收到此消息时,它将检查提供的密钥是否可以接受认证,如果是,则检查该签名是否正确。密码:客户端发送包含明文密码的消息,该明文密码受传输层协议的加密保护。hostbased:认证是在客户端的主机而不是客户端本身执行的。因此,支持多个客户端的主机将为其所有客户端提供身份验证。该方法通过使客户端发送使用客户端主机的私钥创建的签名来工作。因此,SSH服务器不是直接验证用户的身份,而是验证客户端主机的身份,然后在客户端表示用户已经认证的时候相信主机。 连接协议SSH连接协议运行在SSH传输层协议之上,并假设安全认证连接正在使用中。称为隧道的安全认证连接由连接协议用于复用多个逻辑信道。 RFC 4254“安全Shell(SSH)连接协议”指出,连接协议运行在传输层协议和用户身份验证协议之上。 RFC 4251“SSH协议架构”指出,连接协议运行在用户身份验证协议上。实际上,连接协议在传输层协议上运行,但假设用户认证协议先前已被调用。 使用单独的通道支持使用SSH的所有类型的通信,如终端会话。任何一方都可以打开一个频道。对于每个通道,每一方都关联唯一的通道号,两端不需要相同。通道使用窗口机构进行流量控制。在接收到消息以指示该窗口空间可用之前,不能向通道发送数据。频道的生命通过三个阶段进行:开通频道,数据传输和关闭频道。 当任一方希望打开一个新的频道时,它会为频道分配一个本地号码,然后发送一个以下格式的消息: byte SSH_MSG_CHANNEL_OPENstring channel typeuint32 sender channeluint32 initial window sizeuint32 maximum packet size…. channel type specific data follow 其中uint32表示无符号32位整数。通道类型标识该通道的应用程序,如下所述。发送方信道是本地信道号。初始窗口大小指定在不调整窗口的情况下可以向该消息的发送者发送多少字节的信道数据。最大数据包大小指定可以发送给发送方的单个数据包的最大大小。例如,可能需要使用较小的数据包进行交互式连接,以便在慢速链接上获得更好的交互式响应。 如果远程端能够打开通道,它将返回一个SSH_MSG_CHANNEL_OPEN_CONFIRMATION消息,其中包含发送方通道号,收件人通道号以及入站流量的窗口和数据包大小值。否则,远程端返回一条SSH_MSG_CHANNEL_OPEN_FAILURE消息,其中包含原因代码,指示失败原因。 在通道打开后,使用SSH_MSG_CHANNEL_DATA消息执行数据传输,该消息包括接收方频道号码和数据块。只要通道打开,这两个方向的这些消息可以继续。 当任一方希望关闭频道时,会发送一个SSH_MSG_CHANNEL_CLOSE消息,其中包含收件人频道号码。图4提供了一个连接协议交换的例子。图4:SSH连接协议示例消息交换 SSH连接协议规范中识别了四种通道类型: 会话:会话是指程序的远程执行。该程序可能是一个shell,一个应用程序,如文件传输或电子邮件,一个系统命令或一些内置的子系统。当会话通道打开时,后续请求用于启动远程程序。 x11:该通道类型是指X Window系统,一种为联网计算机提供GUI的计算机软件系统和网络协议。 X允许应用程序在网络服务器上运行,但显示在台式机上。 forwarding-tcpip:该通道类型是远程端口转发,如下所述。 direct-tcpip:该通道类型是本地端口转发,如下所述。 SSH最有用的功能之一就是端口转发。端口转发功能可以将任何不安全的TCP连接转换为安全的SSH连接。它也被称为SSH隧道。我们需要知道一个端口在这个上下文中。端口是TCP用户的标识符。因此,任何在TCP上运行的应用程序都有一个端口号。基于端口号将传入的TCP流量传送到适当的应用程序。应用程序可以使用多个端口号。例如,对于简单邮件传输协议(SMTP),服务器端通常在端口25上侦听,以便传入的SMTP请求使用TCP并将数据解析到目标端口25. TCP识别该地址是SMTP服务器地址,将数据路由到SMTP服务器应用程序。 图5:SSH传输层数据包交换 图5说明了端口转发的基本概念。我们有一个由端口号x标识的客户端应用程序和由端口号y标识的服务器应用程序。在某些时候,客户端应用程序调用本地TCP实体,并请求在端口y上连接到远程服务器。本地TCP实体与远程TCP实体协商TCP连接,使得连接将本地端口x链接到远程端口y。 要确保此连接,SSH配置为使SSH传输层协议分别在TCP客户端和服务器实体之间建立TCP连接,TCP端口号为a和b。通过此TCP连接建立安全的SSH隧道。从端口x的客户端的流量被重定向到本地SSH实体,并通过远程SSH实体将数据传送到端口y上的服务器应用的隧道。另一方面的交通也被重新定向。 SSH支持两种端口转发:本地转发和远程转发。本地转发允许客户端设置“劫持者”进程。此过程将拦截选定的应用程序级别的流量,并将其从不安全的TCP连接重定向到安全的SSH隧道。 SSH配置为侦听所选端口。 SSH使用所选端口抓取所有流量,并通过SSH隧道发送。另一方面,SSH服务器将传入流量发送到由客户端应用程序指定的目标端口。 以下示例应帮助澄清本地转发。假设您的桌面上有一个电子邮件客户端,并使用它从邮件服务器通过邮局协议(POP)获取电子邮件。 POP3的分配的端口号是端口110.我们可以通过以下方式保护此流量: SSH客户端建立与远程服务器的连接。选择一个未使用的本地端口号,例如9999,并配置SSH接受从端口110到服务器端口的流量。SSH客户端通知SSH服务器创建到目的地的连接,在这种情况下是邮件服务器端口110。客户端将任何位发送到本地端口9999,并将其发送到加密的SSH会话中的服务器。 SSH服务器解密传入位,并将明文发送到端口110。另一方面,SSH服务器在端口110上接收到任何位,并将它们发送到SSH会话中,并返回到客户端,客户端解密并将其发送到连接到端口9999的进程。通过远程转发,用户的SSH客户端代表服务器。客户端接收到具有给定目标端口号的流量,将流量置于正确的端口,并将其发送到用户选择的目的地。 远程转发的典型示例如下:您希望从家庭计算机访问工作中的服务器。由于工作服务器位于防火墙后面,因此不会从家庭计算机接收SSH请求。但是,从工作中可以使用远程转发设置SSH隧道。 此过程涉及以下步骤: 从工作计算机,设置SSH连接到您的家庭计算机。防火墙将允许这一点,因为它是受保护的传出连接。 配置SSH服务器侦听本地端口(如22),并通过指向远程端口的SSH连接(如2222)传送数据。 您现在可以访问家庭计算机并配置SSH以接受2222端口的流量。 您现在有一个SSH隧道,可以用于远程登录到工作服务器。 概要 SSH是最常用的密码应用程序之一。它为各种各样的任务提供了极大的灵活性和多功能性,包括远程管理,文件传输,Web开发和渗透测试。","categories":[{"name":"SSH","slug":"SSH","permalink":"http://coderpreacher.top/categories/SSH/"}],"tags":[{"name":"SSH","slug":"SSH","permalink":"http://coderpreacher.top/tags/SSH/"},{"name":"SSH协议","slug":"SSH协议","permalink":"http://coderpreacher.top/tags/SSH协议/"}]},{"title":"单元测试的艺术","slug":"单元测试的艺术","date":"2017-04-27T07:42:57.000Z","updated":"2017-04-27T12:36:34.000Z","comments":true,"path":"2017/04/27/单元测试的艺术/","link":"","permalink":"http://coderpreacher.top/2017/04/27/单元测试的艺术/","excerpt":"","text":"什么是单元测试? 单元测试是开发人员对其所实现功能的代码进行的另外编写的测试,用于检测其代码功能的完整性、正确性和其运行效率,从而提高代码质量,并且在写单元测试时发现功能代码间的依赖等设计问题,从而提高产品的可扩展性。 为什么需要单元测试?当编写项目的时刻,如果我们假设底层的代码是正确无误的,那么先是高层代码中使用了底层代码;然后这些高层代码又被更高层的代码所使用,如此往复。当基本的底层代码不再可靠时,那么必需的改动就无法只局限在底层。虽然你可以修正底层的问题,但是这些对底层代码的修改必然会影响到高层代码。于是,一个对底层代码的修正,可能会导致对几乎所有代码的一连串改动,从而使修改越来越多,也越来越复杂。从而使整个项目也以失败告终。 而单元测试的核心内涵:这个简单有效的技术就是为了令代码变得更加完美。 编写优秀的单元测试的好处 单元测试集中注意力于程序的基本组成部分,首先保证每个单元测试通过,才能使下一步把单元组装成部件并测试其正确性具有基础。单元是整个软件的构成基础,像硬件系统中的零部件一样,只有保证零部件的质量,这个设备的质量才有基础,单元的质量也是整个软件质量的基础。因此,单元测试的效果会直接影响软件的后期测试,最终在很大程度上影响到产品的质量。 单元测试可以平行开展,这样可以使多人同时测试多个单元,提高了测试的效率。 单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于调试工作。 单元的规模和复杂性特点,使单元测试中可以使用包括白盒测试的覆盖分析在内的许多测试技术,能够进行比较充分细致的测试,是整个程序测试满足语句覆盖和分支覆盖要求的基础。 单元测试的测试效果是最显而易见的。做好单元测试,不仅后期的系统集成联调或集成测试和系统测试会很顺利,节约很多时间;而且在单元测试过程中能发现一些很深层次的问题,同时还会发现一些很容易发现而在集成测试和系统测试很难发现的问题;更重要的是单元测试不仅仅是证明这些代码做了什么,是如何做的,而且证明是否做了它该做的事情而没有做不该做的事情。 单元测试的好与坏不仅直接关系到测试成本(因为如果单元测试中易发现的问题拖到后期测试发现,那么其成本将成倍数上升),而且也会直接影响到产品质量,因为可能就是由于代码中的某一个小错误就导致了整个产品的质量降低一个指标,或者导致更严重的后果。 单元测试帮助设计单元测试迫使我们从关注实现转向关注接口,编写单元测试的过程就是设计接口的过程,使单元测试通过的过程是我们编写实现的过程。我一直觉得这是单元测试最重要的好处,让我们关注的重点放在接口上而非实现的细节。 单元测试帮助编码应用单元测试会使我们主动消除和减少不必要的耦合,虽然出发点可能是为了更方便的完成单元测试,但结果通常是类型的职责更加内聚,类型间的耦合显著降低。这是已知的提升编码质量的有效手段,也是提升开发人员编码水平的有效手段。 单元测试帮助重构对于现有项目的重构,从编写单元测试开始是更好的选择。先从局部代码进行重构,提取接口进行单元测试,然后再进行类型和层次级别的重构。 事实上,单元测试是一种验证行为—— 测试和验证程序中每一项功能的正确性,为以后的开发提供支持;单元测试是一种设计行为—— 编写单元测试将使我们从调用者观察、思考,特别是要先考虑测试,这样就可把程序设计成易于调用和可测试的,并努力降低软件中的耦合,还可以使编码人员在编码时产生预测试,将程序的缺陷降低到最小;单元测试是一种编写文档的行为—— 是展示函数或类如何使用的最佳文档;单元测试具有回归性—— 自动化的单元测试有助于进行回归测试。 单元测试在设计、编码和调试上的作用足以使其成为软件开发相关人员的必备技能。 断言(Assert)断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。 使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言(Xunit/Nunit)。 使用伪对象伪对象可以解决要测试的代码中使用了无法测试的外部依赖问题,更重要的是通过接口抽象实现了低耦合。例如通过抽象IConfigurationManager接口来使用ConfigurationManager对象,看起来似乎只是为了单元测试而增加更多的代码,实际上我们通常不关心后去的配置是否是通过ConfigurationManager静态类读取的config文件,我们只关心配置的取值,此时使用IConfigurationManager既可以不依赖具体的ConfigurationManager类型,又可以在系统需要扩展时使用其他实现了IConfigurationManager接口的实现类。 使用伪对象解决外部依赖的主要步骤: 使用接口依赖取代原始类型依赖。 通过对原始类型的适配实现上述接口。 手动创建用于单元测试的接口实现类或在单元测试时使用Mock框架生成接口的实例。 手动创建的实现类完整的实现了接口,这样的实现类可以在多个测试中使用。可以选择使用Mock框架生成对应接口的实例,只需要对当前测试需要调用的方法进行模拟,通常需要根据参数进行逻辑判断,返回不同的结果。无论是手动实现的模拟类对象还是Mock生成的伪对象都称为桩对象,即Stub对象。Stub对象的本质是被测试类依赖接口的伪对象,它保证了被测试类可以被测试代码正常调用。 解决了被测试类的依赖问题,还需要解决无法直接在被测试方法上使用Assert断言的情况。此时我们需要在另一类伪对象上使用Assert,通常我们把Assert使用的模拟对象称为模拟对象,即Mock对象。Mock对象的本质是用来提供给Assert进行验证的,它保证了在无法直接使用断言时可以正常验证被测试类。 Stub和Mock对象都是伪对象,即Fake对象。 Stub或Mock对象的区分明白了就很简单,从被测试类的角度讲Stub对象,从Assert的角度讲Mock对象。然而,即使不了解相关的含义和区别也不会在使用时产生问题。比如测试邮件发送,我们通常不能直接在被测试代码上应用Assert,我们会在模拟的STMP服务器对象上应用Assert判断是否成功接收到邮件,这个SMTPServer模拟对象就是Mock对象而不是Stub对象。比如写日志,我们通常可以直接在ILogger接口的相关方法上应用Assert判断是否成功,此时的Logger对象即是Stub对象也是Mock对象。 .NET单元测试常用框架和组件XUnitXUnit是目前最为流行的.NET单元测试框架。NUnit出现的较早被广泛使用,如nopCommerce、Orchard等项目从开始就一直使用的是NUnit。XUnit目前是比NUnit更好的选择,从github上可以看到asp.net mvc等一系列的微软项目使用的就是XUnit框架。 xUnit是各种代码驱动测试框架的统称,可以测试软件的不同单元。xUnit的特点是:提供了一个自动化测试3的解决方案,无须多次编写重复的测试代码,也无须记住该测试的预期结果。 四要素: 测试Fixtures Fixture指被测试的目标。而测试Fixture是一组单元测试成功的预定条件或预期结果的设定。 测试集 测试集是一组测试用例。但同一组内的测试用例必须有相同的测试Fixture。 测试执行 单个的单元测试的执行需要按照一定的方式进行。 断言 断言是验证被测试的程序在测试中的行为或状态的一个宏4或函数。若断言失败,则代表引发异常,终止测试的继续执行。 NUnit NUnit作为xUnit家族中的.Net成员,是.NET的单元测试框架,xUnit是一套适合于多种语言的单元测试工具。它具有如下特征: 提供了API,使得我们可以创建一个带有“通过/失败”结果的重复单元。 包括了运行测试和表示结果所需的工具。 允许多个测试作为一个组在一个批处理中运行。 非常灵巧,操作简单,我们花费很少的时间即可学会并且不会给测试的程序添加额外的负担。 功能可以扩展,如果希望更多的功能,可以很容易的扩展它。 官方主页:http://www.NUnit.org MSTestMS Test框架是Visual Studio自带的测试框架,可以通过新建一个Unit Test Project工程,也可以建一个Class Libary,然后添加对Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll的引用。然后就是创建测试用例,进行测试即可。其主要特点是与Visual Studio完美集成。 MSTest、NUnit、xUnit.net 属性对照表 MSTest NUnit xUnit.net Comments [TestMethod] [Test] [Fact] Marks a test method. [TestClass] [TestFixture] n/a xUnit.net does not require an attribute for a test class; it looks for all test methods in all public (exported) classes in the assembly. [ExpectedException] [ExpectedException] Assert.Throws Record.Exception xUnit.net has done away with the ExpectedException attribute in favor of Assert.Throws. [TestInitialize] [SetUp] Constructor We believe that use of [SetUp]is generally bad. However, you can implement a parameterless constructor as a direct replacement. [TestCleanup] [TearDown] IDisposable.Dispose We believe that use of[TearDown] is generally bad. However, you can implementIDisposable.Dispose as a direct replacement. [ClassInitialize] [TestFixtureSetUp] IUseFixture<T> To get per-fixture setup, implement IUseFixture<T> on your test class. [ClassCleanup] [TestFixtureTearDown] IUseFixture<T> To get per-fixture teardown, implement IUseFixture<T> on your test class. [Ignore] [Ignore] [Fact(Skip=”reason”)] Set the Skip parameter on the[Fact] attribute to temporarily skip a test. [Timeout] [Timeout] [Fact(Timeout=n)] Set the Timeout parameter on the [Fact] attribute to cause a test to fail if it takes too long to run. Note that the timeout value for xUnit.net is in milliseconds. [TestCategory] [Category] [Trait] [TestProperty] [Property] [Trait] Set arbitrary metadata on a test [DataSource] n/a [Theory], [XxxData] Theory (data-driven test). MSTest、NUnit、xUnit.net 断言对照表 MSTest NUnit xUnit.net Comments AreEqual AreEqual Equal MSTest and xUnit.net support generic versions of this method. AreNotEqual AreNotEqual NotEqual MSTest and xUnit.net support generic versions of this method. AreNotSame AreNotSame NotSame AreSame AreSame Same Contains (on CollectionAssert) Contains Contains n/a DoAssert n/a DoesNotContain (on CollectionAssert) n/a DoesNotContain n/a n/a DoesNotThrow Ensures that the code does not throw any exceptions Fail Fail n/a xUnit.net alternative: Assert.True(false, “message”) n/a Pass n/a n/a Greater n/a xUnit.net alternative: Assert.True(x > y) n/a GreaterOrEqual n/a Inconclusive Ignore n/a n/a n/a InRange Ensures that a value is in a given inclusive range (note: NUnit and MSTest have limited support for InRange on their AreEqual methods) n/a IsAssignableFrom IsAssignableFrom n/a IsEmpty Empty IsFalse IsFalse False IsInstanceOfType IsInstanceOfType IsType n/a IsNaN n/a xUnit.net alternative: Assert.True(double.IsNaN(x)) n/a IsNotAssignableFrom n/a xUnit.net alternative: Assert.False(obj is Type); n/a IsNotEmpty NotEmpty IsNotInstanceOfType IsNotInstanceOfType IsNotType IsNotNull IsNotNull NotNull IsNull IsNull Null IsTrue IsTrue True n/a Less n/a xUnit.net alternative: Assert.True(x < y) n/a LessOrEqual n/a n/a n/a NotInRange Ensures that a value is not in a given inclusive range n/a Throws Throws Ensures that the code throws an exact exception n/a IsAssignableFrom n/a n/a IsNotAssignableFrom n/a","categories":[{"name":"Unit Test","slug":"Unit-Test","permalink":"http://coderpreacher.top/categories/Unit-Test/"}],"tags":[{"name":"测试","slug":"测试","permalink":"http://coderpreacher.top/tags/测试/"},{"name":"单元测试","slug":"单元测试","permalink":"http://coderpreacher.top/tags/单元测试/"},{"name":"Unit Test","slug":"Unit-Test","permalink":"http://coderpreacher.top/tags/Unit-Test/"},{"name":"Test","slug":"Test","permalink":"http://coderpreacher.top/tags/Test/"}]},{"title":"为什么你应该从Svn切换到Git","slug":"为什么你应该从Svn切换到Git","date":"2017-04-26T14:26:05.000Z","updated":"2017-04-27T04:32:07.000Z","comments":true,"path":"2017/04/26/为什么你应该从Svn切换到Git/","link":"","permalink":"http://coderpreacher.top/2017/04/26/为什么你应该从Svn切换到Git/","excerpt":"","text":"Git介绍Git是一款免费、开源的目前世界上最先进的分布式版本控制系统(没有之一),用于敏捷高效地处理任何或小或大的项目, Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 Git的诞生很多人都知道,Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了。 Linus虽然创建了Linux,但Linux的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为Linux编写代码,那Linux的代码是如何管理的呢? 事实是,在2002年以前,世界各地的志愿者把源代码文件通过diff的方式发给Linus,然后由Linus本人通过手工方式合并代码! 你也许会想,为什么Linus不把Linux代码放到版本控制系统里呢?不是有CVS、SVN这些免费的版本控制系统吗?因为Linus坚定地反对CVS和SVN,这些集中式的版本控制系统不但速度慢,而且必须联网才能使用。有一些商用的版本控制系统,虽然比CVS、SVN好用,但那是付费的,和Linux的开源精神不符。 不过,到了2002年,Linux系统已经发展了十年了,代码库之大让Linus很难继续通过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,于是Linus选择了一个商业的版本控制系统BitKeeper,BitKeeper的东家BitMover公司出于人道主义精神,授权Linux社区免费使用这个版本控制系统。 安定团结的大好局面在2005年就被打破了,原因是Linux社区牛人聚集,不免沾染了一些梁山好汉的江湖习气。开发Samba的Andrew试图破解BitKeeper的协议(这么干的其实也不只他一个),被BitMover公司发现了(监控工作做得不错!),于是BitMover公司怒了,要收回Linux社区的免费使用权。 Linus可以向BitMover公司道个歉,保证以后严格管教弟兄们,嗯,这是不可能的。实际情况是这样的: Linus花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!牛是怎么定义的呢?大家可以体会一下。 Git迅速成为最流行的分布式版本控制系统,尤其是2008年,GitHub网站上线了,它为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub,包括jQuery,PHP,Ruby等等。 历史就是这么偶然,如果不是当年BitMover公司威胁Linux社区,可能现在我们就没有免费而超级好用的Git了。 集中式vs分布式Linus一直痛恨的CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢? 先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。 集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 那分布式版本控制系统与集中式版本控制系统有何不同呢?首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 当然,Git的优势不单是不必联网这么简单,后面我们还会看到Git极其强大的分支管理,把SVN等远远抛在了后面。 CVS作为最早的开源而且免费的集中式版本控制系统,直到现在还有不少人在用。由于CVS自身设计的问题,会造成提交文件不完整,版本库莫名其妙损坏的情况。同样是开源而且免费的SVN修正了CVS的一些稳定性问题,是目前用得最多的集中式版本库控制系统。 除了免费的外,还有收费的集中式版本控制系统,比如IBM的ClearCase(以前是Rational公司的,被IBM收购了),特点是安装比Windows还大,运行比蜗牛还慢,能用ClearCase的一般是世界500强,他们有个共同的特点是财大气粗,或者人傻钱多。 微软自己也有一个集中式版本控制系统叫VSS,集成在Visual Studio中。由于其反人类的设计,连微软自己都不好意思用了。 分布式版本控制系统除了Git以及促使Git诞生的BitKeeper外,还有类似Git的Mercurial和Bazaar等。这些分布式版本控制系统各有特点,但最快、最简单也最流行的依然是Git! 轻量级分支:无摩擦上下文切换在我开始解释这之前,这实际上是我最喜欢的Git功能,我需要你帮我一个忙。忘记你对分支机构的了解。您对Subversion中“分支”意味着什么的知识是有毒的,特别是如果您在1.5之前内部化,就像我一样,在Subversion终于增加了一些基本的合并跟踪功能之前。忘记合并多么痛苦,忘记切换分支花费多长时间,忘记从不同一个分支合并的可能性–Git在分支和合并方面给你一个全新的世界。 在Git中,分支机构不是一个肮脏的词汇 - 它们经常被使用并经常合并,在许多情况下,开发人员将为每个功能创建一个功能,并且每天可以将它们合并在一起,并且通常是无痛的。这是Git首先吸引我的,实际上改变了我对我发展的整个方式。 当您在Git中创建分支时,它会在本地进行,并且发生得非常快。以下是创建一个分支,然后切换到新分支以开始开发的示例。 拉请求(Pull Requests)许多源代码管理工具,比如Bitbucket,可以通过拉请求来增强核心的Git功能。 拉请求是要求另一开发人员将您的一个分支合并到其存储库中的一种方式。 这不仅使项目潜在客户能够更轻松地跟踪变更情况,还可以让开发人员在将其与其他代码集成在一起之前展开工作。 由于它们本质上是附加到要素分支的注释线程,所以拉请求是非常通用的。 当开发者遇到困难的问题时,他们可以打开一个拉动请求,要求其他团队的帮助。 或者,初级开发人员可以相信,他们不会通过将拉请求视为正式代码审查来破坏整个项目。 Reference: why you should switch from subversion to git。","categories":[{"name":"git","slug":"git","permalink":"http://coderpreacher.top/categories/git/"}],"tags":[{"name":"版本控制","slug":"版本控制","permalink":"http://coderpreacher.top/tags/版本控制/"},{"name":"Git","slug":"Git","permalink":"http://coderpreacher.top/tags/Git/"},{"name":"Subversion","slug":"Subversion","permalink":"http://coderpreacher.top/tags/Subversion/"},{"name":"GitHub","slug":"GitHub","permalink":"http://coderpreacher.top/tags/GitHub/"}]},{"title":"分布式一致性协议","slug":"分布式一致性协议","date":"2017-04-17T14:11:02.000Z","updated":"2017-04-19T14:27:28.000Z","comments":true,"path":"2017/04/17/分布式一致性协议/","link":"","permalink":"http://coderpreacher.top/2017/04/17/分布式一致性协议/","excerpt":"","text":"一致性协议为了解决分布式一致性问题,在长期的探索研究的过程中,涌现出了一大批经典的一致性协议和算法,其中最著名的就是二阶段、三阶段提交协议和Paxos算法。 2PC与3PC在分布式系统中,每一个机器节点虽然都能明确的知道自己执行的事务是成功还是失败,但是却无法知道其他分布式节点的事务执行情况。因此,当一个事务要跨越多个分布式节点的时候(比如,淘宝下单流程,下单系统和库存系统可能就是分别部署在不同的分布式节点中),为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。 2PC(Two Phase Commitment Protocol)2PC,是 Two-phase commit的缩写,即二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事物处理过程中能够保持原子性和一致性而设计的一种算法。通常,二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前,绝大部分关系型数据库都是采用二阶段提交协议来完成分布式事务处理的,利用该协议能够非常方便的完成所有分布式参与者的协调,统一决定事物的提交或回滚,从而能够有效的保证分布式数据一致性,因此二阶段提交协议被广泛的应用在许多分布式系统中。 协议说明 (第一阶段)提交请求阶段: 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作) 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。 (第二阶段)提交执行阶段: 当协调者节点从所有参与者节点获得的相应消息都为”同意”时: 协调者节点向所有参与者节点发出”正式提交(commit)”的请求。 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。 参与者节点向协调者节点发送”完成”消息。 协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。 如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时: 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。 参与者节点向协调者节点发送”回滚完成”消息。 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。 不管最后结果如何,第二阶段都会结束当前事务。 缺点 1、执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。 2、参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制) 3、协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。 4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。 3PC(Three-phase commit protocol)3PC,是 Three-phase commit的缩写,即三阶段提交,是2PC的改进版,其将二阶段的”提交请求阶段”一分为二,形成了由CanCommit,PreCommit和do Commit三个阶段组成的一致性协议。引入超时机制。同时在协调者和参与者中都引入超时机制(如下图)。 协议说明阶段一:CanCommit 1.事务询问。 协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 2.各参与者向协调者反馈事务询问的响应。 参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。 阶段二:PreCommit在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能。 执行事务预提交 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。 1.发送预提交请求。 协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段。 2.事务预提交。 参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。 3.各参与者向协调者反馈事务执行的响应。 如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)。 中断事务 假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。 1.发送中断请求。 协调者向所有参与者节点发出abort请求。 2.中断事务。 无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务。 阶段三:doCommit该阶段将进行真正的事务提交,会存在以下两种可能的情况。 执行提交 1.发送提交请求。 进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求。 2.事务提交。 参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。 3.反馈事务提交结果。 参与者在完成事务提交之后,向协调者发送Ack消息。 4.完成事务。 协调者接收到所有参与者反馈的Ack消息后,完成事务。 中断事务 进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。 1.发送中断请求。 协调者向所有的参与者节点发送abort请求。 2.事务回滚。 参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。 3.反馈事务回滚结果。 参与者在完成事务回滚之后,向协调者发送Ack消息。 4.中断事务。 协调者接收到所有参与者反馈的Ack消息后,中断事务。 需要注意的是,一旦进入阶段三,可能会存在以下两种故障。 协调者出现问题。 协调者和参与者之间的网络出现故障。 无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。 优缺点三阶段提交协议的优点:相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。 三阶段提交协议的缺点:三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。 Paxos算法 Paxos算法是莱斯利·兰伯特(Leslie Lamport)1990年提出的一种基于消息传递的一致性算法。Paxos算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。在工程实践意义上来说,就是可以通过Paxos实现多副本一致性,分布式锁,名字管理,序列号分配等。比如,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。本文首先会讲原始的Paxos算法(Basic Paxos),主要描述二阶段提交过程,然后会着重讲Paxos算法的变种(Multi Paxos),它是对Basic Paxos的优化,而且更适合工程实践,最后我会通过Q&A的方式,给出我在学习Paxos算法中的疑问,以及我对这些疑问的理解。 概念与术语Proposer:提议发起者,处理客户端请求,将客户端的请求发送到集群中,以便决定这个值是否可以被批准。 Acceptor:提议批准者,负责处理接收到的提议,他们的回复就是一次投票,会存储一些状态来决定是否接收一个值。 Replica:节点或者副本,分布式系统中的一个server,一般是一台单独的物理机或者虚拟机,同时承担paxos中的提议者和接收者角色。 ProposalId:每个提议都有一个编号,编号高的提议优先级高。 Paxos Instance:Paxos中用来在多个节点之间对同一个值达成一致的过程,比如同一个日志序列号:logIndex,不同的logIndex属于不同的Paxos Instance。 acceptedProposal:在一个Paxos Instance内,已经接收过的提议。 acceptedValue:在一个Paxos Instance内,已经接收过的提议对应的值。 minProposal:在一个Paxos Instance内,当前接收的最小提议值,会不断更新。 Basic-Paxos算法基于Paxos协议构建的系统,只需要系统中超过半数的节点在线且相互通信正常即可正常对外提供服务。它的核心实现Paxos Instance主要包括两个阶段:准备阶段(prepare phase)和提议阶段(accept phase)。如下图所示: 获取一个ProposalId,为了保证ProposalId递增,可以采用时间戳+serverId方式生成; 提议者向所有节点广播prepare(n)请求; 接收者比较n和minProposal,如果n>minProposal,表示有更新的提议,minProposal=n;否则将(acceptedProposal,acceptedValue)返回; 提议者接收到过半数请求后,如果发现有acceptedValue返回,表示有更新的提议,保存acceptedValue到本地,然后跳转1,生成一个更高的提议; 到这里表示在当前paxos instance内,没有优先级更高的提议,可以进入第二阶段,广播accept(n,value)到所有节点; 接收者比较n和minProposal,如果n>=minProposal,则acceptedProposal=minProposal=n,acceptedValue=value,本地持久化后,返回;否则,返回minProposal 提议者接收到过半数请求后,如果发现有返回值>n,表示有更新的提议,跳转1;否则value达成一致。从上述流程可知,并发情况下,可能会出现第4步或者第7步频繁重试的情况,导致性能低下,更严重者可能导致永远都无法达成一致的情况,就是所谓的“活锁”,如下图所示: S1作为提议者,发起prepare(3.1),并在S1,S2和S3达成多数派; 随后S5作为提议者 ,发起了prepare(3.5),并在S3,S4和S5达成多数派; S1发起accept(3.1,value1),由于S3上提议 3.5>3.1,导致accept请求无法达成多数派,S1尝试重新生成提议 S1发起prepare(4.1),并在S1,S2和S3达成多数派 S5发起accpet(3.5,value5),由于S3上提议4.1>3.5,导致accept请求无法达成多数派,S5尝试重新生成提议 S5发起prepare(5.5),并在S3,S4和S5达成多数派,导致后续的S1发起的accept(4.1,value1)失败…… prepare阶段的作用从Basic-Paxos的描述可知,需要通过两阶段来最终确定一个值,由于轮回多,导致性能低下,至少两次网络RTT。那么prepare阶段能否省去?如下图所示: S1首先发起accept(1,red),并在S1,S2和S3达成多数派,red在S1,S2,S3上持久化 随后S5发起accept(5,blue),对于S3而言,由于接收到更新的提议,会将acceptedValue值改为blue 那么S3,S4和S5达成多数派,blue在S3,S4和S5持久化 最后的结果是,S1和S2的值是red,而S3,S4和S5的值是blue,没有达成一致。 所以两阶段必不可少,Prepare阶段的作用是阻塞旧的提议,并且返回已经接收到的acceptedProposal。同时也可以看到的是,假设只有S1提议,则不会出现问题,这就是我们下面要讲的Multi-Paxos。 Multi-paxos算法 Paxos是对一个值达成一致,Multi-Paxos是连续多个paxos instance来对多个值达成一致,这里最核心的原因是multi-paxos协议中有一个Leader。Leader是系统中唯一的Proposal,在lease租约周期内所有提案都有相同的ProposalId,可以跳过prepare阶段,议案只有accept过程,一个ProposalId可以对应多个Value,所以称为Multi-Paxos。 选举首先我们需要有一个leader,其实选主的实质也是一次Paxos算法的过程,只不过这次Paxos确定的“谁是leader”这个值。由于任何一个节点都可以发起提议,在并发情况下,可能会出现多主的情况,比如A,B先后当选为leader。为了避免频繁选主,当选leader的节点要马上树立自己的leader权威(让其它节点知道它是leader),写一条特殊日志(start-working日志)确认其身份。根据多数派原则,只有一个leader的startworking日志可以达成多数派。leader确认身份后,可以通过了lease机制(租约)维持自己的leader身份,使得其它proposal不再发起提案,这样就进入了leader任期,由于没有并发冲突,因此可以跳过prepare阶段,直接进入accept阶段。通过分析可知,选出leader后,leader任期内的所有日志都只需要一个网络RTT(Round Trip Time)即可达成一致。 新主恢复流程由于Paxos中并没有限制,任何节点都可以参与选主并最终成为leader,这就无法保证新选出的leader包含了所有日志,可能存在空洞,因此在真正提供服务前,还存在一个获取所有已提交日志的恢复过程。新主向所有成员查询最大logId的请求,收到多数派响应后,选择最大的logId作为日志恢复结束点,这里多数派的意义在于恢复结束点包含了所有达成一致的日志,当然也可能包含了没有达成多数派的日志。拿到logId后,从头开始对每个logId逐条进行paxos协议,因为在新主获得所有日志之前,系统是无法提供服务的。为了优化,引入了confirm机制,就是将已经达成一致的logId告诉其它acceptor,acceptor写一条confirm日志到日志文件中。那么新主在重启后,扫描本地日志,对于已经拥有confirm日志的log,就不会重新发起paxos了。同样的,在响应客户端请求时,对于没有confirm日志的log,需要重新发起一轮paxos。由于没有严格要求confirm日志的位置,可以批量发送。为了确保重启时,不需要对太多已提价的log进行paxos,需要将confirm日志与最新提交的logId保持一定的距离。 性能优化Basic-Paxos一次日志确认,需要至少2次磁盘写操作(prepare,promise)和2次网络RTT(prepare,promise)。Multi-Paxos利用一阶段提交(省去Prepare阶段),将一次日志确认缩短为一个RTT和一次磁盘写;通过confirm机制,可以缩短新主的恢复时间。为了提高性能,我们还可以实现一批日志作为一个组提交,要么成功一批,要么都不成功,这点类似于group-commit,通过RT换取吞吐量。 安全性(异常处理) Leader异常Leader在任期内,需要定期给各个节点发送心跳,已告知它还活着(正常工作),如果一个节点在超时时间内仍然没有收到心跳,它会尝试发起选主流程。Leader异常了,则所有的节点先后都会出现超时,进入选主流程,选出新的主,然后新主进入恢复流程,最后再对外提供服务。我们通常所说的异常包括以下三类: 进程crash(OS crash) Leader进程crash和Os crash类似,只要重启时间大于心跳超时时间都会导致节点认为leader挂了,触发重新选主流程。 节点网络异常(节点所在网络分区) Leader网络异常同样会导致其它节点收不到心跳,但有可能leader是活着的,只不过发生了网络抖动,因此心跳超时不能设置的太短,否则容易因为网络抖动造成频繁选主。另外一种情况是,节点所在的IDC发生了分区,则同一个IDC的节点相互还可以通信,如果IDC中节点能构成多数派,则正常对外服务,如果不能,比如总共4个节点,两个IDC,发生分区后会发现任何一个IDC都无法达成多数派,导致无法选出主的问题。因此一般Paxos节点数都是奇数个,而且在部署节点时,IDC节点的分布也要考虑。 磁盘故障 前面两种异常,磁盘都是OK的,即已接收到的日志以及对应confirm日志都在。如果磁盘故障了,节点再加入就类似于一个新节点,上面没有任何日志和Proposal信息。这种情况会导致一个问题就是,这个节点可能会promise一个比已经promise过的最大proposalID更小的proposal,这就违背了Paxos原则。因此重启后,节点不能参与Paxos Instance,它需要先追上Leader,当观察到一次完整的paxos instance时该节点结束不能promise/ack状态。 Follower异常(宕机,磁盘损坏等)对于Follower异常,则处理要简单的多,因为follower本身不对外提供服务(日志可能不全),对于leader而言,只要能达成多数派,就可以对外提供服务。follower重启后,没有promise能力,直到追上leader为止。","categories":[{"name":"分布式","slug":"分布式","permalink":"http://coderpreacher.top/categories/分布式/"}],"tags":[{"name":"分布式系统","slug":"分布式系统","permalink":"http://coderpreacher.top/tags/分布式系统/"},{"name":"分布式一致性","slug":"分布式一致性","permalink":"http://coderpreacher.top/tags/分布式一致性/"},{"name":"分布式事务","slug":"分布式事务","permalink":"http://coderpreacher.top/tags/分布式事务/"},{"name":"2PC","slug":"2PC","permalink":"http://coderpreacher.top/tags/2PC/"},{"name":"3PC","slug":"3PC","permalink":"http://coderpreacher.top/tags/3PC/"}]},{"title":"docker 介绍","slug":"docker-介绍","date":"2017-04-14T13:41:18.000Z","updated":"2017-04-14T15:55:18.000Z","comments":true,"path":"2017/04/14/docker-介绍/","link":"","permalink":"http://coderpreacher.top/2017/04/14/docker-介绍/","excerpt":"","text":"Docker 介绍如果您是程序员或技术人员,您至少可以听说Docker:一个有用的工具,用于在“容器”中打包,运送和运行应用程序。这很难让所有的注意力得到 这些天 - 从开发人员和系统管理员一样。 即使像Google,VMware和亚马逊这样的大公司也在建设服务来支持它。 Docker是一个新的容器化的技术,它轻巧,且易移植,号称“build once, configure once and run anywhere”。 无论您是否对Docker有进行使用过,我仍然认为了解一些关于“容器”的基本概念以及如何与虚拟机(VM)进行比较是非常重要的。 虽然互联网充满了Docker的优秀使用指南,但我找不到许多初学者友好的概念指南,特别是在容器组成的方面。 所以,希望这篇文章会解决这个问题。 我们先来了解什么VM和容器呢? 什么是“容器”和“虚拟机”?容器和虚拟机的目标是相似的:将应用程序及其依赖项隔离成可以在任何地方运行的独立单元。此外,容器和虚拟机不再需要物理硬件,从而在能源消耗和成本效益方面更有效地利用计算资源。容器和虚拟机之间的主要区别在于它们的架构方法。 我们来看看吧。 虚拟机虚拟机本质上是一个真正的计算机的仿真,它执行像真正的计算机那样的程序。虚拟机使用“虚拟机监控程序”运行在物理机的顶部。管理程序又可以在主机或“裸机”上运行。我们来解释这个行话:虚拟机管理程序是虚拟机在其上运行的一个软件,固件或硬件。虚拟机管理程序本身运行在物理计算机上,被称为“主机”。主机为VM提供资源,包括RAM和CPU。这些资源在虚拟机之间划分,可以根据您的需要进行分发。因此,如果一个虚拟机正在运行资源较多的应用程序,则可能会为在同一主机上运行的其他虚拟机分配更多的资源。在主机上运行的虚拟机(再次使用虚拟机管理程序)通常也称为“客户机”。此客机包含应用程序以及运行该应用程序所需的任何应用程序(例如系统二进制程序和库)。它还具有自己的整个虚拟化硬件堆栈,包括虚拟化网络适配器,存储和CPU - 这意味着它也有自己的成熟的客户操作系统。从内部,客机作为自己的单位,拥有自己的专属资源。从外面,我们知道这是一个VM - 共享主机提供的资源。 如上所述,客机可以在托管管理程序或裸机管理程序上运行。他们之间有一些重要的区别。首先,托管虚拟化管理程序在主机的操作系统上运行。例如,运行OSX的计算机可以在该OS之上安装VM(例如VirtualBox或VMware Workstation 8)。 VM不能直接访问硬件,所以它必须经过主机操作系统(在我们的例子中是Mac的OSX)。托管管理程序的好处是底层硬件不那么重要。主机的操作系统负责硬件驱动程序而不是管理程序本身,因此被认为具有更多的“硬件兼容性”。另一方面,硬件和管理程序之间的这个附加层会产生更多的资源开销,从而降低虚拟机的性能。裸机管理程序环境通过在主机硬件上安装和运行来解决性能问题。因为它直接与底层硬件接口,所以不需要主机操作系统来运行。在这种情况下,作为操作系统安装在主机服务器上的第一件事就是管理程序。与托管虚拟机管理程序不同,裸机管理程序具有自己的设备驱动程序,并直接与每个组件进行交互,用于任何I / O,处理或特定于操作系统的任务。这导致更好的性能,可扩展性和稳定性。这里的折衷是硬件兼容性受到限制,因为管理程序只能在其中内置许多设备驱动程序。所有这些谈论虚拟机管理程序之后,您可能会想知道为什么我们需要在VM和主机之间的这个额外的“虚拟机管理程序”层。那么,由于虚拟机具有自己的虚拟操作系统,虚拟机管理程序在为虚拟机提供一个管理和执行客户机操作系统的平台方面发挥重要作用。它允许主机计算机在作为其上的客户端运行的虚拟机之间共享其资源。 如图所示,虚拟机将虚拟硬件,内核(即OS)和每个新虚拟机的用户空间进行打包。 容器与提供硬件虚拟化的虚拟机不同,容器通过抽象“用户空间”来提供操作系统级的虚拟化。 当我们解开容器术语时,你会看到我的意思。出于所有目的和目的,容器看起来像一个虚拟机。 例如,它们具有用于处理的私有空间,可以以root身份执行命令,具有专用网络接口和IP地址,允许自定义路由和iptable规则,可以挂载文件系统等。 容器和虚拟机之间的一个很大的区别是容器与其他容器共享主机系统的内核。 该图显示了容器仅包含用户空间,而不是像VM那样的内核或虚拟硬件。 每个容器都拥有自己的隔离用户空间,允许多个容器在单个主机上运行。 我们可以看到,所有的操作系统级架构正在容器间共享。 从头创建的唯一部分是bin和libs。 这就是容器如此轻便。 Docker从哪里入手?Docker是一个基于Linux容器的开源项目。 它使用Linux内核功能(如命名空间和控制组)来在操作系统之上创建容器。集装箱距离不远; Google多年来一直在使用自己的集装箱技术。 其他Linux容器技术包括已经存在多年的Solaris Zones,BSD监狱和LXC。 是什么原因让Docker变得如此受欢迎呢? 易于使用:Docker使开发人员,系统管理员,架构师和其他人更容易利用容器来快速构建和测试便携式应用程序。它允许任何人在他们的笔记本电脑上打包应用程序,而这些应用程序又可以在任何公共云,私有云甚至裸机上运行。咒语是:“建立一次,在任何地方运行”。 速度:Docker容器非常轻便和快速。由于容器只是在内核上运行的沙盒环境,因此它们占用的资源较少。与可能需要更长时间的VM相比,您可以在几秒钟内创建和运行Docker容器,因为每次都需要启动完整的虚拟操作系统。 Docker Hub:Docker用户也受益于Docker Hub日益丰富的生态系统,您可以将其视为“Docker镜像的应用商店”。Docker Hub拥有成千上万的社区创建的公共图片,可随时获得用来。搜索满足您需求的镜像非常容易,随时可以下拉和使用,无需修改。 模块化和可扩展性:Docker可以轻松地将应用程序的功能分解成单个容器。例如,您的Postgres数据库可能会在一个容器中运行,并且您的Redis服务器在另一个容器中运行,而Node.js应用程序位于另一容器中。使用Docker,将这些容器链接到一起创建应用程序变得更加容易,以便将来可以轻松地自动扩展或更新组件。 基本的Docker概念现在我们已经有了很大的发展空间,我们先看一下Docker的基本部分: Docker引擎Docker引擎是Docker运行的层。 它是一个轻量级的运行时和工具,用于管理容器,镜像,构建等。 它在Linux系统上本机运行,由以下组成: 在主机中运行的Docker守护进程。 一个Docker客户端,然后与Docker守护进程通信以执行命令。 一个用于远程与Docker守护进行交互的REST API。 Docker客户端Docker客户端是您作为Docker的最终用户进行通信的对象。 认为它是Docker的UI。 例如,当你做 1docker build iampeekay/someImage . 您正在与Docker客户端通信,Docker客户端会将您的指令传达给Docker守护进程。 Docker守护进程Docker守护程序是实际执行发送到Docker Client的命令,如构建,运行和分发容器。 Docker守护程序在主机上运行,但作为用户,您不会直接与守护进程通信。 Docker客户端也可以在主机上运行,但不需要。 它可以在不同的机器上运行,并与主机上运行的Docker守护程序进行通信。 DockerfileDockerfile是您编写构建Docker镜像的说明的地方。 这些说明可以是: RUN apt-get y install some-package:安装一个软件包; EXPOSE 8000: 对外开放端口; ENV ANT_HOME /usr/local/apache-ant 传递一个环境变量;等等。一旦设置了Dockerfile,就可以使用docker build命令来构建一个镜像。 以下是Docker文件的示例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384# Start with ubuntu 14.04FROM ubuntu:14.04MAINTAINER preethi kasireddy iam.preethi.k@gmail.com# For SSH access and port redirectionENV ROOTPASSWORD sample# Turn off prompts during installationsENV DEBIAN_FRONTEND noninteractiveRUN echo "debconf shared/accepted-oracle-license-v1-1 select true" | debconf-set-selectionsRUN echo "debconf shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections# Update packagesRUN apt-get -y update# Install system tools / librariesRUN apt-get -y install python3-software-properties \\ software-properties-common \\ bzip2 \\ ssh \\ net-tools \\ vim \\ curl \\ expect \\ git \\ nano \\ wget \\ build-essential \\ dialog \\ make \\ build-essential \\ checkinstall \\ bridge-utils \\ virt-viewer \\ python-pip \\ python-setuptools \\ python-dev# Install Node, npmRUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -RUN apt-get install -y nodejs# Add oracle-jdk7 to repositoriesRUN add-apt-repository ppa:webupd8team/java# Make sure the package repository is up to dateRUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list# Update aptRUN apt-get -y update# Install oracle-jdk7RUN apt-get -y install oracle-java7-installer# Export JAVA_HOME variableENV JAVA_HOME /usr/lib/jvm/java-7-oracle# Run sshdRUN apt-get install -y openssh-serverRUN mkdir /var/run/sshdRUN echo "root:$ROOTPASSWORD" | chpasswdRUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config# SSH login fix. Otherwise user is kicked off after loginRUN sed 's@session\\s*required\\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd# Expose Node.js app portEXPOSE 8000# Create tap-to-android app directoryRUN mkdir -p /usr/src/my-appWORKDIR /usr/src/my-app# Install app dependenciesCOPY . /usr/src/my-appRUN npm install# Add entrypointADD entrypoint.sh /entrypoint.shRUN chmod +x /entrypoint.shENTRYPOINT ["/entrypoint.sh"]CMD ["npm", "start"] Docker镜像镜像是从您的Dockerfile中写入的一组说明构建的只读模板。 镜像定义了您想要的打包应用程序及其依赖关系,看起来像和启动时要运行的进程。 Docker镜像使用Dockerfile构建。 Dockerfile中的每个指令都为镜像添加了一个新的“镜像层”,其中镜像层表示镜像文件系统的一部分,它们添加到或替换镜像层下面的镜像层。 层是Docker轻巧而强大结构的关键。 Docker使用Union File System来实现: 联合文件系统(Union File Systems)Docker使用Union File Systems来构建镜像。 您可以将Union File System视为可堆叠文件系统,这意味着单独文件系统(称为分支)的文件和目录可以透明地叠加以形成单个文件系统。 在重叠的分支中具有相同路径的目录的内容被视为单个合并目录,这避免了需要创建每个层的单独副本。 相反,它们都可以被赋予相同资源的指针; 当某些层需要修改时,它会创建一个副本并修改一个本地副本,保留原来的不变。 这就是文件系统如何可以可写,而不实际允许写入。 (换句话说,是一个“写时复制”系统。) 分层系统提供两个主要优点: 无复制:每次使用镜像创建和运行新容器时,镜像层有助于避免复制一组完整的文件,从而实现Docker容器的快速便宜。 层隔离:进行更改更快 - 当您更改镜像时,Docker只会将更新传播到已更改的层。 卷(Volumes)卷是容器的“数据”部分,在容器创建时初始化。 卷允许您持久存储并共享容器的数据。 数据卷与默认的Union File System分开,并且作为主机文件系统上的普通目录和文件存在。 因此,即使您销毁,更新或重建容器,数据卷将保持不变。 当您要更新卷时,您可以直接对其进行更改。 (另外,数据量可以在多个容器之间共享和重复使用,这是非常整齐的。) Docker容器如上所述,Docker容器将应用程序的软件包装到与应用程序需要运行的所有内容的不可见框中。 这包括操作系统,应用程序代码,运行时,系统工具,系统库等。Docker容器是由Docker镜像构建的。 由于镜像是只读的,Docker在镜像的只读文件系统上添加了一个读写文件系统来创建一个容器。 此外,然后创建容器,Docker创建一个网络接口,以便容器可以与本地主机通信,将可用的IP地址附加到容器,并在定义镜像时执行您指定运行应用程序的进程。成功创建容器后,可以在任何环境中运行它,而无需进行更改。 Docker支柱总是让我好奇的一件事是容器如何实际实现,特别是因为容器周围没有任何抽象的基础设施边界。 经过很多阅读,这一切都很有意义,所以这里是我尝试向你解释的! : 术语“容器”实际上只是一个抽象的概念来描述几个不同的特征如何协同工作来可视化“容器”。 让我们快速过一过这些知识点: 1 命名空间(Namespaces) 命名空间为容器提供了自己对底层Linux系统的视图,限制了容器可以看到和访问的内容。运行容器时,Docker创建特定容器将使用的命名空间。 Docker使用的内核中有几种不同类型的命名空间,例如: NET:提供具有自己的系统网络堆栈视图(例如其自己的网络设备,IP地址,IP路由表,/ proc / net目录,端口号等)的容器。 PID:PID代表进程ID。如果您在命令行中运行过ps aux以检查系统上正在运行哪些进程,那么您将看到一个名为“PID”的列。 PID命名空间给容器提供他们可以查看和交互的进程的自己的范围视图,包括一个独立的init(PID 1),它是“所有进程的祖先”。 MNT:给系统自己的“mounts”视图的容器。因此,不同安装名称空间中的进程对文件系统层次结构具有不同的视图。 UTS:UTS代表UNIX分时系统。它允许进程识别系统标识符(即主机名,域名等)。 UTS允许容器具有与其他容器和主机系统无关的自己的主机名和NIS域名。 IPC:IPC代表InterProcess Communication。 IPC命名空间负责在每个容器之间运行的进程之间隔离IPC资源。 USER:此命名空间用于隔离每个容器中的用户。与主机系统相比,它允许容器具有不同的uid(用户ID)和gid(组ID)范围的视图。因此,进程的uid和gid在用户命名空间内外可能会有所不同,这也允许进程在容器之外拥有无特权用户,而不会牺牲容器内的root权限。 Docker将这些命名空间一起使用,以便隔离并开始创建容器。 2 控制组(Control groups) 控制组(也称为cgroups)是一个Linux内核功能,可以隔离,优先排列和记录一组进程的资源使用情况(CPU,内存,磁盘I / O,网络等)。在这个意义上,一个cgroup可以确保Docker容器只能使用他们需要的资源,如果需要,可以设置容器*可以使用什么资源的限制。 Cgroups还确保单个容器不会耗尽其中一个资源并将整个系统关闭。 Docker的未来:Docker和VM将共存 Docker肯定会获得很大的收益,但我不相信它将成为虚拟机的真正威胁。容器将继续获得成功,但有许多使用虚拟机仍然更适合的用例。例如,如果您需要在多个服务器上运行多个应用程序,则使用虚拟机可能是有意义的。另一方面,如果您需要运行多个副本的单个应用程序,Docker提供了一些引人注目的优势。此外,Docker允许您将应用程序分解成更多功能分立的部件,从而创建分离的关注点,这也意味着越来越多的部件要管理,这可能会变得笨重。Docker容器的安全性也是一个令人关切的问题,因为容器共享相同的内核,容器间的隔离更薄。完整的VM只能向主机管理程序发出超级呼叫,Docker容器可以将系统调用到主机内核,从而创建更大的攻击面。当安全性特别重要时,开发人员可能会选择通过抽象硬件隔离的虚拟机,使得彼此之间的干扰更加困难。","categories":[{"name":"Docker","slug":"Docker","permalink":"http://coderpreacher.top/categories/Docker/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://coderpreacher.top/tags/Docker/"},{"name":"Linux","slug":"Linux","permalink":"http://coderpreacher.top/tags/Linux/"},{"name":"Virtualization","slug":"Virtualization","permalink":"http://coderpreacher.top/tags/Virtualization/"},{"name":"虚拟机","slug":"虚拟机","permalink":"http://coderpreacher.top/tags/虚拟机/"},{"name":"容器","slug":"容器","permalink":"http://coderpreacher.top/tags/容器/"}]},{"title":"WebSockets 介绍","slug":"WebSockets-介绍","date":"2017-04-13T14:11:37.000Z","updated":"2017-04-13T14:59:25.000Z","comments":true,"path":"2017/04/13/WebSockets-介绍/","link":"","permalink":"http://coderpreacher.top/2017/04/13/WebSockets-介绍/","excerpt":"","text":"WebSockets介绍WebSocket是允许客户端和服务器/端点之间使用单个TCP连接进行通信的协议。听起来有点像http不是吗? WebSocket通过HTTP的优势是协议是全双工的(允许同时进行双向通信),它的头部比HTTP头部要小得多,因此即使在小数据包上也能实现更高效的通信。 WebSocket的生命周期也很容易理解: 客户端以HTTP升级标头的形式向服务器发送握手请求,并提供有关其尝试连接到的WebSocket的数据。 服务器使用另一个HTTP头响应请求,这是WebSocket连接中最后一次使用HTTP头。如果握手成功,则服务器发送HTTP头,告知客户端切换到WebSocket协议。 现在打开一个常量连接,客户端和服务器可以在连接关闭之前发送任何数量的消息。这些消息只有大约2字节的开销。 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。 轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。 Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。 这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。 WebSockets是如何工作的?WebSockets提供客户端和服务器之间的持久连接,双方可以随时使用该连接开始发送数据。 客户端通过称为WebSocket握手的进程建立WebSocket连接。 此过程从客户端向服务器发送常规HTTP请求开始。 此请求中包含升级标头,通知服务器客户端希望建立WebSocket连接。 以下是初始请求标头的简化示例。 12345GET ws://websocket.example.com/ HTTP/1.1Origin: http://example.comConnection: UpgradeHost: websocket.example.comUpgrade: websocket 注意:WebSocket URL使用ws方案。 对于安全的WebSocket连接,也是相当于HTTPS的wss。 如果服务器支持WebSocket协议,则它同意升级,并通过响应中的升级标头进行通信。 1234HTTP/1.1 101 WebSocket Protocol HandshakeDate: Wed, 16 Oct 2013 10:07:34 GMTConnection: UpgradeUpgrade: WebSocket 现在握手已经完成,初始的HTTP连接被使用相同底层TCP / IP连接的WebSocket连接取代。 此时任何一方都可以开始发送数据。 使用WebSockets,您可以传输尽可能多的数据,而不会产生与传统HTTP请求相关的开销。 数据通过WebSocket作为消息传输,每个消息由包含要发送的数据(有效载荷)的一个或多个帧组成。 为了确保在到达客户端时能够正确地重构消息,每个帧都以4-12字节的有效载荷数据为前缀。 使用这种基于帧的消息系统有助于减少传输的非有效负载数据的数量,从而显着降低延迟。 注意:值得注意的是,一旦接收到所有的帧并重建了原始的消息有效载荷,客户端才会被通知一个新的消息。 WebSockets协议WebSockets有线协议(RFC 6455)包括两个高级组件:用于协商连接参数的开放HTTP握手和二进制消息构框机制,以允许低开销,基于消息的传送 的文本和二进制数据。 WebSockets协议尝试在现有HTTP基础架构的上下文中解决现有双向HTTP技术的目标; 因此,它被设计为通过HTTP端口80和443进行工作。但是,该设计不会将WebSocket限制为HTTP,并且将来的实现可以在专用端口上使用更简单的握手,而无需重新整理整个协议。 WebSockets协议是一种功能齐全的独立协议,可以在浏览器之外使用。 话虽如此,它的主要应用还是基于浏览器的应用程序的双向传输。 二进制框架层客户端和服务器WebSocket应用程序通过面向消息的API进行通信:发送方提供任意的UTF-8或二进制有效负载,并且当整个消息可用时,接收方通知其传送。 为了实现这一点,WebSocket使用自定义的二进制成帧格式(如下图),它将每个应用消息分解成一个或多个帧,将它们传输到目的地,重新组合它们,并且一旦接收到整个消息,最后通知接收器 。 帧 通信的最小单位,每个包含可变长度的帧头和可以携带全部或部分应用消息的有效载荷。 消息 映射到逻辑应用程序消息的完整的帧序列。 将应用消息分解成多个帧的决定是由客户端和服务器帧代码的底层实现来实现的。 因此,应用程序仍然幸福地不知道单个WebSocket框架或框架的执行方式。 话虽如此,了解每个WebSocket框架如何在电线上表现的重点仍然是有用的: 每帧的第一位(FIN)指示该帧是否是消息的最终片段。 消息可能只包含一个帧。 操作码(4位)表示传送帧的类型:用于传送应用数据的文本(1)或二进制(2)或连接关闭(8),ping(9)和pong (10)等控制帧,用于连接活动 检查。 掩码位指示有效负载是否被屏蔽(对于从客户端发送到服务器的消息)。 有效负载长度表示为可变长度字段: 如果0-125,那就是有效载荷长度。 如果为126,那么以下2个字节表示一个16位无符号整数,表示帧长度。 如果127,那么以下8个字节表示一个64位无符号整数,表示帧长度。 屏蔽键包含用于屏蔽有效载荷的32位值。 如果客户端和服务器在建立连接时协商扩展,Payload包含应用程序数据和自定义扩展数据。 注意:所有客户端发起的帧的有效负载都使用帧头中指定的值进行屏蔽:这样可以防止在客户端上执行的恶意脚本对可能无法理解WebSocket协议的中间人执行缓存中毒攻击。 因此,每个服务器发送的WebSocket框架产生2-10个字节的帧开销。 客户端还必须发送一个掩码密钥,该密钥向头添加额外的4个字节,从而导致6-14个字节的开销。 没有其他元数据,例如头域或有关有效载荷的其他信息可用:所有WebSocket通信都是通过交换将处理有效载荷作为不透明的应用程序数据的帧来执行的。","categories":[{"name":"Web","slug":"Web","permalink":"http://coderpreacher.top/categories/Web/"}],"tags":[{"name":"Html5","slug":"Html5","permalink":"http://coderpreacher.top/tags/Html5/"},{"name":"WebSockets","slug":"WebSockets","permalink":"http://coderpreacher.top/tags/WebSockets/"},{"name":"Web App","slug":"Web-App","permalink":"http://coderpreacher.top/tags/Web-App/"},{"name":"JavaScript","slug":"JavaScript","permalink":"http://coderpreacher.top/tags/JavaScript/"}]},{"title":"一个基于.NET轻量级的Web框架:Nancy","slug":"一个基于-NET轻量级的Web框架-Nancy","date":"2017-04-13T13:12:15.000Z","updated":"2017-04-13T14:02:17.000Z","comments":true,"path":"2017/04/13/一个基于-NET轻量级的Web框架-Nancy/","link":"","permalink":"http://coderpreacher.top/2017/04/13/一个基于-NET轻量级的Web框架-Nancy/","excerpt":"","text":"Nancy框架介绍Nancy是一个轻量级的低成本框架,用于在.NET和Mono上构建基于HTTP的服务。框架的目标是尽可能的避开障碍,为所有的互动提供超级快乐的路径。 这意味着Nancy的所有内容都被设置为具有明智的默认和约定,而不是让你跳过环,并且通过配置,只是为了起床和运行。与Nancy,你可以从零到网站在几分钟内。字面上 Nancy旨在处理DELETE,GET,HEAD,OPTIONS,POST,PUT和PATCH请求,并提供简单,优雅的域特定语言(DSL),只需几次按键即可返回响应,让您有更多的时间聚焦在重要的位..你的代码和你的应用程序。 所有这一切都是由社区构建的,作为一个开源框架,意味着您可以完全访问源代码,并根据MIT许可证获得许可。 您可以从Nuget,TeamCity服务器(最新版本)获取Nancy或从GitHub存储库下载源代码。 轻量级Nancy是一个轻量级的低成本框架是一个小的,非常容易使用REST和服务框架。你可以代替ASP.NET MVC中使用它;您可以在自托管环境中使用它自己的,就像如果你正在部署使用例如应用程序的NodeJS你可能会做的。Nancy的实力,然而,来自远不止其微小的尺寸。在默认安装,它管理包一个完整的IoC容器,一个内置的测试框架,建立一些非常复杂的路由规则的能力,以及一个模块化的架构,使得它令人惊讶的简单与很少或根本没有额外的功能。 任何地方运行Nancy是建立在任何地方运行。从一开始Nancy就被设计成不会对现有框架的依赖。内置与.NET Framework客户端配置文件,Nancy可不管你想要的,因为它完全包含有自己的请求和响应对象自我使用相当多。一个在Nancy的核心概念是主机。主机充当主机环境和Nancy,从而使Nancy在现有的技术,如ASP.NET,WCF和OWIN运行的适配器,或集成在任何给定的应用。 特定主机实现不随核心Nancy框架。他们是分开包装的,因为有很多其他的附加功能,如窗体身份验证,从前面提到的来源。构建Nancy应用程序就像是从一个Web框架自助挑选自己喜欢的部分!通常构建Nancy服务时,将使用最低限度的核心框架和主机。 The super-duper-happy-path虽然很难准确地确定它是什么,但这毕竟是一个非常感性的术语,但它背后的基本想法是: “它只是工作” - 你应该可以拿起东西,使用它们,而不会有任何污点。增加了一个新的模块?这是自动发现的。引进了一个新的查看引擎?所有的连线都可以随时准备,而无需执行任何其他操作。即使您向模块添加了新的依赖项,默认情况下,我们将找到该注册表并为其注入 - 不需要配置。 “轻松可定制” - 即使“只是工作”,如果您想要使用要使用的组件的方式工作,则不应该存在阻碍自定义方式的障碍。想要使用另一个容器?没问题!想要调整路线选择的方式吗?前进!通过我们的bootstrapper方法,所有这些都应该是一块蛋糕。 “低礼” - 您在申请中应该需要的“Nancy code”数量应该很少。任何Nancy应用程序的重要部分是您的代码 - 我们的代码应该摆脱您的方式,让您继续构建真棒应用程序。作为一个证明,实际上可以将一个功能性的Nancy应用程序适用于一个单独的Tweet。 “低摩擦” - 当与Nancy一起构建软件时,API应该可以帮助您获得想要去的地方,而不是进入您的路。命名应该是显而易见的,所需的配置应该是最小的,但是当你需要时,功率和可扩展性应该仍然存在。最重要的是,与Nancy创造应用程序应该是一种荣幸,希望有趣!但不会牺牲您的应用程序增长所需的功能或扩展性。","categories":[{"name":".NET","slug":"NET","permalink":"http://coderpreacher.top/categories/NET/"}],"tags":[{"name":"HTTP服务","slug":"HTTP服务","permalink":"http://coderpreacher.top/tags/HTTP服务/"},{"name":"轻量级WEB框架","slug":"轻量级WEB框架","permalink":"http://coderpreacher.top/tags/轻量级WEB框架/"},{"name":"Nancy","slug":"Nancy","permalink":"http://coderpreacher.top/tags/Nancy/"},{"name":"REST","slug":"REST","permalink":"http://coderpreacher.top/tags/REST/"},{"name":"Microservices","slug":"Microservices","permalink":"http://coderpreacher.top/tags/Microservices/"},{"name":"微服务","slug":"微服务","permalink":"http://coderpreacher.top/tags/微服务/"}]},{"title":"node.js 为何如此流行?","slug":"node-js-为何如此流行","date":"2017-04-12T14:09:26.000Z","updated":"2017-04-13T01:49:11.000Z","comments":true,"path":"2017/04/12/node-js-为何如此流行/","link":"","permalink":"http://coderpreacher.top/2017/04/12/node-js-为何如此流行/","excerpt":"","text":"node.js 为何如此流行?Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。Node.js服务器技术用于创建和运行各种Web应用程序,与Ruby On Rails,Spring Framework和ASP.NET相似。 它利用JavaScript作为主要语言,它是一个轻量级的内置Web服务器,并且通过节点包管理器(NPM)管理了大量的插件,可让您自定义构建应用程序以满足您的需求。 它可能听起来像任何其他好的网络技术,但它具有某些功能,使其成为开发人员构建广泛的Web应用程序的受欢迎的选择。 它是无处不在的JavaScriptNode.js如此受欢迎的最大原因之一是因为它使用JavaScript作为其主要语言来构建Web应用程序。 说实话,JavaScript现在是在浏览器中开发Web应用程序的唯一选择。 还有什么! 引入了一个新的,稳健的框架来引导开发人员。 使用Node.js,JavaScript在服务器上已经彻底革新了。 这种语言对于大多数网络开发者是很常见的,当然也是当今这个世界的驱动。 据专家说,这个趋势不会很快消失。 由于JavaScript是大多数开发人员在某个时间点知道或已经使用的语言,因此从另一个Web技术到Node.js的转换是轻而易举的。 这使得它成为Web开发人员的首选。 快如闪电 V8具有以快速的速度编译和执行JavaScript的能力,主要是因为它将JavaScript编译成本机代码。除此之外,Node.js还有一个神奇的事件循环,它是以一个异步方式执行所有I / O操作的单个线程。在Node.js中,当应用程序必须执行I / O操作时,它会向事件循环发送异步任务和回调操作,然后继续执行程序的其余部分。完成同步操作后,事件循环将自动返回到执行回调任务。这与传统的循环非常不同,传统的循环消耗了大量的内存,非常难以执行。 因此,在Node中非常快速地执行文件系统,网络连接和数据库的读/写操作。它允许开发人员构建快速和高度可扩展的网络应用程序,能够处理大量同时具有高吞吐量的连接。 它是轻量级的通常,Node.js使用基本的事件驱动架构。 这意味着它上执行的一切,包括每一个操作和调用,都是一系列异步回调。 这使得Node能够在单个线程上运行,与其他针对每个客户端请求产生新线程的Web技术不同。 这不仅使其轻巧,而且还构成了Node的非阻塞I / O功能的基础。 低学习成本Node.js的另一个主要优点是语言重用。像Spring或ASP.NET这样的其他网络技术要求开发人员使用另一种语言来编写服务器端的代码,无论是VB.NET,Java还是C#。这意味着所有功能必须包括两种语言 - 一种在客户端,另一种在服务器端。相反,Node仅在客户端和服务器端使用JavaScript。因此,Web开发人员必须在所有层中以单一语言进行交互。除此之外,这可以一遍又一遍地重新用于将来的通信。 高性能PayPal使用Node.js,并且报告每秒钟的请求数量翻了一番,并将响应时间缩短了35%。另一方面,零售巨头沃尔玛则在2013年的Node.js中获得了卓越的经验,当时他们将所有通过Node.js进行的移动通信业务都放在黑色星期五,这是当年最繁忙的购物时间。令人惊讶的是,在黑色星期五,沃尔玛服务器的CPU利用率只有1%,而在线部署的用户达到了2亿。 LinkedIn,全球网站,从Ruby转移到Node处理移动流量,将服务器数量减少到30到3,即减少了近90%。新系统的速度提高了20倍。所有这些数字清楚地表明了Node.js的性能能力。 跨平台功能Node.js也是跨平台的。 与Java类似,Node运行时适用于嵌入式系统,Linux和Unix,Windows服务器和桌面以及Mac OS X的所有标准版本。这意味着您可以选择您的软件,只要您小心不要写 任何特定于您期望的文件系统或结构的任何内容,您都可以期望它能在任何地方运行。 这对于可移植性是非常有利的,而不必编写新的代码或实现新的库。 占用空间小Node.js如此受欢迎的另一个原因是运行它的空间很小。 这使得可以将Node运行时嵌入到用于物联网(IoT)应用的低功耗设备,例如支持Web的家庭恒温器或车库门开启器。 这不是一个沉重的系统,所以你可以把它放在嵌入式,小型和便宜的东西上。 这样可以轻松地在Raspberry Pi或Arduino板上运行Node,并执行小型自动化任务,而无需担心语言的膨胀。 易于修改和维护Node.js采用模块化设计, Node.js使用Module模块去划分不同的功能,以简化应用的开发。Modules模块有点像C++语言中的类库。每一个Node.js的类库都包含了十分丰富的各类函数,比如http模块就包含了和http功能相关的很多函数,可以帮助开发者很容易地对比如http,tcp/udp等进行操作,还可以很容易的创建http和tcp/udp的服务器。 NPM: The Node Package Manager当我们讨论 Node.js 的时候,一个绝对不应该忽略地方就是默认内置的模块管理工具 —— NPM。 其灵感来源与 Ruby Gems(具有版本和依赖管理功能,可以通过在线资料库便捷安装可重用的组件的管理工具)。 当然上面举了这么多Node.js的优点,Node.js的优点远不止这些,当然说了Node.js这么多优点,其实也存在一些缺点的,毕竟不能面面俱到,总之Node.js能响应大量的并发请求,Node.js适合运用在高并发、I/O密集、少量业务逻辑的场景。","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"http://coderpreacher.top/categories/JavaScript/"}],"tags":[{"name":"Node.js","slug":"Node-js","permalink":"http://coderpreacher.top/tags/Node-js/"}]},{"title":"kafka 介绍","slug":"kafka-介绍","date":"2017-04-11T13:30:25.000Z","updated":"2017-05-08T04:54:33.000Z","comments":true,"path":"2017/04/11/kafka-介绍/","link":"","permalink":"http://coderpreacher.top/2017/04/11/kafka-介绍/","excerpt":"","text":"介绍Kafka简介Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。 同时支持离线数据处理和实时数据处理。 支持在线水平扩展。 为何使用消息系统 解耦 在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 冗余 有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 扩展性 因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。 灵活性 & 峰值处理能力 在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。 可恢复性 系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 顺序保证 在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。 缓冲 在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。 异步通信 很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。 Apache kafka是一个分布式流媒体平台。这到底是什么意思呢?一个流媒体平台应具有三个关键能力: 它可以让你发布和订阅的记录流。在这方面,它类似于一个消息队列或企业信息系统。 它可以让你存储的记录中的流容错方式。 它可以让他们出现您处理的记录流。 什么场景下使用Kafka?它被用于两大类应用: 建立实时流数据管道不仅能够可靠地获得系统或应用程序之间的数据。 构建实时流式变换或反应数据流应用。 kafka 相关概念首先确定几个概念: Message: 消息,是通信的基本单位,每个producer可以向一个topic(主题)发布一些消息。 Broker: Kafka集群包含一个或多个服务器,这种服务器被称为broker。 Topic: 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic,在其他消息队列系统里面叫做队列名。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处) Partition: Partition是物理上的概念,每个Topic包含一个或多个Partition。 Producer: 消息生产者,发布消息到 kafka 集群的终端或服务。 Consumer:消息消费者,向Kafka broker读取消息的客户端。 Consumer group:high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。(可为每个Consumer指定group name,若不指定group name则属于默认的group)。 kafka运在一个或多个服务器集群。 在被称为主题类别的kafka集群存储的记录流。 每个记录包含一个键,值和时间戳。 kafka有四个核心API: 生产者API允许应用程序发布的记录流至一个或多个kafka的topic。 消费者API允许应用程序订阅一个或多个主题,并处理所产生的对他们记录的数据流。 流API允许应用程序充当流处理器从一个或多个主题消耗的输入流,并产生一个输出流至一个或多个输出的主题,有效地变换所述输入流,以输出流。 连接器API允许构建和运行kafka topic连接到现有的应用程序或数据系统中重用生产者或消费者。例如,一个连接到关系数据库可能会捕捉每一个变化表。 在kafka的客户端和服务器之间的通信是一个简单的,高性能的,与语言无关的TCP协议来完成。此协议版本,并保持向后兼容旧版本的兼容性。我们对kafka提供了一个Java客户端,但是客户端有多种语言可供选择。 Kafka的架构: Kafka的整体架构非常简单,是显式分布式架构,producer、broker(kafka)和consumer都可以有多个。Producer,consumer实现Kafka注册的接口,数据从producer发送到broker,broker承担一个中间缓存和分发的作用。broker分发注册到系统中的consumer。broker的作用类似于缓存,即活跃的数据和离线处理系统之间的缓存。客户端和服务器端的通信,是基于简单,高性能,且与编程语言无关的TCP协议。 消息发送的流程: Producer根据指定的partition方法(round-robin、hash等),将消息发布到指定topic的partition里面。 kafka集群接收到Producer发过来的消息后,将其持久化到硬盘,并保留消息指定时长(可配置),而不关注消息是否被消费。 Consumer从kafka集群pull数据,并控制获取消息的offset。 Kafka的设计: 1、吞吐量 高吞吐是kafka需要实现的核心目标之一,为此kafka做了以下一些设计: 1、数据磁盘持久化:消息不在内存中cache,直接写入到磁盘,充分利用磁盘的顺序读写性能; 2、zero-copy:减少IO操作步骤; 3、数据批量发送; 4、数据压缩; 5、Topic划分为多个partition,提高parallelism; 2、负载均衡 1、producer根据用户指定的算法,将消息发送到指定的partition; 2、存在多个partiiton,每个partition有自己的replica,每个replica分布在不同的Broker节点上; 3、多个partition需要选取出lead partition,lead partition负责读写,并由zookeeper负责fail over; 4、通过zookeeper管理broker与consumer的动态加入与离开; 3、拉取系统 由于kafka broker会持久化数据,broker没有内存压力,因此,consumer非常适合采取pull的方式消费数据,具有以下几点好处: 1、简化kafka设计; 2、consumer根据消费能力自主控制消息拉取速度; 3、consumer根据自身情况自主选择消费模式,例如批量,重复消费,从尾端开始消费等; 4、可扩展性 1、当需要增加broker结点时,新增的broker会向zookeeper注册,而producer及consumer会根据注册在zookeeper上的watcher感知这些变化,并及时作出调整。 Topics and Logs(主题和日志)让我们先潜入核心抽象kafka提供了记录,该主题的流。 Topic是作为一类消息名称记录被公布。在kafka的Topic始终是多用户;也就是说,一个Topic可以有零个,一个或多个消费者订阅写入的数据。 对于每一个主题,Topic集群保持分区日志,看起来像这样: 每个分区是记录一个有序的,一成不变的序列不断追加到一个结构化的提交日志。 在分区中的记录是调用的每个分配的序列ID号的偏移量唯一地标识该分区中的每个记录。 kafka集群保留所有发布的记录,不论其是否具有可配置的保留期限被使用或消费。例如,如果将保留策略设置为两天,然后记录公布后两天,它可用于消费,之后它将被丢弃以腾出空间。kafka的性能相对于数据的大小实际上不变,以便将数据存储很长一段时间是没有问题的。 事实上,保留在每个消费者基础的唯一的元数据是在日志中,消费者的偏移或位置。这种偏移是由消费者控制:通常消费者会促进其线性偏移,因为它读取记录,但事实上,因为其位置是由消费者可以在任何它喜欢的顺序消耗记录进行控制。例如,消费者可以恢复到旧的偏移量从过去的数据再加工或者直接跳到最新的记录,并开始从“现在”消费。 这些功能的结合意味着,kafka的消费者都是很便宜的,他们可以来去无集群上或其他消费者产生太大影响。例如,你可以使用我们的命令行工具“tail”的任何话题,而无需改变什么是任何现有的消费者消费的内容。 日志中的分区,一举数得。首先,它们允许日志扩展到超过一个的大小,将适合在单个服务器上。每个单独的分区必须适合承载它的服务器上,但一个话题可能有很多分区,以便它能够处理任意的数据量。其次,他们作为并行性更上一个位的单位。 Distribution(分布式)日志的分区分布在每台服务器处理数据和请求对分区的份额kafka集群中的服务器。每个分区跨容错服务器配置数量的复制。每个分区有它充当“leader”和零个或多个服务器充当“followers”一台服务器。领导者处理所有的读取和写入分区的要求而被动的追随者复制的领导者。如果领导者失败了,追随者之一将自动成为新的领导者。每个服务器充当一些分区,而对其他跟随的领导者这样的负载是在集群内均衡。 Producers(生产者)生产者数据发布到他们所选择的主题。制片人负责选择分配哪些记录在主题中哪个分区。这可以在一个循环的方式进行简单地平衡负载,也可以根据一些语义分区功能(比如基于记录一些关键)来完成。更多关于在第二使用分区! Consumers(消费者)消费者标榜自己与消费者的组名,并发布到一个话题每个记录每个订阅用户组内交付给消费者的一个实例。消费实例可以在单独的进程或单独的机器上。如果所有的消费者实例具有相同的消费群,那么记录将有效地加载在消费者实例平衡。如果所有的消费者实例有不同的消费群体,那么每个记录将被广播到所有的消费过程。 两个服务器集群kafka举办两个消费群体的四个分区(P0-P3)。一个消费群体有两个消费情况与B组有四个。更常见的,但是,我们已经发现,主题有一个小的消费群体,每一个“逻辑用户”的。每组都是由可扩展性和容错许多消费者实例。这只不过是发布 - 订阅语义在那里用户是消费者,而不是一个单一的过程中群集的更多。消费kafka的实现方式是通过将建立分区日志在Consumer实例,使每个实例是分区的“公平份额”的在任何时间点的独家消费者。维持组中的成员的这个过程是通过动态kafka协议处理。如果新的实例加入该组,他们将接管从该组的其他成员一些分区;如果一个实例死亡,其分区将被分配到剩余的实例。kafka只提供了记录的总订单分区中,而不是一个主题的不同分区之间。每个分区的顺序与键对数据进行分区的能力相结合足以满足大多数应用。但是,如果在记录总共需要为了这个可以与只有一个分区的主题实现的,虽然这将意味着只有一个每个消费群体的消费过程。 kafka 作为一个消息系统如何流的kafka的观念比较传统的企业信息系统?消息历来有两种模型:队列和发布 - 订阅。在队列中,消费者的池可以从服务器读取和记录每一个进入其中的一个;在发布 - 订阅记录被广播到所有的消费者。每个这两种模式具有一定的实力和弱点。排队的优点是它可以让你瓜分了数据在多个消费情况的处理,它可以让您扩展您的处理。不幸的是,队列不是多用户,一旦一个进程读取它不见了数据。发布 - 订阅模式可以让你广播数据到多个进程,但没有,因为每一个消息发送到每个用户的缩放处理的方式。在kafka的消费群的概念推广这两个概念。与队列的消费群让你过的进程的集合(消费群的成员)瓜分处理。与发布 - 订阅,kafka让您发送广播消息到多个消费群体。kafka的模型的优点是,每个主题都有两个属性,它可以扩展的处理,也是多用户,有没有必要选择一个或另一个。kafka具有较强的排序保证比传统的消息系统了。传统的队列保留在服务器上,订单记录,如果多个消费者从队列中消耗那么服务器双手出存储它们的订单记录。然而,尽管服务器为了捧出来的记录,这些记录被异步传递给消费者,让他们可以在不同的消费者到达的顺序。这实际上意味着记录的排序在并行消费的存在都将丢失。消息系统通常解决这个具有“排他性消费”,只允许一个过程从队列中消耗的概念,当然,这意味着有正在处理的并行性。kafka做的更好。通过具有一个概念并行性的分区中的主题,kafka是能够通过消费者的进程池同时提供排序保证和负载平衡。这是通过使每个分区由该组中只有一个消费者所消耗的话题,消费者的消费群在指定的分区来实现的。通过这样做,我们确保消费者的是,分区唯一的读者,为了消耗数据。因为有许多的分区,这还是平衡了许多消费者的情况下的负载。但是请注意,不能在一个消费群体比分区的详细消费情况。 kafka 作为一个存储系统任何消息队列,它允许从消费他们解耦出版消息被有效地充当用于在飞行中消息的存储系统。这就是kafka与其他消息队列系统不同的地方,因为它是一个很好的存储系统。写到kafka数据写入到磁盘和复制的容错。kafka允许生产者在确认等待,以便不被认为是写操作完成,直到它被完全复制,并保证持续下去,即使写入服务器失败。磁盘结构kafka使用很好地扩展,kafka将执行相同的你是否有50 KB或服务器上的持久性数据的50 TB。由于把存储的重视,并允许客户控制自己的读取位置的结果,你能想到kafka作为一种特殊用途的分布式文件系统,致力于高性能,低延迟提交日志存储,复制和传播。 kafka 流处理这是不够的,只是读,写,以及数据的储存流,目的是使数据流的实时处理。在kafka流处理器是任何需要从输入的主题数据的连续流,该输入执行一些处理,并产生数据的连续流,以输出主题。例如,零售应用程序可能需要在销售和出货量和输出的输入流计算关闭此数据重新排序和价格调整的流。这是可以做到的简单处理直接使用生产者和消费者的API。然而,对于更复杂的转换kafka提供了一个完全集成的流API。这允许做不平凡的处理建筑应用程序,计算聚合过流或加入流在一起。该设施有助于解决难题这种类型的应用面的:在处理乱序的数据,再处理输入作为代码的变化,执行有状态的计算等API建立在芯中的基元流提供kafka:它使用用于输入的生产者和消费者的API,使用kafka有状态存储,并使用流处理器实例之间容错同一组的机制。 参考文章: 官文:Introduction to Kafka。 分布式消息系统Kafka。 Kafka背景及架构介绍。","categories":[{"name":"Big Data","slug":"Big-Data","permalink":"http://coderpreacher.top/categories/Big-Data/"}],"tags":[{"name":"Kafka","slug":"Kafka","permalink":"http://coderpreacher.top/tags/Kafka/"},{"name":"Message System","slug":"Message-System","permalink":"http://coderpreacher.top/tags/Message-System/"}]},{"title":"Jwt 介绍","slug":"Jwt-介绍","date":"2017-04-10T14:08:49.000Z","updated":"2017-04-12T15:45:11.000Z","comments":true,"path":"2017/04/10/Jwt-介绍/","link":"","permalink":"http://coderpreacher.top/2017/04/10/Jwt-介绍/","excerpt":"","text":"Jwt是什么? Jwt的全称是JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于将各方之间的信息安全地传输为JSON对象。 该信息可以通过数字签名进行验证和信任。 使用加密(使用HMAC算法)或使用RSA的公钥/私钥对可以对JWT进行签名。 Jwt特点: 紧凑: 因为它们的尺寸较小,所以JWTs可以通过URL,POST参数,或HTTP报头内发送。此外,较小的尺寸意味着传输速度快。 自包含: 负载中包含了所有用户所需要的信息,避免了需要多次查询数据库。 什么时候使用Jwt?下面是一些Jwt的应用场景: 身份认证(Authentication): 这是使用JWT最常见的场景。一旦用户登录,每个后续请求将包括JWT,让用户的接入路径,服务和资源被允许使用该令牌。单点登录是在不同的领域很容易使用的广泛使用JWT如今,由于其小的开销和它的能力的特性。 信息交换: Jwt是在各方之间安全传输信息的好方法,因为它们可以被签名,例如使用公钥/私钥对,您可以确定发件人是谁。 另外,当使用标题和有效载荷计算签名时,您还可以验证内容是否未被篡改。 Jwt数据结构?一个Jwt实际上是由以下三个部分组成: header(头 部): 在header中通常包含了两部分:token类型和采用的加密算法。 1234{ "alg": "HS256", //加密算法 "typ": "JWT" //token 类型} - 接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。 payload(载荷) : Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved , public 和 private . Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者) , exp(过期时间戳) , sub(面向的用户) , aud(接收方) , iat(签发时间) 。 Public claims:根据需要定义自己的字段,注意应该避免冲突。 Private claims:这些是自定义的字段,可以用来在双方之间交换信息。 负载使用的例子: 12345{ "sub": "1234567890", "name": "John Doe", "admin": true} signature(签名) : 创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建: 1234HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 签名用于验证消息的发送者以及消息是没有经过篡改的。 完整的JWT JWT格式的输出是以 . 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。 下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名: 如何使用JWT?在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。 当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization 头部使用 Bearer 模式添加JWT,其内容看起来是下面这样: 1Authorization: Bearer <token> 因为用户的状态在服务端的内存中是不存储的,所以这是一种 无状态 的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。 JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。 下面的序列图展示了该过程: 为什么要使用JWT?相比XML格式,JSON更加简洁,编码之后更小,这使得JWT比SAML更加简洁,更加适合在HTML和HTTP环境中传递。 在安全性方面,SWT只能够使用HMAC算法和共享的对称秘钥进行签名,而JWT和SAML token则可以使用X.509认证的公私秘钥对进行签名。与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。 因为JSON可以直接映射为对象,在大多数编程语言中都提供了JSON解析器,而XML则没有这么自然的文档-对象映射关系,这就使得使用JWT比SAML更方便。 原文: Introduction to JSON Web Tokens。","categories":[{"name":"安全认证","slug":"安全认证","permalink":"http://coderpreacher.top/categories/安全认证/"}],"tags":[{"name":"Jwt","slug":"Jwt","permalink":"http://coderpreacher.top/tags/Jwt/"}]}]}