网络通信之应用层协议--Linux

文章目录

  • 关于应用层协议的理解
  • 应用层协议的制定
    • 理论部分
    • 代码部分
    • 完整代码以及测试
  • HTTP协议
    • 代码测试HTTP协议
  • HTTPS协议
    • 加密原因
    • 基础的加密方式
    • 数据摘要(数据指纹)
    • 数字签名
    • HTTPS的加密方式的选择
  • 总结

关于应用层协议的理解

在之前的一篇关于套接字实现网络通信一文中,进行通信的内容多是字符串,内容形式可谓是很简单了。在传输的过程中只需要控制数据的传输大小,字符串就会通过套接字进行传送接收。但是结合实际生活来看,通信的场景却不只是这么简单的内容,比如很有可能是一个结构体,里面有着各种不同数据类型的变量。这里我们就希望用一种统一的视角来看待各不相同的数据结构体(C/C++)或者是对象(C++),无论是多么复杂的数据结构,我们总可以将我们所需要的数据进行排列并转化成字符串,并将这种转换方式规定为一种约定好的协议,使得大家都认同这种处理方式,以便于从字符串中准确读定位并读取所需要的信息。

简而言之,应用层协议是一种数据转换成字符串的转换方式,使得程序员可以按需传送相当复杂的数据结构。并且,协议可以由程序员自己规定,只要有人认同并使用该程序员的协议,那么就可以实现正常的网络通信了。经过网络发展的几十年,一些比较成熟的协议被几乎所有的程序员所认同,例如著名的http和https,都已经写好并投入使用好多年,作为网络协议的初始学习对象是再合适不过的,但在学习http与https相关协议之前,我们不妨试着自己写一个简单的协议,具体感受一下协议的定制过程,加深对协议的了解。

应用层协议的制定

接下来我就以网络简单版的计算器为示例,制定协议。

理论部分

第一步,在制定协议之前,得先清楚计算器的主要构成:操作数A、操作符、操作数B、计算结果、计算状态码(标识计算是否出现错误)其中操作数A、操作符、操作数B都是客户端的请求数据,而计算结果、计算状态码则是服务端的响应数据,因此我们需要两个结构体来进行数据的传送。

第二步,规定转换的字符串的风格,一般情况下该字符串我们成为报文,由报头和有效载荷两部分构成,如:

image-20230326150709426

如果具体到算式,比如1+1,则有下面的客户端请求的报文形式:

image-20230326151745252

其中报头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;
}

测试结果:

image-20230326203423572

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的长度;

例如:

image-20230326205323265

响应风格

首行: [版本号] + [状态码] + [状态码解释]

Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束

Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.

例如:

image-20230326205648749

上面的正文部分就是一个网页的代码内容。

HTTP的方法

image-20230326224923212

其中最常用的就是GET方法和POST方法。二者的区别就是GET的正文在URL(网址)中,二者都是明文传输数据,数据在网络世界中裸奔。数据并不是隐私的!

HTTP的状态码

image-20230326230101169

最常见的状态码: 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)……

代码测试HTTP协议

那么接下来我们就可以试着自己写一个简易版的网页了,我们只需要写一个服务端就可以,可以使用浏览器充当客户端。因此,使用TCP进行正常的网络通信,只不过协议变成了HTTP协议,响应的内容除了首行和Header,正文的部分必须得是一个网页的代码,这个可以算是网页资源了,我们可以通过文件的方式将网页的代码保存起来,客户端在申请网页资源的时候,可以从服务器中对应文件中读取对应的代码资源,实现网页响应。

例如:

image-20230326211115117

上面是我自己在测试的时候创建的文件,一般默认资源的根目录是wwwroot。

图解

image-20230326222942440

服务端代码

//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>

测试结果

image-20230326223214071

HTTPS协议

HTTPS是在HTTP协议上加了一层加密层的协议,但是为什么要加密呢?

加密原因

由于应用层之间的信息传输是需要交由下层的传输机制来完成,这就意味着在传输层及其以下的位置都有可能被非法的第三方运营商劫持,篡改信息简直是再简单不过的事情。一旦涉及比较重要的数据,就会出现严重的经济和安全问题:银行账户余额、工作评定、卫星导航……因此重要的数据要想安全传输,在应用层与传输层之间加密是必不可少的。

image-20230326232438215

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等,算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概非常低)

摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推原信息,通常用来进行数据对比。

数字签名

摘要经过加密,得到数字签名。这是用来防止数据被篡改的一种机制。

image-20230327110845874

上面这种加密方式设计的非常巧妙,共有两层加密。

第一层加密就是数字摘要与数据的验证,一旦数据被修改,那么数据再次求得的摘要就会和之前的摘要相差很大。

第二层加密是使用私钥对数字摘要进行加密,这是防止第三者修改数据的同时也修改了数字摘要。毕竟公钥是公开的,数据摘要是私钥加密,任何第三者拿着公钥都可以拿到数字摘要并修改,但问题在于,修改完之后是没办法加密回去的!因为缺少私钥!强行使用公钥加密也不是不可以,在验证阶段就会出问题,这时候就知道数据被第三者修了。

