BasicSocket 클래스

BasicSocket 클래스

BasicSocket는 모든 Socket 계열 클래스들의 슈퍼 클래스예요. 소켓의 공통 기능(옵션 설정, 메시지 송수신, 주소 조회 등)을 여기서 다루게 돼요.

출처: Ruby 3.3 API

본문

공개 클래스 메서드 (Public Class Methods)

do_not_reverse_lookup → true or false

전역 do_not_reverse_lookup 플래그를 가져와요.

BasicSocket.do_not_reverse_lookup  #=> false

do_not_reverse_lookup = bool

전역 do_not_reverse_lookup 플래그를 설정해요. 이 플래그는 각 소켓의 do_not_reverse_lookup 초기값으로 쓰여요.

s1 = TCPSocket.new("localhost", 80)
p s1.do_not_reverse_lookup                 #=> true
BasicSocket.do_not_reverse_lookup = false
s2 = TCPSocket.new("localhost", 80)
p s2.do_not_reverse_lookup                 #=> false
p s1.do_not_reverse_lookup                 #=> true

for_fd(fd) → basicsocket

파일 디스크립터 fd를 담고 있는 소켓 객체를 반환해요.

# inetd가 실행해주면 STDIN/STDOUT/STDERR이 소켓이 돼요.
STDIN_SOCK = Socket.for_fd(STDIN.fileno)
p STDIN_SOCK.remote_address

공개 인스턴스 메서드 (Public Instance Methods)

close_read → nil

shutdown 시스템 콜을 이용해 더 이상의 읽기를 막아요.

s1, s2 = UNIXSocket.pair
s1.close_read
s2.puts #=> Broken pipe (Errno::EPIPE)

close_write → nil

shutdown 시스템 콜을 이용해 더 이상의 쓰기를 막아요.

UNIXSocket.pair {|s1, s2|
  s1.print "ping"
  s1.close_write
  p s2.read        #=> "ping"
  s2.print "pong"
  s2.close
  p s1.read        #=> "pong"
}

connect_address()

로컬 머신에서 connect를 하기에 적합한 주소를 반환해요. 기본적으로 self.local_address를 반환하지만, 다음 조건에서는 바꿔줘요.

  • IPv4 unspecified 주소(0.0.0.0)는 IPv4 루프백 주소(127.0.0.1)로 바꿔요.
  • IPv6 unspecified 주소(::)는 IPv6 루프백 주소(::1)로 바꿔요.

로컬 주소가 connect에 적합하지 않으면 SocketError를 던져요. 포트가 0인 IPv4/IPv6 주소는 connect에 적합하지 않고, 경로가 없는 Unix 도메인 소켓도 마찬가지예요.

Addrinfo.tcp("0.0.0.0", 0).listen {|serv|
  p serv.connect_address #=> #<Addrinfo: 127.0.0.1:53660 TCP>
  serv.connect_address.connect {|c|
    s, _ = serv.accept
    p [c, s] #=> [#<Socket:fd 4>, #<Socket:fd 6>]
  }
}

구현은 다음과 같아요.

# File ext/socket/lib/socket.rb, line 255
def connect_address
  addr = local_address
  afamily = addr.afamily
  if afamily == Socket::AF_INET
    raise SocketError, "unbound IPv4 socket" if addr.ip_port == 0
    if addr.ip_address == "0.0.0.0"
      addr = Addrinfo.new(["AF_INET", addr.ip_port, nil, "127.0.0.1"], addr.pfamily, addr.socktype, addr.protocol)
    end
  elsif defined?(Socket::AF_INET6) && afamily == Socket::AF_INET6
    raise SocketError, "unbound IPv6 socket" if addr.ip_port == 0
    if addr.ip_address == "::"
      addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
    elsif addr.ip_address == "0.0.0.0" # MacOS X 10.4는 IPv4-mapped IPv6 주소에 "a.b.c.d"를 반환해요.
      addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
    elsif addr.ip_address == "::ffff:0.0.0.0" # MacOS X 10.6은 IPv4-mapped IPv6 주소에 "::ffff:a.b.c.d"를 반환해요.
      addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
    end
  elsif defined?(Socket::AF_UNIX) && afamily == Socket::AF_UNIX
    raise SocketError, "unbound Unix socket" if addr.unix_path == ""
  end
  addr
