
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《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 ~> 完整代码演示)
-
- [11.1 TcpEchoServer_v1](#11.1 TcpEchoServer_v1)
-
- [11.1.1 服务端](#11.1.1 服务端)
- [11.1.2 客户端](#11.1.2 客户端)
- [11.1.3 运行演示](#11.1.3 运行演示)
- [11.2 TcpEchoServer_v2](#11.2 TcpEchoServer_v2)
-
- [11.2.1 服务端](#11.2.1 服务端)
- [11.2.2 客户端](#11.2.2 客户端)
- [11.2.3 运行演示](#11.2.3 运行演示)
- [11.3 TcpExecServer](#11.3 TcpExecServer)
-
- [11.3.1 服务端](#11.3.1 服务端)
- [11.3.2 客户端](#11.3.2 客户端)
- [11.3.3 运行演示](#11.3.3 运行演示)
- 结尾

3 ~> 开篇引入:网络编程到底在做什么?
网络编程,本质上就是跨主机的进程间通信(IPC)。我们平时上网、刷视频、聊天,底层都是不同设备上的进程在通过网络交换数据。理解了这一点,整个网络编程的学习就会清晰很多。
3.1 上网本质:下载 / 上传 = 计算机 IO
很多人对网络编程的理解停留在 "发个包、收个包",但它的底层逻辑其实和本地文件读写是一样的:
- 下载 :本质是从远端服务器的文件 / 内存,把数据读到本地。
- 上传 :本质是把本地的数据写到远端服务器。
所以,网络编程也可以被看作是一种特殊的 IO 操作 ------ 只不过操作的对象不是本地磁盘文件,而是网络另一端的进程。
3.2 进程间通信本质:IO 通信(本地 + 网络)
进程间通信(IPC)的核心,就是让两个独立的进程可以交换数据。常见的方式有:
- 本地 IPC:管道(pipe)、消息队列、共享内存、信号量。
- 网络 IPC:Socket。
Socket 是目前跨主机进程通信的通用标准,它提供了一套统一的 API,让我们可以像读写文件一样和网络上的其他进程通信。
3.3 TCP vs UDP 核心区别
这是网络编程中最经典的一对 "冤家",理解它们的区别是选择合适协议的第一步:

3.4 学习路线:接口 → 多执行流 → 线程池 → 远程命令服务
本文将按照一个从基础到实战的完整路径展开:
- 基础接口:掌握 TCP 服务端和客户端的核心系统调用(部分工作上篇博客中我们已经做过了)。
- 多执行流:解决单进程服务无法处理多客户端的问题,从多进程、多线程到线程池(上篇博客已经展示了,这里不多赘言)。
- 命令执行:学习管道、重定向与 popen,实现本地命令的执行与结果读取。
- 实战项目:将所有知识点整合,实现一个远程命令执行服务。
4 ~> (简单回顾)TCP 服务端核心流程总览
一个标准的 TCP 服务端,通常遵循固定的 "五步走" 流程。
4.1 标准五步:socket → bind → listen → accept → read/write
这是所有 TCP 服务端都遵循的流程,缺一不可:
- 简单来看一下是哪五步:
socket():创建一个套接字文件描述符,相当于向操作系统申请一个 "网络接口"。bind():给这个套接字绑定地址和端口号,让客户端知道 "去哪里找你"。listen():将套接字标记为被动连接状态,准备好接收客户端的连接请求。accept():阻塞等待客户端连接,成功后返回一个用于通信的新套接字。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
socket():创建套接字。connect():向服务端发起连接请求,建立 TCP 连接。read()/write():和服务端进行数据读写。
6.2 新的系统调用:connect()
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:向指定的服务端地址发起连接请求。如果客户端没有手动 bind,内核会自动为其分配一个临时端口。
- 参数 :
sockfd:客户端的套接字文件描述符。addr:指向服务端地址结构的指针(IP + 端口)。addrlen:地址结构的长度。
- 返回 :连接成功返回
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);
- 一句话总结 :
popen是pipe + 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 白名单机制(推荐)
- 白名单只允许执行预设的、安全的命令。
- 例如,只允许
pwd、whoami、ls、env等命令。 - 在
Exec函数中,先检查命令是否在白名单列表中,不在则直接拒绝执行。
- 例如,只允许
8.3.2 黑名单机制(不推荐)
禁止执行某些危险命令(如 rm、shutdown)。这种方法很容易被绕过,例如通过 r\m 或 ; rm -rf / 等方式注入恶意代码。
8.4 完整调用链
- 客户端通过 TCP 连接发送命令字符串(如
ls -l)。 - 服务端网络层
read到命令字符串,将其交给业务层。 - 业务层
ExecuteCommand::Exec()先检查命令是否在白名单中。 - 安全检查通过后,调用
popen执行命令并获取结果。 - 服务端网络层将结果
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 缓冲区建议设置为 4096 或 8192,过小会导致频繁的系统调用,过大则会浪费内存。
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 编程(上)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
