【Linux网络】Linux 网络编程入门:TCP Socket 编程(下)

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [3 ~> 开篇引入:网络编程到底在做什么?](#3 ~> 开篇引入:网络编程到底在做什么?)
    • [3.1 上网本质:下载 / 上传 = 计算机 IO](#3.1 上网本质:下载 / 上传 = 计算机 IO)
    • [3.2 进程间通信本质:IO 通信(本地 + 网络)](#3.2 进程间通信本质:IO 通信(本地 + 网络))
    • [3.3 TCP vs UDP 核心区别](#3.3 TCP vs UDP 核心区别)
    • [3.4 学习路线:接口 → 多执行流 → 线程池 → 远程命令服务](#3.4 学习路线:接口 → 多执行流 → 线程池 → 远程命令服务)
  • [4 ~> (简单回顾)TCP 服务端核心流程总览](#4 ~> (简单回顾)TCP 服务端核心流程总览)
    • [4.1 标准五步:socket → bind → listen → accept → read/write](#4.1 标准五步:socket → bind → listen → accept → read/write)
    • [4.2 最关键的接口:accept()](#4.2 最关键的接口:accept())
    • [4.3 长服务 vs 短服务](#4.3 长服务 vs 短服务)
  • [5 ~> (回顾:从烂到优)服务端多执行流方案](#5 ~> (回顾:从烂到优)服务端多执行流方案)
    • [5.1 单进程(不可用)](#5.1 单进程(不可用))
    • [5.2 多进程版本(稳定、适合长服务)](#5.2 多进程版本(稳定、适合长服务))
    • [5.3 多线程版本(轻量级进程)](#5.3 多线程版本(轻量级进程))
    • [5.4 线程池版本(最优:可控并发)](#5.4 线程池版本(最优:可控并发))
  • [6 ~> TCP 客户端完整实现](#6 ~> TCP 客户端完整实现)
    • [6.1 客户端三要素:socket → connect → read/write](#6.1 客户端三要素:socket → connect → read/write)
    • [6.2 新的系统调用:connect()](#6.2 新的系统调用:connect())
    • [6.3 客户端通信逻辑](#6.3 客户端通信逻辑)
    • [6.4 关键结论:sockfd 全双工,读写对等](#6.4 关键结论:sockfd 全双工,读写对等)
  • [7 ~> 管道与重定向:命令执行的底层原理](#7 ~> 管道与重定向:命令执行的底层原理)
    • [7.1 原生管道:pipe + dup2 + exec](#7.1 原生管道:pipe + dup2 + exec)
      • [7.1.1 完整流程解析:](#7.1.1 完整流程解析:)
    • [7.2 封装接口:popen / pclose](#7.2 封装接口:popen / pclose)
  • [8 ~> 实战项目:远程命令执行服务](#8 ~> 实战项目:远程命令执行服务)
    • [8.1 架构:网络层 + 业务层](#8.1 架构:网络层 + 业务层)
    • [8.2 业务类 ExecuteCommand](#8.2 业务类 ExecuteCommand)
    • [8.3 安全机制:白名单 > 黑名单](#8.3 安全机制:白名单 > 黑名单)
      • [8.3.1 白名单机制(推荐)](#8.3.1 白名单机制(推荐))
      • [8.3.2 黑名单机制(不推荐)](#8.3.2 黑名单机制(不推荐))
    • [8.4 完整调用链](#8.4 完整调用链)
  • [9 ~> TCP 字节流核心坑点](#9 ~> TCP 字节流核心坑点)
    • [9.1 粘包问题](#9.1 粘包问题)
    • [9.2 read/write 不一定一次完成](#9.2 read/write 不一定一次完成)
    • [9.3 缓冲区大小](#9.3 缓冲区大小)
    • [9.4 read 返回 0](#9.4 read 返回 0)
  • [10 ~> 总结](#10 ~> 总结)
  • [11 ~> 完整代码演示](#11 ~> 完整代码演示)
  • 结尾


3 ~> 开篇引入:网络编程到底在做什么?

网络编程,本质上就是跨主机的进程间通信(IPC)。我们平时上网、刷视频、聊天,底层都是不同设备上的进程在通过网络交换数据。理解了这一点,整个网络编程的学习就会清晰很多。

3.1 上网本质:下载 / 上传 = 计算机 IO

很多人对网络编程的理解停留在 "发个包、收个包",但它的底层逻辑其实和本地文件读写是一样的:

  • 下载 :本质是从远端服务器的文件 / 内存,把数据到本地。
  • 上传 :本质是把本地的数据到远端服务器。

所以,网络编程也可以被看作是一种特殊的 IO 操作 ------ 只不过操作的对象不是本地磁盘文件,而是网络另一端的进程。

3.2 进程间通信本质:IO 通信(本地 + 网络)

进程间通信(IPC)的核心,就是让两个独立的进程可以交换数据。常见的方式有:

  • 本地 IPC:管道(pipe)、消息队列、共享内存、信号量。
  • 网络 IPC:Socket。

Socket 是目前跨主机进程通信的通用标准,它提供了一套统一的 API,让我们可以像读写文件一样和网络上的其他进程通信。

3.3 TCP vs UDP 核心区别

这是网络编程中最经典的一对 "冤家",理解它们的区别是选择合适协议的第一步:

3.4 学习路线:接口 → 多执行流 → 线程池 → 远程命令服务

本文将按照一个从基础到实战的完整路径展开:

  1. 基础接口:掌握 TCP 服务端和客户端的核心系统调用(部分工作上篇博客中我们已经做过了)。
  2. 多执行流:解决单进程服务无法处理多客户端的问题,从多进程、多线程到线程池(上篇博客已经展示了,这里不多赘言)。
  3. 命令执行:学习管道、重定向与 popen,实现本地命令的执行与结果读取。
  4. 实战项目:将所有知识点整合,实现一个远程命令执行服务。

4 ~> (简单回顾)TCP 服务端核心流程总览

一个标准的 TCP 服务端,通常遵循固定的 "五步走" 流程。

4.1 标准五步:socket → bind → listen → accept → read/write

这是所有 TCP 服务端都遵循的流程,缺一不可:

  • 简单来看一下是哪五步:
  1. socket():创建一个套接字文件描述符,相当于向操作系统申请一个 "网络接口"。
  2. bind():给这个套接字绑定地址和端口号,让客户端知道 "去哪里找你"。
  3. listen():将套接字标记为被动连接状态,准备好接收客户端的连接请求。
  4. accept():阻塞等待客户端连接,成功后返回一个用于通信的新套接字。
  5. read()/write():通过新套接字,和客户端进行数据读写。

4.2 最关键的接口:accept()

accept()是服务端程序的 "心脏",它的行为直接决定了服务的并发模型。

  • 作用 :从内核的已完成连接队列中取出一个连接请求。如果队列为空,accept()会阻塞等待。
  • 返回 :成功时返回一个全新的、已连接的文件描述符(connfd),后续和客户端的通信都通过这个新描述符进行。监听套接字 listenfd 则继续等待下一个连接。

4.3 长服务 vs 短服务

在设计服务时,首先要明确你的服务是哪种类型:

  • 长服务 :连接建立后,会保持很长时间,持续进行数据交互。例如:
    • 游戏服务器
    • SSH 远程登录
    • 即时通讯软件
  • 短服务 :连接建立后,只完成一次请求 - 响应就断开。例如:
    • HTTP/HTTPS 请求
    • 登录、注册接口
    • 简单的 API 调用

5 ~> (回顾:从烂到优)服务端多执行流方案

单进程的服务端一次只能处理一个客户端,在实际应用中毫无价值。我们需要引入多执行流模型,让服务端可以同时为多个客户端服务。

单进程的服务端一次只能处理一个客户端,在实际应用中毫无价值。我们需要引入多执行流模型,让服务端可以同时为多个客户端服务。

5.1 单进程(不可用)

cpp 复制代码
            // Version0 -- 不会被使用的单进程版本
            Service(sockfd, clientaddress);
  • 问题:处理一个客户端时,整个进程会被阻塞,无法响应其他客户端的连接请求。这在实际场景中完全不可用。

5.2 多进程版本(稳定、适合长服务)

cpp 复制代码
 // Version1 -- 多进程版本优化
            pid_t id = fork();
            if(id < 0)   // 创建失败
            {
                LOG(LogLevel::ERROR) << "fork error";
                close(sockfd);
            }
            else if(id == 0)
            {
                // 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
                if(fork() > 0)  // > 0才是父进程退出
                    exit(0);
                // 孙子进程
                Service(sockfd,clientaddress);
                exit(0);
            }
            else
            { }
            // 关闭没有时序问题,各管各的,不影响
            close(sockfd);
            // 父进程
            pid_t rid = waitpid(id,nullptr,0);
            (void)rid;
  • 流程 :父进程 accept 到连接后,立刻 fork 一个子进程,由子进程专门负责与该客户端通信,父进程则继续 accept 新的连接。
  • 优点:进程间完全隔离,一个客户端的崩溃不会影响其他客户端或主进程,稳定性极高。
  • 缺点:进程创建开销大,占用内存高,并发量受限于系统进程数,不适合高并发场景。

5.3 多线程版本(轻量级进程)

cpp 复制代码
// Version2 -- 多线程版本
            // --> 多线程这里不能关闭任何自己不用的sockfd <--
            // 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
            // 我不想创建进程,我要使用多线程降低创建进程的开销
            pthread_t tid;
            // pthread_create(&tid,nullptr);
            // 以new的方式新建,其它线程也能够看见
            ThreadData *td = new ThreadData(sockfd,clientaddress,this);
            pthread_create(&tid,nullptr,Threadrun,(void*)td);

            // // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
            // pthread_join(tid,nullptr);
  • 流程 :主线程 accept 到连接后,创建一个工作线程,由工作线程处理客户端通信。
  • 优点:线程创建开销远小于进程,切换快,资源占用低。
  • 缺点
    • 无上限:客户端连接过多时,会创建大量线程,耗尽系统内存和 CPU。
    • 数据共享问题:多线程共享进程地址空间,需要额外处理锁同步,稍有不慎就会出现竞态条件。

5.4 线程池版本(最优:可控并发)

线程池的核心思想是预创建一批固定数量的线程,并将任务放到队列中,由线程池中的线程来消费任务

cpp 复制代码
            // Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
            // [](){} -- lambda
            // 按值捕获 sockfd 和 clientaddress,捕获 this 指针
            ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
                Service(sockfd,clientaddress);  // 这样线程池中的线程就能访问到客户端 socket 和地址信息
            });
  • 适用:短服务场景,因为任务处理快,线程可以被快速回收复用。
  • 不适用长服务场景,如果一个线程被一个长连接长期占用,线程池很快就会被占满,导致新请求无法处理。
  • 优点
    • 可控并发:线程数量固定,不会因客户端过多而压垮系统。
    • 性能高:避免了频繁创建 / 销毁线程的开销,任务队列实现了解耦。

6 ~> TCP 客户端完整实现

客户端的实现比服务端简单很多,核心是和服务端建立连接并收发数据。

6.1 客户端三要素:socket → connect → read/write

  1. socket():创建套接字。
  2. connect():向服务端发起连接请求,建立 TCP 连接。
  3. read()/write():和服务端进行数据读写。

6.2 新的系统调用:connect()

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. 功能:向指定的服务端地址发起连接请求。如果客户端没有手动 bind,内核会自动为其分配一个临时端口。
  2. 参数
    • sockfd:客户端的套接字文件描述符。
    • addr:指向服务端地址结构的指针(IP + 端口)。
    • addrlen:地址结构的长度。
  3. 返回 :连接成功返回 0,失败返回 -1,并设置 errno。常见错误如 ECONNREFUSED(连接被拒绝)、ETIMEDOUT(连接超时)。

6.3 客户端通信逻辑

一个典型的客户端交互流程:

1、从标准输入读取用户输入的数据。

2、write()发送数据到服务端。

3、read()阻塞等待服务端的响应。

4、将服务端的响应打印到标准输出。

5、循环执行以上步骤,直到用户输入退出命令。

6.4 关键结论:sockfd 全双工,读写对等

  • 一个 TCP 连接的 sockfd全双工的,这意味着它可以同时进行读写操作,互不干扰。
  • 内核为每个 TCP 套接字维护了两个独立的缓冲区:发送缓冲区接收缓冲区write()操作只是将数据拷贝到发送缓冲区,内核会负责后续的发送;read()操作则是从接收缓冲区中读取数据。

7 ~> 管道与重定向:命令执行的底层原理

要实现远程命令执行,我们需要先掌握在本地执行命令并获取输出的方法,这离不开管道和重定向。

7.1 原生管道:pipe + dup2 + exec

以命令ls -a | grep txt为例,这是 Linux 中非常常见的管道用法,其底层实现完全依赖这三个系统调用。

  • pipe():创建一个管道,返回两个文件描述符pipefd[0](读端)和pipefd[1](写端)。
  • dup2():复制文件描述符,实现重定向。例如dup2(pipefd[1], STDOUT_FILENO)会将进程的标准输出重定向到管道的写端。
  • exec():执行新的程序,替换当前进程的地址空间,但文件描述符会被保留。

7.1.1 完整流程解析:

1、父进程创建管道pipe(pipefd)

2、fork出第一个子进程(执行ls -a):

  • 关闭管道读端close(pipefd[0])
  • 将标准输出重定向到管道写端dup2(pipefd[1], STDOUT_FILENO)
  • 关闭原管道写端close(pipefd[1])
  • exec执行ls -a,其输出会写入管道。

3、fork出第二个子进程(执行grep txt):

  • 关闭管道写端close(pipefd[1])
  • 将标准输入重定向到管道读端dup2(pipefd[0], STDIN_FILENO)
  • 关闭原管道读端close(pipefd[0])
  • exec 执行grep txt,它会从管道中读取ls的输出并过滤。

7.2 封装接口:popen / pclose

手动pipe + fork + dup2 + exec流程繁琐,标准库提供了popen来简化这一过程。

c 复制代码
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
  • 一句话总结popenpipe + fork + exec + shell的封装。
  • 用法
    • FILE* fp = popen("ls -l", "r");:以读模式执行命令,命令的标准输出会通过FILE*流返回给父进程。
    • 然后可以用fgets(fp, buf, sizeof(buf))读取命令的输出结果。
  • 配套 :使用完后必须调用pclose(fp),它会调用waitpid等待子进程退出,回收资源,避免产生僵尸进程。

8 ~> 实战项目:远程命令执行服务

现在,我们可以将前面学到的所有知识点整合起来,实现一个简单的远程命令执行服务。

8.1 架构:网络层 + 业务层

我们将服务分为两层,实现解耦:

  • 网络层:负责处理客户端连接和数据收发,使用线程池处理并发。
  • 业务层:负责接收客户端发来的命令,执行命令并返回结果。

8.2 业务类 ExecuteCommand

  • 接口 :定义一个 Exec(const std::string& cmd) 函数,输入命令字符串,返回命令执行结果字符串。
  • 实现 :在 Exec 函数内部,使用 popen 执行命令,并用 fgets 读取输出到缓冲区,最后返回。
cpp 复制代码
std::string ExecuteCommand::Exec(const std::string& cmd) {
    char buffer[4096] = {0};
    std::string result;
    FILE* fp = popen(cmd.c_str(), "r");
    if (fp == nullptr) {
        return "Error executing command.";
    }
    while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
        result += buffer;
    }
    pclose(fp);
    return result;
}

8.3 安全机制:白名单 > 黑名单

直接执行客户端发来的任意命令是极度危险的,攻击者可以执行 rm -rf / 等恶意命令。

8.3.1 白名单机制(推荐)

  • 白名单只允许执行预设的、安全的命令。
    • 例如,只允许 pwdwhoamilsenv 等命令。
    • Exec 函数中,先检查命令是否在白名单列表中,不在则直接拒绝执行。

8.3.2 黑名单机制(不推荐)

禁止执行某些危险命令(如 rmshutdown)。这种方法很容易被绕过,例如通过 r\m; rm -rf / 等方式注入恶意代码。

8.4 完整调用链

  1. 客户端通过 TCP 连接发送命令字符串(如 ls -l)。
  2. 服务端网络层 read 到命令字符串,将其交给业务层。
  3. 业务层ExecuteCommand::Exec() 先检查命令是否在白名单中。
  4. 安全检查通过后,调用 popen 执行命令并获取结果。
  5. 服务端网络层将结果 write 回客户端。

9 ~> TCP 字节流核心坑点

9.1 粘包问题

  • 现象:TCP 是字节流协议,没有消息边界。如果客户端连续发送两次数据,服务端可能一次 read 就读到了两次发送的数据,这就是 "粘包"。

  • 解决:应用层需要自己定义消息边界,常见方案:

    • 定长包:每次都发送固定长度的数据。
    • 分隔符:用特殊字符(如 \n)分隔不同的消息。
    • 长度字段:发送前先发送一个表示后续消息长度的字段,服务端先读长度,再读对应长度的数据。

9.2 read/write 不一定一次完成

  • read/write 系统调用的返回值可能小于请求的字节数。这在网络编程中非常常见。
  • 必须循环读写:为了保证数据完整收发,必须使用循环,直到读 / 写够指定的字节数。
c 复制代码
// 写够len个字节
ssize_t write_all(int fd, const void *buf, size_t len) {
    size_t written = 0;
    const char *p = (const char*)buf;
    while (written < len) {
        ssize_t ret = write(fd, p + written, len - written);
        if (ret <= 0) return ret;
        written += ret;
    }
    return written;
}

9.3 缓冲区大小

read 缓冲区建议设置为 40968192,过小会导致频繁的系统调用,过大则会浪费内存。

9.4 read 返回 0

  • read 返回 0 不是错误,而是表示对端已经关闭了连接 (收到了 FIN 包)。此时服务端应该主动关闭本地的 sockfd,并结束当前客户端的处理流程。

10 ~> 总结

  • 服务端socket -> bind -> listen -> accept -> 多执行流(进程/线程/池) -> read/write
  • 客户端socket -> connect -> 收发数据
  • 命令执行 :底层 pipe/dup2/exec,高层 popen 封装。
  • 安全 :远程执行命令必须使用白名单机制。
  • TCP 本质:全双工、字节流、面向连接的可靠传输协议。应用层必须自己处理消息边界和数据完整性问题。

11 ~> 完整代码演示

11.1 TcpEchoServer_v1

文件目录结构如下:

11.1.1 服务端

TcpEchoServer.hpp
cpp 复制代码
#ifndef __TcpEchoServer_HPP
#define __TcpEchoServer_HPP

#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

// 使用原生的线程
#include <pthread.h>

// 使用自己封装的线程池(还有两个附加的小挂件:互斥锁和条件变量,都要加上)
#include "ThreadPool.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

// 这样未来我就可以定义任务、构建任务类型
#include <functional>
using task_t = std::function<void()>;

// 网络通信三剑客
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的
#include "Logger.hpp"
#include "InetAddr.hpp" // 今天不做主机序列和网络序列的转换工作了,直接把客户端地址拿过来

// 调用日志,命名空间
using namespace LogModule;

// 定义一个默认全局的端口号
static uint16_t gdefaultport = 8080;
// 设置全连接个数
static const int gbacklog = 32;

class TcpEchoServer
{
private:
    // sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
    // 通过这个套接字给对应的客户端提供服务
    void Service(int sockfd,InetAddr client)
    {
        // 我想让这个服务一直进行
        while(true)
        {
            // TCP可以直接采用文件的接口,不用网络接口,我今天先直接验证read/write
            char inbuffer[1024];
            // 1.读取数据
            ssize_t n = read(sockfd,inbuffer,sizeof(inbuffer) - 1); // 默认字符串
            if(n > 0)   // 读取成功
            {
                inbuffer[n] = 0;
                LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;  // 客户端给我发了个什么消息
            }
            else if(n == 0) // 返回值为0表示读到文件结尾,= 0相当于客户端退出了
            {
                LOG(LogLevel::INFO) << client.StringAddress() << "close sockfd" << sockfd << ", me too!";
            }
            else    // 读取客户端异常,但是这个日志等级要注意,一个客户端出错不影响其他客户端(餐厅吃饭,一个客人出问题不影响其他客人吃饭)
            {
                LOG(LogLevel::ERROR) << "read sockfd error" << sockfd;
                break; 
            }

            // 2.加工处理数据
            std::string echo_string = "server echo# ";
            echo_string += inbuffer;

            // 3.写回数据
            n = write(sockfd,echo_string.c_str(),echo_string.size());
            if(n <= 0)
            {
                LOG(LogLevel::ERROR) << "write sockfd error" << sockfd;
                break;
            }
        }
    }
public:
    TcpEchoServer(uint16_t port = gdefaultport) : _port(port),_listsockfd(-1)
    {}
    void Init()
    {
        // 1.创建套接字
        _listsockfd = socket(AF_INET,SOCK_STREAM,0); // IPPROTO_TCP,设置0就知道是TCP了
        // 创建套接字失败
        if(_listsockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << _listsockfd;
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listsockfd;    // 这里012被占用,就是3

        // 2.bind:设置服务器的socket信息
        struct sockaddr_in local;
        // 把local字段清零
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 这里使用的是构造函数传入的_port
        // 把主机序列转成网络序列
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        // local.sin_addr = inet_addr(_ip);

        int n = bind(_listsockfd,(const sockaddr *)&local,sizeof(local));
        if(n < 0)
        {
            // 如果bind返回值小于0,表示绑定失败
            LOG(LogLevel::FATAL) << "bind error: " << _listsockfd;
            exit(3); // 直接记录FATAL日志,并且退出进程
        }
        LOG(LogLevel::INFO) << "bind success: " << _listsockfd;

        // 3.将socket设置为监听状态
        n = listen(_listsockfd,gbacklog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error" << _listsockfd;
            exit(4);
        }
        LOG(LogLevel::INFO) << "listen success" << _listsockfd;
    }

    // 设计一个内部类:要它线程完成任务(传sockfd);线程的数据也要传
    class ThreadData
    {
    public:
        // 还是写一下构造函数
        ThreadData(int sockfd,InetAddr addr,TcpEchoServer *owner)   // owner要初始化一下
        :_sockfd(sockfd),
         _address(addr),
         _owner(owner)
         {}
    public:
        int _sockfd;
        InetAddr _address;
        // Service属于类内方法,static里面不能直接使用
        TcpEchoServer *_owner;
    };

    // 定义ThreadRun,里面有this指针,要加static
    static void* Threadrun(void *args)
    {
        // 主线程不能阻塞,那要怎么做?
        // 线程分离 --> detach这个接口就是为今天这种情况准备的!
        pthread_detach(pthread_self());

        // 调用的时候要能够拿到sockfd
        ThreadData *td = static_cast<ThreadData *>(args);
        // Service属于类内方法,static里面不能直接使用-->必须加一个函数成员owner
        td->_owner->Service(td->_sockfd,td->_address);  // 这样就可以使用方法了

        delete td;
        return nullptr;
    }

    void Start()
    {
        // // 最佳实践
        // signal(SIGCHLD,SIG_IGN);    // 直接对子进程退出信号做忽略
        // 死循环
        while(true)
        {
            struct sockaddr_in clientaddr;  // 客户端地址
            socklen_t len = sizeof(clientaddr); // 客户端地址长度,提供的缓冲区大小
            int sockfd = accept(_listsockfd,(struct sockaddr *)&clientaddr,&len);    // 强转
            if(sockfd < 0)
            {
                // 张三推销被拒绝会一蹶不振吗?拉客失败最多warning,不影响后面的拉客动作,继续accept
                LOG(LogLevel::WARNING) << "accpet error";
                continue;
            }
            // 获取地址了,就知道客户端连接了,得获取一下(IP地址和端口号)
            InetAddr clientaddress(clientaddr);
            LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
            // sleep(1);

            // 处理连接,进行IO通信
            // // Version0 -- 不会被使用的单进程版本
            // Service(sockfd, clientaddress);

            // // Version1 -- 多进程版本优化
            // pid_t id = fork();
            // if(id < 0)   // 创建失败
            // {
            //     LOG(LogLevel::ERROR) << "fork error";
            //     close(sockfd);
            // }
            // else if(id == 0)
            // {
            //     // 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
            //     if(fork() > 0)  // > 0才是父进程退出
            //         exit(0);
            //     // 孙子进程
            //     Service(sockfd,clientaddress);
            //     exit(0);
            // }
            // else
            // { }
            // // 关闭没有时序问题,各管各的,不影响
            // close(sockfd);
            // // 父进程
            // pid_t rid = waitpid(id,nullptr,0);
            // (void)rid;

            // // Version2 -- 多线程版本
            // // --> 多线程这里不能关闭任何自己不用的sockfd <--
            // // 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
            // // 我不想创建进程,我要使用多线程降低创建进程的开销
            // pthread_t tid;
            // // pthread_create(&tid,nullptr);
            // // 以new的方式新建,其它线程也能够看见
            // ThreadData *td = new ThreadData(sockfd,clientaddress,this);
            // pthread_create(&tid,nullptr,Threadrun,(void*)td);

            // // // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
            // // pthread_join(tid,nullptr);

            // Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
            // [](){} -- lambda
            // 按值捕获 sockfd 和 clientaddress,捕获 this 指针
            ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
                Service(sockfd,clientaddress);  // 这样线程池中的线程就能访问到客户端 socket 和地址信息
            });
        }
    }
     ~TcpEchoServer()
    {}
private:
    uint16_t _port;
    // 监听套接字
    int _listsockfd;
};

#endif
TcpEchoServer.cc
cpp 复制代码
#include "TcpEchoServer.hpp"

#include <iostream>
#include <memory>
#include <string>

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << "ServerPort" << std::endl;
}

// ./tcp_echo_server 8080
int main(int argc,char *argv[])
{
    // 输出一个简单的手册
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 将日志输出方式设置为 输出到屏幕(终端/控制台) ,而不是写入文件
    ENABLE_CONSOLE_LOG_STRATEGY();
    // 智能指针
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

11.1.2 客户端

TcpEchoClient.cc
cpp 复制代码
#include <iostream>
#include <string>

int main()
{}

11.1.3 运行演示

11.2 TcpEchoServer_v2

文件目录结构如下:

11.2.1 服务端

TcpEchoServer.hpp
cpp 复制代码
#ifndef __TcpEchoServer_HPP
#define __TcpEchoServer_HPP

#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

// 使用原生的线程
#include <pthread.h>

// 使用自己封装的线程池(还有两个附加的小挂件:互斥锁和条件变量,都要加上)
#include "ThreadPool.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

// 这样未来我就可以定义任务、构建任务类型
#include <functional>
using task_t = std::function<void()>;

// 网络通信三剑客
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的
#include "Logger.hpp"
#include "InetAddr.hpp" // 今天不做主机序列和网络序列的转换工作了,直接把客户端地址拿过来

// 调用日志,命名空间
using namespace LogModule;

// 定义一个默认全局的端口号
static uint16_t gdefaultport = 8080;
// 设置全连接个数
static const int gbacklog = 32;

class TcpEchoServer
{
private:
    // sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
    // 通过这个套接字给对应的客户端提供服务
    void Service(int sockfd,InetAddr client)
    {
        char name[128];
        pthread_getname_np(pthread_self(),name,sizeof(name));
        // 长连接服务,不适合用线程池
        // 比较适合短服务

        // 我想让这个服务从一直进行的长服务变成短服务 --> 把while(true)注释掉
        // while(true)
        // {
        // TCP可以直接采用文件的接口,不用网络接口,我今天先直接验证read/write
        char inbuffer[1024];
        // 1.读取数据
        ssize_t n = read(sockfd,inbuffer,sizeof(inbuffer) - 1); // 默认字符串
        if(n > 0)   // 读取成功
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;  // 客户端给我发了个什么消息
        }
        else if(n == 0) // 返回值为0表示读到文件结尾,= 0相当于客户端退出了
        {
            LOG(LogLevel::INFO) << client.StringAddress() << "close sockfd" << sockfd << ", me too!";
        }
        else    // 读取客户端异常,但是这个日志等级要注意,一个客户端出错不影响其他客户端(餐厅吃饭,一个客人出问题不影响其他客人吃饭)
        {
            LOG(LogLevel::ERROR) << "read sockfd error" << sockfd;
            // break;
        }

        // 2.加工处理数据
        std::string echo_string = "server echo# ";
        echo_string += inbuffer;

        // 3.写回数据
        n = write(sockfd,echo_string.c_str(),echo_string.size());
        if(n <= 0)
        {
            LOG(LogLevel::ERROR) << "write sockfd error" << sockfd;
            // break;
        }
    // }
    }
public:
    TcpEchoServer(uint16_t port = gdefaultport) : _port(port),_listsockfd(-1)
    {}
    void Init()
    {
        // 1.创建套接字
        _listsockfd = socket(AF_INET,SOCK_STREAM,0); // IPPROTO_TCP,设置0就知道是TCP了
        // 创建套接字失败
        if(_listsockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << _listsockfd;
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listsockfd;    // 这里012被占用,就是3

        // 2.bind:设置服务器的socket信息
        struct sockaddr_in local;
        // 把local字段清零
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 这里使用的是构造函数传入的_port
        // 把主机序列转成网络序列
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        // local.sin_addr = inet_addr(_ip);

        int n = bind(_listsockfd,(const sockaddr *)&local,sizeof(local));
        if(n < 0)
        {
            // 如果bind返回值小于0,表示绑定失败
            LOG(LogLevel::FATAL) << "bind error: " << _listsockfd;
            exit(3); // 直接记录FATAL日志,并且退出进程
        }
        LOG(LogLevel::INFO) << "bind success: " << _listsockfd;

        // 3.将socket设置为监听状态
        n = listen(_listsockfd,gbacklog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error" << _listsockfd;
            exit(4);
        }
        LOG(LogLevel::INFO) << "listen success" << _listsockfd;
    }

    // 设计一个内部类:要它线程完成任务(传sockfd);线程的数据也要传
    class ThreadData
    {
    public:
        // 还是写一下构造函数
        ThreadData(int sockfd,InetAddr addr,TcpEchoServer *owner)   // owner要初始化一下
        :_sockfd(sockfd),
         _address(addr),
         _owner(owner)
         {}
    public:
        int _sockfd;
        InetAddr _address;
        // Service属于类内方法,static里面不能直接使用
        TcpEchoServer *_owner;
    };

    // 定义ThreadRun,里面有this指针,要加static
    static void* Threadrun(void *args)
    {
        // 主线程不能阻塞,那要怎么做?
        // 线程分离 --> detach这个接口就是为今天这种情况准备的!
        pthread_detach(pthread_self());

        // 调用的时候要能够拿到sockfd
        ThreadData *td = static_cast<ThreadData *>(args);
        // Service属于类内方法,static里面不能直接使用-->必须加一个函数成员owner
        td->_owner->Service(td->_sockfd,td->_address);  // 这样就可以使用方法了

        delete td;
        return nullptr;
    }

    void Start()
    {
        // // 最佳实践
        // signal(SIGCHLD,SIG_IGN);    // 直接对子进程退出信号做忽略
        // 死循环
        while(true)
        {
            struct sockaddr_in clientaddr;  // 客户端地址
            socklen_t len = sizeof(clientaddr); // 客户端地址长度,提供的缓冲区大小
            int sockfd = accept(_listsockfd,(struct sockaddr *)&clientaddr,&len);    // 强转
            if(sockfd < 0)
            {
                // 张三推销被拒绝会一蹶不振吗?拉客失败最多warning,不影响后面的拉客动作,继续accept
                LOG(LogLevel::WARNING) << "accpet error";
                continue;
            }
            // 获取地址了,就知道客户端连接了,得获取一下(IP地址和端口号)
            InetAddr clientaddress(clientaddr);
            LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
            // sleep(1);

            // 处理连接,进行IO通信
            // // Version0 -- 不会被使用的单进程版本
            // Service(sockfd, clientaddress);

            // // Version1 -- 多进程版本优化
            // pid_t id = fork();
            // if(id < 0)   // 创建失败
            // {
            //     LOG(LogLevel::ERROR) << "fork error";
            //     close(sockfd);
            // }
            // else if(id == 0)
            // {
            //     // 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
            //     if(fork() > 0)  // > 0才是父进程退出
            //         exit(0);
            //     // 孙子进程
            //     Service(sockfd,clientaddress);
            //     exit(0);
            // }
            // else
            // { }
            // // 关闭没有时序问题,各管各的,不影响
            // close(sockfd);
            // // 父进程
            // pid_t rid = waitpid(id,nullptr,0);
            // (void)rid;

            // // Version2 -- 多线程版本
            // // --> 多线程这里不能关闭任何自己不用的sockfd <--
            // // 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
            // // 我不想创建进程,我要使用多线程降低创建进程的开销
            // pthread_t tid;
            // // pthread_create(&tid,nullptr);
            // // 以new的方式新建,其它线程也能够看见
            // ThreadData *td = new ThreadData(sockfd,clientaddress,this);
            // pthread_create(&tid,nullptr,Threadrun,(void*)td);

            // // // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
            // // pthread_join(tid,nullptr);

            // Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
            // [](){} -- lambda
            // 按值捕获 sockfd 和 clientaddress,捕获 this 指针
            ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
                Service(sockfd,clientaddress);  // 这样线程池中的线程就能访问到客户端 socket 和地址信息
            });
        }
    }
     ~TcpEchoServer()
    {}
private:
    uint16_t _port;
    // 监听套接字
    int _listsockfd;
};

#endif
TcpEchoServer.cc
cpp 复制代码
#include "TcpEchoServer.hpp"

#include <iostream>
#include <memory>
#include <string>

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << " <ServerPort>" << std::endl;
}

// ./tcp_echo_server 8080
int main(int argc,char *argv[])
{
    // 输出一个简单的手册
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 将日志输出方式设置为 输出到屏幕(终端/控制台) ,而不是写入文件
    ENABLE_CONSOLE_LOG_STRATEGY();
    // 智能指针
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

11.2.2 客户端

TcpEchoClient.cc
cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
// 头文件三件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的客户端地址
#include "InetAddr.hpp"

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << "ServerIp ServerPort" << std::endl;
}

// 运行:./TcpEchoClient server_ip server_port(客户端一定要知道服务器的IP和端口号)
int main(int argc,char *argv[])
{  
    // 输出一个简单的手册
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::string ServerIp = argv[1];
    uint16_t ServerPort = std::stoi(argv[2]);   // 把字符串转成整数

    // 1.创建TCP socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // Client需要明确地进行bind吗?不需要显示绑定
    // Client必须要有socket信息,即自己的IP + Port,一定需要bind,不用用户bind,OS自动随机bind的!
    // 那么,OS什么时候bind?

    // 2.建立连接
    // a.首次建立连接的时候,Client会自动bind自己的socket信息 
    // b.向{ServerIp,ServerPort}发起建立连接的请求
    // TCP通信,是面向连接的
    InetAddr serveraddress(ServerPort,ServerIp);
    int n = connect(sockfd,serveraddress.Addr(),serveraddress.AddrLen());
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    // 3.sockfd通信过程
    while(true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin,line);

        // TCP sockfd是全双工的
        // 发送数据
        ssize_t n = write(sockfd,line.c_str(),line.size());
        (void)n;

        char buffer[256];
        // TCP通信,双方的地位是对等的
        n = read(sockfd,buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "-> " << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cerr << "server quit" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

    // 要关闭文件描述符
    close(sockfd);

    return 0;
}

11.2.3 运行演示

11.3 TcpExecServer

文件目录结构如下:

11.3.1 服务端

TcpServer.hpp
cpp 复制代码
#ifndef __TcpEchoServer_HPP
#define __TcpEchoServer_HPP

#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

// 使用原生的线程
#include <pthread.h>

// 使用自己封装的线程池(还有两个附加的小挂件:互斥锁和条件变量,都要加上)
#include "ThreadPool.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

// 这样未来我就可以定义任务、构建任务类型
#include <functional>
using task_t = std::function<void()>;
using callback_t = std::function<std::string(std::string)>;
using callback = callback_t;

// 网络通信三剑客
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的
#include "Logger.hpp"
#include "InetAddr.hpp" // 今天不做主机序列和网络序列的转换工作了,直接把客户端地址拿过来

// 调用日志,命名空间
using namespace LogModule;

// 定义一个默认全局的端口号
static uint16_t gdefaultport = 8080;
// 设置全连接个数
static const int gbacklog = 32;

class TcpServer
{
private:
    // sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
    // 通过这个套接字给对应的客户端提供服务
    void Service(int sockfd,InetAddr client)
    {
        // 我想让这个服务一直进行
        while(true)
        {
            // TCP可以直接采用文件的接口,不用网络接口,我今天先直接验证read/write
            char inbuffer[1024];
            // 1.读取数据
            ssize_t n = read(sockfd,inbuffer,sizeof(inbuffer) - 1); // 默认字符串
            if(n > 0)   // 读取成功
            {
                inbuffer[n] = 0;
                LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer;  // 客户端给我发了个什么消息
            }
            else if(n == 0) // 返回值为0表示读到文件结尾,= 0相当于客户端退出了
            {
                LOG(LogLevel::INFO) << client.StringAddress() << "close sockfd" << sockfd << ", me too!";
            }
            else    // 读取客户端异常,但是这个日志等级要注意,一个客户端出错不影响其他客户端(餐厅吃饭,一个客人出问题不影响其他客人吃饭)
            {
                LOG(LogLevel::ERROR) << "read sockfd error" << sockfd;
                break; 
            }

            // 2.加工处理数据(命令行字符串),inbuffer -> string -> 命令字符串 -> ls -a -l / rm -rf xxx / pwd / which......
            std::string result = _cb(inbuffer);

            // 3.写回数据
            n = write(sockfd,result.c_str(),result.size());
            if(n <= 0)
            {
                LOG(LogLevel::ERROR) << "write sockfd error" << sockfd;
                break;
            }
        }
    }
public:
    TcpServer(uint16_t port = gdefaultport) : _port(port),_listsockfd(-1)
    {}
    void Init(callback cb)
    {
        _cb = cb;

        // 1.创建套接字
        _listsockfd = socket(AF_INET,SOCK_STREAM,0); // IPPROTO_TCP,设置0就知道是TCP了
        // 创建套接字失败
        if(_listsockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << _listsockfd;
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listsockfd;    // 这里012被占用,就是3

        // 2.bind:设置服务器的socket信息
        struct sockaddr_in local;
        // 把local字段清零
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);  // 这里使用的是构造函数传入的_port
        // 把主机序列转成网络序列
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        // local.sin_addr = inet_addr(_ip);

        int n = bind(_listsockfd,(const sockaddr *)&local,sizeof(local));
        if(n < 0)
        {
            // 如果bind返回值小于0,表示绑定失败
            LOG(LogLevel::FATAL) << "bind error: " << _listsockfd;
            exit(3); // 直接记录FATAL日志,并且退出进程
        }
        LOG(LogLevel::INFO) << "bind success: " << _listsockfd;

        // 3.将socket设置为监听状态
        n = listen(_listsockfd,gbacklog);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error" << _listsockfd;
            exit(4);
        }
        LOG(LogLevel::INFO) << "listen success" << _listsockfd;
    }

    // 设计一个内部类:要它线程完成任务(传sockfd);线程的数据也要传
    class ThreadData
    {
    public:
        ThreadData(int sockfd,InetAddr addr,TcpServer *owner)
        :_sockfd(sockfd),
         _address(addr),
         _owner(owner)
         {}
    public:
        int _sockfd;
        InetAddr _address;
        TcpServer *_owner;
    };

    // 定义ThreadRun,里面有this指针,要加static
    static void* Threadrun(void *args)
    {
        // 主线程不能阻塞,那要怎么做?
        // 线程分离 --> detach这个接口就是为今天这种情况准备的!
        pthread_detach(pthread_self());

        // 调用的时候要能够拿到sockfd
        ThreadData *td = static_cast<ThreadData *>(args);
        // Service属于类内方法,static里面不能直接使用-->必须加一个函数成员owner
        td->_owner->Service(td->_sockfd,td->_address);  // 这样就可以使用方法了

        delete td;
        return nullptr;
    }

    void Start()
    {
        // // 最佳实践
        // signal(SIGCHLD,SIG_IGN);    // 直接对子进程退出信号做忽略
        // 死循环
        while(true)
        {
            struct sockaddr_in clientaddr;  // 客户端地址
            socklen_t len = sizeof(clientaddr); // 客户端地址长度,提供的缓冲区大小
            int sockfd = accept(_listsockfd,(struct sockaddr *)&clientaddr,&len);    // 强转
            if(sockfd < 0)
            {
                // 张三推销被拒绝会一蹶不振吗?拉客失败最多warning,不影响后面的拉客动作,继续accept
                LOG(LogLevel::WARNING) << "accpet error";
                continue;
            }
            // 获取地址了,就知道客户端连接了,得获取一下(IP地址和端口号)
            InetAddr clientaddress(clientaddr);
            LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
            // sleep(1);

            // 处理连接,进行IO通信
            // // Version0 -- 不会被使用的单进程版本
            // Service(sockfd, clientaddress);

            // Version1 -- 多进程版本优化
            // 今天我要实现一个长服务,我就不选择线程池版本了,不想处理短服务了
            // 要实现一个字典翻译、命令行解析的业务
            pid_t id = fork();
            if(id < 0)   // 创建失败  
            {
                LOG(LogLevel::ERROR) << "fork error";
                close(sockfd);
            }
            else if(id == 0)
            {
                // 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
                if(fork() > 0)  // > 0才是父进程退出
                    exit(0);
                // 孙子进程
                Service(sockfd,clientaddress);
                exit(0);
            }
            else
            { }
            // 关闭没有时序问题,各管各的,不影响
            close(sockfd);
            // 父进程
            pid_t rid = waitpid(id,nullptr,0);
            (void)rid;

            // // Version2 -- 多线程版本
            // // --> 多线程这里不能关闭任何自己不用的sockfd <--
            // // 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
            // // 我不想创建进程,我要使用多线程降低创建进程的开销
            // pthread_t tid;
            // // pthread_create(&tid,nullptr);
            // // 以new的方式新建,其它线程也能够看见
            // ThreadData *td = new ThreadData(sockfd,clientaddress,this);
            // pthread_create(&tid,nullptr,Threadrun,(void*)td);

            // // // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
            // // pthread_join(tid,nullptr);

            // // Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
            // // [](){} -- lambda
            // // 按值捕获 sockfd 和 clientaddress,捕获 this 指针
            // // 线程池不适合进行长服务,线程池适合短服务,这里TcpServer要进行长服务,实现命令行解析功能,就不用线程池了
            // ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
            //     Service(sockfd,clientaddress);  // 这样线程池中的线程就能访问到客户端 socket 和地址信息
            // });
        }
    }
     ~TcpServer()
    {}
private:
    uint16_t _port;
    // 监听套接字
    int _listsockfd;
    // 回调
    callback_t _cb;
};

#endif
TcpExecServer.cc
cpp 复制代码
#include "TcpServer.hpp"
#include "ExecuteCommand.hpp"

#include <iostream>
#include <memory>
#include <string>

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << " ServerPort" << std::endl;
}

// ./tcp_echo_server 8080
int main(int argc,char *argv[])
{
    // 输出一个简单的手册
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 将日志输出方式设置为 输出到屏幕(终端/控制台) ,而不是写入文件
    ENABLE_CONSOLE_LOG_STRATEGY();
    // 智能指针
    uint16_t port = std::stoi(argv[1]);

    // TcpEchoServer tsvr;  // 不太推荐

    // 1.创建业务层软件
    std::unique_ptr<ExecuteCommand> Exec = std::make_unique<ExecuteCommand>();

    // 2.创建网络通信模块
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);

    // 3.关联(lambda)
    tsvr->Init([&Exec](std::string cmdstring)->std::string{
        return Exec->Exec(cmdstring);
    });
    tsvr->Start();

    return 0;
}
ExecuteCommand.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <vector>
#include "Logger.hpp"

// 使用了自己封装的日志,要包含命名空间
using namespace LogModule;

class ExecuteCommand
{
public:
    ExecuteCommand()
    {
        // // 黑名单机制(不行)--> 查子串,容易误删
        // // 凡是有一些命令在黑名单里面,就不允许用户使用,在名单里面的都不执行
        // _cmds.push_back("rm");  // rm -rf呢?或者来个组合命令:ls -al && rm 8 -rf呢?
        // _cmds_push_back("unlink");
        // _cmds_push_back("mkdir");
        // _cmds_push_back("cp");

        // 白名单机制
        // 只允许执行这几个在白名单里面的命令(这里放的都是查询命令)
        _white_list.push_back("pwd");
        _white_list.push_back("env");
        _white_list.push_back("who");
        _white_list.push_back("whoami");
        _white_list.push_back("ls -a -l");
    }
    ~ExecuteCommand()
    {}
private:
    // 需要一个判断命令是否安全的bool类型
    // true:安全
    // false:不安全
    bool IsSafe(const std::string &cmdstr)
    {
        for(auto &cmd : _white_list)
        {
            if(cmd == cmdstr)
                return true;
        }

        return false;
    }

public:
    // "ls -a -l" --> 结果
    std::string Exec(std::string cmdstr)
    {
        // 判断一下
        if(!IsSafe(cmdstr))
        {
            return "Unsafe";
        }
        FILE *fp = popen(cmdstr.c_str(),"r");
        if(fp == nullptr)
        {
            LOG(LogLevel::ERROR) << "exec error: " << cmdstr;
            return "error";
        }

        // 打开和关闭的中间执行处理结果
        std::string result;
        char buffer[512];
        // fgets
        while(fgets(buffer,sizeof(buffer),fp) != nullptr)
        {
            result += buffer;
            // pwd有缓冲区问题,还是指向了ls(或者不是ls就是指向了上一个解析结果)
            // 清空一下字符串
            buffer[0] = 0;  // 但是env还是有问题,可能和客户端有关系 --> 缓冲区设置256还是太小,改成4096就可以了(改大点)
        }

        // 有打开就得有关闭
        pclose(fp);
        return result;
    }
private:
    // 黑名单
    std::vector<std::string> _cmds;
    // 白名单
    std::vector<std::string> _white_list;
};

11.3.2 客户端

TcpEchoClient.cc
cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
// 头文件三件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的客户端地址
#include "InetAddr.hpp"

void Usage(std::string procname)
{
    std::cout << "Usage: " << procname << "ServerIp ServerPort" << std::endl;
}

// 运行:./TcpEchoClient server_ip server_port(客户端一定要知道服务器的IP和端口号)
int main(int argc,char *argv[])
{
    // 输出一个简单的手册
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::string ServerIp = argv[1];
    uint16_t ServerPort = std::stoi(argv[2]);   // 把字符串转成整数

    // 1.创建TCP socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // Client需要明确地进行bind吗?不需要显示绑定
    // Client必须要有socket信息,即自己的IP + Port,一定需要bind,不用用户bind,OS自动随机bind的!
    // 那么,OS什么时候bind?

    // 2.建立连接
    // a.首次建立连接的时候,Client会自动bind自己的socket信息 
    // b.向{ServerIp,ServerPort}发起建立连接的请求
    // TCP通信,是面向连接的
    InetAddr serveraddress(ServerPort,ServerIp);
    int n = connect(sockfd,serveraddress.Addr(),serveraddress.AddrLen());
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    // 3.sockfd通信过程
    while(true)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin,line);

        // TCP sockfd是全双工的
        // 发送数据
        ssize_t n = write(sockfd,line.c_str(),line.size());
        (void)n;

        // 但是env还是有问题,可能和客户端有关系 --> 缓冲区设置256还是太小,改成4096就可以了(改大点)
        // char buffer[256]; 
        char buffer[4096];  // 再读一次,这次应该就读完了
        // TCP通信,双方的地位是对等的
        n = read(sockfd,buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "-> " << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cerr << "server quit" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

    // 要关闭文件描述符
    close(sockfd);

    return 0;
}

11.3.3 运行演示


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux网络】Linux 网络编程入门:TCP Socket 编程(上)

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
yuezhilangniao1 小时前
Ansible基础 ansible入门 针对不同python3版本 - 含 Terraform 入门联动
运维·自动化·ansible
宵时待雨1 小时前
linux笔记归纳4:进程概念
linux·运维·服务器·c++·笔记
零K沁雪1 小时前
OpenV_X_N 2.5.x 配置文件选项详解
linux
凯瑟琳.奥古斯特1 小时前
力扣2760 C++滑动窗口解法
数据结构·c++·算法·leetcode·职场和发展
一勺菠萝丶1 小时前
如何在 Linux 服务器上使用 Speedtest 官方 CLI 测试带宽(小白教程)
java·服务器·前端
w1wi1 小时前
【Vibe Coding】TCP/UDP包篡改重放工具
人工智能·网络协议·tcp/ip·ai·udp·ai编程
原来是猿1 小时前
TCP Echo Server 深度解析:从单进程到线程池的演进之路(中)
linux·服务器·数据库
treesforest2 小时前
IP地址段查询完全指南:从单IP查到IPv4段批量归属地查询
网络·数据库·网络协议·tcp/ip·网络安全·运维开发
leoZ2312 小时前
Linux 环境常用服务一键部署文档(Docker 版)
运维·docker·容器