tcp如何实现http协议

1,083 阅读3分钟

目标:

  1. 了解 http 协议的内容
  2. 理解node中tcp的基本使用
  3. 用net实现一个http协议

http 通常跑在TCP/IP协议栈上的,所以http一定是基于tcp实现的,那它是怎么实现的呢?让我们一起来研究一下。

启动一个node http服务器;

tcp实现http/demo1

const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
  if (['/get.html'].includes(req.url)) {
    res.writeHead(200, {
      'Context-type': "text-html"
    });
    res.end(fs.readFileSync(path.join(__dirname, 'static', req.url.slice(1))));
  } else if (req.url === '/get') {
    res.writeHead(200, {
      'Context-type': "text-plain"
    });
    res.end('get');
  }
});
server.listen(8081);

通过wireshark 过滤条件 tcp.port == 8081;得到如下两种图:

请求报文的图:

image.png

响应报文的图:

image.png

浏览器图:

image.png

结论:

请求报文的组成部分

GET /get HTTP/1.1
Host: localhost:8081
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"
age: 10
name: zhufeng
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8081/get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

请求报文的组成部分:

  1. 请求行由三部分构成:
    1. 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
    2. 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
    3. 版本号:表示报文使用的 HTTP 协议版本。

这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束。

Method SP URL SP Version CRLF

  1. 头部字段集合(header):使用 key-value 形式更详细地说明报文;

Context-type: text-plain Date: Tue, 20 Apr 2021 12:20:53 GMT Field Name : Field Value CRLF

  1. 响应体

画图表示: image.png

大致了解了http中的协议,我们来研究下node中的tcp。

node的net模块

node中的net模块时基于tcp实现的,所以按道理说,我们可以基于net模块实现自己的http服务器;

运行tcp模块

const net = require('net');
const fs = require('fs')
const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    socket.write(134)
    socket.end();
  });
})
server.on('error', (err) => {
  console.error(err);
});

server.listen(5555, () => {
  console.log('服务器已经启动', server.address());
});

浏览器访问 http://localhost:5555/get 会发现页面没有内容。这是因为我们虽然启动了tcp服务器,但是我们没有按照http的协议来解析和发送,所以浏览器无法正常显示。

解析http协议

tcp实现 http/demo2

const net = require('net');
const fs = require('fs')
const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    let request = data.toString();
    // 解析请求行
    let [requestLine, ...headerRows] = request.split('\r\n');
    // 获取  请求的方法,请求的路径
    let [method, path] = requestLine.split(' ');
    // 解析头部字段集合,获取header;
    let headers = headerRows.slice(0, -2).reduce((memo, row) => {
      let [key, value] = row.split(': ');
      memo[key] = value;
      return memo;
    }, {});
    console.log('method', method);
    console.log('path', path);
    console.log('headers', headers);
    var obj = {
      method,
      path,
      headers
    }
    let rows = [] 
    if(path==='/get'){
       	 // 按照响应报文进行返回;
         rows.push(`HTTP/1.1 200 OK`);
         rows.push(`Context-type: text-plain`);
         rows.push(`Date: ${new Date().toGMTString()}`);
         rows.push(`Connection: keep-alive`);
         rows.push(`Transfer-Encoding: chunked`);
          let responseBody = `{"name":"124"}`;
          rows.push(`\r\n${Buffer.byteLength(responseBody).toString(16)}\r\n${responseBody}\r\n0\r\n\r\n`);
    }
 
    let response = rows.join('\r\n');
    socket.write(Buffer.from(response))
    socket.end();
  });
})
server.on('error', (err) => {
  console.error(err);
});

server.listen(5555, () => {
  console.log('服务器已经启动', server.address());
});

在通过浏览器访问 http://localhost:5555/get ; 浏览器返回 {"name":"124"}, 这个就代表我们解析报文成功了。

解析post协议

由于post请求传递的参数是放在请求体里面的,所以解析http协议的内容会麻烦许多,因此我们采用”状态机“的方式来解析字符串。

tcp实现 http/demo3 状态机逻辑分析: 通过逐个分析字符,根据字符的状态,进入不同的分析,解析出来自己想要的信息; image.png

