前言:
打印宏在我们的日常使用中具有重要的辅助作用。
通过打印宏,我们在调试代码的时候,打印信息的管理就会变得异常容易。
包括我们可以带上打印位置的行号与文件名,还有时间信息,方便我们快速定位位置。
以前我们在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 时,ERR 和 DBG 都打印,但 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的全部端口号。
创建服务端套接字接口的过程还是有点说法的,对于一个标准的服务器初始化流程应该是:
- 创建套接字 (
socket) - 设置地址重用 (
setsockopt) - 绑定地址 (
bind) - 开始监听 (
listen) - 设置非阻塞 (
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);
}
};