end

do_not_reverse_lookup → true or false

basicsocket의 do_not_reverse_lookup 플래그를 가져와요.

require 'socket'

BasicSocket.do_not_reverse_lookup = false
TCPSocket.open("www.ruby-lang.org", 80) {|sock|
  p sock.do_not_reverse_lookup      #=> false
}
BasicSocket.do_not_reverse_lookup = true
TCPSocket.open("www.ruby-lang.org", 80) {|sock|
  p sock.do_not_reverse_lookup      #=> true
}

do_not_reverse_lookup = bool

basicsocket의 do_not_reverse_lookup 플래그를 설정해요.

TCPSocket.open("www.ruby-lang.org", 80) {|sock|
  p sock.do_not_reverse_lookup       #=> true
  p sock.peeraddr                    #=> ["AF_INET", 80, "221.186.184.68", "221.186.184.68"]
  sock.do_not_reverse_lookup = false
  p sock.peeraddr                    #=> ["AF_INET", 80, "carbon.ruby-lang.org", "54.163.249.195"]
}

getpeereid → [euid, egid]

UNIX 소켓 피어(상대편)의 사용자와 그룹을 반환해요. 결과는 effective uid와 effective gid를 담은 2요소 배열이에요.

Socket.unix_server_loop("/tmp/sock") {|s|
  begin
    euid, egid = s.getpeereid

    # 연결된 클라이언트가 나 자신인지 확인해요.
    next if euid != Process.uid

    # 내 리소스에 대해 뭔가를 처리해요.

  ensure
    s.close
  end
}

getpeername → sockaddr

소켓의 원격 주소를 sockaddr 문자열로 반환해요.

TCPServer.open("127.0.0.1", 1440) {|serv|
  c = TCPSocket.new("127.0.0.1", 1440)
  s = serv.accept
  p s.getpeername #=> "\x02\x00\x82u\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"
}

이진 문자열보다 Addrinfo 객체가 더 좋다면 BasicSocket#remote_address를 써보세요.

getsockname → sockaddr

소켓의 로컬 주소를 sockaddr 문자열로 반환해요.

TCPServer.open("127.0.0.1", 15120) {|serv|
  p serv.getsockname #=> "\x02\x00;\x10\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"
}

이진 문자열보다 Addrinfo 객체가 더 좋다면 BasicSocket#local_address를 써보세요.

getsockopt(level, optname) → socketoption

소켓 옵션을 가져와요. 옵션은 프로토콜·시스템마다 정해져 있으니 자세한 내용은 로컬 시스템 문서를 참고해야 해요. 옵션은 Socket::Option 객체로 반환돼요.

파라미터

  • level은 정수로, 보통 Socket::SOL_SOCKET 같은 SOL_ 상수 또는 프로토콜 레벨이에요. 접두사 없는 이름의 문자열이나 심볼도 받아줘요.
  • optname은 정수로, 보통 Socket::SO_REUSEADDR 같은 SO_ 상수예요. 접두사 없는 이름의 문자열이나 심볼도 받아줘요.

예시

부울 값을 갖는 정수형 소켓 옵션은 이렇게 호출할 수 있어요.

reuseaddr = sock.getsockopt(:SOCKET, :REUSEADDR).bool

optval = sock.getsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR)
optval = optval.unpack "i"
reuseaddr = optval[0] == 0 ? false : true

숫자 값을 갖는 정수형 옵션은 이렇게요.

ipttl = sock.getsockopt(:IP, :TTL).int

optval = sock.getsockopt(Socket::IPPROTO_IP, Socket::IP_TTL)
ipttl = optval.unpack1("i")

옵션 값이 구조체일 수도 있어요. 디코딩은 시스템 헤더를 살펴 정확한 정의를 알아야 하므로 복잡할 수 있어요. 예를 들어 struct linger는 이렇게 정의돼 있을 수 있어요.

struct linger {
  int l_onoff;
  int l_linger;
};

이 경우엔 이렇게 호출해요.

# Socket::Option이 linger 구조를 알아요.
onoff, linger = sock.getsockopt(:SOCKET, :LINGER).linger

optval =  sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER)
onoff, linger = optval.unpack "ii"
onoff = onoff == 0 ? false : true

local_address → addrinfo

