目錄
一.應(yīng)用層
1.協(xié)議
2.網(wǎng)絡(luò)版計(jì)算器
3.HTTP協(xié)議
(1)了解url和http
(2)http的用處
(3)urlencode和urldecode
(4)http協(xié)議格式
4.HTTPS協(xié)議
?(1)加密
(2)為什么要加密
(3)常見的加密方式
(4)數(shù)據(jù)摘要(數(shù)據(jù)指紋)
(5)加密方案
(6)證書
(7)數(shù)字簽名(數(shù)據(jù)簽名)
(8)常見問題
(9)總結(jié)
二.傳輸層
1.端口號(hào)
?(1)端口號(hào)范圍劃分
(2)知名端口號(hào)
(3)兩個(gè)問題
(4)兩個(gè)指令
2.UDP協(xié)議
(1)UDP協(xié)議格式
(2)UDP的特點(diǎn)
(3)面向數(shù)據(jù)報(bào)
(4)UDP的緩沖區(qū)
(5)注意事項(xiàng)
(6)基于UDP的應(yīng)用層協(xié)議
3.TCP協(xié)議
(1)TCP協(xié)議端格式
(2)確認(rèn)認(rèn)答(ACK)機(jī)制
(3)超時(shí)重傳機(jī)制
(4)連接管理機(jī)制(3握4揮)
(5)滑動(dòng)窗口
(6)流量控制?
(7)擁塞控制
(8)延遲應(yīng)答
?(9)捎帶應(yīng)答
(10)面向字節(jié)流
(11)粘包問題
(12)TCP異常情況
(13)TCP小結(jié)
(14)基于TCP的應(yīng)用層協(xié)議
(15)TCP,UDP對(duì)比
(16)如何用UDP實(shí)現(xiàn)可靠傳輸
前言:這一篇主要介紹應(yīng)用層的http,以及傳輸層的UDP、TCP,可以說這部分是網(wǎng)絡(luò)中最重要的一部分了。
一.應(yīng)用層
????????程序員寫的一個(gè)個(gè)解決我們實(shí)際問題, 滿足我們?nèi)粘P枨蟮木W(wǎng)絡(luò)程序, 都在應(yīng)用層
1.協(xié)議
為了滿足不同的應(yīng)用場(chǎng)景,已經(jīng)有了應(yīng)用層協(xié)議:http, https, DNS, ftp, smtp等。
① 直接發(fā)送同樣的結(jié)構(gòu)體對(duì)象,是不可取的,雖然在某些情況下,它確實(shí)行(需要進(jìn)行序列化和反序列化)
② 我們需要定制協(xié)議的時(shí)候,序列化之后,我們可以將長(zhǎng)度設(shè)置為4字節(jié),將長(zhǎng)度放入序列化之后的字符串的開始
struct
{
? ? ? ? int x;? ? ? ? int y;
? ? ? ? char op;
};
約定:一共三個(gè)區(qū)域,用 : 分割
? ? ? ? 前兩個(gè)是int,后一個(gè)是char
data d = {10, 20, '+'}
序列化,轉(zhuǎn)化為字符串 10:20:+
收到"字符串",反序列化,轉(zhuǎn)換為原結(jié)構(gòu)體data d = {x, y, op}
序列化和反序列化就相當(dāng)于在用戶發(fā)送之前加了一層軟件層。
2.網(wǎng)絡(luò)版計(jì)算器
????????下面通過利用序列化和反序列化,實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)版計(jì)算器:
????????這里面同時(shí)實(shí)現(xiàn)了通過自己實(shí)現(xiàn)的序列化和反序列化版本,也實(shí)現(xiàn)了使用json來實(shí)現(xiàn)的序列化和反序列化版本
json是一個(gè)獨(dú)立的第三方庫(kù),因此要使用json要先安裝對(duì)應(yīng)的庫(kù):
sudo yum install -y jsoncpp.devel
Makefile:
.PHONY:all
all:clientTcp serverTcp
Method=#-DMY_SELF
clientTcp:clientTcp.cc
g++ -o $@ $^ $(Method) -std=c++11 -lpthread -ljsoncpp
serverTcp:serverTcp.cc
g++ -o $@ $^ $(Method) -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -f clientTcp serverTcp
????????這里面因?yàn)橐褂胘son來實(shí)現(xiàn)序列化和反序列化,所以要鏈接上對(duì)應(yīng)的庫(kù) -ljson.cpp
? ? ? ? 而Method=#-DMY_SELF就是一種在Makefile中的#define
? ? ? ? 因?yàn)槲覀儽A袅俗约簩?shí)現(xiàn)的序列化和反序列化版本,并采用條件編譯的方式根據(jù)傳入的不同去調(diào)用不同的版本,那么既可以在代碼內(nèi)部去修改,也可以像這樣直接在Makefile中修改。
? ? ? ??Method=#-DMY_SELF就是不采用MY_SELF,而Method=-DMY_SELF(去掉#)就是采用。
Protocol.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"
// 我們要在這里進(jìn)行我們自己的協(xié)議定制!
// 網(wǎng)絡(luò)版本的計(jì)算器
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"
// #define MY_SELF 1
// decode,整個(gè)序列化之后的字符串進(jìn)行提取長(zhǎng)度
// 1. 必須具有完整的長(zhǎng)度
// 2. 必須具有和len相符合的有效載荷
// 我們才返回有效載荷和len
// 否則,我們就是一個(gè)檢測(cè)函數(shù)!
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
assert(len);
// 1. 確認(rèn)是否是一個(gè)包含len的有效字符串
*len = 0;
std::size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)
// 2. 提取長(zhǎng)度
std::string inLen = in.substr(0, pos);
int intLen = atoi(inLen.c_str());
// 3. 確認(rèn)有效載荷也是符合要求的
int surplus = in.size() - 2 * CRLF_LEN - pos;
if (surplus < intLen)
return "";
// 4. 確認(rèn)有完整的報(bào)文結(jié)構(gòu)
std::string package = in.substr(pos + CRLF_LEN, intLen);
*len = intLen;
// 5. 將當(dāng)前報(bào)文完整的從in中全部移除掉
int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
in.erase(0, removeLen);
// 6. 正常返回
return package;
}
// encode, 整個(gè)序列化之后的字符串進(jìn)行添加長(zhǎng)度
std::string encode(const std::string &in, uint32_t len)
{
// "exitCode_ result_"
// "len\r\n""exitCode_ result_\r\n"
std::string encodein = std::to_string(len);
encodein += CRLF;
encodein += in;
encodein += CRLF;
return encodein;
}
// 定制的請(qǐng)求 x_ op y_
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化 -- 結(jié)構(gòu)化的數(shù)據(jù) -> 字符串
// 認(rèn)為結(jié)構(gòu)化字段中的內(nèi)容已經(jīng)被填充
void serialize(std::string *out)
{
#ifdef MY_SELF
std::string xstr = std::to_string(x_);
std::string ystr = std::to_string(y_);
// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
#else
//json
// 1. Value對(duì)象,萬能對(duì)象
// 2. json是基于KV
// 3. json有兩套操作方法
// 4. 序列化的時(shí)候,會(huì)將所有的數(shù)據(jù)內(nèi)容,轉(zhuǎn)換成為字符串
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化 -- 字符串 -> 結(jié)構(gòu)化的數(shù)據(jù)
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// 100 + 200
std::size_t spaceOne = in.find(SPACE);
if (std::string::npos == spaceOne)
return false;
std::size_t spaceTwo = in.rfind(SPACE);
if (std::string::npos == spaceTwo)
return false;
std::string dataOne = in.substr(0, spaceOne);
std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
if (oper.size() != 1)
return false;
// 轉(zhuǎn)成內(nèi)部成員
x_ = atoi(dataOne.c_str());
y_ = atoi(dataTwo.c_str());
op_ = oper[0];
return true;
#else
//json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "x_: " << x_ << std::endl;
std::cout << "op_: " << op_ << std::endl;
std::cout << "y_: " << y_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 需要計(jì)算的數(shù)據(jù)
int x_;
int y_;
// 需要進(jìn)行的計(jì)算種類
char op_; // + - * / %
};
// 定制的響應(yīng)
class Response
{
public:
Response() : exitCode_(0), result_(0)
{
}
~Response()
{
}
// 序列化 -- 不僅僅是在網(wǎng)絡(luò)中應(yīng)用,本地也是可以直接使用的!
void serialize(std::string *out)
{
#ifdef MY_SELF
// "exitCode_ result_"
std::string ec = std::to_string(exitCode_);
std::string res = std::to_string(result_);
*out = ec;
*out += SPACE;
*out += res;
#else
//json
Json::Value root;
root["exitcode"] = exitCode_;
root["result"] = result_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// "0 100"
std::size_t pos = in.find(SPACE);
if (std::string::npos == pos)
return false;
std::string codestr = in.substr(0, pos);
std::string reststr = in.substr(pos + SPACE_LEN);
// 將反序列化的結(jié)果寫入到內(nèi)部成員中,形成結(jié)構(gòu)化數(shù)據(jù)
exitCode_ = atoi(codestr.c_str());
result_ = atoi(reststr.c_str());
return true;
#else
//json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
exitCode_ = root["exitcode"].asInt();
result_ = root["result"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "exitCode_: " << exitCode_ << std::endl;
std::cout << "result_: " << result_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 退出狀態(tài),0標(biāo)識(shí)運(yùn)算結(jié)果合法,非0標(biāo)識(shí)運(yùn)行結(jié)果是非法的,!0是幾就表示是什么原因錯(cuò)了!
int exitCode_;
// 運(yùn)算結(jié)果
int result_;
};
bool makeReuquest(const std::string &str, Request *req)
{
// 123+1 1*1 1/1
char strtmp[BUFFER_SIZE];
snprintf(strtmp, sizeof strtmp, "%s", str.c_str());
char *left = strtok(strtmp, OPS);
if (!left)
return false;
char *right = strtok(nullptr, OPS);
if (!right)
return false;
char mid = str[strlen(left)];
req->x_ = atoi(left);
req->y_ = atoi(right);
req->op_ = mid;
return true;
}
serverTcp.cc:
#include "Protocol.hpp"
#include "util.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "daemonize.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
class ServerTcp; // 申明一下ServerTcp
// 大小寫轉(zhuǎn)化服務(wù)
// TCP && UDP: 支持全雙工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char inbuffer[BUFFER_SIZE];
while (true)
{
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我們認(rèn)為我們讀到的都是字符串
if (s > 0)
{
// read success
inbuffer[s] = '\0';
if (strcasecmp(inbuffer, "quit") == 0)
{
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
// 可以進(jìn)行大小寫轉(zhuǎn)化了
for (int i = 0; i < s; i++)
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
inbuffer[i] = toupper(inbuffer[i]);
}
logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
write(sock, inbuffer, strlen(inbuffer));
}
else if (s == 0)
{
// pipe: 讀端一直在讀,寫端不寫了,并且關(guān)閉了寫端,讀端會(huì)如何?s == 0,代表對(duì)端關(guān)閉
// s == 0: 代表對(duì)方關(guān)閉,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到這里,一定是client退出了,服務(wù)到此結(jié)束
close(sock); // 如果一個(gè)進(jìn)程對(duì)應(yīng)的文件fd,打開了沒有被歸還,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char command[BUFFER_SIZE];
while (true)
{
ssize_t s = read(sock, command, sizeof(command) - 1); // 我們認(rèn)為我們讀到的都是字符串
if (s > 0)
{
command[s] = '\0';
logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);
// 考慮安全
std::string safe = command;
if ((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
{
break;
}
// 我們是以r方式打開的文件,沒有寫入
// 所以我們無法通過dup的方式得到對(duì)應(yīng)的結(jié)果
FILE *fp = popen(command, "r");
if (fp == nullptr)
{
logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));
break;
}
char line[1024];
while (fgets(line, sizeof(line) - 1, fp) != nullptr)
{
write(sock, line, strlen(line));
}
// dup2(fd, 1);
// dup2(sock, fp->_fileno);
// fflush(fp);
pclose(fp);
logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);
}
else if (s == 0)
{
// pipe: 讀端一直在讀,寫端不寫了,并且關(guān)閉了寫端,讀端會(huì)如何?s == 0,代表對(duì)端關(guān)閉
// s == 0: 代表對(duì)方關(guān)閉,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到這里,一定是client退出了,服務(wù)到此結(jié)束
close(sock); // 如果一個(gè)進(jìn)程對(duì)應(yīng)的文件fd,打開了沒有被歸還,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
static Response calculator(const Request &req)
{
Response resp;
switch (req.op_)
{
case '+':
resp.result_ = req.x_ + req.y_;
break;
case '-':
resp.result_ = req.x_ - req.y_;
break;
case '*':
resp.result_ = req.x_ * req.y_;
break;
case '/':
{ // x_ / y_
if (req.y_ == 0) resp.exitCode_ = -1; // -1. 除0
else resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{ // x_ / y_
if (req.y_ == 0) resp.exitCode_ = -2; // -2. 模0
else resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.exitCode_ = -3; // -3: 非法操作符
break;
}
return resp;
}
// 1. 全部手寫 -- done
// 2. 部分采用別人的方案--序列化和反序列化的問題 -- xml,json,protobuf
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string inbuffer;
while (true)
{
Request req;
char buff[128];
ssize_t s = read(sock, buff, sizeof(buff) - 1);
if (s == 0)
{
logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort);
break;
}
else if (s < 0)
{
logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s",
clientIp.c_str(), clientPort, errno, strerror(errno));
break;
}
// read success
buff[s] = 0;
inbuffer += buff;
std::cout << "inbuffer: " << inbuffer << std::endl;
// 1. 檢查inbuffer是不是已經(jīng)具有了一個(gè)strPackage
uint32_t packageLen = 0;
std::string package = decode(inbuffer, &packageLen);
if (packageLen == 0) continue; // 無法提取一個(gè)完整的報(bào)文,繼續(xù)努力讀取吧
std::cout << "package: " << package << std::endl;
// 2. 已經(jīng)獲得一個(gè)完整的package
if (req.deserialize(package))
{
req.debug();
// 3. 處理邏輯, 輸入的是一個(gè)req,得到一個(gè)resp
Response resp = calculator(req); //resp是一個(gè)結(jié)構(gòu)化的數(shù)據(jù)
// 4. 對(duì)resp進(jìn)行序列化
std::string respPackage;
resp.serialize(&respPackage);
// 5. 對(duì)報(bào)文進(jìn)行encode --
respPackage = encode(respPackage, respPackage.size());
// 6. 簡(jiǎn)單進(jìn)行發(fā)送 -- 后續(xù)處理
write(sock, respPackage.c_str(), respPackage.size());
}
}
}
class ThreadData
{
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
ServerTcp *this_;
public:
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts)
{
}
};
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1),
tp_(nullptr)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 創(chuàng)建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "socket: %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);
// 2. bind綁定
// 2.1 填充服務(wù)器信息
struct sockaddr_in local; // 用戶棧
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,寫入sock_對(duì)應(yīng)的內(nèi)核區(qū)域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind: %s", strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);
// 3. 監(jiān)聽socket,為何要監(jiān)聽呢?tcp是面向連接的!
if (listen(listenSock_, 5 /*后面再說*/) < 0)
{
logMessage(FATAL, "listen: %s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
// 運(yùn)行別人來連接你了
// 4. 加載線程池
tp_ = ThreadPool<Task>::getInstance();
}
// static void *threadRoutine(void *args)
// {
// pthread_detach(pthread_self()); //設(shè)置線程分離
// ThreadData *td = static_cast<ThreadData *>(args);
// td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
// delete td;
// return nullptr;
// }
void loop()
{
// signal(SIGCHLD, SIG_IGN); // only Linux
tp_->start();
logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 獲取連接, accept 的返回值是一個(gè)新的socket fd ??
// 4.1 listenSock_: 監(jiān)聽 && 獲取新的鏈接-> sock
// 4.2 serviceSock: 給用戶提供新的socket服務(wù)
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 獲取鏈接失敗
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 獲取客戶端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服務(wù), echo -> 小寫 -> 大寫
// 5.0 v0 版本 -- 單進(jìn)程 -- 一旦進(jìn)入transService,主執(zhí)行流,就無法進(jìn)行向后執(zhí)行,只能提供完畢服務(wù)之后才能進(jìn)行accept
// transService(serviceSock, peerIp, peerPort);
// 5.1 v1 版本 -- 多進(jìn)程版本 -- 父進(jìn)程打開的文件會(huì)被子進(jìn)程繼承嗎?會(huì)的
// pid_t id = fork();
// assert(id != -1);
// if(id == 0)
// {
// close(listenSock_); //建議
// //子進(jìn)程
// transService(serviceSock, peerIp, peerPort);
// exit(0); // 進(jìn)入僵尸
// }
// // 父進(jìn)程
// close(serviceSock); //這一步是一定要做的!
// 5.1 v1.1 版本 -- 多進(jìn)程版本 -- 也是可以的
// 爺爺進(jìn)程
// pid_t id = fork();
// if(id == 0)
// {
// // 爸爸進(jìn)程
// close(listenSock_);//建議
// // 又進(jìn)行了一次fork,讓 爸爸進(jìn)程
// if(fork() > 0) exit(0);
// // 孫子進(jìn)程 -- 就沒有爸爸 -- 孤兒進(jìn)程 -- 被系統(tǒng)領(lǐng)養(yǎng) -- 回收問題就交給了系統(tǒng)來回收
// transService(serviceSock, peerIp, peerPort);
// exit(0);
// }
// // 父進(jìn)程
// close(serviceSock); //這一步是一定要做的!
// // 爸爸進(jìn)程直接終止,立馬得到退出碼,釋放僵尸進(jìn)程狀態(tài)
// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
// assert(ret > 0);
// (void)ret;
// 5.2 v2 版本 -- 多線程
// 這里不需要進(jìn)行關(guān)閉文件描述符嗎??不需要啦
// 多線程是會(huì)共享文件描述符表的!
// ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// 5.3 v3 版本 --- 線程池版本
// 5.3.1 構(gòu)建任務(wù)
// 5.3 v3.1
// Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// tp_->push(t);
// 5.3 v3.2
// Task t(serviceSock, peerIp, peerPort, transService);
// tp_->push(t);
// 5.3 v3.3
// Task t(serviceSock, peerIp, peerPort, execCommand);
// tp_->push(t);
// 5.4 v3.3
Task t(serviceSock, peerIp, peerPort, netCal);
tp_->push(t);
// waitpid(); 默認(rèn)是阻塞等待!WNOHANG
// 方案1
// logMessage(DEBUG, "server 提供 service start ...");
// sleep(1);
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 引入線程池
ThreadPool<Task> *tp_;
// 安全退出
bool quit_;
};
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"
<< std::endl;
}
ServerTcp *svrp = nullptr;
void sigHandler(int signo)
{
if (signo == 3 && svrp != nullptr)
svrp->quitServer();
logMessage(DEBUG, "server quit save!");
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if (argc == 3)
ip = argv[2];
// daemonize(); // 我們的進(jìn)程就會(huì)成為守護(hù)進(jìn)程
signal(3, sigHandler);
// Log log;
// log.enable();
ServerTcp svr(port, ip);
svr.init();
svrp = &svr;
svr.loop();
return 0;
}
clientTcp.cc:
#include "util.hpp"
#include "Protocol.hpp"
#include <cstdio>
// 2. 需要bind嗎??需要,但是不需要自己顯示的bind! 不要自己bind!?。。?// 3. 需要listen嗎?不需要的!
// 4. 需要accept嗎?不需要的!
volatile bool quit = false;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1. 創(chuàng)建socket SOCK_STREAM
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 2. connect,發(fā)起鏈接請(qǐng)求,你想誰發(fā)起請(qǐng)求呢??當(dāng)然是向服務(wù)器發(fā)起請(qǐng)求嘍
// 2.1 先填充需要連接的遠(yuǎn)端主機(jī)的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 發(fā)起請(qǐng)求,connect 會(huì)自動(dòng)幫我們進(jìn)行bind!
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(CONN_ERR);
}
std::cout << "info : connect success: " << sock << std::endl;
std::string message;
while (!quit)
{
message.clear();
std::cout << "請(qǐng)輸入表達(dá)式>>> "; // 1 + 1
std::getline(std::cin, message); // 結(jié)尾不會(huì)有\(zhòng)n
if (strcasecmp(message.c_str(), "quit") == 0){
quit = true;
continue;
}
// message = trimStr(message); // 1+1 1 +1 1+ 1 1+ 1 1 +1 => 1+1 -- 不處理
Request req;
if(!makeReuquest(message, &req)) continue;
// req.debug();
std::string package;
req.serialize(&package); // done
std::cout << "debug->serialize-> " << package << std::endl;
package = encode(package, package.size()); // done
std::cout << "debug->encode-> \n" << package << std::endl;
ssize_t s = write(sock, package.c_str(), package.size());
if (s > 0)
{
char buff[1024];
size_t s = read(sock, buff, sizeof(buff)-1);
if(s > 0) buff[s] = 0;
std::string echoPackage = buff;
Response resp;
uint32_t len = 0;
// std::cout << "debug->get response->\n" << echoPackage << std::endl;
std::string tmp = decode(echoPackage, &len); // done
if(len > 0)
{
echoPackage = tmp;
// std::cout << "debug->decode-> " << echoPackage << std::endl;
resp.deserialize(echoPackage);
printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
}
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
Lock.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&lock_, nullptr);
}
void lock()
{
pthread_mutex_lock(&lock_);
}
void unlock()
{
pthread_mutex_unlock(&lock_);
}
~Mutex()
{
pthread_mutex_destroy(&lock_);
}
private:
pthread_mutex_t lock_;
};
class LockGuard
{
public:
LockGuard(Mutex *mutex) : mutex_(mutex)
{
mutex_->lock();
std::cout << "加鎖成功..." << std::endl;
}
~LockGuard()
{
mutex_->unlock();
std::cout << "解鎖成功...." << std::endl;
}
private:
Mutex *mutex_;
};
log.hpp:
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
#define LOGFILE "serverTcp.log"
class Log
{
public:
Log():logFd(-1)
{}
void enable()
{
umask(0);
logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
assert(logFd != -1);
dup2(logFd, 1);
dup2(logFd, 2);
}
~Log()
{
if(logFd != -1)
{
fsync(logFd);
close(logFd);
}
}
private:
int logFd;
};
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap); // ap = NULL
// 每次打開太麻煩
// umask(0);
// int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
// assert(fd >= 0);
FILE *out = (level == FATAL) ? stderr : stdout;
fprintf(out, "%s | %u | %s | %s\n",
log_level[level],
(unsigned int)time(nullptr),
name == nullptr ? "unknow" : name,
logInfo);
fflush(out); // 將C緩沖區(qū)中的數(shù)據(jù)刷新到OS
fsync(fileno(out)); // 將OS中的數(shù)據(jù)盡快刷盤
// close(fd);
// char *s = format;
// while(s){
// case '%':
// if(*(s+1) == 'd') int x = va_arg(ap, int);
// break;
// }
}
Task.hpp:
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"
class Task
{
public:
//等價(jià)于
// typedef std::function<void (int, std::string, uint16_t)> callback_t;
using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
int sock_; // 給用戶提供IO服務(wù)的sock
uint16_t port_; // client port
std::string ip_; // client ip
callback_t func_; // 回調(diào)方法
public:
Task():sock_(-1), port_(-1)
{}
Task(int sock, std::string ip, uint16_t port, callback_t func)
: sock_(sock), ip_(ip), port_(port), func_(func)
{}
void operator () ()
{
logMessage(DEBUG, "線程ID[%p]處理%s:%d的請(qǐng)求 開始啦...",\
pthread_self(), ip_.c_str(), port_);
func_(sock_, ip_, port_);
logMessage(DEBUG, "線程ID[%p]處理%s:%d的請(qǐng)求 結(jié)束啦...",\
pthread_self(), ip_.c_str(), port_);
}
~Task()
{}
};
ThreadPool.hpp:
#pragma once
#include <iostream>
#include <cassert>
#include <queue>
#include <memory>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "Lock.hpp"
using namespace std;
int gThreadNum = 15;
template <class T>
class ThreadPool
{
private:
ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false)
{
assert(threadNum_ > 0);
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
ThreadPool(const ThreadPool<T> &) = delete;
void operator=(const ThreadPool<T>&) = delete;
public:
static ThreadPool<T> *getInstance()
{
static Mutex mutex;
if (nullptr == instance) //僅僅是過濾重復(fù)的判斷
{
LockGuard lockguard(&mutex); //進(jìn)入代碼塊,加鎖。退出代碼塊,自動(dòng)解鎖
if (nullptr == instance)
{
instance = new ThreadPool<T>();
}
}
return instance;
}
//類內(nèi)成員, 成員函數(shù),都有默認(rèn)參數(shù)this
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
// prctl(PR_SET_NAME, "follower"); // 更改線程名稱
while (1)
{
tp->lockQueue();
while (!tp->haveTask())
{
tp->waitForTask();
}
//這個(gè)任務(wù)就被拿到了線程的上下文中
T t = tp->pop();
tp->unlockQueue();
t(); // 讓指定的先處理這個(gè)任務(wù)
}
}
void start()
{
assert(!isStart_);
for (int i = 0; i < threadNum_; i++)
{
pthread_t temp;
pthread_create(&temp, nullptr, threadRoutine, this);
}
isStart_ = true;
}
void push(const T &in)
{
lockQueue();
taskQueue_.push(in);
choiceThreadForHandler();
unlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
int threadNum()
{
return threadNum_;
}
private:
void lockQueue() { pthread_mutex_lock(&mutex_); }
void unlockQueue() { pthread_mutex_unlock(&mutex_); }
bool haveTask() { return !taskQueue_.empty(); }
void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
T pop()
{
T temp = taskQueue_.front();
taskQueue_.pop();
return temp;
}
private:
bool isStart_;
int threadNum_;
queue<T> taskQueue_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *instance;
// const static int a = 100;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
util.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
自己寫的版本測(cè)試:
使用json寫的序列化和反序列化版本測(cè)試:
上面實(shí)現(xiàn)網(wǎng)絡(luò)版計(jì)算器所做的所有工作都是屬于應(yīng)用層。
完成這樣的一個(gè)工作所應(yīng)做的任務(wù):
1.基本系統(tǒng)socket接口的使用
2.定制協(xié)議
3.編寫業(yè)務(wù)
????????上面的協(xié)議是我們自定義的,但是現(xiàn)在已經(jīng)有了一些別人寫的自定義協(xié)議,因?yàn)閳?chǎng)景的使用較多,并且非常成熟,寫的很好,就成為了應(yīng)用層定協(xié)議的標(biāo)準(zhǔn),就可以被別人直接使用。
? ? ? ? 這樣的協(xié)議有http, https, smtp, ftp, DNS等。
3.HTTP協(xié)議
(1)了解url和http
URL:
????????平時(shí)我們俗稱的 "網(wǎng)址" 其實(shí)就是說的 URL。
? ? ? ? 先看一個(gè)百度的URL:
https://www.baidu.com/
? ? ? ? 域名:www.com
? ? ? ? 這里面的域名會(huì)被轉(zhuǎn)換成為IP。
? ? ? ? 網(wǎng)絡(luò)通信的本質(zhì):socket,IP+port
? ? ? ? 在使用確定協(xié)議的時(shí)候,一般顯示的時(shí)候,會(huì)缺省端口號(hào)(URL上不顯示端口號(hào)),所以,瀏覽器訪問指定的url的時(shí)候,瀏覽器必須給我們自動(dòng)添加port。
? ? ? ? 那瀏覽器是如何得知url匹配的port是誰呢?
? ? ? ? 特定的眾所周知的服務(wù),端口號(hào)是確定的。
httpserver->80
httpsServer-->443
sshd->22
? ? ? ? 在[0, 1023]的端口號(hào)基本上都是一些被確定的端口號(hào),因此我們寫的網(wǎng)絡(luò)服務(wù)bind端口的時(shí)候要選擇[1024, n](1024后面的端口號(hào))。
(2)http的用處
無論我們是查閱文檔,還是看音視頻等,都是以網(wǎng)頁(yè)的方式呈現(xiàn)的。
這種都是.html文件,而http中獲取 網(wǎng)頁(yè)資源、視頻、音頻的也都是文件。
????????http是向特定的服務(wù)器申請(qǐng)?zhí)囟ǖ摹辟Y源“,然后獲取到本地,進(jìn)行展示或者某種使用的。如果我們client沒有獲取的時(shí)候,資源在哪里呢?
? ? ? ? 就在我們的網(wǎng)絡(luò)服務(wù)器(軟件)所在的服務(wù)器(硬件,計(jì)算機(jī))上。【服務(wù)器都是linux系統(tǒng)的,這些資源也都是文件】。
? ? ? ? 這些資源文件都在Linux服務(wù)器上。
? ? ? ? 要打開資源文件,并讀取、發(fā)送給客戶端的前提是:要先找到軟件服務(wù)器這個(gè)文件。
? ? ? ? Linux要找到這個(gè)文件就要通過路徑來找。
????????在這個(gè)url里的/dir/index.htm就是這個(gè)文件的路徑。?
? ? ? ? 而這個(gè)路徑最開始的/dir并不一定是Linux的根目錄。
(3)urlencode和urldecode
????????像 / ? : 等這樣的字符,已經(jīng)被url當(dāng)做特殊意義理解了,因此這些字符不能隨意出現(xiàn)。
????????比如, 某個(gè)參數(shù)中需要帶有這些特殊字符, 就必須先對(duì)特殊字符進(jìn)行轉(zhuǎn)義。
????????轉(zhuǎn)義的規(guī)則如下:
????????將需要轉(zhuǎn)碼的字符轉(zhuǎn)為16進(jìn)制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式。
C++經(jīng)過轉(zhuǎn)義會(huì)變成C%2B%2B
? ? ? ? urlencode就是轉(zhuǎn)義的過程。
? ? ? ? 而urldecode就是urlencode的逆過程
(4)http協(xié)議格式
①請(qǐng)求和響應(yīng)格式
????????請(qǐng)求格式中,請(qǐng)求行、請(qǐng)求報(bào)頭和空行可以當(dāng)作http協(xié)議的報(bào)頭,有效載荷是自己的個(gè)人信息。
????????響應(yīng)格式也是如此
任何協(xié)議的request or response都有:報(bào)頭 + 有效載荷
????????http如何保證自己的報(bào)頭和有效載荷被全部讀取呢?
1.讀取完整報(bào)頭:按行讀取,直到讀取到空行。
2.報(bào)頭能讀取完畢,請(qǐng)求或者響應(yīng)屬性中,一定要包含正文的長(zhǎng)度。(保證能讀取到完整的正文)。
???????
這里的 /a 第一個(gè) / 不是根目錄,而是web根目錄,但是可以設(shè)置成為根目錄。
// path = "/a/b/index.html";
// recource = "./wwwroot"; // 我們的web根目錄
// recource += path; // ./wwwroot/a/b/index.html
? ? ? ? 這樣就可以從web根目錄開始找了?
請(qǐng)求格式:
首行: [方法] + [url] + [版本]
Content-Type:網(wǎng)頁(yè)顯示的形式Content-Length:有效載荷
響應(yīng)格式:
首行: [版本號(hào)] + [狀態(tài)碼] + [狀態(tài)碼解釋]
其它與請(qǐng)求格式相同
②HTTP的方法
? ? ? ? 我們的網(wǎng)絡(luò)行為無非有兩種:
1.想把遠(yuǎn)端的資源到自己的本地:GET /index.html http/1.1
2.想把自己的屬性字段,提交到遠(yuǎn)端:GET or POST
????????下面我們來測(cè)試一下GET和POST方法的區(qū)別:
測(cè)試代碼:
Makefile:
serverTcp:serverTcp.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f serverTcp
server.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
using namespace std;
std::string getPath(std::string http_request)
{
std::size_t pos = http_request.find(CRLF);
if(pos == std::string::npos) return "";
std::string request_line = http_request.substr(0, pos);
//GET /a/b/c http/1.1
std::size_t first = request_line.find(SPACE);
if(pos == std::string::npos) return "";
std::size_t second = request_line.rfind(SPACE);
if(pos == std::string::npos) return "";
std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
return path;
}
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
if(!in.is_open()) return "404";
std::string content;
std::string line;
while(std::getline(in, line)) content += line;
in.close();
return content;
}
void handlerHttpRequest(int sock)
{
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
std::string path = getPath(buffer);
// path = "/a/b/index.html";
// recource = "./wwwroot"; // 我們的web根目錄
// recource += path; // ./wwwroot/a/b/index.html
// 1. 文件在哪里? 在請(qǐng)求的請(qǐng)求行中,第二個(gè)字段就是你要訪問的文件
// 2. 如何讀取
std::string recource = ROOT_PATH;
recource += path;
std::cout << recource << std::endl;
std::string html = readFile(recource);
std::size_t pos = recource.rfind(".");
std::string suffix = recource.substr(pos);
cout << suffix << endl;
//開始響應(yīng)
std::string response;
response = "HTTP/1.0 200 OK\r\n";
if(suffix == ".jpg") response += "Content-Type: image/jpeg\r\n";
else response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
response += "\r\n";
response += html;
send(sock, response.c_str(), response.size(), 0);
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 創(chuàng)建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind綁定
// 2.1 填充服務(wù)器信息
struct sockaddr_in local; // 用戶棧
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,寫入sock_對(duì)應(yīng)的內(nèi)核區(qū)域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 監(jiān)聽socket,為何要監(jiān)聽呢?tcp是面向連接的!
if (listen(listenSock_, 5 /*后面再說*/) < 0)
{
exit(3);
}
// 運(yùn)行別人來連接你了
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 獲取鏈接失敗
cerr << "accept error ...." << endl;
continue;
}
// 5.1 v1 版本 -- 多進(jìn)程版本 -- 父進(jìn)程打開的文件會(huì)被子進(jìn)程繼承嗎?會(huì)的
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
close(listenSock_); //建議
if(fork() > 0) exit(0);
//孫子進(jìn)程
handlerHttpRequest(serviceSock);
exit(0); // 進(jìn)入僵尸
}
close(serviceSock);
wait(nullptr);
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 安全退出
bool quit_;
};
serverTcp.cc
#include "server.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080\n"
<< std::endl;
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
ServerTcp svr(port);
svr.init();
svr.loop();
return 0;
}
wwwroot:
image:
? ? ? ? pulpit.jpg
index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>http 測(cè)試</title> </head> <body> <h3>hello my server!</h3> <p>代碼測(cè)試完成</p> <form action="/a/b/c.html" method="get"> Username: <input type="text" name="user"><br> Password: <input type="password" name="passwd"><br> <input type="submit" value="Submit"> </form> <!-- <img border="0" src="https://img1.baidu.com/it/u=1691233364,820181697&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="Pulpit rock" width="304" height="228"> --> </body> </html>
箭頭所示位置,若為get,則采用get方法; 若為post,則采用post方法?
????????先來測(cè)試 get 請(qǐng)求
?????????這里我們用戶名輸入: xiaoliu, 密碼輸入: 123456abc
????????這里因?yàn)槲覀儧]有實(shí)現(xiàn)接下來的網(wǎng)頁(yè)所有顯示404,但這個(gè)不是重點(diǎn)。
????????注意看這個(gè)網(wǎng)址,這里的用戶名和密碼都顯示在了url中
? ? ? ? 再來測(cè)試 post 請(qǐng)求:
????????這里界面是相同的,用戶名輸入: wangwu, 密碼輸入: 123456abc
?
????????再注意看這個(gè)網(wǎng)址,這里并沒有顯示出用戶名和密碼。
????????但是,當(dāng)我們看server的響應(yīng)時(shí),注意看這里的最后一行(正文處),我們會(huì)看到我們剛剛輸入的用戶名和密碼。(在空行的后面)
總結(jié):
? ? ? ? 1.GET方法提交參數(shù),會(huì)以明文方式將我們對(duì)應(yīng)的參數(shù)信息,拼接到url中
? ? ? ? 2.POST方法提交參數(shù),會(huì)以明文方式將我們對(duì)應(yīng)的參數(shù)信息,拼接到http的正文中
比較 GET 和 POST:
? ? ? ? 1.GET通過url傳參
? ? ? ? 2.POST通過正文傳參
? ? ? ? 3.GET方法傳參不私密
? ? ? ? 4.POST方法因?yàn)橥ㄟ^正文傳參,所以相對(duì)比較私密一些(只是私密,但是都不安全)
? ? ? ? 5.一般比較大的內(nèi)容通過POST方法傳參(因?yàn)镻OST通過正文傳參,GET通過url傳參,正文所能包含的內(nèi)容要多于url能包含的內(nèi)容)
????????除了 get 和 post 方法,還有別的方法,但是其它方法用的都很少(尤其是DELETE):
這里最常用的就是GET方法和POST方法。
③HTTP狀態(tài)碼
????????最常見的狀態(tài)碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
這里主要介紹一下3開頭的狀態(tài)碼:
? ? ? ? 301:永久重定向
? ? ? ? 302:臨時(shí)重定向
?
????????這里去請(qǐng)求訪問其中一個(gè)url時(shí),接受到了301或者302狀態(tài)碼,然后就會(huì)重定向到其中保存的new url中。
301和302的區(qū)別:
? ? ? ? 跟名稱一樣,301就是目標(biāo)服務(wù)器永久不想被訪問,永久取消(比如我們覺得當(dāng)前url的名稱不好聽,想要換一個(gè)名,那么就用301,將其永久重定向到另一個(gè)url中);302就是目標(biāo)服務(wù)器臨時(shí)不想被訪問,臨時(shí)取消(比如我們想要對(duì)這個(gè)服務(wù)器進(jìn)行升級(jí),這時(shí)候不想用戶訪問到該url,就將其臨時(shí)重定向到另一個(gè)url中,待升級(jí)完畢后,再取消該重定向)。
????????下面來測(cè)試,用上面的代碼,只需修改server.hpp即可:
server.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
using namespace std;
std::string getPath(std::string http_request)
{
std::size_t pos = http_request.find(CRLF);
if(pos == std::string::npos) return "";
std::string request_line = http_request.substr(0, pos);
//GET /a/b/c http/1.1
std::size_t first = request_line.find(SPACE);
if(pos == std::string::npos) return "";
std::size_t second = request_line.rfind(SPACE);
if(pos == std::string::npos) return "";
std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
return path;
}
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
if(!in.is_open()) return "404";
std::string content;
std::string line;
while(std::getline(in, line)) content += line;
in.close();
return content;
}
void handlerHttpRequest(int sock)
{
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
std::string response = "HTTP/1.1 302 Temporarily Moved\r\n";
response += "Location: https://www.qq.com/\t\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 創(chuàng)建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind綁定
// 2.1 填充服務(wù)器信息
struct sockaddr_in local; // 用戶棧
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,寫入sock_對(duì)應(yīng)的內(nèi)核區(qū)域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 監(jiān)聽socket,為何要監(jiān)聽呢?tcp是面向連接的!
if (listen(listenSock_, 5 /*后面再說*/) < 0)
{
exit(3);
}
// 運(yùn)行別人來連接你了
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 獲取鏈接失敗
cerr << "accept error ...." << endl;
continue;
}
// 5.1 v1 版本 -- 多進(jìn)程版本 -- 父進(jìn)程打開的文件會(huì)被子進(jìn)程繼承嗎?會(huì)的
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
close(listenSock_); //建議
if(fork() > 0) exit(0);
//孫子進(jìn)程
handlerHttpRequest(serviceSock);
exit(0); // 進(jìn)入僵尸
}
close(serviceSock);
wait(nullptr);
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 安全退出
bool quit_;
};
????????這里我們用302重定向到騰訊網(wǎng)
當(dāng)然,用301也是可以的:
測(cè)試結(jié)果:
????????這里我們輸入我們的測(cè)試url,就發(fā)現(xiàn)已經(jīng)變成了騰訊網(wǎng)
ps:(這里不能給騰訊網(wǎng)截圖啦,違規(guī))
④ HTTP常見Header
Content-Type: 數(shù)據(jù)類型(text/html等)
Content-Length: Body的長(zhǎng)度
Host: 客戶端告知服務(wù)器, 所請(qǐng)求的資源是在哪個(gè)主機(jī)的哪個(gè)端口上;
User-Agent: 聲明用戶的操作系統(tǒng)和瀏覽器版本信息;
referer: 當(dāng)前頁(yè)面是從哪個(gè)頁(yè)面跳轉(zhuǎn)過來的;
Location: 搭配3xx狀態(tài)碼使用, 告訴客戶端接下來要去哪里訪問;
Cookie: 用于在客戶端存儲(chǔ)少量信息. 通常用于實(shí)現(xiàn)會(huì)話(session)的功能
?
http協(xié)議特點(diǎn)之一:無狀態(tài)(http不會(huì)記錄用戶的信息)
? ? ? ?但是用戶需要被記?。簳?huì)話保持
? ? ? ?一旦登陸,就會(huì)有各種會(huì)話保持的策略
????????其中一種策略就是Cookie
? ? ? ? Cookie是瀏覽器維護(hù)的文件,有多種存在形式:磁盤級(jí)或者內(nèi)存級(jí)
????????比如我們進(jìn)b站時(shí),如果之前登陸了,即使關(guān)掉重進(jìn)也是登陸狀態(tài),但是如果我們把Cookie里的都刪掉,那么再刷新一下就會(huì)發(fā)現(xiàn)需要重新登陸了。
? ? ? ? ?這說明http本身是無狀態(tài)的,是不會(huì)保存用戶的信息的,但是用戶需要,所以就有了像Cookie這樣的會(huì)話,去保存用戶的信息。
? ? ? ? 下面我們來寫個(gè)Cookie(依舊是修改server.hpp):
server.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
using namespace std;
std::string getPath(std::string http_request)
{
std::size_t pos = http_request.find(CRLF);
if(pos == std::string::npos) return "";
std::string request_line = http_request.substr(0, pos);
//GET /a/b/c http/1.1
std::size_t first = request_line.find(SPACE);
if(pos == std::string::npos) return "";
std::size_t second = request_line.rfind(SPACE);
if(pos == std::string::npos) return "";
std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
return path;
}
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
if(!in.is_open()) return "404";
std::string content;
std::string line;
while(std::getline(in, line)) content += line;
in.close();
return content;
}
void handlerHttpRequest(int sock)
{
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
std::string path = getPath(buffer);
// 1. 文件在哪里? 在請(qǐng)求的請(qǐng)求行中,第二個(gè)字段就是你要訪問的文件
// 2. 如何讀取
std::string recource = ROOT_PATH;
recource += path;
std::cout << recource << std::endl;
std::string html = readFile(recource);
std::size_t pos = recource.rfind(".");
std::string suffix = recource.substr(pos);
cout << suffix << endl;
//開始響應(yīng)
std::string response;
response = "HTTP/1.0 200 OK\r\n";
if(suffix == ".jpg") response += "Content-Type: image/jpeg\r\n";
else response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
response += "Set-Cookie: this is my cookie content;\r\n";
response += "\r\n";
response += html;
send(sock, response.c_str(), response.size(), 0);
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 創(chuàng)建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind綁定
// 2.1 填充服務(wù)器信息
struct sockaddr_in local; // 用戶棧
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,寫入sock_對(duì)應(yīng)的內(nèi)核區(qū)域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 監(jiān)聽socket,為何要監(jiān)聽呢?tcp是面向連接的!
if (listen(listenSock_, 5 /*后面再說*/) < 0)
{
exit(3);
}
// 運(yùn)行別人來連接你了
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 獲取鏈接失敗
cerr << "accept error ...." << endl;
continue;
}
// 5.1 v1 版本 -- 多進(jìn)程版本 -- 父進(jìn)程打開的文件會(huì)被子進(jìn)程繼承嗎?會(huì)的
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
close(listenSock_); //建議
if(fork() > 0) exit(0);
//孫子進(jìn)程
handlerHttpRequest(serviceSock);
exit(0); // 進(jìn)入僵尸
}
close(serviceSock);
wait(nullptr);
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 安全退出
bool quit_;
};
? ? ? ? 結(jié)果:
????????這里可以看到我們剛剛寫的Cookie:this is my cookie content
????????這里最后一行也是Cookie的內(nèi)容
????????但是Cookie是不安全的,某個(gè)網(wǎng)站你輸入了賬號(hào)密碼,這時(shí)Cookie就保存了你所輸入的賬號(hào)密碼,然后有像黑客這樣的通過這個(gè)網(wǎng)站盜取了你的Cookie,那么他就可以通過該Cookie登陸上你的賬號(hào)。
? ? ? ? ?因此為了防止這種情況,就有了相對(duì)安全的Cookie + session(也不是絕對(duì)安全的)
????????
Cookie + session:
? ? ? ? 當(dāng)我們輸入完賬號(hào)密碼后,不會(huì)直接返回保存,會(huì)先形成session文件(用戶的臨時(shí)私密信息都保存在這個(gè)文件中,session文件會(huì)自動(dòng)形成文件名【具備唯一性】),然后返回該文件名的id值:session_id,瀏覽器收到后,就將session_id寫入到本地的Cookie中。
? ? ? ? 瀏覽器為了證明用戶存在過,就通過Cookie中的session_id來判斷,如果存在就可以確定該用戶存在過。
http特點(diǎn)之二:無連接
????????http協(xié)議和tcp協(xié)議是兩個(gè)協(xié)議,tcp是面向連接的,這和http沒有關(guān)系。http是無連接的,http只是用了tcp的能力。
Connect: close
? ? ? ? 只支持短連接,網(wǎng)頁(yè)中可能存在圖片,音視頻等資源,就導(dǎo)致用戶所看到的完整的網(wǎng)頁(yè)內(nèi)容 -- 背后可能是無數(shù)次http請(qǐng)求(http底層主流采用的就是tcp協(xié)議),每次都會(huì)進(jìn)行3次握手、4次揮手,效率較低。因此有了長(zhǎng)連接方案。
Connect: keep-alive
? ? ? ?支持長(zhǎng)連接,可以把很多次請(qǐng)求全部放在一起,一次進(jìn)行了多次的請(qǐng)求和響應(yīng),不用進(jìn)行多次的3次握手、四次揮手,這樣就提高了效率。(只有雙方都是 Connect: keep-alive 時(shí),才可以使用長(zhǎng)連接)。
http定義:超文本傳輸協(xié)議,無連接,無狀態(tài)的應(yīng)用層協(xié)議。
pipeline技術(shù):什么順序請(qǐng)求,就什么順序響應(yīng)(http發(fā)起了幾個(gè)請(qǐng)求,服務(wù)端對(duì)這幾個(gè)請(qǐng)求進(jìn)行了處理,響應(yīng)時(shí)按順序進(jìn)行響應(yīng))。
4.HTTPS協(xié)議
HTTPS也是一個(gè)應(yīng)用層協(xié)議,是在HTTP協(xié)議的基礎(chǔ)上引入了一個(gè)加密層。
HTTP協(xié)議內(nèi)容都是按照文本的方式明文傳輸?shù)?,就?dǎo)致在傳輸過程中出現(xiàn)一些被篡改的情況。
?(1)加密
????????加密就是把明文(要傳輸?shù)男畔ⅲ┻M(jìn)行一系列變換,生成密文。
????????解密就是把密文再進(jìn)行一系列變化,還原成明文。
????????在這個(gè)加密和解密的過程中,往往需要一個(gè)或多個(gè)中間的數(shù)據(jù),輔助進(jìn)行這個(gè)過程,這樣的數(shù)據(jù)稱為密鑰。
(2)為什么要加密
????????我們一定遇到過這樣的情況:我們要下載某一個(gè)軟件,但是下載之后發(fā)現(xiàn)變成了另一個(gè)軟件,這里就是中間人(運(yùn)營(yíng)商)更改劫持了我們想要下載這個(gè)軟件的HTTP請(qǐng)求,然后在響應(yīng)時(shí)就更改成了另一個(gè)軟件的下載地址了。目的就是為了推廣更改后的那個(gè)軟件。
? ? ? ? 上面的例子就是一種中間人攻擊。
? ? ? ? 因?yàn)閔ttp的內(nèi)容是明文傳輸?shù)?,明文?shù)據(jù)會(huì)經(jīng)過路由器、wifi熱點(diǎn)、通信服務(wù)運(yùn)營(yíng)商、代理服務(wù)器等多個(gè)節(jié)點(diǎn),如果信息在傳輸過程中被劫持,傳輸?shù)膬?nèi)容就完全暴露了,劫持者還可以篡改傳輸?shù)男畔⑶也槐浑p方察覺,這就是中間人攻擊,所以我們需要對(duì)信息進(jìn)行加密。
? ? ? ? 除了上面的例子中運(yùn)營(yíng)商可以劫持。其他的黑客也可以用類似的手段進(jìn)行劫持,來竊取用戶的隱私信息,或者篡改內(nèi)容。
? ? ? ? 因此,在互聯(lián)網(wǎng)上,明文傳輸是比較危險(xiǎn)的事情。
? ? ? ? HTTPS就是在HTTP的基礎(chǔ)上進(jìn)行了加密,進(jìn)一步的保證用戶的信息安全。
加密就能解決上面的問題嗎?
? ? ? ? 不能。
? ? ? ? 要想解決這些問題,是需要一套綜合的方案的。
? ? ? ? 并且互聯(lián)網(wǎng)是不存在絕對(duì)的安全的,只不過想要破解加密的內(nèi)容需要花費(fèi)的時(shí)間太長(zhǎng)或者成本太高,就是相對(duì)安全的。
(3)常見的加密方式
①對(duì)稱加密
? ? ? ? 采用單鑰密碼系統(tǒng)的加密方法,同一個(gè)密鑰可以同時(shí)用作信息的加密和解密,稱加密,也稱為單密鑰加密。特征:加密和解密所用的密鑰是相同的。
? ? ? ? 常見的對(duì)稱加密算法:DES、3DES、AES、TDEA、Blowfish、RC2等
? ? ? ? 特點(diǎn):算法公開、計(jì)算量小、加密速度快、加密效率高
②非對(duì)稱加密
? ? ? ? 需要兩個(gè)密鑰來進(jìn)行加密和解密,這兩個(gè)密鑰是公開密鑰(簡(jiǎn)稱公鑰)和私有密鑰(簡(jiǎn)稱私鑰)。
? ? ? ? 常見的非對(duì)稱加密算法:RSA、DSA、ECDSA
? ? ? ? 特點(diǎn):算法強(qiáng)度復(fù)雜,安全性依賴于算法于密鑰,但是由于其算法復(fù)雜,而使得加密解密使得沒有對(duì)稱加密解密的速度快算
????????
? ? ? ? 非對(duì)稱加密要用到兩個(gè)密鑰,一個(gè)公鑰,一個(gè)私鑰。
? ? ? ? 公鑰和私鑰是配對(duì)的,最大的缺點(diǎn)就是運(yùn)算速度非常慢,比對(duì)稱加密要慢很多。
? ? ? ? 既可以通過公鑰對(duì)明文加密,變成密文;通過私鑰對(duì)密文解密,變成明文。也可以反著用,通過私鑰對(duì)明文加密,變成密文;通過公鑰對(duì)密文解密,變成明文。
(4)數(shù)據(jù)摘要(數(shù)據(jù)指紋)
????????數(shù)據(jù)摘要((數(shù)據(jù)指紋):其基本原理是利用單向散列函數(shù)(Hash函數(shù))對(duì)信息進(jìn)行運(yùn)算,生成一串固定長(zhǎng)度的數(shù)據(jù)摘要。數(shù)據(jù)摘要并不是一種加密機(jī)制,但可以用來判斷數(shù)據(jù)數(shù)據(jù)有沒有被篡改。
? ? ? ? 摘要常見算法:MD5、SHA1、SHA256、SHA512等。
? ? ? ? 算法把無限的映射成有限,因此可能會(huì)有碰撞(兩個(gè)不同的信息,算出的摘要相同,但是概率非常低)。
? ? ? ? 摘要特征:和加密算法的區(qū)別是,摘要嚴(yán)格意義不是加密,因?yàn)闆]有解密。只不過從摘要很難反推原信息,通常用來進(jìn)行數(shù)據(jù)對(duì)比。
(5)加密方案
我們看看下面這兩個(gè)問題:
????????對(duì)http進(jìn)行對(duì)稱加密,能否解決數(shù)據(jù)通信安全的問題?問題是什么?
? ? ? ? 為何要用非對(duì)稱加密?為何不全用非對(duì)稱加密?
解答:
HTTPS的工作過程
? ? ? ? 既然要保證數(shù)據(jù)安全,就需要進(jìn)行“加密”
????????網(wǎng)絡(luò)傳輸中不再直接傳輸明文了。而是加密之后的“密文”
? ? ? ? 加密的方式有很多,但是整體可以分為兩大類:對(duì)稱加密和非對(duì)稱加密。
①方案1 - 只使用對(duì)稱加密
? ? ? ? 如果通信雙方都各自持有同一個(gè)密鑰X,且沒有別人知道,這兩方的通信安全當(dāng)然是可以被保證的(除非密鑰被破解)。
? ? ? ? 引用對(duì)稱加密之后,即使數(shù)據(jù)被截獲,由于黑客不知道密鑰是啥,因此就無法進(jìn)行解密,也就不知道請(qǐng)求的真實(shí)內(nèi)容是什么了。
? ? ? ? 但是不只是這么簡(jiǎn)單,服務(wù)器同一時(shí)刻其實(shí)是給很多客戶端提供了服務(wù)的。這么多客戶端,每個(gè)人用的密鑰都必須是不同的(如果是相同的,那密鑰就太容易擴(kuò)散了,黑客就也能拿到了)。因此服務(wù)器就需要維護(hù)每個(gè)客戶端和每個(gè)密鑰之間的關(guān)聯(lián)關(guān)系,這也很麻煩。
? ? ? ? 比較理想的做法,就是能在客戶端和服務(wù)器建立連接的時(shí)候,雙方協(xié)商確定這次的密鑰是什么。但是如果直接把密鑰明文傳輸,那么黑客也就能獲得密鑰了,此時(shí)后續(xù)的加密操作就形同虛設(shè)了。
? ? ? ? 因此密鑰的傳輸也必須加密傳輸。
? ? ? ? 但是要想對(duì)密鑰進(jìn)行對(duì)稱加密,就仍然需要先協(xié)商確定一個(gè)“密鑰的密鑰”,這樣就成了“先有雞還是先有蛋”的問題。此時(shí)密鑰的傳輸再用對(duì)稱加密就行不通了。
②方案2 - 只使用非對(duì)稱加密
? ? ? ? 鑒于非對(duì)稱加密的機(jī)制,如果服務(wù)器先把公鑰以明文方式傳輸給瀏覽器,之后瀏覽器想服務(wù)器傳數(shù)據(jù)前都先用足夠公鑰加密好再傳,那么從客戶端到服務(wù)器的信道似乎是安全的(有安全問題),因?yàn)橹挥蟹?wù)器有相應(yīng)的私鑰能解開公鑰加密的數(shù)據(jù)。
? ? ? ? 但是服務(wù)器到瀏覽器怎么保障安全。
? ? ? ? 如果服務(wù)器用它的私鑰加密數(shù)據(jù)傳給瀏覽器(只能用私鑰加密,因?yàn)榭蛻舳藳]有私鑰,無法解密用公鑰加密的數(shù)據(jù)),那么瀏覽器用公鑰可以解密,而這個(gè)公鑰是一開始通過明文傳輸給瀏覽器的。那么如果公鑰被中間人劫持了,那他也能用該公鑰解密服務(wù)器傳來的消息了。這一定是不安全的。
③方案3 - 雙方都使用非對(duì)稱加密
實(shí)現(xiàn)步驟:
????????1.服務(wù)端擁有公鑰S與對(duì)應(yīng)的私鑰S',客戶端擁有公鑰C與對(duì)應(yīng)的私鑰C'
? ? ? ? 2.客戶和服務(wù)端交換公鑰
? ? ? ? 3.客戶端給服務(wù)端發(fā)信息:先用S對(duì)數(shù)據(jù)加密,再發(fā)送,只能由服務(wù)器解密,因?yàn)橹挥蟹?wù)器有私鑰S'
? ? ? ? 4.服務(wù)端給客戶端發(fā)信息:先用C對(duì)數(shù)據(jù)加密,再發(fā)送,只能由客戶端解密,因?yàn)橹挥锌蛻舳擞兴借€C'
看起來這個(gè)方案是可行的,但是缺點(diǎn)很大:
? ? ? ? 效率太低
? ? ? ? 依舊有安全問題
④方案4 - 非對(duì)稱加密 + 對(duì)稱加密
實(shí)現(xiàn)步驟:
? ? ? ? 1.服務(wù)器具有非對(duì)稱公鑰S和私鑰S‘
? ? ? ? 2.客戶端發(fā)起https請(qǐng)求,獲取服務(wù)端公鑰S
? ? ? ? 3.客戶端在本地生成對(duì)稱密鑰C,通過公鑰S加密,發(fā)送給服務(wù)器
? ? ? ? 4.由于中間的網(wǎng)絡(luò)設(shè)備沒有私鑰,即使截獲了數(shù)據(jù),也無法還原出內(nèi)部的原文,也就無法獲取到對(duì)稱密鑰
? ? ? ? 5.服務(wù)器通過私鑰S'解密,還原出客戶端發(fā)送的對(duì)稱密鑰C,并且使用這個(gè)對(duì)稱密鑰加密給客戶端返回的響應(yīng)數(shù)據(jù)
? ? ? ? 6.后續(xù)客戶端和服務(wù)器的通信都只用對(duì)稱加密即可,由于該密鑰只有客戶端和服務(wù)器兩個(gè)主機(jī)知道,其他主機(jī)/設(shè)備不知道密鑰,即使截獲數(shù)據(jù)也沒有意義。
由于對(duì)稱加密的效率比非對(duì)稱加密高很多,因此只是在開始階段協(xié)商密鑰的時(shí)候使用非對(duì)稱加密,后續(xù)的傳輸仍然使用對(duì)稱加密。
? ? ? ? 這個(gè)方案看起來沒有問題,但是也是存在安全問題的。
? ? ? ? 不只這個(gè)方案,方案2,3,4都存在這個(gè)問題,如果最開始,中間人就開始攻擊了呢?
中間人攻擊 - 針對(duì)上面的場(chǎng)景
Man-in-the-MiddleAttack,簡(jiǎn)稱"MITM攻擊"
? ? ? ? 在方案2/3/4中,客戶端獲取到公鑰S之后,對(duì)客戶端形成的對(duì)稱密鑰X用服務(wù)端給客戶端的公鑰S進(jìn)行加密,中間人即使去竊取到了數(shù)據(jù),此時(shí)中間人確實(shí)無法解出客戶端形成的密鑰X,因?yàn)橹挥蟹?wù)器有私鑰S'
? ? ? ? 但是中間人的攻擊,如果在最開始協(xié)商的時(shí)候就進(jìn)行了,那就不一定安全了,假設(shè)黑客已經(jīng)成功成為中間人。
? ? ? ? 1.服務(wù)器具有非對(duì)稱加密算法的公鑰S,私鑰S'
? ? ? ? 2.中間人具有非對(duì)稱加密算法的公鑰M,私鑰M'
? ? ? ? 3.客戶端向服務(wù)器發(fā)送請(qǐng)求,服務(wù)器明文傳送公鑰S給客戶端
? ? ? ? 4.中間人劫持?jǐn)?shù)據(jù)報(bào)文,提取公鑰S并保存好,然后將被劫持報(bào)文中的公鑰S替換成為自己的公鑰M,并將偽造報(bào)文發(fā)給客戶端。
? ? ? ? 5.客戶端收到報(bào)文,提取公鑰M(客戶端自己不知道公鑰被更換過了),自己形成對(duì)稱密鑰X,用公鑰M加密X,形成報(bào)文發(fā)送給服務(wù)器。
? ? ? ? 6.中間人劫持后,直接用自己的私鑰M'進(jìn)行解密,得到通信密鑰X,再用曾經(jīng)保存的服務(wù)端公鑰S加密后,將報(bào)文推送給服務(wù)器。
? ? ? ? 7.服務(wù)器拿到報(bào)文后,用自己的私鑰S'解密,得到通信密鑰X
? ? ? ? 8.雙方開始采用X進(jìn)行對(duì)稱加密,進(jìn)行通信。但是一切都在中間人的掌握中,劫持?jǐn)?shù)據(jù),進(jìn)行竊聽甚至修改,都是可以的。
上面的攻擊方案,同樣適用于方案2、方案3。
????????問題本質(zhì)在于客戶端無法確定:收到的含有公鑰的數(shù)據(jù)報(bào)文,是目標(biāo)服務(wù)器發(fā)送過來的,還是中間人發(fā)送過來的。
(6)證書
????????CA認(rèn)證
????????服務(wù)端在使用HTTPS前,需要向CA機(jī)構(gòu)申領(lǐng)一份數(shù)字證書,數(shù)字證書里含有證書申請(qǐng)者信息、公鑰信息等。服務(wù)器把證書傳輸給瀏覽器,瀏覽器從證書里獲取公鑰就行了,證書就如身份證,證明服務(wù)端公鑰的權(quán)威性。
? ? ? ? 這個(gè)證書可以理解成是一個(gè)結(jié)構(gòu)化的字符串,里面包含了以下信息:
①證書發(fā)布機(jī)構(gòu)
②證書有效期
③公鑰
④證書所有者
⑤簽名
⑥ ...
? ? ? ? 需要注意的是:申請(qǐng)證書的時(shí)候,需要在特定平臺(tái)生成一對(duì)密鑰對(duì),即公鑰和私鑰。這對(duì)密鑰對(duì)就是用來在網(wǎng)絡(luò)通信中進(jìn)行明文加密以及數(shù)字簽名的。
? ? ? ? 其中公鑰會(huì)隨著CSR文件,一起發(fā)給CA進(jìn)行權(quán)威認(rèn)證,私鑰服務(wù)端自己保留,用來后續(xù)進(jìn)行通信(其實(shí)主要就是用來交換對(duì)稱密鑰)。
????????形成CSR之后,后續(xù)就是向CA進(jìn)?申請(qǐng)認(rèn)證,不過?般認(rèn)證過程很繁瑣,?絡(luò)各種提供證書申請(qǐng)的服務(wù)商,?般真的需要,可以直接找平臺(tái)解決。
(7)數(shù)字簽名(數(shù)據(jù)簽名)
? ? ? ? 摘要經(jīng)過加密,就得到數(shù)字簽名。
? ? ? ? 簽名的形成是基于非對(duì)稱加密算法的。目前暫時(shí)和https沒有關(guān)系,不要和https中的公鑰私鑰搞混。
????????原始數(shù)據(jù)散列成摘要,再對(duì)其進(jìn)行私鑰加密,形成簽名,再把簽名的數(shù)據(jù)和原始數(shù)據(jù)組合在一起就叫做攜帶了數(shù)字簽名的數(shù)據(jù)。
? ? ? ? 如果有人拿到了這個(gè)攜帶了數(shù)字簽名的數(shù)據(jù),對(duì)這個(gè)內(nèi)容/簽名/內(nèi)容和簽名作修改,我們?cè)趺粗滥兀?/p>
? ? ? ? 首先把這個(gè)攜帶了數(shù)字簽名的數(shù)據(jù)的內(nèi)容(原始數(shù)據(jù))拿出來,再把簽名拿出來,然后對(duì)原始數(shù)據(jù)進(jìn)行哈希散列,形成散列值,再將剛剛形成的簽名用公鑰來解密,也得到散列值,如果這兩個(gè)散列值相等,證明數(shù)字簽名和這個(gè)數(shù)據(jù)是一致的;如果散列值不相等,證明內(nèi)容/簽名/內(nèi)容和簽名被修改過。
? ? ? ? 所以,攜帶數(shù)據(jù)簽名的意義:防止內(nèi)容被篡改
????????當(dāng)服務(wù)端申請(qǐng)CA證書的時(shí)候,CA機(jī)構(gòu)會(huì)對(duì)該服務(wù)端進(jìn)?審核,并專門為該網(wǎng)站形成數(shù)字簽名,過程如下:
1. CA機(jī)構(gòu)擁有非對(duì)稱加密的私鑰A和公鑰A'。
2. CA機(jī)構(gòu)對(duì)服務(wù)端申請(qǐng)的證書明文數(shù)據(jù)進(jìn)行hash,形成數(shù)據(jù)摘要。
3. 然后對(duì)數(shù)據(jù)摘要用CA私鑰A'加密,得到數(shù)字簽名S。
????????服務(wù)端申請(qǐng)的證書明文和數(shù)字簽名S共同組成了數(shù)字證書,這樣?份數(shù)字證書就可以頒發(fā)給服務(wù)端了。
因此,有了最終方案:
⑤方案5 - 非對(duì)稱加密 + 對(duì)稱加密 + 證書認(rèn)證
????????在客戶端和服務(wù)器剛?建?連接的時(shí)候,服務(wù)器給客戶端返回?個(gè)證書,證書包含了之前服務(wù)端的公鑰,也包含了網(wǎng)站的身份信息。
客戶端會(huì)進(jìn)行認(rèn)證:
當(dāng)客戶端獲取到這個(gè)證書之后,會(huì)對(duì)證書進(jìn)行校驗(yàn)(防止證書是偽造的)。
????????判定證書的有效期是否過期。
????????判定證書的發(fā)布機(jī)構(gòu)是否受信任(操作系統(tǒng)中已內(nèi)置的受信任的證書發(fā)布機(jī)構(gòu))。
????????驗(yàn)證證書是否被篡改:從系統(tǒng)中拿到該證書發(fā)布機(jī)構(gòu)的公鑰,對(duì)簽名解密,得到?個(gè)hash值(稱為數(shù)據(jù)摘要),設(shè)為hash1。然后計(jì)算整個(gè)證書的hash值,設(shè)為hash2。對(duì)?hash1和hash2是否相等。如果相等,則說明證書是沒有被篡改過的。
? ? ? ? 瀏覽器中也都會(huì)包含送信任證書的發(fā)布機(jī)構(gòu)。
中間人有沒有可能篡改該證書?
? ? ? ? 假設(shè)中間人篡改了證書的明文。
? ? ? ? 由于他沒有CA機(jī)構(gòu)的私鑰,所以無法hash之后用私鑰加密形成簽名,那么也就沒辦法對(duì)篡改后的證書形成匹配的簽名。
? ? ? ? 如果強(qiáng)行篡改,客戶端收到該證書后會(huì)發(fā)現(xiàn)明文和簽名解密后的值不一致,則說明證書已被篡改,證書不可信,從而終止向服務(wù)器傳輸信息,防止信息泄露給中間人。
中間人有沒有可能掉包證書?
? ? ? ? 因?yàn)橹虚g人沒有CA私鑰,所以無法制作假的證書。
? ? ? ? 所以中間人只能向CA申請(qǐng)真證書,然后用自己申請(qǐng)的證書進(jìn)行掉包。(此時(shí)中間人就暴露了自己的信息)
? ? ? ? 這個(gè)確實(shí)能做到證書的整體掉包,但是證書明文中包含了域名等服務(wù)端認(rèn)證信息,如果整體掉包??蛻舳艘琅f能夠識(shí)別出來。
? ? ? ? 中間人沒有CA私鑰,所以對(duì)任何證書都無法進(jìn)行合法修改,也包括自己的。
(8)常見問題
為什么摘要內(nèi)容在網(wǎng)絡(luò)傳輸?shù)臅r(shí)候一定要加密形成簽名?
? ? ? ? 常見的加密算法有:MD5和SHA系列
? ? ? ? 以MD5為例,我們不需要研究具體的計(jì)算簽名的過程,只需要了解MD5的特點(diǎn):
①定長(zhǎng):無論多長(zhǎng)的字符串,計(jì)算出來的MD5值都是固定長(zhǎng)度(16字節(jié)版本或者32字節(jié)版本)
②分散:源字符串只要改變一點(diǎn)點(diǎn),最終得到的MD5值都會(huì)差別很大
③不可逆:通過源字符串生成MD5很容易,但是通過MD5還原成原串理論上是不可能的
????????正因?yàn)镸D5有這樣的特性,我們可以認(rèn)為如果兩個(gè)字符串的MD5值相同,則認(rèn)為這兩個(gè)字符串相同。
? ? ? ??
判定證書篡改的過程(這個(gè)過程就好比判定這個(gè)身份證是不是偽造的身份證):
????????假設(shè)我們的證書只是?個(gè)簡(jiǎn)單的字符串hello,對(duì)這個(gè)字符串計(jì)算hash值(比如md5),結(jié)果為
BC4B2A76B9719D91。
????????如果hello中有任意的字符被篡改了,比如變成了hella,那么計(jì)算的md5值就會(huì)變化很大,
BDBD6F9CF51F2FD8。
????????然后我們可以把這個(gè)字符串hello和哈希值BC4B2A76B9719D91從服務(wù)器返回給客戶端,此時(shí)客戶端如何驗(yàn)證hello是否是被篡改過呢?
????????那么就只要計(jì)算hello哈希值,看看是不是BC4B2A76B9719D91即可。
????????但是還有個(gè)問題,如果?客把hello篡改了,同時(shí)也把哈希值重新計(jì)算下,客戶端就分辨不出來了。
????????所以被傳輸?shù)墓V挡荒軅鬏斆魑?,需要傳輸密文?/p>
????????所以,對(duì)證書明?(這?就是“hello”)hash形成散列摘要,然后CA使?自己的私鑰加密形成簽名,將hello和加密的簽名合起來形成CA證書,頒發(fā)給服務(wù)端,當(dāng)客戶端請(qǐng)求的時(shí)候,就發(fā)送給客戶端,即使中間人截獲了,因?yàn)闆]有CA私鑰,就無法更改或者整體掉包,就能安全的證明,證書的合法性。最后,客戶端通過操作系統(tǒng)?已經(jīng)存在了的證書發(fā)布機(jī)構(gòu)的公鑰進(jìn)行解密,還原出原始的哈希值,再進(jìn)行校驗(yàn)。
為什么簽名不直接加密,而是要先hash形成摘要呢?
? ? ? ? 為了縮小簽名密文的長(zhǎng)度,加快數(shù)字簽名的驗(yàn)證簽名的運(yùn)算速度。
如何成為中間人?
? ? ? ? ① ARP欺騙:在局域網(wǎng)中,黑客經(jīng)過收到ARP的Request廣播包,能夠偷聽到其它節(jié)點(diǎn)的(IP,MAC)地址。例如:黑客收到兩個(gè)主機(jī)A,B的地址,告訴B(受害者),自己是A,使得B在發(fā)送給A的數(shù)據(jù)包都被黑客截取
????????② ICMP攻擊:由于ICMP協(xié)議中有重定向的報(bào)文類型,那么我們就可以偽造?個(gè)ICMP信息然后發(fā)送給局域網(wǎng)中的客戶端,并偽裝自己是?個(gè)更好的路由通路。從而導(dǎo)致目標(biāo)所有的上網(wǎng)流量都會(huì)發(fā)送到我們指定的接口上,達(dá)到和ARP欺騙同樣的效果。
? ? ? ? ③ 假wifi,假網(wǎng)站等。
(9)總結(jié)
? ? ? ? HTTPS的工作過程中涉及到的密鑰有三組:
? ? ? ??第?組(非對(duì)稱加密):用于校驗(yàn)證書是否被篡改。服務(wù)器持有私鑰(私鑰在形成CSR文件與申請(qǐng)證書時(shí)獲得),客戶端持有公鑰(操作系統(tǒng)包含了可信任的CA認(rèn)證機(jī)構(gòu)有哪些,同時(shí)持有對(duì)應(yīng)的公鑰)。服務(wù)器在客戶端請(qǐng)求是:返回?cái)y帶簽名的證書。客戶端通過這個(gè)公鑰進(jìn)行證書驗(yàn)證,保證證書的合法性,進(jìn)?步保證證書中攜帶的服務(wù)端公鑰權(quán)威性。
? ? ? ??第二組(非對(duì)稱加密):用于協(xié)商生成對(duì)稱加密的密鑰??蛻舳擞檬盏降腃A證書中的公鑰(是可被信任的)給隨機(jī)?成的對(duì)稱加密的密鑰加密,傳輸給服務(wù)器,服務(wù)器通過私鑰解密獲取到對(duì)稱加密密鑰。
? ? ? ??第三組(對(duì)稱加密):客戶端和服務(wù)器后續(xù)傳輸?shù)臄?shù)據(jù)都通過這個(gè)對(duì)稱密鑰加密解密。
? ? ? ?其實(shí)?切的關(guān)鍵都是圍繞這個(gè)對(duì)稱加密的密鑰。其他的機(jī)制都是輔助這個(gè)密鑰工作的:
第二組非對(duì)稱加密的密鑰是為了讓客戶端把這個(gè)對(duì)稱密鑰傳給服務(wù)器。
第一組非對(duì)稱加密的密鑰是為了讓客戶端拿到第?組非對(duì)稱加密的公鑰。
二.傳輸層
1.端口號(hào)
????????端口號(hào)(Port)標(biāo)識(shí)了一個(gè)主機(jī)上進(jìn)行通信的不同的應(yīng)用程序。
????????在TCP/IP協(xié)議中, 用 "源IP", "源端口號(hào)", "目的IP", "目的端口號(hào)", "協(xié)議號(hào)" 這樣一個(gè)五元組來標(biāo)識(shí)一個(gè)通信(可以通過netstat -n查看)。
?(1)端口號(hào)范圍劃分
? ? ? ? ① 0 - 1023: 知名端口號(hào), HTTP, FTP, SSH等這些廣為使用的應(yīng)用層協(xié)議, 他們的端口號(hào)都是固定的。
????????② 1024 - 65535: 操作系統(tǒng)動(dòng)態(tài)分配的端口號(hào). 客戶端程序的端口號(hào), 就是由操作系統(tǒng)從這個(gè)范圍分配的。
(2)知名端口號(hào)
????????有些服務(wù)器是非常常用的, 為了使用方便, 人們約定一些常用的服務(wù)器, 都是用以下這些固定的端口號(hào):
ssh服務(wù)器, 使用22端口
ftp服務(wù)器, 使用21端口
telnet服務(wù)器, 使用23端口
http服務(wù)器, 使用80端口
https服務(wù)器, 使用443
? ? ? ? 執(zhí)行下面的命令, 可以看到知名端口號(hào):
cat /etc/services
? ? ? ? 我們自己寫一個(gè)程序使用端口號(hào)時(shí), 要避開這些知名端口號(hào)
(3)兩個(gè)問題
1.一個(gè)進(jìn)程是否可以bind多個(gè)端口號(hào)?
? ? ? ? 可以
2.一個(gè)端口號(hào)是否可以被多個(gè)進(jìn)程bind?
? ? ? ? 不可以
(4)兩個(gè)指令
① netstat
????????netstat是一個(gè)用來查看網(wǎng)絡(luò)狀態(tài)的重要工具。
語法: netstat [選項(xiàng)]
功能:查看網(wǎng)絡(luò)狀態(tài)
常用選項(xiàng):
-n:拒絕顯示別名,能顯示數(shù)字的全部轉(zhuǎn)化成數(shù)字
-l:僅列出有在 Listen (監(jiān)聽) 的服務(wù)狀態(tài)
-p:顯示建立相關(guān)鏈接的程序名
-t:(tcp)僅顯示tcp相關(guān)選項(xiàng)
-u:(udp)僅顯示udp相關(guān)選項(xiàng)
-a:(all)顯示所有選項(xiàng),默認(rèn)不顯示LISTEN相關(guān)
? ? ? ? 最常使用:netstat -nltp
② pidof
????????在查看服務(wù)器的進(jìn)程id時(shí)非常方便
語法: pidof [進(jìn)程名]
功能:通過進(jìn)程名, 查看進(jìn)程id
2.UDP協(xié)議
(1)UDP協(xié)議格式
① 16位UDP長(zhǎng)度, 表示整個(gè)數(shù)據(jù)報(bào)(UDP首部+UDP數(shù)據(jù))的最大長(zhǎng)度;
② 如果校驗(yàn)和出錯(cuò), 報(bào)文就會(huì)直接被丟棄
? ? ? ? 網(wǎng)絡(luò)協(xié)議棧的tcp/ip協(xié)議,是內(nèi)核中實(shí)現(xiàn)的,內(nèi)核是用C語言實(shí)現(xiàn)的。
? ? ? ? 報(bào)頭本質(zhì)上就是一個(gè)結(jié)構(gòu)體struct:
struct udp_hdr
{
? ? ? ? unsigned int src_port :16; // 位段
? ? ? ? unsigned int dst_port: 16;
? ? ? ? unsigned int udp_len: 16;
? ? ? ? unsigned int udp_check 16;
};
? ? ? ? 添加報(bào)頭的本質(zhì):就是拷貝對(duì)象(添加位段的屬性,然后把對(duì)象拷貝到數(shù)據(jù)的前面,形成報(bào)文)
(2)UDP的特點(diǎn)
UDP傳輸?shù)倪^程類似于寄信:
無連接: 知道對(duì)端的IP和端口號(hào)就直接進(jìn)行傳輸, 不需要建立連接。
不可靠: 沒有確認(rèn)機(jī)制, 沒有重傳機(jī)制; 如果因?yàn)榫W(wǎng)絡(luò)故障該段無法發(fā)到對(duì)方, UDP協(xié)議層也不會(huì)給應(yīng)用層返回任何錯(cuò)誤信息。
面向數(shù)據(jù)報(bào): 不能夠靈活的控制讀寫數(shù)據(jù)的次數(shù)和數(shù)量。
(3)面向數(shù)據(jù)報(bào)
????????發(fā)送一次報(bào)文,就接收一次。
????????無論應(yīng)用層交給UDP多長(zhǎng)的報(bào)文, UDP都會(huì)原樣發(fā)送, 既不會(huì)拆分, 也不會(huì)合并。
????????用UDP傳輸100個(gè)字節(jié)的數(shù)據(jù):
????????如果發(fā)送端調(diào)用一次sendto, 發(fā)送100個(gè)字節(jié), 那么接收端也必須調(diào)用對(duì)應(yīng)的一次recvfrom, 接收100個(gè)字節(jié)。而不能循環(huán)調(diào)用10次recvfrom, 每次接收10個(gè)字節(jié)。
(4)UDP的緩沖區(qū)
????????UDP沒有真正意義上的?發(fā)送緩沖區(qū)。調(diào)用sendto會(huì)直接交給內(nèi)核,由內(nèi)核將數(shù)據(jù)傳給網(wǎng)絡(luò)層協(xié)議進(jìn)行后續(xù)的傳輸動(dòng)作。
????????UDP具有接收緩沖區(qū),但是這個(gè)接收緩沖區(qū)不能保證收到的UDP報(bào)的順序和發(fā)送UDP報(bào)的順序一致。如果緩沖區(qū)滿了, 再到達(dá)的UDP數(shù)據(jù)就會(huì)被丟棄。
????????UDP的socket既能讀,也能寫。這個(gè)概念叫做 全雙工。
(5)注意事項(xiàng)
????????我們注意到,UDP協(xié)議首部中有一個(gè)16位的最大長(zhǎng)度。也就是說一個(gè)UDP能傳輸?shù)臄?shù)據(jù)最大長(zhǎng)度是64K(包含UDP首部)。
????????然而64K在當(dāng)今的互聯(lián)網(wǎng)環(huán)境下,是一個(gè)非常小的數(shù)字。
????????如果我們需要傳輸?shù)臄?shù)據(jù)超過64K,就需要在應(yīng)用層手動(dòng)的分包,多次發(fā)送,并在接收端手動(dòng)拼裝。
(6)基于UDP的應(yīng)用層協(xié)議
NFS: 網(wǎng)絡(luò)文件系統(tǒng)
TFTP: 簡(jiǎn)單文件傳輸協(xié)議
DHCP: 動(dòng)態(tài)主機(jī)配置協(xié)議
BOOTP: 啟動(dòng)協(xié)議(用于無盤設(shè)備啟動(dòng))
DNS: 域名解析協(xié)議
????????當(dāng)然, 也包括我們自己寫UDP程序時(shí)自定義的應(yīng)用層協(xié)議
3.TCP協(xié)議
????????TCP全稱為 "傳輸控制協(xié)議(Transmission Control Protocol")。需要對(duì)數(shù)據(jù)的傳輸進(jìn)行一個(gè)詳細(xì)的控制。
(1)TCP協(xié)議端格式
① 源/目的端口號(hào): 表示數(shù)據(jù)是從哪個(gè)進(jìn)程來, 到哪個(gè)進(jìn)程去;
② 32位序號(hào): 發(fā)送出去的序號(hào)。
? ? 32位確認(rèn)號(hào): 為了告訴發(fā)送方特定序號(hào)之前的已經(jīng)全都收到了
? ? ? ? 序號(hào)和確認(rèn)號(hào)的目的是為了保證可靠性(可靠性具體看下面的確認(rèn)應(yīng)答(ACK)機(jī)制)。
例如:
????????客戶端發(fā)送的序號(hào)為1、2、3、5、6、7,服務(wù)端全部都收到了,那么服務(wù)端發(fā)送的確認(rèn)號(hào)是多少呢?
? ? ? ? 答案是4,因?yàn)檫@里雖然最后的序號(hào)到7了,但是沒有4,服務(wù)端只能保證4前面的序號(hào)全都收到了,所以發(fā)送的確認(rèn)號(hào)是4。
????????這樣看只需要一個(gè)序號(hào)就夠了,又為什么要有一個(gè)序號(hào)和一個(gè)確認(rèn)號(hào)呢?
? ? ? ? 因?yàn)門CP協(xié)議是全雙工的,在發(fā)消息的同時(shí),也可以收消息。
? ? ? ? 如果服務(wù)端在給客戶端應(yīng)答的時(shí)候,又給客戶端發(fā)消息,這就一定需要兩個(gè)序號(hào)(既要有確認(rèn)號(hào),又要攜帶自己的序號(hào))。
? ? ? ?
????????報(bào)文在發(fā)送的時(shí)候,還可能是亂序到達(dá)的,這是不可靠的一種。
? ? ? ? 這就需要讓我們的報(bào)文進(jìn)行按序到達(dá),這怎么做到?
? ? ? ? 這就要用到序號(hào),根據(jù)序號(hào)進(jìn)行排序就相當(dāng)于按序到達(dá)了,這時(shí)序號(hào)的第二個(gè)作用。
? ? ? ? 序號(hào)的第三個(gè)作用是在超時(shí)重傳中用來去重。
③ 4位TCP報(bào)頭長(zhǎng)度:
????????報(bào)頭長(zhǎng)度出現(xiàn)的原因是因?yàn)閳?bào)頭的標(biāo)準(zhǔn)長(zhǎng)度是20字節(jié),但是也可以大于這個(gè)數(shù),上圖中下面的選項(xiàng)就是可擴(kuò)展空間(這個(gè)空間是40字節(jié))。因此有了TCP報(bào)頭長(zhǎng)度。TCP報(bào)頭長(zhǎng)度有4個(gè)bit位,大小最大是15,但是這里的單位是4字節(jié),即1代表4字節(jié),2代表8字節(jié),所以TCP頭部最大長(zhǎng)度是15 * 4 = 60字節(jié)。因此實(shí)際范圍是 5-15(長(zhǎng)度最低為20字節(jié),最高為60字節(jié))。
④ 6位標(biāo)志位:
URG: 緊急指針標(biāo)記位。
? ? ? ? 上面中我們說到序號(hào)可以用來排序,使之按序到達(dá)。但是如果數(shù)據(jù)必須在TCP中進(jìn)行按序到達(dá)的話,那么有些數(shù)據(jù)優(yōu)先級(jí)更高,但是序號(hào)較晚,那么怎么辦?
? ? ? ? 這就可以將URG置1,讓數(shù)據(jù)被緊急處理,即優(yōu)先處理該數(shù)據(jù),而不用被序號(hào)所限制。
? ? ? ? 緊急數(shù)據(jù)通常是用來作狀態(tài)詢問的(詢問另一端主機(jī)出現(xiàn)異常情況時(shí)當(dāng)前的狀態(tài))。
ACK: 確認(rèn)標(biāo)記位,表示該報(bào)文是對(duì)歷史報(bào)文的確認(rèn)(不僅包括確認(rèn),也可能會(huì)有給對(duì)方發(fā)的消息)【一般在大部分正式通信的情況下,ACK都是1】
PSH: 提示接收端應(yīng)用程序立刻從TCP緩沖區(qū)把數(shù)據(jù)讀走。(當(dāng)發(fā)送端發(fā)給接收的數(shù)據(jù)超過某一個(gè)大小,或者接收端的接收緩沖區(qū)要滿了,那么發(fā)送端就把PSH置1,提醒接收端可以進(jìn)行讀取了)。
RST: 對(duì)方要求重新建立連接,我們把攜帶RST標(biāo)識(shí)的稱為復(fù)位報(bào)文段。
? ? ? ?如何建立連接,具體看下面三次握手四次揮手。
???????比如我們?cè)L問一個(gè)網(wǎng)站,跟這個(gè)網(wǎng)站建立了一個(gè)連接,時(shí)間長(zhǎng)不用,那么OS就可以把客戶端的連接關(guān)了,服務(wù)器也可能把服務(wù)端關(guān)了,這時(shí)就會(huì)導(dǎo)致一個(gè)認(rèn)為建立了連接,一個(gè)認(rèn)為沒有建立連接。這時(shí)雙方進(jìn)行通信的時(shí)候,一方收到了一個(gè)沒有建立連接就發(fā)送的數(shù)據(jù),就會(huì)對(duì)這個(gè)數(shù)據(jù)進(jìn)行應(yīng)答,并將RST置1,告知它立刻將連接重置,重新建立連接。
? ? ? ? 也可能出現(xiàn)在丟包率比較高或者服務(wù)器的壓力比較大的情況。
SYN: 請(qǐng)求建立連接,只要報(bào)文時(shí)健建立連鏈接的請(qǐng)求,SYN都需要被設(shè)置位1。我們把攜帶SYN標(biāo)識(shí)的稱為同步報(bào)文段
FIN: 請(qǐng)求端斷開鏈接。我們稱攜帶FIN標(biāo)識(shí)的為結(jié)束報(bào)文段。
⑤ 16位窗口大小:?
? ? ? ? 上圖中,調(diào)用write/send這樣的函數(shù),并不是直接通過網(wǎng)絡(luò)寫入到對(duì)方,而是先寫入到發(fā)送緩沖區(qū)(OS)中,然后什么時(shí)候發(fā)送就由OS來決定了,read/recv也是如此,是讀取接收緩沖區(qū)中的數(shù)據(jù)。
????????IO函數(shù),本質(zhì)都是拷貝函數(shù)。
? ? ? ? 因此,數(shù)據(jù)什么時(shí)候發(fā)送,發(fā)送發(fā)送多少,出錯(cuò)了怎么辦,要不要添加提高效率的策略。這些都是由OS內(nèi)的TCP自主決定的,所以TCP才叫做傳輸控制協(xié)議。
? ? ? ? TCP要發(fā)送數(shù)據(jù)在拷貝數(shù)據(jù)到緩沖區(qū)的時(shí)候,不妨礙對(duì)方發(fā)送數(shù)據(jù),因此使用一個(gè)文件描述符可以既讀又寫,這就說明TCP通信是全雙工的。
那么如果client端發(fā)送的太快了,server的接收緩沖區(qū)滿了無法接收了怎么辦?
? ? ? ? 這就需要讓client知道server的接收能力(接收緩沖區(qū)剩余空間的大小)。
? ? ? ? client怎么知道呢?這就要靠應(yīng)答了。
? ? ? ? 應(yīng)答本質(zhì)就是要包含TCP報(bào)頭,TCP報(bào)頭可以有保存server接收能力的屬性字段,這就是窗口大小。
? ? ? ? 如果client發(fā)送報(bào)文,那么填充的窗口大小就一定是自己的,因?yàn)閏lient是要發(fā)送給對(duì)方的,所以就要發(fā)送自己接收緩沖區(qū)剩余空間的大小給對(duì)方,讓server知道,然后在server再發(fā)送過來時(shí),就可以根據(jù)client發(fā)送來的報(bào)文中的窗口大小來判斷是否要繼續(xù)發(fā)送。
? ? ? ? 反過來,server發(fā)送給client也是如此。
? ? ? ? 這種策略叫做流量控制,流量控制是雙向的。
? ? ? ? 這個(gè)雖然只有16位bit位,即最大為64kb,但是下面的選項(xiàng)是可以選擇擴(kuò)大這個(gè)空間的,所有實(shí)際上這個(gè)空間的大小還是比較大的。
⑥ 16位校驗(yàn)和: 發(fā)送端填充, CRC校驗(yàn)。接收端校驗(yàn)不通過, 則認(rèn)為數(shù)據(jù)有問題。此處的檢驗(yàn)和不光包含TCP首部, 也包含TCP數(shù)據(jù)部分。
⑦ 16位緊急指針: 標(biāo)識(shí)哪部分?jǐn)?shù)據(jù)是緊急數(shù)據(jù)。
? ? ? ? 上面在6位標(biāo)志位中,URG為緊急指針標(biāo)記位,如果該位為1,就說明存在緊急數(shù)據(jù),這16位緊急指針就標(biāo)識(shí)的是哪個(gè)數(shù)據(jù)是緊急數(shù)據(jù)(這個(gè)數(shù)據(jù)只有1個(gè)字節(jié))
⑧ 40字節(jié)頭部選項(xiàng): 暫時(shí)忽略。
(2)確認(rèn)認(rèn)答(ACK)機(jī)制
????????這個(gè)機(jī)制是為了解決可靠性問題的。
????????首先我們要知道什么是不可靠的:丟包,亂序,校驗(yàn)失敗...
那么怎么確認(rèn)一個(gè)報(bào)文是否丟失呢?
????????采用確認(rèn)應(yīng)答機(jī)制。
? ? ? ? 客戶端發(fā)送給服務(wù)端一個(gè)消息時(shí),只要客戶端得到了服務(wù)端的應(yīng)答就說明客戶端發(fā)送的消息服務(wù)端100%收到了。
? ? ? ? 客戶端收到了應(yīng)答,就可以確認(rèn)沒有丟失;而沒有收到,則是不確定服務(wù)端是否收到。
? ? ? ? 這時(shí)我們會(huì)發(fā)現(xiàn)這樣一種情況:永遠(yuǎn)會(huì)有一條最新的數(shù)據(jù)是沒有應(yīng)答的(這個(gè)也是沒辦法的,但是只有這一條影響不大)。
? ? ? ? 因此是不存在完全100%可靠的,但是有局部完全可靠的TCP。
? ? ? ? 根據(jù)這個(gè)確認(rèn)應(yīng)答機(jī)制,就在報(bào)頭中有了序號(hào)和確認(rèn)號(hào)。
????????TCP將每個(gè)字節(jié)的數(shù)據(jù)都進(jìn)行了編號(hào),即為序號(hào):
(3)超時(shí)重傳機(jī)制
????????主機(jī)A發(fā)送數(shù)據(jù)給B之后, 可能因?yàn)榫W(wǎng)絡(luò)擁堵等原因, 數(shù)據(jù)無法到達(dá)主機(jī)B。
????????如果主機(jī)A在一個(gè)特定時(shí)間間隔內(nèi)沒有收到B發(fā)來的確認(rèn)應(yīng)答, 就會(huì)進(jìn)行重發(fā)。
但是, 主機(jī)A未收到B發(fā)來的確認(rèn)應(yīng)答,也可能是因?yàn)橹鳈C(jī)應(yīng)答的ACK丟失了。
????????因此主機(jī)B會(huì)收到很多重復(fù)數(shù)據(jù)。那么TCP協(xié)議需要能夠識(shí)別出那些包是重復(fù)的包,并且把重復(fù)的丟棄掉。這時(shí)候我們可以利用前面提到的序列號(hào),就可以很容易做到去重的效果。
那么, 超時(shí)的時(shí)間如何確定?
????????最理想的情況下, 找到一個(gè)最小的時(shí)間, 保證 "確認(rèn)應(yīng)答一定能在這個(gè)時(shí)間內(nèi)返回"。
????????但是這個(gè)時(shí)間的長(zhǎng)短, 隨著網(wǎng)絡(luò)環(huán)境的不同, 是有差異的。????????如果超時(shí)時(shí)間設(shè)的太長(zhǎng), 會(huì)影響整體的重傳效率;如果超時(shí)時(shí)間設(shè)的太短, 有可能會(huì)頻繁發(fā)送重復(fù)的包。
TCP為了保證無論在任何環(huán)境下都能比較高性能的通信,因此會(huì)動(dòng)態(tài)計(jì)算這個(gè)最大超時(shí)時(shí)間。
????????Linux中(BSD Unix和Windows也是如此),超時(shí)以500ms為一個(gè)單位進(jìn)行控制, 每次判定超時(shí)重發(fā)的超時(shí)時(shí)間都是500ms的整數(shù)倍。
????????如果重發(fā)一次之后, 仍然得不到應(yīng)答, 等待 2 * 500ms 后再進(jìn)行重傳。
????????如果仍然得不到應(yīng)答, 等待 4 * 500ms 進(jìn)行重傳。依次類推,以指數(shù)形式遞增。
????????累計(jì)到一定的重傳次數(shù),TCP會(huì)認(rèn)為網(wǎng)絡(luò)或者對(duì)端主機(jī)出現(xiàn)異常,強(qiáng)制關(guān)閉連接。
(4)連接管理機(jī)制(3握4揮)
????????在正常情況下, TCP要經(jīng)過三次握手建立連接, 四次揮手?jǐn)嚅_連接。
????????TCP是面向連接的,那么如何理解連接?? ? ? ?
????????TCP的服務(wù)端會(huì)收到大量的連接,那么OS就需要管理這些連接,就需要先描述,再組織。
? ? ? ? 建立好連接后,連接雙方一定要為了維護(hù)連接建立結(jié)構(gòu)體,包括建立的時(shí)間,起始序號(hào)等等(建立連接是需要花時(shí)間和空間的,維護(hù)連接時(shí)需要成本的)。
????????為什么是三次握手呢?? ? ? ?
????????三次握手是不一定成功的,雖然TCP保證可靠性,但是不代表三次握手一定成功。不一定成功的原因是因?yàn)樽詈笠淮挝帐謺r(shí)是沒有應(yīng)答的。
? ? ? ? 如果只有一次握手,那么服務(wù)端收到客戶端的SYN就會(huì)建立連接。這時(shí)如果一臺(tái)主機(jī)不斷的向服務(wù)端發(fā)送SYN,那么一臺(tái)主機(jī)就會(huì)讓服務(wù)端的資源被耗光,從而讓正常的連接無法建立,這種攻擊方式叫做 SYN洪水。??
? ? ? ? 如果是兩次握手,與一次握手類似,客戶端發(fā)送SYN請(qǐng)求時(shí),服務(wù)端在應(yīng)答時(shí)就需要做維護(hù)連接的工作了,這時(shí)依舊會(huì)很容易的受到SYN洪水攻擊。
? ? ? ? 而三次握手就可以把最后一次的確認(rèn)機(jī)會(huì)交給服務(wù)端,在第二次握手后,客戶端必須要做出應(yīng)答,這時(shí)服務(wù)端收到了這個(gè)應(yīng)答才會(huì)去建立連接。在這個(gè)過程中,客戶端為了做出應(yīng)答就必須也要為了維護(hù)連接創(chuàng)建結(jié)構(gòu)體。這樣如果這個(gè)主機(jī)想要攻擊服務(wù)端,那么就很難了。主機(jī)和服務(wù)端會(huì)同時(shí)消耗資源,而主機(jī)可用資源一定比服務(wù)端小的多,就沒法通過一臺(tái)主機(jī)使得服務(wù)端崩潰。并且客戶端因?yàn)樽龀隽藨?yīng)答需要維護(hù)一個(gè)完整的連接,而服務(wù)端只需要維護(hù)一個(gè)半連接(因?yàn)槟壳斑B接沒有成功),所以服務(wù)端消耗的資源頁(yè)更少。奇數(shù)次握手可以把最后一次報(bào)文丟失的成本嫁接給客戶端。
? ? ? ? 不過三次握手還是會(huì)受到SYN洪水攻擊的,但是服務(wù)端不會(huì)隨便被一臺(tái)主機(jī)被攻擊就崩潰了。如果黑客通過散播一些病毒程序,入侵到別人的主機(jī)中,然后在某一個(gè)時(shí)間令這些主機(jī)同時(shí)對(duì)一個(gè)服務(wù)端建立連接,就會(huì)導(dǎo)致服務(wù)端在很短的時(shí)間內(nèi)建立很多連接,導(dǎo)致崩潰。
? ? ? ? 還有一個(gè)理由是客戶端和服務(wù)端都有一個(gè)發(fā)送的過程和一個(gè)接收的過程,就可以驗(yàn)證客戶端和服務(wù)端的輸入輸出是否正常,即驗(yàn)證全雙工。
????????三次握手以最小成本的方式驗(yàn)證了全雙工(握手次數(shù)大于三次就沒有意義了,再增加次數(shù)只會(huì)導(dǎo)致效率的降低)。
????????如果在第三次握手是,客戶端發(fā)出的ACK應(yīng)答,服務(wù)端沒有收到,那么客戶端會(huì)建立連接,并且認(rèn)為連接建立好了,而服務(wù)端則不會(huì)建立連接,認(rèn)為連接沒有建立好,那么此時(shí)客戶端會(huì)做什么呢?
? ? ? ? 客戶端既然已經(jīng)認(rèn)為連接建立好了,當(dāng)然會(huì)直接給服務(wù)端發(fā)送數(shù)據(jù)了。服務(wù)端此時(shí)是認(rèn)為沒有建立好連接的,但是服務(wù)端發(fā)現(xiàn)客戶端傳輸過來數(shù)據(jù)了,這時(shí)服務(wù)端就會(huì)意識(shí)到客戶端的連接建立是有問題的。因此,服務(wù)端就會(huì)立刻給服務(wù)端發(fā)來的數(shù)據(jù)進(jìn)行ACK響應(yīng),將響應(yīng)報(bào)文的RST標(biāo)記位置為1,告知客戶端將連接進(jìn)行重置。
? ? ? ? 為什么是四次揮手呢?
? ? ? ? 主要原因就是其中一端想要斷開連接要收到ACK,而另一端要斷開連接,也要收到這一端的ACK嗎,因此一共四次。
? ? ? ??第一次揮手是其中一端想要與另一端斷開連接,第二次揮手是另一端對(duì)該端斷開連接的應(yīng)答。第三次揮手則是另一端和該端斷開連接,第四次揮手是該端對(duì)另一端斷開連接的應(yīng)答。
? ? ? ? 第二次揮手和第四次揮手都是對(duì)對(duì)方FIN斷開連接的應(yīng)答,這個(gè)必須要有。
? ? ? ? 前兩次揮手結(jié)束后,只代表該端與另一端斷開了連接,但是另一端還是可以給該端發(fā)送數(shù)據(jù)的,等到另一端的數(shù)據(jù)發(fā)送完后,再進(jìn)行第三次揮手和第四次揮手,使另一端和該端也斷開連接。
? ? ? ? 這里之所以不是兩次揮手直接互相FIN斷開連接的原因就是該端和另一端斷開了連接,但是另一端不一定想和該端斷開連接,可能還有數(shù)據(jù)沒有發(fā)送完。
? ? ? ? 特殊情況:可能會(huì)存在三次揮手的情況,當(dāng)其中一端發(fā)送FIN要斷開連接,此時(shí)另一端也完成了數(shù)據(jù)的傳輸想要發(fā)送FIN斷開連接,那么這另一端就可以同時(shí)發(fā)送FIN和ACK,這一端接收到后,再發(fā)送ACK應(yīng)答即可。
? ? ? ? 四次揮手一定會(huì)成功嗎(包括TIME_WAIT問題)?
? ? ? ? 不一定。最后一次ACK應(yīng)答時(shí),是不能確定另一端一定能收到的,并且發(fā)出端如果在發(fā)出ACK應(yīng)答時(shí)立刻從TIME_WAIT變成CLOSED,那么此時(shí)ACK可能在發(fā)送的過程中,就導(dǎo)致出現(xiàn)問題。因此發(fā)出端會(huì)在TIME_WAIT等一段時(shí)間。
? ? ? ? 這個(gè)時(shí)間就與另一端有關(guān)了,因?yàn)榈谌挝帐质橇硪欢税l(fā)出的,如果過了一段時(shí)間還沒有收到ACK應(yīng)答,那么它就知道是該端的應(yīng)答出現(xiàn)了問題。于是另一端就會(huì)進(jìn)行重傳FIN,如果該端收到了重傳的FIN(這個(gè)時(shí)間段一直處于TIME_WAIT),那么該端就知道自己的ACK應(yīng)答出了問題,也會(huì)重發(fā)。而如果在這個(gè)時(shí)間段內(nèi)沒有收到,就說明另一端成功收到ACK應(yīng)答了,該端也就可以進(jìn)入CLOSED了。這個(gè)處于TIME_WAIT時(shí)間一般是兩個(gè)MSL(最長(zhǎng)報(bào)文段壽命)時(shí)間。
? ? ? ? 這個(gè)TIME_WAIT雖然有用,但是會(huì)引起bind失敗。服務(wù)器需要處理大量的客戶端的連接,如果此時(shí)連接過多,那么服務(wù)器想要主動(dòng)關(guān)閉連接重啟,就會(huì)產(chǎn)生大量的TIME_WAIT,這時(shí)為了防止出現(xiàn)這種情況就沒法直接關(guān)閉連接了。
? ? ? ? 于是就可以通過 setsockopt()函數(shù)來解決:
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
? ? ? ? 這個(gè)函數(shù)的作用是允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符。
listen第二個(gè)參數(shù):
????????listen的第二個(gè)參數(shù):用于維護(hù)連接的長(zhǎng)度(如果是2,服務(wù)端就可以連接3個(gè)客戶端)。
? ? ? ? 不能太長(zhǎng),也不能沒有。
? ? ? ? 不能沒有的原因:為了讓服務(wù)器在有閑置的情況下,可以從底層拿一個(gè)去進(jìn)行連接。如果沒有,就會(huì)導(dǎo)致服務(wù)器出現(xiàn)閑置情況,浪費(fèi)資源。
? ? ? ? 不能太長(zhǎng)的原因:太長(zhǎng)會(huì)影響客戶體驗(yàn),同時(shí)也會(huì)過于占用系統(tǒng)資源,反而可能導(dǎo)致服務(wù)器效率變低。
服務(wù)端狀態(tài)轉(zhuǎn)化:
? ? ? ? [CLOSED -> LISTEN] 服務(wù)器端調(diào)用listen后進(jìn)入LISTEN狀態(tài), 等待客戶端連接;
????????[LISTEN -> SYN_RCVD] 一旦監(jiān)聽到連接請(qǐng)求(同步報(bào)文段), 就將該連接放入內(nèi)核等待隊(duì)列中, 并向客戶端發(fā)送SYN確認(rèn)報(bào)文.????????[SYN_RCVD -> ESTABLISHED] 服務(wù)端一旦收到客戶端的確認(rèn)報(bào)文, 就進(jìn)入ESTABLISHED狀態(tài), 可以進(jìn)行讀寫數(shù)據(jù)了.
????????[ESTABLISHED -> CLOSE_WAIT] 當(dāng)客戶端主動(dòng)關(guān)閉連接(調(diào)用close), 服務(wù)器會(huì)收到結(jié)束報(bào)文段, 服務(wù)器返回確認(rèn)報(bào)文段并進(jìn)入CLOSE_WAIT;
????????[CLOSE_WAIT -> LAST_ACK] 進(jìn)入CLOSE_WAIT后說明服務(wù)器準(zhǔn)備關(guān)閉連接(需要處理完之前的數(shù)據(jù)); 當(dāng)服務(wù)器真正調(diào)用close關(guān)閉連接時(shí), 會(huì)向客戶端發(fā)送FIN, 此時(shí)服務(wù)器進(jìn)入LAST_ACK狀態(tài), 等待最后一個(gè)ACK到來(這個(gè)ACK是客戶端確認(rèn)收到了FIN)
????????[LAST_ACK -> CLOSED] 服務(wù)器收到了對(duì)FIN的ACK, 徹底關(guān)閉連接.
客戶端狀態(tài)轉(zhuǎn)化:
????????[CLOSED -> SYN_SENT] 客戶端調(diào)用connect, 發(fā)送同步報(bào)文段;
????????[SYN_SENT -> ESTABLISHED] connect調(diào)用成功, 則進(jìn)入ESTABLISHED狀態(tài), 開始讀寫數(shù)據(jù);
????????[ESTABLISHED -> FIN_WAIT_1] 客戶端主動(dòng)調(diào)用close時(shí), 向服務(wù)器發(fā)送結(jié)束報(bào)文段, 同時(shí)進(jìn)入FIN_WAIT_1;
????????[FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務(wù)器對(duì)結(jié)束報(bào)文段的確認(rèn), 則進(jìn)入FIN_WAIT_2, 開始等待服務(wù)器的結(jié)束報(bào)文段;
????????[FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務(wù)器發(fā)來的結(jié)束報(bào)文段, 進(jìn)入TIME_WAIT, 并發(fā)出LAST_ACK;
????????[TIME_WAIT -> CLOSED] 客戶端要等待一個(gè)2MSL(Max Segment Life, 報(bào)文最大生存時(shí)間)的時(shí)間, 才會(huì)進(jìn)入CLOSED狀態(tài).
TCP狀態(tài)轉(zhuǎn)換的一個(gè)匯總:
較粗的虛線表示服務(wù)端的狀態(tài)變化情況;
較粗的實(shí)線表示客戶端的狀態(tài)變化情況;
CLOSED是一個(gè)假想的起始點(diǎn), 不是真實(shí)狀態(tài);
TIME_WAIT狀態(tài):
? ? ? ? 如果首先啟動(dòng)server,然后啟動(dòng)client,然后用Ctrl-C使server終止,這時(shí)馬上再運(yùn)行server,會(huì)bind失敗,因?yàn)殡m然server的應(yīng)用程序終止了,但TCP協(xié)議層的連接并沒有完全斷開,因此不能再次監(jiān)聽同樣的server端口.
????????TCP協(xié)議規(guī)定,主動(dòng)關(guān)閉連接的一方要處于TIME_ WAIT狀態(tài),等待兩個(gè)MSL(maximum segment lifetime)的時(shí)間后才能回到CLOSED狀態(tài).
????????我們使用Ctrl-C終止了server, 所以server是主動(dòng)關(guān)閉連接的一方, 在TIME_WAIT期間仍然不能再次監(jiān)聽同樣的server端口;
????????MSL在RFC1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實(shí)現(xiàn)不同, 在Centos7上默認(rèn)配置的值是60s;
????????可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
為什么是TIME_WAIT的時(shí)間是2MSL?
????????MSL是TCP報(bào)文的最大生存時(shí)間, 因此TIME_WAIT持續(xù)存在2MSL的話
????????就能保證在兩個(gè)傳輸方向上的尚未被接收或遲到的報(bào)文段都已經(jīng)消失(否則服務(wù)器立刻重啟, 可能會(huì)收到來自上一個(gè)進(jìn)程的遲到的數(shù)據(jù), 但是這種數(shù)據(jù)很可能是錯(cuò)誤的);
????????同時(shí)也是在理論上保證最后一個(gè)報(bào)文可靠到達(dá)(假設(shè)最后一個(gè)ACK丟失, 那么服務(wù)器會(huì)再重發(fā)一個(gè)FIN. 這時(shí)雖然客戶端的進(jìn)程不在了, 但是TCP連接還在, 仍然可以重發(fā)LAST_ACK.
TIME_WAIT狀態(tài)引起的bind失敗的原因:
????????在server的TCP連接沒有完全斷開之前不允許重新監(jiān)聽, 某些情況下可能是不合理的.
????????服務(wù)器需要處理非常大量的客戶端的連接(每個(gè)連接的生存時(shí)間可能很短, 但是每秒都有很大數(shù)量的客戶端來請(qǐng)求).
????????這個(gè)時(shí)候如果由服務(wù)器端主動(dòng)關(guān)閉連接(比如某些客戶端不活躍, 就需要被服務(wù)器端主動(dòng)清理掉), 就會(huì)產(chǎn)生大量TIME_WAIT連接.
????????由于我們的請(qǐng)求量很大, 就可能導(dǎo)致TIME_WAIT的連接數(shù)很多, 每個(gè)連接都會(huì)占用一個(gè)通信五元組(源ip,源端口, 目的ip, 目的端口, 協(xié)議). 其中服務(wù)器的ip和端口和協(xié)議是固定的. 如果新來的客戶端連接的ip和端口號(hào)和TIME_WAIT占用的鏈接重復(fù)了, 就會(huì)出現(xiàn)問題.
解決TIME_WAIT狀態(tài)引起的bind失敗的方法:
?????????通過 setsockopt()函數(shù)來解決:
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
? ? ? ? 這個(gè)函數(shù)的作用是允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符。
CLOSE_WAIT狀態(tài):
????????對(duì)于服務(wù)器上出現(xiàn)大量的 CLOSE_WAIT 狀態(tài), 原因就是服務(wù)器沒有正確的關(guān)閉 socket, 導(dǎo)致四次揮手沒有正確完成. 這是一個(gè) BUG. 只需要加上對(duì)應(yīng)的 close 即可解決問題
(5)滑動(dòng)窗口
????????當(dāng)一次發(fā)送一批報(bào)文出現(xiàn)丟失怎么辦?超時(shí)重傳?
? ? ? ? 當(dāng)我們得知已經(jīng)丟包的時(shí)候,這段時(shí)間內(nèi)有一個(gè)檢測(cè)丟包的超時(shí)重傳的時(shí)間窗口,超時(shí)了能重傳意味著數(shù)據(jù)一發(fā)出不能把發(fā)送緩沖區(qū)內(nèi)的數(shù)據(jù)立刻清除,而要把它暫時(shí)保存起來。
?
????????對(duì)每一個(gè)發(fā)送的數(shù)據(jù)段,都要給一個(gè)ACK確認(rèn)應(yīng)答,收到ACK后再發(fā)送下一個(gè)數(shù)據(jù)段。這樣做有一個(gè)比較大的缺點(diǎn),就是性能較差,尤其是數(shù)據(jù)往返的時(shí)間長(zhǎng)的時(shí)候。
????????既然這樣一發(fā)一收的方式性能較低, 那么我們一次發(fā)送多條數(shù)據(jù), 就可以大大的提高性能(其實(shí)是將多個(gè)段的等待時(shí)間重疊在一起了)。
?
?????????窗口大小指的是無需等待確認(rèn)應(yīng)答而可以繼續(xù)發(fā)送數(shù)據(jù)的最大值. 上圖的窗口大小就是4000個(gè)字節(jié)(四個(gè)段).
????????發(fā)送前四個(gè)段的時(shí)候, 不需要等待任何ACK, 直接發(fā)送;
????????收到第一個(gè)ACK后, 滑動(dòng)窗口向后移動(dòng), 繼續(xù)發(fā)送第五個(gè)段的數(shù)據(jù); 依次類推;
????????操作系統(tǒng)內(nèi)核為了維護(hù)這個(gè)滑動(dòng)窗口, 需要開辟 發(fā)送緩沖區(qū) 來記錄當(dāng)前還有哪些數(shù)據(jù)沒有應(yīng)答; 只有確認(rèn)應(yīng)答過的數(shù)據(jù), 才能從緩沖區(qū)刪掉;????????窗口越大, 則網(wǎng)絡(luò)的吞吐率就越高;
?
滑動(dòng)窗口一定會(huì)向右移動(dòng)嗎?滑動(dòng)窗口可以變大嗎?可以變小嗎?
????????滑動(dòng)窗口只會(huì)向右移動(dòng),但也可能不動(dòng)。
? ? ? ? 滑動(dòng)窗口既可以變大也可以變小。
滑動(dòng)窗口的大小由什么決定?
? ? ? ? 由對(duì)方的接收能力決定。就是收到的TCP數(shù)據(jù)報(bào)頭中的窗口大小。(后面到擁塞控制會(huì)修改)
那么如果出現(xiàn)了丟包, 如何進(jìn)行重傳? 這里分兩種情況討論:
情況一:數(shù)據(jù)包已經(jīng)遞達(dá),ACK被丟了。
?這種情況下, 部分ACK丟了并不要緊, 因?yàn)榭梢酝ㄟ^后續(xù)的ACK進(jìn)行確認(rèn)。
? ? ? ? 圖中的對(duì)1001,2001,3001和4001的ACK都丟了,但是后續(xù)有5001和6001的ACK,就說明前面的數(shù)據(jù)包都接收到了,前面的ACK應(yīng)答有沒有都無所謂了。
? ? ? ? 如果前面1001-4001,而5001和6001的ACK丟了,那么就只會(huì)到4001了。
情況二:數(shù)據(jù)包丟了。
????????當(dāng)某一段報(bào)文段丟失之后, 發(fā)送端會(huì)一直收到 1001 這樣的ACK, 就像是在提醒發(fā)送端 "我想要的是 1001"一樣;
????????如果發(fā)送端主機(jī)連續(xù)三次收到了同樣一個(gè) "1001" 這樣的應(yīng)答, 就會(huì)將對(duì)應(yīng)的數(shù)據(jù) 1001 - 2000 重新發(fā)送;
????????這個(gè)時(shí)候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因?yàn)?001 - 7000接收端其實(shí)之前就已經(jīng)收到了, 被放到了接收端操作系統(tǒng)內(nèi)核的接收緩沖區(qū)中)。
? ? ? ? 假設(shè)前面1001-4001,以及6001的報(bào)文都沒有丟,只有5001的報(bào)文丟了,那么也只會(huì)響應(yīng)4001,因?yàn)?001的報(bào)文丟了。
這種機(jī)制被稱為 "高速重發(fā)控制"(也叫 "快重傳")。
(6)流量控制?
????????接收端處理數(shù)據(jù)的速度是有限的. 如果發(fā)送端發(fā)的太快, 導(dǎo)致接收端的緩沖區(qū)被打滿, 這個(gè)時(shí)候如果發(fā)送端繼續(xù)發(fā)送,就會(huì)造成丟包, 繼而引起丟包重傳等等一系列連鎖反應(yīng)。
????????因此TCP根據(jù)接收端的處理能力, 來決定發(fā)送端的發(fā)送速度. 這個(gè)機(jī)制就叫做流量控制(Flow Control)。
????????接收端將自己可以接收的緩沖區(qū)大小放入 TCP 首部中的 "窗口大小" 字段, 通過ACK端通知發(fā)送端;
????????窗口大小字段越大, 說明網(wǎng)絡(luò)的吞吐量越高;
????????接收端一旦發(fā)現(xiàn)自己的緩沖區(qū)快滿了, 就會(huì)將窗口大小設(shè)置成一個(gè)更小的值通知給發(fā)送端;
????????發(fā)送端接受到這個(gè)窗口之后, 就會(huì)減慢自己的發(fā)送速度;
????????如果接收端緩沖區(qū)滿了, 就會(huì)將窗口置為0; 這時(shí)發(fā)送方不再發(fā)送數(shù)據(jù), 但是需要定期發(fā)送一個(gè)窗口探測(cè)數(shù)據(jù)段, 使接收端把窗口大小告訴發(fā)送端.
接收端如何把窗口大小告訴發(fā)送端呢?
????????回憶我們的TCP首部中, 有一個(gè)16位窗口字段, 就是存放了窗口大小信息。
????????那么問題來了, 16位數(shù)字最大表示65535, 那么TCP窗口最大就是65535字節(jié)么?
????????實(shí)際上, TCP首部40字節(jié)選項(xiàng)中還包含了一個(gè)窗口擴(kuò)大因子M, 實(shí)際窗口大小是 窗口字段的值左移 M 位。
(7)擁塞控制
? ? ? ? 在TCP的流量控制,確認(rèn)應(yīng)答,超時(shí)重傳,序號(hào),滑動(dòng)窗口的機(jī)制下,如果一千個(gè)數(shù)據(jù)出現(xiàn)幾個(gè)丟包那么是正常的,但是如果出現(xiàn)了幾百個(gè)丟包,那么就不是用戶端和服務(wù)端的關(guān)系了,而是中間出現(xiàn)了問題,即網(wǎng)絡(luò)出現(xiàn)了問題。
? ? ? ? 網(wǎng)絡(luò)出了問題是不能重傳的,一個(gè)網(wǎng)絡(luò)會(huì)連接很多服務(wù)器(winodow,linux等),一個(gè)服務(wù)器出現(xiàn)了問題,那么其它很多服務(wù)器也會(huì)出現(xiàn)問題,那么這時(shí)網(wǎng)絡(luò)本就有問題,這些服務(wù)器還都進(jìn)行重傳,只會(huì)加重服務(wù)器的壓力。所以不能進(jìn)行重傳。
? ? ? ? 這里網(wǎng)絡(luò)出現(xiàn)問題,如果是硬件的問題,那只能去解決硬件,而這里是我們談?wù)摰木W(wǎng)絡(luò)軟件出了問題:比如網(wǎng)絡(luò)中積壓了大量的報(bào)文,導(dǎo)致轉(zhuǎn)發(fā)壓力太大了,導(dǎo)致出現(xiàn)超時(shí)丟包的問題。
? ? ? ? 這種只考慮網(wǎng)絡(luò)軟件的問題叫做網(wǎng)絡(luò)擁塞問題,
????????雖然TCP有了滑動(dòng)窗口這個(gè)大殺器, 能夠高效可靠的發(fā)送大量的數(shù)據(jù). 但是如果在剛開始階段就發(fā)送大量的數(shù)據(jù), 仍然可能引發(fā)問題.
????????因?yàn)榫W(wǎng)絡(luò)上有很多的計(jì)算機(jī), 可能當(dāng)前的網(wǎng)絡(luò)狀態(tài)就已經(jīng)比較擁堵. 在不清楚當(dāng)前網(wǎng)絡(luò)狀態(tài)下, 貿(mào)然發(fā)送大量的數(shù)據(jù),是很有可能引起雪上加霜的.
????????TCP引入 慢啟動(dòng) 機(jī)制, 先發(fā)少量的數(shù)據(jù), 探探路, 摸清當(dāng)前的網(wǎng)絡(luò)擁堵狀態(tài), 再?zèng)Q定按照多大的速度傳輸數(shù)據(jù);
????????此處引入一個(gè)概念程為擁塞窗口。
????????發(fā)送開始的時(shí)候, 定義擁塞窗口大小為1;
????????每次收到一個(gè)ACK應(yīng)答, 擁塞窗口加1;
????????每次發(fā)送數(shù)據(jù)包的時(shí)候, 將擁塞窗口和接收端主機(jī)反饋的窗口大小做比較, 取較小的值作為實(shí)際發(fā)送的窗口。
????????因此:一次向目標(biāo)主機(jī)發(fā)送數(shù)據(jù)的量 = min(對(duì)方的接受能力, 擁塞窗口)
? ? ? ? 即:滑動(dòng)窗口的大小 = min(對(duì)方的窗口大小,擁塞窗口)
????????像上面這樣的擁塞窗口增長(zhǎng)速度, 是指數(shù)級(jí)別的. "慢啟動(dòng)" 只是指初期時(shí)慢, 但是增長(zhǎng)速度非???。
????????為了不增長(zhǎng)的那么快,因此不能使擁塞窗口單純的加倍,此處引入一個(gè)叫做慢啟動(dòng)的閾值,當(dāng)擁塞窗口超過這個(gè)閾值的時(shí)候, 不再按照指數(shù)方式增長(zhǎng), 而是按照線性方式增長(zhǎng)。
?
????????當(dāng)TCP開始啟動(dòng)的時(shí)候, 慢啟動(dòng)閾值等于窗口最大值;
????????在每次超時(shí)重發(fā)的時(shí)候, 慢啟動(dòng)閾值會(huì)變成原來的一半, 同時(shí)擁塞窗口置回1。
????????少量的丟包, 我們僅僅是觸發(fā)超時(shí)重傳; 大量的丟包, 我們就認(rèn)為網(wǎng)絡(luò)擁塞;
????????當(dāng)TCP通信開始后, 網(wǎng)絡(luò)吞吐量會(huì)逐漸上升; 隨著網(wǎng)絡(luò)發(fā)生擁堵, 吞吐量會(huì)立刻下降;
????????擁塞控制, 歸根結(jié)底是TCP協(xié)議想盡可能快的把數(shù)據(jù)傳輸給對(duì)方, 但是又要避免給網(wǎng)絡(luò)造成太大壓力的折中方案.
指數(shù)增長(zhǎng)前期慢,意味著前期都可以發(fā)送少量的數(shù)據(jù),增長(zhǎng)速度快
指數(shù)增長(zhǎng):前期慢 + 后期增長(zhǎng)快(可以盡快恢復(fù)網(wǎng)絡(luò)通信的正常速度)
增長(zhǎng)到一定程度,就正常的線性增長(zhǎng)
每次增長(zhǎng)到網(wǎng)絡(luò)擁塞時(shí),下一次線性增長(zhǎng)的位置就變成上次網(wǎng)絡(luò)擁塞的一半
(8)延遲應(yīng)答
????????如果接收數(shù)據(jù)的主機(jī)立刻返回ACK應(yīng)答,這時(shí)候返回的窗口可能比較小。
????????假設(shè)接收端緩沖區(qū)為1M. 一次收到了500K的數(shù)據(jù); 如果立刻應(yīng)答, 返回的窗口就是500K;
????????但實(shí)際上可能處理端處理的速度很快, 10ms之內(nèi)就把500K數(shù)據(jù)從緩沖區(qū)消費(fèi)掉了;
????????在這種情況下, 接收端處理還遠(yuǎn)沒有達(dá)到自己的極限, 即使窗口再放大一些, 也能處理過來;
????????如果接收端稍微等一會(huì)再應(yīng)答, 比如等待200ms再應(yīng)答, 那么這個(gè)時(shí)候返回的窗口大小就是1M;
????????窗口越大, 網(wǎng)絡(luò)吞吐量就越大, 傳輸效率就越高. 我們的目標(biāo)是在保證網(wǎng)絡(luò)不擁塞的情況下盡量提高傳輸效率。
????????那么所有的包都可以延遲應(yīng)答么? 肯定不是。
數(shù)量限制: 每隔N個(gè)包就應(yīng)答一次;
時(shí)間限制: 超過最大延遲時(shí)間就應(yīng)答一次;
????????具體的數(shù)量和超時(shí)時(shí)間, 依操作系統(tǒng)不同也有差異; 一般N取2, 超時(shí)時(shí)間取200ms;
?(9)捎帶應(yīng)答
????????在延遲應(yīng)答的基礎(chǔ)上, 我們發(fā)現(xiàn), 很多情況下, 客戶端服務(wù)器在應(yīng)用層也是 "一發(fā)一收" 的. 意味著客戶端給服務(wù)器說了 "How are you", 服務(wù)器也會(huì)給客戶端回一個(gè) "Fine, thank you";
????????那么這個(gè)時(shí)候ACK就可以搭順風(fēng)車, 和服務(wù)器回應(yīng)的 "Fine, thank you" 一起回給客戶端
????????在發(fā)送確認(rèn)應(yīng)答ACK等字段時(shí),順便把數(shù)據(jù)也一起發(fā)送過去,以此減少發(fā)送此時(shí),提高效率。
(10)面向字節(jié)流
? ? ? ? TCP是面向字節(jié)流的,不關(guān)心任何的數(shù)據(jù)格式,但是要正確使用這個(gè)數(shù)據(jù),必須得有特定的格式。
? ? ? ? 誰來解釋這個(gè)格式?應(yīng)用層。
? ? ? ? 創(chuàng)建一個(gè)TCP的socket, 同時(shí)在內(nèi)核中創(chuàng)建一個(gè)發(fā)送緩沖區(qū)和一個(gè)接收緩沖區(qū);
????????調(diào)用write時(shí), 數(shù)據(jù)會(huì)先寫入發(fā)送緩沖區(qū)中;
????????如果發(fā)送的字節(jié)數(shù)太長(zhǎng), 會(huì)被拆分成多個(gè)TCP的數(shù)據(jù)包發(fā)出;
????????如果發(fā)送的字節(jié)數(shù)太短, 就會(huì)先在緩沖區(qū)里等待, 等到緩沖區(qū)長(zhǎng)度差不多了, 或者其他合適的時(shí)機(jī)發(fā)送出去;
????????接收數(shù)據(jù)的時(shí)候, 數(shù)據(jù)也是從網(wǎng)卡驅(qū)動(dòng)程序到達(dá)內(nèi)核的接收緩沖區(qū);
????????然后應(yīng)用程序可以調(diào)用read從接收緩沖區(qū)拿數(shù)據(jù);
????????另一方面, TCP的一個(gè)連接, 既有發(fā)送緩沖區(qū), 也有接收緩沖區(qū), 那么對(duì)于這一個(gè)連接, 既可以讀數(shù)據(jù), 也可以寫數(shù)據(jù). 這個(gè)概念叫做全雙工。
????????由于緩沖區(qū)的存在, TCP程序的讀和寫不需要一一匹配, 例如:
????????寫100個(gè)字節(jié)數(shù)據(jù)時(shí), 可以調(diào)用一次write寫100個(gè)字節(jié), 也可以調(diào)用100次write, 每次寫一個(gè)字節(jié);
????????讀100個(gè)字節(jié)數(shù)據(jù)時(shí), 也完全不需要考慮寫的時(shí)候是怎么寫的, 既可以一次read 100個(gè)字節(jié), 也可以一次read一個(gè)字節(jié), 重復(fù)100次;
(11)粘包問題
????????首先要明確, 粘包問題中的 "包" , 是指的應(yīng)用層的數(shù)據(jù)包.
????????在TCP的協(xié)議頭中, 沒有如同UDP一樣的 "報(bào)文長(zhǎng)度" 這樣的字段, 但是有一個(gè)序號(hào)這樣的字段.
????????站在傳輸層的角度, TCP是一個(gè)一個(gè)報(bào)文過來的. 按照序號(hào)排好序放在緩沖區(qū)中.
????????站在應(yīng)用層的角度, 看到的只是一串連續(xù)的字節(jié)數(shù)據(jù).
????????那么應(yīng)用程序看到了這么一連串的字節(jié)數(shù)據(jù), 就不知道從哪個(gè)部分開始到哪個(gè)部分, 因?yàn)檫@是一個(gè)完整的應(yīng)用層數(shù)據(jù)包
????????那么如何避免粘包問題呢? 歸根結(jié)底就是一句話, 明確兩個(gè)包(報(bào)文和報(bào)文)之間的邊界。
????????對(duì)于定長(zhǎng)的包, 保證每次都按固定大小讀取即可; 例如上面的Request結(jié)構(gòu), 是固定大小的, 那么就從緩沖區(qū)從頭開始按sizeof(Request)依次讀取即可;
????????對(duì)于變長(zhǎng)的包, 可以在包頭的位置, 約定一個(gè)包總長(zhǎng)度的字段, 從而就知道了包的結(jié)束位置;
????????對(duì)于變長(zhǎng)的包, 還可以在包和包之間使用明確的分隔符(應(yīng)用層協(xié)議, 是程序員自己來定的, 只要保證分隔符不和正文沖突即可);
????????對(duì)于UDP協(xié)議來說, 是否也存在 "粘包問題" 呢?
????????對(duì)于UDP, 如果還沒有上層交付數(shù)據(jù), UDP的報(bào)文長(zhǎng)度仍然在. 同時(shí), UDP是一個(gè)一個(gè)把數(shù)據(jù)交付給應(yīng)用層. 就有很明確的數(shù)據(jù)邊界。
???????站在應(yīng)用層的角度, 使用UDP的時(shí)候, 要么收到完整的UDP報(bào)文, 要么不收. 不會(huì)出現(xiàn)"半個(gè)"的情況。
(12)TCP異常情況
進(jìn)程終止: 進(jìn)程終止會(huì)釋放文件描述符, 仍然可以發(fā)送FIN。和正常關(guān)閉沒有什么區(qū)別(正常四次揮手結(jié)束)
機(jī)器重啟: 和進(jìn)程終止的情況相同。
機(jī)器斷電/網(wǎng)線斷開: 接收端認(rèn)為連接還在, 一旦接收端有寫入操作, 接收端發(fā)現(xiàn)連接已經(jīng)不在了, 就會(huì)進(jìn)行reset。即使沒有寫入操作, TCP自己也內(nèi)置了一個(gè)?;疃〞r(shí)器, 會(huì)定期詢問對(duì)方是否還在. 如果對(duì)方不在, 也會(huì)把連接釋放。
? ? ? ? 另外, 應(yīng)用層的某些協(xié)議, 也有一些這樣的檢測(cè)機(jī)制。例如HTTP長(zhǎng)連接中, 也會(huì)定期檢測(cè)對(duì)方的狀態(tài). 例如QQ, 在QQ斷線之后, 也會(huì)定期嘗試重新連接。
(13)TCP小結(jié)
????????為什么TCP這么復(fù)雜? 因?yàn)橐WC可靠性, 同時(shí)又盡可能的提高性能。
①可靠性
校驗(yàn)和
序列號(hào)(按序到達(dá))
確認(rèn)應(yīng)答
超時(shí)重發(fā)
連接管理
流量控制
擁塞控制
②提高性能
滑動(dòng)窗口
快速重傳
延遲應(yīng)答
捎帶應(yīng)答
③其他
定時(shí)器(超時(shí)重傳定時(shí)器, ?;疃〞r(shí)器, TIME_WAIT定時(shí)器等)
(14)基于TCP的應(yīng)用層協(xié)議
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
????????當(dāng)然, 也包括自己寫TCP程序時(shí)自定義的應(yīng)用層協(xié)議
(15)TCP,UDP對(duì)比
????????TCP是可靠連接, 那么是不是TCP一定就優(yōu)于UDP呢?
????????TCP和UDP之間的優(yōu)點(diǎn)和缺點(diǎn), 不能簡(jiǎn)單、絕對(duì)的進(jìn)行比較。
????????TCP用于可靠傳輸?shù)那闆r, 應(yīng)用于文件傳輸, 重要狀態(tài)更新等場(chǎng)景;
????????UDP用于對(duì)高速傳輸和實(shí)時(shí)性要求較高的通信領(lǐng)域, 例如, 早期的QQ, 視頻傳輸?shù)? 另外UDP可以用于廣播
????????歸根結(jié)底, TCP和UDP都是程序員的工具, 什么時(shí)機(jī)用, 具體怎么用, 還是要根據(jù)具體的需求場(chǎng)景去判定。
(16)如何用UDP實(shí)現(xiàn)可靠傳輸
????????參考TCP的可靠性機(jī)制, 在應(yīng)用層實(shí)現(xiàn)類似的邏輯。
? ? ? ? 同時(shí)根據(jù)使用UDP的方向來引入對(duì)應(yīng)TCP的可靠性機(jī)制。
例如:
引入序列號(hào), 保證數(shù)據(jù)順序;
引入確認(rèn)應(yīng)答, 確保對(duì)端收到了數(shù)據(jù);
引入超時(shí)重傳, 如果隔一段時(shí)間沒有應(yīng)答, 就重發(fā)數(shù)據(jù);文章來源:http://www.zghlxwxcb.cn/news/detail-424745.html????????等等...文章來源地址http://www.zghlxwxcb.cn/news/detail-424745.html
到了這里,關(guān)于網(wǎng)絡(luò)基礎(chǔ)2【HTTP、UDP、TCP】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!