http网络编程

http协议基础

所谓网络编程,指的是应用层和传输层。































层级 内容
应用层 <应用层>
TELNET,SSH,HTTP,SMTP,POP,SSL/LTS,FTP,MINME,HTML,SNMP,MIB,SIP,RTP
表示层
会话
传输 <传输层>
TCP,UDP,UDP-lite,SCTP,DCCP
网络层 <网络层>
ipv4,ipv6,ARP,ICMP,IPsec
数据链路层 以太网,wlan ,ppp
物理层

http全称超文本传输协议(HyperText Transfer Protocol),是当今互联网使用最为广泛的传输协议。

当前主流的版本仍然是http1.1。

常用http状态码

状态码 描述
100 继续相应剩余部分
200 成功处理请求
301 资源永久移动
302 资源临时移动
304 未修改,响应中不包含资源内容
401 未授权,要求身份验证
403 禁止,请求被拒绝
404 资源不存在
500 服务器内部错误
503 服务不可用

常用的请求方法

koa中推荐用户使用REST规范,比如下面四种请求对应了增删改查:

方法 接口地址 描述
post http://api.test.com/users 增加用户
delete http://api.test.com/users/:id 删除用户
put http://api.test.com/users/:id 修改用户
get http://api.test.com/users/:id 查询用户

http首部字段

执行以下命令行:

  curl -v http://www.baidu.com

[图片上传失败...(image-301af8-1562308370476)]

即可打印出请求到的页面代码。

[图片上传失败...(image-4800c3-1562308370476)]

首部反映的是http传输过程中的重要信息:

字段名 描述
User-Agent http客户端的信息
Last-Midified 资源最后修改日期
Contnet-Length 实体主体大小,单位为字节
Contnet-Encoding 实体主体适用的编码方式
Content-Type 实体主体的媒体类型,如img/png,application/x-javascript,text/html
Expires 实体主体的过期时间
Set-Cookie 开始状态管理所使用的cookie信息。
Cookie 服务器接收到的cookie
Cache-Control 控制缓存的行为:如public/private/no-cache
ETag 资源匹配信息
Vary 代理服务器的缓存信息
Server http服务器的缓存信息

http实践

接口请求

星际争霸就是用这个实现的。

写一个api服务器,规定路由和接口:

// api.js
const http=require('http');
const fs=require('fs');

http.createServer((req,res)=>{
    const {method,url}=req;
    if(method=='GET'&&url=='/'){
        fs.readFile('./index.html',(err,data)=>{
            res.setHeader('Contnent-Type','text/html');
            res.end(data);
        })
    }else if(method=='GET'&&url=='/api/users'){
        res.setHeader('Contnet-Type','application/json');
        res.end(JSON.stringify([{
            name:'djtao'
        }]));

    }
}).listen(3000)

对应的静态html如下:




    
    
    
    Document


    

那么一个带请求的页面就实现了。

埋点

最简单的请求,无需使用axios库:

var img=new Image();
img.src='/api?name=123';

这种请求通常用于百度统计。

跨域

浏览器的同源策略:以下三项有任意一个不等,就会引起跨域问题。

  • 协议
  • 端口
  • 域名

现在制造一种不同源的情况,考虑搭建两台服务器:

修改api.js

const http=require('http');
const fs=require('fs');
const app= http.createServer((req,res)=>{
    const {method,url}=req;
    console.log(method,url)
    if(method=='GET'&&url=='/'){
        fs.readFile('./index.html',(err,data)=>{
            res.setHeader('Contnent-Type','text/html');
            res.end(data);
        })
    }else if(method=='GET'&&url=='/api/users'){
        res.setHeader('Contnet-Type','application/json');
        res.end(JSON.stringify([{
            name:'djtao'
        }]));

    }
});
module.exports=app;

新建一个proxy.js,为方便处理,此处调用了express:

const express=require('express');
const app=express();

app.use(express.static(__dirname+'/'));
module.exports=app;

Html内的信息改为:

(async ()=>{
                    axios.defaults.baseURL='http://localhost:4000';
            const res=await axios.get('/api/users');
            console.log(JSON.stringify(res.data))
            document.querySelector('#root').innerHTML=`Response:${JSON.stringify(res.data)}`
        })()

这时访问localhost:4000就触发了协议相同,端口不同的跨域错误。

[图片上传失败...(image-730a08-1562308370476)]

此时network是200,但仍然被浏览器阻拦。

出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。
浏览器限制跨域请求一般有两种方式:

  1. 浏览器限制发起跨域请求
  2. 跨域请求可以正常发起,但是返回的结果被浏览器拦截了

