手寫Node資源文件服務(wù)器

想寫靜態(tài)資源服務(wù)器,首先我們需要知道如何創(chuàng)建一個http服務(wù)器供屉,它的原理是什么

http服務(wù)器是繼承自tcp服務(wù)器 http協(xié)議是應(yīng)用層協(xié)議溺蕉,是基于TCP的

http的原理是對請求和響應(yīng)進行了包裝,當(dāng)客戶端連接上來之后先觸發(fā)connection事件哗魂,然后可以多次發(fā)送請求漓雅,每次請求都會觸發(fā)request事件

let server = http.createServer();
let url = require('url');
server.on('connection', function (socket) {
    console.log('客戶端連接 ');
});
server.on('request', function (req, res) {
    let { pathname, query } = url.parse(req.url, true);
    let result = [];
    req.on('data', function (data) {
        result.push(data);
    });
    req.on('end', function () {
        let r = Buffer.concat(result);
        res.end(r);
    })
});
server.on('close', function (req, res) {
    console.log('服務(wù)器關(guān)閉 ');
});
server.on('error', function (err) {
    console.log('服務(wù)器錯誤 ');
});
server.listen(8080, function () {
    console.log('server started at http://localhost:8080');
});
  • req 代表客戶端的連接羹与,server服務(wù)器把客戶端的請求信息進行解析庶灿,然后放在req上面
  • res 代表響應(yīng)往踢,如果希望向客戶端回應(yīng)消息徘层,需要通過 res
  • reqres都是從socket來的,先監(jiān)聽socketdata事件瘦癌,然后等事件發(fā)生的時候跷敬,進行解析,解析出請頭對象斤寇,再創(chuàng)建請求對象,再根據(jù)請求對象創(chuàng)建響應(yīng)對象
  • req.url 獲取請求路徑
  • req.headers 請求頭對象

接下來我們對一些核心功能進行講解

深刻理解并實現(xiàn)壓縮和解壓

為什么要壓縮呢娘锁?有什么好處?

  • 可以使用zlib模塊進行壓縮及解壓縮處理,壓縮文件以后可以減少體積莫秆,加快傳輸速度和節(jié)約帶寬代碼

壓縮和解壓縮對象都是transform轉(zhuǎn)換流,繼承自duplex雙工流即可讀可寫流

  • zlib.createGzip:返回Gzip流對象镊屎,使用Gzip算法對數(shù)據(jù)進行壓縮處理
  • zlib.createGunzip:返回Gzip流對象伟端,使用Gzip算法對壓縮的數(shù)據(jù)進行解壓縮處理
  • zlib.createDeflate:返回Deflate流對象,使用Deflate算法對數(shù)據(jù)進行壓縮處理
  • zlib.createInflate:返回Deflate流對象党巾,使用Deflate算法對數(shù)據(jù)進行解壓縮處理

實現(xiàn)壓縮和解壓

因為壓縮我文件可能很大也可能很小霜医,所以為了提高處理速度,我們用流來實現(xiàn)

let fs = require("fs");
let path = require("path");
let zlib = require("zlib");
function gzip(src) {
  fs
    .createReadStream(src)
    .pipe(zlib.createGzip())
    .pipe(fs.createWriteStream(src + ".gz"));
}
gzip(path.join(__dirname,'msg.txt'));
function gunzip(src) {
  fs
    .createReadStream(src)
    .pipe(zlib.createGunzip())
    .pipe(
      fs.createWriteStream(path.join(__dirname, path.basename(src, ".gz")))
    );
}
gunzip(path.join(__dirname, "msg.txt.gz"));

  • gzip方法用于實現(xiàn)壓縮
  • gunzip方法用于實現(xiàn)解壓
  • 其中文件msg.txt是同級目錄
  • 為什么需要這么寫:gzip(path.join(__dirname,'msg.txt'));
  • 因為console.log(process.cwd());打印出當(dāng)前工作目錄是根目錄署海,并不是文件所在目錄,如果這么寫gzip('msg.txt');找不到文件就會報錯
  • basename 從一個路徑中得到文件名砸狞,包括擴展名的捻勉,可以傳一個擴展名參數(shù)踱启,去掉擴展名
  • extname 獲取擴展名
  • 壓縮的格式和解壓的格式需要對上研底,否則會報錯

有些時候我們拿到的字符串不是一個流,那怎么解決呢

