mirror of https://github.com/adamdruppe/arsd.git
websocket polling/blocking api
This commit is contained in:
parent
9ad7b08ce0
commit
acb5825c7c
243
http2.d
243
http2.d
|
@ -2052,11 +2052,70 @@ private bool bicmp(in ubyte[] item, in char[] search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
WebSocket client, based on the browser api.
|
WebSocket client, based on the browser api, though also with other api options.
|
||||||
|
|
||||||
|
---
|
||||||
|
auto ws = new WebSocket(URI("ws://...."));
|
||||||
|
|
||||||
|
ws.onmessage = (in char[] msg) {
|
||||||
|
ws.send("a reply");
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.connect();
|
||||||
|
|
||||||
|
WebSocket.eventLoop();
|
||||||
|
---
|
||||||
|
|
||||||
|
Symbol_groups:
|
||||||
|
foundational =
|
||||||
|
Used with all API styles.
|
||||||
|
|
||||||
|
browser_api =
|
||||||
|
API based on the standard in the browser.
|
||||||
|
|
||||||
|
event_loop_integration =
|
||||||
|
Integrating with external event loops is done through static functions. You should
|
||||||
|
call these BEFORE doing anything else with the WebSocket module or class.
|
||||||
|
|
||||||
|
$(PITFALL NOT IMPLEMENTED)
|
||||||
|
---
|
||||||
|
WebSocket.setEventLoopProxy(arsd.simpledisplay.EventLoop.proxy.tupleof);
|
||||||
|
// or something like that. it is not implemented yet.
|
||||||
|
---
|
||||||
|
$(PITFALL NOT IMPLEMENTED)
|
||||||
|
|
||||||
|
blocking_api =
|
||||||
|
The blocking API is best used when you only need basic functionality with a single connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
WebSocketMessage msg;
|
||||||
|
do {
|
||||||
|
// FIXME good demo
|
||||||
|
} while(msg);
|
||||||
|
---
|
||||||
|
|
||||||
|
Or to check for blocks before calling:
|
||||||
|
|
||||||
|
---
|
||||||
|
try_to_process_more:
|
||||||
|
while(ws.isMessageBuffered()) {
|
||||||
|
auto msg = ws.waitForNextMessage();
|
||||||
|
// process msg
|
||||||
|
}
|
||||||
|
if(ws.isDataPending()) {
|
||||||
|
ws.lowLevelReceive();
|
||||||
|
goto try_to_process_more;
|
||||||
|
} else {
|
||||||
|
// nothing ready, you can do other things
|
||||||
|
// or at least sleep a while before trying
|
||||||
|
// to process more.
|
||||||
|
if(ws.readyState == WebSocket.OPEN) {
|
||||||
|
Thread.sleep(1.seconds);
|
||||||
|
goto try_to_process_more;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
on receive
|
|
||||||
on frame
|
|
||||||
on message
|
|
||||||
+/
|
+/
|
||||||
class WebSocket {
|
class WebSocket {
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
|
@ -2083,6 +2142,7 @@ class WebSocket {
|
||||||
/++
|
/++
|
||||||
|
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
static struct Config {
|
static struct Config {
|
||||||
/++
|
/++
|
||||||
These control the size of the receive buffer.
|
These control the size of the receive buffer.
|
||||||
|
@ -2104,6 +2164,8 @@ class WebSocket {
|
||||||
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
|
string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;
|
||||||
string origin; /// Origin URL to send with the handshake, if desired.
|
string origin; /// Origin URL to send with the handshake, if desired.
|
||||||
string protocol; /// the protocol header, if desired.
|
string protocol; /// the protocol header, if desired.
|
||||||
|
|
||||||
|
int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
@ -2116,6 +2178,7 @@ class WebSocket {
|
||||||
/++
|
/++
|
||||||
wss://echo.websocket.org
|
wss://echo.websocket.org
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
this(Uri uri, Config config = Config.init)
|
this(Uri uri, Config config = Config.init)
|
||||||
in (uri.scheme == "ws" || uri.scheme == "wss")
|
in (uri.scheme == "ws" || uri.scheme == "wss")
|
||||||
{
|
{
|
||||||
|
@ -2141,6 +2204,7 @@ wss://echo.websocket.org
|
||||||
/++
|
/++
|
||||||
|
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
void connect() {
|
void connect() {
|
||||||
socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support...
|
socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support...
|
||||||
// FIXME: websocket handshake could and really should be async too.
|
// FIXME: websocket handshake could and really should be async too.
|
||||||
|
@ -2289,7 +2353,7 @@ wss://echo.websocket.org
|
||||||
|
|
||||||
|
|
||||||
// FIXME: check protocol if config requested one
|
// FIXME: check protocol if config requested one
|
||||||
// FIXME: check accept
|
// FIXME: check accept for the right hash
|
||||||
|
|
||||||
receiveBuffer[0 .. used.length] = used[];
|
receiveBuffer[0 .. used.length] = used[];
|
||||||
receiveBufferUsedLength = used.length;
|
receiveBufferUsedLength = used.length;
|
||||||
|
@ -2301,8 +2365,9 @@ wss://echo.websocket.org
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
Closes the connection, sending a graceful teardown message to the other side.
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
void close(int code = 0, string reason = null)
|
void close(int code = 0, string reason = null)
|
||||||
in (reason.length < 123)
|
in (reason.length < 123)
|
||||||
{
|
{
|
||||||
|
@ -2318,8 +2383,9 @@ wss://echo.websocket.org
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
void ping() {
|
void ping() {
|
||||||
WebSocketMessage wss;
|
WebSocketMessage wss;
|
||||||
wss.fin = true;
|
wss.fin = true;
|
||||||
|
@ -2336,8 +2402,9 @@ wss://echo.websocket.org
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
Sends a text message through the websocket.
|
||||||
+/
|
+/
|
||||||
|
/// Group: foundational
|
||||||
void send(in char[] textData) {
|
void send(in char[] textData) {
|
||||||
WebSocketMessage wss;
|
WebSocketMessage wss;
|
||||||
wss.fin = true;
|
wss.fin = true;
|
||||||
|
@ -2346,6 +2413,19 @@ wss://echo.websocket.org
|
||||||
wss.send(&llsend);
|
wss.send(&llsend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Sends a binary message through the websocket.
|
||||||
|
+/
|
||||||
|
/// Group: foundational
|
||||||
|
void send(in ubyte[] binaryData) {
|
||||||
|
WebSocketMessage wss;
|
||||||
|
wss.fin = true;
|
||||||
|
wss.opcode = WebSocketOpcode.binary;
|
||||||
|
wss.data = cast(ubyte[]) binaryData;
|
||||||
|
wss.send(&llsend);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void llsend(ubyte[] d) {
|
private void llsend(ubyte[] d) {
|
||||||
while(d.length) {
|
while(d.length) {
|
||||||
auto r = socket.send(d);
|
auto r = socket.send(d);
|
||||||
|
@ -2354,7 +2434,13 @@ wss://echo.websocket.org
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private*/ bool llreceive() {
|
/++
|
||||||
|
Waits for more data off the low-level socket and adds it to the pending buffer.
|
||||||
|
|
||||||
|
Returns `true` if the connection is still active.
|
||||||
|
+/
|
||||||
|
/// Group: blocking_api
|
||||||
|
public bool lowLevelReceive() {
|
||||||
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
|
auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]);
|
||||||
if(r == 0)
|
if(r == 0)
|
||||||
return false;
|
return false;
|
||||||
|
@ -2364,47 +2450,146 @@ wss://echo.websocket.org
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void autoprocess() {
|
/++
|
||||||
|
Waits for and returns the next complete message on the socket.
|
||||||
|
|
||||||
|
Note that the onmessage function is still called, right before
|
||||||
|
this returns.
|
||||||
|
+/
|
||||||
|
/// Group: blocking_api
|
||||||
|
public WebSocketMessage waitForNextMessage() {
|
||||||
do {
|
do {
|
||||||
|
auto m = processOnce();
|
||||||
|
if(m.populated)
|
||||||
|
return m;
|
||||||
|
} while(lowLevelReceive());
|
||||||
|
|
||||||
|
return WebSocketMessage.init; // FIXME? maybe.
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Tells if [waitForNextMessage] would block.
|
||||||
|
+/
|
||||||
|
/// Group: blocking_api
|
||||||
|
public bool waitForNextMessageWouldBlock() {
|
||||||
|
checkAgain:
|
||||||
|
if(isMessageBuffered())
|
||||||
|
return false;
|
||||||
|
if(!isDataPending())
|
||||||
|
return true;
|
||||||
|
while(isDataPending())
|
||||||
|
lowLevelReceive();
|
||||||
|
goto checkAgain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Is there a message in the buffer already?
|
||||||
|
If `true`, [waitForNextMessage] is guaranteed to return immediately.
|
||||||
|
If `false`, check [isDataPending] as the next step.
|
||||||
|
+/
|
||||||
|
/// Group: blocking_api
|
||||||
|
public bool isMessageBuffered() {
|
||||||
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
||||||
auto s = d;
|
auto s = d;
|
||||||
while(d.length) {
|
if(d.length) {
|
||||||
auto orig = d;
|
auto orig = d;
|
||||||
auto m = WebSocketMessage.read(d);
|
auto m = WebSocketMessage.read(d);
|
||||||
|
// that's how it indicates that it needs more data
|
||||||
|
if(d !is orig)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Is data pending on the socket? Also check [isMessageBuffered] to see if there
|
||||||
|
is already a message in memory too.
|
||||||
|
|
||||||
|
If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered]
|
||||||
|
again.
|
||||||
|
+/
|
||||||
|
/// Group: blocking_api
|
||||||
|
public bool isDataPending() {
|
||||||
|
static SocketSet readSet;
|
||||||
|
if(readSet is null)
|
||||||
|
readSet = new SocketSet();
|
||||||
|
|
||||||
|
version(with_openssl)
|
||||||
|
if(auto s = cast(SslClientSocket) socket) {
|
||||||
|
// select doesn't handle the case with stuff
|
||||||
|
// left in the ssl buffer so i'm checking it separately
|
||||||
|
if(s.dataPending()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readSet.add(socket);
|
||||||
|
|
||||||
|
//tryAgain:
|
||||||
|
auto selectGot = Socket.select(readSet, null, null, 0.seconds /* timeout */);
|
||||||
|
if(selectGot == 0) { /* timeout */
|
||||||
|
// timeout
|
||||||
|
return false;
|
||||||
|
} else if(selectGot == -1) { /* interrupted */
|
||||||
|
return false;
|
||||||
|
} else { /* ready */
|
||||||
|
if(readSet.isSet(socket)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebSocketMessage processOnce() {
|
||||||
|
ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];
|
||||||
|
auto s = d;
|
||||||
|
// FIXME: handle continuation frames
|
||||||
|
WebSocketMessage m;
|
||||||
|
if(d.length) {
|
||||||
|
auto orig = d;
|
||||||
|
m = WebSocketMessage.read(d);
|
||||||
|
// that's how it indicates that it needs more data
|
||||||
if(d is orig)
|
if(d is orig)
|
||||||
break;
|
return WebSocketMessage.init;
|
||||||
if(m.opcode == WebSocketOpcode.close) {
|
if(m.opcode == WebSocketOpcode.close) {
|
||||||
readyState_ = CLOSED;
|
readyState_ = CLOSED;
|
||||||
if(onclose)
|
if(onclose)
|
||||||
onclose();
|
onclose();
|
||||||
} else if(m.opcode == WebSocketOpcode.text) {
|
} else if(m.opcode == WebSocketOpcode.text) {
|
||||||
if(onmessage)
|
if(ontextmessage)
|
||||||
onmessage(m.textData);
|
ontextmessage(m.textData);
|
||||||
} else {
|
} else {
|
||||||
// FIXME
|
// FIXME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
receiveBufferUsedLength -= s.length - d.length;
|
receiveBufferUsedLength -= s.length - d.length;
|
||||||
} while(llreceive());
|
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void autoprocess() {
|
||||||
|
// FIXME
|
||||||
|
do {
|
||||||
|
processOnce();
|
||||||
|
} while(lowLevelReceive());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void delegate() onclose; ///
|
||||||
|
void delegate() onerror; ///
|
||||||
|
void delegate(in char[]) ontextmessage; ///
|
||||||
|
void delegate() onopen; ///
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
|
||||||
+/
|
+/
|
||||||
void send(in ubyte[] binaryData) {
|
/// Group: browser_api
|
||||||
WebSocketMessage wss;
|
void onmessage(void delegate(in char[]) dg) {
|
||||||
wss.fin = true;
|
ontextmessage = dg;
|
||||||
wss.opcode = WebSocketOpcode.binary;
|
|
||||||
wss.data = cast(ubyte[]) binaryData;
|
|
||||||
wss.send(&llsend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void delegate() onclose; ///
|
|
||||||
void delegate() onerror; ///
|
|
||||||
void delegate(in char[]) onmessage; ///
|
|
||||||
void delegate() onopen; ///
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const int bufferedAmount // amount pending
|
const int bufferedAmount // amount pending
|
||||||
const string extensions
|
const string extensions
|
||||||
|
@ -2417,6 +2602,7 @@ wss://echo.websocket.org
|
||||||
/* copy/paste from cgi.d */
|
/* copy/paste from cgi.d */
|
||||||
private {
|
private {
|
||||||
enum WebSocketOpcode : ubyte {
|
enum WebSocketOpcode : ubyte {
|
||||||
|
continuation = 0,
|
||||||
text = 1,
|
text = 1,
|
||||||
binary = 2,
|
binary = 2,
|
||||||
// 3, 4, 5, 6, 7 RESERVED
|
// 3, 4, 5, 6, 7 RESERVED
|
||||||
|
@ -2426,7 +2612,8 @@ private {
|
||||||
// 11,12,13,14,15 RESERVED
|
// 11,12,13,14,15 RESERVED
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebSocketMessage {
|
public struct WebSocketMessage {
|
||||||
|
private bool populated;
|
||||||
bool fin;
|
bool fin;
|
||||||
bool rsv1;
|
bool rsv1;
|
||||||
bool rsv2;
|
bool rsv2;
|
||||||
|
@ -2533,6 +2720,8 @@ private {
|
||||||
|
|
||||||
ubyte b = d[0];
|
ubyte b = d[0];
|
||||||
|
|
||||||
|
msg.populated = true;
|
||||||
|
|
||||||
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
|
msg.opcode = cast(WebSocketOpcode) (b & 0x0f);
|
||||||
b >>= 4;
|
b >>= 4;
|
||||||
msg.rsv3 = b & 0x01;
|
msg.rsv3 = b & 0x01;
|
||||||
|
|
Loading…
Reference in New Issue