019-Linux-Socket编程-TCP

Socket编程-TCP

1. 相关接口和命令

1.1 设置套接字为监听状态-listen

c 复制代码
#include <sys/socket.h>

int listen(int sockfd, int backlog);

功能listen() 是一个系统调用函数,用于将一个主动连接的套接字(socket)转换为被动监听状态,以便接受来自客户端的连接请求。它通常在 bind() 之后、accept() 之前调用。

参数说明

  • sockfd :通过 socket() 创建并经过 bind() 绑定地址和端口的套接字文件描述符。
  • backlog :指定等待连接队列的最大长度。当有新的连接请求到达时,如果队列已满,客户端可能会收到连接错误(如 ECONNREFUSED)。

返回值

  • 成功时返回 0
  • 失败时返回 -1,并设置 errno 表示具体错误(如 EINVALEADDRINUSE 等)。

关键特性

  1. 1.被动套接字 :调用后,sockfd 不再用于发起连接,而是用于接受连接。
  2. 2.连接队列 :内核维护一个已完成连接队列(通过 accept() 获取)和一个待处理连接队列(由 backlog 限制)。
  3. 3.协议支持 :仅对 SOCK_STREAM(TCP)和 SOCK_SEQPACKET 类型的套接字有效,对 SOCK_DGRAM(UDP)无效。

1.2 获取新连接-accept

c 复制代码
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);

功能accept() 是一个系统调用函数,用于从监听套接字的连接队列中取出一个已完成的连接,并返回一个新的套接字描述符用于与客户端通信。

参数说明

  • sockfd :已通过 listen() 设置为监听状态的套接字描述符。
  • addr :(可选)指向 struct sockaddr 结构的指针,用于存储客户端的地址信息。如果为 NULL,则不获取地址。
  • addrlen :(可选)指向 socklen_t 的指针,用于指定 addr 缓冲区的大小(输入),并返回实际地址长度(输出)。如果 addrNULL,则此参数应设为 0

返回值

  • 成功时,返回一个新的套接字描述符,用于与客户端通信。
  • 失败时,返回 -1,并设置 errno(如 EAGAINEWOULDBLOCK 等)。

关键特性

  1. 阻塞行为 :默认情况下,accept() 会阻塞直到有连接到达。若套接字设置为非阻塞(如 fcntl 设置 O_NONBLOCK),则在没有连接时立即返回 -1 并设置 EAGAINEWOULDBLOCK
  2. 新套接字 :返回的套接字描述符是全新的,与原始监听套接字 sockfd 无关。原始 sockfd 继续监听更多连接。
  3. 地址信息 :若提供 addraddrlen,可获取客户端的 IP 和端口,用于日志或访问控制。

1.3 发起新链接-connect

c 复制代码
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能connect() 是一个系统调用函数,用于客户端发起与服务器的连接。它尝试建立一个到指定地址和端口的连接,对于 TCP 套接字,这将完成三次握手过程。

参数说明

  • sockfd :客户端通过 socket() 创建的套接字描述符。
  • addr :指向目标服务器地址结构的指针(如 struct sockaddr_in 用于 IPv4)。
  • addrlen:地址结构的大小(以字节为单位)。

返回值

  • 成功时返回 0(连接已建立或已排队)。
  • 失败时返回 -1,并设置 errno 表示具体错误。

关键特性

  1. 阻塞行为 :默认情况下,connect() 会阻塞直到连接成功建立或失败(超时)。对于非阻塞套接字(使用 fcntl 设置 O_NONBLOCK),connect() 会立即返回 -1 并设置 errnoEINPROGRESS,表示连接正在后台进行,需要后续检查连接状态。

  2. 不同协议的行为

    • TCP(SOCK_STREAM):执行三次握手,建立可靠连接。

    • UDP(SOCK_DGRAM) :不实际建立连接,但会将地址信息关联到套接字,后续 send()/recv() 可以省略地址参数。

    • 本地套接字(AF_UNIX):连接到本地文件系统上的套接字。

  3. 错误处理

    常见错误码:

    • ECONNREFUSED:服务器拒绝连接(无监听进程或队列满)
    • ETIMEDOUT:连接超时
    • ENETUNREACH:网络不可达
    • EALREADY:套接字已有正在进行的连接(非阻塞模式)
  4. bind() 的关系

    客户端通常不需要显式调用 bind(),系统会自动分配一个临时端口(通常称为"ephemeral port")。如果需要指定客户端端口,可以先 bind()connect()