let zlib=require('zlib');
let str='hello';
zlib.gzip(str,(err,buffer)=>{
    console.log(buffer.length);
    zlib.unzip(buffer,(err,data)=>{
        console.log(data.toString());
    })
});
  • 有可能壓縮后的內(nèi)容比原來還大冠蒋,要是內(nèi)容太少的話乾胶,壓縮也沒什么意義了
  • 文本壓縮的效果會好一點,因為有規(guī)律

在http中應(yīng)用壓縮和解壓
下面實現(xiàn)這樣一個功能牙躺,如圖:

1521182342949.png

客戶端向服務(wù)器發(fā)起請求的時候腕扶,會通過accept-encoding(比如:Accept-Encoding:gzip,default)告訴服務(wù)器我支持的解壓縮的格式

  • 服務(wù)器端需要根據(jù)Accept-Encoding顯示的格式進行壓縮,沒有的格式就不能壓縮脓恕,因為瀏覽器無法解壓
  • 如果客戶端需要的Accept-Encoding中的格式服務(wù)端沒有窿侈,也無法實現(xiàn)壓縮
let http = require("http");
let path = require("path");
let url = require("url");
let zlib = require("zlib");
let fs = require("fs");
let { promisify } = require("util");
let mime = require("mime");
//把一個異步方法轉(zhuǎn)成一個返回promise的方法
let stat = promisify(fs.stat);
http.createServer(request).listen(8080);
async function request(req, res) {
  let { pathname } = url.parse(req.url); 
  let filepath = path.join(__dirname, pathname); 
  // fs.stat(filepath,(err,stat)=>{});現(xiàn)在不這么寫了,異步的處理起來比較麻煩
  try {
    let statObj = await stat(filepath);
    res.setHeader("Content-Type", mime.getType(pathname));
    let acceptEncoding = req.headers["accept-encoding"];
    if (acceptEncoding) {
      if (acceptEncoding.match(/\bgzip\b/)) {
       
        res.setHeader("Content-Encoding", "gzip");
        fs
          .createReadStream(filepath)
          .pipe(zlib.createGzip())
          .pipe(res);
      } else if (acceptEncoding.match(/\bdeflate\b/)) {
        res.setHeader("Content-Encoding", "deflate");
        fs
          .createReadStream(filepath)
          .pipe(zlib.createDeflate())
          .pipe(res);
      } else {
        fs.createReadStream(filepath).pipe(res);
      }
    } else {
      fs.createReadStream(filepath).pipe(res);
    }
  } catch (e) {
    res.statusCode = 404;
    res.end("Not Found");
  }
}

  • mime:通過文件的名稱乃秀、路徑拿到一個文件的內(nèi)容類型跺讯, 可以根據(jù)不同的文件內(nèi)容類型返回不同的Content-Type
  • acceptEncoding:全部寫成小寫是為了兼容不同的瀏覽器,node把所有的請求頭全轉(zhuǎn)成了小寫
  • filepath:得到文件的絕對路徑
  • 啟動服務(wù)后刀脏,訪問http://localhost:8080/msg.txt 可看到結(jié)果

深刻理解并實現(xiàn)緩存

為什么要緩存呢超凳,緩存有什么好處耀态?

  • 減少了冗余的數(shù)據(jù)傳輸首装,節(jié)省了網(wǎng)費擎析。
  • 減少了服務(wù)器的負擔(dān), 大大提高了網(wǎng)站的性能
  • 加快了客戶端加載網(wǎng)頁的速度

緩存的分類

強制緩存:

  • 強制緩存揍魂,在緩存數(shù)據(jù)未失效的情況下现斋,可以直接使用緩存數(shù)據(jù)
  • 在沒有緩存數(shù)據(jù)的時候偎蘸,瀏覽器向服務(wù)器請求數(shù)據(jù)時,服務(wù)器會將數(shù)據(jù)和緩存規(guī)則一并返回限书,緩存規(guī)則信息包含在響應(yīng)header中


    1521244966116.png

    1521245010921.png

對比緩存:

  • 瀏覽器第一次請求數(shù)據(jù)時倦西,服務(wù)器會將緩存標識與數(shù)據(jù)一起返回給客戶端,客戶端將二者備份至緩存數(shù)據(jù)庫中
  • 再次請求數(shù)據(jù)時扰柠,客戶端將備份的緩存標識發(fā)送給服務(wù)器疼约,服務(wù)器根據(jù)緩存標識進行判斷,判斷成功后劝枣,返回304狀態(tài)碼织鲸,通知客戶端比較成功,可以使用緩存數(shù)據(jù)


    1521245033835.png

    1521245044876.png

