Node.js 學習(三): 構建 Web 應用(服務器端)

1. 構建 Web 應用(服務器端)

1.1. 基礎功能

對象 http.Server 的 'request' 事件發(fā)生于網(wǎng)絡連接建立,客戶端向服務器端發(fā)送報文,服務器段解析報文,發(fā)現(xiàn) HTTP 請求報文的報文頭時。在已出發(fā) 'request' 事件前,http 模塊已準備好 IncomingMessage 和 ServerResponse 對象以對應請求和響應報文的操作。

const http = require('http');
http.createServer( function(req, res) {    // 'request' 事件的偵聽器
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end();
}).listen(1337, '127.0.0.1');

對于一個 Web 應用除了上面的業(yè)務還有如下的需求:

  • 請求方法的判斷
  • URL 的路徑解析
  • URL 中的查詢字符串的解析
  • Cookie 和 Session 的解析
  • Basic 認證
  • 表單數(shù)據(jù)的解析
  • 任意格式文件的上傳處理

Web 應用可以看成是將上述需求進行線性組合,最終生成 'request' 事件的偵聽器,通過高階函數(shù)將它傳遞給 http.createServer() 方法。

const app = express();
// TODO
http.createServer(app).listen(1337);

1.1.1. HTTP Parser

Node 底層使用 HTTP_Parser 這個 C 語言模塊來解析 HTTP 協(xié)議數(shù)據(jù), 它解析的主要信息有:

  • 頭部字段和對應值(Header)
  • Content-Length
  • 請求方法(Method)
  • 響應狀態(tài)碼(Status Code)
  • 傳輸編碼
  • HTTP 版本
  • 請求 URL
  • 報文主體

1.1.2. 請求方法

HTTP_Parser 在解析請求報文時,將報文頭抽取出來并將請求方式抽象為 req.method 屬性。

1.1.3. 路徑解析

url 模塊提供了 URL 的解析。URL 是由多個具有意義的字段組成的字符串,具體描述如下:

HTTP_Parser 將請求報文頭的路徑字段解析成名為 req.url 的 URL 字符串, 它可通過 url.parse() 方法解析成 URL 對象,對象中的 urlObject.pathname 屬性反映了 URL 字符串的 path 字段中的 pathname 部分。

1.1.4. 查詢字符串

pathname 部分后就是查詢字符串,這部分內容經(jīng)常需要為業(yè)務邏輯所用, Node 提供了 qureystring 模塊來處理這部分數(shù)據(jù)。注意,業(yè)務的判斷一定要檢查值是數(shù)組還是字符串。

1.1.5. Cookie

HTTP 是一個無狀態(tài)協(xié)議,無法區(qū)分用戶之間的身份。如何標識和認證一個用戶,最早的方案就是 Cookie 。

Cookie 的處理分為如下幾步:

  • 服務器向客戶端發(fā)送 Cookie
  • 瀏覽器將 Cookie 保存
  • 之后每次瀏覽器都會將 Cookie 發(fā)送給服務器端

1.1.5.1. 服務器端解析 Cookie

HTTP_Parser 會將請求報文頭的所有字段解析到 req.headers 上,Cookie 就是 req.headers.cookie 。 Cookie 值的格式是鍵值對,Express 的中間件 cookie-parser 將其掛載在 req 對象上,讓業(yè)務代碼可以直接訪問。

function cookieParser (options) {
  return function cookieParser (req, res, next) {
    if (req.cookies) {
      return next()
    }
    var cookies = req.headers.cookie
    req.cookies = Object.create(null)
    // no cookies
    if (!cookies) {
      return next()
    }
    req.cookies = cookie.parse(cookies, options) // 這里調用了 cookie 模塊 (https://github.com/jshttp/cookie)
    next()
  }
}

1.1.5.2. 客戶端初始 Cookie

