基于Epoll与多线程实现Http服务器(前端部分: 中国象棋)

项目展示

1. 前端界面

  • 登录注册
    在这里插入图片描述
  • 游戏开始

在这里插入图片描述

  • 项目结构

在这里插入图片描述

2. 项目说明

  • 项目类型主要是后端的http服务器。前端部分的中国的象棋来自于网上的源码,然后加上登录注册功能,以及简单的cookie机制实现认证功能。
  • 开发平台:CentOS Linux release 7.8.2003。编译器:gcc version 8.3.1 20190311 (Red Hat 8.3.1-3)。编辑器:vscode。

3. 涉及到的技术点

  • Epoll多路复用
  • 多线程,线程池,互斥锁,条件变量
  • 程序替换,管道,环境变量
  • C语言连接MySQL数据库

4. 克服的难点

  • 支持长连接,主线程Epoll服务器专注于读取链接,获取Http请求。将每一个链接封装成一个Connection对象,每一个链接可以对应有多个Http请求。线程池负责解析Http请求,构建响应,发送静态网页文件或者创建子进程并程序替换为cgi程序。
  • 链接超时管理,利用链表以及哈希表对链接进行管理,链表的排列顺序对应于链接的活跃程度,每一次主线程epoll_wait获取就绪事件完毕之后,会对链表进行检测,链表头部对应的就是最不活跃,冷却时间最长的链接。
  • 简单模拟Cookie技术。用户注册,以及登录时,将访问时间戳经过哈希得到哈希值作为用户的uid,构建响应时通过Set-Cookie字段返回给客户端浏览器。后续用户登录成功后开始游戏,对局结束更新结果时,前端通过Ajax获取当前Cookie,并向服务器发送post请求。cgi程序通过用户名与Cookie中的uid进行认证。认证成功则访问更新数据库,如果认证失败则用户异常登录,账号存在风险。

5. 存在的问题

  • 目前Cookie不支持在一个浏览器客户端上同时有多个账号访问服务,因为服务器只设置了一个Cookie。

代码细节具体逻辑展示

1. EpollServer部分

  • 初步的设计思路
  1. EpollServer高效就在于无需等待,一旦有时间就绪,立即获知,并做出响应。所以在设计的时候可以让EpollServer专注于获取链接,而后台的多线程处理请求,构建响应。
  2. 这里我将EpollServer获取到的每一个Tcp链接都封装成一个Connection对象,在这个对象身上有读取缓冲区(对于EpollServer来说因为只关心读取,所以此处只需要有读取缓冲区即可)。
  3. 因为使用的是HTTP/1.1,开启了长连接,所以一个Tcp链接下对应可能会有多个Http请求,所以Connection对象身上的读取缓冲区会存在有多个Http请求。
  4. 此时面临的一个问题就是在读取缓冲区中分离出一个完整的Http请求,然后封装成任务,交给线程池。Http请求中采用换行符结尾,并且包含一个空行,因此可以使用 “\n\n” 作为分隔符。
  5. 但是还有一个问题,Http请求当中也有可能会有请求体,比如使用post方法,所以势必需要对Http请求进行初步的解析,判断请求方法以及Content-Length字段。
  • 说了这么多来两张图帮你理解一下我的思路,首先来看一张主逻辑图
    在这里插入图片描述

  • 再来看一张代码层次图

  • CentralProcess 类和 HttpRequest 类以及 HttpResponse 类之间是has-a组合关系。CentralProcess类包含请求和响应对象的指针,同时该类身上也包含着解析Http请求,以及构建Http响应的方法。这三个类属于Http协议的具体业务层,属于上层。下图左边的EpollServer类和Connection类属于Tcp链接层,属于下层,上下两层之间不存在组合,继承关系,只存在数据交付。
    在这里插入图片描述

  • EpollServer会将每一个链接封装成Connection对象,Connection类如下:

class EpollServer;
class Connection;

using func_t = std::function<void(Connection *)>;
class Connection
{
public:
    Connection(int sock = -1) : _sock(sock), _eps(nullptr)
    {
    }
    void setCallBack(func_t _recv, func_t _send, func_t _exep)
    {
        _recv_cb = _recv;
        _send_cb = _send;
        _exep_cb = _exep;
    }
public:
    // 负责进行IO的文件描述符
    int _sock;
    // 三个回调方法,表征的就是对_sock进行特定读写对应的方法
    func_t _recv_cb;
    func_t _send_cb;
    func_t _exep_cb;
    // 设置对EpollServer的回值指针
    EpollServer *_eps;
    // 链表迭代器, 该迭代器指向该链接自身,方便插入删除操作
    std::list<Connection *>::iterator _myself;
    // 接收缓冲区&&发送缓冲区
    std::string inBuffer;
    std::string outBuffer; 
    // 时间戳
    uint64_t _lasttimestamp; // time();
};
  • 每一个Http请求对应任务队列中的一个任务,Task类如下:
class Task
{
private:
    CentralProcess* _cp;
public:
    Task()
    {}
    Task(CentralProcess* cp):_cp(cp)
    {}
    void operator()()
    {
        std::cout << "线程开始处理http请求, pthread_id" << pthread_self() << std::endl;

        _cp->buildHttpResponse();
        _cp->sendHttpResponse();
        delete _cp; //当前http请求处理完毕,释放动态申请的资源

        std::cout << "线程处理http请求完毕, pthread_id" << pthread_self() << std::endl;
    }
    ~Task()
    {}
};
  • EpollServer类的属性
private:
    int _listen_sock;
    uint16_t _port;
    Epoll _epoll;
    struct epoll_event *_revs;
    int _revs_num;
    std::unordered_map<int, Connection *> _conn;//哈希表,每一个链接对应一个sock
    std::list<Connection *> _conn_time_list;    //超时时间管理链表
    ThreadPool<Task> *_tp;
  • EpollServer构造函数
 EpollServer(uint16_t port = default_port) : _port(port), _revs_num(gnum)
    {
        // 创建监听套接字
        _listen_sock = Sock::Socket();
        Sock::Bind(_listen_sock, _port);
        Sock::Listen(_listen_sock);
        // 创建epoll模型
        _epoll.CreateEpoll();
        LOG(DEBUG) << msg("create epoll sucess...");

        // 忽略SIGPIPE信号,防止在写入时,读端异常关闭,server直接崩溃
        signal(SIGPIPE, SIG_IGN);
        // 获取线程池单例,初始化线程池
        _tp = ThreadPool<Task>::getThreadPool();
        if (_tp->initThreadPool())
            LOG(INFO) << "init ThreadPool success..." << std::endl;
        else
            LOG(FATAL) << "init ThreadPool fail !!!" << std::endl;

        // 创建epoll就绪事件用户空间
        _revs = new struct epoll_event[_revs_num];
        // 将监听套接字封装成Connection对象,同时添加到epoll模型中
        AddConnection(_listen_sock,
                      std::bind(&EpollServer::Accepter, this, std::placeholders::_1),
                      nullptr, nullptr);
        LOG(DEBUG) << msg("add listen_sock success...");
    }
  • 添加链接,将每一个sock封装成Connection对象
