Linux高并发服务器开发(六)webserver服务器项目 个人总结

1 项目本身构架

1.1 大框架实现

(一开始还不会uml图 所以用的思维导图)

为什么要用线程池和互斥锁?

:创建线程&销毁线程会耗费计算机资源,采用线程池可以解决计算机无限制创建线程的情况。当有新的任务到来时可以去线程池中寻找空闲线程去服务。锁机制用来确保线程安全。

1.2 各个模块具体代码(框架)
互斥锁 – locker.h
#ifndef LOCKER_H
#define LOCKER_H

#include <pthread.h>
#include <exception>    // 异常
#include <semaphore.h>

// 线程同步机制分装类

// 互斥锁类
class locker {
public:
    // 构造函数
    locker(){
        if(pthread_mutex_init(&m_mutex, NULL) != 0){
            throw std::exception();
        }
    }

    // 上锁
    bool lock() {
        return pthread_mutex_lock(&m_mutex) == 0;
    }

    // 解锁
    bool unlock() {
        return pthread_mutex_unlock(&m_mutex) == 0;
    }

    pthread_mutex_t * get() {
        return &m_mutex;
    }

    // 析构函数
    ~locker() {
        pthread_mutex_destroy(&m_mutex);
    }
private:
    pthread_mutex_t m_mutex;    // 互斥锁
};


// 条件变量类
class cond {
public:
    cond() {
        if(pthread_cond_init(&m_cond, NULL) != 0){
            throw std::exception();
        }
    }

    ~cond() {
        pthread_cond_destroy(&m_cond);
    }

    bool wait(pthread_mutex_t * mutex) {
        return pthread_cond_wait(&m_cond, mutex) == 0;
    }

    bool timewait(pthread_mutex_t * mutex, struct timespec t) {
        return pthread_cond_timedwait(&m_cond, mutex, &t) == 0;
    }

    bool signal() {
        return pthread_cond_signal(&m_cond) == 0;
    }

    bool broadcast() {
        return pthread_cond_broadcast(&m_cond) == 0;
    }
private:
    pthread_cond_t m_cond;
};


// 信号量类
class sem {
public:
    sem() {
        if(sem_init(&m_sem, 0, 0) != 0) {
            throw std::exception();
        }
    }

    sem(int num) {
        if(sem_init(&m_sem, 0, num) != 0) {
            throw std::exception();
        }
    }   

    ~sem() {
        sem_destroy(&m_sem);
    }

    // 等待信号量
    bool wait() {
        return sem_wait(&m_sem) == 0;
    }

    // 增加信号量
    bool post() {
        return sem_post(&m_sem) == 0;
    }
private:
    sem_t m_sem;
};

#endif
线程池 – threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <pthread.h>
#include <list>
#include "locker.h"
#include <exception>
#include <cstdio>

// 线程池类 定义成模板类:为了代码的复用
template<typename T>
class threadpool {
public:
    threadpool(int thread_number = 8, int m_max_requests = 10000);
    ~threadpool();
    // 添加任务
    bool append(T * request);

private:
    static void* worker(void * arg);
    void run();

private:
    // 线程数量
    int m_thread_number;

    // 容器:线程池数组,大小为线程数量
    pthread_t * m_threads;

    // 请求队列中最多允许的等待处理的请求数量
    int m_max_requests;

    // 请求队列
    std::list< T*> m_workqueue;

    // 互斥锁
    locker m_queuelocker;

    // 信号量:用来判断是否有任务要处理
    sem m_queuestat;

    // 是否结束线程
    bool m_stop;

};

