Linux知识点 -- 网络基础 -- 应用层

Linux知识点 – 网络基础 – 应用层


在这里插入图片描述

一、使用协议来实现一个网络版的计算器

1.自定义协议

定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这个过程叫做"序列化"和"反序列化”;

在这里插入图片描述

Sock.hpp
将套接字封装成对象,其中包含套接字的创建与连接成员函数

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"

class Sock
{
private:
    const static int gbacklog = 20;

public:
    Sock() {}

    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }

    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    // 一般经验:
    // const string& 输入型参数
    // string* 输出型参数
    // string& 输入输出型参数

    int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof src;
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;
        }
        if(port)
        {
            *port = ntohs(src.sin_port);
        }
        if(ip)
        {
            *ip = inet_ntoa(src.sin_addr);
            return servicesock;
        }
    }

    bool Connect(int sock, const std::string& server_ip, const uint16_t& server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());
        if(connect(sock, (struct sockaddr*)&server, sizeof server) == 0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    ~Sock() {}
};

TcpServer.hpp
封装TCP服务接口的类;
注意:类内回调函数由于参数有this指针,无法正-常回调,因此需要设置成static成员,再通过参数传进this指针,来访问类内非静态成员;

#pragma once

#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>

namespace ns_tcpserver
{
    using func_t = std::function<void(int)>;
    class TcpServer;
    class ThreadData // 传入回调函数的参数
    {
    public:
        ThreadData(int sock, TcpServer *server)
            : _sock(sock), _server(server)
        {
        }
        ~ThreadData() {}

    public:
        int _sock;
        TcpServer *_server; // 里面有TcpServer对象的指针,由于回调函数是静态成员函数,无法访问非静态成员
                            // 这里的TcpServer对象指针是用来在回调函数中访问非静态成员的
    };

    class TcpServer
    {
    private:
        //如果是类内成员函数,参数中是有this指针的,多线程回调会出问题
        //因此需设置成静态成员,才可以回调
        static void* ThreadRoutine(void* args)
        {
            pthread_detach(pthread_self());//线程分离
            ThreadData* td = static_cast<ThreadData*>(args);//类型转换
            td->_server->Excute(td->_sock);//通过对象this指针调用成员函数
            close(td->_sock);
            return nullptr;
        }
    public:
        TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
        {
            // 创建套接字,绑定并监听
            _listensock = _sock.Socket();
            _sock.Bind(_listensock, port, ip);
            _sock.Listen(_listensock);
        }

        // 将服务请求放入函数队列
        void BindService(func_t func)
        {
            _func.push_back(func);
        }

        // 执行服务
        void Excute(int sock)
        {
            for (auto &f : _func)
            {
                f(sock);
            }
        }

        void Start()
        {
            for (;;)
            {
                std::string cli_ip;
                uint16_t cli_port;
                int sock = _sock.Accept(_listensock, &cli_ip, &cli_port);
                if (sock == -1)
                {
                    continue;
                }
                logMessage(NORMAL, "create new link succsee, sock: %d", sock);

                // 多线程处理请求
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, this);
                pthread_create(&tid, nullptr, ThreadRoutine, td);
            }
        }

        ~TcpServer()
        {
            if (_listensock >= 0)
            {
                close(_listensock);
            }
        }

    private:
        int _listensock;
        Sock _sock;
        std::vector<func_t> _func; // 回调函数列表
    };

}

Protocol.hpp
定制协议:
分别有计算请求的序列化和计算结果的序列化;

  • TCP协议的读写接口(read和write)都是将数据拷贝到缓冲区或者从缓冲区拷贝出来,并不是直接发送到对方主机;发送给对方主机是由TCP传输控制协议决定的
  • 由于TCP是面向字节流的协议,因此,发送和接受的次数,每次发送多少字符,都不受控制(UDP协议每次发送和接受的都是完整的报文),有可能每次接收到的不一定是完整的报文,也有可能一次读取了多个报文,所以需要自己定制协议解包代码;在读取时不能简单地receive,而需要对读取的数据进行解析;
  • 自主定制的协议使用"length\r\nx_ op_ y_\r\n"协议,前面加上数据长度;
    在这里插入图片描述
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"

namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0

    class Request // 计算请求序列
    {
    public:
        Request() {}
        Request(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {
        }

        ~Request() {}

        std::string Serialize() // 序列化
        {
#ifdef MYSELF
            // 使用自定义序列化方案
            // 将请求传换成string:_x _op _y的形式
            std::string str;
            str = std::to_string(_x);
            str += SPACE;
            str += _op;
            str += SPACE;
            str += std::to_string(_y);
            return str;
#else
            // 使用现成方案
            std::cout << "to do" << std::endl;
#endif
        }

        bool Deserialized(const std::string &str) // 反序列化
        {
#ifdef MYSELF
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
            {
                return false;
            }
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
            {
                return false;
            }
            _x = atoi(str.substr(0, left).c_str());
            _y = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
            {
                return false;
            }
            else
            {
                _op = str[left + SPACE_LEN];
            }
            return true;

#else
            std::cout << "to do" << std::endl;
#endif
        }

    public:
        int _x;
        int _y;
        char _op; // + - * / %
    };

    class Response // 计算结果响应序列
    {
    public:
        Response() {}

        Response(int result, int code)
            : _result(result), _code(code)
        {
        }

        ~Response() {}

        std::string Serialize() // 序列化:_code _result
        {
#ifdef MYSELF
            // 使用自定义序列化方案
            // 将请求传换成string:_x _op _y的形式
            std::string str;
            str = std::to_string(_code);
            str += SPACE;
            str += std::to_string(_result);
            return str;
#else
            // 使用现成方案
            std::cout << "to do" << std::endl;
#endif
        }

        bool Deserialized(const std::string &str) // 反序列化
        {
#ifdef MYSELF
            std::size_t pos = str.find(SPACE);
            if (pos == std::string::npos)
            {
                return false;
            }

            _code = atoi(str.substr(0, pos).c_str());
            _code = atoi(str.substr(pos + SPACE_LEN).c_str());
            return true;

#else
            std::cout << "to do" << std::endl;
#endif
        }

    public:
        int _result; // 计算结果
        int _code;   // 计算结果的状态码:运算是否成功
    };

    // 临时方案
    // 期望返回的是一个完整地报文
    bool Recv(int sock, std::string* out)
    {
        //TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求
        //因此需要解析协议,查看数据是否完整
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if(s == 0)
        {
            //客户端退出
            return false;
        }
        else
        {
            //读取错误
            return false;
        }
        return true;
    }

    void Send(int sock, const std::string str)
    {
        send(sock, str.c_str(), str.size(), 0);
    }

    //添加报文
    // "XXXXXX"
    // "123\r\nXXXXXX\r\n"
    std::string Encode(std::string &s)
    {
        std::string new_package = std::to_string(s.size());
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }


    //解析报文
    //规定报文的格式为:"length\r\nx_ op_ y_\r\n..."
    std::string Decode(std::string& buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if(pos == std::string::npos)
        {
            return "";//如果没找到分隔符,返回空串
        }
        int size = atoi(buffer.substr(0, pos).c_str());
        int surplus = buffer.size() - pos - 2*SEP_LEN;
        if(surplus >= size)
        {
            //至少有一份合法的报文,可以手动提取了
            buffer.erase(0, pos + SEP_LEN);
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);
            return s;
        }
        else
        {
            return "";//没有完整地报文,继续接收
        }
    }
}

CalServer.cc
计算服务

  • 服务器运行时,对端如果直接关闭,我们收到的就是空的信息,send的也是已经关闭的文件描述符,就可能导致服务器关闭;
    方案一:对SIGPIPE信号忽略,这样即使正在发送信息时对方关闭,也可以保证服务器不退出;

    在这里插入图片描述
    方案二:接收到信息时,需要判断信息的完整性,读取是否成功
  • 一般经验:在server编写的时候,要有较为严谨的判断逻辑;
    一般服务器都是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题;
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>

