小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【linux】网络套接字编程(六)TCP服务器与客户端的实现——线程池版,客户端如何实现断线自动重连,服务器与客户端实现英汉翻译——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络套接字编程(七)后台进程,守护进程,TCP服务器与客户端实现——守护进程版,setsid,简单理解TCP的三次握手和四次挥手,TCP的通信是全双工的


一、后台进程

  1. 当我们登录云服务器的时候,系统会分配给我们创建一个bash进程,用于接收我们的命令,所以此时小编进行输入就可以在命令行中看到命令的结果
    在这里插入图片描述
  2. 那么为了更便捷的了解后台进程,我们先编写一个死循环间隔两秒打印的代码,如下
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    while(true)
    {
        cout << "hello..." << endl;
        sleep(2);
    }

    return 0;
}
  1. 紧接着我们在命令行将这个可执行程序运行起来,然后再次在命令行输入指令发现bash对于我们在命令行输入的指令不响应了
    在这里插入图片描述
  2. 这是为什么呢?那么小编就要讲解一下什么是登录,登陆后系统为我们做了什么,以及什么是前台进程,什么是后台进程了
    在这里插入图片描述
  3. 我们知道云服务器是可以允许多个人同时登录并使用的,那么此时系统中为我们做了什么呢?今天张三打卡xshell登录上了云服务器,那么linux操作系统就会在系统中为张三创建一个会话session,并且在会话中,每一个会话都会配备一个bash命令行进程
    在这里插入图片描述
  4. 如上便是一个会话,会话中系统已经为我们创建好了一个bash命令行解释器进程
    在这里插入图片描述
  5. 张三登录xshell系统会张三创建一个session会话,并且在session中创建一个bash命令行解释器进程,初始的时候,让bash获取键盘的输入,初始的时候,默认bash就是前台进程,所以在一个session会话中只能有一个前台进程在运行,键盘信号只能发送给前台进程
  6. 换句话来讲,只有前台进程才能获取键盘输入,所以最开始的时候,我们刚刚登录上云服务器,所以bash是前台进程,那么我们在键盘上的输入就会被bash获取,因此bash获取到了用户的键盘输入,它才会去执行命令,进行命令的解释,将命令执行的结果回显打印给用户
  7. 那么当小编运行了process进程之后,一个session会话中只能有一个前台进程在运行,所以原本处于前台的进程bash就会被系统放到后台运行,而process进程就作为前台进程在运行了,那么此时小编从键盘上再输入指令,键盘的输入只能被前台进程process获取,所以此时处于后台的进程bash就无法获取键盘上输入的指令了,所以bash自然而然就无法执行命令了,所以自然bash就无法对我们的命令做出响应了
  8. 所以呢?所以此时的bash作为后台进程,特点是什么?无法获取键盘输入,所以此时我们就得知了什么是后台进程:无法获取键盘输入的进程是后台进程 ,可以获取键盘输入的是前台进程,所以前台进程和后台进程的本质区别就是谁可以获取键盘输入,而linux下一切皆文件,所以键盘也是文件,所以更本质点来讲,就是前台进程和后台进程的区别就是谁拥有键盘文件
  9. 那么当小编将正在运行的process进程ctrl+c终止,所以此时bash就从后台切换到了前台获取用户的在键盘上的输入,进行命令行解释,将执行结果回显打印到命令行中反馈给用户
    在这里插入图片描述
  10. 所以我们还可以得出,当前台进程被终止的时候,此时位于后台的bash会自动被系统切换到前台作为前台进程运行,获取用户的输入,执行命令,所以也就意味着,在命令行中,前台进程要一直存在
  11. 所以今天,如果小编想要运行process进程的时候,直接让process以后台进程的形式运行,那么该如何做呢?很简单 在运行指令的结尾加上 & 即可,所以我们 ./process & 就可以让process以后台进程的形式运行了,那么此时bash仍然是前台进程,所以仍然可以获取用户输入执行命令
    在这里插入图片描述
  12. 所以小编使用以 ./process & 的形式运行process进程,所以process以后台进程的形式运行了,那么此时bash仍然是前台进程,那么bash作为前台进程就可以获取用户的输入,所以bash就可以执行命令,所以我们输入ls,pwd等指令就可以被bash执行,无误,并且在上图中,小编又使用了 ./process & 运行了第二个process进程,所以也就意味着此时session会话中会同时存在多个后台进程,那么这究竟对不对呢?下面小编使用指令查看一下
