Linux认识协议

目录

  • TCP协议通信流程
    • TCP三次握手
    • 数据传输过程
    • 四次挥手过程
    • TCP 和 UDP 对比
  • 认识协议
    • 协议的概念
    • 结构化数据的传输
    • 序列化和反序列化
  • 网络版计算器
    • 服务端代码
      • 面向字节流
    • 协议定制
    • 客户端代码编写
    • 代码测试
    • 守护进程
      • 守护进程创建
    • 关于协议制定中使用现成方法实现

TCP协议通信流程

下图是基于TCP协议的客户端/服务器程序的一般流程:
在这里插入图片描述

TCP三次握手

服务器初始化

  • 调用socket,创建文件描述符;
  • 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt,并阻塞, 等待客户端连接过来。

建立连接的过程

  • 调用socket,创建文件描述符;
  • 调用connect,向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答;(第一次)
  • 服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接";(第二次)
  • 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段;(第三次)

在这里插入图片描述
在此我们需要注意的是accept并不参与三次握手的过程,因为三次握手本身就是底层TCP所做的工作。accept要做的只是将底层已经建立好的连接拿到用户层,如果底层没有建立好的连接,那么accept函数就会阻塞住直到有建立好的连接。

数据传输过程

  • 建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同一条连接中,同一时刻, 通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
  • 服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待;
  • 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答;
  • 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
  • 客户端收到后从read()返回,发送下一条请求,如此循环下去。

在这里插入图片描述

四次挥手过程

当双方通信结束之后,需要通过四次挥手的方案使双方断开连接,当客户端调用close关闭连接后,服务器最终也会关闭对应的连接。而其中一次close就对应两次挥手,因此一对close最终对应的就是四次挥手。
在这里插入图片描述

  • 如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
  • 此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次);
  • read返回之后,服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN;(第三次)
  • 客户端收到FIN,再返回一个ACK给服务器;(第四次)

在学习socket API时要注意应用程序和TCP协议层是如何交互的:

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

为什么要断开连接?

建立连接本质上就是要保证双方的通信,建立连接以后,我们就会传输数据,如果建立连接以后不断开,就会造成系统资源越来越少。

操作系统同样会对这些连接进行管理,在服务端会对这些连接产生的数据结构进行管理,随着连接的增加,维护此数据结构的时间和空间成本也随之增加,所以双方通信以后就应该断开连接。

TCP 和 UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

认识协议

协议的概念

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

结构化数据的传输

通信双方在进行网络通信时:

如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。

但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。

如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。

将结构化的数据组合成一个字符串

  • 客户端发送一个形如“1+1”的字符串。
  • 这个字符串中有两个操作数,都是整型。
  • 两个数字之间会有一个字符是运算符。
  • 数字和运算符之间没有空格。

定制结构体+序列化和反序列化

  • 定制结构体来表示需要交互的信息。
  • 发送数据时将这个结构体按照一个规则转换成网络标准数据格式,接收数据时再按照相同的规则把接收到的数据转化为结构体。
  • 这个过程叫做“序列化”和“反序列化”。

序列化和反序列化

  • 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
  • 反序列化是把字节序列恢复为对象的过程。

在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。

序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

网络版计算器

引入日志文件

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 "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif

    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

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

    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    printf("%s%s\n", stdBuffer, logBuffer);
}

首先我们对各类接口例如创建套接字,绑定,连接等一系列接口进行封装:

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(ERROR, "create socket error:%d:%s", errno, strerror(errno));
            exit(0);
        }

        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(ERROR, "bind error:%d:%s", errno, strerror(errno));
            exit(1);
        }
    }

    void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(ERROR, "listen error:%d:%s", errno, strerror(errno));
            exit(2);
        }

        logMessage(NORMAL, "init server success...");
    }

    int Accept(int listensock, uint16_t *port, std::string *ip)
    {
        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);
            exit(3);
        }

        if (port)
            *port = htons(src.sin_port);
        if (ip)
            *ip = inet_ntoa(src.sin_addr);

        return servicesock;
    }

    bool Connect(int sock, const uint16_t &server_port, const std::string &server_ip)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        socklen_t len = 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, len) == 0)
            return true;
        else
            return false;
    }

    ~Sock()
    {
    }
};

服务端代码

我们所需要做的就是先初始化服务器,然后就是启动服务器了,启动服务器后就是不断调用accept函数,从监听套接字当中获取新链接,每当获取一个链接之后就创建一个新线程,该线程为服务端提供服务。

TcpServer.hpp

#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_;
    };

    class TcpServer
    {
    private:
        static void *ThreadRoutine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadData *td = static_cast<ThreadData *>(args);
            td->server_->Excute(td->sock_);
            close(td->sock_);
            // delete td;
            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 BindServer(func_t func)
        {
            func_.push_back(func);
        }
        void Excute(int sock)
        {
            for (auto &f : func_)
            {
                f(sock);
            }
        }
        void start()
        {
            for (;;)
            {
                std::string clientip;
                uint16_t clientport;
                int sock = sock_.Accept(listensock_, &clientport, &clientip);
                if (sock == -1)
                    continue;
                logMessage(NORMAL, "create new link success, 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_;
    };
}

CalServer.cc

#include <iostream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include "TcpServer.hpp"
#include "Protocol.hpp"

using namespace ns_tcpserver;
using namespace ns_protocol;

static void usage(const std::string &proc)
{
    std::cout << "\nusage:" << proc << "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 ret = Recv(sock, &inbuffer);
        if (!ret)
            break;
        // std::cout << "begin: inbuffer: " << inbuffer << std::endl;
        // 2. 协议解析,获取完整报文
        std::string message = Decode(inbuffer);
        if (message.empty())
            continue;
        // std::cout << "end: inbuffer: " << inbuffer << std::endl;
        // std::cout << "packge: " << message << std::endl;
        logMessage(NORMAL, "%s", message.c_str());

        // 3. 保证该报文是一个完整报文
        Request req;

        // 4. 反序列化 字节流->结构化
        req.Deseserialize(message);

        // 5. 业务逻辑
        Response resp = calculatorHelper(req);

        // 6. 序列化
        std::string respString = resp.Serialize();

        // 7. 添加长度信息,形成一个完整报文
        // std::cout << "respString: " << respString << std::endl;
        respString = Encode(respString);
        // std::cout << "encode: respString: " << respString << std::endl;

        Send(sock, respString);
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(0);
    }
    signal(SIGPIPE, SIG_IGN);

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

    server->BindServer(calculator);
    server->start();

    return 0;
}

上面服务端代码包含了许多细节类的问题,因为我们需要进行结构化数据的传输,所以我们的服务端接收数据的过程是将序列化数据反序列化,因为我们服务端是要进行计算的,我们的网络计算器底层是以TCP来实现的,就存在字节流->结构化的转换问题,就像我们在读取数据的过程中,此时单纯的recv已经不能满足我们的需求,所以我们在接下来定制协议的过程,就需要考虑数据完整性的问题。

Server在编写的时候,要有较为严谨性的判断逻辑,一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题。

服务端创建新线程时,需要将调用accept获取到套接字作为参数传递给该线程,为了避免该套接字被下一次获取到的套接字覆盖,最好在堆区开辟空间存储该文件描述符的值。

面向字节流

当创建一个TCP协议时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区。

就像我们的网络计算器一样,服务端客户端都会存在一个发送缓冲区和一个接收缓冲区:

  • 调用write函数就可以将数据写入缓冲区,写入以后write函数就可以返回了,此时发送缓冲区中数据是由TCP进行发送的,至于他什么时候发?发多少?出错了怎么办?这都是由我们TCP协议决定的;
  • 如果发送的字节数太长,TCP会将其拆成多个数据包进行发生,如果发送的字节数太长太短,TCP会将其留在发送缓冲区中,等待长度合适后在进行发送;
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,可以通过调用read函数来读取接收缓冲区当中的数据。而调用read函数读取接收缓冲区中的数据时,也可以按任意字节数进行读取;
  • 由于缓冲区的存在,TCP程序的读和写不需要一一匹配。

实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
在这里插入图片描述

协议定制

要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。

  • 请求结构体中需要包括两个操作数,以及对应需要进行的操作,而且支持序列化与反序列化操作。
  • 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的,而且支持序列化与反序列化操作。
  • 必须保证读取的数据具有完整性,必须是两个操作数以及一个操作符,这样才可以计算成功,同样,发送数据也是一样。

协议的制定可以分为自主实现和使用现成的方案,为了更好的理解协议,我们下面使用自主实现的方法,在文中最后面我们会介绍使用现成的方法:

#pragma once

#include <iostream>
#include <string>
#include <cstring>

namespace ns_protocol
{
#define MYSELF 1

#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)

    // 两种方法
    // 1. 自主实现
    // 2. 使用现成的方案
    //  请求
    class Request
    {
    public:
        std::string Serialize()
        {
#ifdef MYSELF
            std::string str;
            str = std::to_string(x_);
            str += SPACE;
            str += op_;
            str += SPACE;
            str += std::to_string(y_);

            return str;
#else

#endif
        }
        // "x_ op_ y_"
        // "123 + 456"
        bool Deseserialize(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

#endif
        }

    public:
        Request()
        {
        }

        Request(int x, int y, char op)
            : x_(x), y_(y), op_(op)
        {
        }

        ~Request()
        {
        }

    public:
        int x_;
        int y_;
        char op_;
    };

    // 响应
    class Response
    {
    public:
        Response()
        {
        }
        Response(int code, int result)
            : code_(code), result_(result)
        {
        }

        ~Response()
        {
        }
        std::string Serialize()
        {
#ifdef MYSELF
            std::string s;
            s = std::to_string(code_);
            s += SPACE;
            s += std::to_string(result_);

            return s;
#else

#endif
        }
        //"code_ result_"
        //" 0 100"
        bool Deseserialize(const std::string &s)
        {
#ifdef MYSELF
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            code_ = atoi(s.substr(0, pos).c_str());
            result_ = atoi(s.substr(pos + SPACE_LEN).c_str());

            return true;
#else

#endif
        }

    public:
        int result_; // 计算结果
        int code_;   // 计算结果状态码
    };

    // 必须保证收到的是一个完整的的需求
    // TCP是面向字节流的
    bool Recv(int sock, std::string *out)
    {
        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)
        {
            std::cout << "client quit" << std::endl;
            return false;
        }
        else
        {
            std::cout << "recv error" << std::endl;
            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;
        }
    }

    std::string Decode(std::string &buffer)
    {
        // "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
        // "x_ op_ y_\r\n length\r\nXXX\r\n"
        
        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 "";
        }
    }

    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;
    }
}

