Problems reading from socket

Asked by Wayne Iba

I'm not sure if this is a bug in SBCL, or in CLISP, or neither, but I'm hoping someone here can shed light on the matter. (I'm using SBCL 1.0.29.11.debian 64bit but I have the same problem with 1.0.43 64bit.)

I'm reading a series of lists from a socket served by a C# process outside of my control. The lists are not terminated with a newline. CLISP reads all the lists just fine; SBCL hangs on the last one; but despite any buffering issues in my sample server/client below, I believe that is not the issue here.

Here are some snippets to get us in the ball-park of what I'm encountering:
====================== SETTING UP A PRIMITIVE SERVER
;; make server socket
(setf myss
      (let ((sock (make-instance 'sb-bsd-sockets:inet-socket
     :type :stream
     :protocol :tcp)))
 (setf (sb-bsd-sockets:sockopt-reuse-address sock) t)
 (sb-bsd-sockets:socket-bind sock (vector 0 0 0 0) 9876)
 (sb-bsd-sockets:socket-listen sock 15)
 sock))

;; accept-connection from client
(setf myserv
      (progn
 (setf (sb-bsd-sockets:non-blocking-mode myss) nil)
 (let ((s (sb-bsd-sockets:socket-accept myss)))
   (if s
       (sb-bsd-sockets:socket-make-stream
        s :input t :output t
        :element-type 'character
        :buffering :line
        )
       (sleep nil)))))

=================================AND A CORRESPONDING CLIENT
;; make simple socket to server
(setf myclient
      (let ((socket (make-instance 'sb-bsd-sockets:inet-socket
       :type :stream :protocol :tcp)))
 (sb-bsd-sockets:socket-connect socket
           (sb-bsd-sockets::host-ent-address
     (sb-bsd-sockets:get-host-by-name "localhost"))
           9876)
 (sb-bsd-sockets:socket-make-stream
  socket :input t :output t :buffering :line
  :element-type 'character)))
======================================

Here is the difference between CLISP and SBCL (note, I'm consistently using SBCL for the server whether CLISP or SBCL for the client; this should be irrelevant since I just trying to model a C# server that I cannot change):
From the server, if I evaluate:
* (format myserv "(this)~%")

NIL

And then on the SBCL client evaluate:
* (read myclient)

(THIS)
* (listen myclient)

NIL

But with CLISP and a corresponding client, I get:

[9]> (read myclient)
(THIS)
[10]> (listen myclient)
T
[11]> (read-char myclient)
#\Newline
[12]> (listen myclient)
NIL

So CLISP reads the list up to the closing paren and leaves the newline in the stream, whereas SBCL consumes the newline.
By reading the C#-served stream char by char, I know there is no newline (or any other character) at the end of the list following the closing paren. Yet CLISP reads the list while SBCL hangs. If using my primitive server, I evaluate:
* (format myserv "(this)") ;; no newline

NIL
* (force-output myserv)

then both clients (SBCL and CLISP) report something ready to be read, but only CLISP will read the list with SBCL hanging.

So, is this a bug in SBCL? It seems clear that in general, the reader must NOT consume input following a closing paren. But in this case, I'm not sure it is technically wrong. Thanks for reading.

Question information

Language:
English Edit question
Status:
Solved
For:
SBCL Edit question
Assignee:
No assignee Edit question
Solved by:
Nikodemus Siivola
Solved:
Last query:
Last reply:
Revision history for this message
Nikodemus Siivola (nikodemus) said :
#1

Sockets are a red herring here: this is an issue of READ vs. READ-PRESERVING-WHITESPACE:

(with-input-from-string (s (format nil "(this)~%"))
  (values (read s)
          (read-char s nil :eof)))

(with-input-from-string (s (format nil "(this)~%"))
  (values (read-preserving-whitespace s)
          (read-char s nil :eof)))

READ is allowed (possibly even required, not sure offhand) to throw away syntactically meaningless whitespace following the read object -- in this case the #\Newline character.

Using READ-PRESERVING-WHITESPACE instead your code should work on both Clisp and SBCL.

Also, I would advice against line-buffering if you wish portable parts of your application to be ... portable: it is better to FINISH-OUTPUT explicitly when you wish stuff to be actually sent to the other end -- it may be sent sooner, of course, FINISH-OUTPUT just flushes any pending output. (Note: FINISH-OUTPUT, not FORCE-OUTPUT.)

If portability is not a concern and you're always careful to terminate your messages with a newline, :LINE buffering should of course work just fine.

Revision history for this message
Wayne Iba (wayne-iba) said :
#2

Thanks very much for this. Indeed, I had discovered read-preserving-whitespace as a _work-around_ for my immediate needs. However, that cannot be the "right" response and I consider it only a work-around because, in the case of the problem in question, there is no whitespace to be preserved. The problem is that SBCL's read hangs when no such following whitespace exists (over a socket).

Interestingly,
(with-input-from-string (s "(this)") (read s))

does work in SBCL as one would hope without hanging. So there does seem to be some issue with sockets vs. with arbitrary input streams, as SBCL hangs on the same read over a socket.

ps: I appreciate the comments on portability and have learned from them. However, please keep in mind that, in this case, I have no control over how the server sends data over the socket, and that the question really has to do with reading a non-whitespace-terminated expression over a socket.

Revision history for this message
Nikodemus Siivola (nikodemus) said :
#3

You say:

> However, that cannot be the "right" response and I consider it only a work-around because, in
> the case of the problem in question, there is no whitespace to be preserved. The problem is
> that SBCL's read hangs when no such following whitespace exists (over a socket).

Which I don't understand at all.

When the first call to READ occurs, there is #\( #\t #\h #\i #\s #\) #\newline available on the socket. READ consumes __all__ of this, including the newline. Then there is nothing more to read, of READ-CHAR hangs instead of getting the newline.

I repeat that the socket stream is a non-issue here. You will have identical results on SBCL if you do

(with-open-file (f "/tmp/test.sexp"
                   :direction :output
                   :if-exists :supersede)
  (format f "(this)~%"))

(with-open-file (f "/tmp/test.sexp")
  (values (read f)
          (read-char f nil :eof))) ; => (THIS), :EOF

where READ has consumed the newline before READ-CHAR could get at it.

The only difference is that in case of a pipe or a socket instead of getting an EOF from READ-CHAR the call will block instead, waiting for more input. This would happen just the same way if you had opened a named pipe called instead.

Revision history for this message
Wayne Iba (wayne-iba) said :
#4

I think you missed my followup example withOUT the #\Newline. When I create a file without newline at end (i.e., consisting entirely of the six characters: #\( #\t #\h #\i #\s #\) ), read from the file works just fine. However, when sent over a socket, the simple read hangs.

Note, with the socket receiving the non-newline-terminated list, I get the following:

* (loop while (listen mysock) collect (read-char mysock))

(#\( #\t #\h #\i #\s #\))

but,

* (read myclient)

on the same sent message hangs.

At this point, I'm convinced this is a bug but I'd prefer the two of us come to a common understanding before submitting. Common Lisp (read) allows the consumption of whitespace following numbers and symbols, but lists are self terminated. Like I said before, I can use read-preserving-whitespace as a work-around, but there is a difference here between the way read reads from a file and from a socket.

Thanks for thinking about this with me!

Revision history for this message
Nikodemus Siivola (nikodemus) said :
#5

Oh, sorry! I missed that.

You're right. There's a bug in READ. It uses READ-CHAR without LISTEN to check for and discard following whitespace, and therefore can hang unless EOF has been reached.

Revision history for this message
Best Nikodemus Siivola (nikodemus) said :
#6

Recorded as bug 667775.

Revision history for this message
Wayne Iba (wayne-iba) said :
#7

Thanks so much!

Revision history for this message
Wayne Iba (wayne-iba) said :
#8

Thanks Nikodemus Siivola, that solved my question.