ps axj | head -1 && ps axj | grep process | grep -v grep

在这里插入图片描述

  1. 果然,此时session会话中同时存在两个后台进程process,所以也就意味着一个session会话中前台进程只能有一个,而后台进程可以有多个,并且后台进程的打印结果是向显示器进行打印,同样的前台进程bash的命令执行结果的打印也是向显示器上打印,所以我们就可以得出前台进程和后台进程的打印(标准输出,标准错误)都可以向显示器进行打印,但是键盘的输入(标准输入)只能被前台进程获取,即后台进程无法获取键盘的输入(标准输入)

二、后台进程的操作

  1. 可是后台进程中如果有输出打印的话,那么会很影响我们向bash这个前台进程中输入,所以我们可以将后台进程的输出追加重定向到文件中
    在这里插入图片描述
  2. 并且我们可以看到第一个运行的process进程对应的任务号是1,第二个运行的process进程对应的任务号是2,并且成功的将后台进程的输出重定向到了文件中
  3. 那么我们如果想看当前的后台进程的信息,可以使用jobs来进行查看
    在这里插入图片描述
  4. 那么我们该如何终止一个后台进程呢?很简单,使用fg + 任务号,然后使用ctrl + c即可终止,终止后,我们使用jobs查看后台进程,果然后台进程中的其中一个已经被终止了,如下,无误
    在这里插入图片描述
  5. 那么如果我们想暂停一个进程呢?很简单,使用fg + 任务号,然后ctrl + z即可,那么我们使用jobs查看后台进程,此时这个后台进程就被暂停了,如下无误
    在这里插入图片描述
  6. 那么我们如果想将这个被暂停的后台进程重新运行起来该如何做呢?很简单,使用bg + 任务号,即可将被暂停的后台进程重新运行起来,所以此时我们再使用jobs查看后台进程的状态,果然这个原本被暂停的进程被重新运行起来了,如下,无误
    在这里插入图片描述

三、linux的进程间关系

  1. 那么接下来小编先创建一个后台进程./process >> log1.txt & 然后再创建三个后台进程sleep,那么观察下面
    在这里插入图片描述
  2. 那么三个sleep之间是采用两个管道连接起来的,本质上还是创建了3个sleep进程,然后第一个sleep进程休眠结束后,结果是什么都没有,然后将这个结果通过管道交给第二个sleep进程,然后第二个sleep进程开始休眠,同理结束后通过管道交给第三个sleep进程,所以这三个sleep进程合起来在完成同一个任务,process这个后台进程自己在完成一个任务
  3. 所以我们使用如下指令查找一行运行的进程中,带有sleep或者process的进程,grep -E ‘内容|内容’ 是进行正则匹配对应的内容
ps axj | head -1 && ps axj | grep -E 'sleep|process' | grep -v grep

