基于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 数据库配置
- Mysql安装与启动
sudo apt-get install mysql-server
sudo service mysql start
-
利用navicate登陆服务器建两张表
user表:user_id, user_name, passward
session表:id, user_id, session_id, expire_time -
引入所需头文件,初始化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)
接口
- login(POST)
- query_user(GET)
2.5 支持application/json格式的数据传输
请参考:https://zhuanlan.zhihu.com/p/261361394