计算机网络(四) —— 简单Tcp网络程序

目录

一,服务器初始化

1.0 部分文件代码

1.1 关于Tcp协议

1.2 创建和绑定套接字 

1.3 监听

二,服务器启动

2.1 获取连接

2.2 提供服务

2.3 客户端启动源文件 Main.cc 

二,客户端编写

2.1 关于Tcp客户端

2.2 客户端代码 

2.3 效果演示

2.4 优化

三,字段翻译的应用场景

3.1 翻译功能实现

3.2 效果演示

四,守护进程

4.1 理解“会话”,“前台”和“后台”

4.1 关于守护进程

4.4 将服务器实现成守护进程版本


一,服务器初始化

1.0 部分文件代码

代码文件:计算机网络/网络编程套接字/Tcp · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com)

此网络程序用到的头文件有这几个,可以先全部创建出来:

Log.hpp 日志文件:

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

Log log;

然后是makefile文件:

.PHONY:all
all:tcpserver tcpclient
tcpserver:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f tcpserver tcpclient 

 然后是线程池文件ThreadPool,这个文件其实就是我们之前写的线程池”:Linux系统编程——线程池_linux系统编程 线程池-CSDN博客

#pragma once

#include <vector>
#include <queue>
#include <unistd.h>

#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"

const int g_thread_num = 3; // 表示默认创建线程个数

// 线程池本质是一个生产消费模型
template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex() // 获取锁的地址
    {
        return &_lock;
    }
    bool isEmpty() // 判断队列是否为空
    {
        return task_queue_.empty();
    }

    void waitCond()
    {
        pthread_cond_wait(&_cond, &_lock); // 等待的时候释放锁,唤醒时再申请锁
    }

    T getTask()
    {
        T t = task_queue_.front();
        task_queue_.pop();
        return t;
    }

public:
    void run() // 线程池启动
    {
        for (int i = 1; i <= _num; i++)
        {
            _threads.push_back(new Thread(i, routine, this)); // 传this指针,让回调方法能够访问类
        }

        for (auto &iter : _threads)
        {
            iter->start(); // 执行thread_create函数,创建线程,创建的数量由数组大小来定,而数组大小在构造函数定义好了,
            // std::cout << iter->GetName() << "启动成功" << std::endl;
            logMessage(NORMAL, "%s%s", iter->GetName().c_str(), "启动成功");
        }
    }
    // 取任务
    // 如果定义在类内,会有隐藏this指针从而影响使用,所以加上static
    // 如果一个类内部成员用static,那么它只能使用静态成员再调用静态方法,无法使用类内的成员属性和方法
    // 如果这个静态的routine是所谓的消费线程,那么要pop队列,但是编译时会报错,这就坑了
    // 所以为了能让routine拿到类内属性,我们再上面push_back的插入Thread对象时,可以把this指针传过来,通过函数来进行访问(与其让它拿到task_queue,不如让它拿到整体对象)
    static void *routine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);            // 该操作形象点说就是改文件后缀,这里的后缀是args指针
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(td->_args); // 然后这一步相当于解压操作,拿到指针指向对象的线程池指针
        // 消费逻辑
        // 先加锁,while(task_queue_.empty()) wait(); 如果任务队列为空就等待
        // 不为空就获取任务,然后处理,处理完就解锁
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex()); // 通过this指针调用getMutex获得锁的地址,实现加锁,保证该代码块是安全的代码块
                while (tp->isEmpty())
                    tp->waitCond(); // 判断队列是否为空,为空就等待
                // 读取任务
                task = tp->getTask(); // 任务队列是共享的,这句话就是将任务从共享,拿到自己的私有空间
            }
            task(td->_name); // 执行任务,task是队列里的数据,也就是Task类,改类重载了operator(),所以可以直接使用圆括号执行任务

            // 测试能否传入this指针
            // tp->show();
            // sleep(1);
        }
    }
    // 往队列里塞任务
    void pushTask(const T &task)
    {
        lockGuard lockguard(&_lock); // 只单纯加锁,加了任务后还应该要唤醒对应的消费线程来消费
        task_queue_.push(task);
        pthread_cond_signal(&_cond);
    }

    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == _tp) // 首次使用时创建对象,并且在加锁前先判断一次,能减少加锁解锁的次数,提高效率
        {
            pthread_mutex_lock(&_mutex);
            if (nullptr == _tp)
            {
                std::cout << "创建单例" << std::endl;
                _tp = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&_mutex);
        }
        return _tp;
    }