怎么解决呢?

后端设置报头

可以在后端设置请求例外(在这里是http://localhost:3000):

res.setHeader('Access-Control-Allow-Origin','http://localhost:3000');
        res.setHeader('Contnet-Type','application/json');
        res.end(JSON.stringify([{
            name:'djtao'
        }]));
预检请求(preflight)

作为前端,我想在header中带上token:

const res=await axios.get('/api/users',{
                headers:{'X-Token':'token'}
            });

那么请求又没有结果了。(请求无应答)

[图片上传失败...(image-fdf943-1562308370476)]

如果打印出来。会发现req.method是OPTION.

一般的跨域都是浏览器拦截,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。

为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。

哪些情况需要预检

首先需要明确,简单请求 不会触发CORS预检请求,“简属于单请求”术语并不属于Fetch(其中定义了CORS)规范。若满足所有下述条件,则该请求可视为“简单请求”:get,head,post。

使用了下面任一 HTTP 方法,都会触发预检:

  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH

或者人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。

我们自定义了一个X-Token,触发了预检请求,所以需要特殊判断:

else if(method=='OPTION'&&url=='/api/users'){
  res.writeHead(200,{
    'Access-Control-Allow-Origin':'http://localhost:3000',
    'Access-Control-Allow-Headers':'X-Token,Contnet-Type',
    'Access-Control-Allow-Methods':'PUT'
  });
  res.end();
}

请求头也带上了:

[图片上传失败...(image-163d09-1562308370476)]

鉴权(携带cookie信息)
// 预检options中和/users接口中均需添加 
res.setHeader('Access-Control-Allow-Credentials', 'true'); 
// get请求中设置cookie
res.setHeader('Set-Cookie', 'cookie1=va222;')
// 观察cookie存在 
console.log('cookie',req.headers.cookie) 
// ajax服务
axios.defaults.withCredentials = true

第二次请求中cookie就打印出来了。

Proxy代理模式

使用代理中间件:http-proxy-middleware

简单说就是把4000的端口反向代理到3000:

// proxy.js
const express=require('express');
const proxy=require('http-proxy-middleware');

const app=express();
app.use(express.static(__dirname+'/'));
app.use('/api',proxy({
  target:'http://localhost:4000',
  changeOrigin:false
}))
module.exports=app;

把axios的请求改为3000端口,就完事了!

bodyparser

现在研究下post,改写index.html,注释掉ajax请求:

然后配置api.js:

else if(url=='/api/save'&&method=='POST'){
        let reqData=[];
        let size=0;

        req.on('data',data=>{
            console.log('>>>req on ',data);
            // 接收buffer
            reqData.push(data);
            size+=data.length
        });

        req.on('end',()=>{
            console.log('end');
            const data=Buffer.concat(reqData,size);
            console.log('data',size,data.toString());
            res.end('formdata:'+data.toString())
        })
    }

执行结果为:

[图片上传失败...(image-804818-1562308370476)]

[图片上传失败...(image-d25684-1562308370476)]

吧很多歌buffer字节连接起来。这样就拿到了post请求数据。

这实在是太麻烦了。还记得bodyparser吗?装一个epress版的呗

新建api2.js

const http = require('http');
const fs = require('fs');
const bodyParser = require('body-parser');
const express=require('express');

const app=express();

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())

app.post('/api/save',(req,res)=>{
    console.log(req.body.name);
    res.end(req.body.name)
})


module.exports = app;

好了,req.body就可以拿到请求。

上传文件

主要指的是上传文件。

改写上传文件的逻辑:



后端处理bodyparser只能处理简单的请求,如果上传文件,需要装multer

var path=require('path')
var multer  = require('multer')
var upload = multer()

app.post('/api/upload',upload.single('file'),async (req,res)=>{
    let file=req.file
    console.log(file)
    // 以原文件名写入!
    await fs.writeFile(path.resolve(__dirname,file.originalname), file.buffer, err => {    
        console.log('写入成功');
    })
    res.end('1')
})

应用

爬虫爬取豆瓣电影top250

爬虫是一个很有魅力的话题,它意味着不用什么努力就拿到了别人辛苦付出的的劳动成果。

其原理就是:服务端请求数据,然后爬取页面内容。常用的请求库是request,常用的爬虫工具是cheerio——它可以像jq一样爬取你想要的东西。

npm i cheerio request iconv-lite -S

很多大网站都会反爬虫。但是豆瓣top250的还不错。看完这些电影,应该就不是250了。

下面就将实现这个功能。

