最新资讯

  • 【linux】网络套接字编程(七)后台进程,守护进程,TCP服务器与客户端实现——守护进程版,setsid,简单理解TCP的三次握手和四次挥手,TCP的通信是全双工的

【linux】网络套接字编程(七)后台进程,守护进程,TCP服务器与客户端实现——守护进程版,setsid,简单理解TCP的三次握手和四次挥手,TCP的通信是全双工的

2026-01-30 19:13:20 栏目:最新资讯 3 阅读

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


目录

    • 前言
    • 一、后台进程
    • 二、后台进程的操作
    • 三、linux的进程间关系
    • 四、守护进程
    • 五、TCP服务器与客户端实现——守护进程版
      • Daemon.hpp
        • setsid
      • Main.cc
      • TcpServer.hpp
        • 测试
    • 六、补充知识
      • 简单理解TCP的三次握手和四次挥手
      • TCP的通信是全双工的
      • 如何理解连接
    • 七、源代码
      • TcpServer.hpp
      • Main.cc
      • Daemon.hpp
      • Init.hpp
      • Log.hpp
      • makefile
      • Task.hpp
      • TcpClient.cc
      • ThreadPool.hpp
    • 总结


前言

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


一、后台进程

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

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 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include "TcpServer.hpp"


void Usage(const std::string str)
{
    std::cout << "
	Usage: " << str << " port[1024+]
" << 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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#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(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 
#include 
#include "TcpServer.hpp"

void Usage(const std::string str)
{
    std::cout << "
	Usage: " << str << " port[1024+]
" << 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 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void Usage(const std::string& str)
{
    std::cout << "
	Usage: " << 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 
#include 
#include 
#include 
#include 


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;

总结

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

本文地址:https://www.yitenyun.com/3513.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 香港站群服务器 多IP服务器 香港站群 站群服务器 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #运维 #进程控制 #开发语言 #云原生 #iventoy #VmWare #OpenEuler #fastapi #html #css #docker #后端 #数据库 #Trae #IDE #AI 原生集成开发环境 #Trae AI #低代码 #爬虫 #音视频 #cpolar #MobaXterm #ubuntu #Conda # 私有索引 # 包管理 #物联网 #websocket #内网穿透 #网络 #人工智能 #node.js #开源 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #学习 #算法 #大数据 #kylin #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #vscode #mobaxterm #深度学习 #计算机视觉 #android #腾讯云 #c# #架构 #安全 #nginx #tcp/ip #面试 #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #windows #web安全 #udp #c++ #c语言 #网络协议 #qt #java #jar #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #claude #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #华为 #ModelEngine #mvp #个人开发 #设计模式 #金融 #大模型 #mcp #金融投资Agent #Agent #github #git #n8n #本地部署 #我的世界 #hadoop #hbase #hive #zookeeper #spark #kafka #flink #vue.js #前端 #ssh #C++ #云计算 #我的世界服务器搭建 #minecraft #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #libosinfo #openlayers #bmap #tile #server #vue #todesk #ide #unity #游戏引擎 #jenkins #需求分析 #scala #测试用例 #测试工具 #压力测试 #微信小程序 #小程序 #微信 #健身房预约系统 #健身房管理系统 #健身管理系统 #性能优化 #缓存 #MCP #MCP服务器 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #apache #http #cpp #项目 #高并发 #spring boot #部署 #NPU #CANN #阿里云 #screen 命令 #stm32 #macos #JumpServer #堡垒机 #AI编程 #振镜 #振镜焊接 #pycharm #单元测试 #集成测试 #编辑器 #DisM++ # GLM-4.6V # 系统维护 #京东云 #gpu算力 #SRS #流媒体 #直播 #守护进程 #复用 #screen #unity3d #游戏 #服务器框架 #Fantasy #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #YOLOFuse # Base64编码 # 多模态检测 #麒麟OS #mamba #oracle #Android #Bluedroid #智能手机 #epoll #科技 #自然语言处理 #神经网络 #langchain #大模型开发 #程序员 #AI #centos #大模型部署 #mindie #大模型推理 #mysql #Dify #ARM架构 #鲲鹏 #EMC存储 #存储维护 #NetApp存储 #自动化 #maven #gitlab #课程设计 #SSH反向隧道 # Miniconda # Jupyter远程访问 #chatgpt #codex #yum #windows11 #microsoft #系统修复 #三维 #3D #三维重建 #信令服务器 #Janus #MediaSoup #单片机 #嵌入式硬件 #PTP_1588 #gPTP #微服务 #NAS #Termux #Samba #Linux #p2p #Windows #webrtc #idm #万悟 #联通元景 #智能体 #镜像 #mcu #react.js #YOLO # GPU租赁 # 自建服务器 #SSE # AI翻译机 # 实时翻译 #php #asp.net #sqlserver #debian #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #deepseek #risc-v #1024程序员节 #flask #SSH公钥认证 # PyTorch # 安全加固 #GPU服务器 #8U #硬件架构 #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #fiddler #vnstat #监控 #搜索引擎 #智能路由器 #5G #运维开发 #pytorch #C2000 #TI #实时控制MCU #AI服务器电源 #黑群晖 #虚拟机 #无U盘 #纯小白 #银河麒麟 #系统升级 #信创 #国产化 #东方仙盟 #蓝湖 #Axure原型发布 #管道Pipe #system V #jvm #SAP #ebs #metaerp #oracle ebs #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #910B #昇腾 #AIGC #ida #ai #DeepSeek #蓝耘智算 #Anaconda配置云虚拟环境 #openHiTLS #TLCP #DTLCP #密码学 #商用密码算法 #tomcat #svn #可信计算技术 #RAID #RAID技术 #磁盘 #存储 #华为云 #测评 #CCE #Dify-LLM #Flexus #Nacos #web #cursor #elasticsearch #进程 #操作系统 #进程创建与终止 #shell #SPA #单页应用 #django #web3.py #ollama #llm #RustDesk #IndexTTS 2.0 #本地化部署 #信息与通信 #信号处理 #tcpdump #swagger #毕业设计 #车辆排放 #ms-swift # 大模型 # 模型训练 #PyTorch # Triton # 高并发部署 #transformer #javascript #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #工具集 #sqlite #java-ee #电气工程 #C# #PLC #sql #golang #rdp #ssl #经验分享 #创业创新 #业界资讯 #计算机网络 #laravel #milvus #VMware #简单数论 #埃氏筛法 #TCP #客户端 #嵌入式 #DIY机器人工房 #prometheus #grafana #redis #CosyVoice3 # 语音合成 #机器学习 #eBPF #Puppet # IndexTTS2 # TTS #高级IO #select #语音识别 #说话人验证 #声纹识别 #CAM++ #C语言 #mongodb #x86_64 #数字人系统 #ansible #其他 #rtsp #转发 #unix #rust #网络安全 #fpga开发 #CVE-2025-61686 #漏洞 #路径遍历高危漏洞 #gitea #分布式 #SQL注入主机 #json #制造 #ping通服务器 #读不了内网数据库 #bug菌问答团队 #GPU #AutoDL ##租显卡 #进程等待 #wait #waitpid #大模型教程 #AI大模型 #大模型学习 #结构体 #flutter #数码相机 #ThingsBoard MCP #LangFlow # 智能运维 # 性能瓶颈分析 #数据分析 #推荐算法 #devops #戴尔服务器 #戴尔730 #装系统 #web服务器 #渗透测试 #黑客技术 #计算机 #文件上传漏洞 #bug #HeyGem # 服务器IP访问 # 端口映射 #CTF #A2A #GenAI #遛狗 #聊天小程序 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #arm64 # 一锤定音 # 大模型微调 #adb #nodejs #NFC #智能公交 #服务器计费 #数据挖掘 #FP-增长 #数据安全 #注入漏洞 #tdengine #时序数据库 #涛思数据 #esb接口 #走处理类报异常 #vllm #ffmpeg #CUDA #Triton #交互 #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #语言模型 #论文笔记 #dynadot #域名 #intellij-idea #idea #intellij idea #serverless #dify #arm开发 #昇腾300I DUO #PowerBI #企业 #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #Streamlit #Qwen #AI聊天机器人 #ui #opencv #Qwen3-14B # 大模型部署 # 私有化AI #AI 推理 #NV #文心一言 #AI智能体 #vp9 #攻防演练 #Java web #红队 #leetcode #iBMC #UltraISO #支付 #数据结构 #远程桌面 #远程控制 #SSH跳板机 # Python3.11 #LVDS #高速ADC #DDR #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #API限流 # 频率限制 # 令牌桶算法 #驱动开发 #chrome #处理器 #bash #screen命令 #notepad++ #mariadb #排序算法 #Gunicorn #WSGI #Flask #并发模型 #容器化 #Python #性能调优 #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #RAG #LLM #chat #机器人 #llama #门禁 #梯控 #智能一卡通 #门禁一卡通 #消费一卡通 #智能梯控 #一卡通 #源代码管理 #超时设置 #客户端/服务器 #网络编程 # 目标检测 #ai编程 #aws #YOLO26 #目标检测 #电脑 #Miniconda #SSH #远程开发 #react native #web server #请求处理流程 #国产化OS #springboot #知识库 #交通物流 #vivado license #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #html5 #计算几何 #斜率 #方向归一化 #叉积 # 批量管理 #个人博客 #ASR #SenseVoice #星图GPU #中间件 #MQTT协议 #winscp #ONLYOFFICE #MCP 服务器 #uni-app #H5 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #rocketmq #selenium #证书 #scrapy #fabric #postgresql #嵌入式编译 #ccache #distcc #CPU # 双因素认证 # TensorFlow #毕设 #服务器繁忙 #蓝牙 #LE Audio #BAP #rustdesk #连接数据库报错 #链表 #puppeteer #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #mybatis #安全威胁分析 #源码 #闲置物品交易系统 #仙盟创梦IDE #硬件工程 #智能家居 #动态规划 #负载均衡 #pyqt #xlwings #Excel #DNS #Spring AI #STDIO传输 #SSE传输 #WebMVC #WebFlux #bootstrap #nfs #iscsi #企业微信 #C #spring cloud #spring #prompt #YOLOv8 # Docker镜像 #文件管理 #文件服务器 #visual studio code #Java #凤希AI伴侣 #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #树莓派4b安装系统 #LangGraph #CLI #JavaScript #langgraph.json #paddleocr #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #pve #wsl #jdk #排序 #ddos #系统架构 #KMS激活 #大语言模型 #数据仓库 #Ansible #Playbook #AI服务器 #openEuler #欧拉 #numpy #CSDN #LobeChat #vLLM #GPU加速 #openresty #lua #wordpress #雨云 #海外服务器安装宝塔面板 #翻译 #开源工具 #SSH保活 #人脸识别sdk #视频编解码 #人脸识别 #elk #rabbitmq #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus #esp32 arduino #挖漏洞 #日志分析 #决策树 #Zabbix #语音合成 #HistoryServer #Spark #YARN #jobhistory #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #FASTMCP #DooTask #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #Clawdbot #ComfyUI # 推理服务器 #防毒面罩 #防尘面罩 #n8n解惑 #编程助手 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #.net #net core #kestrel #web-server #asp.net-core # GLM-4.6V-Flash-WEB # 显卡驱动备份 #Socket #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #模拟退火算法 #集成学习 #计算机毕业设计 #程序定制 #毕设代做 #课设 #Hadoop #vuejs #postman #产品运营 #内存接口 # 澜起科技 # 服务器主板 #大模型入门 #homelab #Lattepanda #Jellyfin #Plex #Emby #Kodi #yolov12 #研究生life #开关电源 #热敏电阻 #PTC热敏电阻 #能源 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #eureka #云服务器 #个人电脑 #KMS 激活 #Harbor #wireshark #广播 #组播 #并发服务器 #nacos #银河麒麟aarch64 #MC #MC群组服务器 #uvicorn #uvloop #asgi #event # 服务器迁移 # 回滚方案 #https #zabbix #CS2 #debian13 #BoringSSL #企业存储 #RustFS #对象存储 #高可用 #es安装 #Docker #云计算运维 #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #asp.net上传大文件 #excel #gpu #nvcc #cuda #nvidia #漏洞挖掘 #gpt #TensorRT # 推理优化 #SSH别名 # CUDA #log4j #Jetty # CosyVoice3 # 嵌入式服务器 #模块 #STUN #turn #ICE #信创国产化 #达梦数据库 #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #群晖 #音乐 # ARM服务器 # 鲲鹏 #IntelliJ IDEA #Spring Boot #FTP服务器 #neo4j #NoSQL #SQL #http头信息 #Llama-Factory # 大模型推理 #Coturn #TURN #ci/cd #k8s #银河麒麟服务器系统 # 代理转发 # 跳板机 #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #echarts #harmonyos #鸿蒙PC #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL # 服务器IP # 端口7860 #pdf #建筑缺陷 #红外 #数据集 #TCP服务器 #开发实战 #SMARC #ARM #全文检索 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman # 公钥认证 #Reactor #Kylin-Server #国产操作系统 #服务器安装 #Android16 #音频性能实战 #音频进阶 #短剧 #短剧小程序 #短剧系统 #微剧 #空间计算 #原型模式 #hibernate #nosql #VibeVoice # 云服务器 #无人机 #junit #新人首发 #汽车 #VMWare Tool #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #clickhouse #代理 #数据访问 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #IO #插件 #开源软件 #DHCP #网络安全大赛 #C++ UA Server #SDK #跨平台开发 #信息可视化 #agent #ai大模型 #r-tree #FHSS #eclipse #servlet #CNAS #CMA #程序文件 #word #SSH复用 # 远程开发 #Deepoc #具身模型 #开发板 #未来 #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #自动化运维 #串口服务器 #Modbus #MOXA #GATT服务器 #蓝牙低功耗 #googlecloud #lucene #DAG #服务器解析漏洞 #云服务器选购 #Saas #线程 #散列表 #哈希算法 #机器视觉 #6D位姿 #UOS #海光K100 #统信 #outlook #错误代码2603 #无网络连接 #2603 #dba #mssql #wpf #实时检测 #卷积神经网络 #密码 #firefox #safari #LoRA # RTX 3090 # lora-scripts #具身智能 #SSH密钥 # ControlMaster #ETL管道 #向量存储 #数据预处理 #DocumentReader #硬件 #Fun-ASR # 语音识别 # WebUI #HarmonyOS APP #windbg分析蓝屏教程 #jupyter #AI电商客服 #le audio #低功耗音频 #通信 #连接 #spring ai #oauth2 #数据可视化 #网路编程 #百万并发 #nmodbus4类库使用教程 #docker-compose #目标跟踪 #rtmp #IFix # 高温监控 #c++20 # 远程连接 #fs7TF #Buck #NVIDIA #算力 #交错并联 #DGX #ROS # 局域网访问 # 批量处理 #内存治理 #cosmic # 树莓派 # ARM架构 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #gerrit #opc ua #opc #npu # 环境迁移 #大剑师 #nodejs面试题 #ServBay #matplotlib #安全架构 #ESP32 # OTA升级 # 黄山派 #内网 # 网络延迟 #指针 #anaconda #虚拟环境 #ranger #MySQL8.0 #GB28181 #SIP信令 #SpringBoot #视频监控 #远程软件 #WT-2026-0001 #QVD-2026-4572 #smartermail #游戏机 # GLM-TTS # 数据安全 #xshell #host key #UDP的API使用 #TTS私有化 # IndexTTS # 音色克隆 #代理服务器 #rsync # 数据同步 #ip #Modbus-TCP #blender #设计师 #图像处理 #游戏美术 #技术美术 # Connection refused #系统管理 #服务 #teamviewer #视频 #微PE # GLM # 服务连通性 #azure #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #ceph #ambari #arm #多线程 #claudeCode #content7 #Socket网络编程 #跳槽 #工作 #挖矿 #Linux病毒 #sql注入 #网安应急响应 #odoo #appche #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #Ubuntu #muduo #TcpServer #accept #高并发服务器 #鸿蒙 # 高并发 #版本控制 #Git入门 #开发工具 #代码托管 #SSH跳转 #TTS #go # GPU集群 #Gateway #认证服务器集成详解 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #ftp #sftp #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #框架搭建 #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #状态模式 #AI-native # 轻量化镜像 # 边缘计算 #Tokio #华为od #华为机试 #OpenHarmony #cpu #套接字 #I/O多路复用 #字节序 #weston #x11 #x11显示服务器 #研发管理 #禅道 #禅道云端部署 #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 # 批量部署 #samba #RSO #机器人操作系统 #glibc #媒体 #opc模拟服务器 #远程连接 #AI部署 # ms-swift #PN 结 #后端框架 #ArkUI #ArkTS #鸿蒙开发 #服务器线程 # SSL通信 # 动态结构体 #RWK35xx #语音流 #实时传输 #node #超算中心 #PBS #lsf #政务 #报表制作 #职场 #用数据讲故事 #深度优先 #DFS #语音生成 #AI写作 #前端框架 #个人助理 #数字员工 #JNI #pxe #lvs # 数字人系统 # 远程部署 #adobe # TURN # NAT穿透 #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #数据迁移 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #可再生能源 #绿色算力 #风电 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #express #cherry studio #Node.js # child_process #free #vmstat #sar #KMS #slmgr #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 # 自动化运维 #r语言 #spine #若依 #TRO #TRO侵权 #TRO和解 #运维工具 #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #IPv6 #POC #问答 #交付 #网络攻击模型 #Discord机器人 #云部署 #程序那些事 #AI应用编程 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #3d #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #系统安全 #Minecraft #Minecraft服务器 #PaperMC #我的世界服务器 #ipmitool #BMC # 黑屏模式 # TTS服务器 #前端开发 #EN4FE #领域驱动 #自由表达演说平台 #演说 #程序员创富 #文件IO #输入输出流 #流程图 #论文阅读 #图论 #国产开源制品管理工具 #Hadess #一文上手 #jetty #蓝桥杯 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #okhttp #embedding #IndexTTS2 # 阿里云安骑士 # 木马查杀 #范式 #入侵 #日志排查 #kmeans #聚类 #Karalon #AI Test #人大金仓 #Kingbase #小艺 #搜索 #代理模式 #Spring AOP #程序人生 #健康医疗 #scanf #printf #getchar #putchar #cin #cout #AI应用 #CMake #Make #C/C++ #图像识别 #多进程 #python技巧 #高考 #企业级存储 #网络设备 #iot #软件工程 #生信 #Smokeping #工程实践 #策略模式 #租显卡 #训练推理 #taro #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #wps #Linux多线程 #bigtop #hdp #hue #kerberos #pencil #pencil.dev #设计 #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #zotero #WebDAV #同步失败 #轻量化 #低配服务器 #Beidou #北斗 #SSR #Anything-LLM #IDC服务器 #私有化部署 #麒麟 #V11 #kylinos #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #raid #raid阵列 #API #Langchain-Chatchat # 国产化服务器 # 信创 #软件 #本地生活 #电商系统 #商城 #poll #PyCharm # 远程调试 # YOLOFuse #simulink #matlab #journalctl #aiohttp #asyncio #异步 #信息安全 #信息收集 #Syslog #系统日志 #日志监控 #生产服务器问题查询 #日志过滤 #Autodl私有云 #深度服务器配置 # 水冷服务器 # 风冷服务器 #.netcore #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #webpack #儿童AI #图像生成 #pjsip # 模型微调 #传统行业 #AI生成 # outputs目录 # 自动化 #攻击溯源 #编程 #stl #漏洞修复 #IIS Crypto #warp #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 # AI部署 #everything #材料工程 #智能电视 #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #sglang #Go并发 #高并发架构 #Goroutine #系统设计 #交换机 #三层交换机 #SSH Agent Forwarding # 容器化 #高斯溅射 #UEFI #BIOS #Legacy BIOS #云开发 #性能 #优化 #RAM #AI智能棋盘 #Rock Pi S #边缘计算 #c++高并发 # 权限修复 #uip # HiChatBox # 离线AI #X11转发 #SMTP # 内容安全 # Qwen3Guard #MinIO服务器启动与配置详解 #改行学it #平板 #零售 #智能硬件 #vncdotool #链接VNC服务器 #如何隐藏光标 #gateway #Comate #算力建设 #b树 #Proxmox VE #虚拟化 #声源定位 #MUSIC #memory mcp #Cursor #smtp #smtp服务器 #PHP # 远程访问 #tensorflow #memcache #飞牛nas #fnos #ansys #ansys问题解决办法 #分布式数据库 #集中式数据库 #业务需求 #选型误 #HarmonyOS #雨云服务器 #教程 #MCSM面板 # 串口服务器 # NPort5630 # 服务器配置 # GPU #Python办公自动化 #Python办公 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #量子计算 #copilot #硬盘克隆 #DiskGenius # 键鼠锁定 #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #反向代理 #参数估计 #矩估计 #概率论 #powerbi #系统安装 #MinIO #gmssh #宝塔 #1panel #Exchange #sentinel #scikit-learn #随机森林 #静脉曲张 #腿部健康 #运动 #AI Agent #开发者工具 #边缘AI # Kontron # SMARC-sAMX8 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #计算机外设 #ET模式 #非阻塞 #remote-ssh #OpenAI #故障 #多模态 #微调 #超参 #LLamafactory #产品经理 #就业 #vps #docker安装seata # IndexTTS 2.0 #全链路优化 #实战教程 #database #AI论文写作工具 #学术写作辅助 #论文创作效率提升 #AI写论文实测 #AB包