规定状态字段对应的含义:

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

客户端代码编写

同样,客户端也需要进行初始化,初始化完成以后就调用connect进行连接服务端,连接完成以后此时发送请求,这里可以让用户输入两个操作数和一个操作符构建一个计算请求,然后将该请求发送给服务端。而当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。

同样,我们服务端进行操作时也会存在遵守我们的协议规定操作。

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

using namespace ns_protocol;

static void usage(const std::string &proc)
{
    std::cout << "\nusage" << proc << "port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(0);
    }

    uint16_t server_port = atoi(argv[2]);
    std::string server_ip = argv[1];

    Sock sock;
    int sockfd = sock.Socket();

    if (!sock.Connect(sockfd, server_port, server_ip))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }

    std::string buffer;
    bool quit = false;
    while (true)
    {
        // 1. 获取需求
        Request req;
        std::cout << "Please Enter# ";
        std::cin >> req.x_ >> req.op_ >> req.y_;

        // 2.序列化
        std::string s = req.Serialize();

        // 3. 添加报头长度
        s = Encode(s);

        // 4. 发送给服务端
        Send(sockfd, s);

        // 5. 正常读取
        while (true)
        {
            bool ret = Recv(sockfd, &buffer);
            if (!ret)
            {
                quit = true;
                break;
            }

            std::string message = Decode(buffer);
            if (message.empty())
                continue;

            Response resp;
            resp.Deseserialize(message);

            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_ << std::endl;
                break;
            }
            if (!err.empty())
                std::cout << err << std::endl;
            break;
        }
    }

    close(sockfd);
    return 0;
}

代码测试

