同样,我们在之前的代码中进行迭代,增加ddos和epoll实现并发的功能
ddos这里从网上“借鉴”一下,不过多的去梳理,能用就行
代码如下:
#if ENABLE_DDOS_DETECT
// 如果启用DDoS攻击检测
#define CAPTURE_WINDOWS 256
static double tresh = 1200.0; // 阈值,用于判断DDoS攻击的阈值
static uint32_t p_setbits[CAPTURE_WINDOWS] = {0}; // 存储每个窗口的设置位数
static uint32_t p_totbits[CAPTURE_WINDOWS] = {0}; // 存储每个窗口的总位数
static double p_entropy[CAPTURE_WINDOWS] = {0}; // 存储每个窗口的熵
static int pkt_idx = 0; // 当前窗口的索引
/*
23651/17408, E(nan)
1805328/4456448 Entropy(nan), Total_Entropy(4339971.575790)
23526/17408, E(nan)
1805203/4456448 Entropy(nan), Total_Entropy(4339902.272671)
17847/17408, E(nan)
1799524/4456448 Entropy(nan), Total_Entropy(4336731.547140)
17670/17408, E(nan)
1799347/4456448 Entropy(nan), Total_Entropy(4336632.027009)
17774/17408, E(nan)
1799451/4456448 Entropy(nan), Total_Entropy(4336690.507219)
*/
// 一些示例数据,可能是用于调试和演示的数据
static double ddos_entropy(double set_bits, double total_bits) {
// 计算熵的函数
return ( - set_bits) * (log2(set_bits) - log2(total_bits)) // 1
- (total_bits - set_bits) * (log2(total_bits - set_bits) - log2(total_bits))
+ log2(total_bits);
}
static uint32_t count_bit(uint8_t *msg, const uint32_t length) {
// 计算位数的函数,根据输入消息和长度返回设置的位数
}
static int ddos_detect(struct rte_mbuf *pkt) {
// DDoS攻击检测函数,用于检测DDoS攻击
static char flag = 0; // 1表示DDoS攻击,0表示没有攻击
uint8_t *msg = rte_pktmbuf_mtod(pkt, uint8_t *);
uint32_t set_bits = count_bit(msg, pkt->buf_len); // 计算消息中的设置位数
uint32_t tot_bits = pkt->buf_len * 8; // 消息的总位数
p_setbits[pkt_idx % CAPTURE_WINDOWS] = set_bits; // 存储设置位数
p_totbits[pkt_idx % CAPTURE_WINDOWS] = tot_bits; // 存储总位数
p_entropy[pkt_idx % CAPTURE_WINDOWS] = ddos_entropy(set_bits, tot_bits); // 存储熵值
if (pkt_idx >= CAPTURE_WINDOWS) {
// 如果达到了窗口大小
int i = 0;
uint32_t total_set = 0, total_bit = 0;
double sum_entropy = 0.0;
for (i = 0; i < CAPTURE_WINDOWS; i++) {
total_set += p_setbits[i]; // 累计设置位数
total_bit += p_totbits[i]; // 累计总位数
sum_entropy += p_entropy[i]; // 累计熵值
}
double entropy = ddos_entropy(total_set, total_bit); // 计算总熵值
if (tresh < sum_entropy - entropy) { // 如果总熵值超过阈值,表示DDoS攻击
if (!flag) { // 如果之前没有检测到攻击
rte_exit(EXIT_FAILURE, "ddos attack !!! Entropy(%f) < Total_Entropy(%f)\n",
entropy, sum_entropy); // 输出攻击信息并退出
}
flag = 1; // 设置攻击标志
} else {
if (flag) { // 如果之前检测到攻击
printf("no new !!! Entropy(%f) < Total_Entropy(%f)\n",
entropy, sum_entropy); // 输出没有新攻击的信息
}
flag = 0; // 清除攻击标志
}
pkt_idx = (pkt_idx + 1) % CAPTURE_WINDOWS + CAPTURE_WINDOWS; // 更新窗口索引
} else {
pkt_idx++; // 增加窗口索引
}
return 0; // 返回检测结果
}
#endif
main函数中通过调用ddos_detect进行防ddos攻击
#if ENABLE_DDOS_DETECT
unsigned i = 0;
for (i = 0;i < num_recvd;i ++) {
ddos_detect(rx[i]);
}
#endif
在之前的文章中,我们已经实现了完整的协议栈,但是只能用于处理一个客户端的请求,接下来我们就在原有的代码中增加epoll来实现并发的功能
先定义几个用于epoll的结构体
typedef union epoll_data {
void *ptr; // 用于存储一个指针,用于关联自定义数据
int fd; // 用于存储一个文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 定义一个名为 'struct epoll_event' 的数据结构,用于描述 epoll 事件的信息
struct epoll_event {
uint32_t events; // 表示事件的类型和状态,可以是多个标志的组合
epoll_data_t data; // 存储与事件相关的数据,可以是一个指针或文件描述符
};
// 表示 epoll 实例中的事件项
struct epitem {
RB_ENTRY(epitem) rbn; // 红黑树节点,用于按套接字进行快速查找
LIST_ENTRY(epitem) rdlink; // 链表节点,用于存储就绪的事件项
int rdy; // 是否在链表中的标志(存在于链表中则为非零)
int sockfd; // 套接字描述符
struct epoll_event event; // 关联的 epoll 事件
};
// 定义一个名为 'struct eventpoll' 的数据结构,用于管理 epoll 实例的状态
struct eventpoll {
int fd; // epoll 实例的文件描述符(fd)
ep_rb_tree rbr; // 红黑树,用于存储注册的事件
int rbcnt; // 红黑树节点计数
LIST_HEAD( ,epitem) rdlist; // 链表,用于存储就绪的事件项
int rdnum; // 就绪事件项的数量
int waiting; // 表示是否有线程正在等待事件的发生
pthread_mutex_t mtx; // 用于对红黑树进行更新的互斥锁
pthread_spinlock_t lock; // 用于对就绪事件链表进行更新的自旋锁
pthread_cond_t cond; // 用于线程等待事件的条件变量
pthread_mutex_t cdmtx; // 用于条件变量的互斥锁
};
接下来是实现epoll的主要函数
#if ENABLE_SINGLE_EPOLL
// epoll 事件回调函数,用于处理套接字事件
int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {
// 创建一个临时的 epitem 结构体,用于查找对应套接字的事件项
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
printf("rbtree not exist\n"); // 如果找不到对应的事件项,输出错误信息
return -1;
}
if (epi->rdy) {
// 如果事件项已经在就绪链表中,更新事件类型并返回 1
epi->event.events |= event;
return 1;
}
printf("epoll_event_callback --> %d\n", epi->sockfd);
// 如果事件项不在就绪链表中,加锁处理
pthread_spin_lock(&ep->lock);
epi->rdy = 1; // 设置事件项为就绪状态
LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink); // 将事件项添加到就绪链表头部
ep->rdnum ++; // 增加就绪事件数量
pthread_spin_unlock(&ep->lock); // 解锁就绪链表
pthread_mutex_lock(&ep->cdmtx); // 加锁条件变量互斥锁
pthread_cond_signal(&ep->cond); // 发送条件信号,通知等待的线程有新事件就绪
pthread_mutex_unlock(&ep->cdmtx); // 解锁条件变量互斥锁
}
// 创建一个新的 epoll 实例
int nepoll_create(int size) {
// 参数检查
if (size <= 0) return -1;
// 从位图中获取一个可用的文件描述符(fd),用于 epoll 实例
int epfd = get_fd_frombitmap(); //tcp, udp
// 为 epoll 实例分配内存
struct eventpoll *ep = (struct eventpoll*)rte_malloc("eventpoll", sizeof(struct eventpoll), 0);
if (!ep) {
// 如果内存分配失败,释放已分配的 fd 并返回错误
set_fd_frombitmap(epfd);
return -1;
}
// 获取全局的 TCP 表格实例,并将 epoll 实例关联到其中
struct ng_tcp_table *table = tcpInstance();
table->ep = ep;
// 初始化 epoll 实例的各个字段
ep->fd = epfd;
ep->rbcnt = 0;
RB_INIT(&ep->rbr);
LIST_INIT(&ep->rdlist);
// 初始化 epoll 实例的互斥锁
if (pthread_mutex_init(&ep->mtx, NULL)) {
free(ep);
set_fd_frombitmap(epfd);
return -2;
}
// 初始化 epoll 实例的条件变量互斥锁
if (pthread_mutex_init(&ep->cdmtx, NULL)) {
pthread_mutex_destroy(&ep->mtx);
free(ep);
set_fd_frombitmap(epfd);
return -2;
}
// 初始化 epoll 实例的条件变量
if (pthread_cond_init(&ep->cond, NULL)) {
pthread_mutex_destroy(&ep->cdmtx);
pthread_mutex_destroy(&ep->mtx);
free(ep);
set_fd_frombitmap(epfd);
return -2;
}
// 初始化 epoll 实例的自旋锁
if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {
pthread_cond_destroy(&ep->cond);
pthread_mutex_destroy(&ep->cdmtx);
pthread_mutex_destroy(&ep->mtx);
free(ep);
set_fd_frombitmap(epfd);
return -2;
}
// 返回创建的 epoll 实例的文件描述符(fd)
return epfd;
}
// 管理 epoll 实例中的事件关联
int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event) {
// 从文件描述符获取 epoll 实例的信息
struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);
// 如果 epoll 实例无效或事件为空且操作不是删除操作,返回错误
if (!ep || (!event && op != EPOLL_CTL_DEL)) {
errno = -EINVAL; // 设置错误码为无效参数
return -1;
}
// 如果操作是添加操作
if (op == EPOLL_CTL_ADD) {
// 获取 epoll 实例的互斥锁,用于保护对 epoll 实例的操作
pthread_mutex_lock(&ep->mtx);
// 创建一个临时的事件项,用于查找是否已存在相同的套接字
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
// 如果已经存在相同的套接字,解锁互斥锁并返回错误
if (epi) {
pthread_mutex_unlock(&ep->mtx);
return -1;
}
// 分配内存以存储事件项
epi = (struct epitem*)rte_malloc("epitem", sizeof(struct epitem), 0);
// 如果内存分配失败,解锁互斥锁并返回错误
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
rte_errno = -ENOMEM; // 设置错误码为内存不足
return -1;
}
epi->sockfd = sockid;
memcpy(&epi->event, event, sizeof(struct epoll_event));
// 在红黑树中插入事件项,表示注册了新的套接字
epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
// 增加 epoll 实例中的事件项计数
ep->rbcnt++;
// 解锁互斥锁
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_DEL) {
// 如果操作是删除操作
// 获取 epoll 实例的互斥锁
pthread_mutex_lock(&ep->mtx);
// 创建一个临时的事件项,用于查找要删除的套接字
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
// 如果未找到要删除的套接字,解锁互斥锁并返回错误
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
return -1;
}
// 从红黑树中移除事件项,表示取消了套接字的注册
epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
return -1;
}
// 减少 epoll 实例中的事件项计数
ep->rbcnt--;
// 释放事件项的内存
free(epi);
// 解锁互斥锁
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_MOD) {
// 如果操作是修改操作
// 创建一个临时的事件项,用于查找要修改的套接字
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
// 如果找到要修改的套接字
if (epi) {
// 更新套接字的事件类型
epi->event.events = event->events;
// 同时添加 EPOLLERR 和 EPOLLHUP 标志,表示关注错误和挂起事件
epi->event.events |= EPOLLERR | EPOLLHUP;
} else {
rte_errno = -ENOENT; // 设置错误码为未找到
return -1;
}
}
return 0; // 操作成功,返回 0
}
// 等待 epoll 事件的发生
int nepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) {
// 从文件描述符获取 epoll 实例的信息
struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);
// 如果 epoll 实例无效、事件数组为空或 maxevents 不合法,返回错误
if (!ep || !events || maxevents <= 0) {
rte_errno = -EINVAL; // 设置错误码为无效参数
return -1;
}
// 尝试获取 epoll 实例的条件变量互斥锁
if (pthread_mutex_lock(&ep->cdmtx)) {
// 如果获取锁失败,根据错误码输出相应信息
if (rte_errno == EDEADLK) {
printf("epoll lock blocked\n"); // 输出锁阻塞信息
}
}
// 当没有就绪的事件项并且超时时间不为零时,进入等待循环
while (ep->rdnum == 0 && timeout != 0) {
ep->waiting = 1; // 表示有线程正在等待事件的发生
if (timeout > 0) {
// 计算绝对超时时间
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
if (timeout >= 1000) {
int sec;
sec = timeout / 1000;
deadline.tv_sec += sec;
timeout -= sec * 1000;
}
deadline.tv_nsec += timeout * 1000000;
if (deadline.tv_nsec >= 1000000000) {
deadline.tv_sec++;
deadline.tv_nsec -= 1000000000;
}
// 使用条件变量的超时等待
int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
if (ret && ret != ETIMEDOUT) {
printf("pthread_cond_timewait\n");
pthread_mutex_unlock(&ep->cdmtx);
return -1;
}
timeout = 0; // 清除超时标志
} else if (timeout < 0) {
// 使用条件变量的等待,直到事件发生
int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
if (ret) {
printf("pthread_cond_wait\n");
pthread_mutex_unlock(&ep->cdmtx);
return -1;
}
}
ep->waiting = 0; // 表示等待结束
}
// 解锁条件变量互斥锁
pthread_mutex_unlock(&ep->cdmtx);
// 使用自旋锁对就绪事件链表进行操作
pthread_spin_lock(&ep->lock);
int cnt = 0; // 记录就绪事件的数量
int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum); // 计算最多处理的事件数
int i = 0;
// 遍历就绪事件链表,将事件拷贝到用户提供的事件数组中
while (num != 0 && !LIST_EMPTY(&ep->rdlist)) {
struct epitem *epi = LIST_FIRST(&ep->rdlist);
LIST_REMOVE(epi, rdlink);
epi->rdy = 0;
memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
num --;
cnt ++;
ep->rdnum --;
}
// 解锁自旋锁
pthread_spin_unlock(&ep->lock);
return cnt; // 返回处理的就绪事件数量
}
#endif
接下来修改我们之前的tcp服务器入口函数
// TCP 服务器入口函数
static int tcp_server_entry(__attribute__((unused)) void *arg) {
// 创建 TCP 套接字用于监听连接请求
int listenfd = nsocket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
return -1;
}
// 配置服务器监听地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
// 将套接字绑定到监听地址
nbind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
// 监听连接请求,最大等待队列长度为 10
nlisten(listenfd, 10);
// 创建 epoll 实例,用于事件管理
int epfd = nepoll_create(1); // event poll
// 初始化要监听的事件,关注可读事件
struct epoll_event ev, events[128];
ev.events = EPOLLIN;
ev.data.fd = listenfd;
// 将监听套接字添加到 epoll 实例中
nepoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
char buff[BUFFER_SIZE] = {0};
while(1) {
// 使用 epoll 等待就绪事件
int nready = nepoll_wait(epfd, events, 128, 5);
if (nready < 0) continue;
int i = 0;
for (i = 0;i < nready;i ++) {
if (listenfd == events[i].data.fd) {
// 如果监听套接字有事件,表示有新的客户端连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 接受客户端连接请求,创建新的连接套接字
int connfd = naccept(listenfd, (struct sockaddr*)&client, &len);
// 初始化要监听的事件,关注可读事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = connfd;
// 将连接套接字添加到 epoll 实例中
nepoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else { // clientfd
int connfd = events[i].data.fd;
// 接收客户端发送的数据
int n = nrecv(connfd, buff, BUFFER_SIZE, 0); // 阻塞
if (n > 0) {
// 如果成功接收数据,打印并回传给客户端
printf("recv: %s\n", buff);
nsend(connfd, buff, n, 0);
} else {
// 如果连接断开,从 epoll 实例中删除连接套接字并关闭连接
nepoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
nclose(connfd);
}
}
}
}
}
这次更新完,代码就有点多了,不在这里展示了,完整链接,方便自己以后回顾