TCP

TCP

여기서는 Racket으로 TCP 네트워크 프로그래밍을 할 때 쓰는 racket/tcp 라이브러리를 다뤄요. 서버 리스너를 만들고, 클라이언트로 연결하며, 연결된 포트로 데이터를 주고받는 절차를 살펴봐요.

출처: Racket Reference

본문

(require racket/tcp)  ; package: base

이 섹션에서 다루는 바인딩은 racket/tcpracket 라이브러리가 제공하며, racket/base는 제공하지 않아요.

TCP에 대한 일반적인 정보는 W. Richard Stevens의 《TCP/IP Illustrated, Volume 1》을 참고하세요.

(tcp-listen port-no
            [max-allow-wait reuse? hostname]) → tcp-listener?
  port-no : listen-port-number?
  max-allow-wait : exact-nonnegative-integer? = 4
  reuse? : any/c = #f
  hostname : (or/c string? #f) = #f

port-no가 지정한 포트 번호로 로컬 머신에 "리스닝(listening)" 서버를 만들어요. port-no가 0이면 소켓은 임시(ephemeral) 포트에 바인딩되는데, 이는 tcp-addresses를 호출해 알아낼 수 있어요. max-allow-wait 인자는 수락을 기다릴 수 있는 최대 클라이언트 연결 수를 결정해요. (max-allow-wait개의 클라이언트가 수락을 기다리는 동안에는 새 클라이언트 연결을 만들 수 없어요.)

reuse? 인자가 참이면, 포트가 TIME_WAIT 상태에 연루돼 있더라도 tcp-listen은 리스너를 만들어요. 이렇게 reuse?를 쓰는 것은 TCP 프로토콜의 특정 보장을 무너뜨려요. 자세한 내용은 Stevens의 책을 참고하세요. 게다가 많은 현대 플랫폼에서 reuse?의 참 값은, 리스너가 이전에 참 값의 reuse?로 만들어졌을 때만 TIME_WAIT을 덮어써요.

hostname#f(기본값)면 리스너는 리스닝 머신의 모든 주소로의 연결을 수락해요. 그렇지 않으면 리스너는 주어진 hostname에 연결된 인터페이스(들)에서만 연결을 수락해요. 예를 들어 hostname으로 "127.0.0.1"을 주면 로컬 머신에서 "127.0.0.1"(루프백 인터페이스)로의 연결만 수락하는 리스너를 만들어요.

Racket은 서로 다른 프로토콜 패밀리의 여러 주소를 수용하기 위해, 필요하면 여러 소켓으로 리스너를 구현해요. Linux에서 hostname이 IPv4와 IPv6 주소 둘 다에 매핑되면, 동작은 IPv6 지원 여부와 IPv6 소켓이 IPv6 연결만 리스닝하도록 구성될 수 있는지에 따라 달라져요: IPv6가 지원되지 않거나 IPv6 소켓을 구성할 수 없으면 IPv6 주소는 무시되고, 그렇지 않으면 각 IPv6 리스너는 IPv6 연결만 수락해요.

FD_CLOEXEC를 지원하는 Unix 및 MacOS 변종에서 리스너 소켓에는 그 플래그가 주어져 subprocess가 만든 하위 프로세스와 공유되지 않아요.

tcp-listen의 반환 값은 TCP 리스너예요. 이 값은 이후 tcp-accept, tcp-accept-ready?, tcp-close 호출에 사용할 수 있어요. 새 TCP 리스너 값은 각각 현재 커스토디언(custodian)의 관리에 들어가요(커스토디언 참고).

tcp-listen이 서버를 시작할 수 없으면 exn:fail:network 예외가 발생해요.

TCP 리스너는 동기화 가능한 이벤트(이벤트 참고)로 사용할 수 있어요. tcp-accept가 블록되지 않을 때 TCP 리스너는 동기화할 준비가 되며, TCP 리스너의 동기화 결과는 그 TCP 리스너 자체예요.

base 패키지 8.11.1.6 버전에서 운영 체제가 지원하는 곳에서 FD_CLOEXEC를 사용하도록 바뀌었어요.

(tcp-connect hostname port-no
             [local-hostname local-port-no]) → input-port? output-port?
  hostname : string?
  port-no : port-number?
  local-hostname : (or/c string? #f) = #f
  local-port-no : (or/c port-number? #f) = #f

리스닝 서버에 클라이언트로 연결을 시도해요. hostname 인자는 서버 호스트의 인터넷 주소 이름이고, port-no는 서버가 리스닝하는 포트 번호예요.

(hostname이 여러 주소와 연관되면, 연결이 성공할 때까지 하나씩 시도해요. "localhost"라는 이름은 일반적으로 로컬 머신을 가리켜요.)

선택적 local-hostnamelocal-port-no는 클라이언트의 주소와 포트를 지정해요. 둘 다 #f(기본값)이면 클라이언트의 주소와 포트가 자동으로 선택돼요. local-hostname#f가 아니면 local-port-no#f가 아니어야 해요. local-port-no#f가 아니고 local-hostname#f이면 주어진 포트를 쓰되 주소는 자동으로 선택돼요.

tcp-connect는 두 값을 반환해요: 입력 포트와 출력 포트. 입력 포트로 서버에서 데이터를 받고 출력 포트로 서버에 데이터를 보낼 수 있어요. 서버가 Racket 프로그램이라면 tcp-accept로 클라이언트와 통신할 포트를 얻을 수 있어요. 이 포트들은 현재 커스토디언의 관리에 들어가요(커스토디언 참고).

처음에 반환된 입력 포트는 블록 버퍼링(block-buffered)되고, 반환된 출력 포트도 블록 버퍼링돼요. file-stream-buffer-mode로 버퍼 모드를 바꾸세요. TCP 출력 포트가 블록 버퍼링될 때, 그 포트에 대해 Nagle 알고리즘이 꺼지는데, 이는 TCP_NODELAY 소켓 옵션을 설정하는 것과 대응돼요.

TCP 연결을 끝내려면 반환된 두 포트를 모두 닫아야 해요. 두 포트가 모두 열려 있는 동안 close-output-port로 출력 포트를 닫으면 서버에 TCP close가 보내져요(서버가 포트로 연결을 읽으면 end-of-file로 보여요). 반면 tcp-abandon-port(아래 참고)는 출력 포트를 닫지만 입력 포트도 닫힐 때까지 TCP close를 보내지 않아요.

TCP 프로토콜은 한 쪽이 보내려 해도 읽지 않으려는 상태를 지원하지 않고, 한 쪽 연결이 완전히 닫혔을 때의 자동 메시지도 포함하지 않아요. 대신 연결의 다른 쪽은 데이터를 보낼 때에만 한 쪽이 완전히 닫혔음을 발견해요. 특히 아직 열려 있는 쪽의 일부 쓰기는 성공한 것처럼 보일 수 있지만, 쓰기는 결국 오류를 만들어내요.

FD_CLOEXEC를 지원하는 Unix 및 MacOS 변종에서 연결 소켓에는 그 플래그가 주어져 subprocess가 만든 하위 프로세스와 공유되지 않아요.

tcp-connect가 연결을 설정할 수 없으면 exn:fail:network 예외가 발생해요.

변경 사항: base 패키지 8.8.0.8 버전에서 블록 버퍼링이 TCP_NODELAY를 의미하도록 바뀌었어요. 8.11.1.6 버전에서 운영 체제가 지원하는 곳에서 FD_CLOEXEC를 사용하도록 바뀌었어요.

(tcp-connect/enable-break hostname port-no [local-hostname] local-port-no)
  → input-port? output-port?
  hostname : string?
  port-no : port-number?
  local-hostname : (or/c string? #f) = #f
  local-port-no : (or/c port-number? #f)

tcp-connect와 같지만, 연결을 시도하는 동안 브레이크(break)가 활성화돼요(브레이크 참고). tcp-connect/enable-break를 호출할 때 브레이크가 비활성화되어 있으면, 포트가 반환되거나 exn:break 예외가 발생하며, 둘 다는 아니에요.

(tcp-accept listener) → input-port? output-port?
  listener : tcp-listener?

listener와 연관된 서버의 클라이언트 연결을 수락해요. 리스닝 포트에서 대기 중인 클라이언트 연결이 없으면 tcp-accept 호출은 블록돼요. (tcp-accept-ready?도 함께 보세요.)

tcp-accept는 두 값을 반환해요: 입력 포트와 출력 포트. 입력 포트로 클라이언트에서 데이터를 받고 출력 포트로 클라이언트에 데이터를 보낼 수 있어요. 이 포트들은 현재 커스토디언의 관리에 들어가요(커스토디언 참고).

버퍼링과 연결 상태 면에서 이 포트들은 tcp-connect의 포트와 똑같이 동작해요.

FD_CLOEXEC를 지원하는 Unix 및 MacOS 변종에서 수락된 소켓에는 그 플래그가 주어져 subprocess가 만든 하위 프로세스와 공유되지 않아요.

tcp-accept가 연결을 수락할 수 없거나 리스너가 닫혀 있으면 exn:fail:network 예외가 발생해요.

base 패키지 8.11.1.6 버전에서 운영 체제가 지원하는 곳에서 FD_CLOEXEC를 사용하도록 바뀌었어요.

(tcp-accept/enable-break listener) → input-port? output-port?
  listener : tcp-listener?

tcp-accept와 같지만, 연결을 수락하는 동안 브레이크가 활성화돼요(브레이크 참고). tcp-accept/enable-break를 호출할 때 브레이크가 비활성화되어 있으면, 포트가 반환되거나 exn:break 예외가 발생하며, 둘 다는 아니에요.

(tcp-accept-ready? listener) → boolean?
  listener : tcp-listener?

수락되지 않은 클라이언트가 listener와 연관된 서버에 연결했는지 검사해요. 클라이언트가 대기 중이면 #t, 아니면 #f를 반환해요. 클라이언트는 tcp-accept 프로시저로 수락되며, 이는 클라이언트와 통신할 포트를 반환하고 수락되지 않은 클라이언트 목록에서 클라이언트를 제거해요.

리스너가 닫혀 있으면 exn:fail:network 예외가 발생해요.

(tcp-close listener) → void?
  listener : tcp-listener?

listener와 연관된 서버를 종료해요. 수락되지 않은 모든 클라이언트는 서버로부터 end-of-file을 받고, 수락된 클라이언트와의 연결은 영향을 받지 않아요.

리스너가 이미 닫혀 있으면 exn:fail:network 예외가 발생해요.

리스너의 포트 번호는 (기본 reuse? 인자를 가진 tcp-listen의 경우) 새 리스너에 즉시 사용 가능하지 않을 수 있어요. 자세한 내용은 Stevens의 TIME_WAIT TCP 상태에 대한 설명을 참고하세요.

(tcp-listener? v) → boolean?
  v : any/c

vtcp-listen이 만든 TCP 리스너이면 #t, 아니면 #f를 반환해요.

(tcp-accept-evt listener) → evt?
  listener : tcp-listener?

listener에 대한 tcp-accept가 블록되지 않을 때 동기화할 준비가 되는 동기화 가능한 이벤트(이벤트 참고)를 반환해요. 동기화 결과는 tcp-accept의 두 결과에 대응하는 두 항목의 목록이에요. (이벤트가 sync에서 선택되지 않으면 수락되는 연결이 없어요.) 포트들은 tcp-accept-evt가 호출될 때 현재 커스토디언인 커스토디언의 관리에 들어가요(커스토디언 참고).

(tcp-abandon-port tcp-port) → void?
  tcp-port : tcp-port?

close-output-port 또는 close-input-port와 같지만(tcp-port가 출력 포트인지 입력 포트인지에 따라), tcp-port가 출력 포트이고 연관된 입력 포트가 아직 닫히지 않았다면, TCP 연결의 다른 쪽은 입력 포트도 닫힐 때까지 TCP close 메시지를 받지 않아요.

TCP 프로토콜은 연결에 "더 이상 읽지 않음" 상태를 포함하지 않으므로, tcp-abandon-port는 입력 TCP 포트에서는 close-input-port와 동등해요.

(tcp-addresses tcp-port [port-numbers?])
  → (or/c (values string? string?)
          (values string? port-number? string? listen-port-number?))
  tcp-port : (or/c tcp-port? tcp-listener? udp?)
  port-numbers? : any/c = #f

port-numbers?#f(기본값)일 때 두 문자열을 반환해요. 첫 번째 문자열은 주어진 TCP 포트의 연결, TCP 리스너, 또는 UDP 소켓이 보는 로컬 머신의 인터넷 주소예요. (머신이 여러 주소를 서비스할 때, 일반적으로 루프백 장치를 세면 그렇듯, 결과는 연결별 또는 리스너별로 달라져요.) 리스너나 UDP 소켓이 주어졌는데 특정 호스트가 없으면 첫 번째 문자열 결과는 "0.0.0.0"이에요. 두 번째 문자열은 연결의 다른 쪽 끝의 인터넷 주소이고, 리스너나 연결되지 않은 UDP 소켓에 대해서는 항상 "0.0.0.0"이에요.

port-numbers?가 참이면 네 결과가 반환돼요: 로컬 머신 주소용 문자열, 로컬 머신 포트 번호용 1과 65535 사이의 정확한 정수, 원격 머신 주소용 문자열, 그리고 원격 머신 포트 번호(1과 65535 사이) 또는 리스너용 0.

주어진 포트, 리스너 또는 소켓이 닫혀 있으면 exn:fail:network 예외가 발생해요.

(tcp-port? v) → boolean?
  v : any/c

v가 TCP 포트—즉 tcp-accept, tcp-connect, tcp-accept/enable-break, 또는 tcp-connect/enable-break가 반환한 포트—이면 #t, 아니면 #f를 반환해요.

port-number? : contract?

(integer-in 1 65535)와 동등해요.

base 패키지 6.3 버전에서 추가.

listen-port-number? : contract?

(integer-in 0 65535)와 동등해요.

base 패키지 6.3 버전에서 추가.

더 알아보기

  • 커스토디언(Custodians)
  • 이벤트(Events)와 동기화
  • subprocess, 브레이크(Breaks)