打印宏与socket模块设计

前言:

打印宏在我们的日常使用中具有重要的辅助作用。

通过打印宏,我们在调试代码的时候,打印信息的管理就会变得异常容易。

包括我们可以带上打印位置的行号与文件名,还有时间信息,方便我们快速定位位置。

以前我们在Linux网络编程的学习中曾经写过一个log文件,但是这个log文件整体还是偏臃肿了,他的很多功能其实都可以被宏平替。准确的说,我们不用再使用复杂的继承体系类来实现log,只需通过宏的使用,以及调整宏的打印等级,就可以轻松的实现打印信息,取消打印信息,以及打印到日志文件的功能。

我们本篇文章的前面紧接前一篇文章的Buffer代码设计,在开始socket的代码设计之前,我们先来讲一些打印宏代码的编写。

打印宏的代码实现:

#define 是打印宏的核心,它的编译期文本替换功能让日志打印变得极其灵活和高效。

日志等级

一个打印宏的日志等级将会控制其是否可以被打印,我们通常将其定义为常数,从0开始。而日志等级通常可以被分为以下:

  • INF(信息):普通运行信息
  • DBG(调试):调试信息,开发时用
  • ERR(错误):出错了必须打印

数字越小,级别越低;数字越大,级别越高。

比如:

c++ 复制代码
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG   // 当前日志级别设置为 DBG

这里的LOG_LEVEL宏是我们定义出来的一个门槛等级。我们可以通过比较大小,来选择性的打印。LOG_LEVEL 决定了哪些级别的日志会被打印。比如设置为 DBG 时,ERRDBG 都打印,但 INF 不打印。

获取相关信息

我们在打印日志的时候,为了快速获取当前测试代码的信息,我们一般会要求打印出当前的文件,行号还有时间信息,甚至是进程信息。

此时我们就需要一些功能函数来帮助我们获取信息,但是如何打印呢?

宏只有一行,那么我们此时就可以借助do while语句以及续行符' \ '来使用一些功能函数。

首先就是时间信息,我们可以使用以下函数:

  • time(NULL):获取当前时间(从1970年1月1日到现在的秒数)
  • localtime(&t):把秒数转换成 struct tm 结构体(包含年、月、日、时、分、秒等)
  • strftime():把时间格式化输出(类似 printf 但针对时间)

再用%H:%M:%S分别表示时、分、秒(24小时制)并打印结果输出。

比如:

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

int main() 
{
    time_t t = time(NULL);
    struct tm *ltm = localtime(&t);
    char tmp[32] = {0};
    strftime(tmp, 31, "%H:%M:%S", ltm);
    printf("当前时间: %s\n", tmp);
    return 0;
}

打印结果为:

复制代码
当前时间: 14:23:45

接下来就是获取线程ID和文件名行号,我们需要熟练掌握可变参数宏的相关:

  • pthread_self():获取当前线程的ID(返回 pthread_t 类型)
  • %p:以十六进制打印指针/地址
  • __FILE__:预定义宏,当前文件名(字符串)
  • __LINE__:预定义宏,当前行号(整数)
  • __VA_ARGS__:可变参数宏,接收 ... 中的所有参数

上面四个知识点都很好懂,但是什么是可变参数宏呢?

在普通宏中,我们的参数其实是固定了的,当我们也不知道我们要传多少个参数的时候,就需要可变参数宏来帮助我们,就像是printf的调用,他并不知道我们后面要打印多少个参数。

其标准用法为:

c 复制代码
#define 宏名(固定参数, ...)  替换文本 __VA_ARGS__

... :表示可变参数的位置

__VA_ARGS__:代表所有可变参数的内容

我们通常在固定参数的位置上,传入我们的格式化字符串(就是带%d,%s的那种字符串)。这个字符串通常是外界传入的,因为我们不知道有多少个打印的参数,所以就让外界传入参数的同时把字符串也传进来。