客戶端的 Cookie 最初來自服務器端,服務器端告知客戶端的方式是通過響應報文實現(xiàn)的,響應的 Cookie 值在 Set-Cookie 字段中設置。具體格式如下所示:

Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
  • path 標識這個 Cookie 影響到的路徑。
  • ExpiresMax-Age 告知瀏覽器這個 Cookie 何時過期。
  • Secure 該屬性為 true 時,表示 Cookie 只能通過 HTTPS 協(xié)議傳遞。

Express 中間件 express-session 處理 Set-Cookie :

function setcookie(res, name, val, secret, options) {
  var signed = 's:' + signature.sign(val, secret);
  var data = cookie.serialize(name, signed, options);
  var prev = res.getHeader('set-cookie') || [];
  var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
  res.setHeader('set-cookie', header)
}

1.1.6. Session

Cookie 的缺點是無法保護敏感數(shù)據(jù),因此 Session 應運而生。 Session 的數(shù)據(jù)只保留在服務器端,客戶端是無法更改的。服務器端是如何將每個用戶和 Session 數(shù)據(jù)對應起來的?通常是基于 Cookie 來實現(xiàn)映射關系的,具體步驟如下:

  • 服務器端生成 Session 和 sessionID(口令)
  • 將 Session 數(shù)據(jù) 和 sessionID 映射存儲在 Store 中(redis、mongodb、memory)
  • 通過 Set-Cookie 將 sessionID 作為 Cookie 的鍵值對發(fā)送給客戶端。
  • 對于客戶端的請求,服務器端每次檢查 Cookie 中的 sessionID,并對應保留在服務器端 Store 的 Session 數(shù)據(jù)。
  • [ 服務器端更新存儲 Session 數(shù)據(jù) ]

Express 的中間件 express-session 將 Session 數(shù)據(jù)掛載在 req.session,方便業(yè)務邏輯使用。同時 express-session 還提供了多種 Store 。

1.1.7. Basic 認證

Basic 認證是一個通過用戶名和密碼實現(xiàn)的身份認證方式。如果用戶首次訪問網(wǎng)頁, URL 地址中沒有攜帶認證內容,那么瀏覽器會到得一個 401 未授權的響應。

var http = require('http')
var auth = require('basic-auth')

// Create server
var server = http.createServer(function (req, res) {
  var credentials = auth(req)

  if (!credentials || credentials.name !== 'john' || credentials.pass !== 'secret') {
    res.statusCode = 401
    res.setHeader('WWW-Authenticate', 'Basic realm="example"')
    res.end('Access denied')
  } else {
    res.end('Access granted')
  }
})

// Listen
server.listen(3000)

響應頭中的 WWW-Authenticate 字段告知瀏覽器采用什么樣的認證和加密方式。

瀏覽器在后續(xù)請求中都攜帶上 Authorization 信息,服務器會檢查請求報文頭中的 Authorization 字段的內容,該字段有認證方式和加密值構成。

function auth (req) {
  // get header
  var header = req.headers.authorization
  // parse header
  var match = CREDENTIALS_REGEXP.exec(string) // CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/
  if (!match) {
    return undefined
  }
  // decode user pass
  var userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1]))  // USER_PASS_REGEXP = /^([^:]*):(.*)$/
  if (!userPass) {
    return undefined
  }
  // return credentials object
  return new Credentials(userPass[1], userPass[2])
}

function decodeBase64 (str) {
  return new Buffer(str, 'base64').toString()
}

function Credentials (name, pass) {
  this.name = name
  this.pass = pass
}

1.2. 數(shù)據(jù)上傳

報文頭部中的內容已經(jīng)能夠讓服務器端進行大多數(shù)業(yè)務邏輯操作了,但是單純的報文頭部無法攜帶大量的數(shù)據(jù),請求報文中還有攜帶內容的報文體,這部分需要用戶自行接收和解析。通過報文頭部的 Transfer-EncodingContent-Length 字段即可判斷請求中是否帶有報文體。

function hasbody (req) {
  return req.headers['transfer-encoding'] !== undefined ||
    !isNaN(req.headers['content-length'])
}

