TCP

一、OSI模型
OSI模型由七层组成,分别为物理层,数据链路层,网络层,传输层,会话层,表示层,应用层。

  • 应用层协议基于tcp协议构建,比如http,smtp。
  • 表示层主要负责加密,解密。
  • 会话层负责通信连接和维持会话。
  • 传输层由TCP/UDP组成。
  • 网络层IP

TCP是面向连接的协议,需要经过三次握手才能连接,连接后才能相互发送数据。连接的过程中客户端和服务器端分别提供一个套接字,这两个套接字共同形成一个连接。套接字是ip地址和端口的组合,应用程序可以通过它发送和接收数据。

创建一个tcp服务

let net = require('net')
let server = net.createServer(function(socket){
    
})

net.createServer(listener),listener是创建服务的侦听器

TCP服务的事件
代码分为服务器事件和连接事件

  1. 服务器事件
    对net.createServer而言,它是一个eventEmitter实例,它的自定义事件有

  2. server.listen(port, 侦听器)

  3. connecttion,每个客户端的套接字连接到服务器时触发

  4. close,当close()调用后,服务器停止接收新的套接字。

  5. 连接事件
    服务器可以同时与多个客户端保持连接,对于每个连接而言是典型的可写可读stream对象。Stream对象可以用于服务器端和客户端之间的通信,既可以通过data事件从一端读取另一个发来的数据,也可以通过write从一端向另一端发送数据。

data,当一端向另一端发送数据时,接收端会触发该事件
connect,用于客户端,当套接字与服务器端连接成功时触发
drain,当任意一端调用write发送数据时会触发
close,当套接字完全关闭时触发

HTTP

http服务器继承自tcp服务器,它能与多个客户端保持连接,由于其采用事件驱动,并不会为每一个连接创建额外的线程或进程,保持很低的内存占用,所以能实现高并发。http服务与tcp服务模型的区别在于,开启keepalive后,一个tcp会话可以用于多次请求和响应。tcp服务以connection为单位服务,http以request为单位进行服务。http模块是将connection到request的过程进行了封装。

http请求:
请求报文的请求头一般包含请求的url,方法,请求地址

function (req,res) {
   let buffers = [];
  req.on('data', function(chunk){
      buffers.push(chunk)
  }).on('end', function(){
      let buffer = Buffer.concat(buffers)
  })

http响应
结束时务必调用res.end结束请求,否则客户端将一直处于等待的状。当然也可以通过end实现客户端和服务器端的延时连接。

http服务的事件
connection事件:在开始http请求和响应前,客户端和服务器需要建立底层的tcp连接,这个连接可能开启了keep-alive可以在多次请求和响应使用,当这个连接建立时,触发一次connection事件。

request事件: 当请求数据发送到服务器端,在解析出http请求头后,将会触发该事件,res.end执行后,tcp连接可能会用于下一次请求。

close事件:调用close方法后停止接受新的连接,当已有的连接都断开时触发该事件。

http代理
如同服务器端的实现一般,http提供的clientRequest对象也是基于tcp连接实现的。

websocket
websocket实现了服务器端与客户端之间的长连接,它能够双向数据通信,在websocket之前,数据通信最高效的技术是comet,实现细节是长轮询,原理是客户端向服务器发起请求,服务器端只有在超时或者数据响应时断开连接(res.end)客户端在收到数据或者超时后重新发起请求。使用websocket只需要一个tcp连接就可以完成双向通信,websocket主要分为两部分,握手和数据传输

网络服务与安全

  1. 密钥,tls/ssl是一个典型的公钥/私钥结构,它是一个非对称的结构,每个服务器和客户端都有自己的公钥和私钥,公钥加密传输的数据,私钥解密收到的数据,所以在建立安全传输之前,服务器端和客户端需要先交换公钥。客户端发送数据需要使用服务器端的公钥加密数据,服务器端发送的数据则需要客户端的公钥进行加密。
    公私钥的非对称加密虽然好,但是网络中依然存在窃听的情况。典型的例子是中间人攻击,在客户端和服务器交换公钥的过程中,中间人对服务器扮演客户端的角色,对客户端扮演服务器的角色。因此客户端和服务器端几乎感觉不到中间人的存在。为了解决这种问题,数据传输过程中还需要对公钥认证,以确保公钥来自于目标服务器。为了解决这个问题tsl/ssl引入了数字证书来进行认证。数字证书中有颁发机构的签名,在建立连接前,会通过证书中的签名确认收到的公钥来自目标服务器,从而产生信任关系。
  2. 数字证书
    ca的作用是为站点颁发证书,且这个证书中具有ca实现的签名。
    为了得到签名证书,服务器需要通过自己的私钥生成csr文件,ca将通过这个文件颁发签名证书。
    客户端需要通过ca的证书验证公钥的真伪,知名的ca机构的证书一般预装在浏览器中。
function (req,res) {
  var id = req.cookies[key];
  if (!id) {
    req.session = generate()
  } else {
    store.get(id, function(err, session){
       if (session) {
       } else {
           
        }
    }
})
  }
}

