TCP

2019-11-17  本文已影响0人  fanstastic

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

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。

数据上传与安全

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

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

路由解析

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')完成映射

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)
}

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

我们通过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渲染。

产品化

上一篇下一篇

猜你喜欢

热点阅读