// 构造函数
template<typename T>
threadpool<T>::threadpool(int thread_number, int m_max_requests):
    m_thread_number(thread_number),m_max_requests(m_max_requests),
    m_stop(false), m_threads(NULL) {

        if((thread_number <= 0) || (m_max_requests <= 0) ) {
             throw std::exception();
        }

        // new一个数组空间
        m_threads = new pthread_t[m_thread_number]; 
            // 从堆中分配 m_thread_number 个 pthread_t 类型的内存,返回到指针 m_thread 中
        if(!m_thread_number) {
            throw std::exception();
        }

        // 创建thread_num个线程,并将它们设置为线程脱离
        for(int i = 0; i < thread_number; ++i) {
            printf("create the %dth thread
", i);

            // 创建线程:在C++中,第三个参数worker需要设置为静态函数
            if(pthread_create(m_threads + i, NULL, worker, this) != 0) {
                delete [] m_threads;
                throw std::exception();
            }

            // 设置线程分离
            if(pthread_detach(m_threads[i])) {
                delete[] m_threads;
                throw std::exception();
            }
        }
    
    }

// 析构函数
template<typename T>
threadpool<T>::~threadpool() {
    delete[] m_threads;
    m_stop = true;
}

// 添加任务函数:等待处理请求数量
template<typename T>
bool threadpool<T>::append(T * request) {

    m_queuelocker.lock();   // 上锁,开始操作
    if(m_workqueue.size() > m_max_requests) {
        m_queuelocker.unlock();
        return false;
    }

    m_workqueue.push_back(request); // 将元素添加到容器末尾
    m_queuelocker.unlock(); // 解锁,停止操作
    m_queuestat.post(); // 解锁,信号量+1 
    return true;
}

// 静态worker -- 在创建线程函数中为参数
template<typename T>
void* threadpool<T>::worker(void * arg) {
    threadpool * pool = (threadpool *) arg; 
    pool->run();    // 调用private中的方法run()
    return pool;    // 返回该对象
}

template<typename T>
void threadpool<T>::run() {
    while(!m_stop) {
        m_queuestat.wait();    // 若该信号量有值,则不会阻塞,信号量-1;若为0,则阻塞等待
        m_queuelocker.lock();   // 上锁,开始操作
        if(m_workqueue.empty()) {
            m_queuelocker.unlock();
            continue;
        }

        T* requests = m_workqueue.front();  // 取出队首元素 
        m_workqueue.pop_front();   // 取出第一个元素
        m_queuelocker.unlock();

        if(!requests) {
            continue;
        }

        requests->process();    //运行任务
    }

}

#endif
http连接部分 – http_coon.h
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H

#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/mman.h>   //
#include <stdarg.h>    //
#include <errno.h>
#include "locker.h"
#include <sys/uio.h>


class http_conn {
public:

    static int m_epollfd;   // 所有的socket上的事件都被注册到同一个epoll对象中,所以设置成静态的
    static int m_user_count;    // 统计用户的数量

    http_conn() {

    }
    ~http_conn() {
        
    }
    void process();    // 处理客户端请求
    void init(int sockfd, const sockaddr_in & addr);    // 初始化新接收的连接
    void close_conn();  // 关闭连接
    bool read();    // 非阻塞的读
    bool write();   // 非阻塞的写

private:
    int m_sockfd;   // 该http连接的socket套接字
    sockaddr_in m_address;  // 通信的socket地址
    

};

#endif
http连接部分 – http_coon.cpp
#include "http_conn.h"

int http_conn::m_epollfd = -1;  
int http_conn::m_user_count = 0;

// 设置文件描述符非阻塞
int setnonblocking(int fd) {
    int old_flag = fcntl(fd, F_GETFL);
    int new_flag = old_flag | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_flag);
}

// 用来向epoll中添加需要监听的文件描述符
void addfd(int epollfd, int fd, bool one_shot) {
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLRDHUP;

    if(one_shot) {
        event.events |= EPOLLONESHOT;   // 按位或并赋值
    }

    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);

    // 设置文件描述符非阻塞
    setnonblocking(fd);    //**
}