using namespace ns_protocol;
using namespace ns_tcpserver;

static void Usage(const std::string &process)
{
    std::cout << "\nUsage: " << process << " port\n"
              << std::endl;
}

// 进行计算
static Response calculatorHelper(const Request &req)
{
    Response resp(0, 0);
    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 (0 == req._y)
            resp._code = 1;
        else
            resp._result = req._x / req._y;
        break;
    case '%':
        if (0 == req._y)
            resp._code = 2;
        else
            resp._result = req._x % req._y;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    std::string inbuffer;//每次读取到的缓冲区
    while (true)
    {
        //1.读取成功
        bool res = Recv(sock, &inbuffer); // 读到了一个请求
        if(!res)
        {
            break;
        }
        //2.协议解析,保证得到一个完整的报文
        std::string package = Decode(inbuffer);
        if(package.empty())
        {
            continue; //如果读到的报文不完整,继续读取
        }
        logMessage(NORMAL, "%s", package.c_str());
        //3.保证该报文是一个完整的报文
        Request req;
        //4.反序列化,字节流->结构化
        req.Deserialized(package); // 反序列化
        //5.业务逻辑
        Response resp = calculatorHelper(req);
        //6.序列化
        std::string respString = resp.Serialize();//对计算结果进行序列化
        //7.添加长度信息,形成一个完整的报文
        respString = Encode(respString);
        //8.发送
        Send(sock, respString);//将结果序列发回给客户端
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    signal(SIGPIPE, SIG_IGN);

    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(calculator);
    server->Start();

    return 0;
}

CalClient.cc
客户端

#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"

using namespace ns_protocol;
static void Usage(const std::string &process)
{
    std::cout << "\nUsage: " << process << " serverIp serverPort\n"
              << std::endl;
}
// ./client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Sock sock;
    int sockfd = sock.Socket();
    if (!sock.Connect(sockfd, server_ip, server_port))
    {
        std::cerr << "Connect error" << std::endl;
        exit(2);
    }
    bool quit = false;
    std::string buffer;
    while (!quit)
    {
        // 1. 获取需求
        Request req;
        std::cout << "Please Enter # ";
        std::cin >> req._x >> req._op >> req._y;
        // 2. 序列化
        std::string s = req.Serialize();
        // std::string temp = s;
        // 3. 添加长度报头
        s = Encode(s);
        // 4. 发送给服务端
        Send(sockfd, s);
        // 5. 正常读取
        while (true)
        {
            bool res = Recv(sockfd, &buffer);
            if (!res)
            {
                quit = true;
                break;
            }
            std::string package = Decode(buffer);
            if (package.empty())
                continue;
            Response resp;
            resp.Deserialized(package);
            std::string err;
            switch (resp._code)
            {
            case 1:
                err = "除0错误";
                break;
            case 2:
                err = "模0错误";
                break;
            case 3:
                err = "非法操作";
                break;
            default:
                std::cout << resp._result << " [success]" << std::endl;
                break;
            }
            if(!err.empty()) std::cerr << err << std::endl;
            // sleep(1);
            break;//完整读取一个报文就退出
        }
    }
    close(sockfd);
    return 0;
}

运行结果:
在这里插入图片描述

2.守护进程

  • (1)前台进程:和终端关联的进程;在终端下能读取输入并作出反应(如bash);
    (2)任何xshell登陆,只允许一个前台进程和多个后台进程;
    (3)进程除了有自己的pid, ppid, 还有一个组ID;
    (4)在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash ->可以用匿名管道来进行通信;
    (5)而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程
    (6)任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程,或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
    (7)当用户退出登陆的时候,整个会话中的进程组都会结束;
    想让一个进程不再属于用户的会话,而是自成一个会话,这个进程称为守护进程
    (8)如何将进程变为守护进程->setsid()接口;
    (9)setsid要成功被调用,必须保证当前进程不是进程组的组长,可以通过fork创建的子进程实现;
    (10)守护进程不能直接向显示器打印消息,一旦打印,会被暂停,终止;

  • 如何在Linux正确的写一个让进程守护进程化的代码:
    写一个函数,让进程调用这个函数,自动变成守护进程;

  • /dev/null文件
    可以理解为一个文件黑洞,可以向里面打印数据,也可以从里面读取,但都不会有实际的数据输入输出;
    因此可以将标准输入,标准输出,标准错误重定向到devnull文件中;

