Socket网络编程(1)——Echo Server

目录

引言

代码部分

网络地址封装类-InetAddr.hpp

日志类-Log.hpp

线程封装类-Thread.hpp

线程池-ThreadPool.hpp

锁的封装类-LockGuard.hpp

Tcp服务端源文件-TcpServerMain.cc

Tcp服务端头文件

具体说明:

[1. 未处理阻塞和非阻塞模式](#1. 未处理阻塞和非阻塞模式)

[2. 单线程处理限制](#2. 单线程处理限制)

[1. 资源自动回收](#1. 资源自动回收)

[2. 避免死锁风险](#2. 避免死锁风险)

[3. 适用于独立运行的任务线程](#3. 适用于独立运行的任务线程)

Tcp客户端源文件-TcpClientMain.cc

数据传输特性差异

数据可靠性保证机制不同

历史和设计习惯因素


引言

我们已经讲过udp-server的3种基础业务模式了,我们现在来讲讲tcp-server的模式。我们还是跟之前一样,先讲echo-server,也就是基础的打印网络传输的数据,先见见tcp通信的基础结构,再在后面加上其他业务,只不过这回我们就直接加上多线程的版本。

代码部分

网络地址封装类-InetAddr.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#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);
        char ip_buf[32];
        ::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
        _ip = ip_buf;
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost(addr); // 将addr进行转换
    }

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

    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip && this->_port == addr._port);
    }

    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr_in Addr()
    {
        return _addr;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

这个网络地址封装类还是跟之前一样的,这里我们就不说明了。

日志类-Log.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <ctime>
#include <stdarg.h>
#include <fstream>
#include <string.h>
#include <pthread.h>


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 "UNKNOW";
        }
    }

    std::string GetCurrTime()
    {
        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 _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

    #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) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                lg._level.c_str(),
                lg._id,
                lg._filename.c_str(),
                lg._filenumber,
                lg._curr_time.c_str(),
                lg._message_info.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._id,
                    lg._filename.c_str(),
                    lg._filenumber,
                    lg._curr_time.c_str(),
                    lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            pthread_mutex_lock(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
            pthread_mutex_unlock(&glock);
        }

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

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

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

            // 打印出日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

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

    Log lg;

    #define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0)
    #define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0)
    #define EnableFile() do {lg.Enable(FILE_TYPE);}while(0)
}

日志类这里我们跟之前也是一样的。

线程封装类-Thread.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <functional>
#include <pthread.h>

namespace ThreadMudle
{
    using func_t = std::function<void(const std::string &)>;
    class Thread
    {
    public:
        void Excute()
        {
            _isrunning = true;
            _func(_name);
            _isrunning = false;
        }

    public:
        Thread(const std::string &name, func_t func)
            : _name(name), _func(func)
        {
        }
        static void *ThreadRoutine(void *args) // 新线程都会执行该方法
        {
            Thread *self = static_cast<Thread *>(args); // 获得了当前对象
            self->Excute();                             // 调用回调函数func的方法
            return nullptr;
        }

        bool Start() // 启动线程
        {
            //::强调此调用为系统调用
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0)
                return false; // 创建失败返回false
            return true;
        }
        std::string Status() // 获取当前状态
        {
            if (_isrunning)
                return "running";
            else
                return "sleep";
        }

        void Stop() // 中止线程
        {
            if (_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
            }
        }

        void Join() // 等待回收线程
        {
            ::pthread_join(_tid, nullptr);
        }

        std::string Name() // 返回线程的名字
        {
            return _name;
        }

        ~Thread()
        {
        }

    private:
        std::string _name; // 线程名
        pthread_t _tid;    // 线程id
        bool _isrunning;   // 线程是否在运行
        func_t _func;      // 线程要执行的回调函数
    };
}

线程封装类用的也是跟我们udp线程池版一样的。

线程池-ThreadPool.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <unistd.h>
#include <vector>
#include <functional>
#include <queue>
#include <pthread.h>
#include "Thread.hpp"
#include "Log.hpp"
using namespace log_ns;

