Node和http:一本通【附tcp实现http小代码】

  • TCP与HTTP
  • HTTP报文格式
    • 请求报文
      • 请求行
      • 请求头
      • 请求体
    • 响应报文
      • 响应行
      • 响应头
      • 响应体
    • 关于url
    • 关于method
    • 关于请求体
    • 关于状态码
      • 2xx 请求正常
      • 3xx 缓存和重定向
      • 4xx 客户端错误
      • 5xx 服务端错误
  • 在node中获取请求报文
  • 在node中建立http服务器与客户端
    • 建立一个服务器
    • 建立一个客户端
      • 响应注意事项
  • 关于响应实体和Content-Type
  • 用tcp实现一个简单的http服务器
    • 注册响应回调
    • parser分离请求报文与发射request
    • 解析请求头
    • demo代码
  • http其它
    • 非短链接
    • 管线化

pre-notify

previously:html

TCP是HTTP的基石,对tcp还不是灰常清楚的小伙伴能够看看个人这篇NodeJS和TCP:一本通node

本文主要用于我的知识梳理,可能篇幅较长,So专门在开头copy了一份目录以便配合右下方链接点击分段阅读(づ ̄ 3 ̄)づgit

TCP与HTTP

首先,HTTP 是基于 TCP 协议的,只有当tcp链接顺利创建时,浏览器客户端才能向服务器发送http请求。(详见TCP三次握手)github

TCP 让让一台pc端对端的链接上另外一台pc后,两台机器之间能够互通数据,但这个数据并无通过什么额外的加工,是纯粹的数据,即用户输入什么数据,服务器就会拿到什么数据。web

HTTP 有些许不同, 一个http请求会将用户的输入通过浏览器包装后再发送给服务端,而包装后的数据便是咱们说的 http请求报文。对应的,服务器要向浏览器回复响应,也须要通过一层包装,包装成 http响应报文再响应给客户端。npm

从传输层面上来说,http仅仅是tcp的一项子集,一种再封装。编程

HTTP报文格式

HTTP报文格式 是对 http协议最直观的阐释,也是咱们学习http协议最有效率的手段。json

HTTP报文主要分文两大类,请求报文响应报文,请求报文和响应报文又都分为 三部分,而且头和体这两部分之间有 空行 隔开。跨域

请求报文

如下图片出自于Android网络编程随想录(2) 浏览器

请求行

请求行分为如下三部分,每一个部分之间用空格隔开

  • method: 主要是用来标识是要传数据仍是获取数据
  • path: url地址
  • protocol http的协议版本号

请求头

请求头和请求行不同,它是多行的,每一行都是一组键值对,键和值之间用:空格隔开。

  • 请求首部: Host:xxx.com
  • 通用首部: 请求和响应都有的,好比 Connection:keep-alive
  • 实体首部: 以 Content-开头的
  • 其它

请求体

正经的,用户想要传给服务器的数据

响应报文

如下图片出自于Android网络编程随想录(2)

响应行

  • protocol http协议版本号
  • statusCode 状态码
  • statusCode-reason 缘由短语,状态码的解释

响应头

同请求头

响应体

同请求体

关于url

客户端封装请求报文时会将url中的hash给去掉(query是会保留的)

故服务器端是永远接收不到客户端的hash值的

关于method

GET 获取资源

POST 想服务器端发送数据,传输实体主体

PUT 传输文件 , RESTful中是更新修改操做

HEAD 获取报文首部

DELETE 删除文件

OPTIONS 询问支持的方法 ,试探方法,好比跨域,会先询问服务端可否跨域

TRACE 追踪路径

关于请求体

当提交的表单只包含一条数据时,且表单类型为默认时,请求报文长这样

若是是多条数据,会用空行隔开

但若是是multipart/form-data编码时,请求体中多端数据间则是用特殊的分隔符来隔开的

即便只有一段数据也会用特殊的分隔符包裹住

多段数据时

关于状态码

状态码主要分为五大类

  • 1xx imformational(信息状态码) websocket
  • 2xx Success
  • 3xx Redirect
  • 4xx Client Error
  • 5xx Server Error

