WebServer(2)

耿敏达
2023-12-01

完善http_conn.h和http_conn.cpp两个文件的逻辑,并有效排错使其能正确运行

http_conn.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>
#include <string.h>

class http_conn {
public:
    static const int FILENAME_LEN = 200;        // 文件名的最大长度
  // 读写缓冲大小
  static const int READ_BUFFER_SIZE = 2048;
  static const int WRITE_BUFFER_SIZE = 1024;

  // 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 };
  
  /*
      服务器处理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 };
  
  // 从状态机的三种可能状态,即行的读取状态,分别表示
  // 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整
  enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };

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

private:
  void init(); // 初始化连接其余的信息
  HTTP_CODE process_read(); // 解析HTTP请求
  bool process_write( HTTP_CODE ret );    // 填充HTTP应答
  
  // 下面这一组函数被process_read调用以分析HTTP请求
  HTTP_CODE parse_request_line(char * text); // 解析请求首行
  HTTP_CODE parse_headers(char * text);  // 解析请求首行
  HTTP_CODE parse_content(char * text);  // 解析请求首行
  HTTP_CODE do_request();
  char * get_line() { return m_read_buf + m_start_line; }
  LINE_STATUS parse_line();

  // 这一组函数被process_write调用以填充HTTP应答。
  void unmap();
  bool add_response( const char* format, ... );
  bool add_content( const char* content );
  bool add_content_type();
  bool add_status_line( int status, const char* title );
  bool add_headers( int content_length );
  bool add_content_length( int content_length );
  bool add_linger();
  bool add_blank_line();

public:
  // 所有的socket上的事件都被注册到一个epoll对象中
  static int m_epollfd; 
  // 统计用户的数量
  static int m_user_count;

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

  char m_read_buf[READ_BUFFER_SIZE]; // 读缓冲区
  int m_read_idx; // 标识读缓冲区中以及读入的客户端数据的最后一个字节的下一个位置
  int m_checked_idx; // 正在分析的缓冲区位置
  int m_start_line; // 解析起始位置

  CHECK_STATE m_check_state; // 主状态机当前所处的状态
  METHOD m_method; // 请求方法枚举

  char m_real_file[ FILENAME_LEN ];       // 客户请求的目标文件的完整路径,其内容等于 doc_root + m_url, doc_root是网站根目录
  char * m_url; // 请求目标文件的文件名
  char * m_version; // 协议版本,只支持HTTP1.1
  char * m_host; // 主机名
  int m_content_length;                   // HTTP请求的消息总长度
  bool m_linger; // 判断HTTP请求是否保持连接


  char m_write_buf[ WRITE_BUFFER_SIZE ];  // 写缓冲区
  int m_write_idx;                        // 写缓冲区中待发送的字节数
  char* m_file_address;                   // 客户请求的目标文件被mmap到内存中的起始位置
  struct stat m_file_stat;                // 目标文件的状态。通过它我们可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息
  struct iovec m_iv[2];                   // 我们将采用writev来执行写操作,所以定义下面两个成员,其中m_iv_count表示被写内存块的数量。
  int m_iv_count;

  int bytes_to_send;              // 将要发送的数据的字节数
  int bytes_have_send;            // 已经发送的字节数

};

#endif

http_conn.cpp

#include "http_conn.h"
// 定义HTTP响应的一些状态信息
const char* ok_200_title = "OK";
const char* error_400_title = "Bad Request";
const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char* error_403_title = "Forbidden";
const char* error_403_form = "You do not have permission to get file from this server.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The requested file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was an unusual problem serving the requested file.\n";

// 网站的根目录/home/nowcoder/webserver/resources
const char* doc_root = "/home/nowcoder/linux/exercise01/resources";

//设置文件描述符非阻塞
void 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;
  // event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
  // 2.6.17后版本,对方异常连接断开,可以在底层处理,不需要移交上层
  if (one_shot) {
    event.events |= EPOLLONESHOT; // |=重视
  }
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
  // 设置文件描述符非阻塞
  setnonblocking(fd);
}

// 从epoll中删除文件描述符
/**
 * @brief 即使使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题
 * 比如一个线程在读取某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读
 * (EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据,于是就出现了两个线程同时操作一个socket的局面
 * 一个socket连接在任一时刻都只被一个线程处理,可以使用epoll的EPOLLONESHOT事件实现
 * 
 * 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件
 * 
 * @param epollfd 
 * @param fd 
 */
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;
   // 重视     event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
  event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
  epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

// 所有的客户数
int http_conn::m_user_count = 0;
// 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
int http_conn::m_epollfd = -1;

// 关闭连接
void http_conn::close_conn() {
  if (m_sockfd != -1) {
    removefd(m_epollfd, m_sockfd); // 移除
    m_sockfd = -1; // 值赋为-1,就没用了
    m_user_count--; // 关闭一个连接,用户数量减一
  }
}

// 初始化连接
void http_conn::init(int sockfd, const sockaddr_in & addr) {
  m_sockfd = sockfd;
  m_address = addr;
  // 端口复用
  int reuse = 1;
  setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
  // 添加到epoll对象中
  addfd(m_epollfd, sockfd, true);
  m_user_count++;//总用户数加1
  init();
}

void http_conn::init() {
  bytes_to_send = 0;
  bytes_have_send = 0;

  m_check_state = CHECK_STATE_REQUESTLINE;
  m_linger = false;

  m_method = GET;
  m_url = 0;
  m_version = 0;
  m_content_length = 0;
  m_host = 0;
  m_start_line = 0;
  m_checked_idx = 0;
  m_read_idx = 0;
  m_write_idx = 0;

  bzero(m_read_buf, READ_BUFFER_SIZE);
  bzero(m_write_buf, READ_BUFFER_SIZE);
  bzero(m_real_file, FILENAME_LEN);
}

// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
  if (m_read_idx >= READ_BUFFER_SIZE) {
    return false;
  }
  // 定义已经读取到的字节
  int bytes_read = 0;
  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) {
        // 没有数据了
        break;
      }
      return false;
    }
    else if (bytes_read == 0) {
      // 对方关闭连接
      return false;
    }
    m_read_idx += bytes_read;
  }
  printf("读取到了数据: %s\n", m_read_buf);
  return true;
}


