瀏覽代碼

Done WebSocket

ithewei 4 年之前
父節點
當前提交
97ab7fbb53

+ 1 - 0
Makefile.vars

@@ -77,6 +77,7 @@ HTTP_HEADERS =  http/httpdef.h\
 
 HTTP_CLIENT_HEADERS =   http/client/http_client.h\
 						http/client/requests.h\
+						http/client/WebSocketClient.h\
 
 HTTP_SERVER_HEADERS =   http/server/HttpService.h\
 						http/server/HttpServer.h\

+ 1 - 0
README.md

@@ -18,6 +18,7 @@ but simpler api and richer protocols.
 - WITH_OPENSSL or WITH_MBEDTLS
 - http client/server (include https http1/x http2 grpc)
 - http web service, indexof service, api service (support RESTful API)
+- websocket client/server
 - protocols
     - dns
     - ftp

+ 5 - 1
cmake/vars.cmake

@@ -80,7 +80,11 @@ set(HTTP_HEADERS
     http/WebSocketChannel.h
 )
 
-set(HTTP_CLIENT_HEADERS http/client/http_client.h http/client/requests.h)
+set(HTTP_CLIENT_HEADERS
+    http/client/http_client.h
+    http/client/requests.h
+    http/client/WebSocketClient.h)
+
 set(HTTP_SERVER_HEADERS
     http/server/HttpService.h
     http/server/HttpServer.h

+ 1 - 1
docs/PLAN.md

@@ -3,6 +3,7 @@
 - event: select/poll/epoll/kqueue/port
 - evpp: c++ EventLoop interface similar to muduo and evpp
 - http client/server: include https http1/x http2
+- websocket client/server
 
 ## Improving
 
@@ -10,6 +11,5 @@
 
 ## Plan
 
-- websocket client/server
 - mqtt client
 - redis client

+ 38 - 4
examples/websocket_client_test.cpp

@@ -1,11 +1,45 @@
 /*
- * TODO
- * @see html/websocket_client.html
+ * websocket client
+ *
+ * @build   make examples
+ * @server  bin/websocket_server_test 8888
+ * @client  bin/websocket_client_test ws://127.0.0.1:8888/
+ * @js      html/websocket_client.html
  *
  */
 
-// #include "WebSocketClient.h"
+#include "WebSocketClient.h"
+
+using namespace hv;
+
+int main(int argc, char** argv) {
+    if (argc < 2) {
+        printf("Usage: %s url\n", argv[0]);
+        return -10;
+    }
+    const char* url = argv[1];
+
+    WebSocketClient ws;
+    ws.onopen = [&ws]() {
+        printf("onopen\n");
+        ws.send("hello");
+    };
+    ws.onclose = []() {
+        printf("onclose\n");
+    };
+    ws.onmessage = [](const std::string& msg) {
+        printf("onmessage: %s\n", msg.c_str());
+    };
+
+    // reconnect: 1,2,4,8,10,10,10...
+    ReconnectInfo reconn;
+    reconn.min_delay = 1000;
+    reconn.max_delay = 10000;
+    reconn.delay_policy = 2;
+    ws.setReconnect(&reconn);
+
+    ws.open(url);
 
-int main() {
+    while (1) hv_delay(1000);
     return 0;
 }

+ 18 - 2
examples/websocket_server_test.cpp

@@ -1,10 +1,26 @@
+/*
+ * websocket server
+ *
+ * @build   make examples
+ * @server  bin/websocket_server_test 8888
+ * @client  bin/websocket_client_test ws://127.0.0.1:8888/
+ * @js      html/websocket_client.html
+ *
+ */
+
 #include "WebSocketServer.h"
 #include "EventLoop.h"
 #include "htime.h"
 
 using namespace hv;
 
-int main() {
+int main(int argc, char** argv) {
+    if (argc < 2) {
+        printf("Usage: %s port\n", argv[0]);
+        return -10;
+    }
+    int port = atoi(argv[1]);
+
     WebSocketServerCallbacks ws;
     ws.onopen = [](const WebSocketChannelPtr& channel, const std::string& url) {
         printf("onopen: GET %s\n", url.c_str());
@@ -28,7 +44,7 @@ int main() {
     };
 
     websocket_server_t server;
-    server.port = 8888;
+    server.port = port;
     server.ws = &ws;
     websocket_server_run(&server);
     return 0;

+ 1 - 1
http/WebSocketChannel.h

@@ -17,7 +17,7 @@ public:
     {}
     ~WebSocketChannel() {}
 
-    // IsConnected, send, close
+    // isConnected, send, close
 
     int send(const std::string& msg) {
         bool has_mask = false;

+ 3 - 1
http/WebSocketParser.h

@@ -1,6 +1,8 @@
 #ifndef HV_WEBSOCKET_PARSER_H_
 #define HV_WEBSOCKET_PARSER_H_
 
+#include "hexport.h"
+
 #include <string>
 #include <memory>
 #include <functional>
@@ -14,7 +16,7 @@ enum websocket_parser_state {
 
 struct websocket_parser_settings;
 struct websocket_parser;
-class WebSocketParser {
+class HV_EXPORT WebSocketParser {
 public:
     static websocket_parser_settings*   cbs;
     websocket_parser*                   parser;

+ 174 - 0
http/client/WebSocketClient.cpp

@@ -0,0 +1,174 @@
+#include "WebSocketClient.h"
+
+#include "http_parser.h" // for http_parser_url
+#include "base64.h"
+#include "hlog.h"
+
+namespace hv {
+
+WebSocketClient::WebSocketClient()
+    : TcpClientTmpl<WebSocketChannel>()
+{
+    state = WS_CLOSED;
+}
+
+WebSocketClient::~WebSocketClient() {
+    close();
+}
+
+/*
+ * ParseUrl => createsocket => start =>
+ * TCP::onConnection => websocket_handshake => WS::onopen =>
+ * TCP::onMessage => WebSocketParser => WS::onmessage =>
+ * TCP::onConnection => WS::onclose
+ */
+int WebSocketClient::open(const char* _url) {
+    close();
+
+    // ParseUrl
+    if (_url) {
+        if (strncmp(_url, "ws", 2) != 0) {
+            url = "ws://";
+            url += _url;
+        } else {
+            url = _url;
+        }
+    }
+    hlogi("%s", url.c_str());
+    http_parser_url parser;
+    http_parser_url_init(&parser);
+    http_parser_parse_url(url.c_str(), url.size(), 0, &parser);
+    // scheme
+    bool wss = !strncmp(url.c_str(), "wss", 3);
+    // host
+    std::string host = "127.0.0.1";
+    if (parser.field_set & (1<<UF_HOST)) {
+        host = url.substr(parser.field_data[UF_HOST].off, parser.field_data[UF_HOST].len);
+    }
+    // port
+    int port = parser.port ? parser.port : wss ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
+    // path
+    std::string path = "/";
+    if (parser.field_set & (1<<UF_PATH)) {
+        path = url.c_str() + parser.field_data[UF_PATH].off;
+    }
+
+    int connfd = createsocket(port, host.c_str());
+    if (connfd < 0) {
+        return connfd;
+    }
+
+    onConnection = [this](const WebSocketChannelPtr& channel) {
+        if (channel->isConnected()) {
+            state = CONNECTED;
+            // websocket_handshake
+            http_req.reset(new HttpRequest);
+            http_req->method = HTTP_GET;
+            // ws => http
+            http_req->url = "http" + url.substr(2, -1);
+            http_req->headers["Connection"] = "Upgrade";
+            http_req->headers["Upgrade"] = "websocket";
+            // generate SEC_WEBSOCKET_KEY
+            unsigned char rand_key[16] = {0};
+            int *p = (int*)rand_key;
+            for (int i = 0; i < 4; ++i, ++p) {
+                *p = rand();
+            }
+            char ws_key[32] = {0};
+            base64_encode(rand_key, 16, ws_key);
+            http_req->headers[SEC_WEBSOCKET_KEY] = ws_key;
+            http_req->headers[SEC_WEBSOCKET_VERSION] = "13";
+            std::string http_msg = http_req->Dump(true, true);
+            // printf("%s", http_msg.c_str());
+            // NOTE: not use WebSocketChannel::send
+            channel->write(http_msg);
+            state = WS_UPGRADING;
+            // prepare HttpParser
+            http_parser.reset(HttpParser::New(HTTP_CLIENT, HTTP_V1));
+            http_resp.reset(new HttpResponse);
+            http_parser->InitResponse(http_resp.get());
+        } else {
+            state = WS_CLOSED;
+            if (onclose) onclose();
+        }
+    };
+    onMessage = [this](const WebSocketChannelPtr& channel, Buffer* buf) {
+        if (state == WS_UPGRADING) {
+            int nparse = http_parser->FeedRecvData((const char*)buf->data(), buf->size());
+            if (nparse != buf->size()) {
+                hloge("http parse error!");
+                channel->close();
+                return;
+            }
+            if (http_parser->IsComplete()) {
+                if (http_resp->status_code != HTTP_STATUS_SWITCHING_PROTOCOLS) {
+                    hloge("server side not support websockt!");
+                    channel->close();
+                    return;
+                }
+                std::string ws_key = http_req->GetHeader(SEC_WEBSOCKET_KEY);
+                char ws_accept[32] = {0};
+                ws_encode_key(ws_key.c_str(), ws_accept);
+                std::string ws_accept2 = http_resp->GetHeader(SEC_WEBSOCKET_ACCEPT);
+                if (strcmp(ws_accept, ws_accept2.c_str()) != 0) {
+                    hloge("Sec-WebSocket-Accept not match!");
+                    channel->close();
+                    return;
+                }
+                ws_parser.reset(new WebSocketParser);
+                // websocket_onmessage
+                ws_parser->onMessage = [this, &channel](int opcode, const std::string& msg) {
+                    switch (opcode) {
+                    case WS_OPCODE_CLOSE:
+                        channel->close();
+                        break;
+                    case WS_OPCODE_PING:
+                    {
+                        // printf("recv ping\n");
+                        // printf("send pong\n");
+                        channel->write(WS_CLIENT_PONG_FRAME, WS_CLIENT_MIN_FRAME_SIZE);
+                        break;
+                    }
+                    case WS_OPCODE_PONG:
+                        // printf("recv pong\n");
+                        break;
+                    case WS_OPCODE_TEXT:
+                    case WS_OPCODE_BINARY:
+                        if (onmessage) onmessage(msg);
+                        break;
+                    default:
+                        break;
+                    }
+                };
+                state = WS_OPENED;
+                if (onopen) onopen();
+            }
+        } else {
+            int nparse = ws_parser->FeedRecvData((const char*)buf->data(), buf->size());
+            if (nparse != buf->size()) {
+                hloge("websocket parse error!");
+                channel->close();
+                return;
+            }
+        }
+    };
+
+    state = CONNECTING;
+    start();
+    return 0;
+}
+
+int WebSocketClient::close() {
+    if (channel == NULL) return -1;
+    channel->close();
+    stop();
+    state = WS_CLOSED;
+    return 0;
+}
+
+int WebSocketClient::send(const std::string& msg) {
+    if (channel == NULL) return -1;
+    return channel->send(msg);
+}
+
+}

+ 49 - 0
http/client/WebSocketClient.h

@@ -0,0 +1,49 @@
+#ifndef HV_WEBSOCKET_CLIENT_H_
+#define HV_WEBSOCKET_CLIENT_H_
+
+/*
+ * @demo examples/websocket_client_test.cpp
+ */
+
+#include "hexport.h"
+
+#include "TcpClient.h"
+#include "WebSocketChannel.h"
+
+#include "HttpParser.h"
+#include "WebSocketParser.h"
+
+namespace hv {
+
+class HV_EXPORT WebSocketClient : public TcpClientTmpl<WebSocketChannel> {
+public:
+    std::string           url;
+    std::function<void()> onopen;
+    std::function<void()> onclose;
+    std::function<void(const std::string& msg)> onmessage;
+
+    WebSocketClient();
+    ~WebSocketClient();
+
+    // ws://127.0.0.1:8080/
+    int open(const char* url);
+    int close();
+    int send(const std::string& msg);
+
+private:
+    enum State {
+        CONNECTING,
+        CONNECTED,
+        WS_UPGRADING,
+        WS_OPENED,
+        WS_CLOSED,
+    } state;
+    HttpParserPtr       http_parser;
+    HttpRequestPtr      http_req;
+    HttpResponsePtr     http_resp;
+    WebSocketParserPtr  ws_parser;
+};
+
+}
+
+#endif // HV_WEBSOCKET_CLIENT_H_

+ 4 - 0
http/server/WebSocketServer.h

@@ -1,6 +1,10 @@
 #ifndef HV_WEBSOCKET_SERVER_H_
 #define HV_WEBSOCKET_SERVER_H_
 
+/*
+ * @demo examples/websocket_server_test.cpp
+ */
+
 #include "HttpServer.h"
 #include "WebSocketChannel.h"
 

+ 1 - 0
readme_cn.md

@@ -16,6 +16,7 @@
 - WITH_OPENSSL or WITH_MBEDTLS
 - http client/server (include https http1/x http2 grpc)
 - http web service, indexof service, api service (support RESTful API)
+- websocket client/server
 - protocols
     - dns
     - ftp