Какова цель закрытия сокета в этом примере Common Lisp? - PullRequest
1 голос
/ 21 марта 2020

Я нашел этот пример из Кулинарной книги Common Lisp, в которой показано, как запустить TCP-сервер с помощью usocket.

В этом примере создается объект сокета и устанавливается соединение, а затем пишет в сокет. В случае ошибки запись в сокет оборачивается защитой от размотки, которая закрывает сокет, чтобы его можно было использовать повторно. Я переписал пример, чтобы вызвать ошибку, но когда я запускаю это несколько раз, я получаю 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

1 Ответ

5 голосов
/ 22 марта 2020

Причина, по которой вы получаете это, связана с природой протокола TCP: соединение находится в состоянии TIME-WAIT в конечном автомате TCP, описанном RFC793 . Диаграмма конечного автомата находится на странице 23 RFC793.

Интересный момент конечного автомата - это когда один конец (которого я назову «вами») захочет закрыть соединение - это известно как «активное закрытие», и в этом случае это то, что вы инициируете вызовами socket-close. Я назову другой конец «их». Обычная последовательность событий для активного закрытия:

  1. вы отправляете им пакет FIN;
  2. они принимают ваш FIN и по очереди отправляют FIN;
  3. вы подтверждаете их FIN.

Теперь важно помнить, что любой из этих пакетов (их ACK и FIN обычно являются одним и тем же пакетом, и я думаю, что всегда) может быть потерян, и конечный автомат должен восстановиться отсюда.

Есть один особенно интересный пакет, который является последним ACK: он особенно интересен, потому что это последний отправленный пакет , что означает, что у вас нет возможности узнать, он достиг их .

Итак, рассмотрим ситуацию с обоих концов

С их конца : они отправили FIN и ждут вашего ACK для него , Теперь:

  • либо ACK прибывает в должное время, и в этом случае они знают, что все свернуто, и они могут демонтировать все, что связано с соединением.
  • или, после ожидания в установленное время ACK не приходит , поэтому они должны предположить, что либо их FIN потерян, либо ваш ACK на их FIN потерян, и поэтому они должны повторно отправить FIN и go вернуться к ожиданию ACK.

С вашего конца : вы получили их FIN и отправили последний ACK. Но вы понятия не имеете, дошел ли когда-нибудь до этого ACK. Таким образом, вы ждете в течение предписанного времени, чтобы дать им шанс понять, что ACK не получил их, и повторно отправить их FIN. Во время этого ожидания вы не можете разорвать соединение, потому что в любой момент вы можете получить еще один FIN.

Это состояние ожидания называется TIME-WAIT, и во время него конечная точка соединения не может быть повторно использована , И это проблема, с которой вы сталкиваетесь.

Вам нужно сидеть в TIME-WAIT вдвое больше максимального времени жизни сегмента (MSL): MSL - это то, как долго пакет может находиться в сети.

Существуют и другие состояния ожидания, которые могут возникать до времени ожидания, если ранние пакеты будут потеряны. Но TIME-WAIT является единственным, который всегда встречается.

TIME-WAIT часто называют TIME_WAIT из-за языков, которые не могут обрабатывать дефисы в именах.


...