Connect Node.js as client to a Common Lisp server
Asked Answered
C

2

7

I've got small but CPU heavy app in alpha stage in node.js, it's a small game. I'm running into performance issues and I need to speed it up by at least a factor of 20 to get to beta. And since parallel execution would get me very far, I decided that good start would be to share the game map between processes or threads that would perform parallel operations on it. That's pretty impossible to do in node, so I decided to write the meaty parts in CL (SBCL + Linux) and connect to it through unix domain socket.

The plan is:

[players] <-> (node.js front server) <-> (SBCL performing game ticks)

The point is, I need to pass fast messages between node.js and SBCL in a matter similar to socket.io.


Here is what didn't work (you can skip this part)

On Node side, I can't use plain socket.io because it doesn't support Unix Domain Sockets, but net module does, So I can at least do socket.write('raw data') - better than nothing for now.

On CL side, I tried to run woo web server (it supports local sockets) and I could connect to it from node and pass raw data around, but there are all the unnecessary HTTP parts involved and woo is always running as server; it's waiting for GET / HTTP/1.1 ..... I didn't find a way to actually initiate a message from woo first. Also, it's totally undocumented and uncommented and involves lot of FF calls to C libs, which I'm not at all familiar with.

So I went through several more CL web servers that didn't compile, didn't support unix sockets, were abandoned or undocumented, eventually moved to plain sb-bsd-sockets and finally to iolib, but I still can't figure it out.


iolib looked promising, but I can't connect to it from node.

I've got this:

(with-open-socket (socket :address-family :local
                          :type :datagram
                          ;; :connect  :passive
                          :local-filename "/tmp/socket")

  (format t "Socket created")
  ;; (listen-on socket)
  ;; (bind-address socket (make-address "/tmp/socket"))
  (loop
     (let ((msg (receive-from socket :size 20)))
       (format t msg))))    

and I'm getting

#<Syscall "recvfrom" signalled error EWOULDBLOCK(11) "Resource temporarily unavailable" FD=6>
   [Condition of type IOLIB/SYSCALLS:EWOULDBLOCK]

Restarts:
 0: [IGNORE-SYSCALL-ERROR] Ignore this socket condition
 1: [RETRY-SYSCALL] Try to receive data again
 2: [RETRY] Retry SLIME interactive evaluation request.
 3: [*ABORT] Return to SLIME's top level.
 4: [ABORT] abort thread (#<THREAD "worker" RUNNING {10055169A3}>)

I don't know if I should call something like accept-connection or listen-to on that socket first. All I tried resulted in errors. Also, if I [RETRY-SYSCALL] in repl, the error goes away for about 10 seconds but comes back. In this time, node still can't connect.

This seems to get more complicated than I thought. I've already lost ~6 hours of work on iolib alone and I didn't even start on parsing the messages, learning how to create events from them, converting between JSON and s-exps etc..


My questions are:

  • how do i set this connection up in iolib so that node's net can connect?
  • Assuming I can choose, what type of connection would be best suited for passing events/messages? (datagram / stream)
  • Are there some working tools that I didn't try?
  • Also, are there some other libs than iolib that are perhaps more high-level / better documented?
  • Are there any better/easier/faster approaches to this performance / concurrency problem?
  • Any other ideas?

I'm close to just ditching the idea of CL and use something like in-memory mongo with several node processes instead (..it doesn't really sound fast) but I love lisp, it would be great to have things like lparallel on the backend. I just haven't moved an inch since yesterday morning, I just can't figure out the libs. Perhaps I should learn clojure instead.

PS: I wouldn't normally ask for "write me teh code", but if some good soul is around, I would really appreciate it, even in pseudocode.

PPS: Any radically different approaches are also welcome. Please, speak up your mind :)

Thanks for reading!

