SciViews / svSocket

SciViews R socket server
https://www.sciviews.org/svSocket
Other
12 stars 4 forks source link

Need an example of a custom procfun #7

Open ralmond opened 2 years ago

ralmond commented 2 years ago

I'm trying to write a custom server. The input and output are large JSON objects, so I thought the best solution was to write a custom procfun so I didn't need to worry about escaping quotes.

Here is my test code:

library(svSocket)

processTestSocket <-
function (msg, socket, serverport, ...) 
{
    cat("Message was:",msg,"\n")
    output <- paste0("message:",msg)
    return(output)
}

options(debug.Socket=TRUE)
startSocketServer(port=12525,server.name="EAServer",
                  procfun=processTestSocket)

This returns TRUE in the console.

However, when I try to test the port using telnet, it closes immediately.

$ telnet localhost 12525
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.
$ sudo ss -l |fgrep 12525
tcp   LISTEN 0      4096                                                                                  0.0.0.0:12525                      0.0.0.0:*          
tcp   LISTEN 0      4096                                                                                     [::]:12525                         [::]:*          
$

What am I missing?

phgrosjean commented 2 years ago

svSocket is a little bit more complex. It is not a Web API that you can simply reach through HTTP request or telnet. A “remote R prompt” is installed and you must use it by sending commands and collecting results in a defined way. If you need such an API, my advice would be to try plumber instead. Look here: https://www.rplumber.io https://www.rplumber.io/, especially the last example to send data as JSON.

If I misunderstood your needs and svSocket would be a better solution, please, detail me your use case and we will work it out together. Best,

Philippe Grosjean

..............................................<°}))><........ ) ) ) ) ) ( ( ( ( ( Prof. Philippe Grosjean ) ) ) ) ) ( ( ( ( ( Numerical Ecology ) ) ) ) ) Mons University, Belgium ( ( ( ( ( ..............................................................

On 24 Jan 2022, at 17:59, Russell Almond @.***> wrote:

I'm trying to write a custom server. The input and output are large JSON objects, so I thought the best solution was to write a custom procfun so I didn't need to worry about escaping quotes.

Here is my test code:

library(svSocket)

processTestSocket <- function (msg, socket, serverport, ...) { cat("Message was:",msg,"\n") output <- paste0("message:",msg) return(output) }

options(debug.Socket=TRUE) startSocketServer(port=12525,server.name="EAServer", procfun=processTestSocket)

This returns TRUE in the console.

However, when I try to test the port using telnet, it closes immediately.

$ telnet localhost 12525 Trying ::1... Connected to localhost. Escape character is '^]'. Connection closed by foreign host. $ sudo ss -l |fgrep 12525 tcp LISTEN 0 4096 0.0.0.0:12525 0.0.0.0:
tcp LISTEN 0 4096 [::]:12525 [::]:

$ What am I missing?

— Reply to this email directly, view it on GitHub https://github.com/SciViews/svSocket/issues/7, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAZCO4NZ2UOSLCVKEDSKJRLUXWAPDANCNFSM5MV5MG3Q. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub. You are receiving this because you are subscribed to this thread.

ralmond commented 2 years ago

Thanks for the pointer to plumber. It doesn't solve this problem, but it might mean I can work in R instead of php for some other web apps I'm working on.

A little bit more about the problem.
I'm working on a scoring server for a complex assessment.

The R server needs to a) Set up a rather complex scoring model. b) Process the data for each student. because of (a), I would like it to stay alive and not reload everything to score each student (this is why plumber won't work).

I have another application running on the same machine that stores the student data. My design was to write a small php script that extracted the relevant data from the student record, sent the relevant data (as JSON) to the scoring server, which would respond with the scores (also as JSON) back to the script that would insert the socres into the student record and return it.

The client script is written in php, but I found an example connecting to svSocket from php, so that is fine.

Your code also solves another problem I have with just using raw socketConnection calls in R, there I get all kinds of timeout problems.

The scoring function is a single command, so there is no problem there. The issue with using svSocket is that if I need to pass the JSON as a long string argument, I will need to escape all of the quotes. There is probably an easy way to do that in php, but I don't know it.

I think I could write a simple echo-back procfun, I could easily replace it with my scoring code.

My use of telnet is just a easy way to test the connection.

phgrosjean commented 2 years ago

Hello,

On 25 Jan 2022, at 16:22, Russell Almond @.***> wrote:

Thanks for the pointer to plumber. It doesn't solve this problem, but it might mean I can work in R instead of php for some other web apps I'm working on.

According to your description, it does solve the problem. With plumber, you install a server that accepts a request (possibly with JSON data added to it), and it returns whatever R has computed in response to the request (and R can format its result in JSON, you have plenty of packages to do that).

A little bit more about the problem. I'm working on a scoring server for a complex assessment.

The R server needs to a) Set up a rather complex scoring model. b) Process the data for each student.

I have another application running on the same machine that stores the student data. My design was to write a small php script that extracted the relevant data from the student record, sent the relevant data (as JSON) to the scoring server, which would respond with the scores (also as JSON) back to the script that would insert the socres into the student record and return it.

The script is written in php, but I found an example connecting to svSocket from php, so that is fine.

Good, but you are aware that svSocket initializes a repl communication between the server and the client. More precisely, it accepts a series of commands and returns a series of results, of an undetermined number. As for the regular R prompt, commands could be partial R code and continued with the next command like:

log(1 + 1)

svSocket manages the logic of determining if the code is complete or not (log1 +), and if it should wait for more code 1) before processing it (and it must do so separately for each client that is currently connected).

Likewise, the client receives results asynchronously in chunks of an undetermined number. You have to manage to send a termination sequence at the end of the last chunk… detect it in your PHP client and paste together all chunks received so far before you can process the results. You have to program all this in your PHP code asynchronously. Of course, this is useful in a repl-like interaction with R, but unnecessarily complex for a simple client-server transaction.

Your usage seems much simpler: you send a command with some JSON data, and you expect a result in JSON too. Plumber is really better here. I don’t see the reason why you should cope in your PHP client with all the complexity of partial commands, partial responses send back and forth between the client and the server with svSocket.

Another option is opencpu (https://www.opencpu.org https://www.opencpu.org/). Although the example is using a JavaScript client in the browser., there is a PHP client too here: https://github.com/OpenSILEX/opencpu-client-php https://github.com/OpenSILEX/opencpu-client-php.

I really think plumber and opencpu are better alternatives. Reimplementing the whole svSocket logic into PHP (which is not done yet) seems a lot of useless work to me in your case.

Your code also solves another problem I have with just using raw socketConnection calls in R, there I get all kinds of timeout problems.

The scoring function is a single command, so there is no problem there. The issue with using svSocket is that if I need to pass the JSON as a long string argument, I will need to escape all of the quotes. There is probably an easy way to do that in php, but I don't know it.

I think I could write a simple echo-back procfun, I could easily replace it with my scoring code.

My use of telnet is just a easy way to test the connection.

— Reply to this email directly, view it on GitHub https://github.com/SciViews/svSocket/issues/7#issuecomment-1021300819, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAZCO4PL6JXEXVA222YL4N3UX254DANCNFSM5MV5MG3Q. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub. You are receiving this because you commented.

ralmond commented 2 years ago

I think I have gotten this working.

The key challenge on my part was dealing with the embedded quotes in the JSON string. Using raw syntax r"(...)", or as )" might be a valid data sequence r"---(...)---". Then I wrap the command in print(...,quote=FALSE) to suppress the escaping of quotes in the response.

So my php code looks like this:

// This gets around issues where feof might hang.
function safe_feof($fp, &$start = NULL) {
 $start = microtime(true);
 return feof($fp);
}

//Reads from $fp until `\r\n' and strips the `[1]` from the string.
 function fget_allR($fp) {
     $result= "";
     $start=NULL;
     while (!safe_feof($fp,$start) &&
               (microtime(true) - $start) < self::SOCKET_TIMEOUT) {
         $line = fgets($fp);
         if (str_starts_with($line,"\r\n")) break;
         $result .= $line;
     }
     return substr($result,3);
}

//Finally, the principle method which does the work:
    function process_message_svRaw($input_mess) {
        $input_string = json_encode($input_mess,
            JSON_UNESCAPED_UNICODE | 
            JSON_UNESCAPED_SLASHES);

        //Request that R process the file through svSocket
        $socket = fsockopen ($this->host, $this->port, 
                                           $errno, $errstr,300) or
                die ("Error opening socket: $errstr.\n");
        $in = 'print(' .self::RCMD. '(r"---('.$input_string.')---"), 
                           quote=FALSE)';

        fwrite(STDERR,"Message to R:".$in."\n");
        fputs($socket,"$in\r") or die("Error writing to svSocket.");
        //$reply = fgets($socket);
        $reply=$this->fget_allR($socket);
        fwrite(STDERR,"Reply from R:".$reply."\n");
        fclose($socket);

        return json_decode($reply,true);
    }

This seems to work (particularly, when I set the self::RCMD constant to "identity" for testing).

I don't need to deal with the multiple lines of input code, although I'm not sure whether or not json_encode() always returns a single line, so it is comforting to know the svSocket should deal with them.

By trial and error, I've determined that the reply is followed by a line starting with \r\n'. It would be nice to replace that with a unique string. I think that according to the docs, I can set this with theparSocket(...,last="ThisIsTheEnd.\n")or something similar, but I'm not quite sure what the...` used to identify my socket should be. (The example only works with a fake socket). I'm starting the socket with the (R) command:

startSocketServer(port=12525,server.name="EAServer")
phgrosjean commented 2 years ago

@ralmond Yes, this is it (note that I have not tested your code, but it seems valid to me). Regarding the multiline feature of {svSocket}, you don't have to worry about it: if you always submit a complete R code, you will never hit it. For the text you want as a marker for the end of result, yes, you specify it with the last= argument.