Skip to content

Commit

Permalink
Sockets refactor: allow any family/type/protocol association
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
ysbaddaden authored and Ary Borenszweig committed Dec 23, 2016
1 parent 94fc8ab commit 56dec21
Show file tree
Hide file tree
Showing 25 changed files with 960 additions and 581 deletions.
136 changes: 84 additions & 52 deletions spec/std/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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("")

Expand All @@ -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|
Expand All @@ -244,6 +270,7 @@ describe UNIXSocket do

left << "ping"
right.gets(4).should eq("ping")

right << "pong"
left.gets(4).should eq("pong")
end
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/errno.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/lib_c/amd64-unknown-openbsd/c/sys/socket.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/lib_c/i686-linux-gnu/c/netdb.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
14 changes: 10 additions & 4 deletions src/lib_c/i686-linux-gnu/c/sys/socket.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/lib_c/i686-linux-musl/c/netdb.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 6 additions & 0 deletions src/lib_c/i686-linux-musl/c/sys/socket.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 56dec21

Please sign in to comment.