private:
    ThreadPool(int thread_num = g_thread_num)
        : _num(thread_num)
    {
        pthread_mutex_init(&_lock, nullptr); // 初始化锁
        pthread_cond_init(&_cond, nullptr);  // 初始化条件变量
    }
    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join(); // 在释放前join下
            delete iter;
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c

private:
    std::vector<Thread *> _threads; // 这个数组存的是将来要创建的线程
    int _num;
    std::queue<T> task_queue_; // 别人发任务来放到队列里,然后派发给指定线程去执行,所以只要添加到队列里,就自动叫醒一个线程来处理
    pthread_mutex_t _lock;
    pthread_cond_t _cond;

    // 另一种方案:
    // 我们一开始定义两个队列queue1,queue2
    // 然后再定义两个制作std::queue<T> *p_queue,  *c_queue
    // 然后p_queue->queue1,  c_queue->queue2
    // 当生产一批任务后,我们放到queue1里,然后swap(p_queue, c)queue);
    // 然后消费者处理完毕后再swap(p_queue, c_queue);
    // 所以因为我们生产和消费用的是不同的队列,未来我们进行资源任务处理的时候,仅仅只需要交换制作,而且也只要把这个交换这一句加锁即可

    static ThreadPool<T> *_tp;
    static pthread_mutex_t _mutex;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr; // 静态成员一般在类外面进行初始化

template <class T>
pthread_mutex_t ThreadPool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER;

其余的文件,在后面的讲解中会一一讲解的 

1.1 关于Tcp协议

首先,我们把服务器封装成一个类,这个类包含服务器的初始化函数和启动函数

Tcp协议服务器初始化的基本步骤和Udp是一样的,只是Tcp多了一点东西:

  • 在创建套接字是协议家族选择AF_INET,表示进行网络通信
  • 创建套接字时,服务类型选择SOCK_STREAM,表示有序的,可靠的,全双工的已经基于连接的流式服务,也就是Tcp协议

1.2 创建和绑定套接字 

 下面是

#pragma once
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <signal.h>
// 下面四个是套接字编程基本头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "ThreadPool.hpp"
#include "Log.hpp"
#include "Task.hpp"
#include "daemon.hpp"

extern Log log;

const int defaultfd = -1; // 套接字初始化为-1
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 这个是listen第二个参数,一般设置的时候不要设置太大,该参数和Tcp协议内部的一个等待队列有关,目前只要知道这个队列不能太长就可以了,以后详细解释Tcp协议时会讲解

enum
{
    UsageError = 1,
    SOCKET_ERR,
    BIND_ERR,
    ListenError
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *t)
        : sockfd(fd), clientip(ip), clientport(port), tsvr(t)
    {
    }