// 从epoll中移除监听的文件描述符
void removefd(int epollfd, int fd) {
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

// 修改文件描述符,重置socket上的EPOLLONESHOT事件,以确保下一次可读时,EPOLLIN事件可被触发
void modfd(int epollfd, int fd, int ev) {
    epoll_event event;
    event.data.fd = fd;
    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

// 初始化连接
void http_conn::init(int sockfd, const sockaddr_in & addr) {
    m_sockfd = sockfd;
    m_address = addr;

    // 设置sockfd的端口复用
    int reuse = 1;
    setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 添加到epoll对象中
    addfd(m_epollfd, m_sockfd, true);    //添加EPOLLEPOLLONESHOT事件
    m_user_count++;    // 总用户数+1
}

// 关闭连接
void http_conn::close_conn() {
    if(m_sockfd != -1) {
        // 删除
        removefd(m_epollfd, m_sockfd);
        m_sockfd = -1;  // 当sockfd为-1时,就没有用了
        m_user_count--;    // 关闭连接,客户总数量-1
    }
}

// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
    printf("read data
");
    return true;
}

bool http_conn::write() {
    printf("write data
");
    return true;
}

// 业务逻辑
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
    // 解析HTTP请求

    printf("parse request, create response
");

    // 生成响应

}
main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include "locker.h"
#include "threadpool.h"
#include <signal.h>
#include "http_conn.h"

#define MAX_FD 65535    // 最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000     // 一次监听的最大的时间数量

// 添加信号捕捉
void addsig(int sig, void(handler)(int)) {
    struct sigaction sa;    // 创建一个sigaction结构体变量sa
    memset(&sa, '', sizeof(sa));
    sa.sa_handler = handler;    // 函数指针 指向的函数就是信号捕捉到之后的处理函数
    sigfillset(&sa.sa_mask);    // 将信号集中的所有标志位都置为1 
    sigaction(sig, &sa, NULL);    // 检查或改变信号的处理,进行信号捕捉
        // 需要捕捉的信号编号或宏值,捕捉后的处理动作,对上一次捕捉相关的设置

}

// 添加文件描述符到epoll中
extern void addfd(int epollfd, int fd, bool oneshot);
// 从epoll中删除文件描述符
extern void removefd(int epollfd, int fd);
// 修改文件描述符
extern void modfd(int epollfd, int fd, int ev);

/*
    传递参数:argc -- 端口号
*/ 
int main(int argc, char * argv[]) {

    if( argc <= 1) {
        printf("按照如下格式运行:%s port_number
", basename(argv[0]));
        exit(-1);
    }

    // 获取端口号
    int port = atoi(argv[1]);

    // 对SIGPIPE信号进行处理
    addsig(SIGPIPE, SIG_IGN);  //忽略该信号

    // 创建线程池并初始化
    threadpool<http_conn> * pool =  NULL;
    try {
        pool = new threadpool<http_conn>;
    } catch(...) {
        exit(-1);
    }

    // 创建一个数组,用于保存所有的客户端信息
    http_conn * users = new http_conn[ MAX_FD ];

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);    // 创建一个监听的套接字
    /*
    if(listenfd = -1) {
        printf("error socket
");
        perror("socket");
        exit(-1);
    }
    */
    // 设置端口复用
    int reuse = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 绑定
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;   // 任何可以绑定的   
    address.sin_port = htons(port);    // 监听端口 主机字节序-->网络字节序

    bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    /*
    if(ret = -1) {
        perror("bind");
        exit(-1); 
    }
    */
   
    // 监听
    listen(listenfd, 5);
    /*
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }
    */

    // 创建epoll对象,事件数组,添加监听的文件描述符
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create(5);

    // 将监听的文件描述符添加到epoll对象中
    addfd(epollfd, listenfd, false);    // 监听的文件描述符不需要添加EPOLLONESHOT事件
    http_conn::m_epollfd = epollfd;

    //主线程不断循环检查有哪些事件发生
    while(true) {
        // 检测到了几个事件
        int num = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if((num < 0) && (errno != EINTR)) {
            // 调用失败
            printf("epoll failure
");
            break;
        }

        // 循环遍历事件数组
        for(int i = 0; i < num; i++) {
            // 获取监听到的文件描述符
            int sockfd = events[i].data.fd;
            if(sockfd == listenfd) {
                // 有客户端连接进来
                struct sockaddr_in client_address;
                socklen_t client_addrlen = sizeof(client_address);
                
                // 连接客户端
                int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);

                if(http_conn::m_user_count >= MAX_FD) {
                    // 说明目前连接数满了
                    // 给客户端写一个信息:服务器正忙(。。。)
                    close(connfd);
                    continue;
                }

                // 将新的客户数据初始化,放入数组中(把connfd作为users的索引)
                users[connfd].init(connfd, client_address);    // 初始化

            } else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {

                // 对方异常断开或者错误等事件
                // 关闭连接
                users[sockfd].close_conn();

            } else if(events[i].events & EPOLLIN) {
                
                // 判断读事件
                if(users[sockfd].read()) {
                    // 一次性把所有数据都读出来
                    pool->append(users + sockfd);   //**
                } else {
                    users[sockfd].close_conn();
                }

            } else if(events[i].events & EPOLLOUT) {
                
                // 判断写事件
                if( !users[sockfd].write() ){
                    // 一次性写完所有数据
                    users[sockfd].close_conn();
                }
            }
        }
    }

    close(epollfd);
    close(listenfd);
    delete [] users;
    delete pool;

    return 0;
}

1.3 测试运行(框架)

① 编译后生成“a.out”的exe文件,输入命令运行(我的Linux的ip地址为192.168.200.133)

./a.out 10000    // 10000是端口号 

② 在Windows网页中(或Linux的火狐浏览器)输入网址连接

http://192.168.200.133:10000

此时xshell中显示运行结果

2 解析HTTP报文

主要是对http_conn中的具体实现进行编写

2.1 read部分

在类中增加如下信息:

将添加监听文件改成边沿触发:(存在问题:listenfd也变成了边沿触发)

read() :

// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
    
    if(m_read_idx >= READ_BUFFER_SIZE) {
        return false;
    }

    // 定义一个读取到的字节
    int bytes_read = 0;
    while (1)
    {
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
        if(bytes_read == -1) {
            if(errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞的读的时候会出现这两个错误 -- 没有数据的情况
                break;  // 已读完,退出循环
            }
            return false;
        } else if(bytes_read == 0) {
            // 对方关闭连接
            return false;
        }
        m_read_idx += bytes_read;
    }
    
    printf("read data:%s
",m_read_buf);
    return true;
}

测试运行:读取到的请求报文

2.2 有限状态机

(http_conn.h)利用枚举设置状态(请求方法、主状态机、从状态机):

    // HTTP请求方法,这里只支持GET
    enum METHOD {GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT};
    
    /*
        解析客户端请求时,主状态机 的状态
        CHECK_STATE_REQUESTLINE:当前正在分析 请求行
        CHECK_STATE_HEADER:当前正在分析 头部字段
        CHECK_STATE_CONTENT:当前正在解析 请求体
    */
    enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };
    
    // 从状态机 的三种可能状态,即行的读取状态,分别表示
    // 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整
    enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };

    /*
        服务器处理HTTP请求的可能结果,报文解析的结果
        NO_REQUEST          :   请求不完整,需要继续读取客户数据
        GET_REQUEST         :   表示获得了一个完成的客户请求
        BAD_REQUEST         :   表示客户请求语法错误
        NO_RESOURCE         :   表示服务器没有资源
        FORBIDDEN_REQUEST   :   表示客户对资源没有足够的访问权限
        FILE_REQUEST        :   文件请求,获取文件成功
        INTERNAL_ERROR      :   表示服务器内部错误
        CLOSED_CONNECTION   :   表示客户端已经关闭连接了
    */
    enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
2.3 具体代码实现(完整)

3 C++相关知识点

3.1 抛出异常

throw 关键字用来显式地抛出异常;C++中的 throw详解_c++ throw-CSDN博客

#include <exception>

throw std::exception();
3.2 new

从堆中分配内存,并返回一个指向所分配内存的指针

3.3 this指针

指向当前对象的地址(调用该成员函数的对象的地址)

3.4 basename 用法

获取当前路径最后一个

3.5 memset 内存初始化函数

memset(指向内存区域的指针,要设置的特定值,要设置的字节段);

3.6 try-catch

try {

// 可能发生异常的语句

} catch {

// 处理异常语句

}

3.7 extern 关键字

在别的文件中定义了,提醒编译器去找

3.8 类作用域

如本项目,在http_conn.h中定义了一些枚举类,定义了一些成员函数返回这些枚举类。(类内定义)

在类外实现时(http_conn.cpp)需在枚举类和成员函数名前都加上类的作用域,不然会报错。

3.9 内联函数inline

参考资料:

【C++】C++中内联函数详解(搞清内联的本质及用法)-CSDN博客

3.10 用到的string库中的一些函数

4 问题解决

4.1 编译错误

在完成主体框架后进行编译(gcc *.cpp -pthread)提示错误如下:

解决参考资料:使用gcc编译c++程序时出现类似对‘operator new[](unsigned long)’未定义的引用_对‘operator new(unsigned long)’未定义的引用-CSDN博客

添加-lstdc++,即可编译成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值