否则,我想依靠epoll
(或类似名称)来编写异步网络代码,该代码看起来像不依赖回调的常规代码。
该代码必须看起来像同步代码,但是与同步代码不同,它不是阻塞等待网络io,而是必须挂起当前的协程,并在文件描述符准备好后重新启动。
答案 0 :(得分:1)
我最初的想法是依靠生成器和yield
。但是this was a mistake在某种程度上被python过去滥用yield from
的事实所误导。
无论如何,脆性纤维和I adapted it to chez scheme是一种很大的暗示。
这是示例服务器代码:
(define (handler request port)
(values 200 #f (http-get "https://httpbin.davecheney.com/ip")))
(untangle (lambda ()
(run-server "127.0.0.1" 8888)))
handler
根据httpbin服务返回其IP。该代码在call / cc实际上是call / 1cc的帮助下看起来是同步的。
untangle
将使用作为参数传递的lambda启动事件循环!
这是run-server
的定义:
(define (run-server ip port handler)
(log 'info "HTTP server running at ~a:~a" ip port)
(let* ((sock (socket 'inet 'stream 'ipv4)))
(socket:setsockopt sock 1 2 1) ;; re-use address
(socket:bind sock (make-address ip port))
(socket:listen sock 1024)
(let loop ()
(let ((client (accept sock)))
(let ((port (fd->port client)))
(spawn (lambda () (run-once handler port)))
(loop))))))
如您所见,没有回调。与简单的同步Web服务器唯一不同的是spawn
过程,它将以自己的协程来处理请求。特别是accept
是异步的。
run-once
会将方案请求传递给handler
并采用其3个值来构建响应。不太有趣。上面http-get
看起来是同步的,但实际上是异步的。
鉴于http-get要求引入自定义二进制端口,我只会解释一下accept是如何工作的,但是可以说这是相同的行为...
(define (accept fd)
(let ((out (socket:%accept fd 0 0)))
(if (= out -1)
(let ((code (socket:errno)))
(if (= code EWOULDBLOCK)
(begin
(abort-to-prompt fd 'read)
(accept fd))
(error 'accept (socket:strerror code))))
out)))
如您所见,它调用了一个过程abort-to-prompt
,我们可以简单地调用pause
来“停止”协程并调用提示处理程序。
abort-to-prompt
与call-with-prompt
合作。
由于chez方案没有提示,因此我使用两个连续拍摄call/1cc
(define %prompt #f)
(define %abort (list 'abort))
(define (call-with-prompt thunk handler)
(call-with-values (lambda ()
(call/1cc
(lambda (k)
(set! %prompt k)
(thunk))))
(lambda out
(cond
((and (pair? out) (eq? (car out) %abort))
(apply handler (cdr out)))
(else (apply values out))))))
(define (abort-to-prompt . args)
(call/1cc
(lambda (k)
(let ((prompt %prompt))
(set! %prompt #f)
(apply prompt (cons %abort (cons k args)))))))
call-with-prompt
将启动一个名为set!
的全局%prompt
的延续,这意味着THUNK
会有一个提示。如果连续参数OUT
(call-with-values
的第二个lambda)以唯一对象%abort
开始,则意味着通过abort-to-prompt
达到了连续。它将调用带有HANDLER
延续性的abort-to-prompt
以及传递给call-with-prompt
延续参数(apply handler (cons k (cdr out)))
的任何参数。
abort-to-promp
将在代码执行存储在%prompt
中的提示的继续符之后,启动一个新的继续符,以便能够返回。
call-with-prompt
是事件循环的核心。分为两部分:
(define (exec epoll thunk waiting)
(call-with-prompt
thunk
(lambda (k fd mode) ;; k is abort-to-prompt continuation that
;; will allow to restart the coroutine
;; add fd to the correct epoll set
(case mode
((write) (epoll-wait-write epoll fd))
((read) (epoll-wait-read epoll fd))
(else (error 'untangle "mode not supported" mode)))
(scheme:hash-table-set! waiting fd (make-event k mode)))))
(define (event-loop-run-once epoll waiting)
;; execute every callback waiting in queue,
;; call the above exec procedure
(let loop ()
(unless (null? %queue)
;; XXX: This is done like that because, exec might spawn
;; new coroutine, so we need to cut %queue right now.
(let ((head (car %queue))
(tail (cdr %queue)))
(set! %queue tail)
(exec epoll head waiting)
(loop))))
;; wait for ONE event
(let ((fd (epoll-wait-one epoll (inf))
(let ((event (scheme:hash-table-ref waiting fd)))
;; the event is / will be processed, no need to keep around
(scheme:hash-table-delete! waiting fd)
(case (event-mode event)
((write) (epoll-ctl epoll 2 fd (make-epoll-event-out fd)))
((read) (epoll-ctl epoll 2 fd (make-epoll-event-in fd))))
;; here it will schedule the event continuation that is the
;; abort-to-prompt continuation that will be executed by the
;; next call the above event loop event-loop-run-once
(spawn (event-continuation event))))))
我认为就这些。
答案 1 :(得分:1)
如果您使用的是chez模式,则有chez-a-sync。它使用POSIX poll而不是epoll(epoll是Linux特定的)。 guile-a-sync2也可用于guile-2.2 / 3.0。