getsockname으로 얻은 로컬 주소를 Addrinfo 객체로 반환해요. addrinfo.protocol은 0으로 채워진다는 점 참고하세요.

TCPSocket.open("www.ruby-lang.org", 80) {|s|
  p s.local_address #=> #<Addrinfo: 192.168.0.129:36873 TCP>
}

TCPServer.open("127.0.0.1", 1512) {|serv|
  p serv.local_address #=> #<Addrinfo: 127.0.0.1:1512 TCP>
}

recv(maxlen[, flags[, outbuf]]) → mesg

메시지를 수신해요. maxlen은 받을 최대 바이트 수예요. flagsSocket::MSG_* 상수들의 비트 OR이에요. outbuf는 호출 전에 비어 있지 않아도, 호출 후에는 받은 데이터만 담겨요.

UNIXSocket.pair {|s1, s2|
  s1.puts "Hello World"
  p s2.recv(4)                     #=> "Hell"
  p s2.recv(4, Socket::MSG_PEEK)   #=> "o Wo"
  p s2.recv(4)                     #=> "o Wo"
  p s2.recv(10)                    #=> "rld\n"
}

recv_nonblock(maxlen [, flags [, buf [, options ]]]) → mesg

기반 파일 디스크립터에 O_NONBLOCK을 설정한 뒤 recvfrom(2)로 소켓에서 최대 maxlen 바이트를 받아요. flagsMSG_ 옵션 0개 이상이에요. 결과 mesg가 받은 데이터예요.

recvfrom(2)가 0을 반환하면 Socket#recv_nonblocknil을 돌려줘요. 대부분은 연결이 닫혔다는 뜻이지만, UDP 연결에서는 빈 패킷을 받았다는 뜻일 수도 있어요 — 기반 API가 이 둘을 구분할 수 없기 때문이에요.

파라미터

  • maxlen - 소켓에서 받을 바이트 수
  • flags - MSG_ 옵션 0개 이상
  • buf - 목적지 String 버퍼
  • options - exception: false를 지원하는 키워드 해시

예시

serv = TCPServer.new("127.0.0.1", 0)
af, port, host, addr = serv.addr
c = TCPSocket.new(addr, port)
s = serv.accept
c.send "aaa", 0
begin # 블로킹 recv를 흉내내요.
  p s.recv_nonblock(10) #=> "aaa"
rescue IO::WaitReadable
  IO.select([s])
  retry
end

recv_nonblock이 실패할 때 던질 수 있는 예외는 Socket#recvfrom을 참고하세요. BasicSocket#recv_nonblockErrno::EWOULDBLOCK을 포함해 recvfrom(2) 실패에 해당하는 어떤 에러든 던질 수 있어요. 예외가 Errno::EWOULDBLOCK 또는 Errno::EAGAIN이면 IO::WaitReadable로 확장돼요. 그래서 IO::WaitReadable로 rescue해 재시도할 수 있어요. 키워드 인자 exception: false를 주면 IO::WaitReadable 예외를 던지는 대신 :wait_readable 심볼을 반환하게 할 수 있어요.

참고 - Socket#recvfrom

# File ext/socket/lib/socket.rb, line 376
def recv_nonblock(len, flag = 0, str = nil, exception: true)
  __recv_nonblock(len, flag, str, exception)
end

recvmsg(maxmesglen=nil, flags=0, maxcontrollen=nil, opts={}) → [mesg, sender_addrinfo, rflags, *controls]

recvmsg(2) 시스템 콜을 블로킹 방식으로 호출해 메시지를 받아요. maxmesglen은 받을 mesg의 최대 길이, flagsSocket::MSG_PEEK 같은 MSG_* 상수들의 비트 OR, maxcontrollen은 받을 컨트롤(부가 데이터)의 최대 길이예요. opts는 옵션 해시로, 현재는 :scm_rights=>bool만 지원해요.

:scm_rights 옵션은 애플리케이션이 SCM_RIGHTS 컨트롤 메시지를 기대한다는 뜻이에요. 값이 nil이나 false면 기대하지 않는 것이고, 이 경우 recvmsg는 전달받은 파일 디스크립터를 즉시 닫아요. 이것이 기본 동작이에요. 값이 nilfalse도 아니면 기대하는 것이고, recvmsgSocket::AncillaryData#unix_rights 메서드를 위해 각 파일 디스크립터마다 IO 객체를 만들어요.