session与安全

session的口令依然保存在客户端,这里会存在口令被盗用的情况,如果让口令更安全,有一种做法是将这个口令通过私钥加密进行签名,使得伪造成本较高。由于不知道私钥,签名信息很难伪造。这样一来,即使知道sessionId的值,只要不知道秘钥的值。当然,如果攻击者获得了真实的id值和签名,就有可能实现身份的伪装。一种方案是将客户端的某些独有信息与口令作为原值,然后签名,这样攻击者一旦不在原始的客户端进行访问,就会导致签名失败。这些独有信息包括用户ip和用户代理。
但是原始用户与攻击者之间也存在上述信息相同的可能新,如局域网出口ip相同,客户端信息,xss漏洞,通过xss漏洞拿到用户口令。

xss漏洞
xss的全称是跨站脚本攻击,通常都是由网站开发者决定哪些脚本可以执行在浏览器端,不过xss漏洞会让别的脚本执行。它的主要原因多数是用户的输入没有被转义,而是直接被执行。

缓存

首先,发起请求,是否有本地文件,如果是,要看一下是否可用,如果可用再采用本地文件,本地没有文件必然发出请求。然后将文件缓存,如果不能确定这份文件是否可用,它将会发起一次条件请求,所谓条件请求就是在get请求中。附带if-modified-since字段,它将询问服务器端是否有更新版本,本地文件的最后修改时间。如果服务器端没有新的版本,服务器返回304,客户端直接使用缓存,如果服务器端有新的版本就使用新的版本。
服务器使用etag作为唯一标识,服务器端可以决定etag的生存规则,根据文件内容生成散列值.
与if-modified-since/last-modified不同,if-none-match/etag是作为请求和响应。浏览器在收到etag的请求后,会在后续的请求中添加if-none-match,如何让浏览器不发送请求直接在本地获取缓存,在响应里设置cache-controled和expires头。expires是一个gmt格式的时间字符串,浏览器在接到这个过期值后,只要本地还存在缓存文件,在到期时间之前都不会发起请求。expires的缺陷是浏览器和服务器时间之间不一致,如果文件提前过期,但到期后并没有删除。cache-control可以设置max-age,使用倒计时的方式判断缓存是否删除。如果两者同时存在max-age会覆盖expires。

  • 清除缓存
    缓存一旦设定,当服务器意外更新时,却无法通知客户端更新。这使得我们在使用缓存时也要为其设定版本号,所幸浏览器是根据url进行缓存,那么一旦内容更新,我们就让浏览器发起新的url请求,使得新内容能够被客户端更新。根据文件内容形成的hash值更加标准。
    因为内容没有更新时,版本号的改动毫无意义。

  • basic认证
    basic认证会检查报文头中authorization 字段,该字段由认证方式和加密值组成。

  • 数据上传
    将收到的buffer列表转化为一个Buffer对象后,再通过toString方法转换成字符串

  • 附件上传
    content-type: multipart/form-data; boundary-adsfa 它代表本次提交内容由多部分组成,每部分的分界符

