复现已有算法
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user