1.4 从套接字中读取信息-recv

cpp 复制代码
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能recv() 是一个系统调用函数,用于从已连接的套接字(TCP)或已绑定地址的套接字(UDP)接收数据。它是网络编程中数据接收的核心函数。

参数说明

  • sockfd:已连接的套接字描述符(TCP)或已绑定地址的套接字(UDP)。
  • buf:指向存储接收数据的缓冲区的指针。
  • len:缓冲区的最大长度(以字节为单位)。
  • flags:接收操作的标志,用于控制行为(可为0或按位或组合多个标志)。

返回值

  • >0:实际接收的字节数。
  • 0:对端关闭了连接(对于TCP,表示连接已正常关闭)。
  • -1 :发生错误,errno 被设置为具体错误码(如 EAGAINEWOULDBLOCK 等)。

关键特性

  1. 阻塞行为 :默认情况下,recv() 会阻塞直到有数据到达或连接关闭。如果套接字设置为非阻塞(fcntl 设置 O_NONBLOCK),且没有数据可用,则立即返回 -1 并设置 errnoEAGAINEWOULDBLOCK
  2. 协议差异
    • TCP(SOCK_STREAM) :提供可靠、有序、无边界的数据流。recv() 返回的字节数可能小于请求的长度(len),因为TCP是流式协议。
    • UDP(SOCK_DGRAM) :提供不可靠、有边界的数据报。每次调用 recv() 通常返回一个完整的数据报(如果缓冲区足够大),否则数据会被截断(部分数据丢失)。
  3. 接收模式
    • 标准接收recv(sockfd, buf, len, 0),阻塞或非阻塞模式。
    • 窥视数据 :使用 MSG_PEEK 标志,读取数据但不从接收队列中移除。
    • 等待数据 :使用 MSG_WAITALL 标志,阻塞直到接收到指定长度的数据(TCP)或一个完整数据报(UDP)。

标志(flags)选项

标志 描述
0 默认行为,阻塞接收
MSG_DONTWAIT 非阻塞模式,等效于设置套接字非阻塞
MSG_PEEK 查看数据但不从队列中移除
MSG_WAITALL 阻塞直到接收到指定长度的数据(TCP)或一个完整数据报(UDP)
MSG_OOB 接收带外数据(紧急数据,TCP)
MSG_TRUNC 对于数据报套接字,即使数据被截断也返回实际长度
MSG_ERRQUEUE 接收错误队列中的数据(需要 SOL_SOCKET 层)

1.5 向套接字发送信息-send

C 复制代码
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能send() 是一个系统调用函数,用于向已连接的套接字(TCP)或已指定地址的套接字(UDP)发送数据。它是网络编程中数据发送的核心函数。

参数说明

  • sockfd:已连接的套接字描述符(TCP)或已绑定地址的套接字(UDP)。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送数据的长度(以字节为单位)。
  • flags:发送操作的标志,用于控制行为(可为0或按位或组合多个标志)。

返回值

  • >0 :实际发送的字节数(可能小于请求的长度 len)。
  • -1 :发生错误,errno 被设置为具体错误码(如 EAGAINEWOULDBLOCKEPIPE 等)。

