HTTP HTTP 构建在 TCP 基础之上,以前用 C#的 Socket 实现过简单地只能响应首页的 Server,这次用 net.createServer 来实现,但是 writeHeader 一直不起作用,暂时无果,再探索探索
更新: 使用 TCP socket 实现最最最基本的 web server,不起作用是我把 header 写错了,header 是
1 2 3 4 HTTP/1.1 200 OK\r\nContent-Type : text/htmlxxx // <- 响应内容
注意键值中间是冒号分开,不是等号,键+冒号+空格+值
,header 写完加一个空行,不懂什么 http 规范搞的焦头烂额…
响应内容是 request 的 path
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 var net = require ("net" );var fs = require ("fs" );var util = require ("util" );var server = net.createServer (function (socket ) { var name = util.format ("%s:%s" , socket.remoteAddress , socket.remotePort ); var buf = new Buffer (0 ); socket.on ("data" , function (data ) { buf = Buffer .concat ([buf, data]); if (!has_header ()) return ; var method = buf.toString ().split (" " )[0 ]; var url = buf.toString ().split (" " )[1 ]; console .log ("%s %s %s..." , name, method, url); socket.removeAllListeners ("data" ); socket.write ("\ HTTP/1.1 200 OK\r\n\ Content-Type: text/plain\r\n\ charset: utf-8\r\n\ \r\n\ " ); socket.end (url); function has_header ( ) { var end = "\r\n\r\n" ; var return_val = false ; [].forEach .call (buf, function (cur, i, arr ) { if (cur === 0xd && buf.binarySlice (i, i + 4 ) === end) { return (return_val = true ); has_header.index = i; } }); return return_val; } }); socket.on ("end" , function ( ) { console .log ("%s end..." , name); socket.removeAllListeners ("data" ); }); }); server.listen (5000 , function ( ) { console .log (server.address ()); });
简单地 server,用 http.createServer 就简单多了,端口 1337,是 nodejs 官网首页上的例子
1 2 3 4 5 6 var http = require ("http" );var server = http.createServer (function (request, response ) { response.write ("Hello World!" ); response.end (); }); server.listen (1337 );
浏览器访问得到 Hello World
server 是 http.Server 实例,传进去的回调是 server 的 request 事件的 listener
创建好 server 之后使用 listen 方法,就与 net.createServer.listen 一样了,参数里面的回调是 listening 事件的 handler,表示成功绑定端口,正在监听
http.IncomingMessage req request 是 http.IncomingMessage 实例 Doc : http.IncomingMessage api
http.request()的 response 也是 IncomingMessage 类型,这个类型的某些属性只在一种情况有效
请求报文 1 2 GET / HTTP/1.1 Host : localhost:8000
第一行 : 请求方法(GET) 请求路径(/) 请求协议(HTTP)/HTTP 版本(1.1) 下面就是请求 headers,其中 Host 是 HTTP 1.1 强制的,咋能没有了~
request.method HTTP 1.1 请求方法一览,HTTP 1.0 只支持 GET/POST/HEAD
包含请求的 headers,请求可以包含数据,如 post data 上去
request.url 包含所有的东西,像 http/https 啊,host,path,querystring,hash 就是#xxx 那些 可以使用 require(‘url’).parse(url),关心哪个部分直接取就行,pathname 就是 request 的 path,我做静态服务器有用到…
request.socket 它包含的 socket,是net.Socket
实例
request.on data 对于客户端 POST 得到的数据
1 2 3 4 5 6 7 8 var buf = new Buffer (0 );req.on ("data" , function (data ) { Buffer .concat ([buf, data]); }); req.on ("end" , function ( ) { var s = buf.toString (); });
http.ServerResponse res res 是 http.ServerResponse Doc : http.ServerResponse api
响应报文 1 2 3 4 HTTP/1.1 200 OKDate : Sun, 21 Jul 2013 22:14:26 GMTConnection : keep-aliveTransfer-Encoding : chunked
第一行 : 回应 HTTP 协议&版本 状态码 StatusCode(200) 原因说明 reason-phrase(OK) 后面是 response-headers
res.statusCode 可读可写 表示响应状态码
http.STATUS_CODE 属性表示了状态码与对于的原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 > http.STATUS_CODES { '100' : 'Continue' , '101' : 'Switching Protocols' , '102' : 'Processing' , '200' : 'OK' , '201' : 'Created' , '202' : 'Accepted' , '203' : 'Non-Authoritative Information' , '204' : 'No Content' , '205' : 'Reset Content' , '206' : 'Partial Content' , '207' : 'Multi-Status' , '300' : 'Multiple Choices' , '301' : 'Moved Permanently' , '302' : 'Moved Temporarily' , '303' : 'See Other' , '304' : 'Not Modified' , '305' : 'Use Proxy' , '307' : 'Temporary Redirect' , '400' : 'Bad Request' , '401' : 'Unauthorized' , '402' : 'Payment Required' , '403' : 'Forbidden' , '404' : 'Not Found' , '405' : 'Method Not Allowed' , '406' : 'Not Acceptable' , '407' : 'Proxy Authentication Required' , '408' : 'Request Time-out' , '409' : 'Conflict' , '410' : 'Gone' , '411' : 'Length Required' , '412' : 'Precondition Failed' , '413' : 'Request Entity Too Large' , '414' : 'Request-URI Too Large' , '415' : 'Unsupported Media Type' , '416' : 'Requested Range Not Satisfiable' , '417' : 'Expectation Failed' , '418' : 'I\' m a teapot', ' 422 ': ' Unprocessable Entity', ' 423 ': ' Locked', ' 424 ': ' Failed Dependency', ' 425 ': ' Unordered Collection', ' 426 ': ' Upgrade Required', ' 428 ': ' Precondition Required', ' 429 ': ' Too Many Requests', ' 431 ': ' Request Header Fields Too Large', ' 500 ': ' Internal Server Error', ' 501 ': ' Not Implemented', ' 502 ': ' Bad Gateway',
输出 headerres.writeHead(statusCode, [reasonPhrase], [headers])
输出/修改 headergetHeader(name)
/setHeader(name,value)
/removeHeader(name)
使用 headerSent 来检查 header 有没有被发送出去
Cookie 数据,通过 Set-Cookie 头来发送 Cookie 到客户端
1 2 3 4 5 response.setHeader ("Set-Cookie" , [ "name=Colin; Expires=Sat, 10 Jan 2015 20:00:00 GMT;Domain=foo.com; HttpOnly; Secure" , "foo=bar; Max-Age=3600" ]);
响应
res.write(data,[encoding]);
res.end([data],[encoding]);
必须显示调用 res.end() 表示服务器对本次响应的输出结束了
data 可以是 string 或 Buffer 数据,encoding 默认是 utf8
中间件 middleware 实现类似 connect 那种形式,将 middleware 换成 connect 就一样了 不到 30 行的代码,可以实现,也就是拦截一下,没有错误处理的部分,太复杂不搞了…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var http = require ("http" );function middleware ( ) { var cur; var middlewares = []; var req, res; function real_handler (_req, _res ) { cur = -1 ; req = _req; res = _res; next (); } real_handler.use = function (middle ) { middlewares.push (middle); }; function next ( ) { cur++; if (cur === middlewares.length ) return res.end (); middlewares[cur](req, res, next); } return real_handler; } var app = middleware ();app.use (function (req, res, next ) { res.end (req.url ); }); http.createServer (app).listen (1234 ); console .log ("端口 1234 !" );
像 query 实现的是:将 url 中的 querystring 解析后绑定至res.query
字段中
发起请求 http.request 作为 http 客户端发起请求
1 http.request (options, callback);
option 字段表示请求的详细内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 var http = require ("http" );var request = http.request ( { hostname : "localhost" , port : 8000 , path : "/" , method : "GET" , headers : { Host : "localhost:8000" } }, function (response ) { var statusCode = response.statusCode ; var headers = response.headers ; var statusLine = "HTTP/" + response.httpVersion + " " + statusCode + " " + http.STATUS_CODES [statusCode]; console .log (statusLine); for (header in headers) { console .log (header + ": " + headers[header]); } console .log (); response.setEncoding ("utf8" ); response.on ("data" , function (data ) { process.stdout .write (data); }); response.on ("end" , function ( ) { console .log ("end ..." ); }); } ); request.end ();
正如前文所说,这个 response 是 http.IncomingMessage 实例
request 必须显式调用 end()表示数据发送结束,例如 post 数据
option 可以用一个 string 代替,不过不能表示 request 的方法,默认为 get,就和 http.get 一样了
使用 POST 数据
准备 post 数据,var body = "name=zhangsan&age=18";
request 并设置 headers
1 2 "Content-Type" : "application/x-www-form-urlencoded" ,"Content-Length" : Buffer . byteLength(body )
request.end(body);
request 第三方模块 简化 http.request 操作…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var request = require ("request" );request ( { uri : "http://localhost:8000/" , method : "POST" , headers : { Host : "localhost:8000" }, form : { foo : "bar" , baz : [1 , 2 ] } }, function (error, response, body ) { console .log (body); } );
将要 POST 的数据,直接写在 form 字段里,不需要手动 write/end,支持的字段如下:
request 对 Cookie 的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 var request = require ("request" );var jar = request.jar ();var cookie = request.cookie ("count=1" );jar.add (cookie); request ( { url : "http://localhost:8000/" , jar : jar }, function (error, response, body ) { console .log (jar); } );
创建了一个”罐子”jar 来装 cookie,server setcookie 也会帮我们更新
SServer 实现简单静态服务器 了解 http 之后,可以实现一个小的 server 玩玩,使用fs.createReadStream.pipe(res)
做法,无缓存,静态服务器,实测比 Express 使用 static 还是快点的,毕竟没那么多逻辑,用来浏览 hexo 生成的静态页,也很快…
支持
html/htm
js/json/xml
css
jpg/jpeg/png/bmp/gif 图片
其他全视为 text/plain
没有使用什么 mime type 库,小工具不应该有什么依赖的才对嘛 *^_^*
使用
1 2 3 4 5 6 Usage : node SServer [option] h|help 帮助信息 -p|-port 指定端口 默认5000 -r|-root 指定根目录 默认当前目录
实现 SServer.js (static server)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 var http = require ("http" );var fs = require ("fs" );var util = require ("util" );var pathFn = require ("path" );var parse = require ("url" ).parse ;var arg = parseArgv ();var PORT = arg.port || 5000 ;var ROOT = arg.root || process.cwd ();ROOT = pathFn.resolve (ROOT );if (arg.help ) { console .log ( "\ Usage :\n\ node SServer [option]\n\ \n\ h|help 帮助信息\n\ -p|-port 指定端口 默认5000\n\ -r|-root 指定根目录 默认当前目录\n\ \n" ); return ; } http .createServer (function (req, res ) { req.url = decodeURI (req.url ); getFile (req, res, function (file ) { fs.exists (file, function (exists ) { if (exists) { var content_type = getContentType (file); res.writeHead (200 , { "Content-Type" : content_type, "Powerd-By" : "SServer" }); fs.createReadStream (file).pipe (res); } else { console .error ("%s 请求失败,找不到 %s\n" , req.url , file); res.writeHead (404 ); res.end (util.format (getNotFound (), req.url )); } }); }); }) .listen (PORT , function ( ) { process.title = "静态服务器SServer@localhost:" + PORT ; console .log ( "静态服务器SServer : \n 根目录 : %s \n访问地址 : http://localhost:%d\n" , pathFn.resolve (ROOT ), PORT ); require ("child_process" ).exec ("start http://localhost:" + PORT ); }); function getFile (req, res, callback ) { var url = decodeURI (parse (req.url ).pathname ); if (url.slice (-1 ) === "/" ) { fs.exists (ROOT + url + "index.html" , function (exists ) { if (exists) { callback (ROOT + url + "index.html" ); } else { callback (ROOT + url + "index.htm" ); } }); } else if (url.slice (url.lastIndexOf ("/" )).indexOf ("." ) === -1 ) { res.writeHead (301 , { Location : url + "/" }); res.end (); } else { callback (ROOT + url); } } function getContentType (file ) { var ext = pathFn.extname (file).slice (1 ); switch (ext) { case "html" : case "htm" : return "text/html" ; case "js" : return "application/javascript" ; case "json" : return "application/json" ; case "xml" : return "application/xml" ; case "css" : return "text/css" ; case "jpg" : case "jpeg" : return "image/jpeg" ; case "png" : return "image/png" ; case "gif" : return "image/gif" ; case "bmp" : return "image/bitmap" ; default : return "text/plain" ; } } function getNotFound ( ) { return "\ <html>\ <head>\ <title>Page Not Found</title>\ <style>\ *{\ font-family : 'YaHei Consolas Hybrid',微软雅黑,'Microsoft YaHei','Microsoft YaHei UI';\ }\ </style>\ </head>\ <body>\ <div style='font-size:100px;margin:100px 0 40px 100px;'>失败!</div>\ <h1 style='margin:20px 0 0 100px;'>找不到 %s</h1>\ <h3 style='margin:20px 0 0 100px;'>Powerd-By SServer(Static Server) 2014</h3>\ </body>\ </html>\ " ;} function parseArgv ( ) { var result = {}; var args = process.argv .slice (2 ); if (!args) return result; for (var i = 0 ; i < args.length ; i++) { var arg = args[i]; if (arg == "help" || arg == "h" ) { result.help = true ; } else if (arg == "-port" || arg == "-p" ) { result.port = parseInt (args[++i]); continue ; } else if (arg == "-r" || arg == "-root" ) { result.root = args[++i]; continue ; } } return result; }