利用线程池多线程并发实现TCP两端通信交互,并将服务端设为守护进程(二)

简介: 利用线程池多线程并发实现TCP两端通信交互,并将服务端设为守护进程(二)

客户端

客户端不需要显示的绑定端口号,而是由操作系统随机去绑定。TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept。TCP的客户端只需要向服务端发起链接请求

Client.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"
using namespace std;
class Client
{
public:
    Client(const string &serverip, const uint16_t &port)
        : _serverip(serverip), _port(port), _sock(-1)
    {
    }
    void Init()
    {
        // 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            LogMessage(FATAL, "create socket error");
            exit(1);
        }
        // TCP的客户端也不需要显示绑定端口,让操作系统随机绑定
        // TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept
        // TCP的客户端只需要向服务端发起链接请求
    }
    void start()
    {
        // 向服务端发起链接请求
        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(_serverip.c_str());
        if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
            LogMessage(ERROR, "connect socket error");
        // 和服务端通信
        else
        {
            string line;
            while (1)
            {
                cout << "Please cin: " << endl;
                getline(cin, line);
                // 向服务端写
                write(_sock, line.c_str(), line.size());
                // 读服务端返回来的数据
                char buff[1024];
                int n = read(_sock, buff, sizeof(buff) - 1);
                if (n > 0)
                {
                    buff[n] = 0;
                    cout << "接收到的消息为:" << buff << endl;
                }
                else
                    break;
            }
        }
    }
    ~Client()
    {
        if(_sock >= 0)
            close(_sock);
    }
private:
    int _sock;
    string _serverip;
    uint16_t _port;
};

Client.cc

#include "Client.hpp"
#include <memory>
// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}
int main(int argc, char* argv[])
{
    // 再运行客户端时,输入的指令需要包括主机ip和端口号
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t port = atoi(argv[2]);
    unique_ptr<Client> client(new Client(serverip, port));
    client->Init();
    client->start();
    return 0;
}

服务端

那么对于服务端而言,必须要显式的去绑定端口号。则创建的套接字并不是负责通信的。创建好套接字和绑定完网络信息后,需要设置创建的套接字为监听状态。和UDP一样,服务端是不能指定IP的.

还需要注意的是:因为封装的线程池是单例模式,所以不需要创建对象,直接调用静态对象去调用类方法即可

步骤可分为:

  1. 创建监听套接字
  2. 绑定网络信息
  3. 设置套接字为监听状态
  4. 获取链接,得到通信的套接字
  5. 通信
  6. 关闭不需要的套接字

Server.hpp

#pragma once
#include "Task.hpp"
#include "ThreadPool.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
class Server
{
public:
    Server(const uint16_t &port = 8000)
        : _port(port)
    {
    }
    void Init()
    {
        // 创建负责监听的套接字 面向字节流
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            LogMessage(FATAL, "create socket error!");
            exit(1);
        }
        LogMessage(NORMAL, "create socket %d success!", _listenSock);
        // 绑定网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listenSock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LogMessage(FATAL, "bind socket error!");
            exit(3);
        }
        LogMessage(NORMAL, "bind socket success!");
        // 设置socket为监听状态
        if (listen(_listenSock, 5) < 0)
        {
            LogMessage(FATAL, "listen socket error!");
            exit(4);
        }
        LogMessage(NORMAL, "listen socket success!");
    }
    void start()
    {
        while (1)
        {
            // 因为线程池时单例模式,所以直接调用初始化
            ThreadPool<Task>::getInstance()->run();
            LogMessage(NORMAL, "Thread init success");
            // server获取建立新连接
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t len = sizeof(peer);
            // 创建通信的套接字
            // accept的返回值才是真正用于通信的套接字
            _sock = accept(_listenSock, (struct sockaddr *)&peer, &len);
            if (_sock < 0)
            {
                // 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已
                LogMessage(ERROR, "accept socket error, next");
                continue;
            }
            LogMessage(NORMAL, "accept socket %d success", _sock);
            cout << "sock: " << _sock << endl;
            // 往线程池的任务队列里插入任务
            ThreadPool<Task>::getInstance()->push(Task(_sock, ServerIO));
        }
    }