在这里插入图片描述

  1. 那么观察上图,其中PPID是指的是父进程的pid,那么由于是由bash命令行解释器进程执行的命令,所以bash进程就会fork创建子进程,所以自然而然process以及三个sleep进程的父进程就是bash进程了,所以也就可以理解process以及三个sleep进程的PID是bash进程的PID了
  2. 那么第二列PID则是在系统中进程独属于自己的标识,第三列紧接着是PGID则是进程组ID,什么是组的概念呢?就是一起完成同一份任务,由于三个sleep进程小编使用了管道将这三个sleep关联起来,执行一个共同的sleep任务,所以这三个sleep进程属于同一个进程组
  3. 那么进程组ID是如何来的呢?我们可以明显的看到,这三个sleep进程的进程组ID都是31510,居然和第一个sleep进程的PID一样,没错第一个sleep进程就是最先执行sleep的,所以第一个sleep进程在进程组中就属于组长,所以自然进程组的PGID就要以组长的PID为标识,其它的sleep进程就属于组员,组员属于进程组的一份子,所以组员自然就要使用进程组的PGID了
  4. 那么我们继续看SID实际上是session会话的ID,这个SID和bash的PID相同,没错,seddion会话的ID使用的就是会话中bash的PID,那么无论是这个会话中的前台进程还是后台进程,那么本质上都是这个session会话的进程,所以前台进程和后台进程的会话ID相同
  5. 那么进程组和任务有什么关系呢?一个进程组就是完成的一个任务,例如process进程自己完成一个任务,所以process进程自己就是一个进程组,并且process进程自己就是组长,然后进程组ID就是使用组长,自己的PID
  6. 所以多个任务(进程组)在同一个session会话内启动的会话的IP,即SID是相同的
    在这里插入图片描述
  7. 那么我们再看TTY,就是指的是终端,STAT则是指的是当前进程对应的状态
  8. 那么后台进程究竟是否收到用户登录和退出的影响呢?所以小编目前是登录状态,后台进程在后台跑的很欢快,所以现在小编不想登录了,那么要退出,所以小编就直接将xshell关闭,所以自然就退出登录了,那么小编再重新打开xshell进行登录,然后再进行查看后台进程是否存在,结果如下,之前小编运行的后台进程已经被系统清理了
    在这里插入图片描述
  9. 所以说后台进程是会受到用户登录和退出的影响的,那么如果我今天的服务器运行起来之后,我不想要让我们的服务器受到任何用户的登录和退出的影响,所以我们该怎么办呢?守护进程化

四、守护进程

  1. 守护进程居然可以办到让服务器不受到任务用户的登录和退出的影响,那么守护进程究竟是什么原理呢?如下
    在这里插入图片描述
  2. 还是拿我们最初用户登录的图来进行解释,张三今天登录上了linux操作系统,那么linxu操作系统就会为张三这个用户创建属于张三用户的session会话,那么张三在自己的session会话中创建运行了一个进程,然后张三一直保持登录状态,所以这个进程就会一直在张三的session会话里运行
  3. 那么过了几分钟,李四来了,李四也登录上了linux操作系统,那么linux操作系统就会为李四这个用户创建属于李四用户的session会话,那么李四也在自己的session会话中创建了一个进程,但是运行了几分钟之后,李四突然不想登录了,所以李四就将xshell关闭了,即李四退出了,那么李四session会话中的进程一定会受到李四退出的影响,因为李四的进程是在李四的session会话中运行的,那么张三的进程是否会受到李四退出的影响呢?一定不会
  4. 一定不会,因为张三和李四是两个不同的用户,linux操作系统会为这两个用户创建不同的session会话,session会话和session会话之间互相不影响,自然而然的,那么张三的session会话中运行的进程一定不会受到李四退出的影响,所以此时张三在自己session会话中运行的进程我们就可以称之为守护进程
  5. 所以自成进程组,自成会话的进程,我们称之为守护进程
  6. 但是linux操作系统中的可能有很多个用户在使用,所以操作系统就会为这些用户每一个用户创建一个对应的session会话,所以系统中就会同时存在多个session会话,那么linux操作系统要不要将这些session会话管理起来?要,那么如何进行管理呢?
  7. 先描述再组织,先使用struct结构体将session会话描述起来,然后采用一定的数据结构,例如链表,将这些struct结构体实例化的session会话的描述对象组织起来,从此以后对session会话的管理就转化成了对链表的增删查改
  8. 所以系统中一定有对应的系统调用可以创建session会话,所以这也就使得让服务器成为守护进程有了可能

五、TCP服务器与客户端实现——守护进程版

Daemon.hpp

  1. 所以在之前的文章中,小编已经将TCP服务器与客户端基于线程池进行了优化 详情请点击<——,性能方面已经很好了,但是服务器是要一直运行的,所以并且我们不期望服务器的运行会被用户的登录或退出状态影响,所有我们要将我们的服务器进程守护进程化,那么该如何做呢?那么就需要使用到setsid系统调用接口
setsid