반환값은 4요소 배열이에요.

  • mesg - 받은 메시지 문자열
  • sender_addrinfo - connection-less 소켓의 발신 소켓 주소로 Addrinfo 객체예요. TCP 같은 connection-oriented 소켓에선 플랫폼에 따라 달라져요.
  • rflags - 받은 메시지의 플래그로 Socket::MSG_TRUNC 같은 MSG_* 상수들의 비트 OR이에요. 4.3BSD 스타일의 옛 recvmsg 시스템 콜을 쓰는 시스템에선 nil이 돼요.
  • controls - 부가 데이터로 Socket::AncillaryData 객체들의 배열이에요.
#<Socket::AncillaryData: AF_UNIX SOCKET RIGHTS 7>

maxmesglenmaxcontrollennil이 될 수 있어요. 그 경우 메시지가 잘리지(truncate) 않을 때까지 버퍼를 키워요. 내부적으로 MSG_PEEK를 쓰고, 버퍼가 꽉 찼거나 MSG_CTRUNC인지 확인해 잘림을 판단해요.

recvmsgrecv_io를 이렇게 구현할 수 있어요.

mesg, sender_sockaddr, rflags, *controls = sock.recvmsg(:scm_rights=>true)
controls.each {|ancdata|
  if ancdata.cmsg_is?(:SOCKET, :RIGHTS)
    return ancdata.unix_rights[0]
  end
}
# File ext/socket/lib/socket.rb, line 431
def recvmsg(dlen = nil, flags = 0, clen = nil, scm_rights: false)
  __recvmsg(dlen, flags, clen, scm_rights)
end

recvmsg_nonblock(maxdatalen=nil, flags=0, maxcontrollen=nil, opts={}) → [data, sender_addrinfo, rflags, *controls]

recvmsg(2) 시스템 콜을 논블로킹 방식으로 호출해 메시지를 받아요. BasicSocket#recvmsg와 비슷하지만 시스템 콜 전에 논블로킹 플래그가 설정되고 시스템 콜을 재시도하지 않아요. 키워드 인자 exception: false를 주면 IO::WaitReadable 예외 대신 :wait_readable 심볼을 반환해요.

# File ext/socket/lib/socket.rb, line 447
def recvmsg_nonblock(dlen = nil, flags = 0, clen = nil,
                     scm_rights: false, exception: true)
  __recvmsg_nonblock(dlen, flags, clen, scm_rights, exception)
end

remote_address → addrinfo

getpeername으로 얻은 원격 주소를 Addrinfo 객체로 반환해요. addrinfo.protocol은 0으로 채워진다는 점 참고하세요.

TCPSocket.open("www.ruby-lang.org", 80) {|s|
  p s.remote_address #=> #<Addrinfo: 221.186.184.68:80 TCP>
}

TCPServer.open("127.0.0.1", 1728) {|serv|
  c = TCPSocket.new("127.0.0.1", 1728)
  s = serv.accept
  p s.remote_address #=> #<Addrinfo: 127.0.0.1:36504 TCP>
}

send(mesg, flags [, dest_sockaddr]) → numbytes_sent

basicsocket을 통해 mesg를 보내요. mesg는 문자열이어야 하고, flagsSocket::MSG_* 상수들의 비트 OR이에요. dest_sockaddr은 packed sockaddr 문자열이나 addrinfo예요.

TCPSocket.open("localhost", 80) {|s|
  s.send "GET / HTTP/1.0\r\n\r\n", 0
  p s.read
}

sendmsg(mesg, flags=0, dest_sockaddr=nil, *controls) → numbytes_sent

sendmsg(2) 시스템 콜을 블로킹 방식으로 호출해 메시지를 보내요. mesg는 보낼 문자열, flagsSocket::MSG_OOB 같은 MSG_* 상수들의 비트 OR이에요. dest_sockaddr은 connection-less 소켓의 목적지 주소로, Socket.sockaddr_in 결과 같은 sockaddr이에요. Addrinfo 객체도 쓸 수 있어요. controls는 부가 데이터 목록으로, 요소는 Socket::AncillaryData이거나 cmsg_level, cmsg_type, data를 담은 3요소 배열이에요. 반환값 numbytes_sent는 보낸 바이트 수예요.