我们在后面的代码中可以理解这个行为。


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

int main() {
    printf("线程ID: %p\n", (void*)pthread_self());
    printf("当前文件: %s\n", __FILE__);
    printf("当前行号: %d\n", __LINE__);
    return 0;
}

打印结果类似:

c++ 复制代码
线程ID: 0x7f8a9b0c0d00
当前文件: test.c
当前行号: 6

那么接下来我们就尝试一下来编写一个简单的日志打印宏:

在这个例子中我们外部需要传入format格式化字符串,随后我们将会使用fprintf依次向显示器打印线程信息,时间,文件名与行号。

__VA_ARGS__就是我们前面用来接收不定参的...中的参数,使其进行一个打印的效果。

其打印所需的对应的格式占位符我们已经通过format传入。

但是这里还是有个问题,当我们的不定参数是无的时候,也就是没有参数的时候,我们的代码就会报错,这是因为fprintf的参数列表中的末尾多了一个,,那么我们此时就需要##__VA_ARGS__来连接可变参数。

当我们在其前面加上##时,其对应的不定参数不存在时就会忽略这个逗号。


但是我们的日志等级在上面的代码中并没有真实的用上,此时我们可以通过简单的多一层宏封装固定的日志等级,加上我们一开始定义的日志级别等级,二者进行比较,通过比较的结果来判断此次我们是否严格打印:

这里我们又定义了三个宏,这是对其LOG的三个外部封装,固定其宏的打印等级,这样我们会在LOG内部通过传入的等级与我们一开始的定义的level等级相比较来判断此次结果是否被打印。

我们用这样的一个代码来测试:

c++ 复制代码
// 模拟一个网络函数
int fake_recv(int *sockfd, char *buf, int len) 
{
    // 模拟错误
    errno = EAGAIN;  // 设置全局错误码
    return -1;
}

int main() 
{
    INF_LOG("程序开始运行");
    
    int sockfd = 3;
    char buffer[1024];
    
    DBG_LOG("准备接收数据,sockfd=%d, buffer大小=%d", sockfd, (int)sizeof(buffer));
    
    int ret = fake_recv(&sockfd, buffer, sizeof(buffer));
    if (ret < 0) {
        ERR_LOG("recv失败,错误码=%d (%s)", errno, strerror(errno));
    } else {
        INF_LOG("recv成功,收到 %d 字节", ret);
    }
    
    INF_LOG("程序结束");
    return 0;
}

运行结果为:

复制代码
[0x7c8eba101500 10:47:37 test.cc:18] 准备接收数据,sockfd=3, buffer大小=1024
[0x7c8eba101500 10:47:37 test.cc:22] recv失败,错误码=11 (Resource temporarily unavailable)

你会发现,我们默认设置的等级是DBG,所以我们的INF等级的日志打印消息就不会打印出来。而当我们调整等级为ERR时,就只会打印出一个:

复制代码
[0x7c8eba101500 10:47:37 test.cc:22] recv失败,错误码=11 (Resource temporarily unavailable)

Socket模块代码设计与实现

那么接下来,我们就来实现一下我们的socket模块。

所谓的socket,其实就是最底层的系统调用封装 ,几乎所有上层组件都依赖Socket

我们要做的其实就是涉及一个socket类,在这个类中实现配套函数,一个socket与一个sockfd绑定在一起,方便我们对其进行管理。

他需要实现的基础功能有:

复制代码
创建套接字
绑定地址信息
开始监听
向服务器发起连接
获取新连接
接收数据
发送数据
关闭套接字
创建一个监听连接
创建一个客户端连接
设置地址端口的重用
设置套接字是否阻塞的属性

我们在Linux网络的知识点中学习过socket的使用,所以大部分代码其实都是不用讲解的很详细的,包括其函数的使用,因此我只会对一些代码进行注释补充说明。