在这里插入图片描述

  1. setsid的作用是创建一个session会话,并且将调用这个setsid函数的进程变成新创建会话的一个运行的进程,但是这个调用setsid的函数的进程不能是进程组中的组长,只有进程组中的组员才可以调用setsid
  2. 那么的服务器一运行起来就是进程组的组长呀,那么这该怎么办,难道做不到了吗?很简单,那么我们让我们的服务器,开始的时候就fork创建子进程,此时父进程和子进程会构成一个进程组,父进程是最开始运行的,所以父进程在进程组中是组长的身份,那么子进程自然而然在进程组中就是组员的身份
  3. 那么毕竟父进程fork创建出子进程之后父进程的任务就结束了,后续的服务器守护进程化就由子进程来完成,由于子进程在进程组中担当的是组员的身份,所以这个子进程就可以调用setsid创建session会话,然后这个子进程就可以执行服务器的后续代码,所以此时子进程作为服务器成为守护进程了
  4. 那么此时子进程的父进程已经退出,所以子进程会被操作系统1号进程领养,所以子进程此时就会成为孤儿进程,所以守护进程的本质也就是孤儿进程
  5. 那么这个守护进程化的服务器也会有很多cout标准输出,cerr标准错误,并且服务器也不需要使用键盘进行输入,所以还有标准输入,那么对于服务器来讲,既然已经成为了守护进程,那么这些标准输入,标准输出,标准错误我统统不需要了,因为我服务器已经成为了独立的一个session会话了,用户的登录和退出已经无法对我服务器造成影响了,自然用户也不关心我服务器,所以自然用户的标准输入,即键盘的输入无法输入到我服务器上了,并且我服务器的标准错误,标准输出的打印也不需要了,因为无人关心,所以我服务器干脆就直接将标准输入0,标准输出1,标准错误2,当作垃圾信息扔掉,那么如何扔掉呢?
  6. 那么使用打开 /dev/null 文件,所有向 /dev/null文件中写入的信息,那么自动就会被扔掉,被清理,所以我们可以open打开 /dev/null 文件,然后将标准输入,标准输出,标准错误全部dup2重定向到打开的 /dev/null 文件中,所以这样服务器就无法获取标准输入,无法打印标准输出,标准错误了
  7. 但是在服务器运行的过程中显然会有很多的日志信息,那么在之前的文章中对于服务器的日志,小编对于日志默认都是向显示器上进行打印的,那么 详情请点击<——,而今天的服务器已经成为了一个守护进程了,守护进程一旦运行,默认守护进程要放在系统的根目录下运行,所以服务器也要放到根目录,那么如何将服务器的工作目录调整至根目录呢?那么我们使用chdir即可,谁调用chdir就将调用的进程的工作目录进行更改
  8. 那么服务器的日志之前我们是向显示器进行打印的,可是今天显示器对应的标准输出,标准错误已经被我们关闭了,那么我们要获取服务器运行的日志,不能从显示器上获取了,那么应该如何获取服务器运行过程中的日志呢?
  9. 很简单,小编在当初编写日志插件的时候,就可以设置日志的选项,让日志将日志信息分门别类的放到文件中,所以今天我们的服务器也应该设置日志选项,让日志将日志信息分门别类的放到不同的文件中,但是还需要我们创建一个在系统根目录下创建一个log文件用于日志将信息放到log文件下的不同文件中,至于这些不同文件,日志会自动创建的,由于我们的服务器的工作目录被我们设置成了系统的根目录,所以同样的,那么日志需要一个在当前服务器工作目录下有一个log文件才能将日志信息分门别类的放入文件中,所以我们就应该系统的根目录下创建一个log文件
  10. 那么服务器运行起来之后,我们不希望服务器随随便便的就被一些信号终止,或暂停,所以我们就使用signal将暂停信号,管道信号,子进程信号等其它信号进行忽略
#include <iostream>
#include <string>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

void Daemon(const std::string& cwd = "")
{
    //忽略其它信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    //创建子进程,然后终止父进程,子进程会拷贝父进程
    //资源,例如文件描述符,页表,地址空间,代码等,
    //然后由子进程继续向后执行代码
    if(fork() > 0)
        exit(0);

    //创建session会话,子进程成为服务器,然后守护进程化
    setsid();

    //更改当前子进程代表的服务器进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());

    //打开 /dev/null 文件
    int fd = open(nullfile.c_str(), O_WRONLY);

    //重定向 标准输入,标准输出,标准错误
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

Main.cc

  1. 那么我们在main函数中,使用enable传入Classfile调整日志打印到文件中,并且在根目录下创建log文件
    在这里插入图片描述
#include <iostream>
#include <memory>
#include "TcpServer.hpp"


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


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

    lg.Enable(Classfile); //调整日志打印到文件中
    
    uint16_t port = std::stoi(argv[1]);

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


    return 0;
}

