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 套接字数据传输时自然沿用了这两个函数,后来在跨平台等场景下,这种使用方式也被广泛接受。
相关推荐
明天会有多晴朗2 小时前
C语言入门教程(第1讲):最通俗的C语言常见概念详解与实战讲解
c语言·开发语言·c++
爱上妖精的尾巴2 小时前
5-20 WPS JS宏 every与some数组的[与或]迭代(数组的逻辑判断)
开发语言·前端·javascript·wps·js宏·jsa
gopher95112 小时前
Go 语言的 panic 和 recover
开发语言·golang
豆沙沙包?2 小时前
2025年--Lc165--H637.二叉树的层平均值(二叉树的层序遍历)--Java版
java·开发语言
24zhgjx-fuhao3 小时前
基于时间的ACL
运维·网络
小蒜学长3 小时前
springboot二手儿童绘本交易系统设计与实现(代码+数据库+LW)
java·开发语言·spring boot·后端
李小白663 小时前
Python文件操作
开发语言·python
xqlily3 小时前
Go语言:高效简洁的现代编程语言
开发语言·后端·golang
数据知道4 小时前
Go语言:数据压缩与解压详解
服务器·开发语言·网络·后端·golang·go语言