Gone With the Wind

TCP的粘包和半包

什么是粘包

首先请执行以下以下nodejs代码

1
const net = require( 'net' );

const app = net.createServer( connect => {

  connect.on( 'data', function ( data ) {
    console.log( data.toString() ); 
  } );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

  let client = net.connect( { port: 8080, host: '127.0.0.1' } );

  client.write( 'Hi, server!' );

  client.write( 'Hi, server!' );

} );

你看到的输出一定是 Hi, server!Hi, server!,并且data事件的回调函数只执行了一次,WTF.

将代码稍作修改, 客户端write的时候加一个间隔时间

1
const net = require( 'net' );

const app = net.createServer( connect => {

  connect.on( 'data', function ( data ) {
    console.log( data.toString() ); 
  } );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

  let client = net.connect( { port: 8080, host: '127.0.0.1' } );

  client.write( 'Hi, server!' );

  setTimeout( () => {
    client.write( 'Hi, server!' );
  }, 1000 );

} );

你会看到输出分了两次执行,回调函数也执行了两次,符合了我们的预期,但是,总不能我们的所有代码都加一个延时吧?

为什么会出现粘包?

网上的解释已经很明确,这里引用一段,出处已经无法考证了

1
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

如何处理粘包或者半包的情况?

这里采用了一个类似HTTP的方法,定义一个HTTP头:Content-Length: 100,然后这个数据包的大小就是100,我只需要在读到头数据之后,再读100,就是我这个数据包的带下了,如果数据还有剩下,那么就是下一个包的内容,如果数据不足100,那就说明这里只传了半个包,等待下次data事件触发,后半部分的数据就是这个包里面。

举个例子

假设我要传送一个数据: hello world!,首先我需要进行封包,一个数据包由头部和身体组成,其中,头部组成如下:

1
||||00000012

||||这个是头部开始的标示,由服务端和客户端互相约定,00000012,这个是数据体的大小,前面的0为了补全大小位,这里采用了8位,因为默认情况下,TCP的缓冲区大小是8K,8K换成byte就是: 8388608。

所以我们的头部其实固定为12位,那么接下来的就都是数据体,hello world!组成一个数据包之后的样子就是: ||||00000012hello world!,服务端的解包就不说了,就是自己维护一个缓冲区,从头开始解就行了。

better-packet

better-packet,提供了一个封包的解包的方法,专门为了用来解决粘包问题,看看使用了better-package之后会怎么样.

server.js

1
const net = require( 'net' );

const betterPacket = require( './index' );

const app = net.createServer( ( connect ) => {
  
  let unpackager = new betterPacket.UnPackager();

  connect.on( 'data', unpackager.addBuffer.bind( unpackager ) );

  connect.on( 'error', () => {} );

  unpackager.on( 'package', function ( data ) {

    // Hi, server!
    // Hi, server!
    console.log( data.toString() );

  } );

} );

app.on( 'error', error => {} );

app.listen( '8080', '0.0.0.0' );

client.js

1
const net = require( 'net' );

const betterPacket = require( './index' );

const client = net.connect( { port: 8080, host: '127.0.0.1' } );

const packager = new betterPacket.Packager();

let sendData = packager.packageData( 'Hi, server!' );

client.write( sendData );

client.write( sendData );

这里服务端的数据就变得正常了,而且data事件的回调函数也被调了两次。

丧心病狂的adblock

有天在开发一个页面,里面用到了一张图片,图片对应的是一个名字叫ads的数据服务,全称大概是analyse data service,分析型数据服务,图片名字就取了一个ads.png,一开始也觉得没啥,就这么开发了,但是测试的时候发现,这张图片死活加载不出来,控制台提示net::ERR_BLOCKED_BY_CLIENT

一开始以为是图片格式有问题,于是重新转了一下图片,发现问题依旧,单独在新TAB打开这个图片又能正常显示,所以把图片格式问题排除了。

于是就把问题放到这个报错信息上net::ERR_BLOCKED_BY_CLIENT,blocked by client,被客户端,也就是浏览器给拒绝了,难道是返回的头信息有什么问题?server端是webpack起的一个静态文件服务,看了看浏览器的请求记录,发现只有request,都没有response,看来请求都没有走出浏览器。