void AddConnection(int sock, func_t _recv, func_t _send, func_t _exep)
    {
        // 除了_listensock,未来我们会存在大量的socket,每一个sock都必须被封装成为一个Connection
        // 当服务器中存在大量的Connection的时候,EpollServer就需要将所有的Connection要进行管理:
        // 1.设置文件描述符为非阻塞模式
        assert(Sock::setNonBlock(sock));
        // 2.根据sock创建链接对象,注册响应事件回调方法
        Connection *con = new Connection(sock);
        con->setCallBack(_recv, _send, _exep);
        con->_eps = this;
        if (sock != _listen_sock)   //将非listen套接字对应的Connection添加到活跃时间管理链表中
        {
            _conn_time_list.push_back(con);
            con->_myself = --(_conn_time_list.end());
            con->_lasttimestamp = time(nullptr);
        }
        // 3.将sock以及相应的事件添加到epoll模型, 默认关心读事件, ET模式边缘触发
        _epoll.AddSockToEpoll(sock, EPOLLIN | EPOLLET);
        // 4.文件描述符与Connection*的映射关系添加到哈希表中
        _conn.insert(std::make_pair(sock, con));
    }
  • 每一个链接都有读取方法,下面是listen套接字的读取方法
    void Accepter(Connection *conn)
    {
        while (true)
        {
            int err_num = 0;
            std::string client_ip;
            uint16_t client_port;
            int sock = Sock::Accept(_listen_sock, &client_ip, &client_port, &err_num);
            if (sock >= 0)
            {
                LOG(DEBUG) << msg("get a new link, sock:%d, client_ip:%s, client_port:%d", sock, client_ip.c_str(), client_port);
                AddConnection(sock, std::bind(&EpollServer::Recver, this, std::placeholders::_1),
                              std::bind(&EpollServer::Sender, this, std::placeholders::_1),
                              std::bind(&EpollServer::Exceper, this, std::placeholders::_1));
            }
            else if (err_num == EAGAIN || err_num == EWOULDBLOCK)
            {
                LOG(DEBUG) << msg("没有链接了,待会再来吧");
                break;
            }
            else if (err_num == EINTR)
            {
                LOG(DEBUG) << msg("被信号中断了,再来一次");
                continue;
            }
            else
            {
                LOG(DEBUG) << msg("accept error");
                break;
            }
        }
    }
  • service 套接字的读取方法
    void Recver(Connection *conn)
    {
        // 链接上有事件就绪,说明当前链接是最新的活跃链接
        //_conn_time_list 链表从头到尾的顺序,就代表着这个链接最近活跃的时间顺序
        conn->_lasttimestamp = time(nullptr);      // 更新最近活跃时间
        _conn_time_list.erase(conn->_myself);      // 根据自身的回指迭代器,快速在链表中找到自己,然后将自身从链表中删除
        _conn_time_list.push_back(conn);           // 重新将自身插入到链表尾部
        conn->_myself = --(_conn_time_list.end()); // 更新自身内部的回指指针,end()返回最后一个元素的下一位置,因此end()--,指向自身

        LOG(DEBUG) << msg("数据来了。。。。");
        const int num = 512;
        bool err = false;
        while (true)
        {
            char buffer[num];
            ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
            if (n < 0)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                else if (errno == EINTR)
                    continue;
                else
                {
                    LOG(ERROR) << msg("recv error, %d:%s", errno, strerror(errno));
                    conn->_exep_cb(conn);
                    err = true;
                    break;
                }
            }
            else if (n == 0)
            {
                LOG(DEBUG) << msg("client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
                conn->_exep_cb(conn);
                err = true;
                break;
            }
            else
            {
                // 读取成功
                buffer[n] = 0;
                conn->inBuffer += buffer;
            }
        } // end while
        if (!err)
        {
            // LOG(INFO) << msg("inbuffer: %s", conn->inBuffer.c_str());
            string message;
            string message_sep;
            int index = 0;
            while (true)
            {
                int sep_no = SpliteHttpMessage(conn->inBuffer, &message, &index);//从inbuffer中提取出一个完整的http报文
                if (sep_no == 3)
                    break;
                // LOG(DEBUG) << "收到一个http请求:\n " << message << std::endl;
                message_sep = sep1[sep_no];
                CentralProcess* cp = new CentralProcess(conn->_sock);
                //因为不确定该请求是否有请求体,因此需要进一步对http请求解析
                //判断是否为POST方法,且含有Content-Length,是则需要继续从当前链接的inbuffer输入缓冲区上读取Content-Length个字节
                if (recvHttpRequest(cp, message, message_sep) && parseHttpRequest(cp, &index))
                {
                    Task t(cp);
                    _tp->pushTask(t); // 成功分离出一个完整的http请求,封装成任务,push到任务队列中,交给线程池解析请求,构建并发送http响应
                }
                else {
                    delete cp;     // 到这里说明提取出的http报文是有问题的,直接丢弃
                }
                // LOG(DEBUG) << "分隔符是: " << sep[sep_no] <<w sep_no << std::endl;
            }
            //while循环结束后,说明这一次连接缓冲区inbuffer内数据处理完毕
            //通过index的位置来判断是否刚好处理了整数个http报文,因为连接缓冲区中可能读上来了不完整的http报文,比如 1.5个http报文
            if(index == conn->inBuffer.size())  //刚好处理玩,缓冲区清空
                conn->inBuffer.clear();
            else if(index < conn->inBuffer.size())  //缓冲区当中存在不完整的http报文,对inbuffer重新赋值
                conn->inBuffer = conn->inBuffer.substr(index);
            else
                LOG(ERROR) << msg("从连接缓冲区中提取http报文出错....");
        }
    }
  • listen套接字封装添加完毕,初始化工作完成后,EpollServer开始等待事件就绪,一旦检测到有链接上事件就绪,就会去执行之前注册好的相应链接上的读写方法。
	// 根据就绪的事件,进行特定事件的派发
    void Dispatcher()
    {
        while (true)
        {
            ConnectAliveCheck();
            LoopOnce();
        }
    }

    void LoopOnce()
    {
        int n = _epoll.WaitEpoll(_revs, _revs_num);
        if (n > 0)
        {
            LOG(DEBUG) << msg("有%d个事件响应就绪了", n);
            for (int i = 0; i < n; i++)
            {
                int sock = _revs[i].data.fd;
                uint32_t revents = _revs[i].events;
                // 将所有的异常,全部交给read或者write来统一处理
                if (revents & EPOLLERR)
                    revents |= (EPOLLIN | EPOLLOUT);
                if (revents & EPOLLHUP)
                    revents |= (EPOLLIN | EPOLLOUT);

                // 读事件就绪
                if (revents & EPOLLIN)
                {
                    if (isExist(sock) && _conn[sock]->_recv_cb != nullptr)
                        _conn[sock]->_recv_cb(_conn[sock]);
                }
                // 写事件就绪
                if (revents & EPOLLOUT)
                {
                    if (isExist(sock) && _conn[sock]->_send_cb != nullptr)
                        _conn[sock]->_send_cb(_conn[sock]);
                }
            }
        }
        else if (n == 0)
        {
            LOG(DEBUG) << msg("timeout...");
        }
        else
        {
            LOG(ERROR) << msg("epoll_wait error %d:%s", errno, strerror(errno));
        }
    }
  • 这里对超时时间管理链表解释一下
    //对活跃时间链表进行检测,将所有冷却时间大于20s的连接关闭
    void ConnectAliveCheck()
    {
        // 链表的的插入删除操作是按照连接的最近活跃时间来操作的,最活跃的连接总是插入链表尾部
        // 也就是说,当前链表从头到尾的顺序就是冷却时间从大到小的顺序,链表头部就是冷却时间最长,最不活跃的连接
        // 所以,只需要从链表头部开始判断其最近活跃时间,跟当前的时间戳比较
        // first - cur > 10 , 说明超时了,调用自身注册的异常函数,关闭连接,回收资源,然后循环继续判断下一个连接
        // first - cur < 10 , 说明没超时,最不活跃的都没超时,后面的都不会超时,循环退出,检测完毕。
        uint64_t cur = time(nullptr);
        auto conn = _conn_time_list.begin();
        while (!_conn_time_list.empty() && (cur - (*conn)->_lasttimestamp) >= 20)
        {
            if ((*conn)->_exep_cb != nullptr)
            {
                LOG(INFO) << msg("client[%d] timeout... , close sock: %d", (*conn)->_sock, (*conn)->_sock);
                auto cur = conn;
                cur++;
                (*conn)->_exep_cb((*conn));
                conn = cur;
                continue;
            }
            else
            {
                logMessage(ERROR, "Connextion 链表不为空,但是注册的异常回调函数为空,未知错误.....");
                break;
            }
        }
    }