HTTP_Parser 模塊通過觸發(fā) 'data' 事件獲取 req.rawBody ,然后針對不同類型的報文體進行相應的解析。 Express 中間件 body-parser 針對 JSON 的解析如下:

  function parse (body) {
    if (body.length === 0) {
      // special-case empty json body, as it's a common client-side mistake
      // TODO: maybe make this configurable or part of "strict" option
      return {}
    }
    if (strict) {
      var first = firstchar(body)   // FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*(.)/ // eslint-disable-line no-control-regex
      if (first !== '{' && first !== '[') {
        throw new SyntaxError('Unexpected token ' + first)
      }
    }
    return JSON.parse(body)
  }

1.2.1. 表單數(shù)據(jù)

在表單提交的請求頭中 Content-Type 字段值為 application/x-www-form-urlencoded ,也就是其內容通過 urlencoded 的方式編碼內容形成報文體,node-formidable 模塊解析表單提交大概如下:

// 判斷報文頭
if (this.headers['content-type'].match(/urlencoded/i)) {
  this._initUrlencoded();
  return;
}
// 事件發(fā)布
IncomingForm.prototype._initUrlencoded = function() {
  this.type = 'urlencoded';
  var parser = new QuerystringParser(this.maxFields);
  parser.onField = function(key, val) {
    self.emit('field', key, val);
  };
};
// 事件訂閱
IncomingForm.prototype.parse = function(req, cb) {
  if (cb) {
    this
      .on('field', function(name, value) {
        fields[name] = value;
      })
  }
}

1.2.2. 附件上傳

一種特殊的表單需要提交文件,該表單中可以含有 file 類型的控件,以及需要指定表單屬性 enctypemultipart/form-data 。因為表單中含有多種控件,所有使用名為 boundary 的分隔符進行分割。

模塊 node-formidable 將解析上傳文件和處理普通表單數(shù)據(jù)進行了統(tǒng)一化處理,以下是文件上傳的實例:

var formidable = require('formidable'),
    http = require('http'),
    util = require('util');

http.createServer(function(req, res) {
  if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // parse a file upload
    var form = new formidable.IncomingForm();

    form.parse(req, function(err, fields, files) {
      res.writeHead(200, {'content-type': 'text/plain'});
      res.write('received upload:\n\n');
      res.end(util.inspect({fields: fields, files: files}));
    });
    return;
  }

  // show a file upload form
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(
    '<form action="/upload" enctype="multipart/form-data" method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
  );
}).listen(8080);

Express 的中間件 Multer 也提供了類似的功能,但是它只能處理特殊的表單也就是表單屬性含有 multipart/form-data

var express = require('express')
var multer  = require('multer')
var upload = multer({ dest: 'uploads/' })

var app = express()

app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file is the `avatar` file
  // req.body will hold the text fields, if there were any
})

app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
  // req.files is array of `photos` files
  // req.body will contain the text fields, if there were any
})

var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
  //
  // e.g.
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // req.body will contain the text fields, if there were any
})

1.2.3. 跨站請求偽造 ( CSRF )

通常解決 CSRF 的方法是在表單中添加隨機值。 首先服務器端生成一個隨機值,然后將隨機值內嵌到前端表單,前端表單的請求中攜帶該隨機值,服務器端收到并解析后對比判定是否一致。

Express 中間件 csurf 默認情況下會自動生成隨機值,并且會將該隨機值掛載到 req.session.csrfSecret 上。

1.3. 路由解析

1.3.1. 文件路徑型

這種路由的處理方式,就是將請求路徑中的文件發(fā)送給客戶端即可,而請求 URL 中的文件路徑與文件所在的具體路徑相對應。

1.3.2. MVC

MVC 模型將業(yè)務邏輯按職責分離:

  • 模型 (Model) 數(shù)據(jù)相關的操作和封裝
  • 控制器(Controller) 行為的集合
  • 視圖 (View) 頁面的渲染

它的工作模式:

  • 路由解析,根據(jù) URL 查找到對應的控制器及其所定義的行為
  • 行為調用相關的模型,進行數(shù)據(jù)操作
  • 將操作后的數(shù)據(jù)結合相應的視圖進行頁面渲染,并將頁面返回給客戶端

在 MVC 模型中,路由也是非常重要的概念,它主要實現(xiàn)了 URL 和控制器的映射,具體實現(xiàn)的方式有:

  • 手工映射
    • 靜態(tài)映射
    • 正則匹配
    • 參數(shù)解析
  • 自然映射

1.3.3. RESTful

REST 的中文含義為表現(xiàn)層狀態(tài)轉化, 符合 REST 規(guī)范的設計成為 RESTful 設計。 它的設計哲學是將服務器端提供的內容實體看作為一個資源,并表現(xiàn)在 URL 上。其中 URL 中的 Method 代表了對這個資源的操作方法。

POST /user/jacksontian  // 創(chuàng)建新用戶
DELETE /user/jacksontian  // 刪除用戶
PUT /user/jacksontian    // 更改用戶
GET /user/jacksontian   // 查詢用戶

在 RESTful 設計中,客戶端能夠接受資源的具體格式由請求報文頭中的 Accept 字段給出:

Accept: application/json,application/xml

而服務器端在響應報文中,通過 Content-Type 字段告知客戶端是什么格式:

Content-Type: application/json 

所以 RESTful 的設計就是, 通過 URL 設計資源、請求方法定義資源的操作和通過 Accept 決定資源的具體格式。

1.4. 中間件

上述工作有太多的繁瑣細節(jié)要完成,為了簡化和隔離這些基礎功能,讓開發(fā)者關注業(yè)務邏輯的實現(xiàn),引入了中間件這個定義。中間件組件是一個函數(shù),它攔截 HTTP 服務器提供的請求和響應對象,執(zhí)行邏輯,然后或者結束響應,或者傳遞給下一個中間件。

Node 的 http 模塊提供了應用層協(xié)議的封裝,但是對具體業(yè)務沒有支持(小而靈活),因此必須有開發(fā)框架對業(yè)務提供支持。 通過中間件的形式搭建開發(fā)框架,完成各種基礎功能,最終匯成強大的基礎框架。每一種基礎框架對中間件的組織形式不盡相同,下圖是基礎框架 Express 的實現(xiàn)機制。

Middleware
Middleware

1.4.1. 普通中間件

在 Express 中,中間件按慣例會接受三個參數(shù):一個請求對象,一個響應對象,還有一個通常命名為 next 的參數(shù),它是一個回調函數(shù),表明該組件已經(jīng)完成了工作,可以執(zhí)行下一個中間件組件了。中間件的分派主要依賴于 next 這個回調函數(shù)的尾觸發(fā),這樣前一個中間件組件完成后才能進入下一個中間件組件。

在 Web 應用中,路由是個至關重要的概念,它會把請求 URL 映射到實現(xiàn)業(yè)務邏輯的函數(shù)上。通過中間件和業(yè)務邏輯的結合可以完成對路由的執(zhí)行。首先使用 app.use() 等方法將所有的中間件和業(yè)務邏輯以及相應的掛載點有序的放入路由數(shù)組,然后通過請求路徑與掛載點的對比,將匹配的數(shù)據(jù)元素重組為新的數(shù)組,最后通過分發(fā)執(zhí)行中間件,中間件執(zhí)行完畢后通過 next() 函數(shù)將結果轉入到下一個匹配的數(shù)組元素。

var handle = function (req, res, stack) {
  var next = function () {
    // 從stack數(shù)組中取出中間件并執(zhí)行
    var middleware = stack.shift();
    if (middleware) {
      // 傳入next()函數(shù)自身,使中間件能夠執(zhí)行結束后遞歸
      middleware(req, res, next);
    }
  };

  // 啟動執(zhí)行
  next();
};
dispatch

