【Linux】TCP的服务端 + 客户端

news2024/11/22 1:47:18

文章目录

  • 📖 前言
  • 1. 服务端基本结构
    • 1.1 类成员变量:
    • 1.2 头文件
    • 1.3 初始化:
      • 1.3 - 1 全双工与半双工
      • 1.3 - 2 inet_aton
      • 1.3 - 3 listen
  • 2. 服务端运行接口
    • 2.1 accept:
    • 2.2 服务接口:
  • 3. 客户端
    • 3.1 connect:
    • 3.2 客户端的实现:
  • 4. 提供服务
    • 4.1 单进程版本:
    • 4.2 多进程1.0版本:
    • 4.3 多进程1.1版本:
    • 4.4 多线程版本:
    • 4.5 线程池版本:
    • 4.6 执行客户端指令:

📖 前言

上一节,我们用了udp写了一个服务端和客户端之间通信的代码,只要函数了解认识到位,上手编写是很容易的。
本章我们开始编写tcp的服务端和客户端之前通信的代码,要认识一批新的接口,并将我们之前学习的系统知识加进来,做到融会贯通…


1. 服务端基本结构

对于TCP服务器和UDP服务器的初始化接口,确实有一些相似之处,但是它们在选择字节流进行初始化方面存在一些区别。

  • 首先,无论是TCP服务器还是UDP服务器,都需要进行套接字的创建、绑定和监听操作。这些初始化步骤是相同的。
  • 区别在于,TCP服务器使用字节流(byte stream) 进行数据传输,而UDP服务器使用数据报(datagram) 进行数据传输。
  • 对于UDP协议,任何人都可以向服务器发送数据报,而且不需要等待服务器响应。UDP协议是无连接的传输协议,数据报发送出去后就结束。
  • TCP协议是面向连接的传输协议,需要先建立连接才能进行数据传输,并且在连接建立、数据传输和断开连接的过程中需要互相响应。

1.1 类成员变量:

class Task
{
	// ....
	
private:
    int sock_;        // 给用户提供IO服务的sock
    uint16_t port_;   // client port
    std::string ip_;  // client ip
    callback_t func_; // 回调方法
};

1.2 头文件

因为每个源文件都要包好多相同的头文件,所以我们将要用到的头文件一并打包在一个头文件里:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"

#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5

#define BUFFER_SIZE 1024

一般涉及到struct sockaddr_in,都要包含这两个头文件:
在这里插入图片描述

1.3 初始化:

TCP是面向字节流的:

在这里插入图片描述

1.3 - 1 全双工与半双工

全双工(Full Duplex)和半双工(Half Duplex)是通信中两种不同的传输模式:

  • 全双工是指通信双方可以同时进行双向的数据传输。
    • 在全双工模式下,通信双方的发送和接收操作是独立进行的,彼此之间不会互相干扰。
    • 这种模式可以实现实时的双向通信,类似于我们平时打电话或进行视频通话时的交流方式。
  • 半双工是指通信双方在同一时间内只能进行单向的数据传输。
    • 在半双工模式下,通信双方轮流地进行发送和接收操作,不能同时进行。
    • 当一方发送数据时,另一方只能等待接收,反之亦然。
    • 这种模式类似于对讲机的使用方式,一方讲话时,另一方只能听取,无法即时回应。

套接字和管道:

  • 管道只能通过一个文件描述符读,一个文件描述符写,所以叫做单向管道。
  • 而在TCP中读写用的都是一个套接字fd,UDP在读写时用的也是一个套接字。
  • TCP/UDP都支持全双工。

1.3 - 2 inet_aton

int inet_aton(const char *cp, struct in_addr *inp);
  • 它的作用是将一个点分十进制的IP地址字符串(cp)转换为网络字节序的二进制数,并将结果存储在in_addr结构体(inp)中。
  • 因此,inet_aton函数的第一个参数是要转换的IP地址字符串,第二个参数是存储转换结果的结构体指针。
  • 函数的返回值是一个整数,表示转换是否成功。如果转换成功,返回值为非零;如果转换失败,返回值为零。

1.3 - 3 listen

listen函数用于将一个已经建立连接的套接字(通常是一个服务端的套接字)标记为被动模式,开始监听来自客户端的连接请求。

在这里插入图片描述

它接受两个参数:sockfd是要设置为被动模式的套接字文件描述符,backlog是指定等待连接队列的最大长度。

accept第一个参数监听到了之后,然后返回一个值之后,再继续去监听。

listen的第二个参数我们以后再讲…

监听socket,为何要监听呢?

  • 因为udp是无连接的(通信可以,但是不用建立连接,直接发消息就可以了),而tcp是面向连接的!
  • 面向就是在做任何事之前要先干什么这就是面向的意思,面向连接就是在做其他工作之前先把连接建立好。
  • 不管有没有客户端连接,得让服务器将来任何时候被别人连接,所以要将套接字设置成监听状态。

下面的初始化就和之前udp的初始化大差不差了…

void init()
{
    // 1. 创建socket
    listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
    if (listenSock_ < 0)
    {
        logMessage(FATAL, "socket: %s", strerror(errno));
        exit(SOCKET_ERR);
    }
    logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);

    // 2. bind绑定
    // 2.1 填充服务器信息
    struct sockaddr_in local; // 用户栈
    memset(&local, 0, sizeof local);
    local.sin_family = PF_INET;
    local.sin_port = htons(port_);
    ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
    // 2.2 本地socket信息,写入sock_对应的内核区域
    if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
    {
        logMessage(FATAL, "bind: %s", strerror(errno));
        exit(BIND_ERR);
    }
    logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);

    // 3. 监听socket,为何要监听呢?tcp是面向连接的!
    if (listen(listenSock_, 5 /*后面再说*/) < 0)
    {
        logMessage(FATAL, "listen: %s", strerror(errno));
        exit(LISTEN_ERR);
    }
    logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
    // 走到这就意味着允许别人来连接你了

    // 4. 加载线程池
    // tp_ = ThreadPool<Task>::getInstance();
}

2. 服务端运行接口

2.1 accept:

accept函数用于接受客户端连接的请求。它被用于一个已经处于被动监听状态的套接字(通常是服务端的套接字)。
当有新的客户端连接请求到达时,accept函数将会返回一个新的套接字文件描述符,此后服务端就可以通过这个新的套接字与客户端进行通信。

在这里插入图片描述

  • sockfd表示要接受连接的套接字文件描述符。
  • addr指向保存客户端地址信息的结构体指针(可以传入NULL)。
  • addrlen表示addr结构体的长度。

后面两个参数和recvfrom后两个参数的含义一模一样,是想拿到是哪个客户端连接的。

  • 第一个参数sockfd是套接字描述符: 用来获取新连接的套接字,叫做监听socket
  • 这个监听套接字负责监听指定的网络地址和端口,等待客户端的连接请求。
  • 返回值是一个套接字描述符: 主要是为用户提供网络服务的socket,主要是进行IO
  • 当有客户端发起连接请求时,accept()函数就会返回一个新的套接字。
  • 这个新的套接字与客户端的套接字建立连接,用于后续的数据传输。
    在这里插入图片描述

accept函数的阻塞:

  • accept函数是在网络编程中用于接受客户端连接的函数。
  • 当调用accept函数时,如果有客户端连接请求到达,它会立即返回一个新的套接字来与该客户端进行通信。
  • 如果没有客户端连接请求到达,accept函数将会阻塞,即一直等待直到有新的连接请求到达为止。

在阻塞状态下,程序会停止执行后续代码,直到有新的连接请求到达或者发生错误。因此,可以将accept函数放在一个循环中,反复接受多个客户端连接。需要注意的是,在某些情况下,可以通过设置套接字为非阻塞模式来避免accept函数的阻塞,这样程序可以继续执行其他操作。

void loop()
{
    tp_->start();
    logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());

    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 4. 获取连接, accept 的返回值是一个新的socket fd ??
        // 4.1 listenSock_: 监听 && 获取新的链接-> sock
        // 4.2 serviceSock: 给用户提供新的socket服务
        int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
            continue;
        }

        // 4.1 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);

        logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                   strerror(errno), peerIp.c_str(), peerPort, serviceSock);
                   
        // 提供服务....
    }
}

2.2 服务接口:

提供的服务,将小写转成大写:

// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    char inbuffer[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串
        if (s > 0)
        {
            // read success
            inbuffer[s] = '\0';
            if (strcasecmp(inbuffer, "quit") == 0)
            {
                logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
                break;
            }
            logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);

            // 可以进行大小写转化了
            for (int i = 0; i < s; i++)
            {
                if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                    inbuffer[i] = toupper(inbuffer[i]);
            }
            logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);

            write(sock, inbuffer, strlen(inbuffer));
        }
        else if (s == 0)
        {
            // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
            // s == 0: 代表对方关闭,client 退出
            logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }

    // 只要走到这里,一定是client退出了,服务到此结束
    close(sock); 
    logMessage(DEBUG, "server close %d done", sock);
}

recvfromsendto是专门针对udp发送用户数据报的,它是一 个固定大小的报文,在那里它是专函数专用的,专门为udp提供的。而tcp就通用的多,因为tcp是流式服务,我们这里直接可以当做是处理文件的方式来进行读写。

如果一个进程对应的文件fd,打开了没有被归还,这种现象叫做文件描述符泄漏!

  • 如果不关,来一个客户端打开一个文件描述符,会导致该服务端进程可用文件描述符越来越少。
  • 文件描述符表是有上限的,时间一久,会导致服务器无法获取新连接,申请文件描述符时发现所有文件描述符都被占用了。
  • 此时服务器就无法对外提供服务了。

3. 客户端

3.1 connect:

connect是一个系统调用函数,用于建立与远程主机的连接。它通常用于创建客户端套接字,并将其连接到服务器套接字。
在这里插入图片描述

  • sockfd:套接字文件描述符,由socket函数创建获得。
  • addr:指向远程主机的地址结构体的指针,可以是struct sockaddr_instruct sockaddr_in6
  • addrlen:远程主机地址结构体的长度。

connect 会自动帮我们进行bind!

connect函数通过sockfdaddr参数指定的地址信息,将本地套接字与远程主机的套接字连接起来。如果连接成功,返回0;如果连接失败,返回-1,并设置全局变量errno表示错误类型。

在这里插入图片描述
注意:在使用connect函数之前,必须先创建一个套接字,并确保套接字是可用的,可以使用socket函数进行创建。

三个问题:

  • 客户端需要绑定吗?需要但是不需要自己显示的bind!
  • 需要监听吗?不需要,监听是让别人来连你,作为客户端不用被连!
  • 需要accept吗?都没人来连你,根本不需要获取连接!

3.2 客户端的实现:

有了上面的分析,再加上之前udp编写的基础,我们很容易就能将tcp的客户端编写完成:

#include "util.hpp"

volatile bool quit = false;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
    std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
              << std::endl;
}

// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);

    // 1. 创建socket SOCK_STREAM
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket: " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }

    // 2. connect,发起链接请求,你向谁发起请求呢??当然是向服务器发起请求喽!
    // 2.1 先填充需要连接的远端主机的基本信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_aton(serverIp.c_str(), &server.sin_addr);

    // 2.2 发起请求,(隐性的概念)connect 会自动帮我们进行bind!
    if (connect(sock, (const struct sockaddr *)&server, sizeof server) != 0)
    {
        std::cerr << "connect: " << strerror(errno) << std::endl;
        exit(CONN_ERR);
    }
    std::cout << "info : connect success: " << sock << std::endl;

    std::string message;
    while (!quit)
    {
        message.clear();
        std::cout << "请输入你的消息>>> ";
        std::getline(std::cin, message);
        
        if (strcasecmp(message.c_str(), "quit") == 0)
            quit = true;

        // 向服务器发消息
        ssize_t s = write(sock, message.c_str(), message.size());
        std::cout << "read before" << std::endl;
        
        if (s > 0)
        {
            message.resize(1024);
            ssize_t s = read(sock, (char *)(message.c_str()), 1024);

            if (s > 0)  message[s] = 0;
            std::cout << "Server Echo>>> " << message << std::endl;
        }
        else if (s <= 0)
        {
            break;
        }
    }

    close(sock);
    return 0;
}

日志重定向:

之前我们将日志全部都打印在显示器上,这次我们将日志全部都打印到一个文件中,方便以后查看:

在这里插入图片描述
客户端连接服务器:

在这里插入图片描述


4. 提供服务

4.1 单进程版本:

