TCP

作者: fanstastic | 来源:发表于2019-11-17 16:58 被阅读0次

    一、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脚本。

    产品化

    • 项目工程化
      所谓的工程化,可以理解为项目的组织能力。最基本的几步是目录结构、构建工具、编码规范、代码审查。

    相关文章

      网友评论

          本文标题:TCP

          本文链接:https://www.haomeiwen.com/subject/vvieictx.html