首先就是我们基础的类成员变量肯定就是一个sockfd,套接字的描述符,一个套接字肯定是对应着一个套接字文件描述符的。随后就是基础的构造与析构函数。唯一我们要注意的就是socket不应该有拷贝构造函数,这是因为Socket 是一个"资源句柄"(Handle),它和操作系统内核的套接字对象是 1:1 绑定的

所以我们需要禁用拷贝,但是有时候,我们需要把 Socket 从一个地方转移到另一个地方(比如 Accept 返回新的客户端连接),所以我们需要手动实现移动构造,

c++ 复制代码
class Socket
{
private:
    int _sockfd;

public:
    Socket() {}
    Socket(int fd) : _sockfd(fd) {}
    ~Socket() {}
    int Fd() { return _sockfd; }

    //  禁止拷贝,Socket 是一个"资源句柄",它和操作系统内核的套接字对象是 1:1 绑定的
    Socket(const Socket &) = delete;
    Socket &operator=(const Socket &) = delete;
};

首先就是我们最基础的三个功能,创建套接字,绑定与监听:

c++ 复制代码
bool Create()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 三个参数分别是协议族,套接字类型,协议

        if (_sockfd < 0)
        {
            ERR_LOG("CREATE SOCKET FAILED!!");
            return false;
        }

        return true;
    }
    bool Bind(const std::string &ip, uint16_t port)
    {
        // 把套接字和本地 IP 地址、端口号关联起来(服务器专用)。
        // bind的三个参数分别是套接字描述符,指向 sockaddr_in 结构体的指针(含 IP 和端口)与 addr 结构体的大小

        struct sockaddr_in _addr;                      // 定义一个 IPv4 地址结构体,用来存放 IP 和端口信息。
        _addr.sin_family = AF_INET;                    // 告诉系统使用 IPv4 地址族。
        _addr.sin_port = htons(port);                  // 把端口号设置到地址结构体中。
        _addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 把字符串形式的 IP 地址(如 "192.168.1.100")转换成网络字节序的 32 位整数。

        socklen_t len = sizeof(struct sockaddr_in); // 计算结构体大小

        int ret = bind(_sockfd, (struct sockaddr *)&_addr, len); // bind() 是通用函数,设计用于 IPv4、IPv6、Unix域等多种地址。
        // 所以它接受的是通用类型 struct sockaddr*,而不是特定的 struct sockaddr_in*,因此我们要进行强转

        if (ret < 0)
        {
            ERR_LOG("BIND ADDRESS FAILED!");
            return false;
        }
        return true;
    }
    bool Listen(int backlog = MAX_LISTEN)
    {
        int ret = listen(_sockfd, backlog); // 开始监听,backlog决定了全连接队列的长度
        if (ret < 0)
        {
            ERR_LOG("SOCKET LISTEN FAILED!");
            return false;
        }
        return true;
    }

这里的代码都是固定的代码,我们就不过多赘述。listen接口的时候会用到一个参数backlog,我们这里设置为1024,即全连接队列的最大长度。

connect的接口的使用方法与bind类似,只不过二者目的并不相同,connect()是客户端在使用,我们要将其连接到远程服务器地址和端口

c++ 复制代码
 bool Connect(const std::string &ip, uint16_t port)
    {
        struct sockaddr_in _addr;                      // 定义一个 IPv4 地址结构体,用来存放 IP 和端口信息。
        _addr.sin_family = AF_INET;                    // 告诉系统使用 IPv4 地址族。
        _addr.sin_port = htons(port);                  // 把端口号设置到地址结构体中。
        _addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 把字符串形式的 IP 地址(如 "192.168.1.100")转换成网络字节序的 32 位整数。

        socklen_t len = sizeof(struct sockaddr_in); // 计算结构体大小

        int ret = connect(_sockfd, (struct sockaddr *)&_addr, len);

        if (ret < 0)
        {
            ERR_LOG("CONNECT SERVER FAILED!");
            return false;
        }
        return true;
    }