// 提供服务, echo -> 小写 -> 大写
// 0.0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept

transService(serviceSock, peerIp, peerPort);

我们不重定向,方便我们进行实验。

实验结果:

在这里插入图片描述
在这里插入图片描述
如果ctrl + c杀掉客户端进程的话:

  • ctrl + c异常终止的话,文件是只有这个进程打开的,文件的生命周期是随进程的。
  • 如果强制的将客户端ctrl + c掉,操作系统会自动的关闭掉进程所对应的文件描述符。
  • 进程退出,PCB被文件释放,文件描述符表被释放,文件指针指向的struct file结构体引用计数减减。
  • 因为只有一个指向文件结构体,就减到0,操作系统自动关闭这个文件描述符。
  • 已关闭该文件,服务端读文件就会读到0,就类似于读到文件结束。

多个客户端连接服务器(有问题的):

我们发现一个客户端连接服务器的时候,客户端可以正常的显示出服务器处理过的结果。
但是,一旦我们有两个或者两个以上的客户端连接服务器就会出问题:新连接的客户端会卡在那里。

在这里插入图片描述
原因解释(看我笔记吧):

在这里插入图片描述

4.2 多进程1.0版本:

// 1.0 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的!
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
    close(listenSock_); // 建议关掉

    transService(serviceSock, peerIp, peerPort);
    exit(0); // 任务处理完就退出,进入僵尸
}
// 父进程 -- 父进程不用对外提供服务
close(serviceSock); // 这一步是一定要做的!
// waitpid(); 默认是阻塞等待!WNOHANG

(服务函数放在类内,类外都行)

  • 子进程也会把曾经父进程打开的listen套接字继承下去。
  • 通过创建子进程,让其去做父进程代码的一部分。
  • close(listenSock_);建议关掉。
    • 万一子进程将listenSock_文件描述符给写了,可能影响将来accept
  • close(serviceSock);这一步是一定要做的!
    • 如果父进程不关掉,那么随着连接来的客户端的增多,父进程可用的文件描述符就会越来越少。
    • 父进程获取servicSock文件描述符是为了让子进程继承下去,自己是不用的,就不应该继续占着,如果不关闭,最后可能导致文件描述符泄漏的问题。

我们知道子进程退出之后就会进入僵尸状态!等待父进程回收!
那我们敢让父进程阻塞式等待吗,显然是不能!因为我们的目的是让服务器并发起来,现在还阻塞着。

如果用非阻塞等待WNOHANG,这是可以的,我们要所有子进程的PID保存起来,非阻塞等待的时候每一次都要轮询所有的子进程,但是比较麻烦。👉 进程等待复习 - 传送门

或者直接忽略SIGCHLD

// 不用等子进程了
// signal(SIGCHLD, SIG_IGN); // only Linux

忽略SIGCHLD,👉 复习传送门。

4.3 多进程1.1版本:

// 1.1 版本 -- 多进程版本  -- 这样写也是可以的
// 爷爷进程
pid_t id = fork();
if(id == 0)
{
    // 爸爸进程
    close(listenSock_);// 建议关掉
    // 又进行了一次fork,让 爸爸进程
    if(fork() > 0) exit(0);

    // 孙子进程 -- 就没有爸爸 -- 就变成了孤儿进程 -- 被系统领养 -- 孙子进程就交给了系统来回收
    transService(serviceSock, peerIp, peerPort);
    exit(0);
}

// 父进程
close(serviceSock); // 这一步是一定要做的!
// 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
pid_t ret = waitpid(id, nullptr, 0); // 就用阻塞式
assert(ret > 0);
(void)ret;
  • 我们这里用到了 爷爷、爸爸、孙子 三个进程。
  • 爷爷进程创建爸爸进程,爸爸进程再创建孙子进程。
  • 只不过爸爸进程在创建完孙子进程之后直接就退出,由爷爷进程对其进行回收。
  • 将服务任务交由孙子进程去做。

孙子进程,没有了父进程,就变成了孤儿进程,被系统领养,孙子进程就交给了系统来回收,就不用我们来回收了。

子进程是从fork函数开始执行的。👉 复习传送门

(服务函数放在类内,类外都行)

4.4 多线程版本:

因为我们是线程函数是设置在类内的方法,所以成员函数第一个参数是隐藏的this指针,我们要设置成静态的。
静态成员函数里要想获取到类内成员变量的话,还要搞一些获取类内成员的接口,我们直接将现这些数据封装一下:

// 先声明一下
class ServerTcp;

class ThreadData
{
public:
    uint16_t clientPort_;
    std::string clinetIp_;
    int sock_;
    ServerTcp *this_;

public:
    ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
        : clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts)
    {}
};

线程函数:

// 类内方法,形参默认带有this指针
static void *threadRoutine(void *args)
{
    pthread_detach(pthread_self()); // 设置线程分离
    ThreadData *td = static_cast<ThreadData*>(args);
    td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
    delete td;
    return nullptr;
}

(此时服务函数放在了类里面)

// 2.0 版本 -- 多线程
ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// 不可进行线程等待,一等待,主线程就阻塞了,只能用线程分离
  • 这里不需要进行关闭文件描述符吗??不需要啦!!
  • 多线程是会共享文件描述符表的!

不可进行线程等待(pthread_join),一等待,主线程就阻塞了,只能用线程分离。

4.5 线程池版本:

Task任务需要我们重写:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"

class Task
{
public:
    // 下面两个等价
    // typedef std::function<void (int, std::string, uint16_t)> callback_t;
    using callback_t = std::function<void (int, std::string, uint16_t)>;
public:
    Task():sock_(-1), port_(-1)
    {}
    Task(int sock, std::string ip, uint16_t port, callback_t func)
    : sock_(sock), ip_(ip), port_(port), func_(func)
    {}

    void operator () ()
    {
        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\
            pthread_self(), ip_.c_str(), port_);

        func_(sock_, ip_, port_);

        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\
            pthread_self(), ip_.c_str(), port_);
    }
    ~Task()
    {}
private:
    int sock_;        // 给用户提供IO服务的sock
    uint16_t port_;   // client port
    std::string ip_;  // client ip
    callback_t func_; // 回调方法
};

交给线程池处理:

// 3.0 版本 -- 线程池
// transService服务在类外
Task t(serviceSock, peerIp, peerPort, transService);
tp_->push(t);

(服务函数放在类外)

我们在初始化服务器的方法的最后,加了一个启动线程池。👉 线程池 - 复习
还需要再loop函数循环之前,将线程池中的线程加载好。

我们将服务方法通过Task打包封装一下加载进线程池当中,然后Task有个仿函数里面就是调用回调函数。

在这里插入图片描述
之前我们在学C++11的时候,学过bind,我们这里可以用起来:

Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
tp_->push(t);

bind不熟悉的看过来,👉 复习传送门

(服务函数放在类内)

4.6 执行客户端指令:

popen函数:

在这里插入图片描述
第一件事情,创建管道,第二件事情,fork会自动帮我们创建子进程,让子进程去执行command代码,子进程执行完了之后,让父进程通过文件能够读到结果。

具体来说,popen函数会创建一个管道,其中写入端口(write end)被父进程保留,而读出端口(read end)被子进程保留。然后,popen函数调用fork创建一个新的子进程,该子进程会继承父进程的文件描述符,包括管道的读写端口。匿名管道用于在父进程和子进程之间进行双向通信。

void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    char command[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, command, sizeof(command) - 1); // 我们认为我们读到的都是字符串
        if (s > 0)
        {
            command[s] = '\0';
            logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);

            // 考虑安全
            std::string safe = command;
            if ((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
            {
                break;
            }

            // 我们是以r方式打开的文件,没有写入
            // 所以我们无法通过dup的方式得到对应的结果
            FILE *fp = popen(command, "r");
            if (fp == nullptr)
            {
                logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));
                break;
            }

            char line[1024];
            while (fgets(line, sizeof(line) - 1, fp) != nullptr)
            {
                write(sock, line, strlen(line));
            }

            pclose(fp);
            logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);
        }
        else if (s == 0)
        {
            // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0, 代表对端关闭
            // s == 0: 代表对方关闭,client 退出
            logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }

    // 只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
    logMessage(DEBUG, "server close %d done", sock);
}

同样的也是通过线程池的方式提供服务:

Task t(serviceSock, peerIp, peerPort, execCommand);
tp_->push(t);

