0%

使用muduo编写webserver

使用muduo编写webserver

因为学习了muduo库,想通过muduo库写一个webserver作为项目,在muduo和tinyWebserver的基础上改了一下,简单的把他们融合了一下。

httpserver

在muduo原本的httpserver的基础上进行了改造,加入简单的数据库操作的部分。针对http请求处理的逻辑部分封装在了onHttpProcess中。

HttpServer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#ifndef _HTTPSERVER_H
#define _HTTPSERVER_H

#include "../../net/TcpServer.h"
#include "../../base/SqlConnectionPool.h"

namespace tinyMuduo
{
namespace net
{

class HttpRequest;
class HttpResponse;

/// A simple embeddable HTTP server designed for report status of a program.
/// It is not a fully HTTP 1.1 compliant server, but provides minimum features
/// that can communicate with HttpClient and Web browser.
/// It is synchronous, just like Java Servlet.
class HttpServer : boost::noncopyable
{
public:
typedef std::function<void(const HttpRequest &,
HttpResponse *)>
HttpCallback;
HttpServer(EventLoop *loop,
const InetAddress &listenAddr,
const string &name,
const string &user,
const string &passwd,
const string &databaseName,
int sqlNum,
TcpServer::Option option = TcpServer::kNoReusePort);

EventLoop *getLoop() const { return server_.getLoop(); }

/// Not thread safe, callback be registered before calling start().
void setHttpCallback(const HttpCallback &cb)
{
httpCallback_ = cb;
}

void setThreadNum(int numThreads)
{
server_.setThreadNum(numThreads);
}

void start();

private:
void onConnection(const TcpConnectionPtr &conn);
void onMessage(const TcpConnectionPtr &conn,
Buffer *buf,
Timestamp receiveTime);
void onRequest(const TcpConnectionPtr &, const HttpRequest &);
void onWriteComplete(const TcpConnectionPtr &conn);
void initmysql(ConnectionPool *connPool);

void onHttpProcess(const HttpRequest &req, HttpResponse *resp);
TcpServer server_;
HttpCallback httpCallback_;

ConnectionPool *connPool_; // 数据库相关
string user_; // 登陆数据库用户名
string passwd_; // 登陆数据库密码
string databaseName_; // 使用数据库名
int sqlNum_;
map<string, string> users;
};

} // namespace net
} // namespace tinyMuduo

#endif // _HTTPSERVER_H

HttpServer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304

#include "../../net/http/HttpServer.h"

#include "../../base/Logging.h"
#include "../../net/http/HttpContext.h"
#include "../../net/http/HttpRequest.h"
#include "../../net/http/HttpResponse.h"
#include <sys/stat.h>

using namespace tinyMuduo;
using namespace tinyMuduo::net;

namespace tinyMuduo
{
namespace net
{
namespace detail
{

void defaultHttpCallback(const HttpRequest &, HttpResponse *resp)
{
resp->setStatusCode(HttpResponse::k404NotFound);
resp->setStatusMessage("Not Found");
resp->setCloseConnection(true);
}

} // namespace detail
} // namespace net
} // namespace tinyMuduo

HttpServer::HttpServer(EventLoop *loop,
const InetAddress &listenAddr,
const string &name,
const string &user,
const string &passwd,
const string &databaseName,
int sqlNum,
TcpServer::Option option)
: server_(loop, listenAddr, name, option), user_(user), passwd_(passwd), databaseName_(databaseName), sqlNum_(sqlNum)
{
server_.setConnectionCallback(
std::bind(&HttpServer::onConnection, this, _1));
server_.setMessageCallback(
std::bind(&HttpServer::onMessage, this, _1, _2, _3));
server_.setWriteCompleteCallback(
std::bind(&HttpServer::onWriteComplete, this, _1));

this->setHttpCallback(std::bind(&HttpServer::onHttpProcess, this, _1, _2));
// user_ = "root";
// passwd_ = "123456";
// databaseName_ = "yourdb";
// sqlNum_ = 8;
}

/**
* @brief 初始化数据库
*
* @param connPool
*/
void HttpServer::initmysql(ConnectionPool *connPool)
{
// 先从连接池中取一个连接
MYSQL *mysql = NULL;
tinyMuduo::ConnectionRAII mysqlcon(&mysql, connPool);

// 在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR << "SELECT error: " << mysql_error(mysql);
}

