赞
踩
目录
前言:这一篇主要介绍应用层的http,以及传输层的UDP、TCP,可以说这部分是网络中最重要的一部分了。
程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都在应用层
为了满足不同的应用场景,已经有了应用层协议:http, https, DNS, ftp, smtp等。
① 直接发送同样的结构体对象,是不可取的,虽然在某些情况下,它确实行(需要进行序列化和反序列化)
② 我们需要定制协议的时候,序列化之后,我们可以将长度设置为4字节,将长度放入序列化之后的字符串的开始
struct
{
int x;int y;
char op;
};
约定:一共三个区域,用 : 分割
前两个是int,后一个是char
data d = {10, 20, '+'}
序列化,转化为字符串 10:20:+
收到"字符串",反序列化,转换为原结构体data d = {x, y, op}
序列化和反序列化就相当于在用户发送之前加了一层软件层。
下面通过利用序列化和反序列化,实现一个网络版计算器:
这里面同时实现了通过自己实现的序列化和反序列化版本,也实现了使用json来实现的序列化和反序列化版本
json是一个独立的第三方库,因此要使用json要先安装对应的库:
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
这里面因为要使用json来实现序列化和反序列化,所以要链接上对应的库 -ljson.cpp
而Method=#-DMY_SELF就是一种在Makefile中的#define
因为我们保留了自己实现的序列化和反序列化版本,并采用条件编译的方式根据传入的不同去调用不同的版本,那么既可以在代码内部去修改,也可以像这样直接在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"
-
- // 我们要在这里进行我们自己的协议定制!
- // 网络版本的计算器
-
- #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,整个序列化之后的字符串进行提取长度
- // 1. 必须具有完整的长度
- // 2. 必须具有和len相符合的有效载荷
- // 我们才返回有效载荷和len
- // 否则,我们就是一个检测函数!
- // 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
- std::string decode(std::string &in, uint32_t *len)
- {
- assert(len);
- // 1. 确认是否是一个包含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. 提取长度
- std::string inLen = in.substr(0, pos);
- int intLen = atoi(inLen.c_str());
- // 3. 确认有效载荷也是符合要求的
- int surplus = in.size() - 2 * CRLF_LEN - pos;
- if (surplus < intLen)
- return "";
- // 4. 确认有完整的报文结构
- std::string package = in.substr(pos + CRLF_LEN, intLen);
- *len = intLen;
- // 5. 将当前报文完整的从in中全部移除掉
- int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
- in.erase(0, removeLen);
- // 6. 正常返回
- return package;
- }
-
- // encode, 整个序列化之后的字符串进行添加长度
- 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;
- }
-
- // 定制的请求 x_ op y_
- class Request
- {
- public:
- Request()
- {
- }
- ~Request()
- {
- }
- // 序列化 -- 结构化的数据 -> 字符串
- // 认为结构化字段中的内容已经被填充
- 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对象,万能对象
- // 2. json是基于KV
- // 3. json有两套操作方法
- // 4. 序列化的时候,会将所有的数据内容,转换成为字符串
- Json::Value root;
- root["x"] = x_;
- root["y"] = y_;
- root["op"] = op_;
-
- Json::FastWriter fw;
- // Json::StyledWriter fw;
- *out = fw.write(root);
- #endif
- }
-
- // 反序列化 -- 字符串 -> 结构化的数据
- 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;
-
- // 转成内部成员
- 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:
- // 需要计算的数据
- int x_;
- int y_;
- // 需要进行的计算种类
- char op_; // + - * / %
- };
-
- // 定制的响应
- class Response
- {
- public:
- Response() : exitCode_(0), result_(0)
- {
- }
- ~Response()
- {
- }
- // 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
- 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);
-
- // 将反序列化的结果写入到内部成员中,形成结构化数据
- 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:
- // 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!
- int exitCode_;
- // 运算结果
- 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
-
- // 大小写转化服务
- // 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); // 我们认为我们读到的都是字符串
- 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);
- // 可以进行大小写转化了
- 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: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
- // s == 0: 代表对方关闭,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退出了,服务到此结束
- close(sock); // 如果一个进程对应的文件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); // 我们认为我们读到的都是字符串
- 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的方式得到对应的结果
- 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: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
- // s == 0: 代表对方关闭,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退出了,服务到此结束
- close(sock); // 如果一个进程对应的文件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是不是已经具有了一个strPackage
- uint32_t packageLen = 0;
- std::string package = decode(inbuffer, &packageLen);
- if (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧
- std::cout << "package: " << package << std::endl;
- // 2. 已经获得一个完整的package
- if (req.deserialize(package))
- {
- req.debug();
- // 3. 处理逻辑, 输入的是一个req,得到一个resp
- Response resp = calculator(req); //resp是一个结构化的数据
- // 4. 对resp进行序列化
- std::string respPackage;
- resp.serialize(&respPackage);
- // 5. 对报文进行encode --
- respPackage = encode(respPackage, respPackage.size());
- // 6. 简单进行发送 -- 后续处理
- 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. 创建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 填充服务器信息
- 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_对应的内核区域
- 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. 监听socket,为何要监听呢?tcp是面向连接的!
- if (listen(listenSock_, 5 /*后面再说*/) < 0)
- {
- logMessage(FATAL, "listen: %s", strerror(errno));
- exit(LISTEN_ERR);
- }
- logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
- // 运行别人来连接你了
-
- // 4. 加载线程池
- tp_ = ThreadPool<Task>::getInstance();
- }
- // static void *threadRoutine(void *args)
- // {
- // pthread_detach(pthread_self()); //设置线程分离
- // 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 的返回值是一个新的socket fd ??
- // 4.1 listenSock_: 监听 && 获取新的链接-> sock
- // 4.2 serviceSock: 给用户提供新的socket服务
- 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 提供服务, echo -> 小写 -> 大写
- // 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
- // transService(serviceSock, peerIp, peerPort);
-
- // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
- // pid_t id = fork();
- // assert(id != -1);
- // if(id == 0)
- // {
- // close(listenSock_); //建议
- // //子进程
- // transService(serviceSock, peerIp, peerPort);
- // exit(0); // 进入僵尸
- // }
- // // 父进程
- // close(serviceSock); //这一步是一定要做的!
-
- // 5.1 v1.1 版本 -- 多进程版本 -- 也是可以的
- // 爷爷进程
- // pid_t id = fork();
- // if(id == 0)
- // {
- // // 爸爸进程
- // close(listenSock_);//建议
- // // 又进行了一次fork,让 爸爸进程
- // if(fork() > 0) exit(0);
- // // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
- // transService(serviceSock, peerIp, peerPort);
- // exit(0);
- // }
- // // 父进程
- // close(serviceSock); //这一步是一定要做的!
- // // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
- // pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
- // assert(ret > 0);
- // (void)ret;
-
- // 5.2 v2 版本 -- 多线程
- // 这里不需要进行关闭文件描述符吗??不需要啦
- // 多线程是会共享文件描述符表的!
- // ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
- // pthread_t tid;
- // pthread_create(&tid, nullptr, threadRoutine, (void*)td);
-
- // 5.3 v3 版本 --- 线程池版本
- // 5.3.1 构建任务
- // 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(); 默认是阻塞等待!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(); // 我们的进程就会成为守护进程
- 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. 创建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,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
- // 2.1 先填充需要连接的远端主机的基本信息
- 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 发起请求,connect 会自动帮我们进行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 << "请输入表达式>>> "; // 1 + 1
- std::getline(std::cin, message); // 结尾不会有\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缓冲区中的数据刷新到OS
- fsync(fileno(out)); // 将OS中的数据尽快刷盘
-
- // 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:
- //等价于
- // 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服务的sock
- uint16_t port_; // client port
- std::string ip_; // client ip
- callback_t func_; // 回调方法
- 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的请求 开始啦...",\
- pthread_self(), ip_.c_str(), port_);
-
- func_(sock_, ip_, port_);
-
- logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\
- 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) //仅仅是过滤重复的判断
- {
- LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁
- if (nullptr == instance)
- {
- instance = new ThreadPool<T>();
- }
- }
-
- return instance;
- }
- //类内成员, 成员函数,都有默认参数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();
- }
- //这个任务就被拿到了线程的上下文中
- T t = tp->pop();
- tp->unlockQueue();
- t(); // 让指定的先处理这个任务
- }
- }
- 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

自己写的版本测试:
使用json写的序列化和反序列化版本测试:
上面实现网络版计算器所做的所有工作都是属于应用层。
完成这样的一个工作所应做的任务:
1.基本系统socket接口的使用
2.定制协议
3.编写业务
上面的协议是我们自定义的,但是现在已经有了一些别人写的自定义协议,因为场景的使用较多,并且非常成熟,写的很好,就成为了应用层定协议的标准,就可以被别人直接使用。
这样的协议有http, https, smtp, ftp, DNS等。
URL:
平时我们俗称的 "网址" 其实就是说的 URL。
先看一个百度的URL:
https://www.baidu.com/
域名:www.com
这里面的域名会被转换成为IP。
网络通信的本质:socket,IP+port
在使用确定协议的时候,一般显示的时候,会缺省端口号(URL上不显示端口号),所以,浏览器访问指定的url的时候,浏览器必须给我们自动添加port。
那浏览器是如何得知url匹配的port是谁呢?
特定的众所周知的服务,端口号是确定的。
httpserver->80
httpsServer-->443
sshd->22
在[0, 1023]的端口号基本上都是一些被确定的端口号,因此我们写的网络服务bind端口的时候要选择[1024, n](1024后面的端口号)。
无论我们是查阅文档,还是看音视频等,都是以网页的方式呈现的。
这种都是.html文件,而http中获取 网页资源、视频、音频的也都是文件。
http是向特定的服务器申请特定的”资源“,然后获取到本地,进行展示或者某种使用的。如果我们client没有获取的时候,资源在哪里呢?
就在我们的网络服务器(软件)所在的服务器(硬件,计算机)上。【服务器都是linux系统的,这些资源也都是文件】。
这些资源文件都在Linux服务器上。
要打开资源文件,并读取、发送给客户端的前提是:要先找到软件服务器这个文件。
Linux要找到这个文件就要通过路径来找。
在这个url里的/dir/index.htm就是这个文件的路径。
而这个路径最开始的/dir并不一定是Linux的根目录。
像 / ? : 等这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
C++经过转义会变成C%2B%2B
urlencode就是转义的过程。
而urldecode就是urlencode的逆过程
①请求和响应格式
请求格式中,请求行、请求报头和空行可以当作http协议的报头,有效载荷是自己的个人信息。
响应格式也是如此
任何协议的request or response都有:报头 + 有效载荷
http如何保证自己的报头和有效载荷被全部读取呢?
1.读取完整报头:按行读取,直到读取到空行。
2.报头能读取完毕,请求或者响应属性中,一定要包含正文的长度。(保证能读取到完整的正文)。
这里的 /a 第一个 / 不是根目录,而是web根目录,但是可以设置成为根目录。
// path = "/a/b/index.html";
// recource = "./wwwroot"; // 我们的web根目录
// recource += path; // ./wwwroot/a/b/index.html
这样就可以从web根目录开始找了
请求格式:
首行: [方法] + [url] + [版本]
Content-Type:网页显示的形式Content-Length:有效载荷
响应格式:
首行: [版本号] + [状态码] + [状态码解释]
其它与请求格式相同
②HTTP的方法
我们的网络行为无非有两种:
1.想把远端的资源到自己的本地:GET /index.html http/1.1
2.想把自己的属性字段,提交到远端:GET or POST
下面我们来测试一下GET和POST方法的区别:
测试代码:
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. 文件在哪里? 在请求的请求行中,第二个字段就是你要访问的文件
- // 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;
-
- //开始响应
- 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. 创建socket
- listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
- if (listenSock_ < 0)
- {
- exit(1);
- }
- // 2. bind绑定
- // 2.1 填充服务器信息
- 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_对应的内核区域
- if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
- {
- exit(2);
- }
-
- // 3. 监听socket,为何要监听呢?tcp是面向连接的!
- if (listen(listenSock_, 5 /*后面再说*/) < 0)
- {
- exit(3);
- }
- // 运行别人来连接你了
- }
- 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 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
- pid_t id = fork();
- assert(id != -1);
- if(id == 0)
- {
- close(listenSock_); //建议
- if(fork() > 0) exit(0);
- //孙子进程
- handlerHttpRequest(serviceSock);
- exit(0); // 进入僵尸
- }
- 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 测试</title> </head> <body> <h3>hello my server!</h3> <p>代码测试完成</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方法
先来测试 get 请求
这里我们用户名输入: xiaoliu, 密码输入: 123456abc
这里因为我们没有实现接下来的网页所有显示404,但这个不是重点。
注意看这个网址,这里的用户名和密码都显示在了url中
再来测试 post 请求:
这里界面是相同的,用户名输入: wangwu, 密码输入: 123456abc
再注意看这个网址,这里并没有显示出用户名和密码。
但是,当我们看server的响应时,注意看这里的最后一行(正文处),我们会看到我们刚刚输入的用户名和密码。(在空行的后面)
总结:
1.GET方法提交参数,会以明文方式将我们对应的参数信息,拼接到url中
2.POST方法提交参数,会以明文方式将我们对应的参数信息,拼接到http的正文中
比较 GET 和 POST:
1.GET通过url传参
2.POST通过正文传参
3.GET方法传参不私密
4.POST方法因为通过正文传参,所以相对比较私密一些(只是私密,但是都不安全)
5.一般比较大的内容通过POST方法传参(因为POST通过正文传参,GET通过url传参,正文所能包含的内容要多于url能包含的内容)
除了 get 和 post 方法,还有别的方法,但是其它方法用的都很少(尤其是DELETE):
这里最常用的就是GET方法和POST方法。
③HTTP状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
这里主要介绍一下3开头的状态码:
301:永久重定向
302:临时重定向
这里去请求访问其中一个url时,接受到了301或者302状态码,然后就会重定向到其中保存的new url中。
301和302的区别:
跟名称一样,301就是目标服务器永久不想被访问,永久取消(比如我们觉得当前url的名称不好听,想要换一个名,那么就用301,将其永久重定向到另一个url中);302就是目标服务器临时不想被访问,临时取消(比如我们想要对这个服务器进行升级,这时候不想用户访问到该url,就将其临时重定向到另一个url中,待升级完毕后,再取消该重定向)。
下面来测试,用上面的代码,只需修改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. 创建socket
- listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
- if (listenSock_ < 0)
- {
- exit(1);
- }
- // 2. bind绑定
- // 2.1 填充服务器信息
- 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_对应的内核区域
- if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
- {
- exit(2);
- }
-
- // 3. 监听socket,为何要监听呢?tcp是面向连接的!
- if (listen(listenSock_, 5 /*后面再说*/) < 0)
- {
- exit(3);
- }
- // 运行别人来连接你了
- }
- 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 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
- pid_t id = fork();
- assert(id != -1);
- if(id == 0)
- {
- close(listenSock_); //建议
- if(fork() > 0) exit(0);
- //孙子进程
- handlerHttpRequest(serviceSock);
- exit(0); // 进入僵尸
- }
- 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重定向到腾讯网
当然,用301也是可以的:
测试结果:
这里我们输入我们的测试url,就发现已经变成了腾讯网
ps:(这里不能给腾讯网截图啦,违规)
④ HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
http协议特点之一:无状态(http不会记录用户的信息)
但是用户需要被记住:会话保持
一旦登陆,就会有各种会话保持的策略
其中一种策略就是Cookie
Cookie是浏览器维护的文件,有多种存在形式:磁盘级或者内存级
比如我们进b站时,如果之前登陆了,即使关掉重进也是登陆状态,但是如果我们把Cookie里的都删掉,那么再刷新一下就会发现需要重新登陆了。
这说明http本身是无状态的,是不会保存用户的信息的,但是用户需要,所以就有了像Cookie这样的会话,去保存用户的信息。
下面我们来写个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. 文件在哪里? 在请求的请求行中,第二个字段就是你要访问的文件
- // 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;
-
- //开始响应
- 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. 创建socket
- listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
- if (listenSock_ < 0)
- {
- exit(1);
- }
- // 2. bind绑定
- // 2.1 填充服务器信息
- 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_对应的内核区域
- if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
- {
- exit(2);
- }
-
- // 3. 监听socket,为何要监听呢?tcp是面向连接的!
- if (listen(listenSock_, 5 /*后面再说*/) < 0)
- {
- exit(3);
- }
- // 运行别人来连接你了
- }
- 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 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
- pid_t id = fork();
- assert(id != -1);
- if(id == 0)
- {
- close(listenSock_); //建议
- if(fork() > 0) exit(0);
- //孙子进程
- handlerHttpRequest(serviceSock);
- exit(0); // 进入僵尸
- }
- close(serviceSock);
- wait(nullptr);
- }
- }
-
- bool quitServer()
- {
- quit_ = true;
- return true;
- }
-
- private:
- // sock
- int listenSock_;
- // port
- uint16_t port_;
- // ip
- std::string ip_;
- // 安全退出
- bool quit_;
- };

结果:
这里可以看到我们刚刚写的Cookie:this is my cookie content
这里最后一行也是Cookie的内容
但是Cookie是不安全的,某个网站你输入了账号密码,这时Cookie就保存了你所输入的账号密码,然后有像黑客这样的通过这个网站盗取了你的Cookie,那么他就可以通过该Cookie登陆上你的账号。
因此为了防止这种情况,就有了相对安全的Cookie + session(也不是绝对安全的)
Cookie + session:
当我们输入完账号密码后,不会直接返回保存,会先形成session文件(用户的临时私密信息都保存在这个文件中,session文件会自动形成文件名【具备唯一性】),然后返回该文件名的id值:session_id,浏览器收到后,就将session_id写入到本地的Cookie中。
浏览器为了证明用户存在过,就通过Cookie中的session_id来判断,如果存在就可以确定该用户存在过。
http特点之二:无连接
http协议和tcp协议是两个协议,tcp是面向连接的,这和http没有关系。http是无连接的,http只是用了tcp的能力。
Connect: close
只支持短连接,网页中可能存在图片,音视频等资源,就导致用户所看到的完整的网页内容 -- 背后可能是无数次http请求(http底层主流采用的就是tcp协议),每次都会进行3次握手、4次挥手,效率较低。因此有了长连接方案。
Connect: keep-alive
支持长连接,可以把很多次请求全部放在一起,一次进行了多次的请求和响应,不用进行多次的3次握手、四次挥手,这样就提高了效率。(只有双方都是 Connect: keep-alive 时,才可以使用长连接)。
http定义:超文本传输协议,无连接,无状态的应用层协议。
pipeline技术:什么顺序请求,就什么顺序响应(http发起了几个请求,服务端对这几个请求进行了处理,响应时按顺序进行响应)。
HTTPS也是一个应用层协议,是在HTTP协议的基础上引入了一个加密层。
HTTP协议内容都是按照文本的方式明文传输的,就导致在传输过程中出现一些被篡改的情况。
加密就是把明文(要传输的信息)进行一系列变换,生成密文。
解密就是把密文再进行一系列变化,还原成明文。
在这个加密和解密的过程中,往往需要一个或多个中间的数据,辅助进行这个过程,这样的数据称为密钥。
我们一定遇到过这样的情况:我们要下载某一个软件,但是下载之后发现变成了另一个软件,这里就是中间人(运营商)更改劫持了我们想要下载这个软件的HTTP请求,然后在响应时就更改成了另一个软件的下载地址了。目的就是为了推广更改后的那个软件。
上面的例子就是一种中间人攻击。
因为http的内容是明文传输的,明文数据会经过路由器、wifi热点、通信服务运营商、代理服务器等多个节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了,劫持者还可以篡改传输的信息且不被双方察觉,这就是中间人攻击,所以我们需要对信息进行加密。
除了上面的例子中运营商可以劫持。其他的黑客也可以用类似的手段进行劫持,来窃取用户的隐私信息,或者篡改内容。
因此,在互联网上,明文传输是比较危险的事情。
HTTPS就是在HTTP的基础上进行了加密,进一步的保证用户的信息安全。
加密就能解决上面的问题吗?
不能。
要想解决这些问题,是需要一套综合的方案的。
并且互联网是不存在绝对的安全的,只不过想要破解加密的内容需要花费的时间太长或者成本太高,就是相对安全的。
①对称加密
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,称加密,也称为单密钥加密。特征:加密和解密所用的密钥是相同的。
常见的对称加密算法:DES、3DES、AES、TDEA、Blowfish、RC2等
特点:算法公开、计算量小、加密速度快、加密效率高
②非对称加密
需要两个密钥来进行加密和解密,这两个密钥是公开密钥(简称公钥)和私有密钥(简称私钥)。
常见的非对称加密算法:RSA、DSA、ECDSA
特点:算法强度复杂,安全性依赖于算法于密钥,但是由于其算法复杂,而使得加密解密使得没有对称加密解密的速度快算
非对称加密要用到两个密钥,一个公钥,一个私钥。
公钥和私钥是配对的,最大的缺点就是运算速度非常慢,比对称加密要慢很多。
既可以通过公钥对明文加密,变成密文;通过私钥对密文解密,变成明文。也可以反着用,通过私钥对明文加密,变成密文;通过公钥对密文解密,变成明文。
数据摘要((数据指纹):其基本原理是利用单向散列函数(Hash函数)对信息进行运算,生成一串固定长度的数据摘要。数据摘要并不是一种加密机制,但可以用来判断数据数据有没有被篡改。
摘要常见算法:MD5、SHA1、SHA256、SHA512等。
算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概率非常低)。
摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密。只不过从摘要很难反推原信息,通常用来进行数据对比。
我们看看下面这两个问题:
对http进行对称加密,能否解决数据通信安全的问题?问题是什么?
为何要用非对称加密?为何不全用非对称加密?
解答:
HTTPS的工作过程
既然要保证数据安全,就需要进行“加密”
网络传输中不再直接传输明文了。而是加密之后的“密文”
加密的方式有很多,但是整体可以分为两大类:对称加密和非对称加密。
①方案1 - 只使用对称加密
如果通信双方都各自持有同一个密钥X,且没有别人知道,这两方的通信安全当然是可以被保证的(除非密钥被破解)。
引用对称加密之后,即使数据被截获,由于黑客不知道密钥是啥,因此就无法进行解密,也就不知道请求的真实内容是什么了。
但是不只是这么简单,服务器同一时刻其实是给很多客户端提供了服务的。这么多客户端,每个人用的密钥都必须是不同的(如果是相同的,那密钥就太容易扩散了,黑客就也能拿到了)。因此服务器就需要维护每个客户端和每个密钥之间的关联关系,这也很麻烦。
比较理想的做法,就是能在客户端和服务器建立连接的时候,双方协商确定这次的密钥是什么。但是如果直接把密钥明文传输,那么黑客也就能获得密钥了,此时后续的加密操作就形同虚设了。
因此密钥的传输也必须加密传输。
但是要想对密钥进行对称加密,就仍然需要先协商确定一个“密钥的密钥”,这样就成了“先有鸡还是先有蛋”的问题。此时密钥的传输再用对称加密就行不通了。
②方案2 - 只使用非对称加密
鉴于非对称加密的机制,如果服务器先把公钥以明文方式传输给浏览器,之后浏览器想服务器传数据前都先用足够公钥加密好再传,那么从客户端到服务器的信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据。
但是服务器到浏览器怎么保障安全。
如果服务器用它的私钥加密数据传给浏览器(只能用私钥加密,因为客户端没有私钥,无法解密用公钥加密的数据),那么浏览器用公钥可以解密,而这个公钥是一开始通过明文传输给浏览器的。那么如果公钥被中间人劫持了,那他也能用该公钥解密服务器传来的消息了。这一定是不安全的。
③方案3 - 双方都使用非对称加密
实现步骤:
1.服务端拥有公钥S与对应的私钥S',客户端拥有公钥C与对应的私钥C'
2.客户和服务端交换公钥
3.客户端给服务端发信息:先用S对数据加密,再发送,只能由服务器解密,因为只有服务器有私钥S'
4.服务端给客户端发信息:先用C对数据加密,再发送,只能由客户端解密,因为只有客户端有私钥C'
看起来这个方案是可行的,但是缺点很大:
效率太低
依旧有安全问题
④方案4 - 非对称加密 + 对称加密
实现步骤:
1.服务器具有非对称公钥S和私钥S‘
2.客户端发起https请求,获取服务端公钥S
3.客户端在本地生成对称密钥C,通过公钥S加密,发送给服务器
4.由于中间的网络设备没有私钥,即使截获了数据,也无法还原出内部的原文,也就无法获取到对称密钥
5.服务器通过私钥S'解密,还原出客户端发送的对称密钥C,并且使用这个对称密钥加密给客户端返回的响应数据
6.后续客户端和服务器的通信都只用对称加密即可,由于该密钥只有客户端和服务器两个主机知道,其他主机/设备不知道密钥,即使截获数据也没有意义。
由于对称加密的效率比非对称加密高很多,因此只是在开始阶段协商密钥的时候使用非对称加密,后续的传输仍然使用对称加密。
这个方案看起来没有问题,但是也是存在安全问题的。
不只这个方案,方案2,3,4都存在这个问题,如果最开始,中间人就开始攻击了呢?
中间人攻击 - 针对上面的场景
Man-in-the-MiddleAttack,简称"MITM攻击"
在方案2/3/4中,客户端获取到公钥S之后,对客户端形成的对称密钥X用服务端给客户端的公钥S进行加密,中间人即使去窃取到了数据,此时中间人确实无法解出客户端形成的密钥X,因为只有服务器有私钥S'
但是中间人的攻击,如果在最开始协商的时候就进行了,那就不一定安全了,假设黑客已经成功成为中间人。
1.服务器具有非对称加密算法的公钥S,私钥S'
2.中间人具有非对称加密算法的公钥M,私钥M'
3.客户端向服务器发送请求,服务器明文传送公钥S给客户端
4.中间人劫持数据报文,提取公钥S并保存好,然后将被劫持报文中的公钥S替换成为自己的公钥M,并将伪造报文发给客户端。
5.客户端收到报文,提取公钥M(客户端自己不知道公钥被更换过了),自己形成对称密钥X,用公钥M加密X,形成报文发送给服务器。
6.中间人劫持后,直接用自己的私钥M'进行解密,得到通信密钥X,再用曾经保存的服务端公钥S加密后,将报文推送给服务器。
7.服务器拿到报文后,用自己的私钥S'解密,得到通信密钥X
8.双方开始采用X进行对称加密,进行通信。但是一切都在中间人的掌握中,劫持数据,进行窃听甚至修改,都是可以的。
上面的攻击方案,同样适用于方案2、方案3。
问题本质在于客户端无法确定:收到的含有公钥的数据报文,是目标服务器发送过来的,还是中间人发送过来的。
CA认证
服务端在使用HTTPS前,需要向CA机构申领一份数字证书,数字证书里含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明服务端公钥的权威性。
这个证书可以理解成是一个结构化的字符串,里面包含了以下信息:
①证书发布机构
②证书有效期
③公钥
④证书所有者
⑤签名
⑥ ...
需要注意的是:申请证书的时候,需要在特定平台生成一对密钥对,即公钥和私钥。这对密钥对就是用来在网络通信中进行明文加密以及数字签名的。
其中公钥会随着CSR文件,一起发给CA进行权威认证,私钥服务端自己保留,用来后续进行通信(其实主要就是用来交换对称密钥)。
形成CSR之后,后续就是向CA进⾏申请认证,不过⼀般认证过程很繁琐,⽹络各种提供证书申请的服务商,⼀般真的需要,可以直接找平台解决。
摘要经过加密,就得到数字签名。
签名的形成是基于非对称加密算法的。目前暂时和https没有关系,不要和https中的公钥私钥搞混。
原始数据散列成摘要,再对其进行私钥加密,形成签名,再把签名的数据和原始数据组合在一起就叫做携带了数字签名的数据。
如果有人拿到了这个携带了数字签名的数据,对这个内容/签名/内容和签名作修改,我们怎么知道呢?
首先把这个携带了数字签名的数据的内容(原始数据)拿出来,再把签名拿出来,然后对原始数据进行哈希散列,形成散列值,再将刚刚形成的签名用公钥来解密,也得到散列值,如果这两个散列值相等,证明数字签名和这个数据是一致的;如果散列值不相等,证明内容/签名/内容和签名被修改过。
所以,携带数据签名的意义:防止内容被篡改
当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专门为该网站形成数字签名,过程如下:
1. CA机构拥有非对称加密的私钥A和公钥A'。
2. CA机构对服务端申请的证书明文数据进行hash,形成数据摘要。
3. 然后对数据摘要用CA私钥A'加密,得到数字签名S。
服务端申请的证书明文和数字签名S共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了。
因此,有了最终方案:
⑤方案5 - 非对称加密 + 对称加密 + 证书认证
在客户端和服务器刚⼀建⽴连接的时候,服务器给客户端返回⼀个证书,证书包含了之前服务端的公钥,也包含了网站的身份信息。
客户端会进行认证:
当客户端获取到这个证书之后,会对证书进行校验(防止证书是伪造的)。
判定证书的有效期是否过期。
判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构)。
验证证书是否被篡改:从系统中拿到该证书发布机构的公钥,对签名解密,得到⼀个hash值(称为数据摘要),设为hash1。然后计算整个证书的hash值,设为hash2。对⽐hash1和hash2是否相等。如果相等,则说明证书是没有被篡改过的。
浏览器中也都会包含送信任证书的发布机构。
中间人有没有可能篡改该证书?
假设中间人篡改了证书的明文。
由于他没有CA机构的私钥,所以无法hash之后用私钥加密形成签名,那么也就没办法对篡改后的证书形成匹配的签名。
如果强行篡改,客户端收到该证书后会发现明文和签名解密后的值不一致,则说明证书已被篡改,证书不可信,从而终止向服务器传输信息,防止信息泄露给中间人。
中间人有没有可能掉包证书?
因为中间人没有CA私钥,所以无法制作假的证书。
所以中间人只能向CA申请真证书,然后用自己申请的证书进行掉包。(此时中间人就暴露了自己的信息)
这个确实能做到证书的整体掉包,但是证书明文中包含了域名等服务端认证信息,如果整体掉包。客户端依旧能够识别出来。
中间人没有CA私钥,所以对任何证书都无法进行合法修改,也包括自己的。
为什么摘要内容在网络传输的时候一定要加密形成签名?
常见的加密算法有:MD5和SHA系列
以MD5为例,我们不需要研究具体的计算签名的过程,只需要了解MD5的特点:
①定长:无论多长的字符串,计算出来的MD5值都是固定长度(16字节版本或者32字节版本)
②分散:源字符串只要改变一点点,最终得到的MD5值都会差别很大
③不可逆:通过源字符串生成MD5很容易,但是通过MD5还原成原串理论上是不可能的
正因为MD5有这样的特性,我们可以认为如果两个字符串的MD5值相同,则认为这两个字符串相同。
判定证书篡改的过程(这个过程就好比判定这个身份证是不是伪造的身份证):
假设我们的证书只是⼀个简单的字符串hello,对这个字符串计算hash值(比如md5),结果为
BC4B2A76B9719D91。
如果hello中有任意的字符被篡改了,比如变成了hella,那么计算的md5值就会变化很大,
BDBD6F9CF51F2FD8。
然后我们可以把这个字符串hello和哈希值BC4B2A76B9719D91从服务器返回给客户端,此时客户端如何验证hello是否是被篡改过呢?
那么就只要计算hello哈希值,看看是不是BC4B2A76B9719D91即可。
但是还有个问题,如果⿊客把hello篡改了,同时也把哈希值重新计算下,客户端就分辨不出来了。
所以被传输的哈希值不能传输明文,需要传输密文。
所以,对证书明⽂(这⾥就是“hello”)hash形成散列摘要,然后CA使⽤自己的私钥加密形成签名,将hello和加密的签名合起来形成CA证书,颁发给服务端,当客户端请求的时候,就发送给客户端,即使中间人截获了,因为没有CA私钥,就无法更改或者整体掉包,就能安全的证明,证书的合法性。最后,客户端通过操作系统⾥已经存在了的证书发布机构的公钥进行解密,还原出原始的哈希值,再进行校验。
为什么签名不直接加密,而是要先hash形成摘要呢?
为了缩小签名密文的长度,加快数字签名的验证签名的运算速度。
如何成为中间人?
① ARP欺骗:在局域网中,黑客经过收到ARP的Request广播包,能够偷听到其它节点的(IP,MAC)地址。例如:黑客收到两个主机A,B的地址,告诉B(受害者),自己是A,使得B在发送给A的数据包都被黑客截取
② ICMP攻击:由于ICMP协议中有重定向的报文类型,那么我们就可以伪造⼀个ICMP信息然后发送给局域网中的客户端,并伪装自己是⼀个更好的路由通路。从而导致目标所有的上网流量都会发送到我们指定的接口上,达到和ARP欺骗同样的效果。
③ 假wifi,假网站等。
HTTPS的工作过程中涉及到的密钥有三组:
第⼀组(非对称加密):用于校验证书是否被篡改。服务器持有私钥(私钥在形成CSR文件与申请证书时获得),客户端持有公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的公钥)。服务器在客户端请求是:返回携带签名的证书。客户端通过这个公钥进行证书验证,保证证书的合法性,进⼀步保证证书中携带的服务端公钥权威性。
第二组(非对称加密):用于协商生成对称加密的密钥。客户端用收到的CA证书中的公钥(是可被信任的)给随机⽣成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥。
第三组(对称加密):客户端和服务器后续传输的数据都通过这个对称密钥加密解密。
其实⼀切的关键都是围绕这个对称加密的密钥。其他的机制都是辅助这个密钥工作的:
第二组非对称加密的密钥是为了让客户端把这个对称密钥传给服务器。
第一组非对称加密的密钥是为了让客户端拿到第⼆组非对称加密的公钥。
端口号(Port)标识了一个主机上进行通信的不同的应用程序。
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看)。
① 0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。
② 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443
执行下面的命令, 可以看到知名端口号:
cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号
1.一个进程是否可以bind多个端口号?
可以
2.一个端口号是否可以被多个进程bind?
不可以
① netstat
netstat是一个用来查看网络状态的重要工具。
语法: netstat [选项]
功能:查看网络状态
常用选项:
-n:拒绝显示别名,能显示数字的全部转化成数字
-l:仅列出有在 Listen (监听) 的服务状态
-p:显示建立相关链接的程序名
-t:(tcp)仅显示tcp相关选项
-u:(udp)仅显示udp相关选项
-a:(all)显示所有选项,默认不显示LISTEN相关
最常使用:netstat -nltp
② pidof
在查看服务器的进程id时非常方便
语法: pidof [进程名]
功能:通过进程名, 查看进程id
① 16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
② 如果校验和出错, 报文就会直接被丢弃
网络协议栈的tcp/ip协议,是内核中实现的,内核是用C语言实现的。
报头本质上就是一个结构体struct:
struct udp_hdr
{
unsigned int src_port :16; // 位段
unsigned int dst_port: 16;
unsigned int udp_len: 16;
unsigned int udp_check 16;
};
添加报头的本质:就是拷贝对象(添加位段的属性,然后把对象拷贝到数据的前面,形成报文)
UDP传输的过程类似于寄信:
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接。
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息。
面向数据报: 不能够灵活的控制读写数据的次数和数量。
发送一次报文,就接收一次。
无论应用层交给UDP多长的报文, UDP都会原样发送, 既不会拆分, 也不会合并。
用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节。而不能循环调用10次recvfrom, 每次接收10个字节。
UDP没有真正意义上的 发送缓冲区。调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了, 再到达的UDP数据就会被丢弃。
UDP的socket既能读,也能写。这个概念叫做 全双工。
我们注意到,UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。
然而64K在当今的互联网环境下,是一个非常小的数字。
如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然, 也包括我们自己写UDP程序时自定义的应用层协议
TCP全称为 "传输控制协议(Transmission Control Protocol")。需要对数据的传输进行一个详细的控制。
① 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
② 32位序号: 发送出去的序号。
32位确认号: 为了告诉发送方特定序号之前的已经全都收到了
序号和确认号的目的是为了保证可靠性(可靠性具体看下面的确认应答(ACK)机制)。
例如:
客户端发送的序号为1、2、3、5、6、7,服务端全部都收到了,那么服务端发送的确认号是多少呢?
答案是4,因为这里虽然最后的序号到7了,但是没有4,服务端只能保证4前面的序号全都收到了,所以发送的确认号是4。
这样看只需要一个序号就够了,又为什么要有一个序号和一个确认号呢?
因为TCP协议是全双工的,在发消息的同时,也可以收消息。
如果服务端在给客户端应答的时候,又给客户端发消息,这就一定需要两个序号(既要有确认号,又要携带自己的序号)。
报文在发送的时候,还可能是乱序到达的,这是不可靠的一种。
这就需要让我们的报文进行按序到达,这怎么做到?
这就要用到序号,根据序号进行排序就相当于按序到达了,这时序号的第二个作用。
序号的第三个作用是在超时重传中用来去重。
③ 4位TCP报头长度:
报头长度出现的原因是因为报头的标准长度是20字节,但是也可以大于这个数,上图中下面的选项就是可扩展空间(这个空间是40字节)。因此有了TCP报头长度。TCP报头长度有4个bit位,大小最大是15,但是这里的单位是4字节,即1代表4字节,2代表8字节,所以TCP头部最大长度是15 * 4 = 60字节。因此实际范围是 5-15(长度最低为20字节,最高为60字节)。
④ 6位标志位:
URG: 紧急指针标记位。
上面中我们说到序号可以用来排序,使之按序到达。但是如果数据必须在TCP中进行按序到达的话,那么有些数据优先级更高,但是序号较晚,那么怎么办?
这就可以将URG置1,让数据被紧急处理,即优先处理该数据,而不用被序号所限制。
紧急数据通常是用来作状态询问的(询问另一端主机出现异常情况时当前的状态)。
ACK: 确认标记位,表示该报文是对历史报文的确认(不仅包括确认,也可能会有给对方发的消息)【一般在大部分正式通信的情况下,ACK都是1】
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。(当发送端发给接收的数据超过某一个大小,或者接收端的接收缓冲区要满了,那么发送端就把PSH置1,提醒接收端可以进行读取了)。
RST: 对方要求重新建立连接,我们把携带RST标识的称为复位报文段。
如何建立连接,具体看下面三次握手四次挥手。
比如我们访问一个网站,跟这个网站建立了一个连接,时间长不用,那么OS就可以把客户端的连接关了,服务器也可能把服务端关了,这时就会导致一个认为建立了连接,一个认为没有建立连接。这时双方进行通信的时候,一方收到了一个没有建立连接就发送的数据,就会对这个数据进行应答,并将RST置1,告知它立刻将连接重置,重新建立连接。
也可能出现在丢包率比较高或者服务器的压力比较大的情况。
SYN: 请求建立连接,只要报文时健建立连链接的请求,SYN都需要被设置位1。我们把携带SYN标识的称为同步报文段
FIN: 请求端断开链接。我们称携带FIN标识的为结束报文段。
⑤ 16位窗口大小:
上图中,调用write/send这样的函数,并不是直接通过网络写入到对方,而是先写入到发送缓冲区(OS)中,然后什么时候发送就由OS来决定了,read/recv也是如此,是读取接收缓冲区中的数据。
IO函数,本质都是拷贝函数。
因此,数据什么时候发送,发送发送多少,出错了怎么办,要不要添加提高效率的策略。这些都是由OS内的TCP自主决定的,所以TCP才叫做传输控制协议。
TCP要发送数据在拷贝数据到缓冲区的时候,不妨碍对方发送数据,因此使用一个文件描述符可以既读又写,这就说明TCP通信是全双工的。
那么如果client端发送的太快了,server的接收缓冲区满了无法接收了怎么办?
这就需要让client知道server的接收能力(接收缓冲区剩余空间的大小)。
client怎么知道呢?这就要靠应答了。
应答本质就是要包含TCP报头,TCP报头可以有保存server接收能力的属性字段,这就是窗口大小。
如果client发送报文,那么填充的窗口大小就一定是自己的,因为client是要发送给对方的,所以就要发送自己接收缓冲区剩余空间的大小给对方,让server知道,然后在server再发送过来时,就可以根据client发送来的报文中的窗口大小来判断是否要继续发送。
反过来,server发送给client也是如此。
这种策略叫做流量控制,流量控制是双向的。
这个虽然只有16位bit位,即最大为64kb,但是下面的选项是可以选择扩大这个空间的,所有实际上这个空间的大小还是比较大的。
⑥ 16位校验和: 发送端填充, CRC校验。接收端校验不通过, 则认为数据有问题。此处的检验和不光包含TCP首部, 也包含TCP数据部分。
⑦ 16位紧急指针: 标识哪部分数据是紧急数据。
上面在6位标志位中,URG为紧急指针标记位,如果该位为1,就说明存在紧急数据,这16位紧急指针就标识的是哪个数据是紧急数据(这个数据只有1个字节)
⑧ 40字节头部选项: 暂时忽略。
这个机制是为了解决可靠性问题的。
首先我们要知道什么是不可靠的:丢包,乱序,校验失败...
那么怎么确认一个报文是否丢失呢?
采用确认应答机制。
客户端发送给服务端一个消息时,只要客户端得到了服务端的应答就说明客户端发送的消息服务端100%收到了。
客户端收到了应答,就可以确认没有丢失;而没有收到,则是不确定服务端是否收到。
这时我们会发现这样一种情况:永远会有一条最新的数据是没有应答的(这个也是没办法的,但是只有这一条影响不大)。
因此是不存在完全100%可靠的,但是有局部完全可靠的TCP。
根据这个确认应答机制,就在报头中有了序号和确认号。
TCP将每个字节的数据都进行了编号,即为序号:
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B。
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发。
但是, 主机A未收到B发来的确认应答,也可能是因为主机应答的ACK丢失了。
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
那么, 超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回"。
但是这个时间的长短, 随着网络环境的不同, 是有差异的。如果超时时间设的太长, 会影响整体的重传效率;如果超时时间设的太短, 有可能会频繁发送重复的包。
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
如果重发一次之后, 仍然得不到应答, 等待 2 * 500ms 后再进行重传。
如果仍然得不到应答, 等待 4 * 500ms 进行重传。依次类推,以指数形式递增。
累计到一定的重传次数,TCP会认为网络或者对端主机出现异常,强制关闭连接。
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。
TCP是面向连接的,那么如何理解连接?
TCP的服务端会收到大量的连接,那么OS就需要管理这些连接,就需要先描述,再组织。
建立好连接后,连接双方一定要为了维护连接建立结构体,包括建立的时间,起始序号等等(建立连接是需要花时间和空间的,维护连接时需要成本的)。
为什么是三次握手呢?
三次握手是不一定成功的,虽然TCP保证可靠性,但是不代表三次握手一定成功。不一定成功的原因是因为最后一次握手时是没有应答的。
如果只有一次握手,那么服务端收到客户端的SYN就会建立连接。这时如果一台主机不断的向服务端发送SYN,那么一台主机就会让服务端的资源被耗光,从而让正常的连接无法建立,这种攻击方式叫做 SYN洪水。
如果是两次握手,与一次握手类似,客户端发送SYN请求时,服务端在应答时就需要做维护连接的工作了,这时依旧会很容易的受到SYN洪水攻击。
而三次握手就可以把最后一次的确认机会交给服务端,在第二次握手后,客户端必须要做出应答,这时服务端收到了这个应答才会去建立连接。在这个过程中,客户端为了做出应答就必须也要为了维护连接创建结构体。这样如果这个主机想要攻击服务端,那么就很难了。主机和服务端会同时消耗资源,而主机可用资源一定比服务端小的多,就没法通过一台主机使得服务端崩溃。并且客户端因为做出了应答需要维护一个完整的连接,而服务端只需要维护一个半连接(因为目前连接没有成功),所以服务端消耗的资源页更少。奇数次握手可以把最后一次报文丢失的成本嫁接给客户端。
不过三次握手还是会受到SYN洪水攻击的,但是服务端不会随便被一台主机被攻击就崩溃了。如果黑客通过散播一些病毒程序,入侵到别人的主机中,然后在某一个时间令这些主机同时对一个服务端建立连接,就会导致服务端在很短的时间内建立很多连接,导致崩溃。
还有一个理由是客户端和服务端都有一个发送的过程和一个接收的过程,就可以验证客户端和服务端的输入输出是否正常,即验证全双工。
三次握手以最小成本的方式验证了全双工(握手次数大于三次就没有意义了,再增加次数只会导致效率的降低)。
如果在第三次握手是,客户端发出的ACK应答,服务端没有收到,那么客户端会建立连接,并且认为连接建立好了,而服务端则不会建立连接,认为连接没有建立好,那么此时客户端会做什么呢?
客户端既然已经认为连接建立好了,当然会直接给服务端发送数据了。服务端此时是认为没有建立好连接的,但是服务端发现客户端传输过来数据了,这时服务端就会意识到客户端的连接建立是有问题的。因此,服务端就会立刻给服务端发来的数据进行ACK响应,将响应报文的RST标记位置为1,告知客户端将连接进行重置。
为什么是四次挥手呢?
主要原因就是其中一端想要断开连接要收到ACK,而另一端要断开连接,也要收到这一端的ACK吗,因此一共四次。
第一次挥手是其中一端想要与另一端断开连接,第二次挥手是另一端对该端断开连接的应答。第三次挥手则是另一端和该端断开连接,第四次挥手是该端对另一端断开连接的应答。
第二次挥手和第四次挥手都是对对方FIN断开连接的应答,这个必须要有。
前两次挥手结束后,只代表该端与另一端断开了连接,但是另一端还是可以给该端发送数据的,等到另一端的数据发送完后,再进行第三次挥手和第四次挥手,使另一端和该端也断开连接。
这里之所以不是两次挥手直接互相FIN断开连接的原因就是该端和另一端断开了连接,但是另一端不一定想和该端断开连接,可能还有数据没有发送完。
特殊情况:可能会存在三次挥手的情况,当其中一端发送FIN要断开连接,此时另一端也完成了数据的传输想要发送FIN断开连接,那么这另一端就可以同时发送FIN和ACK,这一端接收到后,再发送ACK应答即可。
四次挥手一定会成功吗(包括TIME_WAIT问题)?
不一定。最后一次ACK应答时,是不能确定另一端一定能收到的,并且发出端如果在发出ACK应答时立刻从TIME_WAIT变成CLOSED,那么此时ACK可能在发送的过程中,就导致出现问题。因此发出端会在TIME_WAIT等一段时间。
这个时间就与另一端有关了,因为第三次握手是另一端发出的,如果过了一段时间还没有收到ACK应答,那么它就知道是该端的应答出现了问题。于是另一端就会进行重传FIN,如果该端收到了重传的FIN(这个时间段一直处于TIME_WAIT),那么该端就知道自己的ACK应答出了问题,也会重发。而如果在这个时间段内没有收到,就说明另一端成功收到ACK应答了,该端也就可以进入CLOSED了。这个处于TIME_WAIT时间一般是两个MSL(最长报文段寿命)时间。
这个TIME_WAIT虽然有用,但是会引起bind失败。服务器需要处理大量的客户端的连接,如果此时连接过多,那么服务器想要主动关闭连接重启,就会产生大量的TIME_WAIT,这时为了防止出现这种情况就没法直接关闭连接了。
于是就可以通过 setsockopt()函数来解决:
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));这个函数的作用是允许创建端口号相同但IP地址不同的多个socket描述符。
listen第二个参数:
listen的第二个参数:用于维护连接的长度(如果是2,服务端就可以连接3个客户端)。
不能太长,也不能没有。
不能没有的原因:为了让服务器在有闲置的情况下,可以从底层拿一个去进行连接。如果没有,就会导致服务器出现闲置情况,浪费资源。
不能太长的原因:太长会影响客户体验,同时也会过于占用系统资源,反而可能导致服务器效率变低。
服务端状态转化:
[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.
TCP状态转换的一个汇总:
较粗的虚线表示服务端的状态变化情况;
较粗的实线表示客户端的状态变化情况;
CLOSED是一个假想的起始点, 不是真实状态;
TIME_WAIT状态:
如果首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,会bind失败,因为虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口.
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话
就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK.
TIME_WAIT状态引起的bind失败的原因:
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的.
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题.
解决TIME_WAIT状态引起的bind失败的方法:
通过 setsockopt()函数来解决:
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));这个函数的作用是允许创建端口号相同但IP地址不同的多个socket描述符。
CLOSE_WAIT状态:
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题
当一次发送一批报文出现丢失怎么办?超时重传
当我们得知已经丢包的时候,这段时间内有一个检测丢包的超时重传的时间窗口,超时了能重传意味着数据一发出不能把发送缓冲区内的数据立刻清除,而要把它暂时保存起来。
对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段).
发送前四个段的时候, 不需要等待任何ACK, 直接发送;
收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;窗口越大, 则网络的吞吐率就越高;
滑动窗口一定会向右移动吗?滑动窗口可以变大吗?可以变小吗?
滑动窗口只会向右移动,但也可能不动。
滑动窗口既可以变大也可以变小。
滑动窗口的大小由什么决定?
由对方的接收能力决定。就是收到的TCP数据报头中的窗口大小。(后面到拥塞控制会修改)
那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论:
情况一:数据包已经递达,ACK被丢了。
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。
图中的对1001,2001,3001和4001的ACK都丢了,但是后续有5001和6001的ACK,就说明前面的数据包都接收到了,前面的ACK应答有没有都无所谓了。
如果前面1001-4001,而5001和6001的ACK丢了,那么就只会到4001了。
情况二:数据包丢了。
当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中)。
假设前面1001-4001,以及6001的报文都没有丢,只有5001的报文丢了,那么也只会响应4001,因为5001的报文丢了。
这种机制被称为 "高速重发控制"(也叫 "快重传")。
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
因此TCP根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)。
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
接收端如何把窗口大小告诉发送端呢?
回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息。
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位。
在TCP的流量控制,确认应答,超时重传,序号,滑动窗口的机制下,如果一千个数据出现几个丢包那么是正常的,但是如果出现了几百个丢包,那么就不是用户端和服务端的关系了,而是中间出现了问题,即网络出现了问题。
网络出了问题是不能重传的,一个网络会连接很多服务器(winodow,linux等),一个服务器出现了问题,那么其它很多服务器也会出现问题,那么这时网络本就有问题,这些服务器还都进行重传,只会加重服务器的压力。所以不能进行重传。
这里网络出现问题,如果是硬件的问题,那只能去解决硬件,而这里是我们谈论的网络软件出了问题:比如网络中积压了大量的报文,导致转发压力太大了,导致出现超时丢包的问题。
这种只考虑网络软件的问题叫做网络拥塞问题,
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念程为拥塞窗口。
发送开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。
因此:一次向目标主机发送数据的量 = min(对方的接受能力, 拥塞窗口)
即:滑动窗口的大小 = min(对方的窗口大小,拥塞窗口)
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初期时慢, 但是增长速度非常快。
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1。
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
指数增长前期慢,意味着前期都可以发送少量的数据,增长速度快
指数增长:前期慢 + 后期增长快(可以尽快恢复网络通信的正常速度)
增长到一定程度,就正常的线性增长
每次增长到网络拥塞时,下一次线性增长的位置就变成上次网络拥塞的一半
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
那么所有的包都可以延迟应答么? 肯定不是。
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端
在发送确认应答ACK等字段时,顺便把数据也一起发送过去,以此减少发送此时,提高效率。
TCP是面向字节流的,不关心任何的数据格式,但是要正确使用这个数据,必须得有特定的格式。
谁来解释这个格式?应用层。
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;
调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做全双工。
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 因为这是一个完整的应用层数据包
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包(报文和报文)之间的边界。
对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可);
对于UDP协议来说, 是否也存在 "粘包问题" 呢?
对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界。
站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN。和正常关闭没有什么区别(正常四次挥手结束)
机器重启: 和进程终止的情况相同。
机器断电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset。即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放。
另外, 应用层的某些协议, 也有一些这样的检测机制。例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接。
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能。
①可靠性
校验和
序列号(按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制
②提高性能
滑动窗口
快速重传
延迟应答
捎带应答
③其他
定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
当然, 也包括自己写TCP程序时自定义的应用层协议
TCP是可靠连接, 那么是不是TCP一定就优于UDP呢?
TCP和UDP之间的优点和缺点, 不能简单、绝对的进行比较。
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定。
参考TCP的可靠性机制, 在应用层实现类似的逻辑。
同时根据使用UDP的方向来引入对应TCP的可靠性机制。
例如:
引入序列号, 保证数据顺序;
引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据;等等...
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。