TcpServer.hpp

  1. 包含守护进程的头文件Daemon.hpp,那么main函数中调用服务器的StartServer函数,即服务器启动,我们就调用Daemon函数将服务器守护进程化即可
void StartServer()
{
    Daemon();
    ThreadPool<Task>::GetInstance()->Start();
    lg(Info, "tcpserver is running...");
    
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        
        int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
            continue;
        }
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

        lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
            sockfd, clientip, clientport);

        //version 4 线程池版
        Task t(sockfd, clientip, clientport);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}
测试
  1. 那么我们编译运行服务器tcpserver,由于我们的日志信息是向文件中写入,日志需要根据日志等级,向系统路径下的log文件,创建不同等级的日志文件,由于我们当前用户是普通用户,所以如果以普通用户的身份在系统路径下的log文件中创建不同等级的日志文件权限不足,所以我们需要以sudo以root用户的权限运行我们的服务器tcpserver才可以向系统路径下的log文件创建不同种类的日志文件,并且向不同种类的日志文件中进行写入
    在这里插入图片描述
  2. 下面我们使用指令查看一下tcpserver是否存在,并且是否是孤儿进程已经被系统1号进程领养,并且使用sudo ls -l /proc/PID,查看tcpserver的工作路径是否已经被调整到了根目录,这里必须要使用sudo,因为我们的普通用户,普通用户没有权限查看root权限运行的进程的信息,所以这里需要sudo提权才可以查看服务器进程的PPID,以及服务器进程的工作目录,如下无误
ps axj | head -1 && ps axj | grep tcpserver | grep -v grep

在这里插入图片描述

  1. 既然当前的服务器进程已经守护进程化了,所以应该自成session会话,不受当前用户的登录和退出影响了,所以下面小编关掉xshell,然后再打开,并且重新使用如下指令查看服务器进程是否存在,如下无误,存在
ps axj | head -1 && ps axj | grep tcpserver | grep -v grep

在这里插入图片描述

  1. 所以下面小编使用客户端进程连接服务器,进行查阅英语单词的汉语,如下无误
    在这里插入图片描述

  2. 那么我们接下来切换到根目录查看,日志信息是否正常被打印到了日志文件中,如下,无误
    在这里插入图片描述

  3. 那么我们如何关闭守护进程化的服务器呢?很简单,使用ps,grep指令查到服务器的PID,然后kill -9 PID即可

ps axj | head -1 && ps axj | grep tcpserver | grep -v grep

在这里插入图片描述

  1. 至此,我们的TCP服务器与客户端实现——守护进程版done

六、补充知识

简单理解TCP的三次握手和四次挥手