数据上传与安全

  1. 内存限制
    在解析表单、json、xml部分,我们采用的策略是先保存用户提交的所有数据,然后解析处理交给业务逻辑。这种策略的问题在于仅仅适合小数据的提交请求。要解决这个问题有两种方案,一是限制上传的内容大小。二是通过流式解析,将数据导向到磁盘中,node中只保留路径。

  2. csrf
    跨站请求伪造,csrf不需要知道用户的sessionid就能让用户中招,举例,某网站通过接口提交留言,服务器端会从session数据中判断是谁提交的,正常情况下,谁提交的留言,就会在列表中显示谁的信息。 在b网站中往a网站提交数据,诱导用户触发表单提交,就会将所携带的cookie一同提交,尽管这个提交来自b站,但是服务器和用户都不知道。解决csrf攻击的方案有添加随机值的方式,如下所示:
    每次在表单提交时增加一个随机值,然后在服务器端对这个随机值进行验证,同源页面在每次发请求的时候带上token给后端验证

路由解析

  • restful
    restful的设计哲学主要将服务器端提供的内容实体看做一个资源,并表现在url上。比如一个用户地址 /users/jackjsontian 这个地址代表了一个资源,对这个资源的操作,主要体现在http请求方法上,不是体现在url上。过去增删改查的url的设计方式会将操作体现在url,比如/users.remove?username=jackjsontian,在restful中DELETE /user/jacksontian.对这个请求资源的表现形态也不体现在url上,而是体现在http请求报文中的accept字段,然后服务器端在response中的报文中通过content-type字段体现。
    restful的设计就是通过url定义资源,请求方法定义操作,accept定义资源的表现形式。
let routes = { 'all' : []}
let app = {}
app.use = (path, action) => {
    routes.all.push([path, action])
}
['get', 'post', 'put', 'delete'].forEach(method => {
    routes[method] = {}
    app[method] = () => {
        routes[method].push()
    }
})

通过app.post('/user/:username', 'get')完成映射

  • 中间件
    使用中间件简化和隔离基础设施与业务逻辑之间的细节,让开发者能够关注在业务开发上,中间件的含义是封装底层细节,为上层提供服务,这里提供的中间件是为我们封装所有http请求细节处理的中间件。
    从http请求到具体业务之间,有很多细节要处理。node的http模块提供了应用层协议网络的封装。
    中间件的上下文就是请求对象和响应对象:由于node异步的原因,我们需要一种机制,在当前中间件执行完成后,通知下一个中间件执行。
let querystring = (req, res, next) => {
    req.query = url.parse(req.url, true).query
    next()
}

let cookie = (req, res, next) => {
    var cookie = req.headers.cookie
    var cookies = {}
    if (cookie) {
        var list = cook.split(';')
        for (let i=0;i<list.length) {
             let pair = list[i].split('=') 
            cookies[pair[0].trim()] = pair[1]
        }
    }
    req.cookies = cookies
    next()
}

app.use = (path) => {
  let handle = {
    path: pathRegexp(path),
    // static返回一个数组,存储中间件函数,将use函数除了第一个参数后的所有参数都添加到stack数组中
,也就是说use函数后参数可以传递多个中间件函数
    stack: Array.prototype.slice.call(arguments, 1)
  }
  routes.all.push(handle)
}