image-20230327113319280

这样一来,只要保证好私钥是唯一的,并且拥有者是比较有公信力的,不会监守自盗,那么就能保证数据安全了。

签名的数据:证书

🔺为什么签名不直接加密,而是要先hash形成摘要?原因是缩小签名密文的长度,加快数字签名的验证签名的运算速度。

HTTPS的加密方式的选择

基础的加密方式有对称加密和非对称加密,接下来尝试通过不同的组合进行数据加密。

1.对称加密

image-20230327193947171

密钥暴露,数据不安全!

2.用两对非对称加密

image-20230327202643975

如此看来,这种加密方式仍是不妥,仍是密钥被攻破了。

3.非对称加密+对称加密

这个我就不多描述被攻击的情况了,基本和第二种情况一样,都是密钥出了问题。之所以考虑这种配合主要是优化两对非对称加密的复杂性,毕竟对称加密是比较简单的。具体看图:

image-20230327204905979

4.非对称加密+对称加密+证书认证(最终方案)

既然问题出在了最开始的公钥传送阶段,那么我们可以使用上面我们已经讲过的数字签证的手段,用来加密公钥。也就是说,在最开始传送的时候,服务端给客户端发送的是一个证书,里面有双方通信所需要的公钥。

image-20230327212654234

上面这种加密方式才是如今HTTPS协议所采用的,基本可以实现数据的加密了。

总结

经过上面的学习,协议的雏形以及具体用法已经浮出水面了,这是网络知识的基石,写下这篇文章也是用来巩固自己所学知识。应用层的协议基本就讲到这里,以上内容有任何问题的欢迎在评论区留言~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/3873.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

天梯赛刷题小记 —— L2

最近在重刷 天梯赛&#xff0c;浅浅记录一下&#xff0c;进入L2阶段了 L2-001 紧急救援 解题思路&#xff1a;典型的dijkstra模板题&#xff0c;带路径记录与权重&#xff0c;方案数记录&#xff0c;解析出过 Dijkstra(兼路径) #include <bits/stdc.h> #define inf…

【数据分析之道-基础知识(三)】元组

文章目录专栏导读1、元组简介2、元组创建3、元组查找操作4、元组删除操作5、元组修改操作6、元组增加操作7、元组内置函数专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN Python领域新星创作者&#xff0c;专注于分享python领域知识。 ✍ 本文录入于《数据分析之道》&…

自动驾驶控制概况

文章目录1. 第一章行为决策在自动驾驶系统架构中的位置2. 路径跟踪控制的种类2.1 基于自行车模型的路径跟踪控制算法2.1.1 纯跟踪控制&#xff08;Pure Pursuit&#xff09;算法2.1.2 后轮反馈控制算法&#xff08;Rear wheel feedback&#xff09;2.1.3前轮反馈控制算法&#…

防火墙 NAT地址转换

网络地址转换&#xff08;NAT&#xff09;是一种用于访问Internet访问模式广域网&#xff08;WAN&#xff09;的技术&#xff0c;用于将私有&#xff08;保留&#xff09;地址转换为合法IP地址。NAT不仅能够有效地额抵抗外部网络攻击&#xff0c;还能够在IP地址分配不理想&…

Windows权限提升—令牌窃取、UAC提权、进程注入等提权

Windows权限提升—令牌窃取、UNC提权、进程注入等提权1. 前言2. at本地命令提权2.1. 适用范围2.2. 命令使用2.3. 操作步骤2.3.1. 模拟提权2.3.2. at配合msf提权2.3.2.1. 生成木马文件2.3.2.2. 设置监听2.3.2.3. 设置反弹2.3.2.4. 查看反弹效果3. sc本地命令提权3.1. 适用范围3.…

瑟瑟发抖吧——用了这款软件,我的开发效率提升了50%

一、前言 开发中&#xff0c;一直听到有人讨论是否需要重复造轮子&#xff0c;我觉得有能力的人&#xff0c;轮子得造。但是往往开发周期短&#xff0c;用轮子所节省的时间去更好的理解业务&#xff0c;应用到业务中&#xff0c;也能清晰发现轮子的利弊&#xff0c;一定意义上…

Warshall算法

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a;> 算法 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我…

树莓派Linux源码配置,树莓派Linux内核编译,树莓派Linux内核更换

目录 一 树莓派Linux的源码配置 ① 内核源码下载说明 ② 三种方法配置源码 二 树莓派Linux内核编译 ① 内核编译 ② 编译时报错及解决方案&#xff08;亲测&#xff09; 三 更换树莓派Linux内核 操作步骤说明 ● dmesg报错及解决方案&#xff08;亲测&#xff0…

算法刷题笔记

