webserver总结
前言:
最近做毕设刚好看了这个项目,所以做了一个总结,同时也希望可以帮到其他人。如果本文中有一些概念看不懂,比如说epoll之类的概念,可以自行百度,对这些概念的理解不求深入,对这些概念有一个了解,就可以看懂代码了,如果有不清楚的地方欢迎在评论区交流。
一.服务器主线:
1.server主进程
主要功能是根据epoll中的event对象来处理事件,根据event对象中的文件描述符fd来判断事件类型。事件的类型有:根据网络套接字描述符创建新的http连接,发生异常关闭一个http连接,处理一个定时器信号检测有无超时事件,处理客户端http请求的发送的数据,向客户端写入数据。
void WebServer::eventLoop()
{
bool timeout = false;
bool stop_server = false;
while (!stop_server)
{
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;
//处理新到的客户连接
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);
}
//处理信号
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
{
bool flag = dealwithsignal(timeout, stop_server);
if (false == flag)
LOG_ERROR("%s", "dealclientdata failure");
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
dealwithread(sockfd);
}
else if (events[i].events & EPOLLOUT)
{
dealwithwrite(sockfd);
}
}
if (timeout)
{
utils.timer_handler();
LOG_INFO("%s", "timer tick");
timeout = false;
}
}
}
2.处理新的http连接
当通过m_listened的套接字描述符监听到新的http连接时,通过accept函数和作为参数m-listened获取到当前请求的网络套接字描述符和IP地址以及端口号。
然后就可以根据网络套接字描述符初始化对应的http类的对象以及初始化对应的定时器类对象,然后将对应的定时器指针添加到一个截止时间递增的双端队列里,每5秒检测一次,用来处理超时的请求。
bool WebServer::dealclinetdata()
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
if (0 == m_LISTENTrigmode)
{
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
return false;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
return false;
}
timer(connfd, client_address);
}
else
{
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;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
break;
}
timer(connfd, client_address);
}
return false;
}
return true;
}
这段代码主要就是http的初始化和定时器的建立。
void WebServer::timer(int connfd, struct sockaddr_in client_address)
{
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;
timer->user_data = &users_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
utils.m_timer_lst.add_timer(timer);
}
3.处理错误
当epoll事件返回的状态表明发生了错误时,服务器就会关闭连接。
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
然后通过一个函数关闭该连接
void cb_func(client_data *user_data)
{
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
close(user_data->sockfd);
http_conn::m_user_count--;
}
4.处理定时信号
定时信号也通过epoll事件来实现,具体操作是使用一个管道,从管道的一端写入另一端读取,当有固定时间间隔到达了,处理所有的连接。
以下这段代码的作用是绑定了读端的套接字,同时设定了ALARM函数到达指定时间时的处理信号函数。
utils.setnonblocking(m_pipefd[1]);
utils.addfd(m_epollfd, m_pipefd[0], false, 0);
utils.addsig(SIGPIPE, SIG_IGN);
utils.addsig(SIGALRM, utils.sig_handler, false);
utils.addsig(SIGTERM, utils.sig_handler, false);
alarm(TIMESLOT);
//工具类,信号和描述符基础操作
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
当ALARM函数到达指定时间时,需要对epoll事件进行一次处理,对于该事件的处理,要么设置超时事件,要么检测到关闭,停止服务器端运行。
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
int ret = 0;
int sig;
char signals[1024];
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
return false;
}
else if (ret == 0)
{
return false;
}
else
{
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
break;
}
}
}
}
return true;
}
当发生超时事件后,具体的处理过程是这样的:该函数的主要作用是处理定时器队列和重新设置时间,使用tick函数处理超时事件。
void Utils::timer_handler()
{
m_timer_lst.tick();
alarm(m_TIMESLOT);
}
void sort_timer_lst::tick()
{
if (!head)
{
return;
}
time_t cur = time(NULL);
util_timer *tmp = head;
while (tmp)
{
if (cur < tmp->expire)
{
break;
}
tmp->cb_func(tmp->user_data);
head = tmp->next;
if (head)
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}
5.处理http请求-读
若检测到读事件,首先调整该http请求的定时器,延长三个时间间隔,后把该读事件放入到请求队列当中,用多线程的方式来处理。然后有两种模式,同步和异步的模式,同步的情况下会等待该事件http请求处理完毕,如果发生错误则会把time_flag置为1,此时则会关闭此连接,异步则在使用请求后不用等待执行完毕。
void WebServer::dealwithread(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;
//reactor
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}
//若监测到读事件,将该事件放入请求队列
m_pool->append(users + sockfd, 0);
while (true)
{
if (1 == users[sockfd].improv)
{
if (1 == users[sockfd].timer_flag)
{
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else
{
//proactor
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)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}
(1)read_once
然后通过read_once函数来读取数据,如果收到读的数据,但是使用recv函数没有读取字符,则说明出现了错误。
bool http_conn::read_once()
{
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;
//LT读取数据
if (0 == m_TRIGMode)
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read;
if (bytes_read <= 0)
{
return false;
}
return true;
}
//ET读数据
else
{
while (true)
{
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) //EAGAIN信号代表没有数据可读
break;
return false;
}
else if (bytes_read == 0)
{
return false;
}
m_read_idx += bytes_read;
}
return true;
}
}
(2)run函数
现在来详细研究一下多线程中每一个线程的代码:这个代码中创建了8个线程,这里使用的才是真正的信号量(不是条件变量),在这里的同步模式中,由从线程实现处理,在异步的模式下,由主线程负责处理一部分内容,如果read_once函数发生错误,就会将time_flag置为1。
void threadpool<T>::run()
{
while (true)
{
m_queuestat.wait();
m_queuelocker.lock();
if (m_workqueue.empty())
{
m_queuelocker.unlock();
continue;
}
T *request = m_workqueue.front();
m_workqueue.pop_front();
m_queuelocker.unlock();
if (!request)
continue;
if (1 == m_actor_model)
{
if (0 == request->m_state)
{
if (request->read_once())
{
request->improv = 1;
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
else
{
if (request->write())
{
request->improv = 1;
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
}
else
{
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
}
}
(3)process函数
然后在使用process函数来处理http请求,同时为准备好向客户端写入的数据。
void http_conn::process()
{
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
return;
}
bool write_ret = process_write(read_ret);
if (!write_ret)
{
close_conn();
}
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}
(4)process_read函数
这个函数处理了http的请求,登录或者查看图片。NO_REQUEST代表请求还没有读取完成,GET_REQUEST表示请求已经完全收到。
同时对于每一行,收到的数据可能也不是完整的,每次收到一行后才能处理,如果不完整的话,将会继续重新注册epoll事件,继续监听剩余部分。
http_conn::HTTP_CODE http_conn::process_read()
{
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char *text = 0;
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
{
text = get_line();
m_start_line = m_checked_idx;
LOG_INFO("%s", text);
switch (m_check_state)
{
case CHECK_STATE_REQUESTLINE:
{
ret = parse_request_line(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
break;
}
case CHECK_STATE_HEADER:
{
ret = parse_headers(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
else if (ret == GET_REQUEST)
{
return do_request();
}
break;
}
case CHECK_STATE_CONTENT:
{
ret = parse_content(text);
if (ret == GET_REQUEST)
return do_request();
line_status = LINE_OPEN;
break;
}
default:
return INTERNAL_ERROR;
}
}
return NO_REQUEST;
}
(5)process_write函数
该函数的作用就是准备好写入的数据,根据分析的状态确定向客户端做什么样的反馈,如果是文件请求在do_request函数中将处理好一切,包括要发送的数据以及状态,同时把地址mmap在某个内存区域的,以加快访问速度。
bool http_conn::process_write(HTTP_CODE ret)
{
switch (ret)
{
case INTERNAL_ERROR:
{
add_status_line(500, error_500_title);
add_headers(strlen(error_500_form));
if (!add_content(error_500_form))
return false;
break;
}
case BAD_REQUEST:
{
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}
case FORBIDDEN_REQUEST:
{
add_status_line(403, error_403_title);
add_headers(strlen(error_403_form));
if (!add_content(error_403_form))
return false;
break;
}
case FILE_REQUEST:
{
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0)
{
add_headers(m_file_stat.st_size);
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else
{
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
default:
return false;
}
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}
6.处理http请求-写
同这里使用了两种模式一种是多线程处理,另一种是在主进程中处理,在向客户端写数据的时候一次性写完,这里的区别感觉不是很大。
void WebServer::dealwithwrite(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;
//reactor
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}
m_pool->append(users + sockfd, 1);
while (true)
{
if (1 == users[sockfd].improv)
{
if (1 == users[sockfd].timer_flag)
{
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else
{
//proactor
if (users[sockfd].write())
{
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
if (timer)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}
具体的向客户端写的函数如下:
bool http_conn::write()
{
int temp = 0;
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
init();
return true;
}
while (1)
{
temp = writev(m_sockfd, m_iv, m_iv_count);
if (temp < 0)
{
if (errno == EAGAIN)
{
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
unmap();
return false;
}
bytes_have_send += temp;
bytes_to_send -= temp;
if (bytes_have_send >= m_iv[0].iov_len)
{
m_iv[0].iov_len = 0;
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
m_iv[1].iov_len = bytes_to_send;
}
else
{
m_iv[0].iov_base = m_write_buf + bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
}
if (bytes_to_send <= 0)
{
unmap();
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
if (m_linger)
{
init();
return true;
}
else
{
return false;
}
}
}
}
二.服务器支线:
1.日志系统
日志系统也有一个独立的线程,类似生产者和消费者系统。
2.线程池
在服务器初始化过程中就创建了8个http线程
3.MYSQL
在服务器初始化时,会建立MYSQL多条连接,也就是相当于建立了一个SQL连接池,主要作用是将本地存储的数据导入服务器的map,并且将新的用户名和密码存入到MYSQL库中。
踩坑:
1.这里使用的信号量和我平时学习的不太一样(其实是我把条件变量和信号量弄混了),并没有计数的功能,所以可能就是在只有一个资源的情况,两个进程被唤醒后相互竞争,由于没有计数器,两个进程都认为可以获得资源,但是一个进程在获取资源后,另一个进程就只能while继续等待了。然后关于wait(cond,mutex)函数的用法可以查看网站说明,注意mutex在使用函数前一定要上锁。
标签:return,users,项目,read,TinyWebserver,timer,简述,sockfd,http From: https://www.cnblogs.com/y61329697/p/17059460.html