简体   繁体   English

Common Lisp中的SSE服务器

[英]SSE Server in Common Lisp

I'm trying to write a simple async server in common lisp. 我试图用通用Lisp编写一个简单的异步服务器。 Emphasis on simple. 强调简单。 Here's Take 2 (thanks to Rainer for advice and formatting) : 这是Take 2 (感谢Rainer的建议和格式化)

(ql:quickload (list :cl-ppcre :usocket))
(defpackage :test-server (:use :cl :cl-ppcre :usocket))
(in-package :test-server)

(defvar *socket-handle* nil)
(defparameter *channel* nil)

(defclass buffer ()
  ((contents :accessor contents :initform nil)
   (started :reader started :initform (get-universal-time))
   (state :accessor state :initform :empty)))

(defun listen-on (port &optional (stream *standard-output*))
  (setf *socket-handle* (socket-listen "127.0.0.1" port :reuse-address t))
  (let ((conns (list *socket-handle*))
        (buffers (make-hash-table)))
    (loop (loop for ready in (wait-for-input conns :ready-only t)
                do (if (typep ready 'stream-server-usocket)
                       (push (socket-accept ready) conns)
                     (let ((buf (gethash ready buffers (make-instance 'buffer))))
                       (buffered-read! (socket-stream ready) buf)
                       (when (starts-with? (list #\newline #\return #\newline #\return)
                                           (contents buf))
                         (format stream "COMPLETE ~s~%"
                                 (coerce (reverse (contents buf)) 'string))
                         (setf conns (remove ready conns))
                         (remhash ready buffers)
                         (let ((parsed (parse buf)))
                           (format stream "PARSED: ~s~%" parsed)
                           (handle-request ready (parse buf))))))))))

(defmethod parse ((buf buffer))
  (let ((lines (split "\\r?\\n" (coerce (reverse (contents buf)) 'string))))
    (second (split " " (first lines)))))

HTTP writing: HTTP编写:

(defmethod http-write (stream (line-end (eql :crlf)))
  (declare (ignore line-end))
  (write-char #\return stream)
  (write-char #\linefeed stream)
  (values))

(defmethod http-write (stream (line string))
  (write-string line stream)
  (http-write stream :crlf)
  (values))

(defmethod http-write (stream (lst list))
  (mapc (lambda (thing) (http-write stream thing)) lst)
  (values))

How to handle a request: 如何处理要求:

(defmethod handle-request (socket request)
  (let ((s (socket-stream socket)))
    (cond ((string= "/sub" request)
           (subscribe! socket))
          ((string= "/pub" request)
           (publish! "Got a message!")
           (http-write s (list "HTTP/1.1 200 OK"
                               "Content-Type: text/plain; charset=UTF-8"
                               "Cache-Control: no-cache, no-store, must-revalidate"
                               "Content-Length: 10" :crlf
                               "Published!" :crlf))
           (socket-close socket))
          (t (http-write s (list "HTTP/1.1 200 OK" 
                                 "Content-Type: text/plain; charset=UTF-9" 
                                 "Content-Length: 2" :crlf 
                                 "Ok" :crlf))
             (socket-close socket)))))

Publish! 发布!

(defun publish! (msg)
  (loop for sock in *channel*
     do (handler-case
            (let ((s (socket-stream sock)))
              (format s "data: ~a" msg)
              (http-write s (list :crlf :crlf))
              (force-output s))
          (error (e)
             (declare (ignore e))
             (setf *channel* (remove sock *channel*))))))

Subscribe! 订阅!

(defun subscribe! (sock)
  (let ((s (socket-stream sock)))
    (http-write s (list "HTTP/1.1 200 OK" 
                        "Content-Type: text/event-stream; charset=utf-8"
                        "Transfer-Encoding: chunked"
                        "Connection: keep-alive"
                        "Expires: Thu, 01 Jan 1970 00:00:01 GMT"
                        "Cache-Control: no-cache, no-store, must-revalidate" :crlf))
    (force-output s)
    (push sock *channel*)))

Basic utility: 基本实用程序:

(defmethod starts-with? ((prefix list) (list list) &optional (test #'eql))
  (loop for (p . rest-p) on prefix for (l . rest-l) on list
     when (or (and rest-p (not rest-l)) (not (funcall test p l))) 
     do (return nil)
     finally (return t)))

(defun stop ()
  (when *socket-handle*
    (loop while (socket-close *socket-handle*))
    (setf *socket-handle* nil
      *channel* nil)))

(defmethod buffered-read! (stream (buffer buffer))
  (loop for char = (read-char-no-hang stream nil :eof)
     until (or (null char) (eql :eof char))
     do (push char (contents buffer))))

The summary is: 摘要是:

  1. It listens on a specified port and dumps request data to the specified stream 它侦听指定的端口并将请求数据转储到指定的流
  2. If it gets a request to "/sub" , it's supposed to keep that socket around for further writes. 如果它收到对"/sub"的请求,则应该保留该套接字以进行进一步的写操作。
  3. If it gets a request to "/pub" , it's supposed to send a short message out to all existing subscribers 如果收到"/pub"的请求,则应该向所有现有订阅者发送一条短消息
  4. It sends back a plain-text "Ok" on any other request. 它会在其他任何请求上发回plain-text "Ok"

All feedback welcome, as usual. 与往常一样,欢迎所有反馈。 As of version 2 (added HTTP-friendly line-endings and a couple strategically placed force-output calls) , browsers seem happier with me, but Chrome still chokes when a message is actually sent to an existing channel. 从版本2开始(添加了HTTP友好的行尾以及策略性地放置了两个force-output调用) ,浏览器对我来说似乎更快乐,但是当实际将消息发送到现有频道时,Chrome仍然令人窒息。 Any idea what the remaining bugs in publish! 知道剩下的错误还有多少publish! are? 是?

To be clear, doing 明确地说,

var src = new EventSource("/sub");
src.onerror = function (e) { console.log("ERROR", e); };
src.onopen = function (e) { console.log("OPEN", e); };
src.onmessage = function (e) { console.log("MESSAGE", e) };

Now gets me a working event stream in FireFox (it triggers onopen , and triggers an onmessage per sent update) . 现在,我在FireFox中获得了一个工作事件流(它触发onopen ,并且onmessage发送更新都会触发onmessage But fails in Chrome (triggers onopen , with each update triggering onerror instead of onmessage ) . 但是在Chrome中失败(触发onopen ,每次更新触发onerror而不是onmessage

Any help is appreciated. 任何帮助表示赞赏。

One thing I would make sure: it should handle the CRLF correctly on both input and output. 我要确保的一件事:它应该在输入和输出上正确处理CRLF。 CRLF are used in HTTP. CRLF在HTTP中使用。

There are two Common Lisp characters: #\\return and #\\linefeed . 有两个Common Lisp字符: #\\return#\\linefeed

Don't use #\\newline . 不要使用#\\newline This is a special character which depends on the operating system and the particular CL implementation. 这是一个特殊字符,取决于操作系统和特定的CL实现。 On a Unix OS it might be the same as #\\linefeed . 在Unix操作系统上,它可能与#\\linefeed相同。 On a Windows implementation it may be the same as the sequence of return and linefeed. 在Windows的实现可能是相同的符和换行符的序列。 Thus also don't use newline as a format instruction ~% . 因此也不要将换行符用作格式指令~%

Always explicitly write return and newline in HTTP protocols as line ends. 始终在HTTP协议的末尾显式写入return和换行符。 Thus you make sure that your code is portable AND does the right thing. 因此,您可以确保代码具有可移植性,并且可以做正确的事情。

Also, side note, make sure that the comparison of characters is not done with EQ . 另外,请注意,请确保未使用EQ完成字符比较。 Characters are not necessarily eq. 字符不一定是eq。 Use EQL to compare for identity, numbers and characters. 使用EQL比较身份,数字和字符。

Ok, so after trying a bunch of things, I have it working, but I have no idea why. 好的,所以在尝试了一堆东西之后,我已经开始工作了,但是我不知道为什么。 That's going to be my next question. 这将是我的下一个问题。

What didn't work: 什么工作:

  • varying the position/presence of force-output calls (unless both the subscribe! and the publish! messages are forced, no events fire on the client side at all) 更改force-output调用的位置/状态(除非同时force-output执行subscribe!publish!消息,否则根本不会在客户端触发任何事件)
  • using babel to encode the SSE events to octets before sending them (this failed; socket-stream s aren't binary-stream s 在发送八位字节之前使用babel将SSE事件编码为八位字节(此操作失败; socket-stream不是binary-stream
  • Re-writing the server using cl-async , which has its own write routines. 使用cl-async重写服务器,该服务器具有自己的写入例程。 The result of that effort can be seen here , but it didn't help at all. 可以在这里看到这种努力的结果 ,但是完全没有帮助。 Firefox/Iceweasel/Conkeror perform as expected, but Chrom (?:e|ium) still fails the same way. Firefox / Iceweasel / Conkeror的运行正常,但是Chrom (?:e|ium)仍然失败。 That is, the event stream is open as normal, the onopen event fires, but whenever an actual event is sent, onerror triggers instead of onmessage 也就是说,事件流正常打开,会触发onopen事件,但是每发送一个实际事件,就会触发onerror而不是onmessage
  • Outputting the bom as specified in the "Parsing an event stream" section of the SSE spec . 按照SSE spec“解析事件流”部分中指定输出bom Doing (write-char (code-char #xfeff) s) before starting up the stream had no effect. 在启动流之前执行(write-char (code-char #xfeff) s)无效。 The stream would still be accepted by FF et al, and still be rejected by Safari engine browsers. FF等仍将接受该流,而Safari引擎浏览器仍将拒绝该流。

The only thing left at this point was busting out the packet sniffer. 此时剩下的唯一事情就是清除数据包嗅探器。 Using sniffit , I discovered that there was in fact a difference between what the nginx PushStream module was emitting, and what was being emitted by my implementation. 使用sniffit ,我发现nginx PushStream模块发出的内容与我的实现发出的内容实际上是有区别的。

Mine (yes, I pretended to be nginx/1.2.0 just to absolutely minimize the differences between responses): 我的(是的,我假装为nginx/1.2.0只是为了最大程度地减少响应之间的差异):

HTTP/1.1 200 OK
Server: nginx/1.2.0
Date: Sun, 15 Oct 2013 10:29:38 GMT-5
Content-Type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 01 Jan 1970 00:00:01 GMT
Cache-Control: no-cache, no-store, must-revalidate

data: message goes here

The nginx Push Stream module: Nginx推送流模块:

HTTP/1.1 200 OK
Server: nginx/1.2.0
Date: Sun, 15 Sep 2013 14:40:12 GMT
Content-Type: text/event-stream; charset=utf-8
Connection: close
Expires: Thu, 01 Jan 1970 00:00:01 GMT
Cache-Control: no-cache, no-store, must-revalidate
Transfer-Encoding: chunked

6d
data: message goes here

Adding that "6d" line to my implementation made it work properly. 在我的实现中添加“ 6d”行使其可以正常工作。 I have no idea why, unless this is some convention for bom s in UTF-8 that I'm unfamiliar with. 我不知道为什么,除非这是我不熟悉的UTF-8中bom的约定。 In other words, re-writing subscribe! 换句话说,重写subscribe! as

(defun subscribe! (sock)
  (let ((s (socket-stream sock)))
    (http-write s (list "HTTP/1.1 200 OK" 
                        "Content-Type: text/event-stream; charset=utf-8"
                        "Transfer-Encoding: chunked"
                        "Connection: keep-alive"
                        "Expires: Thu, 01 Jan 1970 00:00:01 GMT"
                        "Cache-Control: no-cache, no-store, must-revalidate" :crlf
                        "6d"))
    (force-output s)
    (push sock *channel*)))

does the trick. 绝招。 Chrom (?:e|ium) now properly accept these event streams, and don't error on message sends. Chrom (?:e|ium)现在可以正确接受这些事件流,并且在消息发送时不会出错。

Now I need to understand exactly what the hell happened there... 现在我需要确切地了解那里发生了什么...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM