【探索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日志并进行多维度分析。
目录
相关文章
|
15天前
|
移动开发 网络协议 NoSQL
不为人知的网络编程(十七):冰山之下,一次网络请求背后的技术秘密
本文将抛弃千篇一律的计网知识理论,从现实的互联网技术实践角度,一步步为你分享一次网络请求背后的技术秘密。
41 0
|
9天前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
13 1
|
20天前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
28 1
|
25天前
|
网络协议 Linux 网络性能优化
Linux C/C++之TCP / UDP通信
这篇文章详细介绍了Linux下C/C++语言实现TCP和UDP通信的方法,包括网络基础、通信模型、编程示例以及TCP和UDP的优缺点比较。
31 0
Linux C/C++之TCP / UDP通信
|
1月前
|
网络协议 Linux 网络性能优化
Linux基础-socket详解、TCP/UDP
综上所述,Linux下的Socket编程是网络通信的重要组成部分,通过灵活运用TCP和UDP协议,开发者能够构建出满足不同需求的网络应用程序。掌握这些基础知识,是进行更复杂网络编程任务的基石。
77 1
|
2月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
37 5
|
2月前
|
存储 机器人 Linux
Netty(二)-服务端网络编程常见网络IO模型讲解
Netty(二)-服务端网络编程常见网络IO模型讲解
|
30天前
|
JSON API 开发者
深入解析Python网络编程与Web开发:urllib、requests和http模块的功能、用法及在构建现代网络应用中的关键作用
深入解析Python网络编程与Web开发:urllib、requests和http模块的功能、用法及在构建现代网络应用中的关键作用
14 0
|
11天前
|
运维 安全 Linux
Linux中传输文件文件夹的10个scp命令
【10月更文挑战第18天】本文详细介绍了10种利用scp命令在Linux系统中进行文件传输的方法,涵盖基础文件传输、使用密钥认证、复制整个目录、从远程主机复制文件、同时传输多个文件和目录、保持文件权限、跨多台远程主机传输、指定端口及显示传输进度等场景,旨在帮助用户在不同情况下高效安全地完成文件传输任务。
100 5