3

I've got a node websockets server all set up with a chat service working great. But I want a LAMP server to be able to send periodic messages to users connected to the websocket server. (Either in response to user browser actions or cron jobs). So I needed some PHP code for sending a request to the node server (both on Google Compute Engine).

I found via this answer: https://stackoverflow.com/a/22411059/947374 ...a link to a forum where someone got this pretty well worked out: https://forum.ripple.com/viewtopic.php?f=2&t=6171&p=43313#p43313

My PHP code now looks like:

//local address of node server
$host='XXX.XXX.XXX.XXX';
$port=80;
//location where THIS script is running FROM
$local="http://localhost";
//json the data to send
$data=json_encode($data);

//some very particular headers
$head = "GET / HTTP/1.1"."\r\n".
        "Upgrade: WebSocket"."\r\n".
        "Connection: Upgrade"."\r\n".
        "Origin: $local"."\r\n".
        "Host: $host"."\r\n".
        "Sec-WebSocket-Version: 13"."\r\n".
        "Sec-WebSocket-Key: asdasdaas76da7sd6asd6as7d"."\r\n".
        "Content-Length: ".strlen($data)."\r\n"."\r\n";
//WebSocket handshake
$sock = fsockopen($host, $port, $errno, $errstr, 2);
fwrite($sock, $head ) or die('error:'.$errno.':'.$errstr);
$headers = fread($sock, 2000);
//see the second link above for what hybi10Encode() is doing
fwrite($sock, hybi10Encode($data)) or die('error:'.$errno.':'.$errstr);
$wsdata = fread($sock, 2000);
fclose($sock);

The data gets sent from PHP to node. Beautiful. Praise the Lord.

Only problem is the connection appears to stay open for about 60 seconds. The data gets sent from PHP to node immediately, the webchat responds right away like I want it to. But the browser tab requesting the PHP script spins for those 60 seconds and won't take another request until the first has finished.

I had thought the inclusion of fclose() would make it so the connection doesn't stay open like this.

I also tried changing the headers to: Connection: Close AND/OR tried GET / HTTP/1.0 (As per some advice of other threads with similar issues) This didn't help either.

Any advice on how I can get the PHP script to close the connection immediately or have the node server drop the connection from the LAMP server as soon as it's done processing its request?

Community
  • 1
  • 1
rgbflawed
  • 1,957
  • 1
  • 22
  • 28
  • 1
    Does your node.js service use Redis or another pub/sub service for horizontal scaling? If it does, you can probably publish to the service instead of connecting to node.js using websockets. – Myst Jan 19 '17 at 23:11

3 Answers3

4

Do you actually get to fclose()?

