blueloveTH 1 anno fa
parent
commit
50d3c9adac

+ 1 - 0
3rd/libhv/CMakeLists.txt

@@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
 option(BUILD_SHARED "build shared library" OFF)
 option(BUILD_STATIC "build static library" ON)
 option(BUILD_EXAMPLES "build examples" OFF)
+
 option(WITH_OPENSSL "with openssl library" OFF)
 
 add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/libhv)

+ 44 - 1
3rd/libhv/include/libhv_bindings.hpp

@@ -1,10 +1,53 @@
 #pragma once
 
 #include "pocketpy.h"
+#include "http/HttpMessage.h"
+#include "base/hplatform.h"
 
 extern "C" void pk__add_module_libhv();
 
+void libhv_HttpRequest_create(py_OutRef out, HttpRequestPtr ptr);
+
+py_Type libhv_register_HttpRequest(py_GlobalRef mod);
 py_Type libhv_register_HttpClient(py_GlobalRef mod);
 py_Type libhv_register_HttpServer(py_GlobalRef mod);
 py_Type libhv_register_WebSocketClient(py_GlobalRef mod);
-py_Type libhv_register_WebSocketServer(py_GlobalRef mod);
+
+#include <deque>
+#include <atomic>
+
+template <typename T>
+class libhv_MQ {
+private:
+    std::atomic<bool> lock;
+    std::deque<T> queue;
+
+public:
+    void push(T msg) {
+        while(lock.exchange(true)) {
+            hv_delay(1);
+        }
+        queue.push_back(msg);
+        lock.store(false);
+    }
+
+    bool pop(T* msg) {
+        while(lock.exchange(true)) {
+            hv_delay(1);
+        }
+        if(queue.empty()) {
+            lock.store(false);
+            return false;
+        }
+        *msg = queue.front();
+        queue.pop_front();
+        lock.store(false);
+        return true;
+    }
+};
+
+enum class WsMessageType {
+    onopen,
+    onclose,
+    onmessage,
+};

+ 26 - 14
3rd/libhv/src/HttpClient.cpp

@@ -1,21 +1,23 @@
+#include "HttpMessage.h"
 #include "libhv_bindings.hpp"
 #include "base/herr.h"
 #include "http/client/HttpClient.h"
 
 struct libhv_HttpResponse {
-    HttpResponsePtr ptr;
+    HttpRequestPtr request;
+    HttpResponsePtr response;
     bool ok;
 
-    bool is_valid() { return ok && ptr != NULL; }
+    bool is_valid() { return ok && response != NULL; }
 
-    libhv_HttpResponse() : ptr(NULL), ok(false) {}
+    libhv_HttpResponse(HttpRequestPtr request) : request(request), response(NULL), ok(false) {}
 };
 
 static bool libhv_HttpResponse_status_code(int argc, py_Ref argv) {
     PY_CHECK_ARGC(1);
     libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv);
     if(!resp->is_valid()) return RuntimeError("HttpResponse: no response");
-    py_newint(py_retval(), resp->ptr->status_code);
+    py_newint(py_retval(), resp->response->status_code);
     return true;
 };
 