http_conn::LINE_STATUS http_conn::parse_line()
{
  char temp;
  for ( ; m_checked_idx < m_read_idx; ++m_checked_idx) {
    temp = m_read_buf[m_checked_idx];
    if (temp == '\r') {
      if ((m_checked_idx + 1) == m_read_idx) {
        return LINE_OPEN;
      }
      else if (m_read_buf[m_checked_idx + 1 == '\n']) {
        m_read_buf[m_checked_idx++] = '\0';
        m_read_buf[m_checked_idx++] = '\0';
        return LINE_OK;
      }
      return LINE_BAD;
    }
    else if (temp == '\n') {
      if ((m_checked_idx > 1) && (m_read_buf[m_checked_idx-1] == '\r')) {
        m_read_buf[m_checked_idx-1] = '\0';
        m_read_buf[m_checked_idx++] = '\0';
        return LINE_OK;
      }
      return LINE_BAD;
    }
  }
  return LINE_OPEN;
}


  // 获得请求方法,目标URL,HTTP版本
  http_conn::HTTP_CODE http_conn::parse_request_line(char * text) // 解析请求首行
  {
    // GET /index.html HTTP/1.1
    m_url = strpbrk(text, " \t"); // 重视 一个空格卡死我了
    if (! m_url) { 
        return BAD_REQUEST;
    }

    *m_url++ = '\0'; // 先取出url
    // GET\0/index.html HTTP/1.1
    char * method = text;
    if (strcasecmp(method, "GET") == 0) {
      m_method = GET;
    }
    else {
      return BAD_REQUEST;
    }
    // /index.html HTTP/1.1
    m_version = strpbrk(m_url, " \t");
    if (!m_version) {
      return BAD_REQUEST;
    }
    *m_version++ = '\0';
    if (strcasecmp(m_version, "HTTP/1.1") != 0) {
      return BAD_REQUEST;
    }
    // http://192.168.1.1:10000/index.html
    if (strncasecmp(m_url, "http://", 7) == 0) {
      m_url += 7; // /index.html;
      m_url = strchr(m_url, '/');
    }
    if (!m_url || m_url[0] != '/') {
      return BAD_REQUEST;
    }
    m_check_state = CHECK_STATE_HEADER; // 检查状态变成检查检查请求头
    return NO_REQUEST;
  }

  http_conn::HTTP_CODE http_conn::parse_headers(char * text)  // 解析请求头
  {
    // 遇到空行,表示头部字段解析完毕
    if( text[0] == '\0' ) {
        // 如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体,
        // 状态机转移到CHECK_STATE_CONTENT状态
        if ( m_content_length != 0 ) {
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        // 否则说明我们已经得到了一个完整的HTTP请求
        return GET_REQUEST;
    } else if ( strncasecmp( text, "Connection:", 11 ) == 0 ) {
        // 处理Connection 头部字段  Connection: keep-alive
        text += 11;
        text += strspn( text, " \t" );
        if ( strcasecmp( text, "keep-alive" ) == 0 ) {
            m_linger = true;
        }
    } else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) {
        // 处理Content-Length头部字段
        text += 15;
        text += strspn( text, " \t" );
        m_content_length = atol(text);
    } else if ( strncasecmp( text, "Host:", 5 ) == 0 ) {
        // 处理Host头部字段
        text += 5;
        text += strspn( text, " \t" );
        m_host = text;
    } else {
        printf( "oop! unknow header %s\n", text );
    }
    return NO_REQUEST;
  }

  http_conn::HTTP_CODE http_conn::parse_content(char * text)  // 解析请求体
  {
    if ( m_read_idx >= ( m_content_length + m_checked_idx ) )
    {
        text[ m_content_length ] = '\0';
        return GET_REQUEST;
    }
    return NO_REQUEST;
  }

  http_conn::HTTP_CODE http_conn::process_read() // 解析HTTP请求
  {
    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)) { // 重视重点 = parse_line = 写成==
      // 解析到了一行完整的数据,或者解析到了请求体,也是完成的数据
      // 获取了一行数据
      text = get_line();
      m_start_line = m_checked_idx;
      printf("got 1 http line : %s\n", 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;
  }

http_conn::HTTP_CODE http_conn::do_request() {
    // "/home/nowcoder/webserver/resources"
    strcpy( m_real_file, doc_root );
    int len = strlen( doc_root );
    strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
    // 获取m_real_file文件的相关的状态信息,-1失败,0成功
    if ( stat( m_real_file, &m_file_stat ) < 0 ) {
        return NO_RESOURCE;
    }

    // 判断访问权限
    if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {
        return FORBIDDEN_REQUEST;
    }

    // 判断是否是目录
    if ( S_ISDIR( m_file_stat.st_mode ) ) {
        return BAD_REQUEST;
    }

    // 以只读方式打开文件
    int fd = open( m_real_file, O_RDONLY );
    // 创建内存映射
    m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
    close( fd );
    return FILE_REQUEST;
}

// 对内存映射区执行munmap操作
void http_conn::unmap() {
    if( m_file_address )
    {
        munmap( m_file_address, m_file_stat.st_size );
        m_file_address = 0;
    }
}

bool http_conn::write() {
  // printf("一次性写完数据");
  // return true;
  
    int temp = 0;
    
    if ( bytes_to_send == 0 ) {
        // 将要发送的字节为0,这一次响应结束。
        modfd( m_epollfd, m_sockfd, EPOLLIN ); 
        init();
        return true;
    }

    while(1) {
        // 分散写
        temp = writev(m_sockfd, m_iv, m_iv_count);
        if ( temp <= -1 ) {
            // 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
            // 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
            if( errno == EAGAIN ) {
                modfd( m_epollfd, m_sockfd, EPOLLOUT );
                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 - temp;
        }

        if (bytes_to_send <= 0)
        {
            // 没有数据要发送了
            unmap();
            modfd(m_epollfd, m_sockfd, EPOLLIN);

            if (m_linger)
            {
                init();
                return true;
            }
            else
            {
                return false;
            }
        }

    }
}

// 往写缓冲中写入待发送的数据
bool http_conn::add_response( const char* format, ... ) {
    if( m_write_idx >= WRITE_BUFFER_SIZE ) {
        return false;
    }
    va_list arg_list;
    va_start( arg_list, format );
    int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
    if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
        return false;
    }
    m_write_idx += len;
    va_end( arg_list );
    return true;
}

bool http_conn::add_status_line( int status, const char* title ) {
    return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}

bool http_conn::add_headers(int content_len) {
    add_content_length(content_len);
    add_content_type();
    add_linger();
    add_blank_line();
}

bool http_conn::add_content_length(int content_len) {
    return add_response( "Content-Length: %d\r\n", content_len );
}

bool http_conn::add_linger()
{
    return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}

bool http_conn::add_blank_line()
{
    return add_response( "%s", "\r\n" );
}

bool http_conn::add_content( const char* content )
{
    return add_response( "%s", content );
}

bool http_conn::add_content_type() {
    return add_response("Content-Type:%s\r\n", "text/html");
}


// 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
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( 400, error_400_title );
            add_headers( strlen( error_400_form ) );
            if ( ! add_content( error_400_form ) ) {
                return false;
            }
            break;
        case NO_RESOURCE:
            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 );
            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;
        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;
}