运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。而如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。
在这里插入图片描述

守护进程

我们会发现,每次我们需要运行时都需要输入./server 8080命令才可以,对于对于一个服务器来说,他是一直运行的,就像我们在命令行输入各种命令一样,他是立马执行的,就不需要运行服务器。

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,比如作业规划进程crond等。

Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着,这种进程有一个名称叫守护进程(Daemon)。

我们上面的网络计算器也可以创建一个守护进程,来保证他周期性的运行。

我们首先需要理解进程组与前台后台进程的概念:

进程组

每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。

通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。

需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。

我们可以看见下面三个sleep进程都有一个相同的PPID = 14647,他们是一个进程组,而这个进程组的组长就是我们的sleep 1000,因为他的PGID与PID相等。
在这里插入图片描述

前台进程

直接运行某一可执行程序,例如./可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如S+。
在这里插入图片描述

守护进程创建

我们的守护进程都是以前台进程的方式存在的,任何一台xsell中都只存在一个前台进程和多个后台进程,我们需要知道的就是进程组长并不能成为守护进程,所以我们需要通过创建子进程的方法来进行守护进程的创建。

我们在此就原生创建一个守护进程,步骤如下:

  1. 忽略信号,SIGPIPESIGCHLD
  2. 不要让自己成为组长;
  3. 调用setsid接口;
  4. 标准输入,标准输出,标准错误的重定向到/dev/null。,守护进程不能直接向显示器打印消息。
#pragma once

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

void MyDaemon()
{
    // 1. 忽略信号,SIGPIPE,SIGCHLD
    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);
    }
}

此时将我们的网络计算器就可以添加守护进程,我们只需运行一次,只要客户端一启动,他就可以控制终端并且周期性地执行计算任务。
在这里插入图片描述
最后如果我们如果想关闭这个守护进程,我们就可以使用kill命令来杀死它。
在这里插入图片描述

关于协议制定中使用现成方法实现

我们进行自主实现是为了更好的理解我们的协议制定,接下来我们就可以使用现成的方法来实现,我们只需引入我们的Json库就可以,首先我们来看一段代码来理解一下:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    int a = 10;
    int b = 20;
    char c = '+';

    Json::Value root;
    root["aa"] = a;
    root["bb"] = b;
    root["cc"] = c;

    //Json::StyledWriter writer;
    Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;
    return 0;
}

运行代码就可以发现,我们Json其实就属于一个(key,value)模型。
在这里插入图片描述
此时协议中就可以更改代码为:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#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)

    // 两种方法
    // 1. 自主实现
    // 2. 使用现成的方案
    //  请求
    class Request
    {
    public:
        std::string Serialize()
        {
#ifdef MYSELF
            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
        }
        // "x_ op_ y_"
        // "123 + 456"
        bool Deseserialize(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);
            x_ = root["x"].asInt();
            y_ = root["y"].asInt();
            op_ = root["op"].asInt();
            return true;
#endif
        }

    public:
        Request()
        {
        }

        Request(int x, int y, char op)
            : x_(x), y_(y), op_(op)
        {
        }

        ~Request()
        {
        }

    public:
        int x_;
        int y_;
        char op_;
    };

    // 响应
    class Response
    {
    public:
        Response()
        {
        }
        Response(int code, int result, int x, int y, char op)
            : code_(code), result_(result), x_(x), y_(y), op_(op)
        {
        }

        ~Response()
        {
        }
        std::string Serialize()
        {
#ifdef MYSELF
            std::string s;
            s = std::to_string(code_);
            s += SPACE;
            s += std::to_string(result_);

            return s;
#else
            Json::Value root;
            root["code"] = code_;
            root["result"] = result_;
            root["xx"] = x_;
            root["yy"] = y_;
            root["zz"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        //"code_ result_"
        //" 0 100"
        bool Deseserialize(const std::string &s)
        {
#ifdef MYSELF
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            code_ = atoi(s.substr(0, pos).c_str());
            result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
            return true;
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            code_ = root["code"].asInt();
            result_ = root["result"].asInt();
            x_ = root["xx"].asInt();
            y_ = root["yy"].asInt();
            op_ = root["zz"].asInt();
            return true;
#endif
        }

    public:
        int result_; // 计算结果
        int code_;   // 计算结果状态码

        int x_;
        int y_;
        char op_;
    };

    // 必须保证收到的是一个完整的的需求
    // TCP是面向字节流的
    bool Recv(int sock, std::string *out)
    {
        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)
        {
            std::cout << "client quit" << std::endl;
            return false;
        }
        else
        {
            std::cout << "recv error" << std::endl;
            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;
        }
    }

    std::string Decode(std::string &buffer)
    {
        // "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
        // "x_ op_ y_\r\n length\r\nXXX\r\n"
        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 "";
        }
    }

    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;
    }
}

