diff --git a/README.md b/README.md index 5b6c86b..30b79dd 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,19 @@ implementation file. It can serve as a cruft-free concise reference. You are most welcome to use this code as a reference for creating alternative implementations that may better suit your needs. +News +==== + +*2014-12-06* +Binary frames now supported. Closes issue #38. Automated integration testing +is now supported by running `make test`. The test suite expects GoogleTest to +be installed at `/usr/src/gtest` (`apt-get install libgtest-dev` does the +trick). The test suite uses C++14 (for lambda capture expressions), and thus it +will not work on older compilers. Note that easywsclient itself still +restricted to C++98/C++03, and will continue to build with older compilers. + + + Usage ===== diff --git a/easywsclient.cpp b/easywsclient.cpp index bcc152d..266932d 100644 --- a/easywsclient.cpp +++ b/easywsclient.cpp @@ -74,6 +74,9 @@ #include "easywsclient.hpp" +using easywsclient::Callback_Imp; +using easywsclient::BytesCallback_Imp; + namespace { // private module-only namespace socket_t hostname_connect(const std::string& hostname, int port) { @@ -112,14 +115,16 @@ class _DummyWebSocket : public easywsclient::WebSocket public: void poll(int timeout) { } void send(const std::string& message) { } + void sendBinary(const std::string& message) { } + void sendBinary(const std::vector& message) { } void sendPing() { } void close() { } - void _dispatch(Callback & callable) { } readyStateValues getReadyState() const { return CLOSED; } + void _dispatch(Callback_Imp & callable) { } + void _dispatchBinary(BytesCallback_Imp& callable) { } }; - class _RealWebSocket : public easywsclient::WebSocket { public: @@ -242,7 +247,22 @@ class _RealWebSocket : public easywsclient::WebSocket // lambda: //template //void dispatch(Callable callable) - virtual void _dispatch(WebSocket::Callback & callable) { + virtual void _dispatch(Callback_Imp & callable) { + struct CallbackAdapter : public BytesCallback_Imp + // Adapt void(const std::string&) to void(const std::string&) + { + Callback_Imp& callable; + CallbackAdapter(Callback_Imp& callable) : callable(callable) { } + void operator()(const std::vector& message) { + std::string stringMessage(message.begin(), message.end()); + callable(stringMessage); + } + }; + CallbackAdapter bytesCallback(callable); + _dispatchBinary(bytesCallback); + } + + virtual void _dispatchBinary(BytesCallback_Imp & callable) { // TODO: consider acquiring a lock on rxbuf... while (true) { wsheader_type ws; @@ -293,14 +313,15 @@ class _RealWebSocket : public easywsclient::WebSocket // We got a whole message, now do something with it: if (false) { } - else if (ws.opcode == wsheader_type::TEXT_FRAME + else if ( + ws.opcode == wsheader_type::TEXT_FRAME + || ws.opcode == wsheader_type::BINARY_FRAME || ws.opcode == wsheader_type::CONTINUATION ) { if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed if (ws.fin) { - std::string data(receivedData.begin(), receivedData.end()); - callable((const std::string) data); + callable((const std::vector) receivedData); receivedData.erase(receivedData.begin(), receivedData.end()); std::vector ().swap(receivedData);// free memory } @@ -308,7 +329,7 @@ class _RealWebSocket : public easywsclient::WebSocket else if (ws.opcode == wsheader_type::PING) { if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N); - sendData(wsheader_type::PONG, data); + sendData(wsheader_type::PONG, data.size(), data.begin(), data.end()); } else if (ws.opcode == wsheader_type::PONG) { } else if (ws.opcode == wsheader_type::CLOSE) { close(); } @@ -319,14 +340,24 @@ class _RealWebSocket : public easywsclient::WebSocket } void sendPing() { - sendData(wsheader_type::PING, std::string()); + std::string empty; + sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end()); } void send(const std::string& message) { - sendData(wsheader_type::TEXT_FRAME, message); + sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end()); } - void sendData(wsheader_type::opcode_type type, const std::string& message) { + void sendBinary(const std::string& message) { + sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); + } + + void sendBinary(const std::vector& message) { + sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); + } + + template + void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) { // TODO: // Masking key should (must) be derived from a high quality random // number generator, to mitigate attacks on non-WebSocket friendly @@ -335,7 +366,6 @@ class _RealWebSocket : public easywsclient::WebSocket // TODO: consider acquiring a lock on txbuf... if (readyState == CLOSING || readyState == CLOSED) { return; } std::vector header; - uint64_t message_size = message.size(); header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0); header[0] = 0x80 | type; if (false) { } @@ -378,9 +408,9 @@ class _RealWebSocket : public easywsclient::WebSocket } // N.B. - txbuf will keep growing until it can be transmitted over the socket: txbuf.insert(txbuf.end(), header.begin(), header.end()); - txbuf.insert(txbuf.end(), message.begin(), message.end()); + txbuf.insert(txbuf.end(), message_begin, message_end); if (useMask) { - for (size_t i = 0; i != message.size(); ++i) { *(txbuf.end() - message.size() + i) ^= masking_key[i&0x3]; } + for (size_t i = 0; i != message_size; ++i) { *(txbuf.end() - message_size + i) ^= masking_key[i&0x3]; } } } diff --git a/easywsclient.hpp b/easywsclient.hpp index 3607b66..08c4a7b 100644 --- a/easywsclient.hpp +++ b/easywsclient.hpp @@ -9,9 +9,13 @@ // wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp #include +#include namespace easywsclient { +struct Callback_Imp { virtual void operator()(const std::string& message) = 0; }; +struct BytesCallback_Imp { virtual void operator()(const std::vector& message) = 0; }; + class WebSocket { public: typedef WebSocket * pointer; @@ -26,12 +30,17 @@ class WebSocket { virtual ~WebSocket() { } virtual void poll(int timeout = 0) = 0; // timeout in milliseconds virtual void send(const std::string& message) = 0; + virtual void sendBinary(const std::string& message) = 0; + virtual void sendBinary(const std::vector& message) = 0; virtual void sendPing() = 0; virtual void close() = 0; virtual readyStateValues getReadyState() const = 0; + template - void dispatch(Callable callable) { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers - struct _Callback : public Callback { + void dispatch(Callable callable) + // For callbacks that accept a string argument. + { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers + struct _Callback : public Callback_Imp { Callable& callable; _Callback(Callable& callable) : callable(callable) { } void operator()(const std::string& message) { callable(message); } @@ -40,9 +49,22 @@ class WebSocket { _dispatch(callback); } + template + void dispatchBinary(Callable callable) + // For callbacks that accept a std::vector argument. + { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers + struct _Callback : public BytesCallback_Imp { + Callable& callable; + _Callback(Callable& callable) : callable(callable) { } + void operator()(const std::vector& message) { callable(message); } + }; + _Callback callback(callable); + _dispatchBinary(callback); + } + protected: - struct Callback { virtual void operator()(const std::string& message) = 0; }; - virtual void _dispatch(Callback& callable) = 0; + virtual void _dispatch(Callback_Imp& callable) = 0; + virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0; }; } // namespace easywsclient diff --git a/test/Makefile b/test/Makefile index 82796ee..cc1bc31 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,5 +1,7 @@ +include gtest.mk + CXXFLAGS = -std=gnu++1y -Wall -I.. -LDLIBS = -lstdc++ +LDLIBS += .PHONY: all clean test: easywsclient.t node testServer.js & sleep 1 && ./easywsclient.t @@ -7,5 +9,5 @@ clean: -rm easywsclient.t *.o vpath %.cpp ../ vpath %.hpp ../ -easywsclient.t: easywsclient.t.o easywsclient.o +easywsclient.t: easywsclient.t.o easywsclient.o gtest-all.o easywsclient.o: easywsclient.cpp easywsclient.hpp diff --git a/test/easywsclient.t.cpp b/test/easywsclient.t.cpp index df1cadf..1a0b0e2 100644 --- a/test/easywsclient.t.cpp +++ b/test/easywsclient.t.cpp @@ -11,31 +11,10 @@ #include #include #include +#include using easywsclient::WebSocket; -template -std::string toString(const T& t, size_t maxLen=32) -{ - std::stringstream ss; - ss << t; - std::string s = ss.str(); - if (s.length() > maxLen) { - return s.substr(0, maxLen-3) + "..."; - } - else { - return s; - } -} - -#define ASSERT_EQ(a, b) \ - if ((a) != (b)) { \ - std::cout << "Failed: " #a " == " #b "\n" \ - << "Expected: " << toString(b) << "\n" \ - << " Actual: " << toString(a) << "\n"; \ - throw std::runtime_error("test assertion failed"); \ - } - namespace { #ifdef _WIN32 @@ -88,16 +67,11 @@ std::string makeString(size_t length) } -void test() +TEST(easywsclient, textFramesWork) { -#ifdef _WIN32 - WSAInit wsaInit; -#endif - KillServer killServer; std::unique_ptr ws(WebSocket::from_url("ws://localhost:8123/echoWithSize")); assert(ws); - ws->send("four"); std::string message; while (ws->getReadyState() != WebSocket::CLOSED) { @@ -112,7 +86,13 @@ void test() } } ASSERT_EQ("4\nfour", message); + ws->close(); // hmmm... shouldn't this be RAII? +} +TEST(easywsclient, longTextFramesWork) +{ + std::unique_ptr ws(WebSocket::from_url("ws://localhost:8123/echoWithSize")); + assert(ws); std::vector > v; v.emplace_back( "0", makeString(0)); v.emplace_back( "1", makeString(1)); @@ -134,7 +114,6 @@ void test() v.emplace_back("65535", makeString(65535)); v.emplace_back("65536", makeString(65536)); v.emplace_back("65537", makeString(65537)); - for (auto i = v.begin(); i != v.end(); ++i) { ws->send(i->second); std::string message; @@ -151,17 +130,36 @@ void test() } ASSERT_EQ(i->first + "\n" + i->second, message); } - - ws->close(); + ws->close(); // hmmm... shouldn't this be RAII? } -int main() +TEST(easywsclient, binaryFramesWork) { - try { - test(); + std::unique_ptr ws(WebSocket::from_url("ws://localhost:8123/binaryEchoWithSize")); + assert(ws); + ws->sendBinary(std::vector({1, 2, 3})); + std::vector message; + while (ws->getReadyState() != WebSocket::CLOSED) { + bool gotMessage = false; + ws->poll(); + ws->dispatchBinary([gotMessageOut=&gotMessage, messageOut=&message, ws=ws.get()](const std::vector& message) { + *gotMessageOut = true; + *messageOut = message; + }); + if (gotMessage) { + break; + } } - catch (...) { - throw; - } - return 0; + ASSERT_EQ(std::vector({0, 0, 0, 3, 1, 2, 3}), message); + ws->close(); // hmmm... shouldn't this be RAII? +} + +int main(int argc, char **argv) +{ +#ifdef _WIN32 + WSAInit wsaInit; +#endif + KillServer killServer; // RAII to ensure server gets terminated when tests terminate + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/test/testServer.js b/test/testServer.js index 3653edb..2d456a1 100644 --- a/test/testServer.js +++ b/test/testServer.js @@ -31,6 +31,23 @@ wssEchoWithSize.on('connection', function(ws) { }); }); +var wssBinaryEchoWithSize = new WebSocketServer({server: app, path: '/binaryEchoWithSize'}); +wssBinaryEchoWithSize.on('connection', function(ws) { + ws.on('message', function(data, flags) { + if (!flags.binary) { return; } + //var result = new ArrayBuffer(data.length + 4); + //new DataView(result).setInt32(0, data.length, false); // false = big endian + var result = new Buffer(data.length + 4); + result.writeInt32BE(data.length, 0); + data.copy(result, 4, 0, data.length); + ws.send(result, { binary: true }); + }); + ws.on('close', function() { + }); + ws.on('error', function(e) { + }); +}); + var wssKillServer = new WebSocketServer({server: app, path: '/killServer'}); wssKillServer.on('connection', function(ws) { ws.on('message', function(data, flags) {