复现已有算法

This commit is contained in:
cloud
2026-07-14 15:43:18 +08:00
parent abebd2a683
commit 50b8111fd9
860 changed files with 182250 additions and 18 deletions
@@ -0,0 +1,68 @@
cmake_minimum_required(VERSION 3.5)
project(redis_chat CXX)
include(CheckIncludeFileCXX)
check_include_file_cxx(any HAS_ANY)
check_include_file_cxx(string_view HAS_STRING_VIEW)
check_include_file_cxx(coroutine HAS_COROUTINE)
if (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE)
set(CMAKE_CXX_STANDARD 20)
elseif (HAS_ANY AND HAS_STRING_VIEW)
set(CMAKE_CXX_STANDARD 17)
else ()
set(CMAKE_CXX_STANDARD 14)
endif ()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(${PROJECT_NAME} main.cc)
# ##############################################################################
# If you include the drogon source code locally in your project, use this method
# to add drogon
# add_subdirectory(drogon)
# target_link_libraries(${PROJECT_NAME} PRIVATE drogon)
#
# and comment out the following lines
find_package(Drogon CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
# ##############################################################################
if (CMAKE_CXX_STANDARD LESS 17)
# With C++14, use boost to support any and std::string_view
message(STATUS "use c++14")
find_package(Boost 1.61.0 REQUIRED)
target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIRS})
elseif (CMAKE_CXX_STANDARD LESS 20)
message(STATUS "use c++17")
else ()
message(STATUS "use c++20")
endif ()
aux_source_directory(controllers CTL_SRC)
aux_source_directory(filters FILTER_SRC)
aux_source_directory(plugins PLUGIN_SRC)
aux_source_directory(models MODEL_SRC)
drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
${CMAKE_CURRENT_BINARY_DIR})
# use the following line to create views with namespaces.
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
# ${CMAKE_CURRENT_BINARY_DIR} TRUE)
target_include_directories(${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/models)
target_sources(${PROJECT_NAME}
PRIVATE
${SRC_DIR}
${CTL_SRC}
${FILTER_SRC}
${PLUGIN_SRC}
${MODEL_SRC})
# ##############################################################################
# uncomment the following line for dynamically loading views
# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON)
+25
View File
@@ -0,0 +1,25 @@
# Redis example
A simple redis chat server
## Usage
First you need redis running on the port 6379.
### Connect with username
ws://localhost:8080/chat?name=<your-name>
### Enter room
Send message `ENTER <roomNo>` to enter a room.
room number should be 0 - 99.
### Quit room
Send message `QUIT` to quit room.
### Send message
Send whatever else will be a message.
@@ -0,0 +1,258 @@
#include "Chat.h"
#include <memory>
#include <unordered_set>
static void redisLogin(std::function<void(int)> &&callback,
const std::string &loginKey,
unsigned int timeout);
static void redisLogout(std::function<void(int)> &&callback,
const std::string &loginKey);
struct ClientContext
{
std::string name_;
std::string loginKey_;
std::string room_;
std::shared_ptr<nosql::RedisSubscriber> subscriber_;
};
static bool checkRoomNumber(const std::string &room)
{
if (room.empty() || room.size() > 2 || (room.size() == 2 && room[0] == '0'))
{
return false;
}
for (char c : room)
{
if (c < '0' || c > '9')
{
return false;
}
}
return true;
}
void Chat::handleNewMessage(const WebSocketConnectionPtr &wsConnPtr,
std::string &&message,
const WebSocketMessageType &type)
{
if (type == WebSocketMessageType::Close ||
type == WebSocketMessageType::Ping)
{
return;
}
if (type != WebSocketMessageType::Text &&
type != WebSocketMessageType::Pong)
{
LOG_ERROR << "Unsupported message type " << (int)type;
return;
}
LOG_DEBUG << "WsClient new message from "
<< wsConnPtr->peerAddr().toIpPort();
auto context = wsConnPtr->getContext<ClientContext>();
if (!context || context->name_.empty())
{
wsConnPtr->send("ERROR: You are not logged in.");
wsConnPtr->forceClose();
return;
}
auto redisClient = drogon::app().getRedisClient();
if (type == WebSocketMessageType::Pong)
{
redisClient->execCommandAsync(
[wsConnPtr](const nosql::RedisResult &) {
// Do nothing
},
[wsConnPtr](const nosql::RedisException &ex) {
LOG_ERROR << "Update user status failed: " << ex.what();
wsConnPtr->send("ERROR: Service unavailable.");
wsConnPtr->forceClose();
},
"SET %s 1 EX 120",
context->loginKey_.c_str());
return;
}
int operation = 0;
std::string room;
if (message.compare(0, 6, "ENTER ") == 0)
{
room = message.substr(6, message.find_last_not_of(" \n") - 5);
if (!checkRoomNumber(room))
{
wsConnPtr->send("ERROR: Invalid room number, should be [0-99].");
return;
}
operation = 1;
}
else if (message == "QUIT")
{
operation = 2;
}
switch (operation)
{
case 0: // Message
{
if (context->room_.empty())
{
wsConnPtr->send(
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
"room first.");
return;
}
// Publish message
std::string msg =
"[" + context->room_ + "][" + context->name_ + "] " + message;
// NOTICE: Dangerous to concat username into redis command!!!
// Do not use in production.
redisClient->execCommandAsync(
[](const nosql::RedisResult &) {},
[wsConnPtr](const nosql::RedisException &ex) {
wsConnPtr->send(std::string("ERROR: ") + ex.what());
},
"publish %s %s",
context->room_.c_str(),
msg.c_str());
break;
}
case 1: // Enter room
{
if (context->room_ == room)
{
wsConnPtr->send("ERROR: Already in room " + context->room_);
return;
}
if (!context->room_.empty())
{
context->subscriber_->unsubscribe(context->room_);
}
wsConnPtr->send("INFO: Enter room " + room);
context->subscriber_->subscribe(
room, [wsConnPtr](const std::string &, const std::string &msg) {
wsConnPtr->send(msg);
});
context->room_ = room;
break;
}
case 2: // Quit room
{
if (!context->room_.empty())
{
context->subscriber_->unsubscribe(context->room_);
wsConnPtr->send("INFO: Quit room " + context->room_);
context->room_.clear();
}
else
{
wsConnPtr->send(
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
"room first.");
}
break;
}
default:
break;
}
}
void Chat::handleNewConnection(const HttpRequestPtr &req,
const WebSocketConnectionPtr &wsConnPtr)
{
LOG_DEBUG << "WsClient new connection from "
<< wsConnPtr->peerAddr().toIpPort();
const std::string name = req->getParameter("name");
if (name.empty())
{
wsConnPtr->send("Please give your name in parameters.");
wsConnPtr->forceClose();
}
std::string loginKey = "redis_chat:user:" + drogon::utils::getMd5(name);
redisLogin(
[wsConnPtr, name, loginKey](int status) {
if (status < 0)
{
wsConnPtr->send("ERROR: Service unavailable.");
wsConnPtr->shutdown();
return;
}
if (status == 0)
{
wsConnPtr->send("ERROR: User [" + name +
"] already logged in.");
wsConnPtr->shutdown();
return;
}
std::shared_ptr<ClientContext> context =
std::make_shared<ClientContext>();
context->subscriber_ =
drogon::app().getRedisClient()->newSubscriber();
context->name_ = name;
context->loginKey_ = loginKey;
wsConnPtr->setContext(context);
wsConnPtr->send("Hello, " + name + "!");
},
loginKey,
120);
}
void Chat::handleConnectionClosed(const WebSocketConnectionPtr &wsConnPtr)
{
LOG_DEBUG << "WsClient close connection from "
<< wsConnPtr->peerAddr().toIpPort();
auto context = wsConnPtr->getContext<ClientContext>();
// Channels will be auto unsubscribed when subscriber destructed.
// for (auto& channel : context->channels_)
// {
// context->subscriber_->unsubscribe(channel);
// }
if (context)
{
redisLogout([](int) {}, context->loginKey_);
}
}
static void redisLogin(std::function<void(int)> &&callback,
const std::string &loginKey,
unsigned int timeout)
{
static const char script[] = R"(
local exists = redis.call('GET', KEYS[1]);
if exists then return 0 end;
redis.call('SET', KEYS[1], 1, 'EX', ARGV[1]);
return 1;
)";
drogon::app().getRedisClient()->execCommandAsync(
[callback](const nosql::RedisResult &result) {
callback((int)result.asInteger());
},
[callback](const nosql::RedisException &ex) {
LOG_ERROR << "Login error: " << ex.what();
callback(-1);
},
"EVAL %s 1 %s %u",
script,
loginKey.c_str(),
timeout);
}
static void redisLogout(std::function<void(int)> &&callback,
const std::string &loginKey)
{
drogon::app().getRedisClient()->execCommandAsync(
[callback](const nosql::RedisResult &result) {
callback((int)result.asInteger());
},
[callback](const nosql::RedisException &ex) {
LOG_ERROR << "Logout error: " << ex.what();
callback(-1);
},
"DEL %s",
loginKey.c_str());
}
@@ -0,0 +1,19 @@
#pragma once
#include <drogon/WebSocketController.h>
using namespace drogon;
class Chat : public drogon::WebSocketController<Chat>
{
public:
void handleNewMessage(const WebSocketConnectionPtr &,
std::string &&,
const WebSocketMessageType &) override;
void handleNewConnection(const HttpRequestPtr &,
const WebSocketConnectionPtr &) override;
void handleConnectionClosed(const WebSocketConnectionPtr &) override;
WS_PATH_LIST_BEGIN
WS_PATH_ADD("/chat");
WS_PATH_LIST_END
};
+9
View File
@@ -0,0 +1,9 @@
#include <drogon/drogon.h>
int main()
{
drogon::app().addListener("0.0.0.0", 8080);
drogon::app().createRedisClient("127.0.0.1", 6379);
drogon::app().run();
return 0;
}