关键特性

  1. 阻塞行为 :默认情况下,send() 会阻塞直到数据被发送到内核发送缓冲区(对于TCP)或发送完成(对于UDP)。如果套接字设置为非阻塞(fcntl 设置 O_NONBLOCK),且内核发送缓冲区已满,则立即返回 -1 并设置 errnoEAGAINEWOULDBLOCK

  2. 协议差异

    • TCP(SOCK_STREAM) :提供可靠、有序的数据流。send() 将数据放入内核发送缓冲区,由TCP协议栈负责传输。返回值可能小于 len,表示部分数据已放入缓冲区。
    • UDP(SOCK_DGRAM) :提供不可靠、有边界的数据报。send() 通常将整个数据报一次性发送(如果 len 不超过最大数据报大小)。返回值等于 len 表示成功发送。
  3. 发送模式

    • 标准发送send(sockfd, buf, len, 0),阻塞或非阻塞模式。
    • 快速发送 :使用 MSG_DONTWAIT 标志,非阻塞模式。
    • 发送带外数据 :使用 MSG_OOB 标志(仅TCP)。
  4. 连接状态

    • 对于TCP,如果对端关闭了连接,send() 可能会触发 SIGPIPE 信号(默认终止进程),并返回 -1errnoEPIPE。使用 MSG_NOSIGNAL 标志可以避免信号。
    • 对于UDP,发送到未连接的套接字需要指定地址(使用 sendto()),但已连接的UDP套接字可以使用 send()

标志(flags)选项

标志 描述
0 默认行为,阻塞发送
MSG_DONTWAIT 非阻塞模式,等效于设置套接字非阻塞
MSG_NOSIGNAL 当对端关闭连接时,不发送 SIGPIPE 信号
MSG_OOB 发送带外数据(紧急数据,TCP)
MSG_CONFIRM 仅用于UDP,请求确认(已弃用,由内核自动处理)
MSG_MORE 表示还有更多数据要发送(Linux特有,用于TCP)
MSG_NOSIGNAL 防止对端关闭时产生 SIGPIPE 信号
MSG_FASTOPEN 用于TCP快速打开(需要内核支持)

1.6 执行shell命令-popen

c 复制代码
#include <stdio.h>

FILE *popen(const char *command, const char *type);

功能popen() 是一个标准库函数,用于执行一个 shell 命令并创建一个管道,允许与该命令的标准输入、输出或错误流进行交互。

参数说明

  • command:要执行的 shell 命令字符串。
  • type :指定管道模式,可以是:
    • "r":从命令的标准输出读取数据
    • "w":向命令的标准输入写入数据
    • "r+":可读写(不常用)
    • "w+":可读写(不常用)

返回值

  • 成功:返回指向 FILE 流的指针,用于与命令的输入/输出交互(该指针需要使用pclose关闭)。
  • 失败 :返回 NULL,并设置 errno(如 EMFILEEINVAL 等)。

关键特性

  1. 执行环境
    • popen() 通过 shell(通常是 /bin/sh)执行命令,因此可以使用 shell 语法(管道、重定向等)。
    • 命令字符串可以包含 shell 特殊字符,如 |><$ 等。
  2. 管道创建
    • 对于 "r" 模式:创建一个管道,命令的标准输出连接到返回的 FILE 流。
    • 对于 "w" 模式:创建一个管道,命令的标准输入连接到返回的 FILE 流。
  3. 进程管理
    • popen() 会 fork 一个子进程来执行命令。
    • 必须使用 pclose() 关闭 FILE 流并等待子进程结束,否则会产生僵尸进程。

2. 前提头文件

下面代码中会使用我们之前文章中实现过的一些头文件。

2.1 nocopy.hpp

以nocopy为基类的文件无法被拷贝,无法被复制。

cpp 复制代码
#pragma once