优化后的中间件处理函数
app.use = (path) => {
  let handle;
  if (typeof path == 'string') {
    hanle = {
        path: pathRegexp(path),
       // arguments作为要处理的数组元素本身,传入1作为参数执行slice方法,arg作为类数组没有slice方法,所以要调用原型的slice方法
        stack: Array.prototype.slice.call(arguments, 1)
    }
  } else {
    hanle = {
      path: pathRegexp('/') // 如果没有传入路径,那么就是默认/下的所有路径,
      stack: Array.prototype.slice.call(arguments, 0)
    }
  }
  routes.add.push(handle)
}  

let handle = (req,res,stack) => {
    let next = () => {
        
    }
}
  • 中间件的异常处理
var handle  = (req, res, stack) => {
  let next = (err) => {
    if (err) hanle500()
    try { middleware(req, res, next) } catch() { next(err) }
  }
  return next()
}

由于异步方法的不能直接捕获异常,中间件的异常需要自己传递出来。

let session = (req, res, next) => {
  let id = req.cookies.sessionid
  store.get(id, (err, session) => {
    if (err) next(err)
  })
}

next方法接到异常对象后,会将其交给handle500处理。

let handle500 = (err, req, res, stack) => {
  stack = stack.filter((middleware) => {
      return middleware.length === 4
  })
  let next = () => {
    
  }
  return next()
}
  1. 合理使用路由
    拥有一堆的中间件后,并不意味着每个中间件我们都使用,合理的路由使得不必要的中间件不参与请求处理的过程。
    假设我们有一个静态文件的中间件,它会对请求进行判断,如果磁盘上存在对应的文件,就响应对应的静态文件,否则就交由下游的中间件处理。
let staticFile = (req, res, next) => {
  let pathname = url.parse(req,url).pathname
  
}

页面渲染

响应可能是一个html网页,也可能是css,js文件或者其他多媒体文件。

  1. MIME
    浏览器根据不同的content-type采用了不同的处理方式,这个值我们简称MIME。
  2. 附件下载
    在一些场景下,无论响应的内容是什么样的MIME值,需求中并不要求客户端去开发它,只需要弹出并下载它,可以使用content-disposition字段,它还可以通过参数指定保存时的文件名。
    我们设计一个响应附件下载的api
res.sendFile = (filepath) => {
  fs.stat(filepath)
}

当我们的url因为某些问题不能处理当前请求,需要将用户跳转到别的url时候,我们可以使用302.

模板引擎

模板技术的本质就是模板文件和数据通过模板引擎生成最终的html代码
模板技术四要素:模板语言,模板文件,数据,模板引擎
模板语言就是java,jsp等语言,模板引擎就是web容器
数据+模板经过模板引擎处理变成html

我们通过render方法实现一个简单的模板引擎

  1. 语法分解。提取出普通字符串和表达式,这个过程通常用正则表达式匹配出来,
  2. 处理表达式。将标签表达式转换成普通的语言表达式
  3. 生成待执行的语句
  4. 与数据一起执行,生成最终的字符串
let render = (str, data) => {
  let tpl = str.replace(/<%=([\s|S]+?)%>/g, (match, code) => {
       return `${data}.code`
  })
  let tpl = `${tpl}\nreturn tpl`
  let compiled = new Function(tpl)
  return compiled(data)
}
  • 模板编译
    为了能够最终与数据一起生成字符串,我们需要将原始的字符串转换成一个函数对象。
function(obj) {
  let tpl = 'Hello ' + obj.username + '.';
  return tpl
}

这个过程称为模板编译,生成的中间函数只和模板字符串相关,与具体的数据无关。如果每次都生成这个中间函数,就会浪费cpu。为了提升模板渲染的性能速度,我们通常会采用模板预编译的方式。

let compile = (str) => {
  // 将标签表达式变成字符串表达式
    let tpl = str.replace(/<%=([\s\S+?])%>/g, (match, code) => {
        return `obj.${code}`
    })
    tpl = `${var tpl = tpl + }\nreturn tpl;`
    // 执行字符串表达式,生成最终的字符串
    return new Function('obj, escape', tpl)
}

