增加websocket支持

This commit is contained in:
Dev User
2026-08-04 20:07:32 +08:00
parent 176eb29ef9
commit db95627dd8
19 changed files with 803 additions and 161 deletions

49
.gitignore vendored
View File

@@ -11,3 +11,52 @@
/log
/3rdparty/HP-Socket/lib
.fastweb_runtime
module/.install/1/fastweb.lua
module/.install/1/fastweb/auto_lock.lua
module/.install/1/fastweb/codec.lua
module/.install/1/fastweb/ini.lua
module/.install/1/fastweb/interceptor.lua
module/.install/1/fastweb/mutex.lua
module/.install/1/fastweb/process.lua
module/.install/1/fastweb/queue.lua
module/.install/1/fastweb/request.lua
module/.install/1/fastweb/response.lua
module/.install/1/fastweb/session.lua
module/.install/1/fastweb/subscribe.lua
module/.install/1/fastweb/timer.lua
module/.install/1/fastweb/websocket.lua
module/.install/9/base64.lua
module/.install/9/cache.lua
module/.install/9/httpclient.lua
module/.install/9/utils.lua
module/.install/9/aliyun/ai.lua
module/.install/9/aliyun/email.lua
module/.install/9/aliyun/oss.lua
module/.install/9/deepseek/ai.lua
module/.install/9/fwutils/config.lua
module/.install/9/fwutils/funs.lua
module/.install/9/fwutils/init.lua
module/.install/9/fwutils/request_config.lua
module/.install/9/fwutils/stopwatch.lua
module/.install/9/fwutils/token.lua
module/.install/9/fwutils/webapi.lua
module/.install/9/fwutils/develop/controll/menu.lua
module/.install/9/fwutils/develop/controll/role_permissions.lua
module/.install/9/fwutils/develop/controll/role.lua
module/.install/9/fwutils/develop/controll/template.lua
module/.install/9/fwutils/develop/function/menu.lua
module/.install/9/fwutils/develop/function/role_permissions.lua
module/.install/9/fwutils/develop/function/role.lua
module/.install/9/fwutils/develop/function/template.lua
module/.install/9/fwutils/fwutils/controll/upload.lua
module/.install/9/fwutils/fwutils/function/files.lua
module/.install/9/fwutils/fwutils/function/upload.lua
module/.install/9/submail/sms.lua
module/.install/9/tencent/board.lua
module/.install/9/tencent/cos.lua
module/.install/9/tencent/qywx.lua
module/.install/9/tencent/wxofficial.lua
module/.install/9/tencent/wxpay.lua
module/.install/9/tencent/board/generate_user_sig.lua
module/.install/9/unisound/unisound.lua

77
.vscode/launch.json vendored
View File

@@ -1,40 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch", // 配置名称,可以根据需要修改
"type": "cppdbg", // 调试器类型
"request": "launch", // 请求类型:启动调试
"program": "${workspaceFolder}/build/fastweb", // 指定要调试的可执行程序路径
"args": [
"start",
"/home/nianhua/www/config.ini"
], // 程序运行时的命令行参数
"stopAtEntry": false, // 是否在程序入口停止
"cwd": "/home/nianhua/www", // 工作目录
"environment": [], // 运行时的环境变量
"externalConsole": false, // 是否使用外部终端
"MIMode": "gdb", // 使用GDB作为调试器
"miDebuggerPath": "/usr/bin/gdb", // GDB的路径
"preLaunchTask": "build", // 调试前执行的任务
"logging": {
"engineLogging": true // 启用引擎日志记录
},
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
}
{
"name": "fastweb: start",
"type": "gdb",
"request": "launch",
"target": "/root/fastweb/build/fastweb",
"arguments": "start config.ini",
"cwd": "/root/fastweb",
"gdbpath": "/usr/bin/gdb",
"valuesFormatting": "parseText",
"printCalls": false,
"stopAtEntry": false,
"autorun": [
"set pagination off",
"set print thread-events off",
"set non-stop off",
"handle SIGPIPE nostop noprint pass",
"handle SIG33 nostop noprint pass",
"handle SIG34 nostop noprint pass",
"handle SIG60 nostop noprint pass",
"handle SIGUSR1 nostop noprint pass",
"handle SIGUSR2 nostop noprint pass",
"handle SIGABRT stop print nopass",
"break abort",
"break __assert_fail"
]
},
{
"name": "fastweb: start (CodeLLDB)",
"type": "lldb",
"request": "launch",
"program": "/usr/local/bin/fastweb",
"args": ["start", "config.ini"],
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"initCommands": [
"settings set target.process.thread.step-out-avoid-nodebug false"
],
"breakpointCommands": []
}
]
}
}