// 从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);

// 返回结果集中的列数
int num_fields = mysql_num_fields(result);

// 返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);

// 从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}

void HttpServer::start()
{
LOG_WARN << "HttpServer[" << server_.name()
<< "] starts listening on " << server_.ipPort();
server_.start();
// 初始化数据库连接池
connPool_ = ConnectionPool::getInstance();
connPool_->init("127.0.0.1", user_, passwd_, databaseName_, 3306, sqlNum_);
initmysql(connPool_);
}

void HttpServer::onConnection(const TcpConnectionPtr &conn)
{
if (conn->connected())
{
conn->setContext(HttpContext());
}
}

void HttpServer::onMessage(const TcpConnectionPtr &conn,
Buffer *buf,
Timestamp receiveTime)
{
HttpContext *context = boost::any_cast<HttpContext>(conn->getMutableContext());
// LOG_INFO << buf->toStringPiece();
if (!context->parseRequest(buf, receiveTime))
{
conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");
conn->shutdown();
}

if (context->gotAll())
{
onRequest(conn, context->request());
context->reset();
}
}

void HttpServer::onRequest(const TcpConnectionPtr &conn, const HttpRequest &req)
{
const string &connection = req.getHeader("Connection");
bool close = connection == "close" ||
(req.getVersion() == HttpRequest::kHttp10 && connection != "Keep-Alive");
HttpResponse response(close);
httpCallback_(req, &response);
Buffer buf;
response.appendToBuffer(&buf);
conn->send(&buf);
const char *file = response.g_file.c_str();
FILE *fp = ::fopen(file, "rb");
if (fp)
{
TcpConnection::FilePtr ctx(fp, ::fclose);
// conn->setContext(ctx);
conn->filePtr_.swap(ctx);
char buf[TcpConnection::kBufSize];
size_t nread = ::fread(buf, 1, sizeof buf, fp);
conn->send(buf, static_cast<int>(nread));
}
else
{
LOG_INFO << file << " no such file";
conn->shutdown();
}
}

void HttpServer::onWriteComplete(const TcpConnectionPtr &conn)
{

char buf[TcpConnection::kBufSize];
size_t nread = 0;
if (conn->filePtr_)
nread = ::fread(buf, 1, sizeof buf, get_pointer(conn->filePtr_));
if (nread > 0)
{
conn->send(buf, static_cast<int>(nread));
}
else
{
conn->filePtr_.reset();
LOG_INFO << "FileServer - done";
}
}

/**
* @brief 对http消息进行处理的函数
*
* @param req
* @param resp
*/
void HttpServer::onHttpProcess(const HttpRequest &req, HttpResponse *resp)
{
LOG_INFO << "Headers " << req.methodString() << " " << req.path();

std::string file;
if (!req.body().empty())
{
const char *bodyStr = req.body().c_str();
// 将用户名和密码提取出来
// user=123&passwd=123
char name[100], password[100];
int i;
for (i = 5; bodyStr[i] != '&'; ++i)
name[i - 5] = bodyStr[i];
name[i - 5] = '\0';

int j = 0;
for (i = i + 10; bodyStr[i] != '\0'; ++i, ++j)
password[j] = bodyStr[i];
password[j] = '\0';

if (req.path() == "/3CGISQL.cgi")
{
// 表示注册
if (users.count(name))
{
file.append("resources/registerError.html");
}
else
{
// 如果是注册,先检测数据库中是否有重名的
// 没有重名的,进行增加数据
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");

// 先从连接池中取一个连接
MYSQL *mysql = NULL;
ConnectionRAII mysqlcon(&mysql, connPool_);
// 此处感觉需要锁一下
int res = mysql_query(mysql, sql_insert);
if (!res)
{
users[name] = password;
file.append("resources/login.html");
}
else
{
file.append("resources/registerError.html");
}
}
}
else if (req.path() == "/2CGISQL.cgi")
{
// 表示登录
if (users.count(name) && users[name] == password)
{

file.append("resources/welcome.html");
}
else
{
file.append("resources/logError.html");
}
}
}
else if (req.path() == "/0")
{
file.append("resources/register.html");
}
else if (req.path() == "/1")
{
file.append("resources/login.html");
}
else if (req.path() == "/5")
{
file.append("resources/picture.html");
}
else if (req.path() == "/6")
{
file.append("resources/video.html");
}
else if (req.path() == "/7")
{
file.append("resources/fans.html");
}
else if (req.path() == "/404")
{
file.append("resources/404.html");
}
else
{
// strcpy(file, "resources");
file.append("resources");
// int len = strlen(file);
const char *url_real = req.path().c_str();
file.append(url_real);
}

// 读取文件状态
struct stat fileStat;
if (stat(file.c_str(), &fileStat) < 0)
{
LOG_INFO << file << " no such file";
file.clear();
file.append("resources/404.html");
stat(file.c_str(), &fileStat);
}

// if (!(fileStat.st_mode & S_IROTH))
// return;

// if (S_ISDIR(fileStat.st_mode))
// return;

resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->addHeader("Server", "tinyMuduo");
resp->setContentLength(fileStat.st_size);
resp->setFile(file);
}

