要說http就繞不開tcp,TCP協議對應於傳輸層,而HTTP協議對應於應用層,從本質上來講,兩者沒有可比性。可是,http是基於tcp協議的。javascript
物理層 | 鏈路層 |
---|---|
將二進制的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協議是創建在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 服務器上沒有找到請求的資源 |
寫一個靜態服務(命令行工具)web
客戶端–發送帶有SYN標誌的數據包–一次握手–服務端 服務端–發送帶有SYN/ACK標誌的數據包–二次握手–客戶端 客戶端–發送帶有帶有ACK標誌的數據包–三次握手–服務端ajax
客戶端-發送一個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 統一資源定位符 location ,統一資源命名符
> http://(協議)name:password(登陸信息,認證)@www.fs.ip(服務器地址):8080(端口號)/dir/index.htm(文件路徑)?a=a(查詢字符串)#asd(片斷標識符)
複製代碼
咱們訪問一個路徑,路徑回去dns域上找對應的ip地址,中間包括查找瀏覽器緩存,本地文件等等,最後將ip地址返回,http主要針對應用層,應用層會有一些報文,主要經過tcp傳輸,http基於tcp,TCP會將HTTP拆分紅不少段,把每一個報文可靠的傳給對方,udp是不可靠的會丟包,拼完以後返回服務器
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差的是報文,是一種不保存但能夠保持狀態的協議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
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)
複製代碼
//命令行輸入測試
//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");
})
複製代碼
//測試命令行輸入
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來實現的,包括緩存,
客戶端 請求頭 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文件
多語言也是用的頭
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