基于tcp的http应用,断点续传,范围请求

TCP


要说http就绕不开tcp,TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来讲,两者没有可比性。可是,http是基于tcp协议的。javascript

TCP/IP 协议七层模型


物理层 链路层
将二进制的0和1和电压高低,光的闪灭和电波的强弱信号进行转换 驱动

网络层css

- 使用 IP 协议,IP 协议基于 IP 转发分包数据
- IP 协议是个不可靠协议,不会重发
- IP 协议发送失败会使用ICMP 协议通知失败
- ARP 解析 IP 中的 MAC 地址,MAC 地址由网卡出厂提供
- IP 还隐含链路层的功能,无论双方底层的链路层是啥,都能通讯
复制代码

传输层 通用的 TCP 和 UDP 协议(tcp比udp安全)html

TCP 协议面向有链接,能正确处理丢包,传输顺序错乱的问题,可是为了创建与断开链接,须要至少7次的发包收包,资源浪费
UDP 面向无链接,无论对方有没有收到,若是要获得通知,须要经过应用层
复制代码

会话层以上分层 TCP/IP 分层中,会话层,表示层,应用层集中在一块儿 网络管理经过 SNMP 协议前端

HTTP

Http协议是创建在TCP协议基础之上的,当浏览器须要从服务器获取网页数据的时候,会发出一次Http请求。Http会经过TCP创建起一个到服务器的链接通道,当本次请求须要的数据完毕后,Http会当即将TCP链接断开,这个过程是很短的。因此Http链接是一种短链接,是一种无状态的链接。java

keep-alive http虽然没有状态,可是能够经过会话例如session保持链接 连接可复用,节约拆桥时间。node

状态码


1XX 2XX 3XX 4XX 5XX
信息性状态码 成功状态码 重定向 客户端错误状态码 服务端错误状态码
少见 200 OK 301 永久性重定向 400 请求报文语法错误 500服务器请求错误
  204 响应报文不含实体的主体部分 302 临时性重定向(负载均衡) 401发送的请求须要有经过 HTTP 认证的认证信息 307 和302含义相同 503 服务器暂时处于超负载或正在停机维护,没法处理请求
  206 范围请求 303 资源存在着另外一个 URL,应使用 GET 方法定向获取资源 403 对请求资源的访问被服务器拒绝  
    304 客户端已经执行了GET,但文件未变化。 404 服务器上没有找到请求的资源  

console.log错误状态码


  • SyntaxError是解析代码时发生的语法错误
  • Uncaught ReferenceError:引用错误
  • RangeError:范围错误
  • TypeError类型错误
  • URIError,URL错误
  • EvalError eval()函数执行错误
  1. 咱们今天要说的是网络层的http;咱们知道http是用来写服务端的,那么他能够作什么呢? 像ajax交互、状态码{304: 服务器的缓存问题 ,206: 范围请求(部分请求) } 、压缩{ Content-Encoding:gzip deflate}、图片的防盗链 、加密问题{对称,非对称}、服务器代理(正向代理,反向代理)

写一个静态服务(命令行工具)web

输入url到页面加载都发生了什么事情?


3次握手


客户端–发送带有SYN标志的数据包–一次握手–服务端 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端 客户端–发送带有带有ACK标志的数据包–三次握手–服务端ajax

4次挥手


客户端-发送一个FIN,用来关闭客户端到服务器的数据传送 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN同样,一个FIN将占用一个序号 服务器-关闭与客户端的链接,发送一个FIN给客户端 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1跨域