在这里插入图片描述

2. Http协议主逻辑部分

  • HttpRequest类
// 每一个http请求对应一个HttpRequest类的实体对象
class HttpRequest
{
public:
    string request_line;         // 请求行 GET / HTTP/1.1
    vector<string> request_head; // 请求头
    string request_body;         // 请求体

    // http请求解析后的结果 方法, 协议版本, 资源路径, 参数, 后缀等
    string method;
    string version;
    string uri; // uri = path?args, 这里注意uri中可能包含有参数
    string path;
    string args;
    string suffix; // 请求的资源后缀,代表着所请求资源的类型
    bool cgi;
    int file_size;

    // 将http请求头拆分为键值对
    unordered_map<string, string> head_map;
    int content_length;

public:
    HttpRequest() : cgi(false), content_length(0) {}
    ~HttpRequest(){};
};
  • HttpResponse类
// 每一个http响应对应一个HttpResponse类的实体对象
class HttpResponse
{
public:
    string response_line;         // 响应行
    vector<string> response_head; // 响应头
    string blank;                 // 空行
    string response_body;         // 响应体
    int status_code;
    int fd;
    string uid; // 用户ID
    HttpResponse() : blank(BLANK_LINE), status_code(OK), fd(-1) {}
    ~HttpResponse() {}
};
  • CentralProcess 类的属性,这个类上有解析Http请求和构建发送响应的方法
