基于Linux的轻量级Web服务器

一、项目介绍

基于C++在Linux下实现多线程Web服务器,支持用户登录功能和会话保持;

  • 使用多线程机制处理客户端连接,增加并行服务数量;
  • 解析get/post请求并返回http响应。get请求返回静态资源,post请求访问mysql数据库返回动态消息;
  • 支持application/json格式的数据传输,成功与vue前端实现通信;
  • 利用SessionID实现会话保持。

环境:WSL Ubuntu18.04 gcc 7.5.0 mysql 5.7.42

完整代码链接:websever_yao

二、 模块开发

2.1 数据库配置

  1. Mysql安装与启动
	sudo apt-get install mysql-server  
	sudo service mysql start 
  1. 利用navicate登陆服务器建两张表
    user表:user_id, user_name, passward
    session表:id, user_id, session_id, expire_time

  2. 引入所需头文件,初始化mysql:

//mysql.h
#ifndef _MMYSQL_H_
#define _MMYSQL_H_

#include <mysql/mysql.h>
#include <string.h>
#include <vector>
#include <iostream>
using namespace std;

class mysql_con
{
public:
    string m_host;			 //主机地址
    int m_Port;		 //数据库端口号
    string m_User;		 //登陆数据库用户名
    string m_PassWord;	 //登陆数据库密码
    string m_DatabaseName; //使用数据库名
    MYSQL* mysql;

public:
    bool ConnectDB();
    vector<vector<string>> getDatafromUserDB(string table_name,string user_name);
    vector<vector<string>> getDatafromSessionDB(string table_name,int user_id=0, string session_id="");
    bool InsertUserDB(string table_name, string m_name, string pass);
    bool UpdateUserDB(string table_name, int user_id, string m_name, string pass);
    bool DeleteUserDB(string table_name, string m_name);
    bool InsertSessionDB(string table_name, int user_id, string session_id, string expire_time);
    bool UpdateSessionDB(string table_name, string session_id, string expire_time);
	mysql_con();
	~mysql_con();

};

#endif

//mysql.cpp
#include "mmysql.h"
using namespace std;

mysql_con:: mysql_con()
{
	m_host = "localhost";
	m_Port = 3306;
	m_User = "root";
	m_PassWord = "Raner123!";
	m_DatabaseName = "databaseyao";
    mysql = NULL;
}

mysql_con::	~mysql_con(){
    if(!mysql)
    {
        mysql_close(mysql);
    }
}

bool mysql_con:: ConnectDB()
{
	// MYSQL* mysql = NULL;
	mysql = mysql_init(mysql);
	if (!mysql) {
		cout<<"Mysql error"<<endl;//返回并打印错误信息函数
		return false;
	}
	mysql_options(mysql, MYSQL_SET_CHARSET_NAME, "gbk");//连接设置
	mysql = mysql_real_connect(mysql, m_host.c_str(), m_User.c_str(), m_PassWord.c_str(), m_DatabaseName.c_str(), m_Port, NULL, 0);
	//中间分别是主机,用户名,密码,数据库名,端口号(可以写默认0或者3306等),可以先写成参数再传进去
	if (mysql == NULL) {
		cout<<"Mysql error"<<endl;
		return false;
	}
	cout<<"Database connected successful"<<endl;//连接成功反馈
    return true;
}

vector<vector<string>> mysql_con::  getDatafromUserDB(string table_name, string user_name) 
{
	std::vector<std::vector<std::string> > data;
	std::string queryStr = "select * from "+ table_name;
	if(user_name==""){
		queryStr+=";";
	}else{
		string con = " where user_name = '"+user_name+"' ;";
		queryStr+=con;
	}

	if (0 != mysql_query(mysql, queryStr.c_str())) {
		cout<<"mysql select error"<<endl;
		return data;
	}
	MYSQL_RES* result = mysql_store_result(mysql);//获得数据库结果集
	int row = mysql_num_rows(result);//获得结果集中的行数
	int field = mysql_num_fields(result);//获得结果集中的列数

	MYSQL_ROW line = NULL;
	line = mysql_fetch_row(result);
	string temp;
	while (NULL != line) {
		vector<string> linedata;
		for (int i = 0; i < field; i++) {//获取每一行的内容
			if (line[i]) {
				temp = line[i];
				linedata.push_back(temp);
			}
			else {
				temp = "NULL";
				linedata.push_back(temp);
			}
		}
		line = mysql_fetch_row(result);
		data.push_back(linedata);
	}
	return data;
}


