云计算百科
云计算领域专业知识百科平台

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

1 项目本身构架

1.1 大框架实现

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

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

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\\n", 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\\n");
return true;
}

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

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

printf("parse request, create response\\n");

// 生成响应

}

        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, '\\0', 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\\n", 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\\n");
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\\n");
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\\n",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++,即可编译成功

赞(0)
未经允许不得转载:网硕互联帮助中心 » Linux高并发服务器开发(六)webserver服务器项目 个人总结
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!