Converting to PIMPL, adding a dummy WebSocket, and introducing a compilation

unit via EASYWSCLIENT_COMPILATION_UNIT.
This commit is contained in:
David Baird
2012-08-20 13:29:53 -06:00
parent d4e6176868
commit ff7780f09b
4 changed files with 179 additions and 92 deletions
+31 -10
View File
@@ -15,9 +15,14 @@ pretty conventional). It does not require you to be using this-or-that
particular asynchronous library. It only requires your OS to support particular asynchronous library. It only requires your OS to support
sockets! sockets!
More rationale: So, you were handed a project that needs a WebSocket.
You must do a demo right away. You don't have time to figure out how the
project's magic asynchronous event processing works (or worse, it doesn't
have any consistency). So what do you do? Panic? No. Use this library.
However! This is probably not the end-point for your project, However! This is probably not the end-point for your project,
as this library puts a lot of crap into the header file as this library puts a lot of crap into the header file
(easier to use, but eliminates the benefits of [separate (easier to use, but reduces the benefits of [separate
compilation](http://en.wikipedia.org/wiki/Single_Compilation_Unit)). compilation](http://en.wikipedia.org/wiki/Single_Compilation_Unit)).
Also, this library does not work in cooperation with any asynchronous Also, this library does not work in cooperation with any asynchronous
event processing scheduler. The good news is that the code here is event processing scheduler. The good news is that the code here is
@@ -35,6 +40,8 @@ The interface looks somewhat like this:
// Factory method to create a WebSocket: // Factory method to create a WebSocket:
static pointer from_url(std::string url); static pointer from_url(std::string url);
// Factory method to create a dummy WebSocket (all operations are noop):
static pointer create_dummy();
// Function to perform actual network send()/recv() I/O: // Function to perform actual network send()/recv() I/O:
void poll(); void poll();
@@ -53,14 +60,26 @@ The interface looks somewhat like this:
Put altogether, this will look something like this: Put altogether, this will look something like this:
using easywsclient::WebSocket; // This #define must occur in _exactly one_ of your .cpp files, before
WebSocket::pointer ws = WebSocket::from_url("ws://localhost:8126/foo"); // #including the header. (This will put private implementation details in
assert(ws); // just that one file.):
while (true) { #define EASYWSCLIENT_COMPILATION_UNIT // <-- must be put in exactly one .cpp file
ws->poll(); #include "easywsclient.hpp"
ws->send("hello");
ws->dispatch(handle_message); int
// ...do more stuff... main()
{
...
using easywsclient::WebSocket;
WebSocket::pointer ws = WebSocket::from_url("ws://localhost:8126/foo");
assert(ws);
while (true) {
ws->poll();
ws->send("hello");
ws->dispatch(handle_message);
// ...do more stuff...
}
...
} }
Example Example
@@ -86,7 +105,9 @@ Threading
========= =========
This library is not thread safe. The user must take care to use locks if This library is not thread safe. The user must take care to use locks if
accessing an instance of `WebSocket` from multiple threads. accessing an instance of `WebSocket` from multiple threads. If you need
a quick threading library and don't have Boost or something else already,
I recommend [TinyThread++](http://tinythreadpp.bitsnbites.eu/).
Future Work Future Work
=========== ===========
+146 -82
View File
@@ -21,9 +21,60 @@
namespace easywsclient { namespace easywsclient {
struct WebSocket struct WebSocket {
{
typedef WebSocket * pointer; typedef WebSocket * pointer;
struct Callback { virtual void operator()(const std::string & message) = 0; };
// Factories (in the spirit of PIMPL):
static pointer create_dummy();
static pointer from_url(std::string url);
static int hostname_connect(std::string hostname, int port); // <-- convenience function
// Interfaces:
virtual ~WebSocket() { }
virtual void poll() = 0;
virtual void send(std::string message) = 0;
template<class Callable>
void dispatch(Callable callable) {
// ...don't you just want to love C++, right about here?
// (N.B. this is why the C++11 lambda syntax was invented)
struct _Callback : public Callback {
Callable & callable;
_Callback(Callable & callable) : callable(callable) { }
void operator()(const std::string & message) { callable(message); }
};
_Callback callback(callable);
_dispatch(callback);
}
private:
virtual void _dispatch(Callback & callable) = 0;
};
// Everything in this #if block could be separated out into a .cpp file, if you
// so desired,
#if defined(EASYWSCLIENT_COMPILATION_UNIT)
struct _DummyWebSocket : public WebSocket
{
void poll() { }
void send(std::string message) { }
void _dispatch(Callback & callable) { }
};
struct _RealWebSocket : public WebSocket
{
#if 0 #if 0
http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
@@ -69,85 +120,8 @@ struct WebSocket
int sockfd; int sockfd;
bool closed; bool closed;
static int hostname_connect(std::string hostname, int port) {
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *p;
int ret;
int sockfd = -1;
char sport[16];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(sport, 16, "%d", port);
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
for(p = result; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) { continue; }
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != -1) {
break;
}
close(sockfd);
sockfd = -1;
}
freeaddrinfo(result);
return sockfd;
}
static pointer from_url(std::string url) { _RealWebSocket(int sockfd) : sockfd(sockfd), closed(false) {
char host[128];
int port;
char path[128];
if (false) { }
else if (sscanf(url.c_str(), "ws://%[^:]:%d/%s", host, &port, path) == 3) {
}
else if (sscanf(url.c_str(), "ws://%[^/]/%s", host, path) == 2) {
port = 80;
}
else {
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
return NULL;
}
int sockfd = hostname_connect(host, port);
if (sockfd == -1) {
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
return NULL;
}
{
// XXX: this should be done non-blocking,
char line[256];
int status;
int i;
snprintf(line, 256, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Upgrade: websocket\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Connection: Upgrade\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Sec-WebSocket-Version: 13\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
for (i = 0; i < 2 || i < 255 && line[i-2] != '\r' && line[i-1] != '\n'; ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
line[i] = 0;
if (i == 255) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
// TODO: verify response headers,
while (true) {
for (i = 0; i < 2 || i < 255 && line[i-2] != '\r' && line[i-1] != '\n'; ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
if (line[0] == '\r' && line[1] == '\n') { break; }
}
}
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
fcntl(sockfd, F_SETFL, O_NONBLOCK);
fprintf(stderr, "Connected to: %s\n", url.c_str());
return pointer(new WebSocket(sockfd));
}
WebSocket(int sockfd) : sockfd(sockfd), closed(false) {
} }
void poll() { void poll() {
@@ -184,8 +158,9 @@ struct WebSocket
// Callable must have signature: void(const std::string & message). // Callable must have signature: void(const std::string & message).
// Should work with C functions, C++ functors, and C++11 std::function and // Should work with C functions, C++ functors, and C++11 std::function and
// lambda: // lambda:
template<class Callable> //template<class Callable>
void dispatch(Callable callable) { //void dispatch(Callable callable) {
virtual void _dispatch(WebSocket::Callback & 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;
@@ -281,6 +256,95 @@ struct WebSocket
}; };
int WebSocket::hostname_connect(std::string hostname, int port) {
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *p;
int ret;
int sockfd = -1;
char sport[16];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(sport, 16, "%d", port);
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
for(p = result; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) { continue; }
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != -1) {
break;
}
close(sockfd);
sockfd = -1;
}
freeaddrinfo(result);
return sockfd;
}
WebSocket::pointer WebSocket::create_dummy() {
static pointer dummy = pointer(new _DummyWebSocket);
return dummy;
}
WebSocket::pointer WebSocket::from_url(std::string url) {
char host[128];
int port;
char path[128];
if (false) { }
else if (sscanf(url.c_str(), "ws://%[^:]:%d/%s", host, &port, path) == 3) {
}
else if (sscanf(url.c_str(), "ws://%[^/]/%s", host, path) == 2) {
port = 80;
}
else {
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
return NULL;
}
int sockfd = hostname_connect(host, port);
if (sockfd == -1) {
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
return NULL;
}
{
// XXX: this should be done non-blocking,
char line[256];
int status;
int i;
snprintf(line, 256, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Upgrade: websocket\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Connection: Upgrade\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "Sec-WebSocket-Version: 13\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 256, "\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
for (i = 0; i < 2 || i < 255 && line[i-2] != '\r' && line[i-1] != '\n'; ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
line[i] = 0;
if (i == 255) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
// TODO: verify response headers,
while (true) {
for (i = 0; i < 2 || i < 255 && line[i-2] != '\r' && line[i-1] != '\n'; ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
if (line[0] == '\r' && line[1] == '\n') { break; }
}
}
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
fcntl(sockfd, F_SETFL, O_NONBLOCK);
fprintf(stderr, "Connected to: %s\n", url.c_str());
return pointer(new _RealWebSocket(sockfd));
}
#endif // defined(EASYWSCLIENT_COMPILATION_UNIT)
} // namespace easywsclient } // namespace easywsclient
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */ #endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
+1
View File
@@ -1,5 +1,6 @@
// Compile with: // Compile with:
// g++ -std=gnu++0x example-client-cpp11.cpp -o example-client-cpp11 // g++ -std=gnu++0x example-client-cpp11.cpp -o example-client-cpp11
#define EASYWSCLIENT_COMPILATION_UNIT
#include "easywsclient.hpp" #include "easywsclient.hpp"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
+1
View File
@@ -1,3 +1,4 @@
#define EASYWSCLIENT_COMPILATION_UNIT
#include "easywsclient.hpp" #include "easywsclient.hpp"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>