using namespace ThreadMudle; // 开放封装好的线程的命名空间

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);
    }
    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool IsEmpty()
    {
        return _task_queue.empty();
    }
    // 处理任务
    void HandlerTask(const std::string &name) // this
    {
        while (true)
        {
            // 取任务
            LockQueue();                    // 给任务队列上锁
            while (IsEmpty() && _isrunning) // 如果这个线程还在运行任务且任务队列为空,就让线程去休息
            {
                _sleep_thread_num++;
                LOG(INFO, "%s thread sleep begin!\n", name.c_str());
                Sleep();
                LOG(INFO, "%s thread wakeup!\n", name.c_str());
                _sleep_thread_num--;
            }
            // 判定一种情况
            if (IsEmpty() && !_isrunning) // 如果任务为空且线程不处于运行状态就可以让这个线程退出了
            {
                UnlockQueue();
                LOG(INFO, "%s thread quit\n", name.c_str());
                break;
            }

            // 有任务
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();

            // 处理任务
            t(); // 处理任务,此处不用/不能再临界区中处理
            // std::cout << name << ": " << t.result() << std::endl;
            // LOG(DEBUG, "hander task done, task is : %s\n", t.result().c_str());
        }
    }

    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);
            LOG(DEBUG, "construct thread obj %s done, init sucess\n", threadname.c_str());
        }
    }
    void Start() // 复用封装好的线程类里面的Start方法
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str());
            thread.Start();
        }
    }

    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> &t) = delete;
    void operator=(const ThreadPool<T> &t) = delete;

public:
    void Stop() // 停止执行任务
    {
        LockQueue();
        _isrunning = false;
        WakeupAll();
        UnlockQueue();
        LOG(INFO, "thread Pool Stop Success!\n");
    }

    static ThreadPool<T> *GetInstance()
    {
        if (_tp == nullptr)
        {
            pthread_mutex_lock(&_sig_mutex);
            if (_tp == nullptr)
            {
                LOG(INFO, "creat threadpool\n");
                _tp = new ThreadPool<T>();
                _tp->Init();
                _tp->Start();
            }
            else
            {
                LOG(INFO, "get threadpool\n");
            }
            pthread_mutex_unlock(&_sig_mutex);
        }
        return _tp;
    }

    void Equeue(const T &in) // 生产任务
    {
        LockQueue();
        if (_isrunning)
        {
            _task_queue.push(in);
            if (_sleep_thread_num > 0)
            {
                Wakeup(); // 唤醒之前Sleep的线程
            }
        }
        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;

线程池也是一样,以上这四个类我们都是复用udp线程池版本的代码,我这里就不多说明了,因为在udp聊天室的文章里我已经讲得很清楚了。

接下来我们来看看新的封装类。

锁的封装类-LockGuard.hpp

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;
};

锁的封装类比较简单,我们就只是对加锁和解锁操作进行了封装,且我们只需要构造函数和析构函数这两个函数就可以解决,这样做的好处是当我们要对某个功能进行锁的操作时,我们不需要在其生命周期解锁,因为当其生命周期结束时,它自己会调用析构函数进行解锁,大大帮助我们降低了因操作不当而造成死锁的局面。

Tcp服务端源文件-TcpServerMain.cc

cpp 复制代码
#include "TcpServer.hpp"

#include <memory>

// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

在源文件这一块,我们的tcp与udp的编写逻辑别无二致,都是先获取命令行参数,且同样只需要端口号,不需要ip,验证通过之后,我们再创建服务端类的指针对象,然后调用创建服务接口和启动服务接口就可以了。

Tcp服务端头文件

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.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 gbacklog = 8;

