edicl/hunchentoot

Detach-socket example?

oladon opened this issue · 6 comments

Greetings!

Minimal(ish) test case:

(defvar *api-server* nil)
(defvar *acceptor* ())
(defvar *temp-stream* ())
(defvar *temp-stream2* ())

(defun stop ()
  (when *api-server* (hunchentoot:stop *api-server*) (setq *api-server* nil)))

(defun start (&key (port 5000))
  (stop)
  (setq *api-server* (hunchentoot:start (setf *acceptor* (make-instance 'hunchentoot:easy-acceptor :port port)))))

(hunchentoot:define-easy-handler (test :uri "/test") ()
  (format t "Got here... ~A~%" (hunchentoot:within-request-p))
  (setf (hunchentoot:content-type*) "text/event-stream")
  (setf *temp-stream* (hunchentoot:detach-socket hunchentoot:*acceptor*))
  (let ((stream (hunchentoot:send-headers)))
    (format t "~&And here... ~a~%" (type-of stream))
    (setf *temp-stream2* stream)
    (write-sequence (flexi-streams:string-to-octets
                     (format nil "~&data: here is event #~d for thread #1~%" i)
                     :external-format :utf8)
                    stream)
    (finish-output stream)))

(start)

Then, run curl -v -N -H 'Accept: text/event-stream' -H 'Connection: keep-alive' localhost:5000/test

My understanding, based on the documentation, is that the call to detach-socket should return the socket (which should be the same as the one returned by send-headers), and prevent Hunchentoot from closing the connection. Unfortunately... the connection still gets closed, and the format messages show that detach-socket returns NIL.

I suspect I'm misunderstanding something about how to use this function — could someone provide feedback, and/or add a simple example to the documentation?

Thanks!

DETACH-SOCKET was incorrectly documented to return a stream, when it in reality always returns NIL. You'll have to use the stream returned by SEND-HEADERS, i.e.:

(hunchentoot:define-easy-handler (test :uri "/test") ()
  (format t "Got here... ~A~%" (hunchentoot:within-request-p))
  (setf (hunchentoot:content-type*) "text/event-stream")
  (let ((stream (hunchentoot:send-headers)))
    (format t "~&And here... ~a~%" (type-of stream))
    (setf *temp-stream2* stream)
    (write-sequence (flexi-streams:string-to-octets
                     (format nil "~&data: here is the event~%")
                     :external-format :utf8)
                    stream)
    (finish-output stream)
    (hunchentoot:detach-socket hunchentoot:*acceptor*)
    (setf *temp-stream* stream)))

I've opened #176 to correct the documentation.

Got it, thanks Hans. I can confirm that send-headers does return a chunked-io-stream.

That said, I'm still experiencing the connection being immediately closed when the handler returns, instead of being left open. Do you have any thoughts or suggestions on that?

How do you determine that the connection is immediately closed? When I use

(drakma:http-request "http://localhost:5000" :keep-alive t :close nil)

to send a request to the running server, I see that the connection stays open until Hunchentoot decides to close it because of a timeout.

Hmm... I'm going off curl exiting, and the browser client I have set up reconnecting every 15 seconds. It seems like I'm probably missing something... would you be able to help me get to a minimal example of, say, a counter which sends an incrementing number to a set of sockets every few seconds?

Here's a self-contained example, again using DRAKMA as client. This is to show that HUNCHENTOOT in fact can detach the socket and that that socket can then be used to talk to the client in whatever fashion desired. To test, LOAD this file and then call (test-request) to establish a connection and see the messages sent by the server.

(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :hunchentoot)
  (ql:quickload :drakma))

(defvar *server* nil)

(defun stop ()
  (when *server*
    (hunchentoot:stop *server*)
    (setf *server* nil)))

(defun start (&key (port 5000))
  (stop)
  (setf *server* (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port port))))

(defun send-socket (stream format &rest args)
  (let ((message (with-output-to-string (*standard-output*)
                   (apply #'format t format args)
                   (write-char #\Return)
                   (write-char #\Linefeed)
                   (finish-output))))
    (write-sequence (flexi-streams:string-to-octets message :external-format :utf8)
                    stream)
    (finish-output stream)))

(hunchentoot:define-easy-handler (test :uri "/test") ()
  (setf (hunchentoot:content-type*) "text/event-stream")
  (let ((stream (hunchentoot:send-headers)))
    (hunchentoot:detach-socket hunchentoot:*acceptor*)
    (sb-thread:make-thread (lambda ()
                             (loop
                               (send-socket stream "Hello ~D" (get-universal-time))
                               (sleep 1))))))

(defun test-request ()
  (let ((stream (nth-value 4 (drakma:http-request "http://localhost:5000/test" :keep-alive t :close nil))))
    (format t "Connected~%")
    (loop
      (format t "Server says: ~A~%" (read-line stream)))))

(start)

Thank you! I'll play around with that and I believe I'll be able to get what I need.