public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port = 8888, const std::string &ip = defaultip)
        : _listensockfd(defaultfd), _port(port), _ip(ip)
    {
    }

    void InitServer()
    {
        // 1,创建Tcp套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM表示可靠的,双向的基于连接的字节流服务,就是Tcp协议
        if (_listensockfd < 0)                           // 创建失败
        {
            log(Fatal, "listensocket create error: %d, errorstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        log(Info, "listensocket create success,_listensockfd: %d", _listensockfd); // 创建成功,输出日志

        int opt = 1;
        setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器挂掉后无法立即重启,(Tcp协议理论再详细了解)

        // 2,绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); //这两个的效果一样,把字符串转四字节
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // local.sin_addr.s_addr = INADDR_ANY;

        int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            log(Fatal, "bind errno: %d, errorstring: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind success, errno: %d, errorstring: %s", errno, strerror(errno));
        // Tcp是面向连接的,所以服务器一般是比较“被动的”,必须随时应为来自客户端的请求,所以服务器要一直处于一种等待连接到来的状态
        // 3,监听后面实现
   
    }

    void Start()
    {
        // 后面实现  
    }

    ~TcpServer() {}

private:
    int _listensockfd; // 套接字
    uint16_t _port;    // 端口号
    std::string _ip;   // IP
};

1.3 监听

Udp服务器的初始化只有上面创建套接字绑定套接字两步,但是Tcp不一样:

服务器一般都是比较“被动的”,因为服务器必须随时应对来自客户端的请求,所以服务器要一直处于一种等到连接到来的状态,所以就需要将Tcp服务器从创建的套接字设置为监听状态。

问题:你这上面说了跟没说一样,我就想知道监听到底是啥,为什么Tcp需要监听,Udp不需要?

场景:有一家饭店甲开在景区,每到放假就有很多人来,但是店对面也开了一家饭店乙,所以为了提高竞争力,所以饭店甲派出了宣传员,在店门口招揽客人;当有客人来时,宣传员就把客人请进店里,然后招呼店里的服务员来提供服务;然后宣传员就又跑店门口宣传去了。

解答:在上面的场景中,我们把宣传员在店门口“拉客”这种行为,就叫做“监听”,对应到程序中,“监听”的作用就是:将连到我这个服务器的套接字从底层“拉”上来,方便服务器提供服务;因为Tcp是面向连接的,对于连接的各方面的细节把控都要比Udp严格。

监听的sock API和它的英文翻译一样,就是listen

 第一个参数就是需要设置成监听的套接字,而对于第二个参数,我们在后面讲Tcp协议报头的时候再讲,现在只需要知道这个东西和一个队列有关,而这个队列不能太长,目前我们设置为10即可

// Tcp是面向连接的,所以服务器一般是比较“被动的”,必须随时应为来自客户端的请求,所以服务器要一直处于一种等待连接到来的状态
// 3,listen监听(man 2 listen):表示将套接字设置为监听状态,成功返回0,错误返回-1,错误码被设置
if (listen(_listensockfd, backlog) < 0)
{
    log(Fatal, "listen error: %d, errorstring: %s", errno, strerror(errno));
    exit(ListenError);
}
log(Info, "listen success, errno: %d, errorstring: %s", errno, strerror(errno));

二,服务器启动

2.1 获取连接

服务器启动之后,要做的事情主要也就是两个:获取连接,然后进行处理

Tcp是面向连接的,所以Tcp在真正进行数据通信前,都要先与客户端建立连接才能通信,用到的sock API也和连接的英文翻译一样:accept函数:

  • sockfd:这个就是我们前面listen监听函数使用的那个,因为_listrnsockfd核心工作是在底层获取新连接,真正提供数据通信服务的,是accept返回的sockfd
  • addr:我们的老朋友sockaddr的结构体指针,作为输出型参数,保存客户端的各种信息,方便后面返回信息给客户端
  • addrlen:表示sockaddr的大小
void Start()
{
    log(Info, "tcpserver is running...");
    while (true)
    {
        // 1,获取新连接
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
        // 问题:Udp只要一个套接字,为啥Tcp有上面的sockfd和类的_listensockfd两个甚至以后会有多个呢?
        //_listensockfd核心工作就是在底层获取新的连接,真正提供数据通信服务的,是accept返回的sockfd。所以我们会有两个套接字,获取新链接的套接字叫做“监听套接字”
        if (sockfd < 0)
        {
            log(Warning, "accept errno: %d, errorstring: %s", errno, strerror(errno));
            continue; // 一次获取失败就重新获取
        }
        uint16_t clientport = ntohs(client.sin_port); // 获取客户端port、
        char clientip[32];
        inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 获取客户端的ip地址
        // 2,根据新连接来进行通信
        log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
        Service(sockfd, clientip, clientport); // 给连接过来的ip进行服务
        //  问题:1,客户端退了服务器咋办  2,客户端断线了咋办
        close(sockfd);
    }
}

2.2 提供服务

确认连接成功之后,就是提供服务了,提供服务也分为三步:

  1. 读取客户端发来的数据
  2. 处理好数据
  3. 将结果返回给客户端

后面两点和Udp一样,也很好理解,但是第一点读取数据,Tcp的处理方式和Udp有很大差别: 

  • Udp服务器是客户端直接发给服务器的,所以Udp服务器需要用recvfrom函数去阻塞式地接受信息
  • 但是Tcp协议在底层做了很多工作,就比如Tcp是直接维护了网卡文件,将客户端发来的数据直接保存在了网卡文件里
  • 所以我们Tcp服务器要想获取链接,可以用recvfrom阻塞式读取,也可以直接用系统的read接口,以sockfd为文件描述符,直接像读文件那样读取客户端发来的消息即可

下面是处理函数Service的代码:

void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
{
    char buffer[4096];
    while (true)
    {
        ssize_t n = read(sockfd, buffer, sizeof(buffer)); // 可以用文件的接口从sockfd里面读数据
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
            std::string echo_string = "tcpserver echo# ";
            echo_string += buffer;
            write(sockfd, echo_string.c_str(), echo_string.size()); // 也可以用文件的接口往sockfd写回数据
        }
        else if (n == 0) // 客户端退出会关闭套接字,那么read会读出错,返回值n会赋值为0
        {
            log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
            break;
        }
        else // 读取出错
        {
            log(Warning, "read error, sockfd: %d, client port: %d", sockfd, clientip.c_str(), clientport);
            break;
        }
    }
}