private:
    int _listenSock; // 负责监听的套接字
    int _sock;       // 通信的套接字
    uint16_t _port;  // 端口号
};

Server.cc

#include "Server.hpp"
#include "daemon.hpp"
#include <memory>
// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}
int main(int argc, char* argv[])
{
    // 启动服务端不需要指定IP
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<Server> server(new Server(port));
    server->Init();
    server->start();
    return 0;
}

实现效果


da72ba49101fc0f538cbbc82f9be1eec.png

可以看到多个客户端同时访问也没有问题,并且所对应的套接字也就是文件描述符也不一样。

守护进程

守护进程是一种特殊的孤儿进程,其运行于后台,生存期较长并且独立与终端周期性的执行任务或者等待处理任务

进程分为前台运行和后台运行,每一个进程都会属于一个会话组里。每一个会话组都有且只有能一个前台进程。像上述的服务端,当运行服务端时,操作系统会将其分到含有bash的会话组内,并且将服务端置为前台任务进程,因此服务端运行时bash把放置后台这也就是为什么用户不能再bash继续输入命令的原因。

每一个会话组都会有一个组长,一般而言在bash中输入命令执行的进程都会分到bash的会话组内,这个会话组的组长即为bash。可以通过查看进程的SID确认进程的会话组


3084002446b47093da3f5d67a6d6a594.png

可以看到上述图片中运行了三个进程并置于后台,他们的SID也就是会话组都是一样的。那么如果将他们置于前台运行会发生什么呢

6c7fdbf4019feda2f4cfabc43ea79e07.png


可以看到,置于前台运行后,命令行输入什么都没有反应了。也就是说,此时的bash被自动的放到了后台运行,证实了一个会话组只能有一个前台进程


6086484df2f9f6795a08d3c64215490d.png


输入ctr + Z 之后前台的进程就会把切回后台,但是切回后台后进程是阻塞状态的,因此输入bg + 作业号就可让进程启动。

服务端守护进程化

那么很显然,在业务逻辑上服务端肯定是需要守护进程化的。因为服务端没有特殊情况是不会关闭的,需要一直运行。如果服务端是前台进程的话,那服务端运行时bash都不能用了,显然不符合。

这里要介绍一个接口:

#include <unistd.h>
pid_t setsid(void);

这个接口的作用是使调用的进程独立成为一个会话组并且为该组的组长。但是调用这个接口是有前置条件的:调用这个接口的进程不能为某个会话组的组长

守护进程化的步骤:

  1. 让调用进程忽略掉异常信号,因为其不受终端控制的
  2. 让调用进程不为组长
  3. 关闭或者重定向之前默认打开的文件,如0 1 2文件描述符
#pragma once
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
    // 1. 让调用进程忽略掉异常的信号
    signal(SIGPIPE, SIG_IGN);
    // 2. 让自己不是组长,setsid
    if (fork() > 0)
        exit(0);
    // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
    pid_t n = setsid();
    assert(n != -1);
    // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
    int fd = open(DEV, O_RDWR);
    if(fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }
}

接着只需要服务端在初始化完成后调用这个函数,将自己设为守护进程化即可

6b0e40f74684cf5a9bddbed27ec2478d.png

一起来看看效果:

414ea0886b448507a75aa4cec150ead4.png

可以看到服务端启动后并不会影响bash,仍然可以在bash上输入指令去执行。客户端也能够很好的接收到数据,这就符合现实中服务端的逻辑。


目录
相关文章
|
23天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
101 38
|
25天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
36 1
[Java]线程生命周期与线程通信
|
9天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
11天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
25 3
|
13天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
10天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
22 1
|
16天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
33 2
|
17天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
21天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
46 2
|
23天前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
60 4

相关实验场景

更多