打印宏与socket模块设计

📅 2026/7/13 2:35:31 👁️ 阅读次数 📝 编程学习
打印宏与socket模块设计

前言:

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

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

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

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

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

打印宏的代码实现:

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

日志等级

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

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

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

比如:

#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小时制)并打印结果输出。

比如:

#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的调用,他并不知道我们后面要打印多少个参数。

其标准用法为:

#define宏名(固定参数,...)替换文本 __VA_ARGS__

… :表示可变参数的位置
__VA_ARGS__:代表所有可变参数的内容

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

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


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

打印结果类似:

线程ID: 0x7f8a9b0c0d00 当前文件: test.c 当前行号: 6

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

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

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

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

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

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


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

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

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

// 模拟一个网络函数 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返回新的客户端连接),所以我们需要手动实现移动构造,

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

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

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()是客户端在使用,我们要将其连接到远程服务器地址和端口

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对象。

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重要的接收与发送两个接口,我们要注意其返回值的处理,因为这两个接口函数的返回值不同,代表不同的情况。

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; // 实际发送的数据长度 }

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

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关闭套接字:

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

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

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

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

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

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

因此:

// 创建一个服务端连接 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 之前)

代码为:

// 创建一个服务端连接 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); }

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

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事件管理的模块的代码设计与完善,希望对大家有所帮助。

完整代码如下:

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