兩類緩存的區(qū)別和聯(lián)系

強制緩存如果生效琢唾,不需要再和服務(wù)器發(fā)生交互盾饮,而對比緩存不管是否生效懒熙,都需要與服務(wù)端發(fā)生交互

兩類緩存規(guī)則可以同時存在普办,強制緩存優(yōu)先級高于對比緩存,也就是說肢娘,當(dāng)執(zhí)行強制緩存的規(guī)則時舆驶,如果緩存生效,直接使用緩存沙廉,不再執(zhí)行對比緩存規(guī)則

實現(xiàn)對比緩存

實現(xiàn)對比緩存一般是按照以下步驟:

  • 第一次訪問服務(wù)器的時候,服務(wù)器返回資源和緩存的標識珊皿,客戶端則會把此資源緩存在本地的緩存數(shù)據(jù)庫中巨税。
  • 第二次客戶端需要此數(shù)據(jù)的時候,要取得緩存的標識驶兜,然后去問一下服務(wù)器我的資源是否是最新的。
  • 如果是最新的則直接使用緩存數(shù)據(jù)促王,如果不是最新的則服務(wù)器返回新的資源和緩存規(guī)則而晒,客戶端根據(jù)緩存規(guī)則緩存新的數(shù)據(jù)

實現(xiàn)對比緩存一般有兩種方式
通過最后修改時間來判斷緩存是否可用

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
// http://localhost:8080/index.html
http.createServer(function (req, res) {
    let { pathname } = url.parse(req.url, true);
    //D:\vipcode\201801\20.cache\index.html
    let filepath = path.join(__dirname, pathname);
    fs.stat(filepath, (err, stat) => {
        if (err) {
            return sendError(req, res);
        } else {
            let ifModifiedSince = req.headers['if-modified-since'];
            let LastModified = stat.ctime.toGMTString();
            if (ifModifiedSince == LastModified) {
                res.writeHead(304);
                res.end('');
            } else {
                return send(req, res, filepath, stat);
            }
        }
    });
}).listen(8080);
function sendError(req, res) {
    res.end('Not Found');
}
function send(req, res, filepath, stat) {
    res.setHeader('Content-Type', mime.getType(filepath));
    //發(fā)給客戶端之后,客戶端會把此時間保存起來迅耘,下次再獲取此資源的時候會把這個時間再發(fā)回服務(wù)器
    res.setHeader('Last-Modified', stat.ctime.toGMTString());
    fs.createReadStream(filepath).pipe(res);
}

這種方式有很多缺陷

  • 某些服務(wù)器不能精確得到文件的最后修改時間监署, 這樣就無法通過最后修改時間來判斷文件是否更新了
  • 某些文件的修改非常頻繁,在秒以下的時間內(nèi)進行修改.Last-Modified只能精確到秒栖秕。
  • 一些文件的最后修改時間改變了晓避,但是內(nèi)容并未改變只壳。 我們不希望客戶端認為這個文件修改了
  • 如果同樣的一個文件位于多個CDN服務(wù)器上的時候內(nèi)容雖然一樣暑塑,修改時間不一樣

ETag

ETag是根據(jù)實體內(nèi)容生成的一段hash字符串,可以標識資源的狀態(tài)
資源發(fā)生改變時,ETag也隨之發(fā)生變化惕艳。 ETag是Web服務(wù)端產(chǎn)生的,然后發(fā)給瀏覽器客戶端

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('crypto');