let render = (complied, data) => {
  // data是还没处理过的字符串
  return compiled(data)
}

通过预编译缓存模板编译后的结果,实际应用中就可以实现一次编译,多次执行,而原始的方式每次执行过程中都要进行一次编译,一次执行。

  • with的应用
    上面的模板引擎只能实现变量替换

  • 模板安全
    前文提到的xss漏洞,它的产生大多和模板相关,如果上文的username是一个script脚本,那么这个脚本就会被直接执行,为了提高安全性,模板都会有转义的功能。

我们通过compile函数将待处理的模板编译成待执行的字符串。
为了防止每一次请求都重新去读模板文件,我们需要优化render函数

let cache = {}
let view_folder = 'path/views'
res.render  = (viewname, data) => {
  if (!cache[viewname]) {
    let text;
    try {
      text = fs.readFileSync(path.join(view_folder, viewname), 'utf8')
    } catch(e) {
      
    }
  }
}

这个render实现的过程中,虽然有同步读取文件的情况,但由于采用了缓存,只会在第一次读取的时候造成整个进程阻塞,一旦缓存生效将不会反复读取模板文件。其次缓存前已经进行了编译,不会每次都进行编译。

bigPipe

为了解决重数据页面的加载速度问题,最终的html要在所有的数据都获取完成之后才输出到浏览器。node通过异步将多个数据源的获取并行起来。在数据响应前用户看到的是空白,体验并不好。
bigpipe的解决思路是将页面划分成多个部分,先向用户输出没有数据的布局,再将每个部分逐步输出到前端,再最终渲染填充框架,完成页面渲染。

玩转进程

node在选型时基于v8构建,我们的js将会运行在单个进程的单个线程上。我们的js是运行在单个进程的单个线程上。它的好处是程序状态单一,在没有多线程的情况下,没有锁和线程同步的问题。
单线程有一个问题就是如何充分利用多核cpu,另外,node执行在单线程上,一旦单线程上的异常没有被捕获,就会引起整个进程的崩溃。这抛出了第二个问题,如何保证进程的健壮性和稳定性。
严格来说,node并非真正的单线程架构,node自身还有一定的io线程存在,这些io线程由底层的libuv处理,这部分线程对js开发者来说是透明的。

多进程架构
面对单进程单线程对多核使用不足的问题,前人的经验是启动多进程即可。每个进程各利用一个cpu,以此实现多核cpu的利用。node提供了child_process模块。
在浏览器中,js主线程与ui渲染共用一个线程,执行js的时候ui渲染是停滞的,渲染ui时,js执行是停滞的,两者相互阻塞。webwork允许创建工作线程并在后台运行,使得一些阻塞较为严重的计算不影响主线程上的ui渲染。

  • 持续集成
    将项目工程化可以帮助我们把项目组织成比较固定的结构,对实际项目而言,频繁的迭代是常见的状态,如何记录版本的迭代信息,需要一个持续集成的环境。
    利用travis-ci实现持续集成,用户在push代码后会触发一个hook脚本。

产品化

  • 项目工程化
    所谓的工程化,可以理解为项目的组织能力。最基本的几步是目录结构、构建工具、编码规范、代码审查。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容

  • 1、TCP状态linux查看tcp的状态命令:1)、netstat -nat 查看TCP各个状态的数量2)、lso...
    北辰青阅读 9,419评论 0 11
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,051评论 0 8
  • 转。。。。。。。。 SOCKET,TCP/UDP,HTTP,FTP (一)TCP/UDP,SOCKET,HTTP,...
    zeqinjie阅读 3,276评论 1 53
  • TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。关于TC...
    字节码阅读 1,007评论 0 3
  • 感赏自己坚持朗诵魔力 感赏老公和女儿开心玩耍 感赏妈妈帮助我分担家务 感赏自己做晚饭 感赏今天吃的炸鸡好美味 感赏...
    艳荣宝宝阅读 101评论 0 0