View File

@@ -104,7 +104,7 @@ else()
link_directories(/usr/local/lib)
target_link_libraries(${FASTWEBCORE}
hpsocket
ylib
ylib_d
leveldb
crypto
/opt/lua54/lib/liblua.a
@@ -145,7 +145,7 @@ else()
target_link_libraries(${PROJECT_NAME}
${FASTWEBCORE}
hpsocket
ylib
ylib_d
pthread
mysqlcppconn
ssl

View File

@@ -25,6 +25,8 @@ domain=local.newobj.org,0.0.0.0
direct_url_mapping=1
; 最大上传大小限制(MB),0为不限制
max_upload_size=10
; 是否启用WebSocket
websocket_enable=1
[log]
; 开启或关闭

View File

@@ -62,6 +62,7 @@ bool fastweb::app::start(const std::string& config_filepath)
network::http::start_config config;
network::http::website_config ws_config;
config.max_upload_size = this->config->website.max_upload_size;
config.websocket_enable = this->config->website.websocket_enable;
for_iter(iter, this->config->domain)
{
network::http::host_config host_config;

View File

@@ -80,7 +80,7 @@ void fastweb::config::cache()
website.domain = strutils::split(m_ini.read("website", "domain"),',');
website.direct_url_mapping = m_ini.read("website", "direct_url_mapping") == "1";
website.max_upload_size = ylib::stoll(m_ini.read("website", "max_upload_size"));
website.websocket_enable = m_ini.read("website", "websocket_enable") == "1";
log.enable = m_ini.read("log", "enable") == "1";
log.dir = PATH_EX(m_ini.read("log", "dir"));
log.name = m_ini.read("log", "name");

View File

@@ -32,6 +32,7 @@ namespace fastweb
std::vector<std::string> domain;
bool direct_url_mapping = false;
uint64 max_upload_size = 0;
bool websocket_enable = false;
};
struct log {

View File

@@ -29,6 +29,7 @@ If you have any questions, please contact us: 1585346868@qq.com Or visit our web
#include "module/http/request.h"
#include "module/http/response.h"
#include "module/http/websocket.h"
#include "module/http/httpclient.h"
#include "module/http/interceptor.h"
#include "module/http/subscribe.h"
@@ -194,6 +195,7 @@ void fastweb::module_manager::load_core(sol::state* lua)
module::request::regist(lua);
module::response::regist(lua);
module::websocket::regist(lua);
module::interceptor::regist(lua);
module::subscribe::regist(lua);
module::httpclient::regist(lua);

View File

@@ -21,6 +21,7 @@ If you have any questions, please contact us: 1585346868@qq.com Or visit our web
#include "core/statemanager.h"
#include "module/http/request.h"
#include "module/http/response.h"
#include "module/http/websocket.h"
#include "net/http_subscribe.h"
fastweb::subscribe_manager::subscribe_manager(fastweb::app* app) :Interface(app)
{
@@ -32,14 +33,24 @@ fastweb::subscribe_manager::~subscribe_manager()
void fastweb::subscribe_manager::start()
{
app()->router->other([&](network::http::request* request, network::http::response* response) {
this->other(request, response);
app()->router->other([&](network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message) {
this->other(request, response, ws_message);
});
app()->router->on_close([&](uint64 connid,network::http::websocket_message* ws_message) {
// CLOSE 无 request/responsecallback 内异常不可再抛出(上层无 catch会 terminate
try {
ws_message->connid = connid;
this->callback(nullptr, nullptr, ws_message, "", ws_message->filepath);
}
catch (const std::exception& e) {
LOG_ERROR(std::string("[subscribe][ws_close]: ") + e.what());
}
});
}
bool fastweb::subscribe_manager::add(const std::string& pattern, const std::string& filepath)
{
app()->router->subscribe()->add(pattern, filepath, [&](network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath)->bool {
return this->callback(request, response, pattern, filepath);
app()->router->subscribe()->add(pattern, filepath, [&](network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath)->bool {
return this->callback(request, response, ws_message, pattern, filepath);
});
return true;
}
@@ -57,7 +68,7 @@ void fastweb::subscribe_manager::clear()
app()->router->subscribe()->clear();
}
bool fastweb::subscribe_manager::callback(network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath)
bool fastweb::subscribe_manager::callback(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath)
{
bool ok_continue = false;
auto lua = app()->state->get();
@@ -71,9 +82,10 @@ bool fastweb::subscribe_manager::callback(network::http::request* request, netwo
}
module::request m_request(request);
module::response m_response(response);
module::websocket m_websocket(ws_message);
(*lua->state)["fw_response"] = m_response;
(*lua->state)["fw_request"] = m_request;
(*lua->state)["fw_websocket"] = m_websocket;
sol::protected_function_result result = script();
if (!result.valid()) {
sol::error err = result;
@@ -84,19 +96,25 @@ bool fastweb::subscribe_manager::callback(network::http::request* request, netwo
{
exception_string = e.what();
if (app()->config->website.debug)
LOG_ERROR("[subscribe][" + request->filepath() + "]: " + e.what());
{
std::string where = filepath;
if (where.empty() && request != nullptr)
where = request->filepath();
LOG_ERROR("[subscribe][" + where + "]: " + e.what());
}
}
// 释放lua资源
lua->state->collect_garbage();
app()->state->push(lua);
if (exception_string.empty() == false)
// CLOSErequest/response 为空)只记日志,不再上抛,避免线程里未捕获异常 abort
if (exception_string.empty() == false && request != nullptr)
throw ylib::exception(exception_string);
return ok_continue;
}
void fastweb::subscribe_manager::other(network::http::request* request, network::http::response* response)
void fastweb::subscribe_manager::other(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message)
{
std::string filepath = request->filepath();
if (filepath.empty())
@@ -129,7 +147,7 @@ void fastweb::subscribe_manager::other(network::http::request* request, network:
}
else if(ext == "lua" && app()->config->website.direct_url_mapping)
{
callback(request, response, filepath, filepath);
callback(request, response, ws_message, filepath, filepath);
return;
}
response->send_file(absolute_path);

View File

@@ -31,13 +31,13 @@ namespace fastweb
/// <param name="response"></param>
/// <param name="pattern"></param>
/// <param name="filepath"></param>
bool callback(network::http::request* request, network::http::response* response, const std::string& pattern, const std::string& filepath);
bool callback(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message, const std::string& pattern, const std::string& filepath);
/// <summary>
/// 其它
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
void other(network::http::request* request, network::http::response* response);
void other(network::http::request* request, network::http::response* response,network::http::websocket_message* ws_message);
private:
/// <summary>
/// 未找到页面

View File

@@ -33,6 +33,8 @@ module::request::~request()
}
sol::table module::request::body_param(sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
sol::state_view lua(s);
sol::table result_table = lua.create_table();
auto &map = m_request->body_param();
@@ -42,6 +44,8 @@ sol::table module::request::body_param(sol::this_state s)
}
sol::table module::request::url_param(sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
sol::state_view lua(s);
sol::table result_table = lua.create_table();
auto& map = m_request->url_param();
@@ -51,10 +55,14 @@ sol::table module::request::url_param(sol::this_state s)
}
std::string module::request::body()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->body();
}
sol::table module::request::multipart(sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
auto multipart = m_request->multipart();
sol::state_view lua(s);
sol::table result_table = lua.create_table();
@@ -80,12 +88,16 @@ sol::table module::request::multipart(sol::this_state s)
}
std::string module::request::multipart_content(int id)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
multipart_content_check(id);
auto mult_info = m_request->multipart()->at(id - 1);
return std::string(m_request->body().data() + mult_info.offset, mult_info.length);
}
bool module::request::multipart_content_save(int id, const std::string& filepath)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
ylib::file_io file;
if (file.open(filepath) == false)
{
@@ -126,14 +138,20 @@ bool module::request::multipart_content_save(int id, const std::string& filepath
//}
void module::request::set(const std::string& name, const std::string& value)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
m_request->reqpack()->extra()[name] = value;
}
std::string module::request::get(const std::string& name)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->reqpack()->extra()[name].to<std::string>();
}
sol::table module::request::gets(sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
sol::state_view lua(s);
sol::table result_table = lua.create_table();
@@ -146,10 +164,14 @@ sol::table module::request::gets(sol::this_state s)
}
network::http::website* module::request::website()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->website();
}
bool module::request::save_body(const std::string& filepath)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return ylib::file::write(filepath, m_request->body());
}
void module::request::regist(sol::state* lua)
@@ -179,6 +201,8 @@ void module::request::regist(sol::state* lua)
void module::request::multipart_content_check(int id)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
if (m_request->multipart()->size() < id)
{
throw ylib::exception("The maximum ID is " + std::to_string(m_request->multipart()->size()) + ", and the incoming ID is " + std::to_string(id));
@@ -191,12 +215,16 @@ void module::request::multipart_content_check(int id)
}
std::string module::request::header(const std::string& name)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
std::string value;
m_request->header(name, value);
return value;
}
sol::table module::request::headers(sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
sol::state_view lua(s);
sol::table result_table = lua.create_table();
@@ -207,18 +235,26 @@ sol::table module::request::headers(sol::this_state s)
}
std::string module::request::method()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->method();
}
std::string module::request::filepath()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->filepath();
}
std::string module::request::host()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->host();
}
sol::object module::request::param(const std::string& name, bool throw_,sol::this_state s)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
std::string value;
bool result = request_param(name, value);
if (result == false){
@@ -232,14 +268,20 @@ sol::object module::request::param(const std::string& name, bool throw_,sol::thi
std::string module::request::remote_ipaddress()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->remote().address;
}
ushort module::request::remote_port()
{
if (m_request == nullptr)
throw ylib::exception("request is null");
return m_request->remote().port;
}
bool module::request::request_param(const std::string& name, std::string& value)
{
if (m_request == nullptr)
throw ylib::exception("request is null");
if (m_request->get_url_param(name, value) == false)
return m_request->get_body_param(name, value);
return true;

View File

@@ -115,9 +115,6 @@ namespace module
/// </summary>
/// <returns></returns>
bool save_body(const std::string& filepath);
/// <summary>
/// 推送到自动释放队列
/// </summary>

View File

@@ -61,11 +61,23 @@ void module::response::sets(const sol::table& lua_table)
m_sets = parse_lua_table(lua_table, parse_lua_table);
}
void module::response::response_done()
{
if (m_response == nullptr)
throw ylib::exception("response is null");
m_response->response_done();
}
bool module::response::is_response_done()
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->is_response_done();
}
void module::response::regist(sol::state* lua)
{
// 绑定 Request 类到 Lua
lua->new_usertype<module::response>("fw_response",
"send_header", &module::response::send_header,
"send_data", &module::response::send_data,
"send", &module::response::send,
"sendex", &module::response::sendex,
@@ -74,23 +86,46 @@ void module::response::regist(sol::state* lua)
"redirect", &module::response::redirect,
"forward", &module::response::forward,
"set", &module::response::set,
"sets", &module::response::sets
"sets", &module::response::sets,
"response_done", &module::response::response_done,
"is_response_done", &module::response::is_response_done,
"send_ws", &module::response::send_ws
);
}
bool module::response::send_ws(const std::string_view& data, int opcode)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->send_ws(data.data(), data.size(), opcode);
}
bool module::response::send_header(ushort stateNum, const std::string& stateDesc)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->send_header(stateNum, stateDesc);
}
bool module::response::send_data(const char* buf, size_t buf_len, ushort stateNum, const std::string& stateDesc)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->send(buf, buf_len, stateNum, stateDesc);
}
bool module::response::send(const std::string& value)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->send(value);
}
bool module::response::sendex(const std::string& value, ushort stateNum, const std::string& stateDesc)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->send(value, stateNum, stateDesc);
}
bool module::response::send_file(const std::string& filepath, int32 downbaud, ushort stateNum, const std::string& stateDesc)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
std::string end_filepath = system::temp_path() + "/" + codec::md5(filepath) + ".tmp";
if (m_sets.size() != 0 && ylib::file::size(filepath) < 1024 * 1024)
@@ -106,13 +141,19 @@ bool module::response::send_file(const std::string& filepath, int32 downbaud, us
}
bool module::response::redirect(const std::string& filepath, bool MovedPermanently)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->redirect(filepath, MovedPermanently);
}
bool module::response::forward(const std::string& filepath)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
return m_response->forward(filepath);
}
void module::response::header(const std::string& name, const std::string& value)
{
if (m_response == nullptr)
throw ylib::exception("response is null");
m_response->headers()->emplace(name, value);
}