@@ -28,7 +30,7 @@ static bool libhv_HttpResponse_headers(int argc, py_Ref argv) {
         py_newdict(headers);
         py_Ref _0 = py_pushtmp();
         py_Ref _1 = py_pushtmp();
-        for(auto& kv: resp->ptr->headers) {
+        for(auto& kv: resp->response->headers) {
             py_newstr(_0, kv.first.c_str());
             py_newstr(_1, kv.second.c_str());
             py_dict_setitem(headers, _0, _1);
@@ -46,8 +48,8 @@ static bool libhv_HttpResponse_text(int argc, py_Ref argv) {
     py_Ref text = py_getslot(argv, 1);
     if(py_isnil(text)) {
         c11_sv sv;
-        sv.data = resp->ptr->body.c_str();
-        sv.size = resp->ptr->body.size();
+        sv.data = resp->response->body.c_str();
+        sv.size = resp->response->body.size();
         py_newstrv(text, sv);
     }
     py_assign(py_retval(), text);
@@ -60,9 +62,9 @@ static bool libhv_HttpResponse_content(int argc, py_Ref argv) {
     if(!resp->is_valid()) return RuntimeError("HttpResponse: no response");
     py_Ref content = py_getslot(argv, 2);
     if(py_isnil(content)) {
-        int size = resp->ptr->body.size();
+        int size = resp->response->body.size();
         unsigned char* buf = py_newbytes(content, size);
-        memcpy(buf, resp->ptr->body.data(), size);
+        memcpy(buf, resp->response->body.data(), size);
     }
     py_assign(py_retval(), content);
     return true;
@@ -72,7 +74,7 @@ static bool libhv_HttpResponse_json(int argc, py_Ref argv) {
     PY_CHECK_ARGC(1);
     libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv);
     if(!resp->is_valid()) return RuntimeError("HttpResponse: no response");
-    const char* source = resp->ptr->body.c_str();  // json string is null-terminated
+    const char* source = resp->response->body.c_str();  // json string is null-terminated
     return py_json_loads(source);
 };
 
@@ -111,11 +113,19 @@ static bool libhv_HttpResponse__repr__(int argc, py_Ref argv) {
     if(!resp->is_valid()) {
         py_newstr(py_retval(), "<HttpResponse: no response>");
     } else {
-        py_newfstr(py_retval(), "<HttpResponse: %d>", (int)resp->ptr->status_code);
+        py_newfstr(py_retval(), "<HttpResponse: %d>", (int)resp->response->status_code);
     }
     return true;
 }
 
+static bool libhv_HttpResponse_cancel(int argc, py_Ref argv) {
+    PY_CHECK_ARGC(1);
+    libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv);
+    resp->request->Cancel();
+    py_newnone(py_retval());
+    return true;
+}
+
 static py_Type libhv_register_HttpResponse(py_GlobalRef mod) {
     py_Type type = py_newtype("HttpResponse", tp_object, mod, [](void* ud) {
         ((libhv_HttpResponse*)ud)->~libhv_HttpResponse();
@@ -133,6 +143,8 @@ static py_Type libhv_register_HttpResponse(py_GlobalRef mod) {
     py_bindmagic(type, __repr__, libhv_HttpResponse__repr__);
     // completed
     py_bindproperty(type, "completed", libhv_HttpResponse_completed, NULL);
+    // cancel
+    py_bindmethod(type, "cancel", libhv_HttpResponse_cancel);
     return type;
 }
 
@@ -218,15 +230,15 @@ static bool libhv_HttpClient__send_request(py_Ref arg_self,
                                           3,  // headers, text, content
                                           sizeof(libhv_HttpResponse));
     // placement new
-    new (retval) libhv_HttpResponse();
+    new (retval) libhv_HttpResponse(req);
 
     int code = cli->sendAsync(req, [retval](const HttpResponsePtr& resp) {
         if(resp == NULL) {
             retval->ok = false;
-            retval->ptr = NULL;
+            retval->response = NULL;
         } else {
             retval->ok = true;
-            retval->ptr = resp;
+            retval->response = resp;
         }
     });
     if(code != 0) {

+ 114 - 0
3rd/libhv/src/HttpRequest.cpp

@@ -0,0 +1,114 @@
+#include "libhv_bindings.hpp"
+#include "HttpMessage.h"
+
+struct libhv_HttpRequest {
+    HttpRequestPtr ptr;
+
+    libhv_HttpRequest(HttpRequestPtr ptr) : ptr(ptr) {}
+};
+
+void libhv_HttpRequest_create(py_OutRef out, HttpRequestPtr ptr) {
+    py_Type type = py_gettype("libhv", py_name("HttpRequest"));
+    libhv_HttpRequest* self =
+        (libhv_HttpRequest*)py_newobject(out, type, 2, sizeof(libhv_HttpRequest));
+    new (self) libhv_HttpRequest(ptr);
+}
+
+py_Type libhv_register_HttpRequest(py_GlobalRef mod) {
+    py_Type type = py_newtype("HttpRequest", tp_object, mod, [](void* ud) {
+        ((libhv_HttpRequest*)ud)->~libhv_HttpRequest();
+    });
+
+    py_bindmagic(type, __new__, [](int argc, py_Ref argv) {
+        return py_exception(tp_NotImplementedError, "");
+    });
+
+    py_bindproperty(
+        type,
+        "method",
+        [](int argc, py_Ref argv) {
+            PY_CHECK_ARGC(1);
+            libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv);
+            py_newstr(py_retval(), req->ptr->Method());
+            return true;
+        },
+        NULL);
+
+    py_bindproperty(
+        type,
+        "url",
+        [](int argc, py_Ref argv) {
+            PY_CHECK_ARGC(1);
+            libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv);
+            py_newstr(py_retval(), req->ptr->Url().c_str());
+            return true;
+        },
+        NULL);
+
+    py_bindproperty(
+        type,
+        "path",
+        [](int argc, py_Ref argv) {
+            PY_CHECK_ARGC(1);
+            libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv);
+            py_newstr(py_retval(), req->ptr->Path().c_str());
+            return true;
+        },
+        NULL);
+
+    // headers (cache in slots[0])
+    py_bindproperty(
+        type,
+        "headers",
+        [](int argc, py_Ref argv) {
+            PY_CHECK_ARGC(1);
+            libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv);
+            py_Ref headers = py_getslot(argv, 0);
+            if(py_isnil(headers)) {
+                py_newdict(headers);
+                py_Ref _0 = py_pushtmp();
+                py_Ref _1 = py_pushtmp();
+                for(auto& kv: req->ptr->headers) {
+                    py_newstr(_0, kv.first.c_str());  // TODO: tolower
+                    py_newstr(_1, kv.second.c_str());
+                    py_dict_setitem(headers, _0, _1);
+                }
+                py_shrink(2);
+            }
+            py_assign(py_retval(), headers);
+            return true;
+        },
+        NULL);
+
+    // data (cache in slots[1])
+    py_bindproperty(
+        type,
+        "data",
+        [](int argc, py_Ref argv) {
+            PY_CHECK_ARGC(1);
+            libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv);
+            py_Ref data = py_getslot(argv, 1);
+
+            if(py_isnil(data)) {
+                auto content_type = req->ptr->ContentType();
+                bool is_text_data = content_type == TEXT_PLAIN ||
+                                    content_type == APPLICATION_JSON ||
+                                    content_type == APPLICATION_XML || content_type == TEXT_HTML ||
+                                    content_type == CONTENT_TYPE_NONE;
+                if(is_text_data) {
+                    c11_sv sv;
+                    sv.data = req->ptr->body.data();
+                    sv.size = req->ptr->body.size();
+                    py_newstrv(data, sv);
+                } else {
+                    unsigned char* buf = py_newbytes(data, req->ptr->body.size());
+                    memcpy(buf, req->ptr->body.data(), req->ptr->body.size());
+                }
+            }
+            py_assign(py_retval(), data);
+            return true;
+        },
+        NULL);
+
+    return type;
+}

+ 149 - 122
3rd/libhv/src/HttpServer.cpp

@@ -1,38 +1,24 @@
+#include "HttpMessage.h"
+#include "WebSocketChannel.h"
 #include "libhv_bindings.hpp"
-#include "http/server/HttpServer.h"
-#include "base/herr.h"
-
-#include <deque>
-#include <atomic>
-
-template <typename T_in, typename T_out>
-struct libhv_MQ {
-    std::atomic<bool> lock_in;
-    std::atomic<bool> lock_out;
-    std::deque<T_in> queue_in;
-    std::deque<T_out> queue_out;
-
-    void begin_in() {
-        while(lock_in.exchange(true)) {
-            hv_delay(1);
-        }
-    }
+#include "http/server/WebSocketServer.h"
+#include "pocketpy/pocketpy.h"
 
-    void end_in() { lock_in.store(false); }
+struct libhv_HttpServer {
+    hv::HttpService http_service;
+    hv::WebSocketService ws_service;
+    hv::WebSocketServer server;
 
-    void begin_out() {
-        while(lock_out.exchange(true)) {
-            hv_delay(1);
-        }
-    }
+    libhv_MQ<std::pair<HttpContextPtr, std::atomic<int>>*> mq;
 
-    void end_out() { lock_out.store(false); }
-};
+    struct WsMessage {
+        WsMessageType type;
+        hv::WebSocketChannel* channel;
+        HttpRequestPtr request;
+        std::string body;
+    };
 
-struct libhv_HttpServer {
-    hv::HttpService service;
-    hv::HttpServer server;
-    libhv_MQ<HttpContextPtr, std::pair<HttpContextPtr, int>> mq;
+    libhv_MQ<WsMessage> ws_mq;
 };
 
 static bool libhv_HttpServer__new__(int argc, py_Ref argv) {
@@ -49,31 +35,37 @@ static bool libhv_HttpServer__init__(int argc, py_Ref argv) {
     PY_CHECK_ARG_TYPE(2, tp_int);
     const char* host = py_tostr(py_arg(1));
     int port = py_toint(py_arg(2));
+    self->server.setHost(host);
+    self->server.setPort(port);
 
-    self->service.AllowCORS();
+    // http
+    self->http_service.AllowCORS();
     http_ctx_handler internal_handler = [self](const HttpContextPtr& ctx) {
-        self->mq.begin_in();
-        self->mq.queue_in.push_back(ctx);
-        self->mq.end_in();
-
-        while(true) {
-            self->mq.begin_out();
-            if(!self->mq.queue_out.empty()) {
-                auto& msg = self->mq.queue_out.front();
-                if(msg.first == ctx) {
-                    self->mq.queue_out.pop_front();
-                    self->mq.end_out();
-                    return msg.second;
-                }
-            }
-            self->mq.end_out();
-            hv_delay(1);
-        }
+        std::pair<HttpContextPtr, std::atomic<int>> msg(ctx, 0);
+        self->mq.push(&msg);
+        int code;
+        do {
+            code = msg.second.load();
+        } while(code == 0);
+        return code;
     };
-    self->service.Any("*", internal_handler);
-    self->server.registerHttpService(&self->service);
-    self->server.setHost(host);
-    self->server.setPort(port);
+    self->http_service.Any("*", internal_handler);
+    self->server.registerHttpService(&self->http_service);
+
+    // websocket
+    self->ws_service.onopen = [self](const WebSocketChannelPtr& channel,
+                                     const HttpRequestPtr& req) {
+        self->ws_mq.push({WsMessageType::onopen, channel.get(), req, ""});
+    };
+    self->ws_service.onmessage = [self](const WebSocketChannelPtr& channel,
+                                        const std::string& msg) {
+        self->ws_mq.push({WsMessageType::onmessage, channel.get(), nullptr, msg});
+    };
+    self->ws_service.onclose = [self](const WebSocketChannelPtr& channel) {
+        self->ws_mq.push({WsMessageType::onclose, channel.get(), nullptr, ""});
+    };
+    self->server.registerWebSocketService(&self->ws_service);
+
     py_newnone(py_retval());
     return true;
 }
@@ -84,75 +76,45 @@ static bool libhv_HttpServer_dispatch(int argc, py_Ref argv) {
     py_Ref callable = py_arg(1);
     if(!py_callable(callable)) return TypeError("dispatcher must be callable");
 
-    self->mq.begin_in();
-    if(self->mq.queue_in.empty()) {
-        self->mq.end_in();
+    std::pair<HttpContextPtr, std::atomic<int>>* mq_msg;
+    if(!self->mq.pop(&mq_msg)) {
         py_newbool(py_retval(), false);
         return true;
     } else {
-        HttpContextPtr ctx = self->mq.queue_in.front();
-        self->mq.queue_in.pop_front();
-        self->mq.end_in();
-
-        const char* method = ctx->request->Method();
-        std::string path = ctx->request->Path();
-        const http_headers& headers = ctx->request->headers;
-        const std::string& data = ctx->request->body;
-
-        py_OutRef msg = py_pushtmp();
-        py_newdict(msg);
-        py_Ref _0 = py_pushtmp();
-        py_Ref _1 = py_pushtmp();
-        py_Ref _2 = py_pushtmp();
-        py_Ref _3 = py_pushtmp();
-
-        // method
-        py_newstr(_0, "method");
-        py_newstr(_1, method);
-        py_dict_setitem(msg, _0, _1);
-        // path
-        py_newstr(_0, "path");
-        py_newstr(_1, path.c_str());
-        py_dict_setitem(msg, _0, _1);
-        // headers
-        py_newstr(_0, "headers");
-        py_newdict(_1);
-        py_dict_setitem(msg, _0, _1);
-        for(auto& header: headers) {
-            py_newstr(_2, header.first.c_str());
-            py_newstr(_3, header.second.c_str());
-            py_dict_setitem(_1, _2, _3);
-        }
-        // data
-        py_newstr(_0, "data");
-        auto content_type = ctx->request->ContentType();
-        bool is_text_data = content_type == TEXT_PLAIN || content_type == APPLICATION_JSON ||
-                            content_type == APPLICATION_XML || content_type == TEXT_HTML ||
-                            content_type == CONTENT_TYPE_NONE;
-        if(is_text_data) {
-            py_newstrv(_1, {data.c_str(), (int)data.size()});
-        } else {
-            unsigned char* buf = py_newbytes(_1, data.size());
-            memcpy(buf, data.data(), data.size());
-        }
-        py_dict_setitem(msg, _0, _1);
-        py_assign(py_retval(), msg);
-        py_shrink(5);
-
+        HttpContextPtr ctx = mq_msg->first;
+        libhv_HttpRequest_create(py_retval(), ctx->request);
         // call dispatcher
-        if(!py_call(callable, 1, py_retval())) { return false; }
+        if(!py_call(callable, 1, py_retval())) return false;
 
         py_Ref object;
         int status_code = 200;
         if(py_istuple(py_retval())) {
-            // "Hello, world!", 200
-            if(py_tuple_len(py_retval()) != 2) {
-                return ValueError("dispatcher should return `object | tuple[object, int]`");
+            int length = py_tuple_len(py_retval());
+            if(length == 2 || length == 3) {
+                // "Hello, world!", 200
+                object = py_tuple_getitem(py_retval(), 0);
+                py_ItemRef status_code_object = py_tuple_getitem(py_retval(), 1);
+                if(!py_checkint(status_code_object)) return false;
+                status_code = py_toint(status_code_object);
+
+                if(length == 3) {
+                    // "Hello, world!", 200, {"Content-Type": "text/plain"}
+                    py_ItemRef headers_object = py_tuple_getitem(py_retval(), 2);
+                    if(!py_checktype(headers_object, tp_dict)) return false;
+                    bool ok = py_dict_apply(
+                        headers_object,
+                        [](py_Ref key, py_Ref value, void* ctx_) {
+                            if(!py_checkstr(key) || !py_checkstr(value)) return false;
+                            ((hv::HttpContext*)ctx_)
+                                ->response->SetHeader(py_tostr(key), py_tostr(value));
+                            return true;
+                        },
+                        ctx.get());
+                    if(!ok) return false;
+                }
+            } else {
+                return TypeError("dispatcher return tuple must have 2 or 3 elements");
             }
-            object = py_tuple_getitem(py_retval(), 0);
-            py_ItemRef status_code_object = py_tuple_getitem(py_retval(), 1);
-            if(!py_checkint(status_code_object)) return false;
-            status_code = py_toint(status_code_object);
         } else {
             // "Hello, world!"
             object = py_retval();
@@ -182,9 +144,7 @@ static bool libhv_HttpServer_dispatch(int argc, py_Ref argv) {
             }
         }
 
-        self->mq.begin_out();
-        self->mq.queue_out.push_back({ctx, status_code});
-        self->mq.end_out();
+        mq_msg->second.store(status_code);
     }
     py_newbool(py_retval(), true);
     return true;
@@ -194,21 +154,84 @@ static bool libhv_HttpServer_start(int argc, py_Ref argv) {
     PY_CHECK_ARGC(1);
     libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0));
     int code = self->server.start();
-    if(code != 0) {
-        return RuntimeError("HttpServer start failed: %s (%d)", hv_strerror(code), code);
-    }
-    py_newnone(py_retval());
+    py_newint(py_retval(), code);
     return true;
 }
 
 static bool libhv_HttpServer_stop(int argc, py_Ref argv) {
     PY_CHECK_ARGC(1);
     libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0));
-    self->server.stop();
+    int code = self->server.stop();
+    py_newint(py_retval(), code);
+    return true;
+}
+
+static bool libhv_HttpServer_ws_set_ping_interval(int argc, py_Ref argv) {
+    PY_CHECK_ARGC(2);
+    libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0));
+    PY_CHECK_ARG_TYPE(1, tp_int);
+    int interval = py_toint(py_arg(1));
+    self->ws_service.setPingInterval(interval);
     py_newnone(py_retval());
     return true;
 }
 
+static bool libhv_HttpServer_ws_send(int argc, py_Ref argv) {
+    PY_CHECK_ARGC(3);
+    libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0));
+    PY_CHECK_ARG_TYPE(1, tp_int);
+    PY_CHECK_ARG_TYPE(2, tp_str);
+    py_i64 channel = py_toint(py_arg(1));
+    const char* msg = py_tostr(py_arg(2));
+
+    hv::WebSocketChannel* p_channel = reinterpret_cast<hv::WebSocketChannel*>(channel);
+    int code = p_channel->send(msg);
+    py_newint(py_retval(), code);
+    return true;
+}
+
+static bool libhv_HttpServer_ws_recv(int argc, py_Ref argv) {
+    PY_CHECK_ARGC(1);
+    libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0));
+    libhv_HttpServer::WsMessage msg;
+    if(!self->ws_mq.pop(&msg)) {
+        py_newnone(py_retval());
+        return true;
+    }
+    py_newtuple(py_retval(), 2);
+    switch(msg.type) {
+        case WsMessageType::onopen: {
+            // "onopen", (channel, request)
+            assert(msg.request != nullptr);
+            py_newstr(py_tuple_getitem(py_retval(), 0), "onopen");
+            py_Ref args = py_tuple_getitem(py_retval(), 1);
+            py_newtuple(args, 2);
+            py_newint(py_tuple_getitem(args, 0), (py_i64)msg.channel);
+            libhv_HttpRequest_create(py_tuple_getitem(args, 1), msg.request);
+            break;
+        }
+        case WsMessageType::onclose: {
+            // "onclose", channel
+            py_newstr(py_tuple_getitem(py_retval(), 0), "onclose");
+            py_newint(py_tuple_getitem(py_retval(), 1), (py_i64)msg.channel);
+            break;
+        }
+        case WsMessageType::onmessage: {
+            // "onmessage", (channel, body)
+            py_newstr(py_tuple_getitem(py_retval(), 0), "onmessage");
+            py_Ref args = py_tuple_getitem(py_retval(), 1);
+            py_newtuple(args, 2);
+            py_newint(py_tuple_getitem(args, 0), (py_i64)msg.channel);
+            c11_sv sv;
+            sv.data = msg.body.data();
+            sv.size = msg.body.size();
+            py_newstrv(py_tuple_getitem(args, 1), sv);
+            break;
+        }
+    }
+    return true;
+}
+
 py_Type libhv_register_HttpServer(py_GlobalRef mod) {
     py_Type type = py_newtype("HttpServer", tp_object, mod, [](void* ud) {
         libhv_HttpServer* self = (libhv_HttpServer*)ud;
@@ -217,8 +240,12 @@ py_Type libhv_register_HttpServer(py_GlobalRef mod) {
 
     py_bindmagic(type, __new__, libhv_HttpServer__new__);
     py_bindmagic(type, __init__, libhv_HttpServer__init__);
-    py_bindmethod(type, "dispatch", libhv_HttpServer_dispatch);
     py_bindmethod(type, "start", libhv_HttpServer_start);
     py_bindmethod(type, "stop", libhv_HttpServer_stop);
+    py_bindmethod(type, "dispatch", libhv_HttpServer_dispatch);
+
+    py_bindmethod(type, "ws_set_ping_interval", libhv_HttpServer_ws_set_ping_interval);
+    py_bindmethod(type, "ws_send", libhv_HttpServer_ws_send);
+    py_bindmethod(type, "ws_recv", libhv_HttpServer_ws_recv);
     return type;
-}
+}

+ 120 - 2
3rd/libhv/src/WebSocketClient.cpp

@@ -1,7 +1,125 @@
+#include "HttpMessage.h"
 #include "libhv_bindings.hpp"
+#include "pocketpy/pocketpy.h"
 #include "http/client/WebSocketClient.h"
 
+struct libhv_WebSocketClient {
+    hv::WebSocketClient ws;
+
+    libhv_MQ<std::pair<WsMessageType, std::string>> mq;
+
+    libhv_WebSocketClient() {
+        ws.onopen = [this]() {
+            mq.push({WsMessageType::onopen, ""});
+        };
+        ws.onclose = [this]() {
+            mq.push({WsMessageType::onclose, ""});
+        };
+        ws.onmessage = [this](const std::string& msg) {
+            mq.push({WsMessageType::onmessage, msg});
+        };
+
+        // reconnect: 1,2,4,8,10,10,10...
+        reconn_setting_t reconn;
+        reconn_setting_init(&reconn);
+        reconn.min_delay = 1000;
+        reconn.max_delay = 10000;
+        reconn.delay_policy = 2;
+        ws.setReconnect(&reconn);
+    }
+};
+
 py_Type libhv_register_WebSocketClient(py_GlobalRef mod) {
-    py_Type type = py_newtype("WebSocketClient", tp_object, mod, NULL);
+    py_Type type = py_newtype("WebSocketClient", tp_object, mod, [](void* ud) {
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)ud;
+        self->~libhv_WebSocketClient();
+    });
+
+    py_bindmagic(type, __new__, [](int argc, py_Ref argv) {
+        PY_CHECK_ARGC(1);
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)
+            py_newobject(py_retval(), py_totype(argv), 0, sizeof(libhv_WebSocketClient));
+        new (self) libhv_WebSocketClient();
+        return true;
+    });
+
+    py_bindmethod(type, "open", [](int argc, py_Ref argv) {
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv);
+        PY_CHECK_ARG_TYPE(1, tp_str);
+        const char* url = py_tostr(py_arg(1));
+        http_headers headers = DefaultHeaders;
+        if(argc == 2) {
+            // open(self, url)
+        } else if(argc == 3) {
+            // open(self, url, headers)
+            if(!py_checktype(py_arg(2), tp_dict)) return false;
+            bool ok = py_dict_apply(
+                py_arg(2),
+                [](py_Ref key, py_Ref value, void* ctx) {
+                    http_headers* p_headers = (http_headers*)ctx;
+                    if(!py_checkstr(key)) return false;
+                    if(!py_checkstr(value)) return false;
+                    p_headers->operator[](py_tostr(key)) = py_tostr(value);
+                    return true;
+                },
+                &headers);
+            if(!ok) return false;
+        } else {
+            return TypeError("open() takes 2 or 3 arguments");
+        }
+        int code = self->ws.open(url, headers);
+        py_newint(py_retval(), code);
+        return true;
+    });
+
+    py_bindmethod(type, "close", [](int argc, py_Ref argv) {
+        PY_CHECK_ARGC(1);
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv);
+        int code = self->ws.close();
+        py_newint(py_retval(), code);
+        return true;
+    });
+
+    py_bindmethod(type, "send", [](int argc, py_Ref argv) {
+        PY_CHECK_ARGC(2);
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv);
+        PY_CHECK_ARG_TYPE(1, tp_str);
+        const char* msg = py_tostr(py_arg(1));
+        int code = self->ws.send(msg);
+        py_newint(py_retval(), code);
+        return true;
+    });
+
+    py_bindmethod(type, "recv", [](int argc, py_Ref argv) {
+        PY_CHECK_ARGC(1);
+        libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(py_arg(0));
+
+        std::pair<WsMessageType, std::string> mq_msg;
+        if(!self->mq.pop(&mq_msg)) {
+            py_newnone(py_retval());
+            return true;
+        } else {
+            py_newtuple(py_retval(), 2);
+            switch(mq_msg.first) {
+                case WsMessageType::onopen: {
+                    py_newstr(py_tuple_getitem(py_retval(), 0), "onopen");
+                    py_newnone(py_tuple_getitem(py_retval(), 1));
+                    break;
+                }
+                case WsMessageType::onclose: {
+                    py_newstr(py_tuple_getitem(py_retval(), 0), "onclose");
+                    py_newnone(py_tuple_getitem(py_retval(), 1));
+                    break;
+                }
+                case WsMessageType::onmessage: {
+                    py_newstr(py_tuple_getitem(py_retval(), 0), "onmessage");
+                    py_newstrv(py_tuple_getitem(py_retval(), 1),
+                               {mq_msg.second.data(), (int)mq_msg.second.size()});
+                    break;
+                }
+            }
+            return true;
+        }
+    });
     return type;