Daemon.hpp

#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void MyDaemon()
{
    //1.忽略信号,SIPPIPE, SIGCHID
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    //2.不要让自己成为组长
    if(fork() > 0)
    {
        exit(0);//父进程退出,剩下子进程其实是孤儿进程
    }
    //3.调用setsid
    setsid();
    //4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
    int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
    if(devnull > 0)
    {
        dup2(0, devnull);
        dup2(1, devnull);
        dup2(2, devnull);
        close(devnull);
    }
}

CalServer.cc
在服务器进程中调用守护进程函数,让服务器进程成为守护进程;

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"

using namespace ns_protocol;
using namespace ns_tcpserver;

static void Usage(const std::string &process)
{
    std::cout << "\nUsage: " << process << " port\n"
              << std::endl;
}

// 进行计算
static Response calculatorHelper(const Request &req)
{
    Response resp(0, 0);
    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 (0 == req._y)
            resp._code = 1;
        else
            resp._result = req._x / req._y;
        break;
    case '%':
        if (0 == req._y)
            resp._code = 2;
        else
            resp._result = req._x % req._y;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    std::string inbuffer;//每次读取到的缓冲区
    while (true)
    {
        //1.读取成功
        bool res = Recv(sock, &inbuffer); // 读到了一个请求
        if(!res)
        {
            break;
        }
        //2.协议解析,保证得到一个完整的报文
        std::string package = Decode(inbuffer);
        if(package.empty())
        {
            continue; //如果读到的报文不完整,继续读取
        }
        logMessage(NORMAL, "%s", package.c_str());
        //3.保证该报文是一个完整的报文
        Request req;
        //4.反序列化,字节流->结构化
        req.Deserialized(package); // 反序列化
        //5.业务逻辑
        Response resp = calculatorHelper(req);
        //6.序列化
        std::string respString = resp.Serialize();//对计算结果进行序列化
        //7.添加长度信息,形成一个完整的报文
        respString = Encode(respString);
        //8.发送
        Send(sock, respString);//将结果序列发回给客户端
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    signal(SIGPIPE, SIG_IGN);
    MyDaemon();//让该进程成为守护进程,自成一个会话

    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(calculator);
    server->Start();

    return 0;
}

运行结果:
在这里插入图片描述
注:
在这里插入图片描述
守护进程实际上是孤儿进程,但是没有被系统领养,而是自成会话

这样下来,服务器进程成为了守护进程,自成一个会话,即使用户退出登录,该进程也不会退出;

3.使用json来完成序列化

json:网络通信的格式

  • 在Linux上安装json:
    在这里插入图片描述
  • json实际上是一个结构化数据格式,里面是很多的kv结构:
    在这里插入图片描述
  • json库的使用:
    在这里插入图片描述
    StyleWriter对象,两个kv对象之间有换行符;
    StyleWriter对象的write函数会将root中的kv内容直接转换为对应的string;

    运行结果:
    在这里插入图片描述
    在这里插入图片描述
    FastWriter对象,中间没有换行符
    运行结果:
    在这里插入图片描述
    json里面是可以套json的
    在这里插入图片描述

使用json协议完成序列化和反序列化:
由于使用的是非cpp官方库,因此需要添加编译选项:
makefile

.PHONY:all
all:CalClient CalServer

CalClient:CalClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
CalServer:CalServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp

.PHONY:clean
clean:
	rm -f CalClient CalServer

Protocol.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>

namespace ns_protocol
{
//#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0

    class Request // 计算请求序列
    {
    public:
        Request() {}
        Request(int x, int y, char op)
            : _x(x), _y(y), _op(op)
        {
        }

        ~Request() {}

        std::string Serialize() // 序列化
        {
#ifdef MYSELF
            // 使用自定义序列化方案
            // 将请求传换成string:_x _op _y的形式
            std::string str;
            str = std::to_string(_x);
            str += SPACE;
            str += _op;
            str += SPACE;
            str += std::to_string(_y);
            return str;
#else
            // 使用现成方案
            Json::Value root;
            root["x"] = _x;
            root["y"] = _y;
            root["op"] = _op;
            Json::FastWriter writer;
            return writer.write(root);  
#endif
        }

        bool Deserialized(const std::string &str) // 反序列化
        {
#ifdef MYSELF
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
            {
                return false;
            }
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
            {
                return false;
            }
            _x = atoi(str.substr(0, left).c_str());
            _y = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
            {
                return false;
            }
            else
            {
                _op = str[left + SPACE_LEN];
            }
            return true;

#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);//parse函数能够将序列化的json字符串直接读取到Value对象中
            _x = root["x"].asInt();
            _x = root["y"].asInt();
            _x = root["op"].asInt();//char类型实质也是int
            return true;
#endif
        }

    public:
        int _x;
        int _y;
        char _op; // + - * / %
    };

    class Response // 计算结果响应序列
    {
    public:
        Response() {}

        Response(int result, int code)
            : _result(result), _code(code)
        {
        }

        ~Response() {}

        std::string Serialize() // 序列化:_code _result
        {
#ifdef MYSELF
            // 使用自定义序列化方案
            // 将请求传换成string:_x _op _y的形式
            std::string str;
            str = std::to_string(_code);
            str += SPACE;
            str += std::to_string(_result);
            return str;
#else
            // 使用现成方案
            Json::Value root;
            root["code"] = _code;
            root["result"] = _result;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }

        bool Deserialized(const std::string &str) // 反序列化
        {
#ifdef MYSELF
            std::size_t pos = str.find(SPACE);
            if (pos == std::string::npos)
            {
                return false;
            }

            _code = atoi(str.substr(0, pos).c_str());
            _result = atoi(str.substr(pos + SPACE_LEN).c_str());
            return true;

#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            _code = root["code"].asInt();
            _result = root["result"].asInt();
            return true;
#endif
        }

    public:
        int _result; // 计算结果
        int _code;   // 计算结果的状态码:运算是否成功
    };

    // 临时方案
    // 期望返回的是一个完整地报文
    bool Recv(int sock, std::string* out)
    {
        //TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求
        //因此需要解析协议,查看数据是否完整
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if(s == 0)
        {
            //客户端退出
            return false;
        }
        else
        {
            //读取错误
            return false;
        }
        return true;
    }

    void Send(int sock, const std::string str)
    {
        int n = send(sock, str.c_str(), str.size(), 0);
        if(n < 0)
        {
            std::cout << "send error" << std::endl;
        }
    }

    //添加报头
    // "XXXXXX"
    // "123\r\nXXXXXX\r\n"
    std::string Encode(std::string &s)
    {
        std::string new_package = std::to_string(s.size());
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }


    //解析报文
    //规定报文的格式为:"length\r\nx_ op_ y_\r\n..."
    std::string Decode(std::string& buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if(pos == std::string::npos)
        {
            return "";//如果没找到分隔符,返回空串
        }
        int size = atoi(buffer.substr(0, pos).c_str());
        int surplus = buffer.size() - pos - 2*SEP_LEN;
        if(surplus >= size)
        {
            //至少有一份合法的报文,可以手动提取了
            buffer.erase(0, pos + SEP_LEN);
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);
            return s;
        }
        else
        {
            return "";//没有完整地报文,继续接收
        }
    }

}