特定方法 KMP算法&#xff1a;字符串匹配 逆波兰表达式&#xff1a;计算值 斐波那契数&#xff1a;动态规划 强制类型转换&#xff1a;整型->字符串&#xff1a;to_string&#xff0c;字符串->整型&#xff1a;stoi 一、数组 数组&#xff1a;下标从0开始&#xff0c;内存…

蓝桥杯嵌入式--LCD屏幕使用提升

前言之前在专栏里已经介绍过LCD相关库文件的移植&#xff0c;今天来介绍一下对于LCD屏幕的使用技巧。屏幕基本配置与函数一、屏幕初始化使用lcd前的必要步骤就是对LCD屏幕进行初始化操作&#xff0c;这也是一个容易忘记的操作。LCD_Init();\\使用lcd前的必要步骤就是对LCD屏幕进…

蓝桥杯倒计时 | 倒计时17天

作者&#x1f575;️‍♂️&#xff1a;让机器理解语言か 专栏&#x1f387;&#xff1a;蓝桥杯倒计时冲刺 描述&#x1f3a8;&#xff1a;蓝桥杯冲刺阶段&#xff0c;一定要沉住气&#xff0c;一步一个脚印&#xff0c;胜利就在前方&#xff01; 寄语&#x1f493;&#xff1a…

将一段数字转为字符串

将一段数字转为字符串 string turn(long long x){string str;while(x){int tx%10;// 数字0的ascii码为48&#xff01;char ct48;strc;// string类拼接方式x/10;}reverse(str.begin(),str.end()); // 不要忘了反转字符串return str; }例: #include<iostream> #include&l…

使用VS Code 配置Ubuntu远程C++开发环境

使用VS Code 配置Ubuntu远程C开发环境 环境准备 VS CodeWSL Ubuntu 虚拟机 配置步骤 在Ubuntu 中配置ssh远程登录 Ubuntu 配置远程登录 VsCode 安装 Remote-ssh 插件 打开vscode ssh configure ,填入相关信息 ​ Host : 主机名称&#xff0c;在左侧列表中显示的名称 ​ …

【Linux】[万字] Linux下的文件操作 及 Linux文件描述符fd 详解

在Linux操作系统中, 文件描述符是一个至关重要的概念. 理解了文件描述符, 其实就可以相当于理解了Linux系统的关于内存文件系统的整个大致框架和逻辑 但是在介绍文件描述符之前, Linux关于文件还存在许多 概念和文件操作 的知识需要介绍一下, 就当作是为解释文件描述符所做的…

IDEA连接Linux服务器进行文件操作

IDEA连接Linux服务器进行文件操作 文章目录IDEA连接Linux服务器进行文件操作连接的作用和意义安装openssh开启openssh服务验证是否开启服务安装网络工具包查看虚拟机IP地址Idea连接Linux虚拟机打开配置页面配置SFTP配置SSH完成后出现的配置文件安装big data tools插件连接的作用…

【RV1126】调试GT911,1024x600 7寸 MIPI 电容触摸屏

文章目录一、驱动注册失败二、触摸屏可以触摸&#xff0c;但是x轴数据反了三、可以触摸了&#xff0c;但是Y轴数据跳变&#xff0c;几乎只有一半的屏幕是可以正常滑动的三、汇顶触摸屏配置文件解析四、使用新的配置文件4.1 新配置解决问题4.2 测试触摸的方法在kernel增加frame …

【学习经验分享NO.21】学习资料分享(持续更新)

本博客将收集整理人工智能深度学习相关资料&#xff0c;进行整理&#xff0c;供大家学习使用。如果有需要帮忙整理的请留言。将不断更新&#xff0c;请持续关注。 一、深度学习论文资料 链接&#xff1a;https://pan.baidu.com/s/18LO5df0dp9-IE8Z3aFyrPg 提取码&#xff1a;c…

记录springboot+vue+fastdfs实现简易的文件(上传、下载、删除、预览)操作

前言说明&#xff1a;springboot vue FastDFS实现文件上传&#xff08;支持预览&#xff09;升级版 FASTDFS部分 FASTDFS安装过程&#xff1a;基于centos 7安装FastDFS文件服务器 SpringBoot部分 springboot源码实现 package com.core.doc.controller;import com.baomid…

【多线程】CAS

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;Java EE初阶&#x1f447; 目 录&#x1f40d;一. 什么是 CAS&#x1f98e;二. CAS 是怎么实现的&#x1f996;三. CAS 典型应用场景&#x1f436;1. 实现原子类&#x1f431;2. 实现自旋锁&#x1f995;四. …

进程间通信----信号量

文章目录信号量1. 问题2. 什么是信号量3. 信号量的使用4. 信号量的控制6. 实例信号量 1. 问题 程序中&#xff0c;有时存在一种特殊代码&#xff0c;最多只允许一个进程执行该部分代码。这部分区域&#xff0c;称为“临界区” 然而在多进程并发执行时&#xff0c;当一个进程进…
最新文章