sendmsgsend_io를 이렇게 구현할 수 있어요.

# Socket::AncillaryData 사용.
ancdata = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, io.fileno)
sock.sendmsg("a", 0, nil, ancdata)

# 3요소 배열 사용.
ancdata = [:SOCKET, :RIGHTS, [io.fileno].pack("i!")]
sock.sendmsg("\0", 0, nil, ancdata)
# File ext/socket/lib/socket.rb, line 307
def sendmsg(mesg, flags = 0, dest_sockaddr = nil, *controls)
  __sendmsg(mesg, flags, dest_sockaddr, controls)
end

sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls, opts={}) → numbytes_sent

sendmsg(2) 시스템 콜을 논블로킹 방식으로 호출해 메시지를 보내요. BasicSocket#sendmsg와 비슷하지만 시스템 콜 전에 논블로킹 플래그가 설정되고 재시도하지 않아요. 키워드 인자 exception: false를 주면 IO::WaitWritable 예외 대신 :wait_writable 심볼을 반환해요.

# File ext/socket/lib/socket.rb, line 323
def sendmsg_nonblock(mesg, flags = 0, dest_sockaddr = nil, *controls,
                     exception: true)
  __sendmsg_nonblock(mesg, flags, dest_sockaddr, controls, exception)
end

setsockopt(level, optname, optval) / setsockopt(socketoption)

소켓 옵션을 설정해요. 옵션은 프로토콜·시스템마다 정해져 있으니 로컬 시스템 문서를 참고해야 해요.

파라미터

  • level은 정수로, 보통 Socket::SOL_SOCKET 같은 SOL_ 상수 또는 프로토콜 레벨이에요. 접두사 없는 이름의 문자열이나 심볼도 받아줘요.
  • optname은 정수로, 보통 Socket::SO_REUSEADDR 같은 SO_ 상수예요. 접두사 없는 이름의 문자열이나 심볼도 받아줘요.
  • optval의 종류:
    • Integer - 값이 int로 할당되고, int에 대한 포인터가 sizeof(int) 길이로 전달돼요.
    • true/false - 1 또는 0이 int로 할당돼 같은 방식으로 전달돼요. nil이 아니라 false를 넘겨야 한다는 점 주의하세요.
    • String - 문자열의 데이터와 길이가 소켓에 전달돼요.
  • socketoptionSocket::Option 인스턴스예요.

예시

부울 값을 갖는 정수형 옵션:

sock.setsockopt(:SOCKET, :REUSEADDR, true)
sock.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
sock.setsockopt(Socket::Option.bool(:INET, :SOCKET, :REUSEADDR, true))

숫자 값을 갖는 정수형 옵션:

sock.setsockopt(:IP, :TTL, 255)
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, 255)
sock.setsockopt(Socket::Option.int(:INET, :IP, :TTL, 255))

옵션 값이 구조체일 수도 있어요. 예를 들어 ip_mreq는 이렇게 정의돼 있을 수 있어요.

struct ip_mreq {
  struct  in_addr imr_multiaddr;
  struct  in_addr imr_interface;
};

이 경우엔 이렇게 호출해요.

optval = IPAddr.new("224.0.0.251").hton +
         IPAddr.new(Socket::INADDR_ANY, Socket::AF_INET).hton
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, optval)

shutdown([how]) → 0

shutdown(2) 시스템 콜을 호출해요.

  • s.shutdown(Socket::SHUT_RD) - 더 이상 읽기 금지
  • s.shutdown(Socket::SHUT_WR) - 더 이상 쓰기 금지
  • s.shutdown(Socket::SHUT_RDWR) - 읽기·쓰기 모두 금지

how는 심볼이나 문자열이 될 수 있어요.

  • :RD, :SHUT_RD, "RD", "SHUT_RD" - Socket::SHUT_RD
  • :WR, :SHUT_WR, "WR", "SHUT_WR" - Socket::SHUT_WR
  • :RDWR, :SHUT_RDWR, "RDWR", "SHUT_RDWR" - Socket::SHUT_RDWR
s1.puts "ping"
s1.shutdown(:WR)
p s2.read          #=> "ping\n"
s2.puts "pong"
s2.close
p s1.read          #=> "pong\n"