在这个 Common Lisp 示例中,socket-close 的目的是什么?
What is the purpose of the socket-close in this Common Lisp example?
我从 Common Lisp Cookbook 中找到 this example,它展示了如何使用 usocket 启动 TCP 服务器。
示例创建套接字对象并建立连接,然后写入套接字。万一出现错误,套接字写入将被包裹在一个 unwind-protect 中,它将关闭套接字以便它可以被重用。我已经重写了导致错误的示例,但是当我多次 运行 时,我得到了 USOCKET:ADDRESS-IN-USE-ERROR
。如果我删除 socket-close
函数调用,行为是相同的。
(load "~/quicklisp/setup.lisp")
(ql:quickload "usocket")
(let* ((socket (usocket:socket-listen "localhost" 8080))
(connection (usocket:socket-accept socket)))
(unwind-protect
(progn
(error "error 1"))
(progn
(usocket:socket-close connection)
(usocket:socket-close socket)
(print "Error clean up"))))
Unhandled USOCKET:ADDRESS-IN-USE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
{10005E85B3}>:
Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled.
Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {10005E85B3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK *INVOKE-DEBUGGER-HOOK* #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
2: (INVOKE-DEBUGGER #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
3: (ERROR #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
4: (USOCKET:SOCKET-LISTEN "localhost" 8080 :REUSEADDRESS NIL :REUSE-ADDRESS NIL :BACKLOG 5 :ELEMENT-TYPE CHARACTER)
5: ((LAMBDA NIL :IN "/home/sam/test/serve.lisp"))
6: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) #<NULL-LEXENV>)
7: (EVAL-TLF (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) 2 NIL)
8: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) 2)
9: ((LAMBDA (SB-KERNEL:FORM &KEY :CURRENT-INDEX &ALLOW-OTHER-KEYS) :IN SB-INT:LOAD-AS-SOURCE) (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) :CURRENT-INDEX 2)
10: (SB-C::%DO-FORMS-FROM-INFO #<CLOSURE (LAMBDA (SB-KERNEL:FORM &KEY :CURRENT-INDEX &ALLOW-OTHER-KEYS) :IN SB-INT:LOAD-AS-SOURCE) {1001B7128B}> #<SB-C::SOURCE-INFO {1001B71243}> SB-C::INPUT-ERROR-IN-LOAD)
11: (SB-INT:LOAD-AS-SOURCE #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
12: ((FLET SB-FASL::THUNK :IN LOAD))
13: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7FFFF63E769B}> #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}>)
14: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> NIL)
15: (LOAD #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
16: ((FLET SB-IMPL::LOAD-SCRIPT :IN SB-IMPL::PROCESS-SCRIPT) #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}>)
17: ((FLET SB-UNIX::BODY :IN SB-IMPL::PROCESS-SCRIPT))
18: ((FLET "WITHOUT-INTERRUPTS-BODY-3" :IN SB-IMPL::PROCESS-SCRIPT))
19: (SB-IMPL::PROCESS-SCRIPT "serve.lisp")
20: (SB-IMPL::TOPLEVEL-INIT)
21: ((FLET SB-UNIX::BODY :IN SAVE-LISP-AND-DIE))
22: ((FLET "WITHOUT-INTERRUPTS-BODY-36" :IN SAVE-LISP-AND-DIE))
23: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting
你得到这个的原因是因为 TCP 协议的性质:在 RFC793 的 RFC793. A diagram of the state machine is on page 23 描述的 TCP 状态机中,连接处于称为 TIME-WAIT 的状态。
状态机的有趣之处在于当一端(我称之为 'you')想要关闭连接时 – 这被称为 'active close',在这种情况下它是您通过 socket-close
调用启动的内容。我将呼叫另一端 'them'。主动关闭的正常事件顺序是:
- 您向他们发送了一个 FIN 数据包;
- 他们确认您的 FIN 并依次发送 FIN;
- 你确认了他们的 FIN。
现在重要的是要记住这些数据包中的任何一个(它们的 ACK 和 FIN 通常是同一个数据包,我认为总是如此)可能会丢失,并且状态机需要从中恢复。
有一个特别有趣的数据包,它是最后一个 ACK:它特别有趣,因为它是有史以来发送的最后一个数据包,这意味着你无法知道它是否到达了他们.
所以要从两端考虑
从他们这边:他们已经发送了一个 FIN,正在等待你的 ACK。现在:
- 要么 ACK 在适当的时候到达,在这种情况下,他们知道一切都结束了,他们可以拆除连接中涉及的一切。
- 或者,在等待规定的时间后,ACK 没有到达,所以他们必须假设他们的 FIN 丢失了或者你对他们 FIN 的 ACK 丢失了,因此他们必须重新发送 FIN 并返回等待 ACK。
从你这边:你得到了他们的 FIN 并发送了最后一个 ACK。但是你不知道那个 ACK 是否到达了他们。所以你等待一个规定的时间,以便给 他们 一个机会来意识到 ACK 没有得到他们并重新发送他们的 FIN。在此等待期间您无法断开连接,因为您随时可能获得另一个 FIN。
这种等待状态称为 TIME-WAIT,在此期间连接的端点不能被重用。这就是您看到的问题。
您需要在 TIME-WAIT 中等待最长段生命周期 (MSL) 的两倍:MSL 是数据包预计在网络中停留的时间。
当然,如果较早的数据包丢失,还有其他等待状态可能发生在 TIME-WAIT 之前。但是TIME-WAIT是唯一一个总是发生的。
由于语言无法处理名称中的连字符,TIME-WAIT 通常被称为 TIME_WAIT。
我从 Common Lisp Cookbook 中找到 this example,它展示了如何使用 usocket 启动 TCP 服务器。
示例创建套接字对象并建立连接,然后写入套接字。万一出现错误,套接字写入将被包裹在一个 unwind-protect 中,它将关闭套接字以便它可以被重用。我已经重写了导致错误的示例,但是当我多次 运行 时,我得到了 USOCKET:ADDRESS-IN-USE-ERROR
。如果我删除 socket-close
函数调用,行为是相同的。
(load "~/quicklisp/setup.lisp")
(ql:quickload "usocket")
(let* ((socket (usocket:socket-listen "localhost" 8080))
(connection (usocket:socket-accept socket)))
(unwind-protect
(progn
(error "error 1"))
(progn
(usocket:socket-close connection)
(usocket:socket-close socket)
(print "Error clean up"))))
Unhandled USOCKET:ADDRESS-IN-USE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
{10005E85B3}>:
Condition USOCKET:ADDRESS-IN-USE-ERROR was signalled.
Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {10005E85B3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK *INVOKE-DEBUGGER-HOOK* #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
2: (INVOKE-DEBUGGER #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
3: (ERROR #<USOCKET:ADDRESS-IN-USE-ERROR {1003FDC063}>)
4: (USOCKET:SOCKET-LISTEN "localhost" 8080 :REUSEADDRESS NIL :REUSE-ADDRESS NIL :BACKLOG 5 :ELEMENT-TYPE CHARACTER)
5: ((LAMBDA NIL :IN "/home/sam/test/serve.lisp"))
6: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) #<NULL-LEXENV>)
7: (EVAL-TLF (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) 2 NIL)
8: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) 2)
9: ((LAMBDA (SB-KERNEL:FORM &KEY :CURRENT-INDEX &ALLOW-OTHER-KEYS) :IN SB-INT:LOAD-AS-SOURCE) (LET* ((SOCKET (USOCKET:SOCKET-LISTEN "localhost" 8080)) (CONNECTION (USOCKET:SOCKET-ACCEPT SOCKET))) (UNWIND-PROTECT (PROGN (ERROR "error 1")) (PROGN (USOCKET:SOCKET-CLOSE CONNECTION) (USOCKET:SOCKET-CLOSE SOCKET) (PRINT "Error clean up")))) :CURRENT-INDEX 2)
10: (SB-C::%DO-FORMS-FROM-INFO #<CLOSURE (LAMBDA (SB-KERNEL:FORM &KEY :CURRENT-INDEX &ALLOW-OTHER-KEYS) :IN SB-INT:LOAD-AS-SOURCE) {1001B7128B}> #<SB-C::SOURCE-INFO {1001B71243}> SB-C::INPUT-ERROR-IN-LOAD)
11: (SB-INT:LOAD-AS-SOURCE #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
12: ((FLET SB-FASL::THUNK :IN LOAD))
13: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7FFFF63E769B}> #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}>)
14: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> NIL)
15: (LOAD #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
16: ((FLET SB-IMPL::LOAD-SCRIPT :IN SB-IMPL::PROCESS-SCRIPT) #<SB-SYS:FD-STREAM for "file /home/sam/test/serve.lisp" {1001B66763}>)
17: ((FLET SB-UNIX::BODY :IN SB-IMPL::PROCESS-SCRIPT))
18: ((FLET "WITHOUT-INTERRUPTS-BODY-3" :IN SB-IMPL::PROCESS-SCRIPT))
19: (SB-IMPL::PROCESS-SCRIPT "serve.lisp")
20: (SB-IMPL::TOPLEVEL-INIT)
21: ((FLET SB-UNIX::BODY :IN SAVE-LISP-AND-DIE))
22: ((FLET "WITHOUT-INTERRUPTS-BODY-36" :IN SAVE-LISP-AND-DIE))
23: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting
你得到这个的原因是因为 TCP 协议的性质:在 RFC793 的 RFC793. A diagram of the state machine is on page 23 描述的 TCP 状态机中,连接处于称为 TIME-WAIT 的状态。
状态机的有趣之处在于当一端(我称之为 'you')想要关闭连接时 – 这被称为 'active close',在这种情况下它是您通过 socket-close
调用启动的内容。我将呼叫另一端 'them'。主动关闭的正常事件顺序是:
- 您向他们发送了一个 FIN 数据包;
- 他们确认您的 FIN 并依次发送 FIN;
- 你确认了他们的 FIN。
现在重要的是要记住这些数据包中的任何一个(它们的 ACK 和 FIN 通常是同一个数据包,我认为总是如此)可能会丢失,并且状态机需要从中恢复。
有一个特别有趣的数据包,它是最后一个 ACK:它特别有趣,因为它是有史以来发送的最后一个数据包,这意味着你无法知道它是否到达了他们.
所以要从两端考虑
从他们这边:他们已经发送了一个 FIN,正在等待你的 ACK。现在:
- 要么 ACK 在适当的时候到达,在这种情况下,他们知道一切都结束了,他们可以拆除连接中涉及的一切。
- 或者,在等待规定的时间后,ACK 没有到达,所以他们必须假设他们的 FIN 丢失了或者你对他们 FIN 的 ACK 丢失了,因此他们必须重新发送 FIN 并返回等待 ACK。
从你这边:你得到了他们的 FIN 并发送了最后一个 ACK。但是你不知道那个 ACK 是否到达了他们。所以你等待一个规定的时间,以便给 他们 一个机会来意识到 ACK 没有得到他们并重新发送他们的 FIN。在此等待期间您无法断开连接,因为您随时可能获得另一个 FIN。
这种等待状态称为 TIME-WAIT,在此期间连接的端点不能被重用。这就是您看到的问题。
您需要在 TIME-WAIT 中等待最长段生命周期 (MSL) 的两倍:MSL 是数据包预计在网络中停留的时间。
当然,如果较早的数据包丢失,还有其他等待状态可能发生在 TIME-WAIT 之前。但是TIME-WAIT是唯一一个总是发生的。
由于语言无法处理名称中的连字符,TIME-WAIT 通常被称为 TIME_WAIT。