为了验证上述问题,我把服务停了,然后然后手动更改图片的URL链接,使得重新加载,发现任然报错了,那么就可以确定问题在浏览器端。

各种google,stackoverflow,baidu,都没有找到一个合适的解答,而且其他图片全都没问题,唯独这张图片加载不出来,尝试用命令行起chrome,然后看看控制台输出,也没看到什么有价值的log。心如死灰之际,起身去了趟厕所,尿尿过程中,想到,不如把图片上传到CDN或者一些云服务试试看。

回来立马就传到了OTS上,因为传上去之后文件名会被改变,变成了一些自动生成的信息,然后把网址拿过来,放到了页面上,居然神奇得发现图片能正常显示了,看来找对了方向,和之前的一对比,马上就发现了文件名上有一些不一样,再一看之前的文件名ads,不就是广告么,一下就想到了adblock,至此,折腾了这么久,总算是找到原因了,也找到元凶: adblock,不过这也算是adblock的一个策略吧,从文件名上入手,去搜索了一下adblock的一些别的策略,谨防再次入坑。

nodejs在世界范围内的大型应用

paypal

实现相同的功能,2个人用nodejs干了5个人以前用java干的活,性能提高一倍,代码量减少33%,文件减少了40%。

nodejs 基金会主席:Danese Cooper,2014年加入paypal。

Microsoft

微软自家研发的JS引擎:chakra,主要用来解决在跑在ARM上的windows系统不支持nodejs的问题,微软已经有意向提PR到nodejs主分支上去。

Uber

整个Uber的分布式调度系统都是基于nodejs和redis开发。

API层的业务逻辑基于python,mysql和mongo。

Groupon

原先使用Ruby on Rails,有一次大家PK完之后,发现了nodejs的带来的好处,所以转换成nodejs开发,2013年宣布成功从ROR迁移至nodejs。

目前现在Groupon的技术架构:web front-end mvc / mobile client <–> json <–> nodejs <–> service server

service server可能也是nodejs。

New York Times

移动端核心业务使用nodejs, Express作为web框架,使用dust.js作为模板渲染,所有node代码用coffeeScript,同时使用redis作为node层和核心业务层之间的缓存。

GitHub

找到的资料不多,只找到一条Twitter,是GitHub官网账号在2010年12月20日发布的.

1
Git tag/branch downloads aren't working current.  We're upgrading Node.js.

NASA

因为其背景,找到的资料也不多,2008年5月份的时候,nodesource.com的CEO发布了一条Twitter

1
Welcome to #Nodejs at NASA。

沃尔玛

沃尔玛实验室,在Github上有自己的主页,发布了很多项目,大部分都跟nodejs还有javascript有关,有自己的nodejs框架:hapi,提供restful的API服务,以配置为核心,提供鉴权需求、输入验证、数据缓存和预加载等功能。
另外还有hoekflodloutMUPD8Lumbar等很多服务于nodejs的工具。

并发场景下,mysql的重复插入问题

背景

有如下一张用户表

id uid name is_deleted
1 111 xiaoming 0
2 222 xiaohong 0

为了更好地做记录,同时保证安全,代码不做物理删除,只做逻辑删除,将is_deleted字段置为1,即表示删除。

登陆依赖别人的SSO,如果登陆了之后访问过我的网站的话,那么我就会把这个人记录到自己的系统里面,同时做一些别的操作,uid字段是由SSO系统生成的,每个人唯一。

问题

当两个请求同时访问我的系统时,同时去查询数据库,两次查询都没有查到当前用户,于是这两个请求的逻辑就都走到插入新用户逻辑,同时,因为增加了is_deleted字段,我无法对uid字段加唯一索引,一个用户可以被删除多次,也无法对uidis_deleted两个字段做复合唯一索引,所以这两个请求的插入动作都能成功执行。这时,表里面就有了两条完全相同的用户记录了,问题发生。

问题原因

如果没有is_deleted字段,那么其实不存在这个问题,给uid字段加一个唯一索引,第二次插入的时候必然会报错,所以究其原因,我们需要一把锁,一把在这张表之外的锁。

