引言:
在海量文件存储中尤其是小文件我们通常会用上fastdfs对数据进行高效存储,在现实生产中fastdfs通常用于图片,文档,音频等中小文件。
一.项目中用到的基础组件(Base)
1.网络库(muduo)
我们就以muduo网络库为例子讲解IO多路复用和reactor网络模型
1.1 IO多路复用
我们可以借用陈硕大神的原话来理解IO的同步和异步↓↓↓
在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步IO。
一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和"数据读写"。
数据就绪阶段分为---->阻塞和非阻塞。
数据读写阶段分为---->同步和异步。
我们重点介绍epoll
epoll相比于其他IO模型来讲好的太多了,在muduo网络库重的底层IO多路复用我们采用的就是epoll来处理IO事件的。
epoll 适合大规模高并发场景,是 Linux 下高性能网络编程首选。
1.2 Reactor模型
Reactor模型是被广泛使用在生产环境的中的一个网络模型
重要组件:Event事件、Reactor反应堆、Demultiplex事件分发器、Evanthandler事件处理器
muduo库的Multiple Reactors模型如下:
1.3 muduo网络库的核心代码模块
①Channel
fd、events、revents、callbacks 两种channel listenfd-acceptorChannel connfdconnectionChannel
②Poller和EPollPoller - Demultiplex
std::unordered_map<int, Channel*> channels
③EventLoop - Reactor
ChannelList activeChannels_;std::unique_ptr poller_;int wakeupFd ; -> loopstd::unique_ptr wakeupChannel ;
④Thread和EventLoopThread
⑤EventLoopThreadPool
getNextLoop() : 通过轮询算法获取下一个subloop baseLoop一个thread对应一个loop => one loop per thread
⑥Socket
⑦Acceptor
主要封装了listenfd相关的操作 socket bind listen baseLoop
⑧Buffer
缓冲区 应用写数据 -> 缓冲区 -> Tcp发送缓冲区 -> sendprependable readeridx writeridx
⑨TcpConnection
一个连接成功的客户端对应一个TcpConnection Socket Channel 各种回调 发送和接收缓冲区
⑩TcpServer
Acceptor EventLoopThreadPoolConnectionMap connections_;
1.4 muduo网络库的核心思想是one loop peer thread
什么是one loop peer thread呢?是指示一个loop对应一个线程
每个eventloop中都对应着一个线程,我们可以看到有一个poller和很多channel,并管理着这些channel。
在main主线程中会生成一个执行一个eventloop 对应着mainreactor这个loop负责管理主线程中对客户端的连接,当有连接来的时候会分发给其他loop 对应着subreactor
1.5我们介绍一些核心的代码块
①根据poller通知的channel发生的具体事件,由channel负责调具体的回调操作
//根据poller通知的channel发生的具体事件,由channel负责调用具体的回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
LOG_INFO("channel handleEvents revents:%d\n",revents_);
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN))
{
if (closecallback_)
{
closecallback_();
}
}
if (revents_ & EPOLLERR)
{
if (errorcallback_)
{
errorcallback_();
}
}
if (revents_ & (EPOLLIN | EPOLLPRI))
{
if (readcallback_)
{
readcallback_(receiveTime);
}
}
if (revents_ & EPOLLOUT)
{
if (writecallback_)
{
writecallback_();
}
}
}
处理挂起事件(EPOLLHUP)
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN))
如果发生了挂起事件(如对端关闭连接),并且没有可读事件,则调用 closecallback_() 关闭回调。
处理错误事件(EPOLLERR)
if (revents_ & EPOLLERR)
如果发生了错误事件,则调用 errorcallback_() 错误回调。
处理读事件(EPOLLIN | EPOLLPRI)
if (revents_ & (EPOLLIN | EPOLLPRI))
如果有可读事件(普通或优先级数据),则调用 readcallback_() 读回调,并传递接收时间。
处理写事件(EPOLLOUT)
if (revents_ & EPOLLOUT)
如果有可写事件,则调用 writecallback_() 写回调。
该函数根据 epoll 返回的事件类型,安全地调用相应的回调函数,完成事件驱动的分发和处理。这样可以让上层业务只需关注回调逻辑,而不用关心底层事件分发细节。
②evetloop:loop开启事件循环代码模块
// 开启事件循环
void EventLoop::loop()
{
looping_ = true;
quit_ = false;
LOG_INFO("eventloop %p start looping\n", this);
while (!quit_)
{
activeChannels_.clear();
// 监听两类fd 一种是client的fd 一种wakeupfd
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (Channel *channel : activeChannels_)
{
// Poller监听哪些channel发生事件了 然后上报给EventLoop 通知channel处理相应的事件
channel->handleEvent(pollReturnTime_);
}
// 执行当前eventloop事件循环 需要处理的回调操作
/*
IO线程 mainloop accept fd《=channel subloop
mainloop 事先注册一个回调cb (需要subloop来执行) wakeup subloop后,执行下面的方法,执行之前mainloop注册的cb操作
*/
doPendingFunctors();
}
LOG_INFO("EventLoop%p stop looping \n", this);
looping_ = false;
}
开启loop-->会执行poller->poll接口开启事件监听和对channel的管理,不断的处理活跃的channel和活跃的事件并不断的执行回调函数.
③one loop peer thread 的具体体现
EventLoop *EventLoopThread::startloop()
{
thread_.start(); // 启动底层新线程
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(mutex_);
while (loop_ == nullptr)
{
cond_.wait(lock);
}
loop=loop_;
}
return loop;
}
// 下面这个方法是在单独的新线程里面运行的
void EventLoopThread::threadFunc()
{
// “One loop per thread”(每个线程一个事件循环)
EventLoop loop; // 创建了一个独立的eventloop 和 上面的线程是一一对应的 one loop per thread模型
if (callback_)
{
callback_(&loop);
}
{
std::unique_lock<std::mutex> lock(mutex_);
loop_ = &loop;
cond_.notify_one();
}
loop.loop();//EventLoop loop => Poller.poll
std::unique_lock<std::mutex> lock(mutex_);
loop_=nullptr;
}
main主线程会主动执行start 从而 启动一个maineventloop和mainreactor进行对连接的监听,我们可以启动任意个分线程从而对应着起任意个subloop。
1.6 muduo网络库的具体案例
#include <iostream>
#include <mymuduo/TcpServer.h>
#include <ostream>
#include <string>
#include <mymuduo/logger.h>
#include <functional>
class EchoServer
{
public:
EchoServer(EventLoop *loop, const InetAddress &addr, const std::string &name)
: server_(loop, addr, name),
loop_(loop)
{
// 注册回调函数
server_.setConnectionCallback(std::bind(&EchoServer::onConnection, this, std::placeholders::_1));
server_.setMessageCallback(std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 设置合适的loop线程数量 loopthread
server_.setThreadNum(3);
}
void start()
{
server_.start();
}
private:
// 连接建立或者断开的回调
void onConnection(const TcpConnectionPtr &conn)
{
if (conn->connected())
{
LOG_INFO("conn up:%s", conn->peerAddress().toIpPort().c_str());
}
else
{
LOG_INFO("conn down:%s", conn->peerAddress().toIpPort().c_str());
}
}
// 可读写事件回调
void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time)
{
std::string msg = buf->retrieveAllAsString();
std::cout<<msg<<std::endl;
conn->send(msg);
// conn->shutdown(); // 写端 EPOLLHUP => closecallback
}
EventLoop *loop_;
TcpServer server_;
};
int main()
{
EventLoop loop;
InetAddress addr(8080, "192.168.217.148");
EchoServer echoServer(&loop, addr, "myserver-01"); // accpertor non-blocking listenfd create bind
echoServer.start(); // listen loopthread listenfd => accpetorchannle => mainloop
loop.loop(); // 启动mainloop的底层poller
return 0;
}
2.Thread_pool线程池
.h
#pragma once
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <atomic>
class ThreadPool {
public:
ThreadPool(size_t thread_count);
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
std::atomic<bool> stop;
void worker();
};
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
// 推导任务的返回类型
using return_type = typename std::result_of<F(Args...)>::type;
// 用packaged_task包装任务和参数,支持返回值
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 获取future,用于异步获取任务结果
std::future<return_type> res = task->get_future();
{
// 加锁保护任务队列
std::lock_guard<std::mutex> lock(queue_mutex);
// 如果线程池已停止,抛出异常
if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");
// 将任务包装为无参void函数,放入任务队列
tasks.emplace([task]() { (*task)(); });
}
// 通知一个工作线程有新任务
condition.notify_one();
// 返回future给调用者
return res;
}
.c
#include "Thread_pool.h"
ThreadPool::ThreadPool(size_t thread_count) : stop(false) {
for (size_t i = 0; i < thread_count; ++i) {
workers.emplace_back(&ThreadPool::worker, this);
}
}
void ThreadPool::worker() {
while (!stop) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
ThreadPool::~ThreadPool() {
stop = true;
condition.notify_all();
for (auto& t : workers) {
if (t.joinable()) t.join();
}
}
// =================== 示例用法 ===================
#include <iostream>
int add(int a, int b) {
return a + b;
}
void print_task(int i) {
std::cout << "Task " << i << " is running in thread "
<< std::this_thread::get_id() << std::endl;
}
int main() {
ThreadPool pool(4);
auto fut = pool.enqueue(add, 3, 5);
std::cout << "add(3,5) = " << fut.get() << std::endl;
for (int i = 0; i < 8; ++i) {
pool.enqueue(std::bind(print_task, i));
}
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
+-------------------+
| ThreadPool |
+-------------------+
| - workers |----> [std::thread, std::thread, ...]
| - tasks |----> +--------------------------+
| - queue_mutex | | 任务队列 (std::queue) |
| - condition | +--------------------------+
| - stop |
+-------------------+
|
| 1. 主线程调用 enqueue() 提交任务
v
+--------------------------+
| 任务队列 (tasks) |
+--------------------------+
|
| 2. 工作线程等待任务
v
+--------------------------+
| 工作线程 (worker) |
|--------------------------|
| while (!stop) |
| 等待任务 |
| 取出任务 |
| 执行任务 |
+--------------------------+
|
| 3. 析构时 stop=true, notify_all 唤醒所有线程安全退出
v
+--------------------------+
| 线程安全销毁 |
+--------------------------+
线程池主要是基于生产者消费者模型进行设计的
生产者源源不断往任务队列tasks里面添加任务task, 消费线程也就是工作线程不断从任务队列里面取数据进行任务处理,没有任务的时候工作线程进入休眠等待,当任务队列有任务的时候通过condition_wait唤醒工作线程。
3.mysql连接池
MySQL 连接池是一种复用数据库连接资源、提升高并发访问效率的技术。其核心思想是:预先创建一定数量的数据库连接,放入池中,业务线程需要时从池中获取,用完后归还,而不是每次都新建和销毁连接。
我们主要介绍CDBConn这个类来介绍对mysql进行建表,插入,更新,查询等操作
// 初始化数据库连接
int CDBConn::Init()
{
m_mysql = mysql_init(NULL); // 初始化mysql连接
if (!m_mysql)
{
LOG_ERROR << "mysql_init failed";
return 1;
}
bool reconnect = true;
mysql_options(m_mysql, MYSQL_OPT_RECONNECT, &reconnect);
mysql_options(m_mysql, MYSQL_SET_CHARSET_NAME, "utf8mb4"); // 设置字符集
// 连接数据库
if (!mysql_real_connect(m_mysql, m_pDBPool->GetDBServerIP(), m_pDBPool->GetUsername(), m_pDBPool->GetPasswrod(),
m_pDBPool->GetDBName(), m_pDBPool->GetDBServerPort(), NULL, 0))
{
LOG_ERROR << "mysql_real_connect failed: " << mysql_error(m_mysql);
return 2;
}
return 0;
}
// 获取连接池名称
const char *CDBConn::GetPoolName()
{
return m_pDBPool->GetPoolName();
}
// 执行建表、插入等操作
bool CDBConn::ExecuteCreate(const char *sql_query)
{
mysql_ping(m_mysql);
if (mysql_real_query(m_mysql, sql_query, strlen(sql_query)))
{
LOG_ERROR << "mysql_real_query failed: " << mysql_error(m_mysql) << ", sql: start transaction";
return false;
}
return true;
}
// 执行更新操作
bool CDBConn::ExecutePassQuery(const char *sql_query)
{
mysql_ping(m_mysql);
if (mysql_real_query(m_mysql, sql_query, strlen(sql_query)))
{
LOG_ERROR << "mysql_real_query failed: " << mysql_error(m_mysql) << ", sql: start transaction";
return false;
}
return true;
}
// 执行删除操作
bool CDBConn::ExecuteDrop(const char *sql_query)
{
mysql_ping(m_mysql);
if (mysql_real_query(m_mysql, sql_query, strlen(sql_query)))
{
LOG_ERROR << "mysql_real_query failed: " << mysql_error(m_mysql) << ", sql: start transaction";
return false;
}
return true;
}
// 执行查询操作,返回结果集
CResultSet *CDBConn::ExecuteQuery(const char *sql_query)
{
mysql_ping(m_mysql);
row_num = 0;
if (mysql_real_query(m_mysql, sql_query, strlen(sql_query)))
{
LOG_ERROR << "mysql_real_query failed: " << mysql_error(m_mysql) << ", sql: %s\n" << sql_query;
return NULL;
}
MYSQL_RES *res = mysql_store_result(m_mysql); // 获取结果集
if (!res)
{
LOG_ERROR << "mysql_store_result failed: " << mysql_error(m_mysql);
return NULL;
}
row_num = mysql_num_rows(res);
LOG_INFO << "row_num: " << row_num;
CResultSet *result_set = new CResultSet(res);
return result_set;
}
MYSQL *m_mysql; // MySQL连接指针
我们通过对m_mysql连接指针进行初始化操作 我们拿到一个对mysql的连接。
mysql_options 接口
用于在建立 MySQL 连接前设置连接相关的参数和行为(如自动重连、字符集等),要在mysql_real_connect接口执行之前执行
mysql_real_connect 接口
建立与 MySQL 服务器的实际连接,后续所有数据库操作都基于该连接句柄进行。
mysql_ping 接口
是 MySQL C API 的一个函数,用于检测当前数据库连接是否可用,如果连接已断开会尝试自动重连。
mysql_real_query 接口
是 MySQL C API 中用于执行 SQL 语句的函数,可以发送任意 SQL(如 SELECT、INSERT、UPDATE、DELETE 等)到服务器执行
二.注册,登录API接口的实现
注册和登录功能是我们http服务中的最入门的接口,我们在实现这两个接口的前提,我们需要了解json协议
1.json数据交换格式
sequenceDiagram
participant Client as 客户端 (例如: 浏览器, 移动应用)
participant Server as 服务器 (例如: Web API)
Client->>Client: 1. 准备数据 (应用内部对象/结构)
Client->>Client: 2. 将数据序列化为 JSON 字符串
Note over Client,Server: HTTP 请求 (例如: POST, GET)
Client->>Server: 3. 发送 HTTP 请求 (请求体包含 JSON 数据, Content-Type: application/json)
Server->>Server: 4. 接收 HTTP 请求
Server->>Server: 5. 解析请求体中的 JSON 字符串为服务器端对象/结构
Server->>Server: 6. 处理业务逻辑 (例如: 查询数据库, 执行操作)
Server->>Server: 7. 准备响应数据 (服务器端对象/结构)
Server->>Server: 8. 将响应数据序列化为 JSON 字符串
Note over Client,Server: HTTP 响应
Server->>Client: 9. 发送 HTTP 响应 (响应体包含 JSON 数据, Content-Type: application/json)
Client->>Client: 10. 接收 HTTP 响应
Client->>Client: 11. 解析响应体中的 JSON 字符串为客户端对象/结构
Client->>Client: 12. 使用数据 (例如: 更新 UI, 存储数据)
- 客户端准备数据:应用程序在客户端生成需要发送的数据。
- 序列化为 JSON:客户端将这些数据转换(序列化)成 JSON 格式的字符串。
- 发送 HTTP 请求:客户端通过 HTTP 协议向服务器发送请求,JSON 字符串通常放在请求体 (request body) 中。
- 服务器接收请求:服务器接收到 HTTP 请求。
- 解析 JSON:服务器从请求体中提取 JSON 字符串,并将其转换(反序列化/解析)为服务器端编程语言可以处理的数据结构(如对象、字典等)。
- 处理业务逻辑:服务器根据解析后的数据执行相应的业务操作。
- 服务器准备响应数据:服务器生成要返回给客户端的数据。
- 序列化为 JSON:服务器将响应数据序列化为 JSON 格式的字符串。
- 发送 HTTP 响应:服务器通过 HTTP 协议将包含 JSON 数据的响应发送回客户端。
- 客户端接收响应:客户端接收到服务器的 HTTP 响应。
- 解析 JSON:客户端解析响应体中的 JSON 字符串,将其转换为客户端编程语言可以处理的数据结构。
- 客户端使用数据:客户端使用这些解析后的数据进行后续操作,如更新用户界面。
2.注册
①注册的入口函数
// 注册接口主流程
int ApiRegisterUser(string &url, string &post_data, string &str_json)
{
UNUSED(url);
int ret = 0;
string user_name;
string nick_name;
string pwd;
string phone;
string email;
// 判断数据是否为空
if (post_data.empty())
{
return -1;
}
// 解析json
if (decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email) < 0)
{
LOG_ERROR << "decodeRegisterJson failed";
encodeRegisterJson(1, str_json);
return -1;
}
// 注册账号
ret = registerUser(user_name, nick_name, pwd, phone, email);
encodeRegisterJson(ret, str_json);
return ret;
}
②解析json数据
int decodeRegisterJson(const std::string &str_json, string &user_name, string &nick_name, string &pwd, string &phone, string &email)
{
bool res;
Json::Value root;
Json::Reader jsonReader;
res = jsonReader.parse(str_json, root);
if (!res)
{
LOG_ERROR << "parse reg json failed ";
return -1;
}
// 用户名
if (root["userName"].isNull())
{
LOG_ERROR << "userName null\n";
return -1;
}
user_name = root["userName"].asString();
// 昵称
if (root["nickName"].isNull())
{
LOG_ERROR << "nickName null\n";
return -1;
}
nick_name = root["nickName"].asString();
// 密码
if (root["firstPwd"].isNull())
{
LOG_ERROR << "firstPwd null\n";
return -1;
}
pwd = root["firstPwd"].asString();
// 电话(非必须)
if (root["phone"].isNull())
{
LOG_WARN << "phone null\n";
}
else
{
phone = root["phone"].asString();
}
// 邮箱(非必须)
if (root["email"].isNull())
{
LOG_WARN << "email null\n";
}
else
{
email = root["email"].asString();
}
return 0;
}
③封装注册的用户信息
int encodeRegisterJson(int ret, string &str_json)
{
Json::Value root;
root["code"] = ret;
Json::FastWriter writer;
str_json = writer.write(root);
return 0;
}
④把用户信息注册到数据库中
// 注册用户到数据库
int registerUser(string &user_name, string &nick_name, string &pwd, string &phone, string &email)
{
int ret = 0;
uint32_t user_id;
CDBManager *pDBManager = CDBManager::getInstance();
CDBConn *pDBConn = pDBManager->GetDBConn("tuchuang_slave");
AUTO_REAL_DBCONN(pDBManager, pDBConn);
// 先查看用户是否存在
string strSql;
strSql = formatString2("select * from user_info where user_name='%s'", user_name.c_str());
CResultSet *pResultSet = pDBConn->ExecuteQuery(strSql.c_str());
if (pResultSet && pResultSet->Next())
{ // 检测是否存在用户记录
// 存在则返回
LOG_WARN << "id: " << pResultSet->GetInt("id") << ", user_name: " << pResultSet->GetString("user_name") << " 已经存在";
delete pResultSet;
ret = 2;
}
else
{ // 如果不存在则注册
time_t now;
char create_time[TIME_STRING_LEN];
// 获取当前时间
now = time(NULL);
strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S", localtime(&now));
strSql = "insert into user_info (`user_name`,`nick_name`,`password`,`phone`,`email`,`create_time`) values(?,?,?,?,?,?)";
LOG_INFO << "执行: " << strSql;
// 必须在释放连接前delete CPrepareStatement对象,否则有可能多个线程操作mysql对象,会crash
CPrepareStatement *stmt = new CPrepareStatement();
if (stmt->Init(pDBConn->GetMysql(), strSql))
{
uint32_t index = 0;
string c_time = create_time;
stmt->SetParam(index++, user_name);
stmt->SetParam(index++, nick_name);
stmt->SetParam(index++, pwd);
stmt->SetParam(index++, phone);
stmt->SetParam(index++, email);
stmt->SetParam(index++, c_time);
bool bRet = stmt->ExecuteUpdate();
if (bRet)
{
ret = 0;
user_id = pDBConn->GetInsertId();
LOG_INFO << "insert user " << user_id;
}
else
{
LOG_ERROR << "insert user_info failed. " << strSql;
ret = 1;
}
}
delete stmt;
}
return ret;
}
总结:ApiRegisterUser接口 拿到http请求传来的数据 通过decodeRegisterJson接口进行json解析,从而得到用户的信息调用encodeRegisterJson接口 封装注册用户的信息,然后调用registerUser接口把用户信息注册到数据库中
3.登录
①用户登录的入口函数
* @brief 用户登录主流程
*
* @param url 请求url
* @param post_data 请求体(json字符串)
* @param str_json 返回的json字符串
* @returns 成功: 0,失败:-1
*/
int ApiUserLogin(string &url, string &post_data, string &str_json)
{
UNUSED(url);
string user_name;
string pwd;
string token;
// 判断数据是否为空
if (post_data.empty())
{
return -1;
}
// 解析json
if (decodeLoginJson(post_data, user_name, pwd) < 0)
{
LOG_ERROR << "decodeRegisterJson failed";
encodeLoginJson(1, token, str_json);
return -1;
}
// 验证账号和密码是否匹配
if (verifyUserPassword(user_name, pwd) < 0)
{
LOG_ERROR << "verifyUserPassword failed";
encodeLoginJson(1, token, str_json);
return -1;
}
// 生成token
if (setToken(user_name, token) < 0)
{
LOG_ERROR << "setToken failed";
encodeLoginJson(1, token, str_json);
return -1;
}
// 返回登录成功结果
encodeLoginJson(0, token, str_json);
return 0;
}
②解析json数据
// 解析登录信息,将json字符串解析为用户名和密码
int decodeLoginJson(const std::string &str_json, string &user_name, string &pwd)
{
bool res;
Json::Value root;
Json::Reader jsonReader;
res = jsonReader.parse(str_json, root);
if (!res)
{
LOG_ERROR << "parse reg json failed ";
return -1;
}
// 用户名
if (root["user"].isNull())
{
LOG_ERROR << "user null\n";
return -1;
}
user_name = root["user"].asString();
// 密码
if (root["pwd"].isNull())
{
LOG_ERROR << "pwd null\n";
return -1;
}
pwd = root["pwd"].asString();
return 0;
}
③封装登录的用户信息
// 封装登录结果的json,将登录结果和token写入json字符串
int encodeLoginJson(int ret, string &token, string &str_json)
{
Json::Value root;
root["code"] = ret;
if (ret == 0)
{
root["token"] = token; // 正常返回的时候才写入token
}
Json::FastWriter writer;
str_json = writer.write(root);
return 0;
}
④在数据库中验证用户是否存在
int verifyUserPassword(string &user_name, string &pwd)
{
int ret = 0;
CDBManager *pDBManager = CDBManager::getInstance();
CDBConn *pDBConn = pDBManager->GetDBConn("tuchuang_slave");
AUTO_REAL_DBCONN(pDBManager, pDBConn); // 自动归还数据库连接
// 查询用户密码
string strSql = formatString1("select password from user_info where user_name='%s'", user_name.c_str());
CResultSet *pResultSet = pDBConn->ExecuteQuery(strSql.c_str());
uint32_t nowtime = time(NULL);
if (pResultSet && pResultSet->Next())
{
// 用户存在,校验密码
string password = pResultSet->GetString("password");
LOG_INFO << "mysql-pwd: " << password << ", user-pwd: " << pwd;
if (pResultSet->GetString("password") == pwd)
ret = 0;
else
ret = -1;
}
else
{ // 用户不存在
ret = -1;
}
delete pResultSet;
return ret;
}
⑤生成token
int setToken(string &user_name, string &token)
{
int ret = 0;
CacheManager *pCacheManager = CacheManager::getInstance();
// 获取Redis连接
CacheConn *pCacheConn = pCacheManager->GetCacheConn("token");
// 通过RAII自动归还连接
AUTO_REAL_CACHECONN(pCacheManager, pCacheConn);
token = RandomString(32); // 生成32位随机token
if (pCacheConn)
{
// 用户名:token, 86400秒有效(24小时)
pCacheConn->setex(user_name, 86400, token);
}
else
{
ret = -1;
}
return ret;
}
总结: ApiUserLogin接口 拿到http请求传来的数据 通过decodeLoginJson接口进行json解析,从而得到用户的信息调用encodeLoginJson接口 封装登录用户的信息,然后调用verifyUserPassword接口把用户信息与数据库中的数据进行校验中 setToken接口 给用户返回一个token
token 的作用
客户端在后续的请求中(例如,请求受保护的资源、执行敏感操作等),需要在请求头或请求体中携带这个 token。
服务器收到请求后,会验证 token 的有效性(例如,检查 token 是否存在、是否过期、是否被篡改等)。如果 token 有效,服务器就认为该用户已经登录,并允许其访问