HTTP协议
// 观察HTTP协议
curl -v http://www.baidu.com
- HTTP协议详解
- 创建接⼝,api.js
// http/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("Content-Type", "text/html");
res.end(data);
});
} else if (method == "GET" && url == "/api/users") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify([{ name: "tom", age: 20 }]));
}
})
.listen(3000);
- 请求接⼝
// index.html
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
(async () => {
const res = await axios.get("/api/users")
console.log('data', res.data)
document.writeln(`Response : ${JSON.stringify(res.data)}`)
})()
</script>
- 埋点更容易
const img = new Image()
img.src='/api/users?abc=123'
协议 端⼝ host
- 跨域:浏览器同源策略引起的接⼝调⽤问题
// proxy.js
const express = require('express')
const app = express()
app.use(express.static(__dirname + '/'))
module.exports = app
// index.js
const api = require('./api')
const proxy = require('./proxy')
api.listen(4000)
proxy.listen(3000)
或者通过baseURL⽅式
axios.defaults.baseURL = 'http://localhost:4000'
-
浏览器抛出跨域错误
- 常⽤解决⽅案:
- JSONP(JSON with Padding),前端+后端⽅案,绕过跨域
前端构造script标签请求指定URL(由script标签发出的GET请求不受同源策略限制),服务器返回⼀个函数执⾏语句,该函数名称通常由查询参callback的值决定,函数的参数为服务器返回的json数据。该函数在前端执⾏后即可获取数据。
- 代理服务器
请求同源服务器,通过该服务器转发请求⾄⽬标服务器,得到结果再转发给前端。
前端开发中测试服务器的代理功能就是采⽤的该解决⽅案,但是最终发布上线时如果web应⽤和
接⼝服务器不在⼀起仍会跨域。
- CORS(Cross Origin Resource Share) - 跨域资源共享,后端⽅案,解决跨域
预检请求
原理:cors是w3c规范,真正意义上解决跨域问题。它需要服务器对请求进⾏检查并对响应头做相应处理,从⽽允许跨域请求。
具体实现:
- 响应简单请求: 动词为get/post/head,没有⾃定义请求头,Content-Type是application/x-wwwform-urlencoded,multipart/form-data或text/plain之⼀,通过添加以下响应头解决:
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
该案例中可以通过添加⾃定义的x-token请求头使请求变为preflight请求
// index.html
axios.defaults.baseURL = 'http://localhost:3000';
axios.get("/users", {headers:{'X-Token':'jilei'}})
- 响应preflight请求,需要响应浏览器发出的options请求(预检请求),并根据情况设置响应头:
else if (method == "OPTIONS" && url == "/api/users") {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:3000",
"Access-Control-Allow-Headers": "X-Token,Content-Type",
"Access-Control-Allow-Methods": "PUT"
});
res.end();
}
则服务器需要允许x-token,若请求为post,还传递了参数:
// index.html
axios.post("http://localhost:3000/users", {foo:'bar'}, {headers:{'XToken':'jilei'}})
// http-server.js
else if ((method == "GET" || method == "POST") && url == "/users") {}
则服务器还需要允许content-type请求头
- 如果要携带cookie信息,则请求变为credential请求:
// index.js
// 预检options中和/users接⼝中均需添加
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 设置cookie
res.setHeader('Set-Cookie', 'cookie1=va222;')
// index.html
// 观察cookie存在
console.log('cookie',req.headers.cookie)
// ajax服务
axios.defaults.withCredentials = true
Proxy代理模式
var 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
对⽐⼀下nginx 与webpack devserver
// vue.config.js
module.exports = {
devServer: {
disableHostCheck: true,
compress: true,
port: 5000,
proxy: {
'/api/': {
target: 'http://localhost:4000',
changeOrigin: true,
},
},
},
}
nginx
server {
listen 80;
# server_name www.josephxia.com;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri / /index.html;
}
location / api {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X - Real - IP $remote_addr;
proxy_set_header X - Forwarded - For $proxy_add_x_forwarded_for;
}
}
Bodyparser
- application/x-www-form-urlencoded
<form action="/api/save" method="post">
<input type="text" name="abc" value="123">
<input type="submit" value="save">
</form>
// api.js
else if (method === "POST" && url === "/api/save") {
let reqData = [];
let size = 0;
req.on('data', data => {
console.log('>>>req on', data);
reqData.push(data);
size += data.length;
});
req.on('end', function () {
console.log('end')
const data = Buffer.concat(reqData, size);
console.log('data:', size, data.toString())
res.end(`formdata:${data.toString()}`)
});
}
- application/json
await axios.post("/api/save", {
a: 1,
b: 2
})
// 模拟application/x-www-form-urlencoded
await axios.post("/api/save", 'a=1&b=3', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
上传⽂件
// Stream pipe
request.pipe(fis)
response.end()
// Buffer connect
request.on('data',data => {
chunk.push(data)
size += data.length
console.log('data:',data ,size)
})
request.on('end',() => {
console.log('end...')
const buffer = Buffer.concat(chunk,size)
size = 0
fs.writeFileSync(outputFile,buffer)
response.end()
})
// 流事件写⼊
request.on('data', data => {
console.log('data:',data)
fis.write(data)
})
request.on('end', () => {
fis.end()
response.end()
})
实战⼀个爬⾍
原理:服务端模拟客户端发送请求到⽬标服务器获取⻚⾯内容并解析,获取其中关注部分的数据。
// spider.js
const originRequest = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");
function request(url, callback) {
const options = {
url: url,
encoding: null
};
originRequest(url, options, callback);
}
for (let i = 100553; i < 100563; i++) {
const url = `https://www.dy2018.com/i/${i}.html`;
request(url, function (err, res, body) {
const html = iconv.decode(body, "gb2312");
const $ = cheerio.load(html);
console.log($(".title_all h1").text());
});
}
实现⼀个即时通讯IM
- 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 localhost 9000
- Http实现
原理:客户端通过ajax⽅式发送数据给http服务器,服务器缓存消息,其他客户端通过轮询⽅式查询最新数据并更新列表。
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<input v-model="message">
<button v-on:click="send">发送</button>
<button v-on:click="clear">清空</button>
<div v-for="item in list">{{item}}</div>
</div>
<script>
const host = 'http://localhost:3000'
var app = new Vue({
el: '#app',
data: {
list: [],
message: 'Hello Vue!'
},
methods: {
send: async function () {
let res = await axios.post(host + '/send', {
message: this.message
})
this.list = res.data
},
clear: async function () {
let res = await axios.post(host + '/clear')
this.list = res.data
}
},
mounted: function () {
setInterval(async () => {
const res = await axios.get(host + '/list')
this.list = res.data
}, 1000);
}
});
</script>
</body>
</html>
const express = require('express')
const app = express()
const bodyParser = require('body-parser');
const path = require('path')
app.use(bodyParser.json());
const list = ['ccc', 'ddd']
app.get('/', (req, res) => {
res.sendFile(path.resolve('./index.html'))
})
app.get('/list', (req, res) => {
res.end(JSON.stringify(list))
})
app.post('/send', (req, res) => {
list.push(req.body.message)
res.end(JSON.stringify(list))
})
app.post('/clear', (req, res) => {
list.length = 0
res.end(JSON.stringify(list))
})
app.listen(3000);
- Socket.IO实现
- 安装: npm install --save socket.io
- 两部分:nodejs模块,客户端js
// 服务端: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');
//响应某⽤户发送消息
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 () {
console.log('user disconnected');
});
});
http.listen(3000, function () {
console.log('listening on *:3000');
});
// 客户端:index.html
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font: 13px Helvetica, Arial;
}
form {
background: #000;
padding: 3px;
position: fixed;
bottom: 0;
width: 100%;
}
form input {
border: 0;
padding: 10px;
width: 90%;
margin-right: 0.5%;
}
form button {
width: 9%;
background: rgb(130, 224, 255);
border: none;
padding: 10px;
}
#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
#messages li {
padding: 5px 10px;
}
#messages li:nth-child(odd) {
background: #eee;
}
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js">
</script>
<script src="http://libs.baidu.com/jquery/2.1.1/jquery.min.js"></script>
<script>
$(function () {
var socket = io();
$("form").submit(function (e) {
e.preventDefault(); // 避免表单提交⾏为
socket.emit("chat message", $("#m").val());
$("#m").val("");
return false;
});
socket.on("chat message", function (msg) {
$("#messages").append($("<li>").text(msg));
});
});
</script>
</body>
</html>
Socket.IO库特点:
源于HTML5标准
⽀持优雅降级
WebSocket
WebSocket over FLash
XHR Polling
XHR Multipart Streaming
Forever Iframe
JSONP Polling
Https
- 创建证书
# 创建私钥
openssl genrsa -out privatekey.pem 1024
# 创建证书签名请求
openssl req -new -key privatekey.pem -out certrequest.csr
# 获取证书,线上证书需要经过证书授证中⼼签名的⽂件;下⾯只创建⼀个学习使⽤证书
openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out
certificate.pem
# 创建pfx⽂件
openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out
certificate.pfx
Http2
- 多路复⽤ - 雪碧图、多域名CDN、接⼝合并
官⽅演示 - https://http2.akamai.com/demo - 多路复⽤允许同时通过单⼀的 HTTP/2 连接发起多重的请求-响应消息;⽽HTTP/1.1协议中,
浏览器客户端在同⼀时间,针对同⼀域名下的请求有⼀定数量限制。超过限制数⽬的请求会
被阻塞** - ⾸部压缩
http/1.x 的 header 由于 cookie 和 user agent很容易膨胀,⽽且每次都要重复发送。
http/2使⽤ encoder 来减少需要传输的 header ⼤⼩,通讯双⽅各⾃ cache⼀份
header fields 表,既避免了重复 header 的传输,⼜减⼩了需要传输的⼤⼩。⾼效的压
缩算法可以很⼤的压缩 header,减少发送包的数量从⽽降低延迟 - 服务端推送
在 HTTP/2 中,服务器可以对客户端的⼀个请求发送多个响应。举个例⼦,如果⼀个请
求请求的是index.html,服务器很可能会同时响应index.html、logo.jpg 以及 css 和 js
⽂件,因为它知道客户端会⽤到这些东⻄。这相当于在⼀个 HTML ⽂档内集合了所有的
资源
资料来自:开课吧