(服务函数放在类外)

但是这时候我们要将客户端的读取服务器返回的消息给屏蔽掉,不然客户端会一直阻塞式(read在等)的等待服务端发消息回来。

备注:

如果我们设置了对应的任务是死循环,那么线程池提供服务,就显得有不太合适了,我们应该给线程池抛入的任务是短任务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1057729.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RSIC-V工具链介绍及其安装教程

前言 &#xff08;1&#xff09;此系列文章是跟着汪辰老师的RISC-V课程所记录的学习笔记。 &#xff08;2&#xff09;该课程相关代码gitee链接&#xff1b; &#xff08;3&#xff09;PLCT实验室实习生长期招聘&#xff1a;招聘信息链接 &#xff08;4&#xff09;在配置RSIC-…

私有云OpenStack保姆级教学

一、Openstack介绍 OpenStack是由美国国家航空航天局(NASA)与Rackspace公司合作研发并发起的&#xff0c;以Apache许可证授权的自由软件和开放源代码的云计算技术解决方案&#xff0c;其是一个项目也是一个软件&#xff0c;主要用于实现云项目&#xff0c;因云项目操作系统而存…

“把握拐点,洞悉投资者情绪与比特币价格的未来之路!“

“本来这篇文章是昨天晚上发的&#xff0c;国庆节庆祝喝多了&#xff0c;心有余而力不足&#xff01;直接头躺马桶GG了” 标准普尔 500 指数 200 天移动平均线云是我几个月来一直分享的下行目标&#xff0c;上周正式重新测试了该目标。200 日移动平均线云表示为: 200 天指数移…

iMazing 2.17.10官方中文版含2023最新激活许可证码

iMazing 2.17.10官方中文版是一款iOS设备管理软件&#xff0c;该软件支持对基于iOS系统的设备进行数据传输与备份&#xff0c;用户可以将包括&#xff1a;照片、音乐、铃声、视频、电子书及通讯录等在内的众多信息在Windows/Mac电脑中传输/备份/管理。 iMazing 2.17.10官方中文…

JavaSE | 初识Java(八) | 类和对象

在 java 中定义类时需要用到 class 关键字 &#xff0c;具体语法如下 // 创建类 class ClassName{field; // 字段(属性) 或者 成员变量method; // 行为 或者 成员方法 } class 为 定义类的关键字&#xff0c; ClassName 为类的名字&#xff0c; {} 中为类的主体。 类中包含的内…

笔试强训Day11

T1&#xff1a;二叉树 链接&#xff1a;二叉树_牛客题霸_牛客网 (nowcoder.com)​​​​​​​s 题意&#xff1a;给你一颗二叉树&#xff0c;求俩个点的最近公共祖先&#xff08;LCA&#xff09; 因为比较特殊&#xff0c;树是一颗二叉树&#xff0c;二叉树的编号很特殊&…

Multisim 14.3如何修改默认安装路径及下载

Multisim 14.3默认安装到C盘&#xff0c;而且没有修改安装路径选项&#xff0c;给安装带来了很多不便&#xff0c;经过网络查询、实际操作&#xff0c;成功安装到了D盘&#xff0c;希望对想修改默认安装路径的朋友有所帮助。 一、安装前准备工作&#xff0c;以下实操真对初学者…

OpenNebula的配置与应用

学习了OpenNebula的安装之后&#xff0c;接下来就是配置OpenNebula&#xff0c;内容包括配置Sunstone&#xff0c;VDC和集群&#xff0c;设置影像&#xff0c;模板管理&#xff0c;虚拟机管理等。OpenNebula还有大量的工作要做&#xff0c;这些工作主要来自映像、模板和虚拟机管…

Spring Boot的创建和使用(JavaEE进阶系列2)

目录 前言&#xff1a; 1.什么是Spring Boot&#xff1f;为什么要学习Spring Boot&#xff1f; 2.Spring Boot优点 3.创建Spring Boot项目 3.1准备工作 3.2Spring Boot创建 3.2.1通过idea的方式创建 3.2.2通过网页创建 4.Spring Boot中的配置文件 4.1Spring Boot配置…

树莓集团涉足直播产业园区运营,成都直播产业园区再添黑马

