Skip to content
This repository

sockets not working #595

Closed
dwj opened this Issue September 26, 2012 · 64 comments

3 participants

dwj Alon Zakai Janus Troelsen
dwj

According to various fixed bugs you have sockets working, but they don't seem to be working for me (unless I'm doing something dumb, which is possible).

This code:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
    int s = socket (AF_INET, SOCK_STREAM, 0);
    printf("got %d\n", s);
}

generates this error:

/Users/davidj/a.out.js:944
  var $3=_socket(1, 200, 0);
         ^
TypeError: undefined is not a function
    at _main (/Users/davidj/a.out.js:944:10)
    at Object.callMain (/Users/davidj/a.out.js:2175:10)
    at doRun (/Users/davidj/a.out.js:2210:20)
    at run (/Users/davidj/a.out.js:2234:12)
    at Object.<anonymous> (/Users/davidj/a.out.js:2256:13)
    at Module._compile (module.js:449:26)
    at Object.Module._extensions..js (module.js:467:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Module.runMain (module.js:492:10)
Janus Troelsen

which bugs are that? this one? i don't think that was merged yet.

dwj

The bugs are 390 and 538. If you look at 538 you'll see they actually use socket() in their code, and as the bug is closed I naturally (but perhaps incorrectly) assumed that socket() was working :)

Janus Troelsen

As you will only get the error when running the code (the call to "socket", that is), it doesn't matter if the code contains calls to "socket" if they don't use it. I think only the definitions were added so that the code compiles. Still doesn't run. Web browsers wouldn't allow raw sockets anyway (see issue 563). I am currently looking into wrapping websock.js from websockify.

Issue 390 is the same. Only thing added are the macro defintions.

Also, consider this: the POSIX socket API is blocking and synchronous by default. How would you emulate that in JavaScript? sleep in Emscripten is currently a busy-wait. Makes select(2) pretty hard to emulate. I was considering only supporting async and non-blocking sockets. It almost wouldn't work with any real life code, but I still think it'd be nice. Does Emscripten have signalling support? We'd need that for O_ASYNC.

dwj

Ok, I see issue 390 that sockets aren't available yet. We use synchronous non-blocking TCP sockets with select() in our code.

Alon Zakai
Owner

Yeah, there has been talk and some headers added, but no socket implementation yet.

It should be very easy to implement select and wait with timeout 0, since that is basically asynchronous. That's how libraries like ENet normally use it anyhow.

Janus Troelsen

What does ENet do when not in select? I imagine it's sleeping? So in Emscripten it will essentially busy-wait when there's no data?

AFAIK, the O_ASYNC sockets would be do-able without busy-waits and

  1. code written using them would work without modification
  2. the SIGIO could be triggered (i.e. its callback called) when the WebSocket has data. Which means no busy-waits.
Alon Zakai
Owner

ENet is just a library, it doesn't do anything by itself. A typical app using ENet like Sauerbraten will, in each iteration of the main loop, call enet_host_service (with timeout 0) to get all events that are ready (until that function says no more messages remain). So, basically the same as SDL_PollEvent.

dwj

We're using select with a non-zero timeout, but it's pretty easy to refactor our code to use a zero timeout - we have to do that anyway to support a single-iteration main loop. I imagine the same is true for most socket code. That seems to be the best compromise (at least, for our code).

Alon Zakai
Owner

Thanks for the info, yeah, that's what I was assuming too. Might need refactoring in some codebases, but should not be too hard, and this is refactoring you need to do anyhow to make the main loop async for the web.

If this feature is important to you, the best thing - aside from a pull request ;) - would be to make a full testcase. That is, some C code that should be compilable into something that works, and some node.js code to run a server with sockets for testing, so that with both of those, I can implement the missing bits and end up with a working test. With that preparation I could implement sockets in emscripten very quickly.

Janus Troelsen

What about something like this:

% ./a.out 
5
74
65
73
74
2
  • Current Emscripten output: var $2=_socket(2, 200, 1); ... TypeError: undefined is not a function
  • Expected Emscripten output (after starting websockify server using "~/websockify/other/websockify -vvv 1101 127.0.0.1:1100" and compiling with adjusted port number [1101 instead of 1100]): Same as native
Janus Troelsen

I wrote inet_pton.

  inet_pton__deps: ['__setErrNo', '$ERRNO_CODES'],
  inet_pton: function(af, src, dst) {
        // int af, const char *src, void *dst
        if ((af ^ {{{ cDefine("AF_INET") }}}) !==  0) { ___setErrNo(ERRNO_CODES.EAFNOSUPPORT); return -1; }
        var b = Pointer_stringify(src).split(".");
        if (b.length !== 4) return 0;
        setValue(dst, (Number(b[0]) | (Number(b[1]) << 8) | (Number(b[2]) << 16) | (Number(b[3]) << 24)), 'i32');
        return 1;
  },
