基础的东西总是很少人看,写起来也特别痛苦。不是因为它简单——恰恰是因为它太晦涩,太基础了——以至于没有人乐意用它来充实自己(装逼)。
本文涉及一下内容:
先补白吧。
所谓网络编程,指的是应用层和传输层。
层级 | 内容 |
---|---|
应用层 | <应用层>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。
状态码 | 描述 |
---|---|
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 | 查询用户 |
执行以下命令行:
curl -v http://www.baidu.com
即可打印出请求到的页面代码。
首部反映的是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服务器的缓存信息 |
写一个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如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">
</div>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script><script src=""></script>
<script>
(async ()=>{
const res=await axios.get('api/users');
console.log(JSON.stringify(res.data))
document.querySelector('#root').innerHTML=`Response:${JSON.stringify(res.data)}`
})()
</script>
</body>
</html>
那么一个带请求的页面就实现了。
最简单的请求,无需使用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就触发了协议相同,端口不同的跨域错误。
此时network是200,但仍然被浏览器阻拦。
出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。 浏览器限制跨域请求一般有两种方式:
怎么解决呢?
可以在后端设置请求例外(在这里是http://localhost:3000):
res.setHeader('Access-Control-Allow-Origin','http://localhost:3000');
res.setHeader('Contnet-Type','application/json');
res.end(JSON.stringify([{
name:'djtao'
}]));
作为前端,我想在header中带上token:
const res=await axios.get('/api/users',{
headers:{'X-Token':'token'}
});
那么请求又没有结果了。(请求无应答)
如果打印出来。会发现req.method是OPTIONS.
一般的跨域都是浏览器拦截,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。
为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的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();
}
请求头也带上了:
// 预检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就打印出来了。
使用代理中间件: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端口,就完事了!
现在研究下post,改写index.html,注释掉ajax请求:
<form action="/api/save" method="post">
<input type="text" name="name">
<input type="submit" value="save">
</form>
然后配置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())
})
}
执行结果为:
把很多个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就可以拿到请求。
主要指的是上传文件。
改写上传文件的逻辑:
<input type="file" name="file" id="upload">
<script>
const file = document.querySelector('#upload');
file.addEventListener('change', function (e) {
let _files = this.files;
const formData=new FormData();
if (!_files.length) {
return;
}
if (_files.length == 1) {
formData.append('file',_files[0])
// 只考虑选择单个文件
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/api/upload');
xhr.send(formData);
}else{
}
})
</script>
后端处理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')
})