1. 程式人生 > >日誌簡介和構建web應用

日誌簡介和構建web應用

Logger 日誌規範

請求響應日誌:

  • 需要記錄的引數:請求方法,請求路由,請求引數。作為一個api端需要的token或者其他authorization資訊,響應時間,響應狀態(http status)。
  • 記錄日誌的格式自定義

解決方法:

  • 請求響應日誌 目前我們更多是在前面的 Nginx 那層去記錄,這塊要自己定製一個並不難,ctx.logger 那塊應該可以覆蓋掉預設的 format 的。

日誌可封裝可解析:

  • 直接引入egg-logger後,原生的日誌輸出不符合基本需求,並且格式不統一。

全鏈路可配置:

request

- header中,因公司不同,可能使用的全鏈路唯一標誌不同。
有的公司用traceId、而有的用request-id,諸如此類的,如果都需要去改原始碼去完成,是否對生產的部署是一種障礙?

解決方法:

  • 全鏈路標記可配置 這塊屬於 tracelog 範疇,這塊其實跟企業內部的架構有關,需要去定製化的。我們內部有鷹眼系統,以及對應的外掛,有興趣可以跟進和推動下這個RFC

優雅的使用egg-logger日誌

  • 請求日誌的格式

記錄的引數:請求方法,請求路由,請求引數(query,body;param收集的可能性不大?),作為一個api端需要的token

或者其他authorization資訊,響應時間,響應狀態(http status)

  • 全鏈路標記可配置

request - header中,因公司不同,可能使用的全鏈路唯一標誌不同。
有的公司用traceId、而有的用request-id,諸如此類的,如果都需要去改原始碼去完成,是否對生產的部署是一種障礙?

  • 解決方案:使用koa-log4來複現已經成型的日誌體系
  • 關掉egg-logger

config.logger.level = 'NONE' and config.logger.consoleLevel = 'NONE'

  • 原生日誌輸出 cfork

    一個第三方庫輸出到 stdout 的,這個肯定不好控制,但它並不會被寫入到 file 裡面的,不影響你分析。

  • 全鏈路路由

  • 嘗試中介軟體進行修改日誌格式:

Node網路程式設計

它具有事件驅動,無阻塞,單執行緒等特性。

Node提供了net,dgram,http,https這4個模組,分別用於處理TCP,UDP,HTTP,HTTPS,適用於伺服器端和客戶端。

構建TCP應用(傳輸控制協議)

TCP傳輸控制協議

OSI:開放式系統互聯通訊參考模型。一種概念模型,由國際標準化組織提出,一個試圖使各種計算機在世界範圍內互連為網路的標準框架。

七層:

物理層(網路物理硬體),

資料鏈路層(網路特有的鏈路介面),

網路層(IP),

傳輸層(TCP/UDP)

會話層(通訊連線/維持會話),

表示層(加密/解密等),

應用層(HTTP,SMTP,IMAP等)。

TCP:面向連線的協議,顯著特徵是傳輸之前需要3次握手形成會話。

在建立會話的過程中,伺服器端和客戶端分別提供一個套接字,這兩個套接字共同形成一個連線。伺服器端和客戶端通過套接字實現兩者之間連線的操作。

socket:套接字,使應用程式能夠讀寫與收發通訊協定與資料的程式。

TCP建立過程和連結折除過程是由TCP/IP協議棧自動建立的。TCP的套接字是可寫可讀的stream物件。

TCP服務事件:

伺服器事件,連線事件

TCP針對網路中的小資料報有一定的優化策略:Nagle演算法.如果每次只發送一個位元組的內容而不優化,網路中將充滿只有少數有效資料的資料包,浪費網路資源。

Nagle演算法針對這種情況,當緩衝區的資料達到一定數量或者一定時間後才將其發出,所以小資料寶將會被Nagle演算法合併,以此來優化網路。這種優化雖然使網路頻寬被有效的使用,但是資料可能被延遲傳送。

構建UDP服務 (使用者資料包協議)

一個套接字可以與多個UDP服務通訊。

提供平面向事務的簡單不可靠資訊傳輸入伍,在網路差的情況下存在丟包嚴重的問題。無需連結,資源消耗低,處理快速靈活。