-}
+}

+ 0 - 7
3rd/libhv/src/WebSocketServer.cpp

@@ -1,7 +0,0 @@
-#include "libhv_bindings.hpp"
-#include "http/server/WebSocketServer.h"
-
-py_Type libhv_register_WebSocketServer(py_GlobalRef mod) {
-    py_Type type = py_newtype("WebSocketServer", tp_object, mod, NULL);
-    return type;
-}

+ 11 - 1
3rd/libhv/src/libhv_bindings.cpp

@@ -1,10 +1,20 @@
 #include "libhv_bindings.hpp"
+#include "base/herr.h"
 
 extern "C" void pk__add_module_libhv() {
     py_GlobalRef mod = py_newmodule("libhv");
 
+    libhv_register_HttpRequest(mod);
     libhv_register_HttpClient(mod);
     libhv_register_HttpServer(mod);
     libhv_register_WebSocketClient(mod);
-    libhv_register_WebSocketServer(mod);
+
+    py_bindfunc(mod, "strerror", [](int argc, py_Ref argv) {
+        PY_CHECK_ARGC(1);
+        PY_CHECK_ARG_TYPE(0, tp_int);
+        int code = py_toint(py_arg(0));
+        const char* msg = hv_strerror(code);
+        py_newstr(py_retval(), msg);
+        return true;
+    });
 }