运行结果:
在这里插入图片描述

二、HTTP协议

1.概念

  • 应用层:就是程序员基于socket接口之上编写的具体逻辑,有很多和文本处理相关的工作;http协议一定会有大量的文本分析和处理;

  • URL:我们平时说的网址,其结构如下;
    在这里插入图片描述
    其中,服务器地址IP就是域名,用来标识唯一的主机;冒号后面是端口号,标识特定主机上的特定进程;
    端口号后面是带层次的文件路径,其中第一个文件夹叫做web根目录;文件路径标识客户想访问的资源路径;
    URL:union resource local统一资源定位符,代表本次访问请求的资源位置,定位互联网中唯一的一份资源;
    在用户访问网络资源时,先通过url找到服务器上的特定文件资源,在进行读取或写入;

  • 如果用户想在url中包含url本身作为特殊字符使用的字符时,浏览器会自动对该字符进行编码,在服务端收到后,需要转回特殊字符;
    在这里插入图片描述在这里插入图片描述

2.HTTP协议请求和响应的报文格式

在这里插入图片描述
单纯在报文角度,http可以是基于行的文本协议;

  • 请求报文:
    请求行:方法 URL 协议版本
    http的方法为:
    在这里插入图片描述
    请求报头Header:多行kv结构,都是属性;
    空行:用来区分报头和有效载荷;
    请求正文(可以没有);

  • 响应报文:
    状态行:协议版本 状态码 状态码描述;
    响应报头;
    空行;
    响应正文;