// 不允许派生类对象进行拷贝操作
class nocopy
{
public:
    nocopy() {}
    ~nocopy() {}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

2.2 LockGura.hpp

RAII方式加锁,出作用域即释放。

cpp 复制代码
#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};

2.3 Log.hpp

日志头文件,格式化输出日志,方便调试和观察程序运行。

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <cstdarg>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _pid;
        std::string _filename;
        int _line;
        std::string _time;
        std::string _message;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _type(SCREEN_TYPE), _logfile(logfile)
        {
        }

        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._pid,
                   lg._filename.c_str(),
                   lg._line,
                   lg._time.c_str(),
                   lg._message.c_str());
        }

        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;

            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._pid,
                     lg._filename.c_str(),
                     lg._line,
                     lg._time.c_str(),
                     lg._message.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            LockGuard LockGuard(&glock); // 加锁,为了防止多个线程同时打印日志造成输出信息错乱。
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }

        void logMessage(std::string filename, int line, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._pid = getpid();
            lg._filename = filename;
            lg._line = line;
            lg._time = GetTime();

            va_list ap;
            va_start(ap, format);
            char log_msg[1024];
            vsnprintf(log_msg, sizeof(log_msg), format, ap);
            va_end(ap);
            lg._message = log_msg;

            // print log
            FlushLog(lg);
        }

        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)

#define LOG(Level, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)

}

2.4 Thread.hpp

对线程进行封装,方便使用。

cpp 复制代码
#pragma once

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

using namespace log_ns;

namespace ThreadMoudle
{
    using func_t = std::function<void(const std::string&)>;

    class Thread
    {
    public:
        void Excute()
        {
            LOG(DEBUG, "%s, is running\n", _name.c_str());
            _isrunning = true;
            _func(_name);
            _isrunning = false;
        }
    public:
        Thread(const std::string &name, func_t func):_name(name),_func(func)
        {
            LOG(DEBUG, "create %s done\n", name.c_str());
        }

        static void *ThreadRoutine(void *args)
        {
            Thread *self = static_cast<Thread*>(args);
            self->Excute();
            return nullptr;
        }

        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0) return false;
            return true;
        }

        std::string Status()
        {
            if (_isrunning) return "running";
            return "sleep";
        }

        void Stop()
        {
            if (_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
                LOG(DEBUG, "%s, stop\n", _name.c_str());
            }
        }

        void Join()
        {
            if (!_isrunning)
            {
                ::pthread_join(_tid, nullptr);
                LOG(DEBUG, "%s, joined\n", _name.c_str());
            }
        }

        std::string Name()
        {
            return _name;
        }

        ~Thread()
        {

        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func;
    };
}

2.5 ThreadPool.hpp

单例模式的线程池。

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <functional>
#include <queue>
#include "Thread.hpp"
#include "LockGuard.hpp"

using namespace ThreadMoudle;

static const int gdefaultnum = 5;

template<typename T>
class ThreadPool
{
private:
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }

    void WakeupAll()
    {
        pthread_cond_broadcast(&_cond);
    }

    bool IsEmpty()
    {
        return _task_queue.empty();
    }

    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    void HandlerTask(const std::string &name)
    {
        while (true)
        {
            // 获取任务
            LockQueue();

            // 任务列表为空,且主线程没有调用Stop
            while (IsEmpty() && _isrunning) 
            {
                _sleep_thread_num++;
                LOG(DEBUG, "%s, sleep begin\n", name.c_str());
                Sleep();
                LOG(DEBUG, "%s, sleep end\n", name.c_str());
                _sleep_thread_num--;
            }
            // 任务列表为空,且主线程已调用Stop,线程解锁并退出
            if (IsEmpty() && !_isrunning)
            {
                LOG(DEBUG, "%s, quit\n", name.c_str());
                UnlockQueue();
                break;
            }
            // 任务列表不为空,无论主线程是否调用Stop,都要把剩余任务执行完
            LOG(DEBUG, "%s execute the task\n", name.c_str());
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();

            // 执行任务
            t();
        }
    }

    ThreadPool(int thread_num = gdefaultnum):_thread_num(thread_num), _isrunning(false), _sleep_thread_num(0)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ThreadPool(const ThreadPool<T> &) = delete;
    void operator= (const ThreadPool<T> &) = delete;

    void Init()
    {
        func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        for (int i = 0; i < _thread_num; i++)
        {
            std::string threadname = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(threadname, func);
        }
    }

    void Start()
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            thread.Start();
        }
    }