2.3 客户端启动源文件 Main.cc 

作用主要是读取命令行输入的IP和Port,创建服务器对象,初始化服务器,运行服务器 

#include "TcpServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << "port[1024+]\n"
              << std::endl;
}

// ./tcpserver 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();

    tcp_svr->Start();

    return 0;
}

二,客户端编写

2.1 关于Tcp客户端

客户端大部分内容和Udp客户端差不多,作用很简单,就是发送数据给服务器,服务器处理好数据后发回来,最后客户端打印数据,但是也有下面几个需要注意的地方:

  • 由于是Tcp协议服务器和客户端,所以客户端也要和服务器一样建立连接才能进行通信,所以客户端确认连接需要用到connect函数
  • 客户端必须要有“断线重连机制”,因为Tcp服务是“保证通信可靠的服务”而为了保证“可靠”,所以需要做更多的工作,花费更多的成本,但是也能理解,毕竟“世上没有免费的午餐”

断线重连主要涉及两个地方,一个是连接之前,一个是连接之后;客户端和服务器对这两个时段的断线处理机制都不一样

连接之前:

  • 服务器:就是一直阻塞着等待连接到来,
  • 客户端:客户端在建立链接时,如果第一次没连上,一般不会立即break退出,而是等待一秒,再连一次,这样依次进行下去,当超过重连次数后,客户端才会提示说服务器断线,或者网络连接断开;

连接之后: 

  • 连接成功之后,客户端和服务器就都是对网络文件进行读写,所以会直接在读取网络文件时顺便处理断线问题,因为只要有一方退出了,网络文件就都没了,那么服务器就会读取失败,

2.2 客户端代码 

下面是客户端的代码:包括断线重连机制:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

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

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 填写套接字信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    // server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 字符串转四字节,效果和inet_pton是一样的
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true) // 每次进行翻译后都要重新创建套接字和建立连接,因为目前服务器只会提供一次服务
    {
        // 创捷套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }
        int cnt = 5; // 断线重连次数
        int isreconnect = false;
        do
        {
            // tcp客户端要bind,但是不需要显示bind,客户端发起连接的时候,系统会自动进行bind,随机端口,这点和Udp是一样的
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0) // 连接错误
            {
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                isreconnect = true; // 重连失败,继续重连
                cnt--;
                x = 1;
                sleep(2);
            }
            else
            {
                isreconnect = false; // 重连几次后如果成功就不再重连
                if (x == 1)
                {
                    std::cout << "reconnect success!" << std::endl;
                    x = 0;
                }
            }
            // 连接成功
        } while (cnt && isreconnect);
        if (cnt == 0) // 超过断线重连次数就直接break退出
        {
            std::cerr << "user offline... " << std::endl;
            break;
        }
        // 上面是建立确定连接过程,下面是正常提供服务
        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size()); // 发消息
        if (n < 0)
        {
            std::cerr << "write error..." << std::endl;
        }

        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer)); // 收消息
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else
        {
            std::cerr << "read error" << std::endl;
        }

        close(sockfd);
    }
    return 0;
}

2.3 效果演示

我们先演示正常的通信,再演示断线机制,下面是正常通信:

接下来我们演示断线重连的场景:

2.4 优化

就拿我们这个处理方式作为例子,其实细想一下,可以发现单进程处理任务是有问题的:

单线程处理任务直接导致我们服务器的执行代码是线性的,如果处理时间过长,会直接阻塞住,而这个时候其他客户端再来连接的话就直接访问失败了,因为服务器阻塞着,所以服务器不要这样搞

对于这种情况,下面有几种处理方法:

①多进程处理 