运行代码,我们同样可以看到同样计算器正常运行,此时我们的制定协议采用的是现成方法:
在这里插入图片描述

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

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

相关文章

【JVM】JDBC案例打破双亲委派机制

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 JVM 打破双亲委派机制&#xff08;JDBC案例…

开发直播带货系统源码的技术要点

直播带货系统是一个复杂的技术项目&#xff0c;通常包括前端应用、后端服务器、数据库、支付集成、实时通信以及直播流处理等多个关键组件。以下是开发直播带货系统源码的技术要点&#xff1a; 实时视频流处理 一个成功的直播带货系统需要支持实时视频流的传输和处理。可以使…

【云原生】使用nginx反向代理后台多服务器

背景 随着业务发展&#xff0c; 用户访问量激增&#xff0c;单台服务器已经无法满足现有的访问压力&#xff0c;研究后需要将后台服务从原来的单台升级为多台服务器&#xff0c;那么原来的访问方式无法满足&#xff0c;所以引入nginx来代理多台服务器&#xff0c;统一请求入口…

TCP编程及基础知识

一、端口号 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理&#xff0c;使用端口号来区分TCP端口号与UDP端口号独立端口用两个字节来表示 2byte&#xff08;65535个&#xff09; 众所周知端口&#xff1a;1~1023&#xff08;1~255之间为众所周知端口&#xff…

Ubuntu网络IP地址一直显示127.0.0.1

问题描述&#xff1a; 终端输入ip a显示127.0.0.1&#xff0c;原来类似192.168.231.1的地址不见了。 ip a 点击网络配置&#xff08;ubuntu桌面版&#xff09;&#xff0c;发现无线网络模块看不见了 正常情况应该有wired 模块&#xff0c;就是下面标红的 解决方案&#xff1a…

学为贵雅思写作备考

准确通顺&#xff0c;言之有物 两次读不懂&#xff0c;6分以下&#xff0c; 6分没有印象&#xff0c;味同嚼蜡&#xff0c;但是没错&#xff08;书面语过关&#xff09; 英语比较过关 8-9分&#xff0c;很有见地 6-7单个的句子读得懂&#xff0c;前后是贯通的、逻辑是通顺…

发现一款PDF转换成翻页电子书的网站

​随着科技的发展&#xff0c;电子书越来越受到人们的喜爱。而PDF格式的文件也越来越多地被人们使用。那么&#xff0c;如何将PDF文件转换成翻页电子书呢&#xff1f;今天就为大家推荐一款好用的PDF转翻页电子书网站。 一、网站介绍 这款网站是一款非常实用的在线转换工具&…

Devchat AI尝鲜试用:程序员开发提效利器,告别脏活累活

DevChat 简介 在当今的软件开发领域&#xff0c;程序员们每天都要面对海量的代码和复杂的任务。尽管技术不断发展&#xff0c;但程序员们依然需要花费大量时间进行重复性工作&#xff0c;如代码审查、错误排查、文档编写等。这些脏活累活不仅消耗了程序员们大量的时间和精力&am…

draw.io与项目管理——如何利用流程图工具提高项目管理效率

draw.io 是一款强大的图形绘制工具&#xff0c;用于创建各种类型的图表、流程图、组织结构图、网络图和平面设计等。它提供了丰富的绘图工具和预定义的图形库&#xff0c;使用户能够轻松创建专业水平的图形作品。 draw.io具有直观的界面和简单易用的功能&#xff0c;适合各种用…

gradle学习笔记