访问https://movie.douban.com/top250?start=0&filter=

页面的编码是utf-8.

点击分页器,可知连接请求随着start字段变化。start为0时,请求的是top1-25的电影。

电影内容是所有class=title(每页25个)。cheerio拿到的类选择器可以直接做循环。

当我收集了全部数据后,可存放到本地的data.json中。

// spider.js
const fs = require('fs');
const path = require('path')
const originRequest = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");

// 封装request
function request(url, callback) {
    const options = {
        url: url,
        encoding: null
    };
    originRequest(url, options, callback);
}

//data作为参数加入到迭代中。
const getData = function (i, data) {
    data = data ? data : [];
    const url = `https://movie.douban.com/top250?start=${i * 25}`;
    request(url, function (err, res, body) {
        const html = iconv.decode(body, "utf-8");
        const $ = cheerio.load(body);
        let dataItem = []
        for (let j = 0; j < $('.item .title:first-of-type').length; j++) {
            dataItem.push({
                title: $('.item .title:first-of-type')[j].children[0].data,
                rank: i * 25 + j + 1
            })
        }
        // 衔接数组
        data.push(...dataItem);
        if (i < 11) {
            return getData(i + 1, data)
        } else {
            console.log('获取数据完成')
            fs.writeFile('data.json', JSON.stringify(data), (err) => {
                if (!err) {
                    console.log('写入成功')
                } else {
                    console.log('写入失败')
                }
            })
        }
    });
}

getData(0)

这里考虑到异步循环,所以使用递归来实现。

执行完毕后,内容就存放到data,json中了。

格式化之后:

[图片上传失败...(image-c68905-1562308370476)]

tcp/ip协议:socket实现——一个即时终端聊天室

net模块提供一个异步api能够创建基于流的tcp服务器,客户端和服务端建立连接之后,服务器可以获得一个双工socket对象,服务器可以保存socket对象列表,在接受某客户端消息时,推送给其他客户端。

// socket.js
const net=require('net');
const chatServer=net.createServer();
const clientList=[];

chatServer.on('connection',client=>{
    client.write('Hi\n');
    clientList.push(client);
    // 当接收到数据时:
    client.on('data',data=>{
        console.log(`receive:${data.toString()}`);
        clientList.forEach(v=>{
            v.write(data);
        })
    })
});
chatServer.listen(9000);

如何测试呢?这里用到telnet:

telnet本来是mac os 10.13之前的内置服务,在高级版本中,需要本地安装一下:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install telnet

安装完成之后:

telnet localhost 9000

打印出'hi'

[图片上传失败...(image-44c73a-1562308370476)]

假设再建立一个客户端:同样也能收到消息。

网络聊天室

http的一个重要弱点在于,只能拉取,不能主动推送。所以后端扯皮时会说:'"你不穿东西给我,我就没东西给你。"这个时候只好做轮询(苦了前端)。

从项目角度说,HTTP协议是非持久化的,单向的网络协议,在建立连接后只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。当需要即时通讯时,通过轮询在特定的时间间隔(如1秒),由浏览器向服务器发送Request请求,然后将最新的数据返回给浏览器。这样的方法最明显的缺点就是需要不断的发送请求,而且通常HTTP request的Header是非常长的,为了传输一个很小的数据 需要付出巨大的代价,是很不合算的,占用了很多的宽带

但如果有了socket.io,事情就好办多了。

Socket.io是一个WebSocket库,包括了客户端的js和服务器端的nodejs,它的目标是构建可以在不同浏览器和移动设备上使用的实时应用。它会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5

npm i socket.io -S

在后端基本上不需要做什么处理:

// 服务端:chat-socketio.js
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
    console.log('a user connected');
    io.emit('chat message', {
        type:'notice',
        time:new Date().toLocaleString(),
        name:'新用户',
        msg:'加入了群聊'
    });


    //响应某用户发送消息
    socket.on('chat message', function (msg) {
        console.log('chat message:' + msg);
        // 广播给所有人
        io.emit('chat message', msg);
        // 广播给除了发送者外所有人
        // socket.broadcast.emit('chat message', msg)
    });
    socket.on('disconnect', function () {
        io.emit('chat message', {
            msg:'用户退出了群聊',
            type:'notice',
            name:'有用户',
            time:new Date().toLocaleString(),
        });
        console.log('user disconnected');
    });
});


http.listen(3000, function () {
    console.log('listening on *:3000');
});

前端:




    
    
    
    Document
    



    

[图片上传失败...(image-1c97b7-1562308370476)]

那么就实现了。

你可能感兴趣的:(http网络编程)