文章目录
- 关于应用层协议的理解
- 应用层协议的制定
- 理论部分
- 代码部分
- 完整代码以及测试
- HTTP协议
- 代码测试HTTP协议
- HTTPS协议
- 加密原因
- 基础的加密方式
- 数据摘要(数据指纹)
- 数字签名
- HTTPS的加密方式的选择
- 总结
关于应用层协议的理解
在之前的一篇关于套接字实现网络通信一文中,进行通信的内容多是字符串,内容形式可谓是很简单了。在传输的过程中只需要控制数据的传输大小,字符串就会通过套接字进行传送接收。但是结合实际生活来看,通信的场景却不只是这么简单的内容,比如很有可能是一个结构体,里面有着各种不同数据类型的变量。这里我们就希望用一种统一的视角来看待各不相同的数据结构体(C/C++)或者是对象(C++),无论是多么复杂的数据结构,我们总可以将我们所需要的数据进行排列并转化成字符串,并将这种转换方式规定为一种约定好的协议,使得大家都认同这种处理方式,以便于从字符串中准确读定位并读取所需要的信息。
简而言之,应用层协议是一种数据转换成字符串的转换方式,使得程序员可以按需传送相当复杂的数据结构。并且,协议可以由程序员自己规定,只要有人认同并使用该程序员的协议,那么就可以实现正常的网络通信了。经过网络发展的几十年,一些比较成熟的协议被几乎所有的程序员所认同,例如著名的http和https,都已经写好并投入使用好多年,作为网络协议的初始学习对象是再合适不过的,但在学习http与https相关协议之前,我们不妨试着自己写一个简单的协议,具体感受一下协议的定制过程,加深对协议的了解。
应用层协议的制定
接下来我就以网络简单版的计算器为示例,制定协议。
理论部分
第一步,在制定协议之前,得先清楚计算器的主要构成:操作数A、操作符、操作数B、计算结果、计算状态码(标识计算是否出现错误)其中操作数A、操作符、操作数B都是客户端的请求数据,而计算结果、计算状态码则是服务端的响应数据,因此我们需要两个结构体来进行数据的传送。
第二步,规定转换的字符串的风格,一般情况下该字符串我们成为报文,由报头和有效载荷两部分构成,如:
如果具体到算式,比如1+1,则有下面的客户端请求的报文形式:
其中报头5是有效载荷的长度(单位是字节),1 + 1是有效载荷(字符之间用空格分隔)。报头和有效载荷之间用\r\n分隔开,有效载荷后的\r\n是为了表明一个报文的结束。
有了以上的逻辑,我们接下来考虑的就是该如何将协议运用到代码之中去了。
我们提到了结构体转化成字符串的这一操作,规范一点的叫法就是**序列化。而将字符串再转化成结构体的操作,称之为反序列化。其次还有添加报头与分隔符和删去报头与分隔符的操作,称之为分装和解包**。
值得注意的是,对于请求和响应两种结构体,他们的成员变量不同,因此各自的序列化和反序列化的函数还是要有所不同,但是对于已经序列化后生成的字符串,封装和解包的操作是一致的。
代码部分
自定义的协议头文件:
//Protocl.hpp
// 协议定制
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
using namespace std;
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF)
#define OPS "+-*/%"
// 封装函数
// 增加序列化数据的长度大小的字符串到数据头,并添加\r\n(截断一段完整的信息)
// 例如:5\r\n1 + 1\r\n
string encode(string &in, size_t len)
{
string tmp = to_string(len);
tmp += CRLF;
tmp += in;
tmp += CRLF;
return tmp;
}
// 解包函数
// 删去序列化数据头部表示长度的字符串,并删去\r\n(使信息暴露出来)
// 计算一段数据的长度,将其赋值给len
// 返回解包后的字符串(有效载荷)
string decode(string &in, size_t &len)
{
len = 0;
string package = "";//有效载荷
size_t crlfOne = in.find(CRLF);//报头结尾
size_t crlfTwo = in.rfind(CRLF);//有效载荷结尾
if (crlfOne == string::npos || crlfTwo == string::npos)
{
return package;
}
string lenStr = in.substr(0, crlfOne);//报头字符串
size_t tmpLen = atoi(lenStr.c_str());//报头所表示的数字
// 检测是否有完整的有效载荷
if (tmpLen != (in.size() - 2 * CRLF_LEN - lenStr.size()))//报头数字应该与有效载荷的长度相等
{
return package;
}
len = tmpLen;
// 获得删去\r\n和头部数字的数据
package = in.substr(crlfOne + CRLF_LEN, len);
// 读取过的报文要从in中删去,避免in存在多个报文
in.erase(0, crlfOne + 2 * CRLF_LEN + len);
// 返回有效载荷
return package;
}
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化 结构化的数据-->字符串(即有效载荷)
// 风格:数字|空格|运算符|空格|数字 例:1 + 1
void serialize(string *out) // 输出型参数out
{
string xstr = to_string(x_);
string ystr = to_string(y_);
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
}
// 反序列化 将传进来的字符串(即有效载荷)转换成结构化数据
bool deserialize(string &in)
{
size_t spaceOne = in.find(SPACE);
size_t spaceTwo = in.rfind(SPACE);
if (spaceOne == string::npos || spaceTwo == string::npos)
{
return false;
}
string number1 = in.substr(0, spaceOne);
string number2 = in.substr(spaceTwo + SPACE_LEN);
string op = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
x_ = atoi(number1.c_str());
y_ = atoi(number2.c_str());
op_ = op[0];
return true;
}
void debug()//调试,显示结构体变量
{
cout << "#################################" << endl;
cout << "x_: " << x_ << endl;
cout << "op_: " << op_ << endl;
cout << "y_: " << y_ << endl;
cout << "#################################" << endl;
}
public:
int x_;
int y_;
char op_;
};
class Response
{
public:
Response() : calCode_(0), result_(0)
{
}
~Response()
{
}
// 序列化 结构化的数据-->字符串(即有效载荷)
// 风格: 计算状态码|空格|计算结果 例:0 2
void serialize(string *out)
{
string code = to_string(calCode_);
string result = to_string(result_);
*out = code;
*out += SPACE;
*out += result;
}
// 反序列化 将传进来的字符串(即有效载荷)转换成结构化数据
bool deserialize(string &in)
{
size_t spaceIndex = in.find(SPACE);
if (spaceIndex == string::npos)
{
return false;
}
string code = in.substr(0, spaceIndex);
string result = in.substr(spaceIndex + SPACE_LEN);
calCode_ = atoi(code.c_str());
result_ = atoi(result.c_str());
return true;
}
void debug()//调试,显示结构体变量
{
cout << "#################################" << endl;
cout << "calCode_: " << calCode_ << endl;
cout << "result_: " << result_ << endl;
cout << "#################################" << endl;
}
public:
int calCode_; // 计算状态码,0为正常计算,-1为除零错误,-2为模零错误
int result_;
};
上面其实就是协议的定制内容了,具体的使用我们可以在代码中讲解:
客户端相关代码(代码部分细节省略,后面整体的代码中会补全):
// 根据输入的字符串填充 请求结构体
bool makeRequest(const std::string &str, Request *req)
{
// 123+1
char strtmp[1024];
snprintf(strtmp, sizeof strtmp, "%s", str.c_str());//将in中的数据拷贝到strtmp中
char *left = strtok(strtmp, OPS);//根据操作符,找第一个操作数
if (!left)
return false;//找不到返回false
char *right = strtok(nullptr, OPS);//找第二个操作数
if (!right)
return false;//找不到返回false
char mid = str[strlen(left)];//操作符
req->x_ = atoi(left);//字符串转化成数字
req->y_ = atoi(right);
req->op_ = mid;
return true;
}
//下面是main函数中有关协议使用的相关代码
string message;
cout<<"please enter# ";
getline(cin,message);//获取运算表达式
Request req;
makeRequest(message, &req);//形成请求
std::string package;
req.serialize(&package); //请求序列化
package = encode(package, package.size());//封装
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;
size_t len = 0;
std::string tmp = decode(echoPackage, len); //对报文进行解包,拿到有效载荷
if(len > 0)
{
echoPackage = tmp;
resp.deserialize(echoPackage);//对有效载荷进行反序化
printf("[calCode: %d] %d\n", resp.calCode_, resp.result_);//输出计算结果
}
}
服务端相关代码(代码部分细节省略,后面整体的代码中会补全):
//根据请求生成响应
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 '/':
{
if (req.y_ == 0) resp.calCode_ = -1;
else resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{
if (req.y_ == 0) resp.calCode_ = -2;
else resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.calCode_ = -3;
break;
}
return resp;
}
//下面是服务端关于协议的使用
string inbuffer;
char buff[BUFFER_SIZE];
ssize_t s = read(sock, buff, sizeof(buff) - 1);//读取请求报文
buff[s]=0;
inbuffer+=buff;//报文填充到buffer容器中
Request req;
size_t packageLen=0;
string package=decode(inbuffer,packageLen);//解包,拿到有效载荷
if(req.deserialize(package))//反序列化,填充请求结构体req
{
Response resp=calculator(req);//根据请求,进行计算,生成响应
string respPackage;
resp.serialize(&respPackage);//将响应序列化,生成有效载荷
respPackage=encode(respPackage,respPackage.size());//对有效载荷进行封装,形成报文
resp.debug();
write(sock,respPackage.c_str(),respPackage.size());//将响应报文发送给客户端
}
完整代码以及测试
我们采用守护进程的方式启动服务端,使用TCP传输协议,以单例线程池的方式处理计算任务,并且生成服务端的日志文件(相较于之前的日志函数,更加完善)。详细参考之前写过的一篇文章[网络通信]中TCP的通信模式,这里依旧会给出完整代码。
守护进程头文件:
//daemonize.hpp
#pragma once
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize()
{
int fd = 0;
//忽略一些信号
signal(SIGPIPE,SIG_IGN);
//创建进程后直接结束父进程
if(fork()>0)
{
exit(0);
}
//调用setsid()函数,使得子进程成为一组进程的组长
setsid();
//打开特殊文件“/dev/null”,相当于回收站,一切输入的数据都会被忽略
if((fd=open("/dev/null",O_RDWR))!=-1)
{
//三次重定向使得所有的输出都指向回收文件
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
if(fd>2) close(fd);//关闭特殊文件描述符,避免文件描述符泄露
}
}
上面这个函数调用完之后就会使得进程成为后台进程。
单例线程池:
//ThreadPool.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <string>
using namespace std;
#define NUM 10
template <class T>
class ThreadPool
{
private:
ThreadPool(const int &threadNum = NUM) : 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:
~ThreadPool()
{
//销毁锁和条件变量
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
int threadNum()//获取线程个数
{
return threadNum_;
}
static ThreadPool<T> *getInstance() // 申请类的对象
{
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
if (instance == nullptr)
{
pthread_mutex_lock(&mutex);
if (instance == nullptr)
{
instance = new ThreadPool<T>();
}
pthread_mutex_unlock(&mutex);
}
return instance;
}
static void *threadpool(void *arg) //线程的回调函数
{
pthread_detach(pthread_self());//线程分离
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(arg);
while (true)//抢任务
{
tp->lockQueue();
while (!tp->haveTask())
{
tp->waitForTask();
}
T tmp = tp->pop();
tp->unlockQueue();
tmp();//处理任务
}
return nullptr;
}
void start()//线程池初始化,创建若干个线程
{
assert(!isStart_);
for (int i = 0; i < threadNum_; ++i)
{
pthread_t tmp;
pthread_create(&tmp, nullptr, threadpool, this);
}
}
void put(const T &data)//放入任务
{
lockQueue();
taskQueue_.push(data);
unlockQueue();
choiceThreadForHandler();//选择一个线程来处理任务
}
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;//单例指针
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr; // 懒汉模式
任务类:
//Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "Log.hpp"
using namespace std;
class Task
{
public:
using callback_t = function<void (int, string, uint16_t)>;//方法函数的返回值是void,参数是int、string、uint16_t
Task() : sock_(-1), port_(-1)
{}
Task(int sock, string ip, uint16_t port, callback_t func)//任务的构造函数
: sock_(sock), ip_(ip), port_(port), func_(func)
{}
~Task()
{}
void operator()()//重载(),进行任务的处理
{
logMessage(DEBUG, "线程[%p]处理 %s[%d]的请求, begin...", pthread_self(), ip_.c_str(), port_);
func_(sock_, ip_, port_);
logMessage(DEBUG, "线程[%p]处理 %s[%d]的请求, end...", pthread_self(), ip_.c_str(), port_);
}
private:
int sock_;//网络套接字
string ip_;//IP地址
uint16_t port_;//端口
callback_t func_; // 任务的回调函数
};
日志输出:
//Log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define LOGFILE "serverTcp.log"
class Log
{
public:
Log()
: logFd_(-1)
{
}
static Log* getInstance()//获取单例对象,并保证线程安全
{
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
if(instance_==nullptr)
{
pthread_mutex_lock(&mutex);
if(instance_==nullptr)
{
instance_=new Log();
}
pthread_mutex_unlock(&mutex);
}
return instance_;
}
void enable()//完成对于日志文件LOGFILE的重定向
{
umask(0);
logFd_ = open(LOGFILE, O_CREAT | O_APPEND | O_WRONLY, 0666);
if (logFd_ != -1)//完成对标准输入、输出流的重定向
{
dup2(logFd_, 1);
dup2(logFd_, 2);
}
}
~Log()
{
if (logFd_ != -1)
{
close(logFd_);//关闭文件描述符
}
}
private:
int logFd_;//文件描述符
static Log* instance_;//单例对象
};
Log* Log::instance_=nullptr;
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level, const char *format, ...)//可变参数format
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap;
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap);
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中的数据尽快刷盘
}
服务端与客户端共同使用的头文件集合:
//util.hpp
#pragma once
using namespace std;
#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 <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "daemonize.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
协议头文件:
//Protocl.hpp
// 协议定制
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
using namespace std;
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF)
#define OPS "+-*/%"
// 封装函数
// 增加序列化数据的长度大小的字符串到数据头,并添加\r\n(截断一段完整的信息)
// 例如:5\r\n1 + 1\r\n
string encode(string &in, size_t len)
{
string tmp = to_string(len);
tmp += CRLF;
tmp += in;
tmp += CRLF;
return tmp;
}
// 解包函数
// 删去序列化数据头部表示长度的字符串,并删去\r\n(使信息暴露出来)
// 计算一段数据的长度,将其赋值给len
// 返回解包后的字符串(有效载荷)
string decode(string &in, size_t &len)
{
len = 0;
string package = "";//有效载荷
size_t crlfOne = in.find(CRLF);//报头结尾
size_t crlfTwo = in.rfind(CRLF);//有效载荷结尾
if (crlfOne == string::npos || crlfTwo == string::npos)
{
return package;
}
string lenStr = in.substr(0, crlfOne);//报头字符串
size_t tmpLen = atoi(lenStr.c_str());//报头所表示的数字
// 检测是否有完整的有效载荷
if (tmpLen != (in.size() - 2 * CRLF_LEN - lenStr.size()))//报头数字应该与有效载荷的长度相等
{
return package;
}
len = tmpLen;
// 获得删去\r\n和头部数字的数据
package = in.substr(crlfOne + CRLF_LEN, len);
// 读取过的报文要从in中删去,避免in存在多个报文
in.erase(0, crlfOne + 2 * CRLF_LEN + len);
// 返回有效载荷
return package;
}
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化 结构化的数据-->字符串(即有效载荷)
// 风格:数字|空格|运算符|空格|数字 例:1 + 1
void serialize(string *out) // 输出型参数out
{
string xstr = to_string(x_);
string ystr = to_string(y_);
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
}
// 反序列化 将传进来的字符串(即有效载荷)转换成结构化数据
bool deserialize(string &in)
{
size_t spaceOne = in.find(SPACE);
size_t spaceTwo = in.rfind(SPACE);
if (spaceOne == string::npos || spaceTwo == string::npos)
{
return false;
}
string number1 = in.substr(0, spaceOne);
string number2 = in.substr(spaceTwo + SPACE_LEN);
string op = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
x_ = atoi(number1.c_str());
y_ = atoi(number2.c_str());
op_ = op[0];
return true;
}
void debug()//调试,显示结构体变量
{
cout << "#################################" << endl;
cout << "x_: " << x_ << endl;
cout << "op_: " << op_ << endl;
cout << "y_: " << y_ << endl;
cout << "#################################" << endl;
}
public:
int x_;
int y_;
char op_;
};
class Response
{
public:
Response() : calCode_(0), result_(0)
{
}
~Response()
{
}
// 序列化 结构化的数据-->字符串(即有效载荷)
// 风格: 计算状态码|空格|计算结果 例:0 2
void serialize(string *out)
{
string code = to_string(calCode_);
string result = to_string(result_);
*out = code;
*out += SPACE;
*out += result;
}
// 反序列化 将传进来的字符串(即有效载荷)转换成结构化数据
bool deserialize(string &in)
{
size_t spaceIndex = in.find(SPACE);
if (spaceIndex == string::npos)
{
return false;
}
string code = in.substr(0, spaceIndex);
string result = in.substr(spaceIndex + SPACE_LEN);
calCode_ = atoi(code.c_str());
result_ = atoi(result.c_str());
return true;
}
void debug()//调试,显示结构体变量
{
cout << "#################################" << endl;
cout << "calCode_: " << calCode_ << endl;
cout << "result_: " << result_ << endl;
cout << "#################################" << endl;
}
public:
int calCode_; // 计算状态码,0为正常计算,-1为除零错误,-2为模零错误
int result_;
};
服务端:
//serverTCP.cc
#include "util.hpp"
#include "Protocl.hpp"
//根据请求生成响应
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 '/':
{
if (req.y_ == 0) resp.calCode_ = -1;
else resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{
if (req.y_ == 0) resp.calCode_ = -2;
else resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.calCode_ = -3;
break;
}
return resp;
}
void netCal(int sock, const string &clientIp, uint16_t clientPort)
{
assert(sock > 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
string inbuffer;
while (true)
{
char buff[BUFFER_SIZE];
ssize_t s = read(sock, buff, sizeof(buff) - 1);//读取请求报文
if (s == 0)
{
logMessage(NOTICE, "client[%s:%d] close sock, client quit", clientIp.c_str(), clientPort);
break;
}
else if (s < 0)
{
logMessage(WARINING, "client[%s:%d]-->%s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
buff[s]=0;
inbuffer+=buff;//报文填充到buffer容器中
//cout<<"inbuffer: "<<inbuffer<<endl;//调试信息
Request req;
size_t packageLen=0;
string package=decode(inbuffer,packageLen);//解包,拿到有效载荷
//cout<<"package: "<<package<<endl;//调试信息
if(packageLen==0) continue;//有效载荷不完整,重新读取报文
if(req.deserialize(package))//反序列化,填充请求结构体req
{
Response resp=calculator(req);//根据请求,进行计算,生成响应
string respPackage;
resp.serialize(&respPackage);//将响应序列化,生成有效载荷
respPackage=encode(respPackage,respPackage.size());//对有效载荷进行封装,形成报文
//resp.debug();//调试信息
write(sock,respPackage.c_str(),respPackage.size());//将响应报文发送给客户端
}
}
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const string ip = "")
: ip_(ip), port_(port), listenSock_(-1),quit_(false)
{
}
~ServerTcp()
{
}
void init()
{
// 创建套接字
listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "sock %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "sock %s:%d", strerror(errno), listenSock_);
// 填充基本的网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 绑定套接字与网络信息
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_);
// 监听套接字
if (listen(listenSock_, 5) < 0)
{
logMessage(FATAL, "listen %s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen %s:%d", strerror(errno), listenSock_);
// 获取线程池对象
tp_ = ThreadPool<Task>::getInstance();
}
void loop()
{
// 启动线程池
tp_->start();
logMessage(DEBUG, "ThreadPool start success, threadNum is %d", tp_->threadNum());
// 死循环响应客户端网络请求
while (!quit_)
{
// 为获取远程网络信息做准备
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
string peerIp = inet_ntoa(peer.sin_addr);
uint16_t peerPort = ntohs(peer.sin_port);
if (serviceSock < 0)
{
logMessage(FATAL, "accept %s[%d]", strerror(errno), serviceSock);
continue;
}
logMessage(DEBUG, "accept %s | %s[%d], sock: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 网络计算器
Task t(serviceSock, peerIp, peerPort, netCal);
tp_->put(t);
}
}
void quitServer()
{
quit_=true;
logMessage(NOTICE,"server quit safely.");//🔺🔺🔺此时服务端可能已经卡在了accept函数处,因此客户端还得再次连接一次,才会退出服务器
}
private:
uint16_t port_;
string ip_;
int listenSock_;
ThreadPool<Task> *tp_;
volatile 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();
}
}
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
string ip;
if (argc > 2)
ip = argv[2];
daemonize();
Log* log=Log::getInstance();
log->enable();
signal(3,sigHandler);
ServerTcp svr(port, ip);
svr.init();
svrp=&svr;
svr.loop();
return 0;
}
客户端:
//clientTcp.cc
#include "util.hpp"
#include "Protocl.hpp"
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;
}
// 根据输入的字符串填充 请求结构体
bool makeRequest(const std::string &str, Request *req)
{
// 123+1
char strtmp[1024];
snprintf(strtmp, sizeof strtmp, "%s", str.c_str());//将in中的数据拷贝到strtmp中
char *left = strtok(strtmp, OPS);//根据操作符,找第一个操作数
if (!left)
return false;//找不到返回false
char *right = strtok(nullptr, OPS);//找第二个操作数
if (!right)
return false;//找不到返回false
char mid = str[strlen(left)];//操作符
req->x_ = atoi(left);//字符串转化成数字
req->y_ = atoi(right);
req->op_ = mid;
return true;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
cerr<<"socket: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
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);
if(connect(sock,(const struct sockaddr*)&server,sizeof(server))!=0)
{
cerr<<"connnect: "<<strerror(errno)<<endl;
exit(CONN_ERR);
}
cout<<"client connnect success,sock: "<<sock<<endl;
string message;
while(!quit)
{
message.clear();
cout<<"please enter# ";
getline(cin,message);
if(strcasecmp(message.c_str(),"quit")==0)//读取到退出字符"quit"(不区分大小写)就退出客户端
{
quit=true;
continue;
}
Request req;
if(!makeRequest(message, &req)) continue;//形成请求
//req.debug();//调试信息
std::string package;
req.serialize(&package); //请求序列化
//std::cout << "serialize: " << package << std::endl;//调试信息
package = encode(package, package.size()); //封装
//std::cout << "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;
size_t len = 0;
std::string tmp = decode(echoPackage, len); //对报文进行解包,拿到有效载荷
if(len > 0)
{
echoPackage = tmp;
resp.deserialize(echoPackage);//对有效载荷进行反序化
printf("[calCode: %d] %d\n", resp.calCode_, resp.result_);//输出计算结果
}
}
else if(s<=0)
{
break;
}
}
close(sock);
return 0;
}
测试结果:
HTTP协议
上面我们自己手写了一个简单的协议,接下来认识一下大佬们写的协议。
认识这个协议,依旧是从请求和响应两种信息的协议风格入手:
请求风格:
首行: [方法] + [url] + [版本]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
HTTP常见Header:
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
例如:
响应风格:
首行: [版本号] + [状态码] + [状态码解释]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
例如:
上面的正文部分就是一个网页的代码内容。
HTTP的方法:
其中最常用的就是GET方法和POST方法。二者的区别就是GET的正文在URL(网址)中,二者都是明文传输数据,数据在网络世界中裸奔。数据并不是隐私的!
HTTP的状态码:
最常见的状态码: 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)……
代码测试HTTP协议
那么接下来我们就可以试着自己写一个简易版的网页了,我们只需要写一个服务端就可以,可以使用浏览器充当客户端。因此,使用TCP进行正常的网络通信,只不过协议变成了HTTP协议,响应的内容除了首行和Header,正文的部分必须得是一个网页的代码,这个可以算是网页资源了,我们可以通过文件的方式将网页的代码保存起来,客户端在申请网页资源的时候,可以从服务器中对应文件中读取对应的代码资源,实现网页响应。
例如:
上面是我自己在测试的时候创建的文件,一般默认资源的根目录是wwwroot。
图解:
服务端代码:
//serverTcp.cc
#include <iostream>
#include <fstream>
#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 <sys/wait.h>
#include <cerrno>
#include <signal.h>
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define ROOT_PATH "wwwroot"
#define HOME_PAGE "index.html"
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
using namespace std;
// 获得资源文件的路径
string getPath(const string &message)
{
int pos = message.find(CRLF);
if (pos == string::npos)
return "";
string firstLine = message.substr(0, pos);//获取首行
int firstIndex = firstLine.find(SPACE);
if (firstIndex == string::npos)
return "";
int secondIndex = firstLine.rfind(SPACE);
if (secondIndex == string::npos)
return "";
string path = firstLine.substr(firstIndex + SPACE_LEN, secondIndex - (firstIndex + SPACE_LEN));//获取申请的资源路径
if (path.size() == 1 && path[0] == '/')//如果只有一个'/',默认访问首页
path += HOME_PAGE;
return path;
}
// 从相应的文件中获取资源
string readFile(const string &fileName)
{
ifstream in(fileName, ifstream::binary);//二进制读文件
if (!in.is_open())
{
return "404";
}
string content, line;
while (getline(in, line))
{
content += line;//正文部分(网页代码)获取
}
in.close();
return content;
}
void handerHttpServer(int sock)
{
assert(sock >= 0);
char buffer[BUFFER_SIZE];
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s < 0)
{
cout << "read error: " << strerror(errno);
return;
}
else if (s == 0)
{
cout << "read done……";
return;
}
buffer[s] = 0;
//cout << buffer; //调试信息
string path = getPath(buffer);
string recource = ROOT_PATH;
recource += path;
//cout << recource << endl;//调试信息
string html = readFile(recource); // 拿取网页资源
int pos = recource.rfind(".");
string suffix;
if (pos != string::npos)
{
suffix = recource.substr(pos);//访问资源的类型(.html->文本 .jpg->图片)
//cout << suffix << endl;//调试信息
}
// 开始响应
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: " + to_string(html.size()) + CRLF);
response += CRLF;//标志着正文的结束
//填写正文
response += html;
write(sock,response.c_str(),response.size());//向客户端发送正文
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const string ip = "")
: ip_(ip), port_(port), listenSock_(-1), quit_(false)
{
}
~ServerTcp()
{
}
void init()
{
// 创建套接字
listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(SOCKET_ERR);
}
// 填充基本的网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 绑定套接字与网络信息
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// 监听套接字
if (listen(listenSock_, 5) < 0)
{
exit(LISTEN_ERR);
}
}
void loop()
{
// 死循环响应客户端网络请求
while (!quit_)
{
// 为获取远程网络信息做准备
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
string peerIp = inet_ntoa(peer.sin_addr);
uint16_t peerPort = ntohs(peer.sin_port);
if (serviceSock < 0)
{
continue;
}
// 多进程版本
pid_t id = fork();
if (id == 0)
{
close(listenSock_);
if (fork() > 0)
exit(0);
// http响应
handerHttpServer(serviceSock);
exit(0);
}
close(serviceSock);
int ret = waitpid(0, nullptr, 0);
assert(ret > 0);
(void)ret;
}
}
void quitServer()
{
quit_ = true;
}
private:
uint16_t port_;
string ip_;
int listenSock_;
volatile 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();
}
}
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
string ip;
if (argc > 2)
ip = argv[2];
signal(3, sigHandler);
ServerTcp svr(port, ip);
svrp = &svr;
svr.init();
svr.loop();
return 0;
}
网页代码文件:
这个代码具体的原理并不用特意去学习,从网上随便找一个能用就行。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>http test</title>
</head>
<body>
<h3>hello my server!</h3>
<p>这里几乎什么都没有哦😋</p>
<form action="/a/b/c.html" method="post"> <!--点击submit之后跳转到/a/b/c.html下,由于没有这个文件,最后返回404-->
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>
测试结果:
HTTPS协议
HTTPS是在HTTP协议上加了一层加密层的协议,但是为什么要加密呢?
加密原因
由于应用层之间的信息传输是需要交由下层的传输机制来完成,这就意味着在传输层及其以下的位置都有可能被非法的第三方运营商劫持,篡改信息简直是再简单不过的事情。一旦涉及比较重要的数据,就会出现严重的经济和安全问题:银行账户余额、工作评定、卫星导航……因此重要的数据要想安全传输,在应用层与传输层之间加密是必不可少的。
GET和POST两种方法都是明文传送,这个在前面讲HTTP的时候已经提到过了。有没有一种方法可以将明文转化成密文呢?答案是有的,使用密钥就可以完成这项工作。所谓的密钥,就是一组特定的数据,可以是数字,也可以是一串字符串。通过写定的函数,实现对明文的转化,转换后的最终的信息就是密文了。
例如:
规定密钥为数字:369
客户端传送数字num,生成密文为数字safe=num^369。
服务端接收数字safe,得到明文数字num=safe^369。
上述两次的异或操作分别是加密和解密的过程,这个虽然简单,实际生活中不可能使用如此简单的方法进行加密,但是体现出来的加密思想是正确的。
基础的加密方式
对称加密:
采用单钥密码系统的加密方法,同⼀个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密,特征:加密和解密所用的密钥是相同的。上面的异或加密就是对称加密。
常见对称加密算法:DES、3DES、AES、TDEA、Blowfish、RC2等
特点:算法公开、计算量⼩、加密速度快、加密效率⾼
非对称加密:
需要两个密钥进行加密和解密,分别称之为公开密钥(简称公钥)和私有密钥(简称私钥)。公钥和私钥都能进行加密和解密的操作。
常见非对称加密算法:RSA,DSA,ECDSA等。
特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。
公钥和私钥是相对的,如果用公钥加密,那么就用私钥解密;用私钥加密,那么就用公钥解密。不管如何使用这两个密钥,一般都是一个密钥公之于众,而另一个则不对外公布,且必须要保护好。区分公钥和私钥的意义在于:一个客户端都可以对一份数据使用公钥加密,密文谁都可以拿到,但是私钥只有一份在服务器那里,只有服务器可以使用私钥解密拿取到明文,这就保证了信息的安全性。
数据摘要(数据指纹)
数字指纹(数据摘要),其基本原理是利用单向散列函数(Hash函数)对信息进行运算,生成⼀串固定长度的数字摘要。数字指纹并不是⼀种加密机制,但可以用来判断数据有没有被窜改。
摘要常见算法:有MD5、SHA1、SHA256、SHA512等,算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概非常低)
摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推原信息,通常用来进行数据对比。
数字签名
摘要经过加密,得到数字签名。这是用来防止数据被篡改的一种机制。
上面这种加密方式设计的非常巧妙,共有两层加密。
第一层加密就是数字摘要与数据的验证,一旦数据被修改,那么数据再次求得的摘要就会和之前的摘要相差很大。
第二层加密是使用私钥对数字摘要进行加密,这是防止第三者修改数据的同时也修改了数字摘要。毕竟公钥是公开的,数据摘要是私钥加密,任何第三者拿着公钥都可以拿到数字摘要并修改,但问题在于,修改完之后是没办法加密回去的!因为缺少私钥!强行使用公钥加密也不是不可以,在验证阶段就会出问题,这时候就知道数据被第三者修了。
这样一来,只要保证好私钥是唯一的,并且拥有者是比较有公信力的,不会监守自盗,那么就能保证数据安全了。
签名的数据:证书
🔺为什么签名不直接加密,而是要先hash形成摘要?原因是缩小签名密文的长度,加快数字签名的验证签名的运算速度。
HTTPS的加密方式的选择
基础的加密方式有对称加密和非对称加密,接下来尝试通过不同的组合进行数据加密。
1.对称加密
密钥暴露,数据不安全!
2.用两对非对称加密
如此看来,这种加密方式仍是不妥,仍是密钥被攻破了。
3.非对称加密+对称加密
这个我就不多描述被攻击的情况了,基本和第二种情况一样,都是密钥出了问题。之所以考虑这种配合主要是优化两对非对称加密的复杂性,毕竟对称加密是比较简单的。具体看图:
4.非对称加密+对称加密+证书认证(最终方案)
既然问题出在了最开始的公钥传送阶段,那么我们可以使用上面我们已经讲过的数字签证的手段,用来加密公钥。也就是说,在最开始传送的时候,服务端给客户端发送的是一个证书,里面有双方通信所需要的公钥。
上面这种加密方式才是如今HTTPS协议所采用的,基本可以实现数据的加密了。
总结
经过上面的学习,协议的雏形以及具体用法已经浮出水面了,这是网络知识的基石,写下这篇文章也是用来巩固自己所学知识。应用层的协议基本就讲到这里,以上内容有任何问题的欢迎在评论区留言~