dwj

I've written a quick testcase using our socket library, but it appears that github inexplicably doesn't have any method of attaching files. Anyway, let me know how you'd like it. The testcase is 37 lines long but the socket library is 3k lines so I can't really paste it :)

Janus Troelsen

You can just post a link to a gist made on gist.github.com.

dwj

But even gist won't let you attach files - you have to copy and paste the entire thing, which is a bit ridiculous. Anyway, here is the testcase:

http://www.groupboard.com/emscripten_testcase.zip

To build, just do "g++ *.cpp", then just run a.out. It should connect port 25 on our server, send a HELP command, and print the output. Just press ctrl+c to stop it.

When building with emscripten you may need to add these missing defines: -DFIONREAD=0x541B -DMSG_PEEK=2 -DSO_KEEPALIVE=8 -DO_NDELAY=O_NONBLOCK -D__USE_POSIX -Dunix

Alon Zakai
Owner

Thanks for the code everyone. I'm not sure how to do the server-side of a test for this, though. Is websockify a way to create a websocket server somehow? I'm not familiar with that. It would be best not to add more required deps for our test runner, so I was hoping a pure node.js server could work here. But if not, then we should do it however works of course.

Janus Troelsen

Websockify is basically a wrapper for TCP servers, making them WebSocket servers. I thought it was a good fit since I thought Emscripten was going to emulate the POSIX socket API, which has no support for WebSockets anyway. So if we emulate the POSIX socket API, why not make POSIX sockets servers usable too? That means that the servers need wrapping so that they become WebSocket servers. Websockify provides 1. the server side WebSocket wrapping 2. a thin client-side wrapper to fit with the server side wrapper (websock.js).

I was told yesterday that the author of websockify plans to support Node.js and native Chrome TCP sockets, with the same frontend (websock.js). If we write our own vanilla WebSocket interfacing, we wouldn't get that portability. That would mean that we would need to do our own abstraction of Node.js sockets and WebSockets.

But I do agree dependencies are bad. There is the also the possibility of not emulating the socket API at all, and only speaking WebSocket. But all those TCP servers will then need refactoring to use a native C WebSocket library. With Websockify they just need the wrapping, and we need to speak Websockify's dialect of Websockets (which basically is that binary data is base64 encoded when there is no support, and that we only get a notification that data is buffered, instead of getting it directly. Also stuff like Flash-emulation of Websockets when there is no support for that). Websock.js speaks that dialect, but of course it is a trivial protocol. A bit off-topic: I wrote a net.Stream emulation using Websock.js for my port of node-postgres to the browser, it's here on Github, in /lib/connection.js.

The one-line server I provided above is using netcat, and relies on Websockify to forward Websocket traffic to netcat (that's why the Emscripten port would need to change the port number). If we spoke WebSocket, setting up a server would be more work.

Alon Zakai
Owner

Ah nice, then websockify looks like exactly what we want. It also seems to be able to run as pure python, so we can just bundle it with emscripten.

Janus Troelsen

Note that the Python version has a data transmission problem. I did not have this problem with the C version. There is a node.js version too, for that matter, but I did not try it.

Alon Zakai
Owner

Ok, thanks for all the help so far, I pushed some initial work to the incoming branch now. This should be a complete test (maybe except for the shutdown stuff which is trivial), but no data is received for some reason. onmessage is never called, and we just wait.

This is the first time I do anything with websockets, so perhaps I am misusing the API somehow.

Alon Zakai
Owner

I added a workaround for that python issue (a sleep), but still nothing.

Janus Troelsen

I see you are using the native WebSockets on the client side (here). I don't think that will work. Did you try with websock.js?

But if you think it should: the workaround is not perfect (in my report on websockify I wrote "sometimes"). Did you try with the C server? You can use wstelnet.html from Websockify to test the server. Just enter the WS IP and port and press "connect". It should print test and close the connection extremely quickly. You can click again and get another line. With the Python version it doesn't seem to be working at all (even with the sleep now, I can only get it to work the first time you connect to the server, it seems like the EOF doesn't kill the connection or something)...

Here's a screenshot: http://i.imgur.com/Yw3ah.png Each time I press "Connect", a new line of "test" appears.

Alon Zakai
Owner

I thought that the point of websockify was that it lets you use normal browser websockets? Not a special protocol that requires a clientside library?

