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

目录

一,服务器初始化

[1.0 部分文件代码](#1.0 部分文件代码)

[1.1 关于Tcp协议](#1.1 关于Tcp协议)

[1.2 创建和绑定套接字](#1.2 创建和绑定套接字)

[1.3 监听](#1.3 监听)

二,服务器启动

[2.1 获取连接](#2.1 获取连接)

[2.2 提供服务](#2.2 提供服务)

[2.3 客户端启动源文件 Main.cc](#2.3 客户端启动源文件 Main.cc)

二,客户端编写

[2.1 关于Tcp客户端](#2.1 关于Tcp客户端)

[2.2 客户端代码](#2.2 客户端代码)

[2.3 效果演示](#2.3 效果演示)

[2.4 优化](#2.4 优化)

三,字段翻译的应用场景

[3.1 翻译功能实现](#3.1 翻译功能实现)

[3.2 效果演示](#3.2 效果演示)

四,守护进程

[4.1 理解"会话","前台"和"后台"](#4.1 理解“会话”,“前台”和“后台”)

[4.1 关于守护进程](#4.1 关于守护进程)

[4.4 将服务器实现成守护进程版本](#4.4 将服务器实现成守护进程版本)


一,服务器初始化

1.0 部分文件代码

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

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

Log.hpp 日志文件:

cpp 复制代码
#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文件:

bash 复制代码
.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博客

cpp 复制代码
#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 创建和绑定套接字

下面是

cpp 复制代码
#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即可

cpp 复制代码
// 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的大小
cpp 复制代码
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的代码:

cpp 复制代码
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,创建服务器对象,初始化服务器,运行服务器

cpp 复制代码
#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 客户端代码

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

cpp 复制代码
#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 优化

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

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

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

①多进程处理

cpp 复制代码
// ②:多进程------------------------------------------
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忽略,取消等待
}

​​​

②多线程处理

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

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

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

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

cpp 复制代码
#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;
};
cpp 复制代码
// ④:线程池 ---------------------
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);

三,字段翻译的应用场景

3.1 翻译功能实现

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

翻译需要两个文件:

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

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

bash 复制代码
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:

cpp 复制代码
#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头文件的实现,该文件作用就是将调用这个函数的进程实现为守护进程:

cpp 复制代码
#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函数,即可将服务器实现为守护进程:

相关推荐
洁洁!2 小时前
【计算机网络】数据链路层深度解析
网络·网络协议·计算机网络
Benaso13 小时前
计算机网络通关学习(一)
学习·计算机网络
又是费尽心思不重名的程序猿15 小时前
计算机网络
计算机网络
我命由我1234515 小时前
MAC 地址简化概念(有线 MAC 地址、无线 MAC 地址、MAC 地址的随机化)
android·学习·计算机网络·安卓·学习方法
Harper. Lee18 小时前
【计网】数据链路层:概述之位置|地位|链路|数据链路|帧
网络·笔记·学习·计算机网络·智能路由器
无敌岩雀1 天前
【计算机网络】TCP 协议——详解三次握手与四次挥手
网络·网络协议·tcp/ip·计算机网络
洁洁!1 天前
深入分析计算机网络性能指标
网络·计算机网络·性能优化
邂逅岁月2 天前
【网络原理】❤️Tcp 连接管理机制❤️ “三次握手” “四次挥手”的深度理解, 面试最热门的话题,没有之一, 保姆式教学 !!!
网络·tcp/ip·计算机网络·面试·tcp·网络连接·tcp机制
Aoharu2 天前
【计算机网络】IP, 以太网, ARP, DNS
网络协议·tcp/ip·计算机网络
Aoharu2 天前
【计算机网络】初识网络
网络·计算机网络·智能路由器