在这里插入图片描述

  1. 其实TCP是面向连接的,那么TCP是如何建立连接的呢?通过三次握手建立连接,那么TCP是如何释放连接的呢?通过四次挥手释放连接,所以如何理解三次握手呢?
  2. 客户端通过connect向服务端发起连接请求,然后客户端的connect就阻塞等待服务端的应答,这是第一次握手,然后服务端通过accept接收到了客户端的连接请求,然后服务端给客户端应答,客户端的conncet受到了应答,这是第二次握手,然后connect就会再次给服务端发送应答,然后服务端的accept接收到了客户端的应答,然后accept就会返回一个网络文件描述符sockfd用于通信,这是第三次握手,至此客户端和服务端通过三次握手建立起了连接,然后就可以通过网络文件描述符sockfd进行通信了
  3. 其实,这样讲解还是有点抽象,那么如何形象一点呢?男女朋友关系的确立的例子来了,其实男女朋友的关系就是两个人建立起了长期通信连接的关系,假设屏幕面前的你建模很优秀,在校园里,看到了那个她,但是那个她目前没有看到你,所以你无法和对方成为男女朋友关系,那么你跑到她的面前,说到,做我女朋友吧,那个女生问:什么时候,你说,就现在,所以你们两个顺利的成为了男女朋友的关系
  4. 在这个男女朋友关系建立的过程中其实就类似于三次握手,通信双方始终要有一个先主动发起请求,即你发起了请求,然后另一方问询响应,最后你进行确认响应
  5. 那么什么是四次挥手呢?假设我服务器已经和客户端建立连接了,但是今天我服务器不想和这个客户端进行通信了,所以此时我服务器单方面将网络文件描述符关闭了,然后对方客户端收到了服务器要关闭连接的请求,所以此时你客户端同意了对方服务器关闭连接的请求,此时连接断开了吗?没有,因为对方客户端仍然可以通过对方的网络文件描述符向你发送消息,虽然服务器断开了连接请求,仅仅表示的是服务器不可以通过网络文件描述符向客户端发送消息了,但是客户端的网络文件描述符还没有关闭,所以客户端还可以通过网络文件描述符向服务器发送消息,所以上述仅仅是两次挥手的过程
  6. 加上上面的两次挥手的过程,那么还需要如下的两次挥手才可以将连接彻底断开,那么客户端也发起了关闭连接的请求,服务器同意了关闭连接的请求,所以从此以后,客户端无法通过网络文件描述符向服务器发送请求,服务器同样也无法通过网络文件描述符向客户端发送消息,至此服务器和客户端之间通过四次挥手彻底的将通信连接关闭
  7. 其实,这样讲解还是有点抽象,那么如何形象一点呢?男女朋友分手的例子来了,假设你的女朋友认为和你发生了不可调节的矛盾,然后和你说,我们分手吧,然后你说,好呀,分手就分手,然后你们之间彻底分手了吗?没有,因为这仅仅是两次挥手的过程,即你的女朋友关闭了和你连接的信道,那么代表着你的女朋友不会主动向你发送消息了,但是你这边的连接没有断开呀,所以你仍然可以和你的女朋友发送消息呀,所以你就发消息,但是注意,你的女朋友可以收到你的消息,但是她不会再给你发消息了,因为她已经把她和你连接的信道关闭了,所以2天之后,好吧,我也要和你分手,所以你就跟你的女朋友发起了分手请求,所以你的女朋友同意了分手的请求,至此,你这边完成了两次挥手的过程,从此以后你也不会主动和你的前女友发送消息了,前女友也不会主动和你发送消息了,至此两个人通过了四次挥手的过程,所以你俩彻底分手了

TCP的通信是全双工的

  1. 那么我们该如何理解TCP的通信是全双工的呢?全双工即允许同时读写,即同一时间允许一个线程/进程从sockfd网络文件描述符底层对应的文件对象进行读取,并且同一时间允许一个线程/进程从sockfd网络文件描述符底层对应的文件对象进行写入
    在这里插入图片描述
  2. TCP底层的文件对象中会同时维护两个缓冲区,一个发送缓冲区,一个接收缓冲区,所以同一时间一个线程/进程对发送缓冲区进行写入,是否会影响同一时间另一个进程/线程对接收缓冲区进行读取,不影响,所以TCP的通信是全双工的
  3. 其中写入就是用户通过write系统调用接口,然后借助网络文件描述符sockfd将用户缓冲区的数据拷贝到内核缓冲区中,其中读取就是用户通过read系统调用接口,然后借助网络文件描述符sockfd将内核缓冲区的数据拷贝到用户缓冲区中

如何理解连接

在这里插入图片描述

  1. TCP通信是基于连接的,如何在内核层面理解这个连接呢?以服务器为例进行讲解,作为一个服务器,每天要收到大量客户端的连接请求,即服务器要和大量的客户端进行连接,那么操作系统要不要将这些连接管理起来,要,如何管理?先描述再组织
  2. 所以就会先使用struct结构体将连接的属性描述符起来,比如进行连接的客户端的端口号,客户端的IP地址等属性描述在一个结构体中,然后再使用数据结构,例如双链表将这些连接对应的struct文件描述对象管理起来,至此对连接的管理,就转化为了对双链表的增删查改
  3. 所以当服务端和客户端通过TCP的三次握手建立连接,在内核层面无非就是各自在内核中建立一个连接的struct描述对象,然后再新增到双链表中
  4. 那么四次挥手释放连接,在内核层面无非就是将对应的连接的struct描述对象删除释放