1.4.2. 異常處理

為了捕獲中間件拋出的同步異常,保證 Web 應用的穩(wěn)定和健壯,我們?yōu)?next() 方法添加 err 參數(shù)。這主要是因為異步的異常不能直接捕獲,中間件的異常需要自己傳遞出來。

使用中間件的思路將異常的處理交給中間件,同時為了區(qū)分異常處理中間件和普通中間件的區(qū)別,在其參數(shù)中加入 err 參數(shù):

const middleware = function(options) {
  return function(err, req, res, next) {
    // TODO
    next();
  };
};

每個異常處理中間件可通過 next(err) 方法將異常傳遞給下一個異常處理中間件,其思路與普通中間件完全一致。

如果異常處理中間件沒有設置 next(err) 方法,那它后面的異常處理中間件都不會起作用。

1.5. 頁面渲染

執(zhí)行完中間件及業(yè)務邏輯后,服務器端該如何響應客戶端?一般有兩種方式:

  • 內容響應
  • 視圖渲染

1.5.1. 內容響應

因為服務器端響應的報文,最終都會被客戶端處理,具體終端有可能是命令行,也有可能是瀏覽器。這就使得響應報文頭中的 content-* 字段顯得十分重要。

1.5.1.1. MIME

報文頭中的 Content-Type 字段的值決定采用不同的渲染方式,而這個值就是 MIME值。不同的文件類型具有不同的 MIME 值:

  • JSON 文件: application/json
  • XML 文件: application/xml
  • PDF 文件: application/pdf

1.5.1.2. 附件下載

報文頭中的 Content-Disposition 字段影響的行為是客戶端會根據(jù)它的值判斷是應該將報文數(shù)據(jù)當做及時瀏覽的內容(inline),還是可以下載的附件(attachment)。

1.5.1.3. 響應 JSON

為了快捷的響應 JSON 數(shù)據(jù), Express 封裝了響應對象的 res.json() 方法:

res.json = function json(obj) {
  var val = obj;
  var body = JSON.stringify(val);

  // content-type
  if (!this.get('Content-Type')) {
    this.set('Content-Type', 'application/json');
  }
  return this.send(body);
};

1.5.1.4. 響應跳轉

當前 URL 因為某些原因不能處理, 需要將用戶跳轉到別的 URL 時,Express 同樣封裝了一個快捷方式 res.redirect():

res.redirect = function redirect(url) {
  var address = url;
  var body;
  var status = 302;

  // Set location header
  address = this.location(address).get('Location');

  // Support text/{plain,html} by default
  this.format({
    text: function(){
      body = statuses[status] + '. Redirecting to ' + address
    },

    html: function(){
      var u = escapeHtml(address);
      body = '<p>' + statuses[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
    },

    default: function(){
      body = '';
    }
  });

  // Respond
  this.statusCode = status;
  this.set('Content-Length', Buffer.byteLength(body));

  if (this.req.method === 'HEAD') {
    this.end();
  } else {
    this.end(body);
  }
};
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評論 1 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,796評論 18 139
  • 1. 網(wǎng)絡基礎TCP/IP HTTP基于TCP/IP協(xié)議族,HTTP屬于它內部的一個子集。 把互聯(lián)網(wǎng)相關聯(lián)的協(xié)議集...
    yozosann閱讀 3,455評論 0 20
  • 本篇文章篇幅比較長,先來個思維導圖預覽一下。 一、概述 1.計算機網(wǎng)絡體系結構分層 2.TCP/IP 通信傳輸流 ...
    滌生_Woo閱讀 55,134評論 24 557
  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,327評論 11 349
  • 基礎功能 之前我們通過http模塊創(chuàng)建了一個簡單的服務器,但是對于一個網(wǎng)絡應用來說肯定是遠遠不夠的,在聚義的業(yè)務中...
    exialym閱讀 880評論 1 22