重要的东西是onHttpProcess函数,这个函数是处理http请求的逻辑。其次我觉得重要的是onRequest中发送文件,因为图片、html页面等文件太大,所以一次发可能装不下,所以这里参考了muduo中ftp的实现,在onRequest中发送一次文件,如果没有发送完会在onWriteComplete中接着发送。

SqlConnectionPool

因为涉及连接数据库,简单将tinyWEbserver里面的数据库池搬了过来。

SqlConnectionPool.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#ifndef _SQLCONNECTIONPOOL_H
#define _SQLCONNECTIONPOOL_H

#include <stdio.h>
#include <list>
#include <mysql/mysql.h>
#include <error.h>
#include <string.h>
#include <iostream>
#include <string>
#include "Logging.h"
#include <mutex>
#include <condition_variable>

using namespace std;

namespace tinyMuduo
{
class ConnectionPool
{
public:
MYSQL *getConnection(); // 获取数据库连接
bool releaseConnection(MYSQL *conn); // 释放连接
int getFreeConn(); // 获取连接
void destroyPool(); // 销毁所有连接

// 单例模式
static ConnectionPool *getInstance();

void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn);

private:
ConnectionPool();
~ConnectionPool();

int maxConn_; // 最大连接数
int curConn_; // 当前已使用的连接数
int freeConn_; // 当前空闲的连接数
list<MYSQL *> connList; // 连接池
std::mutex mutex_;
std::condition_variable condition_;

public:
string url_; // 主机地址
string port_; // 数据库端口号
string user_; // 登陆数据库用户名
string passwd_; // 登陆数据库密码
string databaseName_; // 使用数据库名
};

class ConnectionRAII
{

public:
ConnectionRAII(MYSQL **con, ConnectionPool *connPool);
~ConnectionRAII();

private:
MYSQL *conRAII_;
ConnectionPool *poolRAII_;
};
}

#endif // _SQLCONNECTIONPOOL_H

SqlConnectionPool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <mysql/mysql.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <list>
#include <pthread.h>
#include <iostream>
#include "SqlConnectionPool.h"

using namespace std;

namespace tinyMuduo
{
ConnectionPool::ConnectionPool()
{
curConn_ = 0;
freeConn_ = 0;
}

ConnectionPool *ConnectionPool::getInstance()
{
static ConnectionPool connPool;
return &connPool;
}

// 构造初始化
void ConnectionPool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn)
{
url_ = url;
port_ = Port;
user_ = User;
passwd_ = PassWord;
databaseName_ = DBName;

for (int i = 0; i < MaxConn; i++)
{
MYSQL *con = NULL;
con = mysql_init(con);

if (con == NULL)
{
LOG_ERROR << "MySQL Error";
exit(1);
}
con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);

if (con == NULL)
{
LOG_ERROR << "MySQL Error";
exit(1);
}
connList.push_back(con);
++freeConn_;
}

maxConn_ = freeConn_;
}

