Add binary frames support and tests, to close feature request #38.
This commit is contained in:
@@ -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
|
are most welcome to use this code as a reference for creating alternative
|
||||||
implementations that may better suit your needs.
|
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
|
Usage
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|||||||
+43
-13
@@ -74,6 +74,9 @@
|
|||||||
|
|
||||||
#include "easywsclient.hpp"
|
#include "easywsclient.hpp"
|
||||||
|
|
||||||
|
using easywsclient::Callback_Imp;
|
||||||
|
using easywsclient::BytesCallback_Imp;
|
||||||
|
|
||||||
namespace { // private module-only namespace
|
namespace { // private module-only namespace
|
||||||
|
|
||||||
socket_t hostname_connect(const std::string& hostname, int port) {
|
socket_t hostname_connect(const std::string& hostname, int port) {
|
||||||
@@ -112,14 +115,16 @@ class _DummyWebSocket : public easywsclient::WebSocket
|
|||||||
public:
|
public:
|
||||||
void poll(int timeout) { }
|
void poll(int timeout) { }
|
||||||
void send(const std::string& message) { }
|
void send(const std::string& message) { }
|
||||||
|
void sendBinary(const std::string& message) { }
|
||||||
|
void sendBinary(const std::vector<uint8_t>& message) { }
|
||||||
void sendPing() { }
|
void sendPing() { }
|
||||||
void close() { }
|
void close() { }
|
||||||
void _dispatch(Callback & callable) { }
|
|
||||||
readyStateValues getReadyState() const { return CLOSED; }
|
readyStateValues getReadyState() const { return CLOSED; }
|
||||||
|
void _dispatch(Callback_Imp & callable) { }
|
||||||
|
void _dispatchBinary(BytesCallback_Imp& callable) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class _RealWebSocket : public easywsclient::WebSocket
|
class _RealWebSocket : public easywsclient::WebSocket
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -242,7 +247,22 @@ class _RealWebSocket : public easywsclient::WebSocket
|
|||||||
// lambda:
|
// lambda:
|
||||||
//template<class Callable>
|
//template<class Callable>
|
||||||
//void dispatch(Callable callable)
|
//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<uint8_t>&) to void(const std::string&)
|
||||||
|
{
|
||||||
|
Callback_Imp& callable;
|
||||||
|
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
|
||||||
|
void operator()(const std::vector<uint8_t>& 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...
|
// TODO: consider acquiring a lock on rxbuf...
|
||||||
while (true) {
|
while (true) {
|
||||||
wsheader_type ws;
|
wsheader_type ws;
|
||||||
@@ -293,14 +313,15 @@ class _RealWebSocket : public easywsclient::WebSocket
|
|||||||
|
|
||||||
// We got a whole message, now do something with it:
|
// We got a whole message, now do something with it:
|
||||||
if (false) { }
|
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
|
|| 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]; } }
|
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
|
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
|
||||||
if (ws.fin) {
|
if (ws.fin) {
|
||||||
std::string data(receivedData.begin(), receivedData.end());
|
callable((const std::vector<uint8_t>) receivedData);
|
||||||
callable((const std::string) data);
|
|
||||||
receivedData.erase(receivedData.begin(), receivedData.end());
|
receivedData.erase(receivedData.begin(), receivedData.end());
|
||||||
std::vector<uint8_t> ().swap(receivedData);// free memory
|
std::vector<uint8_t> ().swap(receivedData);// free memory
|
||||||
}
|
}
|
||||||
@@ -308,7 +329,7 @@ class _RealWebSocket : public easywsclient::WebSocket
|
|||||||
else if (ws.opcode == wsheader_type::PING) {
|
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]; } }
|
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);
|
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::PONG) { }
|
||||||
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
|
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
|
||||||
@@ -319,14 +340,24 @@ class _RealWebSocket : public easywsclient::WebSocket
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendPing() {
|
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) {
|
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<uint8_t>& message) {
|
||||||
|
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Iterator>
|
||||||
|
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
|
||||||
// TODO:
|
// TODO:
|
||||||
// Masking key should (must) be derived from a high quality random
|
// Masking key should (must) be derived from a high quality random
|
||||||
// number generator, to mitigate attacks on non-WebSocket friendly
|
// 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...
|
// TODO: consider acquiring a lock on txbuf...
|
||||||
if (readyState == CLOSING || readyState == CLOSED) { return; }
|
if (readyState == CLOSING || readyState == CLOSED) { return; }
|
||||||
std::vector<uint8_t> header;
|
std::vector<uint8_t> 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.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
|
||||||
header[0] = 0x80 | type;
|
header[0] = 0x80 | type;
|
||||||
if (false) { }
|
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:
|
// 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(), header.begin(), header.end());
|
||||||
txbuf.insert(txbuf.end(), message.begin(), message.end());
|
txbuf.insert(txbuf.end(), message_begin, message_end);
|
||||||
if (useMask) {
|
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]; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-4
@@ -9,9 +9,13 @@
|
|||||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace easywsclient {
|
namespace easywsclient {
|
||||||
|
|
||||||
|
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
|
||||||
|
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
|
||||||
|
|
||||||
class WebSocket {
|
class WebSocket {
|
||||||
public:
|
public:
|
||||||
typedef WebSocket * pointer;
|
typedef WebSocket * pointer;
|
||||||
@@ -26,12 +30,17 @@ class WebSocket {
|
|||||||
virtual ~WebSocket() { }
|
virtual ~WebSocket() { }
|
||||||
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
||||||
virtual void send(const std::string& message) = 0;
|
virtual void send(const std::string& message) = 0;
|
||||||
|
virtual void sendBinary(const std::string& message) = 0;
|
||||||
|
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
|
||||||
virtual void sendPing() = 0;
|
virtual void sendPing() = 0;
|
||||||
virtual void close() = 0;
|
virtual void close() = 0;
|
||||||
virtual readyStateValues getReadyState() const = 0;
|
virtual readyStateValues getReadyState() const = 0;
|
||||||
|
|
||||||
template<class Callable>
|
template<class Callable>
|
||||||
void dispatch(Callable callable) { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
void dispatch(Callable callable)
|
||||||
struct _Callback : public Callback {
|
// 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;
|
Callable& callable;
|
||||||
_Callback(Callable& callable) : callable(callable) { }
|
_Callback(Callable& callable) : callable(callable) { }
|
||||||
void operator()(const std::string& message) { callable(message); }
|
void operator()(const std::string& message) { callable(message); }
|
||||||
@@ -40,9 +49,22 @@ class WebSocket {
|
|||||||
_dispatch(callback);
|
_dispatch(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<class Callable>
|
||||||
|
void dispatchBinary(Callable callable)
|
||||||
|
// For callbacks that accept a std::vector<uint8_t> 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<uint8_t>& message) { callable(message); }
|
||||||
|
};
|
||||||
|
_Callback callback(callable);
|
||||||
|
_dispatchBinary(callback);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct Callback { virtual void operator()(const std::string& message) = 0; };
|
virtual void _dispatch(Callback_Imp& callable) = 0;
|
||||||
virtual void _dispatch(Callback& callable) = 0;
|
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace easywsclient
|
} // namespace easywsclient
|
||||||
|
|||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
|
include gtest.mk
|
||||||
|
|
||||||
CXXFLAGS = -std=gnu++1y -Wall -I..
|
CXXFLAGS = -std=gnu++1y -Wall -I..
|
||||||
LDLIBS = -lstdc++
|
LDLIBS +=
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
test: easywsclient.t
|
test: easywsclient.t
|
||||||
node testServer.js & sleep 1 && ./easywsclient.t
|
node testServer.js & sleep 1 && ./easywsclient.t
|
||||||
@@ -7,5 +9,5 @@ clean:
|
|||||||
-rm easywsclient.t *.o
|
-rm easywsclient.t *.o
|
||||||
vpath %.cpp ../
|
vpath %.cpp ../
|
||||||
vpath %.hpp ../
|
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
|
easywsclient.o: easywsclient.cpp easywsclient.hpp
|
||||||
|
|||||||
+36
-38
@@ -11,31 +11,10 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
using easywsclient::WebSocket;
|
using easywsclient::WebSocket;
|
||||||
|
|
||||||
template<class T>
|
|
||||||
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 {
|
namespace {
|
||||||
|
|
||||||
#ifdef _WIN32
|
#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<WebSocket> ws(WebSocket::from_url("ws://localhost:8123/echoWithSize"));
|
std::unique_ptr<WebSocket> ws(WebSocket::from_url("ws://localhost:8123/echoWithSize"));
|
||||||
assert(ws);
|
assert(ws);
|
||||||
|
|
||||||
ws->send("four");
|
ws->send("four");
|
||||||
std::string message;
|
std::string message;
|
||||||
while (ws->getReadyState() != WebSocket::CLOSED) {
|
while (ws->getReadyState() != WebSocket::CLOSED) {
|
||||||
@@ -112,7 +86,13 @@ void test()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ASSERT_EQ("4\nfour", message);
|
ASSERT_EQ("4\nfour", message);
|
||||||
|
ws->close(); // hmmm... shouldn't this be RAII?
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(easywsclient, longTextFramesWork)
|
||||||
|
{
|
||||||
|
std::unique_ptr<WebSocket> ws(WebSocket::from_url("ws://localhost:8123/echoWithSize"));
|
||||||
|
assert(ws);
|
||||||
std::vector<std::pair<std::string, std::string> > v;
|
std::vector<std::pair<std::string, std::string> > v;
|
||||||
v.emplace_back( "0", makeString(0));
|
v.emplace_back( "0", makeString(0));
|
||||||
v.emplace_back( "1", makeString(1));
|
v.emplace_back( "1", makeString(1));
|
||||||
@@ -134,7 +114,6 @@ void test()
|
|||||||
v.emplace_back("65535", makeString(65535));
|
v.emplace_back("65535", makeString(65535));
|
||||||
v.emplace_back("65536", makeString(65536));
|
v.emplace_back("65536", makeString(65536));
|
||||||
v.emplace_back("65537", makeString(65537));
|
v.emplace_back("65537", makeString(65537));
|
||||||
|
|
||||||
for (auto i = v.begin(); i != v.end(); ++i) {
|
for (auto i = v.begin(); i != v.end(); ++i) {
|
||||||
ws->send(i->second);
|
ws->send(i->second);
|
||||||
std::string message;
|
std::string message;
|
||||||
@@ -151,17 +130,36 @@ void test()
|
|||||||
}
|
}
|
||||||
ASSERT_EQ(i->first + "\n" + i->second, message);
|
ASSERT_EQ(i->first + "\n" + i->second, message);
|
||||||
}
|
}
|
||||||
|
ws->close(); // hmmm... shouldn't this be RAII?
|
||||||
ws->close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
TEST(easywsclient, binaryFramesWork)
|
||||||
{
|
{
|
||||||
try {
|
std::unique_ptr<WebSocket> ws(WebSocket::from_url("ws://localhost:8123/binaryEchoWithSize"));
|
||||||
test();
|
assert(ws);
|
||||||
|
ws->sendBinary(std::vector<uint8_t>({1, 2, 3}));
|
||||||
|
std::vector<uint8_t> message;
|
||||||
|
while (ws->getReadyState() != WebSocket::CLOSED) {
|
||||||
|
bool gotMessage = false;
|
||||||
|
ws->poll();
|
||||||
|
ws->dispatchBinary([gotMessageOut=&gotMessage, messageOut=&message, ws=ws.get()](const std::vector<uint8_t>& message) {
|
||||||
|
*gotMessageOut = true;
|
||||||
|
*messageOut = message;
|
||||||
|
});
|
||||||
|
if (gotMessage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (...) {
|
ASSERT_EQ(std::vector<uint8_t>({0, 0, 0, 3, 1, 2, 3}), message);
|
||||||
throw;
|
ws->close(); // hmmm... shouldn't this be RAII?
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'});
|
var wssKillServer = new WebSocketServer({server: app, path: '/killServer'});
|
||||||
wssKillServer.on('connection', function(ws) {
|
wssKillServer.on('connection', function(ws) {
|
||||||
ws.on('message', function(data, flags) {
|
ws.on('message', function(data, flags) {
|
||||||
|
|||||||
Reference in New Issue
Block a user