using task_t = std::function<void()>;
class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
        : _port(port), _listensockfd(gsock), _isrunning(false)
    {
    }
    void InitServer()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "listensockfd create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "listensockfd create success, fd: %d\n", _listensockfd);

        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;

        // 2.bind _listensockfd 和 Socket addr
        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, gbacklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO, "listen success\n");
    }
    class ThreadData
    {
    public:
        int _sockfd;
        TcpServer *_self;
        InetAddr _addr;

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

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

            // version 0 --- 不稳定版本
            // Service(sockfd, addr);

            // // version 1 ---多进程版本
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // child
            //     ::close(_listensockfd); // 建议

            //     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");
            // }

            // // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
            // pthread_t tid;
            // ThreadData *td = new ThreadData(sockfd, this, addr);
            // pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离

            // version 3 --- 线程池版本 int sockfd,InetAddr addr
            task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
            ThreadPool<task_t>::GetInstance()->Equeue(t);
        }
        _isrunning = false;
    }
    static void *Execute(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->Service(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }

    void Service(int sockfd, InetAddr addr)
    {
        // 长服务
        while (true)
        {
            char inbuffer[1024]; // 当作字符串
            ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                LOG(INFO, "get message from 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);
    }

    ~TcpServer()
    {
    }

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

我们依旧需要打开我们的日志命名空间,并且我们还需要自定义错误码。三个错误码的意思依次代表socket套接字创建失败,绑定失败和监听失败。

我们再来看看我们的成员变量是怎么设计的,在成员变量这一点上,我们跟udp的设计思路基本上是差不多的,第一个是我们的端口号,第二个是我们的socket套接字,第三个是我们的服务运行状态。

我们需要给这三个值设置一个初始值,端口号我们一般都是设置成8080,然后我们的socket套接字默认设置为-1,方便我们后续的一个判断,gbacklog是监听队列的最大长度 (backlog 参数),用于指定操作系统为该套接字**(_listensockfd)**维护的未完成连接请求队列的上限。

具体说明:

  1. 作用 :当服务器调用 listen() 后,套接字进入监听状态,开始接收客户端的连接请求。客户端发起的连接不会立即建立,而是先进入一个「未完成连接队列」(处于 TCP 三次握手过程中)。gbacklog 就是这个队列的最大容量。

  2. 含义

    • 如果队列已满,新的连接请求会被操作系统拒绝(客户端可能收到 ECONNREFUSED 错误)。
    • 其值通常根据应用场景设置(如 5、10、100 等),具体上限可能受操作系统内核参数限制(例如 Linux 中默认可能为 128)。

task_t就是我们的服务包装器对象的类型了。

构造函数部分,我们需要做的就是给三个变量进行赋值,首先是我们的端口号,用我们自定义的全局端口号变量8080作为我们要初始化的数据,然后是我们的socket套接字和运行状态,这三个成员变量初始化完成,我们的构造函数的任务也就完成了。

cpp 复制代码
    void InitServer()
    {
        // 1.创建socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(FATAL, "listensockfd create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "listensockfd create success, fd: %d\n", _listensockfd);

        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;

        // 2.bind _listensockfd 和 Socket addr
        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, gbacklog) < 0)
        {
            LOG(FATAL, "listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO, "listen success\n");
    }

接下来就是我们服务器的构建工作,我们服务器的构建要完成的任务是什么呢?我这里就不卖关子了。第一步,我们需要创建监听的socket套接字,第二步我们需要绑定监听的socket套接字,第三步就是开始监听了。

我们创建监听套接字的一个与udp不同的就是在第二个参数,我们的udp用的是基于数据报的传输,而我们的tcp用的是基于字节流的传输,所以我们的第二个参数是SOCK_STREAM。

然后我们绑定的socket和本地网络地址信息的方式和udp是一模一样的,这里我就不赘述了。

tcp相比于udp的连接过程来说,会多几步,在代码上的体现就是多了三步,第一步是监听,第二步是创建普通的socket套接字,第三步是accept连接。我们的listen监听就是在这里进行的。tcp为什么需要一个连接呢?这是因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接,而listen系统调用的核心功能是将一个套接字从 "主动连接" 状态转换为 "被动监听" 状态 ,使其能够接收客户端的连接请求。调用 listen 前,_listensockfd 是一个通过 socket() 创建的 "未连接" 套接字(仅分配了资源,未指定角色)。调用 listen 后,该套接字被标记为监听套接字(listening socket) ,专门用于接收客户端的 connect() 请求(仅适用于 TCP 协议,UDP 无需监听)。

这是我们封装的一个线程数据类,它的作用就是帮助我们更好的管理我们所关心的几个变量,比如sockfd,实体类对象以及网络地址。

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

            // version 0 --- 不稳定版本
            // Service(sockfd, addr);

            // // version 1 ---多进程版本
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     // child
            //     ::close(_listensockfd); // 建议

            //     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");
            // }

            // // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
            // pthread_t tid;
            // ThreadData *td = new ThreadData(sockfd, this, addr);
            // pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离

            // version 3 --- 线程池版本 int sockfd,InetAddr addr
            task_t t = std::bind(&TcpServer::Service, this, sockfd, addr);
            ThreadPool<task_t>::GetInstance()->Equeue(t);
        }
        _isrunning = false;
    }

接下来就是我们一直执行服务的内容了。我们先将服务状态设置为运行,然后就可以accept接收要进行通信的客户端了,并创建对应进行通信的socket套接字,接下来就是对数据的处理,但是我们发现我们有四个版本,第一个版本是不稳定的版本:

这段代码是一个简单的网络服务函数,用于处理客户端连接并进行消息回显。不稳定体现在下面这几个方面:

1. 未处理阻塞和非阻塞模式

  • 阻塞模式readwrite 在默认的阻塞模式下,如果客户端长时间不发送数据,read 调用会一直阻塞,导致整个服务线程被挂起,无法及时处理其他客户端连接(如果是单线程服务架构)。
  • 非阻塞模式 :如果套接字被设置为非阻塞模式,read 可能会在没有数据可读时立即返回错误(如 EAGAINEWOULDBLOCK),但当前代码没有对这种情况进行正确处理,可能导致服务逻辑混乱。

2. 单线程处理限制

当前代码在一个无限循环中处理单个客户端连接,如果有大量客户端同时请求连接,单线程的处理方式会导致后面的连接请求长时间等待,无法及时响应,降低了服务的并发处理能力,从用户角度看服务表现不稳定。

接下来我们再来看看第二个版本:

cpp 复制代码
             // version 1 ---多进程版本
             pid_t id = fork();
             if (id == 0)
             {
                 // child
                 ::close(_listensockfd); // 建议

                 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");
             }

第二个是多进程的版本,我们将数据的处理交给子进程去完成,我们的子进程得关闭监听套接字,因为子进程只需要处理数据,监听是父进程的任务,所以相对应的,我们的父进程得关闭通信的socket套接字,并且等待子进程的结束并回收它。

第三个是多线程的版本:

cpp 复制代码
// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了
             pthread_t tid;
             ThreadData *td = new ThreadData(sockfd, this, addr);
             pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离

多线程的版本我们就用到了我们封装的数据类型对象了,将新线程交给线程函数去处理数据

我们用pthread_detach函数将线程进行分离操作,然后处理数据,至于为什么要进行线程分离呢?

在 POSIX 线程编程中,pthread_detach(pthread_self()); 语句的作用是将调用该函数的线程(通过 pthread_self() 获取当前线程标识符 )设置为分离状态。它主要有以下几个方面的作用:

1. 资源自动回收

  • 常规线程回收 :默认情况下,线程创建后处于可结合(joinable)状态,当一个可结合状态的线程结束运行后,它的线程资源(比如线程栈等)并不会立即被系统释放,而是会一直保留,直到有其他线程调用 pthread_join 函数来获取该线程的退出状态,并完成资源回收。这就要求必须有对应的代码逻辑来处理线程回收,否则会造成资源泄漏。
  • 分离线程回收 :而被分离(detached)的线程,在线程执行结束后,其相关的系统资源会由系统自动释放,无需其他线程显式调用 pthread_join 来回收资源,简化了线程资源管理的流程。

2. 避免死锁风险

  • 可结合线程的潜在问题 :如果存在多个线程,并且有线程等待其他可结合线程结束(通过 pthread_join),但由于某些异常情况(比如等待线程自身提前退出、逻辑错误导致没有执行 pthread_join 等 ),使得可结合线程一直无法被正确回收,就可能导致死锁或者资源占用问题。
  • 分离线程的优势:将线程设置为分离状态后,不需要其他线程来等待它的结束,也就不存在因为等待回收而引发的死锁风险,增强了程序的稳定性和健壮性。

3. 适用于独立运行的任务线程

  • 场景说明:有些线程执行的是一些独立的、不需要和主线程或者其他线程进行同步交互、也不需要返回特定结果的任务,例如后台的日志记录线程、定期的系统状态监控线程等。
  • 设置分离的好处:对于这类线程,将其设置为分离状态是很合适的。它们在完成自己的任务后,资源能自动释放,不会干扰其他线程的执行,也不需要额外的同步和回收操作,提高了程序的运行效率和简洁性。

最后一个版本就是我们的线程池版本:

线程池就轻松了,直接将任务打包好交给我们的线程池就可以了。

Tcp客户端源文件-TcpClientMain.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

// ./tcpclient 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)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    // 注意:不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,os会自动bind sockfd,用自己的IP和随机端口号
    // 什么时候进行自动bind?connect
    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);

    int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect sockfd error" << std::endl;
        exit(2);
    }

    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));
        if (n > 0)
        {
            echo_buffer[n] = 0;
            std::cout << echo_buffer << std::endl;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
    return 0;
}

