HTTP面试指南

前言

或许你在面试时遇到过这样的问题:从输入URL到浏览器显示页面发生了什么?
简单的回答就是:

  1. DNS解析
  2. TCP建立连接
  3. 发送HTTP请求
  4. 服务器处理请求

    • 如果有缓存直接读缓存
    • 没有缓存返回响应内容
  5. TCP断开连接
  6. 浏览器解析渲染页面

如果你觉得这样回答过于简单,不如来深入了解一下吧。

网络基础

在此之前,先了解一下TCP/IP基础知识。

TCP/IP参考模型

《HTTP面试指南》

  • 早期的TCP/IP模型是一个四层结构,从下往上依次是网络接口层、互联网层、传输层和应用层,后来将网络接口层划分为了物理层和数据链路层

    • 应用层(Application)提供网络与用户应用软件之间的接口服务
    • 传输层(Transimission)提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
      传输层有两个性质不同的协议:TCP(传输控制协议)和UDP(用户数据报协议)
    • 网络层(Network)处理网络间路由,确保数据及时传送(路由器)
    • 数据链路层(DataLink)负责无错传输数据,确认帧、发错重传等(交换机)
    • 物理层(Physics)提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)

各层常用协议

《HTTP面试指南》

这里可以看到HTTP协议是构建于TCP之上,属于应用层协议

具体过程

1. DNS解析

DNS服务是和HTTP协议一样位于应用层的协议,提供域名到IP地址的解析服务。

得到IP地址后就可以建立连接了,这里还有两个知识需要了解:

持久连接

持久连接(也称为HTTP keep-alive)的特点是,只要任意一段没有提出断开连接,就保持TCP连接状态。

《HTTP面试指南》

管线化

持久连接建立后就可以使用管线化发送了,可以同时并发多个请求,不用等待一个接一个的响应。(在这里我想到了流的pipe方法。)
《HTTP面试指南》

2. TCP连接与断开

2.1 TCP报文格式

《HTTP面试指南》
大致说一下:

  1. 计算机通过端口号识别访问哪个服务,比如http;源端口号进行随机端口,目的端口决定哪个程序进行接收
  2. 数据序号和确认序号用于保障传输数据的完整性和顺序
  3. 需要注意的是TCP的连接、传输和断开都受六个控制位的指挥(比如三次握手和四次挥手)

    • PSH(push急迫位)缓存区将满,立刻速度传输
    • RST(reset重置位)连接断了重新连接
    • URG(urgent紧急位)紧急信号
    • ACK(acknowlegement确认)为1就表示确认号
    • SYN(synchronous建立联机)同步序号位 TCP建立连接时将这个值设为1
  4. 用户数据存储了应用层生成的HTTP报文

了解了这些,那么开始讲重点

2.2 TCP三次握手和四次挥手

三次握手
《HTTP面试指南》

  1. 客户端先发送一个带SYN标志的数据包给服务器端
  2. 服务器收到后,回传一个带有SYN/ACK标志的数据包表示确认收到
  3. 客户端再发送一个带SYN/ACK标志的数据包,代表握手结束

四次挥手
《HTTP面试指南》

  1. 客户端向服务器发出了FIN报文段
  2. 服务器收到后,回复一个ACK应答
  3. 服务器也向客户端发送一个FIN报文段,随后关闭了服务器端的连接
  4. 客户端收到之后,又向服务器回复一个ACK应答,过了一段计时等待,客户端也关闭了连接(计时等待是为了确认服务器端已正常关闭)

四次挥手并不是必然的,当服务器已经没有内容发给客户端了,就直接发送FIN报文段,这样就变成了三次挥手。

3. HTTP请求/响应

3.1 HTTP报文

HTTP报文大致可分为报文首部和报文主体两块,两者由空行(就相当于用了两个换行符rnrn)来划分。报文主体并不是一定要有的。

《HTTP面试指南》

3.1.1 请求报文

《HTTP面试指南》

常用请求行方法:

  • GET 获取资源
  • POST 向服务器端发送数据,传输实体主体
  • PUT 传输文件
  • HEAD 获取报文首部
  • DELETE 删除文件
  • OPTIONS 询问支持的方法
  • TRACE 追踪路径

3.1.2 响应报文

《HTTP面试指南》

