【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

引言

在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。本文将延续上文内容,重点讨论简单的TCP网络程序模拟实现。通过本文的学习,读者将能够深入了解TCP协议的实际应用,并掌握如何编写简单的TCP网络程序。让我们一起深入探讨TCP网络程序的实现细节,为网络编程的学习之旅添上一份精彩的实践经验。

一、TCP协议

TCP(Transmission Control Protocol)是一种面向连接的通信协议,它要求在数据传输前先建立连接,以确保数据的可靠传输。TCP通过序号、确认和重传等机制来保证数据的完整性和可靠性,同时还实现了拥塞控制和流量控制,以适应不同网络环境下的数据传输需求。由于TCP的可靠性和稳定性,它被广泛应用于网络通信中,包括网页浏览、文件传输、电子邮件等各种应用场景,成为互联网协议套件中的重要组成部分。详介绍可以看上一篇文章:UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同

二、TCP网络程序模拟实现

接下来,我们打算运用线程池技术,模拟实现一个简单的TCP网络程序。通过充分利用线程池,我们能够更有效地管理并发连接,从而提高程序的性能和稳定性。这一实践将有助于加深我们对网络编程关键概念和技术的理解和掌握。在前文中已经提到了线程池,这里就不再赘述其原理和作用。详细可以点击传送门:🚩 线程池

1. 预备代码

⭕ThreadPool.hpp(线程池)

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

// 线程信息结构体
struct ThreadInfo
{
    pthread_t tid;  // 线程ID
    std::string name;  // 线程名称
};

static const int defalutnum = 10;  // 默认线程池大小为10

template <class T>
class ThreadPool
{
public:
    void Lock() // 加锁
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock() // 解锁
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup() // 唤醒等待中的线程
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep() // 线程休眠
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty() // 判断任务队列是否为空
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid) // 获取线程名称
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args) // 线程任务处理函数
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty()) // 若任务队列为空,则线程等待
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop(); // 从任务队列中取出任务
            tp->Unlock();

            t(); // 执行任务
        }
    }
    void Start() // 启动线程池
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); // 创建线程
        }
    }
    T Pop() // 从任务队列中取出任务
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t) // 将任务推入任务队列
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance() // 获取线程池实例
    {
        if (nullptr == tp_) // 若线程池实例为空
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_) // 双重检查锁
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>(); // 创建线程池实例
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num) // 构造函数
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool() // 析构函数
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete; // 禁用拷贝构造函数
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 禁用赋值操作符,避免 a=b=c 的写法
private:
    std::vector<ThreadInfo> threads_; // 线程信息数组
    std::queue<T> tasks_; // 任务队列

    pthread_mutex_t mutex_; // 互斥锁
    pthread_cond_t cond_; // 条件变量

    static ThreadPool<T> *tp_; // 线程池实例指针
    static pthread_mutex_t lock_; // 静态互斥锁
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr; // 初始化线程池实例指针为nullptr

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; // 初始化静态互斥锁

以上代码实现了一个简单的线程池模板类 ThreadPool,其中包含了线程池的基本功能和操作。

  1. 首先定义了一个线程信息结构体 ThreadInfo,用来保存线程的ID和名称。
  2. 然后定义了一个模板类 ThreadPool,其中包含了线程池的各种操作和属性:
  • Lock()Unlock() 分别用于加锁和解锁。
  • Wakeup() 用于唤醒等待中的线程。
  • ThreadSleep() 用于使线程进入休眠状态。
  • IsQueueEmpty() 判断任务队列是否为空。
  • GetThreadName() 根据线程ID获取线程名称。
  1. 定义了静态成员函数 HandlerTask,作为线程的任务处理函数。在该函数中,线程会不断地从任务队列中取出任务并执行。
  2. Start() 函数用于启动线程池,创建指定数量的线程,并将线程的任务处理函数设置为 HandlerTask
  3. Pop() 函数用于从任务队列中取出任务。
  4. Push() 函数用于将任务推入任务队列。
  5. GetInstance() 函数用于获取线程池的实例,采用了双重检查锁(Double-Checked Locking)实现单例模式。
  6. 线程池的构造函数和析构函数分别用于初始化和销毁互斥锁和条件变量。
  7. 最后使用静态成员变量初始化了线程池实例指针和静态互斥锁。

⭕makefile文件

.PHONY:all
all:tcpserverd tcpclient

tcpserverd:Main.cc
  g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
  g++ -o $@ $^ -std=c++11


.PHONY:clean
clean:
  rm -f tcpserverd tcpclient

