本文是对github上万star项目源码解析,供大家学习交流
项目地址:
https://github.com/qinguoyi/TinyWebServer
七、webserver
头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>
#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"
1、类的定义
Web服务器的类定义,包含了很多成员函数和变量来实现服务器的各种功能。
成员函数包括了一些初始化函数,线程池、数据库连接池、定时器等的相关操作函数。同时还包含了事件监听函数,读写事件处理函数,信号处理函数等,来实现服务器的功能。
const int MAX_FD = 65536; //最大文件描述符
const int MAX_EVENT_NUMBER = 10000; //最大事件数
const int TIMESLOT = 5; //最小超时单位
class WebServer
{
public:
WebServer();
~WebServer();
void init(int port , string user, string passWord, string databaseName,
int log_write , int opt_linger, int trigmode, int sql_num,
int thread_num, int close_log, int actor_model);
void thread_pool();
void sql_pool();
void log_write();
void trig_mode();
void eventListen();
void eventLoop();
void timer(int connfd, struct sockaddr_in client_address);
void adjust_timer(util_timer *timer);
void deal_timer(util_timer *timer, int sockfd);
bool dealclinetdata();
bool dealwithsignal(bool& timeout, bool& stop_server);
void dealwithread(int sockfd);
void dealwithwrite(int sockfd);
public:
//基础
int m_port; // 服务器监听的端口号
char *m_root; // 网站根目录
int m_log_write; // 日志写入方式
int m_close_log; // 是否关闭日志
int m_actormodel; // 使用的反应堆模型类型
int m_pipefd[2]; // 父进程和子进程间通信的管道描述符
int m_epollfd; // epoll句柄
http_conn *users; // http连接的数组
//数据库相关
connection_pool *m_connPool; // 连接池
string m_user; //登陆数据库用户名
string m_passWord; //登陆数据库密码
string m_databaseName; //使用数据库名
int m_sql_num; // 每个线程执行的最大SQL语句数
//线程池相关
threadpool<http_conn> *m_pool; // 线程池
int m_thread_num; // 线程池中线程数量
//epoll_event相关
epoll_event events[MAX_EVENT_NUMBER]; // 用于存储epoll_wait返回的事件的数组
int m_listenfd; // 监听socket描述符
int m_OPT_LINGER; // 优雅关闭连接选项
int m_TRIGMode; // 触发模式
int m_LISTENTrigmode; // 监听socket的触发模式
int m_CONNTrigmode; // 连接socket的触发模式
//定时器相关
client_data *users_timer; // 客户连接的定时器信息
Utils utils; // 工具集
};
2、构造函数和析构函数
构造函数
WebServer::WebServer(){
// 在堆中初始化http_conn类对象数组,该数组最大长度为MAX_FD
users = new http_conn[MAX_FD];
// 获取当前工作目录,并将其存储在server_path字符数组中
char server_path[200];
getcwd(server_path, 200);
// 将根目录的路径存储在root字符数组中
char root[6] = "/root";
// 使用malloc函数在堆上分配一块内存,大小为server_path和root长度之和加1
m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
// 将server_path和root连接成一个字符串并存储在m_root指针中
strcpy(m_root, server_path);
strcat(m_root, root);
// 这个数组将被用于实现定时器功能
users_timer = new client_data[MAX_FD];
}
析构函数,调用close()函数关闭了WebServer类对象中成员变量m_epollfd、m_listenfd、m_pipefd[1]和m_pipefd[0]所对应的文件描述符,使用delete[]释放了WebServer类对象中的成员变量users和users_timer所分配的内存空间
WebServer::~WebServer(){
close(m_epollfd); // 使用epoll_create1()系统调用创建的epoll实例的文件描述符
close(m_listenfd); // 监听套接字的文件描述符
close(m_pipefd[1]); // 父子进程之间通信的管道写端的文件描述符
close(m_pipefd[0]); // 父子进程之间通信的管道读端的文件描述符
delete[] users; // 管理连接到服务器的客户端连接
delete[] users_timer; // 每个元素都对应一个客户端连接,并存储了该连接的信息,如socket文件描述符、最后一次活跃时间等
delete m_pool; // 线程池对象,用于处理客户端请求
}
3、init()方法
init函数主要用于初始化WebServer类对象的成员变量。其中,该函数的参数包括了需要配置的服务器参数,在函数内部,使用赋值语句为WebServer类对象的成员变量赋值,将传入的参数保存到类中
void WebServer::init(int port, string user, string passWord, string databaseName, int log_write,
int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model){
m_port = port; // 服务器监听的端口号
m_user = user; // 登录数据库所需的用户名
m_passWord = passWord; // 登录数据库所需的密码
m_databaseName = databaseName; // 需要连接的数据库名称
m_sql_num = sql_num; // 每个线程处理的最大数据库任务数
m_thread_num = thread_num; // 线程池中的线程数
m_log_write = log_write; // 日志写入方式,0表示同步写入,1表示异步写入
m_OPT_LINGER = opt_linger; // 优雅关闭连接的方式,0表示不使用,1表示使用
m_TRIGMode = trigmode; // 触发模式,0表示LT(水平触发),1表示ET(边缘触发)
m_close_log = close_log; // 是否关闭日志,0表示不关闭,1表示关闭
m_actormodel = actor_model; // 并发模型选择,0表示Proactor,1表示Reactor
}
4、thread_pool()方法
thread_pool函数用于创建线程池并启动线程池中的线程。该函数使用"threadpool"第三方模板库,通过调用其构造函数new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num),创建了一个线程池,其中:
void WebServer::thread_pool(){
/*
线程池
m_actormodel:并发模型选择,0表示Proactor,1表示Reactor。
m_connPool:连接池对象,用于管理和复用客户端与服务器之间的连接。
m_thread_num:线程池中的线程数。
*/
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}
5、sql_pool()方法
sql_pool函数用于初始化数据库连接池和读取表
- 初始化数据库连接池:调用connection_pool类的GetInstance静态方法获得唯一的连接池实例,并使用该实例的init方法对连接池进行初始化。通过参数指定需要连接的数据库的相关信息,包括地址(“localhost”)、用户名(m_user)、密码(m_passWord)、数据库名称(m_databaseName)、端口号(3306)等。同时,也可以通过设置m_sql_num控制每个线程处理的最大数据库任务数,以及m_close_log控制是否关闭日志输出。
- 初始化数据库读取表:使用users数组中的每个http_conn对象的initmysql_result方法,将数据库连接池对象m_connPool作为参数传入。这样,在每个http_conn对象被创建时,都会自动获取一个可用的数据库连接,并用该连接初始化mysql_result对象,以便后续进行数据库查询操作。
void WebServer::sql_pool(){
// 初始化数据库连接池
m_connPool = connection_pool::GetInstance();
m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log);
// 初始化数据库读取表
users->initmysql_result(m_connPool);
}
6、log_write()方法
log_write函数主要用于初始化日志输出。具体来说,该函数通过检测成员变量m_close_log的值,判断是否需要关闭日志输出
void WebServer::log_write(){
// 如果m_close_log为0(即不需要关闭),则调用Log类的get_instance静态方法获得唯一的Log实例,并使用其init方法初始化日志输出
if (0 == m_close_log){
// 初始化日志,如果m_log_write为1,则采用异步写入方式
if (1 == m_log_write)
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);
// 否则采用同步写入方式
else
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}
7、trig_mode()方法
trig_mode函数主要用于根据参数m_TRIGMode设置监听套接字和连接套接字的触发模式。其中,m_TRIGMode表示触发模式选择,其值0、1、2、3分别代表LT + LT、LT + ET、ET + LT、ET + ET四种组合方式。
在LT模式下,当一个描述符上有可读事件时,每次epoll_wait都能把这个事件通知给用户处理;而在ET模式下,当一个描述符从不可读变为可读时,才将这个事件通知给用户处理一次。通过设置不同的触发模式,可以影响服务器在处理客户端请求时的运行效率和响应速度。
void WebServer::trig_mode(){
/*
m_TRIGMode == 0,则使用LT + LT。
m_TRIGMode == 1,则使用LT + ET。
m_TRIGMode == 2,则使用ET + LT。
m_TRIGMode == 3,则使用ET + ET。
m_LISTENTrigmode表示监听套接字的触发模式
m_CONNTrigmode表示连接套接字的触发模式
*/
// LT + LT
if (0 == m_TRIGMode){
m_LISTENTrigmode = 0;
m_CONNTrigmode = 0;
}
// LT + ET
else if (1 == m_TRIGMode){
m_LISTENTrigmode = 0;
m_CONNTrigmode = 1;
}
// ET + LT
else if (2 == m_TRIGMode){
m_LISTENTrigmode = 1;
m_CONNTrigmode = 0;
}
// ET + ET
else if (3 == m_TRIGMode){
m_LISTENTrigmode = 1;
m_CONNTrigmode = 1;
}
}
8、eventListen()方法
eventListen函数主要用于创建套接字、绑定端口、监听连接请求,并初始化epoll内核事件表
通过调用eventListen函数,WebServer可以正常地监听客户端连接请求、初始化epoll内核事件表并进行相关操作,为后续的客户端请求处理提供了必要的基础设施
void WebServer::eventListen(){
// 网络编程基础步骤,使用socket函数创建TCP套接字
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(m_listenfd >= 0);
// 通过setsockopt函数设置SO_LINGER选项为0或1,从而实现优雅关闭连接
if (0 == m_OPT_LINGER){
struct linger tmp = {0, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
else if (1 == m_OPT_LINGER){
struct linger tmp = {1, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
int ret = 0;
// 初始化一个IPv4地址结构体sockaddr_in address
struct sockaddr_in address;
bzero(&address, sizeof(address)); // 用bzero()函数将address结构体清零,确保其中的所有成员都被初始化为0
address.sin_family = AF_INET; // 设置address结构体中的sin_family成员为AF_INET,表示使用IPv4地址族
address.sin_addr.s_addr = htonl(INADDR_ANY); // 将address结构体中的sin_addr成员设置为任意IP地址,即0.0.0.0,表示该服务器可以接收来自任何网络接口的连接请求
address.sin_port = htons(m_port); // 将address结构体中的sin_port成员设置为指定端口号m_port的网络字节序,以便后续调用bind()函数进行端口绑定
int flag = 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
// 绑定端口和IP地址,使用bind函数将套接字绑定到指定的本地IP地址和端口号上
ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret >= 0);
// 监听连接请求,使用listen函数开始监听客户端连接请求
ret = listen(m_listenfd, 5);
assert(ret >= 0);
// 初始化utils工具类,通过调用utils工具类的init方法,对定时器相关参数进行初始化
utils.init(TIMESLOT);
// 创建epoll内核事件表
epoll_event events[MAX_EVENT_NUMBER];
// 使用epoll_create函数创建一个epoll内核事件表,返回一个文件描述符
m_epollfd = epoll_create(5);
// 若成功则将该文件描述符存储在m_epollfd变量中
assert(m_epollfd != -1);
// 向epoll内核事件表中添加监听套接字,使用utils工具类的addfd方法,将监听套接字添加到epoll内核事件表中,并设置是否采用LT模式等触发方式,以及是否启用ET模式等触发方式
utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
// 设置http_conn类的静态成员变量m_epollfd,将创建好的epoll内核事件表的文件描述符赋值给http_conn类的静态成员变量m_epollfd,以便后续http_conn对象的创建和管理
http_conn::m_epollfd = m_epollfd;
// 创建双向管道,使用socketpair函数创建一个双向管道,并将文件描述符存储在m_pipefd数组中
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
assert(ret != -1);
utils.setnonblocking(m_pipefd[1]);
// 向epoll内核事件表中添加管道读端,使用utils工具类的addfd方法,将管道读端添加到epoll内核事件表中,并设置是否采用LT模式等触发方式,以及是否启用ET模式等触发方式
utils.addfd(m_epollfd, m_pipefd[0], false, 0);
// 设置信号处理函数,使用utils工具类的addsig方法,将SIGPIPE、SIGALRM和SIGTERM三个信号的处理函数设置为SIG_IGN或utils的sig_handler函数
utils.addsig(SIGPIPE, SIG_IGN);
utils.addsig(SIGALRM, utils.sig_handler, false);
utils.addsig(SIGTERM, utils.sig_handler, false);
// 定时器初始化,使用alarm函数定时器,定时时间为TIMESLOT秒,默认为5秒
alarm(TIMESLOT);
// 将双向管道写端、epoll内核事件表文件描述符等变量存储在Utils的静态成员变量u_pipefd和u_epollfd中,以便后续工具类的操作
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
}
9、eventLoop()方法
事件循环主函数,主要作用是接收连接请求、处理数据请求和定时器事件
void WebServer::eventLoop(){
bool timeout = false; // 用于表示是否超时
bool stop_server = false; // 用于表示是否停止服务器
// 当stop_server变量被设置为true时,表示服务器需要停止运行,则跳出循环,结束主函数的执行
while (!stop_server){
// 调用 epoll_wait 函数等待事件的发生,在有事件发生时返回相应的事件类型和文件描述符
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR){
LOG_ERROR("%s", "epoll failure");
break;
}
for (int i = 0; i < number; i++){
int sockfd = events[i].data.fd;
// 如果事件类型是新连接请求,则调用 dealclinetdata 函数进行处理
if (sockfd == m_listenfd){
bool flag = dealclinetdata();
if (false == flag)
continue;
}
// 如果事件类型是客户端断开、服务端关闭连接或出现错误,则移除对应的定时器
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
// 如果事件类型是信号,则调用 dealwithsignal 函数进行处理
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN)){
bool flag = dealwithsignal(timeout, stop_server);
if (false == flag)
LOG_ERROR("%s", "dealclientdata failure");
}
// 如果事件类型是客户端发送数据,则调用 dealwithread 函数进行处理
else if (events[i].events & EPOLLIN){
dealwithread(sockfd);
}
// 如果事件类型是可写,说明之前发起的写操作已完成,则调用 dealwithwrite 函数进行处理
else if (events[i].events & EPOLLOUT){
dealwithwrite(sockfd);
}
}
// 如果timeout变量为 true,则表明当前轮询已经超时,需要处理定时器事件
if (timeout){
// 提供timer_handler函数来处理链表中已经到期的定时器事件
utils.timer_handler();
LOG_INFO("%s", "timer tick");
timeout = false;
}
}
}
10、timer()方法
在每次有新连接时被调用,用于初始化用户数据和创建定时器
该函数的主要目的是为每个连接套接字创建一个定时器,以便在一定时间内未收到客户端请求时能够自动关闭连接,防止服务器资源浪费
void WebServer::timer(int connfd, struct sockaddr_in client_address){
// 初始化用户数据,使用连接套接字connfd和客户端地址client_address初始化一个User对象,并将其保存在全局数组users中,以便后续使用
users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);
// 初始化client_data数据
// 创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
util_timer *timer = new util_timer;
// 绑定用户数据,将该连接套接字对应的User对象的指针保存在定时器的user_data成员变量中,以便在定时器回调函数中使用
timer->user_data = &users_timer[connfd];
// 将定时器的回调函数设置为cb_func
timer->cb_func = cb_func;
time_t cur = time(NULL);
// 将定时器的超时时间设置为当前时间加上3倍的TIMESLOT值(TIMESLOT是一个常量,表示时间片的长度)
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
// 将定时器添加到链表中
utils.m_timer_lst.add_timer(timer);
}
11、adjust_timer()方法
用于调整定时器的函数。在传输数据时,该函数被调用以延迟定时器并对其在链表上的位置进行调整
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
void WebServer::adjust_timer(util_timer *timer){
time_t cur = time(NULL); // 获取当前时间
// 将定时器的超时时间设置为当前时间+3个单位的时间片(TIMESLOT),这意味着定时器将在当前时间之后的3个时间片后过期,即等待数据传输完成后再执行回调函数
timer->expire = cur + 3 * TIMESLOT;
// 对定时器在链表上的位置进行调整
utils.m_timer_lst.adjust_timer(timer);
LOG_INFO("%s", "adjust timer once");
}
12、deal_timer()方法
用于处理定时器的函数。当定时器超时时,该函数被调用以执行定时器回调函数
void WebServer::deal_timer(util_timer *timer, int sockfd){
// 用定时器的回调函数
timer->cb_func(&users_timer[sockfd]);
// 如果定时器存在
if (timer){
// 删除定时器
utils.m_timer_lst.del_timer(timer);
}
LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}
13、dealclinetdata()方法
用于处理客户端数据的函数。在没有数据到达时,该函数会一直阻塞等待客户端连接
bool WebServer::dealclinetdata(){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
// 根据服务器的触发模式(m_LISTENTrigmode),执行不同的操作
// 如果触发模式为0,即LT模式
if (0 == m_LISTENTrigmode){
// 则调用accept()函数接受客户端连接请求,并传入参数m_listenfd作为监听套接字
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
// 如果连接失败,则记录错误信息并返回false
if (connfd < 0){
LOG_ERROR("%s:errno is:%d", "accept error", errno);
return false;
}
// 如果当前连接数超出最大限制(MAX_FD)
if (http_conn::m_user_count >= MAX_FD){
// 则向客户端发送“Internal server busy”消息,并记录错误信息后返回false
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
return false;
}
// 否则就创建一个定时器,并将套接字和客户端地址作为参数传入
timer(connfd, client_address);
}
// 如果触发模式为1,即ET模式
else{
// 使用while循环接受所有连接请求
while (1){
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
// 如果连接失败,则记录错误信息并退出循环
if (connfd < 0){
LOG_ERROR("%s:errno is:%d", "accept error", errno);
break;
}
// 如果当前连接数超出最大限制(MAX_FD)
if (http_conn::m_user_count >= MAX_FD){
// 则向客户端发送“Internal server busy”消息,并记录错误信息后退出循环
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
break;
}
// 否则就创建一个定时器,并将套接字和客户端地址作为参数传入
timer(connfd, client_address);
}
return false;
}
return true;
}
14、dealwithsignal()方法
用于处理信号的函数。它通过管道接收信号,并根据不同的信号类型执行相应的操作
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server){
int ret = 0;
int sig;
char signals[1024];
// 使用recv()函数从管道读取信号,并将信号存储在名为“signals”的字符数组中
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
// 如果读取失败,则返回false
if (ret == -1){
return false;
}
// 如果没有收到任何信号,则返回false
else if (ret == 0){
return false;
}
// 否则,遍历字符数组并根据不同的信号类型执行相应的操作
else{
for (int i = 0; i < ret; ++i){
switch (signals[i]){
// 如果收到SIGALRM信号,则将timeout设置为true,表示定时器超时。这通常是由定时器到期引起的,可以让主函数检查此标志并执行相应的操作
case SIGALRM:{
timeout = true;
break;
}
// 如果收到SIGTERM信号,则将stop_server设置为true,表示需要停止服务器。这通常是由管理员或操作系统发出的信号,用于安全关闭服务器
case SIGTERM:{
stop_server = true;
break;
}
}
}
}
return true;
}
15、dealwithread()方法
用于处理读事件的函数。当服务器监测到套接字上有可读事件时,该函数将被调用以处理数据
void WebServer::dealwithread(int sockfd){
// 获取与套接字相关联的定时器
util_timer *timer = users_timer[sockfd].timer;
// 根据服务器模型(m_actormodel)执行不同的操作
// 如果服务器模型为1,即采用Reactor模型
if (1 == m_actormodel){
if (timer){
// 将定时器延迟3个单位,并将读事件放入请求队列
adjust_timer(timer);
}
//若监测到读事件,将该事件放入请求队列
m_pool->append(users + sockfd, 0);
// 使用while循环等待其他线程处理请求并更新用户状态,直到发现用户状态已经被改变为“improv=1”才退出循环
while (true){
if (1 == users[sockfd].improv){
// 如果定时器标志为1
if (1 == users[sockfd].timer_flag){
// 处理定时器事件
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
// 如果服务器模型为0,即采用Proactor模型
else{
//读取套接字上的数据
if (users[sockfd].read_once()){
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
// 如果读取成功,则将读事件放入请求队列
m_pool->append_p(users + sockfd);
// 如果定时器存在
if (timer){
// 将其延迟3个单位
adjust_timer(timer);
}
}
// 如果读取失败,则调用“deal_timer(timer,sockfd)”函数处理定时器事件
else{
deal_timer(timer, sockfd);
}
}
}
16、dealwithwrite()方法
用于处理写事件的函数。当服务器检测到套接字上有可写事件时,该函数将被调用以向客户端发送数据
void WebServer::dealwithwrite(int sockfd){
// 获取与套接字相关联的定时器
util_timer *timer = users_timer[sockfd].timer;
// 据服务器模型(m_actormodel)执行不同的操作
// 如果服务器模型为1,即采用Reactor模型
if (1 == m_actormodel){
if (timer){
// 将定时器延迟3个单位
adjust_timer(timer);
}
// 将写事件放入请求队列
m_pool->append(users + sockfd, 1);
// 使用while循环等待其他线程处理请求并更新用户状态,直到发现用户状态已经被改变为“improv=1”才退出循环
while (true){
if (1 == users[sockfd].improv){
// 如果定时器标志为1
if (1 == users[sockfd].timer_flag){
// 调用“deal_timer(timer,sockfd)”函数处理定时器事件
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
// 如果服务器模型为0,即采用Proactor模型
else{
// 使用“users[sockfd].write()”函数向客户端发送数据
if (users[sockfd].write()){
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
if (timer){
// 将定时器延迟3个单位
adjust_timer(timer);
}
}
else{
// 果发送失败,则调用“deal_timer(timer,sockfd)”函数处理定时器事件
deal_timer(timer, sockfd);
}
}
}