说到响应报文,就必要谈到状态码:

  • 2XX 成功

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

    • 301(Moved Permanently) 永久重定向
    • 302(Found) 临时重定向,规范要求方法名不变,但是都会改变
    • 303(See Other) 和302类似,但必须用GET方法
    • 304(Not Modified) 状态未改变 配合(If-Match、If-Modified-Since、If-None_Match、If-Range、If-Unmodified-Since) (通常缓存会返回304状态码)
  • 4XX 客户端错误

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

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

3.1.3 首部

通用首部

首部字段名说明
Cache-Control控制缓存行为
Connection连接的管理
Date报文日期
Pragma报文指令
Trailer报文尾部的首部
Trasfer-Encoding指定报文主体的传输编码方式
Upgrade升级为其他协议
Via代理服务器信息
Warning错误通知

请求首部

首部字段名说明
Accept用户代理可处理的媒体类型
Accept-Charset优先的字符集
Accept-Encoding优先的编码
Accept-Langulage优先的语言
AuthorizationWeb认证信息
Expect期待服务器的特定行为
From用户的电子邮箱地址
Host请求资源所在的服务器
If-Match比较实体标记
If-Modified-Since比较资源的更新时间
If-None-Match比较实体标记
If-Range资源未更新时发送实体Byte的范围请求
If-Unmodified-Since比较资源的更新时间(和If-Modified-Since相反)
Max-Forwards最大传输条数
Proxy-Authorization代理服务器需要客户端认证
Range实体字节范围请求
Referer请求中的URI的原始获取方
TE传输编码的优先级
User-AgentHTTP客户端程序的信息

响应首部

首部字段名说明
Accept-Ranges是否接受字节范围
Age资源的创建时间
ETag资源的匹配信息
Location客户端重定向至指定的URI
Proxy-Authenticate代理服务器对客户端的认证信息
Retry-After再次发送请求的时机
Server服务器的信息
Vary代理服务器缓存的管理信息
www-Authenticate服务器对客户端的认证

实体首部

首部字段名说明
Allow资源可支持的HTTP方法
Content-Encoding实体的编码方式
Content-Language实体的自然语言
Content-Length实体的内容大小(字节为单位)
Content-Location替代对应资源的URI
Content-MD5实体的报文摘要
Content-Range实体的位置范围
Content-Type实体主体的媒体类型
Expires实体过期时间
Last-Modified资源的最后修改时间

3.2 实现客户端访问服务端

创建HTTP服务端

let http = require('http');
let app = http.createServer((req, res) => {// req是可读流/res是可写流
    // 获取请求报文信息
    let method = req.method;// 方法
    let httpVersion = req.httpVersion;// HTTP版本
    let url = req.url;
    let headers = req.headers;
    console.log(method, httpVersion, url, headers);
    // 获取请求体(如果请求体的数据大于64k,data事件会被触发多次)
    let buffers = [];
    req.on('data', data => {
        buffers.push(data);
    })
    req.on('end', () => {
        console.log(Buffer.concat(buffers).toString());
        res.write('hello');
        res.end('world');
    })
})
// 监听服务器事件
app.on('connection', socket => {
    console.log('建立连接');
});
app.on('close', () => {
    console.log('服务器关闭')
});
app.on('error', err => {
    console.log(err);
});
app.listen(3000, () => {
    console.log('server is starting on port 3000');
});

创建客户端

let http = require('http');
let options = {
    hostname: 'localhost',
    port: 3000,
    path: '/',
    method: 'GET',
    // 设置实体首部 告诉服务端我当前要给你发什么样的数据
    headers: {
        'content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': 15
    }
}
let req = http.request(options);
req.on('response', res => {
    res.on('data', chunk => {
        console.log(chunk.toString());
    });
});
req.end('name=js&&age=22')

然后使用node运行我们的客户端

《HTTP面试指南》

说了这么多,你可能已经大致了解了
从输入URL到浏览器显示页面发生了什么,不用多说,我们再来看一下缓存

4. 缓存

4.1 缓存作用

  • 减少了冗余的数据传输,节省了网费。
  • 减少了服务器的负担, 大大提高了网站的性能
  • 加快了客户端加载网页的速度

4.2 缓存分类

强制缓存

强制缓存:说白了就是第一次请求数据时,服务端将数据和缓存规则一并返回,下一次请求时浏览器直接根据缓存规则进行判断,有就直接读缓存数据库,不用连接服务器;没有,再去找服务器。
《HTTP面试指南》

对比缓存
  • 对比缓存,顾名思义,需要进行比较判断是否可以使用缓存。
  • 浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
  • 再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知* 客户端比较成功,可以使用缓存数据。