解决办法

  1. 新建一张表,以uid为主键或者唯一索引,在需要插入用户表之前,先插入这张新建的表,如果插入成功了,那么再往业务表里面插入用户信息。
  2. 使用tair``redis等,在准备插入用户表前,先使用uid作为key插一条记录并加上锁,就能够保证只有一个请求能够抢到锁,抢到后再进行用户信息的插入。

缺陷

  1. 几种方法都比较重,都需要新建一张表,甚至引入一种新的存储。
  2. 假设同时两个请求抢锁,没抢到锁的请求处理会比较麻烦,可能需要定时隔一段时间去检测用户信息是否已经插入,逻辑较重。

uplodify中的大坑

项目中有使用到需要上传图片的地方, 项目本身之前是由别的同事开发的, 上传使用了uploadify来上传图片, 我们这次准备重构, 因为时间较紧, 所以也就沿用的之间的上传方法, 第一次使用uploadify, 觉得用的人还挺多, 不会有什么问题, 但是问题接踵而来。

自动发起的http请求.

因为没有影响功能, 一开始没有特别关注, 后来看控制台, 发现老是会多发出去一个请求, URL是当前域名, 因为URL有问题, 所以老是404, 查遍了所有代码都没有找到发送这个请求的地方, 后来把目光锁定到uploadify上面去, 注释了uploadify, 就发现没有这个请求了, 搜了一下, 遇到这个问题的还挺多, 网上随便找个解决办法就行了, 具体就是当你没有设置image_url的时候, 也会发送一个请求去拿这张图片, 下面有个大坑。

flash版本不会带上二级域名的cookie.

说先说下业务场景, 我们使用oauth的方式登录, 域名是: a.test.com, 登录后, 登录系统会将cookie写入到test.com域名下去, 子系统访问的时候拿到test.com下的cookie, 到登录系统中获取用户信息, 完成用户认证流程, 一个很常规的登录流程.

那么问题来了, 我们子系统的域名是a.test.com, 上传功能我们并不想开放给所有用户使用, 会有一些acl鉴权, 所以需要检测登陆态, 在chromeie下一切正常, 当使用safarifirefox的时候, 后端会提示无法获取用户信息的错, 打印了一下后端拿到的headers, 发现只有a.test.com域名下的cookie, test.com域名下的cookie全都没有获取到, 导致拿不到用户信息. 到这里就很清晰了, 一开始我以为是flash的问题, 对flash也不是很熟, 但是想想flash应该不会这么坑, 就下载了一下编译器自己写了一个测试了一下, 果然在所有浏览器下都是正常的, 那么就只能确定是uploadify的问题了, 找了个反编译工具看了下uploadify的代码, 有点多, 反编译后也不大好看, 没有找到具体原因.

最后的解决办法是, 先将上传的鉴权去掉了, 赶项目时间点, 回头马上迁移到html5去.

如何爬带有reCAPTCHA的页面

reCAPTCHA简介

reCAPTCHA项目是由卡内基梅隆大学所发展的系统,主要目的是利用CAPTCHA技术来帮助典籍数字化的进行,这个项目将由书本扫描下来无法准确的被光学文字辨识技术(OCR, Optical Character Recognition)识别的文字显示在CAPTCHA问题中,让人类在回答CAPTCHA问题时用人脑加以识别[1]。reCAPTCHA正数字化《纽约时报》(New York Times)的扫描存文件,目前已经完成20年份的数据,并希望在2010年完成110年份的数据。2009年9月17日,Google宣布收购reCAPTCHA。这是一个伟大的项目,在发挥验证码作用的同时,使得输入验证码的人对全人类做出了贡献。

项目初衷

有江湖的地方就有爬虫,为什么我会去爬一个带有验证码的网站呢?前段时间刚买了车,但是运到店里面需要一些时间,从销售那问了车架号就一直想蠢蠢欲动,看到车托之家有人可以用车架号查询车辆生产以及配置信息,那也是一个别人开发的系统,但是不知道为什么最近一直处于瘫痪状态,好在还有英文网站可以查询。但是因为英文网站带有reCAPTCHA,而天朝又因为某些不可抗力因素导致reCAPTCHA无法使用,除非挂VPN,一般人查不了配置,那么,为什么不自己开发一个呢?

实现过程

