利用线程池多线程并发实现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上输入指令去执行。客户端也能够很好的接收到数据,这就符合现实中服务端的逻辑。


目录
相关文章
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
135 0
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
2月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
243 67
|
3月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
232 5
|
6月前
|
SQL 监控 网络协议
YashanDB进程线程体系
YashanDB进程线程体系
|
7月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
225 20
|
7月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
7月前
|
数据采集 Java 数据处理
Python实用技巧:轻松驾驭多线程与多进程,加速任务执行
在Python编程中,多线程和多进程是提升程序效率的关键工具。多线程适用于I/O密集型任务,如文件读写、网络请求;多进程则适合CPU密集型任务,如科学计算、图像处理。本文详细介绍这两种并发编程方式的基本用法及应用场景,并通过实例代码展示如何使用threading、multiprocessing模块及线程池、进程池来优化程序性能。结合实际案例,帮助读者掌握并发编程技巧,提高程序执行速度和资源利用率。
337 0
|
4月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
187 0