public:
    void Stop()
    {
        LockQueue();
        _isrunning = false;
        WakeupAll();
        UnlockQueue();
    }

    static ThreadPool<T> *GetInstance()
    {
        if (_tp == nullptr)
        {
            LockGuard lockguard(&_sig_mutex);
            if (_tp == nullptr)
            {
                _tp = new ThreadPool();
                _tp->Init();
                _tp->Start();
            }
        }
        
        return _tp;
    }

    void Equeue(const T &in)
    {
        LockQueue();
        if (_isrunning) // 只有处于运行状态,才可以添加任务。
        {
            _task_queue.push(in);
            if (_sleep_thread_num) Wakeup();
        }
        UnlockQueue();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
private:
    int _thread_num;
    std::vector<Thread> _threads;
    std::queue<T> _task_queue;
    bool _isrunning;

    int _sleep_thread_num;

    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    // 单例模式
    static ThreadPool<T> *_tp;
    static pthread_mutex_t _sig_mutex;
};

template<typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;

template<typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;

2.6 InetAddr.hpp

对struct sockaddr_in类进行封装,方便获取和使用。

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <arpa/inet.h>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }
public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }

    ~InetAddr()
    {}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

3. V1 - TCP Echo Server - 远程回显信息

这里我们以tcp的方式实现对客户端发来的信息进行回显。

其中包括四个版本:缺陷版、多进程版、多线程版、线程池版,具体在TcpServer类中有注释说明。

3.1 TcpServer.hpp

封装TcpServer类,负责获取客户端的新连接,然后将新的连接进行处理。

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>

#include <cstring>

#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#include "InetAddr.hpp"
#include "nocopy.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"

using namespace log_ns;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR
};

const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;

using task_t = std::function<void()>;

class TcpServer : public nocopy
{
public:
    struct ThreadData
    {
        ThreadData(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr)
        {}
        
        int _sockfd;
        TcpServer* _self;
        InetAddr _addr;
    };

    TcpServer(uint16_t port = gport): _port(port), _listensockfd(gsock), _isrunning(false)
    {}
    
    ~TcpServer(){}