接下来就是Accept接受新连接的接口实现,这个接口的返回值有两种,第一种是我们可以返回文件描述符,这样解耦合高,代码灵活性高。但是你也可以返回一个socket对象。在现代c++语法的移动语义的帮助下,所花费的开销并不是很大。

二者方法都可以,我这里就直接返回一个socket对象。

c++ 复制代码
	Socket Accept() // 这里也可以返回文件描述符
    {
        int newfd = accept(_sockfd, NULL, NULL); // 当我们并不关心客户端的地址信息时,后面两个参数就可以置为空

        if (newfd < 0)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
            {
                // 没有新连接,这是正常情况,不打印错误
                return Socket(-1);
            }
            ERR_LOG("ACCEPT FAILED!");
            return Socket(-1); // 后续在处理时判断的也是其fd是否为-1
        }
        return Socket(newfd); // RVO 优化
    }

随后就是socket重要的接收与发送两个接口,我们要注意其返回值的处理,因为这两个接口函数的返回值不同,代表不同的情况。

c++ 复制代码
ssize_t Recv(void *buf, size_t len, int flag = 0) // 这个接口我们默认为阻塞的
    {
        ssize_t ret = recv(_sockfd, buf, len, flag);

        if (ret == 0) // 返回值 == 0(对端正常关闭连接)
        {
            return -2; //返回-2表示对端关闭,因为对端关闭我们必须单独判断,虽然处理方法是大概是关闭连接
            //但是有一种情况,就是上一次错误码为EAGAIN,EINTR,如果你不进行单独处理,没有这个判断
            //就会一直进入return 0的情况,然后上层会一直尝试去不断读取,陷入死循环,但是此时你的对端已经关闭了,无意义 
        }

        if(ret < 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0; // 特殊标记:非阻塞模式无数据可读,或者被信号被打断
            }
            ERR_LOG("SOCKET RECV FAILED!!");
            return -1;
        }
        return ret;//返回实际接收到的长度
    }
c++ 复制代码
ssize_t Send(const void *buf, size_t len, int flag = 0)//我们不会修改buf所以可以用const,方便调用该函数时使用的是string.c_str()传入的是个const char*无法变成void*
    {
        ssize_t ret = send(_sockfd, buf, len, flag); // 向socket的发送缓冲区写入数据
        if (ret < 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET SEND FAILED!!");
            return -1;
        }
        return ret; // 实际发送的数据长度
    }

我们上面的两个基础版本的recv与send都是阻塞版本的,事实上我们应该设置两个非阻塞版本的接口。根据之前Buffer的思想,你控制非阻塞只需要替换flag就行了,所以我们只需要单独列两个接口内部调用recv与send就行,只是flag的传参不一样。