http.createServer(function (req, res) {
    let { pathname } = url.parse(req.url, true);
    
    let filepath = path.join(__dirname, pathname);
    fs.stat(filepath, (err, stat) => {
        if (err) {
            return sendError(req, res);
        } else {
            let ifNoneMatch = req.headers['if-none-match'];
            let out = fs.createReadStream(filepath);
            let md5 = crypto.createHash('md5');

            out.on('data', function (data) {
                md5.update(data);
            });
            out.on('end', function () {
           
                let etag = md5.digest('hex');
                let etag = `${stat.size}`;
                if (ifNoneMatch == etag) {
                    res.writeHead(304);
                    res.end('');
                } else {
                    return send(req, res, filepath, etag);
                }
            });

        }
    });
}).listen(8080);
function sendError(req, res) {
    res.end('Not Found');
}
function send(req, res, filepath, etag) {
    res.setHeader('Content-Type', mime.getType(filepath));
   
    res.setHeader('ETag', etag);
    fs.createReadStream(filepath).pipe(res);

}
  • 客戶端想判斷緩存是否可用可以先獲取緩存中文檔的ETag远搪,然后通過If-None-Match發(fā)送請求給Web服務(wù)器詢問此緩存是否可用终娃。
  • 服務(wù)器收到請求蒸甜,將服務(wù)器的中此文件的ETag,跟請求頭中的If-None-Match相比較,如果值是一樣的,說明緩存還是最新的,Web服務(wù)器將發(fā)送304 Not Modified響應(yīng)碼給客戶端表示緩存未修改過余佛,可以使用。
  • 如果不一樣則Web服務(wù)器將發(fā)送該文檔的最新版本給瀏覽器客戶端

實現(xiàn)強制緩存

  • 把資源緩存在客戶端恨憎,如果客戶端再次需要此資源的時候郊楣,先獲取到緩存中的數(shù)據(jù),看是否過期净蚤,如果過期了。再請求服務(wù)器
  • 如果沒過期程梦,則根本不需要向服務(wù)器確認,直接使用本地緩存即可
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('crypto');
http.createServer(function (req, res) {
    let { pathname } = url.parse(req.url, true);
    let filepath = path.join(__dirname, pathname);
    console.log(filepath);
    fs.stat(filepath, (err, stat) => {
        if (err) {
            return sendError(req, res);
        } else {
            send(req, res, filepath);
        }
    });
}).listen(8080);
function sendError(req, res) {
    res.end('Not Found');
}
function send(req, res, filepath) {
    res.setHeader('Content-Type', mime.getType(filepath));
    res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
    res.setHeader('Cache-Control', 'max-age=30');
    fs.createReadStream(filepath).pipe(res);
}
  • 瀏覽器會將文件緩存到Cache目錄屿附,第二次請求時瀏覽器會先檢查Cache目錄下是否含有該文件挺份,如果有,并且還沒到Expires設(shè)置的時間匀泊,即文件還沒有過期,那么此時瀏覽器將直接從Cache目錄中讀取文件探赫,而不再發(fā)送請求
  • Expires是服務(wù)器響應(yīng)消息頭字段,在響應(yīng)http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數(shù)據(jù)
  • Cache-ControlExpires的作用一致妆兑,都是指明當(dāng)前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數(shù)據(jù)還是重新發(fā)請求到服務(wù)器取數(shù)據(jù),如果同時設(shè)置的話搁嗓,其優(yōu)先級高于Expires

下面開始寫靜態(tài)服務(wù)器
首先創(chuàng)建一個http服務(wù)箱靴,配置監(jiān)聽端口

 let http = require('http');
 let server = http.createServer();
        server.on('request', this.request.bind(this));
        server.listen(this.config.port, () => {
            let url = `http://${this.config.host}:${this.config.port}`;
            debug(`server started at ${chalk.green(url)}`);
        });

下面寫個靜態(tài)文件服務(wù)器
先取到客戶端想說的文件或文件夾路徑,如果是目錄的話棍矛,應(yīng)該顯示目錄下面的文件列表

 async request(req, res) {
        let { pathname } = url.parse(req.url);
        if (pathname == '/favicon.ico') {
            return this.sendError('not found', req, res);
        }
        let filepath = path.join(this.config.root, pathname);
        try {
            let statObj = await stat(filepath);
            if (statObj.isDirectory()) {
                let files = await readdir(filepath);
                files = files.map(file => ({
                    name: file,
                    url: path.join(pathname, file)
                }));
                let html = this.list({
                    title: pathname,
                    files
                });
                res.setHeader('Content-Type', 'text/html');
                res.end(html);
            } else {
                this.sendFile(req, res, filepath, statObj);
            }
        } catch (e) {
            debug(inspect(e));
            this.sendError(e, req, res);
        }
    }
    
    sendFile(req, res, filepath, statObj) {
        if (this.handleCache(req, res, filepath, statObj)) return;
        res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
        let encoding = this.getEncoding(req, res);
        let rs = this.getStream(req, res, filepath, statObj);

        if (encoding) {
            rs.pipe(encoding).pipe(res);
        } else {
            rs.pipe(res);
        }
    }