2xx

  • 200 OK 客户端发送过来的数据被正常处理
  • 204 Not Content 正常响应,没有实体
  • 206 Partial Content 范围请求,返回部分数据,响应报文中由Content-Range指定内容

3xx

  • 301 Moved Permanently 永久重定向
  • 302 Found 临时重定向 不必定去哪 跳转到不一样的地方 Nginx
  • 303 See Other和302相似,但必须用GET方法
  • 304 Not Modified 状态未改变 须要和(if-Match、if-Modified-since、if-None_Match、if-Range、if-Unmodified-since)配合使用
  • 307 Temporary Redirect 临时重定向,不改变请求方法

4xx

  • 400 Bad Request 请求报文语法错误
  • 401 unauthorized 须要认证
  • 403 Forbidden 服务器拒绝访问对应的资源
  • 404 Not Found 服务器上没法找到资源

5xx

  • 500 Internal Server Error 服务器故障
  • 503 Service Unavailable 服务器处于超负载或正在停机维护

在node中获取请求报文

console.log(req.method); //请求方法
console.log(req.url); //url地址
console.log(req.httpVersion); //http协议版本
console.log(req.headers); //请求头
复制代码
// 获取请求体
req.on('data',function(data){
    console.log(data.toString());
})
复制代码

在node中建立http服务器与客户端

建立一个服务器

let http = require('http');
let server = http.createServer();
server.on('request',function(req,res){
  res.end('ok');
});
server.listen(8080);
复制代码

你能够能够这样简写

let http = require('http');
let server = http.createServer(function(req,res){
  res.end('ok');
});
server.listen(8080);
复制代码

建立一个客户端

let http = require('http');
let options = {
  host:'localhost'
  ,port:8080
  ,method:'POST'
  ,headers:{
    'Content-Type':'application/x-www-form-urlencoded'
//      ,'Content-Length':15 //通常来讲这个数值会自动计算
  }
}

let req = http.request(options);
req.write('id=999');
// 只有调用end才会真正向服务器发送请求
req.end();

// 当客户端收到服务器响应的时候触发
req.on('response',function(res){ //只有一个参数
  console.log(res.statusCode);
  console.log(res.headers);
  let result = [];
  res.on('data',function(data){
    result.push(data);
  })
  res.on('end',function(data){
    let str = Buffer.concat(result);
    console.log(str.toString());
  })

})
复制代码

还能够把request()on('response')合在一块儿写,不过此时没法像服务端主动发送头之外的数据(只有调用http.request(opt)才会返回req,才能调用write())。

http.get(options,function(res){
    ...
    res.on('data',function(chunk){
      ...
    });
    res.on('end',function(){
      ...
    })
})
复制代码

响应注意事项

end后没法继续写入(可写流规定)

res.write()
res.end()

<<<
Erorr::write after end!
复制代码

设置状态码之后 会自动补全状态码文本描述

res.statusCode = 200; //默认
复制代码

咱们不只能够设置,也能够删除一个准备发送给客户端的响应头

res.setHeader('Content-Type','text/plain');
res.setHeader('name','ahhh');
res.removeHeader('name'); //删除一个准备设置的头
复制代码

writeHead相较于setHead能同时设置多个头,而且连状态码一块儿设置。但它和setHeader最大的不一样在于,writeHeader一旦调用会马上发送。

console.log(res.headersSent) //false
res.writeHead(200,{'Content-Type':'text/plain'}); //writeHead设置完后不能再调用res.setHeader,由于调用writeHead会直接把头发送出去
// res.setHeader('name','zfpx'); //Can't set headers after they are sent. console.log(res.headersSent) //true 复制代码

setHeader设置的头是在调用write方法以后才会发送,另外须要注意的一点是头必须在write以前设置。

console.log('--- --- ---')
console.log(res.headersSent); //false
res.setHeader('name','ahhh');
console.log(res.headersSent) //false
res.write('ok');
console.log(res.headersSent) //true
res.end('end');
console.log(res.headersSent) //true  
console.log('--- --- ---')
复制代码