**输入地址
> 浏览器查找域名的 IP 地址
> 这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...
> 浏览器向 web 服务器发送一个 HTTP 请求
> 服务器的永久重定向响应(从 http://example.com 到 http://www.example.com)
> 浏览器跟踪重定向地址
> 服务器处理请求
> 服务器返回一个 HTTP 响应
> 浏览器显示 HTML
> 浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
> . 浏览器发送异步请求**
复制代码

管线化

能够处理并发promise

URI url urn

uri 统一资源表示符,url 统一资源定位符 location ,统一资源命名符

url 组成

> http://(协议)name:password(登陆信息,认证)@www.fs.ip(服务器地址):8080(端口号)/dir/index.htm(文件路径)?a=a(查询字符串)#asd(片断标识符)
复制代码

咱们访问一个路径,路径回去dns域上找对应的ip地址,中间包括查找浏览器缓存,本地文件等等,最后将ip地址返回,http主要针对应用层,应用层会有一些报文,主要经过tcp传输,http基于tcp,TCP会将HTTP拆分红不少段,把每一个报文可靠的传给对方,udp是不可靠的会丢包,拼完以后返回服务器

node之url模块

let url = require('url');

let Obj  = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1#aaa')
console.log(Obj);

=>Url {
  protocol: 'http:',
  slashes: true,
  auth: 'user:passwrd',
  host: 'www.zdl.cn:80',
  port: '80',
  hostname: 'www.zdl.cn',
  hash: '#aaa',
  search: '?a=1',
  query: 'a=1',
  pathname: '/1.html',
  path: '/1.html?a=1',
  href: 'http://user:passwrd@www.zdl.cn:80/1.html?a=1#aaa' }
复制代码

咱们一般要解析字符串

let url = require('url');

let {pathname ,query,path}  = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1&b=2&c=3&d=4#aaa')
//解析
let str = query;
let obj = {};
str.replace(/([^=&]*)=([^=&])/g,function(){
    obj[arguments[1]] = arguments[2];
})

console.log(obj);
//或者
let {pathname ,query,path}  = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1&b=2&c=3&d=4#aaa',true) //添加true会直接解析
//解析
console.log(query);
复制代码

如上请求报文包括,请求行,请求首部,请求实体(带content的都是实体),,而后请求还会返回响应头,请求头和响应头都有的叫通用首部字段,请求头独有的叫请求首部字段。

这个url会给咱们解析出一个url对象,包括url个组成部分 http和ttp差的是报文,是一种不保存但能够保持状态的协议

restful风格

get post put delete head(获取报文首) options(跨域试探性请求,节约流量) teace(调用盏,追踪路径)

范围请求

curl -v --header "Range:bytes=1-200" https://www.baidu.com/img/bd_logo1.png?qua=high

=>subjectAltName: host "www.baidu.com" matched cert's "*.baidu.com"
*  issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign Organization Validation CA - SHA256 - G2
*  SSL certificate verify ok.
> GET /img/bd_logo1.png?qua=high HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.54.0
> Accept: */*
> Range:bytes=1-200
>
< HTTP/1.1 206 Partial Content
< Accept-Ranges: bytes
< Cache-Control: max-age=315360000
< Connection: Keep-Alive
< Content-Length: 200
< Content-Range: bytes 1-200/7877
< Content-Type: image/png
< Date: Sat, 07 Jul 2018 03:56:46 GMT
< Etag: "1ec5-502264e2ae4c0"
< Expires: Tue, 04 Jul 2028 03:56:46 GMT
< Last-Modified: Wed, 03 Sep 2014 10:00:27 GMT
< P3p: CP=" OTI DSP COR IVA OUR IND COM "
< Server: Apache
< Set-Cookie: BAIDUID=B37F40A5E737D32A9475DC95E0925B45:FG=1; expires=Sun, 07-Jul-19 03:56:46 GMT; max-age=31536000; path=/; domain=.baidu.com; version=1
<
PNG
复制代码

请求头中的Range来指定 资源的byte范围 响应会返回状态码206响应报文 对于多重范围的范围请求,响应会在首部字段Content-Type中标明multipart/byteranges

用tcp写个服务

let http = require('http')

let server = http.createServer();
server.on('request',function(req,res){
    console.log("请求到来了");
    //req,res都是基于socket的 req可读流=> 客户端,res可泻流 => 服务端
    //res.writeHead(200,{...}) 之能写一次,且和setHeader冲突
    res.statusCode = 200;
    res.setHeader('Content-Length',2);
    res.setHeader('Content-Type','text/html;charset=utf8');
    res.end('hello');
    //能够经过服务器,curl ,postman等发送请求
})
//默认连接成功以后会把socket解析成两个东西,req,res 解析后触发事件叫request事件
server.on('connection',function(socket){
    //net 中的socket和res一个效果
// socket.write(`
// HTTP/1.1 200 OK
// Content-Length :2
// Content-Type: text/html;charset=utf8

// ok
// `)
// socket.end()
    console.log("连接成功");
    
})
server.listen(3000)
复制代码

post 和 get 的区别

  • post有请求体,咱们能够吧传递的内容放到请求体内
  • get在url传递,
  • post看似相对安全,其实不安全
  • get只能发64k post能够发送不少

http应用

//命令行输入测试
//curl -X POST -v -d "name=zdl" http://localhost:3000/a?a=1&c=4
let http = require('http')

let server = http.createServer();
server.on('request',function(req,res){
    console.log(req.method);
    console.log(req.url);
    console.log(req.httpVersion);
    console.log(req.headers);//请求头对象,要取里面的参数,能够经过key来取(小写)
    let arr = [];
    req.on('data',function(data){//只要是post须要经过监听事件获取数据,默认触发一次64k
        arr.push(data)
    })
    req.on("end",function(){
        let str = Buffer.concat(arr);
        console.log(str.toString());
        res.end('hello');
    })
})
server.listen(3000,function(socket){
    console.log("server start 3000");
})

复制代码

用tcp模拟HTTP

//测试命令行输入
let net = require('net');
let server = net.createServer();
function parser(socket,callback){
    // socket.on("data",function(){

    // })//接收

    function parserHeader(head){
        let obj = {};
        let headers = head.split(/\r\n/);
        let line = headers.shift();
        let [method,path,version] = line.split(' ');
        let heads = {};
        headers.forEach(line => {
            let [key,value] = line.split(': ');
            heads[key] = value;
        });
        obj['method'] = method;
        obj['path'] = path;
        obj['version'] = version;
        obj['headers'] = headers;
        return obj;
        
    }
    function fn(){
        let result = socket.read().toString();//若是read 不传参数会默认全读
        let [head,content] = result.split(/\r\n\r\n/);
        let obj = parserHeader(head);
        console.log(obj);
        //readble方法会触发屡次,触发一次后就移除掉
        socket.removeListener('readable',fn)
        
    }
    socket.on("readable",fn)//默认把缓存区填满
}
server.on('connection',function(socket){
    parser(socket,function(req,res){
        server.emit('request',req,res);//将socket派发给request
    })
})
server.on('request',function(req,res){
    console.log(req.method);
    console.log(req.url);
    console.log(req.httpVersion);
    console.log(req.headers);//请求头对象,要取里面的参数,能够经过key来取(小写)
    let arr = [];
    req.on('data',function(data){//只要是post须要经过监听事件获取数据,默认触发一次64k
        arr.push(data)
    })
    req.on("end",function(){
        let str = Buffer.concat(arr);
        console.log(str.toString());
        res.end('hello');
    })
})
server.listen(3000)
复制代码

http头的应用

咱们常常用的断点续传,多语言还有防盗链等等都是基于咱们的http来实现的,包括缓存,

断点续传,分段请求,上面咱们提到的分段请求(range:bytes=0-3)

  • 客户端 请求头 Range:bytes=1-200

  • 服务端

响应头
Accept-Ranges: bytes < Content-Length: 200 < Content-Range: bytes 1-200/7877

模拟上述功能

range.js

//服务端
let http = require('http');
let path = require('path');  //路径
let fs = require('fs'); //读文件
let p = path.join(__dirname,'1.txt'); //获取读文件的路径
let {promisify} = require('util');
let stat = promisify(fs.stat); // 将stat方法转化成promise的方法 可能没有end默认所有读取
let server = http.createServer();
server.on('request',async function (req,res) {
    //取请求头,取的到则分段,不然就总体获取
    let range = req.headers['range'];
    try{
        let s = await stat(p);
        let size = s.size;
        if (range) {
            let [, start, end] = range.match(/(\d*)-(\d*)/);//第一个参数是匹配字符串,第二个是第一项,第二个是第二项
            start = start ? Number(start) : 0;
            end = end ? Number(end)-1 : size-1;
            res.statusCode = 206;
            // 告诉客户端当前是范围请求
            res.setHeader('Accept-Ranges','bytes');
            // 返回的内容长度
            res.setHeader('Content-Length',end-start+1);
            res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`); 
            fs.createReadStream(p,{start,end}).pipe(res); //把读取的结果传给res
        } else {
            // 边读边写,返回文件
            fs.createReadStream(p).pipe(res);//res是可写流,在可读流和可写流之间加管道,至关于不停的读文件不一样的调res的write方法
        }
    }catch(e){
        console.log(e);
        
    }
})
server.listen(3000); 

//测试curl -v --header "Range:bytes=3-5" http://localhost:3000
复制代码

实现下载功能

client.js

//客户端,须要启动楼上的服务端,而后在cmd里node 当前文件夹的名字运行客户端
let http = require('http');
let fs = require('fs');
let pause = false; // 默认开启下载模式 true时暂停
let ws = fs.createWriteStream('./download.txt');//但愿下载到这个地方去
let options = {
    hostname: 'localhost',  //主机/路径
    port: 3000, //端口号 还有个头0-3/3-5等等
}
// 实现下载功能
let start = 0;
//监控输入
process.stdin.on('data',function (data) {
    data = data.toString();
    if(data.match(/p/)){
        pause = true;
    }else{
        pause = false;
        download();
    }
})

function download() {
    // 请求以前加个请求头
    options.headers = {
        'Range': `bytes=${start}-${start + 9}`
    }
    start += 10;
    // let socket = http.request(options);//每次调用时请求的文件位置累加
    // socket.write();
    // socket.end()//发送请求
    //等同于
    http.get(options, function (res) { //屡次发送请求 get 没有请求题
        let buffers = [];
        let total = res.headers['content-range'].split('/')[1];
        total = parseInt(total);//58
        res.on('data',function(data){
            buffers.push(data);
        })
        res.on('end', function () {
            let str = Buffer.concat(buffers).toString();
            ws.write(str);//写到文件去
            if (!pause && start < total) { // 没有完毕才继续请求
              setTimeout(() => {
                download()
              }, 1000);
            }
        });
    })
}
download();
复制代码

防盗链

  • 在百度随便找一张图片
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
    <script src="main.js"></script>
</head>
<body>
    <img src="http://d.hiphotos.baidu.com/video/pic/item/e850352ac65c10385fd7b21fbe119313b17e8945.jpg" alt="">
</body>
</html>
复制代码

用http-server启动服务器,结果图片出现裂图,这就是简单的防盗,原理很简单,Referer: 若是当前请求不容许访问,返回裂图

文件结构

index.html

<body>
    <img src="http://localhost:3000/2.jpg" alt="">
</body>
复制代码
let http = require('http');
let path = require('path');
let url = require('url');
let fs = require('fs');
let {promisify } = require('util');
let stat = promisify(fs.stat);

let whiteList = ['www.zdl1.cn'];
let p = path.resolve(__dirname,'public');
let server = http.createServer(async function(req,res){
    let {pathname} = url.parse(req.url); // index.html 2.jpg 1.jpg
    let refer = req.headers['referer'] || req.headers['referred'];
    try{
        let rp = path.join(p,pathname); // 真实的路径
        let s = await stat(rp); // 文件存在就读取相应给客户端
        if(refer){
            // 若是有refer要判断是否和法若是 不合法返回一张裂图
            // 如今再哪里用这张图 www.zdl2.cn
            let hostname = url.parse(refer).hostname;
            // 表明当前文件的主机名 www.zdl1.cn
            let host = req.headers['host'].split(':')[0];
            if(host != hostname ){
                if (whiteList.includes(hostname)){
                return fs.createReadStream(path.join(p, '2.jpg')).pipe(res);
                }
                fs.createReadStream(path.join(p,'1.jpg')).pipe(res);
            }else{
                fs.createReadStream(path.join(p, '2.jpg')).pipe(res);
            }
        }else{
            fs.createReadStream(rp).pipe(res);
        }
    }catch(e){
        res.end(`NOT Found`);
    }
})

server.listen(3000);

复制代码

用 http://localhost:8080访问index文件,和http://www.zdl1.cn:8080/访问是同样的,http://www.zdl2.cn:8080则返回裂图

域名须要自行配置host文件

多语言

多语言也是用的头

  • 请求头格式 Accept-Language: Accept-Language:zh-CN,zh;q=0.9(l浏览器默认发送)
  • 响应头格式 Content-Language:zh-CN

language.js

// 多语言 
let pack = {
    'zh-CN':'你好',
    'zh':'nihao',
    'en':'hello',
    'fr':'Bonjour'
}
let defaultLanguage = 'en'; // 默认是英语
let http = require('http');
http.createServer(function (req,res) {
    let lang = req.headers["accept-language"];
    if(lang){ // 若是有多语言
        let langs = lang.split(',');//拆分语言,每种语言逗号分割
        // [{name:'zh-CN',q:1},{name:'en',q:0.8}]
        langs = langs.map(l=>{
            let [name,q] = l.split(';');
            q = q?Number(q.split('=')[1]):1;
            return {name,q}
        }).sort((lan1,lan2)=>lan2.q-lan1.q);
        for(var i = 0;i<langs.length;i++){ // 循环每一种 语言看看包里有没有,若是有返回对应的语言
            if(pack[langs[i].name]){
                res.setHeader('Content-Language', langs[i].name);
                res.end(pack[langs[i].name]);
                return;
            }
        }
        // 没有默认语言
        res.setHeader('Content-Language', 'en')
        res.end(pack[defaultLanguage]);// 默认语言;
    }else{
        res.setHeader('Content-Language', 'en')
        res.end(pack[defaultLanguage]);// 默认语言;
    }
}).listen(3000);
//accept-language: zh-CN,zh;q=0.7,en;q=0.8,fr;q=0.1
复制代码

测试:curl -v --header "Accept-Language:zh;n;q=0.8,fr;q=1" http://localhost:3000

这个包后台通常是咱们本身写的,前端是有包好比i8n

相关文章
相关标签/搜索