tcp客户端的逻辑跟udp的差不多,前面的命令参数的处理,以及socket套接字的创建,再到后面的IPV4的结构体对象的创建,这些流程跟udp是一模一样的,只不过需要对目标地址进行连接以及对传输数据的方式进行了改变,udp是send发送数据,recv接收数据,而tcp是write和read。主要原因如下做参考:

数据传输特性差异

  • UDP :UDP 是无连接的协议,以数据报(datagram)为单位进行传输 ,数据报之间相互独立,发送的数据边界在接收端能够被保留。 send 系列函数(如 sendtosend 等 )设计上更契合 UDP 这种无连接、面向数据报的特性。例如 sendto 函数,它允许在发送数据时明确指定目标地址,方便 UDP 向不同的目标发送独立的数据报。在接收端,recvfrom 函数可以获取到发送方的地址信息,以便于进行响应,这种机制和 UDP 数据报独立传输、不依赖连接的特点相匹配。
  • TCP :TCP 是面向连接的协议,提供字节流服务,数据被看作是无边界的字节序列。writeread 函数原本是系统 I/O 操作函数,用于对文件、套接字等描述符进行读写。由于 TCP 字节流没有明确的消息边界,使用 write 写入数据就如同向文件中写入字节序列,read 读取数据也类似从文件中按顺序读取字节,更符合 TCP 字节流连续传输的特性,能很好地适应 TCP 这种将数据当作连续字节流处理的方式。