关于响应实体和Content-Type

客户端发送请求和服务端回以响应时都须要设置这个Content-Type头,

对于服务端来讲,它须要拿这个头解析客户端发送过来的实体数据,(纵然很多状况下,请求都没有实体部分,好比get请求)。

let buffers = [];
req.on('data',function(chunk){
    buffers.push(chunk);
})
req.on('end',function(){
    let content = Buffer.concat(buffers).toString();
    if(contentType === 'application/json'){
      console.log(JSON.parse(content).name);
    }else if(contentType === 'application/x-www-form-urlencoded'){
      let queryString = require('querystring');
      console.log(queryString.parse(content).name);
    }
})
复制代码

实际状况下,若是有请求体(实体数据),可能会很复杂。(前面的请求体部分)

而且服务端响应客户端数据时也须要发给它这么一个头以便客户端解析数据,而这个Content-Type每每和要返回给客户端的资源文件的后缀名是相关联的,So咱们通常使用一个npm包帮咱们进行转换,

...
let mime = require('mime');
...
res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
复制代码

用tcp实现一个简单的http服务器

http服务器相较于tcp服务器其实就多作了一件事,即解析请求头,剩下的请求体部分该on data仍是同样on data监听便可。

但须要注意的是,data,即请求体是何时 发射 的呢?嗯,是在分离出请求头并解析完毕请求头后发射的。

注册响应回调

// http.createServer(function(req,res){})

server.on('request',function(req,res){
    //do somtheing like you are doing at tcp
}
复制代码

parser分离请求报文与发射request

let server = net.createServer(function(socket){
  parser(socket,function(req,res){
    server.emit('request',req,res);
  });
});

server.listen(3000);
复制代码

这里分离请求报文是指将 请求体 与其它两部分(请求行,请求头)分红两块,怎么分?嗯,前面说过,请求体和请求头之间有一行空行做为分隔,\r\n\r\n 或则说 0x0d 0x0a 0x0d 0x0a

嗯。。原理就是这么个原理咯,但有一个坑。

socket做为一个双工流,在读取客户端发来的数据时和普通的可读流同样有一个默认读取值,So,这可能致使你要读不少下才摸获得\r\n\r\n这个分隔符,

而且可能最终读到\r\n\r\n 时还会多读出一些不属于"请求头"部分数据,咱们还须要将这部分多余的属于请求体的数据回去,以便发射requset事件时咱们能拿取到完整的 请求体 数据。

嗯。。坑比较多,这里就不献丑贴代码了,有兴趣的小伙伴能够本身去实现如下,有两点须要注意

  • 读取时使用readable暂停模式来读取(以便把多余的数据按回去)

  • 推荐用0x0d 0x0a这种buffer级别的来判断而不是\r\n这种字符级别,由于字符可能会致使乱码很差判断,须要处理的状况就更多了。

解析请求头

这里的请求头 包括 请求行与请求头

function parseHeader(head){
  let lines = head.split(/\r\n/);
  let start = lines.shift();
  let lr = start.split(' ');
  let method = lr[0];
  let url = lr[1];
  let httpVersion = lr[2].split('/')[1];
  let headers = {};
  lines.forEach(line=>{
    let col = line.split(': '); //注意这里的空格
    headers[col[0]] = col[1];
  });
  return {url,method,httpVersion,headers};
}
复制代码

demo代码

仓库:点我点我!

http其它

非短链接

虽然http不想tcp同样能够一直保持长链接,但咱们说过它毕竟是基于tcp的,因此也具备保持链接的能力。

在响应头中每每会包含 Connection:keep-alive 字样的字段,就是让浏览器保持链接不要中断,即便接受完响应信息,这个链接通常也能保持必定的时间(大概,嗯,2min?)

管线化

http发送请求时若是包含多个,能够不用等待就能直接发送下一个请求。

Chrome 并发量约为6个,Firefox 4个。


To be Continue...

相关文章
相关标签/搜索