// 由线程池中地工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
  // 解析HTTP请求
  /**
   * @brief 逻辑单元内部一种执行状态
   * STATE_MACHINE (Package _pack) {
   *    swich(cur_State){
   *        case type_A:
   *        break;
   *    }
   * }
   * 根据三种不同的状态,做相应的操作
   */
  HTTP_CODE read_ret = process_read(); // 有限状态机模型
  if (read_ret == NO_REQUEST) {
    modfd(m_epollfd, m_sockfd, EPOLLIN);
    return;
  }
  // printf("parse request, create response\n---------\n\n");
  // 生成响应
    bool write_ret = process_write( read_ret );
    if ( !write_ret ) {
        close_conn();
    }
    modfd( m_epollfd, m_sockfd, EPOLLOUT);
}


4. Web服务器如何处理以及响应接收到的HTTP请求报文呢?

线程池(半同步半反应堆模式)并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文的解析等等)

为什么要使用线程池?
当你需要限制你应用程序中同时运行的线程数时,线程池非常有用。因为启动一个新线程会带来性能开销,每个线程也会为其堆栈分配一些内存等。为了任务的并发执行,我们可以将这些任务任务传递到线程池,而不是为每个任务动态开启一个新的线程。

GET和POST的区别

  • 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制。(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100(指示信息—表示请求已接收,继续处理)continue,浏览器再发送data,服务器响应200 ok(返回数据)。

5. 数据库连接池是如何运行的

这种功能是服务器端通过用户键入的用户名密码和数据库中已记录下来的用户名密码数据进行校验实现的。若每次用户请求我们都需要新建一个数据库连接,请求结束后我们释放该数据库连接,当用户请求连接过多时,这种做法过于低效,所以类似线程池的做法,我们构建一个数据库连接池,预先生成一些数据库连接放在那里供用户请求使用。

6. 什么是CGI校验

CGI(通用网关接口),它是一个运行在Web服务器上的程序,在编译的时候将相应的.cpp文件编程成.cgi文件并在主程序中调用即可(通过社长的makefile文件内容也可以看出)。这些CGI程序通常通过客户在其浏览器上点击一个button时运行。这些程序通常用来执行一些信息搜索、存储等任务,而且通常会生成一个动态的HTML网页来响应客户的HTTP请求。我们可以发现项目中的sign.cpp文件就是我们的CGI程序,将用户请求中的用户名和密码保存在一个id_passwd.txt文件中,通过将数据库中的用户名和密码存到一个map中用于校验。在主程序中通过execl(m_real_file, &flag, name, password, NULL);这句命令来执行这个CGI文件,这里CGI程序仅用于校验,并未直接返回给用户响应。这个CGI程序的运行通过多进程来实现,根据其返回结果判断校验结果(使用pipe进行父子进程的通信,子进程将校验结果写到pipe的写端,父进程在读端读取)。

7. 生成HTTP响应并返回给用户

若目标文件存在、对所有用户可读且不是目录时,则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功FILE_REQUEST。 接下来要做的就是根据读取结果对用户做出响应了,也就是到了process_write(read_ret);这一步,该函数根据process_read()的返回结果来判断应该返回给用户什么响应,我们最常见的就是404错误了,说明客户请求的文件不存在,除此之外还有其他类型的请求出错的响应,具体的可以去百度。然后呢,假设用户请求的文件存在,而且已经被mmapm_file_address这里了,那么我们就将做如下写操作.

首先将状态行写入写缓存,响应头也是要写进connfd的写缓存(HTTP类自己定义的,与socket无关)中的,对于请求的文件,我们已经直接将其映射到m_file_address里面,然后将该connfd文件描述符上修改为EPOLLOUT(可写)事件,然后epoll_Wait监测到这一事件后,使用writev来将响应信息和请求文件聚集写TCP Socket本身定义的发送缓冲区(这个缓冲区大小一般是默认的,但我们也可以通过setsockopt来修改)中,交由内核发送给用户。

8. 服务器优化:定时器处理非活动链接

如果某一用户connect()到服务器之后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。这时候就应该利用定时器把这些超时的非活动连接释放掉,关闭其占用的文件描述符。这种情况也很常见,当你登录一个网站后长时间没有操作该网站的网页,再次访问的时候你会发现需要重新登录。
项目中使用的是SIGALRM信号来实现定时器,利用alarm函数周期性的触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

9. 服务器优化:日志

单例模式
最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

  • 懒汉模式

    :即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化(实例的初始化放在getinstance函数内部)

    • 经典的线程安全懒汉模式,使用双检测锁模式(p == NULL检测了两次)
    • 利用局部静态变量实现线程安全懒汉模式
  • 饿汉模式:即迫不及待,在程序运行时立即初始化(实例的初始化放在getinstance函数外部,getinstance函数仅返回该唯一实例的指针)。

10. 压测(非常关键)

Webbench是什么,介绍一下原理
父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。

11. 如何在此基础添加功能把社长的变成自己的

添加文件上传功能,实现与服务器的真正交互。

试着调用摄像头,来实现一些更有趣的功能。

12. 参考资料

社长文章地址:

https://huixxi.github.io/2020/06/02/%E5%B0%8F%E7%99%BD%E8%A7%86%E8%A7%92%EF%BC%9A%E4%B8%80%E6%96%87%E8%AF%BB%E6%87%82%E7%A4%BE%E9%95%BF%E7%9A%84TinyWebServer/#more
 类似资料: