增加websocket支持
This commit is contained in:
49
.gitignore
vendored
49
.gitignore
vendored
@@ -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
77
.vscode/launch.json
vendored
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
; 开启或关闭
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/response;callback 内异常不可再抛出(上层无 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)
|
||||
// CLOSE(request/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);
|
||||
|
||||
@@ -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>
|
||||
/// 未找到页面
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -115,9 +115,6 @@ namespace module
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool save_body(const std::string& filepath);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 推送到自动释放队列
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
96
src/module/http/websocket.cpp
Normal file
96
src/module/http/websocket.cpp
Normal 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;
|
||||
}
|
||||
58
src/module/http/websocket.h
Normal file
58
src/module/http/websocket.h
Normal 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
14
www/api/sha1.lua
Normal 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
77
www/api/ws.lua
Normal 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()
|
||||
440
www/index.html
440
www/index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user