From d6c063a672b11449dadd1a86ff7d02ed9f6c3924 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Thu, 22 Dec 2016 00:27:20 +0100 Subject: [PATCH 1/3] Sockets refactor: allow any family/type/protocol association Refactor: - Socket is now enough to create, configure and use any kind of socket association of family, type and protocol is also possible, as long as it's supported by the underlying OS implementation. - The TCPSocket, TCPServer, UDPSocket, UNIXSocket and UNIXServer classes are merely sugar to avoid having to deal with socket details. - UNIXSocket and UNIXServer can now be used in DGRAM type, in addition to the default STREAM type. Features: - Adds Socket::Server type, included by both TCPServer and UNIXServer. - Adds Addrinfo DNS resolver, that wraps results from `getaddrinfo`. Breaking Changes: - IPAddress now automatically detects the address family, so the argument was removed (limited impact). --- spec/std/socket_spec.cr | 136 ++++--- src/errno.cr | 3 +- .../amd64-unknown-openbsd/c/sys/socket.cr | 8 + src/lib_c/i686-linux-gnu/c/netdb.cr | 23 +- src/lib_c/i686-linux-gnu/c/sys/socket.cr | 14 +- src/lib_c/i686-linux-musl/c/netdb.cr | 23 +- src/lib_c/i686-linux-musl/c/sys/socket.cr | 6 + src/lib_c/x86_64-linux-gnu/c/netdb.cr | 23 +- src/lib_c/x86_64-linux-gnu/c/sys/socket.cr | 14 +- src/lib_c/x86_64-linux-musl/c/netdb.cr | 23 +- src/lib_c/x86_64-linux-musl/c/sys/socket.cr | 6 + src/lib_c/x86_64-macosx-darwin/c/netdb.cr | 23 +- .../x86_64-macosx-darwin/c/sys/socket.cr | 8 + src/lib_c/x86_64-portbld-freebsd/c/netdb.cr | 23 +- .../x86_64-portbld-freebsd/c/sys/socket.cr | 8 + src/socket.cr | 373 +++++++++++++----- src/socket/address.cr | 188 +++++++++ src/socket/addrinfo.cr | 91 +++++ src/socket/ip_socket.cr | 128 +----- src/socket/server.cr | 51 +++ src/socket/tcp_server.cr | 120 +----- src/socket/tcp_socket.cr | 20 +- src/socket/udp_socket.cr | 113 +----- src/socket/unix_server.cr | 79 +--- src/socket/unix_socket.cr | 37 +- 25 files changed, 960 insertions(+), 581 deletions(-) create mode 100644 src/socket/address.cr create mode 100644 src/socket/addrinfo.cr create mode 100644 src/socket/server.cr diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 0b0ee66e5269..74617d5eca9a 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -4,7 +4,7 @@ require "socket" describe Socket do # Tests from libc-test: # http://repo.or.cz/libc-test.git/blob/master:/src/functional/inet_pton.c - assert "ip?" do + it ".ip?" do # dotted-decimal notation Socket.ip?("0.0.0.0").should be_true Socket.ip?("127.0.0.1").should be_true @@ -65,29 +65,46 @@ describe Socket do end describe Socket::IPAddress do - it "transforms an IPv4 address into a C struct and back again" do - addr1 = Socket::IPAddress.new(Socket::Family::INET, "127.0.0.1", 8080.to_i16) - addr2 = Socket::IPAddress.new(addr1.sockaddr, addr1.addrlen) - - addr1.family.should eq(addr2.family) - addr1.port.should eq(addr2.port) - addr1.address.should eq(addr2.address) - addr1.to_s.should eq("127.0.0.1:8080") + it "transforms an IPv4 address into a C struct and back" do + addr1 = Socket::IPAddress.new("127.0.0.1", 8080) + addr2 = Socket::IPAddress.from(addr1.to_unsafe, addr1.size) + + addr2.family.should eq(addr1.family) + addr2.port.should eq(addr1.port) + addr2.address.should eq(addr1.address) end - it "transforms an IPv6 address into a C struct and back again" do - addr1 = Socket::IPAddress.new(Socket::Family::INET6, "2001:db8:8714:3a90::12", 8080.to_i16) - addr2 = Socket::IPAddress.new(addr1.sockaddr, addr1.addrlen) + it "transforms an IPv6 address into a C struct and back" do + addr1 = Socket::IPAddress.new("2001:db8:8714:3a90::12", 8080) + addr2 = Socket::IPAddress.from(addr1.to_unsafe, addr1.size) + + addr2.family.should eq(addr1.family) + addr2.port.should eq(addr1.port) + addr2.address.should eq(addr1.address) + end - addr1.family.should eq(addr2.family) - addr1.port.should eq(addr2.port) - addr1.address.should eq(addr2.address) - addr1.to_s.should eq("2001:db8:8714:3a90::12:8080") + it "to_s" do + Socket::IPAddress.new("127.0.0.1", 80).to_s.should eq("127.0.0.1:80") + Socket::IPAddress.new("2001:db8:8714:3a90::12", 443).to_s.should eq("[2001:db8:8714:3a90::12]:443") end end describe Socket::UNIXAddress do - it "does to_s" do + it "transforms into a C struct and back" do + addr1 = Socket::UNIXAddress.new("/tmp/service.sock") + addr2 = Socket::UNIXAddress.from(addr1.to_unsafe, addr1.size) + + addr2.family.should eq(addr1.family) + addr2.path.should eq(addr1.path) + addr2.to_s.should eq("/tmp/service.sock") + end + + it "raises when path is too long" do + path = "/tmp/crystal-test-too-long-unix-socket-#{("a" * 2048)}.sock" + expect_raises(ArgumentError, "Path size exceeds the maximum size") { Socket::UNIXAddress.new(path) } + end + + it "to_s" do Socket::UNIXAddress.new("some_path").to_s.should eq("some_path") end end @@ -211,8 +228,6 @@ describe UNIXSocket do client.local_address.path.should eq(path) server.accept do |sock| - sock.sync?.should eq(server.sync?) - sock.local_address.family.should eq(Socket::Family::UNIX) sock.local_address.path.should eq("") @@ -225,8 +240,19 @@ describe UNIXSocket do client.gets(4).should eq("pong") end end + end + end + + it "sync flag after accept" do + path = "/tmp/crystal-test-unix-sock" + + UNIXServer.open(path) do |server| + UNIXSocket.open(path) do |client| + server.accept do |sock| + sock.sync?.should eq(server.sync?) + end + end - # test sync flag propagation after accept server.sync = !server.sync? UNIXSocket.open(path) do |client| @@ -244,6 +270,7 @@ describe UNIXSocket do left << "ping" right.gets(4).should eq("ping") + right << "pong" left.gets(4).should eq("pong") end @@ -382,31 +409,25 @@ describe TCPSocket do end it "fails when host doesn't exist" do - expect_raises(Socket::Error, /^getaddrinfo: (.+ not known|no address .+|Non-recoverable failure in name resolution|Name does not resolve)$/i) do + expect_raises(Socket::Error, /No address found for localhostttttt:12345/) do TCPSocket.new("localhostttttt", 12345) end end end describe UDPSocket do - it "sends and receives messages by reading and writing" do + it "reads and writes data to server" do port = free_udp_socket_port server = UDPSocket.new(Socket::Family::INET6) server.bind("::", port) - - server.local_address.family.should eq(Socket::Family::INET6) - server.local_address.port.should eq(port) - server.local_address.address.should eq("::") + server.local_address.should eq(Socket::IPAddress.new("::", port)) client = UDPSocket.new(Socket::Family::INET6) client.connect("::1", port) - client.local_address.family.should eq(Socket::Family::INET6) client.local_address.address.should eq("::1") - client.remote_address.family.should eq(Socket::Family::INET6) - client.remote_address.port.should eq(port) - client.remote_address.address.should eq("::1") + client.remote_address.should eq(Socket::IPAddress.new("::1", port)) client << "message" server.gets(7).should eq("message") @@ -415,51 +436,62 @@ describe UDPSocket do server.close end - it "sends and receives messages by send and receive over IPv4" do + it "sends and receives messages over IPv4" do + buffer = uninitialized UInt8[256] + server = UDPSocket.new(Socket::Family::INET) server.bind("127.0.0.1", 0) client = UDPSocket.new(Socket::Family::INET) - - buffer = uninitialized UInt8[256] - client.send("message equal to buffer", server.local_address) - bytes_read, addr1 = server.receive(buffer.to_slice[0, 23]) - message1 = String.new(buffer.to_slice[0, bytes_read]) - message1.should eq("message equal to buffer") - addr1.family.should eq(server.local_address.family) - addr1.address.should eq(server.local_address.address) + + bytes_read, client_addr = server.receive(buffer.to_slice[0, 23]) + message = String.new(buffer.to_slice[0, bytes_read]) + message.should eq("message equal to buffer") + client_addr.should eq(Socket::IPAddress.new("127.0.0.1", client.local_address.port)) client.send("message less than buffer", server.local_address) - bytes_read, addr2 = server.receive(buffer.to_slice) - message2 = String.new(buffer.to_slice[0, bytes_read]) - message2.should eq("message less than buffer") - addr2.family.should eq(server.local_address.family) - addr2.address.should eq(server.local_address.address) + + bytes_read, client_addr = server.receive(buffer.to_slice) + message = String.new(buffer.to_slice[0, bytes_read]) + message.should eq("message less than buffer") + + client.connect server.local_address + client.send "ip4 message" + + message, client_addr = server.receive + message.should eq("ip4 message") + client_addr.should eq(Socket::IPAddress.new("127.0.0.1", client.local_address.port)) server.close client.close end - it "sends and receives messages by send and receive over IPv6" do + it "sends and receives messages over IPv6" do + buffer = uninitialized UInt8[1500] + server = UDPSocket.new(Socket::Family::INET6) server.bind("::1", 0) client = UDPSocket.new(Socket::Family::INET6) + client.send("some message", server.local_address) - buffer = uninitialized UInt8[1500] + bytes_read, client_addr = server.receive(buffer.to_slice) + String.new(buffer.to_slice[0, bytes_read]).should eq("some message") + client_addr.should eq(Socket::IPAddress.new("::1", client.local_address.port)) + + client.connect server.local_address + client.send "ip6 message" - client.send("message", server.local_address) - bytes_read, addr = server.receive(buffer.to_slice) - String.new(buffer.to_slice[0, bytes_read]).should eq("message") - addr.family.should eq(server.local_address.family) - addr.address.should eq(server.local_address.address) + message, client_addr = server.receive(20) + message.should eq("ip6 message") + client_addr.should eq(Socket::IPAddress.new("::1", client.local_address.port)) server.close client.close end - it "broadcast messages" do + it "broadcasts messages" do port = free_udp_socket_port client = UDPSocket.new(Socket::Family::INET) diff --git a/src/errno.cr b/src/errno.cr index 7046793be0ad..f035fadf5c18 100644 --- a/src/errno.cr +++ b/src/errno.cr @@ -212,8 +212,7 @@ class Errno < Exception # raise Errno.new("some_call") # end # ``` - def initialize(message) - errno = Errno.value + def initialize(message, errno = Errno.value) @errno = errno super "#{message}: #{String.new(LibC.strerror(errno))}" end diff --git a/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr b/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr index 06d2108a7c89..a33daa04585c 100644 --- a/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr +++ b/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr @@ -36,6 +36,14 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_len : UChar + ss_family : SaFamilyT + __ss_pad1 : StaticArray(Char, 6) + __ss_pad2 : ULongLong + __ss_pad3 : StaticArray(Char, 240) + end + struct Linger l_onoff : Int l_linger : Int diff --git a/src/lib_c/i686-linux-gnu/c/netdb.cr b/src/lib_c/i686-linux-gnu/c/netdb.cr index f1edb640898a..ce727b4b21ac 100644 --- a/src/lib_c/i686-linux-gnu/c/netdb.cr +++ b/src/lib_c/i686-linux-gnu/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x0001 + AI_CANONNAME = 0x0002 + AI_NUMERICHOST = 0x0004 + AI_NUMERICSERV = 0x0400 + AI_V4MAPPED = 0x0008 + AI_ALL = 0x0010 + AI_ADDRCONFIG = 0x0020 + EAI_AGAIN = -3 + EAI_BADFLAGS = -1 + EAI_FAIL = -4 + EAI_FAMILY = -6 + EAI_MEMORY = -10 + EAI_NONAME = -2 + EAI_SERVICE = -8 + EAI_SOCKTYPE = -7 + EAI_SYSTEM = -11 + EAI_OVERFLOW = -12 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(ai : Addrinfo*) : Void fun gai_strerror(ecode : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(name : Char*, service : Char*, req : Addrinfo*, pai : Addrinfo**) : Int + fun getnameinfo(sa : Sockaddr*, salen : SocklenT, host : Char*, hostlen : SocklenT, serv : Char*, servlen : SocklenT, flags : Int) : Int end diff --git a/src/lib_c/i686-linux-gnu/c/sys/socket.cr b/src/lib_c/i686-linux-gnu/c/sys/socket.cr index 8364aa62cf4c..5d977a78ba3a 100644 --- a/src/lib_c/i686-linux-gnu/c/sys/socket.cr +++ b/src/lib_c/i686-linux-gnu/c/sys/socket.cr @@ -35,6 +35,12 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_family : SaFamilyT + __ss_align : ULong + __ss_padding : StaticArray(Char, 120) + end + struct Linger l_onoff : Int l_linger : Int @@ -47,10 +53,10 @@ lib LibC fun getsockname(fd : Int, addr : Sockaddr*, len : SocklenT*) : Int fun getsockopt(fd : Int, level : Int, optname : Int, optval : Void*, optlen : SocklenT*) : Int fun listen(fd : Int, n : Int) : Int - fun recv(fd : Int, buf : Void*, n : SizeT, flags : Int) : SSizeT - fun recvfrom(fd : Int, buf : Void*, n : SizeT, flags : Int, addr : Sockaddr*, addr_len : SocklenT*) : SSizeT - fun send(fd : Int, buf : Void*, n : SizeT, flags : Int) : SSizeT - fun sendto(fd : Int, buf : Void*, n : SizeT, flags : Int, addr : Sockaddr*, addr_len : SocklenT) : SSizeT + fun recv(fd : Int, buf : Void*, n : Int, flags : Int) : SSizeT + fun recvfrom(fd : Int, buf : Void*, n : Int, flags : Int, addr : Sockaddr*, addr_len : SocklenT*) : SSizeT + fun send(fd : Int, buf : Void*, n : Int, flags : Int) : SSizeT + fun sendto(fd : Int, buf : Void*, n : Int, flags : Int, addr : Sockaddr*, addr_len : SocklenT) : SSizeT fun setsockopt(fd : Int, level : Int, optname : Int, optval : Void*, optlen : SocklenT) : Int fun shutdown(fd : Int, how : Int) : Int fun socket(domain : Int, type : Int, protocol : Int) : Int diff --git a/src/lib_c/i686-linux-musl/c/netdb.cr b/src/lib_c/i686-linux-musl/c/netdb.cr index 4e06b1208dd2..a5ac97843811 100644 --- a/src/lib_c/i686-linux-musl/c/netdb.cr +++ b/src/lib_c/i686-linux-musl/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x01 + AI_CANONNAME = 0x02 + AI_NUMERICHOST = 0x04 + AI_NUMERICSERV = 0x400 + AI_V4MAPPED = 0x08 + AI_ALL = 0x10 + AI_ADDRCONFIG = 0x20 + EAI_AGAIN = -3 + EAI_BADFLAGS = -1 + EAI_FAIL = -4 + EAI_FAMILY = -6 + EAI_MEMORY = -10 + EAI_NONAME = -2 + EAI_SERVICE = -8 + EAI_SOCKTYPE = -7 + EAI_SYSTEM = -11 + EAI_OVERFLOW = -12 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(x0 : Addrinfo*) : Void fun gai_strerror(x0 : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(x0 : Char*, x1 : Char*, x2 : Addrinfo*, x3 : Addrinfo**) : Int + fun getnameinfo(x0 : Sockaddr*, x1 : SocklenT, x2 : Char*, x3 : SocklenT, x4 : Char*, x5 : SocklenT, x6 : Int) : Int end diff --git a/src/lib_c/i686-linux-musl/c/sys/socket.cr b/src/lib_c/i686-linux-musl/c/sys/socket.cr index 1956a5c7b988..b43b14d6bf8b 100644 --- a/src/lib_c/i686-linux-musl/c/sys/socket.cr +++ b/src/lib_c/i686-linux-musl/c/sys/socket.cr @@ -35,6 +35,12 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_family : SaFamilyT + __ss_align : ULong + __ss_padding : StaticArray(Char, 112) + end + struct Linger l_onoff : Int l_linger : Int diff --git a/src/lib_c/x86_64-linux-gnu/c/netdb.cr b/src/lib_c/x86_64-linux-gnu/c/netdb.cr index f1edb640898a..ce727b4b21ac 100644 --- a/src/lib_c/x86_64-linux-gnu/c/netdb.cr +++ b/src/lib_c/x86_64-linux-gnu/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x0001 + AI_CANONNAME = 0x0002 + AI_NUMERICHOST = 0x0004 + AI_NUMERICSERV = 0x0400 + AI_V4MAPPED = 0x0008 + AI_ALL = 0x0010 + AI_ADDRCONFIG = 0x0020 + EAI_AGAIN = -3 + EAI_BADFLAGS = -1 + EAI_FAIL = -4 + EAI_FAMILY = -6 + EAI_MEMORY = -10 + EAI_NONAME = -2 + EAI_SERVICE = -8 + EAI_SOCKTYPE = -7 + EAI_SYSTEM = -11 + EAI_OVERFLOW = -12 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(ai : Addrinfo*) : Void fun gai_strerror(ecode : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(name : Char*, service : Char*, req : Addrinfo*, pai : Addrinfo**) : Int + fun getnameinfo(sa : Sockaddr*, salen : SocklenT, host : Char*, hostlen : SocklenT, serv : Char*, servlen : SocklenT, flags : Int) : Int end diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr b/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr index 8364aa62cf4c..4729b1d158bf 100644 --- a/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr +++ b/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr @@ -35,6 +35,12 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_family : SaFamilyT + __ss_align : ULong + __ss_padding : StaticArray(Char, 112) + end + struct Linger l_onoff : Int l_linger : Int @@ -47,10 +53,10 @@ lib LibC fun getsockname(fd : Int, addr : Sockaddr*, len : SocklenT*) : Int fun getsockopt(fd : Int, level : Int, optname : Int, optval : Void*, optlen : SocklenT*) : Int fun listen(fd : Int, n : Int) : Int - fun recv(fd : Int, buf : Void*, n : SizeT, flags : Int) : SSizeT - fun recvfrom(fd : Int, buf : Void*, n : SizeT, flags : Int, addr : Sockaddr*, addr_len : SocklenT*) : SSizeT - fun send(fd : Int, buf : Void*, n : SizeT, flags : Int) : SSizeT - fun sendto(fd : Int, buf : Void*, n : SizeT, flags : Int, addr : Sockaddr*, addr_len : SocklenT) : SSizeT + fun recv(fd : Int, buf : Void*, n : Int, flags : Int) : SSizeT + fun recvfrom(fd : Int, buf : Void*, n : Int, flags : Int, addr : Sockaddr*, addr_len : SocklenT*) : SSizeT + fun send(fd : Int, buf : Void*, n : Int, flags : Int) : SSizeT + fun sendto(fd : Int, buf : Void*, n : Int, flags : Int, addr : Sockaddr*, addr_len : SocklenT) : SSizeT fun setsockopt(fd : Int, level : Int, optname : Int, optval : Void*, optlen : SocklenT) : Int fun shutdown(fd : Int, how : Int) : Int fun socket(domain : Int, type : Int, protocol : Int) : Int diff --git a/src/lib_c/x86_64-linux-musl/c/netdb.cr b/src/lib_c/x86_64-linux-musl/c/netdb.cr index 4e06b1208dd2..a5ac97843811 100644 --- a/src/lib_c/x86_64-linux-musl/c/netdb.cr +++ b/src/lib_c/x86_64-linux-musl/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x01 + AI_CANONNAME = 0x02 + AI_NUMERICHOST = 0x04 + AI_NUMERICSERV = 0x400 + AI_V4MAPPED = 0x08 + AI_ALL = 0x10 + AI_ADDRCONFIG = 0x20 + EAI_AGAIN = -3 + EAI_BADFLAGS = -1 + EAI_FAIL = -4 + EAI_FAMILY = -6 + EAI_MEMORY = -10 + EAI_NONAME = -2 + EAI_SERVICE = -8 + EAI_SOCKTYPE = -7 + EAI_SYSTEM = -11 + EAI_OVERFLOW = -12 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(x0 : Addrinfo*) : Void fun gai_strerror(x0 : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(x0 : Char*, x1 : Char*, x2 : Addrinfo*, x3 : Addrinfo**) : Int + fun getnameinfo(x0 : Sockaddr*, x1 : SocklenT, x2 : Char*, x3 : SocklenT, x4 : Char*, x5 : SocklenT, x6 : Int) : Int end diff --git a/src/lib_c/x86_64-linux-musl/c/sys/socket.cr b/src/lib_c/x86_64-linux-musl/c/sys/socket.cr index 1956a5c7b988..b43b14d6bf8b 100644 --- a/src/lib_c/x86_64-linux-musl/c/sys/socket.cr +++ b/src/lib_c/x86_64-linux-musl/c/sys/socket.cr @@ -35,6 +35,12 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_family : SaFamilyT + __ss_align : ULong + __ss_padding : StaticArray(Char, 112) + end + struct Linger l_onoff : Int l_linger : Int diff --git a/src/lib_c/x86_64-macosx-darwin/c/netdb.cr b/src/lib_c/x86_64-macosx-darwin/c/netdb.cr index 2127937a02f7..1a37cdafa00c 100644 --- a/src/lib_c/x86_64-macosx-darwin/c/netdb.cr +++ b/src/lib_c/x86_64-macosx-darwin/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x00000001 + AI_CANONNAME = 0x00000002 + AI_NUMERICHOST = 0x00000004 + AI_NUMERICSERV = 0x00001000 + AI_V4MAPPED = 0x00000800 + AI_ALL = 0x00000100 + AI_ADDRCONFIG = 0x00000400 + EAI_AGAIN = 2 + EAI_BADFLAGS = 3 + EAI_FAIL = 4 + EAI_FAMILY = 5 + EAI_MEMORY = 6 + EAI_NONAME = 8 + EAI_SERVICE = 9 + EAI_SOCKTYPE = 10 + EAI_SYSTEM = 11 + EAI_OVERFLOW = 14 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(x0 : Addrinfo*) : Void fun gai_strerror(x0 : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(x0 : Char*, x1 : Char*, x2 : Addrinfo*, x3 : Addrinfo**) : Int + fun getnameinfo(x0 : Sockaddr*, x1 : SocklenT, x2 : Char*, x3 : SocklenT, x4 : Char*, x5 : SocklenT, x6 : Int) : Int end diff --git a/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr b/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr index ca311e2938c5..3ca1dc663f4f 100644 --- a/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr +++ b/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr @@ -35,6 +35,14 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_len : SaFamilyT + ss_family : SaFamilyT + __ss_pad1 : StaticArray(Char, 6) + __ss_align : LongLong + __ss_pad2 : StaticArray(Char, 112) + end + struct Linger l_onoff : Int l_linger : Int diff --git a/src/lib_c/x86_64-portbld-freebsd/c/netdb.cr b/src/lib_c/x86_64-portbld-freebsd/c/netdb.cr index 8be344702749..eee1390fbebe 100644 --- a/src/lib_c/x86_64-portbld-freebsd/c/netdb.cr +++ b/src/lib_c/x86_64-portbld-freebsd/c/netdb.cr @@ -3,6 +3,24 @@ require "./sys/socket" require "./stdint" lib LibC + AI_PASSIVE = 0x00000001 + AI_CANONNAME = 0x00000002 + AI_NUMERICHOST = 0x00000004 + AI_NUMERICSERV = 0x00000008 + AI_V4MAPPED = 0x00000800 + AI_ALL = 0x00000100 + AI_ADDRCONFIG = 0x00000400 + EAI_AGAIN = 2 + EAI_BADFLAGS = 3 + EAI_FAIL = 4 + EAI_FAMILY = 5 + EAI_MEMORY = 6 + EAI_NONAME = 8 + EAI_SERVICE = 9 + EAI_SOCKTYPE = 10 + EAI_SYSTEM = 11 + EAI_OVERFLOW = 14 + struct Addrinfo ai_flags : Int ai_family : Int @@ -14,7 +32,8 @@ lib LibC ai_next : Addrinfo* end + fun freeaddrinfo(x0 : Addrinfo*) : Void fun gai_strerror(x0 : Int) : Char* - fun getaddrinfo(hostname : Char*, servname : Char*, hints : Addrinfo*, res : Addrinfo**) : Int - fun freeaddrinfo(ai : Addrinfo*) + fun getaddrinfo(x0 : Char*, x1 : Char*, x2 : Addrinfo*, x3 : Addrinfo**) : Int + fun getnameinfo(x0 : Void*, x1 : SocklenT, x2 : Char*, x3 : SizeT, x4 : Char*, x5 : SizeT, x6 : Int) : Int end diff --git a/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr b/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr index 92f1ffd3ea36..22b4b6a96ba2 100644 --- a/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr +++ b/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr @@ -36,6 +36,14 @@ lib LibC sa_data : StaticArray(Char, 14) end + struct SockaddrStorage + ss_len : Char + ss_family : SaFamilyT + __ss_pad1 : StaticArray(Char, 6) + __ss_align : Long + __ss_pad2 : StaticArray(Char, 112) + end + struct Linger l_onoff : Int l_linger : Int diff --git a/src/socket.cr b/src/socket.cr index 706f00e1c993..a8287b88ed21 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -31,110 +31,310 @@ class Socket < IO::FileDescriptor INET6 = LibC::AF_INET6 end - struct IPAddress - getter family : Family - getter address : String - getter port : UInt16 - - def initialize(@family : Family, @address : String, port : Int) - if family != Family::INET && family != Family::INET6 - raise ArgumentError.new("Unsupported address family") - end + # :nodoc: + SOMAXCONN = 128 + + property family : Family + property type : Type + property protocol : Protocol + + # Creates a TCP socket. Consider using `TCPSocket` or `TCPServer` unless you + # need full control over the socket. + def self.tcp(family : Family, blocking = false) + new(family, Type::STREAM, Protocol::TCP, blocking) + end + + # Creates an UDP socket. Consider using `UDPSocket` unless you need full + # control over the socket. + def self.udp(family : Family, blocking = false) + new(family, Type::DGRAM, Protocol::UDP, blocking) + end - @port = port.to_u16 + # Creates an UNIX socket. Consider using `UNIXSocket` or `UNIXServer` unless + # you need full control over the socket. + def self.unix(type : Type = Type::Stream, blocking = false) + new(Family::UNIX, type, blocking: blocking) + end + + def initialize(@family, @type, @protocol = Protocol::IP, blocking = false) + fd = LibC.socket(family, type, protocol) + raise Errno.new("failed to create socket:") if fd == -1 + init_close_on_exec(fd) + super(fd, blocking) + self.sync = true + end + + protected def initialize(fd : Int32, @family, @type, @protocol = Protocol::IP) + init_close_on_exec(fd) + super fd, blocking: false + self.sync = true + end + + # Force opened sockets to be closed on `exec(2)`. Only for platforms that don't + # support `SOCK_CLOEXEC` (e.g., Darwin). + protected def init_close_on_exec(fd : Int32) + {% unless LibC.has_constant?(:SOCK_CLOEXEC) %} + LibC.fcntl(fd, LibC::F_SETFD, LibC::FD_CLOEXEC) + {% end %} + end + + # Connects the socket to a remote host:port. + # + # ``` + # sock = Socket.tcp(Socket::Family::INET) + # sock.connect "crystal-lang.org", 80 + # ``` + def connect(host : String, port : Int, connect_timeout = nil) + Addrinfo.resolve(host, port, @family, @type, @protocol) do |addrinfo| + connect(addrinfo, timeout: connect_timeout) { |error| error } end + end - def initialize(sockaddr : LibC::SockaddrIn6, addrlen : LibC::SocklenT) - case addrlen - when LibC::SocklenT.new(sizeof(LibC::SockaddrIn)) - sockaddrin = pointerof(sockaddr).as(LibC::SockaddrIn*).value - addr = sockaddrin.sin_addr - @family = Family::INET - @address = inet_ntop(family.value, pointerof(addr).as(Void*), addrlen) - when LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) - addr6 = sockaddr.sin6_addr - @family = Family::INET6 - @address = inet_ntop(family.value, pointerof(addr6).as(Void*), addrlen) + # Connects the socket to a remote address. Raises if the connection failed. + # + # ``` + # sock = Socket.unix + # sock.connect UNIXAddress.new("/tmp/service.sock") + # ``` + def connect(addr, timeout = nil) : Nil + connect(addr, timeout) { |error| raise error } + end + + # Tries to connect to a remote address. Yields an `IO::Timeout` or an + # `Errno` error if the connection failed. + def connect(addr, timeout = nil) + loop do + if LibC.connect(fd, addr, addr.size) == 0 + return + end + case Errno.value + when Errno::EISCONN + return + when Errno::EINPROGRESS, Errno::EALREADY + wait_writable(msg: "connect timed out", timeout: timeout) do |error| + return yield error + end else - raise ArgumentError.new("Unsupported address family") + return yield Errno.new("connect") end - @port = LibC.htons(sockaddr.sin6_port).to_u16 end + end - def sockaddr - sockaddrin6 = LibC::SockaddrIn6.new - sockaddrin6.sin6_family = LibC::SaFamilyT.new(family.value) - - case family - when Family::INET - sockaddrin = pointerof(sockaddrin6).as(LibC::SockaddrIn*).value - addr = sockaddrin.sin_addr - LibC.inet_pton(family.value, address, pointerof(addr).as(Void*)) - sockaddrin.sin_addr = addr - sockaddrin6 = pointerof(sockaddrin).as(LibC::SockaddrIn6*).value - when Family::INET6 - addr6 = sockaddrin6.sin6_addr - LibC.inet_pton(family.value, address, pointerof(addr6).as(Void*)) - sockaddrin6.sin6_addr = addr6 - end - - sockaddrin6.sin6_port = LibC.ntohs(port).to_i16 - sockaddrin6 + # Binds the socket to a local address. + # + # ``` + # sock = Socket.tcp(Socket::Family::INET) + # sock.bind "localhost", 1234 + # ``` + def bind(host : String, port : Int) + Addrinfo.resolve(host, port, @family, @type, @protocol) do |addrinfo| + bind(addrinfo) { |errno| errno } end + end - def addrlen - case family - when Family::INET then LibC::SocklenT.new(sizeof(LibC::SockaddrIn)) - when Family::INET6 then LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) - else LibC::SocklenT.new(0) - end + # Binds the socket on *port* to all local interfaces. + # + # ``` + # sock = Socket.tcp(Socket::Family::INET6) + # sock.bind "localhost", 1234 + # ``` + def bind(port : Int) + Addrinfo.resolve("::", port, @family, @type, @protocol) do |addrinfo| + bind(addrinfo) { |errno| errno } end + end + + # Binds the socket to a local address. + # + # ``` + # sock = Socket.udp(Socket::Family::INET) + # sock.bind Socket::IPAddress.new("192.168.1.25", 80) + # ``` + def bind(addr) + bind(addr) { |errno| raise errno } + end - def to_s(io) - io << address << ":" << port + # Tries to bind the socket to a local address. Yields an `Errno` if the + # binding failed. + def bind(addr) + unless LibC.bind(fd, addr, addr.size) == 0 + yield Errno.new("bind") end + end - private def inet_ntop(af : Int, src : Void*, len : LibC::SocklenT) - dest = GC.malloc_atomic(addrlen.to_u32).as(UInt8*) - if LibC.inet_ntop(af, src, dest, len).null? - raise Errno.new("Failed to convert IP address") - end - String.new(dest) + # Tells the previously bound socket to listen for incoming connections. + def listen(backlog = SOMAXCONN) + listen(backlog) { |errno| raise errno } + end + + # Tries to listen for connections on the previously bound socket. Yields an + # `Errno` on failure. + def listen(backlog = SOMAXCONN) + unless LibC.listen(fd, backlog) == 0 + yield Errno.new("listen") end end - struct UNIXAddress - getter path : String + # Accepts an incoming connection. + # + # Returns the client socket. Raises an `IO::Error` (closed stream) exception + # if the server is closed after invoking this method. + # + # ``` + # require "socket" + # + # server = TCPServer.new(2202) + # socket = server.accept + # socket.puts Time.now + # socket.close + # ``` + def accept + accept? || raise IO::Error.new("closed stream") + end - def initialize(@path : String) + # Accepts an incoming connection. + # + # Returns the client `Socket` or `nil` if the server is closed after invoking + # this method. + # + # ``` + # require "socket" + # + # server = TCPServer.new(2202) + # if socket = server.accept? + # socket.puts Time.now + # socket.close + # end + # ``` + def accept? + if client_fd = accept_impl + sock = Socket.new(client_fd) + sock.sync = sync? + sock end + end - def family - Family::UNIX + protected def accept_impl + loop do + client_fd = LibC.accept(fd, out client_addr, out client_addrlen) + if client_fd == -1 + if closed? + return + elsif Errno.value == Errno::EAGAIN + wait_readable + else + raise Errno.new("accept") + end + else + return client_fd + end end + end - def to_s(io) - path.to_s(io) + # Sends a text message to a previously connected remote address. + # + # ``` + # sock = Socket.udp(Socket::Family::INET) + # sock.connect("example.com", 2000) + # sock.send("text message") + # ``` + def send(message) + send(message.to_slice) + end + + # Sends a binary message to a previously connected remote address. + # + # ``` + # sock = Socket.unix(Socket::Type::DGRAM) + # sock.connect("/tmp/service.sock") + # sock.send(binary_message) + # ``` + def send(message : Bytes) + bytes_sent = LibC.send(fd, message.to_unsafe.as(Void*), message.size, 0) + raise Errno.new("Error sending datagram") if bytes_sent == -1 + bytes_sent + ensure + # see IO::FileDescriptor#unbuffered_write + if (writers = @writers) && !writers.empty? + add_write_event end end - def initialize(fd, blocking = false) - super fd, blocking - self.sync = true + # Sends a text message to the specified remote address. + # + # ``` + # server = Socket::IPAddress.new("10.0.3.1", 2022) + # sock = Socket.udp(Socket::Family::INET) + # sock.connect("example.com", 2000) + # sock.send("text query", to: server) + # ``` + def send(message, to addr : Address) + send(message.to_slice, to: addr) end - protected def create_socket(family, stype, protocol = 0) - sock = LibC.socket(family, stype, protocol) - raise Errno.new("Error opening socket") if sock <= 0 - init_close_on_exec sock - sock + # Sends a binary message to the specified remote address. + # + # ``` + # server = Socket::IPAddress.new("10.0.3.1", 53) + # sock = Socket.udp(Socket::Family::INET) + # sock.connect("example.com", 2000) + # sock.send(dns_query, to: server) + # ``` + def send(message : Bytes, to addr : Address) + bytes_sent = LibC.sendto(fd, message.to_unsafe.as(Void*), message.size, 0, addr, addr.size) + raise Errno.new("Error sending datagram to #{addr}") if bytes_sent == -1 + bytes_sent end - # only used when SOCK_CLOEXEC doesn't exist on the current platform - protected def init_close_on_exec(fd : Int32) - {% unless LibC.constants.includes?("SOCK_CLOEXEC".id) %} - LibC.fcntl(fd, LibC::F_SETFD, LibC::FD_CLOEXEC) - {% end %} + # Receives a text message from the previously bound address. + # + # ``` + # server = Socket.udp(Socket::Family::INET) + # server.bind("localhost", 1234) + # + # message, client_addr = server.receive + # ``` + def receive(max_message_size = 512) : {String, Address} + bytes = Bytes.new(max_message_size) + bytes_read, sockaddr, addrlen = recvfrom(bytes) + {String.new(bytes.to_unsafe, bytes_read), Address.from(sockaddr, addrlen)} + end + + # Receives a binary message from the previously bound address. + # + # ``` + # server = Socket.udp(Socket::Family::INET) + # server.bind("localhost", 1234) + # + # message = Bytes.new(32) + # bytes_read, client_addr = server.receive(message) + # ``` + def receive(message : Bytes) : {Int32, Address} + bytes_read, sockaddr, addrlen = recvfrom(message) + {bytes_read, Address.from(sockaddr, addrlen)} + end + + protected def recvfrom(message) + sockaddr = Pointer(LibC::SockaddrStorage).malloc.as(LibC::Sockaddr*) + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) + + loop do + bytes_read = LibC.recvfrom(fd, message.to_unsafe.as(Void*), message.size, 0, sockaddr, pointerof(addrlen)) + if bytes_read == -1 + if Errno.value == Errno::EAGAIN + wait_readable + else + raise Errno.new("Error receiving datagram") + end + else + return {bytes_read.to_i, sockaddr, addrlen} + end + end + ensure + # see IO::FileDescriptor#unbuffered_read + if (readers = @readers) && !readers.empty? + add_read_event + end end # Calls shutdown(2) with SHUT_RD @@ -254,27 +454,6 @@ class Socket < IO::FileDescriptor optval end - private def nonblocking_connect(host, port, addrinfo, timeout = nil) - loop do - ret = - {% if flag?(:freebsd) || flag?(:openbsd) %} - LibC.connect(@fd, addrinfo.ai_addr.as(LibC::Sockaddr*), addrinfo.ai_addrlen) - {% else %} - LibC.connect(@fd, addrinfo.ai_addr, addrinfo.ai_addrlen) - {% end %} - return nil if ret == 0 # success - - case Errno.value - when Errno::EISCONN - return nil # success - when Errno::EINPROGRESS, Errno::EALREADY - wait_writable(msg: "connect timed out", timeout: timeout) { |err| return err } - else - return Errno.new("Error connecting to '#{host}:#{port}'") - end - end - end - # Returns true if the string represents a valid IPv4 or IPv6 address. def self.ip?(string : String) addr = LibC::In6Addr.new diff --git a/src/socket/address.cr b/src/socket/address.cr new file mode 100644 index 000000000000..70f9085304e3 --- /dev/null +++ b/src/socket/address.cr @@ -0,0 +1,188 @@ +class Socket + abstract struct Address + getter family : Family + getter size : Int32 + + def self.from(sockaddr : LibC::Sockaddr*, addrlen) : Address + case family = Family.new(sockaddr.value.sa_family) + when Family::INET6 + IPAddress.new(sockaddr.as(LibC::SockaddrIn6*), addrlen.to_i) + when Family::INET + IPAddress.new(sockaddr.as(LibC::SockaddrIn*), addrlen.to_i) + when Family::UNIX + UNIXAddress.new(sockaddr.as(LibC::SockaddrUn*), addrlen.to_i) + else + raise "unsupported family type: #{family} (#{family.value})" + end + end + + def initialize(@family : Family, @size : Int32) + end + + abstract def to_unsafe : LibC::Sockaddr* + + def ==(other) + false + end + end + + struct IPAddress < Address + getter port : Int32 + + @address : String? + @addr6 : LibC::In6Addr? + @addr4 : LibC::InAddr? + + def initialize(@address : String, @port : Int32) + if @addr6 = ip6?(address) + @family = Family::INET6 + @size = sizeof(LibC::SockaddrIn6) + elsif @addr4 = ip4?(address) + @family = Family::INET + @size = sizeof(LibC::SockaddrIn) + else + raise Error.new("Invalid IP address: #{address}") + end + end + + def self.from(sockaddr : LibC::Sockaddr*, addrlen) : IPAddress + case family = Family.new(sockaddr.value.sa_family) + when Family::INET6 + new(sockaddr.as(LibC::SockaddrIn6*), addrlen.to_i) + when Family::INET + new(sockaddr.as(LibC::SockaddrIn*), addrlen.to_i) + else + raise "unsupported family type: #{family} (#{family.value})" + end + end + + protected def initialize(sockaddr : LibC::SockaddrIn6*, @size) + @family = Family::INET6 + @addr6 = sockaddr.value.sin6_addr + @port = LibC.ntohs(sockaddr.value.sin6_port).to_i + end + + protected def initialize(sockaddr : LibC::SockaddrIn*, @size) + @family = Family::INET + @addr4 = sockaddr.value.sin_addr + @port = LibC.ntohs(sockaddr.value.sin_port).to_i + end + + private def ip6?(address) + addr = uninitialized LibC::In6Addr + addr if LibC.inet_pton(LibC::AF_INET6, address, pointerof(addr)) == 1 + end + + private def ip4?(address) + addr = uninitialized LibC::InAddr + addr if LibC.inet_pton(LibC::AF_INET, address, pointerof(addr)) == 1 + end + + def address + @address ||= begin + case family + when Family::INET6 then chars = address(@addr6) + when Family::INET then chars = address(@addr4) + else raise "unsupported IP address family: #{family}" + end + raise Errno.new("Failed to convert IP address") unless chars + String.new(chars) + end + end + + private def address(addr : LibC::In6Addr) + chars = GC.malloc_atomic(46).as(UInt8*) + chars if LibC.inet_ntop(family, pointerof(addr).as(Void*), chars, 46) + end + + private def address(addr : LibC::InAddr) + chars = GC.malloc_atomic(16).as(UInt8*) + chars if LibC.inet_ntop(family, pointerof(addr).as(Void*), chars, 16) + end + + private def address(addr) : Nil + # shouldn't happen + end + + def ==(other : IPAddress) + family == other.family && + port == other.port && + address == other.address + end + + def to_s(io) + if family == Family::INET6 + io << '[' << address << ']' << ':' << port + else + io << address << ':' << port + end + end + + def to_unsafe : LibC::Sockaddr* + case family + when Family::INET6 + to_sockaddr_in6 + when Family::INET + to_sockaddr_in + else + raise "unsupported IP address family: #{family}" + end + end + + private def to_sockaddr_in6 + sockaddr = Pointer(LibC::SockaddrIn6).malloc + sockaddr.value.sin6_family = family + sockaddr.value.sin6_port = LibC.htons(port) + sockaddr.value.sin6_addr = @addr6.not_nil! + sockaddr.as(LibC::Sockaddr*) + end + + private def to_sockaddr_in + sockaddr = Pointer(LibC::SockaddrIn).malloc + sockaddr.value.sin_family = family + sockaddr.value.sin_port = LibC.htons(port) + sockaddr.value.sin_addr = @addr4.not_nil! + sockaddr.as(LibC::Sockaddr*) + end + end + + struct UNIXAddress < Address + getter path : String + + # :nodoc: + MAX_PATH_SIZE = LibC::SockaddrUn.new.sun_path.size - 1 + + def initialize(@path : String) + if @path.bytesize + 1 > MAX_PATH_SIZE + raise ArgumentError.new("Path size exceeds the maximum size of #{MAX_PATH_SIZE} bytes") + end + @family = Family::UNIX + @size = sizeof(LibC::SockaddrUn) + end + + def self.from(sockaddr : LibC::Sockaddr*, addrlen) : UNIXAddress + new(sockaddr.as(LibC::SockaddrUn*), addrlen.to_i) + end + + protected def initialize(sockaddr : LibC::SockaddrUn*, size) + @family = Family::UNIX + @path = String.new(sockaddr.value.sun_path.to_unsafe) + @size = size || sizeof(LibC::SockaddrUn) + end + + def ==(other : UNIXAddress) + path == other.path + end + + def to_s(io) + io << path + end + + def to_unsafe : LibC::Sockaddr* + sockaddr = Pointer(LibC::SockaddrUn).malloc + sockaddr.value.sun_family = family + sockaddr.value.sun_path.to_unsafe.copy_from(@path.to_unsafe, @path.bytesize + 1) + sockaddr.as(LibC::Sockaddr*) + end + end +end diff --git a/src/socket/addrinfo.cr b/src/socket/addrinfo.cr new file mode 100644 index 000000000000..2560267bdbc3 --- /dev/null +++ b/src/socket/addrinfo.cr @@ -0,0 +1,91 @@ +class Socket + struct Addrinfo + getter family : Family + getter type : Type + getter protocol : Protocol + getter size : Int32 + + @addr : LibC::Sockaddr* + @next : LibC::Addrinfo* + + def self.resolve(host, service, family : Family, type : Type, protocol : Protocol = Protocol::IP, timeout = nil) + hints = LibC::Addrinfo.new + hints.ai_family = (family || Family::UNSPEC).to_i32 + hints.ai_socktype = type + hints.ai_protocol = protocol + hints.ai_flags = 0 + + if service.is_a?(Int) + hints.ai_flags |= LibC::AI_NUMERICSERV + end + + case ret = LibC.getaddrinfo(host, service.to_s, pointerof(hints), out ptr) + when 0 + # success + when LibC::EAI_NONAME + raise Socket::Error.new("No address found for #{host}:#{service} over #{protocol}") + else + raise Socket::Error.new("getaddrinfo: #{String.new(LibC.gai_strerror(ret))}") + end + + begin + addrinfo = new(ptr) + error = nil + + loop do + error = yield addrinfo.not_nil! + return unless error + + unless addrinfo = addrinfo.try(&.next?) + if error.is_a?(Errno) && error.errno == Errno::ECONNREFUSED + raise Errno.new("Error connecting to '#{host}:#{service}': Connection refused", error.errno) + else + raise error if error + end + end + end + ensure + LibC.freeaddrinfo(ptr) + end + end + + def self.tcp(host, service, family = Family::UNSPEC, timeout = nil) + resolve(host, service, family, Type::STREAM, Protocol::TCP) { |addrinfo| yield addrinfo } + end + + def self.udp(host, service, family = Family::UNSPEC, timeout = nil) + resolve(host, service, family, Type::DGRAM, Protocol::UDP) { |addrinfo| yield addrinfo } + end + + protected def initialize(addrinfo : LibC::Addrinfo*) + @family = Family.from_value(addrinfo.value.ai_family) + @type = Type.from_value(addrinfo.value.ai_socktype) + @protocol = Protocol.from_value(addrinfo.value.ai_protocol) + @size = addrinfo.value.ai_addrlen.to_i + + @addr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) + @next = addrinfo.value.ai_next + + case @family + when Family::INET6 + addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(@addr.as(LibC::SockaddrIn6*), 1) + when Family::INET + addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(@addr.as(LibC::SockaddrIn*), 1) + end + end + + def ip_address + @ip_address = IPAddress.from(@addr, @addrlen) + end + + def to_unsafe + @addr + end + + protected def next? + if addrinfo = @next + Addrinfo.new(addrinfo) + end + end + end +end diff --git a/src/socket/ip_socket.cr b/src/socket/ip_socket.cr index c274bbf1a440..5996dd000955 100644 --- a/src/socket/ip_socket.cr +++ b/src/socket/ip_socket.cr @@ -1,139 +1,25 @@ class IPSocket < Socket # Returns the `IPAddress` for the local end of the IP socket. def local_address - sockaddr = uninitialized LibC::SockaddrIn6 + sockaddr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) - if LibC.getsockname(fd, pointerof(sockaddr).as(LibC::Sockaddr*), pointerof(addrlen)) != 0 + if LibC.getsockname(fd, sockaddr, pointerof(addrlen)) != 0 raise Errno.new("getsockname") end - IPAddress.new(sockaddr, addrlen) + IPAddress.from(sockaddr, addrlen) end # Returns the `IPAddress` for the remote end of the IP socket. def remote_address - sockaddr = uninitialized LibC::SockaddrIn6 - addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) + sockaddr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) - if LibC.getpeername(fd, pointerof(sockaddr).as(LibC::Sockaddr*), pointerof(addrlen)) != 0 + if LibC.getpeername(fd, sockaddr, pointerof(addrlen)) != 0 raise Errno.new("getpeername") end - IPAddress.new(sockaddr, addrlen) - end - - class DnsRequestCbArg - getter value : Int32 | Pointer(LibC::Addrinfo) | Nil - @fiber : Fiber - - def initialize - @fiber = Fiber.current - end - - def value=(val) - @value = val - @fiber.resume - end - end - - # Yields LibC::Addrinfo to the block while the block returns false and there are more LibC::Addrinfo results. - # - # The block must return true if it succeeded using that addressinfo - # (to connect or bind, for example), and false otherwise. If it returns false and - # the LibC::Addrinfo has a next LibC::Addrinfo, it is yielded to the block, and so on. - private def getaddrinfo(host, port, family, socktype, protocol = Protocol::IP, timeout = nil) - # Using getaddrinfo from libevent doesn't work well, - # see https://github.com/crystal-lang/crystal/issues/2660 - # - # For now it's better to have this working well but maybe a bit slow than - # having it working fast but something working bad or not seeing some networks. - IPSocket.getaddrinfo_c_call(host, port, family, socktype, protocol, timeout) { |ai| yield ai } - end - - # :nodoc: - def self.getaddrinfo_c_call(host, port, family, socktype, protocol = Protocol::IP, timeout = nil) - hints = LibC::Addrinfo.new - hints.ai_family = (family || Family::UNSPEC).to_i32 - hints.ai_socktype = socktype - hints.ai_protocol = protocol - hints.ai_flags = 0 - - ret = LibC.getaddrinfo(host, port.to_s, pointerof(hints), out addrinfo) - raise Socket::Error.new("getaddrinfo: #{String.new(LibC.gai_strerror(ret))}") if ret != 0 - - begin - current_addrinfo = addrinfo - while current_addrinfo - success = yield current_addrinfo.value - break if success - current_addrinfo = current_addrinfo.value.ai_next - end - ensure - LibC.freeaddrinfo(addrinfo) - end - end - - # :nodoc: - def self.getaddrinfo_libevent(host, port, family, socktype, protocol = Protocol::IP, timeout = nil) - hints = LibC::Addrinfo.new - hints.ai_family = (family || Family::UNSPEC).to_i32 - hints.ai_socktype = socktype - hints.ai_protocol = protocol - hints.ai_flags = 0 - - dns_req = DnsRequestCbArg.new - - # may fire immediately or on the next event loop - req = Scheduler.create_dns_request(host, port.to_s, pointerof(hints), dns_req) do |err, addr, data| - dreq = data.as(DnsRequestCbArg) - - if err == 0 - dreq.value = addr - else - dreq.value = err - end - end - - if timeout && req - spawn do - sleep timeout.not_nil! - req.not_nil!.cancel unless dns_req.value - end - end - - success = false - - value = dns_req.value - # BUG: not thread safe. change when threads are implemented - unless value - Scheduler.reschedule - value = dns_req.value - end - - if value.is_a?(LibC::Addrinfo*) - begin - cur_addr = value - while cur_addr - success = yield cur_addr.value - - break if success - cur_addr = cur_addr.value.ai_next - end - ensure - LibEvent2.evutil_freeaddrinfo value - end - elsif value.is_a?(Int) - if value == LibEvent2::EVUTIL_EAI_CANCEL - raise IO::Timeout.new("Failed to resolve #{host} in #{timeout} seconds") - end - error_message = String.new(LibC.gai_strerror(value)) - raise Socket::Error.new("getaddrinfo: #{error_message}") - else - raise "unknown type #{value.inspect}" - end - - # shouldn't raise - raise Socket::Error.new("getaddrinfo: unspecified error") unless success + IPAddress.from(sockaddr, addrlen) end end diff --git a/src/socket/server.cr b/src/socket/server.cr new file mode 100644 index 000000000000..4154187f722e --- /dev/null +++ b/src/socket/server.cr @@ -0,0 +1,51 @@ +class Socket + module Server + # Accepts an incoming connection and yields the client socket to the block. + # Eventually closes the connection when the block returns. + # + # Returns the value of the block. If the server is closed after invoking this + # method, an `IO::Error` (closed stream) exception will be raised. + # + # ``` + # require "socket" + # + # server = TCPServer.new(2202) + # server.accept do |socket| + # socket.puts Time.now + # end + # ``` + def accept + sock = accept + begin + yield sock + ensure + sock.close + end + end + + # Accepts an incoming connection and yields the client socket to the block. + # Eventualy closes the connection when the block returns. + # + # Returns the value of the block or `nil` if the server is closed after + # invoking this method. + # + # ``` + # require "socket" + # + # server = UNIXServer.new("/tmp/service.sock") + # server.accept? do |socket| + # socket.puts Time.now + # end + # ``` + def accept? + sock = accept? + return unless sock + + begin + yield sock + ensure + sock.close + end + end + end +end diff --git a/src/socket/tcp_server.cr b/src/socket/tcp_server.cr index 6e77c06daf78..b8c81631b3b8 100644 --- a/src/socket/tcp_server.cr +++ b/src/socket/tcp_server.cr @@ -15,32 +15,28 @@ require "./tcp_socket" # end # ``` class TCPServer < TCPSocket - def initialize(host, port, backlog = 128) - getaddrinfo(host, port, nil, Type::STREAM, Protocol::TCP) do |addrinfo| - super create_socket(addrinfo.ai_family, addrinfo.ai_socktype, addrinfo.ai_protocol) + include Socket::Server + + def initialize(host : String, port : Int, backlog = SOMAXCONN, dns_timeout = nil) + Addrinfo.tcp(host, port, timeout: dns_timeout) do |addrinfo| + super(addrinfo.family, addrinfo.type, addrinfo.protocol) self.reuse_address = true - if LibC.bind(@fd, addrinfo.ai_addr.as(LibC::Sockaddr*), addrinfo.ai_addrlen) != 0 - errno = Errno.new("Error binding TCP server at #{host}:#{port}") + if errno = bind(addrinfo) { |errno| errno } close - next false if addrinfo.ai_next - raise errno + next errno end - if LibC.listen(@fd, backlog) != 0 - errno = Errno.new("Error listening TCP server at #{host}:#{port}") + if errno = listen(backlog) { |errno| errno } close - next false if addrinfo.ai_next - raise errno + next errno end - - true end end # Creates a new TCP server, listening on all local interfaces (`::`). - def self.new(port : Int, backlog = 128) + def self.new(port : Int, backlog = SOMAXCONN) new("::", port, backlog) end @@ -48,7 +44,7 @@ class TCPServer < TCPSocket # server socket when the block returns. # # Returns the value of the block. - def self.open(host, port, backlog = 128) + def self.open(host, port, backlog = SOMAXCONN) server = new(host, port, backlog) begin yield server @@ -61,7 +57,7 @@ class TCPServer < TCPSocket # block. Eventually closes the server socket when the block returns. # # Returns the value of the block. - def self.open(port : Int, backlog = 128) + def self.open(port : Int, backlog = SOMAXCONN) server = new(port, backlog) begin yield server @@ -70,80 +66,15 @@ class TCPServer < TCPSocket end end - # Accepts an incoming connection and yields the client socket to the block. - # Eventually closes the connection when the block returns. - # - # Returns the value of the block. If the server is closed after invoking this - # method, an `IO::Error` (closed stream) exception will be raised. - # - # ``` - # require "socket" - # - # server = TCPServer.new(2202) - # server.accept do |socket| - # socket.puts Time.now - # end - # ``` - def accept - sock = accept - begin - yield sock - ensure - sock.close - end - end - - # Accepts an incoming connection and yields the client socket to the block. - # Eventualy closes the connection when the block returns. - # - # Returns the value of the block or `nil` if the server is closed after - # invoking this method. - # - # ``` - # require "socket" - # - # server = TCPServer.new(2202) - # server.accept? do |socket| - # socket.puts Time.now - # end - # ``` - def accept? - sock = accept? - return unless sock - - begin - yield sock - ensure - sock.close - end - end - - # Accepts an incoming connection. - # - # Returns the client socket. Raises an `IO::Error` (closed stream) exception - # if the server is closed after invoking this method. - # - # ``` - # require "socket" - # - # server = TCPServer.new(2202) - # socket = server.accept - # socket.puts Time.now - # socket.close - # ``` - def accept : TCPSocket - accept? || raise IO::Error.new("closed stream") - end - # Accepts an incoming connection. # - # Returns the client socket or `nil` if the server is closed after invoking + # Returns the client `TCPSocket` or `nil` if the server is closed after invoking # this method. # # ``` # require "socket" # - # server = TCPServer.new(2202) + # server = TCPServer.new(2022) # loop do # if socket = server.accept? # # handle the client in a fiber @@ -154,24 +85,11 @@ class TCPServer < TCPSocket # end # end # ``` - def accept? : TCPSocket? - loop do - client_addr = uninitialized LibC::SockaddrIn6 - client_addr_len = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) - client_fd = LibC.accept(fd, pointerof(client_addr).as(LibC::Sockaddr*), pointerof(client_addr_len)) - if client_fd == -1 - return nil if closed? - - if Errno.value == Errno::EAGAIN - wait_readable - else - raise Errno.new "Error accepting socket" - end - else - sock = TCPSocket.new(client_fd) - sock.sync = sync? - return sock - end + def accept? + if client_fd = accept_impl + sock = TCPSocket.new(client_fd, family, type, protocol) + sock.sync = sync? + sock end end end diff --git a/src/socket/tcp_socket.cr b/src/socket/tcp_socket.cr index b0d5705caacf..88749a6abe13 100644 --- a/src/socket/tcp_socket.cr +++ b/src/socket/tcp_socket.cr @@ -20,21 +20,21 @@ class TCPSocket < IPSocket # # Note that `dns_timeout` is currently ignored. def initialize(host, port, dns_timeout = nil, connect_timeout = nil) - getaddrinfo(host, port, nil, Type::STREAM, Protocol::TCP, timeout: dns_timeout) do |addrinfo| - super create_socket(addrinfo.ai_family, addrinfo.ai_socktype, addrinfo.ai_protocol) - - if err = nonblocking_connect(host, port, addrinfo, timeout: connect_timeout) + Addrinfo.tcp(host, port, timeout: dns_timeout) do |addrinfo| + super(addrinfo.family, addrinfo.type, addrinfo.protocol) + connect(addrinfo, timeout: connect_timeout) do |error| close - next false if addrinfo.ai_next - raise err + error end - - true end end - protected def initialize(fd : Int32) - super fd + protected def initialize(family : Family, type : Type, protocol : Protocol) + super family, type, protocol + end + + protected def initialize(fd : Int32, family : Family, type : Type, protocol : Protocol) + super fd, family, type, protocol end # Opens a TCP socket to a remote TCP server, yields it to the block, then diff --git a/src/socket/udp_socket.cr b/src/socket/udp_socket.cr index 00324c194b46..a38916c8df82 100644 --- a/src/socket/udp_socket.cr +++ b/src/socket/udp_socket.cr @@ -1,6 +1,6 @@ require "./ip_socket" -# A User Datagram Protocol socket. +# A User Datagram Protocol (UDP) socket. # # UDP runs on top of the Internet Protocol (IP) and was developed for applications that do # not require reliability, acknowledgement, or flow control features at the transport layer. @@ -27,8 +27,11 @@ require "./ip_socket" # client = UDPSocket.new # client.connect "localhost", 1234 # -# client.puts "message" # send message to server -# server.gets # => "message\n" +# # Send a text message to server +# client.send "message" +# +# # Receive text message from client +# message, client_addr = server.receive # # # Close client and server # client.close @@ -48,113 +51,35 @@ require "./ip_socket" # end # ``` class UDPSocket < IPSocket - def initialize(@family : Family = Family::INET) - super create_socket(family.value, Type::DGRAM, Protocol::UDP) + def initialize(family : Family = Family::INET) + super(family, Type::DGRAM, Protocol::UDP) end - # Binds the UDP socket to a local address. + # Receives a text message from the previously bound address. # # ``` # server = UDPSocket.new - # server.bind "localhost", 1234 - # ``` - def bind(host, port, dns_timeout = nil) - getaddrinfo(host, port, @family, Type::DGRAM, Protocol::UDP, timeout: dns_timeout) do |addrinfo| - self.reuse_address = true - - ret = - {% if flag?(:freebsd) || flag?(:openbsd) %} - LibC.bind(fd, addrinfo.ai_addr.as(LibC::Sockaddr*), addrinfo.ai_addrlen) - {% else %} - LibC.bind(fd, addrinfo.ai_addr, addrinfo.ai_addrlen) - {% end %} - unless ret == 0 - next false if addrinfo.ai_next - raise Errno.new("Error binding UDP socket at #{host}:#{port}") - end - - true - end - end - - # Connects the UDP socket to a remote address to send messages to. + # server.bind("localhost", 1234) # + # message, client_addr = server.receive # ``` - # client = UDPSocket.new - # client.connect("localhost", 1234) - # client.send("a text message") - # ``` - def connect(host, port, dns_timeout = nil, connect_timeout = nil) - getaddrinfo(host, port, @family, Type::DGRAM, Protocol::UDP, timeout: dns_timeout) do |addrinfo| - if err = nonblocking_connect host, port, addrinfo, timeout: connect_timeout - next false if addrinfo.ai_next - raise err - end - - true - end + def receive(max_message_size = 512) : {String, IPAddress} + bytes = Bytes.new(max_message_size) + bytes_read, sockaddr, addrlen = recvfrom(bytes) + {String.new(bytes.to_unsafe, bytes_read), IPAddress.from(sockaddr, addrlen)} end - # Sends a text message to the previously connected remote address. See - # `#connect`. - def send(message : String) - send(message.to_slice) - end - - # Sends a binary message to the previously connected remote address. See - # `#connect`. - def send(message : Bytes) - bytes_sent = LibC.send(fd, (message.to_unsafe.as(Void*)), message.size, 0) - raise Errno.new("Error sending datagram") if bytes_sent == -1 - bytes_sent - ensure - if (writers = @writers) && !writers.empty? - add_write_event - end - end - - # Sends a text message to the specified remote address. - def send(message : String, addr : IPAddress) - send(message.to_slice, addr) - end - - # Sends a binary message to the specified remote address. - def send(message : Bytes, addr : IPAddress) - sockaddr = addr.sockaddr - bytes_sent = LibC.sendto(fd, (message.to_unsafe.as(Void*)), message.size, 0, pointerof(sockaddr).as(LibC::Sockaddr*), addr.addrlen) - raise Errno.new("Error sending datagram to #{addr}") if bytes_sent == -1 - bytes_sent - end - - # Receives a binary message on the previously bound address. + # Receives a binary message from the previously bound address. # # ``` # server = UDPSocket.new # server.bind "localhost", 1234 # # message = Bytes.new(32) - # message_size, client_addr = server.receive(message) + # bytes_read, client_addr = server.receive(message) # ``` def receive(message : Bytes) : {Int32, IPAddress} - loop do - sockaddr = uninitialized LibC::SockaddrIn6 - addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) - bytes_read = LibC.recvfrom(fd, (message.to_unsafe.as(Void*)), message.size, 0, pointerof(sockaddr).as(LibC::Sockaddr*), pointerof(addrlen)) - - if bytes_read == -1 - if Errno.value == Errno::EAGAIN - wait_readable - else - raise Errno.new("Error receiving datagram") - end - else - return {bytes_read.to_i32, IPAddress.new(sockaddr, addrlen)} - end - end - ensure - # see IO::FileDescriptor#unbuffered_read - if (readers = @readers) && !readers.empty? - add_read_event - end + bytes_read, sockaddr, addrlen = recvfrom(message) + {bytes_read, IPAddress.from(sockaddr, addrlen)} end end diff --git a/src/socket/unix_server.cr b/src/socket/unix_server.cr index 4c3cdb14578f..15feb2768181 100644 --- a/src/socket/unix_server.cr +++ b/src/socket/unix_server.cr @@ -13,6 +13,8 @@ require "./unix_socket" # server.puts message # ``` class UNIXServer < UNIXSocket + include Socket::Server + # Creates a named UNIX socket, listening on a filesystem pathname. # # Always deletes any existing filesystam pathname first, in order to cleanup @@ -24,24 +26,16 @@ class UNIXServer < UNIXSocket # UNIXServer.new("/tmp/dgram.sock", Socket::Type::DGRAM) # ``` def initialize(@path : String, type : Type = Type::STREAM, backlog = 128) - addr = LibC::SockaddrUn.new - addr.sun_family = typeof(addr.sun_family).new(Family::UNIX) - - if path.bytesize + 1 > addr.sun_path.size - raise ArgumentError.new("Path size exceeds the maximum size of #{addr.sun_path.size - 1} bytes") - end - addr.sun_path.to_unsafe.copy_from(path.to_unsafe, path.bytesize + 1) + super(Family::UNIX, type) - super create_socket(Family::UNIX, type, 0) - - if LibC.bind(@fd, (pointerof(addr).as(LibC::Sockaddr*)), sizeof(LibC::SockaddrUn)) != 0 + bind(UNIXAddress.new(path)) do |error| close - raise Errno.new("Error binding UNIX server at #{path}") + raise error end - if LibC.listen(@fd, backlog) != 0 + listen(backlog) do |error| close - raise Errno.new("Error listening UNIX server at #{path}") + raise error end end @@ -58,68 +52,19 @@ class UNIXServer < UNIXSocket end end - # Accepts an incoming connection and yields the client socket to the block. - # Eventually closes the connection when the block returns. - # - # Returns the value of the block. If the server is closed after invoking this - # method, an `IO::Error` (closed stream) exception will be raised. - def accept - sock = accept - begin - yield sock - ensure - sock.close - end - end - - # Accepts an incoming connection and yields the client socket to the block. - # Eventualy closes the connection when the block returns. - # - # Returns the value of the block or `nil` if the server is closed after - # invoking this method. - def accept? - sock = accept? - return unless sock - - begin - yield sock - ensure - sock.close - end - end - - # Accepts an incoming connection. - # - # Returns the client socket. Raises an `IO::Error` (closed stream) exception - # if the server is closed after invoking this method. - def accept : UNIXSocket - accept? || raise IO::Error.new("closed stream") - end - # Accepts an incoming connection. # # Returns the client socket or `nil` if the server is closed after invoking # this method. def accept? : UNIXSocket? - loop do - client_fd = LibC.accept(fd, out client_addr, out client_addrlen) - if client_fd == -1 - return nil if closed? - - if Errno.value == Errno::EAGAIN - wait_readable - else - raise Errno.new("Error accepting socket at #{path}") - end - else - sock = UNIXSocket.new(client_fd) - sock.sync = sync? - return sock - end + if client_fd = accept_impl + sock = UNIXSocket.new(client_fd, type) + sock.sync = sync? + sock end end - # Closes the socket and deletes the filesystem pathname. + # Closes the socket, then deletes the filesystem pathname if it exists. def close super ensure diff --git a/src/socket/unix_socket.cr b/src/socket/unix_socket.cr index f5d75da7e77e..777941efca6f 100644 --- a/src/socket/unix_socket.cr +++ b/src/socket/unix_socket.cr @@ -16,25 +16,20 @@ class UNIXSocket < Socket # Connects a named UNIX socket, bound to a filesystem pathname. def initialize(@path : String, type : Type = Type::STREAM) - addr = LibC::SockaddrUn.new - addr.sun_family = LibC::SaFamilyT.new(Family::UNIX) + super(Family::UNIX, type, Protocol::IP) - if path.bytesize + 1 > addr.sun_path.size - raise ArgumentError.new("Path size exceeds the maximum size of #{addr.sun_path.size - 1} bytes") - end - addr.sun_path.to_unsafe.copy_from(path.to_unsafe, path.bytesize + 1) - - super create_socket(Family::UNIX, type, 0) - - if LibC.connect(@fd, (pointerof(addr).as(LibC::Sockaddr*)), sizeof(LibC::SockaddrUn)) != 0 + connect(UNIXAddress.new(path)) do |error| close - raise Errno.new("Error connecting to '#{path}'") + raise error end end - protected def initialize(fd : Int32) - init_close_on_exec fd - super fd + protected def initialize(family : Family, type : Type) + super family, type, Protocol::IP + end + + protected def initialize(fd : Int32, type : Type) + super fd, Family::UNIX, type, Protocol::IP end # Opens an UNIX socket to a filesystem pathname, yields it to the block, then @@ -65,15 +60,18 @@ class UNIXSocket < Socket # left.gets # => "message" # ``` def self.pair(type : Type = Type::STREAM) - fds = StaticArray(Int32, 2).new { 0_i32 } + fds = uninitialized Int32[2] + socktype = type.value - {% if LibC.constants.includes?("SOCK_CLOEXEC".id) %} + {% if LibC.has_constant?(:SOCK_CLOEXEC) %} socktype |= LibC::SOCK_CLOEXEC {% end %} + if LibC.socketpair(Family::UNIX, socktype, 0, fds) != 0 raise Errno.new("socketpair:") end - fds.map { |fd| UNIXSocket.new(fd) } + + {UNIXSocket.new(fds[0], type), UNIXSocket.new(fds[1], type)} end # Creates a pair of unamed UNIX sockets (see `pair`) and yields them to the @@ -97,4 +95,9 @@ class UNIXSocket < Socket def remote_address UNIXAddress.new(path.to_s) end + + def receive + bytes_read, sockaddr, addrlen = recvfrom + {bytes_read, UNIXAddress.from(sockaddr, addrlen)} + end end From afd3b2bf85fa84c3e9f8b9a303289ef7e08a1beb Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Thu, 22 Dec 2016 22:41:29 +0100 Subject: [PATCH 2/3] Sockets refactor: avoid extra allocations --- src/socket.cr | 18 +++++++++++------- src/socket/address.cr | 22 ++++++++++++++-------- src/socket/addrinfo.cr | 10 +++++----- src/socket/ip_socket.cr | 8 +++++--- src/socket/udp_socket.cr | 10 +++++++--- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/socket.cr b/src/socket.cr index a8287b88ed21..14d127450a78 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -34,9 +34,9 @@ class Socket < IO::FileDescriptor # :nodoc: SOMAXCONN = 128 - property family : Family - property type : Type - property protocol : Protocol + getter family : Family + getter type : Type + getter protocol : Protocol # Creates a TCP socket. Consider using `TCPSocket` or `TCPServer` unless you # need full control over the socket. @@ -136,7 +136,7 @@ class Socket < IO::FileDescriptor # # ``` # sock = Socket.tcp(Socket::Family::INET6) - # sock.bind "localhost", 1234 + # sock.bind 1234 # ``` def bind(port : Int) Addrinfo.resolve("::", port, @family, @type, @protocol) do |addrinfo| @@ -295,9 +295,13 @@ class Socket < IO::FileDescriptor # message, client_addr = server.receive # ``` def receive(max_message_size = 512) : {String, Address} - bytes = Bytes.new(max_message_size) - bytes_read, sockaddr, addrlen = recvfrom(bytes) - {String.new(bytes.to_unsafe, bytes_read), Address.from(sockaddr, addrlen)} + address = nil + message = String.new(max_message_size) do |buffer| + bytes_read, sockaddr, addrlen = recvfrom(Slice.new(buffer, max_message_size)) + address = Address.from(sockaddr, addrlen) + {bytes_read, 0} + end + {message, address.not_nil!} end # Receives a binary message from the previously bound address. diff --git a/src/socket/address.cr b/src/socket/address.cr index 70f9085304e3..d88fc86adf06 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -81,23 +81,29 @@ class Socket def address @address ||= begin case family - when Family::INET6 then chars = address(@addr6) - when Family::INET then chars = address(@addr4) + when Family::INET6 then address(@addr6) + when Family::INET then address(@addr4) else raise "unsupported IP address family: #{family}" end - raise Errno.new("Failed to convert IP address") unless chars - String.new(chars) end end private def address(addr : LibC::In6Addr) - chars = GC.malloc_atomic(46).as(UInt8*) - chars if LibC.inet_ntop(family, pointerof(addr).as(Void*), chars, 46) + String.new(46) do |buffer| + unless LibC.inet_ntop(family, pointerof(addr).as(Void*), buffer, 46) + raise Errno.new("Failed to convert IP address") + end + {LibC.strlen(buffer), 0} + end end private def address(addr : LibC::InAddr) - chars = GC.malloc_atomic(16).as(UInt8*) - chars if LibC.inet_ntop(family, pointerof(addr).as(Void*), chars, 16) + String.new(16) do |buffer| + unless LibC.inet_ntop(family, pointerof(addr).as(Void*), buffer, 16) + raise Errno.new("Failed to convert IP address") + end + {LibC.strlen(buffer), 0} + end end private def address(addr) : Nil diff --git a/src/socket/addrinfo.cr b/src/socket/addrinfo.cr index 2560267bdbc3..3cf302ab30b3 100644 --- a/src/socket/addrinfo.cr +++ b/src/socket/addrinfo.cr @@ -5,7 +5,7 @@ class Socket getter protocol : Protocol getter size : Int32 - @addr : LibC::Sockaddr* + @addr : LibC::SockaddrIn6 @next : LibC::Addrinfo* def self.resolve(host, service, family : Family, type : Type, protocol : Protocol = Protocol::IP, timeout = nil) @@ -63,14 +63,14 @@ class Socket @protocol = Protocol.from_value(addrinfo.value.ai_protocol) @size = addrinfo.value.ai_addrlen.to_i - @addr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) + @addr = uninitialized LibC::SockaddrIn6 @next = addrinfo.value.ai_next case @family when Family::INET6 - addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(@addr.as(LibC::SockaddrIn6*), 1) + addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(pointerof(@addr).as(LibC::SockaddrIn6*), 1) when Family::INET - addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(@addr.as(LibC::SockaddrIn*), 1) + addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(pointerof(@addr).as(LibC::SockaddrIn*), 1) end end @@ -79,7 +79,7 @@ class Socket end def to_unsafe - @addr + pointerof(@addr).as(LibC::Sockaddr*) end protected def next? diff --git a/src/socket/ip_socket.cr b/src/socket/ip_socket.cr index 5996dd000955..12b63b556438 100644 --- a/src/socket/ip_socket.cr +++ b/src/socket/ip_socket.cr @@ -1,7 +1,8 @@ class IPSocket < Socket # Returns the `IPAddress` for the local end of the IP socket. def local_address - sockaddr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) + sockaddr6 = uninitialized LibC::SockaddrIn6 + sockaddr = pointerof(sockaddr6).as(LibC::Sockaddr*) addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) if LibC.getsockname(fd, sockaddr, pointerof(addrlen)) != 0 @@ -13,8 +14,9 @@ class IPSocket < Socket # Returns the `IPAddress` for the remote end of the IP socket. def remote_address - sockaddr = Pointer(LibC::SockaddrIn6).malloc.as(LibC::Sockaddr*) - addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) + sockaddr6 = uninitialized LibC::SockaddrIn6 + sockaddr = pointerof(sockaddr6).as(LibC::Sockaddr*) + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) if LibC.getpeername(fd, sockaddr, pointerof(addrlen)) != 0 raise Errno.new("getpeername") diff --git a/src/socket/udp_socket.cr b/src/socket/udp_socket.cr index a38916c8df82..6cab1cf44f32 100644 --- a/src/socket/udp_socket.cr +++ b/src/socket/udp_socket.cr @@ -64,9 +64,13 @@ class UDPSocket < IPSocket # message, client_addr = server.receive # ``` def receive(max_message_size = 512) : {String, IPAddress} - bytes = Bytes.new(max_message_size) - bytes_read, sockaddr, addrlen = recvfrom(bytes) - {String.new(bytes.to_unsafe, bytes_read), IPAddress.from(sockaddr, addrlen)} + address = nil + message = String.new(max_message_size) do |buffer| + bytes_read, sockaddr, addrlen = recvfrom(Slice.new(buffer, max_message_size)) + address = IPAddress.from(sockaddr, addrlen) + {bytes_read, 0} + end + {message, address.not_nil!} end # Receives a binary message from the previously bound address. From 9b37ca383e593f8936eb9978c21ac7519eb22462 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Thu, 22 Dec 2016 23:05:40 +0100 Subject: [PATCH 3/3] sockets: add SO_REUSEPORT option --- spec/std/socket_spec.cr | 15 ++++++++++++--- src/lib_c/aarch64-linux-gnu/c/sys/socket.cr | 1 + src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr | 1 + src/lib_c/arm-linux-gnueabihf/c/sys/socket.cr | 1 + src/lib_c/i686-linux-gnu/c/sys/socket.cr | 1 + src/lib_c/i686-linux-musl/c/sys/socket.cr | 1 + src/lib_c/x86_64-linux-gnu/c/sys/socket.cr | 1 + src/lib_c/x86_64-linux-musl/c/sys/socket.cr | 1 + src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr | 1 + src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr | 1 + src/socket.cr | 8 ++++++++ src/socket/tcp_server.cr | 1 + 12 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 74617d5eca9a..c96947a38220 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -310,10 +310,19 @@ end describe TCPServer do it "fails when port is in use" do + port = free_udp_socket_port + expect_raises Errno, /(already|Address) in use/ do - TCPServer.open("::", 0) do |server| - TCPServer.open("::", server.local_address.port) { } - end + sock = Socket.tcp(Socket::Family::INET6) + sock.bind(Socket::IPAddress.new("::1", port)) + + TCPServer.open("::1", port) {} + end + end + + it "allows to share the same port (SO_REUSEPORT)" do + TCPServer.open("::", 0) do |server| + TCPServer.open("::", server.local_address.port) {} end end end diff --git a/src/lib_c/aarch64-linux-gnu/c/sys/socket.cr b/src/lib_c/aarch64-linux-gnu/c/sys/socket.cr index 22fdf388c5b8..11785c128f52 100644 --- a/src/lib_c/aarch64-linux-gnu/c/sys/socket.cr +++ b/src/lib_c/aarch64-linux-gnu/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr b/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr index a33daa04585c..cb2f1fa8123b 100644 --- a/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr +++ b/src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 0x0080 SO_RCVBUF = 0x1002 SO_REUSEADDR = 0x0004 + SO_REUSEPORT = 0x0200 SO_SNDBUF = 0x1001 PF_INET = LibC::AF_INET PF_INET6 = LibC::AF_INET6 diff --git a/src/lib_c/arm-linux-gnueabihf/c/sys/socket.cr b/src/lib_c/arm-linux-gnueabihf/c/sys/socket.cr index 15db2cee8c23..3a61519d9baa 100644 --- a/src/lib_c/arm-linux-gnueabihf/c/sys/socket.cr +++ b/src/lib_c/arm-linux-gnueabihf/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/i686-linux-gnu/c/sys/socket.cr b/src/lib_c/i686-linux-gnu/c/sys/socket.cr index 5d977a78ba3a..b02ed75210dc 100644 --- a/src/lib_c/i686-linux-gnu/c/sys/socket.cr +++ b/src/lib_c/i686-linux-gnu/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/i686-linux-musl/c/sys/socket.cr b/src/lib_c/i686-linux-musl/c/sys/socket.cr index b43b14d6bf8b..361e5b8040d6 100644 --- a/src/lib_c/i686-linux-musl/c/sys/socket.cr +++ b/src/lib_c/i686-linux-musl/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr b/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr index 4729b1d158bf..6cee17373bca 100644 --- a/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr +++ b/src/lib_c/x86_64-linux-gnu/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/x86_64-linux-musl/c/sys/socket.cr b/src/lib_c/x86_64-linux-musl/c/sys/socket.cr index b43b14d6bf8b..361e5b8040d6 100644 --- a/src/lib_c/x86_64-linux-musl/c/sys/socket.cr +++ b/src/lib_c/x86_64-linux-musl/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 13 SO_RCVBUF = 8 SO_REUSEADDR = 2 + SO_REUSEPORT = 15 SO_SNDBUF = 7 PF_INET = 2 PF_INET6 = 10 diff --git a/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr b/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr index 3ca1dc663f4f..907a0460e68a 100644 --- a/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr +++ b/src/lib_c/x86_64-macosx-darwin/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 0x0080 SO_RCVBUF = 0x1002 SO_REUSEADDR = 0x0004 + SO_REUSEPORT = 0x0200 SO_SNDBUF = 0x1001 PF_INET = LibC::AF_INET PF_INET6 = LibC::AF_INET6 diff --git a/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr b/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr index 22b4b6a96ba2..8a731c8ee82c 100644 --- a/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr +++ b/src/lib_c/x86_64-portbld-freebsd/c/sys/socket.cr @@ -11,6 +11,7 @@ lib LibC SO_LINGER = 0x0080 SO_RCVBUF = 0x1002 SO_REUSEADDR = 0x0004 + SO_REUSEPORT = 0x0200 SO_SNDBUF = 0x1001 PF_INET = LibC::AF_INET PF_INET6 = LibC::AF_INET6 diff --git a/src/socket.cr b/src/socket.cr index 14d127450a78..b02be5c6d574 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -387,6 +387,14 @@ class Socket < IO::FileDescriptor setsockopt_bool LibC::SO_REUSEADDR, val end + def reuse_port? + getsockopt_bool LibC::SO_REUSEPORT + end + + def reuse_port=(val : Bool) + setsockopt_bool LibC::SO_REUSEPORT, val + end + def broadcast? getsockopt_bool LibC::SO_BROADCAST end diff --git a/src/socket/tcp_server.cr b/src/socket/tcp_server.cr index b8c81631b3b8..a3f43222c069 100644 --- a/src/socket/tcp_server.cr +++ b/src/socket/tcp_server.cr @@ -22,6 +22,7 @@ class TCPServer < TCPSocket super(addrinfo.family, addrinfo.type, addrinfo.protocol) self.reuse_address = true + self.reuse_port = true if errno = bind(addrinfo) { |errno| errno } close