c++ 复制代码
	ssize_t NonBlockRecv(void *buf, size_t len)
    {
        return Recv(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前接收为非阻塞。
    }
    ssize_t NonBlockSend(void *buf, size_t len)
    {
        if (len == 0)
            return 0;
        return Send(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前发送为非阻塞。
    }

然后就是关闭我们的套接字接口,内部调用close关闭套接字:

c++ 复制代码
// 关闭套接字
    void Close()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

以上就是我们基础的几个接口,但是我们还需要集成几个调用基础接口的整合接口,以此直接创建出一个服务端套接字或者客户端套接字。并且,不知道大家注意没有,作为一个服务器,如果他突然挂掉了,我们是需要立即对他进行重启的。

但是,在 TCP 协议中,当服务器主动关闭一个连接时,这个连接不会立刻消失,而是进入 TIME_WAIT 状态(通常会持续 2 分钟,即 2MSL)。这是 TCP 协议为了保证网络中迟到的数据包被正确丢弃而设计的机制。

**如果此时服务器重启或重新部署,尝试再次绑定(Bind)刚刚关闭的那个端口,操作系统会直接报错 Address already in use,导致服务器启动失败。

因此我们还需要设置一下地址端口的复用功能。

并且,我们的Accept等接口,我们要求服务器的sockfd在调用Accept的时候不能阻塞,所以我们还需要一个接口来设置非阻塞属性!!

因此:

c++ 复制代码
// 创建一个服务端连接
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
    {
    }
    // 创建一个客户端连接
    bool CreateClient(uint16_t port, const std::string &ip)
    {
    }
    // 设置套接字选项---开启地址端口重用
    void ReuseAddress()
    {
    }
    // 设置套接字阻塞属性-- 设置为非阻塞
    void NonBlock()
    {
    }

先来实现CreateServer,我们需要外界传入一个端口号,对于服务器来说,我们默认是绑定这个ip的全部端口号。

创建服务端套接字接口的过程还是有点说法的,对于一个标准的服务器初始化流程应该是:

  1. 创建套接字 (socket)
  2. 设置地址重用 (setsockopt)
  3. 绑定地址 (bind)
  4. 开始监听 (listen)
  5. 设置非阻塞 (fcntl) (注:非阻塞设置在 Bind 之后、Listen 之前,或 Listen 之后均可,但地址重用必须在 Bind 之前)

代码为:

c++ 复制代码
 // 创建一个服务端连接
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
    {
        // 创建套接字 (socket)
        if (Create() == false)
        {
            return false;
        }

        // 设置地址重用 (setsockopt)
        ReuseAddress();

        // 绑定地址 (bind)
        if (Bind(ip, port) == false)
            return false;
        // 开始监听 (listen)

        if (Listen() == false)
            return false;

        // 设置非阻塞 (fcntl) (注:非阻塞设置在 Bind 之后、Listen 之前,或 Listen 之后均可,但地址重用必须在 Bind 之前)
        if (block_flag)
            NonBlock();

        return true;
    }

    // 创建一个客户端连接
    bool CreateClient(uint16_t port, const std::string &ip)
    {
        // 1. 创建套接字,2.指向连接服务器

        if (Create() == false)
            return false;

        return Connect(ip, port);
    }
    // 设置套接字选项---开启地址端口重用
    void ReuseAddress()
    {
        // int setsockopt(int fd, int leve, int optname, void *val, int vallen)
        int val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
        val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
    }
    // 设置套接字阻塞属性-- 设置为非阻塞
    void NonBlock()
    {
        // int fcntl(int fd, int cmd, ... /* arg */ );
        int flag = fcntl(_sockfd, F_GETFL, 0);
        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
    }

到这里,我们的接口部分基本上就已经初步写好了,但是我们之前遗落掉的小瑕疵我们还需要完善上,就是我们的析构函数与自己写的移动语义接口并没有实现好:

c++ 复制代码
 Socket() : _sockfd(-1) {}
    Socket(int fd) : _sockfd(fd) {}
    ~Socket() { Close(); }
    int Fd() { return _sockfd; }

    //  禁止拷贝,Socket 是一个"资源句柄",它和操作系统内核的套接字对象是 1:1 绑定的
    Socket(const Socket &) = delete;
    Socket &operator=(const Socket &) = delete;

    // 手动实现移动语义
    Socket(Socket &&other) : _sockfd(other._sockfd)
    {
        other._sockfd = -1;
    }

    Socket &operator=(Socket &&other)
    {
        if (this != &other)
        {
            Close(); // 释放当前资源
            _sockfd = other._sockfd;
            other._sockfd = -1;
        }
        return *this;
    }

至此,那么我们的socket模块基本上就实现完全了,接下来我们将会完成Channel事件管理的模块的代码设计与完善,希望对大家有所帮助。

完整代码如下:

c++ 复制代码
#define MAX_LISTEN 1024

class Socket
{
private:
    int _sockfd;

public:
    Socket() : _sockfd(-1) {}
    Socket(int fd) : _sockfd(fd) {}
    ~Socket() { Close(); }
    int Fd() { return _sockfd; }

    //  禁止拷贝,Socket 是一个"资源句柄",它和操作系统内核的套接字对象是 1:1 绑定的
    Socket(const Socket &) = delete;
    Socket &operator=(const Socket &) = delete;

    // 手动实现移动语义
    Socket(Socket &&other) : _sockfd(other._sockfd)
    {
        other._sockfd = -1;
    }

    Socket &operator=(Socket &&other)
    {
        if (this != &other)
        {
            Close(); // 释放当前资源
            _sockfd = other._sockfd;
            other._sockfd = -1;
        }
        return *this;
    }

    bool Create()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 三个参数分别是协议族,套接字类型,协议

        if (_sockfd < 0)
        {
            ERR_LOG("CREATE SOCKET FAILED!!");
            return false;
        }

        return true;
    }
    bool Bind(const std::string &ip, uint16_t port)
    {
        // 把套接字和本地 IP 地址、端口号关联起来(服务器专用)。
        // bind的三个参数分别是套接字描述符,指向 sockaddr_in 结构体的指针(含 IP 和端口)与 addr 结构体的大小

        struct sockaddr_in _addr;                      // 定义一个 IPv4 地址结构体,用来存放 IP 和端口信息。
        _addr.sin_family = AF_INET;                    // 告诉系统使用 IPv4 地址族。
        _addr.sin_port = htons(port);                  // 把端口号设置到地址结构体中。
        _addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 把字符串形式的 IP 地址(如 "192.168.1.100")转换成网络字节序的 32 位整数。

        socklen_t len = sizeof(struct sockaddr_in); // 计算结构体大小

        int ret = bind(_sockfd, (struct sockaddr *)&_addr, len); // bind() 是通用函数,设计用于 IPv4、IPv6、Unix域等多种地址。
        // 所以它接受的是通用类型 struct sockaddr*,而不是特定的 struct sockaddr_in*,因此我们要进行强转

        if (ret < 0)
        {
            ERR_LOG("BIND ADDRESS FAILED!");
            return false;
        }
        return true;
    }
    bool Listen(int backlog = MAX_LISTEN)
    {
        int ret = listen(_sockfd, backlog); // 开始监听,backlog决定了全连接队列的长度
        if (ret < 0)
        {
            ERR_LOG("SOCKET LISTEN FAILED!");
            return false;
        }
        return true;
    }

    bool Connect(const std::string &ip, uint16_t port)
    {
        struct sockaddr_in _addr;                      // 定义一个 IPv4 地址结构体,用来存放 IP 和端口信息。
        _addr.sin_family = AF_INET;                    // 告诉系统使用 IPv4 地址族。
        _addr.sin_port = htons(port);                  // 把端口号设置到地址结构体中。
        _addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 把字符串形式的 IP 地址(如 "192.168.1.100")转换成网络字节序的 32 位整数。

        socklen_t len = sizeof(struct sockaddr_in); // 计算结构体大小

        int ret = connect(_sockfd, (struct sockaddr *)&_addr, len);

        if (ret < 0)
        {
            ERR_LOG("CONNECT SERVER FAILED!");
            return false;
        }
        return true;
    }

    Socket Accept() // 这里也可以返回文件描述符
    {
        int newfd = accept(_sockfd, NULL, NULL); // 当我们并不关心客户端的地址信息时,后面两个参数就可以置为空

        if (newfd < 0)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
            {
                // 没有新连接,这是正常情况,不打印错误
                return Socket(-1);
            }
            ERR_LOG("ACCEPT FAILED!");
            return Socket(-1); // 后续在处理时判断的也是其fd是否为-1
        }
        return Socket(newfd); // RVO 优化
    }

    ssize_t Recv(void *buf, size_t len, int flag = 0) // 这个接口我们默认为阻塞的
    {
        ssize_t ret = recv(_sockfd, buf, len, flag);

        if (ret == 0) // 返回值 == 0(对端正常关闭连接)
        {
            return -2; // 返回-2表示对端关闭,因为对端关闭我们必须单独判断,虽然处理方法是大概是关闭连接
            // 但是有一种情况,就是上一次错误码为EAGAIN,EINTR,如果你不进行单独处理,没有这个判断
            // 就会一直进入return 0的情况,然后上层会一直尝试去不断读取,陷入死循环,但是此时你的对端已经关闭了,无意义
        }

        if (ret < 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0; // 特殊标记:非阻塞模式无数据可读,或者被信号被打断
            }
            ERR_LOG("SOCKET RECV FAILED!!");
            return -1;
        }
        return ret; // 返回实际接收到的长度
    }

    ssize_t Send(const void *buf, size_t len, int flag = 0) // 我们不会修改buf所以可以用const,方便调用该函数时使用的是string.c_str()传入的是个const char*无法变成void*
    {
        ssize_t ret = send(_sockfd, buf, len, flag); // 向socket的发送缓冲区写入数据
        if (ret < 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET SEND FAILED!!");
            return -1;
        }
        return ret; // 实际发送的数据长度
    }

    ssize_t NonBlockRecv(void *buf, size_t len)
    {
        return Recv(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前接收为非阻塞。
    }
    ssize_t NonBlockSend(const void *buf, size_t len)
    {
        if (len == 0)
            return 0;
        return Send(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前发送为非阻塞。
    }

    // 关闭套接字
    void Close()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

    // 创建一个服务端连接
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
    {
        // 创建套接字 (socket)
        if (Create() == false)
        {
            return false;
        }

        // 设置地址重用 (setsockopt)
        ReuseAddress();

        // 绑定地址 (bind)
        if (Bind(ip, port) == false)
            return false;
        // 开始监听 (listen)

        if (Listen() == false)
            return false;

        // 设置非阻塞 (fcntl) (注:非阻塞设置在 Bind 之后、Listen 之前,或 Listen 之后均可,但地址重用必须在 Bind 之前)
        if (block_flag)
            NonBlock();

        return true;
    }

    // 创建一个客户端连接
    bool CreateClient(uint16_t port, const std::string &ip)
    {
        // 1. 创建套接字,2.指向连接服务器

        if (Create() == false)
            return false;

        return Connect(ip, port);
    }
    // 设置套接字选项---开启地址端口重用
    void ReuseAddress()
    {
        // int setsockopt(int fd, int leve, int optname, void *val, int vallen)
        int val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
        val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
    }
    // 设置套接字阻塞属性-- 设置为非阻塞
    void NonBlock()
    {
        // int fcntl(int fd, int cmd, ... /* arg */ );
        int flag = fcntl(_sockfd, F_GETFL, 0);
        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
    }
};
相关推荐
炸薯条!1 小时前
从零开始学C++(4) --类和对象
开发语言·c++·算法
aneasystone本尊1 小时前
学习 OpenMontage 的参考视频玩法
人工智能
Linging_241 小时前
基于Langchain得AI Agent相关知识学习
人工智能·学习·langchain
haluhalu.1 小时前
prompts.chat:07-few-shot-prompting
人工智能·算法
AI创界者1 小时前
打造内网安全防线:使用 Kali Linux 进行企业级漏洞风险自查与防御指南
linux·运维·安全
Wang's Blog1 小时前
Go-Zero框架上手前奏2: 微服务核心要素 —— 拆分、通信与无状态设计
开发语言·微服务·golang
智码看视界1 小时前
Day18 SpringBoot自动配置原理:从@SpringBootApplication开始
java·spring boot·后端·自动装配
weixin_439857541 小时前
在Ubuntu 26.04版本上借助本地AI助手在AMD显卡基础上安装并运行Qwen3.6-35B-A3B,作为本地编程AI
linux·人工智能·ubuntu