多进程 + IO,1个连接对应1个进程的多进程模式,实现低并发:
相较于多进程IO,多线程共享进程空间,线程的通信变得简单、成本低、但是需要考虑线程安全的问题(互斥、锁实现)
利用并发模型4:单个反应堆 + 线程池的模式,实现一个支持高并发的小型的echo服务器。
反应堆模式(epoll)负责事件分发 + 线程池处理具体服务逻辑(echo服务):
注:客户端保证在断开之前会发送完整的数据,且客户端一定会调用
close()
#include "head.h"
#include "common.h"
#include "thread_pool.h"
#define QUEUESIZE 100//任务队列大小
#define INS 4//线程数量
#define MAXEVENTS 5//epoll_event的最大数量
#define MAXCLIENTS 2000//最大客户端数量为2000
int epollfd;
int clients[MAXCLIENTS];//每次出现新的套接字时便存储到该数组中 保证临时文件描述符fd在循环中被修改后 操作的对象也不会改变
pthread_mutex_t mutex[MAXCLIENTS];//对用户临时数据上锁
char *data[MAXCLIENTS];//线程进行具体业务操作时 存储每个客户端产生的临时数据
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void do_work(int fd) {
/* 对fd文件进行的具体逻辑操作 */
int rsize;
char buff[4096] = {0};
DBG(BLUE" : data is ready on %d.\n" NONE, fd);
//1.数据接收recv
if ((rsize = recv(fd, buff, sizeof(buff), 0)) < 0) {
/* 如果接受数据出错 则使用epoll_ctl将fd文件描述符删除 */
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
DBG(RED" : %d is close!\n" NONE, fd);
close(fd);//检测到客户端关闭连接 服务端也关闭连接
return;
}
//2.对数据进行简单的处理 并进行最后的send操作
int ind = strlen(data[fd]);
pthread_mutex_lock(&mutex[fd]);
for (int i = 0; i < rsize; ++i) {
if (buff[i] >= 'A' && buff[i] <= 'Z') {
data[fd][ind++] = buff[i] + 32;//大写转小写
} else if (buff[i] >= 'a' && buff[i] <= 'z') {
data[fd][ind++] = buff[i] - 32;//小写转大写
} else {
data[fd][ind++] = buff[i];//其他字符不处理
if (buff[i] == '\n') {
DBG(GREEN" : \\n recved!\n" NONE);
send(fd, data[fd], ind, 0);
}
}
}
pthread_mutex_unlock(&mutex[fd]);
}
void *thread_run(void *arg) {
pthread_detach(pthread_self());
struct task_queue *taskQueue = (struct task_queue *)arg;
while (1) {
int *fd = task_queue_pop(taskQueue);
do_work(*fd);
}
}
int main(int argc, char *argv[]) {
int opt;
int port;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port!\n", argv[0]);
exit(1);
}
}
DBG(YELLOW" : Server will listen on port [%d]\n" NONE, port);
//1.创建监听套接字
int server_listen;
if ((server_listen = socket_create(port)) < 0) handle_error("socket_create");
clients[server_listen] = server_listen;//文件描述符作为数组下标 存储新出现的listen_socket文件描述符
DBG(YELLOW" : Server_listen starts.\n" NONE);
//2.初始化任务队列和锁
for (int i = 0; i < MAXCLIENTS; ++i) data[i] = (char *)calloc(1, 4096 + 10);//使用calloc在reacotr thread中开辟内存空间
struct task_queue *taskQueue = (struct task_queue *)calloc(1, sizeof(struct task_queue));
task_queue_init(taskQueue, QUEUESIZE);
DBG(YELLOW" : task queue init successfully.\n" NONE);
for (int i = 0; i < MAXCLIENTS; ++i) pthread_mutex_init(&mutex[i], NULL);
DBG(YELLOW" : mutex init successfully.\n" NONE);
//3.创建多个线程进行工作
pthread_t *tid = (pthread_t *)calloc(INS, sizeof(pthread_t));
for (int i = 0; i < INS; ++i) pthread_create(&tid[i], NULL, thread_run, (void *)taskQueue);
DBG(YELLOW" : threads create successfully.\n" NONE);
//4.利用反应堆模式epoll进行事件分发
int sockfd;
// 4-1 epoll_create
if ((epollfd = epoll_create(1)) < 0) handle_error("epoll_create");//文件描述符被占用完就可能会出错
// 4-2 epoll_ctl将server_listen文件描述符注册到epoll实例中
struct epoll_event events[MAXEVENTS], ev;
ev.data.fd = server_listen;//关注的文件描述符
ev.events = EPOLLIN;//关注的事件、需要注册的事件
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_listen, &ev) < 0) handle_error("epoll_ctl");//注册操作
DBG(YELLOW" : server_listen is added to epollfd successfully.\n" NONE);
for (;;) {
// 4-3 epoll_wait开始监听
int nfds = epoll_wait(epollfd, events, MAXEVENTS, -1);//nfds epoll检测到事件发生的个数
if (nfds == -1) handle_error("epoll_wait");//可能被时钟中断 or 其他问题
for (int i = 0; i < nfds; ++i) {
// 4-3-1 对epoll_wait监测到发生的所有事件进行遍历,进行事件分发
int fd = events[i].data.fd;
if (fd == server_listen && (events[i].events & EPOLLIN)) {
/* 返回的fd为server_listen可读 表示已经有客户端进行3次握手了 */
/* events[i].events & EPOLLIN 表示至少有一个可读 */
// 4-3-2将accept到的文件描述符注册到epoll实例中,实现文件监听
if ((sockfd = accept(server_listen, NULL, NULL)) < 0) handle_error("accept");
//如果是实际应用情况,如果出现错误应该想办法处理错误,并恢复实际业务
clients[sockfd] = sockfd;//文件描述符作为数组下标 存储新出现的conn_socket文件描述符
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;//设置为边缘触发模式
make_nonblock(sockfd);
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) < 0) handle_error("epoll_ctl");//注册操作
} else {
/* 返回的fd不是server_listen可读 是普通的套接字 */
// 4-3-3 将监测到事件event的文件描述符fd加入任务队列中,交给线程池处理
if (events[i].events & EPOLLIN) {
/* 套接字属于就绪状态 有数据输入需要执行 */
task_queue_push(taskQueue, (void *)&clients[fd]);
//不可直接将fd传入 需要保证传入的fd值总是不同,创建clients[]数组保证每次传入的fd值不同
//当把地址作为参数传递给函数后(特别是在循环中),下一次fd的值会不断的被修改(传入的值是会变化的)
} else {
/* 套接字不属于就绪状态出错 将该事件的文件描述符从注册的epoll实例中删除 */
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}//for
}//for
return 0;
}
//common.h
#ifndef _COMMON_H
#define _COMMON_H
int socket_create(int port);
int socket_connect(const char *ip, int port);
int make_nonblock(int fd);
int make_block(int fd);
#endif
//common.c
#include "head.h"
#include "common.h"
int socket_create(int port) {
int sockfd;
struct sockaddr_in server;
//1.创建套截字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
//2.绑定套接字与结构体信息
server.sin_family = AF_INET;//ipv4
server.sin_port = htons(port);//host to net short 本地字节序的短整型转换为网络字节序
server.sin_addr.s_addr = htonl(INADDR_ANY);//host to net short 本地字节序的长整型转换为网络字节序 INADDR_ANY监听任何一个地址 0.0.0.0
int reuse = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int));//服务端地址重用
if (bind(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) return -1;
//3.将主动套接字转为被动
if (listen(sockfd, 20) < 0) return -1;// 20: backlog正在连接的连接序列有多少个
return sockfd;
}
int socket_connect(const char *ip, int port) {
int sockfd;
//1.客户端打开一个socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
//2.定义结构体用于绑定端口号、ip地址(存放服务端的具体信息)
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);//端口号
server.sin_addr.s_addr = inet_addr(ip);//ip地址
//3.建立连接connection
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) return -1;
return sockfd;
}
int make_nonblock(int fd) {
int flags;
if ((flags = fcntl(fd, F_GETFD)) < 0) return -1;
flags |= O_NONBLOCK;//为GF加上可爱属性
if (fcntl(fd, F_SETFL, flags) < 0) return -1;
return 0;
}
int make_block(int fd) {
int flags;
if ((flags = fcntl(fd, F_GETFD)) < 0) return -1;
flags &= ~O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) return -1;
return 0;
}
//thread_pool.h
#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H
#include "head.h"
struct task_queue {
int head, tail;
int size;//队列容量
int count;//已经入队的元素
void **data;//模拟任务
pthread_mutex_t mutex;//需要互斥锁加锁
pthread_cond_t cond;//信号量
};
void task_queue_init(struct task_queue *taskQueue, int size);//队列初始化
void task_queue_push(struct task_queue *taskQueue, void *data);//入队
void *task_queue_pop(struct task_queue *taskQueue);//出队
#endif
//thread_pool.c
#include "head.h"
#include "thread_pool.h"
void task_queue_init(struct task_queue *taskQueue, int size) {
taskQueue->size = size;
taskQueue->count = taskQueue->head = taskQueue->tail = 0;
taskQueue->data = calloc(size, sizeof(void *));
pthread_mutex_init(&taskQueue->mutex, NULL);
pthread_cond_init(&taskQueue->cond, NULL);
}
void task_queue_push(struct task_queue *taskQueue, void *data) {
/* 为了保证线程的安全性 所有对临界区的操作都需要加锁 */
pthread_mutex_lock(&taskQueue->mutex);
if (taskQueue->count == taskQueue->size) {
DBG(YELLOW" : taskQueue is full\n" NONE);
pthread_mutex_unlock(&taskQueue->mutex);
return;
}
taskQueue->data[taskQueue->tail] = data;
DBG(GREEN" : data is pushed!\n" NONE);
taskQueue->tail++;
taskQueue->count++;
/* 考虑循环队列 */
if (taskQueue->tail == taskQueue->size) {
DBG(YELLOW" : taskQueue tail reach end!\n" NONE);
taskQueue->tail = 0;
}
pthread_cond_signal(&taskQueue->cond);//信号量
pthread_mutex_unlock(&taskQueue->mutex);
return;
}
void *task_queue_pop(struct task_queue *taskQueue) {
pthread_mutex_lock(&taskQueue->mutex);
//使用while循环而不是if语句 处理惊群效应
while (taskQueue->count == 0) {
/* 当任务队列中没有任务时 线程选择等待而不是直接return
1.如果让线程直接返回则意味着一会还需要让线程轮训回来(轮询时间有要求)
2.轮询时间太短则消耗CPU
3.轮询时间太长则相应能力下降
*/
pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);//cond与mutex同时使用
}
void *data = taskQueue->data[taskQueue->head];
DBG(RED" : data is poped!\n" NONE);
taskQueue->count--;
taskQueue->head++;
/* 考虑循环队列 */
if (taskQueue->head == taskQueue->size) {
DBG(YELLOW" : taskQueue head reach end!\n" NONE);
taskQueue->head = 0;
}
pthread_mutex_unlock(&taskQueue->mutex);
return data;
}
几个问题:
在任务队列满时,是将数据丢弃还是继续等待直到数据可以被加入队列?答案是显然的线程必须进行等待,不可将数据随意丢弃。
再次考虑操作4-3-3,当文件有events动作时,将监测到事件event的文件描述符fd加入任务队列中,
发现可能存在一种情况,当task_queue_push将events的文件描述符fd加入队列后,然后立刻发生了一个events事件需要将新的fd加入到任务队列中(导致同一个文件fd在任务队列中出现两次)
当在线程池中的线程在进行recv操作时,可能出现第一次recv操作直接将两次数据都给接收了,导致第二次recv操作返回-1(发生在do_work函数中的数据接收部分),
考虑到这种recv返回情况的多样性,需要对do_work函数中的recv返回值判断进行修改,不能直接使用epoll_ctl将fd文件描述符删除,而是需要进一步判断是否存在上述发生的情况(通过判定errno是否为EAGAIN来进行处理):
//1.数据接收recv
if ((rsize = recv(fd, buff, sizeof(buff), 0)) < 0) {
if (errno != EAGAIN) {
/* 如果接受数据出错 则使用epoll_ctl将fd文件描述符删除 */
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
DBG(RED" : %d is close!\n" NONE, fd);
close(fd);//检测到客户端关闭连接 服务端也关闭连接
}
return;
}
当任务队列中出现了1个文件描述符的4次事件(依次出现在任务队列中),线程池中的线程会依次拿到该文件描述符的4次事件(但是4个线程的执行先后顺序是不清楚的),
如果4次事件中的某次事件为文件读取操作,那么其读取到的数据到底是什么数据?是会读取到最后一次响应的数据,还是会读取到最先开始的数据。
网络连接就是字节流,整个套接字网络数据就像一个水管,水管里面排的就是一个个字节。不论fd文件的events任务触发的顺序是如何的,当每次进行recv操作时,一定是从字节流的一端取出需要的数据。
多个线程并发的去处理数据时,会产生线程不安全性问题,难道对网络数据进行recv操作时,不会竞争吗?recv操作是线程安全的吗?