这段代码是一个简单的 Makefile 文件,用于编译生成两个可执行文件 tcpserverdtcpclient

  1. .PHONY: all:声明 all 为一个伪目标,表示 all 不是一个实际的文件名,而是一个指定的操作。
  2. all: tcpserverd tcpclient:定义了 all 目标,它依赖于 tcpserverdtcpclient 目标。当执行 make all 时,会先编译 tcpserverdtcpclient
  3. tcpserverd: Main.cc:定义了生成 tcpserverd 可执行文件的规则,依赖于 Main.cc 源文件。使用 g++ 编译器进行编译,指定输出文件名为 tcpserverd,使用 C++11 标准,并链接 pthread 库。
  4. tcpclient: TcpClient.cc:定义了生成 tcpclient 可执行文件的规则,依赖于 TcpClient.cc 源文件。同样使用 g++ 编译器进行编译,指定输出文件名为 tcpclient,使用 C++11 标准。
  5. .PHONY: clean:声明 clean 为一个伪目标。
  6. clean: rm -f tcpserverd tcpclient:定义了 clean 目标,用于清理生成的可执行文件。执行 make clean 时将删除 tcpserverdtcpclient 可执行文件。

⭕打印日志文件

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen; // 默认打印方式为屏幕输出
        path = "./log/"; // 默认日志文件路径为当前目录下的"log/"目录
    }

    void Enable(int method)
    {
        printMethod = method; // 设置打印方式
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl; // 在屏幕上输出日志信息
            break;
        case Onefile:
            printOneFile(LogFile, logtxt); // 将日志信息写入单个文件中
            break;
        case Classfile:
            printClassFile(level, logtxt); // 根据日志级别将日志信息写入不同的文件中
            break;
        default:
            break;
        }
    }

    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname; // 拼接日志文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打开或创建一个文件,以追加方式写入
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size()); // 将日志信息写入文件
        close(fd); // 关闭文件
    }

    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // 生成日志文件名,例如"log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt); // 将日志信息写入对应级别的文件中
    }

    ~Log()
    {
    }

    // 重载()运算符,用于输出日志信息
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t); // 获取当前时间

        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 格式化左侧部分,包括日志级别和时间信息

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // 格式化右侧部分,即用户自定义的日志内容
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // 拼接左右两侧的日志内容
        printLog(level, logtxt); // 打印日志信息
    }

private:
    int printMethod; // 打印方式
    std::string path; // 日志文件路径
};

这段代码是一个简单的日志记录类 Log,它提供了几种不同的日志输出方式和日志级别。

  • #pragma once: 使用编译器指令,确保头文件只被编译一次。
  • 定义了一些常量:
  • SIZE: 缓冲区大小为 1024。
  • 日志级别常量:Info, Debug, Warning, Error, Fatal
  • 打印方式常量:Screen, Onefile, Classfile
  • 日志文件名常量:LogFile
  • Log类包含以下成员函数和变量:
  • printMethod: 记录当前的打印方式,默认为屏幕输出。
  • path: 日志文件路径,默认为"./log/"。
  • 构造函数 Log() 初始化 printMethodpath
  • Enable(int method): 设置日志的打印方式。
  • levelToString(int level): 将日志级别转换为对应的字符串。
  • printLog(int level, const std::string &logtxt): 根据打印方式输出日志信息。
  • printOneFile(const std::string &logname, const std::string &logtxt): 将日志信息写入单个文件中。
  • printClassFile(int level, const std::string &logtxt): 根据日志级别将日志信息写入不同的文件中。
  • 析构函数 ~Log()
  • 重载的函数调用运算符 operator(): 接受日志级别和格式化字符串,格式化输出日志信息到不同的输出位置。

⭕将当前进程转变为守护进程

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null"; // 定义空设备文件路径

// 将当前进程变为守护进程的函数
void Daemon(const std::string &cwd = "")
{
    // 1. 忽略一些异常信号,以避免对守护进程造成影响
    signal(SIGCLD, SIG_IGN); // 忽略子进程结束信号
    signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号
    signal(SIGSTOP, SIG_IGN); // 忽略终止信号

    // 2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话
    if (fork() > 0)
        exit(0); // 父进程退出
    setsid(); // 创建新的会话,并成为该会话的首进程

    // 3. 更改当前调用进程的工作目录,如果指定了工作目录则切换到相应目录
    if (!cwd.empty())
        chdir(cwd.c_str()); // 切换工作目录到指定路径

    // 4. 将标准输入,标准输出,标准错误重定向至/dev/null,关闭不需要的文件描述符
    int fd = open(nullfile.c_str(), O_RDWR); // 打开空设备文件
    if (fd > 0)
    {
        dup2(fd, 0); // 标准输入重定向至空设备
        dup2(fd, 1); // 标准输出重定向至空设备
        dup2(fd, 2); // 标准错误重定向至空设备
        close(fd); // 关闭打开的文件描述符
    }
}