3.使用HTTP协议进行网络通信

Log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./http.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    FILE *fp = fopen(LOGFILE, "a");
    // printf("%s%s\n", stdBuffer, logBuffer);
    fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    fclose(fp);
}

Sock.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"

class Sock
{
private:
    const static int gbacklog = 20;

public:
    Sock() {}
    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }
    void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    // 一般经验
    // const std::string &: 输入型参数
    // std::string *: 输出型参数
    // std::string &: 输入输出型参数
    int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;
        }
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }
    bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }
    ~Sock() {}
};

Usage.hpp

#pragma once
#include <iostream>
#include <string>
void Usage(std::string proc)
{
    std::cout << "\nUsage: " << proc <<  " port\n" << std::endl;
}

Util.hpp
工具类,分割字符串

#pragma once

#include <iostream>
#include <vector>

class Util
{
public:
    // aaaa\r\nbbbbb\r\nccc\r\n\r\n
    static void cutString(std::string s, const std::string &sep, std::vector<std::string> *out)
    {
        std::size_t start = 0;
        while (start < s.size())
        {
            auto pos = s.find(sep, start);
            if (pos == std::string::npos) break;
            std::string sub = s.substr(start, pos - start);
            // std::cout << "----" << sub << std::endl;
            out->push_back(sub);
            start += sub.size();
            start += sep.size();
        }
        if(start < s.size()) out->push_back(s.substr(start));
    }
};

HttpServer.hpp

#pragma once

#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"

class HttpServer
{
public:
    using func_t = std::function<void(int)>;
private:
    int listensock_;
    uint16_t port_;
    Sock sock;
    func_t func_;
public:
    HttpServer(const uint16_t &port, func_t func): port_(port),func_(func)
    {
        listensock_ = sock.Socket();
        sock.Bind(listensock_, port_);
        sock.Listen(listensock_);
    }
    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        for( ; ; )
        {
            std::string clientIp;
            uint16_t clientPort = 0;
            int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);
            if(sockfd < 0) continue;
            if(fork() == 0)
            {
                close(listensock_);
                func_(sockfd);
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }
    ~HttpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }
};

HttpServer.cc
这里是主要的对http协议进行解析的代码,逐行解析,提取首行url,访问目标资源;

#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{
    // 1. 读取请求 for test
    char buffer[10240];
    ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (s > 0)
    {
        buffer[s] = 0;
        // std::cout << buffer << "--------------------\n" << std::endl;
    }
    std::vector<std::string> vline; // 取出http请求的每一行
    Util::cutString(buffer, "\n", &vline);
    std::vector<std::string> vblock; // 取出第一行的每一个子串
    Util::cutString(vline[0], " ", &vblock);
    std::string file = vblock[1]; // 请求的资源
    std::string target = ROOT;
    if(file == "/") file = "/index.html";
    target += file; //请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录
    std::cout << target << std::endl;
    std::string content;
    std::ifstream in(target); // 打开文件
    if(in.is_open())
    {
        std::string line;
        while(std::getline(in, line))
        {
            content += line;
        }
        in.close();
    }
    std::string HttpResponse;
    if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n";
    else HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    HttpResponse += content;
        // std::cout << "####start################" << std::endl;
        // for(auto &iter : vblock)
        // {
        //     std::cout << "---" << iter << "\n" << std::endl;
        // }
        // std::cout << "#####end###############" << std::endl;
        // 2. 试着构建一个http的响应
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