Ok, looks like the python server was the issue. Switching to the C server, I do manage to connect. However the data I get looks like garbage for some reason.

edit: oh i see, i need to b64decode it

Alon Zakai
Owner

Ok, websockets test in incoming branch now passes and is enabled.

Let me know if you find any issues and what else we need to implement here (so far we just have the functions used in that testcase, https://github.com/kripken/emscripten/blob/incoming/tests/websockets.c

Janus Troelsen

Thanks a lot, I'm sure I'll find use of this sometime.

The testcase by @dwj needs:

  • fcntl (*channel, F_SETFL, O_NDELAY) -- a no-op?
  • ioctl (channel, FIONREAD, &bytes_waiting)
  • recvfrom
  • getsockopt
  • getpeername
  • SO_KEEPALIVE
Alon Zakai
Owner

I implemented a bit more here, but since we can't implement all the socket api anyhow (we can't do blocking etc.) i'm just filling in the straightforward stuff. If there is something fundamental missing please let me know. Closing this for now.

Alon Zakai kripken closed this September 28, 2012
dwj
dwj commented October 01, 2012

Ok, that's awesome - we're getting close. Right now we're missing gethostbyname and inet_addr. One or the other would be useful. inet_addr seems pretty straightforward to implement, and there are various free versions if you google inet_addr.c.

Not sure how you would do gethostbyname unless there is a WebSocket API for that. Probably need some server side code. However we can live without that and just use ips.

I really appreciate the work you've put into emscripten. I still can't quite believe that it's possible to run C code in a browser.

Alon Zakai
Owner

Ok, I added inet_addr now. (I also added send a few hours ago btw ;)

Does gethostbyname translate things like www.example.com into x.y.z.w? There isn't an API in browsers for that, but we can probably get around it. Websockets calls can use hostnames and not just ip numbers, so we could translate www.example.com into an "impossible" IP address, maintain an internal lookup table, and when that impossible IP is used to connect, replace it with the string from the lookup table that says www.example.com. This would work in the normal case, but not if you look at the contents of the ip address ;) Would that be useful?

dwj
dwj commented October 01, 2012

Yes, gethostbyname just converts the hostname into an ip address and stores it in a data structure. That's a good idea of using a dummy ip with lookup table - I think that should suffice in most cases.

dwj
dwj commented October 02, 2012

Ok, getting closer. select() isn't implemented, but our code can use poll(). However it doesn't appear to be connecting.

Although connect() returns true, I'm getting an error 'tried to send data to closed socket' whenever I try to send data to the socket. I think this error is generated by WebSocket, as I don't see that anywhere in the emscripten code. Also there are lots of 'waiting for socket in order to send' messages in the java console (from emscripten) - this error seems to be generated once every main loop.

Also, it appears that no connection is ever being made to the server (using netstat, and checking sendmail log I see nothing).

(For testing I'm using a sendmail server running on TCP port 5087, connecting using latest Chrome on OSX, and I'm calling emscripten_set_main_loop with 1 second framerate. The sendmail server is on the same server hosting the html page, and there are no security errors).

Alon Zakai
Owner

gethostbyname fake ip support pushed to incoming.

Alon Zakai
Owner

First question about your problem to connect - are you running a websocket server, or just a plain tcp server? if it's a plain tcp server, you need something like websockify, see the test suite.

If it's already a websocket-friendly server, can you make as small a complete testcase as possible for me to debug?

dwj
dwj commented October 02, 2012

Yes, you were right - it turns out I didn't know how websockets worked :)

Now I have websockify up and running, and it's accepting connections but then erroring out. Could be that I'm not running it properly. If I use the python version of websockify, it gives this error each time a client connects:

"Client must support 'binary' or 'base64' protocol"

If I use the C version, I get this error in Chrome's javascript console:

"Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"

I also get these same errors with the testcase. Are you testing with Chrome, or some other way?

Alon Zakai
Owner

I am testing with firefox over here. I'm not an expert on the websocket protocol, but I do know there are some variants on it. Over here the protocol interaction with the websockify C server worked ok if I did ['arraybuffer'] for the websocket protocol in the call to new WebSocket(..) (other options did not work and gave errors). If you find we need something else on chrome let me know.

dwj
dwj commented October 02, 2012

When I test using Firefox, my testcase works perfectly. Need to use the C version of websockify - as you mention previously, the python version doesn't seem to work.

Here is some info on the error that Chrome is generating:

http://stackoverflow.com/questions/11300694/chrome-20-websocket-handshake

It looks like it's a bug in the websocket handshake on the server side, so presumably a bug in websockify.

Janus Troelsen