《HTTP面试指南》

4.3 请求流程

第一次请求,此时没有缓存

《HTTP面试指南》

第二次请求

《HTTP面试指南》

从上张图我们可以看到,判断缓存是否可用,有两种方式

  • ETag是实体标签的缩写,根据实体内容生成的一段hash字符串,可以标识资源的状态。当资源发生改变时,ETag也随之发生变化。ETag是Web服务端产生的,然后发给浏览器客户端。
  • Last-Modified是此资源的最后修改时间,

    • 如果客户端在请求到的资源中发现实体首部里有Last-Modified声明,再次请求就会在头里带上if-Modified-Since字段
    • 服务端收到请求后发现if-Modified-Since字段则与被请求资源的最后修改时间进行对比

说了这么多,不如直接来实现一下缓存

通过最后修改时间来判断缓存是否可用

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let app = http.createServer((req, res) => {
    // 根据url获取客户端要请求的文件路径
    let { parsename } = url.parse(req.url);
    let p = path.join(__dirname, 'public', '.' + pathname);
    // fs.stat()用来读取文件信息,文件最后修改时间就是stat.ctime
    fs.stat(p, (err, stat) => {
        if (!err) {
            let since = req.headers['if-modified-since'];//客户端发来的文件最后修改时间
            if (since) {
                if (since === stat.ctime.toUTCString()) {//最后修改时间相等,读缓存
                    res.statusCode = 304;
                    res.end();
                } else {
                    sendFile(req, res, p, stat);//最后修改时间不相等,返回新内容
                }
            } else {
                sendError(res);
            }
        }
    })
})
function sendError(res) {
    res.statusCode = 404;
    res.end();
}
function sendFile(req, res, p, stat) {
    res.setHeader('Cache-Control', 'no-cache');// 设置通用首部字段 控制缓存行为
    res.setHeader('Last-Modified', stat.ctime.toUTCString());// 实体首部字段 资源最后修改时间
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
    fs.createReadStream(p).pipe(res);
}
app.listen(3000, () => {
    console.log('server is starting on port 3000');
});

最后修改时间存在问题:

1. 某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否更新了。

2. 某些文件的修改非常频繁,在秒以下的时间内进行修改. Last-Modified只能精确到秒。

3. 一些文件的最后修改时间改变了,但是内容并未改变。 我们不希望客户端认为这个文件修改了。

4. 如果同样的一个文件位于多个CDN服务器上的时候内容虽然一样,修改时间不一样。

通过ETag来判断缓存是否可用

ETag就是根据文件内容来判断,说白了就是采用MD5(md5并不叫加密算法,它不可逆,应该叫摘要算法)产生信息摘要,用摘要来进行比对。

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
// crypto是node.js中实现加密和解密的模块 具体详解请自行了解
let crypto = require('crypto');
let app = http.createServer((req, res) => {
    // 根据url获取客户端要请求的文件路径
    let { parsename } = url.parse(req.url);
    let p = path.join(__dirname, 'public', '.' + pathname);
    // fs.stat()用来读取文件信息,文件最后修改时间就是stat.ctime
    fs.stat(p, (err, stat) => {
        let md5 = crypto.createHash('md5');//创建md5对象
        let rs = fs.createReadStream(p);
        rs.on('data', function (data) {
            md5.update(data);
        });
        rs.on('end', () => {
            let r = md5.digest('hex'); // 对文件进行md5加密
            // 下次就拿最新文件的加密值 和客户端请求来比较
            let ifNoneMatch = req.headers['if-none-match'];
            if (ifNoneMatch) {
                if (ifNoneMatch === r) {
                    res.statusCode = 304;
                    res.end();
                } else {
                    sendFile(req, res, p, r);
                }
            } else {
                sendFile(req, res, p, r);
            }
        });
    })
});
function sendError(res) {
    res.statusCode = 404;
    res.end();
}
function sendFile(req, res, p, stat) {
    res.setHeader('Cache-Control', 'no-cache');// 设置通用首部字段 控制缓存行为
    res.setHeader('Etag', r);// 响应首部字段 资源的匹配信息
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
    fs.createReadStream(p).pipe(res);
}
app.listen(3000, () => {
    console.log('server is starting on port 3000');
});

最后

想深入学习http的同学,我推荐一本书《图解HTTP》
本人水平有限,有不足之处,望大家指出改正。

    原文作者:acdseen
    原文地址: https://segmentfault.com/a/1190000014579248
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