这段代码实现了将当前进程转变为守护进程的函数 Daemon

  1. 忽略一些异常信号,避免对守护进程产生影响。
  2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话。
  3. 更改当前调用进程的工作目录,如果指定了工作目录,则切换到相应目录。
  4. 将标准输入、标准输出和标准错误重定向至 /dev/null,即空设备文件,关闭不需要的文件描述符,确保守护进程不产生输出和错误信息。

2. TCP 服务器端实现(TcpServer.hpp)

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 最大连接请求队列长度

extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer;

// 线程数据结构,用于传递给线程处理函数
class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

// TCP服务器类
class TcpServer
{
public:
    // 构造函数,初始化端口和IP地址
    TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip)
    {
    }

    // 初始化服务器
    void InitServer()
    {
        // 创建套接字
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        // 设置套接字选项,允许地址重用
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定本地地址
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(local.sin_addr));

        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info, "bind socket success, listensock_: %d");

        // 监听套接字,开始接受连接请求
        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg(Info, "listen socket success, listensock_: %d");
    }

    // 启动服务器
    void Start()
    {
        // 将当前进程变为守护进程
        Daemon();

        // 启动线程池
        ThreadPool<Task>::GetInstance()->Start();

        lg(Info, "tcpServer is running....");
        
        // 循环接受客户端连接并处理
        while(true)
        {
            // 获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); 
                continue;
            }

            // 获取客户端IP和端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            // 打印客户端连接信息
            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);

            // 创建任务对象并加入线程池处理
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

    // 析构函数
    ~TcpServer() {}

private:
    int listensock_;  // 监听套接字
    uint16_t port_;   // 端口号
    std::string ip_;  // IP地址
};

这段代码是一个简单的TCP服务器的实现,包括了创建套接字、绑定地址、监听连接、接受客户端连接等基本操作。

3. TCP 客户端实现(main函数)

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcpclient serverip serverport
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]);

    // 设置服务器地址信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        int cnt = 5; // 连接重试次数
        int isreconnect = false; // 是否需要重连
        int sockfd = 0;

        // 创建套接字
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }

        do
        {
            // 尝试连接服务器
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                sleep(2); // 等待一段时间后重连
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        // 与服务器建立连接后进行通信
        while (true)
        {
            std::string message;
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);

            // 向服务器发送消息
            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                std::cerr << "write error..." << std::endl;
            }

            // 从服务器接收消息并显示
            char inbuffer[4096];
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << inbuffer << std::endl;
            }
        }

        // 关闭套接字
        close(sockfd);
    }

    return 0;
}

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2天前
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
18 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
24天前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
52 3
|
1月前
|
域名解析 网络协议 安全
|
2月前
|
运维 监控 网络协议
|
2月前
|
Web App开发 缓存 网络协议
不为人知的网络编程(十八):UDP比TCP高效?还真不一定!
熟悉网络编程的(尤其搞实时音视频聊天技术的)同学们都有个约定俗成的主观论调,一提起UDP和TCP,马上想到的是UDP没有TCP可靠,但UDP肯定比TCP高效。说到UDP比TCP高效,理由是什么呢?事实真是这样吗?跟着本文咱们一探究竟!
68 10
|
1月前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
1月前
|
存储 Ubuntu Linux
2024全网最全面及最新且最为详细的网络安全技巧 (三) 之 linux提权各类技巧 上集
在本节实验中,我们学习了 Linux 系统登录认证的过程,文件的意义,并通过做实验的方式对 Linux 系统 passwd 文件提权方法有了深入的理解。祝你在接下来的技巧课程中学习愉快,学有所获~和文件是 Linux 系统登录认证的关键文件,如果系统运维人员对shadow或shadow文件的内容或权限配置有误,则可以被利用来进行系统提权。上一章中,我们已经学习了文件的提权方法, 在本章节中,我们将学习如何利用来完成系统提权。在本节实验中,我们学习了。
|
2月前
|
网络协议 Linux 网络性能优化
Linux C/C++之TCP / UDP通信
这篇文章详细介绍了Linux下C/C++语言实现TCP和UDP通信的方法,包括网络基础、通信模型、编程示例以及TCP和UDP的优缺点比较。
47 0
Linux C/C++之TCP / UDP通信
|
2月前
|
网络协议 Java API
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
68 2
|
5天前
|
SQL 安全 网络安全
网络安全与信息安全:知识分享####
【10月更文挑战第21天】 随着数字化时代的快速发展,网络安全和信息安全已成为个人和企业不可忽视的关键问题。本文将探讨网络安全漏洞、加密技术以及安全意识的重要性,并提供一些实用的建议,帮助读者提高自身的网络安全防护能力。 ####
42 17
下一篇
DataWorks