其他爬页面的细节就不说了,主要还是说说reCAPTCHA如何爬,验证码也没想着自动识别,那太复杂了,就把验证码爬下来让用户自己输入吧,reCAPTCHA使用很简单,在页面上嵌入一段JS,然后去问谷歌这个验证码的输入正确与否就OK了,所有遇到的问题如下:

  • script标签发出的请求带有referer头,显然申请reCAPTCHA服务的时候会生成一个唯一性的token,带着token才能请求到验证码,而发送请求的时候会验证`referer头,如果不是127.0.0.1或者localhost或者申请时候填写的网址,那么谷歌就拒绝提供服务。解决办法:本身因为天朝地址无法访问谷歌服务,就准备在服务端对这些请求做一次转发,转发时候不要带上referer头即可。

  • reCAPTCHA的运行原理很简单,首先页面加载一个谷歌JS文件:http://www.google.com/recaptcha/api/challenge?k=6Ldlev8SAAAAAF4fPVvI5c4IPSfhuDZp6_HR-APV,这个k参数就是上面提到申请reCAPTCHA服务的时候给你一个唯一性的token,当然要经过服务器转发,不然天朝子民无法获得这个请求,这个JS的返回格式如下:

    1
    
    var RecaptchaState = {
        challenge : '03AHJ_VutQSbh3e1Zy9JyHmt_Zmt83Gl7r0UpTa7Hq8KHlT7XGb1zxpMDyON6W70ZB3K2rY1kxWj31xkzVCJEy4SI-KsxOac-Rvh32JMoIT2-IX6gbAgj24p-SiYSYhHyYu00OAcePod4nf5QZuFdOWNRwZfcCkemd55F6dfJOqDTmPp-i2ySI2lId8Eo5d557NGZvpYYBmLLwFtyewzBfhpEwYJgR2L0lSA',
        timeout : 1800,
        lang : 'zh-CN',
        server : 'https://www.google.com',
        site : '6Ldlev8SAAAAAF4fPVvI5c4IPSfhuDZp6_HR-APV',
        error_message : '',
        programming_error : '',
        is_incorrect : false,
        rtl : false,
        t1 : 'Ly93d3cuZ29vZ2xlLmNvbS9qcy90aC9TMFJ2cEhDbHY2a29udUt6cUtXYkxMUnY0WHM0bEVSblphTGlucDNfelo4Lmpz',
        t2 : '',
        t3 : 'OXVBWHdpOTZlcEp....(这里是一大串类似密文的东西)'
    };
    
    document.write('<scr'+'ipt type="text/javascript" s'+'rc="' + RecaptchaState.server + 'js/recaptcha.js"></scr'+'ipt>');

注意这个地方有个server属性,谷歌还是挺良心的,后续发的所有请求,比如获取验证码图片或者语音的请求,host都是这个server属性所带的值,同样,这些请求需要转发,不然请求不到,在服务端获取这个JS文件时,直接把server属性替换掉,替换你自己服务器转发地址,后续的请求就都往你自己服务器上发了,既能让天朝子民访问reCAPTCHA,又能在自己的域名上爬到别人域名的reCAPTCHA验证码。

  • 谷歌毕竟是谷歌,即使爬到了验证码,他也不会让你好用,谷歌的识别能力还是挺强的,原本想带上一些随机agentx-forwarded-for之类的头来模拟我是一个代理而已,但是无一例外,都被识别为了爬虫而拒绝服务,最后采用的还是没有带上任何头,直接请求,至少服务能用,但是等到用的人多了之后,验证码就会变得非常复杂,好在还有语音验证码可以用。

总结

这次爬得不算完美,当PV超过2 300的时候,验证码就变得非常恶心了,常人一般难以识别,这块还在想办法。最后贴出这个应用实例吧:宝马中文车架号查询系统

coffee在编写过程中的一些坑

CoffeeScript在编写过程之中,相对于JS可以带来很多便利性,但是同时也会引入一些问题,JS相对于Coffee来说有一些繁琐,但是相对严谨。

这里会列出coffee诸多的坑,其实也不能算是坑,只是在还没有灵活运用时,很难排查的一些小问题,也要注意一下自己平时的编写习惯。

  • 在生产过程中的一个案例。
1
{
  mode,
  base_cdn, 
  # data : encodeURIComponent JSON.stringify { formData : body, type },
  web_config: JSON.stringify web_config,
  user : locals.user
}

这个JSON原来的代码是:

1
{
  mode,
  base_cdn, 
  # data : encodeURIComponent JSON.stringify { formData : body, type },
  web_config: JSON.stringify web_config,
}

因为Coffee在描述JSON时候无需添加逗号,然后写这段代码的同事恰好将每一行后面都加上了逗号,然后我一看,没啥问题,紧接着加了一个属性”user”,乍一看,基于coffee松散的语法,毫无任何问题。然后,发现后面,这个JSON的user属性始终是undefined,因为后面有很长一段逻辑,一直以为是后面的逻辑出了问题,经过一个下午的调试后,才开始怀疑是JSON的问题,编译一看,顿时内牛满面:

1
{
  mode: mode,
  base_cdn: base_cdn,
  web_config: JSON.stringify(web_config, {
    user: locals.user
  }
}

node cover

nodejs 的三大特色:

nodejs

异步解决方案历史变迁

回调嵌套 -> event-pipe, async -> co -> fibjs

koa

基于co, 使用generator来使得异步代码使用起来同步化。

需求来了

  1. koa封装了大量逻辑, 只想使用类似connect的提供原始IncomingMessageServerResponse对象.

  2. 很多中间件都是采用connect中间件方式写的, 如果切换到koa, 必定要对原先的代码做修改.

node-cover

  1. 只提供基础的http server服务.

  2. 兼容connect中间件, 不兼容koa中间件, 但是可以使用ES6写的各种模块.

  3. 无缝切换现有项目, 同时使用co写同步代码.

使用方法

coffee-script v1.8

一. coffee-script的github master分支已经是1.8版本, 对es6很多语法都做了支持, 其中就包括generator.

1
# Function
( test ) ->
  ....

# GeneratorFuntion
( test ) ->
  yield ->

二. 使用git地址安装npm包

package.json

1
{
  .......
  dependencies : {
    "coffee-script" : "git+ssh://git@github.com:jashkenas/coffeescript.git"
  }
}

//git://github.com/user/project.git#commit-ish
//git+ssh://user@hostname:project.git#commit-ish
//git+ssh://user@hostname/project.git#commit-ish
//git+http://user@hostname/project/blah.git#commit-ish
//git+https://user@hostname/project/blah.git#commit-ish

三. 同时, 1.8的coffee修正了一些逻辑, 偷懒的同学注意了

1
( @server ) ->

对于如上代码,1.8 版本以前的coffee会编译成

1
  fucntion( server ){
  @server = server
.....
}

但是对于1.8版本coffee会编译成:

1
function( _at_server ){
  @server = _at_server
  .....
}

如果直接在函数中使用server变量的话,1.8版本之前是可以的,但是在1.8版本之后就会报错了。

如何使用hexo搭建一个个人博客

如何快速搭建一个hexo博客。

一. 安装hexo

1
npm install hexo -g

二. 初始化hexo目录

1
mkdir hexo
cd hexo 
hexo init
npm install

三. 所有的博客文章都在source->_posts文件夹下面, 一个markdown文件就是一篇博客, 执行一下命令, 就能够在本地起一个服务预览一下。

1
# 将markdown文件编译成html
hexo generate
# 启动服务
hexo server

然后访问: http://127.0.0.1:4000, 就可以查看博客预览了。

四. 注册一个github账号, 创建一个一下名字的项目:#{github name}.github.io, 例如perterpon.github.io.

五. 修改hexo配置文件, 配置文件位于/_config.yml

e.g

1
deploy: 
  # type 选择github
  type: github
  # 填写github地址
  repository: https://github.com/zippera/zippera.github.io.git
  # 选择相应分支, 使用github pages的时候一般默认是使用gh-pages分支作为使用的分支, 可以在github上设置.
  branch: master

六. 创建github pages, 在之前创建好perterpon.github.io项目中, 点击settings- Automatic page generator按钮, 跟着提示一路确定就能完成创建了。

七. 在hexo文件夹中, 执行hexo deploy, 提示成功后等待几分钟访问http://perterpon.github.io, 就能访问自己的博客了。

八. 大量主题样式等等, 评论系统: 多说.