Linux操作系统学习记录之----自定义协议(网络计算器)


I- 封装套接字(TcpServer.hpp)

Socket 作为ip+端口号的集合 , 可以表示唯一一台电脑上的唯一网络进程 , 是网络通信的基础 . 但是 , socket的原生接口有c语言实现 , 因此面向过程式的使用很不优雅 , 因此先进行c++式面向对象的封装 .

I.1- 骨架(socket的基本构建):

#Linux/网络/socket封装/模板设计模式

  • 套接字socket分为tcp和udp两大类 , 两者在操作上大部分相同 , 因此使用模板设计模式
  • 这里只使用Tcp套接字 , 但这样的设计也方便之后的代码复用.

下面代码中:

  • 基类Socket , 派生类TcpSocket
  • 基类中将 套接字创建- 绑定 - 监听设置为纯虚函数 ,强制派生类重写 ; 并在基类中定义构建方法 BuildServerTcp实现优雅的Tcp服务端初始化
c++ 复制代码
class Socket
{
public:
    //创建套接字
    virtual void SocketOrDie() = 0;
    //绑定
    virtual void BindOrDie(uint16_t port) = 0;
    //监听
    virtual void ListenOrDie(int backlog) = 0;
public:
    //构建ServerTcp
    void BuildServerTcp(uint16_t port,int backlog)
    {
        SocketOrDie();
        BindOrDie(port);
        ListenOrDie(backlog);
    }
};


class TcpSocket : public Socket
{
public:
    void SocketOrDie() override
    {
        _fd = socket(AF_INET , SOCK_STREAM , 0);
        if(_fd < 0)
        {
            LOG(LogLevel::FATAL) << "套接字创建失败\n";
            exit(ExitNum::SocketErr);
        }
        LOG(LogLevel::INFO) << "套接字创建成功,fd=" << std::to_string(_fd) << "\n";
    }
    void BindOrDie(uint16_t port) override
    {
        AddrIn addr(port);
        int n =bind(_fd,addr.GetAddr(),addr.GetSize());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败\n";
            exit(ExitNum::BindErr);
        }
        LOG(LogLevel::INFO) << "服务端绑定成功!!!\n";
    }
    void ListenOrDie(int backlog) override
    {
        int n = listen(_fd,backlog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "监听失败\n";
            exit(ExitNum::ListenErr);
        }
        LOG(LogLevel::INFO) << "服务器监听成功!!!\n";
    }
private:
    int _fd;
};

I.2- Accept函数:

  • Tcp通信面向连接 , 而accept函数的调用正是告诉未来的客户端 : "我接受你们的连接" .
  • 而客户端的连接信息同样需要记录 , 所以调用 accept又会创建一个独立于 socket的套接字 .

下面的代码中:

  • 理解这段代码的设计需要站在调用方的角度 , 即套接字的使用者---TcpServer .
  • 进行网络通信时需要 accept函数返回的套接字 , 而TcpSocket类里已经有了一个_sock来记录socket函数返回的套接字,此时有两种做法:
    1. 在TcpSocket类的成员变量里定义两个套接字 : listen_socketservice_socket
    2. 不新增套接字成员变量 , 而是让Accept函数返回一个TcpSocket对象的指针(涉及返回值拷贝,所以用shared_ptr).
  • 进行网络通信时同样需要 客户端的网络地址信息 , 因此使用传递一个AddrIn类的输出型参数来将客户端信息带出来.

I.2.1- 踩过的坑:

这是调用方(TcpServer的Start函数中 )期望的用法 :

c++ 复制代码
AddrIn addr;
std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
//然后就可以通过sock对象来进行send和receive了 (其中sock对象的_fd是新的 , 由Accept而来,而非之前的socket)

我的坑 :

可以看到 , 我直接在第三行把 当前对象的成员变量_fd给覆盖了!!! 于是我虽然返回了正确的Socket对象 ,但是把原来那个给破坏了.

c++ 复制代码
std::shared_ptr<Socket> Accept(AddrIn& addr) override  
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        _fd = accept(_fd,CONV(peer),&len);    //大坑!!!!!!!!!!
        if(fd < 0)
        {
            LOG(LogLevel::FATAL) << "接受失败\n";
            return nullptr;
        }
        LOG(LogLevel::INFO) << "accept成功!!!\n";
        addr.SetAddr(peer);
        return std::make_shared<TcpSocket>();    
    }

纠正:

c++ 复制代码
//添加构造函数
TcpSocket(int fd) :_fd(fd)
{}

//正确的Accept
std::shared_ptr<Socket> Accept(AddrIn& addr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(_fd,CONV(peer),&len);  //使用局部变量接受新fd,而非成员变量_fd
        if(new_sock < 0)
        {
            LOG(LogLevel::FATAL) << "接受失败\n";
            return nullptr;
        }
        LOG(LogLevel::INFO) << "accept成功!!! , 新fd = " << std::to_string(_fd);
        addr.SetAddr(peer);
        return std::make_shared<TcpSocket>(new_sock);  //使用新的局部fd来构造新对象的指针
    }