構建HTTP服務

傳輸層http,smtp

HTTP:超文字傳輸協議

構建websocket服務

在websocket之前,網頁客戶端與伺服器端進行通訊最高效的是comet技術,實現comet技術的細節是採用長輪詢或iframe流,長輪詢的原理是客戶端向服務端發起請求,服務端只在超時或有資料相應時斷開連線,客戶端在收到資料或者超時後重新發起請求,這個請求行為拖長長的尾巴。

構建web應用

憑藉事件驅動和V8高效能,成為服務端額佼佼者。

  • 前後端都是JS的時候,在跨越HTTP進行溝通時:

無需切換語言環境,部分知識不會因為語言環境的切換而丟失,上下文一致性好。

資料(因為JSON)可以很好地實現跨前後端直接使用。

業務,可以輕量的選擇在前端還是在後端進行,語言相通,代價小。

  • request事件發生於網路連線
  • 具體業務的需求

請求方法的判斷,URL的路徑解析,Cookie的解析,Basic認證,表單資料的解析,任意格式檔案的上傳處理。

BASIC認證:一種用來允許網頁瀏覽器或其他客戶端程式在請求時提供使用者名稱口令形式的身份憑證的一種登入驗證方式

高階函式的介紹:可能無限的複雜,但是隻要對總結果返回一個上面的函式作為引數,傳遞給CreateServer()偵聽器就好了

  • RESTful類web服務中請求的方法十分重要,因為他會決定資源的操作行為。

PUT代表一個新建一個資源,POST表示更新一個資源,GET表示檢視一個資源,DELETE表示刪除一個資源。

  • 路徑解析
  • cookie

效能的影響:一旦伺服器向客戶端附送了設定Cookie的意圖,除非Cookie過期,否則客戶端每次請求都會發送這些cookie到服務端,cookie過多會導致報頭較大。

YSlow效能規則:

減小Cookie的大小

為靜態元件使用不同的域名,將域名轉換IP需要進行DNS查詢,多一個域名就會多一次DNS查詢

減少DNS查詢

  • session

如何將每一個客戶和伺服器中的資料一一對應起來:

基於cookie來實現使用者和資料的對映

伺服器開啟了session,將約定一個鍵值作為session的口令,伺服器沒有檢查到使用者請求cookie中攜帶,將會生成一個值唯一不重複,並設定超時時間。

通過查詢字串來實現瀏覽器和伺服器端資料的對應

風險:因為要將位址列中的地址傳送給另外一個人,那麼他就和你相同的身份。cookie的方案在換了瀏覽器或者電腦之後無法生效,較為安全。

  • session集中化的工具:redis,memcached等,Node程序無須在內部維護資料物件,垃圾回收和記憶體限制的問題,並且告訴的快取設計的快取過期策略更加合理。
  • 使用第三方快取的原因

Node與快取服務保持長連結,而非頻繁的短連線,握手導致的延遲隻影響初始化。

快取記憶體直接在記憶體中進行資料儲存和訪問。

快取服務通常與Node程序執行在相同的機器上或者相同的機房裡,網路速度受到的影響較小。

session需要非同步的方式獲取。

  • session安全

將值通過私鑰簽名:

var sign=function(val,secret){
  return val+'.'+crypto.createHmac('sha256'.secret).update(val).digest('base64').replace(/\=+$/,'');
};
  • 快取的規則

新增expire或cache-control到報頭中

配置ETags

讓ajax可快取

  • 條件請求

檢測本地資源是否可用會進行條件請求,在普通的GET請求報文中附帶If-Modified-Since欄位。

如果伺服器沒有新的版本,響應一個304的狀態碼,客戶端使用本地的版本。

如果伺服器有新的版本,將新的內容傳送給客戶端,客戶端放棄本地版本。

// 檢測本地資源是否可用

var handle = function(req, res) {
  fs.stat(filename, function(err, stat) {
    var lastModified = stat.mtime.toUTCString();
    if (lastModified === req.headers['if-modified-since']) {
      res.writeHead(304, 'Not Modified');
      res.end();
    } else {
      fs.readFile(filename, function(err, file) {
        var lastModified = stat.mtime.toUTCString();
        res.setHeader('Last-Modified', lastModified);
        res.writeHead(200, 'OK');
        res.end();
      });
    }
  });
};
  • 時間戳的缺陷