// 中央处理模块,http协议的主要业务逻辑
// 根据EpollServer获取上来的http请求,构建并发送响应。返回静态网页文件,或者执行CGI程序,由具体线程负责处理
class CentralProcess
{
public:
    int _sock;
    HttpRequest *_request;
    HttpResponse *_response;
  • 解析Http请求
 void buildHttpResponse()
    {
        string path;
        auto &status_code = _response->status_code;
        size_t suffix_pos = 0;
        if (_request->method != "GET" && _request->method != "POST")
        {
            LOG(WARNING) << "method is invalid..." << std::endl;
            status_code = NOT_FOUND;
            goto END;
        }
        if (_request->method == "GET")
        {
            size_t pos = _request->uri.find("?");
            if (pos != std::string::npos)
            {
                Util::cutString(_request->uri, _request->path, _request->args, "?");
                _request->cgi = true;
            }
            else
            {
                _request->path = _request->uri;
            }
        }
        else
        { //_request->method == "POST"
            _request->cgi = true;
            _request->path = _request->uri;
        }
        path = _request->path;
        _request->path = WEB_HOME;
        _request->path += path;
        if (_request->path[_request->path.size() - 1] == '/')
        {
            _request->path += HOME_PAGE;
        }
        struct stat st;
        if (stat(_request->path.c_str(), &st) == 0) // 访问的资源存在
        {
            status_code = OK;
            if (S_ISDIR(st.st_mode)) // 访问的是一个目录
            {
                _request->path += "/";
                _request->path += HOME_PAGE;
                if (stat(_request->path.c_str(), &st) != 0)
                {
                    LOG(WARNING) << _request->path + " NOT FOUND" << std::endl;
                    status_code = NOT_FOUND;
                    goto END;
                }
            }
            else if (_request->cgi == true) // 说明前面成功设置了cgi, 此时为post方法,或者get方法带参数,只需判断是否为可执行文件
            {
                if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
                    ; // 访问的是一个可执行文件
                else
                {
                    LOG(WARNING) << _request->path + " NOT FOUND" << std::endl;
                    status_code = NOT_FOUND;
                    goto END;
                }
            }
            else if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
            { // 走到这儿说明get方法无参访问cgi程序, 属于非法访问
                status_code = BAD_REQUEST;
                goto END;
            }
            _request->file_size = st.st_size;
        }
        else // 访问的资源不存在
        {
            LOG(WARNING) << _request->path + " Not Found" << std::endl;
            status_code = NOT_FOUND;
            goto END;
        }
        suffix_pos = _request->path.rfind(".");
        if (suffix_pos != string::npos)
        {
            _request->suffix = _request->path.substr(suffix_pos);
        }
        else
        {
            _request->suffix = "text/html";
        }
        if (_request->cgi)
        {
            status_code = processWithCGI();	// 执行cgi程序
        }
        else
        {
            status_code = processNoneCGI(); // 返回一个静态网页
        }
    END:
        buildHttpResponseHelper();
    }
  • 线程创建子进程并程序替换,执行cgi程序
    int processWithCGI()
    {
        LOG(INFO) << "Process With CGI" << std::endl;
        int code = OK;
        auto &bin = _request->path;
        auto &method = _request->method;
        auto &body = _request->request_body;
        auto &args = _request->args;
        int input[2];
        int output[2];
        if (pipe(input) < 0)
        {
            // 创建管道失败,服务器发生内部错误
            LOG(ERROR) << "create pipe fail!" << std::endl;
            code = SERVER_ERROR;
            return code;
        }
        if (pipe(output) < 0)
        {
            LOG(ERROR) << "create pipe fail!" << std::endl;
            code = SERVER_ERROR;
            return code;
        }
        //这里要在创建子进程之前更新用户ID信息,因为父子进程会有写时拷贝
        if (_request->path == "www/wwwroot/login_cgi" || _request->path == "www/wwwroot/register_cgi") 
        {   // 用户执行的是登录或者注册操作, 获取当前时间戳,获取哈希值作为用户ID;
            // 这里的用户ID会作为访问 updateRequest_cgi 更新用户数据时的认证机制
            size_t time_stamp = time(nullptr); 
            std::hash<string> szHash;
            size_t uid = szHash(std::to_string(time_stamp)) - 1;
            _response->uid = std::to_string(uid);
        }

        pid_t id = fork();
        if (id < 0)
        {
            LOG(ERROR) << "fork error" << std::endl;
            code = SERVER_ERROR;
            return code;
        }

        else if (id == 0) // child process
        {
            close(input[0]);
            close(output[1]);
            // string method_env = "METHOD=";   //进程程序替换并不会替换环境变量,但是注意这里存放环境变量用static, 存放在子进程地址空间的全局数据区
            // method_env += method;                   //如果是局部变量,则存放在栈区,但是栈是动态改变的,putenv()函数,是将环境变量字符串的地址放到环境变量表当中
            // putenv((char *)method_env.c_str());     //此时,栈区的动态增长有可能导致,导入的环境变量丢失,从而在进程替换后,程序无法获取环境变量

            // 使用setenv, 可以避免putenv()的浅拷贝问题
            setenv("METHOD", method.c_str(), 1);

            if (method == "GET")
            {
                // static string query_env = "QUERY_STRING=";
                // query_env += args;
                setenv("QUERY_STRING", args.c_str(), 1);
            }
            else if (method == "POST")
            {
                setenv("USER_ID", _response->uid.c_str(), 1);  //将用户ID导入环境变量
                // setenv("USER_ID", "15391271231361086382", 1);  //模拟动态生成的用户ID 与 数据库中原有的用户ID相同的情况
                // static string content_length_env = "CONTENT_LENGTH=";
                // content_length_env += std::to_string(_request->content_length);
                // putenv((char *)content_length_env.c_str());
                setenv("CONTENT_LENGTH", std::to_string(_request->content_length).c_str(), 1);
            }

            dup2(output[0], 0);
            dup2(input[1], 1);

            execl(bin.c_str(), bin.c_str(), nullptr);
            exit(1);
        }
        else
        { // parent process
            close(input[1]);
            close(output[0]);
            if ("POST" == method)
            {
                const char *start = body.c_str();
                LOG(DEBUG) << "body " << body << std::endl;
                ssize_t has_write = 0; // 已经写入的字节数
                ssize_t size = 0;      // 每次成功写入的字节数
                while ((size = write(output[1], start + has_write, body.size() - has_write)) > 0)
                {
                    has_write += size;
                }
                char ch = '\n';
                write(output[1], &ch, 1);
            }
            char ch = 'a';
            while (read(input[0], &ch, 1) > 0)
            {
                _response->response_body.push_back(ch);
            }
            int status = 0;
            int ret = waitpid(id, &status, 0);
            if (ret == id)
            {
                if (WIFEXITED(status))
                {
                    if (WEXITSTATUS(status) == 0)
                    {
                        code = OK;
                    }
                    else
                    {
                        code = BAD_REQUEST;
                    }
                }
                else
                {
                    code = SERVER_ERROR;
                }
            }
            close(input[0]);
            close(output[1]);
        }
        return code;
    }
  • 构建响应行和响应头
    void buildOKResponse()
    {
        auto &resp_head = _response->response_head;

        string head = "Connection: keep-alive"; // 告诉浏览器客户端,支持长连接
        head += BLANK_LINE;
        resp_head.push_back(head);

        head = "Content-Type: ";
        if (_request->cgi)
        {
            if (_request->path == "www/wwwroot/updateResult_cgi")
                head += suffix2Desc(".json");
            else
                head += "text/html";
            head += BLANK_LINE;
        }
        else
        {
            head += suffix2Desc(_request->suffix);
            head += BLANK_LINE;
        }
        resp_head.push_back(head);

        head = "Content-Length: ";
        if (_request->cgi)
            head += std::to_string(_response->response_body.size());
        else
            head += std::to_string(_request->file_size);
        head += BLANK_LINE;
        resp_head.push_back(head);

        if (_request->cgi)
        {
            if (_request->path == "www/wwwroot/login_cgi" || _request->path == "www/wwwroot/register_cgi")
            {
                head = "Set-Cookie: ";
                head += " uid=";
                head += _response->uid;
                head += BLANK_LINE;
                LOG(DEBUG) << "uid: " << _response->uid << std::endl;
                resp_head.push_back(head);
                LOG(DEBUG) << "cookie: " << head << std::endl;
            }
        }
    }
  • 发送响应,注意发送响应时要设置为阻塞模式发送,因为是让线程发送响应。在非阻塞模式下发送,如果文件比较大,一次把发送缓冲区打满之后,会返回-1,并且设置错误码为EAGAIN或者EWOULDBLOCK,此时需要重新发送,逻辑更复杂。因为在设计之初只考虑让EpollServer按照ET模式负责读取,因此多线程采用阻塞模式发送响应。
    void sendHttpResponse()
    {
        LOG(DEBUG) << _response->response_line << std::endl;
        Sock::setBlock(_sock);                                                             // 阻塞式发送
        send(_sock, _response->response_line.c_str(), _response->response_line.size(), 0); // 发送响应行
        for (auto it : _response->response_head)                                           // 发送响应头
        {
            send(_sock, it.c_str(), it.size(), 0);
            LOG(DEBUG) << it << std::endl;
        }
        send(_sock, _response->blank.c_str(), _response->blank.size(), 0); // 发送空行
        LOG(DEBUG) << _response->blank << std::endl;

        if (_request->cgi)
        {
            auto &resp_body = _response->response_body;
            size_t size = 0;
            size_t has_send = 0;
            const char *start = _response->response_body.c_str();
            while (has_send < resp_body.size() && (size = send(_sock, start + has_send, resp_body.size() - has_send, 0)) > 0)
            {
                has_send += size;
            }
            LOG(DEBUG) << _response->response_body << std::endl;
        }
        else
        {
            std::cout << "response_file_size: " << _request->file_size << std::endl;
            ssize_t sz = sendfile(_sock, _response->fd, nullptr, _request->file_size);
            if (sz < 0)
            {
                LOG(ERROR) << msg("errno: %d, strerr: %s", errno, strerror(errno));
            }
            else
            {
                LOG(DEBUG) << "sz: " << sz << std::endl;
            }
        }
        Sock::setNonBlock(_sock);
        close(_response->fd);
    }

3. 用户登录注册,更新数据