The code could be stuck on fread() if you have it on blocking mode (http://php.net/manual/en/function.stream-set-blocking.php) and don't send 2000 bytes or eof- in otherwords fread will just time out.

To test: - put echo 'Closing'; before fclose so you know you're getting there - add stream_set_blocking ($sock, false); - shorten the socket timeout (http://php.net/manual/en/function.stream-set-timeout.php)

Small warning: if you set non-blocking, you may need to loop around $headers = fread($sock, 2000); as that will return immediately even if no data present. Create a while loop that loops unlit you manually time it out, or you get the response you expect.

Robbie
  • 17,605
  • 4
  • 35
  • 72
  • You were right, it wasn't even getting to fclose(). And your pointing towards the 2000 in fread() was spot on too. I didn't realize that defined bytes expected to be returned. In all of the cases I plan on using this, I actually don't care about any response from the websocket. So I experimented with changing the second input to zero instead of 2000. PHP didn't like that. So then I tried changing the value to 1. And now everything works perfectly. But I'm still a bit worried that I don't understand exactly what's happening. Do you think setting this value to 1 is a tenable solution? – rgbflawed Jan 20 '17 at 15:46
  • @rgbflawed If you do not expect a response from the server, don't call `fread` at all. Either send `hybi10Encode(1000, 'close')` (or similar) to cleanly initiate the close sequence from the server, or just call `fclose` straightaway. – bishop Jan 20 '17 at 16:31
  • As bishop said! Just close without attempting to read the response if there's never a response to read. But using non blocking mode with a manual time out will give you more control over the first fread, but keep it simple by leaving in blocking mode and not reading the second response. – Robbie Jan 20 '17 at 22:23
  • Ah, now I get what fread() is doing! Removing it completely works just the same as fread(...,1). So out it goes. Except strange thing, I had set it up so node double checked to make sure the `remoteAddress` was the expected internal IP address. With fread() out, that IP address doesn't show up. It comes out as undefined. Anyways... both of your help is very appreciated! Thank you!!! – rgbflawed Jan 21 '17 at 11:45
  • Glad to have helped. Good luck with the rest. You can do some nifty stuff with sockets so have fun. – Robbie Jan 21 '17 at 12:48
2

WebSockets is a two-way persistent connection. The connection is expected to stay open until either the client explicitly closes it, or the server does. As you're not sending a close packet (the second parameter to hybi10Encode would be 'close'), the server is waiting around for further communication then hanging up on you after silence.

So, how do you do this? Well, quoting RFC 6455 § 7.1.1:

To Close the WebSocket Connection, an endpoint closes the underlying TCP connection. An endpoint SHOULD use a method that cleanly closes the TCP connection, as well as the TLS session, if applicable, discarding any trailing bytes that may have been received. An endpoint MAY close the connection via any means available when necessary, such as when under attack.

The underlying TCP connection, in most normal cases, SHOULD be closed first by the server, so that it holds the TIME_WAIT state and not the client (as this would prevent it from re-opening the connection for 2 maximum segment lifetimes (2MSL), while there is no corresponding server impact as a TIME_WAIT connection is immediately reopened upon a new SYN with a higher seq number). In abnormal cases (such as not having received a TCP Close from the server after a reasonable amount of time) a client MAY initiate the TCP Close. As such, when a server is instructed to Close the WebSocket Connection it SHOULD initiate a TCP Close immediately, and when a client is instructed to do the same, it SHOULD wait for a TCP Close from the server.

The clean close there means the preferrable way of doing it is sending a close frame with an appropriate status code, like 1000.

I suspect that's not happening because of your second fread(..., 2000). The server hasn't sent you 2000 bytes, so fread is waiting for it. You may need to do a chunked read there, or something more specific to the data coming back. I would not spin in a tight loop.

Also, check the return code of fclose. If it's failing, that would indicate a problem to investigate (though I doubt that would be the case here).

I'd also suggest sending a hybi10Encode(1000, 'close') or some such after you have successfully fread all the data you want in the response to cleanly close the connection per the RFC.

Community
  • 1
  • 1
bishop
  • 37,830
  • 11
  • 104
  • 139
  • Both you and Robbie gave very similar, and very much appreciated answers. I put an additional comment onto his question, but very well could have put it here too. – rgbflawed Jan 20 '17 at 15:47
  • I'm annoyed that it won't let me start an equal separate bounty for you too. I upvoted a bunch of your other answers instead. Thanks again! – rgbflawed Jan 21 '17 at 11:49
  • @rgbflawed Helping is it's own reward. Glad you got this working! – bishop Jan 21 '17 at 13:40
0

Your problem is quite simple really,

$headers = fread($sock, 2000);
//see the second link above for what hybi10Encode() is doing
fwrite($sock, hybi10Encode($data)) or die('error:'.$errno.':'.$errstr);
$wsdata = fread($sock, 2000);
fclose($sock);

your reading 4000 bytes, however if the messages are not 4000 bytes long php will wait until socket timeout occurs, there are 2 options 1 option is your get your node server to terminate the connection at it's end and then php it will stop reading. the second one would be to read it all. using feof() but they way your using this would require reworking of both server and client.

Barkermn01
  • 6,781
  • 33
  • 83