2.2 socket网络编程

socket套接字,用于描述地址和端口,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。这里只包括服务端,用于支持http服务。
服务端:建立socket,声明自身的port和IP,并绑定到socket,使用listen监听,然后不断用accept去查看是否有连接。如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket。
socket网络编程经典代码:见EventListen()和EventLoop()函数

//websever.h
#ifndef WEBSERVER_H
#define WEBSERVER_H

#include<stdio.h>
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
#include <pthread.h>
#include"mmysql.h"

struct pthread_info{
    int m_clinfid;
    mysql_con* mysql;
};

class webserver{
    public: 
        webserver();
        ~webserver();
        void InitSql();
        bool EventListen();
        void EventLoop();
     
    public: 
        struct pthread_info pinfos[128]; 
        unsigned int  m_port;
        int m_listenfd;
        mysql_con mysql;
};

#endif

//websever.cpp
void * working( void *arg){
    struct pthread_info *pinfo = (struct pthread_info *)arg;
    int m_clintfd =pinfo->m_clinfid;
    mysql_con* mysql = pinfo->mysql;
    httpconn user_http(m_clintfd, mysql);
    user_http.handle_client();
    close(m_clintfd);
    pinfo->m_clinfid=-1;
    return NULL;
}

bool webserver:: EventListen()
{
    //create socket for listen
    m_listenfd=socket(AF_INET,SOCK_STREAM,0);// AF_INET协议族 IPv4  SOCK_STREAM流式协议TCP 
    int reuse = 1;
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
    if(m_listenfd==-1)
    {
        printf("socket create fail\n");
    }

    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    serveraddr.sin_port=htons(m_port);// htons // 主机字节序 - 网络字节序
    if(bind(m_listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
    {
        perror("bind");
        return false;
    }

    if(listen(m_listenfd,5)!=0)
    {
        perror("listen");
        return false;
    }
    return true;
}

void webserver::EventLoop(){
    int p_len = sizeof(pinfos)/sizeof(pinfos[0]);
    memset(pinfos,-1,sizeof(pinfos));
    // loop receive client's request
    while(1){
        char clintIP[16];
        int socklen=sizeof(struct sockaddr_in);
        struct sockaddr_in client_addr; //这是一个传出参数,可以知道客户端的ip和port
        int m_clintfd=accept(m_listenfd,(struct sockaddr*)&client_addr, (socklen_t *)&socklen);  //这是一个阻塞函数,返回用于通信的fd
        inet_ntop(AF_INET, &client_addr.sin_addr, clintIP, sizeof(clintIP));
        unsigned short clintPort = ntohs(client_addr.sin_port); 
        if(m_clintfd==-1){
            printf("connect failed\n");
            break;
        }

        printf("client %s: %d has connnected\n",clintIP, clintPort);
        struct pthread_info *pinfo = NULL; ///这样while退出作用域后内存才不会被回收。
        for(int i=0;i<p_len;i++){
            if(pinfos[i].m_clinfid == -1){
                pinfos[i].m_clinfid = m_clintfd;
                pinfos[i].mysql = &mysql;
                pinfo = &pinfos[i];
                break;
            }
        }
        // 创建线程,working函数处理客户端连接
        if(pinfo!=NULL){
            pthread_t tid;
            pthread_create(&tid, NULL, working, (void*)pinfo);
            pthread_detach(tid); //不能用阻塞的pthread_join,因为while循环要往下进行
        }
    }
    close(m_listenfd);
}

2.3解析http请求

// httpconn.h
#ifndef HTTPCONN_H
#define HTTPCONN_H

#include<stdio.h>
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
#include <pthread.h>
#include"mmysql.h"
#include "./include/AIGCJson.hpp"
#include "Session.h"
#include "utils.h"
using namespace std;
using namespace aigc;
//用于struct转换json格式
struct user{
    string username;
    string password;
    AIGC_JSON_HELPER(username, password)
};
/**
 * res_code = 0 请求成功,返回成功状态
 * res_code = 1 请求成功,一般情况的失败消息
 * res_code = 2 请求成功,用户会话超时
*/

struct response{
    int res_code;
    string res_info;
    AIGC_JSON_HELPER(res_code, res_info)
};

struct login_response{
    int res_code;
    string res_info;
    int user_id;
    string session_id;
    AIGC_JSON_HELPER(res_code, res_info,user_id,session_id)
};

class httpconn
{
    public:
        httpconn(int m_clintfd, mysql_con* mysql);
        ~httpconn();
        void handle_client();
    private:
        string session_id(string username, int id);
        bool check_session_state(string sessionid);
        void handle_request(char *request);
        void options_response();
        void query_user(string params);
        void bad_request();
        void login(char *params);

    private:
        int m_clintfd;
        sockaddr_in m_address;
        mysql_con* mysql;
        user usr;
        response res_pons;
        login_response login_res;
        
};
#endif

// httpconn.cpp
#include "httpconn.h"

using namespace std;

httpconn::httpconn(int clintfd, mysql_con* mysqlconn){
    m_clintfd = clintfd;
    mysql = mysqlconn;
}

httpconn::~httpconn(){
}

void httpconn:: handle_client() {
    char read_buffer[1024];
    // 读取客户端发送的HTTP请求
    ssize_t request_size = recv(m_clintfd, read_buffer, sizeof(read_buffer), 0);
    if (request_size == -1) {
        perror("Receive failed");
        return;
    }
    read_buffer[request_size] = '\0';
    printf("%s\n", read_buffer);
    // 处理HTTP请求
    handle_request(read_buffer);
}

void httpconn:: handle_request(char *request) {
    char method[1024], url[1024], http_version[1024];
    char *header, *body;
    sscanf(request, "%s %s %s", method, url, http_version);
    body = strstr(request, "\r\n\r\n");
    // 处理GET请求
    if (strcmp(method, "GET") == 0) {
        if(strcmp(url, "/query_user") == 0){
            header = strstr(request, "SessionID")+11;
            string str_header = header;
            string session_id = str_header.substr(0,32);
            query_user(session_id);
        }
    }
    // 处理POST请求
    else if (strcmp(method, "POST") == 0) {
        body = strstr(request, "\r\n\r\n");
        if (body == NULL) {
            return;
        }
        body=body+4;
        if (strcmp(url, "/login") == 0) {
            login(body);
        }else{
            bad_request();
        }
    }else if(strcmp(method, "OPTIONS") == 0){
        options_response();
    }
}

void httpconn:: login(char *params){
    JsonHelper::JsonToObject(usr, params);
    vector<vector<string>>  users = mysql->getDatafromUserDB("user",usr.username);
    if(users.size()==0){
        login_res.res_code = 1;
        login_res.res_info = "user incorrect!";
        login_res.user_id = 0;
        login_res.session_id = "";
    }else if(users.size()>0){
       vector<string> local_user = users[0];
       if(local_user[2] == usr.password){
            int usr_id = stoi(local_user[0]);
            string sessionid = session_id(usr.username,usr_id);
            login_res.res_code = 0;
            login_res.res_info = "login success";
            login_res.user_id = usr_id;
            login_res.session_id = sessionid;
       }else{
            login_res.res_code = 1;
            login_res.res_info = "passward incorrect!";
            login_res.user_id = 0;
            login_res.session_id = "";
       }
    }
    string jsonStr;
    JsonHelper::ObjectToJson(login_res, jsonStr);
    // 构造响应消息,注意允许跨域,准确计算响应体长度
    string response= "HTTP/1.1 200 OK\r\n"
                         "Server: WebServer Yao\r\n"
                         "Content-Type: application/json\r\n"
                         "Access-Control-Allow-Origin: *\r\n"
                         "Access-Control-Allow-Headers: *\r\n"
                         "Access-Control-Allow-Method: GET, POST\r\n"
                         "Connection: keep-alive\r\n";
    int len = jsonStr.size();
    char con_len[64]={};
    sprintf(con_len,"Content-Length: %d\r\n\r\n", len);
    response+=con_len;
    response+=jsonStr;        
    char write_buffer[1024]={0};
    strcpy(write_buffer,response.c_str());
    printf("%s\n", write_buffer);
    send(m_clintfd, write_buffer, strlen(write_buffer), 0);
}

string httpconn:: session_id(string username, int id){
    /***
     * 生成session_id和expire_time,存入mysql数据库
     * 若该user登陆过,则更新expire_time。会话时间为2分钟
     * 返回session_id
    */
    string session_id="";
    vector<vector<string>>  sess = mysql->getDatafromSessionDB("session", id, "");
    time_t now = time(nullptr);
    tm* t = localtime(&now);
    // 工具类函数stime,返回格式化时间:2023-08-10 17:10:42
    string time_now = stime(t);
    t->tm_min+=2; //会话时间2分钟
    string expire_time = stime(t);
    if(sess.size()>0){
        vector<string> ses = sess[0];
        session_id =  ses[2];
        mysql->UpdateSessionDB("session",session_id, expire_time);
    }else{
        unsigned char* str = new unsigned char[16];
        Session session(username,id, expire_time);
        session.SetSessionData();
        session.SetSessionId();
        str = session.GetSessionId();
        char session_str[16];
        for(int i=0;i<16;i++)
        {
            sprintf(session_str, "%02X", str[i]);
            session_id+=session_str;
        }   
        mysql->InsertSessionDB("session",id, session_id, expire_time);
    }
    return session_id;
}

void httpconn::options_response(){
    /***
     * 用于处理axios发起的预检options请求
    */
        // 构造响应消息
        string response= "HTTP/1.1 200 OK\r\n"
                         "Server: WebServer Yao\r\n"
                         "Content-Type: application/json\r\n"
                         "Connection: keep-alive\r\n"
                         "Access-Control-Allow-Origin: *\r\n"
                         "Access-Control-Allow-Headers: *\r\n"
                         "Access-Control-Max-Age: 2100\r\n\r\n";
     
        char write_buffer[1024];
        strcpy(write_buffer,response.c_str());
        printf("%s\n", write_buffer);
        send(m_clintfd, write_buffer, strlen(write_buffer), 0);
}


void httpconn::query_user(string params){
    /***
     * 检查用户的登陆状态
    */
        string sessionid = params;
        //检查登陆状态
        if(check_session_state(sessionid)){
            res_pons.res_code = 0;
            res_pons.res_info = "session state!";
        }else{
            res_pons.res_code = 2;
            res_pons.res_info = "session outdated!";
        }
        // 构造响应消息
        string response= "HTTP/1.1 200 OK\r\n"
                         "Server: WebServer Yao\r\n"
                         "Content-Type: application/json\r\n"
                         "Access-Control-Allow-Origin: *\r\n"
                         "Access-Control-Allow-Headers: *\r\n"
                         "Access-Control-Allow-Method: GET, POST\r\n"
                         "Connection: keep-alive\r\n";
     
        string jsonStr;
        JsonHelper::ObjectToJson(res_pons, jsonStr);
        int len = jsonStr.size();
        char con_len[64]={};
        sprintf(con_len,"Content-Length: %d\r\n\r\n", len);
        response+=con_len;
        response+=jsonStr;        
        char write_buffer[1024];
        strcpy(write_buffer,response.c_str());
        printf("%s\n", write_buffer);
        send(m_clintfd, write_buffer, strlen(write_buffer), 0);
}

2.4 用户登录功能

功能介绍

用户登陆界面,根据用户名和密码验证登录信息(login_user接口),登陆成功后并返回user_id和session_id。

登录后维护用户会话2分钟(query_user接口),超时后再次请求服务会自动转入login页面。在请求头中加入user_id和session_id,用户会话保持和后续的鉴权。

已成功测试postman和基于axios的页面请求(vue)

接口

  1. login(POST)
    在这里插入图片描述
  2. query_user(GET)
    在这里插入图片描述

2.5 支持application/json格式的数据传输

请参考:https://zhuanlan.zhihu.com/p/261361394

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值