I.3- Receive函数:

Tcp通信面向字节流 , 服务器本身不关心接收到的具体是什么内容.

上层期望的调用方式:

对数据流的读取情况做判断

c++ 复制代码
std::string buffer;
int n = sock->Receive(&buffer);
//在根据返回值n来决定处理逻辑

底层实现:

只管数据流式的接受 , 不做任何处理和判断 ; 使用一个输出型参数将读取到的内容带出去

c++ 复制代码
 //接收数据
    virtual int Receive(std::string* st) override
    {
        char buffer[1024];
        int n = recv(_fd , buffer,sizeof(buffer)-1 , 0);  //sizeof(buffer)-1很关键 ,不然可能和buffer[len] = 0;冲突
        
        if(n > 0)
        {
            buffer[n] = '\0';
            *(st) += buffer;
        }
        return n;
    }

II- 服务端代码(一部分)

c++ 复制代码
const int DefaultBackLog = 8;

class TcpServer
{
public:
    TcpServer(uint16_t port, int backlog = DefaultBackLog) : _port(port), _listen_socket(std::make_unique<TcpSocket>()), _isRunning(false)
    {
        _listen_socket->BuildServerTcp(_port, backlog);
    }
    void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            AddrIn addr;
            std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
            // Accept失败则重新尝试
            if (sock == nullptr)
                continue;
            // Accept成功后尝试接收数据
            std::string buffer;
            int n = sock->Receive(&buffer);
            if (n < 0)
            {
                LOG(LogLevel::ERROR) << "数据接受失败\n";
                break;
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "客户端退出...:" << addr.GetInfo() << "\n";
                break;
            }
            else
            {
                //开始创建子进程处理任务
                int pid = fork();
                if(pid == 0)
                {
                    if(fork() > 0)
                    {
                        exit(0);
                    }
                    //此时孙子进程为孤儿进程
                    // TODO : 对于数据的处理逻辑
                    //.................................................
                    //.................................
                    sock->Close();
                    exit(ExitNum::OK);
                }
                else
                {
                    int n = waitpid(pid,nullptr,0);
                    (void)n ;
                }
            }
        }

        _isRunning = false;
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listen_socket;

    bool _isRunning;
};

III- 协议定制(网络计算器) :

到此为止 , 服务器的主要逻辑已经完成 .但是Tcp作为面向字节流的协议 , 想要保证服务端和客户端能够正确的处理字节流 , 就需要我们定制协议了.

  • 目标 : 实现一个网络计算器 , 能够接受类似 "4+8" 这样的客户端请求 , 并返回结果 "12".

III.1- 序列化和反序列化:

III.1.1- 序列化方案 : JSON

III.1.1.1- 头文件

jsoncpp/json/json.hpp

注: 这是第三方库 , 在用g++编译时要带上选项-ljsoncpp

III.1.1.2- 基础类

核心元素

  • Json::Value : Json的格式化数据 , 方便被Json相关类的函数处理.

序列化相关

  • Json::StyledWriter : 可用于生成可读性高的Json串(换行) , 便于调试
  • Json::FastWriter : 可用于生成更精简的Json串(不换行) , 方便网络传输
  • Json::StreamWriter : 可用于更加个性化的Json串 . 常常用 StreamWriterBuilder类对象的newStreamWriter 并结合 streamstring类的对象来初始化.

反序列化相关

  • Json::Reader : 可以使用类成员函数Parse来将

III.1.2- 封装Request和Response类

III.1.2.1- Request类

Request类主要代表客户端 , 用于序列化客户端请求后发送 , 将其反序列化给服务端来接受 .

III.1.2.2- Response类

Response类主要面向服务端 , 用于将服务端的计算结果序列化 , 并在客户端反序列化.

III.2- 协议定制(Protocol.hpp):

III.2.1- 遇到的问题:

III.2.1.1- GetRequest函数的参数问题 :
  • Tcp服务端面向字节流 , 需要面对数据的序列和反序列化处理 .
  • 为了解耦合 , 服务端只负责建立连接和派发任务 . 处理数据和收发数据的动作交给协议 .
  • 服务端通过回调函数调用协议中的相关函数来进行处理.

因此, Protocol类的 GetRequest作为提供给服务端的回调方法 :

  1. 首先要接受套接字信息(保证最基础的网络收发功能)->std::shared_ptr<Socket> sock
  2. 其次 , 为了知道客户端信息 , 也接受网络地址信息 -> const AddrIn &client

我一开始这样传递的 ,完全是无稽之谈_:void GetRequest(std::string & buffer_queue, const AddrIn & addr)

最终的函数声明: void GetRequest(std::shared_ptr<Socket> sock, const AddrIn &client)

III.2.1.2- GetResponse函数的参数问题:
  • 同样是因为Tcp服务端面向字节流的特性 , 客户端接受到的回复也不一定是刚刚好的 , 有可能多余一个有效载荷.

因此 , Protocol类的 GetResponse作为提供给客户端的函数 :

  1. 首先 , 要接受套接字信息(保证最基础的网络收发功能) -> std::shared_ptr<Socket> sock
  2. 其次 , 需要一个局部的缓冲区无条件保存服务端发来的原始消息 -> std::string resp_buffer
  3. 最后 , 需要就原式消息 resp_buffer 进行处理后得到反序列化过的有效载荷 , 并存在对象里方便打印和读取 -> Response& response

最终的函数声明 : bool GetResponse(std::shared_ptr<Socket> sock, std::string& resp_buffer,Response& response)

其中 : 返回值代表是否获得有效数据

III.3- 协议的回调方法(NetCal.hpp)

这个最简单 , 可以使用仿函数实现 .

一个细节 : 除了计算结果之外 , 还要设置Response对象的标志位 . 以便让客户端判断结果的合法性.


IV- 完整代码:

其中:(Log.hpp(日志模块) / Common.hpp(公共的声明定义) / mutex.hpp(给日志用的互斥锁)) 略

IV.1- Protocol.hpp

c++ 复制代码
#pragma once
#include "Common.hpp"
#include "Socket.hpp"

#include <jsoncpp/json/json.h>
#include <sstream>
#include <cstring>
#include <functional>

const char *Sep = "\n\r";
// TODO 为了照顾包装器的定义,声明一下(似乎没用???)
//  class Request;
//  class Reponse;
//  using func_t = std::function<Response(const Request& , const AddrIn&)>;

class Request
{
public:
    Request() = default;
    Request(int x, int y, char oper)
        : _x(x), _y(y), _oper(oper)
    {
    }
    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::StreamWriterBuilder builder;
        std::unique_ptr<Json::StreamWriter> pwriter(builder.newStreamWriter());

        auto &writer = *pwriter;
        std::stringstream ss;
        int OK = writer.write(root, &ss);

        return ss.str();
    }
    bool DeSerialize(const std::string &json)
    {
        Json::Value root;

        Json::Reader reader;
        bool ok = reader.parse(json, root);
        if (!ok)
        {
            std::cout << "客户端解析JSON串失败" << std::endl;
            return false;
        }
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }
    int GetX() const { return _x; }
    int GetY() const { return _y; }
    int Oper() const { return _oper; }
    void ShowMyself()
    {
        std::cout << GetX() << " " << GetY() << " " << Oper() << std::endl;
    }
private:
    int _x;
    int _y;
    int _oper;
};

class Response
{
public:
    Response()
    {
    }
    Response(int result, int flag) : _result(result), _flag(flag)
    {
    }
    std::string Serialize()
    {
        Json::Value root;
        root["result"] = _result;
        root["flag"] = _flag;

        Json::StreamWriterBuilder builder;
        std::unique_ptr<Json::StreamWriter> pwriter(builder.newStreamWriter());

        auto &writer = *pwriter;
        std::stringstream ss;
        int OK = writer.write(root, &ss);

        return ss.str();
    }
    bool DeSerialize(const std::string &json)
    {
        Json::Value root;

        Json::Reader reader;
        bool ok = reader.parse(json, root);
        if (!ok)
        {
            std::cout << "客户端解析JSON串失败" << std::endl;
            return false;
        }
        _result = root["result"].asInt();
        _flag = root["flag"].asInt();

        return true;
    }

    void SetResult(int result)
    {
        _result = result;
    }
    void SetFlag(int flag)
    {
        _flag = flag;
    }
    void ShowRresponse()
    {
        std::cout << _result << "[" << _flag << "]" << std::endl;
    }
private:
    int _result;
    int _flag;
};
using func_t = std::function<Response(const Request &, const AddrIn &)>;
class Protocal
{
public:
    Protocal()
    {

    }
    Protocal(func_t func) : _func(func)
    {
    }
    void EnCode(std::string &st)
    {
        size_t len = st.size();
        st = std::to_string(len) + Sep + st + Sep;
    }
    // 此时收掉了一串 , 可能满足一个报文,也可能多于一个报文
    bool DeCode(std::string *buffer, std::string &buffer_queue)
    {
        // 1,如果没有特定字符,直接凉拌
        size_t pos = buffer_queue.find(Sep);
        if (pos == std::string::npos)
            return false;

        // 2,提取长度
        std::string lenStr = buffer_queue.substr(0, pos);
        int len = std::stoi(lenStr);
        size_t expectedLen = strlen(Sep) * 2 + lenStr.size() + len;

        // 3,看看是否满足至少一个完整报文
        if (buffer_queue.size() < expectedLen)
            return false;

        // 4,从头部截取有效载荷
        *buffer = buffer_queue.substr(pos + strlen(Sep), len);
        buffer_queue.erase(0, expectedLen);

        return true;
    }

    // void GetRequest(std::string & buffer_queue, const AddrIn & addr) // NOTE:这次的参数一开始又传错了
    void GetRequest(std::shared_ptr<Socket> sock, const AddrIn &client)
    {
        LOG(LogLevel::DEBUG) << "代码执行到:GetRequest\n";
        // 1 , 接收数据流
        std::string buffer_queue;
        while (true)
        {
            int n = sock->Receive(&buffer_queue);
            LOG(LogLevel::DEBUG) << "sock->Receive结束";
            if (n < 0)
            {
                LOG(LogLevel::WARNING) << "client:" << client.GetInfo() << ", recv error";
                break;
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client:" << client.GetInfo() << "Quit!";
                break;
            }
            else
            {
                // 2,解码数据
                std::string buffer;

                // bool ok = DeCode(&buffer, buffer_queue);
                // if (ok == false)
                //     return;
                while (DeCode(&buffer, buffer_queue))
                {

                    // 3,反序列化
                    Request request;
                    bool ok = request.DeSerialize(buffer);
                    if (ok == false)
                        return;
                    
                    //LOG(LogLevel::DEBUG) << "服务端调用GetRequest进行反序列化后的结果:" ;
                    request.ShowMyself();
                    
                    // 4,处理
                    Response response(_func(request, client));

                    //LOG(LogLevel::DEBUG) << "服务端调用回调函数后的计算结果:";
                    response.ShowRresponse();

                    // 5,序列化
                    std::string json_str = response.Serialize();

                    // 6,包装
                    EnCode(json_str);

                    // 7,发送
                    //LOG(LogLevel::DEBUG) << "服务端结算出结果:" << json_str << " 并发送给客户端\n";
                    sock->Send(json_str.c_str());
                }
            }
        }
    }

    //std::shared_ptr<Response> GetResponse(std::shared_ptr<Socket> sock, const AddrIn &client) // NOTE:参数不太对哦
    bool GetResponse(std::shared_ptr<Socket> sock, std::string& resp_buffer,Response& response)
    {
        //std::string buffer_queue;

        while (true)
        {
            //int n = sock->Receive(&buffer_queue);  //BUG:接受buffer时弄错啦
            int n = sock->Receive(&resp_buffer);

            // LOG(LogLevel::DEBUG) << "Receive 返回: " << std::to_string(n) 
            //              << ", buffer_queue 当前长度: " << std::to_string(buffer_queue.size())
            //              << ", 内容预览: " << buffer_queue.substr(0, 50);

            if(n <= 0) return false;
            // if (n > 0)
            // {
                // // 1,解码
                // std::string buffer;
                // bool ok = DeCode(&buffer,buffer_queue);
                // if(ok == false)
                //     continue;
                // // 2,反序列化
                // Response response;
                // ok = response.DeSerialize(buffer);
                // if(ok == false)
                //     continue;
                // //return response;
                // return std::make_shared<Response>(response);
                std::string json_buffer;
                //while(DeCode(&json_buffer,resp_buffer)) // BUG:不能用循环,
                if(DeCode(&json_buffer,resp_buffer))
                {
                    return response.DeSerialize(json_buffer);
                }
            // }
            // else if (n == 0)
            // {
            //     LOG(LogLevel::WARNING) << "服务端停止\n";
            //     return false;
            // }
            // else
            // {
            //     LOG(LogLevel::ERROR) << "客户端接受错误\n";
            //     return false;
            // }
        }
    }

    std::string BuildRequestString(int x , int y , char oper)
    {
        Request request(x,y,oper);
        std::string st = request.Serialize();
        EnCode(st);
        return st;
    }
private:
    func_t _func;
};

IV.2- Socket.hpp

c++ 复制代码
#pragma once
#include<iostream>
#include<sys/socket.h>

#include"Common.hpp"



class Socket
{
public:
    //创建套接字
    virtual void SocketOrDie() = 0;
    //绑定
    virtual void BindOrDie(uint16_t port) = 0;
    //监听
    virtual void ListenOrDie(int backlog) = 0;
    //server尝试连接client
    virtual std::shared_ptr<Socket> Accept(AddrIn& addr) = 0;
    //接收数据
    virtual int Receive(std::string* st) = 0;
    //发送数据
    virtual void Send(const std::string& st) = 0;
    //关闭文件描述符
    virtual void Close( )= 0;
    //客户端尝试建立连接
    virtual bool Connect(std::string ip , uint16_t port) = 0;
public:
    //构建ServerTcp
    void BuildServerTcp(uint16_t port,int backlog)
    {
        SocketOrDie();
        BindOrDie(port);
        ListenOrDie(backlog);
    }
    //构建ClientTcp
    void BuildClientTcp()
    {
        SocketOrDie();
    }
};