// ②:多进程------------------------------------------
pid_t id = fork();
if (id == 0) // 子进程
{
    close(_listensockfd); // sockfd是前面打开的描述符,所以正常情况下_listensockfd子进程用不到,所以可以关闭
    if (fork() > 0)       // 创建孙子进程
    {
        // 父进程
        //  exit(0);
        // 这样一写,后面的wait等待就不会被阻塞了,因为在子进程里面又fork了一次,这个父进程退了相当于子进程退了,
        // 但是这个小的父进程的子进程没有退,所以到下面的代码时,其实是孙子进程最后提供的服务
    }
    // 孙子进程,而孙子继承的父进程直接挂掉了,最后就会被系统“领养”,最后被系统自动回收
    Service(sockfd, clientip, clientport); // 给连接过来的ip进行服务
    close(sockfd);
    exit(0);
}
else // 父进程
{
    // 前面父进程获取到的sockfd已经给子进程继承下去给子进程用了,所以父进程就不再关心sockfd了,
    //和子进程不关心_listensockfd一样,如果不关就会导致系统里面有很多打开的文件没有关,所以要关闭不必要的文件描述符
        close(sockfd);
    // 这个步骤和管道重定向有相似之处,可以重复关,因为会有引用计数,关掉了只是把计数-1
    pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待,但是阻塞不满足要求,所以有了孙子进程
    (void)rid;                           // 可以直接用signal忽略,取消等待
}

 ​​​

②多线程处理 

// ③:多线程
// 创建进程是需要代价的,所以多进程版了解一下即可,实际开发中不会用多进程模式去搞,一般都是用线程去搞
ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
// 多线程这里不能和多进程那样关闭文件描述符,因为所有的线程都公用的一个当前进程的文件描述符表

 

③最实用的,就是利用线程池去搞

首先我们会构建任务,然后创建初始化和启动线程池,然后把任务放进线程池里,这样线程池就会自动帮我们处理任务了:

任务头文件Task.hpp的代码如下,对于线程池优化我们会结合后面的翻译场景一起搞,所以所以Init.hpp会在后面实现:

#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include "Log.hpp"
#include "Init.hpp"

extern Log log;
Init init;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
        : _sockfd(sockfd), _clientip(clientip), _clientport(clientport)
    {
    }
    Task()
    {
    }
    void run()
    {
        char buffer[4096];
        // Tcp是面向字节流的,你怎么保证,你读取上来的数据,是"一个" "完整" 的报文呢?
        ssize_t n = read(_sockfd, buffer, sizeof(buffer)); // BUG?
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.translation(buffer); // 执行翻译任务,该操作由线程池执行
            // echo_string += buffer; //对话打开,翻译去掉

            int n = write(_sockfd, echo_string.c_str(), echo_string.size()); // 写的时候万一对应的客户端断开连接了,那么写会崩溃
            if (n < 0)
            {
                log(Warning, "write error, errno: %d, errstring: %s", errno, strerror);
            }
        }
        else if (n == 0)
        {
            log(Info, "%s:%d quit, server close sockfd: %d", _clientip.c_str(), _clientport, _sockfd);
        }
        else
        {
            log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", _sockfd, _clientip.c_str(), _clientport);
        }
        close(_sockfd);
    }
    void operator()()
    {
        run();
    }
    ~Task()
    {
    }

private:
    int _sockfd;
    std::string _clientip;
    uint16_t _clientport;
};
// ④:线程池 ---------------------
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);

 

三,字段翻译的应用场景

3.1 翻译功能实现

客户端什么都不变,比较我们这个客户端的作用很简单,就是发消息接受消息,所以实现翻译功能只要在服务器处理客户端发来的数据的方式上做点事情就好了,上面优化的步骤已经将线程池和任务对象搞好了,然后我们只需要直线翻译的具体实现即可:

翻译需要两个文件:

  • 一个是字典文件,负责存放部分英文单词和对应的中文翻译的键值对(dict.txt
  • 一个是查询程序,负责读取字典文件里的数据,并负责查询并返回(Init.hpp

dict.txt字典文件,负责存放部分英文单词和中文翻译: 

apple:苹果...
banana:香蕉...
red:红色...
yellow:黄色...
the: 这
be: 是
to: 朝向/给/对
and: 和
I: 我
in: 在...里
that: 那个
have: 有
will: 将
for: 为了
but: 但是
as: 像...一样
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他们
we: 我们
their: 他们的
his: 它的
with: 和...一起
she: 她
he: 他(宾格)
it: 它

 Init.hpp查询程序头文件,使用unoedered_map:

#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

// yellow:黄色...
static bool Split(std::string &s, std::string *part1, std::string *part2)
{
    auto pos = s.find(sep);
    if (pos == std::string::npos)
        return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos + 1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if (!in.is_open())
        {
            log(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while (std::getline(in, line)) // 从文件流中读一行
        {
            std::string part1, part2;
            Split(line, &part1, &part2); // 以冒号为分隔符,把字典里的英文和中文隔开然后分别加载到part1和part2里
            dict.insert({part1, part2}); // 然后再把两个东东放到unordered_map和键值对里去
        }
        in.close();
    }
    std::string translation(const std::string &key)
    {
        auto iter = dict.find(key);
        if (iter == dict.end()) // 找到迭代器结尾,表示没找到
            return "Unknow";
        else
            return iter->second;
    }

private:
    std::unordered_map<std::string, std::string> dict;
};

3.2 效果演示

四,守护进程

4.1 理解“会话”,“前台”和“后台”

一个用户尝试在Linux登录时,Linux会形成一个“会话(session)”,每一个会话都会启动一个bash进程,这个bash和我们的键盘和显示器相关。

我们执行程序时,可以在后面加上“ & ”,表示将此进程放到后台运行,jobs命令可以查看后台任务,fg 后套进程编号可以将后台进程重新放回前台

问题:如何理解前台和后台?

解答:哪个进程和标准输入(键盘文件)关联,哪个进程就是前台

  • 我们自己创建的单进程,pid和pgid是一样的,叫做“自成一组” 
  • 而上面我们创建的三个sleep进程,pid和pgid是一样的,叫做“三个自成一组”,这三个sleep合起来就叫做进程组

问题:SID是什么?

解答:用户登录时会创建session会话,当登录的用户多了的时候,session就多了起来,所以Linux需要把这些session也管理起来,“先描述,再组织”,所以系统就会维护一些session结构体,同时为了区分各个session结构体,就会给它们编号,最后就是我们的session id,也就是上面的SID

问题:上面是创建session会话,那么退出会话的时候是什么样的呢?

解答:当终端直接关掉,再重新开个终端重新查的时候,之前的后台pppid全变成1了,TTY变成问号了TPGID变成-1了。这是因为退了后,这几个进程的父进程是bash,bash退了,变孤儿进程了,被1号进程领养,所以这种进程是受到了用户登录和退出的影响的

引出守护进程:如果我们想让一个进程不受用户登录和注销的影响,就要让一个进程守护进程化 

4.1 关于守护进程

这里应该是第一次接触到守护进程的这样一个概念,要想学一个一个概念,还是离不开那三个问题:

  1. 是什么
  2. 有什么用
  3. 咋做到的

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

Linux大多数服务器都是守护进程化的,Internet服务器的inetd,Web服务器的httpd等。Linux启动时会启动很多系统服务进程,这些进程都是在Linux启动时创建,因此不受用户登录注销的影响,ps ajx可以查看系统中的进程 

问题:如何做到守护进程?

解答 :有个接口作用是创建一个会话,setsid函数:

  •  调用这个函数的进程不能是一个进程组的组长,但是如果是自成进程组长的进程,就难搞了
  • 需要保证自己不是组长,第一个进程是组长,那么让我不是第一个进程就好了,这里就要用到fork,父进程直接exit,子进程调用setsid();

所以守护进程的本质也是孤儿进程,但是这个孤儿拒绝被领养,自强成为一个会话

4.4 将服务器实现成守护进程版本

下面是Deamon.hpp头文件的实现,该文件作用就是将调用这个函数的进程实现为守护进程:

#pragma once

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

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "") // 不传参数的话就是默认把守护进程工作目录放到根目录去
{
    // 1,守护进程需要忽略其它信信号
    signal(SIGCLD, SIG_IGN);  // 直接忽略17号信号,为了防止万一出现一些读端关掉了,写端还在写的情况,守护进程
    signal(SIGPIPE, SIG_IGN); // 直接忽略13号信号
    signal(SIGSTOP, SIG_IGN); // 忽略19号暂停信号

    // 2,将自己变成独立的会话
    if (fork() > 0)
        exit(0); // 直接干掉父进程
    setsid();    // 子进程自成会话

    // 3,更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());

    // 4,不能直接关闭三个输入流,打印时会出错,Linux中有一个/dev/null 字符文件,类似垃圾桶,所有往这个文件写的数据会被直接丢弃,读也读不到
    // 所以可以把标准输入输出错误全部重定向到这个文件中
    // 如果需要就往文件里写,反正不能再打印到屏幕上了
    int fd = open(nullfile.c_str(), O_RDWR); // 以读写方式打开
    if (fd > 0)                              // 打开成功
    {
        // 把三个默认流全部重定向到垃圾桶的null的套接字里去
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

然后直接在服务器启动函数开头,调用Daemon函数,即可将服务器实现为守护进程:

 

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

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

相关文章

新书宣传:《量子安全:信息保护新纪元》

《量子安全&#xff1a;信息保护新纪元》 前言本书的看点本书的目录结语 前言 你好&#xff01; 这是我第一次发布类广告的博文&#xff0c;目的也很单纯&#xff0c;希望以作者的身份介绍一下自己出版的图书——《量子安全&#xff1a;信息保护新纪元》。此书于2024年7月出版…

数学建模笔记—— 回归分析

数学建模笔记—— 回归分析 回归分析1. 回归分析的一般步骤2. 一元线性回归分析2.1 具体过程2.1.1 确定回归方程中的解释变量和被解释变量2.1.2 确定回归模型和建立回归方程2.1.3 利用回归直线进行估计和预测2.1.4 对回归方程进行各种检验(补充)1. 回归直线的拟合优度2. 显著性…

Windows下Python和PyCharm的应用(二)__快捷键方式的设定

前言 程序写久了&#xff0c;难免会形成自己的编程习惯。比如对某一套快捷键的使用&#xff0c;已经形成了肌肉记忆。 为了方便快捷键的使用&#xff0c;可以在PyCharm中设置自己喜欢的快捷键。 我比较习惯于微软Visual Studio的快捷键设置。&#xff08;因为早些年VC开发用的…

8.Bug流程管理,禅道的使用(包含笔试/面试题)

一、bug的生命周期&#xff08;重点&#xff09; bug的生命周期就是从bug被发现到bug被关闭的整个过程。 1.bug生命周期&#xff1a; 新建&#xff08;提交bug&#xff09; - 指派 - 已解决 - 待验 - 关闭 new&#xff08;新建&#xff09; - assign额的&…

SVGJS操作

svgjs用于操作 SVG 和动画的轻量级库。 官网 SVG.js v3.2 |家 (svgjs.dev) 效果 代码如下 <template><h3>测试操作已有SVG</h3><button click"changeText()">利用ID定位</button><button click"changeChild()">chan…

git:分支管理

目录 一、分支概念 二、创建分支 三、切换分支 四、合并分支 五、删除分支 六、合并冲突 七、分支管理策略 八、分支策略 九、bug分支 十、强制删除分支 一、分支概念 在版本回退里&#xff0c;每次提交&#xff0c;git都把它们串成一条时间线&#xff0c;这条时间线可以…

计算语言学(一)基础

概率论的几个概念 熵、互信息 神经网络基础 MLP CNN RNN Seq2Seq LSTM Transformer 语料库与知识库

在Windows系统上进行浏览器维护和故障排除技巧

使用Windows系统的电脑时&#xff0c;浏览器是连接互联网的重要工具。然而&#xff0c;随着时间的推移和使用频率的增加&#xff0c;浏览器可能会面临各种问题&#xff0c;如加载缓慢、频繁崩溃或者广告弹窗过多等。为了确保顺畅的上网体验&#xff0c;本文将向您展示如何进行浏…

光伏发电并网系统的能量管理

光伏发电是发展前景良好的清洁型能源。近年来&#xff0c;光伏发电站陆续投入运营&#xff0c;受到光照和温度等因素的影响&#xff0c;电力输出功率存在间歇波动性和随机性&#xff0c;因此&#xff0c;为了维持电网稳定性采用储能装置十分必要。 1储能技术在光伏并网发电系统…

AttackGen - AI 网络安全事件响应测试工具,附下载链接

为了提高我们团队在安全活动中的响应效率&#xff0c;我关注到了一款叫 AttackGen 的工具&#xff0c;我们需要的是一个既能快速生成场景又能准确反映现实威胁的工具。 在红蓝对抗中&#xff0c;我们经常要模拟各种攻击场景&#xff0c;以测试我们的防御水平。这不仅仅是为了“…

图像和文本统一识别

图像和文本统一识别&#xff08;UNIT&#xff09;是一种尝试将图像识别和文本识别任务统一到单一模型中的技术。这种方法的目标是提高模型在处理包含文本信息的图像时的效率和准确性&#xff0c;从而使模型能够更好地理解和处理视觉内容中的文本信息。 使用的技术&#xff1a;…

【机器学习】7 ——k近邻算法

机器学习7——k近邻 输入&#xff1a;实例的特征向量 输出&#xff1a;类别 懒惰学习&#xff08;lazy learning&#xff09;的代表算法 文章目录 机器学习7——k近邻1.k近邻2.模型——距离&#xff0c;k&#xff0c;分类规则2.1距离——相似程度的反映2.2 k值分类规则 算法实…

排序(插入,希尔,选择,堆,冒泡,快速,归并,计数)

本文中的Swap()函数都是下面这段代码 // 交换 void Swap(int* p1, int* p2) {int tmp *p1;*p1 *p2;*p2 tmp; }文章目录 常见排序&#xff1a;一.插入排序1.直接插入排序&#xff1a;2.希尔排序&#xff1a; 二.选择排序1.选择排序&#xff1a;2.堆排序&#xff1a; 三.交换排…

docker部署rabbitMQ 单机版

获取rabbit镜像&#xff1a;我们选择带有“mangement”的版本&#xff08;包含web管理页面&#xff09;&#xff1b; docker pull rabbitmq:management 创建并运行容器&#xff1a; docker run -d --name rabbitmq -p 5677:5672 -p 15677:15672 rabbitmq:management --name:…

【OpenCV3】图像的翻转、图像的旋转、仿射变换之图像平移、仿射变换之获取变换矩阵、透视变换

1 图像的放大与缩小 2 图像的翻转 3 图像的旋转 4 仿射变换之图像平移 5 仿射变换之获取变换矩阵 6 透视变换 1 图像的放大与缩小 resize(src, dsize[, dst[, fx[, fy[, interpolation]]]]) src: 要缩放的图片dsize: 缩放之后的图片大小, 元组和列表表示均可.dst: 可选参数, 缩…

秋招春招,在线测评题库包含哪些?

各位小伙伴们&#xff0c;秋招春招的号角已经吹响&#xff0c;作为HR&#xff0c;我们又要开始忙碌起来了。面对众多的候选人&#xff0c;如何高效、准确地筛选出合适的人选呢&#xff1f; 在线测评就是一个非常有用的工具。本文就说说在线测评题库里的那些事儿&#xff0c;主…

ant-design-vue中实现a-tree树形控件父子关联选中过滤的算法

在使用ant-design-vue的框架时&#xff0c;a-tree是比较常用的组件&#xff0c;比较适合处理树形结构的数据。 但是在与后台数据进行授权交互时&#xff0c;就不友好了。 在原生官方文档的例子中&#xff0c;若子项被勾选&#xff0c;则父级节点会被关联勾选&#xff0c;但这勾…

天通报警呼叫柱:为边防哨所筑起坚固的通信堡垒

一、背景 边防哨所是国家安全的重要防线&#xff0c;肩负着守护边境安全、维护国家主权和领土完整的神圣使命。由于边防哨所通常位于地理位置偏远、环境恶劣的地区&#xff0c;通信问题成为影响边防工作的重要因素&#xff0c;给边防官兵的日常工作和应急响应带来了不小的挑战…

vue3封装数字上下滚动翻牌器,

优点&#xff1a;可以传入字符串设置初始数字位数&#xff0c;也可以直接传入数字&#xff0c;让他自己根据位数渲染 组件代码&#xff1a; <template><div class"count-flop" :key"compKey"><!-- --><div:class"item ! . ?…

欺诈文本分类检测(十四):GPTQ量化模型

1. 引言 量化的本质&#xff1a;通过将模型参数从高精度&#xff08;例如32位&#xff09;降低到低精度&#xff08;例如8位&#xff09;&#xff0c;来缩小模型体积。 本文将采用一种训练后量化方法GPTQ&#xff0c;对前文已经训练并合并过的模型文件进行量化&#xff0c;通…