檔案的時間戳改動但內容不一定改動。

時間戳只能精確到秒,更新頻繁的內容將無法生效。

HTTP/1.1 ETag來解決。

  • 快取的更新機制

每次釋出,路徑中跟隨web應用的版本號。

每次釋出,路徑中跟隨該檔案內容的Hash值。

  • 認知返回新增賬戶密碼
res.setHeader('WWW-Authenticate','Basic realm="Secure Area"');
  • 判斷是否有帶內容
var hasBody=function(req){
  return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}

// 流的方式處理data
function(req, res) {
  if (hasBody(req)) {
    var buffers = [];
    req.on('data', function() {
      buffers.push(chunk);
    });
    req.on('end', function() {
      req.rawBody = Buffer.concat(buffers).toString();
      handle(req, res);
    });
  } else {
    handle(req, res);
  }
};

判斷content-type:

var mime=function(req){
  var str=req.headers['content-type']||'';
  return str.split(';')[0];
}

解析xml檔案:

var xml2js=require('xml2js');

var handle = function(req,res){
  if(mime(req)==='application/xml'){
    xml2js.parseString(req.rawBody,function(err,xml){
      if(err){
        res.writeHead(400);
        res.end('Invalid xml');
        return;
      }
      req.body=xml;
      todo(req,res);
    })
  }
}
  • 由於是檔案上傳,那麼像普通表單,json,xml那樣先接收內容在解析的方式變得不可接受。

接收大小未知的資料量的時候:

function (req,res){
  if(hasBody(req)){
    var done=function(){
      handle(req,res);
    }
    if(mime(req)==='application/json'){
      parseJSON(req,done);
    }else if(mime(req)==='application/xml'){
      parseXML(req,done);
    }else if(mime(req)==='multipart/form-data'){
      parseMulipart(req,done);
    }
  }else{
    handle(req,res);
  }
}

req這個流物件直接交給對應的解析方法,有解析方法自行處理上傳的內容,或接受內容儲存在記憶體中,或流式處理掉。

  • formiable

基於流式理解解析報文,將接收到的檔案寫入到系統的臨時資料夾,並返回對應的路徑

// formidable 基於流式處理解析報文
var formidable = require('formidable');

var upload = function (req, res) {
  if (hasBody(req)) {
    if (MimeType(req) == 'mulipart/form-data') {
      var form = new formidable.IncomingForm();
      form.parse(req, function (err, fields, files) {
        req.body = fields;
        req.files = files;
        handle(req, res);
      });
    }
  } else {
    handle(req, res);
  }
}
  • 記憶體限制狀態

限制上傳內容的大小,一旦超過限制就停止接受資料,並相應400狀態碼。

通過流式解析,將資料流導向磁碟中,Node只保留檔案路徑等小資料。

// 限制檔案的大小

var bytes = 1024;
var limit = function (req, res) {
  var received = 0;
  var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;

  if (len && len > bytes) {
    res.writeHead(413);
    res.end();
    return;
  }

  //limit
  req.on('data', function (chunk) {
    received += chunk.length;
    if (received > bytes) {
      req.destory();
    }
  });
  handle(req, res);
}

對於沒有content-length的請求報文,在每個data事件中判斷即可,but,json檔案和XML檔案可能無法完成解析。

  • CSRF
// 檢測
var validateCSRF = function (req, res) {
  var token = req.session._csrf || (req.session._csrf = generateRandom(24));
  var _csrf = req.body._csrf;
  if (token !== _csrf) {
    res.writeHead(403);
    res.end('禁止訪問');
  } else {
    handle(req, res);
  }
}

// csrf新增

var generateRandom = function (len) {
  return crypto.randomBytes(Math.ceil(len * 3 / 4)).toString('base64').slice(0, len);
}
  • 路由解析

MVC模型的主要思想是將業務邏輯按職責分離

控制器

模型

檢視

工作模式:

路由解析,行為呼叫相關的模型進行資料操作,資料操作結束後呼叫檢視和相關資料進行頁面的渲染,輸出到客戶端。