SegmentFault 程序猿小卡的前端专栏最新的文章
2020-12-07T13:02:33+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
5分钟入门MP4文件格式
https://segmentfault.com/a/1190000038398499
2020-12-07T13:02:33+08:00
2020-12-07T13:02:33+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
9
<h2>写在前面</h2><p>本文主要内容包括,什么是MP4、MP4文件的基本结构、Box的基本结构、常见且重要的box介绍、普通MP4与fMP4的区别、如何通过代码解析MP4文件 等。</p><p>写作背景:最近经常回答团队小伙伴关于直播 & 短视频的问题,比如 “flv.js的实现原理”、“为什么设计同学给的mp4文件浏览器里播放不了、但本地可以正常播放”、“MP4兼容性很好,可不可以用来做直播” 等。</p><p>在解答的过程中,发现经常涉及 MP4 协议的介绍。之前这块有简单了解过并做了笔记,这里稍微整理一下,顺便作为团队参考文档,如有错漏,敬请指出。</p><h2>什么是MP4</h2><p>首先,介绍下封装格式。多媒体封装格式(也叫容器格式),是指按照一定的规则,将视频数据、音频数据等,放到一个文件中。常见的 MKV、AVI 以及本文介绍的 MP4 等,都是封装格式。</p><p>MP4是最常见的封装格式之一,因为其跨平台的特性而得到广泛应用。MP4文件的后缀为.mp4,基本上主流的播放器、浏览器都支持MP4格式。</p><blockquote>MP4文件的格式主要由 MPEG-4 Part 12、MPEG-4 Part 14 两部分进行定义。其中,MPEG-4 Part 12 定义了ISO基础媒体文件格式,用来存储基于时间的媒体内容。MPEG-4 Part 14 实际定义了MP4文件格式,在MPEG-4 Part 12的基础上进行扩展。</blockquote><p>对从事直播、音视频相关工作的同学,很有必要了解MP4格式,下面简单介绍下。</p><h2>MP4文件格式概览</h2><p>MP4文件由多个box组成,每个box存储不同的信息,且box之间是树状结构,如下图所示。</p><p><img src="/img/remote/1460000038398507" alt="" title=""></p><p>box类型有很多,下面是3个比较重要的顶层box:</p><ul><li>ftyp:File Type Box,描述文件遵从的MP4规范与版本;</li><li>moov:Movie Box,媒体的metadata信息,有且仅有一个。</li><li>mdat:Media Data Box,存放实际的媒体数据,一般有多个;</li></ul><p><img src="/img/remote/1460000038398505" alt="" title=""></p><p>虽然box类型有很多,但基本结构都是一样的。下一节会先介绍box的结构,然后再对常见的box进行进一步讲解。</p><p>下表是常见的box,稍微看下有个大致的印象就好,然后直接跳到下一节。</p><p><img src="/img/remote/1460000038398504" alt="" title=""></p><h2>MP4 Box简介</h2><p>1个box由两部分组成:box header、box body。</p><ol><li>box header:box的元数据,比如box type、box size。</li><li>box body:box的数据部分,实际存储的内容跟box类型有关,比如mdat中body部分存储的媒体数据。</li></ol><p>box header中,只有type、size是必选字段。当size==0时,存在largesize字段。在部分box中,还存在version、flags字段,这样的box叫做Full Box。当box body中嵌套其他box时,这样的box叫做container box。</p><p><img src="/img/remote/1460000038398523" alt="" title=""></p><h3>Box Header</h3><p>字段定义如下:</p><ul><li><p>type:box类型,包括 “预定义类型”、“自定义扩展类型”,占4个字节;</p><ul><li>预定义类型:比如ftyp、moov、mdat等预定义好的类型;</li><li>自定义扩展类型:如果type==uuid,则表示是自定义扩展类型。size(或largesize)随后的16字节,为自定义类型的值(extended_type)</li></ul></li><li><p>size:包含box header在内的整个box的大小,单位是字节。当size为0或1时,需要特殊处理:</p><ul><li>size等于0:box的大小由后续的largesize确定(一般只有装载媒体数据的mdat box会用到largesize);</li><li>size等于1:当前box为文件的最后一个box,通常包含在mdat box中;</li></ul></li><li>largesize:box的大小,占8个字节;</li><li>extended_type:自定义扩展类型,占16个字节;</li></ul><p>Box的伪代码如下:</p><pre><code>aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
unsigned int(32) size;
unsigned int(32) type = boxtype;
if (size==1) {
unsigned int(64) largesize;
} else if (size==0) {
// box extends to end of file
}
if (boxtype==‘uuid’) {
unsigned int(8)[16] usertype = extended_type;
}
}</code></pre><h3>Box Body</h3><p>box数据体,不同box包含的内容不同,需要参考具体box的定义。有的 box body 很简单,比如 ftyp。有的 box 比较复杂,可能嵌套了其他box,比如moov。</p><h3>Box vs FullBox</h3><p>在Box的基础上,扩展出了FullBox类型。相比Box,FullBox 多了 version、flags 字段。</p><ul><li>version:当前box的版本,为扩展做准备,占1个字节;</li><li>flags:标志位,占24位,含义由具体的box自己定义;</li></ul><p>FullBox 伪代码如下:</p><pre><code>aligned(8) class FullBox(unsigned int(32) boxtype, unsigned int(8) v, bit(24) f) extends Box(boxtype) {
unsigned int(8) version = v;
bit(24) flags = f;
}</code></pre><p>FullBox主要在moov中的box用到,比如 <code>moov.mvhd</code>,后面会介绍到。</p><pre><code>aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
// 字段略...
}</code></pre><h2>ftyp(File Type Box)</h2><p>ftyp用来指出当前文件遵循的规范,在介绍ftyp的细节前,先科普下isom。</p><h3>什么是isom</h3><p>isom(ISO Base Media file)是在 MPEG-4 Part 12 中定义的一种基础文件格式,MP4、3gp、QT 等常见的封装格式,都是基于这种基础文件格式衍生的。</p><p>MP4 文件可能遵循的规范有mp41、mp42,而mp41、mp42又是基于isom衍生出来的。</p><blockquote>3gp(3GPP):一种容器格式,主要用于3G手机上;<br>QT:QuickTime的缩写,.qt 文件代表苹果QuickTime媒体文件;</blockquote><h3>ftyp定义</h3><p>ftyp 定义如下:</p><pre><code>aligned(8) class FileTypeBox extends Box(‘ftyp’) {
unsigned int(32) major_brand;
unsigned int(32) minor_version;
unsigned int(32) compatible_brands[]; // to end of the box
} </code></pre><p>下面是是 brand 的描述,其实就是具体封装格式对应的代码,用4个字节的编码来表示,比如 mp41。</p><blockquote>A brand is a four-letter code representing a format or subformat. Each file has a major brand (or primary brand), and also a compatibility list of brands.</blockquote><p>ftyp 的几个字段的含义:</p><ul><li>major_brand:比如常见的 isom、mp41、mp42、avc1、qt等。它表示“最好”基于哪种格式来解析当前的文件。举例,major_brand 是 A,compatible_brands 是 A1,当解码器同时支持 A、A1 规范时,最好使用A规范来解码当前媒体文件,如果不支持A规范,但支持A1规范,那么,可以使用A1规范来解码;</li><li>minor_version:提供 major_brand 的说明信息,比如版本号,不得用来判断媒体文件是否符合某个标准/规范;</li><li>compatible_brands:文件兼容的brand列表。比如 mp41 的兼容 brand 为 isom。通过兼容列表里的 brand 规范,可以将文件 部分(或全部)解码出来;</li></ul><blockquote>在实际使用中,不能把 isom 做为 major_brand,而是需要使用具体的brand(比如mp41),因此,对于 isom,没有定义具体的文件扩展名、mime type。</blockquote><p>下面是常见的几种brand,以及对应的文件扩展名、mime type,更多brand可以参考 <a href="https://link.segmentfault.com/?enc=onuIF3VZ5H1nhgxilDCv%2BA%3D%3D.Dk7SCKy%2Fi6EYsvgWc8fyA4QGHP0CY6KIYbZziIXAjG6D0cYBSR9RU3ZG6fIqZNurovC4Hvf8zf467PqYPDTnyzoFK69OennMyBj3xw6FkJw%3D" rel="nofollow">这里</a> 。</p><p><img src="/img/remote/1460000038398508" alt="" title=""></p><p>下面是实际例子的截图,不赘述。</p><p><img src="/img/remote/1460000038398503" alt="" title=""></p><h3>关于AVC/AVC1</h3><p>在讨论 MP4 规范时,提到AVC,有的时候指的是“AVC文件格式”,有的时候指的是"AVC压缩标准(H.264)",这里简单做下区分。</p><ul><li>AVC文件格式:基于 ISO基础文件格式 衍生的,使用的是AVC压缩标准,可以认为是MP4的扩展格式,对应的brand 通常是 avc1,在MPEG-4 PART 15 中定义。</li><li>AVC压缩标准(H.264):在MPEG-4 Part 10中定义。</li><li>ISO基础文件格式(Base Media File Format) 在 MPEG-4 Part 12 中定义。</li></ul><h2>moov(Movie Box)</h2><p>Movie Box,存储 mp4 的 metadata,一般位于mp4文件的开头。</p><pre><code>aligned(8) class MovieBox extends Box(‘moov’){ }</code></pre><p>moov中,最重要的两个box是 mvhd 和 trak:</p><ul><li>mvhd:Movie Header Box,mp4文件的整体信息,比如创建时间、文件时长等;</li><li>trak:Track Box,一个mp4可以包含一个或多个轨道(比如视频轨道、音频轨道),轨道相关的信息就在trak里。trak是container box,至少包含两个box,tkhd、mdia;</li></ul><blockquote>mvhd针对整个影片,tkhd针对单个track,mdhd针对媒体,vmhd针对视频,smhd针对音频,可以认为是从 宽泛 > 具体,前者一般是从后者推导出来的。</blockquote><h3>mvhd(Movie Header Box)</h3><p>MP4文件的整体信息,跟具体的视频流、音频流无关,比如创建时间、文件时长等。</p><p>定义如下:</p><pre><code>aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) { if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) timescale;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) timescale;
unsigned int(32) duration;
}
template int(32) rate = 0x00010000; // typically 1.0
template int(16) volume = 0x0100; // typically, full volume const bit(16) reserved = 0;
const unsigned int(32)[2] reserved = 0;
template int(32)[9] matrix =
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
// Unity matrix
bit(32)[6] pre_defined = 0;
unsigned int(32) next_track_ID;
}</code></pre><p>字段含义如下:</p><ul><li>creation_time:文件创建时间;</li><li>modification_time:文件修改时间;</li><li>timescale:一秒包含的时间单位(整数)。举个例子,如果timescale等于1000,那么,一秒包含1000个时间单位(后面track等的时间,都要用这个来换算,比如track的duration为10,000,那么,track的实际时长为10,000/1000=10s);</li><li>duration:影片时长(整数),根据文件中的track的信息推导出来,等于时间最长的track的duration;</li><li>rate:推荐的播放速率,32位整数,高16位、低16位分别代表整数部分、小数部分([16.16]),举例 0x0001 0000 代表1.0,正常播放速度;</li><li>volume:播放音量,16位整数,高8位、低8位分别代表整数部分、小数部分([8.8]),举例 0x01 00 表示 1.0,即最大音量;</li><li>matrix:视频的转换矩阵,一般可以忽略不计;</li><li>next_track_ID:32位整数,非0,一般可以忽略不计。当要添加一个新的track到这个影片时,可以使用的track id,必须比当前已经使用的track id要大。也就是说,添加新的track时,需要遍历所有track,确认可用的track id;</li></ul><h3>tkhd(Track Box)</h3><p>单个 track 的 metadata,包含如下字段:</p><ul><li>version:tkhd box的版本;</li><li><p>flags:按位或操作获得,默认值是7(0x000001 | 0x000002 | 0x000004),表示这个track是启用的、用于播放的 且 用于预览的。</p><ul><li>Track_enabled:值为0x000001,表示这个track是启用的,当值为0x000000,表示这个track没有启用;</li><li>Track_in_movie:值为0x000002,表示当前track在播放时会用到;</li><li>Track_in_preview:值为0x000004,表示当前track用于预览模式;</li></ul></li><li>creation_time:当前track的创建时间;</li><li>modification_time:当前track的最近修改时间;</li><li>track_ID:当前track的唯一标识,不能为0,不能重复;</li><li>duration:当前track的完整时长(需要除以timescale得到具体秒数);</li><li>layer:视频轨道的叠加顺序,数字越小越靠近观看者,比如1比2靠上,0比1靠上;</li><li>alternate_group:当前track的分组ID,alternate_group值相同的track在同一个分组里面。同个分组里的track,同一时间只能有一个track处于播放状态。当alternate_group为0时,表示当前track没有跟其他track处于同个分组。一个分组里面,也可以只有一个track;</li><li>volume:audio track的音量,介于0.0~1.0之间;</li><li>matrix:视频的变换矩阵;</li><li>width、height:视频的宽高;</li></ul><p>定义如下:</p><pre><code>aligned(8) class TrackHeaderBox
extends FullBox(‘tkhd’, version, flags){
if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) track_ID;
const unsigned int(32) reserved = 0;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) track_ID;
const unsigned int(32) reserved = 0;
unsigned int(32) duration;
}
const unsigned int(32)[2] reserved = 0;
template int(16) layer = 0;
template int(16) alternate_group = 0;
template int(16) volume = {if track_is_audio 0x0100 else 0}; const unsigned int(16) reserved = 0;
template int(32)[9] matrix= { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }; // unity matrix
unsigned int(32) width;
unsigned int(32) height;
}</code></pre><p>例子如下:</p><p><img src="/img/remote/1460000038398506" alt="" title=""></p><h3>hdlr(Handler Reference Box)</h3><p>声明当前track的类型,以及对应的处理器(handler)。</p><p>handler_type的取值包括:</p><ul><li>vide(0x76 69 64 65),video track;</li><li>soun(0x73 6f 75 6e),audio track;</li><li>hint(0x68 69 6e 74),hint track;</li></ul><p>name为utf8字符串,对handler进行描述,比如 L-SMASH Video Handler(参考 <a href="https://link.segmentfault.com/?enc=UCaxRmBkfpAqf%2F2k756j5g%3D%3D.pT4z3N2whSUNJQir7G872RA3iaYVLvaclZdDWwdh%2ByRvNS9a6%2FuZ9ohVKp6Uw%2BQ8" rel="nofollow">这里</a>)。</p><pre><code>aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
unsigned int(32) pre_defined = 0;
unsigned int(32) handler_type;
const unsigned int(32)[3] reserved = 0;
string name;
}</code></pre><p><img src="/img/remote/1460000038398502" alt="" title=""></p><h2>stbl(Sample Table Box)</h2><p>MP4文件的媒体数据部分在mdat box里,而stbl则包含了这些媒体数据的索引以及时间信息,了解stbl对解码、渲染MP4文件很关键。</p><p>在MP4文件中,媒体数据被分成多个chunk,每个chunk可包含多个sample,而sample则由帧组成(通常1个sample对应1个帧),关系如下:</p><p><img alt="Alt text" title="Alt text"></p><p>stbl中比较关键的box包含stsd、stco、stsc、stsz、stts、stss、ctts。下面先来个概要的介绍,然后再逐个讲解细节。</p><h3>stco / stsc / stsz / stts / stss / ctts / stsd 概述</h3><p>下面是这几个box概要的介绍:</p><ul><li>stsd:给出视频、音频的编码、宽高、音量等信息,以及每个sample中包含多少个frame;</li><li>stco:thunk在文件中的偏移;</li><li>stsc:每个thunk中包含几个sample;</li><li>stsz:每个sample的size(单位是字节);</li><li>stts:每个sample的时长;</li><li>stss:哪些sample是关键帧;</li><li>ctts:帧解码到渲染的时间差值,通常用在B帧的场景;</li></ul><h3>stsd(Sample Description Box)</h3><p>stsd给出sample的描述信息,这里面包含了在解码阶段需要用到的任意初始化信息,比如 编码 等。对于视频、音频来说,所需要的初始化信息不同,这里以视频为例。</p><p>伪代码如下:</p><pre><code>aligned(8) abstract class SampleEntry (unsigned int(32) format) extends Box(format){
const unsigned int(8)[6] reserved = 0;
unsigned int(16) data_reference_index;
}
// Visual Sequences
class VisualSampleEntry(codingname) extends SampleEntry (codingname){
unsigned int(16) pre_defined = 0;
const unsigned int(16) reserved = 0;
unsigned int(32)[3] pre_defined = 0;
unsigned int(16) width;
unsigned int(16) height;
template unsigned int(32) horizresolution = 0x00480000; // 72 dpi
template unsigned int(32) vertresolution = 0x00480000; // 72 dpi
const unsigned int(32) reserved = 0;
template unsigned int(16) frame_count = 1;
string[32] compressorname;
template unsigned int(16) depth = 0x0018;
int(16) pre_defined = -1;
}
// AudioSampleEntry、HintSampleEntry 定义略过
aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type) extends FullBox('stsd', 0, 0){
int i ;
unsigned int(32) entry_count;
for (i = 1 ; i u entry_count ; i++) {
switch (handler_type){
case ‘soun’: // for audio tracks
AudioSampleEntry();
break;
case ‘vide’: // for video tracks
VisualSampleEntry();
break;
case ‘hint’: // Hint track
HintSampleEntry();
break;
}
}
}</code></pre><p>在SampleDescriptionBox 中,handler_type 参数 为 track 的类型(soun、vide、hint),entry_count 变量代表当前box中 smaple description 的条目数。</p><blockquote>stsc 中,sample_description_index 就是指向这些smaple description的索引。</blockquote><p>针对不同的handler_type,SampleDescriptionBox 后续应用不同的 SampleEntry 类型,比如video track为VisualSampleEntry。</p><p>VisualSampleEntry包含如下字段:</p><ul><li>data_reference_index:当MP4文件的数据部分,可以被分割成多个片段,每一段对应一个索引,并分别通过URL地址来获取,此时,data_reference_index 指向对应的片段(比较少用到);</li><li>width、height:视频的宽高,单位是像素;</li><li>horizresolution、vertresolution:水平、垂直方向的分辨率(像素/英寸),16.16定点数,默认是0x00480000(72dpi);</li><li>frame_count:一个sample中包含多少个frame,对video track来说,默认是1;</li><li>compressorname:仅供参考的名字,通常用于展示,占32个字节,比如 AVC Coding。第一个字节,表示这个名字实际要占用N个字节的长度。第2到第N+1个字节,存储这个名字。第N+2到32个字节为填充字节。compressorname 可以设置为0;</li><li>depth:位图的深度信息,比如 0x0018(24),表示不带alpha通道的图片;</li></ul><blockquote>In video tracks, the frame_count field must be 1 unless the specification for the media format explicitly documents this template field and permits larger values. That specification must document both how the individual frames of video are found (their size information) and their timing established. That timing might be as simple as dividing the sample duration by the frame count to establish the frame duration.</blockquote><p>例子如下:</p><p><img src="/img/remote/1460000038398511" alt="" title=""></p><h3>stco(Chunk Offset Box)</h3><p>chunk在文件中的偏移量。针对小文件、大文件,有两种不同的box类型,分别是stco、co64,它们的结构是一样的,只是字段长度不同。</p><p>chunk_offset 指的是在文件本身中的 offset,而不是某个box内部的偏移。</p><p>在构建mp4文件的时候,需要特别注意 moov 所处的位置,它对于chunk_offset 的值是有影响的。有一些MP4文件的 moov 在文件末尾,为了优化首帧速度,需要将 moov 移到文件前面,此时,需要对 chunk_offset 进行改写。</p><p>stco 定义如下:</p><pre><code># Box Type: ‘stco’, ‘co64’
# Container: Sample Table Box (‘stbl’) Mandatory: Yes
# Quantity: Exactly one variant must be present
aligned(8) class ChunkOffsetBox
extends FullBox(‘stco’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i u entry_count; i++) {
unsigned int(32) chunk_offset;
}
}
aligned(8) class ChunkLargeOffsetBox
extends FullBox(‘co64’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i u entry_count; i++) {
unsigned int(64) chunk_offset;
}
}</code></pre><p>如下例子所示,第一个chunk的offset是47564,第二个chunk的偏移是120579,其他类似。</p><p><img src="/img/remote/1460000038398509" alt="" title=""></p><h3>stsc(Sample To Chunk Box)</h3><p>sample 以 chunk 为单位分成多个组。chunk的size可以是不同的,chunk里面的sample的size也可以是不同的。</p><ul><li>entry_count:有多少个表项(每个表项,包含first_chunk、samples_per_chunk、sample_description_index信息);</li><li>first_chunk:当前表项中,对应的第一个chunk的序号;</li><li>samples_per_chunk:每个chunk包含的sample数;</li><li>sample_description_index:指向 stsd 中 sample description 的索引值(参考stsd小节);</li></ul><pre><code>aligned(8) class SampleToChunkBox
extends FullBox(‘stsc’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i u entry_count; i++) {
unsigned int(32) first_chunk;
unsigned int(32) samples_per_chunk;
unsigned int(32) sample_description_index;
}
}</code></pre><p>前面描述比较抽象,这里看个例子,这里表示的是:</p><ul><li>序号1~15的chunk,每个chunk包含15个sample;</li><li>序号16的chunk,包含30个sample;</li><li>序号17以及之后的chunk,每个chunk包含28个sample;</li><li>以上所有chunk中的sample,对应的sample description的索引都是1;</li></ul><table><thead><tr><th align="center">first_chunk</th><th align="center">samples_per_chunk</th><th align="center">sample_description_index</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">15</td><td align="center">1</td></tr><tr><td align="center">16</td><td align="center">30</td><td align="center">1</td></tr><tr><td align="center">17</td><td align="center">28</td><td align="center">1</td></tr></tbody></table><p><img src="/img/remote/1460000038398510" alt="" title=""></p><h3>stsz(Sample Size Boxes)</h3><p>每个sample的大小(字节),根据 sample_size 字段,可以知道当前track包含了多少个sample(或帧)。</p><p>有两种不同的box类型,stsz、stz2。</p><p>stsz:</p><ul><li>sample_size:默认的sample大小(单位是byte),通常为0。如果sample_size不为0,那么,所有的sample都是同样的大小。如果sample_size为0,那么,sample的大小可能不一样。</li><li>sample_count:当前track里面的sample数目。如果 sample_size==0,那么,sample_count 等于下面entry的条目;</li><li>entry_size:单个sample的大小(如果sample_size==0的话);</li></ul><pre><code>aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
unsigned int(32) sample_size;
unsigned int(32) sample_count;
if (sample_size==0) {
for (i=1; i u sample_count; i++) {
unsigned int(32) entry_size;
}
}
}</code></pre><p>stz2:</p><ul><li>field_size:entry表中,每个entry_size占据的位数(bit),可选的值为4、8、16。4比较特殊,当field_size等于4时,一个字节上包含两个entry,高4位为entry[i],低4位为entry[i+1];</li><li>sample_count:等于下面entry的条目;</li><li>entry_size:sample的大小。</li></ul><pre><code>aligned(8) class CompactSampleSizeBox extends FullBox(‘stz2’, version = 0, 0) {
unsigned int(24) reserved = 0;
unisgned int(8) field_size;
unsigned int(32) sample_count;
for (i=1; i u sample_count; i++) {
unsigned int(field_size) entry_size;
}
}</code></pre><p>例子如下:</p><p><img src="/img/remote/1460000038398514" alt="" title=""></p><h3>stts(Decoding Time to Sample Box)</h3><p>stts包含了DTS到sample number的映射表,主要用来推导每个帧的时长。</p><pre><code>aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
unsigned int(32) sample_delta;
}
}</code></pre><ul><li>entry_count:stts 中包含的entry条目数;</li><li>sample_count:单个entry中,具有相同时长(duration 或 sample_delta)的连续sample的个数。</li><li>sample_delta:sample的时长(以timescale为计量)</li></ul><p>还是看例子,如下图,entry_count为3,前250个sample的时长为1000,第251个sample时长为999,第252~283个sample的时长为1000。</p><blockquote>假设timescale为1000,则实际时长需要除以1000。</blockquote><p><img src="/img/remote/1460000038398513" alt="" title=""></p><h3>stss(Sync Sample Box)</h3><p>mp4文件中,关键帧所在的sample序号。如果没有stss的话,所有的sample中都是关键帧。</p><ul><li>entry_count:entry的条目数,可以认为是关键帧的数目;</li><li>sample_number:关键帧对应的sample的序号;(从1开始计算)</li></ul><pre><code>aligned(8) class SyncSampleBox
extends FullBox(‘stss’, version = 0, 0) {
unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_number;
}
}</code></pre><p>例子如下,第1、31、61、91、121...271个sample是关键帧。</p><p><img src="/img/remote/1460000038398518" alt="" title=""></p><h3>ctts(Composition Time to Sample Box)</h3><p>从解码(dts)到渲染(pts)之间的差值。</p><p>对于只有I帧、P帧的视频来说,解码顺序、渲染顺序是一致的,此时,ctts没必要存在。</p><p>对于存在B帧的视频来说,ctts就需要存在了。当PTS、DTS不相等时,就需要ctts了,公式为 CT(n) = DT(n) + CTTS(n) 。</p><pre><code>aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, version = 0, 0) { unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
unsigned int(32) sample_offset;
}
}</code></pre><p>例子如下,不赘述:</p><p><img src="/img/remote/1460000038398512" alt="" title=""></p><h2>fMP4(Fragmented mp4)</h2><p>fMP4 跟普通 mp4 基本文件结构是一样的。普通mp4用于点播场景,fmp4通常用于直播场景。</p><p>它们有以下差别:</p><ul><li>普通mp4的时长、内容通常是固定的。fMP4 时长、内容通常不固定,可以边生成边播放;</li><li>普通mp4完整的metadata都在moov里,需要加载完moov box后,才能对mdat中的媒体数据进行解码渲染;</li><li>fMP4中,媒体数据的metadata在moof box中,moof 跟 mdat (通常)结对出现。moof 中包含了sample duration、sample size等信息,因此,fMP4可以边生成边播放;</li></ul><p>举例来说,普通mp4、fMP4顶层box结构可能如下。以下是通过笔者编写的MP4解析小工具打印出来,代码在文末给出。</p><pre><code>// 普通mp4
ftyp size=32(8+24) curTotalSize=32
moov size=4238(8+4230) curTotalSize=4270
mdat size=1124105(8+1124097) curTotalSize=1128375
// fmp4
ftyp size=36(8+28) curTotalSize=36
moov size=1227(8+1219) curTotalSize=1263
moof size=1252(8+1244) curTotalSize=2515
mdat size=65895(8+65887) curTotalSize=68410
moof size=612(8+604) curTotalSize=69022
mdat size=100386(8+100378) curTotalSize=169408</code></pre><p>怎么判断mp4文件是普通mp4,还是fMP4呢?一般可以看下是否存在存在mvex(Movie Extends Box)。</p><p><img src="/img/remote/1460000038398520" alt="" title=""></p><h2>mvex(Movie Extends Box)</h2><p>当存在mvex时,表示当前文件是fmp4(非严谨)。此时,sample相关的metadata不在moov里,需要通过解析moof box来获得。</p><p>伪代码如下:</p><pre><code>aligned(8) class MovieExtendsBox extends Box(‘mvex’){ }</code></pre><h3>mehd(Movie Extends Header Box)</h3><p>mehd是可选的,用来声明影片的完整时长(fragment_duration)。如果不存在,则需要遍历所有的fragment,来获得完整的时长。对于fmp4的场景,fragment_duration一般没办法提前预知。</p><pre><code>aligned(8) class MovieExtendsHeaderBox extends FullBox(‘mehd’, version, 0) {
if (version==1) {
unsigned int(64) fragment_duration;
} else { // version==0
unsigned int(32) fragment_duration;
}
}</code></pre><h3>trex(Track Extends Box)</h3><p>用来给 fMP4 的 sample 设置各种默认值,比如时长、大小等。</p><pre><code>aligned(8) class TrackExtendsBox extends FullBox(‘trex’, 0, 0){
unsigned int(32) track_ID;
unsigned int(32) default_sample_description_index;
unsigned int(32) default_sample_duration;
unsigned int(32) default_sample_size;
unsigned int(32) default_sample_flags
}</code></pre><p>字段含义如下:</p><ul><li>track_id:对应的 track 的 ID,比如video track、audio track 的ID;</li><li>default_sample_description_index:sample description 的默认 index(指向stsd);</li><li>default_sample_duration:sample 默认时长,一般为0;</li><li>default_sample_size:sample 默认大小,一般为0;</li><li>default_sample_flags:sample 的默认flag,一般为0;</li></ul><p>default_sample_flags 占4个字节,比较复杂,结构如下:</p><blockquote>老版本规范里,前6位都是保留位,新版规范里,只有前4位是保留位。is_leading 含义不是很直观,下一小节会专门讲解下。</blockquote><ul><li>reserved:4 bits,保留位;</li><li><p>is_leading:2 bits,是否 leading sample,可能的取值包括:</p><ul><li>0:当前 sample 不确定是否 leading sample;(一般设为这个值)</li><li>1:当前 sample 是 leading sample,并依赖于 referenced I frame 前面的 sample,因此无法被解码;</li><li>2:当前 sample 不是 leading sample;</li><li>3:当前 sample 是 leading sample,不依赖于 referenced I frame 前面的 sample,因此可以被解码;</li></ul></li><li><p>sample_depends_on:2 bits,是否依赖其他sample,可能的取值包括:</p><ul><li>0:不清楚是否依赖其他sample;</li><li>1:依赖其他sample(不是I帧);</li><li>2:不依赖其他sample(I帧);</li><li>3:保留值;</li></ul></li><li><p>sample_is_depended_on:2 bits,是否被其他sample依赖,可能的取值包括:</p><ul><li>0:不清楚是否有其他sample依赖当前sample;</li><li>1:其他sample可能依赖当前sample;</li><li>2:其他sample不依赖当前sample;</li><li>3:保留值;</li></ul></li><li><p>sample_has_redundancy:2 bits,是否有冗余编码,可能的取值包括:</p><ul><li>0:不清楚是否存在冗余编码;</li><li>1:存在冗余编码;</li><li>2:不存在冗余编码;</li><li>3:保留值;</li></ul></li><li>sample_padding_value:3 bits,填充值;</li><li>sample_is_non_sync_sample:1 bits,不是关键帧;</li><li>sample_degradation_priority:16 bits,降级处理的优先级(一般针对如流传过程中出现的问题);</li></ul><p>例子如下:</p><p><img src="/img/remote/1460000038398516" alt="" title=""></p><h3>关于 is_leading</h3><p>is_leading 不是特别好解释,这里贴上原文,方便大家理解。</p><blockquote>A leading sample (usually a picture in video) is defined relative to a reference sample, which is the immediately prior sample that is marked as “sample_depends_on” having no dependency (an I picture). A leading sample has both a composition time before the reference sample, and possibly also a decoding dependency on a sample before the reference sample. Therefore if, for example, playback and decoding were to start at the reference sample, those samples marked as leading would not be needed and might not be decodable. A leading sample itself must therefore not be marked as having no dependency.</blockquote><p>为方便讲解,下面的 leading frame 对应 leading sample,referenced frame 对应 referenced samle。</p><p>以 H264编码 为例,H264 中存在 I帧、P帧、B帧。由于 B帧 的存在,视频帧的 解码顺序、渲染顺序 可能不一致。</p><p>mp4文件的特点之一,就是支持随机位置播放。比如,在视频网站上,可以拖动进度条快进。</p><p>很多时候,进度条定位的那个时刻,对应的不一定是 I帧。为了能够顺利播放,需要往前查找最近的一个 I帧,如果可能的话,从最近的 I帧 开始解码播放(也就是说,不一定能从前面最近的I帧播放)。</p><p>将上面描述的此刻定位到的帧,称作 leading frame。leading frame 前面最近的一个 I 帧,叫做 referenced frame。</p><p>回顾下 is_leading 为 1 或 3 的情况,同样都是 leading frame,什么时候可以解码(decodable),什么时候不能解码(not decodable)?</p><blockquote>1: this sample is a leading sample that has a dependency before the referenced I‐picture (and is therefore not decodable);<br>3: this sample is a leading sample that has no dependency before the referenced I‐picture (and is therefore decodable);</blockquote><p>1、is_leading 为 1 的例子: 如下所示,帧2(leading frame) 解码依赖 帧1、帧3(referenced frame)。在视频流里,从 帧2 往前查找,最近的 I帧 是 帧3。哪怕已经解码了 帧3,帧2 也解不出来。</p><p><img src="/img/remote/1460000038398524" alt="" title=""></p><p>2、is_leading 为 3 的例子: 如下所示,此时,帧2(leading frame)可以解码出来。</p><p><img src="/img/remote/1460000038398519" alt="" title=""></p><h2>moof(Movie Fragment Box)</h2><p>moof是个container box,相关 metadata 在内嵌box里,比如 mfhd、 tfhd、trun 等。</p><p>伪代码如下:</p><pre><code>aligned(8) class MovieFragmentBox extends Box(‘moof’){ }</code></pre><p><img src="/img/remote/1460000038398515" alt="" title=""></p><h3>mfhd(Movie Fragment Header Box)</h3><p>结构比较简单,sequence_number 为 movie fragment 的序列号。根据 movie fragment 产生的顺序,从1开始递增。</p><pre><code>aligned(8) class MovieFragmentHeaderBox extends FullBox(‘mfhd’, 0, 0){
unsigned int(32) sequence_number;
}</code></pre><h3>traf(Track Fragment Box)</h3><pre><code>aligned(8) class TrackFragmentBox extends Box(‘traf’){ }</code></pre><p>对 fmp4 来说,数据被氛围多个 movie fragment。一个 movie fragment 可包含多个track fragment(每个 track 包含0或多个 track fragment)。每个 track fragment 中,可以包含多个该 track 的 sample。</p><blockquote>每个 track fragment 中,包含多个 track run,每个 track run 代表一组连续的 sample。</blockquote><p><img src="/img/remote/1460000038398517" alt="" title=""></p><h3>tfhd(Track Fragment Header Box)</h3><p>tfhd 用来设置 track fragment 中 的 sample 的 metadata 的默认值。</p><p>伪代码如下,除了 track_ID,其他都是 可选字段。</p><pre><code>aligned(8) class TrackFragmentHeaderBox extends FullBox(‘tfhd’, 0, tf_flags){
unsigned int(32) track_ID;
// all the following are optional fields
unsigned int(64) base_data_offset;
unsigned int(32) sample_description_index;
unsigned int(32) default_sample_duration;
unsigned int(32) default_sample_size;
unsigned int(32) default_sample_flags
}</code></pre><p>sample_description_index、default_sample_duration、default_sample_size 没什么好讲的,这里只讲解下 tf_flags、base_data_offset。</p><p>首先是 tf_flags,不同 flag 的值如下(同样是求按位求或) :</p><ul><li>0x000001 base‐data‐offset‐present:存在 base_data_offset 字段,表示 数据位置 相对于整个文件的 基础偏移量。</li><li>0x000002 sample‐description‐index‐present:存在 sample_description_index 字段;</li><li>0x000008 default‐sample‐duration‐present:存在 default_sample_duration 字段;</li><li>0x000010 default‐sample‐size‐present:存在 default_sample_size 字段;</li><li>0x000020 default‐sample‐flags‐present:存在 default_sample_flags 字段;</li><li>0x010000 duration‐is‐empty:表示当前时间段不存在sample,default_sample_duration 如果存在则为0 ,;</li><li>0x020000 default‐base‐is‐moof:如果 base‐data‐offset‐present 为1,则忽略这个flag。如果 base‐data‐offset‐present 为0,则当前 track fragment 的 base_data_offset 是从 moof 的第一个字节开始计算;</li></ul><p>sample 位置计算公式为 base_data_offset + data_offset,其中,data_offset 每个 sample 单独定义。如果未显式提供 base_data_offset,则 sample 的位置的通常是基于 moof 的相对位置。</p><p>举个例子,比如 tf_flags 等于 57,表示 存在 base_data_offset、default_sample_duration、default_sample_flags。</p><p><img src="/img/remote/1460000038398526" alt="" title=""></p><p>base_data_offset 为 1263 (ftyp、moov 的size 之和为 1263)。</p><p><img src="/img/remote/1460000038398521" alt="" title=""></p><h3>trun(Track Fragment Run Box)</h3><p>trun 伪代码如下:</p><pre><code>aligned(8) class TrackRunBox extends FullBox(‘trun’, version, tr_flags) {
unsigned int(32) sample_count;
// the following are optional fields
signed int(32) data_offset;
unsigned int(32) first_sample_flags;
// all fields in the following array are optional
{
unsigned int(32) sample_duration;
unsigned int(32) sample_size;
unsigned int(32) sample_flags
if (version == 0)
{ unsigned int(32) sample_composition_time_offset; }
else
{ signed int(32) sample_composition_time_offset; }
}[ sample_count ]
}</code></pre><p>前面听过,track run 表示一组连续的 sample,其中:</p><ul><li>sample_count:sample 的数目;</li><li>data_offset:数据部分的偏移量;</li><li>first_sample_flags:可选,针对当前 track run中 第一个 sample 的设置;</li></ul><p>tr_flags 如下,大同小异:</p><ul><li>0x000001 data‐offset‐present:存在 data_offset 字段;</li><li>0x000004 first‐sample‐flags‐present:存在 first_sample_flags 字段,这个字段的值,只会覆盖第一个 sample 的flag设置;当 first_sample_flags 存在时,sample_flags 则不存在;</li><li>0x000100 sample‐duration‐present:每个 sample 都有自己的 sample_duration,否则使用默认值;</li><li>0x000200 sample‐size‐present:每个 sample 都有自己的 sample_size,否则使用默认值;</li><li>0x000400 sample‐flags‐present:每个 sample 都有自己的 sample_flags,否则使用默认值;</li><li>0x000800 sample‐composition‐time‐offsets‐present:每个 sample 都有自己的 sample_composition_time_offset;</li><li>0x000004 first‐sample‐flags‐present,覆盖第一个sample的设置,这样就可以把一组sample中的第一个帧设置为关键帧,其他的设置为非关键帧;</li></ul><p>举例如下,tr_flags 为 2565。此时,存在 data_offset 、first_sample_flags、sample_size、sample_composition_time_offset。</p><p><img src="/img/remote/1460000038398522" alt="" title=""></p><p><img src="/img/remote/1460000038398525" alt="" title=""></p><h2>编程实践:解析MP4文件结构</h2><p>纸上得来终觉浅,绝知此事要coding。根据 mp4 文件规范,可以写个简易的 mp4 文件解析工具,比如前文对比 普通mp4、fMP4 的 box 结构,就是笔者自己写的分析脚本。</p><p>核心代码如下,完整代码有点长,可以在 <a href="https://link.segmentfault.com/?enc=B1TBDkD6dsULsGCrk7nGnQ%3D%3D.mx3BppIt%2BIhk9HRi2B50JlDSXufiL9LdHbanX18BagxGqv9TzgS6V87b53HPRcwmcK5QoU%2BQNImLfesMHBQZD57DUAdYltHRnOi4Wk%2Fls2Q%3D" rel="nofollow">笔者的github</a> 上找到。</p><pre><code class="javascript">class Box {
constructor(boxType, extendedType, buffer) {
this.type = boxType; // 必选,字符串,4个字节,box类型
this.size = 0; // 必选,整数,4个字节,box的大小,单位是字节
this.headerSize = 8; //
this.boxes = [];
// this.largeSize = 0; // 可选,8个字节
// this.extendedType = extendedType || boxType; // 可选,16个字节
this._initialize(buffer);
}
_initialize(buffer) {
this.size = buffer.readUInt32BE(0); // 4个字节
this.type = buffer.slice(4, 8).toString(); // 4个字节
let offset = 8;
if (this.size === 1) {
this.size = buffer.readUIntBE(8, 8); // 8个字节,largeSize
this.headerSize += 8;
offset = 16;
} else if (this.size === 1) {
// last box
}
if (this.type === 'uuid') {
this.type = buffer.slice(offset, 16); // 16个字节
this.headerSize += 16;
}
}
setInnerBoxes(buffer, offset = 0) {
const innerBoxes = getInnerBoxes(buffer.slice(this.headerSize + offset, this.size));
innerBoxes.forEach(item => {
let { type, buffer } = item;
type = type.trim(); // 备注,有些box类型不一定四个字母,比如 url、urn
if (this[type]) {
const box = this[type](buffer);
this.boxes.push(box);
} else {
this.boxes.push('TODO 待实现');
// console.log(`unknowed type: ${type}`);
}
});
}
}
class FullBox extends Box {
constructor(boxType, buffer) {
super(boxType, '', buffer);
const headerSize = this.headerSize;
this.version = buffer.readUInt8(headerSize); // 必选,1个字节
this.flags = buffer.readUIntBE(headerSize + 1, 3); // 必选,3个字节
this.headerSize = headerSize + 4;
}
}
// FileTypeBox、MovieBox、MediaDataBox、MovieFragmentBox 代码有点长这里就不贴了
class Movie {
constructor(buffer) {
this.boxes = [];
this.bytesConsumed = 0;
const innerBoxes = getInnerBoxes(buffer);
innerBoxes.forEach(item => {
const { type, buffer, size } = item;
if (this[type]) {
const box = this[type](buffer);
this.boxes.push(box);
} else {
// 自定义 box 类型
}
this.bytesConsumed += size;
});
}
ftyp(buffer) {
return new FileTypeBox(buffer);
}
moov(buffer) {
return new MovieBox(buffer);
}
mdat(buffer) {
return new MediaDataBox(buffer);
}
moof(buffer) {
return new MovieFragmentBox(buffer);
}
}
function getInnerBoxes(buffer) {
let boxes = [];
let offset = 0;
let totalByteLen = buffer.byteLength;
do {
let box = getBox(buffer, offset);
boxes.push(box);
offset += box.size;
} while(offset < totalByteLen);
return boxes;
}
function getBox(buffer, offset = 0) {
let size = buffer.readUInt32BE(offset); // 4个字节
let type = buffer.slice(offset + 4, offset + 8).toString(); // 4个字节
if (size === 1) {
size = buffer.readUIntBE(offset + 8, 8); // 8个字节,largeSize
} else if (size === 0) {
// last box
}
let boxBuffer = buffer.slice(offset, offset + size);
return {
size,
type,
buffer: boxBuffer
};
}</code></pre><h2>写在后面</h2><p>受限于时间,同时为了方便讲解,部分内容可能不是很严谨,如有错漏,敬请指出。如有问题,也欢迎随时交流。</p><h2>相关链接</h2><p>ISO/IEC 14496-12:2015 Information technology — Coding of audio-visual objects — Part 12: ISO base media file format<br><a href="https://link.segmentfault.com/?enc=zgaUE0GK2jkwyfvnsNO2pQ%3D%3D.rHO9q2eTkwTV6NXwzuCxUuxH6NvgMz7BMtlGLqEqR638oqM%2BaaqmYyZLRXV99iip" rel="nofollow">https://www.iso.org/standard/...</a></p><p>Introduction to QuickTime File Format Specification<br><a href="https://link.segmentfault.com/?enc=Dx23VaCJmDZczpiDLJdwKg%3D%3D.u7jwCeyYrfjNUYKtd7RGrPLscYkAR32LYTBLyYVO05AyaV5ZJwuJ2xG2jFm88D%2FjIphyqcXQbUQjjd%2B%2Fo5pQ9NzTjrwROloppBwFXIDWrZ8xbYUBzK%2FkDiyzMey1CAD%2FbZstlN3rgXTRSFDzx5CwL9FaazzNggaWoCojtJsv9Q7KlEnjlpVOGttxKSf4lHNPdB6opcGMa7K3y19aYLsfHA%3D%3D" rel="nofollow">https://developer.apple.com/l...</a></p><p>AVC_(file_format)<br><a href="https://link.segmentfault.com/?enc=%2BJVBJdXIppsEOrk%2FiJ7txw%3D%3D.0H96%2Bs2frM1wFq9u4emjkJzjvF%2BAnVu9t5bCG4OIpszZqbRV0W7iUQ%2F279hYQDr%2BXasu1R7dNYxXa8DcNKu%2Bjg%3D%3D" rel="nofollow">http://fileformats.archivetea...</a></p><p>AV1 Codec ISO Media File Format Binding<br><a href="https://link.segmentfault.com/?enc=dzgimAwmhcOTNe31t4I4mQ%3D%3D.B0MIYy8WVNY5N1%2B94%2F%2F12FKlUU43tOOiCQiImIcd%2FWkk4eEGcGUfbEdF227i0i0u" rel="nofollow">https://aomediacodec.github.i...</a></p>
WebRTC:一个视频聊天的简单例子
https://segmentfault.com/a/1190000019970102
2019-08-05T08:22:29+08:00
2019-08-05T08:22:29+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
12
<h2>相关API简介</h2>
<p>在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。</p>
<p>WebRTC通信相关的API非常多,主要完成了如下功能:</p>
<ol>
<li>信令交换</li>
<li>通信候选地址交换</li>
<li>音视频采集</li>
<li>音视频发送、接收</li>
</ol>
<p>相关API太多,为避免篇幅过长,文中部分采用了伪代码进行讲解。详细代码参考文章末尾,也可以在<a href="https://link.segmentfault.com/?enc=uL%2BcL4p0xICY%2BjjeZazZZA%3D%3D.BP0nllJpiHwPvOZ%2B0rwax2uO40D%2FEfXugceSuQWdbu9I2QH4dUSG12MOLtLbKY1N25JcJryGft7BGj36yvMkTFECRtxybxnHxmwFS9veDEUyj%2F8WkdNFqA4qK8JF7jpm" rel="nofollow">笔者的Github</a>上找到,有问题欢迎留言交流。</p>
<h2>信令交换</h2>
<p>信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket。</p>
<p>发送方伪代码如下:</p>
<pre><code class="javascript">const pc = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeerViaSignalingServer(SIGNALING_OFFER, offer); // 发送方发送信令消息</code></pre>
<p>接收方伪代码如下:</p>
<pre><code class="javascript">const pc = new RTCPeerConnection(iceConfig);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeerViaSignalingServer(SIGNALING_ANSWER, answer); // 接收方发送信令消息</code></pre>
<h2>候选地址交换服务</h2>
<p>当本地设置了会话描述信息,并添加了媒体流的情况下,ICE框架就会开始收集候选地址。两边收集到候选地址后,需要交换候选地址,并从中知道合适的候选地址对。</p>
<p>候选地址的交换,同样采用前面提到的信令服务,伪代码如下:</p>
<pre><code class="javascript">// 设置本地会话描述信息
const localPeer = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await localPeer.setLocalDescription(offer);
// 本地采集音视频
const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = mediaStream;
// 添加音视频流
mediaStream.getTracks().forEach(track => {
localPeer.addTrack(track, mediaStream);
});
// 交换候选地址
localPeer.onicecandidate = function(evt) {
if (evt.candidate) {
sendToPeerViaSignalingServer(SIGNALING_CANDIDATE, evt.candidate);
}
}</code></pre>
<h2>音视频采集</h2>
<p>可以使用浏览器提供的<code>getUserMedia</code>接口,采集本地的音视频。</p>
<pre><code class="javascript">const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = mediaStream;</code></pre>
<h2>音视频发送、接收</h2>
<p>将采集到的音视频轨道,通过<code>addTrack</code>进行添加,发送给远端。</p>
<pre><code class="javascript">mediaStream.getTracks().forEach(track => {
localPeer.addTrack(track, mediaStream);
});</code></pre>
<p>远端可以通过监听<code>ontrack</code>来监听音视频的到达,并进行播放。</p>
<pre><code class="javascript">remotePeer.ontrack = function(evt) {
const remoteVideo = document.getElementById('remote-video');
remoteVideo.srcObject = evt.streams[0];
}</code></pre>
<h2>完整代码</h2>
<p>包含两部分:客户端代码、服务端代码。</p>
<p>1、客户端代码</p>
<pre><code class="javascript">const socket = io.connect('http://localhost:3000');
const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';
const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
const SERVER_USER_EVENT = 'SERVER_USER_EVENT';
const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录
const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';
const SIGNALING_OFFER = 'SIGNALING_OFFER';
const SIGNALING_ANSWER = 'SIGNALING_ANSWER';
const SIGNALING_CANDIDATE = 'SIGNALING_CANDIDATE';
let remoteUser = ''; // 远端用户
let localUser = ''; // 本地登录用户
function log(msg) {
console.log(`[client] ${msg}`);
}
socket.on('connect', function() {
log('ws connect.');
});
socket.on('connect_error', function() {
log('ws connect_error.');
});
socket.on('error', function(errorMessage) {
log('ws error, ' + errorMessage);
});
socket.on(SERVER_USER_EVENT, function(msg) {
const type = msg.type;
const payload = msg.payload;
switch(type) {
case SERVER_USER_EVENT_UPDATE_USERS:
updateUserList(payload);
break;
}
log(`[${SERVER_USER_EVENT}] [${type}], ${JSON.stringify(msg)}`);
});
socket.on(SERVER_RTC_EVENT, function(msg) {
const {type} = msg;
switch(type) {
case SIGNALING_OFFER:
handleReceiveOffer(msg);
break;
case SIGNALING_ANSWER:
handleReceiveAnswer(msg);
break;
case SIGNALING_CANDIDATE:
handleReceiveCandidate(msg);
break;
}
});
async function handleReceiveOffer(msg) {
log(`receive remote description from ${msg.payload.from}`);
// 设置远端描述
const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
remoteUser = msg.payload.from;
createPeerConnection();
await pc.setRemoteDescription(remoteDescription); // TODO 错误处理
// 本地音视频采集
const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = mediaStream;
mediaStream.getTracks().forEach(track => {
pc.addTrack(track, mediaStream);
// pc.addTransceiver(track, {streams: [mediaStream]}); // 这个也可以
});
// pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃
const answer = await pc.createAnswer(); // TODO 错误处理
await pc.setLocalDescription(answer);
sendRTCEvent({
type: SIGNALING_ANSWER,
payload: {
sdp: answer,
from: localUser,
target: remoteUser
}
});
}
async function handleReceiveAnswer(msg) {
log(`receive remote answer from ${msg.payload.from}`);
const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
remoteUser = msg.payload.from;
await pc.setRemoteDescription(remoteDescription); // TODO 错误处理
}
async function handleReceiveCandidate(msg){
log(`receive candidate from ${msg.payload.from}`);
await pc.addIceCandidate(msg.payload.candidate); // TODO 错误处理
}
/**
* 发送用户相关消息给服务器
* @param {Object} msg 格式如 { type: 'xx', payload: {} }
*/
function sendUserEvent(msg) {
socket.emit(CLIENT_USER_EVENT, JSON.stringify(msg));
}
/**
* 发送RTC相关消息给服务器
* @param {Object} msg 格式如{ type: 'xx', payload: {} }
*/
function sendRTCEvent(msg) {
socket.emit(CLIENT_RTC_EVENT, JSON.stringify(msg));
}
let pc = null;
/**
* 邀请用户加入视频聊天
* 1、本地启动视频采集
* 2、交换信令
*/
async function startVideoTalk() {
// 开启本地视频
const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = mediaStream;
// 创建 peerConnection
createPeerConnection();
// 将媒体流添加到webrtc的音视频收发器
mediaStream.getTracks().forEach(track => {
pc.addTrack(track, mediaStream);
// pc.addTransceiver(track, {streams: [mediaStream]});
});
// pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃
}
function createPeerConnection() {
const iceConfig = {"iceServers": [
{url: 'stun:stun.ekiga.net'},
{url: 'turn:turnserver.com', username: 'user', credential: 'pass'}
]};
pc = new RTCPeerConnection(iceConfig);
pc.onnegotiationneeded = onnegotiationneeded;
pc.onicecandidate = onicecandidate;
pc.onicegatheringstatechange = onicegatheringstatechange;
pc.oniceconnectionstatechange = oniceconnectionstatechange;
pc.onsignalingstatechange = onsignalingstatechange;
pc.ontrack = ontrack;
return pc;
}
async function onnegotiationneeded() {
log(`onnegotiationneeded.`);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer); // TODO 错误处理
sendRTCEvent({
type: SIGNALING_OFFER,
payload: {
from: localUser,
target: remoteUser,
sdp: pc.localDescription // TODO 直接用offer?
}
});
}
function onicecandidate(evt) {
if (evt.candidate) {
log(`onicecandidate.`);
sendRTCEvent({
type: SIGNALING_CANDIDATE,
payload: {
from: localUser,
target: remoteUser,
candidate: evt.candidate
}
});
}
}
function onicegatheringstatechange(evt) {
log(`onicegatheringstatechange, pc.iceGatheringState is ${pc.iceGatheringState}.`);
}
function oniceconnectionstatechange(evt) {
log(`oniceconnectionstatechange, pc.iceConnectionState is ${pc.iceConnectionState}.`);
}
function onsignalingstatechange(evt) {
log(`onsignalingstatechange, pc.signalingstate is ${pc.signalingstate}.`);
}
// 调用 pc.addTrack(track, mediaStream),remote peer的 onTrack 会触发两次
// 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用
// 这个行为有点奇怪,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313
let stream;
function ontrack(evt) {
// if (!stream) {
// stream = evt.streams[0];
// } else {
// console.log(`${stream === evt.streams[0]}`); // 这里为true
// }
log(`ontrack.`);
const remoteVideo = document.getElementById('remote-video');
remoteVideo.srcObject = evt.streams[0];
}
// 点击用户列表
async function handleUserClick(evt) {
const target = evt.target;
const userName = target.getAttribute('data-name').trim();
if (userName === localUser) {
alert('不能跟自己进行视频会话');
return;
}
log(`online user selected: ${userName}`);
remoteUser = userName;
await startVideoTalk(remoteUser);
}
/**
* 更新用户列表
* @param {Array} users 用户列表,比如 [{name: '小明', name: '小强'}]
*/
function updateUserList(users) {
const fragment = document.createDocumentFragment();
const userList = document.getElementById('login-users');
userList.innerHTML = '';
users.forEach(user => {
const li = document.createElement('li');
li.innerHTML = user.userName;
li.setAttribute('data-name', user.userName);
li.addEventListener('click', handleUserClick);
fragment.appendChild(li);
});
userList.appendChild(fragment);
}
/**
* 用户登录
* @param {String} loginName 用户名
*/
function login(loginName) {
localUser = loginName;
sendUserEvent({
type: CLIENT_USER_EVENT_LOGIN,
payload: {
loginName: loginName
}
});
}
// 处理登录
function handleLogin(evt) {
let loginName = document.getElementById('login-name').value.trim();
if (loginName === '') {
alert('用户名为空!');
return;
}
login(loginName);
}
function init() {
document.getElementById('login-btn').addEventListener('click', handleLogin);
}
init();</code></pre>
<p>2、服务端代码</p>
<pre><code class="javascript">// 添加ws服务
const io = require('socket.io')(server);
let connectionList = [];
const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';
const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
const SERVER_USER_EVENT = 'SERVER_USER_EVENT';
const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN';
const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';
function getOnlineUser() {
return connectionList
.filter(item => {
return item.userName !== '';
})
.map(item => {
return {
userName: item.userName
};
});
}
function setUserName(connection, userName) {
connectionList.forEach(item => {
if (item.connection.id === connection.id) {
item.userName = userName;
}
});
}
function updateUsers(connection) {
connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
}
io.on('connection', function (connection) {
connectionList.push({
connection: connection,
userName: ''
});
// 连接上的用户,推送在线用户列表
// connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
updateUsers(connection);
connection.on(CLIENT_USER_EVENT, function(jsonString) {
const msg = JSON.parse(jsonString);
const {type, payload} = msg;
if (type === CLIENT_USER_EVENT_LOGIN) {
setUserName(connection, payload.loginName);
connectionList.forEach(item => {
// item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
updateUsers(item.connection);
});
}
});
connection.on(CLIENT_RTC_EVENT, function(jsonString) {
const msg = JSON.parse(jsonString);
const {payload} = msg;
const target = payload.target;
const targetConn = connectionList.find(item => {
return item.userName === target;
});
if (targetConn) {
targetConn.connection.emit(SERVER_RTC_EVENT, msg);
}
});
connection.on('disconnect', function () {
connectionList = connectionList.filter(item => {
return item.connection.id !== connection.id;
});
connectionList.forEach(item => {
// item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
updateUsers(item.connection);
});
});
});</code></pre>
<h2>写在后面</h2>
<p>WebRTC的API非常多,因为WebRTC本身就比较复杂,随着时间的推移,WebRTC的某些API(包括某些协议细节)也在改动或被废弃,这其中也有向后兼容带来的复杂性,比如本地视频采集后加入传输流,可以采用 addStream 或 addTrack 或 addTransceiver,再比如会话描述版本从plan-b迁移到unified-plan。</p>
<p>建议亲自动手撸一遍代码,加深了解。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=AoxaDbI7K9%2FADqo7d1dB2g%3D%3D.6XB8Pg3WVW3gSdCFnAV3u48O41o8O631lWCWZ5IUQyJKAqV9mPhchiH%2BrOW10oEvfsGnu8N6Oaob5JcJnBYSROF00Cz3MQhpF%2BAS7yjbk1QfqNBK5Be02Dmes6je%2BJyp" rel="nofollow">2019.08.02-video-talk-using-webrtc</a></p>
<p><a href="https://link.segmentfault.com/?enc=MBAKg9e3oH4afC1NurZz%2BA%3D%3D.FoFay1qmcFPrnXgUMMO3rp3hwtls1RVmVLrQoK5NK3%2BlkadULAcNUd5AorCHmZN7R22LUdTSYgBQBkSHGZ5WynWa9qgMpY%2FiZZQgse4c4aM%3D" rel="nofollow">https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection</a></p>
<p><a href="https://link.segmentfault.com/?enc=kZXsjuXZjrqzkwPMSAzm8g%3D%3D.s5nBPUQGMnfI5V%2BTLX3%2F4ZeZYztr3k9DDlcZwhllH4VYT0lSGP4%2BkO6rw1StqAW5LPM8I1x3nX%2F65ESe8DipyQ%3D%3D" rel="nofollow">onremotestream called twice for each remote stream</a></p>
WebRTC:会话描述协议SDP
https://segmentfault.com/a/1190000019900808
2019-07-29T08:53:11+08:00
2019-07-29T08:53:11+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<h2>什么是SDP</h2>
<p>SDP(Session Description Protocol)是一种通用的会话描述协议,主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。</p>
<p>WebRTC主要在连接建立阶段用到SDP,连接双方通过信令服务交换会话信息,包括音视频编解码器(codec)、主机候选地址、网络传输协议等。</p>
<p>下面先简单介绍下SDP的格式、常用属性,然后通过WebRTC连接建立过程生成的SDP实例进行进一步讲解。</p>
<h2>协议格式说明</h2>
<p>SDP的格式非常简单,由多个行组成,每个行都是如下格式。</p>
<pre><code><type>=<value></code></pre>
<p>其中:</p>
<ul>
<li>
<code><type></code>:大小写敏感的一个字符,代表特定的属性,比如<code>v</code>代表版本;</li>
<li>
<code><value></code>:结构化文本,格式与属性类型有关,UTF8编码;</li>
<li>
<code>=</code>两边不允许存在空格;</li>
<li>
<code>=*</code>表示是可选的;</li>
</ul>
<h2>常见属性</h2>
<p>以下面的SDP为例:</p>
<pre><code>v=0
o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
s=
c=IN IP4 host.anywhere.com
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
m=video 51372 RTP/AVP 31
a=rtpmap:31 H261/90000
m=video 53000 RTP/AVP 32
a=rtpmap:32 MPV/90000</code></pre>
<h3>协议版本号:<code>v=</code>
</h3>
<p>格式如下,注意,没有子版本号。</p>
<pre><code>v=0</code></pre>
<h3>会话发起者:<code>o</code>
</h3>
<p>格式如下,其中,username、session-id、nettype、addrtype、unicast-address 一起,唯一标识一个会话。</p>
<pre><code>o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address></code></pre>
<p>各字段含义如下:</p>
<ul>
<li>username:发起者的用户名,不允许存在空格,如果应用不支持用户名,则为<code>-</code>。</li>
<li>sess-id:会话id,由应用自行定义,规范的建议是NTP(Network Time Protocol)时间戳。</li>
<li>sess-version:会话版本,用途由应用自行定义,只要会话数据发生变化时(比如编码),sess-version随着递增就行。同样的,规范的建议是NTP时间戳。</li>
<li>nettype:网络类型,比如<code>IN</code>表示<code>Internet</code>。</li>
<li>addrtype:地址类型,比如<code>IP4</code>、<code>IV6</code>
</li>
<li>unicast-address:域名,或者IP地址。</li>
</ul>
<h3>会话名 <code>s=</code>
</h3>
<p>必选,有且仅有一个<code>s=</code>字段,且不能为空。如果实在没有有意义的会话名,可以赋一个空格,即<code>s= </code>。</p>
<pre><code>s=<session name></code></pre>
<h3>连接数据:c=</h3>
<p>格式如下:</p>
<pre><code>c=<nettype> <addrtype> <connection-address></code></pre>
<p>每个SDP至少需要包含一个会话级别的<code>c=</code>字段,或者在每个媒体描述后面各包含一个<code>c=</code>字段。(媒体描述后的<code>c=</code>会覆盖会话级别的<code>c=</code>)</p>
<ul>
<li>nettype:网络类型,比如<code>IN</code>,表示 Internet。</li>
<li>addrtype:地址类型,比如<code>IP4</code>、<code>IP6</code>。</li>
<li>connection-address:如果是广播,则为广播地址组;如果是单播,则为单播地址;</li>
</ul>
<p>举例01:</p>
<pre><code>c=IN IP4 224.2.36.42/127</code></pre>
<p>举例02:</p>
<pre><code>c=IN IP4 host.anywhere.com</code></pre>
<h3>媒体描述:<code>m=</code>
</h3>
<p>SDP可能同时包含多个媒体描述。格式如下:</p>
<pre><code>m=<media> <port> <proto> <fmt> ...</code></pre>
<p>其中:</p>
<ul>
<li>media:媒体类型。包括 video、audio、text、application、message等。</li>
<li>port:传输媒体流的端口,具体含义取决于使用的网络类型(在<code>c=</code>中声明)和使用的协议(proto,在<code>m=</code>中声明)。</li>
<li>
<p>proto:传输协议,具体含义取决于<code>c=</code>中定义的地址类型,比如<code>c=</code>是IP4,那么这里的传输协议运行在IP4之上。比如:</p>
<ul>
<li>UDP:传输层协议是UDP。</li>
<li>RTP/AVP:针对视频、音频的RTP协议,跑在UDP之上。</li>
<li>RTP/SAVP:针对视频、音频的SRTP协议,跑在UDP之上。</li>
</ul>
</li>
<li>fmt:媒体格式的描述,可能有多个。根据 proto 的不同,fmt 的含义也不同。比如 proto 为 RTP/SAVP 时,fmt 表示 RTP payload 的类型。如果有多个,表示在这次会话中,多种payload类型可能会用到,且第一个为默认的payload类型。</li>
</ul>
<p>举例,下面表示媒体类型是视频,采用SRTP传输流媒体数据,且RTP包的类型可能是122、102...119,默认是122。</p>
<pre><code>m=video 9 UDP/TLS/RTP/SAVPF 122 102 100 101 124 120 123 119</code></pre>
<p>对于 RTP/SAVP,需要注意的是,payload type 又分两种类型:</p>
<ol>
<li>静态类型:参考 <a href="https://link.segmentfault.com/?enc=dZZZXQyk8%2Bn2S0lIi3ALOg%3D%3D.v%2FpRw1Z2qyWG7stuxWSXrwgc5t1icKQJsVqxBfjyTA0v12%2B%2Br%2FKwewCT0UGXTW%2B28qqvScSDdtb1EIU9M50swChnABwegvg3xBsX%2BV5%2B85d3OkXDaNz1SAYqhtGPk5yo" rel="nofollow">RTP/AVP audio and video payload types</a>。</li>
<li>动态类型:在<code>a=fmtp:</code>里进行定义。(<code>a=</code>为附加属性,见后面小节)</li>
</ol>
<p>举例,下面的SDP中:</p>
<ol>
<li>对于audio,111 是动态类型,表示<code>opus/48000/2</code>。</li>
<li>对于video,122 是动态类型,表示<code>H264/90000</code>。</li>
</ol>
<pre><code>m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 126
a=rtpmap:111 opus/48000/2
m=video 9 UDP/TLS/RTP/SAVPF 122 102 100 101 124 120 123 119
a=rtpmap:122 H264/90000</code></pre>
<h3>附加属性:<code>a=</code>
</h3>
<p>作用:用于扩展SDP。</p>
<p>有两种作用范围:会话级别(session-level)、媒体级别(media-level)。</p>
<ol>
<li>媒体级别:媒体描述(m=)后面可以跟任意数量的 a= 字段,对媒体描述进行扩展。</li>
<li>会话级别:在第一个媒体字段(media field)前,添加的 a= 字段是会话级别的。</li>
</ol>
<p>有如下两种格式:</p>
<pre><code>a=<attribute>
a=<attribute>:<value></code></pre>
<p>格式1举例:</p>
<pre><code>a=recvonly</code></pre>
<p>格式2举例:</p>
<pre><code>a=rtpmap:0 PCMU/8000</code></pre>
<h3>时间:<code>t=</code>
</h3>
<p>作用:声明会话的开始、结束时间。</p>
<p>格式如下:</p>
<pre><code>t=<start-time> <stop-time></code></pre>
<p>如果<code><stop-time></code>是0,表示会话没有结束的边界,但是需要在<code><start-time></code>之后会话才是活跃(active)的。如果<code><start-time></code>是0,表示会话是永久的。</p>
<p>举例:</p>
<pre><code>t=0 0</code></pre>
<h2>WebRTC实例</h2>
<p>下面例子来自腾讯云WebRTC服务的远端offer。</p>
<pre><code>// sdp版本号为0
v=0
// o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
// 用户名为空,会话id是8100750360520823155,会话版本是2(后面如果有类似改变编码的操作,sess-version加1),地址类型为IP4,地址为127.0.0.1(这里可以忽略)
o=- 7595655801978680453 2 IN IP4 112.90.139.105
// 会话名为空
s=-
// 会话的起始时间,都为0表示没有限制
t=0 0
a=ice-lite
// 音频、视频的传输的传输采取多路复用,通过同一个RTP通道传输音频、视频,可以参考 https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54
a=group:BUNDLE 0 1
// WMS是WebRTC Media Stram的缩写,这里给Media Stream定义了一个唯一的标识符。一个Media Stream可以有多个track(video track、audio track),这些track就是通过这个唯一标识符关联起来的,具体见下面的媒体行(m=)以及它对应的附加属性(a=ssrc:)
// 可以参考这里 http://tools.ietf.org/html/draft-ietf-mmusic-msid
a=msid-semantic: WMS 5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
// m=<media> <port> <proto> <fmt> ...
// 本次会话有音频,端口为9(可忽略,端口9为Discard Protocol专用),采用UDP传输加密的RTP包,并使用基于SRTCP的音视频反馈机制来提升传输质量,111、103、104等是audio可能采用的编码(参见前面m=的说明)
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 126
// 音频发送者的IP4地址,WebRTC采用ICE,这里的 0.0.0.0 可直接忽略
c=IN IP4 0.0.0.0
// RTCP采用的端口、IP地址(可忽略)
a=rtcp:9 IN IP4 0.0.0.0
// ice-ufrag、ice-pwd 分别为ICE协商用到的认证信息
a=ice-ufrag:58142170598604946
a=ice-pwd:71696ad0528c4adb02bb40e1
// DTLS协商过程的指纹信息
a=fingerprint:sha-256 7F:98:08:AC:17:6A:34:DB:CF:3B:EC:93:ED:57:3F:5A:9E:1F:4A:F3:DB:D5:BF:66:EE:17:58:E0:57:EC:1B:19
// 当前客户端在DTLS协商过程中,既可以作为客户端,也可以作为服务端,具体可参考 RFC4572
a=setup:actpass
// 当前媒体行的标识符(在a=group:BUNDLE 0 1 这行里面用到,这里0表示audio)
a=mid:0
// RTP允许扩展首部,这里表示采用了RFC6464定义的针对audio的扩展首部,用来调节音量,比如在大型会议中,有多个音频流,就可以用这个来调整音频混流的策略
// 这里没有vad=1,表示不启用这个音量控制
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// 表示既可以发送音频,也可以接收音频
a=sendrecv
// 表示启用多路复用,RTP、RTCP共用同个通道
a=rtcp-mux
// 下面几行都是对audio媒体行的补充说明(针对111),包括rtpmap、rtcp-fb、fmtp
// rtpmap:编解码器为opus,采样率是48000,2声道
a=rtpmap:111 opus/48000/2
// rtcp-fb:基于RTCP的反馈控制机制,可以参考 https://tools.ietf.org/html/rfc5124、https://webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02/
a=rtcp-fb:111 transport-cc
a=rtcp-fb:111 nack
// 最小的音频打包时间
a=fmtp:111 minptime=20
// 跟前面的rtpmap类似
a=rtpmap:126 telephone-event/8000
// ssrc用来对媒体进行描述,格式为a=ssrc:<ssrc-id> <attribute>:<value>,具体可参考 RFC5576
// cname用来唯一标识媒体的数据源
a=ssrc:16864608 cname:YZcxBwerFFm6GH69
// msid后面带两个id,第一个是MediaStream的id,第二个是audio track的id(跟后面的mslabel、label对应)
a=ssrc:16864608 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 128f4fa0-81dd-4c3a-bbcd-22e71e29d178
a=ssrc:16864608 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:16864608 label:128f4fa0-81dd-4c3a-bbcd-22e71e29d178
// 跟audio类似,不赘述
m=video 9 UDP/TLS/RTP/SAVPF 122 102 125 107 124 120 123 119
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:58142170598604946
a=ice-pwd:71696ad0528c4adb02bb40e1
a=fingerprint:sha-256 7F:98:08:AC:17:6A:34:DB:CF:3B:EC:93:ED:57:3F:5A:9E:1F:4A:F3:DB:D5:BF:66:EE:17:58:E0:57:EC:1B:19
a=setup:actpass
a=mid:1
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=sendrecv
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:122 H264/90000
a=rtcp-fb:122 ccm fir
a=rtcp-fb:122 nack
a=rtcp-fb:122 nack pli
a=rtcp-fb:122 goog-remb
a=rtcp-fb:122 transport-cc
a=fmtp:122 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:102 rtx/90000
a=fmtp:102 apt=122
a=rtpmap:125 H264/90000
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:124 H264/90000
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=124
a=rtpmap:123 H264/90000
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=123
a=ssrc-group:FID 33718809 50483271
a=ssrc:33718809 cname:ovaCctnHP9Asci9c
a=ssrc:33718809 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:33718809 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:33718809 label:1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:50483271 cname:ovaCctnHP9Asci9c
a=ssrc:50483271 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:50483271 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:50483271 label:1d7fc300-9889-4f94-9f35-c0bcc77a260d</code></pre>
<h2>写在后面</h2>
<p>SDP协议格式本身很简单,难点一般在于应用层在不同场景下扩展出来的属性,以及不同扩展属性对应的含义。比如上面举的例子,扩展属性、属性值的说明分散在数十个RFC里,查找、理解都费了一番功夫。</p>
<p>如有错漏,敬请指出。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=rVb25uAuNYltvkngd6jteg%3D%3D.P1y93BQWL2e3ql5DvucKCvU1gIQvVTZ%2FGWsltKZ1f5G9Sgp%2BKn2xLk8Zc9FoBuO6" rel="nofollow">SDP: Session Description Protocol</a><br><a href="https://link.segmentfault.com/?enc=7ZeHnURwylyDt25hUF4x1A%3D%3D.2KZbb6Zp9X3ya9M1ZBRsNZwv7UAYh%2Fg4zPuIoYZRVq%2FRL2JjGsGmKrLQrbRLSCR%2FOCOCy%2FiPEwKZWNTkns4IHw%3D%3D" rel="nofollow">Annotated Example SDP for WebRTC</a></p>
一点感悟:《Node.js学习笔记》star数突破1000+
https://segmentfault.com/a/1190000015192313
2018-06-06T07:23:23+08:00
2018-06-06T07:23:23+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
64
<h2>写作背景</h2>
<p>笔者前年开始撰写的<a href="https://link.segmentfault.com/?enc=x2Pe2Tffjt8cpyyxcROIXQ%3D%3D.lWd333hbWnJ%2BXcnfDp4mlVKvxHWaxg6KD2655cO4EDZ8xw4HxD1vBsqK%2BTKNt26aOFwh5sc6QlCVkeGEXhByWA%3D%3D" rel="nofollow">《Node.js学习笔记》</a> github star 数突破了1000,算是个里程碑吧。</p>
<p>从第一次提交(2016.11.03)到现在,1年半过去了。突然有些感慨,想要写点东西,谈谈这期间的收获、心路历程,以及如何学习Node.js。</p>
<p><img src="/img/bVbbUnn?w=956&h=110" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVbbUno?w=1094&h=362" alt="clipboard.png" title="clipboard.png"></p>
<h2>心路历程</h2>
<p>笔者一直有做技术笔记的习惯,前几年零零散散的也写了不少Node.js的东西,只不过都存在evernote里。写着写着,觉得有必要系统地整理下,于是就有了这个项目。</p>
<p>粗略统计了下,总共提交了约60篇教程,以及<a href="https://link.segmentfault.com/?enc=%2BwiCNtI1pXpAV3oMm%2BXmqg%3D%3D.DHwXCNdRx1v%2F7tTNm%2FfCmVAxDTBYrQJ%2BghQWIG9Wj%2BiqETxeQLQcELeuaUWMAS%2BiuY%2FZBM%2BqKgGp3mN7yf%2BCq4cHehu%2B31KLz2EZN8cJa0I%3D" rel="nofollow">将近300个范例脚本</a>。</p>
<p><img src="/img/bVbbUnp?w=1630&h=310" alt="clipboard.png" title="clipboard.png"></p>
<p>大部分的commit都集中2016年11、12月份,以及2017年上半年。这段时间其实项目组挺忙的,经常一周6天班,同时在两三个项目间来回切换。</p>
<p>写作的过程挺枯燥的,也有点累人,尤其经常只能抽大半夜或周末的时间来码字,经常写技术文章的同学应该能体会。不管怎么说,一路坚持了下来,感觉还是有不少收获。</p>
<p>1、技术积累。最初存在evernote里的只是零星的笔记,经过整理校对、进一步的思考以及延展性学习,零散的知识点逐渐串联成体系化的知识面。这比单单记住了数百个Node.js的API,以及枯燥的配置项更有用。</p>
<p>2、知识分享。写作的过程中,不少同样正在学习Node.js的同学或通过QQ,或通过私信表达了感谢。对笔者来说,这其实比star数的增加更有意义。</p>
<p>3、技术焦虑有所缓解。众所周知,前端领域变化太快,身处其中的从业者压力非常大,这也是前不久著名的“老子学不动了”的梗突然刷屏的原因。深入学习、思考,掌握学习的方法和规律,能够一定程度上缓解技术焦虑症。</p>
<p>4、意外收获。这期间,收到阿里云栖社区(专家博主)、腾讯云+社区的入驻邀请,多家知名出版社的撰稿邀请,在线教育平台(如慕课)的开课邀请等。</p>
<h2>如何学习Node.js</h2>
<p>2年前在SegmentFault社区上有人问过类似的问题<a href="https://segmentfault.com/q/1010000006807385/a-1020000006811209">《关于nodejs的学习?》</a>,当时简单地回答了下。</p>
<ol>
<li>实践是最好的学习方式,如果能把所学用到实际中去,效率比光学不练要高上很多。</li>
<li>遇到问题,学会使用google、stackoverflow、官方文档。</li>
<li>学习node的障碍,大部分时候不是node本身,而是相关领域知识。</li>
</ol>
<p>实践出真知,这点无需强调。遇到技术问题善用搜索引擎,也算是圈内共识了(初学者需要加强这方面意识)。</p>
<p>其实最难的是第3点,分辨你所遇到的问题。</p>
<p>举个例子,比如现在想学习 <a href="https://link.segmentfault.com/?enc=Y25frVCLeadAxJieppc81g%3D%3D.tObx38r0aORcCLMB4AK%2FbTP%2B4WnrVQGr7rANUWQFP3y4iYPedP4%2BwprO08y%2F1PL4" rel="nofollow">https</a> 这个模块,不少初学者会显得一筹莫展,常见的问题有:</p>
<ol>
<li>问题一:https、http、net 模块长得好像,API也差不多,它们之间是什么关系?</li>
<li>问题二:配置项里有一项是证书,这是个干嘛的?照着指引配好证书了,为什么浏览器会报错?</li>
<li>问题三:server本地跑得好好的,怎么部署到云服务器上就访问不了,明明可以ping通,端口也启动了,为什么提示拒绝访问?</li>
</ol>
<p>正式回答问题前,先祭出一张网络分层架构图,请读者把它牢记在心。</p>
<p><img src="/img/bVbbUnq?w=698&h=474" alt="clipboard.png" title="clipboard.png"></p>
<p>互联网基于分层架构实现,包括应用层、传输层、网络层、链路层、物理层。其中,前端开发者比较熟悉的是应用层(HTTP协议),如果想学习Node服务端编程,那么,至少需要对传输层(TCP)、网络层(IP)也有一定的了解。</p>
<p>对于网络的每个层次,Node.js基本都有对应的模块,比如https、http、net(TCP)、tls/crypto等。</p>
<p>前面列举的几个问题,都是对网络知识、服务器知识了解的欠缺导致的,而不是于Node.js的API有多复杂、难以理解。</p>
<p>这里直接回答问题:</p>
<ol>
<li>问题一:http为应用层模块,主要按照特定协议编解码数据;net为传输层模块,主要负责传输编码后的应用层数据;https是个综合模块(涵盖了http/tls/crypto等),主要用于确保数据安全性;该用哪个模块应该很清楚了。</li>
<li>问题二:安全证书是PKI体系的重要一环,主要用于身份校验。本地调试用的证书如果是自己签署的话,浏览器会视为不安全并报错,可以参考 《<a href="https://link.segmentfault.com/?enc=GwaoB8YcYmP%2Fjn3Hob5k%2Fg%3D%3D.ylcAZPzCvNFPPD5HFDu1BU0WkBjzy4nh0txAPHci7it2gczyKnJxsFSBQ2xkiIpB" rel="nofollow">HTTPS科普扫描帖</a>》。</li>
<li>问题三:这种情况大概率是请求被防火墙拦截。ping走的是ICMP协议,由操作系统内核处理,能够ping通不代表TCP连接就能够建立成功,可以参考 《<a href="https://link.segmentfault.com/?enc=Pyr1tDulWAzVyRqbE3avOQ%3D%3D.Vm%2BE5KmPLIld5uWhA1PGMXJNJZzEpGS54PKc7tg6ItylT62yVwRTrezmNzcZ3B89UOwbBYn3X2DovU%2Ft%2FS6Wpw%3D%3D" rel="nofollow">ping的使用与实现原理剖析</a>》</li>
</ol>
<h2>写在后面</h2>
<p>编写《Node.js学习笔记》的过程收获了不少,也有不少感触,这里就不过多碎碎念。对于“如何学习Node.js”这个问题,其实有挺多东西想写,篇幅所限,后面的文章详细展开。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=gQtZ230C%2F%2FHSX%2B7tKypP3Q%3D%3D.H6oBzvHfbvhfHTupjoDG%2Bd0%2FdLMw1Nx6pysWjrcvXA8pCsbyO2U25ADn02b3ldslrSP2g3eOrscnjLlrvY8K%2BA%3D%3D" rel="nofollow">Nodejs学习笔记</a><br><a href="https://link.segmentfault.com/?enc=WUShm%2BjYNwAj1pZ44x0F2Q%3D%3D.P9Ykwca6j7HOtth%2FJ3b14kv7a17ZxVtqIyDxQULSY4o%3D" rel="nofollow">笔者个人博客</a></p>
<p><img src="/img/bVbbUnr?w=300&h=390" alt="图片描述" title="图片描述"></p>
React 16.3来了:带着全新的Context API
https://segmentfault.com/a/1190000013203396
2018-02-08T08:17:37+08:00
2018-02-08T08:17:37+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
14
<h2>文章概览</h2>
<p>React在版本<code>16.3-alpha</code>里引入了新的Context API,社区一片期待之声。我们先通过简单的例子,看下新的Context API长啥样,然后再简单探讨下新的API的意义。</p>
<p>文中的完整代码示例可在笔者的GitHub上找到,<a href="https://link.segmentfault.com/?enc=R2P5X9TfBrtxQrNx03YcSA%3D%3D.%2BLkQlGpT4dQsXneCLNsQbYVNWnkGS6MBWXeBh0Y3XturRXJPdliHF09f1MwYnbS2LV%2Bq1vpDlBXK1FwaqRrMliEQ%2FaEo53r5Nk%2FdQwEU1GI%3D" rel="nofollow">点击传送门</a>。</p>
<h2>看下新的Context API</h2>
<p>需要安装<code>16.3-alpha</code>版本的react。构建步骤非本文重点,可参考笔者<a href="https://link.segmentfault.com/?enc=p%2BDwDRt4NRBKtfMVIDnOjg%3D%3D.6WLSfa48KDKYi9jtD9GHpnfMDDYMDKSu1zMuqhUz0VHWAagrHBQsybP9O5biRM0WrXIT%2Fxr6VKsSx3Xi1IcGZNqlZ03RXEx%2BYwd4gxp%2BbGw%3D" rel="nofollow">GitHub上的demo</a>。</p>
<pre><code class="bash">npm install react@next react-dom@next</code></pre>
<p>下面,直接来看代码,如果用过<code>react-redux</code>应该会觉得很眼熟。</p>
<p>首先,创建<code>context</code>实例:</p>
<pre><code class="js">import React from 'react';
import ReactDOM from 'react-dom';
// 创建context实例
const ThemeContext = React.createContext({
background: 'red',
color: 'white'
});</code></pre>
<p>然后,定义<code>App</code>组件,注意这里用到了<code>Provider</code>组件,类似<code>react-redux</code>的<code>Provider</code>组件。</p>
<pre><code class="js">class App extends React.Component {
render () {
return (
<ThemeContext.Provider value={{background: 'green', color: 'white'}}>
<Header />
</ThemeContext.Provider>
);
}
}</code></pre>
<p>接下来,定义<code>Header</code>、<code>Title</code>组件。注意:</p>
<ol>
<li>
<code>Title</code>组件用到了<code>Consumer</code>组件,表示要消费<code>Provider</code>传递的数据。</li>
<li>
<code>Title</code>组件是<code>App</code>的<code>孙</code>组件,但跳过了<code>Header</code>消费数据。</li>
</ol>
<pre><code class="js">class Header extends React.Component {
render () {
return (
<Title>Hello React Context API</Title>
);
}
}
class Title extends React.Component {
render () {
return (
<ThemeContext.Consumer>
{context => (
<h1 style={{background: context.background, color: context.color}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
}</code></pre>
<p>最后,常规操作</p>
<pre><code class="js">ReactDOM.render(
<App />,
document.getElementById('container')
);</code></pre>
<p>看下程序运行结果:</p>
<p><img src="/img/remote/1460000013229508?w=934&h=296" alt="" title=""></p>
<h2>为什么有新的Context API</h2>
<p>用过<code>redux + react-redux</code>的同学,应该会觉得新的Context API很眼熟。而有看过<code>react-redux</code>源码的同学就知道,<code>react-redux</code>本身就是基于旧版本的Context API实现的。</p>
<p>既然已经有了现成的解决方案,为什么还会有新的Context API呢?</p>
<ol>
<li>现有Context API的实现存在一定问题:比如当父组件的<code>shouldComponentUpdate</code>性能优化,可能会导致消费了context数据的子组件不更新。</li>
<li>降低复杂度:类似redux全家桶这样的解决方案,给项目引入了一定的复杂度,尤其是对方案了解不足的同学,遇到问题可能一筹莫展。新Context API的引入,一定程度上可以不少项目对redux全家桶的依赖。</li>
</ol>
<h2>写在后面</h2>
<p>新的Context API,个人对于性能上的提升更加期待些。至于降低复杂度、取代redux之类的,不是我关注的重点。下一步的计划就是多构造点用例来进行对比测试。</p>
<p>更多内容,欢迎大家关注我的公众号,后续进行更新</p>
<p><img src="/img/remote/1460000013229509?w=344&h=344" alt="" title=""></p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=SCAmwml6wZ%2BI7gAzgxaDmw%3D%3D.rrz8CdwUopU%2BpCCdDUBFWwKALvu5J6KncQlsAS2piIN5RXkXEzDMdkT7euWp0c21%2BnsdxqPGCgxIunoNTqmZaj6%2BYaakuQfIuUDcBwSIoeg%3D" rel="nofollow">本文完整代码示例</a></p>
<p><a href="https://link.segmentfault.com/?enc=4MLClmPC3pTMVF%2FQaTbYug%3D%3D.98aD5wPTIcgWeL34DstxQZu%2FaEglotmS2lgy1aYNOw3nqYsjl4RIVQNwN6jGoQwt5lQS%2Bs66DOlnc%2BNoSNT8qDfWsM6EG3n4NFwMrodmoIU%3D" rel="nofollow">React新的Context API的RFC</a></p>
关于:Express会被Koa2取代吗?
https://segmentfault.com/a/1190000013155921
2018-02-06T08:40:56+08:00
2018-02-06T08:40:56+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
2
<p>知会上看到有个问题<a href="https://link.segmentfault.com/?enc=jO6%2FW5tkrt6IS2hbHsMTaQ%3D%3D.VTjMJknLVRAaZyGbD%2B6OMNgRWlRZxpauNKZKqRTyfLf4gy1i%2FKc6iMiv1WGCDatbBF5iCKxTOowgQFQC6j23uA%3D%3D" rel="nofollow">《Express会被Koa2取代吗?》</a>。刚好对Express、koa有点小研究,于是简单回答了一下。</p>
<h2>1、先说结论</h2>
<p><strong>目前没有看到Express会被koa2取代的迹象。</strong></p>
<p>目前来说,Express的生态更成熟,入门门槛相对较低。从npm上的下载热度来说,两者的差距还较大,Express的月下载量约为koa2的40倍。</p>
<p>不过koa2的亮点足够吸引人,生态也开始变得完善。</p>
<h2>2、从使用门槛来说</h2>
<p>从使用上来说,Express对初学者更有好些,对着官网修修改改改就能做点东西出来。</p>
<p>koa2入门门槛比Express高些。更精简的内核带来的小问题就是,对使用者搭积木的能力要求更高了,毕竟连核心的路由功能都去掉了。</p>
<p>更不要说koa2中最吸引人的async/await,很多初学者promise都搞不明白,async/await用起来一头雾水,koa2最精华的部分之一就派不上用场了。</p>
<h2>3、从大趋势来说</h2>
<p>node社区壮大后,参与node服务端编程的同学会越来越多。届时,对服务端框架的要求会越来越高,那个时候就是各种企业级解决方案们的战场了。核心很有可能还是基于Express或者koa2,或者其他的。</p>
<p>至于Express和koa2,还是会继续有很大的市场,那个时候版本不知道是多少。</p>
<h2>4、后话</h2>
<p>Express、koa2略有小研究,最近刚撸了一遍源码。另外,常年分享周边科普文,欢迎关注 <a href="https://link.segmentfault.com/?enc=F%2BMm7578frpnj9kLdfxtBQ%3D%3D.%2FnQRaj5C1YCZrNpNcrcO39eBT5zBC8MXQzh69pWdDIU%3D" rel="nofollow">我的GitHub 程序猿小卡</a>,或者star <a href="https://link.segmentfault.com/?enc=nrAVIgpBTFh5cwXPh4pOLg%3D%3D.CUBMphHgSr5P8Xo2BcR0SRhY8p7JYUsMUSuu%2BHZOOGzPvYVQo1pBC6xCaMVoXuVqA1x7z2tOc00xSxbuQgPIpQ%3D%3D" rel="nofollow">《Nodejs学习笔记》</a></p>
<p>后续会继续分享Express或koa2周边相关的技术文章 :-)</p>
Node.js:上传文件,服务端如何获取文件上传进度
https://segmentfault.com/a/1190000013133130
2018-02-05T08:37:01+08:00
2018-02-05T08:37:01+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
7
<h2>内容概述</h2>
<p><code>multer</code>是常用的Express文件上传中间件。服务端如何获取文件上传的进度,是使用的过程中,很常见的一个问题。在SF上也有同学问了类似问题<a href="https://segmentfault.com/q/1010000013118189">《nodejs multer有没有查看文件上传进度的方法?》</a>。稍微回答了下,这里顺便整理出来,有同样疑问的同学可以参考。</p>
<p>下文主要介绍如何利用<code>progress-stream</code>获取文件上传进度,以及该组件使用过程中的注意事项。</p>
<h2>利用<code>progress-stream</code>获取文件上传进度</h2>
<p>如果只是想在服务端获取上传进度,可以试下如下代码。注意,这个模块跟Express、multer并不是强绑定关系,可以独立使用。</p>
<pre><code>var fs = require('fs');
var express = require('express');
var multer = require('multer');
var progressStream = require('progress-stream');
var app = express();
var upload = multer({ dest: 'upload/' });
app.post('/upload', function (req, res, next) {
// 创建progress stream的实例
var progress = progressStream({length: '0'}); // 注意这里 length 设置为 '0'
req.pipe(progress);
progress.headers = req.headers;
// 获取上传文件的真实长度(针对 multipart)
progress.on('length', function nowIKnowMyLength (actualLength) {
console.log('actualLength: %s', actualLength);
progress.setLength(actualLength);
});
// 获取上传进度
progress.on('progress', function (obj) {
console.log('progress: %s', obj.percentage);
});
// 实际上传文件
upload.single('logo')(progress, res, next);
});
app.post('/upload', function (req, res, next) {
res.send({ret_code: '0'});
});
app.get('/form', function(req, res, next){
var form = fs.readFileSync('./form.html', {encoding: 'utf8'});
res.send(form);
});
app.listen(3000);</code></pre>
<h2>如何获取上传文件的真实大小</h2>
<p>multipart类型,需要监听<code>length</code>来获取文件真实大小。(官方文档里是通过<code>conviction</code>事件,其实是有问题的)</p>
<pre><code class="js">// 获取上传文件的真实长度(针对 multipart)
progress.on('length', function nowIKnowMyLength (actualLength) {
console.log('actualLength: %s', actualLength);
progress.setLength(actualLength);
});</code></pre>
<h2>关于<code>progress-stream</code>获取真实文件大小的bug?</h2>
<p>针对multipart文件上传,progress-stream 实例子初始化时,参数length需要传递非数值类型,不然你获取到的进度要一直是0,最后就直接跳到100。</p>
<p>至于为什么会这样,应该是 <code>progress-steram</code> 模块的bug,看下模块的源码。当<code>length</code>是number类型时,代码直接跳过,因此你length一直被认为是0。</p>
<pre><code class="javascript">tr.on('pipe', function(stream) {
if (typeof length === 'number') return;
// Support http module
if (stream.readable && !stream.writable && stream.headers) {
return onlength(parseInt(stream.headers['content-length'] || 0));
}
// Support streams with a length property
if (typeof stream.length === 'number') {
return onlength(stream.length);
}
// Support request module
stream.on('response', function(res) {
if (!res || !res.headers) return;
if (res.headers['content-encoding'] === 'gzip') return;
if (res.headers['content-length']) {
return onlength(parseInt(res.headers['content-length']));
}
});
});</code></pre>
<h2>参考链接</h2>
<p><a href="https://link.segmentfault.com/?enc=1zHuOAoh7Hzak3bTz%2B%2BPQA%3D%3D.UFm3uRmNohGWg%2B4gOrVjplOAYdL2QfAiG2rcS43d8721kH1oed976vLlodNzhiUm" rel="nofollow">https://github.com/expressjs/multer/issues/243</a></p>
<p><a href="https://link.segmentfault.com/?enc=E7K3YvxwuHqvwHXkrSO8fg%3D%3D.XOYtmqKzad364oJdZ4KIu0it%2F0ga3TMSbQblc8e1DWVaKjbRLixprmJt4iXs5%2BUk" rel="nofollow">https://github.com/freeall/progress-stream</a></p>
Nodejs:UDP极简入门例子
https://segmentfault.com/a/1190000013092163
2018-02-02T08:45:47+08:00
2018-02-02T08:45:47+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
7
<h2>模块概览</h2>
<p>dgram模块是对UDP socket的一层封装,相对net模块简单很多,下面看例子。</p>
<blockquote>文本同步收录于GitHub主题系列<a href="https://link.segmentfault.com/?enc=i%2BcX%2Fb6eeOlFOb3nBF07TQ%3D%3D.rHQ%2FJM%2F0VvxSgzP7%2Bw0WjYwmOroUO1zn7BDh%2FVZQ0cNNKNCC4rTpQ1n64aC6T6WS%2Fd%2FEh0wsacnmROGikWzdYg%3D%3D" rel="nofollow">《Nodejs学习笔记》</a>
</blockquote>
<h2>UPD客户端 vs UDP服务端</h2>
<p>首先,启动UDP server,监听来自端口33333的请求。</p>
<p><strong>server.js</strong></p>
<pre><code class="js">// 例子:UDP服务端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
server.on('listening', function () {
var address = server.address();
console.log('UDP Server listening on ' + address.address + ":" + address.port);
});
server.on('message', function (message, remote) {
console.log(remote.address + ':' + remote.port +' - ' + message);
});
server.bind(PORT, HOST);</code></pre>
<p>然后,创建UDP socket,向端口33333发送请求。</p>
<p><strong>client.js</strong></p>
<pre><code class="js">// 例子:UDP客户端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var message = Buffer.from('My KungFu is Good!');
var client = dgram.createSocket('udp4');
client.send(message, PORT, HOST, function(err, bytes) {
if (err) throw err;
console.log('UDP message sent to ' + HOST +':'+ PORT);
client.close();
});</code></pre>
<p>运行 server.js。</p>
<pre><code class="bash">node server.js</code></pre>
<p>运行 client.js。</p>
<pre><code class="bash">➜ 2016.12.22-dgram git:(master) ✗ node client.js
UDP message sent to 127.0.0.1:33333</code></pre>
<p>服务端打印日志如下</p>
<pre><code class="bash">UDP Server listening on 127.0.0.1:33333
127.0.0.1:58940 - My KungFu is Good!</code></pre>
<h2>广播</h2>
<p>通过dgram实现广播功能很简单,服务端代码如下。</p>
<pre><code class="js">var dgram = require('dgram');
var server = dgram.createSocket('udp4');
var port = 33333;
server.on('message', function(message, rinfo){
console.log('server got message from: ' + rinfo.address + ':' + rinfo.port);
});
server.bind(port);</code></pre>
<p>接着创建客户端,向地址'255.255.255.255:33333'进行广播。</p>
<pre><code class="js">var dgram = require('dgram');
var client = dgram.createSocket('udp4');
var msg = Buffer.from('hello world');
var port = 33333;
var host = '255.255.255.255';
client.bind(function(){
client.setBroadcast(true);
client.send(msg, port, host, function(err){
if(err) throw err;
console.log('msg has been sent');
client.close();
});
});</code></pre>
<p>运行程序,最终服务端打印日志如下</p>
<pre><code class="bash">➜ 2016.12.22-dgram git:(master) ✗ node broadcast-server.js
server got message from: 192.168.0.102:61010</code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=sJOF15hP%2F3WXJFwD%2BU3JUA%3D%3D.dq%2F%2FLp5vySq4QlC4VQovzfjMSneTIKzk7NCAt514aP97TpLzyCzkinIuEo4vnyy9" rel="nofollow">https://nodejs.org/api/dgram....</a></p>
转眼人到中年:前端老程序员无法忘怀的一次百度电话面试(二)
https://segmentfault.com/a/1190000013060408
2018-01-31T08:34:59+08:00
2018-01-31T08:34:59+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
14
<h2>一切都不那么真实</h2>
<p>当一面结束时,一切都显得不那么真实。几分钟前还在着急忙慌地接招,随着电话的挂断,周遭又安静了下来,安静到感觉连脑袋都变得有些迟钝。</p>
<p>这种感觉很熟悉。多年前高考结束的那个夜晚,暴雨,回到家,一个人,对着堆成小山的习题集和试卷,说不出话来。一切都结束了,却没有意料中的狂喜。平静,甚至略带一丝压抑。</p>
<h2>等待,再次整装前行</h2>
<p>但眼前的面试还没有结束,真正的挑战也许才刚刚到来,后面还有二面、三面、N面在等着我。开场的这一仗打得有点过于顺利,接下来可能就是硬仗了。</p>
<p>也许,抛开侥幸,心怀谦逊地准备,才能得到幸运之神的垂青。</p>
<p>节后,收到了来自百度的电话,简单明了,商定了电话面试的时间。</p>
<p>经历了第一次的电话面试后,这次的等待从容了很多。也许是因为,第一次电话面试,那个年轻面试官对我的评价,让我稍微有了一些自信。</p>
<p>上课,看书,逛逛技术博客,生活的旋律依旧单调。</p>
<p>转眼间,约定之日到来。</p>
<h2>触不及防的硬仗</h2>
<p>同样的,几句话确认身份后,面试官直奔主题,这倒是不令人意外。</p>
<p>从声音上听来,这次的面试官年龄稍微大些,语调也显得比之前更沉稳和严肃,让人不免心生敬畏。</p>
<p>“为什么HTML跟CSS要分离,用内联样式不行吗?”</p>
<p>“什么是CSS的盒模型?大概介绍一下。”</p>
<p>“IE6的盒模型跟标准的盒模型有什么区别?”</p>
<p>“怎么样实现两栏自适应,有哪些方法?”</p>
<p>“什么是CSS Spirit?有什么作用?”</p>
<p>“CSS有哪些常见的兼容性问题?一般怎么解决?”</p>
<p>。。。</p>
<p>果然是场硬仗,全程问的几乎都是CSS的问题,JS几乎很少提到。</p>
<p>在当时的我看来,“前端工程师”主攻JS,“重构工程师”主攻CSS,因此把大部分的精力都投在了JS问题的准备上。</p>
<p>可能是因为,一面问的都是JS,而二面的侧重点是CSS的考察?</p>
<p>没有太多时间去想这个问题。这次面试的节奏明显更快,当有问题卡壳时,面试官并没有给太多思考的时间,有些实在回答不出来的问题,也就只好乖乖跳过。</p>
<h2>第2次一面?</h2>
<p>一路面下来,一个多小时过去了。从容,紧张,平静,内心经历了这么三个阶段的过渡。</p>
<p>面试官:“还有什么需要补充的吗。”</p>
<p>我:“目前没有,谢谢。”</p>
<p>当最后一个问题问完,我已经做好了最坏的打算。内心莫名的烦躁,想要早点结束这通电话。</p>
<p>感觉手紧张地在发抖。电话那头同样安静了一小会,应该是在记录着什么。</p>
<p>明明只要十来秒,但感觉像过了一个世纪那么长。</p>
<p>电话听筒再次传来面试官声音,语调还是那么的严肃。我下意识攥紧了拳头。</p>
<p>面试官:“这次面试差不多就到这里,面了挺长时间,相应的知识点差不多也都问到了。”</p>
<p>短暂的停顿,感觉电话那头还没讲完。我已经快站不稳了。</p>
<p>面试官:“这次一面算是过了吧,你大概什么时候有时间过来二面?”</p>
<h2>你还是学生?</h2>
<p>“一面算是过了吧”,这句话瞬间在我脑海里不断地盘旋。</p>
<p>这次面试过了。一面。</p>
<p>一面???</p>
<p>我:“您好,有个事情确认一下,今天的面试是一面还是二面?我上周已经通过过一轮电话面试了。”</p>
<p>面试官愣了一下:“啊?已经面过了?”</p>
<p>我:“是的,中秋那天。”</p>
<p>面试官:“稍等,我查一下。”</p>
<h2>带着谜团过关</h2>
<p>面试官:“抱歉久等了。我查了下面试记录,的确之前已经面过了。这样吧,这次算二面。第三面是经理面,请问你什么时候方便过来公司当面面一下。”</p>
<p>我:“能不能下下周的周末?这两周学校还要上课。”</p>
<p>面试官:“上课?。。。你还是学生?今年大几?”</p>
<p>我:“是啊,还没毕业,今年大四。”</p>
<p>面试官:“这样啊。。。我明白了,我来安排一下,到时会有邮件和短信通知,注意查收一下。”</p>
<p>我:“明白,非常感谢。”</p>
<h2>后记</h2>
<p>当天就收到了上海百度的邮件,确认了三面的的时间,还有差旅报销注意事项。</p>
<p>长这么大,没坐过火车,没出过广东省。想到要只身一人去到人生地不熟的上海面试,内心有些期待,又有些忐忑。</p>
<p>然而最终还是没能成行。</p>
<p>听说上海最近下雪了,银装素裹的外滩应该很美吧。</p>
<p>不知道飘落的雪花中,有没有谜底的答案。</p>
Node.js进阶:5分钟入门非对称加密方法
https://segmentfault.com/a/1190000013027111
2018-01-29T08:28:21+08:00
2018-01-29T08:28:21+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
4
<h2>前言</h2>
<p>刚回答了SegmentFault上一个兄弟提的问题<a href="https://segmentfault.com/q/1010000013016668/a-1020000013017090">《非对称解密出错》</a>。这个属于Node.js在安全上的应用,遇到同样问题的人应该不少,基于回答的问题,这里简单总结下。</p>
<p>非对称加密的理论知识,可以参考笔者前面的文章<a href="https://link.segmentfault.com/?enc=yw5rB6hLfDJ9DZAzq9%2BgVQ%3D%3D.8CITQXQ9h10113A0dN1xRftxyTjUb28frWdY1LDejcdiZM1kO78EE2tmVQcFogECcx%2BklU8PRPkByoopD3DouWpni4BLtFqm350yOfkDF3xpNXtGoJEjaOMtRh2rDyLytvDPGp3FP%2FJcd8G0HxeWajSIDxkHcqVYk4nZwRQO%2Fbs%3D" rel="nofollow">《NODEJS进阶:CRYPTO模块之理论篇》</a>。</p>
<p>完整的代码可以在 <a href="https://link.segmentfault.com/?enc=W94z5h%2FnERQs74tRuWLUbg%3D%3D.qDRwGzcEriprz0nG7FxnpytV%2B0t8SHyoSl6uKu1OZEIORfMpOoEA43ymnpODlqt0p575U3YsJo6DCCe53sJrTg%3D%3D" rel="nofollow">《Nodejs学习笔记》</a> 找到,也欢迎大家关注 <a href="https://link.segmentfault.com/?enc=VklTpyzkQG0dy36kyQZUmQ%3D%3D.yboyJ61ex1iedUriVhhq6RK3omdB6R4GL0w%2BKixuthA%3D" rel="nofollow">程序猿小卡的GitHub</a>。</p>
<h2>加密、解密方法</h2>
<p>在Node.js中,负责安全的模块是<code>crypto</code>。非对称加密中,公钥加密,私钥解密,加解密对应的API分别如下。</p>
<p>加密函数:</p>
<pre><code class="javascript">crypto.publicEncrypt(key, buffer)</code></pre>
<p>解密函数:</p>
<pre><code class="javascript">crypto.privateDecrypt(privateKey, buffer)</code></pre>
<h2>入门例子</h2>
<p>假设有如下<code>utils.js</code></p>
<pre><code class="javascript">// utils.js
const crypto = require('crypto');
// 加密方法
exports.encrypt = (data, key) => {
// 注意,第二个参数是Buffer类型
return crypto.publicEncrypt(key, Buffer.from(data));
};
// 解密方法
exports.decrypt = (encrypted, key) => {
// 注意,encrypted是Buffer类型
return crypto.privateDecrypt(key, encrypted);
};</code></pre>
<p>测试代码<code>app.js</code>:</p>
<pre><code class="javascript">const utils = require('./utils');
const keys = require('./keys');
const plainText = '你好,我是程序猿小卡';
const crypted = utils.encrypt(plainText, keys.pubKey); // 加密
const decrypted = utils.decrypt(crypted, keys.privKey); // 解密
console.log(decrypted.toString()); // 你好,我是程序猿小卡</code></pre>
<p>附上公钥、私钥 <code>keys.js</code>:</p>
<pre><code class="javascript">exports.privKey = `-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDFWnl8fChyKI/Tgo1ILB+IlGr8ZECKnnO8XRDwttBbf5EmG0qV
8gs0aGkh649rb75I+tMu2JSNuVj61CncL/7Ct2kAZ6CZZo1vYgtzhlFnxd4V7Ra+
aIwLZaXT/h3eE+/cFsL4VAJI5wXh4Mq4Vtu7uEjeogAOgXACaIqiFyrk3wIDAQAB
AoGBAKdrunYlqfY2fNUVAqAAdnvaVOxqa+psw4g/d3iNzjJhBRTLwDl2TZUXImEZ
QeEFueqVhoROTa/xVg/r3tshiD/QC71EfmPVBjBQJJIvJUbjtZJ/O+L2WxqzSvqe
wzYaTm6Te3kZeG/cULNMIL+xU7XsUmslbGPAurYmHA1jNKFpAkEA48aUogSv8VFn
R2QuYmilz20LkCzffK2aq2+9iSz1ZjCvo+iuFt71Y3+etWomzcZCuJ5sn0w7lcSx
nqyzCFDspQJBAN3O2VdQF3gua0Q5VHmK9AvsoXLmCfRa1RiKuFOtrtC609RfX4DC
FxDxH09UVu/8Hmdau8t6OFExcBriIYJQwDMCQQCZLjFDDHfuiFo2js8K62mnJ6SB
H0xlIrND2+/RUuTuBov4ZUC+rM7GTUtEodDazhyM4C4Yq0HfJNp25Zm5XALpAkBG
atLpO04YI3R+dkzxQUH1PyyKU6m5X9TjM7cNKcikD4wMkjK5p+S2xjYQc1AeZEYq
vc187dJPRIi4oC3PN1+tAkBuW51/5vBj+zmd73mVcTt28OmSKOX6kU29F0lvEh8I
oHiLOo285vG5ZtmXiY58tAiPVQXa7eU8hPQHTHWa9qp6
-----END RSA PRIVATE KEY-----
`;
exports.pubKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFWnl8fChyKI/Tgo1ILB+IlGr8
ZECKnnO8XRDwttBbf5EmG0qV8gs0aGkh649rb75I+tMu2JSNuVj61CncL/7Ct2kA
Z6CZZo1vYgtzhlFnxd4V7Ra+aIwLZaXT/h3eE+/cFsL4VAJI5wXh4Mq4Vtu7uEje
ogAOgXACaIqiFyrk3wIDAQAB
-----END PUBLIC KEY-----
`;</code></pre>
<h2>小结</h2>
<p>可以看到,通过Node.js进行非对称加密、解密还是挺方便的。更多用法,可以参考官方文档。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=b4TxJbOLKrpZVYIjeKJhTw%3D%3D.LB3D2wYCeixvPxYpLjFU%2FsdT5iP%2BWWVpbR7CngbT1MQ%3D" rel="nofollow">程序猿小卡的GitHub</a></p>
<p><a href="https://link.segmentfault.com/?enc=IK5G7DdmfGSUHO3SuV%2BOyA%3D%3D.l%2B6o%2BNs2%2F%2B4cuRYC%2BuMrT3y5K536e3mMJSmlR2isOc3h3hh9X5h0kIJAk0pPYcHMBhphjcZ2Kg1mR2MHRVXoVA%3D%3D" rel="nofollow">Nodejs学习笔记</a></p>
<p><a href="https://segmentfault.com/q/1010000013016668/a-1020000013017090">非对称解密出错</a></p>
<p><a href="https://link.segmentfault.com/?enc=O8%2BJ6jdooaGi2%2FFnmWUCow%3D%3D.NyXmRfOURK%2BDffIvq3lI7Vx6u%2BhBbW%2BqhpuahE6H2y9ZOTldVVersX4BW5iVACjQ" rel="nofollow">https://nodejs.org/api/crypto.html</a></p>
转眼人到中年:前端老程序员无法忘怀的一次百度电话面试
https://segmentfault.com/a/1190000012998107
2018-01-26T08:40:09+08:00
2018-01-26T08:40:09+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
27
<h2>等待,山雨欲来</h2>
<p>2010年9月22日,中秋,记得那天下着零星的小雨。大部分同学都已回了家,深秋的校园显得格外空旷寂寥。平时车来人往的校道,也只剩三三两两的行人低着头走着。</p>
<p>匆匆忙忙吃完早餐,一个人背着书包来到了教学楼,找了僻静的角落坐下。看看手机,8:45左右,离电话面试还有大概15分钟。心里有些紧张,毕竟是大厂的面试,要求肯定很严格,不知道待会会问什么问题。内心突然有些懊恼,应该提前多做些功课的。</p>
<h2>如期而至的电话</h2>
<p>时间一分一秒地流逝,心跳越来越快。9点整,上海的电话如期打来。双方确认了身份后,连自我介绍都不用,直接就进入了技术面试环节。有点出乎意料,在我彼时的设想里,第一个环节应该是自我介绍才对。</p>
<p>没有太多的时间去诧异,电话那头,面试题一个接一个地抛了过来。我把耳机听筒紧紧地贴着耳朵,生怕听不清面试的题目,或者错过关键的信息。其时,脑子飞快地转着,想着如何回答面试官的题目,以及怎么更有条理地组织我的回答。</p>
<p>“JS是如何实现继承的?”</p>
<p>“知不知道什么是跨域?什么情况下会出现跨域?有哪些解决方案?”</p>
<p>“说说你对标签语义化的了解。”</p>
<p>。。。</p>
<p>脑子里一直嗡嗡响,也不知道过了多长时间,面试官突然安静了下来。顿了大约有10秒,那边说:“技术的问题也问得差不多了,就先到这里。你这边有没有什么问题想问的?”</p>
<p>我如释重负,赶紧喘了口气,然后问了几个我之前已经准备好的问题,包括面试部门的工作,员工培训机制,学习建议等,面试官也一一解答。</p>
<h2>也许就要结束了</h2>
<p>面试终于要结束了,一直紧绷着的神经开始松弛下来,人反而紧张了起来。毕竟,后面还有生死未卜的两周在等待着我,而未知总是让人感到恐惧。</p>
<p>接下来,就是我最想听到的那句收尾的话了。</p>
<p>面试官:“那么,面试就先到这里,今天是周末,一个多小时的面试,辛苦你啦。面试结果会在两周之内反馈到你这边。”</p>
<p>周末,还是中秋,仔细想来,也是为了迁就我的时间,面试官才不得不在这么特殊的时间到公司加班。而且面试过程中,面试官挺nice的,并不是预想中高高在上冷冰冰的态度。</p>
<p>面试官的“辛苦你啦”让我有点小内疚,赶紧回道:“挺不好意思的,因为我这边时间的原因,辛苦您周末过来公司加班。中秋节快乐。” </p>
<p>具体怎么说的记不清了,只记得当时态度很真诚,并不是因为客套。</p>
<p>面试官听到我的回答后,明显楞了一下,似乎有些意外。同样祝我节日快乐后,电话那头安静了一会,只有偶尔轻轻的键盘敲击声。</p>
<h2>当幸福来敲门</h2>
<p>感觉过了好长时间,其实也可能只有十来秒。听筒里再次传来面试官的声音,不知道是不是心理作用,感觉面试官的语调跟之前有些不同。</p>
<p>“这样,我提前把面试结果告诉你,你一面通过了。在你之前也面了好多人,到目前为止,你是回答得最好的。二面具体时间稍后通知。加油哦,好好准备下一轮面试。”</p>
<p>幸福来得太突然,感觉握着电话的手都在微微颤抖。</p>
<p>“非常感谢,我一定好好准备。”</p>
<p>直到现在,我还不知道为什么面试官突然决定提前告诉我面试结果。也许,陌生面试者的一句“中秋节快乐”,触动了在他乡拼搏的年轻游子的心。</p>
<p>一切无从求证,记忆也终将随时间淡去,在那个下着蒙蒙细雨的清晨。</p>
<h2>技术面的问题</h2>
<p>一个多小时的面试,问了很多问题,事后稍事整理记录了下来,主要是围绕JavaScript展开。</p>
<p><strong>JS部分:</strong></p>
<ol>
<li>JS是如何实现继承的?</li>
<li>object的prototype是什么?(接上一个问题)</li>
<li>JS如何实现数据以及功能的封装。(即类是如何实现的)</li>
<li>如果一个标签里面包含了10000个image,如何有效地对这10000个image实现事件绑定,比如说click事件。(考察事件冒泡机制)</li>
<li>假设现在有对象A、B,A对象绑定了S事件,如何对B对象也绑定S事件?(其实不清楚)</li>
<li>如何实现跨域请求?你知道的有多少种方法?各有什么优缺点?</li>
<li>当使用隐藏框架实现跨域请求时,如果框架页跟当前页不属于同个父域,是否可以实现跨域?</li>
<li>如何实现私有变量?说出一种方法即可。</li>
<li>函数闭包使用得多吗?什么情况下需要使用函数闭包?</li>
<li>当某个事件发生时,如果获得事件发生的对象。(ff和ie不同)</li>
<li>当绑定事件时,this指针指向的是?</li>
<li>当为document绑定事件时,this指针指向的是?</li>
<li>发送ajax请求有多少个步骤?如何判定发送成功?(readyState和onreadystatechange)</li>
<li>表示请求成功返回的状态码是多少?你还知道哪些状态码?分别表示什么意思?</li>
</ol>
<p><strong>jQuery部分:</strong></p>
<ol>
<li>jQuery里如何绑定事件?有多少种方式?</li>
<li>jQuery绑定事件时,this指针指向的是?(dom对象还是jQuery对象)</li>
<li>对于页面中暂时不存在的对象,如果进行事件绑定?</li>
<li>为什么选用jQuery框架(言下之意就是还有哪些其他的框架,各有什么优缺点,即你对比之后选择的原因)</li>
<li>有没有考虑过jQuery UI?如何对jQuery UI的样式进行定制?</li>
<li>有没有自己写作jQuery插件(即如何写jQuery插件)</li>
</ol>
<p><strong>html+CSS:</strong></p>
<ol>
<li>用html+CSS实现这样的布局效果,左栏固定宽度,右栏宽度自适应并填满剩下空间。</li>
<li>说说<code><strong></code>标签和<code><b></code>标签的区别,如果让你选择,你会选择使用哪个?</li>
<li>说说你对对html标签语义化的理解。</li>
</ol>
<h2>后记:关于二面</h2>
<p>大约一周后,接到了二面的电话。面试的结果有点出乎意料,那种惊讶,夹杂着莫名其妙的情绪,至今还无法忘怀。</p>
<p>故事有点长,未完待续。</p>
React Native:真机断点调试+跨域资源加载出错问题解决
https://segmentfault.com/a/1190000012883545
2018-01-18T08:26:26+08:00
2018-01-18T08:26:26+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
2
<h2>写在前面</h2>
<p>闲来无事,折腾了一下React Native,相比之前,开发体验好了不少。但在真机断点调试那里遇到了跨域资源加载出错的问题,一番探索总算解决,目测是RN新版本调试服务的bug。</p>
<p>遇到类似问题的同学应该不少,这里做下记录,有需要的可以参考下。</p>
<h2>如何断点调试</h2>
<p>首先,在真机上加载运行RN应用(过程略)。</p>
<p>然后,摇动手机,弹出开发菜单,选择“Debug JS Remotely”。</p>
<p><img src="/img/remote/1460000012883550?w=409&h=728" alt="" title=""></p>
<p>chrome会自动打开调试界面,地址是 <a href="https://link.segmentfault.com/?enc=tGd0Nj%2FP17CSQhYRKFYElg%3D%3D.wGlK7urdw4tql1kHSCU0%2FrgXvAMI%2FB5008ZghWGaPj4%3D" rel="nofollow">http://localhost</a>:8081/debugger-ui/ 。打开控制台,找到想要调试的文件,加断点,搞定。</p>
<p><img src="/img/remote/1460000012883551?w=845&h=298" alt="" title=""></p>
<h2>问题:跨域资源加载出错</h2>
<p>理想情况下,上述步骤后,就可以愉快地断点调试了。但实际情况并没有这么顺利,按照 <a href="https://link.segmentfault.com/?enc=Nf%2FmiGsEjCwsnpM2U2mWxw%3D%3D.r9Wn5wLO8dydflyDM0aIXvanQTY00YRia3wAFaHfVOnR1cqN2yBPWy%2Bb2WBE2k9hb9xngYj6Apvzbv56lHkxeVxX6WVMeve%2BYlaUMVkAzpWpxhx44eaI5gaCV60v6lGdWPezb0d35SrgO6h8yy%2FFtQ%3D%3D" rel="nofollow">官方指引</a> 修改了host后,问题依然存在。</p>
<p>在控制台看到的错误信息如图所示,跨域资源加载出错。<strong>192.168.3.126</strong> 是本机内网的ip,而出错资源的域名是 <strong>192.168.3.126.xip.io</strong>。</p>
<p><img src="/img/remote/1460000012883552?w=766&h=172" alt="" title=""></p>
<p>在未对RN有深入了解的情况下,想到两种思路,后文会分别讲述细节。</p>
<ol>
<li>让加载出错的资源,跟调试页面变成同源的</li>
<li>让调试服务支持资源跨域加载</li>
</ol>
<h2>解决方法一:替换主机名</h2>
<p>将<code>localhost</code>替换成<code>192.168.3.126.xip.io</code>,也就是说,我们通过<a href="https://link.segmentfault.com/?enc=owuqasrZ04Cv76Z6xJCFIw%3D%3D.xAUyzZuPVeo%2FhRr%2Bp34tanXexYYS47pfoGQNUq8h%2F%2BE%3D" rel="nofollow">http://192.168.3.126.xip.io</a>:8081/debugger-ui/ 来访问调试界面。</p>
<p>调试界面正常访问,资源加载正常,done。</p>
<p><img src="/img/remote/1460000012883553?w=542&h=262" alt="" title=""></p>
<p>192.168.3.126.xip.io 这个主机名看着有点奇怪,后文会进一步介绍背后的原理。</p>
<h2>解决方法二:CORS</h2>
<p>在github issue《<a href="https://link.segmentfault.com/?enc=Clm1cZDg%2Fj23qR5Osyydhw%3D%3D.2NBXxzNLiRT7BAGocXakByhj%2BuWzphcnR98B2iwNeS%2FqgDg%2BHHZ8US4WFP%2FHO13b9sr06c3b%2FFceb08PqAGmSA%3D%3D" rel="nofollow">CORS issue with JS Remote Debugging when using xip.io</a>》里,有开发者反馈了同样的错误。</p>
<p>他是这样解决的:</p>
<p>找到<code>node_modules/metro</code>模块,修改<code>Server/index.js</code>、<code>index.js.flow</code>文件,在<code>_processDeltaRequest</code>方法里加上下面代码。</p>
<pre><code class="javascript">mres.setHeader("Access-Control-Allow-Origin", "*");</code></pre>
<p>这个方法不推荐,不过如果急着调试的话也不妨试下。</p>
<h2>192.168.3.126.xip.io是什么东东</h2>
<p>看到这个主机名不少同学可能一脸懵逼,一个似乎不存在的主机名怎么可以访问成功。</p>
<p>在控制台下ping了一下返回的是 192.168.3.126 这个ip。</p>
<p><img src="/img/remote/1460000012883554?w=570&h=366" alt="" title=""></p>
<p>其实很简单,<code>xip.io</code>是个特殊的域名,当你查询<code>xxx.xip.io</code>这个域名对应的ip地址时,它会直接返回<code>xxx</code>。</p>
<p>举例:笔者笔记本的内网ip地址是 192.168.3.126,当我 访问 192.168.3.126.xip.io,DNS查询返回的ip地址就是 192.168.3.126。</p>
<p>它的原理也很简单,xip.io 的持有者在公网自建了DNS解析服务,当用户发起 xxx.xip.io 的DNS查询时,它会直接把 xxx 返回。</p>
<h2>写在后面</h2>
<p>前面提到的跨域解决方案,其实都不尽如人意,如有更好的方案,请告诉笔者,谢谢。</p>
<h2>参考链接</h2>
<p><a href="https://link.segmentfault.com/?enc=U9otB00jgpCfPaZb%2B31Hrw%3D%3D.ynp4KrFk2DhhoyURVhJn6A%3D%3D" rel="nofollow">http://xip.io/</a></p>
<p><a href="https://link.segmentfault.com/?enc=qkj22bGt0myIrUgsPkVMOA%3D%3D.LalqojrR8qCMldRJKlo8n7Ctbrf2czckmHR15avn1tiZEoRjDQp2AUVHrExzOti7CPBsUemcick790vIHyazUg%3D%3D" rel="nofollow">CORS issue with JS Remote Debugging when using xip.io</a></p>
<p><a href="https://link.segmentfault.com/?enc=Zvd0Hs0Kv6MY9Hv96csEbQ%3D%3D.Qfy3u652BpqzSIjqhr5aRuZHafG5hxhbJ%2FbVfIBkw0SjPfAYhPt%2FdyoOuaHwfkyvYuzEPlPjwt3%2FxmbClENZ8ygUtPFjvxyTlvfFdSqeTdjKlBOGFg5zhX1c301JvxHJvHopLRX3un5zB6nhgcyw5Q%3D%3D" rel="nofollow">Debugging on a device with Chrome Developer Tools</a></p>
再见乱码:5分钟读懂MySQL字符集设置
https://segmentfault.com/a/1190000012775484
2018-01-10T08:23:52+08:00
2018-01-10T08:23:52+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
25
<h2>一、内容概述</h2>
<p>在MySQL的使用过程中,了解字符集、字符序的概念,以及不同设置对数据存储、比较的影响非常重要。不少同学在日常工作中遇到的“乱码”问题,很有可能就是因为对字符集与字符序的理解不到位、设置错误造成的。</p>
<p>本文由浅入深,分别介绍了如下内容:</p>
<ol>
<li>字符集、字符序的基本概念及联系</li>
<li>MySQL支持的字符集、字符序设置级,各设置级别之间的联系</li>
<li>server、database、table、column级字符集、字符序的查看及设置</li>
<li>应该何时设置字符集、字符序</li>
</ol>
<h2>二、字符集、字符序的概念与联系</h2>
<p>在数据的存储上,MySQL提供了不同的字符集支持。而在数据的对比操作上,则提供了不同的字符序支持。</p>
<p>MySQL提供了不同级别的设置,包括server级、database级、table级、column级,可以提供非常精准的设置。</p>
<p>什么是字符集、字符序?简单的来说:</p>
<ol>
<li>字符集(character set):定义了字符以及字符的编码。</li>
<li>字符序(collation):定义了字符的比较规则。</li>
</ol>
<p>举个例子:</p>
<p>有四个字符:A、B、a、b,这四个字符的编码分别是A = 0, B = 1, a = 2, b = 3。这里的字符 + 编码就构成了字符集(character set)。</p>
<p>如果我们想比较两个字符的大小呢?比如A、B,或者a、b,最直观的比较方式是采用它们的编码,比如因为0 < 1,所以 A < B。</p>
<p>另外,对于A、a,虽然它们编码不同,但我们觉得大小写字符应该是相等的,也就是说 A == a。</p>
<p>这上面定义了两条比较规则,这些比较规则的集合就是collation。</p>
<ol>
<li>同样是大写字符、小写字符,则比较他们的编码大小;</li>
<li>如果两个字符为大小写关系,则它们相等。</li>
</ol>
<h2>三、MySQL支持的字符集、字符序</h2>
<p>MySQL支持多种字符集 与 字符序。</p>
<ol>
<li>一个字符集对应至少一种字符序(一般是1对多)。</li>
<li>两个不同的字符集不能有相同的字符序。</li>
<li>每个字符集都有默认的字符序。</li>
</ol>
<p>上面说的比较抽象,我们看下后面几个小节就知道怎么回事了。</p>
<h3>1、查看支持的字符集</h3>
<p>可以通过以下方式查看MYSQL支持的字符集。</p>
<p>方式一:</p>
<pre><code class="sql">mysql> SHOW CHARACTER SET;
+----------+-----------------------------+---------------------+--------+
| Charset | Description | Default collation | Maxlen |
+----------+-----------------------------+---------------------+--------+
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
| dec8 | DEC West European | dec8_swedish_ci | 1 |
...省略</code></pre>
<p>方式二:</p>
<pre><code class="sql">mysql> use information_schema;
mysql> select * from CHARACTER_SETS;
+--------------------+----------------------+-----------------------------+--------+
| CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION | MAXLEN |
+--------------------+----------------------+-----------------------------+--------+
| big5 | big5_chinese_ci | Big5 Traditional Chinese | 2 |
| dec8 | dec8_swedish_ci | DEC West European | 1 |
...省略</code></pre>
<p>当使用<code>SHOW CHARACTER SET</code>查看时,也可以加上<code>WHERE</code>或<code>LIKE</code>限定条件。</p>
<p>例子一:使用<code>WHERE</code>限定条件。</p>
<pre><code class="bash">mysql> SHOW CHARACTER SET WHERE Charset="utf8";
+---------+---------------+-------------------+--------+
| Charset | Description | Default collation | Maxlen |
+---------+---------------+-------------------+--------+
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
+---------+---------------+-------------------+--------+
1 row in set (0.00 sec)</code></pre>
<p>例子二:使用<code>LIKE</code>限定条件。</p>
<pre><code class="bash">mysql> SHOW CHARACTER SET LIKE "utf8%";
+---------+---------------+--------------------+--------+
| Charset | Description | Default collation | Maxlen |
+---------+---------------+--------------------+--------+
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
+---------+---------------+--------------------+--------+
2 rows in set (0.00 sec)</code></pre>
<h3>2、查看支持的字符序</h3>
<p>类似的,可以通过如下方式查看MYSQL支持的字符序。</p>
<p>方式一:通过<code>SHOW COLLATION</code>进行查看。</p>
<p>可以看到,<code>utf8</code>字符集有超过10种字符序。通过<code>Default</code>的值是否为<code>Yes</code>,判断是否默认的字符序。</p>
<pre><code class="bash">mysql> SHOW COLLATION WHERE Charset = 'utf8';
+--------------------------+---------+-----+---------+----------+---------+
| Collation | Charset | Id | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci | utf8 | 33 | Yes | Yes | 1 |
| utf8_bin | utf8 | 83 | | Yes | 1 |
...略</code></pre>
<p>方式二:查询<code>information_schema.COLLATIONS</code>。</p>
<pre><code class="sql">mysql> USE information_schema;
mysql> SELECT * FROM COLLATIONS WHERE CHARACTER_SET_NAME="utf8";
+--------------------------+--------------------+-----+------------+-------------+---------+
| COLLATION_NAME | CHARACTER_SET_NAME | ID | IS_DEFAULT | IS_COMPILED | SORTLEN |
+--------------------------+--------------------+-----+------------+-------------+---------+
| utf8_general_ci | utf8 | 33 | Yes | Yes | 1 |
| utf8_bin | utf8 | 83 | | Yes | 1 |
| utf8_unicode_ci | utf8 | 192 | | Yes | 8 |</code></pre>
<h3>3、字符序的命名规范</h3>
<p>字符序的命名,以其对应的字符集作为前缀,如下所示。比如字符序<code>utf8_general_ci</code>,标明它是字符集<code>utf8</code>的字符序。</p>
<p>更多规则可以参考 <a href="https://link.segmentfault.com/?enc=x6N4xRMil1eZpGfLI23vvw%3D%3D.WlaNIb4wWH80a%2BKODj7hy2CZYiDf1jscrqrqQnKRVZjtmUpqPWGvsCLcJzV%2BRbcE3i8h8jAfC0BeJ9QEoQ3x10YgS5KFQ8DzQmO85OAqR0s%3D" rel="nofollow">官方文档</a>。</p>
<pre><code class="sql">MariaDB [information_schema]> SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM COLLATIONS WHERE CHARACTER_SET_NAME="utf8" limit 2;
+--------------------+-----------------+
| CHARACTER_SET_NAME | COLLATION_NAME |
+--------------------+-----------------+
| utf8 | utf8_general_ci |
| utf8 | utf8_bin |
+--------------------+-----------------+
2 rows in set (0.00 sec)</code></pre>
<h2>四、server的字符集、字符序</h2>
<p>用途:当你创建数据库,且没有指定字符集、字符序时,server字符集、server字符序就会作为该数据库的默认字符集、排序规则。</p>
<p>如何指定:MySQL服务启动时,可通过命令行参数指定。也可以通过配置文件的变量指定。</p>
<p>server默认字符集、字符序:在MySQL编译的时候,通过编译参数指定。</p>
<p><code>character_set_server</code>、<code>collation_server</code>分别对应server字符集、server字符序。</p>
<h3>1、查看server字符集、字符序</h3>
<p>分别对应<code>character_set_server</code>、<code>collation_server</code>两个系统变量。</p>
<pre><code class="bash">mysql> SHOW VARIABLES LIKE "character_set_server";
mysql> SHOW VARIABLES LIKE "collation_server";</code></pre>
<h3>2、启动服务时指定</h3>
<p>可以在MySQL服务启动时,指定server字符集、字符序。如不指定,默认的字符序分别为<code>latin1</code>、<code>latin1_swedish_ci</code></p>
<pre><code class="sql">mysqld --character-set-server=latin1 \
--collation-server=latin1_swedish_ci</code></pre>
<p>单独指定server字符集,此时,server字符序为<code>latin1</code>的默认字符序<code>latin1_swedish_ci</code>。</p>
<pre><code class="sql">mysqld --character-set-server=latin1</code></pre>
<h3>3、配置文件指定</h3>
<p>除了在命令行参数里指定,也可以在配置文件里指定,如下所示。</p>
<pre><code>[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
collation-server = utf8_unicode_ci
init-connect='SET NAMES utf8'
character-set-server = utf8</code></pre>
<h3>4、运行时修改</h3>
<p>例子:运行时修改(重启后会失效,如果想要重启后保持不变,需要写进配置文件里)</p>
<pre><code class="sql">mysql> SET character_set_server = utf8 ;</code></pre>
<h3>5、编译时指定默认字符集、字符序</h3>
<p><code>character_set_server</code>、<code>collation_server</code>的默认值,可以在MySQL编译时,通过编译选项指定:</p>
<pre><code class="bash">cmake . -DDEFAULT_CHARSET=latin1 \
-DDEFAULT_COLLATION=latin1_german1_ci</code></pre>
<h2>五、database的字符集、字符序</h2>
<p>用途:指定数据库级别的字符集、字符序。同一个MySQL服务下的数据库,可以分别指定不同的字符集/字符序。</p>
<h3>1、设置数据的字符集/字符序</h3>
<p>可以在创建、修改数据库的时候,通过<code>CHARACTER SET</code>、<code>COLLATE</code>指定数据库的字符集、排序规则。</p>
<p>创建数据库:</p>
<pre><code>CREATE DATABASE db_name
[[DEFAULT] CHARACTER SET charset_name]
[[DEFAULT] COLLATE collation_name]</code></pre>
<p>修改数据库:</p>
<pre><code>ALTER DATABASE db_name
[[DEFAULT] CHARACTER SET charset_name]
[[DEFAULT] COLLATE collation_name]</code></pre>
<p>例子:创建数据库<code>test_schema</code>,字符集设置为<code>utf8</code>,此时默认的排序规则为<code>utf8_general_ci</code>。</p>
<pre><code class="sql">CREATE DATABASE `test_schema` DEFAULT CHARACTER SET utf8;</code></pre>
<h3>2、查看数据库的字符集/字符序</h3>
<p>有3种方式可以查看数据库的字符集/字符序。</p>
<p>例子一:查看<code>test_schema</code>的字符集、排序规则。(需要切换默认数据库)</p>
<pre><code class="sql">mysql> use test_schema;
Database changed
mysql> SELECT @@character_set_database, @@collation_database;
+--------------------------+----------------------+
| @@character_set_database | @@collation_database |
+--------------------------+----------------------+
| utf8 | utf8_general_ci |
+--------------------------+----------------------+
1 row in set (0.00 sec)</code></pre>
<p>例子二:也可以通过下面命令查看<code>test_schema</code>的字符集、数据库(不需要切换默认数据库)</p>
<pre><code class="sql">mysql> SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE schema_name="test_schema";
+-------------+----------------------------+------------------------+
| SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME |
+-------------+----------------------------+------------------------+
| test_schema | utf8 | utf8_general_ci |
+-------------+----------------------------+------------------------+
1 row in set (0.00 sec)</code></pre>
<p>例子三:也可以通过查看创建数据库的语句,来查看字符集。</p>
<pre><code class="sql">mysql> SHOW CREATE DATABASE test_schema;
+-------------+----------------------------------------------------------------------+
| Database | Create Database |
+-------------+----------------------------------------------------------------------+
| test_schema | CREATE DATABASE `test_schema` /*!40100 DEFAULT CHARACTER SET utf8 */ |
+-------------+----------------------------------------------------------------------+
1 row in set (0.00 sec)</code></pre>
<h3>3、database字符集、字符序是怎么确定的</h3>
<ul>
<li>创建数据库时,指定了<code>CHARACTER SET</code>或<code>COLLATE</code>,则以对应的字符集、排序规则为准。</li>
<li>创建数据库时,如果没有指定字符集、排序规则,则以<code>character_set_server</code>、<code>collation_server</code>为准。</li>
</ul>
<h2>六、table的字符集、字符序</h2>
<p>创建表、修改表的语法如下,可通过<code>CHARACTER SET</code>、<code>COLLATE</code>设置字符集、字符序。</p>
<pre><code class="sql">CREATE TABLE tbl_name (column_list)
[[DEFAULT] CHARACTER SET charset_name]
[COLLATE collation_name]]
ALTER TABLE tbl_name
[[DEFAULT] CHARACTER SET charset_name]
[COLLATE collation_name]</code></pre>
<h3>1、创建table并指定字符集/字符序</h3>
<p>例子如下,指定字符集为<code>utf8</code>,字符序则采用默认的。</p>
<pre><code class="sql">CREATE TABLE `test_schema`.`test_table` (
`id` INT NOT NULL COMMENT '',
PRIMARY KEY (`id`) COMMENT '')
DEFAULT CHARACTER SET = utf8;</code></pre>
<h3>2、查看table的字符集/字符序</h3>
<p>同样,有3种方式可以查看table的字符集/字符序。</p>
<p>方式一:通过<code>SHOW TABLE STATUS</code>查看table状态,注意<code>Collation</code>为<code>utf8_general_ci</code>,对应的字符集为<code>utf8</code>。</p>
<pre><code class="sql">MariaDB [blog]> SHOW TABLE STATUS FROM test_schema \G;
*************************** 1. row ***************************
Name: test_table
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 0
Avg_row_length: 0
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 11534336
Auto_increment: NULL
Create_time: 2018-01-09 16:10:42
Update_time: NULL
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)</code></pre>
<p>方式二:查看<code>information_schema.TABLES</code>的信息。</p>
<pre><code class="sql">mysql> USE test_schema;
mysql> SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = "test_schema" AND TABLE_NAME = "test_table";
+-----------------+
| TABLE_COLLATION |
+-----------------+
| utf8_general_ci |
+-----------------+</code></pre>
<p>方式三:通过<code>SHOW CREATE TABLE</code>确认。</p>
<pre><code>mysql> SHOW CREATE TABLE test_table;
+------------+----------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+------------+----------------------------------------------------------------------------------------------------------------+
| test_table | CREATE TABLE `test_table` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+------------+----------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)</code></pre>
<h3>3、table字符集、字符序如何确定</h3>
<p>假设<code>CHARACTER SET</code>、<code>COLLATE</code>的值分别是<code>charset_name</code>、<code>collation_name</code>。如果创建table时:</p>
<ul>
<li>明确了<code>charset_name</code>、<code>collation_name</code>,则采用<code>charset_name</code>、<code>collation_name</code>。</li>
<li>只明确了<code>charset_name</code>,但<code>collation_name</code>未明确,则字符集采用<code>charset_name</code>,字符序采用<code>charset_name</code>对应的默认字符序。</li>
<li>只明确了<code>collation_name</code>,但<code>charset_name</code>未明确,则字符序采用<code>collation_name</code>,字符集采用<code>collation_name</code>关联的字符集。</li>
<li>
<code>charset_name</code>、<code>collation_name</code>均未明确,则采用数据库的字符集、字符序设置。</li>
</ul>
<h2>七、column的字符集、排序</h2>
<p>类型为CHAR、VARCHAR、TEXT的列,可以指定字符集/字符序,语法如下:</p>
<pre><code class="sql">col_name {CHAR | VARCHAR | TEXT} (col_length)
[CHARACTER SET charset_name]
[COLLATE collation_name]</code></pre>
<h3>1、新增column并指定字符集/排序规则</h3>
<p>例子如下:(创建table类似)</p>
<pre><code class="sql">mysql> ALTER TABLE test_table ADD COLUMN char_column VARCHAR(25) CHARACTER SET utf8;</code></pre>
<h3>2、查看column的字符集/字符序</h3>
<p>例子如下:</p>
<pre><code class="sql">mysql> SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA="test_schema" AND TABLE_NAME="test_table" AND COLUMN_NAME="char_column";
+--------------------+-----------------+
| CHARACTER_SET_NAME | COLLATION_NAME |
+--------------------+-----------------+
| utf8 | utf8_general_ci |
+--------------------+-----------------+
1 row in set (0.00 sec)</code></pre>
<h3>3、column字符集/排序规则确定</h3>
<p>假设<code>CHARACTER SET</code>、<code>COLLATE</code>的值分别是<code>charset_name</code>、<code>collation_name</code>:</p>
<ul>
<li>如果<code>charset_name</code>、<code>collation_name</code>均明确,则字符集、字符序以<code>charset_name</code>、<code>collation_name</code>为准。</li>
<li>只明确了<code>charset_name</code>,<code>collation_name</code>未明确,则字符集为<code>charset_name</code>,字符序为<code>charset_name</code>的默认字符序。</li>
<li>只明确了<code>collation_name</code>,<code>charset_name</code>未明确,则字符序为<code>collation_name</code>,字符集为<code>collation_name</code>关联的字符集。</li>
<li>
<code>charset_name</code>、<code>collation_name</code>均未明确,则以table的字符集、字符序为准。</li>
</ul>
<h2>八、选择:何时设置字符集、字符序</h2>
<p>一般来说,可以在三个地方进行配置:</p>
<ol>
<li>创建数据库的时候进行配置。</li>
<li>mysql server启动的时候进行配置。</li>
<li>从源码编译mysql的时候,通过编译参数进行配置</li>
</ol>
<h3>1、方式一:创建数据库的时候进行配置</h3>
<p>这种方式比较灵活,也比较保险,它不依赖于默认的字符集/字符序。当你创建数据库的时候指定字符集/字符序,后续创建table、column的时候,如果不特殊指定,会继承对应数据库的字符集/字符序。</p>
<pre><code class="sql">CREATE DATABASE mydb
DEFAULT CHARACTER SET utf8
DEFAULT COLLATE utf8_general_ci;</code></pre>
<h3>2、方式二:mysql server启动的时候进行配置</h3>
<p>可以添加以下配置,这样mysql server启动的时候,会对character-set-server、collation-server进行配置。</p>
<p>当你通过mysql client创建database/table/column,且没有显示声明字符集/字符序,那么就会用character-set-server/collation-server作为默认的字符集/字符序。</p>
<p>另外,client、server连接时的字符集/字符序,还是需要通过SET NAMES进行设置。</p>
<pre><code class="bash">[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci</code></pre>
<h3>3、方式三:从源码编译mysql的时候,通过编译参数进行设置</h3>
<p>编译的时候如果指定了<code>-DDEFAULT_CHARSET</code>和<code>-DDEFAULT_COLLATION</code>,那么:</p>
<ul>
<li>创建database、table时,会将其作为默认的字符集/字符序。</li>
<li>client连接server时,会将其作为默认的字符集/字符序。(不用单独SET NAMES)</li>
</ul>
<pre><code class="bash">shell> cmake . -DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci</code></pre>
<h2>九、写在后面</h2>
<p>本文较为详细地介绍了MySQL中字符集、字符序相关的内容,这部分内容主要针对的是数据的存储与比较。其实还有很重要的一部分内容还没涉及:针对连接的字符集、字符序设置。</p>
<p>由于连接的字符集、字符序设置不当导致的乱码问题也非常多,这部分内容展开来讲内容也不少,放在下一篇文章进行讲解。</p>
<p>篇幅所限,有些内容没有细讲,感兴趣的同学欢迎交流,或者查看官方文档。如有错漏,敬请指出。</p>
<h2>十、相关链接</h2>
<p>10.1 Character Set Support<br><a href="https://link.segmentfault.com/?enc=mF%2FMcn1qBUASLmKCk8KN%2Bw%3D%3D.VRQJ7LMNhNWS7j%2BczOAKQPTO3sSqMK%2F%2Fxx5Ufh4vNFm0ojL9IVVB6ZaxEFTSjEbJvEypgCtGS3fmAolDAuFFJg%3D%3D" rel="nofollow">https://dev.mysql.com/doc/ref...</a></p>
WebSocket:5分钟从入门到精通
https://segmentfault.com/a/1190000012709475
2018-01-05T01:47:39+08:00
2018-01-05T01:47:39+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
97
<h2>一、内容概览</h2>
<p>WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket如何建立连接、交换数据的细节,以及数据帧的格式。此外,还简要介绍了针对WebSocket的安全攻击,以及协议是如何抵御类似攻击的。</p>
<h2>二、什么是WebSocket</h2>
<p>HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。</p>
<p>对大部分web开发者来说,上面这段描述有点枯燥,其实只要记住几点:</p>
<ol>
<li>WebSocket可以在浏览器里使用</li>
<li>支持双向通信</li>
<li>使用很简单</li>
</ol>
<h3>1、有哪些优点</h3>
<p>说到优点,这里的对比参照物是HTTP协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。</p>
<ol>
<li>支持双向通信,实时性更强。</li>
<li>更好的二进制支持。</li>
<li>较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。</li>
<li>支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)</li>
</ol>
<p>对于后面两点,没有研究过WebSocket协议规范的同学可能理解起来不够直观,但不影响对WebSocket的学习和使用。</p>
<h3>2、需要学习哪些东西</h3>
<p>对网络应用层协议的学习来说,最重要的往往就是<strong>连接建立过程</strong>、<strong>数据交换过程</strong>。当然,数据的格式是逃不掉的,因为它直接决定了协议本身的能力。好的数据格式能让协议更高效、扩展性更好。</p>
<p>下文主要围绕下面几点展开:</p>
<ol>
<li>如何建立连接</li>
<li>如何交换数据</li>
<li>数据帧格式</li>
<li>如何维持连接</li>
</ol>
<h2>三、入门例子</h2>
<p>在正式介绍协议细节前,先来看一个简单的例子,有个直观感受。例子包括了WebSocket服务端、WebSocket客户端(网页端)。完整代码可以在 <a href="https://link.segmentfault.com/?enc=LSgb9Na%2FaCQ2EDsEtaAdhA%3D%3D.WdJlxi%2FJLmd98d3EC51VNN7Z8mYc3RqUMVo0UA1zoBopPP9UIJND%2FJTma8GMK45YwD8pKwKvEH4NyI8lSZc0mQ2TvUb0sGw7bfNYogQRA5s%3D" rel="nofollow">这里</a> 找到。</p>
<p>这里服务端用了<code>ws</code>这个库。相比大家熟悉的<code>socket.io</code>,<code>ws</code>实现更轻量,更适合学习的目的。</p>
<h3>1、服务端</h3>
<p>代码如下,监听8080端口。当有新的连接请求到达时,打印日志,同时向客户端发送消息。当收到到来自客户端的消息时,同样打印日志。</p>
<pre><code class="javascript">var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
console.log('server: receive connection.');
ws.on('message', function incoming(message) {
console.log('server: received: %s', message);
});
ws.send('world');
});
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);</code></pre>
<h3>2、客户端</h3>
<p>代码如下,向8080端口发起WebSocket连接。连接建立后,打印日志,同时向服务端发送消息。接收到来自服务端的消息后,同样打印日志。</p>
<pre><code class="htmlbars"><script>
var ws = new WebSocket('ws://localhost:8080');
ws.onopen = function () {
console.log('ws onopen');
ws.send('from client: hello');
};
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
</script></code></pre>
<h3>3、运行结果</h3>
<p>可分别查看服务端、客户端的日志,这里不展开。</p>
<p>服务端输出:</p>
<pre><code class="bash">server: receive connection.
server: received hello</code></pre>
<p>客户端输出:</p>
<pre><code class="bash">client: ws connection is open
client: received world</code></pre>
<h2>四、如何建立连接</h2>
<p>前面提到,WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。</p>
<h3>1、客户端:申请协议升级</h3>
<p>首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持<code>GET</code>方法。</p>
<pre><code class="http">GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==</code></pre>
<p>重点请求首部意义如下:</p>
<ul>
<li>
<code>Connection: Upgrade</code>:表示要升级协议</li>
<li>
<code>Upgrade: websocket</code>:表示要升级到websocket协议。</li>
<li>
<code>Sec-WebSocket-Version: 13</code>:表示websocket的版本。如果服务端不支持该版本,需要返回一个<code>Sec-WebSocket-Version</code>header,里面包含服务端支持的版本号。</li>
<li>
<code>Sec-WebSocket-Key</code>:与后面服务端响应首部的<code>Sec-WebSocket-Accept</code>是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。</li>
</ul>
<blockquote>注意,上面请求省略了部分非重点请求首部。由于是标准的HTTP请求,类似Host、Origin、Cookie等请求首部会照常发送。在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。</blockquote>
<h3>2、服务端:响应协议升级</h3>
<p>服务端返回内容如下,状态代码<code>101</code>表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。</p>
<pre><code class="http">HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
</code></pre>
<blockquote>备注:每个header都以<code>\r\n</code>结尾,并且最后一行加上一个额外的空行<code>\r\n</code>。此外,服务端回应的HTTP状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码。</blockquote>
<h3>3、Sec-WebSocket-Accept的计算</h3>
<p><code>Sec-WebSocket-Accept</code>根据客户端请求首部的<code>Sec-WebSocket-Key</code>计算出来。</p>
<p>计算公式为:</p>
<ol>
<li>将<code>Sec-WebSocket-Key</code>跟<code>258EAFA5-E914-47DA-95CA-C5AB0DC85B11</code>拼接。</li>
<li>通过SHA1计算出摘要,并转成base64字符串。</li>
</ol>
<p>伪代码如下:</p>
<pre><code class="javascript">>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )</code></pre>
<p>验证下前面的返回结果:</p>
<pre><code class="javascript">const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';
let secWebSocketAccept = crypto.createHash('sha1')
.update(secWebSocketKey + magic)
.digest('base64');
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=</code></pre>
<h2>五、数据帧格式</h2>
<p>客户端、服务端数据的交换,离不开数据帧格式的定义。因此,在实际讲解数据交换之前,我们先来看下WebSocket的数据帧格式。</p>
<p>WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。</p>
<ol>
<li>发送端:将消息切割成多个帧,并发送给服务端;</li>
<li>接收端:接收消息帧,并将关联的帧重新组装成完整的消息;</li>
</ol>
<p>本节的重点,就是讲解<strong>数据帧</strong>的格式。详细定义可参考 <a href="https://link.segmentfault.com/?enc=Hz36qtLJGheHMEl3UB%2B5iw%3D%3D.DzgA2X0M2jDch81ezoxpeEyy9VrXdQs5Bo2yCQA0T0B0qIsv%2F7d0XiOEK8ZeA69J" rel="nofollow">RFC6455 5.2节</a> 。</p>
<h3>1、数据帧格式概览</h3>
<p>下面给出了WebSocket数据帧的统一格式。熟悉TCP/IP协议的同学对这样的图应该不陌生。</p>
<ol>
<li>从左到右,单位是比特。比如<code>FIN</code>、<code>RSV1</code>各占据1比特,<code>opcode</code>占据4比特。</li>
<li>内容包括了标识、操作代码、掩码、数据、数据长度等。(下一小节会展开)</li>
</ol>
<pre><code> 0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+</code></pre>
<h3>2、数据帧格式详解</h3>
<p>针对前面的格式概览图,这里逐个字段进行讲解,如有不清楚之处,可参考协议规范,或留言交流。</p>
<p><strong>FIN</strong>:1个比特。</p>
<p>如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。</p>
<p><strong>RSV1, RSV2, RSV3</strong>:各占1个比特。</p>
<p>一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。</p>
<p><strong>Opcode</strong>: 4个比特。</p>
<p>操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:</p>
<ul>
<li>%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。</li>
<li>%x1:表示这是一个文本帧(frame)</li>
<li>%x2:表示这是一个二进制帧(frame)</li>
<li>%x3-7:保留的操作代码,用于后续定义的非控制帧。</li>
<li>%x8:表示连接断开。</li>
<li>%x9:表示这是一个ping操作。</li>
<li>%xA:表示这是一个pong操作。</li>
<li>%xB-F:保留的操作代码,用于后续定义的控制帧。</li>
</ul>
<p><strong>Mask</strong>: 1个比特。</p>
<p>表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。</p>
<p>如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。</p>
<p>如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。</p>
<p>掩码的算法、用途在下一小节讲解。</p>
<p><strong>Payload length</strong>:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。</p>
<p>假设数Payload length === x,如果</p>
<ul>
<li>x为0~126:数据的长度为x字节。</li>
<li>x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。</li>
<li>x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。</li>
</ul>
<p>此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。</p>
<p><strong>Masking-key</strong>:0或4字节(32位)</p>
<p>所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。</p>
<p>备注:载荷数据的长度,不包括mask key的长度。</p>
<p><strong>Payload data</strong>:(x+y) 字节</p>
<p>载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。</p>
<p>扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。</p>
<p>应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。</p>
<h3>3、掩码算法</h3>
<p>掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:</p>
<p>首先,假设:</p>
<ul>
<li>original-octet-i:为原始数据的第i字节。</li>
<li>transformed-octet-i:为转换后的数据的第i字节。</li>
<li>j:为<code>i mod 4</code>的结果。</li>
<li>masking-key-octet-j:为mask key第j字节。</li>
</ul>
<p>算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。</p>
<blockquote>j = i MOD 4<br>transformed-octet-i = original-octet-i XOR masking-key-octet-j</blockquote>
<h2>六、数据传递</h2>
<p>一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。</p>
<p>WebSocket根据<code>opcode</code>来区分操作的类型。比如<code>0x8</code>表示断开连接,<code>0x0</code>-<code>0x2</code>表示数据交互。</p>
<h3>1、数据分片</h3>
<p>WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据<code>FIN</code>的值来判断,是否已经收到消息的最后一个数据帧。</p>
<p>FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。</p>
<p>此外,<code>opcode</code>在数据交换的场景下,表示的是数据的类型。<code>0x01</code>表示文本,<code>0x02</code>表示二进制。而<code>0x00</code>比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。</p>
<h3>2、数据分片例子</h3>
<p>直接看例子更形象些。下面例子来自<a href="https://link.segmentfault.com/?enc=s5WwpYqQIK9%2FWcj2OhoPRA%3D%3D.6HvuK01lx44a2%2FvWxquw0JSTWRpwXZe4nbRzB3VhJYrWlxCI5lhcsibFD%2B%2FtmSuIAoiLwziLyaM2caUsYOOKOrkA5CPctuyVS2Yh4R3k2o%2BkgDrAE2nF6aY%2FBzlYd7yg" rel="nofollow">MDN</a>,可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。</p>
<p><strong>第一条消息</strong></p>
<p>FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。</p>
<p><strong>第二条消息</strong></p>
<ol>
<li>FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。</li>
<li>FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。</li>
<li>FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。</li>
</ol>
<pre><code>Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!</code></pre>
<h2>七、连接保持+心跳</h2>
<p>WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。</p>
<p>但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。</p>
<ul>
<li>发送方->接收方:ping</li>
<li>接收方->发送方:pong</li>
</ul>
<p>ping、pong的操作,对应的是WebSocket的两个控制帧,<code>opcode</code>分别是<code>0x9</code>、<code>0xA</code>。</p>
<p>举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用<code>ws</code>模块)</p>
<pre><code class="javascript">ws.ping('', false, true);</code></pre>
<h2>八、Sec-WebSocket-Key/Accept的作用</h2>
<p>前面提到了,<code>Sec-WebSocket-Key/Sec-WebSocket-Accept</code>在主要作用在于提供基础的防护,减少恶意连接、意外连接。</p>
<p>作用大致归纳如下:</p>
<ol>
<li>避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)</li>
<li>确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)</li>
<li>用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)</li>
<li>可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。</li>
<li>Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。</li>
</ol>
<blockquote>强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证。</blockquote>
<h2>九、数据掩码的作用</h2>
<p>WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。</p>
<p>那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。</p>
<p>答案还是两个字:<strong>安全</strong>。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。</p>
<h3>1、代理缓存污染攻击</h3>
<p>下面摘自2010年关于安全的一段讲话。其中提到了代理服务器在协议实现上的缺陷可能导致的安全问题。<a href="https://link.segmentfault.com/?enc=2n3EuCN8HY85Yufc0nHsVQ%3D%3D.5SJdD3Juy9jY4gRDLwCmkm%2FysuLu1CSOavsqvgYGq4bjd%2BVSZnNnC%2BTvYqrJ9Hsv" rel="nofollow">猛击出处</a>。</p>
<blockquote>“We show, empirically, that the current version of the WebSocket consent mechanism is vulnerable to proxy cache poisoning attacks. Even though the WebSocket handshake is based on HTTP, which should be understood by most network intermediaries, the handshake uses the esoteric “Upgrade” mechanism of HTTP [5]. In our experiment, we find that many proxies do not implement the Upgrade mechanism properly, which causes the handshake to succeed even though subsequent traffic over the socket will be misinterpreted by the proxy.”<p>[TALKING] Huang, L-S., Chen, E., Barth, A., Rescorla, E., and C.</p>
</blockquote>
<pre><code> Jackson, "Talking to Yourself for Fun and Profit", 2010,
</code></pre>
<p>在正式描述攻击步骤之前,我们假设有如下参与者:</p>
<ul>
<li>攻击者、攻击者自己控制的服务器(简称“邪恶服务器”)、攻击者伪造的资源(简称“邪恶资源”)</li>
<li>受害者、受害者想要访问的资源(简称“正义资源”)</li>
<li>受害者实际想要访问的服务器(简称“正义服务器”)</li>
<li>中间代理服务器</li>
</ul>
<p>攻击步骤一:</p>
<ol>
<li>
<strong>攻击者</strong>浏览器 向 <strong>邪恶服务器</strong> 发起WebSocket连接。根据前文,首先是一个协议升级请求。</li>
<li>协议升级请求 实际到达 <strong>代理服务器</strong>。</li>
<li>
<strong>代理服务器</strong> 将协议升级请求转发到 <strong>邪恶服务器</strong>。</li>
<li>
<strong>邪恶服务器</strong> 同意连接,<strong>代理服务器</strong> 将响应转发给 <strong>攻击者</strong>。</li>
</ol>
<p>由于 upgrade 的实现上有缺陷,<strong>代理服务器</strong> 以为之前转发的是普通的HTTP消息。因此,当<strong>协议服务器</strong> 同意连接,<strong>代理服务器</strong> 以为本次会话已经结束。</p>
<p>攻击步骤二:</p>
<ol>
<li>
<strong>攻击者</strong> 在之前建立的连接上,通过WebSocket的接口向 <strong>邪恶服务器</strong> 发送数据,且数据是精心构造的HTTP格式的文本。其中包含了 <strong>正义资源</strong> 的地址,以及一个伪造的host(指向<strong>正义服务器</strong>)。(见后面报文)</li>
<li>请求到达 <strong>代理服务器</strong> 。虽然复用了之前的TCP连接,但 <strong>代理服务器</strong> 以为是新的HTTP请求。</li>
<li>
<strong>代理服务器</strong> 向 <strong>邪恶服务器</strong> 请求 <strong>邪恶资源</strong>。</li>
<li>
<strong>邪恶服务器</strong> 返回 <strong>邪恶资源</strong>。<strong>代理服务器</strong> 缓存住 <strong>邪恶资源</strong>(url是对的,但host是 <strong>正义服务器</strong> 的地址)。</li>
</ol>
<p>到这里,受害者可以登场了:</p>
<ol>
<li>
<strong>受害者</strong> 通过 <strong>代理服务器</strong> 访问 <strong>正义服务器</strong> 的 <strong>正义资源</strong>。</li>
<li>
<strong>代理服务器</strong> 检查该资源的url、host,发现本地有一份缓存(伪造的)。</li>
<li>
<strong>代理服务器</strong> 将 <strong>邪恶资源</strong> 返回给 <strong>受害者</strong>。</li>
<li>
<strong>受害者</strong> 卒。</li>
</ol>
<p>附:前面提到的精心构造的“HTTP请求报文”。</p>
<pre><code>Client → Server:
POST /path/of/attackers/choice HTTP/1.1 Host: host-of-attackers-choice.com Sec-WebSocket-Key: <connection-key>
Server → Client:
HTTP/1.1 200 OK
Sec-WebSocket-Accept: <connection-key></code></pre>
<h3>2、当前解决方案</h3>
<p>最初的提案是对数据进行加密处理。基于安全、效率的考虑,最终采用了折中的方案:对数据载荷进行掩码处理。</p>
<p>需要注意的是,这里只是限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。</p>
<p>但是对浏览器加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。</p>
<h2>十、写在后面</h2>
<p>WebSocket可写的东西还挺多,比如WebSocket扩展。客户端、服务端之间是如何协商、使用扩展的。WebSocket扩展可以给协议本身增加很多能力和想象空间,比如数据的压缩、加密,以及多路复用等。</p>
<p>篇幅所限,这里先不展开,感兴趣的同学可以留言交流。文章如有错漏,敬请指出。</p>
<h2>十一、相关链接</h2>
<p>RFC6455:websocket规范<br><a href="https://link.segmentfault.com/?enc=pgQ%2FrPudcZdV9nl4%2FOOGVg%3D%3D.E6o%2FVx3ItRLtsLSZyccgaQV2ZvDRp9e3bk0gRQldFjHMR3ZFIxwB183DOAb9RVcv" rel="nofollow">https://tools.ietf.org/html/r...</a></p>
<p>规范:数据帧掩码细节<br><a href="https://link.segmentfault.com/?enc=tpDbPyDHdCpFDUQuTsfzMQ%3D%3D.929T%2FgEInkLv0wSVXl12t7hUdtasIHlvRpf013hlioIEd9GGenl8Es7g4%2BUp%2BSxw" rel="nofollow">https://tools.ietf.org/html/r...</a></p>
<p>规范:数据帧格式<br><a href="https://link.segmentfault.com/?enc=NKF8Drh5jFHodnxdS%2BaZ2Q%3D%3D.W0OBb514XIO9CxGwkUKjTsIVX2CS16C7Ge9ya4%2FmBqK84ASLbQYvkT%2F%2BZsvXonAQ" rel="nofollow">https://tools.ietf.org/html/r...</a></p>
<p>server-example<br><a href="https://link.segmentfault.com/?enc=rnDBuZ6JgCO%2F7qIkh72B5Q%3D%3D.5aQKBBvhTKqcViQ6HnsN7Cvr3VZC20MXqnB5TQw0qxV9mlsn6WjKPTBlNXYG1DKL" rel="nofollow">https://github.com/websockets...</a></p>
<p>编写websocket服务器<br><a href="https://link.segmentfault.com/?enc=6D3ge%2Fwo92fax2C7%2Fn6kUA%3D%3D.duoDV0OoBJP0iNIo5qC5vl3FfjUJGY7zpk9DhZCSM7dLceNu%2B1iGrONRyGF%2BGNfe5YlnRbM6ZJKb%2FjxqxoCNiy85yY0X9hjisgbBbNahjMbPBdXbkYha%2FXr9%2BC0xCq73" rel="nofollow">https://developer.mozilla.org...</a></p>
<p>对网络基础设施的攻击(数据掩码操作所要预防的事情)<br><a href="https://link.segmentfault.com/?enc=pU1AsqGXj1PMVMMWOTe4mg%3D%3D.zSGpiVs50xPQ8NCEuMj4IbJwy84YlterntpK5QV1UDM9FcwNIAzd1zM54LnhqYA1SzL6Q03DgaFOWU%2BDiAH%2BoA%3D%3D" rel="nofollow">https://tools.ietf.org/html/r...</a></p>
<p>Talking to Yourself for Fun and Profit(含有攻击描述)<br><a href="https://link.segmentfault.com/?enc=lM9QBJVsD3LtGPl3UuK%2FLA%3D%3D.HooDhiittUd6BIJU1Jm7tXBJpS9V%2BBE9cSzbDgFbFsnpt6Md7EUwmAT1No%2Bwe5sd" rel="nofollow">http://w2spconf.com/2011/pape...</a></p>
<p>What is Sec-WebSocket-Key for?<br><a href="https://link.segmentfault.com/?enc=%2FVruHsvxoSZZXBBBQkMlJw%3D%3D.Kj9OEE%2F7C0V5pjKFX0GF4dSScKKn5gcTQbqlBmpmrT8oOkkyFxFhVd7fYfilec5mtgDTxHNqhuKmYEoapLzF5AqAn17NQt7gPmmnvsGcARs%3D" rel="nofollow">https://stackoverflow.com/que...</a></p>
<p>10.3. Attacks On Infrastructure (Masking)<br><a href="https://link.segmentfault.com/?enc=6%2F3RYOPSnagJ4Emha1X9Dw%3D%3D.Cq7YI9OFtBTd8A3VhnzAKTtXNRd2xM02xhQhab%2BueY71jDyhZE8a2%2BhjUC%2BtWUtAAhnqOonXtIRzlnK%2F40vh%2FA%3D%3D" rel="nofollow">https://tools.ietf.org/html/r...</a></p>
<p>Talking to Yourself for Fun and Profit<br><a href="https://link.segmentfault.com/?enc=bewaz7gs1kCLQS97XnshwA%3D%3D.Z2slLLOibw%2BJlZoDtcEfILldoPsBQ3%2FiQfzCFz4%2FhZsLFGeIkntO0wiTkLiFRg93" rel="nofollow">http://w2spconf.com/2011/pape...</a></p>
<p>Why are WebSockets masked?<br><a href="https://link.segmentfault.com/?enc=HBmrwD76rrYvpkbGn4IXCA%3D%3D.FlWP%2FDIL7ckZj7j2pwThcv1zjVyeUlSVOMqLXrX2fMVBeV8yAwcAKHg%2FG%2BMBg6BNJwMgVjVjb2TH5DjEX%2BoRBa7p6YmehyKO6gG8h8Q9SwQ%3D" rel="nofollow">https://stackoverflow.com/que...</a></p>
<p>How does websocket frame masking protect against cache poisoning?<br><a href="https://link.segmentfault.com/?enc=iPDleeoZraKnfF1WAE9BVQ%3D%3D.%2BU9vsQhNv4XkY382%2FGp1O2FEJSKPvYTW4%2BuyfZgsMji%2BJfJnAQT4jCElk9iofome8MHQJlJ%2B73Zzc5J%2BYLKoToipmT46YFZKy9CvjqJ0nfZF5lAR1u9wW5D%2F7T%2B0fcgpdHSXpWcpPn6%2FPNFC%2F1MyRgK%2B%2BFimyyM3iYiy67Y0NGk%3D" rel="nofollow">https://security.stackexchang...</a></p>
<p>What is the mask in a WebSocket frame?<br><a href="https://link.segmentfault.com/?enc=7Gsb8tUTmIxh%2BjOHFJvx3Q%3D%3D.TvKAKImidzwdU50vYjtjX5eCRCbTQyRalhlKSCFz6y1Fgm81jgIwHyXzQBuH%2BsYSkDSs%2BdMgbqcWr0zNabvxD%2BMjaYyIR3ujJ%2F3ixGZqw3P43z24xRwlEqBr6OSNp%2Fql" rel="nofollow">https://stackoverflow.com/que...</a></p>
Nodejs进阶:crypto模块中你需要掌握的安全基础知识
https://segmentfault.com/a/1190000012677632
2018-01-03T08:02:16+08:00
2018-01-03T08:02:16+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
24
<h2>一、 文章概述</h2>
<p>互联网时代,网络上的数据量每天都在以惊人的速度增长。同时,各类网络安全问题层出不穷。在信息安全重要性日益凸显的今天,作为一名开发者,需要加强对安全的认识,并通过技术手段增强服务的安全性。</p>
<p><code>crypto</code>模块是nodejs的核心模块之一,它提供了安全相关的功能,如摘要运算、加密、电子签名等。很多初学者对着长长的API列表,不知如何上手,因此它背后涉及了大量安全领域的知识。</p>
<p>本文重点讲解API背后的理论知识,主要包括如下内容:</p>
<ol>
<li>摘要(hash)、基于摘要的消息验证码(HMAC)</li>
<li>对称加密、非对称加密、电子签名</li>
<li>分组加密模式</li>
</ol>
<blockquote>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=Uxw7BwhFkeVA05NlwRV6dA%3D%3D.0wKP2WJno751Pg1u3sSotz5%2Fa%2Fonjbrp5tBnqP4UHmwZdz8lQFP9DRB7CyO4Z8jfr0NrdKnG2101DBxLJqvL3Q%3D%3D" rel="nofollow">github主页地址</a>。</blockquote>
<h2>二、摘要(hash)</h2>
<p>摘要(digest):将长度不固定的消息作为输入,通过运行hash函数,生成固定长度的输出,这段输出就叫做摘要。通常用来验证消息完整、未被篡改。</p>
<p>摘要运算是不可逆的。也就是说,输入固定的情况下,产生固定的输出。但知道输出的情况下,无法反推出输入。</p>
<p>伪代码如下。</p>
<blockquote>digest = Hash(message)</blockquote>
<p>常见的摘要算法 与 对应的输出位数如下:</p>
<ul>
<li>MD5:128位</li>
<li>SHA-1:160位</li>
<li>SHA256 :256位</li>
<li>SHA512:512位</li>
</ul>
<p>nodejs中的例子:</p>
<pre><code class="javascript">var crypto = require('crypto');
var md5 = crypto.createHash('md5');
var message = 'hello';
var digest = md5.update(message, 'utf8').digest('hex');
console.log(digest);
// 输出如下:注意这里是16进制
// 5d41402abc4b2a76b9719d911017c592</code></pre>
<blockquote>备注:在各类文章或文献中,摘要、hash、散列 这几个词经常会混用,导致不少初学者看了一脸懵逼,其实大部分时候指的都是一回事,记住上面对摘要的定义就好了。</blockquote>
<h2>三、MAC、HMAC</h2>
<p>MAC(Message Authentication Code):消息认证码,用以保证数据的完整性。运算结果取决于消息本身、秘钥。</p>
<p>MAC可以有多种不同的实现方式,比如HMAC。</p>
<p>HMAC(Hash-based Message Authentication Code):可以粗略地理解为带秘钥的hash函数。</p>
<p>nodejs例子如下:</p>
<pre><code class="javascript">const crypto = require('crypto');
// 参数一:摘要函数
// 参数二:秘钥
let hmac = crypto.createHmac('md5', '123456');
let ret = hmac.update('hello').digest('hex');
console.log(ret);
// 9c699d7af73a49247a239cb0dd2f8139</code></pre>
<h2>四、对称加密、非对称加密</h2>
<p><strong>加密/解密</strong>:给定明文,通过一定的算法,产生加密后的密文,这个过程叫加密。反过来就是解密。</p>
<blockquote>encryptedText = encrypt( plainText )<br>plainText = decrypt( encryptedText )</blockquote>
<p><strong>秘钥</strong>:为了进一步增强加/解密算法的安全性,在加/解密的过程中引入了秘钥。秘钥可以视为加/解密算法的参数,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开。</p>
<blockquote>encryptedText = encrypt(plainText, encryptKey)<br>plainText = decrypt(encryptedText, decryptKey)</blockquote>
<p>根据加密、解密所用的秘钥是否相同,可以将加密算法分为<strong>对称加密</strong>、<strong>非对称加密</strong>。</p>
<h3>1、对称加密</h3>
<p>加密、解密所用的秘钥是相同的,即<code>encryptKey === decryptKey</code>。</p>
<p>常见的对称加密算法:DES、3DES、AES、Blowfish、RC5、IDEA。</p>
<p>加、解密伪代码:</p>
<blockquote>encryptedText = encrypt(plainText, key); // 加密<br>plainText = decrypt(encryptedText, key); // 解密</blockquote>
<h3>2、非对称加密</h3>
<p>又称公开秘钥加密。加密、解密所用的秘钥是不同的,即<code>encryptKey !== decryptKey</code>。</p>
<p>加密秘钥公开,称为公钥。解密秘钥保密,称为秘钥。</p>
<p>常见的非对称加密算法:RSA、DSA、ElGamal。</p>
<p>加、解密伪代码:</p>
<blockquote>encryptedText = encrypt(plainText, publicKey); // 加密<br>plainText = decrypt(encryptedText, priviteKey); // 解密</blockquote>
<h3>3、对比与应用</h3>
<p>除了秘钥的差异,还有运算速度上的差异。通常来说:</p>
<ol>
<li>对称加密速度要快于非对称加密。</li>
<li>非对称加密通常用于加密短文本,对称加密通常用于加密长文本。</li>
</ol>
<p>两者可以结合起来使用,比如HTTPS协议,可以在握手阶段,通过RSA来交换生成对称秘钥。在之后的通讯阶段,可以使用对称加密算法对数据进行加密,秘钥则是握手阶段生成的。</p>
<blockquote>备注:对称秘钥交换不一定通过RSA,还可以通过类似DH来完成,这里不展开。</blockquote>
<h2>五、数字签名</h2>
<p>从<strong>签名</strong>大致可以猜到<strong>数字签名</strong>的用途。主要作用如下:</p>
<ol>
<li>确认信息来源于特定的主体。</li>
<li>确认信息完整、未被篡改。</li>
</ol>
<p>为了达到上述目的,需要有两个过程:</p>
<ol>
<li>发送方:生成签名。</li>
<li>接收方:验证签名。</li>
</ol>
<h3>1、发送方生成签名</h3>
<ol>
<li>计算原始信息的摘要。</li>
<li>通过私钥对摘要进行签名,得到电子签名。</li>
<li>将原始信息、电子签名,发送给接收方。</li>
</ol>
<p>附:签名伪代码</p>
<blockquote>digest = hash(message); // 计算摘要<br>digitalSignature = sign(digest, priviteKey); // 计算数字签名</blockquote>
<h3>2、接收方验证签名</h3>
<ol>
<li>通过公钥解开电子签名,得到摘要D1。(如果解不开,信息来源主体校验失败)</li>
<li>计算原始信息的摘要D2。</li>
<li>对比D1、D2,如果D1等于D2,说明原始信息完整、未被篡改。</li>
</ol>
<p>附:签名验证伪代码</p>
<blockquote>digest1 = verify(digitalSignature, publicKey); // 获取摘要<br>digest2 = hash(message); // 计算原始信息的摘要<br>digest1 === digest2 // 验证是否相等</blockquote>
<h3>3、对比非对称加密</h3>
<p>由于RSA算法的特殊性,加密/解密、签名/验证 看上去特别像,很多同学都很容易混淆。先记住下面结论,后面有时间再详细介绍。</p>
<ol>
<li>加密/解密:公钥加密,私钥解密。</li>
<li>签名/验证:私钥签名,公钥验证。</li>
</ol>
<h2>六、分组加密模式、填充、初始化向量</h2>
<p>常见的对称加密算法,如AES、DES都采用了分组加密模式。这其中,有三个关键的概念需要掌握:模式、填充、初始化向量。</p>
<p>搞清楚这三点,才会知道crypto模块对称加密API的参数代表什么含义,出了错知道如何去排查。</p>
<h3>1、分组加密模式</h3>
<p>所谓的分组加密,就是将(较长的)明文拆分成固定长度的块,然后对拆分的块按照特定的模式进行加密。</p>
<p>常见的分组加密模式有:ECB(不安全)、CBC(最常用)、CFB、OFB、CTR等。</p>
<p>以最简单的ECB为例,先将消息拆分成等分的模块,然后利用秘钥进行加密。</p>
<p><img src="/img/remote/1460000012677635?w=593&h=234" alt="" title=""></p>
<p>图片来源:<a href="https://link.segmentfault.com/?enc=n0%2FTaHo5MV7K5b%2Fb7lQoIA%3D%3D.CyhoaLGamYEghhJLKnmIlzpT5kFM2MmdeZjk3AmOC5ooKD%2Fk2ksZXjysy4CYYCV%2FbYcTYz8RYxKiXqDG64wjtj4cJU2ls0ZLaOAvSGPZx210aZIHiUCPEkZsT05Kipxz" rel="nofollow">这里</a>,更多关于分组加密模式的介绍可以参考 <a href="https://link.segmentfault.com/?enc=BrAdAyHAYr4vFCJhClm8Xg%3D%3D.c5Q6T80Hx%2FbpYUjrlN1wkFrsrTN7G0oH6Hm9qRnMSZ15g7qMaGp85ypDU7Irnhs40JP1CwCwqCTVrqtigNCVWMwsapRB5Foqd6ZgFy%2BSNYU%3D" rel="nofollow">wiki</a>。</p>
<blockquote>后面假设每个块的长度为128位</blockquote>
<h3>2、初始化向量:IV</h3>
<p>为了增强算法的安全性,部分分组加密模式(CFB、OFB、CTR)中引入了初始化向量(IV),使得加密的结果随机化。也就是说,对于同一段明文,IV不同,加密的结果不同。</p>
<p>以CBC为例,每一个数据块,都与前一个加密块进行亦或运算后,再进行加密。对于第一个数据块,则是与IV进行亦或。</p>
<p>IV的大小跟数据块的大小有关(128位),跟秘钥的长度无关。</p>
<p>如图所示,图片来源 <a href="https://link.segmentfault.com/?enc=otdNRFQ6fRgf9brBlXQ%2F%2Fg%3D%3D.Ic%2Fdwna8OzjRNvlxF2YwWTe661WERCuXeiDBkcU4zZCxE3WgTg64Da7qoqU0zqYOv1oxcdHDCDyteiiSHKKsO6xsyP0oSflr4nia2LMyjLm6a4ML1PcUfwFxqezITuzZ" rel="nofollow">这里</a></p>
<p><img src="/img/remote/1460000012677635?w=593&h=234" alt="" title=""></p>
<h3>3、填充:padding</h3>
<p>分组加密模式需要对长度固定的块进行加密。分组拆分完后,最后一个数据块长度可能小于128位,此时需要进行填充以满足长度要求。</p>
<p>填充方式有多重。常见的填充方式有<a href="https://link.segmentfault.com/?enc=C6wa6u1NVLFBTaoPvg5I1A%3D%3D.Z9zjJGlwUpPj1w2c%2BEAnL7wP89lr1liw2vDRyXDgURvH6hn0y5p9x2c%2B8D%2FBY0Cf" rel="nofollow">PKCS7</a>。</p>
<p>假设分组长度为k字节,最后一个分组长度为k-last,可以看到:</p>
<ol>
<li>不管明文长度是多少,加密之前都会会对明文进行填充 (不然解密函数无法区分最后一个分组是否被填充了,因为存在最后一个分组长度刚好等于k的情况)</li>
<li>如果最后一个分组长度等于k-last === k,那么填充内容为一个完整的分组 k k k ... k (k个字节)</li>
<li>如果最后一个分组长度小于k-last < k,那么填充内容为 k-last mod k</li>
</ol>
<pre><code> 01 -- if lth mod k = k-1
02 02 -- if lth mod k = k-2
.
.
.
k k ... k k -- if lth mod k = 0</code></pre>
<h3>概括来说</h3>
<ol>
<li>分组加密:先将明文切分成固定长度的块(128位),再进行加密。</li>
<li>分组加密的几种模式:ECB(不安全)、CBC(最常用)、CFB、OFB、CTR。</li>
<li>填充(padding):部分加密模式,当最后一个块的长度小于128位时,需要通过特定的方式进行填充。(ECB、CBC需要填充,CFB、OFB、CTR不需要填充)</li>
<li>初始化向量(IV):部分加密模式(CFB、OFB、CTR)会将 明文块 与 前一个密文块进行亦或操作。对于第一个明文块,不存在前一个密文块,因此需要提供初始化向量IV(把IV当做第一个明文块 之前的 密文块)。此外,IV也可以让加密结果随机化。</li>
</ol>
<h2>七、写在后面</h2>
<p>crypto模块涉及的安全知识较多,篇幅所限,这里没办法一一展开。为了讲解方便,部分内容可能不够严谨,如有错漏敬请指出。</p>
<p>有疑问或感兴趣的同学欢迎留言交流,也可留意我的github关注最新内容更新<a href="https://link.segmentfault.com/?enc=6ElcIuScbtcLYogYSEgc8g%3D%3D.8XWTKwj%2F8d7Yq2SlTFQlGl4Lj748YYFf3b%2B0maEzHx3EwbdmuvRA8C7pt%2Ba5uZALMBihhhGVwQ%2FHBfAx%2B234mA%3D%3D" rel="nofollow">《nodejs-learning-guide》</a>。</p>
<h2>八、相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=1JO19Cp4onsL9lvklgJFHA%3D%3D.g8ZZA%2BF1Iwb4fozKeghiuw%2FubeUvHQAU1Qs6JhmnSEFA9foFjXSzM%2BeCPkI4b31k6KCsqcfF%2FNpWjhn3bVhTmA%3D%3D" rel="nofollow">Nodejs学习笔记</a></p>
<p><a href="https://link.segmentfault.com/?enc=OFiwwf%2BGr8Q%2FlNIQiw4ufw%3D%3D.zjt402iPvnFz9HhVb6gHBl5MYzlX%2BOLHk8Q4ns22SsK8HiBIyxk3W1wFR4tRpd7zBcvDe8GmENeZPYwDVs81zA%3D%3D" rel="nofollow">Cryptographic hash function</a></p>
<p><a href="https://link.segmentfault.com/?enc=CjSkuMC8EIomadQAerA%2FPA%3D%3D.ooJneadVZeZf6ahwCh5sRNQi4p%2BBOswshE2rHMX3ejvMGRdE0CU8Zu28D7T6wYwUhVEcyML0hBKv6YFFn3F7dIcsDEMIlYBiorI7udy1Wtg%3D" rel="nofollow">Hash-based message authentication code</a></p>
<p><a href="https://link.segmentfault.com/?enc=cPLPmyD9qbiFBzCMInNnGQ%3D%3D.RANDZx5r2WiPRiMKrC1XBbUeLHOBiU4xxXwApv8wfqnDhJjSgWCMjjWNDpFn0A7rYieXCXV%2B0BglJTgkL2C%2BEJChTkBqNE4j7SDqQMiG9oQ%3D" rel="nofollow">HMAC vs MAC functions</a></p>
<p><a href="https://link.segmentfault.com/?enc=HvjfHKER4APsp8kRkGc0zA%3D%3D.0NgewXGAnxzBa%2B%2Fe45EmMlhOsTrdRRwYPYZVAFSPIjHXaqF%2BBJQ6e6ZR9HGakavGKRTVEOBUW2w0pU69OaKBXVP079GIj1rS%2FscHYaIAOtWBTWlBVoeZ%2F2d65E2S67HQ" rel="nofollow">What is the difference between MAC and HMAC?</a></p>
<p><a href="https://link.segmentfault.com/?enc=mccnK2IhoIA37loTlHwbxQ%3D%3D.%2F845kj5EchG2tk%2B9t%2FJkXAFcE5zCWzbXKCVxe4DB%2BhLx4g23vvCHFQqcGtG%2FjdLEfFCAzlh%2Bgq0dh6YBP1feMg%3D%3D" rel="nofollow">Block cipher mode of operation</a></p>
<p><a href="https://link.segmentfault.com/?enc=1IdcbpeJfeP27huMDtsU%2Bw%3D%3D.0DKCBn2JPcro%2BkZFt4UqV%2Bt659TZgD9ejsBrDhfZlZvjTIQ3bvxqYTIkAbAvq0sK4ieaMKzWR1yB1ce5Sh509g%3D%3D" rel="nofollow">RSA的公钥和私钥到底哪个才是用来加密和哪个用来解密? - 刘巍然-学酥的回答 - 知乎</a></p>
Linux基础:用tcpdump抓包
https://segmentfault.com/a/1190000012593192
2017-12-26T18:12:11+08:00
2017-12-26T18:12:11+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
11
<h2>简介</h2>
<p>网络数据包截获分析工具。支持针对网络层、协议、主机、网络或端口的过滤。并提供and、or、not等逻辑语句帮助去除无用的信息。</p>
<blockquote>tcpdump - dump traffic on a network</blockquote>
<h2>例子</h2>
<h3>不指定任何参数</h3>
<p>监听第一块网卡上经过的数据包。主机上可能有不止一块网卡,所以经常需要指定网卡。</p>
<pre><code class="powershell">tcpdump</code></pre>
<h3>监听特定网卡</h3>
<pre><code class="powershell">tcpdump -i en0</code></pre>
<h3>监听特定主机</h3>
<p>例子:监听本机跟主机<code>182.254.38.55</code>之间往来的通信包。</p>
<p>备注:出、入的包都会被监听。</p>
<pre><code class="bash">tcpdump host 182.254.38.55</code></pre>
<h3>特定来源、目标地址的通信</h3>
<p>特定来源</p>
<pre><code class="bash">tcpdump src host hostname</code></pre>
<p>特定目标地址</p>
<pre><code class="bash">tcpdump dst host hostname</code></pre>
<p>如果不指定<code>src</code>跟<code>dst</code>,那么来源 或者目标 是hostname的通信都会被监听</p>
<pre><code class="bash">tcpdump host hostname</code></pre>
<h3>特定端口</h3>
<pre><code class="bash">tcpdump port 3000</code></pre>
<h3>监听TCP/UDP</h3>
<p>服务器上不同服务分别用了TCP、UDP作为传输层,假如只想监听TCP的数据包</p>
<pre><code class="bash">tcpdump tcp</code></pre>
<h3>来源主机+端口+TCP</h3>
<p>监听来自主机<code>123.207.116.169</code>在端口<code>22</code>上的TCP数据包</p>
<pre><code class="bash">tcpdump tcp port 22 and src host 123.207.116.169</code></pre>
<h3>监听特定主机之间的通信</h3>
<pre><code class="powershell">tcpdump ip host 210.27.48.1 and 210.27.48.2</code></pre>
<p><code>210.27.48.1</code>除了和<code>210.27.48.2</code>之外的主机之间的通信</p>
<pre><code>tcpdump ip host 210.27.48.1 and ! 210.27.48.2</code></pre>
<h3>稍微详细点的例子</h3>
<pre><code class="powershell">tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap</code></pre>
<blockquote>(1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型<br>(2)-i eth1 : 只抓经过接口eth1的包<br>(3)-t : 不显示时间戳<br>(4)-s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包<br>(5)-c 100 : 只抓取100个数据包<br>(6)dst port ! 22 : 不抓取目标端口是22的数据包<br>(7)src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24<br>(8)-w ./target.cap : 保存成cap文件,方便用ethereal(即wireshark)分析</blockquote>
<h3>抓http包</h3>
<p>TODO</p>
<h3>限制抓包的数量</h3>
<p>如下,抓到1000个包后,自动退出</p>
<pre><code class="bash">tcpdump -c 1000</code></pre>
<h3>保存到本地</h3>
<p>备注:tcpdump默认会将输出写到缓冲区,只有缓冲区内容达到一定的大小,或者tcpdump退出时,才会将输出写到本地磁盘</p>
<pre><code class="bash">tcpdump -n -vvv -c 1000 -w /tmp/tcpdump_save.cap</code></pre>
<p>也可以加上<code>-U</code>强制立即写到本地磁盘(一般不建议,性能相对较差)</p>
<h2>实战例子</h2>
<p>先看下面一个比较常见的部署方式,在服务器上部署了nodejs server,监听3000端口。nginx反向代理监听80端口,并将请求转发给nodejs server(<code>127.0.0.1:3000</code>)。</p>
<blockquote>浏览器 -> nginx反向代理 -> nodejs server</blockquote>
<p>问题:假设用户(183.14.132.117)访问浏览器,发现请求没有返回,该怎么排查呢?</p>
<p>步骤一:查看请求是否到达nodejs server -> 可通过日志查看。</p>
<p>步骤二:查看nginx是否将请求转发给nodejs server。</p>
<pre><code class="bash">tcpdump port 8383 </code></pre>
<p>这时你会发现没有任何输出,即使nodejs server已经收到了请求。因为nginx转发到的地址是127.0.0.1,用的不是默认的interface,此时需要显示指定interface</p>
<pre><code class="bash">tcpdump port 8383 -i lo</code></pre>
<p>备注:配置nginx,让nginx带上请求侧的host,不然nodejs server无法获取 src host,也就是说,下面的监听是无效的,因为此时对于nodejs server来说,src host 都是 127.0.0.1</p>
<pre><code class="bash">tcpdump port 8383 -i lo and src host 183.14.132.117</code></pre>
<p>步骤三:查看请求是否达到服务器</p>
<pre><code class="bash">tcpdump -n tcp port 8383 -i lo and src host 183.14.132.117</code></pre>
<h2>相关链接</h2>
<p>tcpdump 很详细的<br><a href="https://link.segmentfault.com/?enc=WoHWSJ3ZT4pjUSmxnPrS7g%3D%3D.13NFqw0xYeGwaEJglS%2BKpJpR7k6%2BpvhUs%2BmsnDJdLBbx9EToFpqwGpFAP%2BYsxTHKdYtr4q8zxjWxusg8yQu7JQ%3D%3D" rel="nofollow">http://blog.chinaunix.net/uid...</a></p>
<p><a href="https://link.segmentfault.com/?enc=EytKtAWSO4tkEmUdj8xSLg%3D%3D.wKfdw3zkotpZQ7iHbJdBDqr16QBBZQU2ykxew%2BneQ%2BF3zo1svtF0yQoRAC36qHrlGJ4%2B9ou19OA5%2B3%2F7zI1W9ALpo%2Fy5orU7NH5CdGW2zTk%3D" rel="nofollow">http://www.cnblogs.com/ggjuch...</a><br>Linux tcpdump命令详解</p>
<p>Tcpdump usage examples(推荐)<br><a href="https://link.segmentfault.com/?enc=dLfnh7jHYxivwELgQ%2FOuHA%3D%3D.oJk38MDj%2FH%2F4LeC9hbVHaZ5VjmkvuEk0w6w2Neg85brYi3ruj%2BXL8C92tfzbJUWzLL%2FULUzp5KX8%2F9uHRaGfyQ%3D%3D" rel="nofollow">http://www.rationallyparanoid...</a></p>
<p>使用TCPDUMP抓取HTTP状态头信息<br><a href="https://link.segmentfault.com/?enc=YXADjoS6T1d282i8cx%2FNkA%3D%3D.0eCeyHeRrTILO9umg7VxLsIdHCbXNHPYmYAeLatHngGPdGqSXbKxtLLKQvgeiSbzNrrPdOWsYY27DfhnAc3SYw%3D%3D" rel="nofollow">http://blog.sina.com.cn/s/blo...</a></p>
译:Facebook将修改React、Jest、Flow与Immutable.js授权许可(重磅)
https://segmentfault.com/a/1190000011321860
2017-09-23T10:12:40+08:00
2017-09-23T10:12:40+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
11
<p>下周,我们将用MIT协议重新授权我们的开源项目React、Jest、Flow和Immutable.js。之所以我们要重新授权这些项目,是因为React是很多网络开源软件生态系统的基础,我们不希望因为非技术的原因导致开源生态的倒退。</p>
<blockquote><p>Next week, we are going to relicense our open source projects React, Jest, Flow, and Immutable.js under the MIT license. We're relicensing these projects because React is the foundation of a broad ecosystem of open source software for the web, and we don't want to hold back forward progress for nontechnical reasons.</p></blockquote>
<p>经过几周对我们社区的失望和不确定性后,我们做出了这个决定。虽然我们仍然相信我们的 BSD+Patents<br> 授权许可证给我们的项目的用户带了一些好处,但我们知道我们并没有说服社区。</p>
<blockquote><p>This decision comes after several weeks of disappointment and uncertainty for our community. Although we still believe our BSD + Patents license provides some benefits to users of our projects, we acknowledge that we failed to decisively convince this community.</p></blockquote>
<p>在经历了对我们授权许可的不确定性后,我们知道很多团队经历了选择React替代方案的过程,我们对于这部分团队的流失感到遗憾。我们不指望能够通过修改授权协议挽回这部分团队,但我们希望打开希望的大门。社区友好的合作和竞争一直推动着我们向前,我们想要全面的参与其中。</p>
<blockquote><p>In the wake of uncertainty about our license, we know that many teams went through the process of selecting an alternative library to React. We're sorry for the churn. We don't expect to win these teams back by making this change, but we do want to leave the door open. Friendly cooperation and competition in this space pushes us all forward, and we want to participate fully.</p></blockquote>
<p>这一转变自然引起了对Facebook其他开源项目的疑问。我们许多受欢迎的开源项目目前仍会保持BSD + Patents 授权许可。我们也在重新评估这些项目的授权许可,但每个项目都是不同的,并且替代的授权选项取决于多种因素。</p>
<blockquote><p>This shift naturally raises questions about the rest of Facebook's open source projects. Many of our popular projects will keep the BSD + Patents license for now. We're evaluating those projects' licenses too, but each project is different and alternative licensing options will depend on a variety of factors.</p></blockquote>
<p>我们将在下周React 16的发布中更新授权许可证。我们已经在React 16上花了超过一年的时间来将它的内核完全重写,以此解锁更强大的功能,使用React构建用户界面的开发者将大大获益。我们稍后很快会跟大家分享我们是如何重写React的,同时希望我们的工作能够激励开发者们,不管他们是否使用React。我们希望之前的授权许可的争议能成为过去,从新回到我们真正关心的事情:创造伟大的产品。</p>
<blockquote><p>We'll include the license updates with React 16's release next week. We've been working on React 16 for over a year, and we've completely rewritten its internals in order to unlock powerful features that will benefit everyone building user interfaces at scale. We'll share more soon about how we rewrote React, and we hope that our work will inspire developers everywhere, whether they use React or not. We're looking forward to putting this license discussion behind us and getting back to what we care about most: shipping great products.</p></blockquote>
<h2>相关链接</h2>
<p>原文:<a href="https://link.segmentfault.com/?enc=6sNp3dPHELdwj2wjTkTdDg%3D%3D.%2F1JCDduNftGVraOxcnv%2BoY12WcPbSAMHFBrWgeEwOdzyujFvyQOvd2cusqeZk6ju" rel="nofollow">https://code.facebook.com/pos...</a></p>
Nodejs进阶:服务端字符编解码&乱码处理
https://segmentfault.com/a/1190000010994478
2017-09-04T08:21:08+08:00
2017-09-04T08:21:08+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
5
<h2>写在前面</h2>
<p>在web服务端开发中,字符的编解码几乎每天都要打交道。编解码一旦处理不当,就会出现令人头疼的乱码问题。</p>
<p>不少从事node服务端开发的同学,由于对字符编码码相关知识了解不足,遇到问题时,经常会一筹莫展,花大量的时间在排查、解决问题。</p>
<p>文本先对字符编解码的基础知识进行简单介绍,然后举例说明如何在node中进行编解码,最后是服务端的代码案例。本文相关代码示例可在<a href="https://link.segmentfault.com/?enc=EKATK42Hw%2B3%2BFTidu7HNrg%3D%3D.3HxSHZBvu%2BNpc7FdckVi3X2XtDfihbH6YIDZkP59GsHcZFxEFeC3ZCqe26VQEfQVIvSk20OwX%2BGdPoFvQgwVjA%3D%3D" rel="nofollow">这里</a>找到。</p>
<h2>关于字符编解码</h2>
<p>在网络通信的过程中,传输的都是二进制的比特位,不管发送的内容是文本还是图片,采用的语言是中文还是英文。</p>
<p>举个例子,客户端向服务端发送"你好"。</p>
<blockquote><p>客户端 --- 你好 ---> 服务端</p></blockquote>
<p>这中间包含了两个关键步骤,分别对应的是编码、解码。</p>
<ol>
<li>客户端:将"你好"这个字符串,编码成计算机网络需要的二进制比特位。</li>
<li>服务端:将接收到的二进制比特位,解码成"你好"这个字符串。</li>
</ol>
<p>总结一下:</p>
<ol>
<li>编码:将需要传送的数据,转成对应的二进制比特位。</li>
<li>解码:将二进制比特位,转成原始的数据。</li>
</ol>
<p>上面有些重要的技术细节没有提到,答案在下一小节。</p>
<ul>
<li>客户端怎么知道"你好"这个字符对应的比特位是多少?</li>
<li>服务端收到二进制比特位之后,怎么知道对应的字符串是什么?</li>
</ul>
<h2>关于字符集和字符编码</h2>
<p>上面提到字符、二进制的转换问题。既然两者可以互相转换,也就是说存在明确的转换规则,可以实现<strong>字符<->二进制</strong>的相互转换。</p>
<p>这里提到的转换规则,其实就是我们经常听到的字符集&字符编码。</p>
<p><strong>字符集</strong>是一系列字符(文字、标点符号等)的集合。字符集有很多,常见的有ASCII、Unicode、GBK等。不同字符集主要的区别在于包含字符个数的不同。</p>
<p>了解了字符集的概念后,接下来介绍下字符编码。</p>
<p>字符集告诉我们支持哪些字符,但具体字符怎么编码,是由<strong>字符编码</strong>决定的。比如Unicode字符集,支持的字符编码有UTF8(常用)、UTF16、UTF32。</p>
<p>概括一下:</p>
<ul>
<li>字符集:字符的集合,不同字符集包含的字符数不同。</li>
<li>字符编码:字符集中字符的实际编码方式。</li>
<li>一个字符集可能有多种字符编码方式。</li>
</ul>
<p>可以把字符编码看成一个映射表,客户端、服务端就是根据这个映射表,来实现字符跟二进制的编解码转换。</p>
<p>举个例子,"你"这个字符,在UTF8编码中,占据三个字节<code>0xe4 0xbd 0xa0</code>,而在GBK编码中,占据两个字节<code>0xc4 0xe3</code>。</p>
<h2>字符编解码例子</h2>
<p>上面已经提到了字符编解码所需的基础知识。下面我们看一个简单的例子,这里借助了<code>icon-lite</code>这个库来帮助我们实现编解码的操作。</p>
<p>可以看到,在字符编码时,我们采用了<code>gbk</code>。在解码时,如果同样采用<code>gbk</code>,可以得到原始的字符。而当我们解码时采用<code>utf8</code>时,则出现了乱码。</p>
<pre><code class="javascript">var iconv = require('iconv-lite');
var oriText = '你';
var encodedBuff = iconv.encode(oriText, 'gbk');
console.log(encodedBuff);
// <Buffer c4 e3>
var decodedText = iconv.decode(encodedBuff, 'gbk');
console.log(decodedText);
// 你
var wrongText = iconv.decode(encodedBuff, 'utf8');
console.log(wrongText);
// ��</code></pre>
<h2>实际例子:服务端编解码</h2>
<p>通常我们需要处理编解码的场景有文件读写、网络请求处理。这里距网络请求的例子,介绍如何在服务端进行编解码。</p>
<p>假设我们运行着如下http服务,监听来自客户端的请求。客户端传输数据时采用了<code>gbk</code>编码,而服务端默认采用的是<code>utf8</code>编码。</p>
<p>如果此时采用默认的<code>utf8</code>对请求进行解码,就会出现乱码,因此需要特殊处理。</p>
<p>服务端代码如下(为简化代码,这里跳过了请求方法、请求编码的判断)</p>
<pre><code class="javascript">var http = require('http');
var iconv = require('iconv-lite');
// 假设客户端采用post方法,编码为gbk
var server = http.createServer(function (req, res) {
var chunks = [];
req.on('data', function (chunk) {
chunks.push(chunk)
});
req.on('end', function () {
chunks = Buffer.concat(chunks);
// 对二进制进行解码
var body = iconv.decode(chunks, 'gbk');
console.log(body);
res.end('HELLO FROM SERVER');
});
});
server.listen(3000);</code></pre>
<p>对应的客户端代码如下:</p>
<pre><code class="javascript">var http = require('http');
var iconv = require('iconv-lite');
var charset = 'gbk';
// 对字符"你"进行编码
var reqBuff = iconv.encode('你', charset);
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/',
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Encoding': 'identity',
'Charset': charset // 设置请求字符集编码
}
};
var client = http.request(options, function(res) {
res.pipe(process.stdout);
});
client.end(reqBuff);</code></pre>
<h2>相关链接</h2>
<p>Nodejs学习笔记<br><a href="https://link.segmentfault.com/?enc=voZ1aPe40OdTyf1qsk5%2B7Q%3D%3D.Rm%2B08NVJ%2BqNxh1SqroQnjYuSV9NOtjk%2BpRQzPh6%2B7Qmp%2FrZxcxx2gNE9yt5h84MI3siGMoinovUDLGrk8iewVw%3D%3D" rel="nofollow">https://github.com/chyingp/no...</a></p>
<p>iconv-lite<br><a href="https://link.segmentfault.com/?enc=enHQdoD2RYfIWYLS8MQSbQ%3D%3D.Xfan73F%2Fwrf4wqxBIdGdHHronDDov2GDdbXVl8cta2Cmpt2SLUKcRSijJ%2Fidjf3s" rel="nofollow">https://github.com/ashtuchkin...</a></p>
面试的妹纸问我:web缓存设置不是后台的事情吗?
https://segmentfault.com/a/1190000010937957
2017-08-31T08:11:16+08:00
2017-08-31T08:11:16+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
43
<h2>背景介绍</h2>
<p>团队最近在招前端开发,早上收到一封简历,是个妹纸,从技能点来看还算符合要求,于是约了下午3点过来面试。</p>
<p>整个面试过程持续了大约40分钟,问的题目也比较常规,其中一道题就是“常见的性能优化手段”。期间妹纸提到她看过《图解HTTP》,我就顺带问了下,“是否了解HTTP协议中常见的跟缓存相关的header”。</p>
<p>如果妹纸能够答上一两个,比如<code>max-age</code>之类的,基本这道题就放行了。不过妹纸的回复让我觉得有些意外:(字眼记不清了,大致这么个意思)</p>
<blockquote><p>“web缓存设置不是后台的事情吗?”</p></blockquote>
<h2>为什么觉得web缓存设置是后台的事</h2>
<p>妹纸的逻辑其实不难理解,因为web所需要的资源,如网页、JavaScript、CSS、图片等,都是放在服务器上的。而服务器大部分时候,要嘛是后台同学,要嘛是运维同学在管理,很多前端同学并没有多少机会接触到服务器,更别说上去对静态资源做缓存设置了。</p>
<p>相信不少前端新人也是这么认为的。已经记不清有多少次,在技术群里面,每当有人讨论HTTP协议等似乎跟前端“关系不是特别大”的内容的时候,总会有那么一两个群友怒气值满满地来上一句:</p>
<blockquote><p>别老装逼成吗,前端不就写写网页切切图片,这些东西有毛用,又不能涨工资。</p></blockquote>
<p>对于这种言论,我一般是选择潜水逃离,绝不恋战。</p>
<h2>柳暗花明:凡事皆有因</h2>
<p>对于这个“web缓存设置不是后台的事情吗”这个问题,我内心有自己的答案。</p>
<p>直接抛出结论容易,但并不意味着别人容易接受。于是我采取迂回战术,问妹纸:“你们的线上项目有没有设置缓存?” </p>
<p>妹纸的回答再次让我感到意外:</p>
<blockquote><p>“没有,设置了缓存后,有用户访问到了错误的版本,所以没有设置缓存。”</p></blockquote>
<p>从这回答可以大致判断,妹纸除了不了解缓存设置外,对于静态资源的版本控制应该也不熟悉。</p>
<p>我突然脑海中灵光一闪 —— 传教的机会到了。于是现场瞬间从面试模式切换到上课模式。</p>
<h2>缓存设置带来的问题及解决方案</h2>
<p>问题一:没有设置缓存会带来什么问题?</p>
<p>答:首先当然是性能问题。用户每次访问都需要重新访问服务器,用户访问速度、服务器负载堪忧。<br>其次是成本问题。用了云服务的同学应该了解,带宽跟流量都是用毛爷爷换来的。</p>
<p>问题二:是缓存导致了版本访问错误的问题吗?</p>
<p>答:从出错场景上来看,的确是缓存导致的。但缓存其实无辜的,罪魁祸首是不恰当的静态资源版本控制。</p>
<p>问题三:有什么解决方案?</p>
<p>答:首先对静态资源进行版本控制(比如给静态资源的文件名加上hash值),其次对网页设置合适时长的缓存时间(长短取决于实际场景)。这样就兼顾了版本升级和性能。</p>
<h2>终极发问:web缓存设置真的只跟后台有关吗</h2>
<p>看到这里,相信各位已经得出了自己的答案。</p>
<blockquote><p>从前端开发的角度来看,对静态资源进行缓存设置,可以减少用户的访问耗时,提升用户的访问体验,在绝大部分时候都是基础而重要的优化。</p></blockquote>
<p>那么,静态资源的缓存应该设置多长呢?永不缓存?10分钟?1小时?1年?。。。</p>
<p>其实并没有标准答案,很多时候,都取决于技术架构、业务场景。</p>
<p>比如:像上面妹纸提到的,她们没有做静态资源的版本控制,那么永不缓存也是可以接受的。毕竟很多时候,相对于性能不佳,用户对于服务出错的容忍度是更低的。</p>
<p>再比如:如果已经实现类似文件名加hash这样的版本控制方案,那么大可将JavaScript、CSS、图片等静态资源的缓存时间设置长一些,比如1个月;而网页的缓存时间可以短一些,比如每隔10分钟校验一次。</p>
<blockquote><p>如果前端同学缺少对web缓存设置的了解,认为那是后台的事,那么很多时候就很难提出合理的技术方案。</p></blockquote>
<p>退一万步讲,web缓存设置这件事假设就是由后台包办了(大公司里很有可能就是这样),那么缓存该怎么设置?其实最终还是由前端的场景说了算。缓存相关的header就那么几个,业务/技术场景可能成百上千,这个时候,前端可以看作甲方,后台可以看作是乙方,需求只有前端知道。</p>
<h2>写在后面</h2>
<p>前面啰啰嗦嗦讲了一大通,实际上这个问题只占了整个面试环节的很小一部分,当时也没有在这个问题上过于纠缠。只是觉得不少同学在类似的问题上存在一定的误区,觉得不吐不快。</p>
<p>PS:云汉金融科技招聘前端开发人员,坐标深圳,年薪15w - 30w,有意者可私信或投递简历。岗位介绍可点击 <a href="https://link.segmentfault.com/?enc=h7nliwsAuoZBulXsq1oXyQ%3D%3D.2o6p4Qq1s6%2BuT9mKoQaxMYGK5xG%2FTg5%2FgfSplvipzRcrEZHFgr46rNNCYLq9NsSW" rel="nofollow">传送门</a>。</p>
Nodejs进阶:使用DiffieHellman密钥交换算法
https://segmentfault.com/a/1190000010917737
2017-08-30T08:16:04+08:00
2017-08-30T08:16:04+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<h2>简介</h2>
<p>Diffie-Hellman(简称DH)是密钥交换算法之一,它的作用是保证通信双方在非安全的信道中安全地交换密钥。目前DH最重要的应用场景之一,就是在HTTPS的握手阶段,客户端、服务端利用DH算法交换对称密钥。</p>
<p>下面会先简单介绍DH的数理基础,然后举例说明如何在nodejs中使用DH相关的API。</p>
<h2>数论基础</h2>
<p>要理解DH算法,需要掌握一定的数论基础。感兴趣的可以进一步研究推导过程,或者直接记住下面结论,然后进入下一节。</p>
<ol>
<li><p>假设 Y = a^X mod p,已知X的情况下,很容易算出Y;已知道Y的情况下,很难算出X;</p></li>
<li><p>(a^Xa mod p)^Xb mod p = a^(Xa * Xb) mod p</p></li>
</ol>
<h2>握手步骤说明</h2>
<p>假设客户端、服务端挑选两个素数a、p(都公开),然后</p>
<ul>
<li><p>客户端:选择自然数Xa,Ya = a^Xa mod p,并将Ya发送给服务端;</p></li>
<li><p>服务端:选择自然数Xb,Yb = a^Xb mod p,并将Yb发送给客户端;</p></li>
<li><p>客户端:计算 Ka = Yb^Xa mod p</p></li>
<li><p>服务端:计算 Kb = Ya^Xb mod p</p></li>
</ul>
<blockquote>
<p>Ka = Yb^Xa mod p</p>
<pre><code>= (a^Xb mod p)^Xa mod p
= a^(Xb * Xa) mod p
= (a^Xa mod p)^Xb mod p
= Ya^Xb mod p
= Kb
</code></pre>
</blockquote>
<p>可以看到,尽管客户端、服务端彼此不知道对方的Xa、Xb,但算出了相等的secret。</p>
<h2>Nodejs代码示例</h2>
<p>结合前面小结的介绍来看下面代码,其中,要点之一就是client、server采用相同的素数a、p。</p>
<pre><code class="javascript">var crypto = require('crypto');
var primeLength = 1024; // 素数p的长度
var generator = 5; // 素数a
// 创建客户端的DH实例
var client = crypto.createDiffieHellman(primeLength, generator);
// 产生公、私钥对,Ya = a^Xa mod p
var clientKey = client.generateKeys();
// 创建服务端的DH实例,采用跟客户端相同的素数a、p
var server = crypto.createDiffieHellman(client.getPrime(), client.getGenerator());
// 产生公、私钥对,Yb = a^Xb mod p
var serverKey = server.generateKeys();
// 计算 Ka = Yb^Xa mod p
var clientSecret = client.computeSecret(server.getPublicKey());
// 计算 Kb = Ya^Xb mod p
var serverSecret = server.computeSecret(client.getPublicKey());
// 由于素数p是动态生成的,所以每次打印都不一样
// 但是 clientSecret === serverSecret
console.log(clientSecret.toString('hex'));
console.log(serverSecret.toString('hex'));</code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=F66%2FAwRJSnsDe4xjRuuw5w%3D%3D.lcVZKaz7IRiWrGsmGAcA3glaN%2FDc8LOL1jGkP7igQ11gMUIPv09cwjhPDofaHWh82GHEOSROocl1iB7UyVwi9Ic0erJxvq9C10ii0KhOrz8kINEcp3zcXZDmc469Tsumx3HceMclF%2F%2FlH4uScz2Zp6Wh0TGQxUDEhYeq8eIedfYzr5Cb02INr1%2FUB1gepitN" rel="nofollow">理解 Deffie-Hellman 密钥交换算法</a></p>
<p><a href="https://link.segmentfault.com/?enc=F7%2BypSfm6XDo94CQXvNF2w%3D%3D.ShjtEv06x%2BEeDYGNIdBDQnw%2FqWzcb2MqXxFxsOkND6Ng49Ec1s3YauY3PgAnaxrDeJkkfaRwV97R3UKD1rhSy115jbc%2FO5QrUJkQfNVfKOUFiIpTXFbd8%2BTfGBKOcmAfXp05Px4pug0aIC3bowUD5E8pMsOHTiR41%2B36fSqz4YI%3D" rel="nofollow">迪菲-赫尔曼密钥交换</a></p>
<p><a href="https://link.segmentfault.com/?enc=1iNQReDuMsxOe4uQNnI30w%3D%3D.GB%2FsQFrGEt3FRV3CDmTYPvAgeD8PpLLpiqtde24oLxFUYgkQGGAHDd%2FAGOv12C7cvuoo0R%2FYJGlUXEnPduFK9iahHYdG1nK9LlndqhCgXnY%3D" rel="nofollow">Secure messages in NodeJSusing ECDH</a></p>
<p><a href="https://link.segmentfault.com/?enc=R%2BfRAo9c6hmQnD%2F4iBXp7Q%3D%3D.kcPIQU%2BDUf4PUxPQLStSVgvG5fKs5ORz6RGKR22H3IpfVtqk2Yxjp81QTe6vTTvbPBmQfdza7r0D7jSr7b3YWDX3rFR3rlL4CK%2FKOVjLWd0%3D" rel="nofollow">Keyless SSL: The Nitty Gritty Technical Details</a></p>
拥抱Node.js 8.0,N-API入门极简例子
https://segmentfault.com/a/1190000009643425
2017-06-03T13:35:04+08:00
2017-06-03T13:35:04+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=ER2Jl2S8gWd%2F3KdjSYj5tw%3D%3D.hTFjQVFT6E315Qc7H6BolX5DoJ5TKNZItiDlRgkgeR4TVa7P96nch9SPsUQBpxewv%2B3F1DkPdtDYrVlAvAs6Pg%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=je8qUAOftgQP0mIQxYow9g%3D%3D.K9Pq9dO4R3nzyk%2FDRmt7YzlWCBBmPEI4YUU2P9QJRUf1CRggi6lztkpPE0o2Ieooc9SzEWVAb4xs%2BctpSkrTVtoWA0Fs4V8tm0dKXaKOvrFCFzdleyALMmNK3WXmb8TuYiEVlwvSSF95Wu5%2Ffb%2B1Ew%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>N-API简介</h2>
<p>Node.js 8.0 在2017年6月份发布,升级的特性中,包含了N-API。编写过或者使用过 node扩展的同学,不少都遇到过升级node版本,node扩展编译失败的情况。因为node扩展严重依赖于V8暴露的API,而node不同版本依赖的V8版本可能不同,一旦升级node版本,原先运行正常的node扩展就编译失败了。</p>
<p>这种情况对node生态圈无疑是不利的,N-API的引入正是试图改善这种情况的一种尝试。它跟底层JS引擎无关,只要N-API暴露的API足够稳定,那么node扩展的编写者就不用过分担忧node的升级问题。</p>
<h2>如何使用N-API</h2>
<p>先强调一点,N-API并不是对原有node扩展实现方式的替代,它只是提供了一系列底层无关的API,来帮助开发者编写跨版本的node扩展。至于如何编写、编译、使用扩展,跟原来的差不多。</p>
<p>本文会从一个超级简单的例子,简单介绍N-API的使用,包括环境准备、编写扩展、编译、运行几个步骤。</p>
<blockquote><p>备注:当前N-API还处于试验阶段,官方文档提供的例子都是有问题的,如用于生产环境需格外谨慎。</p></blockquote>
<h2>1、环境准备</h2>
<p>首先,N-API是8.0版本引入的,首先确保本地安装了8.0版本。笔者用的是<code>nvm</code>,读者可自行选择安装方式。</p>
<pre><code class="bash">nvm i 8.0
nvm use 8.0</code></pre>
<p>然后,安装<code>node-gyp</code>,编译扩展会用到。</p>
<pre><code class="bash">npm install -g node-gyp</code></pre>
<p>创建项目目录,并初始化<code>package.json</code>。</p>
<pre><code class="bash">mkdir hello & cd hello # 目录名随便起
npm init -f</code></pre>
<h2>2、编写扩展</h2>
<p>创建<code>hello.cc</code>作为扩展的源文件。</p>
<pre><code class="bash">mkdir src
touch src/hello.cc</code></pre>
<p>编辑<code>hello.cc</code>,输入如下内容。</p>
<pre><code class="c">#include <node_api.h>
// 实际暴露的方法,这里只是简单返回一个字符串
napi_value HelloMethod (napi_env env, napi_callback_info info) {
napi_value world;
napi_create_string_utf8(env, "world", 5, &world);
return world;
}
// 扩展的初始化方法,其中
// env:环境变量
// exports、module:node模块中对外暴露的对象
void Init (napi_env env, napi_value exports, napi_value module, void* priv) {
// napi_property_descriptor 为结构体,作用是描述扩展暴露的 属性/方法 的描述
napi_property_descriptor desc = { "hello", 0, HelloMethod, 0, 0, 0, napi_default, 0 };
napi_define_properties(env, exports, 1, &desc); // 定义暴露的方法
}
NAPI_MODULE(hello, Init); // 注册扩展,扩展名叫做hello,Init为扩展的初始化方法</code></pre>
<h2>3、编译扩展</h2>
<p>首先,创建编译描述文件<code>binding.gyp</code>。</p>
<pre><code class="json">{
"targets": [
{
"target_name": "hello",
"sources": [ "./src/hello.cc" ]
}
]
}</code></pre>
<p>然后,运行如下命令进行编译。</p>
<pre><code class="bash">node-gyp rebuild</code></pre>
<h2>4、调用扩展</h2>
<p>未方便调用扩展,先安装<code>bindings</code>。</p>
<pre><code class="bash">npm install --save bindings</code></pre>
<p>然后,创建<code>app.js</code>,调用刚编译的扩展。</p>
<pre><code class="javascript">var addon = require('bindings')('hello');
console.log( addon.hello() ); // world</code></pre>
<p>运行代码,由于N-API当前尚处于Experimental阶段,记得加上<code>--napi-modules</code>标记。</p>
<pre><code class="bash">node --napi-modules app.js</code></pre>
<p>输出如下</p>
<pre><code class="bash">{"path":"/data/github/abi-stable-node-addon-examples/1_hello_world/napi/build/Release/hello.node"}
world
(node:6500) Warning: N-API is an experimental feature and could change at any time.</code></pre>
<h2>相关链接</h2>
<p>N-API:<a href="https://link.segmentfault.com/?enc=m6iInyF3OB7ekOy95NRf3g%3D%3D.2FmJdnhfYC7GDzFuldNAgmMLBJG%2Fo6Kju%2FcKVykQ%2BDNh4YQU2tSH6HR6OihHTLn2" rel="nofollow">https://nodejs.org/api/n-api....</a></p>
<p>C++ Addons:<a href="https://link.segmentfault.com/?enc=r54o4kEwmJm5f8wZ9uHb3w%3D%3D.PutAXPpGJgqj9ZGBjfSaOBk87b%2BbXMudz2T%2FPJu%2BJdZfZ6Pv9nVfqW9H1c18BNj3" rel="nofollow">https://nodejs.org/api/addons...</a></p>
Nodejs进阶:核心模块Buffer常用API使用总结
https://segmentfault.com/a/1190000009547330
2017-05-25T08:34:27+08:00
2017-05-25T08:34:27+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
2
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=xX06eRGywJlX3XHKJHQfUg%3D%3D.QDKed7BS%2B4efTnRu3OsBmWkf2MOMz7oI84NIA143gMtV8V4ETguZ2US1HucnBbOnzzZXDmdsvun%2BkBHykfC7Ow%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=x463YtC8kXBMIjlqsJRy1A%3D%3D.gKvFYyQgC39ET%2BFl4XvMdCFjNQxyPEqxzaRfCe7CjjzzHl6RegcxNcVzgZ%2BNruruRGhNu3hlFmyp23nawx%2FrnddGy%2FzvvIqvdyhR0hu7wSHexEJfiKAtsFwqu0%2Bk6ScavIUhP8qL52t3r8gEZFfsew%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块概览</h2>
<p>Buffer是node的核心模块,开发者可以利用它来处理二进制数据,比如文件流的读写、网络请求数据的处理等。</p>
<p>Buffer的API非常多,本文仅挑选 比较常用/容易理解 的API进行讲解,包括Buffer实例的创建、比较、连接、拷贝、查找、遍历、类型转换、截取、编码转换等。</p>
<h2>创建</h2>
<ul>
<li><p>new Buffer(array)</p></li>
<li><p>Buffer.alloc(length)</p></li>
<li><p>Buffer.allocUnsafe(length)</p></li>
<li><p>Buffer.from(array)</p></li>
</ul>
<h3>通过 new Buffer(array)</h3>
<pre><code class="js">// Creates a new Buffer containing the ASCII bytes of the string 'buffer'
const buf = new Buffer([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);</code></pre>
<p>验证下:</p>
<pre><code class="js">var array = 'buffer'.split('').map(function(v){
return '0x' + v.charCodeAt(0).toString(16)
});
console.log( array.join() );
// 输出:0x62,0x75,0x66,0x66,0x65,0x72</code></pre>
<h3>通过 Buffer.alloc(length)</h3>
<pre><code class="js">var buf1 = Buffer.alloc(10); // 长度为10的buffer,初始值为0x0
var buf2 = Buffer.alloc(10, 1); // 长度为10的buffer,初始值为0x1</code></pre>
<pre><code class="js">var buf3 = Buffer.allocUnsafe(10); // 长度为10的buffer,初始值不确定</code></pre>
<pre><code class="js">var buf4 = Buffer.from([1, 2, 3]) // 长度为3的buffer,初始值为 0x01, 0x02, 0x03</code></pre>
<h3>通过Buffer.from()</h3>
<p>例子一:Buffer.from(array)</p>
<pre><code class="js">// [0x62, 0x75, 0x66, 0x66, 0x65, 0x72] 为字符串 "buffer"
// 0x62 为16进制,转成十进制就是 98,代表的就是字母 b
var buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
console.log(buf.toString());</code></pre>
<p>例子二:Buffer.from(string[, encoding])</p>
<p>通过string创建buffer,跟将buffer转成字符串时,记得编码保持一致,不然会出现乱码,如下所示。</p>
<pre><code class="js">var buf = Buffer.from('this is a tést'); // 默认采用utf8
// 输出:this is a tést
console.log(buf.toString()); // 默认编码是utf8,所以正常打印
// 输出:this is a tC)st
console.log(buf.toString('ascii')); // 转成字符串时,编码不是utf8,所以乱码</code></pre>
<p>对乱码的分析如下:</p>
<pre><code class="js">var letter = 'é';
var buff = Buffer.from(letter); // 默认编码是utf8,这里占据两个字节 <Buffer c3 a9>
var len = buff.length; // 2
var code = buff[0]; // 第一个字节为0xc3,即195:超出ascii的最大支持范围
var binary = code.toString(2); // 195的二进制:10101001
var finalBinary = binary.slice(1); // 将高位的1舍弃,变成:0101001
var finalCode = parseInt(finalBinary, 2); // 0101001 对应的十进制:67
var finalLetter = String.fromCharCode(finalCode); // 67对应的字符:C
// 同理 0xa9最终转成的ascii字符为)
// 所以,最终输出为 this is a tC)st</code></pre>
<p>例子三:Buffer.from(buffer)</p>
<p>创建新的Buffer实例,并将buffer的数据拷贝到新的实例子中去。</p>
<pre><code class="js">var buff = Buffer.from('buffer');
var buff2 = Buffer.from(buff);
console.log(buff.toString()); // 输出:buffer
console.log(buff2.toString()); // 输出:buffer
buff2[0] = 0x61;
console.log(buff.toString()); // 输出:buffer
console.log(buff2.toString()); // 输出:auffer</code></pre>
<h2>buffer比较</h2>
<h3>buf.equals(otherBuffer)</h3>
<p>判断两个buffer实例存储的数据是否相同,如果是,返回true,否则返回false。</p>
<pre><code class="js">// 例子一:编码一样,内容相同
var buf1 = Buffer.from('A');
var buf2 = Buffer.from('A');
console.log( buf1.equals(buf2) ); // true
// 例子二:编码一样,内容不同
var buf3 = Buffer.from('A');
var buf4 = Buffer.from('B');
console.log( buf3.equals(buf4) ); // false
// 例子三:编码不一样,内容相同
var buf5 = Buffer.from('ABC'); // <Buffer 41 42 43>
var buf6 = Buffer.from('414243', 'hex');
console.log(buf5.equals(buf6));</code></pre>
<h3>buf.compare(target[, targetStart[, targetEnd[, sourceStart[, sourceEnd]]]])</h3>
<p>同样是对两个buffer实例进行比较,不同的是:</p>
<ol>
<li><p>可以指定特定比较的范围(通过start、end指定)</p></li>
<li><p>返回值为整数,达标buf、target的大小关系</p></li>
</ol>
<p>假设返回值为</p>
<ul>
<li><p><code>0</code>:buf、target大小相同。</p></li>
<li><p><code>1</code>:buf大于target,也就是说buf应该排在target之后。</p></li>
<li><p><code>-1</code>:buf小于target,也就是说buf应该排在target之前。</p></li>
</ul>
<p>看例子,官方的例子挺好的,直接贴一下:</p>
<pre><code class="js">const buf1 = Buffer.from('ABC');
const buf2 = Buffer.from('BCD');
const buf3 = Buffer.from('ABCD');
// Prints: 0
console.log(buf1.compare(buf1));
// Prints: -1
console.log(buf1.compare(buf2));
// Prints: -1
console.log(buf1.compare(buf3));
// Prints: 1
console.log(buf2.compare(buf1));
// Prints: 1
console.log(buf2.compare(buf3));
// Prints: [ <Buffer 41 42 43>, <Buffer 41 42 43 44>, <Buffer 42 43 44> ]
// (This result is equal to: [buf1, buf3, buf2])
console.log([buf1, buf2, buf3].sort(Buffer.compare));</code></pre>
<h3>Buffer.compare(buf1, buf2)</h3>
<p>跟 <code>buf.compare(target)</code> 大同小异,一般用于排序。直接贴官方例子:</p>
<pre><code class="js">const buf1 = Buffer.from('1234');
const buf2 = Buffer.from('0123');
const arr = [buf1, buf2];
// Prints: [ <Buffer 30 31 32 33>, <Buffer 31 32 33 34> ]
// (This result is equal to: [buf2, buf1])
console.log(arr.sort(Buffer.compare));</code></pre>
<h2>从Buffer.from([62])谈起</h2>
<p>这里稍微研究下Buffer.from(array)。下面是官方文档对API的说明,也就是说,每个array的元素对应1个字节(8位),取值从0到255。</p>
<blockquote><p>Allocates a new Buffer using an array of octets.</p></blockquote>
<h3>数组元素为数字</h3>
<p>首先看下,传入的元素为数字的场景。下面分别是10进制、8进制、16进制,跟预期中的结果一致。</p>
<pre><code class="js">var buff = Buffer.from([62])
// <Buffer 3e>
// buff[0] === parseInt('3e', 16) === 62</code></pre>
<pre><code class="js">var buff = Buffer.from([062])
// <Buffer 32>
// buff[0] === parseInt(62, 8) === parseInt(32, 16) === 50</code></pre>
<pre><code class="js">var buff = Buffer.from([0x62])
// <Buffer 62>
// buff[0] === parseInt(62, 16) === 98</code></pre>
<h3>数组元素为字符串</h3>
<p>再看下,传入的元素为字符串的场景。</p>
<ol>
<li><p><code>0</code>开头的字符串,在parseInt('062')时,可以解释为62,也可以解释为50(八进制),这里看到采用了第一种解释。</p></li>
<li><p>字符串的场景,跟parseInt()有没有关系,暂未深入探究,只是这样猜想。TODO(找时间研究下)</p></li>
</ol>
<pre><code class="js">var buff = Buffer.from(['62'])
// <Buffer 3e>
// buff[0] === parseInt('3e', 16) === parseInt('62') === 62</code></pre>
<pre><code class="js">var buff = Buffer.from(['062'])
// <Buffer 3e>
// buff[0] === parseInt('3e', 16) === parseInt('062') === 62</code></pre>
<pre><code class="js">var buff = Buffer.from(['0x62'])
// <Buffer 62>
// buff[0] === parseInt('62', 16) === parseInt('0x62') === 98</code></pre>
<h3>数组元素大小超出1个字节</h3>
<p>感兴趣的同学自行探究。</p>
<pre><code class="js">var buff = Buffer.from([256])
// <Buffer 00></code></pre>
<h2>Buffer.from('1')</h2>
<p>一开始不自觉的会将<code>Buffer.from('1')[0]</code>跟<code>"1"</code>划等号,其实<code>"1"</code>对应的编码是49。</p>
<pre><code class="js">var buff = Buffer.from('1') // <Buffer 31>
console.log(buff[0] === 1) // false</code></pre>
<p>这样对比就知道了,编码为1的是个控制字符,表示 Start of Heading。</p>
<pre><code class="js">console.log( String.fromCharCode(49) ) // '1'
console.log( String.fromCharCode(1) ) // '\u0001'</code></pre>
<h2>buffer连接:Buffer.concat(list[, totalLength])</h2>
<p>备注:个人觉得<code>totalLength</code>这个参数挺多余的,从官方文档来看,是处于性能提升的角度考虑。不过内部实现也只是遍历list,将length累加得到totalLength,从这点来看,性能优化是几乎可以忽略不计的。</p>
<pre><code class="js">var buff1 = Buffer.alloc(10);
var buff2 = Buffer.alloc(20);
var totalLength = buff1.length + buff2.length;
console.log(totalLength); // 30
var buff3 = Buffer.concat([buff1, buff2], totalLength);
console.log(buff3.length); // 30</code></pre>
<p>除了上面提到的性能优化,totalLength还有两点需要注意。假设list里面所有buffer的长度累加和为length</p>
<ul>
<li><p>totalLength > length:返回长度为totalLength的Buffer实例,超出长度的部分填充0。</p></li>
<li><p>totalLength < length:返回长度为totalLength的Buffer实例,后面部分舍弃。</p></li>
</ul>
<pre><code class="js">var buff4 = Buffer.from([1, 2]);
var buff5 = Buffer.from([3, 4]);
var buff6 = Buffer.concat([buff4, buff5], 5);
console.log(buff6.length); //
console.log(buff6); // <Buffer 01 02 03 04 00>
var buff7 = Buffer.concat([buff4, buff5], 3);
console.log(buff7.length); // 3
console.log(buff7); // <Buffer 01 02 03></code></pre>
<h2>拷贝:buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])</h2>
<p>使用比较简单,如果忽略后面三个参数,那就是将buf的数据拷贝到target里去,如下所示:</p>
<pre><code class="js">var buff1 = Buffer.from([1, 2]);
var buff2 = Buffer.alloc(2);
buff1.copy(buff2);
console.log(buff2); // <Buffer 01 02></code></pre>
<p>另外三个参数比较直观,直接看官方例子</p>
<pre><code class="js">const buf1 = Buffer.allocUnsafe(26);
const buf2 = Buffer.allocUnsafe(26).fill('!');
for (let i = 0 ; i < 26 ; i++) {
// 97 is the decimal ASCII value for 'a'
buf1[i] = i + 97;
}
buf1.copy(buf2, 8, 16, 20);
// Prints: !!!!!!!!qrst!!!!!!!!!!!!!
console.log(buf2.toString('ascii', 0, 25));</code></pre>
<h2>查找:buf.indexOf(value, byteOffset)</h2>
<p>跟数组的查找差不多,需要注意的是,value可能是String、Buffer、Integer中的任意类型。</p>
<ul>
<li><p>String:如果是字符串,那么encoding就是其对应的编码,默认是utf8。</p></li>
<li><p>Buffer:如果是Buffer实例,那么会将value中的完整数据,跟buf进行对比。</p></li>
<li><p>Integer:如果是数字,那么value会被当做无符号的8位整数,取值范围是0到255。</p></li>
</ul>
<p>另外,可以通过<code>byteOffset</code>来指定起始查找位置。</p>
<p>直接上代码,官方例子妥妥的,耐心看完它基本就理解得差不多了。</p>
<pre><code class="js">const buf = Buffer.from('this is a buffer');
// Prints: 0
console.log(buf.indexOf('this'));
// Prints: 2
console.log(buf.indexOf('is'));
// Prints: 8
console.log(buf.indexOf(Buffer.from('a buffer')));
// Prints: 8
// (97 is the decimal ASCII value for 'a')
console.log(buf.indexOf(97));
// Prints: -1
console.log(buf.indexOf(Buffer.from('a buffer example')));
// Prints: 8
console.log(buf.indexOf(Buffer.from('a buffer example').slice(0, 8)));
const utf16Buffer = Buffer.from('\u039a\u0391\u03a3\u03a3\u0395', 'ucs2');
// Prints: 4
console.log(utf16Buffer.indexOf('\u03a3', 0, 'ucs2'));
// Prints: 6
console.log(utf16Buffer.indexOf('\u03a3', -4, 'ucs2'));</code></pre>
<h2>写:buf.write(string[, offset[, length]][, encoding])</h2>
<p>将sring写入buf实例,同时返回写入的字节数。</p>
<p>参数如下:</p>
<ul>
<li><p>string:写入的字符串。</p></li>
<li><p>offset:从buf的第几位开始写入,默认是0。</p></li>
<li><p>length:写入多少个字节,默认是 buf.length - offset。</p></li>
<li><p>encoding:字符串的编码,默认是utf8。</p></li>
</ul>
<p>看个简单例子</p>
<pre><code class="js">var buff = Buffer.alloc(4);
buff.write('a'); // 返回 1
console.log(buff); // 打印 <Buffer 61 00 00 00>
buff.write('ab'); // 返回 2
console.log(buff); // 打印 <Buffer 61 62 00 00></code></pre>
<h2>填充:buf.fill(value[, offset[, end]][, encoding])</h2>
<p>用<code>value</code>填充buf,常用于初始化buf。参数说明如下:</p>
<ul>
<li><p>value:用来填充的内容,可以是Buffer、String或Integer。</p></li>
<li><p>offset:从第几位开始填充,默认是0。</p></li>
<li><p>end:停止填充的位置,默认是 buf.length。</p></li>
<li><p>encoding:如果<code>value</code>是String,那么为<code>value</code>的编码,默认是utf8。</p></li>
</ul>
<p>例子:</p>
<pre><code class="js">var buff = Buffer.alloc(20).fill('a');
console.log(buff.toString()); // aaaaaaaaaaaaaaaaaaaa</code></pre>
<h2>转成字符串: buf.toString([encoding[, start[, end]]])</h2>
<p>把buf解码成字符串,用法比较直观,看例子</p>
<pre><code class="js">var buff = Buffer.from('hello');
console.log( buff.toString() ); // hello
console.log( buff.toString('utf8', 0, 2) ); // he</code></pre>
<h2>转成JSON字符串:buf.toJSON()</h2>
<pre><code class="js">var buff = Buffer.from('hello');
console.log( buff.toJSON() ); // { type: 'Buffer', data: [ 104, 101, 108, 108, 111 ] }</code></pre>
<h2>遍历:buf.values()、buf.keys()、buf.entries()</h2>
<p>用于对<code>buf</code>进行<code>for...of</code>遍历,直接看例子。</p>
<pre><code class="js">var buff = Buffer.from('abcde');
for(const key of buff.keys()){
console.log('key is %d', key);
}
// key is 0
// key is 1
// key is 2
// key is 3
// key is 4
for(const value of buff.values()){
console.log('value is %d', value);
}
// value is 97
// value is 98
// value is 99
// value is 100
// value is 101
for(const pair of buff.entries()){
console.log('buff[%d] === %d', pair[0], pair[1]);
}
// buff[0] === 97
// buff[1] === 98
// buff[2] === 99
// buff[3] === 100
// buff[4] === 101</code></pre>
<h2>截取:buf.slice([start[, end]])</h2>
<p>用于截取buf,并返回一个新的Buffer实例。需要注意的是,这里返回的Buffer实例,指向的仍然是buf的内存地址,所以对新Buffer实例的修改,也会影响到buf。</p>
<pre><code class="js">var buff1 = Buffer.from('abcde');
console.log(buff1); // <Buffer 61 62 63 64 65>
var buff2 = buff1.slice();
console.log(buff2); // <Buffer 61 62 63 64 65>
var buff3 = buff1.slice(1, 3);
console.log(buff3); // <Buffer 62 63>
buff3[0] = 97; // parseInt(61, 16) ==> 97
console.log(buff1); // <Buffer 62 63></code></pre>
<h2>TODO</h2>
<ol>
<li><p>创建、拷贝、截取、转换、查找</p></li>
<li><p>buffer、arraybuffer、dataview、typedarray</p></li>
<li><p>buffer vs 编码</p></li>
<li><p>Buffer.from()、Buffer.alloc()、Buffer.alocUnsafe()</p></li>
<li><p>Buffer vs TypedArray</p></li>
</ol>
<h2>文档摘要</h2>
<p>关于buffer内存空间的动态分配</p>
<blockquote><p>Instances of the Buffer class are similar to arrays of integers but correspond to fixed-sized, raw memory allocations outside the V8 heap. The size of the Buffer is established when it is created and cannot be resized.</p></blockquote>
<h2>相关链接</h2>
<p>unicode对照表<br><a href="https://link.segmentfault.com/?enc=4RZu2TnamBdl%2BrWwAE8YjQ%3D%3D.O1%2FilSGc1lz6fXkLSN7EpExUFmdWu3VPVvcNYZgnVLtxEfhi96SwcNdHuVv4IR8I" rel="nofollow">https://unicode-table.com/cn/...</a></p>
<p>字符编码笔记:ASCII,Unicode和UTF-8<br><a href="https://link.segmentfault.com/?enc=rbfGf8lmylSYfohrHykC4A%3D%3D.HghVEoNLc36PG3ek9ZeoR7xD53RmpVnif62ebXhkjY%2FzSg3xq1ZtopUrR%2F3IklLtJr2tGg7K0VPyG%2FDNHI5hSFQ2w0yyrmqVBO3mXrkPaFQ%3D" rel="nofollow">http://www.ruanyifeng.com/blo...</a></p>
Nodejs基础:巧用string_decoder将buffer转成string
https://segmentfault.com/a/1190000009536199
2017-05-24T11:53:57+08:00
2017-05-24T11:53:57+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=3c90HT9Yy9J2nPHLIPaJyQ%3D%3D.gxgxlwSkVQKoRozvYJbPmFOCXBYmU7KB%2Bxoh2%2FxL86Q7nMJfsv8zsEK%2BvusN62rE4n%2BLosx05Bf%2BISxNXzEGlQ%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=tE81f4IImvlUrJ6Kvh5eRQ%3D%3D.9YWou%2Bd1QEAQqvsrki3Iro21kXb%2F6qMwvLyWf5TOX55XFOJEBw22u9sPdmGwZLAWfYPfI4mcnUeSRUC34m534SjkgKwsSDX0%2FIj%2BnVEPc7lm%2BmsTTrbEbcxeSXAaLeMimOBjkac3HkyixmvP9YvI2w%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块简介</h2>
<p><code>string_decoder</code>模块用于将Buffer转成对应的字符串。使用者通过调用<code>stringDecoder.write(buffer)</code>,可以获得buffer对应的字符串。</p>
<p>它的特殊之处在于,当传入的buffer不完整(比如三个字节的字符,只传入了两个),内部会维护一个internal buffer将不完整的字节cache住,等到使用者再次调用<code>stringDecoder.write(buffer)</code>传入剩余的字节,来拼成完整的字符。</p>
<p>这样可以有效避免buffer不完整带来的错误,对于很多场景,比如网络请求中的包体解析等,非常有用。</p>
<h2>入门例子</h2>
<p>这节分别演示了<code>decode.write(buffer)</code>、<code>decode.end([buffer])</code>两个主要API的用法。</p>
<p>例子一:</p>
<p><code>decoder.write(buffer)</code>调用传入了Buffer对象<code><Buffer e4 bd a0></code>,相应的返回了对应的字符串<code>你</code>;</p>
<pre><code class="javascript">const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');
// Buffer.from('你') => <Buffer e4 bd a0>
const str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0]));
console.log(str); // 你</code></pre>
<p>例子二:</p>
<p>当<code>decoder.end([buffer])</code>被调用时,内部剩余的buffer会被一次性返回。如果此时带上<code>buffer</code>参数,那么相当于同时调用<code>decoder.write(buffer)</code>和<code>decoder.end()</code>。</p>
<pre><code class="javascript">const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');
// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str); // 你
str = decoder.end(Buffer.from([0xbd]));
console.log(str); // 好</code></pre>
<h2>例子:分多次写入多个字节</h2>
<p>下面的例子,演示了分多次写入多个字节时,<code>string_decoder</code>模块是怎么处理的。</p>
<p>首先,传入了<code><Buffer e4 bd a0 e5 a5></code>,<code>好</code>还差1个字节,此时,<code>decoder.write(xx)</code>返回<code>你</code>。</p>
<p>然后,再次调用<code>decoder.write(Buffer.from([0xbd]))</code>,将剩余的1个字节传入,成功返回<code>好</code>。</p>
<pre><code class="javascript">const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');
// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str); // 你
str = decoder.write(Buffer.from([0xbd]));
console.log(str); // 好</code></pre>
<h2>例子:decoder.end()时,字节数不完整的处理</h2>
<p><code>decoder.end(buffer)</code>时,仅传入了<code>好</code>的第1个字节,此时调用<code>decoder.end()</code>,返回了<code>�</code>,对应的buffer为<code><Buffer ef bf bd></code>。</p>
<pre><code class="javascript">const StringDecoder = require('string_decoder').StringDecoder;
// Buffer.from('好') => <Buffer e5 a5 bd>
let decoder = new StringDecoder('utf8');
let str = decoder.end( Buffer.from([0xe5]) );
console.log(str); // �
console.log(Buffer.from(str)); // <Buffer ef bf bd></code></pre>
<p>官方文档对于这种情况的解释是这样的(跟废话差不多),大约是约定俗成了,当<code>utf8</code>码点无效时,替换成<code>ef bf bd</code>。</p>
<blockquote><p>Returns any remaining input stored in the internal buffer as a string. Bytes representing incomplete UTF-8 and UTF-16 characters will be replaced with substitution characters appropriate for the character encoding.</p></blockquote>
<h2>相关链接</h2>
<p>你应该记住的一个UTF-8字符「EF BF BD」<br><a href="https://link.segmentfault.com/?enc=oi0%2B8BIk9lRo4ogmtbgxHQ%3D%3D.mAeKyV2XyxDfd4T6N%2F8QiJlF74pgXBHz%2FezvuJ5mvEMsj7wPDKGsZcV326mj%2Fh1RfTpCtmrKnj4vJ%2FH9gz57AA%3D%3D" rel="nofollow">http://liudanking.com/golang/...</a></p>
Nodejs基础:stream模块入门介绍与使用
https://segmentfault.com/a/1190000009533590
2017-05-24T09:41:37+08:00
2017-05-24T09:41:37+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
3
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=HqlYyNCUa3wtsgtrBwwo5w%3D%3D.qxwd29DEtizLbx3M3FtvFH2I%2BuZKRdaGrR%2FKdlja5iZeYPW02BkcoUcwNGzzOPoj6ybNbuDdhNFmbgq05fiTPw%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=uVsRGbrYdTFDYsCyPAWiEA%3D%3D.WbyxnhJ%2BoR4IgTGnGO9mZiQdDBjbKZb1quShu0Lpjr7IRMfXpnnFd0Kup2ij7y1VHXDfnijD8O2wvHWNZCbrUPzom%2FYMJHeKP3%2BBT9f6H7jya6kncrHNAAfPjUopNgC41onYam3Ypksv245z5hsGWA%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块概览</h2>
<p>nodejs的核心模块,基本上都是stream的的实例,比如process.stdout、http.clientRequest。</p>
<p>对于大部分的nodejs开发者来说,平常并不会直接用到stream模块,只需要了解stream的运行机制即可(非常重要)。</p>
<p>而对于想要实现自定义stream实例的开发者来说,就得好好研究stream的扩展API了,比如gulp的内部实现就大量用到了自定义的stream类型。</p>
<p>来个简单的例子镇楼,几行代码就实现了读取文件内容,并打印到控制台:</p>
<pre><code class="js">const fs = require('fs');
fs.createReadStream('./sample.txt').pipe(process.stdout);</code></pre>
<h2>Stream分类</h2>
<p>在nodejs中,有四种stream类型:</p>
<ul>
<li><p>Readable:用来读取数据,比如 <code>fs.createReadStream()</code>。</p></li>
<li><p>Writable:用来写数据,比如 <code>fs.createWriteStream()</code>。</p></li>
<li><p>Duplex:可读+可写,比如 <code>net.Socket()</code>。</p></li>
<li><p>Transform:在读写的过程中,可以对数据进行修改,比如 <code>zlib.createDeflate()</code>(数据压缩/解压)。</p></li>
</ul>
<h2>Readable Stream</h2>
<p>以下都是nodejs中常见的Readable Stream,当然还有其他的,可自行查看文档。</p>
<ul>
<li><p>http.IncomingRequest</p></li>
<li><p>fs.createReadStream()</p></li>
<li><p>process.stdin</p></li>
<li><p>其他</p></li>
</ul>
<p>例子一:</p>
<pre><code class="js">var fs = require('fs');
fs.readFile('./sample.txt', 'utf8', function(err, content){
// 文件读取完成,文件内容是 [你好,我是程序猿小卡]
console.log('文件读取完成,文件内容是 [%s]', content);
});</code></pre>
<p>例子二:</p>
<pre><code class="js">var fs = require('fs');
var readStream = fs.createReadStream('./sample.txt');
var content = '';
readStream.setEncoding('utf8');
readStream.on('data', function(chunk){
content += chunk;
});
readStream.on('end', function(chunk){
// 文件读取完成,文件内容是 [你好,我是程序猿小卡]
console.log('文件读取完成,文件内容是 [%s]', content);
});</code></pre>
<p>例子三:</p>
<p>这里使用了<code>.pipe(dest)</code>,好处在于,如果文件</p>
<pre><code class="js">var fs = require('fs');
fs.createReadStream('./sample.txt').pipe(process.stdout);</code></pre>
<p>注意:这里只是原封不动的将内容输出到控制台,所以实际上跟前两个例子有细微差异。可以稍做修改,达到上面同样的效果</p>
<pre><code class="js">var fs = require('fs');
var onEnd = function(){
process.stdout.write(']');
};
var fileStream = fs.createReadStream('./sample.txt');
fileStream.on('end', onEnd)
fileStream.pipe(process.stdout);
process.stdout.write('文件读取完成,文件内容是[');
// 文件读取完成,文件内容是[你好,我是程序猿小卡]</code></pre>
<h2>Writable Stream</h2>
<p>同样以写文件为例子,比如想将<code>hello world</code>写到<code>sample.txt</code>里。</p>
<p>例子一:</p>
<pre><code class="js">var fs = require('fs');
var content = 'hello world';
var filepath = './sample.txt';
fs.writeFile(filepath, content);</code></pre>
<p>例子二:</p>
<pre><code class="js">var fs = require('fs');
var content = 'hello world';
var filepath = './sample.txt';
var writeStram = fs.createWriteStream(filepath);
writeStram.write(content);
writeStram.end();</code></pre>
<h2>Duplex Stream</h2>
<p>最常见的Duplex stream应该就是<code>net.Socket</code>实例了,在前面的文章里有接触过,这里就直接上代码了,这里包含服务端代码、客户端代码。</p>
<p>服务端代码:</p>
<pre><code class="js">var net = require('net');
var opt = {
host: '127.0.0.1',
port: '3000'
};
var client = net.connect(opt, function(){
client.write('msg from client'); // 可写
});
// 可读
client.on('data', function(data){
// server: msg from client [msg from client]
console.log('client: got reply from server [%s]', data);
client.end();
});</code></pre>
<p>客户端代码:</p>
<pre><code class="js">var net = require('net');
var opt = {
host: '127.0.0.1',
port: '3000'
};
var client = net.connect(opt, function(){
client.write('msg from client'); // 可写
});
// 可读
client.on('data', function(data){
// lient: got reply from server [reply from server]
console.log('client: got reply from server [%s]', data);
client.end();
});</code></pre>
<h2>Transform Stream</h2>
<p>Transform stream是Duplex stream的特例,也就是说,Transform stream也同时可读可写。跟Duplex stream的区别点在于,Transform stream的输出与输入是存在相关性的。</p>
<p>常见的Transform stream包括<code>zlib</code>、<code>crypto</code>,这里举个简单例子:文件的gzip压缩。</p>
<pre><code class="js">var fs = require('fs');
var zlib = require('zlib');
var gzip = zlib.createGzip();
var inFile = fs.createReadStream('./extra/fileForCompress.txt');
var out = fs.createWriteStream('./extra/fileForCompress.txt.gz');
inFile.pipe(gzip).pipe(out);</code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=cXIlQp7wEXbIK3zTiwwRBw%3D%3D.a5%2BBAycm6ovdpEdLkcBQr5UA6BV4FJbk9fdIZJ3M%2FcWUwjSB2lFERVRQfTGf%2BvJ%2F" rel="nofollow">https://nodejs.org/api/stream...</a></p>
Nodejs进阶:Express常用中间件body-parser实现解析
https://segmentfault.com/a/1190000009502165
2017-05-22T08:09:52+08:00
2017-05-22T08:09:52+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
7
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=rNkigArB6HJzttYUVPw3eA%3D%3D.ZU2DThhXXWlrKm3KNgtH9IHgtqdkSE5njq2zYHKgtD6r3vIZ1tJgL9DbHFJEVQ%2BNnCQv2JSuMH%2FPFB1R2E80NQ%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=tQmVf6CPPWgYACeKYk8LMw%3D%3D.wSjP4zRjwzaP74Vj%2BvVbFcU0NVgyf3xe%2BaweHb50yMqWYs1Lffy1pgsY58y0XShskhz%2BSBOHIMOKlDLcvZxdUNmtLjmpYiJ1JUH54aPCjaeHlgs9Wy%2F40IJY5zAU3pSZyDLHSNQYKPHd0Tji10wUdw%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>写在前面</h2>
<p><code>body-parser</code>是非常常用的一个<code>express</code>中间件,作用是对post请求的请求体进行解析。使用非常简单,以下两行代码已经覆盖了大部分的使用场景。</p>
<pre><code class="javascript">app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));</code></pre>
<p>本文从简单的例子出发,探究<code>body-parser</code>的内部实现。至于<code>body-parser</code>如何使用,感兴趣的同学可以参考<a href="https://link.segmentfault.com/?enc=CQFr1Okz%2FdRkWvfbaVvh%2FQ%3D%3D.2hMlzPb3T8qCodAc4qqR%2FQCISzyb9JCs65UMgiwfnr%2FHeEuFkf4m1yOgZbRp6wRK" rel="nofollow">官方文档</a>。</p>
<h2>入门基础</h2>
<p>在正式讲解前,我们先来看一个POST请求的报文,如下所示。</p>
<pre><code class="http">POST /test HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: text/plain; charset=utf8
Content-Encoding: gzip
chyingp</code></pre>
<p>其中需要我们注意的有<code>Content-Type</code>、<code>Content-Encoding</code>以及报文主体:</p>
<ul>
<li><p>Content-Type:请求报文主体的类型、编码。常见的类型有<code>text/plain</code>、<code>application/json</code>、<code>application/x-www-form-urlencoded</code>。常见的编码有<code>utf8</code>、<code>gbk</code>等。</p></li>
<li><p>Content-Encoding:声明报文主体的压缩格式,常见的取值有<code>gzip</code>、<code>deflate</code>、<code>identity</code>。</p></li>
<li><p>报文主体:这里是个普通的文本字符串<code>chyingp</code>。</p></li>
</ul>
<h2>body-parser主要做了什么</h2>
<p><code>body-parser</code>实现的要点如下:</p>
<ol>
<li><p>处理不同类型的请求体:比如<code>text</code>、<code>json</code>、<code>urlencoded</code>等,对应的报文主体的格式不同。</p></li>
<li><p>处理不同的编码:比如<code>utf8</code>、<code>gbk</code>等。</p></li>
<li><p>处理不同的压缩类型:比如<code>gzip</code>、<code>deflare</code>等。</p></li>
<li><p>其他边界、异常的处理。</p></li>
</ol>
<h2>一、处理不同类型请求体</h2>
<p>为了方便读者测试,以下例子均包含服务端、客户端代码,完整代码可在<a href="https://link.segmentfault.com/?enc=qHqPZV4HdNDx9Bj%2BQ0un8w%3D%3D.%2FJF0TAwOZEu4R2y4hZU4u7rZGN5JmvCk4%2Bv5VXfqNAaWgnEl9HeQX5sV8%2BEzw0RVDOYl%2BCpz2J9TzmBar40L4xBL5eMxx3fd7KBiSZBU7FjdLOT4B7jA9bWBgQu2B2klOEB8OoiDAQeM1rzZ74pwgA%3D%3D" rel="nofollow">笔者github</a>上找到。</p>
<h3>解析text/plain</h3>
<p>客户端请求的代码如下,采用默认编码,不对请求体进行压缩。请求体类型为<code>text/plain</code>。</p>
<pre><code class="javascript">var http = require('http');
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Encoding': 'identity'
}
};
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end('chyingp');</code></pre>
<p>服务端代码如下。<code>text/plain</code>类型处理比较简单,就是buffer的拼接。</p>
<pre><code class="javascript">var http = require('http');
var parsePostBody = function (req, done) {
var arr = [];
var chunks;
req.on('data', buff => {
arr.push(buff);
});
req.on('end', () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
var server = http.createServer(function (req, res) {
parsePostBody(req, (chunks) => {
var body = chunks.toString();
res.end(`Your nick is ${body}`)
});
});
server.listen(3000);</code></pre>
<h3>解析application/json</h3>
<p>客户端代码如下,把<code>Content-Type</code>换成<code>application/json</code>。</p>
<pre><code class="javascript">var http = require('http');
var querystring = require('querystring');
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'identity'
}
};
var jsonBody = {
nick: 'chyingp'
};
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end( JSON.stringify(jsonBody) );</code></pre>
<p>服务端代码如下,相比<code>text/plain</code>,只是多了个<code>JSON.parse()</code>的过程。</p>
<pre><code class="javascript">var http = require('http');
var parsePostBody = function (req, done) {
var length = req.headers['content-length'] - 0;
var arr = [];
var chunks;
req.on('data', buff => {
arr.push(buff);
});
req.on('end', () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
var server = http.createServer(function (req, res) {
parsePostBody(req, (chunks) => {
var json = JSON.parse( chunks.toString() ); // 关键代码
res.end(`Your nick is ${json.nick}`)
});
});
server.listen(3000);</code></pre>
<h3>解析application/x-www-form-urlencoded</h3>
<p>客户端代码如下,这里通过<code>querystring</code>对请求体进行格式化,得到类似<code>nick=chyingp</code>的字符串。</p>
<pre><code class="javascript">var http = require('http');
var querystring = require('querystring');
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'form/x-www-form-urlencoded',
'Content-Encoding': 'identity'
}
};
var postBody = { nick: 'chyingp' };
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end( querystring.stringify(postBody) );</code></pre>
<p>服务端代码如下,同样跟<code>text/plain</code>的解析差不多,就多了个<code>querystring.parse()</code>的调用。</p>
<pre><code class="javascript">var http = require('http');
var querystring = require('querystring');
var parsePostBody = function (req, done) {
var length = req.headers['content-length'] - 0;
var arr = [];
var chunks;
req.on('data', buff => {
arr.push(buff);
});
req.on('end', () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
var server = http.createServer(function (req, res) {
parsePostBody(req, (chunks) => {
var body = querystring.parse( chunks.toString() ); // 关键代码
res.end(`Your nick is ${body.nick}`)
});
});
server.listen(3000);</code></pre>
<h2>二、处理不同编码</h2>
<p>很多时候,来自客户端的请求,采用的不一定是默认的<code>utf8</code>编码,这个时候,就需要对请求体进行解码处理。</p>
<p>客户端请求如下,有两个要点。</p>
<ol>
<li><p>编码声明:在<code>Content-Type</code>最后加上<code> ;charset=gbk</code></p></li>
<li><p>请求体编码:这里借助了<code>iconv-lite</code>,对请求体进行编码<code>iconv.encode('程序猿小卡', encoding)</code></p></li>
</ol>
<pre><code class="javascript">var http = require('http');
var iconv = require('iconv-lite');
var encoding = 'gbk'; // 请求编码
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=' + encoding,
'Content-Encoding': 'identity',
}
};
// 备注:nodejs本身不支持gbk编码,所以请求发送前,需要先进行编码
var buff = iconv.encode('程序猿小卡', encoding);
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end(buff, encoding);</code></pre>
<p>服务端代码如下,这里多了两个步骤:编码判断、解码操作。首先通过<code>Content-Type</code>获取编码类型<code>gbk</code>,然后通过<code>iconv-lite</code>进行反向解码操作。</p>
<pre><code class="javascript">var http = require('http');
var contentType = require('content-type');
var iconv = require('iconv-lite');
var parsePostBody = function (req, done) {
var obj = contentType.parse(req.headers['content-type']);
var charset = obj.parameters.charset; // 编码判断:这里获取到的值是 'gbk'
var arr = [];
var chunks;
req.on('data', buff => {
arr.push(buff);
});
req.on('end', () => {
chunks = Buffer.concat(arr);
var body = iconv.decode(chunks, charset); // 解码操作
done(body);
});
};
var server = http.createServer(function (req, res) {
parsePostBody(req, (body) => {
res.end(`Your nick is ${body}`)
});
});
server.listen(3000);</code></pre>
<h2>三、处理不同压缩类型</h2>
<p>这里举个<code>gzip</code>压缩的例子。客户端代码如下,要点如下:</p>
<ol>
<li><p>压缩类型声明:<code>Content-Encoding</code>赋值为<code>gzip</code>。</p></li>
<li><p>请求体压缩:通过<code>zlib</code>模块对请求体进行gzip压缩。</p></li>
</ol>
<pre><code class="javascript">var http = require('http');
var zlib = require('zlib');
var options = {
hostname: '127.0.0.1',
port: '3000',
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
}
};
var client = http.request(options, (res) => {
res.pipe(process.stdout);
});
// 注意:将 Content-Encoding 设置为 gzip 的同时,发送给服务端的数据也应该先进行gzip
var buff = zlib.gzipSync('chyingp');
client.end(buff);</code></pre>
<p>服务端代码如下,这里通过<code>zlib</code>模块,对请求体进行了解压缩操作(guzip)。</p>
<pre><code class="javascript">var http = require('http');
var zlib = require('zlib');
var parsePostBody = function (req, done) {
var length = req.headers['content-length'] - 0;
var contentEncoding = req.headers['content-encoding'];
var stream = req;
// 关键代码如下
if(contentEncoding === 'gzip') {
stream = zlib.createGunzip();
req.pipe(stream);
}
var arr = [];
var chunks;
stream.on('data', buff => {
arr.push(buff);
});
stream.on('end', () => {
chunks = Buffer.concat(arr);
done(chunks);
});
stream.on('error', error => console.error(error.message));
};
var server = http.createServer(function (req, res) {
parsePostBody(req, (chunks) => {
var body = chunks.toString();
res.end(`Your nick is ${body}`)
});
});
server.listen(3000);</code></pre>
<h2>写在后面</h2>
<p><code>body-parser</code>的核心实现并不复杂,翻看源码后你会发现,更多的代码是在处理异常跟边界。</p>
<p>另外,对于POST请求,还有一个非常常见的<code>Content-Type</code>是<code>multipart/form-data</code>,这个的处理相对复杂些,<code>body-parser</code>不打算对其进行支持。篇幅有限,后续章节再继续展开。</p>
<p>欢迎交流,如有错漏请指出。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=VWssZyJOIvni3%2FXrKTSCoA%3D%3D.DNQ9DdeyNM4CFE75umzMJKICILCkzKSOcTOzPZ9kUBhK0Nq4N7XvR9l5vx9YmX5O" rel="nofollow">https://github.com/expressjs/...</a></p>
<p><a href="https://link.segmentfault.com/?enc=2BHDRCzia2iwdrODIuz%2BPQ%3D%3D.BsPmjpnOXtabCiuwSVW%2BmqBeYMi9w2n1Nm4LqdA5f9L0iZLQz0KwC0q52i%2BJfWe3" rel="nofollow">https://github.com/ashtuchkin...</a></p>
Nodejs进阶:readline实现日志分析+简易命令行工具
https://segmentfault.com/a/1190000009198417
2017-04-26T09:04:35+08:00
2017-04-26T09:04:35+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
4
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=jml4JcbOkvjbl2a2qOI1iw%3D%3D.Lr7BVCQtUWjsi6JAFeqrAA8Gs%2BbC6PRh73AowfkezHaa%2BAn4wkg35GJeKuMTo3y74Sdy7Niip9DwiNTiXPUdBw%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=ONaeUYgYkOfZ1eBLjKyPcw%3D%3D.M3TA611qCqoK84E2IW2cIOc0TF61KitRIe%2BZf0%2Fge%2BDIGNKGsRJc93CUcCVhARd9DK62r3%2BhOMk1uQdU8WWYizxvp3xHy64WPXBEaKJsnpK93krsWC6Qo2yM8%2Bn3NFlM70Mcw0BwkqbsNipgs08kuQ%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块概览</h2>
<p>readline是个非常实用的模块。如名字所示,主要用来实现逐行读取,比如读取用户输入,或者读取文件内容。常见使用场景有下面几种,本文会逐一举例说明。本文相关代码可在笔者<a href="https://link.segmentfault.com/?enc=kBcUYCzOmYIYY%2FO6sAiedw%3D%3D.A5wtkUmeXw2BxctLN8Mwu9KjxEwOESzgnWOnhUAPm%2B0KGRgDLI7nwGTVRIYcp5aLaIch1HdWenwifj%2F2wwW0na1LrrYmhOhz%2FHueZJ%2FkQo39sGqG4%2Fv40LD8Y1FpmOx%2F" rel="nofollow">github</a>上找到。</p>
<ul>
<li><p>文件逐行读取:比如说进行日志分析。</p></li>
<li><p>自动完成:比如输入npm,自动提示"help init install"。</p></li>
<li><p>命令行工具:比如npm init这种问答式的脚手架工具。</p></li>
</ul>
<h2>基础例子</h2>
<p>先看个简单的例子,要求用户输入一个单词,然后自动转成大写</p>
<pre><code class="js">const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Please input a word: ', function(answer){
console.log('You have entered {%s}', answer.toUpperCase());
rl.close();
});</code></pre>
<p>运行如下:</p>
<pre><code class="bash">➜ toUpperCase git:(master) ✗ node app.js
Please input a word: hello
You have entered {HELLO}</code></pre>
<h2>例子:文件逐行读取:日志分析</h2>
<p>比如我们有如下日志文件access.log,我们想要提取“访问时间+访问地址”,借助<code>readline</code>可以很方便的完成日志分析的工作。</p>
<pre><code>[2016-12-09 13:56:48.407] [INFO] access - ::ffff:127.0.0.1 - - "GET /oc/v/account/user.html HTTP/1.1" 200 213125 "http://www.example.com/oc/v/account/login.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36"
[2016-12-09 14:00:10.618] [INFO] access - ::ffff:127.0.0.1 - - "GET /oc/v/contract/underlying.html HTTP/1.1" 200 216376 "http://www.example.com/oc/v/account/user.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36"
[2016-12-09 14:00:34.200] [INFO] access - ::ffff:127.0.0.1 - - "GET /oc/v/contract/underlying.html HTTP/1.1" 200 216376 "http://www.example.com/oc/v/account/user.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36"</code></pre>
<p>代码如下:</p>
<pre><code class="js">const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: fs.createReadStream('./access.log')
});
rl.on('line', (line) => {
const arr = line.split(' ');
console.log('访问时间:%s %s,访问地址:%s', arr[0], arr[1], arr[13]);
});</code></pre>
<p>运行结果如下:</p>
<pre><code class="bash">➜ lineByLineFromFile git:(master) ✗ node app.js
访问时间:[2016-12-09 13:56:48.407],访问地址:"http://www.example.com/oc/v/account/login.html"
访问时间:[2016-12-09 14:00:10.618],访问地址:"http://www.example.com/oc/v/account/user.html"
访问时间:[2016-12-09 14:00:34.200],访问地址:"http://www.example.com/oc/v/account/user.html"</code></pre>
<h2>例子:自动完成:代码提示</h2>
<p>这里我们实现一个简单的自动完成功能,当用户输入npm时,按tab键,自动提示用户可选的子命令,如help、init、install。</p>
<ul>
<li><p>输入<code>np</code>,按下tab:自动补全为npm</p></li>
<li><p>输入<code>npm in</code>,按下tab:自动提示可选子命令 init、install</p></li>
<li><p>输入<code>npm inst</code>,按下tab:自动补全为 <code>npm install</code></p></li>
</ul>
<pre><code class="js">const readline = require('readline');
const fs = require('fs');
function completer(line) {
const command = 'npm';
const subCommands = ['help', 'init', 'install'];
// 输入为空,或者为npm的一部分,则tab补全为npm
if(line.length < command.length){
return [command.indexOf(line) === 0 ? [command] : [], line];
}
// 输入 npm,tab提示 help init install
// 输入 npm in,tab提示 init install
let hits = subCommands.filter(function(subCommand){
const lineTrippedCommand = line.replace(command, '').trim();
return lineTrippedCommand && subCommand.indexOf( lineTrippedCommand ) === 0;
})
if(hits.length === 1){
hits = hits.map(function(hit){
return [command, hit].join(' ');
});
}
return [hits.length ? hits : subCommands, line];
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
});
rl.prompt();</code></pre>
<p>代码运行效果如下,当输入<code>npm in</code>,按下tab键,则会自动提示可选子命令init、install。</p>
<pre><code class="bash">➜ autoComplete git:(master) ✗ node app.js
> npm in
init install </code></pre>
<h2>例子:命令行工具:npmt init</h2>
<p>下面借助readline实现一个迷你版的<code>npm init</code>功能,运行脚本时,会依次要求用户输入name、version、author属性(其他略过)。</p>
<p>这里用到的是<code>rl.question(msg, cbk)</code>这个方法,它会在控制台输入一行提示,当用户完成输入,敲击回车,<code>cbk</code>就会被调用,并把用户输入作为参数传入。</p>
<pre><code class="js">const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'OHAI> '
});
const preHint = `
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See \`npm help json\` for definitive documentation on these fields
and exactly what they do.
Use \`npm install <pkg> --save\` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
`;
console.log(preHint);
// 问题
let questions = [ 'name', 'version', 'author'];
// 默认答案
let defaultAnswers = [ 'name', '1.0.0', 'none' ];
// 用户答案
let answers = [];
let index = 0;
function createPackageJson(){
var map = {};
questions.forEach(function(question, index){
map[question] = answers[index];
});
fs.writeFileSync('./package.json', JSON.stringify(map, null, 4));
}
function runQuestionLoop() {
if(index === questions.length) {
createPackageJson();
rl.close();
return;
}
let defaultAnswer = defaultAnswers[index];
let question = questions[index] + ': (' + defaultAnswer +') ';
rl.question(question, function(answer){
answers.push(answer || defaultAnswer);
index++;
runQuestionLoop();
});
}
runQuestionLoop();</code></pre>
<p>运行效果如下,最后还像模像样的生成了package.json(害羞脸)。</p>
<pre><code class="bash">➜ commandLine git:(master) ✗ node app.js
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (name) hello
version: (1.0.0) 0.0.1
author: (none) chyingp</code></pre>
<h2>写在后面</h2>
<p>有不少基于readline的有趣的工具,比如各种脚手架工具。限于篇幅不展开,感兴趣的同学可以研究下。</p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=FA1sM6CXY0ZCxl%2FNzgS21Q%3D%3D.nWfEQOQjFt2GG44PI2TnX%2B1nhemv2mPdRgfceQdVwzVA9%2FKamFUfrnJjISpDAl7z" rel="nofollow">https://nodejs.org/api/readli...</a></p>
Nodejs进阶:用debug模块打印调试日志
https://segmentfault.com/a/1190000009183793
2017-04-25T08:23:11+08:00
2017-04-25T08:23:11+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
5
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=g7VNKNswbJvfw%2Fdi%2BIRhpw%3D%3D.fUPT7DkC28MfLcWCSCuGU0EMoVKePONjim2R8s1kKH23mKPZlK62Fqt5Uu0S%2FAvEaAS8y3jnIg5rIAYtaP2ASQ%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=uHNanGdrsW5BqR%2BBq2RaGQ%3D%3D.gWUT4m8SoNjhW9eW68XTI%2Br45%2BQnsDRYDimqDPbSsmiObc%2Bqz7aSvVSTr91MiBaQWd7MkEdrsCwdze%2Fa94BcWOZikuklzaujKcRtVaih0cYPmEpMRZTgcKxDgBDy7jYYNUB%2BanG9vnwt9voUB9bmNw%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>前言</h2>
<p>在node程序开发中时,经常需要打印调试日志。用的比较多的是debug模块,比如express框架中就用到了。下文简单举几个例子进行说明。文中相关代码示例,可在<a href="https://link.segmentfault.com/?enc=We7maWtQUoi5ioU7K43hfA%3D%3D.jtHsJuYWiJ81ZM0EHDb%2BL1dhvZas7LJU3tKQ3fsef%2FIT2M770fMc76Mrvuc0pz16%2FKCi8IVbYjrqKjGyzl8B2VmPTm%2B4%2FBgndegAMHrXjjKl7S1Di0iWWe67mc6GWr3XAhZMXNePg7NgX8Z2nWC%2FMg%3D%3D" rel="nofollow">这里</a>找到。</p>
<blockquote><p>备注:node在0.11.3版本也加入了util.debuglog()用于打印调试日志,使用方法跟debug模块大同小异。</p></blockquote>
<h2>基础例子</h2>
<p>首先,安装<code>debug</code>模块。</p>
<pre><code class="bash">npm install debug</code></pre>
<p>使用很简单,运行node程序时,加上<code>DEBUG=app</code>环境变量即可。</p>
<pre><code class="javascript">/**
* debug基础例子
*/
var debug = require('debug')('app');
// 运行 DEBUG=app node 01.js
// 输出:app hello +0ms
debug('hello');</code></pre>
<h2>例子:命名空间</h2>
<p>当项目程序变得复杂,我们需要对日志进行分类打印,debug支持命令空间,如下所示。</p>
<ul>
<li><p><code>DEBUG=app,api</code>:表示同时打印出命名空间为app、api的调试日志。</p></li>
<li><p><code>DEBUG=a*</code>:支持通配符,所有命名空间为a开头的调试日志都打印出来。</p></li>
</ul>
<pre><code class="javascript">/**
* debug例子:命名空间
*/
var debug = require('debug');
var appDebug = debug('app');
var apiDebug = debug('api');
// 分别运行下面几行命令看下效果
//
// DEBUG=app node 02.js
// DEBUG=api node 02.js
// DEBUG=app,api node 02.js
// DEBUG=a* node 02.js
//
appDebug('hello');
apiDebug('hello');</code></pre>
<h2>例子:命名空间排除</h2>
<p>有的时候,我们想要打印出所有的调试日志,除了个别命名空间下的。这个时候,可以通过<code>-</code>来进行排除,如下所示。<code>-account*</code>表示排除所有以account开头的命名空间的调试日志。</p>
<pre><code class="javascript">/**
* debug例子:排查命名空间
*/
var debug = require('debug');
var listDebug = debug('app:list');
var profileDebug = debug('app:profile');
var loginDebug = debug('account:login');
// 分别运行下面几行命令看下效果
//
// DEBUG=* node 03.js
// DEBUG=*,-account* node 03.js
//
listDebug('hello');
profileDebug('hello');
loginDebug('hello');</code></pre>
<h2>例子:自定义格式化</h2>
<p>debug也支持格式化输出,如下例子所示。</p>
<pre><code class="javascript">var debug = require('debug')('app');
debug('my name is %s', 'chyingp');</code></pre>
<p>此外,也可以自定义格式化内容。</p>
<pre><code class="javascript">/**
* debug:自定义格式化
*/
var createDebug = require('debug')
createDebug.formatters.h = function(v) {
return v.toUpperCase();
};
var debug = createDebug('foo');
// 运行 DEBUG=foo node 04.js
// 输出 foo My name is CHYINGP +0ms
debug('My name is %h', 'chying');</code></pre>
<h2>相关链接</h2>
<p>debug:<a href="https://link.segmentfault.com/?enc=r%2FwSZlIHjCNg4tz0yRGKWQ%3D%3D.9SstR%2BkMBB34G3ET%2FODYI%2BIg64HKoSWnzoNZ9moHwPPBQaC4p6VbRwDVK%2BNwce6h" rel="nofollow">https://github.com/visionmedi...</a><br>debuglog:<a href="https://link.segmentfault.com/?enc=AJIxXFTUxYQ7OfQSrf%2BP2w%3D%3D.A%2BIB1lbT22VDOftWI86Ib1k4l3yBrT2lPQsVzBTDs0DwbFKxRISdPtPuosH7MDH%2BeSKhUHy8hZnOTPyXb1RXOA%3D%3D" rel="nofollow">https://nodejs.org/api/util.h...</a></p>
Nodejs进阶:express+session实现简易身份认证
https://segmentfault.com/a/1190000009170309
2017-04-24T09:02:30+08:00
2017-04-24T09:02:30+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
4
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=Fn4dvx37HidfPzxGQkR1uA%3D%3D.Rfef7TH8EqJU3Bbq1JOf3IHnE4%2BP8bW4%2FSFMz%2FOBZdtmCQ7TDOdq95xvRWS5lV0nQgIuVsf8dz3ME9MSXRojEA%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=Eb7PpQSPbQ%2Ftz320h%2FXJVw%3D%3D.C4VNdJvy7KHRnoq0xOiiC2ZVG9sMehGCnhRn8bzqxfAMNZb9phDeVguxtzYzgMfeG%2Bqgna71oE9DVa3VEAM%2B8eZ0niHsRt2%2BMgVSav1rYphcBfA0i51c9D%2FxWo8%2B284JeO3lHsBYH%2B89ULpTb279IA%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>文档概览</h2>
<p>本文基于express、express-session实现了简易的登录/登出功能,完整的代码示例可以在<a href="https://link.segmentfault.com/?enc=ZoM7u2NBt3Qwkbrr2aFlYQ%3D%3D.9CYGISqNRNVboVj8OpNWSqu5ZUa%2FnGG1PqCYMWpIXFH9le2axzmiDRl3aAm1Avy2T5o%2FxwsP4%2Fl3mOHyVr6Yu0E8tP8G3J3DLEbgJpG2seyidnGnnaj5aKRyX5B13RwM" rel="nofollow">这里</a>找到。</p>
<h2>环境初始化</h2>
<p>首先,初始化项目</p>
<pre><code class="bash">express -e</code></pre>
<p>然后,安装依赖。</p>
<pre><code class="bash">npm install</code></pre>
<p>接着,安装session相关的包。</p>
<pre><code class="bash">npm install --save express-session session-file-store</code></pre>
<h2>session相关配置</h2>
<p>配置如下,并不复杂,可以见代码注释,或者参考<a href="https://link.segmentfault.com/?enc=Y0bev4umCyJkfQ2ql%2FYeqQ%3D%3D.NELINv6ccbvzM3dw815ki2uN4T1M1d%2B%2BbISmF5DxpjXW7ZaCZMXpNgsBndiX6u9t" rel="nofollow">官方文档</a>。</p>
<pre><code class="js">var express = require('express');
var app = express();
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var identityKey = 'skey';
app.use(session({
name: identityKey,
secret: 'chyingp', // 用来对session id相关的cookie进行签名
store: new FileStore(), // 本地存储session(文本文件,也可以选择其他store,比如redis的)
saveUninitialized: false, // 是否自动保存未初始化的会话,建议false
resave: false, // 是否每次都重新保存会话,建议false
cookie: {
maxAge: 10 * 1000 // 有效期,单位是毫秒
}
}));</code></pre>
<h2>实现登录/登出接口</h2>
<h3>创建测试账户数据</h3>
<p>首先,在本地创建个文件,来保存可用于登录的账户信息,避免创建链接数据库的繁琐。</p>
<pre><code class="js">// users.js
module.exports = {
items: [
{name: 'chyingp', password: '123456'}
]
};</code></pre>
<h3>登录、登出接口实现</h3>
<p>实现登录、登出接口,其中:</p>
<ul>
<li><p>登录:如果用户存在,则通过<code>req.regenerate</code>创建session,保存到本地,并通过<code>Set-Cookie</code>将session id保存到用户侧;</p></li>
<li><p>登出:销毁session,并清除cookie;</p></li>
</ul>
<pre><code class="js">var users = require('./users').items;
var findUser = function(name, password){
return users.find(function(item){
return item.name === name && item.password === password;
});
};
// 登录接口
app.post('/login', function(req, res, next){
var sess = req.session;
var user = findUser(req.body.name, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.name;
res.json({ret_code: 0, ret_msg: '登录成功'});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
// 退出登录
app.get('/logout', function(req, res, next){
// 备注:这里用的 session-file-store 在destroy 方法里,并没有销毁cookie
// 所以客户端的 cookie 还是存在,导致的问题 --> 退出登陆后,服务端检测到cookie
// 然后去查找对应的 session 文件,报错
// session-file-store 本身的bug
req.session.destroy(function(err) {
if(err){
res.json({ret_code: 2, ret_msg: '退出登录失败'});
return;
}
// req.session.loginUser = null;
res.clearCookie(identityKey);
res.redirect('/');
});
});</code></pre>
<h3>登录态判断</h3>
<p>用户访问 <a href="https://link.segmentfault.com/?enc=xhQAszm%2Fc1OFeNMkoHmpZQ%3D%3D.LsAZG7tAxitOpTkOqWypmmc7T3Bf%2F7iCxXlhxTtxNpg%3D" rel="nofollow">http://127.0.0.1:3000</a> 时,判断用户是否登录,如果是,则调到用户详情界面(简陋无比);如果没有登录,则跳到登录界面;</p>
<pre><code class="js">app.get('/', function(req, res, next){
var sess = req.session;
var loginUser = sess.loginUser;
var isLogined = !!loginUser;
res.render('index', {
isLogined: isLogined,
name: loginUser || ''
});
});
</code></pre>
<h3>UI界面</h3>
<p>最后,看下登录、登出UI相关的代码。</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>会话管理</title>
</head>
<body>
<h1>会话管理</h1>
<% if(isLogined){ %>
<p>当前登录用户:<%= name %>,<a href="/logout" id="logout">退出登陆</a></p>
<% }else{ %>
<form method="POST" action="/login">
<input type="text" id="name" name="name" value="chyingp" />
<input type="password" id="password" name="password" value="123456" />
<input type="submit" value="登录" id="login" />
</form>
<% } %>
<script type="text/javascript" src="/jquery-3.1.0.min.js"></script>
<script type="text/javascript">
$('#login').click(function(evt){
evt.preventDefault();
$.ajax({
url: '/login',
type: 'POST',
data: {
name: $('#name').val(),
password: $('#password').val()
},
success: function(data){
if(data.ret_code === 0){
location.reload();
}
}
});
});
</script>
</body>
</html></code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=vvfGD0NENVn6%2BsO79aq38Q%3D%3D.J77yi0rioXI94TMHkYXBfST2D5c27ZDfb4v7mnYpbOStNdFQwilfOmrRQXefh0d9" rel="nofollow">https://github.com/expressjs/...</a></p>
Nodejs进阶:MD5入门介绍及crypto模块的应用
https://segmentfault.com/a/1190000009163658
2017-04-23T11:21:11+08:00
2017-04-23T11:21:11+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=7KyG7wRIgHxEKyv8nFXzWA%3D%3D.sWzbq8cgFF1koaghE4Jjx0AicgWanryTG1rx8VJVC5G6ndCoLf6xndRkbu4qRquwzFrYYCXixwKhURq%2BVKcRgw%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=YdiynW0QB%2B%2F03Oe3KIFytQ%3D%3D.9KQtxOXEhfDG%2BRlmGr4NybvW%2BtaOwy6GoRFN49wRGI7IhvAC6ZXF1Ycn161lvbCD3QpnbNBk8lM1OcRenkqJL%2FfVEKsRggwbV0NKgG1Wd%2Bxc6F1H%2FzrGXZZmcxhp7xb2Gz28Vqyc7zd8oFeljHPZpg%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>简介</h2>
<p>MD5(Message-Digest Algorithm)是计算机安全领域广泛使用的散列函数(又称哈希算法、摘要算法),主要用来确保消息的完整和一致性。常见的应用场景有密码保护、下载文件校验等。</p>
<p>本文先对MD5的特点与应用进行简要概述,接着重点介绍MD5在密码保护场景下的应用,最后通过例子对MD5碰撞进行简单介绍。</p>
<h2>特点</h2>
<ol>
<li><p>运算速度快:对<code>jquery.js</code>求md5值,57254个字符,耗时1.907ms</p></li>
<li><p>输出长度固定:输入长度不固定,输出长度固定(128位)。</p></li>
<li><p>运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。</p></li>
<li><p>高度离散:输入的微小变化,可导致运算结果差异巨大。</p></li>
<li><p>弱碰撞性:不同输入的散列值可能相同。</p></li>
</ol>
<h2>应用场景</h2>
<ol>
<li><p>文件完整性校验:比如从网上下载一个软件,一般网站都会将软件的md5值附在网页上,用户下载完软件后,可对下载到本地的软件进行md5运算,然后跟网站上的md5值进行对比,确保下载的软件是完整的(或正确的)</p></li>
<li><p>密码保护:将md5后的密码保存到数据库,而不是保存明文密码,避免拖库等事件发生后,明文密码外泄。</p></li>
<li><p>防篡改:比如数字证书的防篡改,就用到了摘要算法。(当然还要结合数字签名等手段)</p></li>
</ol>
<h2>nodejs中md5运算的例子</h2>
<p>在nodejs中,<code>crypto</code>模块封装了一系列密码学相关的功能,包括摘要运算。基础例子如下,非常简单:</p>
<pre><code class="js">var crypto = require('crypto');
var md5 = crypto.createHash('md5');
var result = md5.update('a').digest('hex');
// 输出:0cc175b9c0f1b6a831c399e269772661
console.log(result);</code></pre>
<h2>例子:密码保护</h2>
<p>前面提到,将明文密码保存到数据库是很不安全的,最不济也要进行md5后进行保存。比如用户密码是<code>123456</code>,md5运行后,得到<code>输出:e10adc3949ba59abbe56e057f20f883e</code>。</p>
<p>这样至少有两个好处:</p>
<ol>
<li><p>防内部攻击:网站主人也不知道用户的明文密码,避免网站主人拿着用户明文密码干坏事。</p></li>
<li><p>防外部攻击:如网站被黑客入侵,黑客也只能拿到md5后的密码,而不是用户的明文密码。</p></li>
</ol>
<p>示例代码如下:</p>
<pre><code class="javascript">var crypto = require('crypto');
function cryptPwd(password) {
var md5 = crypto.createHash('md5');
return md5.update(password).digest('hex');
}
var password = '123456';
var cryptedPassword = cryptPwd(password);
console.log(cryptedPassword);
// 输出:e10adc3949ba59abbe56e057f20f883e</code></pre>
<h3>单纯对密码进行md5不安全</h3>
<p>前面提到,通过对用户密码进行md5运算来提高安全性。但实际上,这样的安全性是很差的,为什么呢?</p>
<p>稍微修改下上面的例子,可能你就明白了。相同的明文密码,md5值也是相同的。</p>
<pre><code class="javascript">var crypto = require('crypto');
function cryptPwd(password) {
var md5 = crypto.createHash('md5');
return md5.update(password).digest('hex');
}
var password = '123456';
console.log( cryptPwd(password) );
// 输出:e10adc3949ba59abbe56e057f20f883e
console.log( cryptPwd(password) );
// 输出:e10adc3949ba59abbe56e057f20f883e</code></pre>
<p>也就是说,当攻击者知道算法是md5,且数据库里存储的密码值为<code>e10adc3949ba59abbe56e057f20f883e</code>时,理论上可以可以猜到,用户的明文密码就是<code>123456</code>。</p>
<p>事实上,彩虹表就是这么进行暴力破解的:事先将常见明文密码的md5值运算好存起来,然后跟网站数据库里存储的密码进行匹配,就能够快速找到用户的明文密码。(这里不探究具体细节)</p>
<p>那么,有什么办法可以进一步提升安全性呢?答案是:密码加盐。</p>
<h2>密码加盐</h2>
<p>“加盐”这个词看上去很玄乎,其实原理很简单,就是在密码特定位置插入特定字符串后,再对修改后的字符串进行md5运算。</p>
<p>例子如下。同样的密码,当“盐”值不一样时,md5值的差异非常大。通过密码加盐,可以防止最初级的暴力破解,如果攻击者事先不知道”盐“值,破解的难度就会非常大。</p>
<pre><code class="javascript">var crypto = require('crypto');
function cryptPwd(password, salt) {
// 密码“加盐”
var saltPassword = password + ':' + salt;
console.log('原始密码:%s', password);
console.log('加盐后的密码:%s', saltPassword);
// 加盐密码的md5值
var md5 = crypto.createHash('md5');
var result = md5.update(saltPassword).digest('hex');
console.log('加盐密码的md5值:%s', result);
}
cryptPwd('123456', 'abc');
// 输出:
// 原始密码:123456
// 加盐后的密码:123456:abc
// 加盐密码的md5值:51011af1892f59e74baf61f3d4389092
cryptPwd('123456', 'bcd');
// 输出:
// 原始密码:123456
// 加盐后的密码:123456:bcd
// 加盐密码的md5值:55a95bcb6bfbaef6906dbbd264ab4531</code></pre>
<h2>密码加盐:随机盐值</h2>
<p>通过密码加盐,密码的安全性已经提高了不少。但其实上面的例子存在不少问题。</p>
<p>假设字符串拼接算法、盐值已外泄,上面的代码至少存在下面问题:</p>
<ol>
<li><p>短盐值:需要穷举的可能性较少,容易暴力破解,一般采用长盐值来解决。</p></li>
<li><p>盐值固定:类似的,攻击者只需要把常用密码+盐值的hash值表算出来,就完事大吉了。</p></li>
</ol>
<p>短盐值自不必说,应该避免。对于为什么不应该使用固定盐值,这里需要多解释一下。很多时候,我们的盐值是硬编码到我们的代码里的(比如配置文件),一旦坏人通过某种手段获知了盐值,那么,只需要针对这串固定的盐值进行暴力穷举就行了。</p>
<p>比如上面的代码,当你知道盐值是<code>abc</code>时,立刻就能猜到<code>51011af1892f59e74baf61f3d4389092</code>对应的明文密码是<code>123456</code>。</p>
<p>那么,该怎么优化呢?答案是:随机盐值。</p>
<p>示例代码如下。可以看到,密码同样是123456,由于采用了随机盐值,前后运算得出的结果是不同的。这样带来的好处是,多个用户,同样的密码,攻击者需要进行多次运算才能够完全破解。同样是纯数字3位短盐值,随机盐值破解所需的运算量,是固定盐值的1000倍。</p>
<pre><code class="javascript">var crypto = require('crypto');
function getRandomSalt(){
return Math.random().toString().slice(2, 5);
}
function cryptPwd(password, salt) {
// 密码“加盐”
var saltPassword = password + ':' + salt;
console.log('原始密码:%s', password);
console.log('加盐后的密码:%s', saltPassword);
// 加盐密码的md5值
var md5 = crypto.createHash('md5');
var result = md5.update(saltPassword).digest('hex');
console.log('加盐密码的md5值:%s', result);
}
var password = '123456';
cryptPwd('123456', getRandomSalt());
// 输出:
// 原始密码:123456
// 加盐后的密码:123456:498
// 加盐密码的md5值:af3b7d32cc2a254a6bf1ebdcfd700115
cryptPwd('123456', getRandomSalt());
// 输出:
// 原始密码:123456
// 加盐后的密码:123456:287
// 加盐密码的md5值:65d7dd044c2db64c5e658d947578d759</code></pre>
<h2>MD5碰撞</h2>
<p>简单的说,就是两段不同的字符串,经过MD5运算后,得出相同的结果。</p>
<p>网上有不少例子,这里就不赘述,直接上例子,参考(这里)[<a href="https://link.segmentfault.com/?enc=xDNYyIwUast5erVPyZF3AQ%3D%3D.vJpOpNjC4dzWkxj%2BL62tBKCS85WGZvjcZwUMBtXMcMmvHVlx23cgO5KK6XrrWDaYZNzrJnxWZcQwEcF48Ljzew%3D%3D" rel="nofollow">http://www.mscs.dal.ca/~selin...</a></p>
<pre><code class="javascript">function getHashResult(hexString){
// 转成16进制,比如 0x4d 0xc9 ...
hexString = hexString.replace(/(\w{2,2})/g, '0x$1 ').trim();
// 转成16进制数组,如 [0x4d, 0xc9, ...]
var arr = hexString.split(' ');
// 转成对应的buffer,如:<Buffer 4d c9 ...>
var buff = Buffer.from(arr);
var crypto = require('crypto');
var hash = crypto.createHash('md5');
// 计算md5值
var result = hash.update(buff).digest('hex');
return result;
}
var str1 = 'd131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70';
var str2 = 'd131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70';
var result1 = getHashResult(str1);
var result2 = getHashResult(str2);
if(result1 === result2) {
console.log(`Got the same md5 result: ${result1}`);
}else{
console.log(`Not the same md5 result`);
}</code></pre>
<h2>写在后面</h2>
<p>如有错漏,敬请指出,欢迎多交流 :)</p>
<h2>相关链接</h2>
<p>MD5碰撞的一些例子<br><a href="https://link.segmentfault.com/?enc=zdlds4BmrRgBfOol%2FKDgzw%3D%3D.mJnNNdQKkb7vmvpPrSgsdapAy%2Fx4cUrreoccDRDxLHTNjm%2FyGLzdXr0SkVUE1yvs" rel="nofollow">http://www.jianshu.com/p/c908...</a></p>
<p>MD5 Collision Demo<br><a href="https://link.segmentfault.com/?enc=R8gl8CtW6n%2FZnuJFk3baOA%3D%3D.mSLVrgdUTuzsr4oNnRqjJu%2FghQFBXXhKpqI3en5c7sgwRLkqu2sZyuylT561t5iU" rel="nofollow">http://www.mscs.dal.ca/~selin...</a></p>
<p>Free Password Hash Cracker<br><a href="https://link.segmentfault.com/?enc=YgB5QNJAmqmr35Mr3zuNuw%3D%3D.65qekWNFSHqcXjvjyiegXxig24Gt15fTSCRapsEITtY%3D" rel="nofollow">https://crackstation.net/</a></p>
Node 进阶:express 默认日志组件 morgan 从入门使用到源码剖析
https://segmentfault.com/a/1190000007769095
2016-12-12T10:03:14+08:00
2016-12-12T10:03:14+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
12
<blockquote><p>本文摘录自个人总结《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=8AadXcEC0skMJxGxdU%2BCeQ%3D%3D.0hLh2gfEGwClAk%2FYm82uc4TJJSZlpg2xnkV4VoL0aEz%2Bhgd3CdxSqF8zAStNP%2BzvpG9zw8y5YNebHMBb1zu6OA%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=jzs4qZNs7ILMcoktj%2BUZag%3D%3D.y%2BDApWfVFVBPbwVNqd53c0K4%2FDxFZ1PGU2x%2Bd7nUKxrZDzIVGL8zJbq%2BRA7WiHbpX9eE4S2cEIo7epbzwZG1S8m2PQPRN%2Bs1wM4TwxKtz5NNiVDmvV%2BCyCFL3p%2BGtXfWbQx%2FamYPnTcRgzjfDWRbKg%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>章节概览</h2>
<p>morgan是express默认的日志中间件,也可以脱离express,作为node.js的日志组件单独使用。本文由浅入深,内容主要包括:</p>
<ul>
<li><p>morgan使用入门例子</p></li>
<li><p>如何将日志保存到本地文件</p></li>
<li><p>核心API使用说明及例子</p></li>
<li><p>进阶使用:1、日志分割 2、将日志写入数据库</p></li>
<li><p>源码剖析:morgan的日志格式以及预编译</p></li>
</ul>
<h2>入门例子</h2>
<p>首先,初始化项目。</p>
<pre><code class="bash">npm install express morgan</code></pre>
<p>然后,在<code>basic.js</code>中添加如下代码。</p>
<pre><code class="js">var express = require('express');
var app = express();
var morgan = require('morgan');
app.use(morgan('short'));
app.use(function(req, res, next){
res.send('ok');
});
app.listen(3000);</code></pre>
<p><code>node basic.js</code>运行程序,并在浏览器里访问 <a href="https://link.segmentfault.com/?enc=RnnYJvN%2FUgOejxgb53Ou%2Fg%3D%3D.NkfBS%2F28TWhBoKPkwGJIzWDtc%2BX2dFs4e6lYvM02bnc%3D" rel="nofollow">http://127.0.0.1:3000</a> ,打印日志如下</p>
<pre><code class="bash">➜ 2016.12.11-advanced-morgan git:(master) ✗ node basic.js
::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 3.019 ms
::ffff:127.0.0.1 - GET /favicon.ico HTTP/1.1 200 2 - 0.984 ms</code></pre>
<h2>将日志打印到本地文件</h2>
<p>morgan支持stream配置项,可以通过它来实现将日志落地的效果,代码如下:</p>
<pre><code class="js">var express = require('express');
var app = express();
var morgan = require('morgan');
var fs = require('fs');
var path = require('path');
var accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), {flags: 'a'});
app.use(morgan('short', {stream: accessLogStream}));
app.use(function(req, res, next){
res.send('ok');
});
app.listen(3000);</code></pre>
<h2>使用讲解</h2>
<h3>核心API</h3>
<p>morgan的API非常少,使用频率最高的就是<code>morgan()</code>,作用是返回一个express日志中间件。</p>
<pre><code class="js">morgan(format, options)</code></pre>
<p>参数说明如下:</p>
<ul>
<li><p>format:可选,morgan与定义了几种日志格式,每种格式都有对应的名称,比如<code>combined</code>、<code>short</code>等,默认是<code>default</code>。不同格式的差别可参考<a href="https://link.segmentfault.com/?enc=%2BeaBx3UJ8dPZ4HssA5WpEw%3D%3D.MJ92z4K%2Fv7sbRFhwRZDQA5d6x2Nk8LTDeH4bmEASLEGdrhLN4kVZ6nlfUqiwEsgXj0pq2TvWSQ7jnVrvSIJpiQ%3D%3D" rel="nofollow">这里</a>。下文会讲解下,如果自定义日志格式。</p></li>
<li>
<p>options:可选,配置项,包含<code>stream(常用)</code>、<code>skip</code>、<code>immediate</code>。</p>
<ul>
<li><p>stream:日志的输出流配置,默认是<code>process.stdout</code>。</p></li>
<li><p>skip:是否跳过日志记录,使用方式可以参考<a href="https://link.segmentfault.com/?enc=uw2FZdyzehhwF5iKH%2FOqYg%3D%3D.juI4nqIDQQL8BP8Al4PUwOVX%2BlnOR71mMtHNti2WpyMRVCItV%2F%2FwuagWGTdSUs7U" rel="nofollow">这里</a>。</p></li>
<li><p>immediate:布尔值,默认是false。当为true时,一收到请求,就记录日志;如果为false,则在请求返回后,再记录日志。</p></li>
</ul>
</li>
</ul>
<h3>自定义日志格式</h3>
<p>首先搞清楚morgan中的两个概念:format 跟 token。非常简单:</p>
<ul>
<li><p>format:日志格式,本质是代表日志格式的字符串,比如 <code>:method :url :status :res[content-length] - :response-time ms</code>。</p></li>
<li><p>token:format的组成部分,比如上面的<code>:method</code>、<code>:url</code>即使所谓的token。</p></li>
</ul>
<p>搞清楚format、token的区别后,就可以看下morgan中,关于自定义日志格式的关键API。</p>
<pre><code class="js">morgan.format(name, format); // 自定义日志格式
morgan.token(name, fn); // 自定义token</code></pre>
<h2>自定义format</h2>
<p>非常简单,首先通过<code>morgan.format()</code>定义名为<code>joke</code>的日志格式,然后通过<code>morgan('joke')</code>调用即可。</p>
<pre><code class="js">var express = require('express');
var app = express();
var morgan = require('morgan');
morgan.format('joke', '[joke] :method :url :status');
app.use(morgan('joke'));
app.use(function(req, res, next){
res.send('ok');
});
app.listen(3000);</code></pre>
<p>我们来看下运行结果</p>
<pre><code class="bash">➜ 2016.12.11-advanced-morgan git:(master) ✗ node morgan.format.js
[joke] GET / 304
[joke] GET /favicon.ico 200</code></pre>
<h2>自定义token</h2>
<p>代码如下,通过<code>morgan.token()</code>自定义token,然后将自定义的token,加入自定义的format中即可。</p>
<pre><code class="js">var express = require('express');
var app = express();
var morgan = require('morgan');
// 自定义token
morgan.token('from', function(req, res){
return req.query.from || '-';
});
// 自定义format,其中包含自定义的token
morgan.format('joke', '[joke] :method :url :status :from');
// 使用自定义的format
app.use(morgan('joke'));
app.use(function(req, res, next){
res.send('ok');
});
app.listen(3000);</code></pre>
<p>运行程序,并在浏览器里先后访问 <a href="https://link.segmentfault.com/?enc=se3Mo8%2BoFZanyrkivWYZWg%3D%3D.RMqZ1Y%2BBwxO3iKgVtXvT%2FtkiYN7yBQFnCOZaEugEP2A0y%2BN1v%2FIPRb4ii%2BIeR3F1" rel="nofollow">http://127.0.0.1:3000/hello?f...</a> 和 <a href="https://link.segmentfault.com/?enc=XyL%2BGLGJNqS7JVS2CUpMJA%3D%3D.8ea5cUi%2FnLlDM9B1SaaVa67y%2BU%2FQhg8fz4MphyvSei41P5DFJH8XstbbOFkku9RH" rel="nofollow">http://127.0.0.1:3000/hello?f...</a></p>
<pre><code class="bash">➜ 2016.12.11-advanced-morgan git:(master) ✗ node morgan.token.js
[joke] GET /hello?from=app 200 app
[joke] GET /favicon.ico 304 -
[joke] GET /hello?from=pc 200 pc
[joke] GET /favicon.ico 304 -</code></pre>
<h2>高级使用</h2>
<h3>日志切割</h3>
<p>一个线上应用,如果所有的日志都落地到同一个本地文件,时间久了,文件会变得非常大,既影响性能,又不便于查看。这时候,就需要用到日志分割了。</p>
<p>借助<code>file-stream-rotator</code>插件,可以轻松完成日志分割的工作。除了<code>file-stream-rotator</code>相关的配置代码,其余跟之前的例子差不多,这里不赘述。</p>
<pre><code class="js">var FileStreamRotator = require('file-stream-rotator')
var express = require('express')
var fs = require('fs')
var morgan = require('morgan')
var path = require('path')
var app = express()
var logDirectory = path.join(__dirname, 'log')
// ensure log directory exists
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory)
// create a rotating write stream
var accessLogStream = FileStreamRotator.getStream({
date_format: 'YYYYMMDD',
filename: path.join(logDirectory, 'access-%DATE%.log'),
frequency: 'daily',
verbose: false
})
// setup the logger
app.use(morgan('combined', {stream: accessLogStream}))
app.get('/', function (req, res) {
res.send('hello, world!')
})
</code></pre>
<h3>日志写入数据库</h3>
<p>有的时候,我们会有这样的需求,将访问日志写入数据库。这种需求常见于需要实时查询统计的日志系统。</p>
<p>在morgan里该如何实现呢?从文档上,并没有看到适合的扩展接口。于是查阅了下<code>morgan</code>的源码,发现实现起来非常简单。</p>
<p>回顾下之前日志写入本地文件的例子,最关键的两行代码如下。通过<code>stream</code>指定日志的输出流。</p>
<pre><code class="js">var accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), {flags: 'a'});
app.use(morgan('short', {stream: accessLogStream}));</code></pre>
<p>在<code>morgan</code>内部,大致实现是这样的(简化后)。</p>
<pre><code class="js">// opt为配置文件
var stream = opts.stream || process.stdout;
var logString = createLogString(); // 伪代码,根据format、token的定义,生成日志
stream.write(logString);</code></pre>
<p>于是,可以用比较取巧的方式来实现目的:声明一个带<code>write</code>方法的对象,并作为<code>stream</code>配置传入。</p>
<pre><code class="js">var express = require('express');
var app = express();
var morgan = require('morgan');
// 带write方法的对象
var dbStream = {
write: function(line){
saveToDatabase(line); // 伪代码,保存到数据库
}
};
// 将 dbStream 作为 stream 配置项的值
app.use(morgan('short', {stream: dbStream}));
app.use(function(req, res, next){
res.send('ok');
});
app.listen(3000);</code></pre>
<h2>深入剖析</h2>
<p>morgan的代码非常简洁,从设计上来说,morgan的生命周期包含:</p>
<blockquote><p>token定义 --> 日志格式定义 -> 日志格式预编译 --> 请求达到/返回 --> 写日志</p></blockquote>
<p>其中,token定义、日志格式定义前面已经讲到,这里就只讲下 <strong>日志格式预编译</strong> 的细节。</p>
<p>跟模板引擎预编译一样,日志格式预编译,也是为了提升性能。源码如下,最关键的代码就是<code>compile(fmt)</code>。</p>
<pre><code class="js">function getFormatFunction (name) {
// lookup format
var fmt = morgan[name] || name || morgan.default
// return compiled format
return typeof fmt !== 'function'
? compile(fmt)
: fmt
}</code></pre>
<p><code>compile()</code>方法的实现细节这里不赘述,着重看下<code>compile(fmt)</code>返回的内容:</p>
<pre><code class="js">var morgan = require('morgan');
var format = morgan['tiny'];
var fn = morgan.compile(format);
console.log(fn.toString());</code></pre>
<p>运行上面程序,输出内容如下,其中<code>tokens</code>其实就是<code>morgan</code>。</p>
<pre><code class="bash">function anonymous(tokens, req, res
/**/) {
return ""
+ (tokens["method"](req, res, undefined) || "-") + " "
+ (tokens["url"](req, res, undefined) || "-") + " "
+ (tokens["status"](req, res, undefined) || "-") + " "
+ (tokens["res"](req, res, "content-length") || "-") + " - "
+ (tokens["response-time"](req, res, undefined) || "-") + " ms";
}</code></pre>
<p>看下<code>morgan.token()</code>的定义,就很清晰了</p>
<pre><code class="js">function token (name, fn) {
morgan[name] = fn
return this
}</code></pre>
<h2>相关链接</h2>
<p>《Nodejs学习笔记》:<a href="https://link.segmentfault.com/?enc=6YQVC8mZyYxv0o05qf2%2BPQ%3D%3D.kSxheTKRi2ABMk%2FloGs7bC3Ai5KQD9%2BeNHHCUsmtBKwIxd18pCMNzklkkWYkFMjTwH0a85EHstyrUPDs6wZeaQ%3D%3D" rel="nofollow">https://github.com/chyingp/no...</a><br>官方文档:<a href="https://link.segmentfault.com/?enc=beJ4blSyN4Gin3EXiRgbmw%3D%3D.jCart3DLucj%2FKMO6r9NczjEC1t3mx3PLtAFSfuKoArONhqCYkVnCaOfIedBduRJ4" rel="nofollow">https://github.com/expressjs/...</a></p>
Nodejs进阶:如何玩转子进程(child_process)
https://segmentfault.com/a/1190000007735211
2016-12-08T11:36:27+08:00
2016-12-08T11:36:27+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
26
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=3Fs3CWYIBVxV6lJu%2BAi5aQ%3D%3D.SQwoOh0%2BYC%2FWALHrrjuerA4jlrAaVWiGC%2Fszw0gHrbFWEIN8GOUxkE4WhdbUzdDoU%2B6xluutgEwh3dPt4lLoKg%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=5kGzrigUxyvftEAMSF%2BDew%3D%3D.WFmytmVZ%2B0gWQc%2BgLJa%2B409m%2FLUw3982dnKr1QAMxSdaMzQFSkLq86vCLfjkTn2jfXb0JpIKcV74EUaWm4PiWlGvALPc%2F7%2Br4Spgqw1i%2Fd0dqgutPTo7spNamEBG%2BgZnymqGadd%2Bbs4pF0iN4wK%2B%2Fg%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块概览</h2>
<p>在node中,child_process这个模块非常重要。掌握了它,等于在node的世界开启了一扇新的大门。熟悉shell脚本的同学,可以用它来完成很多有意思的事情,比如文件压缩、增量部署等,感兴趣的同学,看文本文后可以尝试下。</p>
<p>举个简单的例子:</p>
<pre><code class="javascript">const spawn = require('child_process').spawn;
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});</code></pre>
<h2>几种创建子进程的方式</h2>
<p>注意事项:</p>
<ul>
<li><p>下面列出来的都是异步创建子进程的方式,每一种方式都有对应的同步版本。</p></li>
<li><p><code>.exec()</code>、<code>.execFile()</code>、<code>.fork()</code>底层都是通过<code>.spawn()</code>实现的。</p></li>
<li><p><code>.exec()</code>、<code>execFile()</code>额外提供了回调,当子进程停止的时候执行。</p></li>
</ul>
<blockquote><p>child_process.spawn(command, args)<br>child_process.exec(command, options)<br>child_process.execFile(file, args[, callback])<br>child_process.fork(modulePath, args)</p></blockquote>
<h3>child_process.exec(command, options)</h3>
<p>创建一个shell,然后在shell里执行命令。执行完成后,将stdout、stderr作为参数传入回调方法。</p>
<blockquote><p>spawns a shell and runs a command within that shell, passing the stdout and stderr to a callback function when complete.</p></blockquote>
<p>例子如下:</p>
<ol>
<li><p>执行成功,<code>error</code>为<code>null</code>;执行失败,<code>error</code>为<code>Error</code>实例。<code>error.code</code>为错误码,</p></li>
<li><p><code>stdout</code>、<code>stderr</code>为标准输出、标准错误。默认是字符串,除非<code>options.encoding</code>为<code>buffer</code></p></li>
</ol>
<pre><code class="javascript">var exec = require('child_process').exec;
// 成功的例子
exec('ls -al', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + typeof stderr);
});
// 失败的例子
exec('ls hello.txt', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
});</code></pre>
<h4>参数说明:</h4>
<ul>
<li><p><code>cwd</code>:当前工作路径。</p></li>
<li><p><code>env</code>:环境变量。</p></li>
<li><p><code>encoding</code>:编码,默认是<code>utf8</code>。</p></li>
<li><p><code>shell</code>:用来执行命令的shell,unix上默认是<code>/bin/sh</code>,windows上默认是<code>cmd.exe</code>。</p></li>
<li><p><code>timeout</code>:默认是0。</p></li>
<li><p><code>killSignal</code>:默认是<code>SIGTERM</code>。</p></li>
<li><p><code>uid</code>:执行进程的uid。</p></li>
<li><p><code>gid</code>:执行进程的gid。</p></li>
<li><p><code>maxBuffer</code>:<Number> 标准输出、错误输出最大允许的数据量(单位为字节),如果超出的话,子进程就会被杀死。默认是200*1024(就是200k啦)</p></li>
</ul>
<p>备注:</p>
<ol>
<li><p>如果<code>timeout</code>大于0,那么,当子进程运行超过<code>timeout</code>毫秒,那么,就会给进程发送<code>killSignal</code>指定的信号(比如<code>SIGTERM</code>)。</p></li>
<li><p>如果运行没有出错,那么<code>error</code>为<code>null</code>。如果运行出错,那么,<code>error.code</code>就是退出代码(exist code),<code>error.signal</code>会被设置成终止进程的信号。(比如<code>CTRL+C</code>时发送的<code>SIGINT</code>)</p></li>
</ol>
<h4>风险项</h4>
<p>传入的命令,如果是用户输入的,有可能产生类似sql注入的风险,比如</p>
<pre><code>exec('ls hello.txt; rm -rf *', function(error, stdout, stderr){
if(error) {
console.error('error: ' + error);
// return;
}
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
});</code></pre>
<h4>备注事项</h4>
<p>Note: Unlike the exec(3) POSIX system call, child_process.exec() does not replace the existing process and uses a shell to execute the command.</p>
<h3>child_process.execFile(file, args[, callback])</h3>
<p>跟<code>.exec()</code>类似,不同点在于,没有创建一个新的shell。至少有两点影响</p>
<ol>
<li><p>比<code>child_process.exec()</code>效率高一些。(实际待测试)</p></li>
<li><p>一些操作,比如I/O重定向,文件glob等不支持。</p></li>
</ol>
<blockquote><p>similar to child_process.exec() except that it spawns the command directly without first spawning a shell.</p></blockquote>
<p><code>file</code>:<String> 可执行文件的名字,或者路径。</p>
<p>例子:</p>
<pre><code class="javascript">var child_process = require('child_process');
child_process.execFile('node', ['--version'], function(error, stdout, stderr){
if(error){
throw error;
}
console.log(stdout);
});
child_process.execFile('/Users/a/.nvm/versions/node/v6.1.0/bin/node', ['--version'], function(error, stdout, stderr){
if(error){
throw error;
}
console.log(stdout);
});</code></pre>
<p>====== 扩展阅读 =======</p>
<p>从node源码来看,<code>exec()</code>、<code>execFile()</code>最大的差别,就在于是否创建了shell。(execFile()内部,options.shell === false),那么,可以手动设置shell。以下代码差不多是等价的。win下的shell设置有所不同,感兴趣的同学可以自己试验下。</p>
<p>备注:execFile()内部最终还是通过spawn()实现的, 如果没有设置 {shell: '/bin/bash'},那么 spawm() 内部对命令的解析会有所不同,execFile('ls -al .') 会直接报错。</p>
<pre><code class="javascript">var child_process = require('child_process');
var execFile = child_process.execFile;
var exec = child_process.exec;
exec('ls -al .', function(error, stdout, stderr){
if(error){
throw error;
}
console.log(stdout);
});
execFile('ls -al .', {shell: '/bin/bash'}, function(error, stdout, stderr){
if(error){
throw error;
}
console.log(stdout);
});</code></pre>
<h3>child_process.fork(modulePath, args)</h3>
<p><code>modulePath</code>:子进程运行的模块。</p>
<p>参数说明:(重复的参数说明就不在这里列举)</p>
<ul>
<li><p><code>execPath</code>:<String> 用来创建子进程的可执行文件,默认是<code>/usr/local/bin/node</code>。也就是说,你可通过<code>execPath</code>来指定具体的node可执行文件路径。(比如多个node版本)</p></li>
<li><p><code>execArgv</code>:<Array> 传给可执行文件的字符串参数列表。默认是<code>process.execArgv</code>,跟父进程保持一致。</p></li>
<li><p><code>silent</code>:<Boolean> 默认是<code>false</code>,即子进程的<code>stdio</code>从父进程继承。如果是<code>true</code>,则直接<code>pipe</code>向子进程的<code>child.stdin</code>、<code>child.stdout</code>等。</p></li>
<li><p><code>stdio</code>:<Array> 如果声明了<code>stdio</code>,则会覆盖<code>silent</code>选项的设置。</p></li>
</ul>
<p>例子1:silent</p>
<p><strong>parent.js</strong></p>
<pre><code class="javascript">var child_process = require('child_process');
// 例子一:会打印出 output from the child
// 默认情况,silent 为 false,子进程的 stdout 等
// 从父进程继承
child_process.fork('./child.js', {
silent: false
});
// 例子二:不会打印出 output from the silent child
// silent 为 true,子进程的 stdout 等
// pipe 向父进程
child_process.fork('./silentChild.js', {
silent: true
});
// 例子三:打印出 output from another silent child
var child = child_process.fork('./anotherSilentChild.js', {
silent: true
});
child.stdout.setEncoding('utf8');
child.stdout.on('data', function(data){
console.log(data);
});</code></pre>
<p><strong>child.js</strong></p>
<pre><code class="javascript">console.log('output from the child');</code></pre>
<p><strong>silentChild.js</strong></p>
<pre><code class="javascript">console.log('output from the silent child');</code></pre>
<p><strong>anotherSilentChild.js</strong></p>
<pre><code class="javascript">console.log('output from another silent child');</code></pre>
<p>例子二:ipc</p>
<p>parent.js</p>
<pre><code class="javascript">var child_process = require('child_process');
var child = child_process.fork('./child.js');
child.on('message', function(m){
console.log('message from child: ' + JSON.stringify(m));
});
child.send({from: 'parent'});</code></pre>
<pre><code class="javascript">process.on('message', function(m){
console.log('message from parent: ' + JSON.stringify(m));
});
process.send({from: 'child'});</code></pre>
<p>运行结果</p>
<pre><code class="powershell">➜ ipc git:(master) ✗ node parent.js
message from child: {"from":"child"}
message from parent: {"from":"parent"}</code></pre>
<p>例子三:execArgv</p>
<p>首先,process.execArgv的定义,参考<a href="https://link.segmentfault.com/?enc=2pTX4HcyY47zHR5O7VHdgQ%3D%3D.l6pb1RL58q7HU6KyeotNqYFYFQlOMkAU6jg1klmpD6X6rx%2BAos%2BAzNkTF38pmVwp3XYw%2BEK4UI1bT5S1%2BGJqkg%3D%3D" rel="nofollow">这里</a>。设置<code>execArgv</code>的目的一般在于,让子进程跟父进程保持相同的执行环境。</p>
<p>比如,父进程指定了<code>--harmony</code>,如果子进程没有指定,那么就要跪了。</p>
<p>parent.js</p>
<pre><code class="javascript">var child_process = require('child_process');
console.log('parent execArgv: ' + process.execArgv);
child_process.fork('./child.js', {
execArgv: process.execArgv
});</code></pre>
<p>child.js</p>
<pre><code class="javascript">console.log('child execArgv: ' + process.execArgv);</code></pre>
<p>运行结果</p>
<pre><code class="powershell">➜ execArgv git:(master) ✗ node --harmony parent.js
parent execArgv: --harmony
child execArgv: --harmony</code></pre>
<p>例子3:execPath(TODO 待举例子)</p>
<h3>child_process.spawn(command, args)</h3>
<p><code>command</code>:要执行的命令</p>
<p>options参数说明:</p>
<ul>
<li><p><code>argv0</code>:[String] 这货比较诡异,在uninx、windows上表现不一样。有需要再深究。</p></li>
<li><p><code>stdio</code>:[Array] | [String] 子进程的stdio。参考<a href="https://link.segmentfault.com/?enc=anamKm8Xhgr5yS2E2kCt8w%3D%3D.AneTm0UGVNyF3TCTCRLm1kT1%2B13JBkdFu2XwyzccSiHkO5XXG5O%2BrxeFQgeut510qxc7Ii6krHwHPn%2FPg3dub%2F2a8ZygUHbyM06f98s4cyk%3D" rel="nofollow">这里</a></p></li>
<li><p><code>detached</code>:[Boolean] 让子进程独立于父进程之外运行。同样在不同平台上表现有差异,具体参考<a href="https://link.segmentfault.com/?enc=oC6KxHPm5vLDyUc9o67gPA%3D%3D.w1NCpQ49UdEk5x6Pfnn%2BzWGBq3BgSQMfX87ggk04yFbu9RstcgjIitelAjY%2BMFC54jBj3U1TBLBqGAwrMx3iDbTZ%2Bnw6QHCUdTvq7jeAHEU%3D" rel="nofollow">这里</a></p></li>
<li><p><code>shell</code>:[Boolean] | [String] 如果是<code>true</code>,在shell里运行程序。默认是<code>false</code>。(很有用,比如 可以通过 /bin/sh -c xxx 来实现 .exec() 这样的效果)</p></li>
</ul>
<p>例子1:基础例子</p>
<pre><code class="javascript">var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al']);
ls.stdout.on('data', function(data){
console.log('data from child: ' + data);
});
ls.stderr.on('data', function(data){
console.log('error from child: ' + data);
});
ls.on('close', function(code){
console.log('child exists with code: ' + code);
});</code></pre>
<p>例子2:声明stdio</p>
<pre><code class="javascript">var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al'], {
stdio: 'inherit'
});
ls.on('close', function(code){
console.log('child exists with code: ' + code);
});</code></pre>
<p>例子3:声明使用shell</p>
<pre><code class="javascript">var spawn = require('child_process').spawn;
// 运行 echo "hello nodejs" | wc
var ls = spawn('bash', ['-c', 'echo "hello nodejs" | wc'], {
stdio: 'inherit',
shell: true
});
ls.on('close', function(code){
console.log('child exists with code: ' + code);
});</code></pre>
<p>例子4:错误处理,包含两种场景,这两种场景有不同的处理方式。</p>
<ul>
<li><p>场景1:命令本身不存在,创建子进程报错。</p></li>
<li><p>场景2:命令存在,但运行过程报错。</p></li>
</ul>
<pre><code class="javascript">var spawn = require('child_process').spawn;
var child = spawn('bad_command');
child.on('error', (err) => {
console.log('Failed to start child process 1.');
});
var child2 = spawn('ls', ['nonexistFile']);
child2.stderr.on('data', function(data){
console.log('Error msg from process 2: ' + data);
});
child2.on('error', (err) => {
console.log('Failed to start child process 2.');
});</code></pre>
<p>运行结果如下。</p>
<pre><code class="powershell">➜ spawn git:(master) ✗ node error/error.js
Failed to start child process 1.
Error msg from process 2: ls: nonexistFile: No such file or directory</code></pre>
<p>例子5:echo "hello nodejs" | grep "nodejs"</p>
<pre><code class="javascript">// echo "hello nodejs" | grep "nodejs"
var child_process = require('child_process');
var echo = child_process.spawn('echo', ['hello nodejs']);
var grep = child_process.spawn('grep', ['nodejs']);
grep.stdout.setEncoding('utf8');
echo.stdout.on('data', function(data){
grep.stdin.write(data);
});
echo.on('close', function(code){
if(code!==0){
console.log('echo exists with code: ' + code);
}
grep.stdin.end();
});
grep.stdout.on('data', function(data){
console.log('grep: ' + data);
});
grep.on('close', function(code){
if(code!==0){
console.log('grep exists with code: ' + code);
}
});</code></pre>
<p>运行结果:</p>
<pre><code class="powershell">➜ spawn git:(master) ✗ node pipe/pipe.js
grep: hello nodejs</code></pre>
<h2>关于<code>options.stdio</code>
</h2>
<p>默认值:['pipe', 'pipe', 'pipe'],这意味着:</p>
<ol>
<li><p>child.stdin、child.stdout 不是<code>undefined</code></p></li>
<li><p>可以通过监听 <code>data</code> 事件,来获取数据。</p></li>
</ol>
<h3>基础例子</h3>
<pre><code class="javascript">var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al']);
ls.stdout.on('data', function(data){
console.log('data from child: ' + data);
});
ls.on('close', function(code){
console.log('child exists with code: ' + code);
});</code></pre>
<h3>通过child.stdin.write()写入</h3>
<pre><code class="javascript">var spawn = require('child_process').spawn;
var grep = spawn('grep', ['nodejs']);
setTimeout(function(){
grep.stdin.write('hello nodejs \n hello javascript');
grep.stdin.end();
}, 2000);
grep.stdout.on('data', function(data){
console.log('data from grep: ' + data);
});
grep.on('close', function(code){
console.log('grep exists with code: ' + code);
});</code></pre>
<h2>异步 vs 同步</h2>
<p>大部分时候,子进程的创建是异步的。也就是说,它不会阻塞当前的事件循环,这对于性能的提升很有帮助。</p>
<p>当然,有的时候,同步的方式会更方便(阻塞事件循环),比如通过子进程的方式来执行shell脚本时。</p>
<p>node同样提供同步的版本,比如:</p>
<ul>
<li><p>spawnSync()</p></li>
<li><p>execSync()</p></li>
<li><p>execFileSync()</p></li>
</ul>
<h2>关于<code>options.detached</code>
</h2>
<p>由于木有在windows上做测试,于是先贴原文</p>
<blockquote><p>On Windows, setting options.detached to true makes it possible for the child process to continue running after the parent exits. The child will have its own console window. Once enabled for a child process, it cannot be disabled.</p></blockquote>
<p>在非window是平台上的表现</p>
<blockquote><p>On non-Windows platforms, if options.detached is set to true, the child process will be made the leader of a new process group and session. Note that child processes may continue running after the parent exits regardless of whether they are detached or not. See setsid(2) for more information.</p></blockquote>
<h3>默认情况:父进程等待子进程结束。</h3>
<p>子进程。可以看到,有个定时器一直在跑</p>
<pre><code class="javascript">var times = 0;
setInterval(function(){
console.log(++times);
}, 1000);</code></pre>
<p>运行下面代码,会发现父进程一直hold着不退出。</p>
<pre><code>var child_process = require('child_process');
child_process.spawn('node', ['child.js'], {
// stdio: 'inherit'
});</code></pre>
<h3>通过child.unref()让父进程退出</h3>
<p>调用<code>child.unref()</code>,将子进程从父进程的事件循环中剔除。于是父进程可以愉快的退出。这里有几个要点</p>
<ol>
<li><p>调用<code>child.unref()</code></p></li>
<li><p>设置<code>detached</code>为<code>true</code></p></li>
<li><p>设置<code>stdio</code>为<code>ignore</code>(这点容易忘)</p></li>
</ol>
<pre><code class="javascript">var child_process = require('child_process');
var child = child_process.spawn('node', ['child.js'], {
detached: true,
stdio: 'ignore' // 备注:如果不置为 ignore,那么 父进程还是不会退出
// stdio: 'inherit'
});
child.unref();</code></pre>
<h3>将<code>stdio</code>重定向到文件</h3>
<p>除了直接将stdio设置为<code>ignore</code>,还可以将它重定向到本地的文件。</p>
<pre><code class="javascript">var child_process = require('child_process');
var fs = require('fs');
var out = fs.openSync('./out.log', 'a');
var err = fs.openSync('./err.log', 'a');
var child = child_process.spawn('node', ['child.js'], {
detached: true,
stdio: ['ignore', out, err]
});
child.unref();</code></pre>
<h2>exec()与execFile()之间的区别</h2>
<p>首先,exec() 内部调用 execFile() 来实现,而 execFile() 内部调用 spawn() 来实现。</p>
<blockquote><p>exec() -> execFile() -> spawn()</p></blockquote>
<p>其次,execFile() 内部默认将 options.shell 设置为false,exec() 默认不是false。</p>
<h2>Class: ChildProcess</h2>
<ul>
<li><p>通过<code>child_process.spawn()</code>等创建,一般不直接用构造函数创建。</p></li>
<li><p>继承了<code>EventEmitters</code>,所以有<code>.on()</code>等方法。</p></li>
</ul>
<h3>各种事件</h3>
<h3>close</h3>
<p>当stdio流关闭时触发。这个事件跟<code>exit</code>不同,因为多个进程可以共享同个stdio流。 <br>参数:code(退出码,如果子进程是自己退出的话),signal(结束子进程的信号)<br>问题:code一定是有的吗?(从对code的注解来看好像不是)比如用<code>kill</code>杀死子进程,那么,code是?</p>
<h3>exit</h3>
<p>参数:code、signal,如果子进程是自己退出的,那么<code>code</code>就是退出码,否则为null;如果子进程是通过信号结束的,那么,<code>signal</code>就是结束进程的信号,否则为null。这两者中,一者肯定不为null。<br>注意事项:<code>exit</code>事件触发时,子进程的stdio stream可能还打开着。(场景?)此外,nodejs监听了SIGINT和SIGTERM信号,也就是说,nodejs收到这两个信号时,不会立刻退出,而是先做一些清理的工作,然后重新抛出这两个信号。(目测此时js可以做清理工作了,比如关闭数据库等。)</p>
<p>SIGINT:interrupt,程序终止信号,通常在用户按下CTRL+C时发出,用来通知前台进程终止进程。<br>SIGTERM:terminate,程序结束信号,该信号可以被阻塞和处理,通常用来要求程序自己正常退出。shell命令kill缺省产生这个信号。如果信号终止不了,我们才会尝试SIGKILL(强制终止)。</p>
<blockquote><p>Also, note that Node.js establishes signal handlers for SIGINT and SIGTERM and Node.js processes will not terminate immediately due to receipt of those signals. Rather, Node.js will perform a sequence of cleanup actions and then will re-raise the handled signal.</p></blockquote>
<h3>error</h3>
<p>当发生下列事情时,error就会被触发。当error触发时,exit可能触发,也可能不触发。(内心是崩溃的)</p>
<ul>
<li><p>无法创建子进程。</p></li>
<li><p>进程无法kill。(TODO 举例子)</p></li>
<li><p>向子进程发送消息失败。(TODO 举例子)</p></li>
</ul>
<h3>message</h3>
<p>当采用<code>process.send()</code>来发送消息时触发。<br>参数:<code>message</code>,为json对象,或者primitive value;<code>sendHandle</code>,net.Socket对象,或者net.Server对象(熟悉cluster的同学应该对这个不陌生)</p>
<p><strong>.connected</strong>:当调用<code>.disconnected()</code>时,设为false。代表是否能够从子进程接收消息,或者对子进程发送消息。</p>
<p><strong>.disconnect()</strong>:关闭父进程、子进程之间的IPC通道。当这个方法被调用时,<code>disconnect</code>事件就会触发。如果子进程是node实例(通过child_process.fork()创建),那么在子进程内部也可以主动调用<code>process.disconnect()</code>来终止IPC通道。参考<a href="https://link.segmentfault.com/?enc=iC0HF8LC8kGbJh2yL2t2MA%3D%3D.LUVjJ9SQzCNTRIjBz498YARCpLGANhIjsTFH9y5tOxy%2Fkz4dLWYazYzbV64l6y%2FTmCWkKcn6H1TjW8a2HGhPzA%3D%3D" rel="nofollow">process.disconnect</a>。</p>
<h2>非重要的备忘点</h2>
<h3>windows平台上的<code>cmd</code>、<code>bat</code>
</h3>
<blockquote><p>The importance of the distinction between child_process.exec() and child_process.execFile() can vary based on platform. On Unix-type operating systems (Unix, Linux, OSX) child_process.execFile() can be more efficient because it does not spawn a shell. On Windows, however, .bat and .cmd files are not executable on their own without a terminal, and therefore cannot be launched using child_process.execFile(). When running on Windows, .bat and .cmd files can be invoked using child_process.spawn() with the shell option set, with child_process.exec(), or by spawning cmd.exe and passing the .bat or .cmd file as an argument (which is what the shell option and child_process.exec() do).</p></blockquote>
<pre><code class="javascript">// On Windows Only ...
const spawn = require('child_process').spawn;
const bat = spawn('cmd.exe', ['/c', 'my.bat']);
bat.stdout.on('data', (data) => {
console.log(data);
});
bat.stderr.on('data', (data) => {
console.log(data);
});
bat.on('exit', (code) => {
console.log(`Child exited with code ${code}`);
});
// OR...
const exec = require('child_process').exec;
exec('my.bat', (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
});</code></pre>
<h3>进程标题</h3>
<p>Note: Certain platforms (OS X, Linux) will use the value of argv[0] for the process title while others (Windows, SunOS) will use command.</p>
<p>Note: Node.js currently overwrites argv[0] with process.execPath on startup, so process.argv[0] in a Node.js child process will not match the argv0 parameter passed to spawn from the parent, retrieve it with the process.argv0 property instead.</p>
<h3>代码运行次序的问题</h3>
<p><strong>p.js</strong></p>
<pre><code class="javascript">const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
console.log('1');
n.on('message', (m) => {
console.log('PARENT got message:', m);
});
console.log('2');
n.send({ hello: 'world' });
console.log('3');</code></pre>
<p><strong>sub.js</strong></p>
<pre><code class="javascript">console.log('4');
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
console.log('5');</code></pre>
<p>运行<code>node p.js</code>,打印出来的内容如下</p>
<pre><code class="powershell">➜ ch node p.js
1
2
3
4
5
PARENT got message: { foo: 'bar' }
CHILD got message: { hello: 'world' }</code></pre>
<p>再来个例子</p>
<pre><code class="javascript">// p2.js
var fork = require('child_process').fork;
console.log('p: 1');
fork('./c2.js');
console.log('p: 2');
// 从测试结果来看,同样是70ms,有的时候,定时器回调比子进程先执行,有的时候比子进程慢执行。
const t = 70;
setTimeout(function(){
console.log('p: 3 in %s', t);
}, t);
// c2.js
console.log('c: 1');</code></pre>
<h3>关于NODE_CHANNEL_FD</h3>
<p>child_process.fork()时,如果指定了execPath,那么父、子进程间通过NODE_CHANNEL_FD 进行通信。</p>
<blockquote><p>Node.js processes launched with a custom execPath will communicate with the parent process using the file descriptor (fd) identified using the environment variable NODE_CHANNEL_FD on the child process. The input and output on this fd is expected to be line delimited JSON objects.</p></blockquote>
<h2>写在后面</h2>
<p>内容较多,如有错漏及建议请指出。</p>
<h2>相关链接</h2>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=EJ3%2B96nNtUBUBjLUIPbXsA%3D%3D.DhG%2BCB48gk8JJ7yZthO%2BF52pehrMFSHfXPQHgtg7hXHFwt7ebWrCISZ35mysIpLh" rel="nofollow">https://nodejs.org/api/child_...</a></p>
express+session实现简易身份认证
https://segmentfault.com/a/1190000007721091
2016-12-07T11:33:47+08:00
2016-12-07T11:33:47+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
4
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=KxbhtUOHX6djU9r7XhOsyg%3D%3D.j3bmVZzckArbtqmxPiVOCtNtdZ%2F8PocLUIcx%2Fep34DE%2F223yq8fUVioS%2Byv2iW%2Bl618DXZhfBR9jPnMwNKfYZA%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=Uarv99fIZCi99VouSyWW2Q%3D%3D.LWt1Df8HwYue5qsnifUOkevX%2FIB2hMlA6VZPqHZKOPFENweavhsIr32buZpd8rGj%2FPK%2FwefDehKHWpevtywPoisYX6qT9%2BAuK%2Ba5v5gop3y77ADFgqJiLXcSSaeKnY99hEkRypbjP7wbWeRWPV91Mw%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>文章概览</h2>
<p>本文基于express、express-session实现了简易的登录/登出功能,完整的代码示例可以在<a href="https://link.segmentfault.com/?enc=lqUQpDQvgotiUdw%2FHKs37g%3D%3D.Qn3GJDPY1VDmPaRCzFi7PeG3Emvcu5mW%2BFFzG70%2B2SP6m7Zg%2FAEeVA1YaVnbA7mj%2Bs2rRnlHfyMKN1e1c9maQUcYjaHsNNg2J%2FWg0RNJ062y6kfC4vTN9WmYFMnOvFjb" rel="nofollow">这里</a>找到。</p>
<h2>环境初始化</h2>
<p>首先,初始化项目</p>
<pre><code class="bash">express -e</code></pre>
<p>然后,安装依赖。</p>
<pre><code class="bash">npm install</code></pre>
<p>接着,安装session相关的包。</p>
<pre><code class="bash">npm install --save express-session session-file-store</code></pre>
<h2>session相关配置</h2>
<p>配置如下,并不复杂,可以见代码注释,或者参考<a href="https://link.segmentfault.com/?enc=RJua0yUfttL8jripfdeqMQ%3D%3D.uZL8mZBFU3Ua3Q8BAqXiTvbEhOTCi6BxVWi7Y%2BM56JkQpaKg%2Bb%2FegiI5aQLzSWfh" rel="nofollow">官方文档</a>。</p>
<pre><code class="js">var express = require('express');
var app = express();
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var identityKey = 'skey';
app.use(session({
name: identityKey,
secret: 'chyingp', // 用来对session id相关的cookie进行签名
store: new FileStore(), // 本地存储session(文本文件,也可以选择其他store,比如redis的)
saveUninitialized: false, // 是否自动保存未初始化的会话,建议false
resave: false, // 是否每次都重新保存会话,建议false
cookie: {
maxAge: 10 * 1000 // 有效期,单位是毫秒
}
}));</code></pre>
<h2>实现登录/登出接口</h2>
<h3>创建测试账户数据</h3>
<p>首先,在本地创建个文件,来保存可用于登录的账户信息,避免创建链接数据库的繁琐。</p>
<pre><code class="js">// users.js
module.exports = {
items: [
{name: 'chyingp', password: '123456'}
]
};</code></pre>
<h3>登录、登出接口实现</h3>
<p>实现登录、登出接口,其中:</p>
<ul>
<li><p>登录:如果用户存在,则通过<code>req.regenerate</code>创建session,保存到本地,并通过<code>Set-Cookie</code>将session id保存到用户侧;</p></li>
<li><p>登出:销毁session,并清除cookie;</p></li>
</ul>
<pre><code class="js">var users = require('./users').items;
var findUser = function(name, password){
return users.find(function(item){
return item.name === name && item.password === password;
});
};
// 登录接口
app.post('/login', function(req, res, next){
var sess = req.session;
var user = findUser(req.body.name, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.name;
res.json({ret_code: 0, ret_msg: '登录成功'});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
// 退出登录
app.get('/logout', function(req, res, next){
// 备注:这里用的 session-file-store 在destroy 方法里,并没有销毁cookie
// 所以客户端的 cookie 还是存在,导致的问题 --> 退出登陆后,服务端检测到cookie
// 然后去查找对应的 session 文件,报错
// session-file-store 本身的bug
req.session.destroy(function(err) {
if(err){
res.json({ret_code: 2, ret_msg: '退出登录失败'});
return;
}
// req.session.loginUser = null;
res.clearCookie(identityKey);
res.redirect('/');
});
});</code></pre>
<h3>登录态判断</h3>
<p>用户访问 <a href="https://link.segmentfault.com/?enc=z37Gb7ETZEwhCNuaVqZZzg%3D%3D.obYoeohSs%2FRdzkg%2FY0iSJKOPsEG1SRr%2B6tsF8vInhIg%3D" rel="nofollow">http://127.0.0.1:3000</a> 时,判断用户是否登录,如果是,则调到用户详情界面(简陋无比);如果没有登录,则跳到登录界面;</p>
<pre><code class="js">app.get('/', function(req, res, next){
var sess = req.session;
var loginUser = sess.loginUser;
var isLogined = !!loginUser;
res.render('index', {
isLogined: isLogined,
name: loginUser || ''
});
});
</code></pre>
<h3>UI界面</h3>
<p>最后,看下登录、登出UI相关的代码。</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>会话管理</title>
</head>
<body>
<h1>会话管理</h1>
<% if(isLogined){ %>
<p>当前登录用户:<%= name %>,<a href="/logout" id="logout">退出登陆</a></p>
<% }else{ %>
<form method="POST" action="/login">
<input type="text" id="name" name="name" value="chyingp" />
<input type="password" id="password" name="password" value="123456" />
<input type="submit" value="登录" id="login" />
</form>
<% } %>
<script type="text/javascript" src="/jquery-3.1.0.min.js"></script>
<script type="text/javascript">
$('#login').click(function(evt){
evt.preventDefault();
$.ajax({
url: '/login',
type: 'POST',
data: {
name: $('#name').val(),
password: $('#password').val()
},
success: function(data){
if(data.ret_code === 0){
location.reload();
}
}
});
});
</script>
</body>
</html></code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=Rkvy6ruT5Ltkg66fri6SHQ%3D%3D.xFD7U12RS5mK%2Bq0u4qcfsdphwCQX%2F8F3PQx%2FoX9zzvAqkkLcARCNCn57GLW1z0xf" rel="nofollow">https://github.com/expressjs/...</a></p>
Nodejs进阶:核心模块https 之 如何优雅的访问12306
https://segmentfault.com/a/1190000007544239
2016-11-21T08:16:06+08:00
2016-11-21T08:16:06+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
2
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=9Cv2eA2QoEgwAxiIk8qBLw%3D%3D.6pMGut0CXLuCK51aHZJXttI0uXB12WsIKlSj6NHDqVKh7SDRy2keAE%2B9K%2FJWg5YYIylJLkHO6JvoFtk6KkTUUg%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 <a href="https://link.segmentfault.com/?enc=%2BIe8OtYABHF2zk7%2BeCWwWA%3D%3D.GnVNW66OWRlpcrDGTJQ4903eO3QBjR5OG6ALimBkr38fY%2BqP%2BhbS5tQts%2FU5Td0I%2BKb6gZtL33C5e0AE3gyv2yoBm7aOADNc7wzaT%2F0fvrXnhkI4FpiZl4oJLFGiQBGb%2FYpnbw5Fn84ilGFrhr13eQ%3D%3D" rel="nofollow">197339705</a>。</p></blockquote>
<h2>模块概览</h2>
<p>这个模块的重要性,基本不用强调了。在网络安全问题日益严峻的今天,网站采用HTTPS是个必然的趋势。</p>
<p>在nodejs中,提供了 https 这个模块来完成 HTTPS 相关功能。从官方文档来看,跟 http 模块用法非常相似。</p>
<p>本文主要包含两部分:</p>
<ol>
<li><p>通过客户端、服务端的例子,对https模块进行入门讲解。</p></li>
<li><p>如何访问安全证书不受信任的网站。(以 12306 为例子)</p></li>
</ol>
<p>篇幅所限,本文无法对 HTTPS协议 及 相关技术体系 做过多讲解,有问题欢迎留言交流。</p>
<h2>客户端例子</h2>
<p>跟http模块的用法非常像,只不过请求的地址是https协议的而已,代码如下:</p>
<pre><code class="js">var https = require('https');
https.get('https://www.baidu.com', function(res){
console.log('status code: ' + res.statusCode);
console.log('headers: ' + res.headers);
res.on('data', function(data){
process.stdout.write(data);
});
}).on('error', function(err){
console.error(err);
});</code></pre>
<h2>服务端例子</h2>
<p>对外提供HTTPS服务,需要有HTTPS证书。如果你已经有了HTTPS证书,那么可以跳过证书生成的环节。如果没有,可以参考如下步骤</p>
<h3>生成证书</h3>
<h4>1、创建个目录存放证书。</h4>
<pre><code class="bash">mkdir cert
cd cert</code></pre>
<h4>2、生成私钥。</h4>
<pre><code>openssl genrsa -out chyingp-key.pem 2048</code></pre>
<h4>3、生成证书签名请求(csr是 Certificate Signing Request的意思)。</h4>
<pre><code>openssl req -new \
-sha256
-key chyingp-key.key.pem \
-out chyingp-csr.pem \
-subj "/C=CN/ST=Guandong/L=Shenzhen/O=YH Inc/CN=www.chyingp.com"</code></pre>
<h4>4、生成证书。</h4>
<pre><code>openssl x509 \
-req -in chyingp-csr.pem \
-signkey chyingp-key.pem \
-out chyingp-cert.pem</code></pre>
<h3>HTTPS服务端</h3>
<p>代码如下:</p>
<pre><code class="js">var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./cert/chyingp-key.pem'), // 私钥
cert: fs.readFileSync('./cert/chyingp-cert.pem') // 证书
};
var server = https.createServer(options, function(req, res){
res.end('这是来自HTTPS服务器的返回');
});
server.listen(3000);</code></pre>
<p>由于我并没有 www.chyingp.com 这个域名,于是先配置本地host</p>
<pre><code>127.0.0.1 www.chyingp.com</code></pre>
<p>启动服务,并在浏览器里访问 <a href="https://link.segmentfault.com/?enc=X8hP3jB%2BqEyYzCu1yVsbCw%3D%3D.xuew94idfxeNVXSAb6qE5dtC8EuQ7yBknM%2FgksywOhM%3D" rel="nofollow">http://www.chyingp.com:3000</a>。注意,浏览器会提示你证书不可靠,点击 信任并继续访问 就行了。</p>
<h2>进阶例子:访问安全证书不受信任的网站</h2>
<p>这里以我们最喜爱的12306最为例子。当我们通过浏览器,访问12306的购票页面 <a href="https://link.segmentfault.com/?enc=3q%2B4D%2FSQmuh8LlLhrQAJZQ%3D%3D.P04yPcSUTx3hI741Babkfb7kS%2Fyv5Gcc85vS%2BL%2FtXhT8KI3uap2p15XtOqwb7YzF" rel="nofollow">https://kyfw.12306.cn/otn/reg...</a> 时,chrome会阻止我们访问,这是因为,12306的证书是自己颁发的,chrome无法确认他的安全性。</p>
<p>对这种情况,可以有如下处理方式:</p>
<ol>
<li><p>停止访问:着急抢票回家过年的老乡表示无法接受。</p></li>
<li><p>无视安全警告,继续访问:大部分情况下,浏览器是会放行的,不过安全提示还在。</p></li>
<li><p>导入12306的CA根证书:浏览器乖乖就范,认为访问是安全的。(实际上还是有安全提示,因为12306用的签名算法安全级别不够)</p></li>
</ol>
<h3>例子:触发安全限制</h3>
<p>同样的,通过 node https client 发起请求,也会遇到同样问题。我们做下实验,代码如下:</p>
<pre><code class="js">var https = require('https');
https.get('https://kyfw.12306.cn/otn/regist/init', function(res){
res.on('data', function(data){
process.stdout.write(data);
});
}).on('error', function(err){
console.error(err);
});</code></pre>
<p>运行上面代码,得到下面的错误提示,意思是 安全证书不可靠,拒绝继续访问。</p>
<pre><code class="bash">{ Error: self signed certificate in certificate chain
at Error (native)
at TLSSocket.<anonymous> (_tls_wrap.js:1055:38)
at emitNone (events.js:86:13)
at TLSSocket.emit (events.js:185:7)
at TLSSocket._finishInit (_tls_wrap.js:580:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:412:38) code: 'SELF_SIGNED_CERT_IN_CHAIN' }</code></pre>
<p>ps:个人认为这里的错误提示有点误导人,12306网站的证书并不是自签名的,只是对证书签名的CA是12306自家的,不在可信列表里而已。自签名证书,跟自己CA签名的证书还是不一样的。</p>
<p>类似在浏览器里访问,我们可以采取如下处理:</p>
<ol>
<li><p>不建议:忽略安全警告,继续访问;</p></li>
<li><p>建议:将12306的CA加入受信列表;</p></li>
</ol>
<h3>方法1:忽略安全警告,继续访问</h3>
<p>非常简单,将 rejectUnauthorized 设置为 false 就行,再次运行代码,就可以愉快的返回页面了。</p>
<pre><code class="js">// 例子:忽略安全警告
var https = require('https');
var fs = require('fs');
var options = {
hostname: 'kyfw.12306.cn',
path: '/otn/leftTicket/init',
rejectUnauthorized: false // 忽略安全警告
};
var req = https.get(options, function(res){
res.pipe(process.stdout);
});
req.on('error', function(err){
console.error(err.code);
});</code></pre>
<h3>方法2:将12306的CA加入受信列表</h3>
<p>这里包含3个步骤:</p>
<ol>
<li><p>下载 12306 的CA证书</p></li>
<li><p>将der格式的CA证书,转成pem格式</p></li>
<li><p>修改node https的配置</p></li>
</ol>
<h4>1、下载 12306 的CA证书</h4>
<p>在12306的官网上,提供了CA证书的<a href="https://link.segmentfault.com/?enc=6vBZvEhkNx3ZDppv3hM2MQ%3D%3D.XKTkfMV0AFgTHvQVjztQFcc1GBxICoaU938Kc1gf%2B%2BLu%2F0PRTMogGOS6n9zCby12E7P1xHM%2FDipCcRwDkYAblw%3D%3D" rel="nofollow">下载地址</a>,将它保存到本地,命名为 srca.cer。</p>
<h4>2、将der格式的CA证书,转成pem格式</h4>
<p>https初始化client时,提供了 ca 这个配置项,可以将 12306 的CA证书添加进去。当你访问 12306 的网站时,client就会用ca配置项里的 ca 证书,对当前的证书进行校验,于是就校验通过了。</p>
<p>需要注意的是,ca 配置项只支持 pem 格式,而从12306官网下载的是der格式的。需要转换下格式才能用。关于 pem、der的区别,可参考 <a href="https://link.segmentfault.com/?enc=hMNtegSXqCgsW7RES88qSw%3D%3D.JE5dmifb7UdP7eOJqhYxme8R7ykXOm8J3SURG0%2BwXEfM9bqA49U1gPa9nfC%2FLBu%2FZH11iOz3cxpjQ3wWHzQjSO7HcwlOvuoydw2HkkyaZBVVZi46zmx0Najp9z%2BeT9Br5WBUu7b0U16B%2FrfUINhjg%2BmQc%2FDuaXt7F9BfB32RPDY%3D" rel="nofollow">这里</a>。</p>
<pre><code class="bash">openssl x509 -in srca.cer -inform der -outform pem -out srca.cer.pem</code></pre>
<h4>3、修改node https的配置</h4>
<p>修改后的代码如下,现在可以愉快的访问12306了。</p>
<pre><code class="js">// 例子:将12306的CA证书,加入我们的信任列表里
var https = require('https');
var fs = require('fs');
var ca = fs.readFileSync('./srca.cer.pem');
var options = {
hostname: 'kyfw.12306.cn',
path: '/otn/leftTicket/init',
ca: [ ca ]
};
var req = https.get(options, function(res){
res.pipe(process.stdout);
});
req.on('error', function(err){
console.error(err.code);
});</code></pre>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=QP0BN8o0PiO3sy9y4jS2Qg%3D%3D.UjmJNSP0916JsdjUPbxwTVp9FuKj9JIS%2BpgngrmOb2mq1rvbTjEkFnDLQkYOzj4WdAtMldkDkBWLPXznih%2BAcg%3D%3D" rel="nofollow">Why is my node.js SSL connection failing to connect?</a></p>
<p><a href="https://link.segmentfault.com/?enc=QfTw9ioggcLJBCs8L2E7pg%3D%3D.QmYgOui9Rq5ncIYqo7qqkL0KdMXKJKF64UCkWE%2FNCjyB8hM6yIRj5PpxzIXWMqC0xoCvxObVskjUJtIgnT1Y4xZHWYnq2O5%2BLLCL%2FTwvxNmqfFlW1%2FnMDW%2Btm7488ryU4yQBbNqZXVRwiqYYo3yuP1Z7W81%2BZu4jEBVOLNXZ3ZA%3D" rel="nofollow">DER vs. CRT vs. CER vs. PEM Certificates and How To Convert Them</a></p>
<p><a href="https://link.segmentfault.com/?enc=zH6NkPeHZ3WnwrTMNlrzXQ%3D%3D.63%2B%2BtU%2BVGmP6UQIEZzq3Mzm7ZNRkBeSH1J%2Bhyg6nWgvSkT%2BctpV4DdN9sqdcMkU0ndzVdr9JeKmu0fjGtbwLuBSwc9HpqDW5Q5wakyuRMNb1R35jQp%2B%2FgeZcVK%2FJgcN%2F" rel="nofollow">Painless Self Signed Certificates in node.js</a></p>
<p><a href="https://link.segmentfault.com/?enc=NtILk0uIb2zmX1YKjW%2BIUg%3D%3D.o36QOYmpI9cKA3a%2FJqeYIkf29C04xG%2BYFiXvbOhYsiod2%2FxJdifsFhi3APxhC5D9" rel="nofollow">利用OpenSSL创建自签名的SSL证书备忘(自建ca)</a></p>
<p><a href="https://link.segmentfault.com/?enc=7PNHxAk5QLEUgolKKmQISg%3D%3D.baljqZmbLLzti5V4ucmql9C9%2Bt6eceYMm%2F1H0ZzS7wDgb1Gqxq8aVnetoXQbBFKf8%2B%2Fy18IHP2%2BGjpaj2qTWTw%3D%3D" rel="nofollow">OpenSSL 与 SSL 数字证书概念贴</a></p>
<p><a href="https://link.segmentfault.com/?enc=2PTsIaO63ejFbq0zSgvNVQ%3D%3D.AgyNfjWgAF%2FIhObZdGu%2Fa4E9UOI3m8O63jvF7X1fXM34UXwYQNa%2BONYd20nauvlHOPOA9dWN2T4FX056ScBSAw%3D%3D" rel="nofollow">自签名证书和私有CA签名的证书的区别 创建自签名证书 创建私有CA 证书类型 证书扩展名</a></p>
Nodejs进阶:核心模块net入门与实例讲解
https://segmentfault.com/a/1190000007507322
2016-11-17T08:07:30+08:00
2016-11-17T08:07:30+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
4
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=2Tn85JoVSq6kZiQ6z5jv5Q%3D%3D.Rg9speXbX5Ql0IAJHZ0YWHhRIKgP%2Fk6vCPiggMhykR81zB8ZikxG3DmzwMKvr8mb53hZNnRLUh10aV%2BPSl2gmw%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 197339705。</p></blockquote>
<h2>模块概览</h2>
<p>net模块是同样是nodejs的核心模块。在http模块概览里提到,http.Server继承了net.Server,此外,http客户端与http服务端的通信均依赖于socket(net.Socket)。也就是说,做node服务端编程,net基本是绕不开的一个模块。</p>
<p>从组成来看,net模块主要包含两部分,了解socket编程的同学应该比较熟悉了:</p>
<ul>
<li><p>net.Server:TCP server,内部通过socket来实现与客户端的通信。</p></li>
<li><p>net.Socket:tcp/本地 socket的node版实现,它实现了全双工的stream接口。</p></li>
</ul>
<p>本文从一个简单的 tcp服务端/客户端 的例子开始讲解,好让读者有个概要的认识。接着再分别介绍 net.Server、net.Socket 比较重要的API、属性、事件。</p>
<p>对于初学者,建议把文中的例子本地跑一遍加深理解。</p>
<h2>简单的 server+client 例子</h2>
<p>tcp服务端程序如下:</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp服务端
var server = net.createServer(function(socket){
console.log('服务端:收到来自客户端的请求');
socket.on('data', function(data){
console.log('服务端:收到客户端数据,内容为{'+ data +'}');
// 给客户端返回数据
socket.write('你好,我是服务端');
});
socket.on('close', function(){
console.log('服务端:客户端连接断开');
});
});
server.listen(PORT, HOST, function(){
console.log('服务端:开始监听来自客户端的请求');
});</code></pre>
<p>tcp客户端如下:</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp客户端
var client = net.createConnection(PORT, HOST);
client.on('connect', function(){
console.log('客户端:已经与服务端建立连接');
});
client.on('data', function(data){
console.log('客户端:收到服务端数据,内容为{'+ data +'}');
});
client.on('close', function(data){
console.log('客户端:连接断开');
});
client.end('你好,我是客户端');</code></pre>
<p>运行服务端、客户端代码,控制台分别输出如下:</p>
<p>服务端:</p>
<pre><code class="bash">服务端:开始监听来自客户端的请求
服务端:收到来自客户端的请求
服务端:收到客户端数据,内容为{你好,我是客户端}
服务端:客户端连接断开</code></pre>
<p>客户端:</p>
<pre><code class="bash">客户端:已经与服务端建立连接
客户端:收到服务端数据,内容为{你好,我是服务端}
客户端:连接断开</code></pre>
<h2>服务端 net.Server</h2>
<h3>server.address()</h3>
<p>返回服务端的地址信息,比如绑定的ip地址、端口等。</p>
<pre><code class="js">console.log( server.address() );
// 输出如下 { port: 3000, family: 'IPv4', address: '127.0.0.1' }</code></pre>
<h3>server.close(callback])</h3>
<p>关闭服务器,停止接收新的客户端请求。有几点注意事项:</p>
<ul>
<li><p>对正在处理中的客户端请求,服务器会等待它们处理完(或超时),然后再正式关闭。</p></li>
<li><p>正常关闭的同时,callback 会被执行,同时会触发 close 事件。</p></li>
<li><p>异常关闭的同时,callback 也会执行,同时将对应的 error 作为参数传入。(比如还没调用 server.listen(port) 之前,就调用了server.close())</p></li>
</ul>
<p>下面会通过两个具体的例子进行对比,先把结论列出来</p>
<ul>
<li><p>已调用server.listen():正常关闭,close事件触发,然后callback执行,error参数为undefined</p></li>
<li><p>未调用server.listen():异常关闭,close事件触发,然后callback执行,error为具体的错误信息。(注意,error 事件没有触发)</p></li>
</ul>
<p>例子1:服务端正常关闭</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(noop);
server.listen(PORT, HOST, function(){
server.close(function(error){
if(error){
console.log( 'close回调:服务端异常:' + error.message );
}else{
console.log( 'close回调:服务端正常关闭' );
}
});
});
server.on('close', function(){
console.log( 'close事件:服务端关闭' );
});
server.on('error', function(error){
console.log( 'error事件:服务端异常:' + error.message );
});</code></pre>
<p>输出为:</p>
<pre><code class="bash">close事件:服务端关闭
close回调:服务端正常关闭</code></pre>
<p>例子2:服务端异常关闭</p>
<p>代码如下</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(noop);
// 没有正式启动请求监听
// server.listen(PORT, HOST);
server.on('close', function(){
console.log( 'close事件:服务端关闭' );
});
server.on('error', function(error){
console.log( 'error事件:服务端异常:' + error.message );
});
server.close(function(error){
if(error){
console.log( 'close回调:服务端异常:' + error.message );
}else{
console.log( 'close回调:服务端正常关闭' );
}
});</code></pre>
<p>输出为:</p>
<pre><code class="bash">close事件:服务端关闭
close回调:服务端异常:Not running</code></pre>
<h3>server.ref()/server.unref()</h3>
<p>了解node事件循环的同学对这两个API应该不陌生,主要用于将server 加入事件循环/从事件循环里面剔除,影响就在于会不会影响进程的退出。</p>
<p>对出学习net的同学来说,并不需要特别关注,感兴趣的自己做下实验就好。</p>
<h3>事件 listening/connection/close/error</h3>
<ul>
<li><p>listening:调用 server.listen(),正式开始监听请求的时候触发。</p></li>
<li><p>connection:当有新的请求进来时触发,参数为请求相关的 socket。</p></li>
<li><p>close:服务端关闭的时候触发。</p></li>
<li><p>error:服务出错的时候触发,比如监听了已经被占用的端口。</p></li>
</ul>
<p>几个事件都比较简单,这里仅举个 connection 的例子。</p>
<p>从测试结果可以看出,有新的客户端连接产生时,net.createServer(callback) 中的callback回调 会被调用,同时 connection 事件注册的回调函数也会被调用。</p>
<p>事实上,net.createServer(callback) 中的 callback 在node内部实现中 也是加入了做为 connection事件 的监听函数。感兴趣的可以看下node的源码。</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(function(socket){
socket.write('1. connection 触发\n');
});
server.on('connection', function(socket){
socket.end('2. connection 触发\n');
});
server.listen(PORT, HOST);</code></pre>
<p>通过下面命令测试下效果</p>
<pre><code class="bash">curl http://127.0.0.1:3000</code></pre>
<p>输出:</p>
<pre><code class="bash">1. connection 触发
2. connection 触发</code></pre>
<h2>客户端 net.Socket</h2>
<p>在文章开头已经举过客户端的例子,这里再把例子贴一下。(备注:严格来说不应该把 net.Socket 叫做客户端,这里方便讲解而已)</p>
<p>单从node官方文档来看的话,感觉 net.Socket 比 net.Server 要复杂很多,有更多的API、事件、属性。但实际上,把 net.Socket 相关的API、事件、属性 进行归类下,会发现,其实也不是特别复杂。</p>
<p>具体请看下一小节内容。</p>
<pre><code class="js">var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp客户端
var client = net.createConnection(PORT, HOST);
client.on('connect', function(){
console.log('客户端:已经与服务端建立连接');
});
client.on('data', function(data){
console.log('客户端:收到服务端数据,内容为{'+ data +'}');
});
client.on('close', function(data){
console.log('客户端:连接断开');
});
client.end('你好,我是客户端');</code></pre>
<h2>API、属性归类</h2>
<p>以下对net.Socket的API跟属性,按照用途进行了大致的分类,方便读者更好的理解。大部分API跟属性都比较简单,看下文档就知道做什么的,这里就先不展开。</p>
<h3>连接相关</h3>
<ul>
<li><p>socket.connect():有3种不同的参数,用于不同的场景;</p></li>
<li><p>socket.setTimeout():用来进行连接超时设置。</p></li>
<li><p>socket.setKeepAlive():用来设置长连接。</p></li>
<li><p>socket.destroy()、socket.destroyed:当错误发生时,用来销毁socket,确保这个socket上不会再有其他的IO操作。</p></li>
</ul>
<h3>数据读、写相关</h3>
<p>socket.write()、socket.end()、socket.pause()、socket.resume()、socket.setEncoding()、socket.setNoDelay()</p>
<h3>数据属性相关</h3>
<p>socket.bufferSize、socket.bytesRead、socket.bytesWritten</p>
<h3>事件循环相关</h3>
<p>socket.ref()、socket.unref()</p>
<h3>地址相关</h3>
<ul>
<li><p>socket.address()</p></li>
<li><p>socket.remoteAddress、socket.remoteFamily、socket.remotePort</p></li>
<li><p>socket.localAddress/socket.localPort</p></li>
</ul>
<h2>事件简介</h2>
<ul>
<li><p>data:当收到另一侧传来的数据时触发。</p></li>
<li><p>connect:当连接建立时触发。</p></li>
<li><p>close:连接断开时触发。如果是因为传输错误导致的连接断开,则参数为error。</p></li>
<li><p>end:当连接另一侧发送了 FIN 包的时候触发(读者可以回顾下HTTP如何断开连接的)。默认情况下(allowHalfOpen == false),socket会完成自我销毁操作。但你也可以把 allowHalfOpen 设置为 true,这样就可以继续往socket里写数据。当然,最后你需要手动调用 socket.end()</p></li>
<li><p>error:当有错误发生时,就会触发,参数为error。(官方文档基本一句话带过,不过考虑到出错的可能太多,也可以理解)</p></li>
<li><p>timeout:提示用户,socket 已经超时,需要手动关闭连接。</p></li>
<li><p>drain:当写缓存空了的时候触发。(不是很好描述,具体可以看下stream的介绍)</p></li>
<li><p>lookup:域名解析完成时触发。</p></li>
</ul>
<h2>相关链接</h2>
<p>官方文档:<br><a href="https://link.segmentfault.com/?enc=t8sIISZVx6UG1r0fpJoYSw%3D%3D.AhkOfzGUrk%2BgRQuXtfhoeWb7Z8onsgvloNWP3MAUGlgsxGlUJn3QsR8Zz7ydffRFcJDK3FYg3NTVSl9KA1WVRg%3D%3D" rel="nofollow">https://nodejs.org/api/net.ht...</a></p>
Nodejs进阶:如何将图片转成datauri嵌入到网页中去
https://segmentfault.com/a/1190000007495645
2016-11-16T09:40:25+08:00
2016-11-16T09:40:25+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=g9p8bn7Oo09jrNu1%2BDx2bA%3D%3D.ncbVKaGjJMDLCewDf72Ks%2FG0CUDuXNj5QvhLNJ3KH5a0yPYg4ZfthxpjXDWBRMDHLyYzEU8nMRi8LPIelxIUwg%3D%3D" rel="nofollow">github主页地址</a>。</p></blockquote>
<h2>问题:将图片转成datauri</h2>
<p>今天,在QQ群有个群友问了个问题:“nodejs读取图片,转成base64,怎么读取呢?” 想了一下,他想问的应该是 怎么样把图片嵌入到网页中去,即如何把图片转成对应的 datauri。</p>
<p>是个不错的问题,而且也是个很常用的功能。快速实现了个简单的demo,这里顺便记录一下。</p>
<h2>实现思路</h2>
<p>思路很直观:1、读取图片二进制数据 -> 2、转成base64字符串 -> 3、转成datauri。</p>
<p>关于base64的介绍,可以参考阮一峰老师的<a href="https://link.segmentfault.com/?enc=%2BxbhCtwW%2Fx4GAioYCbINnw%3D%3D.O83XyKYuJbGQMEPzVP8MkWpM%2BYqNzDIYIVVMYSz6JUCwtJq3xttfm%2BsILhEe2kJ9y1cUH4G2%2F8t1YwFuIQsNRA%3D%3D" rel="nofollow">文章</a>。而 datauri 的格式如下</p>
<blockquote><p>data:<mediatype>,<data></p></blockquote>
<p>具体到png图片,大概如下,其中 “xxx” 就是前面的base64字符串了。接下来,我们看下在nodejs里该如何实现</p>
<blockquote><p>data: image/png;base64, xxx</p></blockquote>
<h2>具体实现</h2>
<p>首先,读取本地图片二进制数据。</p>
<pre><code class="js">var fs = require('fs');
var filepath = './1.png';
var bData = fs.readFileSync(filepath);</code></pre>
<p>然后,将二进制数据转换成base64编码的字符串。</p>
<pre><code class="js">var base64Str = bData.toString('base64');</code></pre>
<p>最后,转换成datauri的格式。</p>
<pre><code class="js">var datauri = 'data:image/png;base64,' + base64Str;</code></pre>
<p>完整例子代码如下,代码非常少:</p>
<pre><code class="js">var fs = require('fs');
var filepath = './1.png';
var bData = fs.readFileSync(filepath);
var base64Str = bData.toString('base64');
var datauri = 'data:image/png;base64,' + base64Str;
console.log(datauri);</code></pre>
<h2>github demo地址</h2>
<p>demo地址请<a href="https://link.segmentfault.com/?enc=37iL20WoTjAXqFZwyUH6kg%3D%3D.z7JiQYco6UTqFZGQWDubFOkRwFSGAU7JPymvW9AlWWRK7egR%2FxQ3S7MWdZu2dYVNe2O9BAGks3uO49BS6BdAvKNAR%2Fd6gkVjMbdqaSOb95WSuCEIpm8a4qSKYlwRNErc" rel="nofollow">点击这里</a>,或者</p>
<pre><code class="bash">git clone https://github.com/chyingp/nodejs-learning-guide.git
cd nodejs-learning-guide/examples/2016.11.15-base64-datauri
node server.js</code></pre>
<p>然后在浏览器访问 <a href="https://link.segmentfault.com/?enc=pAbSry2nLbHPj1%2F71qK14Q%3D%3D.I%2BxVwLm1D%2FEgPIPqjMzZwUHgEhNW1lY1%2BU8KJHSZaTM%3D" rel="nofollow">http://127.0.0.1:3000</a>,就可以看到效果 :)</p>
<h2>相关链接</h2>
<p>Base64笔记:<a href="https://link.segmentfault.com/?enc=QWGuCfcQEBPY4%2FQoH%2BbfEQ%3D%3D.o28AIiwYqVoYBL426Naeo24xGVoe5GUxWX28KmgCbH3lwcCQpbtAol9Z1pT9k347j7ItMOmcFkJQP9ol7QR7pA%3D%3D" rel="nofollow">http://www.ruanyifeng.com/blo...</a><br>Data URIs:<a href="https://link.segmentfault.com/?enc=19R6D0VxZlGLzLUPz2dVeA%3D%3D.oemGuzAun9xEVICJOH%2F9yHJmQUBqnsguP74U5AdxZ9wFl1st6V6ctlYcGYA7zVEzvSCpNIFDUE3PyrvbjIm6pmdWex0uMEvSucZNOVG26VI%3D" rel="nofollow">https://developer.mozilla.org...</a></p>
Nodejs进阶:http核心模块简介
https://segmentfault.com/a/1190000007483107
2016-11-15T09:04:19+08:00
2016-11-15T09:04:19+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=0g3zR15FWlv46H4tM23u4A%3D%3D.Wfbf4zeLSRFUq3bS0p6j9ZkjiwYjOhpNYzdCBnTIB4VhikG2vAPoNKWS6q5JAxm7Kg17s7BEqD1FlSriZHI1tA%3D%3D" rel="nofollow">github主页地址</a>。</p></blockquote>
<h2>http模块概览</h2>
<p>大多数nodejs开发者都是冲着开发web server的目的选择了nodejs。正如官网所展示的,借助http模块,可以几行代码就搞定一个超迷你的web server。</p>
<p>在nodejs中,<code>http</code>可以说是最核心的模块,同时也是比较复杂的一个模块。上手很简单,但一旦深入学习,不少初学者就会觉得头疼,不知从何入手。</p>
<p>本文先从一个简单的例子出发,引出<code>http</code>模块最核心的四个实例。看完本文,应该就能够对http模块有个整体的认识。</p>
<h2>一个简单的例子</h2>
<p>在下面的例子中,我们创建了1个web服务器、1个http客户端</p>
<ul>
<li><p>服务器server:接收来自客户端的请求,并将客户端请求的地址返回给客户端。</p></li>
<li><p>客户端client:向服务器发起请求,并将服务器返回的内容打印到控制台。</p></li>
</ul>
<p>代码如下所示,只有几行,但包含了不少信息量。下一小节会进行简单介绍。</p>
<pre><code class="js">var http = require('http');
// http server 例子
var server = http.createServer(function(serverReq, serverRes){
var url = serverReq.url;
serverRes.end( '您访问的地址是:' + url );
});
server.listen(3000);
// http client 例子
var client = http.get('http://127.0.0.1:3000', function(clientRes){
clientRes.pipe(process.stdout);
});
</code></pre>
<h2>例子解释</h2>
<p>在上面这个简单的例子里,涉及了4个实例。大部分时候,serverReq、serverRes 才是主角。</p>
<ul>
<li><p>server:http.Server实例,用来提供服务,处理客户端的请求。</p></li>
<li><p>client:http.ClientReques实例,用来向服务端发起请求。</p></li>
<li><p>serverReq/clientRes:其实都是 http.IncomingMessage实。serverReq 用来获取客户端请求的相关信息,如request header;而clientRes用来获取服务端返回的相关信息,比如response header。</p></li>
<li><p>serverRes:http.ServerResponse实例</p></li>
</ul>
<h2>关于http.IncomingMessage、http.ServerResponse</h2>
<p>先讲下 http.ServerResponse 实例。作用很明确,服务端通过http.ServerResponse 实例,来个请求方发送数据。包括发送响应表头,发送响应主体等。</p>
<p>接下来是 http.IncomingMessage 实例,由于在 server、client 都出现了,初学者难免有点迷茫。它的作用是</p>
<p>在server端:获取请求发送方的信息,比如请求方法、路径、传递的数据等。<br>在client端:获取 server 端发送过来的信息,比如请求方法、路径、传递的数据等。</p>
<p>http.IncomingMessage实例 有三个属性需要注意:method、statusCode、statusMessage。</p>
<ul>
<li><p>method:只在 server 端的实例有(也就是 serverReq.method)</p></li>
<li><p>statusCode/statusMessage:只在 client 端 的实例有(也就是 clientRes.method)</p></li>
</ul>
<h2>关于继承与扩展</h2>
<h3>http.Server</h3>
<ul>
<li><p>http.Server 继承了 net.Server (于是顺带需要学一下 net.Server 的API、属性、相关事件)</p></li>
<li><p>net.createServer(fn),回调中的 <code>socket</code> 是个双工的stream接口,也就是说,读取发送方信息、向发送方发送信息都靠他。</p></li>
</ul>
<p>备注:socket的客户端、服务端是相对的概念,所以其实 net.Server 内部也是用了 net.Socket(不负责任猜想)</p>
<pre><code class="js">// 参考:https://cnodejs.org/topic/4fb1c1fd1975fe1e1310490b
var net = require('net');
var PORT = 8989;
var HOST = '127.0.0.1';
var server = net.createServer(function(socket){
console.log('Connected: ' + socket.remoteAddress + ':' + socket.remotePort);
socket.on('data', function(data){
console.log('DATA ' + socket.remoteAddress + ': ' + data);
console.log('Data is: ' + data);
socket.write('Data from you is "' + data + '"');
});
socket.on('close', function(){
console.log('CLOSED: ' +
socket.remoteAddress + ' ' + socket.remotePort);
});
});
server.listen(PORT, HOST);
console.log(server instanceof net.Server); // true</code></pre>
<h3>http.ClientRequest</h3>
<p>http.ClientRequest 内部创建了一个socket来发起请求,<a href="https://link.segmentfault.com/?enc=k4bzLuaJsqdEJlDFYV7WKw%3D%3D.5Z3c44%2F8FGV%2BnPuEl5JWChukebAjL8g6%2BmlOXunptcvRkRxIcKEBFjGjyLPHSY11JG%2BWc0SGFg%2BaW6kT8f3KKo6KXJFH5iuudNM13L%2BkVVM%3D" rel="nofollow">代码如下</a>。</p>
<p>当你调用 http.request(options) 时,内部是这样的</p>
<pre><code class="javascript">self.onSocket(net.createConnection(options));
</code></pre>
<h3>http.ServerResponse</h3>
<ul><li><p>实现了 Writable Stream interface,内部也是通过socket来发送信息。</p></li></ul>
<h3>http.IncomingMessage</h3>
<ul>
<li><p>实现了 Readable Stream interface,参考<a href="https://link.segmentfault.com/?enc=y3BAjwnxQg%2BNXZBYK3X3Fg%3D%3D.MQuQLVEr9kwEKttCSPfbjo1aX%2FBCmFTIQkFpDAV7m5qRBwkrlPusExPkVBYojePlqJysFxPl%2FXDgH7Ver5nSOU5yOBn1xmlDGmoTZGXPKpU%3D" rel="nofollow">这里</a></p></li>
<li><p>req.socket --> 获得跟这次连接相关的socket</p></li>
</ul>
Nodejs基础:路径处理模块path总结
https://segmentfault.com/a/1190000007471775
2016-11-14T08:48:37+08:00
2016-11-14T08:48:37+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
13
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=ltm2RaONX9xJ%2FnvNY0TNXw%3D%3D.LUy%2BG0WrwOQoMUI7ZobBHG4XWiD2GvlHj1gKsZVoiQAH5r1IORYjEng0M8ElPb62jl5kfUmn%2FFY%2Bg8h7PKQKlQ%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 197339705。</p></blockquote>
<h2>模块概览</h2>
<p>在nodejs中,path是个使用频率很高,但却让人又爱又恨的模块。部分因为文档说的不够清晰,部分因为接口的平台差异性。</p>
<p>将path的接口按照用途归类,仔细琢磨琢磨,也就没那么费解了。</p>
<h2>获取路径/文件名/扩展名</h2>
<ul>
<li><p>获取路径:path.dirname(filepath)</p></li>
<li><p>获取文件名:path.basename(filepath)</p></li>
<li><p>获取扩展名:path.extname(filepath)</p></li>
</ul>
<h3>获取所在路径</h3>
<p>例子如下:</p>
<pre><code class="javascript">var path = require('path');
var filepath = '/tmp/demo/js/test.js';
// 输出:/tmp/demo/js
console.log( path.dirname(filepath) );</code></pre>
<h3>获取文件名</h3>
<p>严格意义上来说,path.basename(filepath) 只是输出路径的最后一部分,并不会判断是否文件名。</p>
<p>但大部分时候,我们可以用它来作为简易的“获取文件名“的方法。</p>
<pre><code class="javascript">var path = require('path');
// 输出:test.js
console.log( path.basename('/tmp/demo/js/test.js') );
// 输出:test
console.log( path.basename('/tmp/demo/js/test/') );
// 输出:test
console.log( path.basename('/tmp/demo/js/test') );</code></pre>
<p>如果只想获取文件名,单不包括文件扩展呢?可以用上第二个参数。</p>
<pre><code class="javascript">// 输出:test
console.log( path.basename('/tmp/demo/js/test.js', '.js') );</code></pre>
<h3>获取文件扩展名</h3>
<p>简单的例子如下:</p>
<pre><code class="javascript">var path = require('path');
var filepath = '/tmp/demo/js/test.js';
// 输出:.js
console.log( path.extname(filepath) );</code></pre>
<p>更详细的规则是如下:(假设 path.basename(filepath) === B )</p>
<ul>
<li><p>从B的最后一个<code>.</code>开始截取,直到最后一个字符。</p></li>
<li><p>如果B中不存在<code>.</code>,或者B的第一个字符就是<code>.</code>,那么返回空字符串。</p></li>
</ul>
<p>直接看<a href="https://link.segmentfault.com/?enc=9SR4tldTG%2FXg0LKOym1O0g%3D%3D.QK1tM5ruwbgD%2BL8ukWCtGqWqXbB7H%2Fz9%2BAYL57Q%2BZuvyL1Oq29LLO8GPzvYKLN6vg9vCHmEBnHJJrfcGNw6iYQ%3D%3D" rel="nofollow">官方文档</a>的例子</p>
<pre><code class="javascript">path.extname('index.html')
// returns '.html'
path.extname('index.coffee.md')
// returns '.md'
path.extname('index.')
// returns '.'
path.extname('index')
// returns ''
path.extname('.index')
// returns ''
</code></pre>
<h2>路径组合</h2>
<ul>
<li><p>path.join([...paths])</p></li>
<li><p>path.resolve([...paths])</p></li>
</ul>
<h3>path.join([...paths])</h3>
<p>把<code>paths</code>拼起来,然后再normalize一下。这句话反正我自己看着也是莫名其妙,可以参考下面的伪代码定义。</p>
<p>例子如下:</p>
<pre><code class="javacript">var path = require('path');
// 输出 '/foo/bar/baz/asdf'
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');</code></pre>
<p>path定义的伪代码如下:</p>
<pre><code class="javascript">module.exports.join = function(){
var paths = Array.prototye.slice.call(arguments, 0);
return this.normalize( paths.join('/') );
};</code></pre>
<h3>path.resolve([...paths])</h3>
<p>这个接口的说明有点啰嗦。你可以想象现在你在shell下面,从左到右运行一遍<code>cd path</code>命令,最终获取的绝对路径/文件名,就是这个接口所返回的结果了。</p>
<p>比如 <code>path.resolve('/foo/bar', './baz')</code> 可以看成下面命令的结果</p>
<pre><code class="bash">cd /foo/bar
cd ./baz</code></pre>
<p>更多对比例子如下:</p>
<pre><code class="javascript">var path = require('path');
// 假设当前工作路径是 /Users/a/Documents/git-code/nodejs-learning-guide/examples/2016.11.08-node-path
// 输出 /Users/a/Documents/git-code/nodejs-learning-guide/examples/2016.11.08-node-path
console.log( path.resolve('') )
// 输出 /Users/a/Documents/git-code/nodejs-learning-guide/examples/2016.11.08-node-path
console.log( path.resolve('.') )
// 输出 /foo/bar/baz
console.log( path.resolve('/foo/bar', './baz') );
// 输出 /foo/bar/baz
console.log( path.resolve('/foo/bar', './baz/') );
// 输出 /tmp/file
console.log( path.resolve('/foo/bar', '/tmp/file/') );
// 输出 /Users/a/Documents/git-code/nodejs-learning-guide/examples/2016.11.08-node-path/www/js/mod.js
console.log( path.resolve('www', 'js/upload', '../mod.js') );
</code></pre>
<h2>路径解析</h2>
<p>path.parse(path)</p>
<h2>path.normalize(filepath)</h2>
<p>从官方文档的描述来看,path.normalize(filepath) 应该是比较简单的一个API,不过用起来总是觉得没底。</p>
<p>为什么呢?API说明过于简略了,包括如下:</p>
<ul>
<li><p>如果路径为空,返回<code>.</code>,相当于当前的工作路径。</p></li>
<li><p>将对路径中重复的路径分隔符(比如linux下的<code>/</code>)合并为一个。</p></li>
<li><p>对路径中的<code>.</code>、<code>..</code>进行处理。(类似于shell里的<code>cd ..</code>)</p></li>
<li><p>如果路径最后有<code>/</code>,那么保留该<code>/</code>。</p></li>
</ul>
<p>感觉stackoverflow上一个兄弟对这个API的解释更实在,<a href="https://link.segmentfault.com/?enc=1yDwoi5oGMHSMx5t4RnQng%3D%3D.AyC5MUF2aVvmpbuqmOhhXdP8b3d8Lj%2FPvzh22CQJvxgPJp3NqBp%2BEykInJ%2BOxAUszgOXZ7lCSm9De0g5U%2BksRIxfG5AKuDLqsEoVwaQU6qV5QL6qobZ3I%2FeX8FH0QgcwwXZOXUU14kJ1%2Fs02KuQOxQ%3D%3D" rel="nofollow">原文链接</a>。</p>
<blockquote><p>In other words, path.normalize is "What is the shortest path I can take that will take me to the same place as the input"</p></blockquote>
<p>代码示例如下。建议读者把代码拷贝出来运行下,看下实际效果。</p>
<pre><code class="javascript">var path = require('path');
var filepath = '/tmp/demo/js/test.js';
var index = 0;
var compare = function(desc, callback){
console.log('[用例%d]:%s', ++index, desc);
callback();
console.log('\n');
};
compare('路径为空', function(){
// 输出 .
console.log( path.normalize('') );
});
compare('路径结尾是否带/', function(){
// 输出 /tmp/demo/js/upload
console.log( path.normalize('/tmp/demo/js/upload') );
// /tmp/demo/js/upload/
console.log( path.normalize('/tmp/demo/js/upload/') );
});
compare('重复的/', function(){
// 输出 /tmp/demo/js
console.log( path.normalize('/tmp/demo//js') );
});
compare('路径带..', function(){
// 输出 /tmp/demo/js
console.log( path.normalize('/tmp/demo/js/upload/..') );
});
compare('相对路径', function(){
// 输出 demo/js/upload/
console.log( path.normalize('./demo/js/upload/') );
// 输出 demo/js/upload/
console.log( path.normalize('demo/js/upload/') );
});
compare('不常用边界', function(){
// 输出 ..
console.log( path.normalize('./..') );
// 输出 ..
console.log( path.normalize('..') );
// 输出 ../
console.log( path.normalize('../') );
// 输出 /
console.log( path.normalize('/../') );
// 输出 /
console.log( path.normalize('/..') );
});</code></pre>
<p>感兴趣的可以看下 path.normalize(filepath) 的node源码如下:<a href="https://link.segmentfault.com/?enc=bnl6O7ZXaAmcnemFk6EFTA%3D%3D.e%2FdrvEb%2FdpOVj8xb6ro1BqPenva0roosbQ0qyXQoP%2BexQ3Xo3l2HhOhPYD1cU4rXixqVg%2BzXqHpqvuxluQuFBw%3D%3D" rel="nofollow">传送门</a></p>
<h2>文件路径分解/组合</h2>
<ul>
<li><p>path.format(pathObject):将pathObject的root、dir、base、name、ext属性,按照一定的规则,组合成一个文件路径。</p></li>
<li><p>path.parse(filepath):path.format()方法的反向操作。</p></li>
</ul>
<p>我们先来看看官网对相关属性的说明。</p>
<p>首先是linux下</p>
<pre><code class="bash">┌─────────────────────┬────────────┐
│ dir │ base │
├──────┬ ├──────┬─────┤
│ root │ │ name │ ext │
" / home/user/dir / file .txt "
└──────┴──────────────┴──────┴─────┘
(all spaces in the "" line should be ignored -- they are purely for formatting)</code></pre>
<p>然后是windows下</p>
<pre><code class="bash">┌─────────────────────┬────────────┐
│ dir │ base │
├──────┬ ├──────┬─────┤
│ root │ │ name │ ext │
" C:\ path\dir \ file .txt "
└──────┴──────────────┴──────┴─────┘
(all spaces in the "" line should be ignored -- they are purely for formatting)</code></pre>
<h3>path.format(pathObject)</h3>
<p>阅读相关API文档说明后发现,path.format(pathObject)中,pathObject的配置属性是可以进一步精简的。</p>
<p>根据接口的描述来看,以下两者是等价的。</p>
<ul>
<li><p><code>root</code> vs <code>dir</code>:两者可以互相替换,区别在于,路径拼接时,<code>root</code>后不会自动加<code>/</code>,而<code>dir</code>会。</p></li>
<li><p><code>base</code> vs <code>name+ext</code>:两者可以互相替换。</p></li>
</ul>
<pre><code class="javascript">var path = require('path');
var p1 = path.format({
root: '/tmp/',
base: 'hello.js'
});
console.log( p1 ); // 输出 /tmp/hello.js
var p2 = path.format({
dir: '/tmp',
name: 'hello',
ext: '.js'
});
console.log( p2 ); // 输出 /tmp/hello.js</code></pre>
<h3>path.parse(filepath)</h3>
<p>path.format(pathObject) 的反向操作,直接上官网例子。</p>
<p>四个属性,对于使用者是挺便利的,不过path.format(pathObject) 中也是四个配置属性,就有点容易搞混。</p>
<pre><code class="javascript">path.parse('/home/user/dir/file.txt')
// returns
// {
// root : "/",
// dir : "/home/user/dir",
// base : "file.txt",
// ext : ".txt",
// name : "file"
// }</code></pre>
<h2>获取相对路径</h2>
<p>接口:path.relative(from, to)</p>
<p>描述:从<code>from</code>路径,到<code>to</code>路径的相对路径。</p>
<p>边界:</p>
<ul>
<li><p>如果<code>from</code>、<code>to</code>指向同个路径,那么,返回空字符串。</p></li>
<li><p>如果<code>from</code>、<code>to</code>中任一者为空,那么,返回当前工作路径。</p></li>
</ul>
<p>上例子:</p>
<pre><code class="javascript">var path = require('path');
var p1 = path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
console.log(p1); // 输出 "../../impl/bbb"
var p2 = path.relative('/data/demo', '/data/demo');
console.log(p2); // 输出 ""
var p3 = path.relative('/data/demo', '');
console.log(p3); // 输出 "../../Users/a/Documents/git-code/nodejs-learning-guide/examples/2016.11.08-node-path"</code></pre>
<h2>平台相关接口/属性</h2>
<p>以下属性、接口,都跟平台的具体实现相关。也就是说,同样的属性、接口,在不同平台上的表现不同。</p>
<ul>
<li><p>path.posix:path相关属性、接口的linux实现。</p></li>
<li><p>path.win32:path相关属性、接口的win32实现。</p></li>
<li><p>path.sep:路径分隔符。在linux上是<code>/</code>,在windows上是``。</p></li>
<li><p>path.delimiter:path设置的分割符。linux上是<code>:</code>,windows上是<code>;</code>。</p></li>
</ul>
<p>注意,当使用 path.win32 相关接口时,参数同样可以使用<code>/</code>做分隔符,但接口返回值的分割符只会是``。</p>
<p>直接来例子更直观。</p>
<pre><code class="bash">> path.win32.join('/tmp', 'fuck')
'\\tmp\\fuck'
> path.win32.sep
'\\'
> path.win32.join('\tmp', 'demo')
'\\tmp\\demo'
> path.win32.join('/tmp', 'demo')
'\\tmp\\demo'</code></pre>
<h3>path.delimiter</h3>
<p>linux系统例子:</p>
<pre><code class="bash">console.log(process.env.PATH)
// '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin'
process.env.PATH.split(path.delimiter)
// returns ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin']</code></pre>
<p>windows系统例子:</p>
<pre><code class="bash">console.log(process.env.PATH)
// 'C:\Windows\system32;C:\Windows;C:\Program Files\node\'
process.env.PATH.split(path.delimiter)
// returns ['C:\\Windows\\system32', 'C:\\Windows', 'C:\\Program Files\\node\\']</code></pre>
<h2>相关链接</h2>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=n%2BbfiMGrXEYVS%2B3zYxvXCQ%3D%3D.kmQpwCbP7Q4CTRxH4KG8mCZCADzzC81y6LJ3Ep8Mf4NwiD45f9menNbyWthPNh2f" rel="nofollow">https://nodejs.org/api/path.h...</a></p>
Nodejs进阶:基于express+multer的文件上传
https://segmentfault.com/a/1190000007412310
2016-11-08T08:23:43+08:00
2016-11-08T08:23:43+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
2
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=Y5Ihbnqcdxna7k9NPKXrlQ%3D%3D.kgJGXZUUpqcOM5mVo6csPDtgRYLlBothNZmTtWQrxAuIVhQugI%2FBWsY6xdk67bhRHm%2B6WOGH6uxePFtCwqAPGw%3D%3D" rel="nofollow">github主页地址</a>。欢迎加群交流,群号 197339705。</p></blockquote>
<h2>概览</h2>
<p>图片上传是web开发中经常用到的功能,node社区在这方面也有了相对完善的支持。</p>
<p>常用的开源组件有<strong>multer</strong>、<strong>formidable</strong>等,借助这两个开源组件,可以轻松搞定图片上传。</p>
<p>本文主要讲解以下内容,后续章节会对技术实现细节进行深入挖掘。本文所有例子均有代码示例,可在<a>这里</a>查看。</p>
<ul>
<li><p>基础例子:借助express、multer实现单图、多图上传。</p></li>
<li><p>常用API:获取上传的图片的信息。</p></li>
<li><p>进阶使用:自定义保存的图片路径、名称。</p></li>
</ul>
<h2>关于作者</h2>
<p>程序猿小卡,前腾讯IMWEB团队成员,阿里云栖社区专家博主。欢迎加入 Express前端交流群(197339705)。</p>
<p>正在填坑:<a href="https://link.segmentfault.com/?enc=NO1z%2B9qnGywMkk1WlT7TfQ%3D%3D.6%2BwgzhuOzK9NmOjKB2ASqlJhG0gPmndEw0hm%2BqnMprBXy1Y6FeynmH69Zyq%2Fo%2FFA" rel="nofollow">《Nodejs学习笔记》</a> / <a href="https://link.segmentfault.com/?enc=jj%2FoZdis1%2Fir5pg08edMXQ%3D%3D.IgId00qgxlxEtTEAmzQ2Dm3S%2B86hrsw77w8hpNu0Z%2B06OpK1iK1googkgc56KSGwfbpIh7mO9y7uYe0EGZZJRg%3D%3D" rel="nofollow">《Express学习笔记》</a> </p>
<p>社区链接:<a href="https://link.segmentfault.com/?enc=lx26dGClzbcWOCGCJEq%2Fcw%3D%3D.aG8C5l6T2aKMDQpzoaGCuJhb5qsrDz7ga1Nt4K95bRNbmMyiU1uVE1sw5yqwz3t2lOh4besQnumiBV18zC%2BJTh%2BuNhXJtcijGY%2BxCKvt1HoAtpzRjEQ113IUAVpol%2By6" rel="nofollow">云栖社区</a> / <a href="https://link.segmentfault.com/?enc=CFe1nSOxezsvQtszs57u0w%3D%3D.vTymOcbmIP1CH%2BN2Ye%2BPRdy%2FWQj%2BQlovlwTSm9fkois%3D" rel="nofollow">github</a> / <a href="https://link.segmentfault.com/?enc=7t%2Bo8v8G4l27DghHV3T%2F4Q%3D%3D.lftVLyJJErusJ7kdgEy7JxQxr88n6yn8UrJtIKwAeW5i4shObl%2FrrENnWG7WRaPf" rel="nofollow">新浪微博</a> / <a href="https://link.segmentfault.com/?enc=c4C%2B5RZnBsHCEsP2zhAjRQ%3D%3D.PoY40s8HDmkoSuhEAYG4jneQxGFG2GwgnadyF1yvEwcfqI3iDhOXF3efiRMpoSoy" rel="nofollow">知乎</a> / <a href="https://segmentfault.com/u/chyingp">Segmentfault</a> / <a href="https://link.segmentfault.com/?enc=ReKgEwigQiTsbcp9V%2FIgCw%3D%3D.x2hciQuk7%2BJIjlxAtSrz97%2B%2BolyGbuOYjmozOw%2BIEUM%3D" rel="nofollow">博客园</a> / <a href="https://link.segmentfault.com/?enc=ATBtMz1q5p0ci%2B5pAw5DJQ%3D%3D.qd%2BaK0Su84y3opGAk9b5j%2BzkkfgZfyG2fO4LANXzPL12MFpqCNY%2BnSEaPCdIE0RI" rel="nofollow">站酷</a></p>
<h2>环境初始化</h2>
<p>非常简单,一行命令。</p>
<pre><code class="bash">npm install express multer multer --save</code></pre>
<p>每个示例下面,都有下面两个文件</p>
<pre><code class="bash">➜ upload-custom-filename git:(master) ✗ tree -L 1
.
├── app.js # 服务端代码,用来处理文件上传请求
├── form.html # 前端页面,用来上传文件</code></pre>
<h2>基础例子:单图上传</h2>
<p>完整示例代码请参考<a>这里</a>。</p>
<p><a>app.js</a>。</p>
<pre><code class="javascript">var fs = require('fs');
var express = require('express');
var multer = require('multer')
var app = express();
var upload = multer({ dest: 'upload/' });
// 单图上传
app.post('/upload', upload.single('logo'), function(req, res, next){
res.send({ret_code: '0'});
});
app.get('/form', function(req, res, next){
var form = fs.readFileSync('./form.html', {encoding: 'utf8'});
res.send(form);
});
app.listen(3000);
</code></pre>
<p><a>form.html</a>。</p>
<pre><code class="html"><form action="/upload-single" method="post" enctype="multipart/form-data">
<h2>单图上传</h2>
<input type="file" name="logo">
<input type="submit" value="提交">
</form></code></pre>
<p>运行服务。</p>
<pre><code class="bash">node app.js</code></pre>
<p>访问 <a href="https://link.segmentfault.com/?enc=SCljgOl9W6FFoKhh8jNixA%3D%3D.dYIOGGM%2Fr6jrdZ9ZQ1ufgRKNKCYeammUppdE5PIUuII%3D" rel="nofollow">http://127.0.0.1:3000/form</a> ,选择图片,点击“提交”,done。然后,你就会看到 upload 目录下多了个图片。</p>
<h2>基础例子:多图上传</h2>
<p>完整示例代码请参考<a>这里</a>。</p>
<p>代码简直不能更简单,将前面的 upload.single('logo') 改成 upload.array('logo', 2) 就行。表示:同时支持2张图片上传,并且 name 属性为 logo。</p>
<p><a>app.js</a>。</p>
<pre><code class="javascript">var fs = require('fs');
var express = require('express');
var multer = require('multer')
var app = express();
var upload = multer({ dest: 'upload/' });
// 多图上传
app.post('/upload', upload.array('logo', 2), function(req, res, next){
res.send({ret_code: '0'});
});
app.get('/form', function(req, res, next){
var form = fs.readFileSync('./form.html', {encoding: 'utf8'});
res.send(form);
});
app.listen(3000);
</code></pre>
<p><a>form.html</a>。</p>
<pre><code class="html"><form action="/upload-multi" method="post" enctype="multipart/form-data">
<h2>多图上传</h2>
<input type="file" name="logos">
<input type="file" name="logos">
<input type="submit" value="提交">
</form></code></pre>
<p>同样的测试步骤,不赘述。</p>
<h2>获取上传的图片的信息</h2>
<p>完整示例代码请参考<a>这里</a>。</p>
<p>很多时候,除了将图片保存在服务器外,我们还需要做很多其他事情,比如将图片的信息存到数据库里。</p>
<p>常用的信息比如原始文件名、文件类型、文件大小、本地保存路径等。借助multer,我们可以很方便的获取这些信息。</p>
<p>还是单文件上传的例子,此时,multer会将文件的信息写到 req.file 上,如下代码所示。</p>
<p><a>app.js</a>。</p>
<pre><code class="javascript">var fs = require('fs');
var express = require('express');
var multer = require('multer')
var app = express();
var upload = multer({ dest: 'upload/' });
// 单图上传
app.post('/upload', upload.single('logo'), function(req, res, next){
var file = req.file;
console.log('文件类型:%s', file.mimetype);
console.log('原始文件名:%s', file.originalname);
console.log('文件大小:%s', file.size);
console.log('文件保存路径:%s', file.path);
res.send({ret_code: '0'});
});
app.get('/form', function(req, res, next){
var form = fs.readFileSync('./form.html', {encoding: 'utf8'});
res.send(form);
});
app.listen(3000);</code></pre>
<p><a>form.html</a>。</p>
<pre><code class="html"><form action="/upload" method="post" enctype="multipart/form-data">
<h2>单图上传</h2>
<input type="file" name="logo">
<input type="submit" value="提交">
</form></code></pre>
<p>启动服务,上传文件后,就会看到控制台下打印出的信息。</p>
<pre><code class="bash">文件类型:image/png
原始文件名:1.png
文件大小:18379
文件保存路径:upload/b7e4bb22375695d92689e45b551873d9</code></pre>
<h2>自定义文件上传路径、名称</h2>
<p>有的时候,我们想要定制文件上传的路径、名称,multer也可以方便的实现。</p>
<h3>自定义本地保存的路径</h3>
<p>非常简单,比如我们想将文件上传到 my-upload 目录下,修改下 dest 配置项就行。</p>
<pre><code>var upload = multer({ dest: 'upload/' });</code></pre>
<p>在上面的配置下,所有资源都是保存在同个目录下。有时我们需要针对不同文件进行个性化设置,那么,可以参考下一小节的内容。</p>
<h3>自定义本地保存的文件名</h3>
<p>完整示例代码请参考<a>这里</a>。</p>
<p>代码稍微长一点,单同样简单。multer 提供了 <strong>storage</strong> 这个参数来对资源保存的路径、文件名进行个性化设置。</p>
<p>使用注意事项如下:</p>
<ul>
<li><p>destination:设置资源的保存路径。注意,如果没有这个配置项,默认会保存在 /tmp/uploads 下。此外,路径需要自己创建。</p></li>
<li><p>filename:设置资源保存在本地的文件名。</p></li>
</ul>
<p><a>app.js</a>。</p>
<pre><code>var fs = require('fs');
var express = require('express');
var multer = require('multer')
var app = express();
var createFolder = function(folder){
try{
fs.accessSync(folder);
}catch(e){
fs.mkdirSync(folder);
}
};
var uploadFolder = './upload/';
createFolder(uploadFolder);
// 通过 filename 属性定制
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadFolder); // 保存的路径,备注:需要自己创建
},
filename: function (req, file, cb) {
// 将保存文件名设置为 字段名 + 时间戳,比如 logo-1478521468943
cb(null, file.fieldname + '-' + Date.now());
}
});
// 通过 storage 选项来对 上传行为 进行定制化
var upload = multer({ storage: storage })
// 单图上传
app.post('/upload', upload.single('logo'), function(req, res, next){
var file = req.file;
res.send({ret_code: '0'});
});
app.get('/form', function(req, res, next){
var form = fs.readFileSync('./form.html', {encoding: 'utf8'});
res.send(form);
});
app.listen(3000);</code></pre>
<p><a>form.html</a>。</p>
<pre><code class="html"><form action="/upload" method="post" enctype="multipart/form-data">
<h2>单图上传</h2>
<input type="file" name="logo">
<input type="submit" value="提交">
</form></code></pre>
<p>测试步骤不赘述,访问一下就知道效果了。</p>
<h2>写在后面</h2>
<p>本文对multer的基础用法进行了介绍,并未涉及过多原理性的东西。俗话说 <strong>授人以渔不如授人以渔</strong>,在后续的章节里,会对文件上传的细节进行挖掘,好让读者朋友对文件上传加深进一步的认识。</p>
<h2>相关链接</h2>
<p>multer官方文档:<a href="https://link.segmentfault.com/?enc=gbVBMpce%2BtUG1efKfUQu9w%3D%3D.%2Fhb7KPNiTQf48vsJeAuFqpAWa8lPvU2bP3Zop02Q5YnZc8Adsk7FwcU%2BCGkb12%2Bm" rel="nofollow">https://github.com/expressjs/...</a></p>
Node基础:url查询参数解析之querystring
https://segmentfault.com/a/1190000007400421
2016-11-07T08:26:48+08:00
2016-11-07T08:26:48+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=3i79w7E7MRDDjmyEHCqEZQ%3D%3D.ccPJn5NEVrwrc3o0QP%2BpvPBzJHf1sY1i2Vu0kHIBVJiJQVxt2IIq%2FIzzT657JbcRlYG7uvMtuDv1kWRKL6pTEA%3D%3D" rel="nofollow">github主页地址</a>。</p></blockquote>
<h2>模块概述</h2>
<p>在nodejs中,提供了querystring这个模块,用来做url查询参数的解析。在做node服务端开发的时候基本都会用到,使用非常简单,一般只需要记住 .parse()、.stringify() 两个方法就可以了。</p>
<p>模块总共有四个方法,绝大部分时,我们只会用到 <strong>.parse()</strong>、 <strong>.stringify()</strong>两个方法。剩余的方法,感兴趣的同学可自行查看文档。</p>
<ul>
<li><p><strong>.parse()</strong>:对url查询参数(字符串)进行解析,生成易于分析的json格式。</p></li>
<li><p><strong>.stringif()</strong>:跟<strong>.parse()</strong>相反,用于拼接查询查询。</p></li>
</ul>
<pre><code class="javascript">querystring.parse(str[, sep[, eq[, options]]])
querystring.stringify(obj[, sep[, eq[, options]]])</code></pre>
<h2>查询参数解析:querystring.parse()</h2>
<blockquote><p>参数:querystring.parse(str[, sep[, eq[, options]]])</p></blockquote>
<p>第四个参数几乎不会用到,直接不讨论. 第二个, 第三个其实也很少用到,但某些时候还是可以用一下。直接看例子</p>
<pre><code class="javascript">var querystring = require('querystring');
var str = 'nick=casper&age=24';
var obj = querystring.parse(str);
console.log(JSON.stringify(obj, null, 4));</code></pre>
<p>输出如下</p>
<pre><code class="javascript">{
"nick": "casper",
"age": "24"
}</code></pre>
<p>再来看下<code>sep</code>、<code>eq</code>有什么作用。相当于可以替换<code>&</code>、<code>=</code>为自定义字符,对于下面的场景来说还是挺省事的。</p>
<pre><code class="javascript">var str1 = 'nick=casper&age=24&extra=name-chyingp|country-cn';
var obj1 = querystring.parse(str1);
var obj2 = querystring.parse(obj1.extra, '|', '-');
console.log(JSON.stringify(obj2, null, 4));</code></pre>
<p>输出如下</p>
<pre><code class="javascript">{
"name": "chyingp",
"country": "cn"
}</code></pre>
<h2>查询参数拼接:querystring.stringify()</h2>
<blockquote><p>querystring.stringify(obj[, sep[, eq[, options]]])</p></blockquote>
<p>没什么好说的,相当于<code>parse</code>的逆向操作。直接看代码</p>
<pre><code class="javascript">var querystring = require('querystring');
var obj1 = {
"nick": "casper",
"age": "24"
};
var str1 = querystring.stringify(obj1);
console.log(str1);
var obj2 = {
"name": "chyingp",
"country": "cn"
};
var str2 = querystring.stringify(obj2, '|', '-');
console.log(str2);</code></pre>
<p>输出如下</p>
<pre><code class="javascript">nick=casper&age=24
name-chyingp|country-cn</code></pre>
<h2>相关链接</h2>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=blQMNp26C4OwTmkffN8Kjw%3D%3D.i9WBu3rgg71X757O1YTvVhJLDK9ZVmklkd54dJ8BL8%2BGtwt4QRYP48MAjdiDLQsi" rel="nofollow">https://nodejs.org/api/querys...</a></p>
Node基础:域名解析DNS(ok)
https://segmentfault.com/a/1190000007385633
2016-11-04T19:01:45+08:00
2016-11-04T19:01:45+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=xZElSc9BPbpUSpvliDEQJQ%3D%3D.SS5oloy2j7oJRg4dqwUszzo%2BzIqfsFcPhh4AQKLkVRaqsCTFzt%2F%2FMOgGFcHQSTWMCp3uX%2BscRr5VNl5cUoEGEw%3D%3D" rel="nofollow">github主页地址</a>。</p></blockquote>
<h2>写在前面</h2>
<p>Nodejs学习手册,基础总结之DNS模块。对从事web开发的同学来说,DNS解析再熟悉不过,在nodejs中也有一个模块可以完成dns解析的工作,使用非常简单。直接进入主题。</p>
<h2>域名解析:dns.lookup()</h2>
<p>比如我们要查询域名 www.qq.com 对应的ip,可以通过 dns.lookup() 。</p>
<pre><code class="javascript">var dns = require('dns');
dns.lookup('www.qq.com', function(err, address, family){
if(err) throw err;
console.log('例子A: ' + address);
});</code></pre>
<p>输出如下:</p>
<pre><code class="bash">例子A: 182.254.34.74</code></pre>
<p>我们知道,同一个域名,可能对应多个不同的ip。那么,如何获取一个域名对应的多个ip呢?可以这样。</p>
<pre><code class="javascript">var dns = require('dns');
var options = {all: true};
dns.lookup('www.qq.com', options, function(err, address, family){
if(err) throw err;
console.log('例子B: ' + address);
});</code></pre>
<p>输出如下:</p>
<pre><code class="bash">例子B: [{"address":"182.254.34.74","family":4},{"address":"240e:e1:8100:28::2:16","family":6}]</code></pre>
<h2>域名解析:dns.resolve4()</h2>
<p>上文的例子,也可以通过 dns.resolve4() 来实现。</p>
<pre><code class="javascript">var dns = require('dns');
dns.resolve4('id.qq.com', function(err, address){
if(err) throw err;
console.log( JSON.stringify(address) );
});</code></pre>
<p>输出如下:</p>
<pre><code class="bash">["61.151.186.39","101.227.139.179"]</code></pre>
<p>如果要获取IPv6的地址,接口也差不多,不赘述。</p>
<h2>dns.lookup()跟dns.resolve4()的区别</h2>
<p>从上面的例子来看,两个方法都可以查询域名的ip列表。那么,它们的区别在什么地方呢?</p>
<p>可能最大的差异就在于,当配置了本地Host时,是否会对查询结果产生影响。</p>
<ul>
<li><p>dns.lookup():有影响。</p></li>
<li><p>dns.resolve4():没有影响。</p></li>
</ul>
<p>举例,在hosts文件里配置了如下规则。</p>
<blockquote><p>127.0.0.1 www.qq.com</p></blockquote>
<p>运行如下对比示例子,就可以看到区别。</p>
<pre><code class="javascript">var dns = require('dns');
dns.lookup('www.qq.com', function(err, address, family){
if(err) throw err;
console.log('配置host后,dns.lokup =>' + address);
});
dns.resolve4('www.qq.com', function(err, address, family){
if(err) throw err;
console.log('配置host后,dns.resolve4 =>' + address);
});</code></pre>
<p>输出如下</p>
<pre><code class="bash">➜ 2016.11.03-node-dns git:(master) ✗ node lookup-vs-resolve4.js
配置host后,dns.resolve4 =>182.254.34.74
配置host后,dns.lokup =>127.0.0.1</code></pre>
<h2>其他接口</h2>
<p>对DNS有了解的同学,应该对A记录、NS记录、CNAME等不陌生,同样可以通过相应的API进行查询,感兴趣的可以自行尝试下。</p>
<h2>相关链接</h2>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=4Mvpe39bZecmlU2fmJKTsQ%3D%3D.4YxN%2BUQdgPioIOqknZoCD5c%2FU96bKi6pCzUKLSXLDlVHcsYtG469lDGk2nl%2FQXhhcpQ3gL2h%2BOY9EjN%2FCjJ%2FTcnd9mIUc%2FjU6JwzT82yCQ4%3D" rel="nofollow">https://nodejs.org/api/dns.ht...</a></p>
Node基础:资源压缩之zlib
https://segmentfault.com/a/1190000007377389
2016-11-04T09:22:15+08:00
2016-11-04T09:22:15+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<blockquote><p>本文摘录自《Nodejs学习笔记》,更多章节及更新,请访问 <a href="https://link.segmentfault.com/?enc=2mGSIU6rYze41iolx5YAdw%3D%3D.MhomUmElpozeIFviU6zQo7U%2Bue2B8pBjHIfe9rTwJgKye18cjuKk3zpeH1a1twDTJ9FMjVhw%2Fck1abBCC6QJaQ%3D%3D" rel="nofollow">github主页地址</a>。</p></blockquote>
<h2>概览</h2>
<p>做过web性能优化的同学,对性能优化大杀器<strong>gzip</strong>应该不陌生。浏览器向服务器发起资源请求,比如下载一个js文件,服务器先对资源进行压缩,再返回给浏览器,以此节省流量,加快访问速度。</p>
<p>浏览器通过HTTP请求头部里加上<strong>Accept-Encoding</strong>,告诉服务器,“你可以用gzip,或者defalte算法压缩资源”。</p>
<blockquote><p>Accept-Encoding:gzip, deflate</p></blockquote>
<p>那么,在nodejs里,是如何对资源进行压缩的呢?答案就是<strong>Zlib</strong>模块。</p>
<h2>入门实例:简单的压缩/解压缩</h2>
<h3>压缩的例子</h3>
<p>非常简单的几行代码,就完成了本地文件的gzip压缩。</p>
<pre><code class="javascript">var fs = require('fs');
var zlib = require('zlib');
var gzip = zlib.createGzip();
var inFile = fs.createReadStream('./extra/fileForCompress.txt');
var out = fs.createWriteStream('./extra/fileForCompress.txt.gz');
inFile.pipe(gzip).pipe(out);</code></pre>
<h3>解压的例子</h3>
<p>同样非常简单,就是个反向操作。</p>
<pre><code class="javascript">var fs = require('fs');
var zlib = require('zlib');
var gunzip = zlib.createGunzip();
var inFile = fs.createReadStream('./extra/fileForCompress.txt.gz');
var outFile = fs.createWriteStream('./extra/fileForCompress1.txt');
inFile.pipe(gunzip).pipe(outFile);</code></pre>
<h2>服务端gzip压缩</h2>
<p>代码超级简单。首先判断 是否包含 <strong>accept-encoding</strong> 首部,且值为<strong>gzip</strong>。</p>
<ul>
<li><p>否:返回未压缩的文件。</p></li>
<li><p>是:返回gzip压缩后的文件。</p></li>
</ul>
<pre><code class="javascript">var http = require('http');
var zlib = require('zlib');
var fs = require('fs');
var filepath = './extra/fileForGzip.html';
var server = http.createServer(function(req, res){
var acceptEncoding = req.headers['accept-encoding'];
var gzip;
if(acceptEncoding.indexOf('gzip')!=-1){ // 判断是否需要gzip压缩
gzip = zlib.createGzip();
// 记得响应 Content-Encoding,告诉浏览器:文件被 gzip 压缩过
res.writeHead(200, {
'Content-Encoding': 'gzip'
});
fs.createReadStream(filepath).pipe(gzip).pipe(res);
}else{
fs.createReadStream(filepath).pipe(res);
}
});
server.listen('3000');</code></pre>
<h2>服务端字符串gzip压缩</h2>
<p>代码跟前面例子大同小异。这里采用了<strong>slib.gzipSync(str)</strong>对字符串进行gzip压缩。</p>
<pre><code class="javascript">var http = require('http');
var zlib = require('zlib');
var responseText = 'hello world';
var server = http.createServer(function(req, res){
var acceptEncoding = req.headers['accept-encoding'];
if(acceptEncoding.indexOf('gzip')!=-1){
res.writeHead(200, {
'content-encoding': 'gzip'
});
res.end( zlib.gzipSync(responseText) );
}else{
res.end(responseText);
}
});
server.listen('3000');</code></pre>
<h2>写在后面</h2>
<p>deflate压缩的使用也差不多,这里就不赘述。更多详细用法可参考<a href="https://link.segmentfault.com/?enc=0oU1vOkWv%2BqSgeEh%2BPlBWw%3D%3D.P0Qn9jb2PCByRObHVSRCTw%2F%2BaBlIOCwJDxrp2MOAaN3zJbBqPfDtkorUX2lWhCP3Z5UfUZqFP0VIfpZ14CUt5A%3D%3D" rel="nofollow">官方文档</a>。</p>
web性能优化之:no-cache与must-revalidate深入探究
https://segmentfault.com/a/1190000007317481
2016-10-29T09:03:25+08:00
2016-10-29T09:03:25+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
54
<h2>引言</h2>
<p>稍微了解HTTP协议的前端同学,想必对<code>Cache-Control</code>不会感到陌生,性能优化时经常都会跟它打交道。</p>
<p>常见的值有有<code>private</code>、<code>public</code>、<code>no-store</code>、<code>no-cache</code>、<code>must-revalidate</code>、<code>max-age</code>等。</p>
<p>各个取值所代表的含义,网上总结挺多的,这里就不打算再进行逐一介绍,感兴趣的可以一起探讨交流。</p>
<p>本文仅挑<code>no-cache</code>、<code>must-revalidate</code> 这两个进行值进行探究对比。在项目实践中,这两个值用的比较多,也比较容易搞混。</p>
<blockquote><p>Cache-Control: no-cache<br>Cache-Control: max-age=60, must-revalidate</p></blockquote>
<p>传送门:<a href="https://link.segmentfault.com/?enc=fho%2BlhWk0kLl4SD8WRkFfQ%3D%3D.udNxSbBIFGzzQsr9EF6XqPmDG4TF0fB31pOw3iNTg42UL0%2FVDsTjdIgj76hhor2HDvJ%2B6kYdJErICZyTu%2BzetQ%3D%3D" rel="nofollow">RFC2616关于Cache-Control首部的介绍</a>。</p>
<h2>no-cache、must-revalidate简介</h2>
<ul>
<li>
<code>no-cache</code>: 告诉浏览器、缓存服务器,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验。</li>
<li>
<code>must-revalidate</code>:告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。</li>
</ul>
<p>上面的介绍涉及三个主体:<strong>浏览器</strong>、<strong>缓存服务器</strong>、<strong>源服务器</strong>。下面小节会简单进行介绍。</p>
<h2>浏览器、缓存服务器、源服务器</h2>
<ul>
<li>浏览器:资源请求直接发起方。</li>
<li>源服务器:资源实际提供方。</li>
<li>缓存服务器:在浏览器、源服务器之间架设的中间服务器,由它代替浏览器,向源服务器发起资源请求;</li>
</ul>
<p>缓存服务器作用如下。缓存服务器不是必须的,浏览器可也可与源服务器直接通信。</p>
<blockquote><p>加速资源访问速度,降低源服务器的负载。缓存服务器从源服务器获取资源,并返回给浏览器。此外,缓存服务器一般还会在本地保存资源的副本,当有相同的资源请求到来,缓存服务器可返回资源副本,以此提高资源访问速度。</p></blockquote>
<p><img src="/img/bVERLo?w=1460&h=280" alt="图片描述" title="图片描述"></p>
<h2>对比测试场景、环境准备</h2>
<h3>对比测试场景</h3>
<p>下文会通过以下两种场景的对比测试,来探究<code>no-cache</code>、<code>must-revalidate</code>的区别。</p>
<ol>
<li>浏览器 直接访问 源服务器。</li>
<li>浏览器 通过 缓存服务器,间接访问 源服务器。</li>
</ol>
<h3>环境准备</h3>
<ul>
<li>操作系统:OSX 10.11.4</li>
<li>浏览器:Chrome 52.0.2743.116 (64-bit)、Firefox 49.0.2</li>
<li>缓存服务器:Squid 3.6</li>
<li>源服务器:Express 4.14.0</li>
</ul>
<p>1、下载实验代码:可以访问<a href="https://link.segmentfault.com/?enc=KaprRri%2B0IOeub4uWymyOg%3D%3D.%2BlR8IWC8xTA3pdY6CsQBushwbIbowWQYNmfNIUO%2BGiocY58y9iI1NqTtChm04eaL" rel="nofollow">github主页</a>获取,也可通过<code>git clone</code>下载到本地。</p>
<pre><code class="powershell">git clone https://github.com/chyingp/tech-experiment.git
cd tech-experiment/2016.10.25-cache-control/
npm install</code></pre>
<p>2、安装Squid,步骤略,<a href="https://link.segmentfault.com/?enc=oRdnwz03mwY61SsPFhedjw%3D%3D.RJP3sW%2F2Io4Ee2yspEo14YDI1pQxy%2BdDWghXo%2BGsmRe0TZ5v%2FAFAN18f6me%2BQsyL" rel="nofollow">下载地址</a>。</p>
<p>3、可选:启动Squid,并将本地http代理设置为Squid的ip和端口。</p>
<blockquote><p>备注:测试场景“通过缓存服务器,间接访问源服务器资源”时,才需要这一步。</p></blockquote>
<p><img src="/img/bVERLt?w=1370&h=1080" alt="图片描述" title="图片描述"></p>
<p>4、可选:将本地代理设置为Charles的地址,然后将Charles的代理地址设置为squid的代理地址。(避免浏览器开发者工具对request header的修改,干扰实验结果)</p>
<h2>场景一:浏览器->源服务器</h2>
<p>首先,通过以下脚本启动本地服务器(源服务器)。</p>
<pre><code class="powershell">cd connect-directly
node server.js </code></pre>
<h3>Cache-Control: no-cache</h3>
<p><strong>用例1:二次访问,源服务器 上 资源 未发生变化</strong></p>
<p>访问地址为:<a href="https://link.segmentfault.com/?enc=8d7UMp4%2Fw8JiJqGiJHq8XQ%3D%3D.BRQFguj3gFS%2B7jylpXvqtDwWhFdkSML3evuDpgzUl4Q%3D" rel="nofollow">http://127.0.0.1:3000/no-cache</a></p>
<p>步骤一:第一次访问,返回内容如下。可以看到,返回了<code>Cache-Control: no-cache</code>。</p>
<p><img src="/img/bVERLx?w=1380&h=118" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Content-Length: 11
ETag: W/"b-s0vwqaICscfrawwztfPIiA"
Date: Wed, 26 Oct 2016 07:46:28 GMT
Connection: keep-alive</code></pre>
<p>步骤二:第二次访问,返回内容如下。返回状态码为<code>304 Not Modified</code>,表示经过校验,源服务器上的资源没有变化,浏览器可以采用本地副本。</p>
<p><img src="/img/bVERLE?w=1222&h=180" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 304 Not Modified
X-Powered-By: Express
Cache-Control: no-cache
ETag: W/"b-s0vwqaICscfrawwztfPIiA"
Date: Wed, 26 Oct 2016 07:47:31 GMT
Connection: keep-alive</code></pre>
<p><strong>用例2:二次访问,源服务器 上 资源 发生变化</strong></p>
<p>步骤一:访问地址为:<a href="https://link.segmentfault.com/?enc=f5c%2FXwjgh1UD4X7VqJ7%2B%2FQ%3D%3D.345yJV%2BypbqyVZgKHChT%2FHVtGlVy68%2BFnvpAAq8%2FLav%2FArlCdWXLxLonntlWrzoF" rel="nofollow">http://127.0.0.1:3000/no-cach...</a> <br>备注:<code>change=1</code>告诉源服务器,每次访问都返回不同内容</p>
<p>步骤一:第一次访问,内容如下,不赘述。</p>
<p><img src="/img/bVERLG?w=1392&h=120" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Content-Length: 11
ETag: W/"b-8n8r0vUN+mIIQCegzmqpuQ"
Date: Wed, 26 Oct 2016 07:48:01 GMT
Connection: keep-alive</code></pre>
<p>步骤二:第二次访问,返回内容如下。注意Etag变化了,表示源服务器资源已发生变化。于是状态码为<code>200 OK</code>,源服务器返回新版本的资源给浏览器。</p>
<p><img src="/img/bVERLK?w=1394&h=120" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Content-Length: 11
ETag: W/"b-0DK7Mx61dfZc1vIPJDSNSQ"
Date: Wed, 26 Oct 2016 07:48:38 GMT
Connection: keep-alive</code></pre>
<h3>Cache-Control: must-revalidate</h3>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=EJjUoFrQGYMiRcbH6Zbhiw%3D%3D.pUEnKKhimdrgYaCSeELeWefB95LftduyMr4cQeOV5jZpnrGdt%2FVGpa%2F%2Br3WaMRTA" rel="nofollow">http://127.0.0.1:3000/must-re...</a><br>可选参数说明:</p>
<ul>
<li>
<code>max-age</code>:源站返回的内容,<code>max-age</code>是多少(单位是s)。</li>
<li>
<code>change</code>:源站返回的内容,是否变化,如果是<code>1</code>,则变化。</li>
</ul>
<p><strong>用例1:二次访问,浏览器缓存未过期</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=IgBDNDjlqkAjbZITfa6tMQ%3D%3D.tnJe6SQu9%2F2KCgDQCVxQRstjJnKlMJkAUJh2CYHHSZ6wckBkcw4ZoKPWAp%2FypdXrmkx4L%2BFdtPaJTThhZ7Ulgg%3D%3D" rel="nofollow">http://127.0.0.1:3000/must-re...</a> <br>备注:<code>max-age=10</code>表示,希望资源缓存10s</p>
<p>步骤一:第一次访问,返回内容如下。</p>
<p><img src="/img/bVERLL?w=1382&h=120" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 200 OK
X-Powered-By: Express
Cache-Control: max-age=10, must-revalidate
Content-Type: text/html; charset=utf-8
Content-Length: 16
ETag: W/"10-dK948plT5cojN3y7Cy717w"
Date: Wed, 26 Oct 2016 08:06:16 GMT
Connection: keep-alive</code></pre>
<p>步骤二:第二次访问(在10s内),如下截图所示,浏览器直接从本地缓存里读取资源副本,并没有重新发起HTTP请求。</p>
<p><img src="/img/bVERLO?w=1394&h=128" alt="图片描述" title="图片描述"></p>
<p><strong>用例2:二次访问,浏览器缓存已过期,源服务器 资源未变化</strong></p>
<p>步骤一:第一次访问略过。第二次访问如下截图所示(10s后),返回<code>304 Not Modified</code>。</p>
<p><img src="/img/bVERLP?w=1392&h=118" alt="图片描述" title="图片描述"></p>
<pre><code class="http">HTTP/1.1 304 Not Modified
X-Powered-By: Express
Cache-Control: max-age=10, must-revalidate
ETag: W/"10-dK948plT5cojN3y7Cy717w"
Date: Wed, 26 Oct 2016 08:09:22 GMT
Connection: keep-alive</code></pre>
<p><strong>用例3:浏览器缓存已过期,源服务器 资源 已变化</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=STIBHWrmImrneFzQllIJlw%3D%3D.yswGnPETOXK6dL65dhPBD9Ax0eu%2BzGxTQ8bWbMlmhvP0ThzcN0vXfzoJZm2cK8fY%2Frxzf%2B%2FRYfHy5%2BrW3MOkDA%3D%3D" rel="nofollow">http://127.0.0.1:3000/must-re...</a></p>
<p>步骤一:第一次访问,截图如下。</p>
<p><img src="/img/bVERLR?w=1546&h=118" alt="图片描述" title="图片描述"></p>
<p>步骤二:第二次访问(10s后),返回截图如下,可以看到返回了<code>200</code>。</p>
<p><img src="/img/bVERLW?w=1376&h=120" alt="图片描述" title="图片描述"></p>
<h2>场景2:浏览器->缓存服务器->源服务器</h2>
<p>从上面的对比实验已经知道,在不经过缓存服务器的情况下,<code>no-cache</code>、<code>must-revalidate</code>在缓存校验方面的差别。</p>
<p>接下来,我们再看下,引入缓存服务器后,二者表现的差异点。</p>
<p>备注:下文我们会通过查看<code>Squid</code>的访问日志,来确认缓存服务器的行为。这里对日志中的几个关键字先粗略解释下:</p>
<ul>
<li>TCP_MISS:没有命中缓存。有可能是缓存服务器不存在资源的副本,也有可能资源副本已过期。</li>
<li>TCP_MEM_HIT:命中了缓存。缓存服务器存在资源的副本,并且副本未过期。</li>
</ul>
<p>再次贴上之前的图。</p>
<p><img src="/img/bVERLo?w=1460&h=280" alt="图片描述" title="图片描述"></p>
<h3>Cache-Control: no-cache</h3>
<p><strong>用例1:chrome第一次访问资源</strong></p>
<p>chrome访问截图如下:<code>200 ok</code></p>
<p><img src="/img/bVERLX?w=1382&h=124" alt="图片描述" title="图片描述"></p>
<p>squid日志:TCP_MISS,表示没有命中本地资源副本。</p>
<pre><code class="accesslog">1477501799.573 17 127.0.0.1 TCP_MISS/200 299 GET http://127.0.0.1:3000/no-cache - HIER_DIRECT/127.0.0.1 text/html</code></pre>
<p><strong>用例2:chrome再次访问该资源。且源服务器上,该资源未变化</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=egwNzyscFi0OjYgogG3F%2Fw%3D%3D.iVYop%2FPgg1AGUp8oM%2FgLMZCjJH32%2FcGVqUeBcR9wU7I%3D" rel="nofollow">http://127.0.0.1:3000/no-cache</a></p>
<p>第一次访问略。第二次访问,chrome访问截图如下:</p>
<p><img src="/img/bVERLY?w=1394&h=116" alt="图片描述" title="图片描述"></p>
<p>squid访问日志如下:TCP_MISS/304 。表示缓存服务器 联系了 源服务器,发现内容没变化,于是返回304。</p>
<pre><code class="accesslog">1477501987.785 1 127.0.0.1 TCP_MISS/304 238 GET http://127.0.0.1:3000/no-cache - HIER_DIRECT/127.0.0.1 -</code></pre>
<p><strong>用例3:chrome再次访问该资源。且源服务器上,该资源已变化</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=dQ%2FOrqP3j2Zrv5b3B%2BzSgg%3D%3D.BshoCAhqGLTE1wSZUQgCj9IRojhKaJDE9EOSMOZ0e4WU2YcdWrxAxCY7Fva75TQV" rel="nofollow">http://127.0.0.1:3000/no-cach...</a> <br>备注:<code>change=1</code> 表示强制每次访问源服务器,返回的资源都是新的。</p>
<p>第一次访问略。第二次访问,chrome截图如下,状态码为<code>200</code>。</p>
<p><img src="/img/bVERLZ?w=1390&h=122" alt="图片描述" title="图片描述"></p>
<p>从squid日志来看,缓存服务器 访问 源服务器,并返回<code>200</code>给浏览器。</p>
<pre><code class="accesslog">1477647837.216 1 127.0.0.1 TCP_MISS/200 299 GET http://127.0.0.1:3000/no-cache? - HIER_DIRECT/127.0.0.1 text/html</code></pre>
<h3>Cache-Control: must-revalidate</h3>
<p><strong>用例1:缓存服务器 已存在 资源副本,且该资源副本 未过期</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=%2FeutqMBgCxv23SVa2H8Z2Q%3D%3D.m%2FVAx3QM%2BW2LMTzsGwgbt0Vg4b4KBdKT4b1JsB6%2FCo%2BXav%2BlUPLYsasJiCO3RTurLkSRTZYU3kxwkykCbZK5wg%3D%3D" rel="nofollow">http://127.0.0.1:3000/must-re...</a><br>备注:<code>max-age=900</code>表示资源有效期是900s</p>
<p>步骤一:</p>
<p>chrome第一次访问 该资源,缓存服务器上没有该资源副本,于是访问源服务器。最终,缓存服务器给浏览器返回200。此时,缓存服务器squid上有了资源的副本。</p>
<p>步骤二:</p>
<p>firefox第一次访问 该资源(900s内)。缓存服务器上已有该资源副本,且该副本未过期。于是,缓存服务器给firefox返回该资源副本,且状态码为200。(缓存命中)</p>
<p>为了验证步骤二中,缓存服务器 返回的是本地资源的副本,查看squid日志。其中,第二条就是firefox的访问记录,<code>TCP_MEM_HIT/200</code>表示命中本地缓存。</p>
<pre><code class="accesslog">1477648947.594 5 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html
1477649012.625 0 127.0.0.1 TCP_MEM_HIT/200 333 GET http://127.0.0.1:3000/must-revalidate? - HIER_NONE/- text/html</code></pre>
<p><strong>用例2:缓存服务器 已存在 资源副本,该资源副本已过期,但源服务器上 资源未改变</strong></p>
<p>访问链接:<a href="https://link.segmentfault.com/?enc=Q4tm%2B2olsx%2FOYRDxe5G85w%3D%3D.Ndt5QAfVPULe1tjGRpKFxrL65MNUFJvTrAWr%2FGWvWsYDOGuFhasbnBOxd0PW7cjROtxVAkbO2F%2BwR4646TX8%2Bw%3D%3D" rel="nofollow">http://127.0.0.1:3000/must-re...</a></p>
<p>用chrome先后访问该资源,其间间隔超过10s。第二次访问时,chrome收到响应如下。</p>
<p><img src="/img/bVERL1?w=1218&h=118" alt="图片描述" title="图片描述"></p>
<p>查看squid日志。可以看到,状态为<code>TCP_MISS/304</code>,表示本地副本已过期,跟源服务器进行校验,发现源服务器上资源未改变。于是,给浏览器返回304。</p>
<pre><code class="accesslog">1477649429.105 11 127.0.0.1 TCP_MISS/304 258 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 -</code></pre>
<p><strong>用例3:缓存服务器 已存在 资源副本,该资源副本 已过期,但源服务器上 资源已改变</strong></p>
<p>访问地址:<a href="https://link.segmentfault.com/?enc=rUC2vMISuqIBqUaloJ1JJQ%3D%3D.4eaZVmRQfZLhm6agUfcautXb0Z6WSMfLZRmdBKfztDgxntaYJtNHc0Vo1VN6k7Ku5M9oq0WVSd5P2qv%2Bi9MuOA%3D%3D" rel="nofollow">http://127.0.0.1:3000/must-re...</a></p>
<p>用chrome先后访问该资源,其间间隔超过10s。第二次访问时,chrome收到响应如下</p>
<p><img src="/img/bVERL2?w=1392&h=122" alt="图片描述" title="图片描述"></p>
<p>squid日志如下,状态都是<code>TCP_MISS/200</code>,表示没有命中缓存。</p>
<pre><code class="accesslog">1477650702.807 8 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html
1477651020.516 4 127.0.0.1 TCP_MISS/200 325 GET http://127.0.0.1:3000/must-revalidate? - HIER_DIRECT/127.0.0.1 text/html</code></pre>
<h2>对比结论</h2>
<p>以下针对的都是浏览器第n次访问资源。(n>1)</p>
<h3>不考虑缓存服务器</h3>
<table>
<thead><tr>
<th align="left">首部</th>
<th align="center">本地缓存是否过期</th>
<th align="center">源服务器资源是否改变</th>
<th align="center">是否重新校验</th>
<th align="center">状态码</th>
</tr></thead>
<tbody>
<tr>
<td align="left">no-cache</td>
<td align="center">不确定</td>
<td align="center">否</td>
<td align="center">是</td>
<td align="center">304</td>
</tr>
<tr>
<td align="left">no-cache</td>
<td align="center">不确定</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">200</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">否</td>
<td align="center">是/否</td>
<td align="center">否</td>
<td align="center">200(来自浏览器缓存)</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">是</td>
<td align="center">否</td>
<td align="center">是</td>
<td align="center">304</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">200</td>
</tr>
</tbody>
</table>
<h3>考虑缓存服务器</h3>
<table>
<thead><tr>
<th align="left">首部</th>
<th align="center">本地缓存是否过期</th>
<th align="center">缓存服务器副本是否过期</th>
<th align="center">源服务器资源是否改变</th>
<th align="center">是否重新校验</th>
<th>状态码</th>
</tr></thead>
<tbody>
<tr>
<td align="left">no-cache</td>
<td align="center">不确定</td>
<td align="center">不确定</td>
<td align="center">否</td>
<td align="center">是</td>
<td>304</td>
</tr>
<tr>
<td align="left">no-cache</td>
<td align="center">不确定</td>
<td align="center">不确定</td>
<td align="center">是</td>
<td align="center">是</td>
<td>200</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">否</td>
<td align="center">是/否</td>
<td align="center">是/否</td>
<td align="center">否</td>
<td>200(来自浏览器缓存)</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">是</td>
<td align="center">否</td>
<td align="center">是/否</td>
<td align="center">是</td>
<td>304(来自缓存服务器)</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">否</td>
<td align="center">是</td>
<td>304</td>
</tr>
<tr>
<td align="left">must-revalidate</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">是</td>
<td align="center">是</td>
<td>200</td>
</tr>
</tbody>
</table>
<h2>写在后面</h2>
<p>经过一轮对比测试,发现<code>no-cache</code>、<code>must-revalidate</code>这两个值还是蛮有意思的。实际上,由于篇幅原因,这里还有一些内容尚未进行对比实验。比如:</p>
<ul>
<li>当<code>must-revalidate</code>或<code>no-cache</code>跟<code>max-stale</code>一起使用时的表现。</li>
<li>
<code>no-cache</code>跟<code>max-age=0, mustvalidate</code>的区别。</li>
<li>
<code>no-chche</code>制定具体的字段名时,跟不指明具体字段名时,缓存校验行为上的区别。</li>
<li>
<code>proxy-revalidate</code>跟<code>must-revalidate</code>的区别。</li>
<li>缓存服务器本身优化算法对实验结果的影响。</li>
</ul>
<p>对比实验过程比较枯燥繁琐,如有不严谨或错漏的地方,敬请指出 :)</p>
<p>这里留个经常会碰到的问题,供读者探讨:<code>no-cache</code>跟<code>max-age=0, mustvalidate</code>的区别。</p>
<h2>相关链接</h2>
<p>RFC2616 14.9: Cache-Control<br><a href="https://link.segmentfault.com/?enc=SdkaatQhg7OXpj7RPAdmhQ%3D%3D.MsHmqaqMQWxlGNFSU8LFhdGc8mUePeB7R1eTFxy2KWn9K7HuAx4F4Hy60rbQg5XDgsRKeAvdw6DO7dD%2FkQK3Ow%3D%3D" rel="nofollow">https://www.w3.org/Protocols/...</a></p>
前端进阶之路:如何高质量完成产品需求开发
https://segmentfault.com/a/1190000007225325
2016-10-20T12:19:05+08:00
2016-10-20T12:19:05+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
18
<h2>写在前面</h2>
<p>作为一个互联网前端老鸟,这么些年下来,做过的项目也不少。从最初的<code>我的QQ中心</code>、<code>QQ圈子</code>,到后面的<code>QQ群项目</code>、<code>腾讯课堂</code>。从几个人的项目,到近百号人的项目都经历过。</p>
<p>这期间,实现了很多的产品需求,也积累了一些经验。这里稍作总结,希望能给新入行的前端小伙伴们一些参考。</p>
<h2>做好需求的关键点</h2>
<p>要说如何做好一个需求,展开来讲,可以写好几篇文章,这里只挑重点来讲。</p>
<p>最基本的,就是把握好<code>3W</code>:what、when、how。</p>
<ul>
<li><p><code>what</code>:做什么?</p></li>
<li><p><code>when</code>:完成时间?</p></li>
<li><p><code>how</code>:如何完成?</p></li>
</ul>
<h2>需求场景假设</h2>
<p>为了下文不至于太过枯燥,这里进行需求场景的模拟,下文主要围绕这个“需求”,从what、when、how 三个点展开来讲。</p>
<blockquote><p>假设现在有个论坛的项目,产品经理小C提了个需求 “给论坛增加评论功能” 。作为 前端工程师 的小A接到需求后,该如何高质量的完成这个需求。</p></blockquote>
<ul>
<li><p>项目名称:兴趣论坛。</p></li>
<li><p>项目组主要成员:前端工程师小A,后台工程师小B,产品经理小C。</p></li>
<li><p>产品需求:给论坛增加评论功能。</p></li>
</ul>
<p>备注:此时我们脑海里浮现的应该是下面这张图。</p>
<p><img src="/img/bVEtNd?w=1298&h=734" alt="clipboard.png" title="clipboard.png"></p>
<h2>What:做什么?</h2>
<p>可能有同学要拍案而起了:Are you kidding me?不就加个评论功能吗,我还能不知道该做啥?</p>
<p>答案很残酷:<strong>是的</strong>。</p>
<p>根据过往经验,不少前端同学,包括一些前端老司机,做需求的时候,的确不知道自己究竟要做什么。导致这种情况发生的原因有哪些呢?</p>
<ol>
<li><p>产品经理:提的需求不明确。</p></li>
<li><p>前端工程师:没做好需求确认。</p></li>
</ol>
<h3>情况1:产品需求不明确</h3>
<p>说到产品需求不明确,前端的兄弟们估计可以坐一起开个诉苦大会,因为实在太常见了。典型的有“拍脑门需求”、“一句话需求”、“贴个图求照抄需求”。</p>
<p>回到之前的例子:给论坛增加个评论功能。</p>
<p>别看连原型图都贴出来了,其实这就是个典型的“需求不明确”。比如:</p>
<ul>
<li><p>是否需要支持富文本输入?</p></li>
<li><p>是否需要支持社会化分享?</p></li>
<li><p>发表评论后,评论怎么展示?</p></li>
<li><p>。。。</p></li>
</ul>
<p>也许经过一番确认,最终的需求会是下图所示。遇到这种情况,一定要做好<strong>需求确认</strong>,避免后期无意义的返工和延期。</p>
<p><img src="/img/bVEtNf?w=1294&h=1054" alt="clipboard.png" title="clipboard.png"></p>
<h3>情况2:未做好需求确认</h3>
<p>再次强调一下,无论何时,一定要做好<strong>需求确认</strong>。再有经验、再负责的产品经理,也几乎不可能提出“100%明确”的需求。</p>
<p>同样,回到上面的需求。</p>
<p>现在已经确认了,需要支持富文本输入、需要展示评论,这就够了吗?其实不够,还有很多需求细节需要进一步确认。比如:</p>
<ul>
<li><p>评论最大支持输入多少个字?(非常重要,关乎后台存储方案的设计)</p></li>
<li><p>1个中文算1个字,多少个英文字母算1个字?(产品语言、技术语言 之间的沟通转换)</p></li>
<li><p>输入内容过长,如何进行错误提示?(交互细节)</p></li>
<li><p>输入内容过长,是否允许提交评论?如允许,是对评论内容进行截断后提交?(容错)</p></li>
<li><p>用户未输入内容的情况下,评论框内默认提示文案是什么?(交互细节)</p></li>
<li><p>。。。</p></li>
</ul>
<p>可以、需要确认的内容太多,这里就不赘述。</p>
<p>看到这里,读者朋友们应该明白,为什么前面会说,几乎不存在“100%明确”的需求。</p>
<p>很多需求细节,同时也跟技术实现细节强相关,不能苛求产品经理都考虑到。这种情况下,作为开发者的我们应该主动找出问题,并与产品经理一起将细节敲定下来。</p>
<p><img src="/img/bVEtNi?w=1294&h=1108" alt="clipboard.png" title="clipboard.png"></p>
<h2>When:完成时间?</h2>
<p>一个同时有前端、后端参与的需求,精简后的需求生命周期,大概是这样的:</p>
<blockquote><p>需求提出-->开发-->联调-->提交测试->需求发布</p></blockquote>
<p>一个需求的实际发布时间,大部分时候取决于实际的<strong>开发工作量</strong>。如何评估开发工作量呢?最基本的,就是明确“做什么”,这也就是上一小节强调的内容。</p>
<p>这里我们假设:</p>
<ol>
<li><p>需求已经明确,小A的开发工作量是<code>3</code>天,小B的开发工作量是<code>3</code>天。</p></li>
<li><p>假设小A <code>9月1号</code>投入开发</p></li>
</ol>
<p>那么,是不是<code>9月3号</code>下班前需求就可以发布了?</p>
<p>答案显然是:<strong>不能</strong>。</p>
<p>要得出一个靠谱的完成时间,至少需要明确以下内容:</p>
<ul>
<li><p>前端、后台 各自的工作量。</p></li>
<li><p>前端、后台 投入研发的时间点。</p></li>
<li><p>前端、后台 联调的工作量、时间点。</p></li>
<li><p>需求提交测试的时间。</p></li>
<li><p>需求测试的工作量。</p></li>
</ul>
<p>最终,需求的完成时间点可能如下:(跟预期的出入很大)</p>
<p><img src="/img/bVEtNp?w=1772&h=246" alt="clipboard.png" title="clipboard.png"></p>
<p>对于需求完成时间的评估,实际情况远比上面说的要更复杂。比如需要考虑节假日、成员休假、多个需求并行开发、需求存在外部依赖项等。以后有机会再展开来讲。</p>
<h2>How:如何完成?</h2>
<p>完成需求容易,如果要高质量完成,那就需要费点功夫了。同样的,只挑一些重要的来讲</p>
<ul>
<li><p>明确需求、关键时间点</p></li>
<li><p>严控开发、自测、提测质量</p></li>
<li><p>及时暴露风险</p></li>
<li><p>推动解决问题</p></li>
<li><p>关注线上质量</p></li>
</ul>
<h3>明确需求/关键时间点</h3>
<p>这块的重要性,再怎么强调也不为过。前面已经讲过了,这里不再赘述。</p>
<h3>严控开发、自测、提测质量</h3>
<blockquote><p>作为一名合格的前端工程师,对自己的开发质量负责,这是最基本的要求。</p></blockquote>
<p>要时常问自己:</p>
<ul>
<li><p><strong>开发</strong>:是否严格按照需求文档完成功能的开发。</p></li>
<li><p><strong>联调</strong>:在与后台同学联调前,是否已经对照测试用例,对自己的模块进行了严格的自测。</p></li>
<li><p><strong>提测</strong>:提测前,是否已自测、联调通过;测试正式介入前,产品是否提前部署到测试环境,并进行初步的验证。</p></li>
</ul>
<p>严格把控开发、自测、提测质量,这不但是能力,更是一种负责任的态度。如果能做到这点,不单节省大家的时间,还可以让其他人觉得自己比较“靠谱”。</p>
<p><em>备注:以下截图,是笔者之前一个需求的自测用例(非完整版)。同样是评论功能,自测用例将近50个。</em></p>
<p><img src="/img/bVEtNu?w=1792&h=734" alt="clipboard.png" title="clipboard.png"></p>
<h3>及时暴露风险</h3>
<p>风险意识非常重要。在需求完成的过程中,经常会有各种意外的小插曲出现。对于前端同学,常见的有:</p>
<ul>
<li><p>视觉稿/交互稿未按时提供。</p></li>
<li><p>需求变更。</p></li>
<li><p>工作量评估不足。</p></li>
<li><p>后台接口未按时、按质完成。</p></li>
<li><p>bug有好多,但修改不及时。</p></li>
</ul>
<p>上面列举的项,都可能导致需求发布delay,要时刻要保持警惕。一旦出现可能可能导致delay的风险,要及时做好同步,准备好应对措施。</p>
<p>打个比方:</p>
<p>前面说到,小A 评估了3天的开发工作量。等到开发的第2天,发现之前工作量评估少了,至少需要4天才能完成。</p>
<p>这个时候,该怎么办呢?</p>
<p>相信不少同学都是这样处理的:咬咬牙,加加班,4天的活3天干,实在完不成了再说。</p>
<p>这样处理潜在的问题不小:</p>
<ol>
<li><p>给自己增加了过重的负担。</p></li>
<li><p>没能让问题及早的暴露解决。</p></li>
<li><p>可能打乱项目的整体节奏。</p></li>
</ol>
<p>更好的处理方式是:及时跟项目组成员同步风险,并落实确认相应解决方案。比如适当调整排期、砍掉部分优先级不高的功能等。</p>
<h3>推动解决问题</h3>
<p>对于一个职场人能力的评判,“解决问题”的能力,是很重要的一个评估标准。解决问题的能力如何体现呢?</p>
<p>举个例子,提测过程中,出现了不少bug,对于小A来说,该怎么办呢?这里分两种情况:</p>
<ul>
<li><p>bug主要是小A的。</p></li>
<li><p>bug主要是小B的。</p></li>
</ul>
<p>第一种情况很简单,自己的坑自己填,抓紧时间改bug,并做好事总结,降低后续需求的bug率。</p>
<p>第二种情况呢?如果小B比较配合,主动快速修复bug,那没什么好说的。但万一不是呢?</p>
<p>遇到这种情况,小A可能会想:“又不是我的bug,干嘛操那份闲心,需求如果delay的话,那也是小B的问题,跟我无关。”</p>
<p>可能不少同学的想法跟小A一样,这在笔者看来,略显消极,处理方式显得不够“职业化”。</p>
<p>为什么呢?</p>
<p>同在一个项目组,得要有团队意识、整体意识。需求延期,首先是所有需求相关人的责任,是要一起打板子的。然后,才会对具体的责任人进行问责。</p>
<p>回到前面的场景,小A更好的处理方式是:<strong>做好沟通工作,主动推进问题解决。</strong></p>
<ol>
<li><p>了解小B没有及时改bug的原因:有可能太忙、bug不好改、没有意识到那是自己的bug。</p></li>
<li><p>如可能,提供必要帮助:比如跟项目经理申请,这段时间小B集中精力改bug,暂不开发新需求</p></li>
<li><p>风险同步:如果小B真的不称职,尽快知会项目负责人,对小B进行批评教育,实在不行就换人。</p></li>
</ol>
<h3>关注线上质量</h3>
<p>这一点非常重要,但又是容易被忽略的一点。需求发布上线,是个重要的里程碑,但并不意味着需求的终点,还得时刻关注以下事项:</p>
<ul>
<li><p>功能是否正常运行?</p></li>
<li><p>各项指标是否正常?比如产品上报数据、性能监控数据、错误监控数据等。</p></li>
<li><p>有哪些可以优化的点?优先级多高?</p></li>
<li><p>。。。</p></li>
</ul>
<p>只管功能开发,一旦需求上线,立刻做甩手掌柜,同样是缺乏责任意识的表现。试想一下,如果你是团队的老大,你会放心把重要的需求交给一个“甩手掌柜”吗。</p>
<h2>写在后面</h2>
<p>本文中,笔者主要从一个前端工程师的角度出发,谈了一些“高质量完成需求”的经验。里面提到的不少内容,放到其他岗位也是适用的。鉴于篇幅原因,很多细节都是点到为止,并没有深入展开。</p>
<p>方法论再多,最终还是需要人去落实。作为一名前端工程师,加强责任意识,主动承担,勤于总结,做社会主义合格的接班人。</p>
解放双手:如何在本地调试远程服务器上的Node代码
https://segmentfault.com/a/1190000006825072
2016-09-06T08:30:00+08:00
2016-09-06T08:30:00+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<h2>写在前面</h2>
<p>谈到node断点调试,目前主要有三种方式,通过<code>node内置调试工具</code>、<code>通过IDE(如vscode)</code>、<code>通过node-inspector</code>,三者本质上差不多。本文着重点在于介绍 <strong>如何在本地通过node-inspector 调试远程服务器上的node代码</strong>。</p>
<p>在进入主题之前,首先会对三种调试方式进行入门讲解,方便新手理解后面的内容。至于老司机们,可以直接跳到主题去。</p>
<h2>方式一:内置debug功能</h2>
<h4>进入调试模式(在第1行断点)</h4>
<pre><code class="powershell">node debug app.js</code></pre>
<p><img src="/img/bVCNET" alt="clipboard.png" title="clipboard.png"></p>
<h4>进入调试模式(在第n行断点)</h4>
<p>比如要在第3行断点。</p>
<p>方式一:通过<code>debugger</code></p>
<p><img src="/img/bVCNE6" alt="clipboard.png" title="clipboard.png"></p>
<p>方式二:通过<code>sb(line)</code>。</p>
<p><img src="/img/bVCNE7" alt="clipboard.png" title="clipboard.png"></p>
<h4>执行下一步</h4>
<p>通过<code>next</code>命令。</p>
<p><img src="/img/bVCNE9" alt="clipboard.png" title="clipboard.png"></p>
<h4>跳到下一个断点</h4>
<p>通过<code>cont</code>命令。</p>
<p><img src="/img/bVCNFa" alt="clipboard.png" title="clipboard.png"></p>
<h4>查看某个变量的值</h4>
<p>输入<code>repl</code>命令后,再次输入变量名,就可以看到变量对应的值。如果想继续执行代码,可以按<code>ctrl+c</code>退出。</p>
<p><img src="/img/bVCNFb" alt="clipboard.png" title="clipboard.png"></p>
<h4>添加/删除watch</h4>
<ul>
<li><p>通过<code>watch(expr)</code>来添加监视对象。</p></li>
<li><p>通过<code>watchers</code>查看当前所有的监视对象。</p></li>
<li><p>通过<code>unwatch(expr)</code>来删除监视对象。</p></li>
</ul>
<p>添加watch:</p>
<p><img src="/img/bVCNFc" alt="clipboard.png" title="clipboard.png"></p>
<p>删除watch:</p>
<p><img src="/img/bVCNFd" alt="clipboard.png" title="clipboard.png"></p>
<h4>进入/跳出函数(step in、step out)</h4>
<ul>
<li><p>进入函数:通过<code>step</code>或者<code>s</code>。</p></li>
<li><p>跳出函数:通过<code>out</code>或者<code>o</code>。</p></li>
</ul>
<p>示例代码如下,假设代码运行到<code>logger(str);</code>这一行,首先跳进函数内部,再跳出函数。</p>
<pre><code>var nick = 'chyingp';
var country = 'China';
var str = nick + ' live in ' + country;
var logger = function(msg){
console.log(msg); // 这里
console.log('这行会跳过'); // 跳过这行
};
logger(str); // 假设运行到这里,想要进入logger方法
console.log(str);</code></pre>
<p>示例截图如下:</p>
<p><img src="/img/bVCNFh" alt="clipboard.png" title="clipboard.png"></p>
<h4>多个文件断点</h4>
<p>通过<code>setBreakpoint('script.js', 1), sb(...)</code>,在某个文件某一行添加断点。反正我是没成功过。。。怎么看都是bug。。。</p>
<h4>重新运行</h4>
<p>每次都退出然后<code>node debug app.js</code>相当烦。直接用<code>restart</code></p>
<p><img src="/img/bVCNFi" alt="clipboard.png" title="clipboard.png"></p>
<h4>远程调试</h4>
<p>比如远程机器ip是<code>192.168.1.126</code>,在远程机器上进入调试模式</p>
<pre><code class="powershell">[root@localhost ex]# node --debug-brk app.js
Debugger listening on port 5858</code></pre>
<p>然后,在本地机器通过<code>node debug 192.168.1.126:5858</code>连接远程机器进行调试。</p>
<pre><code class="powershell">node debug 192.168.1.126:5858</code></pre>
<p>如下:</p>
<pre><code class="powershell">➜ /tmp node debug 192.168.1.126:5858
connecting to 192.168.1.126:5858 ... ok
break in /tmp/ex/app.js:1
> 1 var Logger = require('./logger');
2
3 Logger.info('hello');
debug> n
break in /tmp/ex/app.js:3
1 var Logger = require('./logger');
2
> 3 Logger.info('hello');
4
5 });</code></pre>
<p>当然,还可以通过pid进行远程调试,这里就不举例。</p>
<p>参考:<a href="https://link.segmentfault.com/?enc=ELFXl%2FaTflv%2BdHYNM7Qnsw%3D%3D.ptbTPXjglcloqBb5gvn6ji7j%2Bhk94PxAV3DcGhPv8gI3taTfN6G0i917KjHN9vTxmOkV1mmUUhm%2FlIqm8SEo0g%3D%3D" rel="nofollow">https://nodejs.org/api/debugg...</a></p>
<h2>方式二:通过IDE(vscode)</h2>
<p>首先,在vscode里打开项目</p>
<p><img src="/img/bVCNFl" alt="clipboard.png" title="clipboard.png"></p>
<p>然后,添加调试配置。主要需要修改的是可执行文件的路径。</p>
<p><img src="/img/bVCNFm" alt="clipboard.png" title="clipboard.png"></p>
<p>点击代码左侧添加断点。</p>
<p><img src="/img/bVCNFp" alt="clipboard.png" title="clipboard.png"></p>
<p>开始调试</p>
<p><img src="/img/bVCNFr" alt="clipboard.png" title="clipboard.png"></p>
<p>顺利断点,左侧的变量、监视对象,右侧的调试工具栏,用过<code>chrome dev tool</code>的同学应该很熟悉,不赘述。</p>
<p><img src="/img/bVCNFs" alt="clipboard.png" title="clipboard.png"></p>
<h2>方式三:通过node-inspector</h2>
<p>首先,安装<code>node-inspector</code>。</p>
<pre><code class="powershell">npm install -g node-inspector</code></pre>
<h4>方式一:通过<code>node-debug</code>启动调试</h4>
<p>启动调试,它会自动帮你在浏览器里打开调试界面。</p>
<pre><code class="powershell">➜ debugger git:(master) ✗ node-debug app.js
Node Inspector v0.12.8
Visit http://127.0.0.1:8080/?port=5858 to start debugging.
Debugging `app.js`
Debugger listening on port 5858</code></pre>
<p>调试界面如下,简直不能更亲切。</p>
<p><img src="/img/bVCNFt" alt="clipboard.png" title="clipboard.png"></p>
<h4>方式二:更加灵活的方式</h4>
<p>步骤1:通过<code>node-inspector</code>启动Node Inspector Server</p>
<pre><code class="powershell">➜ debugger git:(master) ✗ node-inspector
Node Inspector v0.12.8
Visit http://127.0.0.1:8080/?port=5858 to start debugging.</code></pre>
<p>步骤2:通过传统方式启动调试。加入<code>--debug-brk</code>,好让代码在第一行断住。</p>
<pre><code class="powershell">➜ debugger git:(master) ✗ node --debug-brk app.js
Debugger listening on port 5858</code></pre>
<p>步骤3:在浏览器里打开调试UI界面。就是步骤1里打印出来的地址 <a href="https://link.segmentfault.com/?enc=dc2ZNLDgUQ97pcdP5%2B%2F8vQ%3D%3D.1HdccEwWOXIAQRbKfv4p5eOAkNx6Y5k45AtgTyTNaYaAe0xu7Wsm7q2iSnxHZfWC" rel="nofollow">http://127.0.0.1:8080/?port=5858</a>。成功</p>
<p><img src="/img/bVCNFu" alt="clipboard.png" title="clipboard.png"></p>
<h4>实现原理</h4>
<p>从上面的例子不难猜想到。(不负责任猜想)</p>
<ul>
<li><p>通过<code>node --debug-brk</code>启动调试,监听<code>5858</code>端口。</p></li>
<li><p><code>node-inspector</code>启动服务,监听8080端口。</p></li>
<li><p>在浏览器里访问<code>http://127.0.0.1:8080/?port=5858</code>。可以看到<code>port=5858</code>这个参数。结合之前讲到的node内置远程调试的功能,可以猜想,在返回UI调试界面的同时,服务内部通过<code>5858</code>端口开始了断点调试。</p></li>
</ul>
<p>另外,从下面截图可以看出,UI调试工具(其实是个网页)跟 <code>inspector服务</code> 之间通过<code>websocket</code>进行通信。</p>
<p>用户在界面上操作时,比如设置断点,就向 <code>inspector服务</code> 发送一条消息,<code>inspector服务</code> 在内部通过v8调试器来实现代码的断点。</p>
<p><img src="/img/bVCNFC" alt="clipboard.png" title="clipboard.png"></p>
<p>可以看到,用到了<code>v8-debug</code>,这个就待深挖了。</p>
<p><img src="/img/bVCNFD" alt="clipboard.png" title="clipboard.png"></p>
<h2>通过node-inspector调试远程代码</h2>
<p>细心的同学可能会发现,node远程调试其实在上面<code>node-inspector</code>章节的讲解里已经覆盖到了。这里还是来个实际的例子。</p>
<p>假设我们的node代码<code>app.js</code>运行在阿里云的服务器上,服务器ip是<code>xxx.xxx.xxx.xxx</code>。</p>
<p>首先,服务器上启动node-inspector服务</p>
<pre><code class="powershell">[root@iZ94wb7tioqZ ~]# node-inspector
Node Inspector v0.12.8
Visit http://127.0.0.1:8080/?port=5858 to start debugging.</code></pre>
<p>其次,通过<code>--debug-brk</code>参数,进入调试模式</p>
<pre><code class="powershell">[root@iZ94wb7tioqZ ex]# node --debug-brk app.js
Debugger listening on port 5858</code></pre>
<p>最后,在本地通过ip地址愉快的访问调试界面。是不是很简单捏。</p>
<p><img src="/img/bVCNFF" alt="clipboard.png" title="clipboard.png"></p>
<h4>常见问题:安全限制</h4>
<p>远程调试常见的问题就是请求被拒绝。这是服务器安全策略的限制。遇到这种情况,开放端口就完事了。</p>
<p><img src="/img/bVCNHD" alt="clipboard.png" title="clipboard.png"></p>
<p>在我们的云主机上,默认安装了<code>firewall-cmd</code>,可以通过<code>--add-port</code>选项来开放<code>8080</code>端口的开放。如果本机没有安装<code>firewall-cmd</code>,也可以通过<code>iptables</code>来实现同样的功能。</p>
<pre><code class="powershell">[root@iZ94wb7tioqZ ex]# firewall-cmd --add-port=8080/tcp
success</code></pre>
<p>然后,就可以愉快的远程调试了。</p>
<p><img src="/img/bVCNHE" alt="clipboard.png" title="clipboard.png"></p>
<h2>相关链接</h2>
<p><a href="https://link.segmentfault.com/?enc=cVuis62ujCdRWx3Z9Oo55A%3D%3D.gTilb%2BZ0aT%2FslBbPm46T6%2BVk9M93MdAQTg6EILw9WfQZPrlmpZQcm4IzRNQwp%2BYn" rel="nofollow">Node Debugger</a></p>
<p><a href="https://link.segmentfault.com/?enc=jj015oOnBxjhlb%2FUgfXmRA%3D%3D.4dywlqG%2FtfbLZ4IgCZWw7eaD8P%2BLnZRU563e5cnbbXkp0vGSJ7rL%2FS1v5GojtzWTRSKUE7OBYcmoM8VTInkKmO2Y%2BEfxQMmWzf6TOjXGTU8%3D" rel="nofollow">How Does a C Debugger Work?</a></p>
<p><a href="https://link.segmentfault.com/?enc=EsEhj%2FDjEl2rbKxl93tXqA%3D%3D.x90DH9W7rSvxM2Y%2Fv9XC8nqMoViQI8ldzvj27zIy5Q5OCgm53g%2F%2BfgNV%2B8lgE6UXYl76PBp2N7DGioSA2SVKIAw4stUtYjGfWDrgW%2BzkUPk%3D" rel="nofollow">How debuggers work: Part 2 - Breakpoints</a></p>
Express使用手记:核心入门
https://segmentfault.com/a/1190000006793550
2016-09-02T08:05:00+08:00
2016-09-02T08:05:00+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
7
<h2>入门简介</h2>
<p>Express是基于nodejs的web开发框架。优点是易上手、高性能、扩展性强。</p>
<ul>
<li><p><strong>易上手</strong>:nodejs最初就是为了开发高性能web服务器而被设计出来的,然而相对底层的API会让不少新手望而却步。express对web开发相关的模块进行了适度的封装,屏蔽了大量复杂繁琐的技术细节,让开发者只需要专注于业务逻辑的开发,极大的降低了入门和学习的成本。</p></li>
<li><p><strong>高性能</strong>:express仅在web应用相关的nodejs模块上进行了适度的封装和扩展,较大程度避免了过度封装导致的性能损耗。</p></li>
<li><p><strong>扩展性强</strong>:基于中间件的开发模式,使得express应用的扩展、模块拆分非常简单,既灵活,扩展性又强。</p></li>
</ul>
<h2>环境准备</h2>
<p>首先,需要安装nodejs,这一步请自行解决。接着,安装express的脚手架工具<code>express-generator</code>,这对于我们学习express很有帮助。</p>
<pre><code>npm install -g express-generator</code></pre>
<h2>第一个demo</h2>
<p>利用之前安装的脚手架工具,初始化我们的demo项目。</p>
<pre><code> /tmp mkdir express-demo
/tmp cd express-demo
express-demo express
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./public/javascripts
create : ./public/images
create : ./public/stylesheets
create : ./public/stylesheets/style.css
create : ./routes
create : ./routes/index.js
create : ./routes/users.js
create : ./views
create : ./views/index.jade
create : ./views/layout.jade
create : ./views/error.jade
create : ./bin
create : ./bin/www
install dependencies:
$ cd . && npm install
run the app:
$ DEBUG=express-demo:* npm start</code></pre>
<p>按照指引,安装依赖。并启动服务</p>
<pre><code>npm install</code></pre>
<p>然后,启动服务器。</p>
<pre><code> express-demo npm start
> ex1@0.0.0 start /private/tmp/ex1
> node ./bin/www</code></pre>
<p>访问浏览器,迈出成功的第一步。</p>
<p><img src="/img/remote/1460000006793553" alt="1" title="1"></p>
<h2>目录结构介绍</h2>
<p>看下demo应用的目录结构。大部分时候,我们的应用目录结构跟这个保持一致就可以了。也可以根据需要自行调整,express并没有对目录结构进行限制。</p>
<p>从目录结构可以大致看出,express应用的核心概念主要包括:<code>路由</code>、<code>中间件</code>、<code>模板引擎</code>。</p>
<pre><code class="powershell"> express-demo tree -L 1
.
├── app.js # 应用的主入口
├── bin # 启动脚本
├── node_modules # 依赖的模块
├── package.json # node模块的配置文件
├── public # 静态资源,如css、js等存放的目录
├── routes # 路由规则存放的目录
└── views # 模板文件存放的目录
5 directories, 2 files</code></pre>
<h2>核心概念简介</h2>
<p>上面提到,express主要包含三个核心概念:路由、中间件、模板引擎。</p>
<blockquote><p>注意,笔者这里用的是<code>核心概念</code>这样的字眼,而不是<code>核心模块</code>,为什么呢?这是因为,虽然express的中间件有它的定义规范,但是express的内核源码中,其实是没有所谓的<em>中间件</em>这样的模块的。</p></blockquote>
<p>言归正传,三者简要的来说就是。</p>
<ul>
<li><p><code>中间件</code>:可以毫不夸张的说,在express应用中,一切皆中间件。各种应用逻辑,如cookie解析、会话处理、日志记录、权限校验等,都是通过中间件来完成的。</p></li>
<li><p><code>路由</code>:地球人都知道,负责寻址的。比如用户发送了个http请求,该定位到哪个资源,就是路由说了算。</p></li>
<li><p><code>模板引擎</code>:负责视图动态渲染。下面会介绍相关配置,以及如何开发自己的模板引擎。</p></li>
</ul>
<h2>核心概念:路由</h2>
<h3>路由分类</h3>
<p>粗略来说,express主要支持四种类型的路由,下面会分别举例进行说明</p>
<ol>
<li><p>字符串类型</p></li>
<li><p>字符串模式类型</p></li>
<li><p>正则表达式类型</p></li>
<li><p>参数类型</p></li>
</ol>
<p>分别举例如下,细节可参考<a href="https://link.segmentfault.com/?enc=29oL2p2aYlEDY8Nn6IdHNw%3D%3D.BHS5Xm3ATNTbv%2BybyxZm56C4WiLf0XsD0yZSa99WqVpxpkTOMM9frKSIFWjHn%2Fey" rel="nofollow">官方文档</a>。</p>
<pre><code class="javascript">var express = require('express');
var app = express();
// 路由:字符串类型
app.get('/book', function(req, res, next){
res.send('book');
});
// 路由:字符串模式
app.get('/user/*man', function(req, res, next){
res.send('user'); // 比如: /user/man, /user/woman
});
// 路由:正则表达式
app.get(/animals?$/, function(req, res, next){
res.send('animal'); // 比如: /animal, /animals
});
// 路由:命名参数
app.get('/employee/:uid/:age', function(req, res, next){
res.json(req.params); // 比如:/111/30,返回 {"uid": 111, "age": 30}
});
app.listen(3000);</code></pre>
<h3>路由拆分</h3>
<p>当你用的应用越来越复杂,不可避免的,路由规则也会越来越复杂。这个时候,对路由进行拆分是个不错的选择。</p>
<p>我们分别看下两段代码,路由拆分的好处就直观的体现出来了。</p>
<p><strong>路由拆分前</strong></p>
<pre><code>var express = require('express');
var app = express();
app.get('/user/list', function(req, res, next){
res.send('/list');
});
app.get('/user/detail', function(req, res, next){
res.send('/detail');
});
app.listen(3000);</code></pre>
<p>这样的代码会带来什么问题呢?无论是新增还是修改路由,都要带着<code>/user</code>前缀,这对于代码的可维护性来说是大忌。这对小应用来说问题不大,但应用复杂度一上来就会是个噩梦。</p>
<p><strong>路由拆分后</strong></p>
<p>可以看到,通过<code>express.Router()</code>进行了路由拆分,新增、修改路由都变得极为便利。</p>
<pre><code class="powershell">var express = require('express');
var app = express();
var user = express.Router();
user.get('/list', function(req, res, next){
res.send('/list');
});
user.get('/detail', function(req, res, next){
res.send('/detail');
});
app.use('/user', user); // mini app,通常做应用拆分
app.listen(3000);</code></pre>
<h2>核心概念:中间件</h2>
<p>一般学习js的时候,我们都会听到一句话:一切皆对象。而在学习express的过程中,很深的一个感受就是:一切皆中间件。比如常见的请求参数解析、cookie解析、gzip等,都可以通过中间件来完成。</p>
<h3>工作机制</h3>
<p>贴上官网的<a href="https://link.segmentfault.com/?enc=Q5GmYGYBYIg3G6NZ54KXew%3D%3D.e9sLNQibe618548eWXBRfrc5z43J3zYoPsEUu89wKwJ8v2fNRHb5EcOB6q%2BD8J9NwZnsslyxvck%2Fo5Y4wnTN7Q%3D%3D" rel="nofollow">一张图</a>镇楼,图中所示就是传说中的中间件了。</p>
<p><img src="/img/remote/1460000006793554" alt="2" title="2"></p>
<p>首先,我们自己编写一个极简的中间件。虽然没什么实用价值,但中间件就长这样子。</p>
<ul>
<li><p><code>参数</code>:三个参数,熟悉<code>http.createServer()</code>的同学应该比较眼熟,其实就是req(客户端请求实例)、res(服务端返回实例),只不过进行了扩展,添加了一些使用方法。</p></li>
<li><p><code>next</code>:回调方法,当next()被调用时,就进入下一个中间件。</p></li>
</ul>
<pre><code class="javascript">function logger(req, res, next){
console.log('here comes request');
next();
}</code></pre>
<p>来看下实际例子:</p>
<pre><code class="javascript">var express = require('express');
var app = express();
app.use(function(req, res, next) {
console.log('1');
next();
});
app.use(function(req, res, next) {
console.log('2');
next();
});
app.use(function(req, res, next) {
console.log('3');
res.send('hello');
});
app.listen(3000);</code></pre>
<p>请求 <a href="https://link.segmentfault.com/?enc=%2BvWQg5epCbAHrQEMVigblg%3D%3D.3bNWIKxpsTTBO7DVuNk2v%2FfMGcMZy20%2FEtTFygTpCFI%3D" rel="nofollow">http://127.0.0.1:3000</a>,看下控制台输出,以及浏览器返回内容。</p>
<pre><code class="powershell"> middleware git:(master) node chains.js
1
2
3</code></pre>
<p><img src="/img/remote/1460000006793555" alt="3" title="3"></p>
<h3>应用级中间件 vs 路由级中间件</h3>
<p>根据作用范围,中间件分为两大类:</p>
<ul>
<li><p>应用级中间件</p></li>
<li><p>路由级中间件。</p></li>
</ul>
<p>两者的区别不容易说清楚,因为从本质来讲,两类中间件是完全等同的,只是使用场景不同。同一个中间件,既可以是应用级中间件、也可以是路由级中间件。</p>
<p>直接上代码可能更直观。参考下面代码,可以简单粗暴的认为:</p>
<ul>
<li><p>应用级中间件:<code>app.use()</code>、<code>app.METHODS()</code>接口中使用的中间件。</p></li>
<li><p>路由级中间件:<code>router.use()</code>、<code>router.METHODS()</code>接口中使用的中间件。</p></li>
</ul>
<pre><code>var express = require('express');
var app = express();
var user = express.Router();
// 应用级
app.use(function(req, res, next){
console.log('收到请求,地址为:' + req.url);
next();
});
// 应用级
app.get('/profile', function(req, res, next){
res.send('profile');
});
// 路由级
user.use('/list', function(req, res, next){
res.send('/user/list');
});
// 路由级
user.get('/detail', function(req, res, next){
res.send('/user/detail');
});
app.use('/user', user);
app.listen(3000);</code></pre>
<h3>开发中间件</h3>
<p>上面也提到了,中间件的开发是是分分钟的事情,不赘述。</p>
<pre><code class="javascript">function logger(req, res, next){
doSomeBusinessLogic(); // 业务逻辑处理,比如权限校验、数据库操作、设置cookie等
next(); // 如果需要进入下一个中间件进行处理,则调用next();
}</code></pre>
<h3>常用中间件</h3>
<p>包括但不限于如下。更多常用中间件,可以点击 <a href="https://link.segmentfault.com/?enc=HnXdJRBejlE60z29WxDyEg%3D%3D.%2BJJMSNS1eYJ3v2DmEpImC9whsxt6hkUSnOvsYEVsMGNrWkzMU0ZOUaIxo7y2cTuKv6oSrITnn%2BpcdcF%2Bj%2FrkpA%3D%3D" rel="nofollow">这里</a></p>
<ul>
<li><p>body-parser</p></li>
<li><p>compression</p></li>
<li><p>serve-static</p></li>
<li><p>session</p></li>
<li><p>cookie-parser</p></li>
<li><p>morgan</p></li>
</ul>
<h2>核心概念:模板引擎</h2>
<p>模板引擎大家不陌生了,关于express模板引擎的介绍可以参考<a href="https://link.segmentfault.com/?enc=H%2Bs%2BntaLkwS3%2FmFQ2JLn5g%3D%3D.TiTAWR8YXvzjIlu1uTN8O23KpQlsSD87bgFJZ08cIPlADGZ3WHiPZ41YGyuihuNraUyPNQo9eDZLcdNSVFB9Dg%3D%3D" rel="nofollow">官方文档</a>。</p>
<p>下面主要讲下使用配置、选型等方面的内容。</p>
<h3>可选的模版引擎</h3>
<p>包括但不限于如下模板引擎</p>
<ul>
<li><p>jade</p></li>
<li><p>ejs</p></li>
<li><p>dust.js</p></li>
<li><p>dot</p></li>
<li><p>mustache</p></li>
<li><p>handlerbar</p></li>
<li><p><a href="https://link.segmentfault.com/?enc=zL8TyX%2BbS0Utvmoncgt4nw%3D%3D.fRG4tQZl7OrwfPGyJVaI6KbsSvz1tEQrDBWf%2BDz4Bhm%2BF9sQO2eFl%2F82%2FFs1xSFrE%2BzTcAaL688mpu3nherNcw%3D%3D" rel="nofollow">nunjunks</a></p></li>
</ul>
<h3>配置说明</h3>
<p>先看代码。</p>
<pre><code class="javascript">// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');</code></pre>
<p>有两个关于模版引擎的配置:</p>
<ol>
<li><p><code>views</code>:模版文件放在哪里,默认是在项目根目录下。举个例子:<code>app.set('views', './views')</code></p></li>
<li><p><code>view engine</code>:使用什么模版引擎,举例:<code>app.set('view engine', 'jade')</code></p></li>
</ol>
<p>可以看到,默认是用<code>jade</code>做模版的。如果不想用<code>jade</code>怎么办呢?下面会提供一些模板引擎选择的思路。</p>
<h3>选择标准</h3>
<p>需要考虑两点:实际业务需求、个人偏好。</p>
<p>首先考虑业务需求,需要支持以下几点特性。</p>
<ul>
<li><p>支持模版继承(extend)</p></li>
<li><p>支持模版扩展(block)</p></li>
<li><p>支持模版组合(include)</p></li>
<li><p>支持预编译</p></li>
</ul>
<p>对比了下,<code>jade</code>、<code>nunjunks</code>都满足要求。个人更习惯<code>nunjunks</code>的风格,于是敲定。那么,怎么样使用呢?</p>
<h3>支持nunjucks</h3>
<p>首先,安装依赖</p>
<pre><code class="javascript">npm install --save nunjucks</code></pre>
<p>然后,添加如下配置</p>
<pre><code>var nunjucks = require('nunjucks');
nunjucks.configure('views', {
autoescape: true,
express: app
});
app.set('view engine', 'html');</code></pre>
<p>看下<code>views/layout.html</code></p>
<pre><code class="javascript"><!DOCTYPE html>
<html>
<head>
<title>
{% block title %}
layout title
{% endblock %}
</title>
</head>
<body>
<h1>
{% block appTitle %}
layout app title
{% endblock %}
</h1>
<p>正文</p>
</body>
</html></code></pre>
<p>看下<code>views/index.html</code></p>
<pre><code class="html">{% extends "layout.html" %}
{% block title %}首页{% endblock %}
{% block appTitle %}首页{% endblock %}</code></pre>
<h3>开发模板引擎</h3>
<p>通过<code>app.engine(engineExt, engineFunc)</code>来注册模板引擎。其中</p>
<ul>
<li><p>engineExt:模板文件后缀名。比如<code>jade</code>。</p></li>
<li><p>engineFunc:模板引擎核心逻辑的定义,一个带三个参数的函数(如下)</p></li>
</ul>
<pre><code class="javascript">// filepath: 模板文件的路径
// options:渲染模板所用的参数
// callback:渲染完成回调
app.engine(engineExt, function(filepath, options, callback){
// 参数一:渲染过程的错误,如成功,则为null
// 参数二:渲染出来的字符串
return callback(null, 'Hello World');
});</code></pre>
<p>比如下面例子,注册模板引擎 + 修改配置一起,于是就可以愉快的使用后缀为<code>tmpl</code>的模板引擎了。</p>
<pre><code class="javascript">app.engine('tmpl', function(filepath, options, callback){
// 参数一:渲染过程的错误,如成功,则为null
// 参数二:渲染出来的字符串
return callback(null, 'Hello World');
});
app.set('views', './views');
app.set('view engine', 'tmpl');</code></pre>
<h3>相关链接</h3>
<p>模板引擎对比:<a href="https://link.segmentfault.com/?enc=SETKnuM%2BCXHk0zDJSJ706g%3D%3D.ck8jR0P%2F%2FD0aL8jbUVQGdq4yVk1aJfpvawzazPn1Jh9amE28uu7JK5FDHmk49T5RxCR8uGMWlELB9GtTjz2O%2BhKAGEcVWf4jDtfv4JZCwssb2Qyw3eEn6orA4K2YWl9hHvegQvgwBwxdDtOL0gTjZ%2FPDvo7j94KaaFBMrfAURfQ%3D" rel="nofollow">点击这里</a></p>
<p>express模版引擎介绍:<a href="https://link.segmentfault.com/?enc=I67%2BArFTrWLV0tnPd4yemA%3D%3D.A%2FjFBV3g9GSGSM22FqZffWQQbcUbXEHFGn%2BzKP9%2FIJbeqQPVoJzlk%2FNZFFOKXxflrhf4V17lbwOmMN%2Fu6d2PDA%3D%3D" rel="nofollow">点击这里</a> </p>
<p>开发模版引擎:<a href="https://link.segmentfault.com/?enc=8tSfxdqE9f9qaYWaSQNX0w%3D%3D.j5ZkD2sjlBnZYlwE2XD6gS37Wb0yB7MszmJMi%2BXT%2B7ixrTpGZKuduDIZ%2Bz0ssGY4d56ajJfCrLbH3ss%2FTDBBxnmoTwuKvSzbQ8Mj0vbqwT8%3D" rel="nofollow">点击这里</a></p>
<h2>更多内容</h2>
<p>前面讲了一些express的入门基础,感兴趣的同学可以查看官方文档。篇幅所限,有些内容在后续文章展开,比如下面列出来的内容等。</p>
<ul>
<li><p>进程管理</p></li>
<li><p>会话管理</p></li>
<li><p>日志管理</p></li>
<li><p>性能优化</p></li>
<li><p>调试</p></li>
<li><p>错误处理</p></li>
<li><p>负载均衡</p></li>
<li><p>数据库支持</p></li>
<li><p>HTTPS支持</p></li>
<li><p>业务实践</p></li>
<li><p>。。。</p></li>
</ul>
<h2>相关链接</h2>
<p>express官网:<a href="https://link.segmentfault.com/?enc=7ULIdhGXCZcYDMRPLZ6hNw%3D%3D.0GncnLdqN9OkYimrV%2FNuWZNeWot6SUr1j9iwYxIRVCw%3D" rel="nofollow">http://expressjs.com/</a></p>
PM2实用入门指南
https://segmentfault.com/a/1190000006793571
2016-09-02T08:05:00+08:00
2016-09-02T08:05:00+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
25
<h2>简介</h2>
<p>PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。</p>
<p>下面就对PM2进行入门性的介绍,基本涵盖了PM2的常用的功能和配置。</p>
<h2>安装</h2>
<p>全局安装,简直不能更简单。</p>
<pre><code class="powershell">npm install -g pm2</code></pre>
<h2>目录介绍</h2>
<p>pm2安装好后,会自动创建下面目录。看文件名基本就知道干嘛的了,就不翻译了。</p>
<ul>
<li><p><code>$HOME/.pm2</code> will contain all PM2 related files</p></li>
<li><p><code>$HOME/.pm2/logs</code> will contain all applications logs</p></li>
<li><p><code>$HOME/.pm2/pids</code> will contain all applications pids</p></li>
<li><p><code>$HOME/.pm2/pm2.log</code> PM2 logs</p></li>
<li><p><code>$HOME/.pm2/pm2.pid</code> PM2 pid</p></li>
<li><p><code>$HOME/.pm2/rpc.sock</code> Socket file for remote commands</p></li>
<li><p><code>$HOME/.pm2/pub.sock</code> Socket file for publishable events</p></li>
<li><p><code>$HOME/.pm2/conf.js</code> PM2 Configuration</p></li>
</ul>
<h2>入门教程</h2>
<p>挑我们最爱的express应用来举例。一般我们都是通过<code>npm start</code>启动应用,其实就是调用<code>node ./bin/www</code>。那么,换成pm2就是</p>
<p>注意,这里用了<code>--watch</code>参数,意味着当你的express应用代码发生变化时,pm2会帮你重启服务,多贴心。</p>
<pre><code class="powershell">pm2 start ./bin/www --watch</code></pre>
<p>入门太简单了,没什么好讲的。直接上官方文档:<a href="https://link.segmentfault.com/?enc=VFCgfP29RdMvNPWmKOJ77Q%3D%3D.21vyt7FuSl%2BwP3ZrUVSobEshmVQU1pEjkPibnt2B6CNy%2Fn27zv7Mkbm3keoZikVr" rel="nofollow">http://pm2.keymetrics.io/docs...</a></p>
<h2>常用命令</h2>
<h3>启动</h3>
<p>参数说明:</p>
<ul>
<li><p><code>--watch</code>:监听应用目录的变化,一旦发生变化,自动重启。如果要精确监听、不见听的目录,最好通过配置文件。</p></li>
<li><p><code>-i --instances</code>:启用多少个实例,可用于负载均衡。如果<code>-i 0</code>或者<code>-i max</code>,则根据当前机器核数确定实例数目。</p></li>
<li><p><code>--ignore-watch</code>:排除监听的目录/文件,可以是特定的文件名,也可以是正则。比如<code>--ignore-watch="test node_modules "some scripts""</code></p></li>
<li><p><code>-n --name</code>:应用的名称。查看应用信息的时候可以用到。</p></li>
<li><p><code>-o --output <path></code>:标准输出日志文件的路径。</p></li>
<li><p><code>-e --error <path></code>:错误输出日志文件的路径。</p></li>
<li><p><code>--interpreter <interpreter></code>:the interpreter pm2 should use for executing app (bash, python...)。比如你用的coffee script来编写应用。</p></li>
</ul>
<p>完整命令行参数列表:<a href="https://link.segmentfault.com/?enc=mGQ1VR10ND5H8EQ58JCRCQ%3D%3D.hnJxH5Nv1Ktxky56GZzJ0qcDv%2FoOUCRLSKoGHlgvMZPvhopZ%2BFUjFXdaeDOhX4ipRVGx5Vq6%2F%2BVb37SL5sPCwg%3D%3D" rel="nofollow">地址</a></p>
<pre><code class="powershell">pm2 start app.js --watch -i 2</code></pre>
<h3>重启</h3>
<pre><code class="powershell">pm2 restart app.js</code></pre>
<h3>停止</h3>
<p>停止特定的应用。可以先通过<code>pm2 list</code>获取应用的名字(--name指定的)或者进程id。</p>
<pre><code class="powershell">pm2 stop app_name|app_id</code></pre>
<p>如果要停止所有应用,可以</p>
<pre><code class="powershell">pm2 stop all</code></pre>
<h3>删除</h3>
<p>类似<code>pm2 stop</code>,如下</p>
<pre><code class="powershell">pm2 stop app_name|app_id
pm2 stop all</code></pre>
<h3>查看进程状态</h3>
<pre><code class="powershell">pm2 list</code></pre>
<h3>查看某个进程的信息</h3>
<pre><code class="powershell">[root@iZ94wb7tioqZ pids]# pm2 describe 0
Describing process with id 0 - name oc-server
┌───────────────────┬──────────────────────────────────────────────────────────────┐
│ status │ online │
│ name │ oc-server │
│ id │ 0 │
│ path │ /data/file/qiquan/over_the_counter/server/bin/www │
│ args │ │
│ exec cwd │ /data/file/qiquan/over_the_counter/server │
│ error log path │ /data/file/qiquan/over_the_counter/server/logs/app-err-0.log │
│ out log path │ /data/file/qiquan/over_the_counter/server/logs/app-out-0.log │
│ pid path │ /root/.pm2/pids/oc-server-0.pid │
│ mode │ fork_mode │
│ node v8 arguments │ │
│ watch & reload │ │
│ interpreter │ node │
│ restarts │ 293 │
│ unstable restarts │ 0 │
│ uptime │ 87m │
│ created at │ 2016-08-26T08:13:43.705Z │
└───────────────────┴──────────────────────────────────────────────────────────────┘</code></pre>
<h2>配置文件</h2>
<h3>简单说明</h3>
<ul>
<li><p>配置文件里的设置项,跟命令行参数基本是一一对应的。</p></li>
<li><p>可以选择<code>yaml</code>或者<code>json</code>文件,就看个人洗好了。</p></li>
<li><p><code>json</code>格式的配置文件,pm2当作普通的js文件来处理,所以可以在里面添加注释或者编写代码,这对于动态调整配置很有好处。</p></li>
<li><p>如果启动的时候指定了配置文件,那么命令行参数会被忽略。(个别参数除外,比如--env)</p></li>
</ul>
<h3>例子</h3>
<p>举个简单例子,完整配置说明请参考<a href="https://link.segmentfault.com/?enc=eydXqAuT5De2bSX6S61Q%2FA%3D%3D.6irlEBtKoCwF%2BApGme4uM2BTmKqSxC4Gv9RmDT8vt%2FuHRuXcex64lrRD8tw3Zw3cjz9OybT51UvLCJ1bmk1bIQ%3D%3D" rel="nofollow">官方文档</a>。</p>
<pre><code class="javascript">{
"name" : "fis-receiver", // 应用名称
"script" : "./bin/www", // 实际启动脚本
"cwd" : "./", // 当前工作路径
"watch": [ // 监控变化的目录,一旦变化,自动重启
"bin",
"routers"
],
"ignore_watch" : [ // 从监控目录中排除
"node_modules",
"logs",
"public"
],
"watch_options": {
"followSymlinks": false
},
"error_file" : "./logs/app-err.log", // 错误日志路径
"out_file" : "./logs/app-out.log", // 普通日志路径
"env": {
"NODE_ENV": "production" // 环境参数,当前指定为生产环境
}
}</code></pre>
<h2>自动重启</h2>
<p>前面已经提到了,这里贴命令行,更多点击<a href="https://link.segmentfault.com/?enc=NCBx01U7pO9ts6PfQRw5fA%3D%3D.7K1JLAKMmhtNTZyqoz7SvhQzbO%2BrfrJcxoU8cnLC8Owdy6ofgGWt8pGvSxMPoOVOj2afS5cgGizxTrSUljkW2zunB6hdRB5a9urqMjnRvL4PAUHsScDmCuJHrrQyVsFM" rel="nofollow">这里</a>。</p>
<pre><code>pm2 start app.js --watch</code></pre>
<p>这里是监控整个项目的文件,如果只想监听指定文件和目录,建议通过配置文件的<code>watch</code>、<code>ignore_watch</code>字段来设置。</p>
<h2>环境切换</h2>
<p>在实际项目开发中,我们的应用经常需要在多个环境下部署,比如开发环境、测试环境、生产环境等。在不同环境下,有时候配置项会有差异,比如链接的数据库地址不同等。</p>
<p>对于这种场景,pm2也是可以很好支持的。首先通过在配置文件中通过<code>env_xx</code>来声明不同环境的配置,然后在启动应用时,通过<code>--env</code>参数指定运行的环境。</p>
<h3>环境配置声明</h3>
<p>首先,在配置文件中,通过<code>env</code>选项声明多个环境配置。简单说明下:</p>
<ul>
<li><p><code>env</code>为默认的环境配置(生产环境),<code>env_dev</code>、<code>env_test</code>则分别是开发、测试环境。可以看到,不同环境下的<code>NODE_ENV</code>、<code>REMOTE_ADDR</code>字段的值是不同的。</p></li>
<li><p>在应用中,可以通过<code>process.env.REMOTE_ADDR</code>等来读取配置中生命的变量。</p></li>
</ul>
<pre><code class="javascript"> "env": {
"NODE_ENV": "production",
"REMOTE_ADDR": "http://www.example.com/"
},
"env_dev": {
"NODE_ENV": "development",
"REMOTE_ADDR": "http://wdev.example.com/"
},
"env_test": {
"NODE_ENV": "test",
"REMOTE_ADDR": "http://wtest.example.com/"
}</code></pre>
<h3>启动指明环境</h3>
<p>假设通过下面启动脚本(开发环境),那么,此时<code>process.env.REMOTE_ADDR</code>的值就是相应的 <a href="https://link.segmentfault.com/?enc=8%2Fk4fRK2ifdYAeKhD1TEew%3D%3D.zzppcmqhtgiflglFhi6R92c9WFduh2nSaw7VqhJaX2g%3D" rel="nofollow">http://wdev.example.com/</a> ,可以自己试验下。</p>
<pre><code class="powershell">pm2 start app.js --env dev</code></pre>
<h2>负载均衡</h2>
<p>命令如下,表示开启三个进程。如果<code>-i 0</code>,则会根据机器当前核数自动开启尽可能多的进程。</p>
<pre><code class="powershell">pm2 start app.js -i 3 # 开启三个进程
pm2 start app.js -i max # 根据机器CPU核数,开启对应数目的进程 </code></pre>
<p>参考文档:<a href="https://link.segmentfault.com/?enc=mfnyVb9SQjVLVjHdnFR2vA%3D%3D.pVDZJRrDvi16L4S%2FB0UaqZxwO%2BeHkS7dJZKrGoq2zPXYEZIgZpzwCj5pLffiblbcDXyx%2BE2Rymd0V5%2BAMyEitdukWekOfbT5TW7u8se26Jw%3D" rel="nofollow">点击查看</a></p>
<h2>日志查看</h2>
<p>除了可以打开日志文件查看日志外,还可以通过<code>pm2 logs</code>来查看实时日志。这点对于线上问题排查非常重要。</p>
<p>比如某个node服务突然异常重启了,那么可以通过pm2提供的日志工具来查看实时日志,看是不是脚本出错之类导致的异常重启。</p>
<pre><code>pm2 logs</code></pre>
<h2>指令tab补全</h2>
<p>运行<code>pm2 --help</code>,可以看到<code>pm2</code>支持的子命令还是蛮多的,这个时候,自动完成的功能就很重要了。</p>
<p>运行如下命令。恭喜,已经能够通过tab自动补全了。细节可参考<a href="https://link.segmentfault.com/?enc=og2HHSLnEqaTUKvsqAt0qg%3D%3D.e38fSUrCZedEFPBPwJVqPTgOhV3r1Uoo%2FuioCocKmgKrk7vJvzEz6PNH0lcebp%2BnipbNjEGBVnDKShupDuqjkQ%3D%3D" rel="nofollow">这里</a>。</p>
<pre><code>pm2 completion install
source ~/.bash_profile</code></pre>
<p><img alt="Alt text" title="Alt text" src=""></p>
<h2>开机自动启动</h2>
<p>可以通过<code>pm2 startup</code>来实现开机自启动。细节可<a href="https://link.segmentfault.com/?enc=B%2FyBf7ysgEFpLLmHoeC9%2Bw%3D%3D.Ba4JR6VuOMSsj0nZUA7n%2BuHP6wWdYSJNhyWXoRVWKLm7Ka3xCdveb9AnbCIpFRbq" rel="nofollow">参考</a>。大致流程如下</p>
<ol>
<li><p>通过<code>pm2 save</code>保存当前进程状态。</p></li>
<li><p>通过<code>pm2 startup [platform]</code>生成开机自启动的命令。(记得查看控制台输出)</p></li>
<li><p>将步骤2生成的命令,粘贴到控制台进行,搞定。</p></li>
</ol>
<h2>传入node args</h2>
<p>直接上例子,分别是通过命令行和配置文件。</p>
<p>命令行:</p>
<pre><code class="powershell">pm2 start app.js --node-args="--harmony"</code></pre>
<p>配置文件:</p>
<pre><code class="json">{
"name" : "oc-server",
"script" : "app.js",
"node_args" : "--harmony"
}</code></pre>
<h3>实例说明</h3>
<p>假设是在<code>centos</code>下,那么运行如下命令,搞定。强烈建议运行完成之后,重启机器,看是否设置成功。</p>
<pre><code>[root@iZ94wb7tioqZ option_analysis]# pm2 save
[root@iZ94wb7tioqZ option_analysis]# pm2 startup centos
[PM2] Generating system init script in /etc/init.d/pm2-init.sh
[PM2] Making script booting at startup...
[PM2] /var/lock/subsys/pm2-init.sh lockfile has been added
[PM2] -centos- Using the command:
su -c "chmod +x /etc/init.d/pm2-init.sh; chkconfig --add pm2-init.sh"
[PM2] Done.
[root@iZ94wb7tioqZ option_analysis]# pm2 save
[PM2] Dumping processes</code></pre>
<h2>远程部署</h2>
<p>可参考官方文档,配置也不复杂,用到的时候再来填写这里的坑。TODO</p>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=2rjKJf%2Fj%2FG5HM3IdmLa%2FKw%3D%3D.l6FAl4eg8JTYr4Hz2FqIf7fwFgtgunwaKUx0yW1D1AecZNlSHs1REOuVOYd0X709CByu%2Bbu3vammB8xl7YTmjw%3D%3D" rel="nofollow">http://pm2.keymetrics.io/docs...</a></p>
<h2>监控(monitor)</h2>
<p>运行如下命令,查看当前通过pm2运行的进程的状态。</p>
<pre><code>pm2 monit</code></pre>
<p>看到类似输出</p>
<pre><code>[root@oneday-dev0 server]# pm2 monit
⌬ PM2 monitoring (To go further check out https://app.keymetrics.io)
[ ] 0 %
⌬ PM2 monitoring (To go further check o[||||||||||||||| ] 196.285 MB
● fis-receiver [ ] 0 %
[1] [fork_mode] [||||| ] 65.773 MB
● www [ ] 0 %
[2] [fork_mode] [||||| ] 74.426 MB
● oc-server [ ] 0 %
[3] [fork_mode] [|||| ] 57.801 MB
● pm2-http-interface [ ] stopped
[4] [fork_mode] [ ] 0 B
● start-production
[5] [fork_mode]</code></pre>
<h2>内存使用超过上限自动重启</h2>
<p>如果想要你的应用,在超过使用内存上限后自动重启,那么可以加上<code>--max-memory-restart</code>参数。(有对应的配置项)</p>
<pre><code class="powershell">pm2 start big-array.js --max-memory-restart 20M</code></pre>
<h2>更新pm2</h2>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=oqYcQnHMFh6O6V24l4qp5Q%3D%3D.AnPRhBZynXt2i1Bszy1E7dCdWGmLDK%2F%2FDsTK6Thi76DN3AAGSSQ90psd5M1aQLs2f%2FwGUeGlWyQZQ5YJexpv%2Bg%3D%3D" rel="nofollow">http://pm2.keymetrics.io/docs...</a></p>
<pre><code>$ pm2 save # 记得保存进程状态
$ npm install pm2 -g
$ pm2 update</code></pre>
<h2>pm2 + nginx</h2>
<p>无非就是在nginx上做个反向代理配置,直接贴配置。</p>
<pre><code class="nginx">
upstream my_nodejs_upstream {
server 127.0.0.1:3001;
}
server {
listen 80;
server_name my_nodejs_server;
root /home/www/project_root;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_max_temp_file_size 0;
proxy_pass http://my_nodejs_upstream/;
proxy_redirect off;
proxy_read_timeout 240s;
}
}</code></pre>
<p>官方文档:<a href="https://link.segmentfault.com/?enc=291NkVP%2F8WB%2FD1VP4bKZSg%3D%3D.FjBCL75kwFPfyBpcY1UdaFFWtoPew8nx%2FpMedUwoHFkBOAe%2BZYsttJ%2FIeY%2B6z86%2BKPBOSx5LPQhHNkzCKZSKqFGXEzNRBlHshBea95PbHRM%3D" rel="nofollow">http://pm2.keymetrics.io/docs...</a></p>
<h2>在线监控系统</h2>
<p>收费服务,使用超级简单,可以方便的对进程的服务情况进行监控。可以试用下,地址在<a href="https://link.segmentfault.com/?enc=tSDNEqpLhonSPr4%2FqAtxLg%3D%3D.0h%2FIVxUoUHHs8vCc3gCCouNBNMn5pskz7cdG93eEZiE%3D" rel="nofollow">这里</a>。</p>
<p>这里贴个项目中试用的截图。</p>
<p><img src="/img/remote/1460000006793574" alt="pm2" title="pm2"></p>
<h2>pm2编程接口</h2>
<p>如果想把pm2的进程监控,跟其他自动化流程整合起来,pm2的编程接口就很有用了。细节可参考官方文档:<br><a href="https://link.segmentfault.com/?enc=XVp6aO%2FCjHUV66D%2FwOZjVA%3D%3D.xybY21KhENJ9XQOjLbqf82L3nujQwvbomb4aT1BK0Myt5DMZhjYVnOJwG667zDTo" rel="nofollow">http://pm2.keymetrics.io/docs...</a></p>
<h2>模块扩展系统</h2>
<p>pm2支持第三方扩展,比如常用的log rotate等。可参考<a href="https://link.segmentfault.com/?enc=WA3gtGDKtM0MmzRQSl%2FG0w%3D%3D.dLqcb0WiXAojmmrm1pTf2dgzRoHXLM8jzYksY%2FietduYA951Oji5D5iBp8slQkLLCiT2mzd3tZnfjUkwU1l9uA%3D%3D" rel="nofollow">官方文档</a>。</p>
<h2>写在后面</h2>
<p>pm2的文档已经写的很好了,学习成本很低,即使是没用过pm2的小伙伴,基本上照着getting started的例子就可以把项目给跑起来,所以文中不少地方都是建议直接参看官方文档。</p>
<p>。。。</p>
Node服务一键离线部署
https://segmentfault.com/a/1190000006793590
2016-09-02T08:05:00+08:00
2016-09-02T08:05:00+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
1
<h2>背景说明</h2>
<p>项目测试通过,到了上线部署阶段。部署的机器安全限制比较严格,不允许访问外网。此外,没有对外网开放ssh服务,无法通过ssh远程操作。</p>
<p>针对上面提到的两条限制条件,通过下面方式解决:</p>
<ul>
<li><p><strong>无法访问外部网络</strong>:将依赖的环境本地下载,打包上传,离线安装;</p></li>
<li><p><strong>无法ssh远程操作</strong>:将安装/初始化步骤脚本化,安装包交给运维人员,一键部署;</p></li>
</ul>
<h2>安装包说明</h2>
<p>让运维同学将安装包置于<code>/data/my_install</code>下。安装包大致如容如下。其中<code>install_scripts</code>目录中,存放的是部署相关的脚本。</p>
<pre><code class="powershell">[root@localhost my_install]# tree -L 1
.
├── control # 各种服务控制脚本
├── install_scripts # 安装脚本
├── node-v5.11.1-linux-x64 # node二进制包
├── npm_modules_global_offline # 全局的npm模块,比如 pm2
├── express_svr # express应用
└── uninstall_scripts # 卸载脚本</code></pre>
<h3>部署脚本说明</h3>
<pre><code class="powershell">[root@localhost install_scripts]# tree -L 1
.
├── install_node.sh # 安装nodejs
├── install_npm_moduels.sh # 安装npm模块
├── install_run_service.sh # 启动服务
├── install_express_svr.sh # 部署express应用
└── install.sh # 部署总入口</code></pre>
<h2>Node安装</h2>
<p>看下<code>nodejs</code>安装脚本。为了安装快些,这里我们采用的是编译好的二进制文件。只需要将相关文件拷贝到指定路径即可。</p>
<h3>Node安装包说明</h3>
<p>以下是<code>nodejs@v5.11.1</code>的目录。</p>
<pre><code class="powershell">[root@localhost node-v5.11.1-linux-x64]# tree -L 2
.
├── bin
│ ├── node # node可执行文件
│ └── npm -> ../lib/node_modules/npm/bin/npm-cli.js # npm可执行文件,其实是个软链接
├── CHANGELOG.md
├── include # 各种包含文件
│ └── node
├── lib
│ └── node_modules # npm模块安装目录
├── LICENSE
├── README.md
└── share
├── doc
├── man # 说明文件
└── systemtap</code></pre>
<p>拷贝路径说明如下</p>
<table>
<thead><tr>
<th align="left">本地路径</th>
<th align="left">拷贝到的路径</th>
<th align="left">备注</th>
</tr></thead>
<tbody>
<tr>
<td align="left">./bin/node</td>
<td align="left">/usr/local/bin/node</td>
<td align="left">node可执行文件</td>
</tr>
<tr>
<td align="left">./bin/npm</td>
<td align="left">/usr/local/bin/node</td>
<td align="left">npm可执行文件,软链接,指向 /usr/local/lib/node_modules/npm/bin/npm-cli.js</td>
</tr>
<tr>
<td align="left">./lib/node_modules/</td>
<td align="left">/usr/local/lib/</td>
<td align="left">npm模块安装目录</td>
</tr>
<tr>
<td align="left">./include/node</td>
<td align="left">/usr/local/include/</td>
<td align="left">各种包含文件</td>
</tr>
<tr>
<td align="left">./share/man/man1/node.1</td>
<td align="left">/usr/local/man/man1/</td>
<td align="left">使用说明</td>
</tr>
</tbody>
</table>
<h3>安装脚本</h3>
<p><strong>install_node.sh</strong></p>
<pre><code class="powershell">[root@localhost install_scripts]# cat install_node.sh
#!/bin/bash
# 安装nodejs
cd /data/my_install/
cd node-v5.11.1-linux-x64/
cp -r ./lib/node_modules/ /usr/local/lib/ # copy the node modules folder to the /lib/ folder
cp -r ./include/node /usr/local/include/ # copy the /include/node folder to /usr/local/include folder
mkdir -p /usr/local/man/man1 # create the man folder
cp ./share/man/man1/node.1 /usr/local/man/man1/ # copy the man file
cp ./bin/node /usr/local/bin/ # copy node to the bin folder
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm ## making the symbolic link to npm</code></pre>
<h2>全局npm模块安装</h2>
<p>这里我们就用到了pm2,需要全局安装。根据npm全局模块的安装方式,需要分两步</p>
<ul>
<li><p>将pm2模块目录拷贝到<code>/usr/local/lib/node_modules</code>下。</p></li>
<li><p>在<code>/usr/local/bin/</code>下,建立软链接,指向<code>/usr/local/lib/node_modules/pm2/bin/</code>下的可执行文件。</p></li>
</ul>
<h3>pm2安装说明</h3>
<p>首先,把pm2包下载下来,这步略。我在这里放到了<code>npm_modules_global_offline</code>目录下,以防以后还有其他全部模块要一起安装。</p>
<p>软链接映射关系如下</p>
<table>
<thead><tr>
<th align="left">目标文件路径</th>
<th align="left">源文件路径</th>
</tr></thead>
<tbody>
<tr>
<td align="left">/usr/local/bin/pm2</td>
<td align="left">/usr/local/lib/node_modules/pm2/bin/pm2</td>
</tr>
<tr>
<td align="left">/usr/local/bin/pm2-dev</td>
<td align="left">/usr/local/lib/node_modules/pm2/bin/pm2-dev</td>
</tr>
</tbody>
</table>
<h3>安装脚本</h3>
<p><strong>install_npm_moduels.sh</strong></p>
<pre><code class="powershell">#!/bin/bash
# 安装全局npm模块
cd /data/my_install/
cd npm_modules_global_offline/
cp -rf ./node_modules/* /usr/local/lib/node_modules/
ln -s /usr/local/lib/node_modules/pm2/bin/pm2 /usr/local/bin/pm2
ln -s /usr/local/lib/node_modules/pm2/bin/pm2-dev /usr/local/bin/pm2-dev</code></pre>
<h2>Express应用安装</h2>
<p>express应用的安装相对比较简单,本地<code>npm install</code>后,连同<code>node_modules</code>目录一起打包即可。</p>
<p>脚本如下,把<code>express_svr</code>拷贝到指定路径即可。</p>
<p><strong>install_express_svr.sh</strong></p>
<pre><code class="powershell">#!/bin/bash
# 安装express应用
cd /data/my_install/
if [ ! -d "/data/web/express_svr" ]; then
mkdir /data/web/express_svr
fi
cp -rf ./express_svr/* /data/express_svr/</code></pre>
<h2>一键部署脚本</h2>
<h3>简易版本</h3>
<p>其实没那么玄乎,无非就是再写个脚本,统一调用下前面提到的脚本。奏是这么简单。</p>
<p><strong>install.sh</strong>:</p>
<pre><code class="powershell">./install_node.sh
./install_npm_moduels.sh
./install_otc_svr.sh
./install_run_service.sh</code></pre>
<p>运行:</p>
<pre><code>./install.sh</code></pre>
<h3>进一步完善</h3>
<p>上面脚本的缺陷比较明显,没有进度提示,也没有运行状态提示。于是优化一下,虽然也不能算是完善,但相比之前的版本的确会好很多。</p>
<pre><code class="powershell">#!/bin/bash
commands=(
./install_node.sh "install nodejs"
./install_npm_moduels.sh "install npm modules"
./install_express_svr.sh "install express application"
./install_run_service.sh "start services"
)
commands_len=${#commands[@]}
for (( i=0; i<$commands_len; i=i+2 ))
do
desc_index=i+1
desc=${commands[$desc_index]}
echo -e $desc" - starts ..."
${commands[$i]}
if [ "$?" == "0" ]; then
echo -e $desc" - ok \n"
else
echo -e $desc" - failed ! \n"
fi
done</code></pre>
<p>运行看下效果:</p>
<pre><code class="powershell">install nodejs - starts ...
install nodejs - ok
install npm modules - starts ...
install npm modules - ok
install express application - starts ...
install express application - ok
start services - starts ...
# pm2启动日志,一大坨,这里忽略
start services - ok </code></pre>
<h2>一键卸载脚本</h2>
<p>从上面的内容可以看到,离线部署的过程,主要包含了几个操作</p>
<ul>
<li><p>文件拷贝</p></li>
<li><p>建立软连接</p></li>
<li><p>启动服务</p></li>
</ul>
<p>那么,卸载无非就是上面几个步骤的反操作。脚本大致如下,跟前面的部署脚本其实是一一对应的。这里就不再赘述。</p>
<pre><code class="powershell">[root@localhost uninstall_scripts]# tree -L 1
.
├── uninstall_run_service.sh
├── uninstall_node.sh
├── uninstall_npm_modules.sh
├── uninstall_express_svr.sh
└── uninstall.sh</code></pre>
<h2>写在后面</h2>
<p>文中提及的node服务离线部署,应该已经可以涵盖大部分的场景,举一反三即可。当然更富在的场景还有,这里就不再展开。<br>。。。</p>
fis-receiver:一行命令将项目部署到远程服务器
https://segmentfault.com/a/1190000004535283
2016-03-03T23:26:56+08:00
2016-03-03T23:26:56+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
0
<h2>前言</h2>
<p>本项目基于FIS2,没了。其实fis项目本身就提供了php版本的范例,这里翻译成node版本。</p>
<p>项目地址:<a href="https://link.segmentfault.com/?enc=HJZgBInsZ6KJKJ5QzeOt3w%3D%3D.HJzus%2FWGfpG5mQCXNsJXcs3rAI%2F%2FeE%2FPt4iHrJd5H6%2Fi6ReGSxd5oDuALsaXDdW4" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=vr7ur6bhFsynkd6txUs%2Fng%3D%3D.fwuuZeWnxDFNS5rdNt9t%2Bcu5ovN%2B%2Fh2e21lHMhnwd4U1oydxtm3aVTzI9hSItq3p" rel="nofollow">https://github.com/chyingp/fis-receiver</a></p>
<h2>服务端接收脚本部署</h2>
<p>首先,克隆项目</p>
<pre><code>git clone https://github.com/chyingp/fis-receiver.git</code></pre>
<p>跟着,安装依赖</p>
<pre><code>cd fis-receiver/
npm install</code></pre>
<p>然后,启动服务</p>
<pre><code>npm start</code></pre>
<h2>配置修改:fis-conf.js</h2>
<blockquote><p>以下内容参考 fis-receiver/examples 的例子</p></blockquote>
<p>在<code>fis-conf.js</code>中加入如下配置。其中:</p>
<ul>
<li><p><code>receiver</code>:修改成服务端脚本实际部署的路径。</p></li>
<li><p><code>to</code>:修改成项目打算部署到的远程服务器上的路径。</p></li>
</ul>
<pre><code>fis.config.merge({
deploy: {
remote: {
receiver: 'http://127.0.0.1:3000/cgi-bin/release', // 接收服务的地址
from: '/',
to: '/tmp/test' // 服务器上部署的的路径
}
}
});</code></pre>
<p>启动远程部署。</p>
<pre><code>fis release -d remote</code></pre>
<p>从打印的日志可以看到项目已经被部署到远程服务器。</p>
<pre><code> δ 7ms
Ω ... 35ms
- [22:53:51] css/index.css >> /tmp/test/css/index.css
- [22:53:51] index.html >> /tmp/test/index.html
- [22:53:51] js/index.js >> /tmp/test/js/index.js
- [22:53:51] map.json >> /tmp/test/map.json</code></pre>
<p>打开远程服务器目录,查看部署结果。</p>
<pre><code>cd /tmp/test
test ll</code></pre>
<p>从目录下的内容来看,部署成功。</p>
<pre><code>total 16
drwxr-xr-x 6 a wheel 204 3 3 22:53 .
drwxrwxrwt 13 root wheel 442 3 3 22:56 ..
drwxr-xr-x 3 a wheel 102 3 3 22:53 css
-rw-r--r-- 1 a wheel 82 3 3 22:53 index.html
drwxr-xr-x 3 a wheel 102 3 3 22:53 js
-rw-r--r-- 1 a wheel 233 3 3 22:53 map.json</code></pre>
<h2>相关链接</h2>
<p>官方部署配置:<br><a href="https://link.segmentfault.com/?enc=bbMf%2BK036EzvHQ4tbta9lQ%3D%3D.kvf7QXaWl3m05BqukIUon5m5kSPs0EL5W99%2FRCMBOySqZOwRSjyhW%2BgKKgbH6Q14x61wzU7f9uNV73pmg0vxtQ%3D%3D" rel="nofollow">http://fex.baidu.com/fis-site/docs/api/fis-conf.html#deploy</a></p>
<p>fis中自带的php版本的例子<br><a href="https://link.segmentfault.com/?enc=fgpJ6XJCRhGc38e9iQPNdA%3D%3D.tL%2FvymJ%2Ftd8Hq9MmsyJrtZrgmcVVWAEKq2L5yuOEfJhTK0AWwm7uwT6yJ2ii70p0LR7mkHqE81R1NsiZDsKuNN6xAylNwrUQhGtrj7yo%2BVg%3D" rel="nofollow">https://github.com/fex-team/fis-command-release/blob/master/tools/receiver.php</a></p>
HTTPS科普扫盲帖
https://segmentfault.com/a/1190000004523659
2016-03-02T13:28:18+08:00
2016-03-02T13:28:18+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
36
<h2>为什么需要HTTPS</h2>
<p>HTTP是明文传输的,也就意味着,介于发送端、接收端中间的任意节点都可以知道你们传输的内容是什么。这些节点可能是路由器、代理等。</p>
<p>举个最常见的例子,用户登陆。用户输入账号,密码,采用HTTP的话,只要在代理服务器上做点手脚就可以拿到你的密码了。</p>
<blockquote><p>用户登陆 --> 代理服务器(做手脚)--> 实际授权服务器</p></blockquote>
<p>在发送端对密码进行加密?没用的,虽然别人不知道你原始密码是多少,但能够拿到加密后的账号密码,照样能登陆。</p>
<h2>HTTPS是如何保障安全的</h2>
<p>HTTPS其实就是<strong>secure http</strong>的意思啦,也就是HTTP的安全升级版。稍微了解网络基础的同学都知道,HTTP是应用层协议,位于HTTP协议之下是传输协议TCP。TCP负责传输,HTTP则定义了数据如何进行包装。</p>
<blockquote><p>HTTP --> TCP (明文传输)</p></blockquote>
<p>HTTPS相对于HTTP有哪些不同呢?其实就是在HTTP跟TCP中间加多了一层加密层<strong>TLS/SSL</strong>。</p>
<p><strong>神马是TLS/SSL?</strong></p>
<p>通俗的讲,TLS、SSL其实是类似的东西,SSL是个加密套件,负责对HTTP的数据进行加密。TLS是SSL的升级版。现在提到HTTPS,加密套件基本指的是TLS。</p>
<p><strong>传输加密的流程</strong></p>
<p>原先是应用层将数据直接给到TCP进行传输,现在改成应用层将数据给到TLS/SSL,将数据加密后,再给到TCP进行传输。</p>
<p>大致如图所示。<br><img src="/img/remote/1460000004835543" alt="enter image description here" title="enter image description here"></p>
<p>就是这么回事。将数据加密后再传输,而不是任由数据在复杂而又充满危险的网络上明文裸奔,在很大程度上确保了数据的安全。这样的话,即使数据被中间节点截获,坏人也看不懂。</p>
<h2>HTTPS是如何加密数据的</h2>
<p>对安全或密码学基础有了解的同学,应该知道常见的加密手段。一般来说,加密分为对称加密、非对称加密(也叫公开密钥加密)。</p>
<h3>对称加密</h3>
<p><strong>对称加密</strong>的意思就是,加密数据用的密钥,跟解密数据用的密钥是一样的。</p>
<p>对称加密的优点在于加密、解密效率通常比较高。缺点在于,数据发送方、数据接收方需要协商、共享同一把密钥,并确保密钥不泄露给其他人。此外,对于多个有数据交换需求的个体,两两之间需要分配并维护一把密钥,这个带来的成本基本是不可接受的。</p>
<h3>非对称加密</h3>
<p><strong>非对称加密</strong>的意思就是,加密数据用的密钥(公钥),跟解密数据用的密钥(私钥)是不一样的。</p>
<p>什么叫做公钥呢?其实就是字面上的意思——公开的密钥,谁都可以查到。因此非对称加密也叫做公开密钥加密。</p>
<p>相对应的,私钥就是非公开的密钥,一般是由网站的管理员持有。</p>
<p>公钥、私钥两个有什么联系呢?</p>
<p>简单的说就是,通过公钥加密的数据,只能通过私钥解开。通过私钥加密的数据,只能通过公钥解开。</p>
<p>很多同学都知道用私钥能解开公钥加密的数据,但忽略了一点,私钥加密的数据,同样可以用公钥解密出来。而这点对于理解HTTPS的整套加密、授权体系非常关键。</p>
<h3>举个非对称加密的例子</h3>
<ul>
<li><p>登陆用户:小明</p></li>
<li><p>授权网站:某知名社交网站(以下简称XX)</p></li>
</ul>
<p>小明都是某知名社交网站XX的用户,XX出于安全考虑在登陆的地方用了非对称加密。小明在登陆界面敲入账号、密码,点击“登陆”。于是,浏览器利用公钥对小明的账号密码进行了加密,并向XX发送登陆请求。XX的登陆授权程序通过私钥,将账号、密码解密,并验证通过。之后,将小明的个人信息(含隐私),通过私钥加密后,传输回浏览器。浏览器通过公钥解密数据,并展示给小明。</p>
<ul>
<li><p>步骤一: 小明输入账号密码 --> 浏览器用公钥加密 --> 请求发送给XX</p></li>
<li><p>步骤二: XX用私钥解密,验证通过 --> 获取小明社交数据,用私钥加密 --> 浏览器用公钥解密数据,并展示。</p></li>
</ul>
<p>用非对称加密,就能解决数据传输安全的问题了吗?前面特意强调了一下,私钥加密的数据,公钥是可以解开的,而公钥又是加密的。也就是说,非对称加密只能保证单向数据传输的安全性。</p>
<p>此外,还有公钥如何分发/获取的问题。下面会对这两个问题进行进一步的探讨。</p>
<h2>公开密钥加密:两个明显的问题</h2>
<p>前面举了小明登陆社交网站XX的例子,并提到,单纯使用公开密钥加密存在两个比较明显的问题。</p>
<ol>
<li><p>公钥如何获取</p></li>
<li><p>数据传输仅单向安全</p></li>
</ol>
<h3>问题一:公钥如何获取</h3>
<p>浏览器是怎么获得XX的公钥的?当然,小明可以自己去网上查,XX也可以将公钥贴在自己的主页。然而,对于一个动不动就成败上千万的社交网站来说,会给用户造成极大的不便利,毕竟大部分用户都不知道“公钥”是什么东西。</p>
<h3>问题二:数据传输仅单向安全</h3>
<p>前面提到,公钥加密的数据,只有私钥能解开,于是小明的账号、密码是安全了,半路不怕被拦截。</p>
<p>然后有个很大的问题:<strong>私钥加密的数据,公钥也能解开</strong>。加上公钥是公开的,小明的隐私数据相当于在网上换了种方式裸奔。(中间代理服务器拿到了公钥后,毫不犹豫的就可以解密小明的数据)</p>
<p>下面就分别针对这两个问题进行解答。</p>
<h2>问题一:公钥如何获取</h2>
<p>这里要涉及两个非常重要的概念:证书、CA(证书颁发机构)。</p>
<p><strong>证书</strong></p>
<p>可以暂时把它理解为网站的身份证。这个身份证里包含了很多信息,其中就包含了上面提到的公钥。</p>
<p>也就是说,当小明、小王、小光等用户访问XX的时候,再也不用满世界的找XX的公钥了。当他们访问XX的时候,XX就会把证书发给浏览器,告诉他们说,乖,用这个里面的公钥加密数据。</p>
<p>这里有个问题,所谓的“证书”是哪来的?这就是下面要提到的CA负责的活了。</p>
<p><strong>CA(证书颁发机构)</strong></p>
<p>强调两点:</p>
<ol>
<li><p>可以颁发证书的CA有很多(国内外都有)。</p></li>
<li><p>只有少数CA被认为是权威、公正的,这些CA颁发的证书,浏览器才认为是信得过的。比如<strong>VeriSign</strong>。(CA自己伪造证书的事情也不是没发生过。。。)</p></li>
</ol>
<p>证书颁发的细节这里先不展开,可以先简单理解为,网站向CA提交了申请,CA审核通过后,将证书颁发给网站,用户访问网站的时候,网站将证书给到用户。</p>
<p>至于证书的细节,同样在后面讲到。</p>
<h2>问题二:数据传输仅单向安全</h2>
<p>上面提到,通过私钥加密的数据,可以用公钥解密还原。那么,这是不是就意味着,网站传给用户的数据是不安全的?</p>
<p>答案是:是!!!(三个叹号表示强调的三次方)</p>
<p>看到这里,可能你心里会有这样想:用了HTTPS,数据还是裸奔,这么不靠谱,还不如直接用HTTP来的省事。</p>
<p>但是,为什么业界对网站HTTPS化的呼声越来越高呢?这明显跟我们的感性认识相违背啊。</p>
<p>因为:HTTPS虽然用到了公开密钥加密,但同时也结合了其他手段,如对称加密,来确保授权、加密传输的效率、安全性。</p>
<p>概括来说,整个简化的加密通信的流程就是:</p>
<ol>
<li><p>小明访问XX,XX将自己的证书给到小明(其实是给到浏览器,小明不会有感知)</p></li>
<li><p>浏览器从证书中拿到XX的公钥A</p></li>
<li><p>浏览器生成一个只有自己知道的对称密钥B,用公钥A加密,并传给XX(其实是有协商的过程,这里为了便于理解先简化)</p></li>
<li><p>XX通过私钥解密,拿到对称密钥B</p></li>
<li><p>浏览器、XX 之后的数据通信,都用密钥B进行加密</p></li>
</ol>
<p>注意:对于每个访问XX的用户,生成的对称密钥B理论上来说都是不一样的。比如小明、小王、小光,可能生成的就是B1、B2、B3.</p>
<p>参考下图:(附上<a href="https://link.segmentfault.com/?enc=Ydauv57OV8%2F%2BFRnnUbaztQ%3D%3D.dfLo3c2vOAZ%2B%2BGra0H92WlYnnOt08usrEteNwzeJOFaR5AxQNDWqrsRjLOfcWk0mh%2F5a2wmUbmBVxbySA%2Fx35bgiDj0p%2BLdTQqq7Cb6PzzU%3D" rel="nofollow">原图出处</a>)</p>
<p><img src="/img/remote/1460000004835978" alt="enter image description here" title="enter image description here"></p>
<h2>证书可能存在哪些问题</h2>
<p>了解了HTTPS加密通信的流程后,对于数据裸奔的疑虑应该基本打消了。然而,细心的观众可能又有疑问了:怎么样确保证书有合法有效的?</p>
<p>证书非法可能有两种情况:</p>
<ol>
<li><p>证书是伪造的:压根不是CA颁发的</p></li>
<li><p>证书被篡改过:比如将XX网站的公钥给替换了</p></li>
</ol>
<p><strong>举个例子:</strong></p>
<p>我们知道,这个世界上存在一种东西叫做代理,于是,上面小明登陆XX网站有可能是这样的,小明的登陆请求先到了代理服务器,代理服务器再将请求转发到的授权服务器。</p>
<blockquote>
<p>小明 --> 邪恶的代理服务器 --> 登陆授权服务器</p>
<p>小明 <-- 邪恶的代理服务器 <-- 登陆授权服务器</p>
</blockquote>
<p>然后,这个世界坏人太多了,某一天,代理服务器动了坏心思(也有可能是被入侵),将小明的请求拦截了。同时,返回了一个非法的证书。</p>
<blockquote>
<p>小明 --> 邪恶的代理服务器 --x--> 登陆授权服务器</p>
<p>小明 <-- 邪恶的代理服务器 --x--> 登陆授权服务器</p>
</blockquote>
<p>如果善良的小明相信了这个证书,那他就再次裸奔了。当然不能这样,那么,是通过什么机制来防止这种事情的放生的呢。</p>
<p>下面,我们先来看看”证书”有哪些内容,然后就可以大致猜到是如何进行预防的了。</p>
<h2>证书简介</h2>
<p>在正式介绍证书的格式前,先插播个小广告,科普下数字签名和摘要,然后再对证书进行非深入的介绍。</p>
<p>为什么呢?因为数字签名、摘要是证书防伪非常关键的武器。</p>
<h3>数字签名与摘要</h3>
<p>简单的来说,“摘要”就是对传输的内容,通过hash算法计算出一段固定长度的串(是不是联想到了文章摘要)。然后,在通过CA的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名”。(这里提到CA的私钥,后面再进行介绍)</p>
<blockquote><p>明文 --> hash运算 --> 摘要 --> 私钥加密 --> 数字签名</p></blockquote>
<p>结合上面内容,我们知道,这段数字签名只有CA的公钥才能够解密。</p>
<p>接下来,我们再来看看神秘的“证书”究竟包含了什么内容,然后就大致猜到是如何对非法证书进行预防的了。</p>
<p>数字签名、摘要进一步了解可参考 <a href="https://link.segmentfault.com/?enc=ElXVutd4%2F5fUo4vH3VlOew%3D%3D.MvByBaITllTXkge5lx4U1OcezCvPCeSQd%2B86jk%2Beu1CrIhBygcwN%2F6YA8vBnehLj9PeF4P1oPXXT9kJKw8KO%2Bg%3D%3D" rel="nofollow">这篇文章</a>。</p>
<h3>证书格式</h3>
<p>先无耻的贴上一大段内容,证书格式来自这篇不错的文章《<a href="https://link.segmentfault.com/?enc=%2FD6qPd%2FjX80K08ZHKWuphg%3D%3D.7TVRqS0lac3yh4oU9e8mHCUpVQDDyxfvNZtNH9mOW5XavHc6WpRrsgLM8T5Rr43hcfIV%2FI%2BFhh%2BKG6JivOl9HA%3D%3D" rel="nofollow">OpenSSL 与 SSL 数字证书概念贴</a>》</p>
<p>内容非常多,这里我们需要关注的有几个点:</p>
<ol>
<li><p>证书包含了颁发证书的机构的名字 -- CA</p></li>
<li><p>证书内容本身的数字签名(用CA私钥加密)</p></li>
<li><p>证书持有者的公钥</p></li>
<li><p>证书签名用到的hash算法</p></li>
</ol>
<p>此外,有一点需要补充下,就是:</p>
<ol>
<li><p>CA本身有自己的证书,江湖人称“根证书”。这个“根证书”是用来证明CA的身份的,本质是一份普通的数字证书。</p></li>
<li><p>浏览器通常会内置大多数主流权威CA的根证书。</p></li>
</ol>
<p><strong>证书格式</strong></p>
<pre><code>1. 证书版本号(Version)
版本号指明X.509证书的格式版本,现在的值可以为:
1) 0: v1
2) 1: v2
3) 2: v3
也为将来的版本进行了预定义
2. 证书序列号(Serial Number)
序列号指定由CA分配给证书的唯一的"数字型标识符"。当证书被取消时,实际上是将此证书的序列号放入由CA签发的CRL中,
这也是序列号唯一的原因。
3. 签名算法标识符(Signature Algorithm)
签名算法标识用来指定由CA签发证书时所使用的"签名算法"。算法标识符用来指定CA签发证书时所使用的:
1) 公开密钥算法
2) hash算法
example: sha256WithRSAEncryption
须向国际知名标准组织(如ISO)注册
4. 签发机构名(Issuer)
此域用来标识签发证书的CA的X.500 DN(DN-Distinguished Name)名字。包括:
1) 国家(C)
2) 省市(ST)
3) 地区(L)
4) 组织机构(O)
5) 单位部门(OU)
6) 通用名(CN)
7) 邮箱地址
5. 有效期(Validity)
指定证书的有效期,包括:
1) 证书开始生效的日期时间
2) 证书失效的日期和时间
每次使用证书时,需要检查证书是否在有效期内。
6. 证书用户名(Subject)
指定证书持有者的X.500唯一名字。包括:
1) 国家(C)
2) 省市(ST)
3) 地区(L)
4) 组织机构(O)
5) 单位部门(OU)
6) 通用名(CN)
7) 邮箱地址
7. 证书持有者公开密钥信息(Subject Public Key Info)
证书持有者公开密钥信息域包含两个重要信息:
1) 证书持有者的公开密钥的值
2) 公开密钥使用的算法标识符。此标识符包含公开密钥算法和hash算法。
8. 扩展项(extension)
X.509 V3证书是在v2的基础上一标准形式或普通形式增加了扩展项,以使证书能够附带额外信息。标准扩展是指
由X.509 V3版本定义的对V2版本增加的具有广泛应用前景的扩展项,任何人都可以向一些权威机构,如ISO,来
注册一些其他扩展,如果这些扩展项应用广泛,也许以后会成为标准扩展项。
9. 签发者唯一标识符(Issuer Unique Identifier)
签发者唯一标识符在第2版加入证书定义中。此域用在当同一个X.500名字用于多个认证机构时,用一比特字符串
来唯一标识签发者的X.500名字。可选。
10. 证书持有者唯一标识符(Subject Unique Identifier)
持有证书者唯一标识符在第2版的标准中加入X.509证书定义。此域用在当同一个X.500名字用于多个证书持有者时,
用一比特字符串来唯一标识证书持有者的X.500名字。可选。
11. 签名算法(Signature Algorithm)
证书签发机构对证书上述内容的签名算法
example: sha256WithRSAEncryption
12. 签名值(Issuer's Signature)
证书签发机构对证书上述内容的签名值</code></pre>
<h3>如何辨别非法证书</h3>
<p>上面提到,XX证书包含了如下内容:</p>
<ol>
<li><p>证书包含了颁发证书的机构的名字 -- CA</p></li>
<li><p>证书内容本身的数字签名(用CA私钥加密)</p></li>
<li><p>证书持有者的公钥</p></li>
<li><p>证书签名用到的hash算法</p></li>
</ol>
<p>浏览器内置的CA的根证书包含了如下关键内容:</p>
<ol><li><p>CA的公钥(非常重要!!!)</p></li></ol>
<p>好了,接下来针对之前提到的两种非法证书的场景,讲解下怎么识别</p>
<h3>完全伪造的证书</h3>
<p>这种情况比较简单,对证书进行检查:</p>
<ol>
<li><p>证书颁发的机构是伪造的:浏览器不认识,直接认为是危险证书</p></li>
<li><p>证书颁发的机构是确实存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。</p></li>
<li><p>用CA的公钥,对伪造的证书的摘要进行解密,发现解不了。认为是危险证书</p></li>
</ol>
<h3>篡改过的证书</h3>
<p>假设代理通过某种途径,拿到XX的证书,然后将证书的公钥偷偷修改成自己的,然后喜滋滋的认为用户要上钩了。然而太单纯了:</p>
<ol>
<li><p>检查证书,根据CA名,找到对应的CA根证书,以及CA的公钥。</p></li>
<li><p>用CA的公钥,对证书的数字签名进行解密,得到对应的证书摘要AA</p></li>
<li><p>根据证书签名使用的hash算法,计算出当前证书的摘要BB</p></li>
<li><p>对比AA跟BB,发现不一致--> 判定是危险证书</p></li>
</ol>
<h2>HTTPS握手流程</h2>
<p>上面啰啰嗦嗦讲了一大通,HTTPS如何确保数据加密传输的安全的机制基本都覆盖到了,太过技术细节的就直接跳过了。</p>
<p>最后还有最后两个问题:</p>
<ol>
<li><p>网站是怎么把证书给到用户(浏览器)的</p></li>
<li><p>上面提到的对称密钥是怎么协商出来的</p></li>
</ol>
<p>上面两个问题,其实就是HTTPS握手阶段要干的事情。HTTPS的数据传输流程整体上跟HTTP是类似的,同样包含两个阶段:握手、数据传输。</p>
<ol>
<li><p>握手:证书下发,密钥协商(这个阶段都是明文的)</p></li>
<li><p>数据传输:这个阶段才是加密的,用的就是握手阶段协商出来的对称密钥</p></li>
</ol>
<p>阮老师的文章写的非常不错,通俗易懂,感兴趣的同学可以看下。</p>
<p>附:《SSL/TLS协议运行机制的概述》:<a href="https://link.segmentfault.com/?enc=kIG%2Bk7ZfOkRsNhrQ7HzH%2FQ%3D%3D.hE5qkues4Qa0y%2FGHdqFxXk4L%2B8F%2F%2BzUEJjgnD%2FGBmJq5OMrk1%2FCO2btWwiy2bT%2Bb%2B68mjvRtcAbX14f4s4uAKw%3D%3D" rel="nofollow">http://www.ruanyifeng.com/blo...</a></p>
<h2>写在后面</h2>
<p>科普性文章,部分内容不够严谨,如有错漏请指出 :)</p>
Reflux系列01:异步操作经验小结
https://segmentfault.com/a/1190000004250062
2016-01-05T13:44:49+08:00
2016-01-05T13:44:49+08:00
程序猿小卡
https://segmentfault.com/u/chyingp
3
<h2>写在前面</h2>
<p>在实际项目中,应用往往充斥着大量的异步操作,如ajax请求,定时器等。一旦应用涉及异步操作,代码便会变得复杂起来。在flux体系中,让人困惑的往往有几点:</p>
<ol>
<li><p>异步操作应该在<strong>actions</strong>还是<strong>store</strong>中进行?</p></li>
<li><p>异步操作的多个状态,如pending(处理中)、completed(成功)、failed(失败),该如何拆解维护?</p></li>
<li><p>请求参数校验:应该在<strong>actions</strong>还是<strong>store</strong>中进行校验?校验的逻辑如何跟业务逻辑本身进行分离?</p></li>
</ol>
<p>本文从简单的同步请求讲起,逐个对上面3个问题进行回答。一家之言并非定则,读者可自行判别。</p>
<p>本文适合对reflux有一定了解的读者,如尚无了解,可先行查看 <a href="https://link.segmentfault.com/?enc=HPxt77h1PO0UXFbsYxhmhA%3D%3D.bXFpZVpL90rJA6gH2degHq43cZbkiMOEE%2FlInyyzU5EQo6KLx6sYbnQFMcE%2BNG3o" rel="nofollow">官方文档</a> 。本文所涉及的代码示例,可在 <a href="https://link.segmentfault.com/?enc=xmMTaonjOi8dZOTTI8oFSQ%3D%3D.FJkmWErYoxxfWcEayfkgQvx08J6NIZLgO6c8ib9jq46ieWcNvYX590SSXQix8FGnMx1yYoMlKOpmOtjDZiS7mzaxFTHcPPBEUyGrn%2FGejgk%3D" rel="nofollow">此处</a>下载。</p>
<h2>Sync Action:同步操作</h2>
<p>同步操作比较简单,没什么好讲的,直接上代码可能更直观。</p>
<pre><code class="js">var Reflux = require('reflux');
var TodoActions = Reflux.createActions({
addTodo: {sync: true}
});
var state = [];
var TodoStore = Reflux.createStore({
listenables: [TodoActions],
onAddTodo: function(text){
state.push(text);
this.trigger(state);
},
getState: function(){
return state;
}
});
TodoStore.listen(function(state){
console.log('state is: ' + state);
});
TodoActions.addTodo('起床');
TodoActions.addTodo('吃早餐');
TodoActions.addTodo('上班');</code></pre>
<p>看下运行结果</p>
<pre><code class="powershell">➜ examples git:(master) ✗ node 01-sync-actions.js
state is: 起床
state is: 起床,吃早餐
state is: 起床,吃早餐,上班</code></pre>
<h2>Async Action:在store中处理</h2>
<p>下面是个简单的异步操作的例子。这里通过<code>addToServer</code>这个方法来模拟异步请求,并通过<code>isSucc</code>字段来控制请求的状态为<strong>成功</strong>还是<strong>失败</strong>。</p>
<p>可以看到,这里对前面例子中的<code>state</code>进行了一定的改造,通过<code>state.status</code>来保存请求的状态,包括:</p>
<ul>
<li><p>pending:请求处理中</p></li>
<li><p>completed:请求处理成功</p></li>
<li><p>failed:请求处理失败</p></li>
</ul>
<pre><code class="js">var Reflux = require('reflux');
/**
* @param {String} options.text
* @param {Boolean} options.isSucc 是否成功
* @param {Function} options.callback 异步回调
* @param {Number} options.delay 异步延迟的时间
*/
var addToServer = function(options){
var ret = {code: 0, text: options.text, msg: '添加成功 :)'};
if(!options.isSucc){
ret = {code: -1, msg: '添加失败!'};
}
setTimeout(function(){
options.callback && options.callback(ret);
}, options.delay);
};
var TodoActions = Reflux.createActions(['addTodo']);
var state = {
items: [],
status: ''
};
var TodoStore = Reflux.createStore({
init: function(){
state.items.push('睡觉');
},
listenables: [TodoActions],
onAddTodo: function(text, isSucc){
var that = this;
state.status = 'pending';
that.trigger(state);
addToServer({
text: text,
isSucc: isSucc,
delay: 500,
callback: function(ret){
if(ret.code===0){
state.status = 'success';
state.items.push(text);
}else{
state.status = 'error';
}
that.trigger(state);
}
});
},
getState: function(){
return state;
}
});
TodoStore.listen(function(state){
console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});
TodoActions.addTodo('起床', true);
TodoActions.addTodo('吃早餐', false);
TodoActions.addTodo('上班', true);</code></pre>
<p>看下运行结果:</p>
<pre><code class="powershell">➜ examples git:(master) ✗ node 02-async-actions-in-store.js
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床
status is: error, current todos is: 睡觉,起床
status is: success, current todos is: 睡觉,起床,上班</code></pre>
<h2>Async Action:在store中处理 潜在的问题</h2>
<p>首先,祭出官方flux架构示意图,相信大家对这张图已经很熟悉了。flux架构最大的特点就是<strong>单向数据流</strong>,它的好处在于 <strong>可预测</strong>、<strong>易测试</strong>。</p>
<p>一旦将异步逻辑引入store,单向数据流被打破,应用的行为相对变得难以预测,同时单元测试的难度也会有所增加。</p>
<p><img src="https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png" alt="enter image description here" title="enter image description here"></p>
<p>ps:在大部分情况下,将异步操作放在store里,简单粗暴有效,反而可以节省不少代码,看着也直观。究竟放在actions、store里,笔者是倾向于放在<code>actions</code>里的,读者可自行斟酌。</p>
<p>毕竟,社区对这个事情也还在吵个不停。。。</p>
<h2>Async 操作:在actions中处理</h2>
<p>还是前面的例子,稍作改造,将异步的逻辑挪到<code>actions</code>里,二话不说上代码。</p>
<p>reflux是比较接地气的flux实现,充分考虑到了异步操作的场景。定义action时,通过<code>asyncResult: true</code>标识:</p>
<ol>
<li><p>操作是异步的。</p></li>
<li><p>异步操作是分状态(生命周期)的,默认的有<code>completed</code>、<code>failed</code>。可以通过<code>children</code>参数自定义请求状态。</p></li>
<li><p>在store里通过类似<code>onAddTodo</code>、<code>onAddTodoCompleted</code>、<code>onAddTodoFailed</code>对请求的不同的状态进行处理。</p></li>
</ol>
<pre><code class="js">var Reflux = require('reflux');
/**
* @param {String} options.text
* @param {Boolean} options.isSucc 是否成功
* @param {Function} options.callback 异步回调
* @param {Number} options.delay 异步延迟的时间
*/
var addToServer = function(options){
var ret = {code: 0, text: options.text, msg: '添加成功 :)'};
if(!options.isSucc){
ret = {code: -1, msg: '添加失败!'};
}
setTimeout(function(){
options.callback && options.callback(ret);
}, options.delay);
};
var TodoActions = Reflux.createActions({
addTodo: {asyncResult: true}
});
TodoActions.addTodo.listen(function(text, isSucc){
var that = this;
addToServer({
text: text,
isSucc: isSucc,
delay: 500,
callback: function(ret){
if(ret.code===0){
that.completed(ret);
}else{
that.failed(ret);
}
}
});
});
var state = {
items: [],
status: ''
};
var TodoStore = Reflux.createStore({
init: function(){
state.items.push('睡觉');
},
listenables: [TodoActions],
onAddTodo: function(text, isSucc){
var that = this;
state.status = 'pending';
this.trigger(state);
},
onAddTodoCompleted: function(ret){
state.status = 'success';
state.items.push(ret.text);
this.trigger(state);
},
onAddTodoFailed: function(ret){
state.status = 'error';
this.trigger(state);
},
getState: function(){
return state;
}
});
TodoStore.listen(function(state){
console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});
TodoActions.addTodo('起床', true);
TodoActions.addTodo('吃早餐', false);
TodoActions.addTodo('上班', true);</code></pre>
<p>运行,看程序输出</p>
<pre><code>➜ examples git:(master) ✗ node 03-async-actions-in-action.js
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床
status is: error, current todos is: 睡觉,起床
status is: success, current todos is: 睡觉,起床,上班</code></pre>
<h2>Async Action:参数校验</h2>
<p>前面已经示范了如何在actions里进行异步请求,接下来简单演示下异步请求的前置步骤:参数校验。</p>
<p>预期中的流程是:</p>
<blockquote><p>流程1:参数校验 --> 校验通过 --> 请求处理中 --> 请求处理成功(失败)<br>流程2:参数校验 --> 校验不通过 --> 请求处理失败</p></blockquote>
<h3>预期之外:store.onAddTodo 触发</h3>
<p>直接对上一小节的代码进行调整。首先判断传入的<code>text</code>参数是否是字符串,如果不是,直接进入错误处理。</p>
<pre><code class="js">var Reflux = require('reflux');
/**
* @param {String} options.text
* @param {Boolean} options.isSucc 是否成功
* @param {Function} options.callback 异步回调
* @param {Number} options.delay 异步延迟的时间
*/
var addToServer = function(options){
var ret = {code: 0, text: options.text, msg: '添加成功 :)'};
if(!options.isSucc){
ret = {code: -1, msg: '添加失败!'};
}
setTimeout(function(){
options.callback && options.callback(ret);
}, options.delay);
};
var TodoActions = Reflux.createActions({
addTodo: {asyncResult: true}
});
TodoActions.addTodo.listen(function(text, isSucc){
var that = this;
if(typeof text !== 'string'){
that.failed({ret: 999, text: text, msg: '非法参数!'});
return;
}
addToServer({
text: text,
isSucc: isSucc,
delay: 500,
callback: function(ret){
if(ret.code===0){
that.completed(ret);
}else{
that.failed(ret);
}
}
});
});
var state = {
items: [],
status: ''
};
var TodoStore = Reflux.createStore({
init: function(){
state.items.push('睡觉');
},
listenables: [TodoActions],
onAddTodo: function(text, isSucc){
var that = this;
state.status = 'pending';
this.trigger(state);
},
onAddTodoCompleted: function(ret){
state.status = 'success';
state.items.push(ret.text);
this.trigger(state);
},
onAddTodoFailed: function(ret){
state.status = 'error';
this.trigger(state);
},
getState: function(){
return state;
}
});
TodoStore.listen(function(state){
console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});
// 非法参数
TodoActions.addTodo(true, true);</code></pre>
<p>运行看看效果。这里发现一个问题,尽管参数校验不通过,但<code>store.onAddTodo</code> 还是被触发了,于是打印出了<code>status is: pending, current todos is: 睡觉</code>。</p>
<p>而按照我们的预期,<code>store.onAddTodo</code>是不应该触发的。</p>
<pre><code>➜ examples git:(master) ✗ node 04-invalid-params.js
status is: pending, current todos is: 睡觉
status is: error, current todos is: 睡觉</code></pre>
<h3>shouldEmit 阻止store.onAddTodo触发</h3>
<p>好在reflux里也考虑到了这样的场景,于是我们可以通过<code>shouldEmit</code>来阻止<code>store.onAddTodo</code>被触发。关于这个配置参数的使用,可参考<a href="https://link.segmentfault.com/?enc=gLuCXB438JVIzBgqhx%2BpZw%3D%3D.inNyFuuoTlBW3Or6Z0Ec5j%2BEZSUmLfUr%2BQVEbUQ6iX50AQTte02N4SSShB6o1H1S" rel="nofollow">文档</a>。</p>
<p>看修改后的代码</p>
<pre><code class="js">var Reflux = require('reflux');
/**
* @param {String} options.text
* @param {Boolean} options.isSucc 是否成功
* @param {Function} options.callback 异步回调
* @param {Number} options.delay 异步延迟的时间
*/
var addToServer = function(options){
var ret = {code: 0, text: options.text, msg: '添加成功 :)'};
if(!options.isSucc){
ret = {code: -1, msg: '添加失败!'};
}
setTimeout(function(){
options.callback && options.callback(ret);
}, options.delay);
};
var TodoActions = Reflux.createActions({
addTodo: {asyncResult: true}
});
TodoActions.addTodo.shouldEmit = function(text, isSucc){
if(typeof text !== 'string'){
this.failed({ret: 999, text: text, msg: '非法参数!'});
return false;
}
return true;
};
TodoActions.addTodo.listen(function(text, isSucc){
var that = this;
addToServer({
text: text,
isSucc: isSucc,
delay: 500,
callback: function(ret){
if(ret.code===0){
that.completed(ret);
}else{
that.failed(ret);
}
}
});
});
var state = {
items: [],
status: ''
};
var TodoStore = Reflux.createStore({
init: function(){
state.items.push('睡觉');
},
listenables: [TodoActions],
onAddTodo: function(text, isSucc){
var that = this;
state.status = 'pending';
this.trigger(state);
},
onAddTodoCompleted: function(ret){
state.status = 'success';
state.items.push(ret.text);
this.trigger(state);
},
onAddTodoFailed: function(ret){
state.status = 'error';
this.trigger(state);
},
getState: function(){
return state;
}
});
TodoStore.listen(function(state){
console.log('status is: ' + state.status + ', current todos is: ' + state.items);
});
// 非法参数
TodoActions.addTodo(true, true);
setTimeout(function(){
TodoActions.addTodo('起床', true);
}, 100)
</code></pre>
<p>再次运行看看效果。通过对比可以看到,当<code>shouldEmit</code>返回<code>false</code>,就达到了之前预期的效果。</p>
<pre><code>➜ examples git:(master) ✗ node 05-invalid-params-shouldEmit.js
status is: error, current todos is: 睡觉
status is: pending, current todos is: 睡觉
status is: success, current todos is: 睡觉,起床</code></pre>
<h2>写在后面</h2>
<p>flux的实现细节存在不少争议,而针对文中例子,reflux的设计比较灵活,同样是使用reflux,也可以有多种实现方式,具体全看判断取舍。</p>
<p>最后,欢迎交流。</p>