diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 236ce34ad21c..fef20485d253 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -221,6 +221,17 @@ module HTTP server.listen end + + it "reuses the TCP port (SO_REUSEPORT)" do + s1 = Server.new(0) { |ctx| } + s1.bind(reuse_port: true) + + s2 = Server.new(s1.port) { |ctx| } + s2.bind(reuse_port: true) + + s1.close + s2.close + end end describe HTTP::Server::RequestProcessor do diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 6e451bb3ac04..3bf7bd11a580 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -424,9 +424,17 @@ describe TCPServer do end end - it "allows to share the same port (SO_REUSEPORT)" do + it "doesn't reuse the TCP port by default (SO_REUSEPORT)" do TCPServer.open("::", 0) do |server| - TCPServer.open("::", server.local_address.port) { } + expect_raises(Errno) do + TCPServer.open("::", server.local_address.port) { } + end + end + end + + it "reuses the TCP port (SO_REUSEPORT)" do + TCPServer.open("::", 0, reuse_port: true) do |server| + TCPServer.open("::", server.local_address.port, reuse_port: true) { } end end end diff --git a/src/http/server.cr b/src/http/server.cr index d5ea3553aa12..b117d8420c18 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -130,6 +130,14 @@ class HTTP::Server @processor = RequestProcessor.new(handler) end + # Returns the TCP port the server is connected to. + # + # For example you may let the system choose a port, then report it: + # ``` + # server = HTTP::Server.new(0) { } + # server.bind + # server.port # => 12345 + # ``` def port if server = @server server.local_address.port.to_i @@ -138,17 +146,26 @@ class HTTP::Server end end - def bind - @server ||= TCPServer.new(@host, @port) + # Creates the underlying `TCPServer` if the doesn't already exist. + # + # You may set *reuse_port* to true to enable the `SO_REUSEPORT` socket option, + # which allows multiple processes to bind to the same port. + def bind(reuse_port = false) + @server ||= TCPServer.new(@host, @port, reuse_port: reuse_port) end - def listen - server = bind + # Starts the server. Blocks until the server is closed. + # + # See `#bind` for details on the *reuse_port* argument. + def listen(reuse_port = false) + server = bind(reuse_port) until @wants_close spawn handle_client(server.accept?) end end + # Gracefully terminates the server. It will process currently accepted + # requests, but it won't accept new connections. def close @wants_close = true @processor.close diff --git a/src/socket/tcp_server.cr b/src/socket/tcp_server.cr index f979268454d7..4a09b11b6a05 100644 --- a/src/socket/tcp_server.cr +++ b/src/socket/tcp_server.cr @@ -14,6 +14,10 @@ require "./tcp_socket" # end # end # ``` +# +# Options: +# - *backlog* to specify how many pending connections are allowed; +# - *reuse_port* to enable multiple processes to bind to the same port (`SO_REUSEPORT`). class TCPServer < TCPSocket include Socket::Server @@ -22,15 +26,13 @@ class TCPServer < TCPSocket super(family) end - def initialize(host : String, port : Int, backlog = SOMAXCONN, dns_timeout = nil) + # Binds a socket to the *host* and *port* combination. + def initialize(host : String, port : Int, backlog = SOMAXCONN, dns_timeout = nil, reuse_port = false) Addrinfo.tcp(host, port, timeout: dns_timeout) do |addrinfo| super(addrinfo.family, addrinfo.type, addrinfo.protocol) self.reuse_address = true - begin - self.reuse_port = true - rescue Errno - end + self.reuse_port = true if reuse_port if errno = bind(addrinfo) { |errno| errno } close @@ -45,16 +47,16 @@ class TCPServer < TCPSocket end # Creates a new TCP server, listening on all local interfaces (`::`). - def self.new(port : Int, backlog = SOMAXCONN) - new("::", port, backlog) + def self.new(port : Int, backlog = SOMAXCONN, reuse_port = false) + new("::", port, backlog, reuse_port: reuse_port) end # Creates a new TCP server and yields it to the block. Eventually closes the # server socket when the block returns. # # Returns the value of the block. - def self.open(host, port, backlog = SOMAXCONN) - server = new(host, port, backlog) + def self.open(host, port, backlog = SOMAXCONN, reuse_port = false) + server = new(host, port, backlog, reuse_port: reuse_port) begin yield server ensure @@ -66,8 +68,8 @@ 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 = SOMAXCONN) - server = new(port, backlog) + def self.open(port : Int, backlog = SOMAXCONN, reuse_port = false) + server = new(port, backlog, reuse_port: reuse_port) begin yield server ensure