I'm pretty sure the error you're getting with the C version is basically the same as what you're getting with the Python version. The Python version should start up okay, the problem with it is data corruption during the connection. kanaka called the Python version more robust somewhere, I think. I think you should definitely get that one to connect first. What version of Chrome are you using?

dwj
dwj commented October 02, 2012

I'm using Chrome 22.0.1229.79 on OSX. Just to clarify: C version of websockify works perfectly with Firefox, python version gives an error about needing base64 or binary. Neither version works with Chrome.

Alon Zakai
Owner

Perhaps there is something better than websockify for testing purposes? Node has websockets support I believe, I don't know how robust though.

dwj
dwj commented October 02, 2012

There seems to be a bug when using emcc -O2. It seems to screw up gethostbyname, so that is connects to some weird ip address when you do the connect(). Without -O2 gethostbyname works fine.

dwj
dwj commented October 02, 2012

Running our actual app, it's getting an error on the send() - basically send() is always returning zero. In the firefox console I'm seeing this error:

Error: uncaught exception: [Exception... "String contains an invalid character" code: "5" nsresult: "0x80530005 (NS_ERROR_DOM_INVALID_CHARACTER_ERR)"

The emcc-generated javascript code at the line in question is this:

for (var i = 0; i < info.sendQueue.length; i++) {
info.socket.send(window.btoa(info.sendQueue[i]));
}

I think the problem is I'm sending binary data down this socket, and firefox doesn't seem to like it. I think maybe sendQueue needs to be made into an ArrayBuffer (although I've never used ArrayBuffers myself, so that's a bit of a wild guess). Some more info here:

http://stackoverflow.com/questions/5766802/send-and-receive-binary-data-over-web-sockets-in-javascript

Alon Zakai
Owner

Fixed the -O2 issue and added a test.

Alon Zakai
Owner

We do use b64 encoding/decoding as mentioned in that stack overflow question. No idea what the problem is. Can you make a testcase I can debug? Or, perhaps modify one of the existing testcases to fail on your problem?

dwj
dwj commented October 03, 2012

Thanks, -O2 issue fixed.

To recreate the exception, do this (in tests/websockets.c):

  • change EXPECTED_BYTES to 500
  • after the connect(), add this code:

unsigned char buffer[1024];
for (int i = 0; i < 1024; i++)
{
buffer[i] = rand();
}
send(SocketFD, buffer, 1024, 0);

  • point websockify at a friendly smtp server (you can't recreate with nc, as nc closes the connection immediately and the problem code never gets executed)

The problem appears to be a long-standing bug in btoa() that never got fixed - it happens in both Chrome and Firefox:

https://bugzilla.mozilla.org/show_bug.cgi?id=213047

I have always used this code, which works perfectly when converting binary to base64 for XHR transmission:

https://gist.github.com/619241
http://pastebin.com/RAEBbJKP

(I think someone converted this to javascript from GPL java code).

Janus Troelsen

@kripken : There is no official support AFAIK, but there are several implementations. The most popular I could find are:

https://github.com/Worlize/WebSocket-Node
https://github.com/einaros/ws

They both mention passing tests. The last one seems to be more active, so I'd recommend that one. It has better examples too, IMHO.

Janus Troelsen

websock.js uses this base64 implementation. I have tested this one with websockify's server side but I guess it doesn't matter which one is used, it is a matter of code style preference. @dwj's version seem to include unnecessary url-encoding routines.

dwj
dwj commented October 04, 2012

If you change the protocols parameter to 'base64' in the WebSocket constructor, it works properly with the python version of websockify:

info.socket = new WebSocket('ws://' + info.host + ':' + info.port, ['base64']);

'arraybuffer' is not a valid sub-protocol as far as I know, and websockify expects 'binary' or 'base64'. I can't find any document that lists the actual valid protocols - I think it's just up to the application, so it makes sense for us to specify base64 as that's what we're sending.

dwj
dwj commented October 04, 2012

Regarding the base64 stuff: the code from websock.js wants an array as it's input (discovered after a lot of headbanging wondering why it was always returning AAAAA :), so it would require some modification to get it working with the emscripten code.

The base64 I posted seems to work fine (and you can probably get rid of the superfluous hex/url code).

Another option might be to just use binary instead of base64. I think you would just need to use an ArrayBuffer instead of calling Pointer_stringify in send().

The only issue is that this bit of code in library.js doesn't work properly on firefox 12 or earlier, and it ends up sometimes passing crap to whatever base64 encoder you end up using:

info.sender = function(data) {
if (data) {

The issue is that setTimeout has a non-standard extra parameter on firefox 12 and lower:

https://developer.mozilla.org/en-US/docs/DOM/window.setTimeout?redirectlocale=en-US&redirectslug=window.setTimeout

Solution is either ignore the problem and wait for firefox 13, or pass a null as the first parameter to the setTimeout. It's not an issue on Chrome.

Janus Troelsen

If websock.js is not to be used, I think we should go with binary support. sendQueue should be an array of Uint8Arrays. There is a pull request on websockify to enable Typed Array support in the C version.

I don't think Firefox 12 should be supported, it is 3 versions back now.

Alon Zakai
Owner

I have no luck with binary websockets. Setting a protocol of 'binary' doesn't help, and assigning to socket.binaryType as the docs say I should has no effect - I always get a string as the message data from the server. Til that's sorted out, not sure how we can use binary websockets.

Alon Zakai
Owner

Perhaps this is an issue with websockify that is used in the test framework?

dwj
dwj commented October 16, 2012

Make sure you're using the python version of websockify. I believe (from a quick look at the C code) that the C version is hardcoded to always use base64.

Alon Zakai
Owner

Oh... but the python version didn't work for us for other reasons? That's why we switched the test harness to use the C version :(

dwj
dwj commented October 16, 2012

The python version works as long as the protocol is set correctly - see #595 (comment)

Alon Zakai
Owner

Still doesn't work. I switched to to base64 as you recommend on the incoming branch now. Running

python tests/runner.py browser.test_zz_websockets

works with the C server, but switching to the python one (switch the commented line in websockify_func in tests/runner.py) makes that test fail.

Janus Troelsen

@kripken: The pull request I linked here adds binary support to the C version.

Alon Zakai
Owner

Thanks, I'll watch that pull.

Janus Troelsen

@kripken : The timing issue with the Python websockify version is now fixed (see websockify/issues/63), so we don't need binary support in the C version anymore, as the Python version has binary support.

I just tested that test_zz_websockets works with the new Python version. I'd submit a patch that makes src/library.js/connect use binary but you mentioned here that you already have the code, so I didn't wanna duplicate the work.

I'd recommend scrapping the base64 code.

EDIT: I now changed to binary support but I can't get the zzz websocket test to work. Here's my branch

Janus Troelsen

@dwj: See the StackOverflow question you linked:

you can encode (window.btoa) any string (no matter what sort of weird binary/unicode values it has in it). To decode a string (window.atob), it must be valid standard base64 encoded. Which means it can only use the standard 64 base64 characters (A-Z, a-z, 0-9, +, /), and it must be padded to a four byte boundary with "=".

If that's true, it's not the binary data that's at fault.

Of course none of this matters if the binary version works.

I'd like to see the binary string that, once encoded, cannot be decoded.

dwj
dwj commented October 30, 2012

ysangkok: see my comment #595 (comment).
Basically the problem is that btoa is broken on firefox and can't be used. It should encode any binary data to base64, but it doesn't.

Anyway, I agree that sending binary data over the websocket is the best solution, so we avoid the overhead of base64.

Janus Troelsen

@dwj: What do you mean by "binary data"? The issue you linked has a test case that fails on encoding the string "日本語": here. AFAIK, a string like that would never be produced by Emscripten since it is building the string from an integer of 0-255 values from the virtual heap.

Again, I'd very much like to see what the contents of info.sendQueue[i] is when you receive that error.

I think that it is possible to encode any byte sequence (I use this term to distinguish it from strings with >255 code points) with btoa. For example, the string in the linked example is equivalent to "\u65e5\u672c\u8a9e". Splitting that into bytes like "\x65\xe5\x67\x2c\x8a\x9e" encodes successfully. Which goes to show that a "binary" string (provided it is really only 1 byte characters) can be encoded as base64. And strings with chars that have >255 code points should never be generated by Emscripten.

I recognize that the quote from kanaka must be wrong, as strings with high code points still are strings, and he was talking about every string.

dwj
dwj commented October 30, 2012

You can see my testcase in the same comment. In theory you are correct, but the code is definitely breaking with a firefox exception in btoa and it works perfectly with any other base64 encoder. Perhaps btoa doesn't allow 00-1f and 7f-9f as valid iso-8859-1 chars. I haven't really looked into it in detail.

Alon Zakai
Owner

Ok thanks, I updated websockify to use the python server, and switched our code to use binary data. Current tests pass with that on incoming. Let me know if this fixes the problems we had before.

Alon Zakai
Owner

I also added a test with large amounts of binary data. Looks like it works correctly.

Alon Zakai
Owner

Made various other fixes and there is now a basic ENet test that works.

Alon Zakai
Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.