在目录下创建web根目录wwwroot,里面创建首页index.html;
在这里插入图片描述
index.html
在vscode下装插件,!table会出现网页模板;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>lmx</title>
</head>
<body>
    <h3>这个一个Linux课程</h3>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
</body>
</html>

运行结果:
在这里插入图片描述

4.HTTP协议的方法

在这里插入图片描述
其中最常用的是GET和POST方法;

  • 用户数据提交到服务器的流程:
    用户发起申请,形成表单,指明提交方法,表单中的数据,会被转成http request的一部分,之后收集用户数据,并把用户数据推送给服务器;

  • GET方法可以将数据从服务器端拿到客户端,也可以将客户端的数据提交到服务器;
    使用GET方法提交请求
    web目录结构:
    在这里插入图片描述
    index.html
    使用GET方法将进行提交
    在这里插入图片描述
    **input是按钮,其中的action是点击按钮后访问的文件,method是方法,这里是GET;
    下面的Username和Password是kv结构输入框,type是内容,name是标签;
    **
    运行结果:
    在这里插入图片描述
    使用浏览器访问建立好的网页,这是一个可以登陆的界面;
    在这里插入图片描述
    输入好用户名和密码后,点击登录;
    在这里插入图片描述
    跳转到如上界面,登陆的时候其实就是把用户信息提交给服务器;
    在这里插入图片描述
    在上面的网址栏可以看到自己输入的用户名和密码,?后面是参数,前面是提交的地址,就是将参数提交到目标文件中;
    服务器收到的请求:
    在这里插入图片描述
    这是因为get方法通过url传参,并将参数回显到url中;

  • POST方法用于将客户端的数据提交到服务器;
    使用POST方法提交请求
    insex.html
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    点击登录:
    在这里插入图片描述
    服务器收到的请求:
    在这里插入图片描述
    POST是不会通过URL传参的,它通过正文传参;

总结

  • GET方法通过URL传参,回显输入的私密信息,不够私密;
  • POST方法通过正文传参,不会回显私密信息,私密性有保证;
  • 私密性不是安全性;
  • 登录和注册一般常用的是POST方法;
    内容较大也建议使用POST方法,因为POST方法里面有正文长度,方便整段读取;

5.HTTP协议的状态码

在这里插入图片描述

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

  • 重定向当网页进行请求时,需要跳转到其他网页;
    301:永久移动,直接重定向到另一个网也,不会返回原来的网页,影响用户后续的请求策略;
    302:临时移动,临时重定向到另一个网页,比如登陆界面,处理好后再返回原始网页,不影响用户后续的请求策略;
    307:临时重定向;

  • 重定向过程
    客户端向服务器发起http请求 -> 服务器返回30X重定向状态码,并携带新的网页地址信息 -> 客户端浏览器拿到新的地址后,自动向新的地址发起请求;
    在这里插入图片描述

重定向实验
HttpServer.cc
在这里插入图片描述
如果读取的文件不存在,返回的状态码为301,会进行重定向操作;
其中Location属性就是重定向后的目标文件地址;

#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{
    // 1. 读取请求 for test
    char buffer[10240];
    ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer << "\n--------------------\n"
                  << std::endl;
    }

    std::vector<std::string> vline; // 取出http请求的每一行
    Util::cutString(buffer, "\n", &vline);

    std::vector<std::string> vblock; // 取出第一行的每一个子串
    Util::cutString(vline[0], " ", &vblock);

    std::string file = vblock[1]; // 请求的资源
    std::string target = ROOT;

    if (file == "/")
        file = "/index.html";

    target += file; // 请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录
    std::cout << target << std::endl;

    std::string content;      // 文件中的内容
    std::ifstream in(target); // 打开文件
    if (in.is_open())
    {
        std::string line;
        while (std::getline(in, line))
        {
            content += line;
        }
        in.close();
    }

    std::string HttpResponse;
    if (content.empty())
    {
        HttpResponse = "HTTP/1.1 301 NotFound\r\n";
        HttpResponse += "Location: http://47.115.213.66:8080/a/b/404.html\r\n";
    }
    else
        HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    HttpResponse += content;
    // std::cout << "####start################" << std::endl;
    // for(auto &iter : vblock)
    // {
    //     std::cout << "---" << iter << "\n" << std::endl;
    // }
    // std::cout << "#####end###############" << std::endl;
    // 2. 试着构建一个http的响应
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

index.html
客户端点击登陆后,会跳转到/a/b/notexit.html这个地址的文件;

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>lmx</title>
</head>

<body>
    <h3>Hello Guests!</h3>
    <form name="input" action="/a/b/notexit.html" method="POST">
        Username: <input type="text" name="user"> <br/>
        Password: <input type="password" name="pwd"> <br/>
        <input type="submit" value="登陆">
    </form>
</body>

</html>

404.html
重定向的目标文件;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>不存在</title>
</head>
<body>
    <h2>你访问的页面不存在</h2>
</body>
</html>

运行结果:
客户端访问网页HOME地址:
在这里插入图片描述
点击登陆后,访问/a/b/notexit.html这个地址的文件,但是这个文件是不存在的,文件读取返回结果为空,状态码为301,触发重定向;
在这里插入图片描述
重定向到了a/b/404.html这个文件;

6.HTTP协议的报头

Content-Type:数据类型(text/html等);
Content-Length:Body(正文)的长度;
Host:客户端告知服务器所请求的资源是在哪个主机的哪个端口上;
User-Agent:声明用户的操作系统和浏览器版本信息;
referer:当前页面是从哪个页面跳转过来的;
location:搭配3xx状态码使用,告诉客户端接下来要去哪里访问;
Cookie:用于在客户端存储少量信息.通常用于实现会话(session)的功能;

  • Content-Type、Content-Length
    添加内容类型及正文长度报头;
    在这里插入图片描述
  • Cookie会话管理
    http的特征:
    a.简单快速;
    b.无连接,指http不维护连接,连接是由TCP维护的;
    c.无状态,http不会记录用户曾经请求的网页,不会对用户的行为做记录;

    http协议是无状态的,但是我们平常在浏览器进行访问网页时,一般网站是会记录下我们的状态的,这是因为http协议为了支持常规用户的会话管理,支持两个报头属性Cookie(请求)、Set-Cookie(响应)
    用户登录后,曾经输入的用户名和密码等信息会保存为一个文件,在今后每次的http请求中,每次都会携带这个文件中的账户密码内容,这个文件就是cookie文件;
    cookie文件的创建与使用流程:
    当用户访问网站后,在网站上输入用户密码信息,之后服务器会将用户信息返回给客户端,客户端的浏览器会将用户信息保存,形成cookie文件,之后用户每次访问该网站,都会将cookie文件再次上传到服务器,进行用户星系比对,不用每次都重新输入信息了;
    在这里插入图片描述
    但是cookie文件中是将用户信息明文保存的,如果被黑客注入木马病毒,是能够盗取用户的私密信息;
    现在的新cookie方案:在网站认证用户信息后,服务端会形成一个用户唯一ID,session id,并返回给用户端,保存到cookie文件中;这样每次用户访问网站,上传的cookie文件都是用户在网站形成的唯一session id,就算被盗取,也不会暴露用户的私密信息;

验证cookie
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.connetion选项

在这里插入图片描述
keep-alive:长连接,网页该有的资源通过一个连接全部拿到;
close:短连接,处理完一个http请求后,就将连接关掉,每次都要建立连接获取图片等资源;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值