Parser.js 状态机代码:

let LF = 10, //换行  line feed
  CR = 13, //回车 carriage return
  SPACE = 32, //空格
  COLON = 58; //冒号
let PARSER_UNINITIALIZED = 0, //未解析
  START = 1, //开始解析
  REQUEST_LINE = 2, // 分析请求行
  HEADER_FIELD_START = 3, // 分析请求头( header)
  HEADER_FIELD = 4,
  HEADER_VALUE_START = 5,
  HEADER_VALUE = 6,
  READING_BODY = 7;
class Parser {
  constructor() {
    this.state = PARSER_UNINITIALIZED;
  }
  parse(buffer) {
    let self = this,
      requestLine = "",
      headers = {},
      body = "",
      i = 0,
      char,
      state = START, //开始解析
      headerField = "",
      headerValue = "";
    // console.log(buffer.toString());
    for (i = 0; i < buffer.length; i++) {
      char = buffer[i];
      switch (state) {
        case START:
          state = REQUEST_LINE; // 分析请求行
          self["requestLineMark"] = i; // 请求行开头字母标记
        case REQUEST_LINE:
          if (char == CR) {
            //换行
            //POST /post HTTP/1.1   0~19 获取请求行
            requestLine = buffer.toString("utf8", self["requestLineMark"], i);
            break;
          } else if (char == LF) {
            //回车
            state = HEADER_FIELD_START; // 分析请求头(header)
          }
          break;
        case HEADER_FIELD_START:
          if (char === CR) {
            state = READING_BODY; // 剩下的就是body的数据了
            self["bodyMark"] = i + 2;
            break;
          } else {
            state = HEADER_FIELD;
            self["headerFieldMark"] = i;
          }
        case HEADER_FIELD:
          if (char == COLON) { // 判断冒号,是不是已经获取到了key
            headerField = buffer.toString("utf8", self["headerFieldMark"], i);
            state = HEADER_VALUE_START;
          }
          break;
        case HEADER_VALUE_START:
          if (char == SPACE) {
            break;
          }
          self["headerValueMark"] = i;
          state = HEADER_VALUE;
        case HEADER_VALUE:
          if (char === CR) { //如果是换行符,代表已经获取到了value
            headerValue = buffer.toString("utf8", self["headerValueMark"], i);
            headers[headerField] = headerValue;
            headerField = "";
            headerValue = "";
          } else if (char === LF) {
            state = HEADER_FIELD_START;
          }
          break;
        default:
          break;
      }
    }
    let [method, url] = requestLine.split(" ");
    // console.log(self["bodyMark"], i, JSON.stringify(buffer.toString("utf8", self["bodyMark"] - 2, i)))
    body = buffer.toString("utf8", self["bodyMark"], i);
    return {
      method,
      url,
      headers,
      body,
    };
  }
}
module.exports = Parser;

使用:

const net = require('net');
const Parer = require('./Parser');
const fs = require('fs')
const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    let parser = new Parer(); 
    let { method, url,headers,body } = parser.parse(data); 
    if (url==='/post'){
         let rows = [];
         rows.push(`HTTP/1.1 200 OK`);
         rows.push(`Context-type: text-plain`);
         rows.push(`Date: ${new Date().toGMTString()}`);
         rows.push(`Connection: keep-alive`);
         rows.push(`Transfer-Encoding: chunked`);
        //  console.log(body)
         rows.push(`\r\n${Buffer.byteLength(body).toString(16)}\r\n${body}\r\n0\r\n\r\n`);
         let response = rows.join('\r\n');
        //  console.log(response)
         socket.end(response);
    } 
   
  });
})
server.on('error', (err) => {
  console.error(err);
});

server.listen(8082, () => {
  console.log('服务器已经启动', server.address());
});

上诉代码就可以获取浏览器的data,并且返回给浏览器;

上面的代码就是我们自己基于tcp实现的一个http服务器;

思考:如何实现上传:

image.png 如果我们上传的信息,在后台如何用tcp解析呢?

参考文献与代码: 代码