数据可靠性保证机制不同

  • UDP :UDP 本身不保证数据的可靠传输,没有内置的确认、重传等机制。sendrecv 系列函数的设计相对简单直接,不会过多干预数据传输的可靠性处理。发送方调用 send 函数将数据报发出后,不会等待对方的确认;接收方调用 recv 函数接收数据报,若数据报丢失也不会触发自动重传等操作,应用程序需要自己处理丢包、重复等情况,这与 UDP 轻量级、低开销的设计理念相符。
  • TCP :TCP 有复杂的可靠性保证机制,如确认应答、超时重传、滑动窗口等。write 函数只是将数据放入内核的发送缓冲区,由 TCP 协议栈按照自身机制来确保数据可靠发送到对端,read 函数从内核接收缓冲区读取数据,TCP 协议栈会负责处理数据的乱序、重复等问题,保证应用程序读取到的是有序、完整的数据,这与 TCP 为应用层提供可靠数据传输服务的目标是一致的。

历史和设计习惯因素

  • UDP :早期网络编程中,为了突出 UDP 无连接、简单快捷传输数据报的特点,专门设计了与之匹配的 sendrecv 相关函数,随着网络编程的发展和普及,这种函数接口使用习惯得以保留和延续。
  • TCP :因为 TCP 套接字本质上也是一种文件描述符(在 Unix 及类 Unix 系统中,一切皆文件的理念下),而 writeread 是系统中通用的针对文件描述符的 I/O 操作函数,在处理 TCP 套接字数据传输时自然沿用了这两个函数,后来在跨平台等场景下,这种使用方式也被广泛接受。
相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理6 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
DianSan_ERP6 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
feifeigo1236 天前
matlab画图工具
开发语言·matlab