    void Init()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "socket create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success, sockfd = %d\n", _listensockfd);

        // 2.bind socket和addr
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (::bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");

        // 3.tcp是面向连接的,tcp需要未来不断的能够获取连接
        if (::listen(_listensockfd, gblcklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO, "listen success\n");
    }

    void Service(int sockfd, InetAddr addr)
    {
        // 长服务
        while(true)
        {
            // TCP是面向字节流的,这里类似于读写文件,可以直接使用read/write进行读写
            char inbuffer[1024];
            ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = '\0';
                LOG(INFO, "client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);

                std::string echo_string = "[server echo] # ";
                echo_string += inbuffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }

    static void* Execute(void* args)
    {
        pthread_detach(pthread_self());// 线程分离,无需被join
        ThreadData* td = static_cast<ThreadData*>(args);
        td->_self->Service(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensockfd, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", addr.AddrStr().c_str(), sockfd);

            // 2.进行通信
            // V1 --- 缺陷版 --- 一次只能和一个客户端建立连接,无并发处理能力
            // Service(sockfd, addr);

            // V2 --- 多进程版 --- 具备并发处理能力,但开销较大,效率较低
            // pid_t id = ::fork();
            // if (id == 0)
            // {
            //     // child
            //     ::close(_listensockfd); // 建议关闭无用文件描述符,防止误操作

            //     // 让父进程不必等待子进程也可以屏蔽SIGCHLD信号(建议),或者使用非阻塞轮询方案,这两个方案之前使用过,这里介绍一个新的方案
            //     // 子进程创建孙子进程,然后子进程退出,马上被父进程等待,孙子进程变成孤儿进程,被bash领养
            //     // 从此以后孙子进程即使执行完,也不需要父进程等待了,OS会自行回收
            //     if (fork() > 0) exit(0);

            //     // 这里实际上是孙子进程再执行
            //     Service(sockfd, addr);
            //     exit(0);
            // }
            // // father
            // ::close(sockfd); // 必须关闭,否则会导致新连接一直在打开但是没关闭,会导致文件描述符泄露,并且可以防止被后面的子进程继承。
            // int n = ::waitpid(id, nullptr, 0);
            // if (n > 0)
            // {
            //     LOG(INFO, "wait child success\n");
            // }

            // V3 -- 多线程版 --- 主线程不能关闭文件描述符,线程共享文件描述符
            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd, this, addr);
            pthread_create(&tid, nullptr, Execute, td);

            // V4 -- 线程池版 --- 像这种长服务并不适合使用线程池,因为线程池的线程数量是有限的,如果用户数量超过线程池中的线程上限,会出问题
            // 因此,还是推荐使用上一个版本,这里只是为了熟悉代码而实现一份
            // task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
            // ThreadPool<task_t>::GetInstance()->Equeue(t);
        }
        _isrunning = false;
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

3.2 ServerMain.cc

服务端框架,创建TcpServer对象,并运行。

cpp 复制代码
#include <memory>
#include "TcpServer.hpp"

// ./server 8888
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

3.3 ClientMain.cc

客户端逻辑,创建套接字,向服务器发送信息,接收信息并回显。

cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace log_ns;

enum
{
    SOCKET_ERROR = 1,
    CONNECT_ERROR
};

// ./client server-ip server-port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.创建socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        LOG(FATAL, "create socket error\n");
        exit(SOCKET_ERROR);
    }
    LOG(INFO, "socket create success, sockfd = %d\n", sockfd);

    // 2.不需要显示bind,但是一定要都自己的ip和port,隐式bind,OS会自动bind,用自己的ip和随机端口号

    // 3.向指定服务器发起连接
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

    int n = ::connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if (n < 0)
    {
        LOG(FATAL, "connect error\n");
        exit(CONNECT_ERROR);
    }
    LOG(INFO, "connect success\n");

    while(true)
    {
        std::string message;
        std::cout << "Enter # ";
        std::getline(std::cin, message);

        write(sockfd, message.c_str(), message.size());

        char echo_buffer[1024];
        n = read(sockfd, echo_buffer, sizeof(echo_buffer) - 1);
        if (n > 0)
        {
            echo_buffer[n] = '\0';
            std::cout << echo_buffer << std::endl;
        }
        else
        {
            LOG(FATAL, "read error\n");
            break;
        }
    }

    ::close(sockfd);

    return 0;
}

3.4 运行展示

4. V2 - TCP Command Server - 远程命令执行

这里我们希望服务端接收到客户端发送的命令,直接进行执行,然后将结果返回给客户端。

对于客户端,我们给一个命令白名单,只有白名单里的命令运行执行。

【注意】对于下面没有提到改造的头文件,直接使用上面的即可。

4.1 Command.hpp

对于TcpServer,我们只希望它接受新连接,不处理消息,而是将消息交给外部处理,让接收消息和处理消息进行解耦,这里封装Command类专门来进行输入命令的处理。

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <set>

#include <cstdio>
#include <cstring>

#include "InetAddr.hpp"
#include "Log.hpp"

class Command
{
public:
    Command()
    {
        // 命令白名单,也可以读取配置文件,这里为了方便就直接写了
        _safe_command.insert("ls");
        _safe_command.insert("pwd");
        _safe_command.insert("which");
        _safe_command.insert("touch");
    }

    ~Command(){}

    bool SafeCheck(const std::string cmdstr) // 简单的安全检查,但是还是存在漏洞,这里就不完善了,否则会非常复杂
    {
        int pos = 0;
        while (pos < cmdstr.size() && cmdstr[pos] != ' ') pos++;
        return _safe_command.count(std::string(cmdstr.begin(), cmdstr.begin() + pos));
    }

    std::string Execute(const std::string& cmdstr)
    {
        std::string result;
        FILE* fp = popen(cmdstr.c_str(), "r");
        if (fp)
        {
            char line[1024];
            while (fgets(line, sizeof(line), fp)) result += line;
            return result.empty() ? cmdstr + " execute success" : result; // 对于没有返回结果的命令返回执行成功提示
        }
        else
        {
            return "execute error";
        }
    }

    void HandlerCommand(int sockfd, InetAddr addr)
    {
        while(true)
        {
            char commandbuf[1024];
            ssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0);
            if (n > 0)
            {
                commandbuf[n] = '\0';
                LOG(INFO, "client %s, command: %s\n", addr.AddrStr().c_str(), commandbuf);
                std::string result = Execute(commandbuf);
                ::send(sockfd, result.c_str(), result.size(), 0);
            }
            else if (n == 0)
            {
                LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
    }
private:
    std::set<std::string> _safe_command;
};

4.2 TcpServer.hpp

改造TcpServer,使处理消息与之解耦。

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>

#include <cstring>

#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#include "InetAddr.hpp"
#include "nocopy.hpp"
#include "Log.hpp"

using namespace log_ns;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR
};

const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;

using command_service_t = std::function<void(int sockfd, InetAddr addr)>;

class TcpServer : public nocopy
{
public:
    struct ThreadData
    {
        ThreadData(int sockfd, TcpServer* self, const InetAddr& addr): _sockfd(sockfd), _self(self), _addr(addr)
        {}
        
        int _sockfd;
        TcpServer* _self;
        InetAddr _addr;
    };

    TcpServer( command_service_t service, uint16_t port = gport): _port(port), _listensockfd(gsock), _isrunning(false), _service(service)
    {}
    
    ~TcpServer(){}

    void Init()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "socket create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success, sockfd = %d\n", _listensockfd);

        // 2.bind socket和addr
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (::bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");

        // 3.tcp是面向连接的,tcp需要未来不断的能够获取连接
        if (::listen(_listensockfd, gblcklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO, "listen success\n");
    }

    static void* Execute(void* args)
    {
        pthread_detach(pthread_self());// 线程分离,无需被join
        ThreadData* td = static_cast<ThreadData*>(args);
        td->_self->_service(td->_sockfd, td->_addr);
        ::close(td->_sockfd);
        delete td;
        return nullptr;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensockfd, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO, "accept success, get a new link, client: %s, sockfd: %d\n", addr.AddrStr().c_str(), sockfd);

            // 2.进行通信
            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd, this, addr);
            pthread_create(&tid, nullptr, Execute, td);
        }
        _isrunning = false;
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;

    command_service_t _service;
};

4.3 ServerMain.cc

将Commcand中处理命令的接口进行bind并传到TcpServer中。

cpp 复制代码
#include <memory>
#include "TcpServer.hpp"
#include "Command.hpp"

// ./server 8888
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    Command cmdservice;

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2), port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

4.4 运行展示

相关推荐
A.A呐7 小时前
【Linux第六章】进程状态和优先级
linux
iambooo8 小时前
Shell在日志分析与故障排查中的实战应用
linux·服务器·网络
一路往蓝-Anbo8 小时前
第 9 章:Linux 设备树 (DTS) ——屏蔽与独占外设
linux·运维·服务器·人工智能·stm32·嵌入式硬件
钛态8 小时前
Flutter for OpenHarmony:dio_cookie_manager 让 Dio 发挥会话管理能力,像浏览器一样自动处理 Cookie 深度解析与鸿蒙适配指南
android·linux·运维·flutter·ui·华为·harmonyos
王码码20358 小时前
Flutter for OpenHarmony:Flutter 三方库 bluez 玩转 Linux 风格的蓝牙操作(蓝牙底层互操作)
linux·运维·服务器·前端·flutter·云原生·harmonyos
A.A呐9 小时前
【Linux第七章】进程切换和命令行参数
linux
抓饼先生9 小时前
iceoryx编译和验证
linux·c++·零拷贝·iceoryx
栈低来信10 小时前
SLUB分配器
linux
吕司10 小时前
Linux信号产生
linux·运维·服务器