View File

@@ -11,6 +11,8 @@ namespace module
response(network::http::response* response);
~response();
bool send_ws(const std::string_view& data, int opcode = 0x1);
bool send_header(ushort stateNum, const std::string& stateDesc);
bool send_data(const char* buf, size_t buf_len, ushort stateNum, const std::string& stateDesc);
bool send(const std::string& value);
bool sendex(const std::string& value, ushort stateNum, const std::string& stateDesc);
@@ -21,6 +23,11 @@ namespace module
void set(const std::string& name,const std::string& value);
void sets(const sol::table& lua_table);
void response_done();
bool is_response_done();
static void regist(sol::state* lua);
private:
network::http::response* m_response = nullptr;

View File

@@ -0,0 +1,96 @@
/*Software License
Copyright(C) 2024[liuyingjie]
License Terms
Usage Rights
Any individual or entity is free to use, copy, and distribute the binary form of this software without modification to the source code, without the need to disclose the source code.
If the source code is modified, the modifications must be open - sourced under the same license.This means that the modifications must be disclosed and accompanied by a copy of this license.
Future Versions Updates
From this version onwards, all future releases will be governed by the terms of the latest version of the license.This license will automatically be nullified and replaced by the new version.
Users must comply with the terms of the new license issued in future releases.
Liability and Disclaimer
This software is provided “as is”, without any express or implied warranties, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non - infringement.In no event shall the author or copyright holder be liable for any claims, damages, or other liabilities, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
Contact Information
If you have any questions, please contact us: 1585346868@qq.com Or visit our website fwlua.com.
*/
#include "websocket.h"
module::websocket::websocket(network::http::websocket_message* ws_message) :m_ws_message(ws_message)
{
}
module::websocket::~websocket()
{
}
bool module::websocket::valid()
{
if (m_ws_message == nullptr)
return false;
return true;
}
int module::websocket::type()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return (int)m_ws_message->type;
}
bool module::websocket::is_final()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return m_ws_message->header._final;
}
int module::websocket::opcode()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return m_ws_message->header.opcode;
}
uint64 module::websocket::length()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return m_ws_message->header.length;
}
sol::table module::websocket::mask(sol::this_state s)
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
sol::state_view lua(s);
sol::table result = lua.create_table(4, 0);
for (int i = 0; i < 4; i++)
result[i + 1] = (int)m_ws_message->header.mask[i];
return result;
}
std::string module::websocket::sec_websocket_key()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return m_ws_message->sec_websocket_key;
}
uint64 module::websocket::connid()
{
if (m_ws_message == nullptr)
throw ylib::exception("websocket message is null");
return m_ws_message->connid;
}
void module::websocket::regist(sol::state* lua)
{
lua->new_usertype<module::websocket>("fw_websocket",
"type", &module::websocket::type,
"final", &module::websocket::is_final,
"opcode", &module::websocket::opcode,
"length", &module::websocket::length,
"mask", &module::websocket::mask,
"valid", &module::websocket::valid,
"sec_websocket_key", &module::websocket::sec_websocket_key,
"connid", &module::websocket::connid
);
// ws_type 枚举,便于脚本判断
(*lua)["FW_WS_UPGRADE"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_UPGRADE;
(*lua)["FW_WS_MESSAGE_HEADER"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_MESSAGE_HEADER;
(*lua)["FW_WS_MESSAGE_BODY"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_MESSAGE_BODY;
(*lua)["FW_WS_CLOSE"] = (int)network::http::HTTP_SERVER_WEBSOCKET_TYPE_CLOSE;
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "net/http_request.h"
#include "net/http_response.h"
#include "sol/sol.hpp"
#include "core/define.h"
namespace module
{
class websocket
{
public:
websocket(network::http::websocket_message* ws_message);
~websocket();
/// <summary>
/// 是否有效
/// </summary>
bool valid();
/// <summary>
/// 消息类型 (ws_type)
/// </summary>
int type();
/// <summary>
/// 是否结束帧
/// </summary>
bool is_final();
/// <summary>
/// 帧操作码
/// </summary>
int opcode();
/// <summary>
/// 载荷长度
/// </summary>
uint64 length();
/// <summary>
/// 掩码 (4字节)
/// </summary>
sol::table mask(sol::this_state s);
/// <summary>
/// Sec-WebSocket-Key
/// </summary>
std::string sec_websocket_key();
/// <summary>
/// 取连接ID
/// </summary>
uint64 connid();
static void regist(sol::state* lua);
private:
network::http::websocket_message* m_ws_message = nullptr;
};
}

14
www/api/sha1.lua Normal file
View File

@@ -0,0 +1,14 @@
-- GET /api/sha1.lua 测试 WebSocket Accept 的 SHA1/Base64 算法
-- RFC6455 样例:
-- Key = dGhlIHNhbXBsZSBub25jZQ==
-- Accept = s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
--
-- 可选参数:?key=你的Sec-WebSocket-Key
local request = require("fastweb.request")
local response = require("fastweb.response")
local codec = require("fastweb.codec")
local base64 = require("base64")
local utils = require("utils")
local cjson = require("cjson")
print("headers:",cjson.encode(request.headers()))

77
www/api/ws.lua Normal file
View File

@@ -0,0 +1,77 @@
local request = require("fastweb.request")
local response = require("fastweb.response")
local ws = require("fastweb.websocket")
local codec = require("fastweb.codec")
local base64 = require("base64")
local utils = require("utils")
if not ws.valid() then
response.response_done()
return
end
if ws.type() == ws.UPGRADE then
print("[WS_UPGRADE] CONNID:" .. ws.connid())
local accept_key = base64.encode(utils.hex_to_bytes(codec.sha1(ws.sec_websocket_key() .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")))
response.header("Sec-WebSocket-Accept", accept_key)
response.header("Connection", "Upgrade")
response.header("Upgrade", "websocket")
response.send_header(101, "Switching Protocols")
elseif ws.type() == ws.MESSAGE_HEADER then
local opcode = ws.opcode()
local log = "[WS_HEADER] CONNID:" .. ws.connid()
if opcode == ws.OPCODE_TEXT then
log = log .. " [TEXT]"
elseif opcode == ws.OPCODE_BINARY then
log = log .. " [BINARY]"
elseif opcode == ws.OPCODE_CLOSE then
log = log .. " [CLOSE]"
elseif opcode == ws.OPCODE_PING then
log = log .. " [PING]"
elseif opcode == ws.OPCODE_PONG then
log = log .. " [PONG]"
else
log = log .. " [OPCODE:" .. opcode .. "]"
end
log = log .. " [FINAL:" .. tostring(ws.final()) .. "] [LENGTH:" .. ws.length() .. "]"
print(log)
elseif ws.type() == ws.MESSAGE_BODY then
local connid = ws.connid()
local opcode = ws.opcode()
local body = request.body()
local log = "[WS_BODY] CONNID:" .. connid
.. " [OPCODE:" .. tostring(opcode) .. "]"
.. " [LENGTH:" .. tostring(#body) .. "]"
print(log)
if opcode == ws.OPCODE_CLOSE then
-- 客户端关闭:回 Close 帧(可带回原载荷),完成握手
print("[WS_BODY] reply CLOSE")
response.send_ws(body, ws.OPCODE_CLOSE)
elseif opcode == ws.OPCODE_PING then
-- Ping 必须回 Pong载荷原样带回
print("[WS_BODY] reply PONG")
response.send_ws(body, ws.OPCODE_PONG)
elseif opcode == ws.OPCODE_PONG then
-- Pong 无需回复
print("[WS_BODY] PONG ignored")
elseif opcode == ws.OPCODE_TEXT or opcode == ws.OPCODE_BINARY then
-- 文本/二进制:原样回显
response.send_ws(body, opcode)
else
print("[WS_BODY] unhandled opcode:" .. tostring(opcode))
end
elseif ws.type() == ws.CLOSE then
-- TCP 已断开,只做清理,不要再发帧
print("[WS_CLOSE] CONNID:" .. ws.connid())
return
else
print("UNKNOWN")
end
response.response_done()

View File

@@ -1,113 +1,345 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
margin: 0;
}
.login-container {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
text-align: center;
width: 350px;
}
.login-container h1 {
margin-bottom: 20px;
font-size: 24px;
}
.login-container input[type="text"], .login-container input[type="password"] {
width: 100%;
padding-top: 15px;
padding-bottom: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.login-container button {
width: 100%;
padding: 15px;
background-color: #007BFF;
border: none;
border-radius: 5px;
color: white;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #0056b3;
}
.error-message {
color: red;
margin-top: 10px;
}
</style>
<script src="/js/jquery-3.4.1.min.js"></script>
<script>
var token = ""
function login() {
const username = $('#username').val();
const password = $('#password').val();
if (!username || !password) {
alert('请输入用户名和密码');
return;
}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket 测试</title>
<style>
:root {
--bg: #0f1419;
--surface: #1a2332;
--border: #2d3a4d;
--text: #e7ecf3;
--muted: #8b9bb4;
--accent: #3d9cf0;
--accent-hover: #5aadf5;
--ok: #3ecf8e;
--err: #f07178;
--warn: #e6c07b;
}
$.ajax({
url: '/api/user.lua?action=login',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ username: username, password: password }),
success: function(response) {
if (response.code === 200) {
token = response.data.token;
fetchUserInfo();
} else {
$('#errorMessage').text(response.msg || '登录失败,请重试');
}
}
});
}
* { box-sizing: border-box; margin: 0; padding: 0; }
function fetchUserInfo() {
$.ajax({
url: '/api/user.lua?action=getinfo',
type: 'GET',
headers:{
"token":token
},
success: function(response) {
if (response.code === 200) {
const userInfo = response.data;
alert(`用户信息:\n登录时间: ${userInfo.login_time}`);
} else {
alert('获取用户信息失败,请重试');
}
},
error: function() {
alert('获取用户信息失败,请重试');
}
});
}
</script>
body {
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%),
radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%),
var(--bg);
color: var(--text);
min-height: 100vh;
padding: 2rem 1.25rem 3rem;
line-height: 1.5;
}
main {
max-width: 720px;
margin: 0 auto;
}
h1 {
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
font-size: 1.35rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 0.35rem;
}
.sub {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 1.75rem;
}
.sub code {
font-family: "IBM Plex Mono", monospace;
font-size: 0.85em;
color: var(--accent);
}
.row {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
margin-bottom: 1rem;
}
.status {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.85rem;
color: var(--muted);
margin-left: auto;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
.dot.connecting { background: var(--warn); }
.dot.closed, .dot.error { background: var(--err); }
button, input {
font: inherit;
}
button {
background: var(--accent);
color: #0a1220;
border: none;
padding: 0.55rem 1.1rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: background 0.15s;
}
button:hover:not(:disabled) { background: var(--accent-hover); }
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
button.ghost {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
button.ghost:hover:not(:disabled) {
border-color: var(--muted);
background: var(--surface);
}
.url {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.65rem 0.85rem;
font-family: "IBM Plex Mono", monospace;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.url:focus {
outline: none;
border-color: var(--accent);
}
.send-row {
display: flex;
gap: 0.6rem;
margin-bottom: 1rem;
}
.send-row input {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.65rem 0.85rem;
font-size: 0.9rem;
}
.send-row input:focus {
outline: none;
border-color: var(--accent);
}
.log {
background: var(--surface);
border: 1px solid var(--border);
height: min(52vh, 420px);
overflow-y: auto;
padding: 0.75rem;
font-family: "IBM Plex Mono", monospace;
font-size: 0.8rem;
}
.log .line {
padding: 0.2rem 0;
border-bottom: 1px solid transparent;
word-break: break-all;
white-space: pre-wrap;
}
.log .line .t { color: var(--muted); margin-right: 0.5rem; }
.log .line.sys { color: var(--muted); }
.log .line.out { color: var(--accent); }
.log .line.in { color: var(--ok); }
.log .line.err { color: var(--err); }
</style>
</head>
<body>
<div class="login-container">
<h1>用户登录</h1>
<input type="text" id="username" placeholder="请输入用户名">
<input type="password" id="password" placeholder="请输入密码">
<button onclick="login()">确认</button>
<div id="errorMessage" class="error-message"></div>
<main>
<h1>WebSocket 测试</h1>
<p class="sub">连接到当前服务器 <code id="pathHint">/api/ws.lua</code></p>
<input class="url" id="url" type="text" spellcheck="false" />
<div class="row">
<button id="btnConnect" type="button">连接</button>
<button id="btnDisconnect" class="ghost" type="button" disabled>断开</button>
<button id="btnClear" class="ghost" type="button">清空日志</button>
<span class="status">
<span class="dot" id="dot"></span>
<span id="statusText">未连接</span>
</span>
</div>
<div class="send-row">
<input id="msg" type="text" placeholder="输入要发送的消息…" disabled />
<button id="btnSend" type="button" disabled>发送</button>
</div>
<div class="log" id="log" aria-live="polite"></div>
</main>
<script>
(function () {
var path = "/api/ws.lua";
var proto = location.protocol === "https:" ? "wss:" : "ws:";
var defaultUrl = proto + "//" + location.host + path;
var urlEl = document.getElementById("url");
var msgEl = document.getElementById("msg");
var logEl = document.getElementById("log");
var dotEl = document.getElementById("dot");
var statusEl = document.getElementById("statusText");
var btnConnect = document.getElementById("btnConnect");
var btnDisconnect = document.getElementById("btnDisconnect");
var btnSend = document.getElementById("btnSend");
var btnClear = document.getElementById("btnClear");
urlEl.value = defaultUrl;
var ws = null;
function now() {
var d = new Date();
return (
String(d.getHours()).padStart(2, "0") + ":" +
String(d.getMinutes()).padStart(2, "0") + ":" +
String(d.getSeconds()).padStart(2, "0") + "." +
String(d.getMilliseconds()).padStart(3, "0")
);
}
function log(kind, text) {
var line = document.createElement("div");
line.className = "line " + kind;
var t = document.createElement("span");
t.className = "t";
t.textContent = now();
line.appendChild(t);
line.appendChild(document.createTextNode(text));
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight;
}
function setState(state, label) {
dotEl.className = "dot " + state;
statusEl.textContent = label;
var open = state === "open";
var connecting = state === "connecting";
btnConnect.disabled = open || connecting;
btnDisconnect.disabled = !open && !connecting;
btnSend.disabled = !open;
msgEl.disabled = !open;
urlEl.disabled = open || connecting;
}
function connect() {
var url = urlEl.value.trim();
if (!url) return;
if (ws) {
try { ws.close(); } catch (e) {}
ws = null;
}
setState("connecting", "连接中…");
log("sys", "→ 连接 " + url);
try {
ws = new WebSocket(url);
} catch (e) {
setState("error", "错误");
log("err", "创建失败: " + e.message);
return;
}
ws.onopen = function () {
setState("open", "已连接");
log("sys", "✓ 连接已建立 (readyState=" + ws.readyState + ")");
msgEl.focus();
};
ws.onmessage = function (ev) {
var data = ev.data;
if (data instanceof Blob) {
data.arrayBuffer().then(function (buf) {
log("in", "← binary " + buf.byteLength + " bytes");
});
} else if (data instanceof ArrayBuffer) {
log("in", "← binary " + data.byteLength + " bytes");
} else {
log("in", "← " + data);
}
};
ws.onerror = function () {
log("err", "WebSocket error");
setState("error", "错误");
};
ws.onclose = function (ev) {
var reason = ev.reason ? " — " + ev.reason : "";
log("sys", "× 已关闭 (code=" + ev.code + reason + ", clean=" + ev.wasClean + ")");
setState("closed", "已断开");
ws = null;
};
}
function disconnect() {
if (!ws) return;
log("sys", "→ 主动关闭");
ws.close(1000, "client close");
}
function send() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
var text = msgEl.value;
ws.send(text);
log("out", "→ " + text);
msgEl.value = "";
msgEl.focus();
}
btnConnect.addEventListener("click", connect);
btnDisconnect.addEventListener("click", disconnect);
btnSend.addEventListener("click", send);
btnClear.addEventListener("click", function () {
logEl.innerHTML = "";
});
msgEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") send();
});
urlEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") connect();
});
setState("closed", "未连接");
})();
</script>
</body>
</html>