  • cgi程序主要有三个,login_cgi,register_cgi,updateResult_cgi。分别对应着用户登录,注册,以及用户对局结束更新结果。这三个cgi程序都需要通过post方法请求,子进程在程序替换之后,通过环境变量以及管道的方式获取用户数据。
  • 首先是comm.hpp,这个类中包含了连接,访问,查询,更新数据库的方法,以及渲染网页的方法。其中渲染网页用到了ctemplate库。
#include <string>
#include <iostream>
#include <jsoncpp/json/json.h>
#include <mysql_connector/mysql.h>
#include <ctemplate/template.h>
#include <unistd.h>
#include <cstdlib>
#include "../../Log.hpp"

using std::string;

struct User
{
    long long hash;
    string name;
    string password;
    int win;
    int lose;
    string uid;
};

bool GetQueryString(string &query_string, string *uid)
{
    string method = getenv("METHOD");
    if (method != "POST")
    {
        std::cerr << "请求方法错误..." << std::endl;
        return false;
    }
    if (uid != nullptr)
        *uid = getenv("USER_ID");

    int content_length = atoi(getenv("CONTENT_LENGTH"));
    char c = 0;
    while (content_length)
    {
        read(0, &c, 1);
        query_string.push_back(c);
        content_length--;
    }
    return true;
}

bool executeSql(const string &sql)
{

    MYSQL *my = mysql_init(nullptr);

    if (nullptr == mysql_real_connect(my, "127.0.0.1", "XX", "XXXXXXX", "chess", 3306, nullptr, 0))
    {
        std::cerr << "数据库连接失败" << std::endl;
        return false;
    }
    // 一定要设置该链接的编码格式, 要不然会出现乱码问题
    mysql_set_character_set(my, "utf8");
    std::cerr << "数据库连接成功..." << std::endl;

    // 执行sql语句
    if (0 != mysql_query(my, sql.c_str()))
    {
        mysql_close(my);
        return false;
    }
    else
    {
        std::cerr << "sql 执行成功" << std::endl;
    }
    mysql_close(my);
    return true;
}

bool querySql(const string &sql, struct User &user, string *uid)
{
    MYSQL *my = mysql_init(nullptr);

    if (nullptr == mysql_real_connect(my, "127.0.0.1", "XXX", "XXXXX", "chess", 3306, nullptr, 0))
    {
        std::cerr << "数据库连接失败" << std::endl;
        return false;
    }
    // 一定要设置该链接的编码格式, 要不然会出现乱码问题
    mysql_set_character_set(my, "utf8");
    std::cerr << "数据库连接成功..." << std::endl;

    // 执行sql语句
    if (0 != mysql_query(my, sql.c_str()))
    {
        mysql_close(my);
        return false;
    }
    else
        std::cerr << "sql 执行成功" << std::endl;

    // 提取结果
    MYSQL_RES *res = mysql_store_result(my);

    // 分析结果
    int rows = mysql_num_rows(res);   // 获得行数量
    int cols = mysql_num_fields(res); // 获得列数量

    std::cerr << "rows: " << rows << " cols: " << cols << std::endl;
    if (rows != 1 || cols != 6)
        return false;
    MYSQL_ROW row = mysql_fetch_row(res);
    user.hash = atoll(row[0]);   //这里有问题。。。。暂时没法解决。。。。。
    user.name = row[1];
    user.password = row[2];
    user.win = atoi(row[3]);
    user.lose = atoi(row[4]);
    user.uid = row[5];

    // 走到这儿,说明用户登录成功,需要更新用户ID
    if (uid != nullptr)
    {
        if(user.uid == *uid) //判断成立则说明,数据库当中存储的uid和需要更新的uid相同
            return false;    //这种情况概率很低,但是如果发生,直接false返回,提示服务器内部错误,需要重新登录

        string updateUID = "update user set uid = \'";
        updateUID += *uid;
        updateUID += "\'";
        updateUID += " where name = \'";
        updateUID += user.name;
        updateUID += "\'";
        std::cerr << updateUID << std::endl;
        //std::cerr << "atoll 转换或的 user.hash: " << user.hash << std::endl;        //转换失败
        //std::cerr << "没有经过转换,直接从sql结果集读取hash: " << row[0] << std::endl;//读取正确
        if (0 != mysql_query(my, updateUID.c_str()))
        {
            std::cerr << "更新用户ID失败。。。。。。" << std::endl;
            mysql_close(my);
            return false;
        }
        else
        {
            std::cerr << "更新用户ID成功" << std::endl;
        }
    }


    // 释放结果空间
    mysql_free_result(res);

    // 关闭mysql连接
    mysql_close(my);

    return true;
}

const static string template_path = "./www/wwwroot/play.html";
// 渲染网页
void ExpandHtml(struct User &user, string *html)
{
    // 形成数字典
    ctemplate::TemplateDictionary root("play");
    root.SetValue("userName", user.name);
    root.SetValue("win", std::to_string(user.win));
    root.SetValue("lose", std::to_string(user.lose));

    // 获取被渲染的html
    ctemplate::Template *tpl = ctemplate::Template::GetTemplate(template_path, ctemplate::DO_NOT_STRIP);

    // 开始完成渲染功能
    tpl->Expand(html, &root);
}

void CutString(std::string &in, const std::string &sep, std::string &out1, std::string &out2)
{
    auto pos = in.find(sep);
    if (std::string::npos != pos)
    {
        out1 = in.substr(0, pos);
        out2 = in.substr(pos + sep.size());
    }
}

void sendError()
{
    std::cout << "<html>";
    std::cout << "<head><meta charset=\"utf-8\"></head>";
    std::cout << "<body><h1>用户名或密码格式错误<br>目前只支持字母和数字组合, 名字密码长度小于等于20, 大于等于5</h1></body></html>";
}

bool check(const string &str)
{
    if (str.size() == 0 || str.size() > 20 || str.size() < 5)
    {
        sendError();
        return false;
    }
    // 判断name password中是否含有非法字符, name以及password由英文字母与数字构成
    for (auto &ch : str)
    {
        if (((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')) || (ch >= 'a' && ch <= 'z'))
            continue;
        else
        {
            std::cerr << "ch: " << ch << std::endl;
            std::cerr << "str: " << str << std::endl;
            std::cerr << "用户名或密码中含有非法字符" << std::endl;
            sendError();
            return false;
        }
    }
    return true;
}
  • login.cc
#include "comm.hpp"

int main()
{
    string query_string;//存放用户的数据,包括用户名以及密码
    string uid;			//用户ID
    // from http
    if(!GetQueryString(query_string, &uid))//从管道以及环境变量中获取数据
        return 1;

    std::cerr << "cgi get the uid is " << uid << std::endl;
    
    std::string name;
    std::string passwd;
    CutString(query_string, "&", name, passwd);

    std::string _name;
    std::string sql_name;
    CutString(name, "=", _name, sql_name);

    std::string _passwd;
    std::string sql_passwd;
    CutString(passwd, "=", _passwd, sql_passwd);

    //检查用户名密码的合法性再查询数据库
    if (!check(sql_name) || !check(sql_passwd))
        return 0;

    struct User user;
    std::hash<std::string> szHash;
    size_t hashVal = szHash(sql_name) - 1; //对用户名使用哈希算法,生成哈希值,作为主键
    string sql = "select * from user where hash = ";
    sql += std::to_string(hashVal);
    std::cerr << sql << std::endl;

    if (querySql(sql, user, &uid))
    {
        //核对用户密码是否正确
        if(sql_passwd == user.password){  //密码正确,登录成功
            string html;
            ExpandHtml(user, &html); // 获取渲染或的网页
            std::cout << html << std::endl;
        }else{
            std::cout << "<html>";
            std::cout << "<head><meta charset=\"utf-8\"></head>";
            std::cout << "<body><h1>密码错误!</h1></body></html>";
        }
    }
    else
    {
        std::cerr << "用户登录失败,用户uid更新失败 " << sql << std::endl;
        std::cout << "<html>";
        std::cout << "<head><meta charset=\"utf-8\"></head>";
        if(user.uid != "")
            std::cout << "<body><h1>服务器内部错误,请重新登录</h1></body></html>";     //用户uid读取成功,说明用户查询成功,但是uid更新失败,属于服务器内部错误
        else
            std::cout << "<body><h1>用户不存在!</h1></body></html>";                   //用户uid读取失败,说明用户查询失败,用户不存在,登录失败
    }
    return 0;
}
  • register.cc,
#include "comm.hpp"

int main()
{
    string query_string;
    string uid;
    if(!GetQueryString(query_string, &uid))
        return 1;

    std::string name;
    std::string passwd;

    CutString(query_string, "&", name, passwd);

    std::string _name;
    std::string sql_name;
    CutString(name, "=", _name, sql_name);

    std::string _passwd;
    std::string sql_passwd;
    CutString(passwd, "=", _passwd, sql_passwd);

    std::cerr << "name: " << sql_name << " password: " << sql_passwd << std::endl;

    if (!check(sql_name) || !check(sql_passwd))
        return 0;
    struct User user;
    std::hash<std::string> szHash;
    size_t hashVal = szHash(sql_name) - 1;              //根据用户名生成哈希值,作为主键

    string sql = "insert into user values(";
    sql += std::to_string(hashVal);
    sql += ", \'";
    sql += sql_name;
    sql += "\', \'";
    sql += sql_passwd;
    sql += "\', 0, 0, \'";
    sql += uid;
    sql += "\')";

    if (executeSql(sql))
    {
        user.name = sql_name;
        user.win = 0;
        user.lose = 0;
        string html;
        ExpandHtml(user, &html); // 获取渲染或的网页
        std::cout << html << std::endl;
        return 0;
    }
    else
    {
        std::cerr << "sql语句执行失败: " << sql << std::endl;
        std::cout << "<html>";
        std::cout << "<head><meta charset=\"utf-8\"></head>";
        std::cout << "<body><h1>注册失败! 用户名已存在</h1></body></html>";
    }
    return 0;
}
  • updateResult.cc
#include "comm.hpp"

int main()
{
    string query_string;
    if(!GetQueryString(query_string, nullptr))
        return 1;

    Json::Value root;
    Json::Reader reader;
    reader.parse(query_string, root);
    int status = root["status"].asInt(); // 获取用户对局状态,1 表示赢, 0 表示输
    string name = root["userName"].asString();
    string uid = root["uid"].asString();

    // 连接数据库,查询并且更新用户数据
    std::hash<string> szHash;
    size_t hashVal = szHash(name) - 1;
    string sql = "select * from user where hash = ";
    sql += std::to_string(hashVal);
    struct User user;
    Json::Value result;
    Json::StyledWriter writer;
    string str;
    if (!querySql(sql, user, nullptr))   //查询用户数据
    {
        goto END;
    }
    if(user.uid != uid)  //根据用户ID, 判断用户登录是否过期, 是否存在风险
    {   //如果前端传过来的uid与数据库的uid不一致, 说明用户账号可能在不同的地方被其他人登录, 存在风险
        std::cerr << "uid 不相等, 用户账号存在风险, uid: " << uid << " user.uid: " << user.uid << std::endl;
        goto END;
    }
    
    if (status == 1)
    {
        sql = "update user set win = ";
        sql += std::to_string(user.win + 1);
        sql += " where hash = ";
        sql += std::to_string(hashVal);
    }else
    {
        sql = "update user set lose = ";
        sql += std::to_string(user.lose + 1);
        sql += " where hash = ";
        sql += std::to_string(hashVal);
    }

    if (!executeSql(sql)){   //更新用户数据
        goto END;
    }
    result["code"] = status == 1 ? user.win + 1 : user.lose + 1;
    str = writer.write(result); 
    std::cout << str << std::endl;
    std::cerr << str << std::endl;
    return 0;
END:
    std::cerr << "sql 语句执行失败, sql: " << sql << std::endl;
    result["code"] = -1;
    str = writer.write(result);
    std::cout << str << std::endl;
    std::cerr << str << std::endl;
    return 0;
}

说明

  • 项目当中有很多细节,很多坑,没有全部展示出来,后续会继续更新。
基于epoll多线程的高并发服务器源码的主要思想是通过利用epoll事件驱动模型和多线程来处理并发连接。 以下是一个简单的示例代码: ```python import socket import select import threading # 处理客户端请求的线程 def handle_client(client): while True: data = client.recv(1024) if not data: break client.send(data) client.close() # 主线程监听连接,并将连接交给处理线程 def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) server.listen(10) epoll = select.epoll() epoll.register(server.fileno(), select.EPOLLIN) connections = {} threads = {} while True: events = epoll.poll() for fileno, event in events: if fileno == server.fileno(): # 新连接 client, address = server.accept() connections[client.fileno()] = client epoll.register(client.fileno(), select.EPOLLIN) threads[client.fileno()] = threading.Thread(target=handle_client, args=(client,)) threads[client.fileno()].start() elif event & select.EPOLLIN: # 有数据可读 client = connections[fileno] threads[fileno].join() # 等待线程结束 del threads[fileno] epoll.unregister(fileno) del connections[fileno] client.close() if __name__ == '__main__': main() ``` 这段代码使用了select模块和epoll来进行事件管理和调度,在主线程中创建了一个套接字并监听端口。随后,将服务器套接字注册到epoll对象中,然后使用epoll的`poll()`方法监听事件。当有新连接到来时,主线程接受连接,并将连接套接字注册到epoll中,并创建一个新的线程来处理该连接的请求。每个线程读取客户端数据并发送回客户端,直到客户端断开连接。最后,线程结束,清理资源。 该源码可以实现简单的高并发服务器,能够同时处理多个客户端的连接请求,并且能够较好地利用系统资源。但值得注意的是,该示例代码仅提供了基本的框架,还需要根据实际需求进行完善和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值