七、源代码

TcpServer.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"


const int defaultfd = -1;
const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};

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

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


class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : listensock_(defaultfd), port_(port), ip_(ip)
    {}

    void InitServer()
    {
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //防止偶发性服务器无法立即重启
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(server.sin_addr));
        socklen_t len = sizeof(server);

        if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, listensock_: %d", listensock_);

        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    // static void* Routine(void* args)
    // {
    //     pthread_detach(pthread_self());

    //     ThreadData* td = static_cast<ThreadData*>(args);
    //     td->tsvr->Service(td->sockfd, td->clientip, td->clientport);

    //     close(td->sockfd);
    //     delete td;

    //     return nullptr;
    // }

    void StartServer()
    {
        Daemon("/");

        ThreadPool<Task>::GetInstance()->Start();
        lg(Info, "tcpserver is running...");
        
        for (;;)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", 
                sockfd, clientip, clientport);
            //version 1 单进程/单线程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            //version 2 多进程版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     //child
            //     close(listensock_);
            //     if(fork() > 0)
            //         exit(0);
            //     //grandchild
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);

            //     exit(0);
            // }
            // close(sockfd);
            // pid_t rid = waitpid(id, nullptr, 0);
            // if(rid == id)
            //     std::cout << "father wait success" << std::endl;

            //version 3 多线程版
            // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);

            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);

            //version 4 线程池版
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

    // void Service(int sockfd, const std::string& clientip, const uint16_t& clientport)
    // {
    //     while (true)
    //     {
    //         char inbuffer[4096];
    //         ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    //         if (n > 0)
    //         {
    //             inbuffer[n] = 0;
    //             std::cout << "client say# " << inbuffer << std::endl;

    //             std::string echo_string = "tcpserver echo# ";
    //             echo_string += inbuffer;

    //             write(sockfd, echo_string.c_str(), echo_string.size());                
    //         }
    //         else if(n == 0)
    //         {
    //             lg(Info, "%s:%d quit, server colse sockfd: %d", clientip.c_str(), clientport, sockfd);
    //             break;
    //         }
    //         else  
    //         {
    //             //异常
    //             lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
    //                 sockfd, clientip.c_str(), clientport);
    //             break;
    //         }
    //     }
    // }

    ~TcpServer()
    {
        if (listensock_ > 0)
            close(listensock_);
    }

private:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

Main.cc

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

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


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

    lg.Enable(Classfile); //调整日志打印到文件中

    uint16_t port = std::stoi(argv[1]);

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


    return 0;
}

Daemon.hpp

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

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

void Daemon(const std::string& cwd = "")
{
    //忽略其它信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    //创建子进程,然后终止父进程,子进程会拷贝父进程
    //资源,例如文件描述符,页表,地址空间,代码等,
    //然后由子进程继续向后执行代码
    if(fork() > 0)
        exit(0);

    //创建session会话,子进程成为服务器,然后守护进程化
    setsid();

    //更改当前子进程代表的服务器进程的工作目录
    if(!cwd.empty())
        chdir(cwd.c_str());

    //打开 /dev/null 文件
    int fd = open(nullfile.c_str(), O_WRONLY);

    //重定向 标准输入,标准输出,标准错误
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

Init.hpp

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

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

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }

        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            if(Split(line, &part1, &part2))
                dict.insert({ part1, part2 });
        }

        in.close();
    }

    bool Split(const std::string line, std::string* part1, std::string* part2)
    {
        size_t pos = line.find(sep);
        if(pos == std::string::npos)
            return false;

        *part1 = line.substr(0, pos);
        *part2 = line.substr(pos + 1);

        return true;
    }    

    std::string Translation(const std::string& key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end())
            return "unknow";

        return iter->second;
    }

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

Log.hpp

#pragma once

#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.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;
    }

    ~Log()
    {}

    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 "Fata";
            default:
                return "";
        }
    }

    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[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }

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

        printOneFile(filename, logtxt);
    }


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

Log lg;

makefile

all:tcpserver tcpclient

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

.PHONT:clean
clean:
	rm -f tcpserver tcpclient

Task.hpp

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


extern Log lg;
Init init;


class Task
{
public:
    Task()
    {}

    Task(int sockfd, const std::string& clientip, const uint16_t& clientport)
        : sockfd_(sockfd)
        , clientip_(clientip)
        , clientport_(clientport)
    {}

    void operator()()
    {
        char inbuffer[4096];
        ssize_t n = read(sockfd_, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "client say# " << inbuffer << std::endl;
            
            std::string echo_string = init.Translation(inbuffer);

            // std::string echo_string = "tcpserver echo# ";
            // echo_string += inbuffer;

            n = write(sockfd_, echo_string.c_str(), echo_string.size());                
            if(n < 0)
                lg(Info, "write err, errno: %d, errstr: %s", errno, strerror(errno));
        }
        else if(n == 0)
        {
            lg(Info, "%s:%d quit, server colse sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
        }
        else
        {
            //异常
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", 
                sockfd_, clientip_.c_str(), clientport_);
        }

        close(sockfd_);
    }

    ~Task()
    {}

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

TcpClient.cc

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

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

// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    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);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    while(true)
    {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd < 0)
        {
            std::cerr << "socket create err" << std::endl;
            return 1;
        }

        bool isreconnect = false;
        int cnt = 5;
        
        do
        {
            int n = connect(sockfd, (struct sockaddr*)&server, len);
            if(n < 0)
            {
                //此时连接失败,那么进行断线自动重连
                isreconnect = true;
                cnt--;
                sleep(2);
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
            }
            else  
                break;
        }while(cnt && isreconnect);

        if(cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            return 2;
        }

        //客户端发起connect请求的时候,操作系统会自动进行端口号的随机bind绑定
        std::string message;
        char inbuffer[4096];

        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if(n < 0)
            std::cerr << "write err" << std::endl;

        n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }

        close(sockfd);
    }

    // std::string message;
    // char inbuffer[4096];
    //
    // while(true)
    // {
    //     std::cout << "Please Enter# ";
    //     std::getline(std::cin, message);

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

    //     n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    //     if(n > 0)
    //     {
    //         inbuffer[n] = 0;
    //         std::cout << inbuffer << std::endl;
    //     }
    //     else
    //     {
    //         break;
    //     }
    // }
    //
    // close(sockfd);


    return 0;
}

ThreadPool.hpp

#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <pthread.h>


static int defaultnum = 5;


struct ThreadInfo
{
    pthread_t tid;
    std::string threadname;
};

template<class T>
class ThreadPool
{
private:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    void WakeUp()
    {
        pthread_cond_signal(&cond_);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }

    std::string GetThreadName(pthread_t tid)
    {
        for(auto& ti : threads_)
        {
            if(ti.tid == tid)
                return ti.threadname;
        }

        return "None";
    }

    T Pop()
    {
        T out = tasks_.front();
        tasks_.pop();

        return out;
    }

    static void* HandlerTask(void* args)
    {
        ThreadPool* tp = static_cast<ThreadPool*>(args);
        std::string name = tp->GetThreadName(pthread_self());

        while(true)
        {
            tp->Lock();

            while(tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }

            T t = tp->Pop();

            tp->Unlock();

            t();
            // std::cout << name << " run" << ", result: " << t.GetResult() << std::endl;
        }


        return nullptr;
    }

public:
    void Start()
    {
        int n = threads_.size();
        for(int i = 0; i < n; i++)
        {
            threads_[i].threadname = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, (void*)this);
        }
    }

    void Push(const T& in)
    {
        Lock();

        tasks_.push(in);
        WakeUp();

        Unlock();
    }

    static ThreadPool<T>* GetInstance()
    {
        if(tp_ == nullptr)
        {
            pthread_mutex_lock(&lock_);
            if(tp_ == nullptr)
            {
                std::cout << "log: singleton create done first" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defaultnum):threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    ThreadPool(const ThreadPool<T>& ) = delete;
    const ThreadPool<T>& operator=(const ThreadPool<T>& ) = delete;

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    std::queue<T> tasks_;
    std::vector<ThreadInfo> threads_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    
    static ThreadPool<T>* tp_;
    static pthread_mutex_t lock_;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

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

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