Single-threaded sequence-reading multi-user usocket server

708 Views Asked by At

I'm trying to write a simple server program using the usocket library that will perform a relatively trivial task - say, echoing data back. I want to make it able to do this with multiple clients, not having the single thread blocked while waiting for input from any single client. I've found that it's possible to check if a given socket has input ready using wait-for-input with :timeout 0. But I'm having a hard time getting read-sequence to work how I want it to. If I give it an array with 50 elements, and only 5 are available, it will wait until 50 are available to put them in the array.

Is there any way to read blocks at a time (efficiently) with only a single thread without constantly having to wait for input? Or do I really have to just call read-byte over and over until I've got everything?

The problem could be circumvented if there was some equivalent to read-sequence that only read as much as was available at the time, or if there was some function that would tell me how many elements are ready to be read so I could size the array appropriately. But I don't know of either of those.

UPDATE: I'm specifically looking for binary solutions that don't require reading characters, so solutions involving read-char-no-hang, listen, etc won't help much unless they have a binary equivalent. I want to not work with characters because some character encodings, such as UTF-8, can have invalid sequences of bytes for which there is no character representation, and I want to be able to process any sequence of bytes. And I'm specifically looking for either solutions that don't require reading one byte at a time over and over, or confirmation that no such solution exists (in the standard), in which case I'd love to hear about the most convenient library that can provide the minimum necessary to accomplish that. It's not just that reading one byte at a time isn't the fastest way, it also requires whatever function I write to do that in a non-blocking manner to use usocket's wait-for-input function for every byte (since listen doesn't work with byte streams), which would require the function to know about the socket and I would have to write an overly-specific read-all-bytes function that wouldn't work with, say, File streams. It's possible, but I'm hoping there's a more general way.

3

There are 3 best solutions below

0
On

Update: Realised this doesn't actually work on clisp - only tested on SBCL, where (listen) seems to do "what you might think" at least for network streams. Therefore this isn't a portable solution.. unless you replace the '(listen)' in the below loop to use some form of #+ features.

since listen doesn't work with byte streams

(listen) works perfectly fine with byte streams on sbcl. That should untie the Gordion knot here, and enable one to easily write something along the lines of:

(defun read-sequence-no-hang (seq stream start end)
  (loop
     for i from start below end
     for num-bytes-read = 0 then (1+ num-bytes-read)
     while (listen stream)
     do (setf (elt seq i) (read-byte stream))
     finally (return num-bytes-read)))
0
On

I struggled with this one too.

One NOT portable option which I ended up using on a project is default to the implementation of SBCL, which I am using.

With the buffer:

(defparameter buf-in (make-array 1024 :element-type '(unsigned-byte 8)))
...
;; suppose variable new-client is your usocket object
(setf my-out (multiple-value-list (sb-bsd-sockets:socket-receive
                     (usocket:socket new-client) bufin nil)))

The output will contain:

(Your buffer, length, address of peer who sent it)

More information for SBCL socket implementation is here

1
On

Well I try a code from rosetta code, a sample usocket echo server, the function that the trick is to create your own function for read. In this case read-all and wait for :eof, I tested it with telnet and it works:

code:

;; Sample usocket echo server from Rosetta Code
;; http://rosettacode.org/wiki/Echo_server#Common_Lisp

(ql:quickload (list :usocket))

(defpackage :echo (:use :cl :usocket))

(in-package :echo)

(defun read-all (stream)
  (loop for char = (read-char-no-hang stream nil :eof)
     until (or (null char) (eq char :eof)) collect char into msg
     finally (return (values msg char))))

(defun echo-server (port &optional (log-stream *standard-output*))
  (let ((connections (list (socket-listen "127.0.0.1" port :reuse-address t))))
    (unwind-protect
     (loop (loop for ready in (wait-for-input connections :ready-only t)
          do (if (typep ready 'stream-server-usocket)
             (push (socket-accept ready) connections)
             (let* ((stream (socket-stream ready))
                (msg (concatenate 'string "You said: " (read-all stream))))
               (format log-stream "Got message...~%")
               (write-string msg stream)
               (socket-close ready)
               (setf connections (remove ready connections))))))
      (loop for c in connections do (loop while (socket-close c))))))

Initialize in lisp:

CL-USER> (in-package :echo)
#<PACKAGE "ECHO">
ECHO> (echo-server 12321)
Got message...

test with telnet:

╭─toni@Antonios-MBP  ~ ‹ruby-2.2.3@laguna› ‹1.7› ‹SBCL 1.3.0›
╰─$ telnet 127.0.0.1 12321
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello, TCP                       #<= Press enter
You said: Hello, TCP
Connection closed by foreign host.

Hope this helps