+ 8 - 2
docs/modules/libhv.md

@@ -7,11 +7,17 @@ label: libhv
 This module is optional. Set option `PK_BUILD_MODULE_LIBHV` to `ON` in your `CMakeLists.txt` to enable it.
 !!!
 
+`libhv` is a git submodule located at `3rd/libhv/libhv`. If you cannot find it, please run the following command to initialize the submodule:
+
+```bash
+git submodule update --init --recursive
+```
+
 Simple bindings for [libhv](https://github.com/ithewei/libhv), which provides cross platform implementation of the following:
 + HTTP server
 + HTTP client
-+ WebSocket server (TODO)
-+ WebSocket client (TODO)
++ WebSocket server
++ WebSocket client
 
 #### Source code
 

+ 73 - 11
include/typings/libhv.pyi

@@ -1,7 +1,15 @@
 from typing import Literal, Generator, Callable
 
+WsMessageType = Literal['onopen', 'onclose', 'onmessage']
+WsChannelId = int
+HttpStatusCode = int
+HttpHeaders = dict[str, str]
+ErrorCode = int
+
 class Future[T]:
+    @property
     def completed(self) -> bool: ...
+    def cancel(self) -> None: ...
     def __iter__(self) -> Generator[T, None, None]: ...
 
 class HttpResponse(Future['HttpResponse']):
@@ -18,21 +26,75 @@ class HttpResponse(Future['HttpResponse']):
 
 
 class HttpClient:
-    def get(self, url: str, params=None, headers=None, timeout=10) -> HttpResponse: ...
-    def post(self, url: str, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ...
-    def put(self, url: str, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ...
-    def delete(self, url: str, params=None, headers=None, timeout=10) -> HttpResponse: ...
+    def get(self, url: str, /, params=None, headers=None, timeout=10) -> HttpResponse: ...
+    def post(self, url: str, /, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ...
+    def put(self, url: str, /, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ...
+    def delete(self, url: str, /, params=None, headers=None, timeout=10) -> HttpResponse: ...
 
 
+class HttpRequest:
+    @property
+    def method(self) -> Literal['GET', 'POST', 'PUT', 'DELETE']: ...
+    @property
+    def path(self) -> str: ...
+    @property
+    def url(self) -> str: ...
+    @property
+    def headers(self) -> HttpHeaders: ...
+    @property
+    def data(self) -> str | bytes: ...
+
 class HttpServer:
-    def __init__(self, host: str, port: int) -> None: ...
-    def dispatch(self, fn: Callable[[dict], object | tuple[object, int]]) -> bool: ...
-    def start(self) -> None: ...
-    def stop(self) -> None: ...
+    def __init__(self, host: str, port: int, /) -> None: ...
+    def start(self) -> ErrorCode: ...
+    def stop(self) -> ErrorCode: ...
+    def dispatch[T](self, fn: Callable[
+        [HttpRequest],
+        T | tuple[T, HttpStatusCode] | tuple[T, HttpStatusCode, HttpHeaders]
+        ], /) -> bool:
+        """Dispatch one HTTP request through `fn`. `fn` should return one of the following:
+
+        + object
+        + (object, status_code)
+        + (object, status_code, headers)
+        
+        Return `True` if dispatched, otherwise `False`.
+        """
+
+    def ws_set_ping_interval(self, milliseconds: int, /) -> None:
+        """Set WebSocket ping interval in milliseconds."""
+
+    def ws_send(self, channel: WsChannelId, data: str, /) -> ErrorCode:
+        """Send WebSocket message through `channel`."""
 
+    def ws_recv(self) -> tuple[
+        WsMessageType,
+        tuple[WsChannelId, HttpRequest] | WsChannelId | tuple[WsChannelId, str]
+        ] | None:
+        """Receive one WebSocket message.
+        Return one of the following or `None` if nothing to receive.
+        
+        + `"onopen"`: (channel, request)
+        + `"onclose"`: channel
+        + `"onmessage"`: (channel, body)
+        """
 
 class WebSocketClient:
-    pass
+    def open(self, url: str, headers=None, /) -> ErrorCode: ...
+    def close(self) -> ErrorCode: ...
+
+    def send(self, data: str, /) -> ErrorCode:
+        """Send WebSocket message."""
+
+    def recv(self) -> tuple[WsMessageType, str | None] | None:
+        """Receive one WebSocket message.
+        Return one of the following or `None` if nothing to receive.
+        
+        + `"onopen"`: `None`
+        + `"onclose"`: `None`
+        + `"onmessage"`: body
+        """
+
 
-class WebSocketServer:
-    pass
+def strerror(errno: ErrorCode, /) -> str:
+    """Get error message by errno via `hv_strerror`."""

+ 1 - 1
src/public/internal.c

@@ -152,7 +152,7 @@ bool py_callcfunc(py_CFunction f, int argc, py_Ref argv) {
     }
     if(py_checkexc(true)) {
         const char* name = py_tpname(pk_current_vm->curr_exception.type);
-        c11__abort("py_CFunction returns `true`, but `%s` is set!", name);
+        c11__abort("py_CFunction returns `true`, but `%s` was set!", name);
     }
     return true;
 }