class TcpSocket : public Socket
{
public:
    TcpSocket() = default;
    TcpSocket(int fd) :_fd(fd)
    {}
    void SocketOrDie() override
    {
        _fd = socket(AF_INET , SOCK_STREAM , 0);
        if(_fd < 0)
        {
            LOG(LogLevel::FATAL) << "套接字创建失败\n";
            exit(ExitNum::SocketErr);
        }
        LOG(LogLevel::INFO) << "套接字创建成功,fd=" << std::to_string(_fd) << "\n";
    }
    void BindOrDie(uint16_t port) override
    {
        AddrIn addr(port);
        int n =bind(_fd,addr.GetAddr(),addr.GetSize());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败\n";
            exit(ExitNum::BindErr);
        }
        LOG(LogLevel::INFO) << "服务端绑定成功!!!\n";
    }
    void ListenOrDie(int backlog) override
    {
        int n = listen(_fd,backlog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "监听失败\n";
            exit(ExitNum::ListenErr);
        }
        LOG(LogLevel::INFO) << "服务器监听成功!!!\n";
    }
    std::shared_ptr<Socket> Accept(AddrIn& addr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(_fd,CONV(peer),&len);
        if(new_sock < 0)
        {
            LOG(LogLevel::FATAL) << "接受失败\n";
            return nullptr;
        }
        LOG(LogLevel::INFO) << "accept成功!!!" << addr.GetInfo() << "\n";
        addr.SetAddr(peer);
        return std::make_shared<TcpSocket>(new_sock);
    }
    //接收数据
    virtual int Receive(std::string* st) override
    {
        char buffer[1024];
        int n = recv(_fd , buffer,sizeof(buffer)-1 , 0);
        
        if(n > 0)
        {
            buffer[n] = '\0';
            *(st) += buffer;
        }
        return n;
    }
    //发送数据
    virtual void Send(const std::string& st) override
    {
        int n = send(_fd,st.c_str(),st.size(),0);
        if(n < 0)
            LOG(LogLevel::WARNING) << "发送失败\n";
    }
    void Close( ) override
    {
        close(_fd);
    }
    bool Connect(std::string ip , uint16_t port) override
    {
        AddrIn addr(ip,port);
        int n = connect(_fd,addr.GetAddr(),addr.GetSize());
        if(n < 0)
        {
            LOG(LogLevel::WARNING) << "客户端连接失败\n";
            return false;       
        }
        LOG(LogLevel::INFO) << "客户端连接成功!!! \n";
        return true;
    }
private:
    int _fd;
};

IV.3- TcpServer.hpp

c++ 复制代码
#include "Common.hpp"
#include "Socket.hpp"

#include <sys/wait.h>
#include <functional>

const int DefaultBackLog = 8;

using iofunc_t = std::function<void(std::shared_ptr<Socket>, const AddrIn &)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, iofunc_t func, int backlog = DefaultBackLog)
        : _port(port),
          _func(func),
          _listen_socket(std::make_unique<TcpSocket>()),
          _isRunning(false)
    {
        _listen_socket->BuildServerTcp(_port, backlog);
    }
    void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            AddrIn addr;
            std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
            // Accept失败则重新尝试
            if (sock == nullptr)
                continue;
            // Accept成功后尝试接收数据   //BUG : 超级无敌大坑,不应该在服务器里接受数据,而是全盘交给协议
            // std::string buffer;
            // int n = sock->Receive(&buffer);
            // if (n < 0)
            // {
            //     LOG(LogLevel::ERROR) << "数据接受失败\n";
            //     break;
            // }
            // else if (n == 0)
            // {
            //     LOG(LogLevel::INFO) << "客户端退出...:" << addr.GetInfo() << "\n";
            //     break;
            // }
            // else
            // {
                // 开始创建子进程处理任务
                int pid = fork();
                if (pid == 0)
                {
                    if (fork() > 0)
                    {
                        exit(ExitNum::OK);
                    }
                    // 此时孙子进程为孤儿进程,执行回调方法
                    //LOG(LogLevel::DEBUG) << "客户端开始执行回调方法\n";
                    _func(sock,addr);

                    //结束后关闭文件描述符并让孙子进程退出
                    sock->Close();
                    exit(ExitNum::OK);
                }
            //     else
            //     {
            //         int n = waitpid(pid, nullptr, 0);
            //         (void)n;
            //     }
            // }
        }

        _isRunning = false;
    }

private:
    uint16_t _port;
    iofunc_t _func;
    std::unique_ptr<Socket> _listen_socket;

    bool _isRunning;
};

IV.4- NetCal.hpp

