My图床项目

发布于:2025-06-07 ⋅ 阅读:(14) ⋅ 点赞:(0)

引言: 

在海量文件存储中尤其是小文件我们通常会用上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 connfd
connectionChannel

Poller和EPollPoller - Demultiplex

std::unordered_map<int, Channel*> channels 

③EventLoop - Reactor 

ChannelList activeChannels_;
std::unique_ptr poller_;
int wakeupFd ; -> loop
std::unique_ptr wakeupChannel ;

 ④Thread和EventLoopThread 

⑤EventLoopThreadPool 

getNextLoop() : 通过轮询算法获取下一个subloop baseLoop
一个thread对应一个loop => one loop per thread

⑥Socket 

⑦Acceptor 

主要封装了listenfd相关的操作 socket bind listen baseLoop 

⑧Buffer 

缓冲区 应用写数据 -> 缓冲区 -> Tcp发送缓冲区 -> send
prependable readeridx writeridx

⑨TcpConnection 

一个连接成功的客户端对应一个TcpConnection Socket Channel 各种回调 发送和接收缓冲

⑩TcpServer 

Acceptor EventLoopThreadPool
ConnectionMap 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, 存储数据)
  1. 客户端准备数据:应用程序在客户端生成需要发送的数据。
  2. 序列化为 JSON:客户端将这些数据转换(序列化)成 JSON 格式的字符串。
  3. 发送 HTTP 请求:客户端通过 HTTP 协议向服务器发送请求,JSON 字符串通常放在请求体 (request body) 中。
  4. 服务器接收请求:服务器接收到 HTTP 请求。
  5. 解析 JSON:服务器从请求体中提取 JSON 字符串,并将其转换(反序列化/解析)为服务器端编程语言可以处理的数据结构(如对象、字典等)。
  6. 处理业务逻辑:服务器根据解析后的数据执行相应的业务操作。
  7. 服务器准备响应数据:服务器生成要返回给客户端的数据。
  8. 序列化为 JSON:服务器将响应数据序列化为 JSON 格式的字符串。
  9. 发送 HTTP 响应:服务器通过 HTTP 协议将包含 JSON 数据的响应发送回客户端。
  10. 客户端接收响应:客户端接收到服务器的 HTTP 响应。
  11. 解析 JSON:客户端解析响应体中的 JSON 字符串,将其转换为客户端编程语言可以处理的数据结构。
  12. 客户端使用数据:客户端使用这些解析后的数据进行后续操作,如更新用户界面。
 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 有效,服务器就认为该用户已经登录,并允许其访问


网站公告

今日签到

点亮在社区的每一天
去签到