Chatoyant answered 15/6, 2016 at 12:40 Comment(1)
You have many questions in this questions, some related to Lisp, other more general (choose TCP vs. UDP) so it might be a little too broad. On the other hand, your question is interesting. Don't hesitate to post more focused questions next time, if you need. There are libraries for asynchronous communication (cl-async), JSON (cl-json, yason) etc. which are stable enough. It might be better to tackle each problem separately, one after the other. Good luck.Megass
C
2

So in the end, I figured it out...

(with-open-socket  (socket :address-family :local
                           :type :stream
                           :connect  :passive
                           :local-filename "/tmp/node-sbcl.sock")

  (log-message :info "Waiting for client...")
  (setf *client* (accept-connection socket :wait t))
  (log-message :info "Accepted client's connection.")

  ;; ...lunch with *client* + the bits for parsing json and separating messages...
  )

I switched to :type :stream and most problems disappeared. accept-connection has to be called on socket, but listen-to must not. I had to write a way to separate messages myself, but it was lot easier than I thought. For some reason, :type :datagram just didn't work, I don't know why.

And in node:

var JsonSocket = require('json-socket');
var net = require('net');
var sbcl = new JsonSocket(net.Socket());


sbcl.connect("/tmp/node-sbcl.sock");
sbcl.on('connect', function(){
    console.log('Connected to SBCL, YAY!');

    console.log('Sending hi!');

    sbcl.sendMessage({'cmd': "heyThere"});

    sbcl.on('message', function(message){
        if(!message.cmd) {
            console.log("We've received msg from SBCL with no CMD!!!'");
            return;
        }

        switch(message.cmd){
        case 'heyNode': console.log('SBCL says hi...'); break;
        }
    });
});

So this works, in case somebody else has some similar chicken ideas of using lisp and node together.

Chatoyant answered 5/7, 2016 at 20:43 Comment(0)
M
5

If I understand correctly your problem, you need to establish a server in Common Lisp. Let me reuse a previous answer of mine which uses USOCKET:

(use-package :usocket)

(defun some-server (hostname port)    
  ;; create a server which establishes an event-loop
  (socket-server hostname ; a string
                 port     ; a number

                 ;; tcp-handler (see API for details)
                 ;; This function is called each time a client connects,
                 ;; and provides a bidirectional stream to communicate with it.
                 ;; The function executes in a context where some useful special
                 ;; variables are bound.
                 ;; The connection is closed automatically on exit.

                 (lambda (stream)
                   ;; format to stream to client
                   (format stream
                           "~D~%"
                           ;; add all elements of the host,
                           ;; a vector of 4 integers
                           (reduce #'+ *remote-host*)))))
Megass answered 15/6, 2016 at 18:18 Comment(0)
C
2

So in the end, I figured it out...

(with-open-socket  (socket :address-family :local
                           :type :stream
                           :connect  :passive
                           :local-filename "/tmp/node-sbcl.sock")

  (log-message :info "Waiting for client...")
  (setf *client* (accept-connection socket :wait t))
  (log-message :info "Accepted client's connection.")

  ;; ...lunch with *client* + the bits for parsing json and separating messages...
  )

I switched to :type :stream and most problems disappeared. accept-connection has to be called on socket, but listen-to must not. I had to write a way to separate messages myself, but it was lot easier than I thought. For some reason, :type :datagram just didn't work, I don't know why.

And in node:

var JsonSocket = require('json-socket');
var net = require('net');
var sbcl = new JsonSocket(net.Socket());


sbcl.connect("/tmp/node-sbcl.sock");
sbcl.on('connect', function(){
    console.log('Connected to SBCL, YAY!');

    console.log('Sending hi!');

    sbcl.sendMessage({'cmd': "heyThere"});

    sbcl.on('message', function(message){
        if(!message.cmd) {
            console.log("We've received msg from SBCL with no CMD!!!'");
            return;
        }

        switch(message.cmd){
        case 'heyNode': console.log('SBCL says hi...'); break;
        }
    });
});

So this works, in case somebody else has some similar chicken ideas of using lisp and node together.

Chatoyant answered 5/7, 2016 at 20:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.