支持斷點續(xù)傳

 getStream(req, res, filepath, statObj) {
        let start = 0;
        let end = statObj.size - 1;
        let range = req.headers['range'];
        if (range) {
            res.setHeader('Accept-Range', 'bytes');
            res.statusCode = 206;
            let result = range.match(/bytes=(\d*)-(\d*)/);
            if (result) {
                start = isNaN(result[1]) ? start : parseInt(result[1]);
                end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
            }
        }
        return fs.createReadStream(filepath, {
            start, end
        });
    }

支持對比緩存够委,通過etag的方式

handleCache(req, res, filepath, statObj) {
        let ifModifiedSince = req.headers['if-modified-since'];
        let isNoneMatch = req.headers['is-none-match'];
        res.setHeader('Cache-Control', 'private,max-age=30');
        res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString());
        let etag = statObj.size;
        let lastModified = statObj.ctime.toGMTString();
        res.setHeader('ETag', etag);
        res.setHeader('Last-Modified', lastModified);
        if (isNoneMatch && isNoneMatch != etag) {
            return fasle;
        }
        if (ifModifiedSince && ifModifiedSince != lastModified) {
            return fasle;
        }
        if (isNoneMatch || ifModifiedSince) {
            res.writeHead(304);
            res.end();
            return true;
        } else {
            return false;
        }
    }

支持文件壓縮

    getEncoding(req, res) {
        let acceptEncoding = req.headers['accept-encoding'];
        if (/\bgzip\b/.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'gzip');
            return zlib.createGzip();
        } else if (/\bdeflate\b/.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'deflate');
            return zlib.createDeflate();
        } else {
            return null;
        }
    }

編譯模板怖现,得到一個渲染的方法,然后傳入實際數(shù)據(jù)數(shù)據(jù)就可以得到渲染后的HTML了

編譯模板,得到一個渲染的方法,然后傳入實際數(shù)據(jù)數(shù)據(jù)就可以得到渲染后的HTML了
function list() {
    let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
    return handlebars.compile(tmpl);
}

這樣一個簡單的靜態(tài)服務(wù)器就完成了潘拨,其中包含了靜態(tài)文件服務(wù)饶号,實現(xiàn)緩存,實現(xiàn)斷點續(xù)傳讨韭,分塊獲取,實現(xiàn)壓縮的功能

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狰闪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子埋泵,更是在濱河造成了極大的恐慌,老刑警劉巖礁蔗,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雁社,死亡現(xiàn)場離奇詭異,居然都是意外死亡霉撵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門撕氧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伦泥,你說我怎么就攤上這事锦溪。” “怎么了刻诊?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我赘被,道長,這世上最難降的妖魔是什么民假? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮事秀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘易迹。我一直安慰自己平道,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袋哼,像睡著了一般闸衫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蔚出,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音衅胀,去河邊找鬼。 笑死滚躯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掸掏。 我是一名探鬼主播宙帝,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼步脓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起靴患,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎农渊,沒想到半個月后或颊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡囱挑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了徽鼎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡悄但,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出檐嚣,到底是詐尸還是另有隱情,我是刑警寧澤嚎京,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布隐解,位于F島的核電站谊迄,受9級特大地震影響狸膏,放射性物質(zhì)發(fā)生泄漏下面。R本人自食惡果不足惜蚓曼,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一钦扭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧其弊,春花似錦、人聲如沸瑞凑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽练慕。三九已至,卻和暖如春铃将,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劲阎。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留龄毡,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓沦零,卻偏偏與公主長得像,于是被迫代替她去往敵國和親路操。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理搞坝,服務(wù)發(fā)現(xiàn)魁袜,斷路器,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • API定義規(guī)范 本規(guī)范設(shè)計基于如下使用場景: 請求頻率不是非常高:如果產(chǎn)品的使用周期內(nèi)請求頻率非常高慌核,建議使用雙通...
    有涯逐無涯閱讀 2,519評論 0 6
  • 一、概念(載錄于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434閱讀 8,328評論 6 152
  • http協(xié)議有http0.9垫桂,http1.0,http1.1和http2三個版本诬滩,但是現(xiàn)在瀏覽器使用的是htt...
    一現(xiàn)_閱讀 1,855評論 0 3
  • 陌生的環(huán)境和陌生的人往往給人不安全感覺灭将,特別還要面對陌生人說話,讓對方了解我們就更有點恐懼庙曙,雖然我經(jīng)歷過...
    盛世贏家葉小華閱讀 237評論 0 0