gradle学习笔记 一、下载安装配置环境变量二、使用gradle管理spring项目1、创建项目2、导入项目至编辑器3、打包部署4、将maven项目转为gradle项目 三、gradle介绍1、常用命令2、Gradle Wrapper包装器3、gradle进阶说明4、gradle插件 四、Groovy 简介 参考博客&#xff1a;http…

VM虚拟机逆向 --- [NCTF 2018]wcyvm 复现

文章目录 前言题目分析 前言 第四题了&#xff0c;搞定&#xff0c;算是独立完成比较多的一题&#xff0c;虽然在还原汇编的时候还是很多问题。 题目分析 代码很简单&#xff0c;就是指令很多。 opcode在unk_6021C0处&#xff0c;解密的数据在dword_6020A0处 opcode [0x08, …

原子化 CSS 真能减少体积么?

前言 最近看到这样一篇文章&#xff1a;《要喷也得先做做功课吧&#xff1f;驳Tailwind不好论》 个人觉得说的还是有一定道理的&#xff0c;就是该作者的语气态度可能稍微冲了点&#xff1a; 不过他说的确实有道理&#xff0c;如果这种原子化工具真的如评论区里那帮人说的那么…

【jvm】虚拟机栈

目录 一、背景二、栈与堆三、声明周期四、作用五、特点&#xff08;优点&#xff09;六、可能出现的异常七、设置栈内存大小八、栈的存储单位九、栈运行原理十、栈帧的内部结构10.1 说明10.2 局部变量表10.3 操作数栈10.4 动态链接10.5 方法返回地址10.6 一些附加信息 十一、代…

明御安全网关任意文件上传漏洞复现

简介 安恒信息明御安全网关(NGFW) 秉持安全可视、简单有效的理念&#xff0c;以资产为视角的全流程防御的下一代安全防护体系&#xff0c;并融合传统防火墙、入侵防御系统、防病毒网关、上网行为管控、VPN网关、威胁情报等安全模块于一体的智慧化安全网关。 较低版本的系统存…

淘宝API技术文档解析,从入门到实战

探索淘宝数据的奥秘&#xff0c;淘宝是目前国内最大的B2C电商平台之一&#xff0c;每天都会产生海量的数据。借助淘宝API技术文档&#xff0c;我们可以轻松地获取到这些数据&#xff0c;从而为电商运营和数据分析提供有力支持。 1.什么是淘宝API&#xff1f; 淘宝API&#xf…

用免费GPU线上优化猫狗识别实践

该部分以“猫狗识别模型”为例&#xff0c;学习如何直接通过平台提供的开发环境调用GPU资源 一.学习准备 获取官方代码文件&#xff1a;https://platform.virtaicloud.com/gemini_web/workspace/space/n9tte8i2aspd/project/list 二.创建项目 1&#xff09;进入趋动云用户工…

轻量封装WebGPU渲染系统示例<12>- 基础3D对象实体(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/main/src/voxgpu/sample/PrimitiveEntityTest.ts 此示例渲染系统实现的特性: 1. 用户态与系统态隔离。 细节请见&#xff1a;引擎系统设计思路 - 用户态与系统态隔离-CSDN博客 2. 高频调用与低频调用隔…

不会写文档的程序员不是好的程序员

在当今数字化的世界中&#xff0c;软件开发行业正经历着前所未有的繁荣。从移动应用到大型企业系统&#xff0c;软件构建了现代社会的基础。在IT行业中&#xff0c;文档是一种非常重要的沟通工具。它可以帮助程序员和客户及团队成员之间进行有效的沟通和协作&#xff0c;提高工…

基于springboot实现原创歌曲分享平台系统项目【项目源码+论文说明】

基于springboot实现原创歌曲分享平台系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理平台应运而生…

工业自动化工厂PLC远程控制网关物联网应用

远程控制网关在工厂自动化领域中起到了至关重要的作用&#xff0c;特别是在工厂PLC数据通讯方面。它充当着数据传输的桥梁&#xff0c;连接了工厂中的各类设备和系统&#xff0c;实现了远程监控和控制的功能。本文将详细介绍远程控制网关在工厂PLC数据通讯中的应用。 远程控制网…