c++ 复制代码
#pragma once
#include "Protocal.hpp"

class Cal
{
public:
    Response operator()(const Request &request, const AddrIn &addr)
    {
        Response response(0, 0);
        switch (request.Oper())
        {
        case '+':
            response.SetResult(request.GetX() + request.GetY());
            break;
        case '-':
            response.SetResult(request.GetX() - request.GetY());
            break;
        case '*':
            response.SetResult(request.GetX() * request.GetY());
            break;
        case '/':
            if (request.GetY() == 0)
            {
                response.SetResult(-1); // HACK : 服务器中除零错误的处理(并非直接让程序崩溃)
                response.SetFlag(1);    // 除零错误
            }
            else
            {
                response.SetResult(request.GetX() / request.GetY());
            }
            break;
        case '%':
            if (request.GetY() == 0)
            {
                response.SetResult(-1); // HACK : 服务器中除零错误的处理(并非直接让程序崩溃)
                response.SetFlag(2);    // 模零错误
            }
            else
            {
                response.SetResult(request.GetX() % request.GetY());
            }
            break;
        default:
            response.SetResult(-1);
            response.SetFlag(3); //符号传递错误
            break;
        }

        return response;
    }
};

IV.5- Server.cpp

c++ 复制代码
#include"NetCal.hpp"
#include"TcpServer.hpp"
#include "Protocal.hpp"


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "服务端参数传递错误";
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);


    Cal cal;

    std::unique_ptr<Protocal> protocal = std::make_unique<Protocal>([&cal](const Request &request, const AddrIn &addr)->Response{
        return cal(request,addr);
    });

    std::unique_ptr<TcpServer> ptr = std::make_unique<TcpServer>(port,[&protocal](std::shared_ptr<Socket> sock, const AddrIn &addr){
        protocal->GetRequest(sock,addr);
    });

    ptr->Start();

    return 0;
}

IV.6- Client.cpp

c++ 复制代码
#include"Common.hpp"
#include"Protocal.hpp"

void GetDataFromStdin(int *x, int *y, char *oper)
{
    std::cout << "Please Enter x: ";
    std::cin >> *x;
    std::cout << "Please Enter y: ";
    std::cin >> *y;
    std::cout << "Please Enter oper: ";
    std::cin >> *oper;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "客户端参数传递错误";
        return 1;
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    std::shared_ptr<Socket> sock = std::make_shared<TcpSocket>();
    sock->BuildClientTcp();

    if(sock->Connect(ip,port) == false)
    {
        LOG(LogLevel::ERROR) << "Connect error";
        exit(ExitNum::ConnectErr);
    }

    //连接成功
    std::unique_ptr<Protocal> protocal = std::make_unique<Protocal>();
    std::string resp_buffer;   //NOTE:这个放在while循环外有风险 , 可能导致数据残留 , 影响下一次读取

    while(true)
    {
        // 1. 从标准输入当中获取数据
        int x, y;
        char oper;
        GetDataFromStdin(&x, &y, &oper);
        // 2. 构建一个请求-> 可以直接发送的字符串
        std::string st = protocal->BuildRequestString(x,y,oper);
        //std::cout << "客户端构建好的JSON串: " << st << std::endl;
        // 3. 发送请求
        sock->Send(st);
        // 4. 获取应答
        Response response;
        bool ok = protocal->GetResponse(sock,resp_buffer,response);
        if(ok == false)
            break;
         // 5. 显示结果
         response.ShowRresponse();
    }
    sock->Close();


    return 0;
}

V- 补充 : 守护进程:

V.1- 进程 / 进程组

对于进程来讲 : 父子进程间是父子关系 ; 由同一个父进程创建的子进程间是兄弟关系 ;

而进程们除了父子关系外 , 还存在 的关系 , 即进程组

V.1.1- 测试:

通过 管道|来依次执行多条命令 ; 通过 & 来让命令后台执行

bash 复制代码
$ sleep 3000 | sleep 5000 | sleep 4000 &  #执行三个后台运行的sleep进程

查看sleep进程相关的信息 , 用 ps

V.1.2- 现象:

  • (看第二行) PID 是进程id , PGID是进程组id
  • 对于 PID , 由于三个sleep进程是一次创建的 , 所以PID也是一次递增.
  • 关键的来了: 对于PGID , 三个sleep进程是相同的 , 并且数值等于第一个sleep进程 . 因此第一个sleep进程就叫做组长进程 , 他和剩下的两个同为一组.
bash 复制代码
ps -ajx | head -1 && ps -ajx | grep sleep 
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND  
  17020   17104   17104   17020 pts/4      17109 S     1000   0:00 sleep 3000
  17020   17105   17104   17020 pts/4      17109 S     1000   0:00 sleep 5000
  17020   17106   17104   17020 pts/4      17109 S     1000   0:00 sleep 4000

V.2- 会话

  • 为了直击本质 , 我们借助Linux理解具体的操作系统时 , 会用到终端软件 , 打开之后是命令行的界面.
  • 有时候为了监控程序的各种状态 , 我们发现还可以在同一台机器上打开多个终端窗口.

这样的一个个窗口 , 在系统层面叫做一个个的会话(session) !!!

V.2.1- SID:

  • 还是一样的输出 , 但是看向第四列 : SID ,代表会话ID .
  • 可以看到 , 因为是同一个终端下执行的指令 , 所以这三个sleep进程的会话id相同 .
bash 复制代码
ps -ajx | head -1 && ps -ajx | grep sleep 
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND  
  17020   17104   17104   17020 pts/4      17109 S     1000   0:00 sleep 3000
  17020   17105   17104   17020 pts/4      17109 S     1000   0:00 sleep 5000
  17020   17106   17104   17020 pts/4      17109 S     1000   0:00 sleep 4000

V.2.2- bash的产生:

上面第四行里各个进程的数值并非凭空产生 , 而是继承自比普通进程更加权威的进程->bash 进程 . 也就是在系统启动时会立刻创建的命令行解释器 , 并且 , 没新建一个终端就会有一个bash进程 .

V.2.3- 验证bash的归属:

使用ps指令查看bash进程的信息

  • 可以看到最后一行的bash进程的pid整好等于上面那三个sleep进程的 SID , 这就说明了由同一个bash创建的进程使用同一个SID.
bash 复制代码
ps -ajx | head -1 && ps -ajx | grep bash
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
   1353    1505    1505    1353 tty1        1505 S+    1000   0:00 -bash
   1739   15599   15599   15599 pts/2      15599 Ss+   1000   0:00 /bin/bash --init-file /home/ha2042894194/.vscode-server/bin/994fd12f8d3a5aa16f17d42c041e5809167e845a/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
  16033   16034   16034   16034 pts/3      17153 Ss    1000   0:00 -bash
  17019   17020   17020   17020 pts/4      17020 Ss+   1000   0:00 -bash

V.2.4- 创建会话( setsid )

只要不是组长进程 , 就可以调用 setsid 函数来新建会话

c++ 复制代码
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);

setsid的作用:

  • 调用进程成为 新会话 的首进程 , 自然 也是组长进程
  • 该进程 没有控制终端 , 同时也会和先前的终端脱离关系

因为 setsid 被组长进程调用时会出错 , 所以 常见的做法是: 利用创建出的子进程来调用此函数.

V.3- 控制终端 :

  • unix系统中 , 用户通过终端 (如xshell) 登录系统后得到一个 shell进程 ;
  • 本质上来说 : 人和计算机的交互就是和进程的交互 , 所以可以认为 shell进程就是终端的化身
    终端控制是 保存在进程PCB里的信息 , 因此由 shell进程创建的其他进程也会继承同样的终端消息
    默认情况下没有重定向 : 终端内所有进程的 标准输入/输出/错误流 都指向控制终端 , 因此进程默认从标准输入里读取内容 / 向终端输出内容 .
  • 一个会话只有一个终端
  • 会话里的第一个进程叫做 控制进程 , 如 bash进程 .
  • 会话中的进程分为 一个 前台进程组 和 多个 后台进程组
  • 当终端接受 中断键 (Ctrl+c)或 退出键 (Ctrl + \) , 会向 前台进程组里的所有进程发送信号.
  • 当终端检测到 调制解调器 (网络) 断开 , 会将挂断信号发给 控制进程 .

!为啥前台进程只有一个?

  • 因为键盘只有一个 , 必须得指定一个进程来接受键盘输入
  • 显示器也只有一个 , 但他本质上是一个文件 , 允许多个进程写入 (不加管控还是会脏数据哈)

V.4- 作业控制 :

V.4.1- 概念 :

  • 简单来说 , 作业就是 进程/进程组 , 只不过更能体现出OS里为用户干活的象征意义
  • 因此 , 适用于 进程/进程组 的概念 , 在作业这里同样适用

V.4.2- 作业号 :

  • 下面的命令行中: 我用结合管道 ' | ' 一次性创建了三个sleep 进程(统称为一个作业) ,并用 ' & ' 将其全部设置为后台作业
  • 如下面的注释所示 : 成功设置后台作业后 , 会显式 作业号 + 进程组号信息
bash 复制代码
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[1] 173253
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[2] 173256
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[3] 173259

V.4.3- 作业状态(相关指令) :

  • 使用 Ctrl + z 会让前台作业停止 ,并显示相关作业信息 :
bash 复制代码
14_Tcp_Cal$ ./server 8080
 
^Z  #终端接收到了 Ctrl+z 产生的暂停信号
[4]+  Stopped      ./server 8080 #被暂停信号的信息
  • 使用 fg 指令让指令重新运行 :
bash 复制代码
14_Tcp_Cal$ fg %
./server 8080  #作业被fg唤醒了
参数( fg指令的 ) 含义
%n n为正整数 , 表示作业号
%string 以字符串开头的命令所对应的作业
%?string 包含字符串的命令所对应的作业
%+或%% 最近提交的一个作业
%- 倒数第二个提交的作业
  • 使用 jobs 指令查看本用户当前后台执行或挂起的作业 (参数 -l 显式详细 ; 参数 -p 则只显示作业pid)
shell 复制代码
14_Tcp_Cal$ jobs #默认使用
[2]   Running                 sleep 1000 | sleep 2000 | sleep 3000 &
[3]-  Running                 sleep 1000 | sleep 2000 | sleep 3000 &
[4]+  Stopped                 ./server 8080

14_Tcp_Cal$ jobs -l #带参数 -l 使用
[2]  173254 Running                 sleep 1000
     173255 Running                 | sleep 2000
     173256 Running                 | sleep 3000 &
[3]- 173257 Running                 sleep 1000
     173258 Running                 | sleep 2000
     173259 Running                 | sleep 3000 &
[4]+ 173324 Stopped                 ./server 8080

14_Tcp_Cal$ jobs -p # 带参数 -p 使用
173254
173257
173324

!作业的状态信息

  • 从上面可以看到 , 有的作业号后面还会携带 -+.
  • + 表示此作业是最近进入后台的
  • - 表示此作业是次最近进入后台的.

V.5- 程序的守护进程化 :

作为服务器 , 不能仅仅是依赖特定的会话 , 所以需要守护进程化 -> 孤儿进程的一种

c++ 复制代码
#pragma once
#include"Common.hpp"

#include<unistd.h> //setsid
#include<signal.h>

#include<fcntl.h>  //open


const char* root = "/";
const char* dev_null = "/dev/null";

void Deamon(bool ischdir , bool isclose)
{
    //1,忽略可能引起程序退出的📶
    signal(SIGPIPE,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);

    //2,避免自己成为组长
    if(fork() > 0)
        exit(0);

    //3,让子进程作为新的会话
    setsid();

    //4,按需求切换目录
    if(ischdir)
        chdir(root);
    
    //5
    if(isclose)
    {
        close(0);
        close(1);
        close(2);
    }
    else
    {
        int fd = open(dev_null,O_RDWR);
        if(fd > 0)
        {
            dup2(fd,0);
            dup2(fd,1);
            dup2(fd,2);
            close(fd);
        }
    }
}

VI- 补充 : 软件的部署与发布(简易版)

守护进程化后的基于tcp的网络计算器就算是一个小项目了 , 可以试着把项目整理成一个软件 : 能够通过shell脚本文件实现自动安装和删除

VI.1- 必要文件 :

  • 服务端的源文件 : server.cpp
  • 客户端的源文件 : client.cpp
  • makefile脚本文件 ( 关键 ) ---- 用于构建项目所需的目录/文件结构 , 并形成项目压缩包
  • 两个 bash 脚本文件 --- 用于实现自动化 安装和卸载

VI.2- makefile :

makefile 复制代码
//编译指令略...

//执行 make output即可自动 创建项目目录->拷贝可执行文件->拷贝bash脚本文件
.PHONY:output
output:
	@mkdir output
	@mkdir -p output/bin
	@mkdir -p output/log
	@cp server output/bin
	@cp client output/bin
	@cp install.sh output/
	@cp uninstall.sh output/
	@tar czf output.tgz output 

VI.3- install.sh

bash 复制代码
#!/usr/bin/bash

cp -f ./bin/server /usr/bin
cp -f ./bin/client /usr/bin

VI.4- uninstall.sh

bash 复制代码
#!/usr/bin/bash

rm -rf /usr/bin/server
rm -f /usr/bin/client
相关推荐
想唱rap4 小时前
MYSQL在ubuntu下的安装
linux·数据库·mysql·ubuntu
振华说技能4 小时前
SolidWorks学习大纲-从基础到全面精通,请看详情
学习
曦月逸霜4 小时前
离散数学-学习笔记(持续更新中~)
笔记·学习·离散数学
糖~醋排骨4 小时前
DHCP服务的搭建
linux·服务器·网络
huohaiyu4 小时前
网络中的一些基本概念
运维·服务器·网络
im_AMBER4 小时前
Leetcode 101 对链表进行插入排序
数据结构·笔记·学习·算法·leetcode·排序算法
llddycidy4 小时前
峰值需求预测中的机器学习:基础、趋势和见解(最新文献)
网络·人工智能·深度学习
小林一直冲4 小时前
华为设备配置与命令
网络
dust_and_stars4 小时前
ubuntu24使用apt安装VS-code-server code-server
linux·服务器·windows