树莓集团涉足成都直播产业园运营领域&#xff0c;这一消息引起了业界的广泛关注。在这个无限可能的直播领域中&#xff0c;树莓集团将与上市公司德商产投紧密合作&#xff0c;立志为成都直播行业的发展注入新的活力。成都天府蜂巢直播产业园推行着一系列创新的政策措施&#xf…

算法通过村第十一关-位运算|黄金笔记|位运算压缩

文章目录 前言用4kb内存寻找重复元素总结 前言 提示&#xff1a;如果谁对你说了地狱般的话&#xff0c;就代表了他的心在地狱。你不需要相信那样的话&#xff0c;就算对方是你的父母也一样。 --高延秀《远看是蔚蓝的春天》 位运算有个很重要的作用就是能用比较小的空间存储比较…

Tensorflow、Pytorch和Ray(张量,计算图)

1.深度学习框架&#xff08;Tensorflow、Pytorch&#xff09; 1.1由来 可以追溯到2016年&#xff0c;当年最著名的事件是alphago战胜人类围棋巅峰柯洁&#xff0c;在那之后&#xff0c;学界普遍认为人工智能已经可以在一些领域超过人类&#xff0c;未来也必将可以在更多领域超过…

网盘搜索引擎:点亮知识星空,畅享数字宝藏!

大家好&#xff01;作为一名资深的网络产品运营人员&#xff0c;我今天要向大家介绍一款让你受益匪浅的神奇工具——网盘搜索引擎&#xff01;它可以帮助你免费搜索查询各种云盘共享资源&#xff0c;包括影视作品、纪录片、小说、动漫等等。现在&#xff0c;我们急需网络流量&a…

手边酒店V2独立版小程序 1.0.21 免授权+小程序前端安装教程

手边酒店小程序独立版酒店宾馆订房系统支持创建多个小程序&#xff0c;让每一个客户单独管理属于自己的小程序。系统无需授权&#xff0c;小程序端用户授权也是采用最新接口。 缺点不开源不影响使用&#xff0c;播播资源安装测试下来未发现或出现BUG情况&#xff0c;用户授权接…

深度学习 图像分割 PSPNet 论文复现(训练 测试 可视化)

Table of Contents 一、PSPNet 介绍1、原理阐述2、论文解释3、网络模型 二、部署实现1、PASCAL VOC 20122、模型训练3、度量指标4、结果分析5、图像测试 一、PSPNet 介绍 PSPNet(Pyramid Scene Parsing Network)来自于CVPR2017的一篇文章&#xff0c;中文翻译为金字塔场景解析…

Redis主从复制、哨兵、cluster集群

目录 Redis 主从复制 主从复制的作用 主从复制流程 搭建Redis 主从复制 实验环境 所有主机安装redis 修改 Redis 配置文件&#xff08;Master节点操作&#xff09; 修改 Redis 配置文件&#xff08;Slave节点操作&#xff09; 验证主从效果 Redis 哨兵模式 哨兵模式的…

【DRAM存储器十】SDRAM介绍-刷新

&#x1f449;个人主页&#xff1a;highman110 &#x1f449;作者简介&#xff1a;一名硬件工程师&#xff0c;持续学习&#xff0c;不断记录&#xff0c;保持思考&#xff0c;输出干货内容 参考资料&#xff1a;《镁光SDRAM数据手册》、《PC SDRAM specification》 从前面的…

2023年中国医疗传感器行业现状分析:市场国有化率低[图]

传感器是对物理刺激&#xff08;如热、光、声、压力、磁或特定的运动&#xff09;作出反应并传送产生的脉冲&#xff08;如用于测量或操作控制&#xff09;的装置。传感器一般由敏感元件、转换元件和转换电路组成。 医疗传感器分类 资料来源&#xff1a;共研产业咨询&#xff…

基于SSM的奶茶店管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

世界前沿技术发展报告2023《世界航天技术发展报告》(二)卫星技术

&#xff08;二&#xff09;卫星技术 1.概述2. 通信卫星2.1 美国太空发展局推进“国防太空体系架构”&#xff0c;持续部署“传输层”卫星2.2 美国军方在近地轨道成功演示验证星间激光通信2.3 DARPA启动“天基自适应通信节点”项目&#xff0c;为增强太空通信在轨互操作能力提供…