// 当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数
MYSQL *ConnectionPool::getConnection()
{
MYSQL *con = NULL;

if (0 == connList.size())
return NULL;

std::unique_lock<std::mutex> lk(mutex_);

con = connList.front();
connList.pop_front();

--freeConn_;
++curConn_;
while (freeConn_ <= 0)
{
/* code */
condition_.wait(lk);
}

return con;
}

// 释放当前使用的连接
bool ConnectionPool::releaseConnection(MYSQL *con)
{
if (NULL == con)
return false;

std::unique_lock<std::mutex> lk(mutex_);

connList.push_back(con);
++freeConn_;
--curConn_;

// condition_.wait(lk);
if (freeConn_ > 0)
{
condition_.notify_all();
}
return true;
}

// 销毁数据库连接池
void ConnectionPool::destroyPool()
{

std::lock_guard<std::mutex> lock(mutex_);

if (connList.size() > 0)
{
list<MYSQL *>::iterator it;
for (it = connList.begin(); it != connList.end(); ++it)
{
MYSQL *con = *it;
mysql_close(con);
}
curConn_ = 0;
freeConn_ = 0;
connList.clear();
}
}

// 当前空闲的连接数
int ConnectionPool::getFreeConn()
{
return this->freeConn_;
}

ConnectionPool::~ConnectionPool()
{
destroyPool();
}

ConnectionRAII::ConnectionRAII(MYSQL **SQL, ConnectionPool *connPool)
{
*SQL = connPool->getConnection();

conRAII_ = *SQL;
poolRAII_ = connPool;
}

ConnectionRAII::~ConnectionRAII()
{
poolRAII_->releaseConnection(conRAII_);
}
} // namespace tinyMuduo

测试SqlConnectionPool_test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "../../base/SqlConnectionPool.h"
#include <map>
#include <iostream>

// 需要修改的数据库信息,登录名,密码,库名
string user = "root";
string passwd = "123456";
string databasename = "yourdb";

map<string, string> users;

void initmysqlResult(tinyMuduo::ConnectionPool *connPool)
{
// 先从连接池中取一个连接
MYSQL *mysql = NULL;
tinyMuduo::ConnectionRAII mysqlcon(&mysql, connPool);

// 在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR << "SELECT error: " << mysql_error(mysql);
}

// 从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);

// 返回结果集中的列数
int num_fields = mysql_num_fields(result);

// 返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);

// 从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}

int main(int argc, char const *argv[])
{
// 初始化数据库连接池
tinyMuduo::ConnectionPool *connPool = tinyMuduo::ConnectionPool::getInstance();
connPool->init("127.0.0.1", user, passwd, databasename, 3306, 8);

// 初始化数据库读取表
initmysqlResult(connPool);
for (auto iter = users.begin(); iter != users.end(); iter++)
{
cout << "username: " << iter->first << " passwd: " << iter->second << std::endl;
}
return 0;
}

mysql是使用docker进行安装的,进入docker查看一下数据

image-20230213144740181

运行测试

image-20230213144824101

可以看到简单的连接是成功的

main

main里面实现很简单,主要是需要写好数据库相关的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "base/Logging.h"
#include "net/EventLoop.h"
#include "net/http/HttpServer.h"
#include "net/http/HttpRequest.h"
#include "net/http/HttpResponse.h"

using namespace tinyMuduo;
using namespace tinyMuduo::net;

int main(int argc, char const *argv[])
{

// 需要修改的数据库信息,登录名,密码,库名
string user = "root";
string passwd = "123456";
string databasename = "yourdb";
int sqlNum = 8;
int numThreads = 5;

EventLoop loop;
HttpServer server(&loop, InetAddress(8080), "webserver", user, passwd, databasename, sqlNum);
server.setThreadNum(numThreads);
server.start();
loop.loop();

return 0;
}

运行结果

登录界面

image-20230213151254186

选择界面

image-20230213151319812

关注界面

image-20230213151339389

感谢

感谢muduo的仓库和tinyWebserver仓库

muduo:https://github.com/chenshuo/muduo

tinyWebServer:https://github.com/qinguoyi/TinyWebServer

总结

在muduo的基础上想写个webserver很简单,只需要在他http的基础上进行改进就可以。

最好代码仓库:https://github.com/bugcat9/tinyMuduo