Current section
Files
Jump to
Current section
Files
lib/connectors/tcp.ex
# Copyright (c) 2018 James Laver
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Bricks.Connector.Tcp do
@moduledoc """
A Connector for TCP sockets, using `:gen_tcp`
Belongs to application `:bricks`
## Create Options
### All
Ordering: Required first, then alphabetical
Option | Type(s) | Default | Raw `gen_tcp` option
:--------------------- | :---------------- | :------------- | :-----------------------
`:host` | `binary` | `(REQUIRED)` | `(POSITIONAL)`
`:port` | `pos_integer` | `(REQUIRED)` | `(POSITIONAL)`
`:connect_timeout` | `timeout` | `5000` | `(POSITIONAL)`
`:bam_window` | `Socket.window` | `10` | `(NONE)`
`:active` | `Socket.active` | `true` | `:active`
`:bind_to_device` | `binary` | `(NONE)` | `:bind_to_device`
`:buffer` | `non_neg_integer` | `(UNKNOWN)` | `:buffer`
`:delay_send?` | `boolean` | `false` | `:delay_send`
`:deliver` | `:port`, `:term` | `(UNKNOWN)` | `:deliver`
`:dont_route?` | `boolean` | `(false?)` | `:dontroute`
`:exit_on_close?` | `boolean` | `true` | `:exit_on_close`
`:header_size` | `non_neg_integer` | `(NONE)` | `:header`
`:high_msgq_watermark` | `pos_integer` | `(UNKNOWN)` | `:high_msgq_watermark`
`:high_watermark` | `non_neg_integer` | `(UNKNOWN)` | `:high_watermark`
`:ipv4?` | `boolean` | `true` | `:inet`, `:ipv6_v6only`
`:ipv6?` | `boolean` | `false` | `:inet6`, `:ipv6_v6only`
`:keepalive?` | `boolean` | `false` | `:keepalive`
`:line_delimiter` | `char` | `?\\n` | `:line_delimiter`
`:linger?` | `linger` | `(NONE)` | `:linger`
`:local_port` | `Socket.port_num` | `0` (random) | `:port`
`:low_msgq_watermark` | `pos_integer` | `(UNKNOWN)` | `:low_msgq_watermark`
`:low_watermark` | `non_neg_integer` | `(UNKNOWN)` | `:low_watermark`
`:network_interface` | `Socket.host` | `(NONE` | `:ip`, `:ifaddr`
`:network_namespace` | `binary` | `(NONE)` | `:netns`
`:nodelay?` | `boolean` | `false` | `:nodelay`
`:packet_type` | `packet_type` | `:raw` | `:packet`
`:packet_size` | `pos_integer` | `0` (no limit) | `:packet_size`
`:priority` | `non_neg_integer` | `(NONE)` | `:priority`
`:raw_fd` | `non_neg_integer` | `(NONE)` | `:fd`
`:receive_buffer` | `non_neg_integer` | `(NONE)` | `:recbuf`
`:receive_tclass?` | `boolean` | `false?` | `:recvtclass`
`:receive_tos?` | `boolean` | `false?` | `:recvtos`
`:receive_ttl?` | `boolean` | `false?` | `:recvttl`
`:receive_timeout` | `timeout` | `5000` | `(POSITIONAL)`
`:send_timeout` | `timeout` | `5000` | `:send_timeout`
`:reuse_addr?` | `boolean` | `false` | `:reuseaddr`
`:send_timeout_close?` | `boolean` | `true` | `:send_timeout_close`
`:show_econnreset?` | `boolean` | `false` | `:show_econnreset`
`:send_buffer` | `non_neg_integer` | `(NONE)` | `:sndbuf`
`:tos` | `non_neg_integer` | `(NONE)` | `:tos`
`:tclass` | `non_neg_integer` | `(NONE)` | `:tclass`
`:tcp_module` | `atom` | `(SEE DOCS)` | `:tcp_module`
### Destination Selection
Option | Type | Default | Raw `gen_tcp` option
:------ | :------------ | :----------- | :-------------------
`:host` | `binary` | `(REQUIRED)` | `(POSITIONAL)`
`:port` | `pos_integer` | `(REQUIRED)` | `(POSITIONAL)`
### IP Version Selection
Option | Type | Default | Raw `gen_tcp` option
:------- | :-------- | :------ | :-----------------------
`:ipv4?` | `boolean` | `true` | `:inet`, `:ipv6_v6only`
`:ipv6?` | `boolean` | `false` | `:inet6`, `:ipv6_v6only`
These options toggle which IP versions may be used. At least one
must be `true` or you will get an error.
### Timeouts
Option | Type | Default | Raw `gen_tcp` option
:----------------- | :-------- | :------ | :-------------------
`:connect_timeout` | `timeout` | `5000` | `(POSITIONAL)`
`:receive_timeout` | `timeout` | `5000` | `(POSITIONAL)`
`:send_timeout` | `timeout` | `5000` | `:send_timeout`
These toggle how long you are prepared to wait for an operation to
complete before a timeout error is returned. They are standard
erlang `timeout` values: non-negative integers or `:infinity`.
### Activity Control
Option | Type | Default | Raw `gen_tcp` option
:------------ | :-------------- | :------ | :-------------------
`:active` | `Socket.active` | `true` | `:active`
`:bam_window` | `Socket.window` | `10` | `(NONE)`
See discussion on socket activity modes in the `Bricks.Socket`
module documentation for more information.
### Erlang Options
Option | Type | Default | Raw `gen_tcp` option
:--------------------- | :---------------- | :----------- | :---------------------
`:buffer` | `non_neg_integer` | `(UNKNOWN)` | `:buffer`
`:delay_send?` | `boolean` | `false` | `:delay_send`
`:exit_on_close?` | `boolean` | `true` | `:exit_on_close`
`:header_size` | `non_neg_integer` | `(NONE)` | `:header`
`:high_msgq_watermark` | `pos_integer` | `(UNKNOWN)` | `:high_msgq_watermark`
`:high_watermark` | `non_neg_integer` | `(UNKNOWN)` | `:high_watermark`
`:line_delimiter` | `char` | `?\\n` | `:line_delimiter`
`:low_msgq_watermark` | `pos_integer` | `(UNKNOWN)` | `:low_msgq_watermark`
`:low_watermark` | `non_neg_integer` | `(UNKNOWN)` | `:low_watermark`
`:packet_type` | `packet_type` | `:raw` | `:packet`
`:send_timeout_close?` | `boolean` | `true` | `:send_timeout_close`
`:show_econnreset?` | `boolean` | `false` | `:show_econnreset`
`:tcp_module` | `atom` | `(SEE DOCS)` | `:tcp_module`
`:tcp_opts` | `proplist` | `[]` | `(ANY)`
#### `:buffer`
The size of the user-level buffer used by the driver. Not to be
confused with options `:send_buffer` and `:receive_buffer`, which
correspond to the Kernel socket buffers. For TCP it is recommended
to have val(buffer) >= val(recbuf) to avoid performance issues
because of unnecessary copying. However, as the size set for recbuf
usually become larger, you are encouraged to use getopts/2 to
analyze the behavior of your operating system.
Note that this is also the maximum amount of data that can be
received from a single `recv` call. If you are using higher than
normal MTU consider setting buffer higher.
#### `:delay_send?`
Normally, when an Erlang process sends to a socket, the driver tries
to send the data immediately. If that fails, the driver uses any
means available to queue up the message to be sent whenever the
operating system says it can handle it. Setting `delay_send: true`
makes all messages queue up. The messages sent to the network are
then larger but fewer. The option affects the scheduling of send
requests versus Erlang processes instead of changing any real
property of the socket. The option is implementation-specific.
#### `:exit_on_close?`
The only reason to set it to false is if you want to continue
sending data to the socket after a close is detected, for example,
if the peer uses `:gen_tcp.shutdown/2` to shut down the write side.
#### `:header_size`
This option is only meaningful if option binary was specified when
the socket was created. If option header is specified, the first
Size number bytes of data received from the socket are elements of a
list, and the remaining data is a binary specified as the tail of
the same list. For example, if set to `2`, the data received matches
`[byte1,byte2|binary]`
#### `:high_msgq_watermark`
The socket message queue is set to a busy state when the amount of
data on the message queue reaches this limit. Notice that this limit
only concerns data that has not yet reached the ERTS internal socket
implementation. Defaults to `8 kB`.
Senders of data to the socket are suspended if either the socket
message queue is busy or the socket itself is busy.
For more information, see options `:low_msgq_watermark`,
`:high_watermark`, and `:low_watermark`.
Notice that distribution sockets disable the use of
`:high_msgq_watermark` and `:low_msgq_watermark`. Instead use the
distribution buffer busy limit, which is a similar feature.
#### `:high_watermark`
The socket is set to a busy state when the amount of data queued
internally by the ERTS socket implementation reaches this
limit. Defaults to `8 kB`.
Senders of data to the socket are suspended if either the socket
message queue is busy or the socket itself is busy.
For more information, see options low_watermark,
high_msgq_watermark, and low_msqg_watermark.
#### `:line_delimiter`
Sets the line delimiting character for line-oriented protocols
(`:line`). Defaults to `?\n`.
#### `:low_msgq_watermark`
If the socket message queue is in a busy state, the socket message
queue is set in a not busy state when the amount of data queued in
the message queue falls below this limit. Notice that this limit
only concerns data that has not yet reached the ERTS internal socket
implementation. Defaults to `4 kB`.
Senders that are suspended because of either a busy message queue or
a busy socket are resumed when the socket message queue and the
socket are not busy.
For more information, see options `:high_msgq_watermark`,
`:high_watermark`, and `:low_watermark`.
Notice that distribution sockets disable the use of
`:high_msgq_watermark` and `:low_msgq_watermark`. Instead they use
the distribution buffer busy limit, which is a similar feature.
#### `:low_watermark`
If the socket is in a busy state, the socket is set in a not busy
state when the amount of data queued internally by the ERTS socket
implementation falls below this limit. Defaults to `4 kB`.
Senders that are suspended because of a busy message queue or a busy
socket are resumed when the socket message queue and the socket are
not busy.
For more information, see options `:high_watermark`,
`:high_msgq_watermark`, and `:low_msgq_watermark`.
#### `:packet_type`
Defines the type of packets to use for a socket. Possible values:
`:raw` | `0`
: No packaging is done.
`1` | `2` | `4`
: Packets consist of a header specifying the number of bytes in the
packet, followed by that number of bytes. The header length can be
one, two, or four bytes, and containing an unsigned integer in
big-endian byte order. Each send operation generates the header,
and the header is stripped off on each receive operation. The
4-byte header is limited to 2Gb.
`:asn1` | `:cdr` | `:sunrm` | `:fcgi` | `:tpkt` | `:line`
: These packet types only have effect on receiving. When sending a
packet, it is the responsibility of the application to supply a
correct header. On receiving, however, one message is sent to the
controlling process for each complete packet received, and,
similarly, each call to `:gen_tcp.recv/2,3` returns one complete
packet. The header is not stripped off.
The meanings of the packet types are as follows:
- `:asn1` - ASN.1 BER
- `:sunrm` - Sun's RPC encoding
- `:cdr` - CORBA (GIOP 1.1)
- `:fcgi` - Fast CGI
- `:tpkt` - TPKT format [RFC1006]
- `:line` - Line mode, a packet is a line-terminated with newline,
lines longer than the receive buffer are truncated
##### `:http` | `:http_bin`
The Hypertext Transfer Protocol. The packets are returned with the
format according to HttpPacket described in
`:erlang.decode_packet/3` in ERTS. A socket in passive mode returns
`{:ok, packet}` from `:gen_tcp.recv` while an active socket sends
messages like `{http, socket_handle, packet}`.
##### `:httph` | `:httph_bin`
These two types are often not needed, as the socket automatically
switches from `:http`/`:http_bin` to `:httph`/`:httph_bin`
internally after the first line is read. However, there can be
occasions when they are useful, such as parsing trailers from
chunked encoding.
#### `:send_timeout_close?`
Used together with `:send_timeout` to specify whether the socket is to
be automatically closed when the send operation returns
`{:error,:timeout}`. The recommended setting is `true`, which
automatically closes the socket.
#### `:show_econnreset?`
When this option is set to `false`, which is default, an RST
received from the TCP peer is treated as a normal close (as though
an FIN was sent). A caller to `:gen_tcp.recv/2` gets `{:error,
:closed}`. In `active` mode, the controlling process receives a
`{:tcp_closed, socket_handle}` message, indicating that the peer has
closed the connection.
Setting this option to `true` allows you to distinguish between a
connection that was closed normally, and one that was aborted
(intentionally or unintentionally) by the TCP peer. A call to
`:gen_tcp.recv/2` returns `{:error, :econnreset}`. In `active` mode,
the controlling process receives a `{:tcp_error, socket_handle,
:econnreset}` message before the usual `{:tcp_closed,
socket_handle}`, as is the case for any other socket error. Calls to
`:gen_tcp.send/2` also returns `{:error, :econnreset}` when it is
detected that a TCP peer has sent an RST.
A connected socket returned from `:gen_tcp.accept/1` inherits the
`:show_econnreset?` setting from the listening socket.
#### `:tcp_module`
Overrides which callback module is used. Defaults to `:inet_tcp` for
IPv4 and `:inet6_tcp` for IPv6.
#### `:tcp_opts`
Raw `gen_tcp`/`inet` options proplist. *Appended* to options.
### OS options
Option | Type | Default | Raw `gen_tcp` option
:------------------- | :---------------- | :------------- | :-------------------
`:bind_to_device` | `binary` | `(NONE)` | `:bind_to_device`
`:deliver` | `:port`, `:term` | `(UNKNOWN)` | `:deliver`
`:dont_route?` | `boolean` | `(false?)` | `:dontroute`
`:keepalive?` | `boolean` | `false` | `:keepalive`
`:linger` | `linger` | `(NONE)` | `:linger`
`:local_port` | `Socket.port_num` | `0` (random) | `:port`
`:network_interface` | `Socket.host` | `(NONE` | `:ip`, `:ifaddr`
`:network_namespace` | `binary` | `(NONE)` | `:netns`
`:nodelay?` | `boolean` | `false` | `:nodelay`
`:packet_size` | `pos_integer` | `0` (no limit) | `:packet_size`
`:priority` | `non_neg_integer` | `(NONE)` | `:priority`
`:raw_fd` | `non_neg_integer` | `(NONE)` | `:fd`
`:receive_buffer` | `non_neg_integer` | `(NONE)` | `:recbuf`
`:receive_tclass?` | `boolean` | `false?` | `:recvtclass`
`:receive_tos?` | `boolean` | `false?` | `:recvtos`
`:receive_ttl?` | `boolean` | `false?` | `:recvttl`
`:reuse_addr?` | `boolean` | `false` | `:reuseaddr`
`:send_buffer` | `non_neg_integer` | `(NONE)` | `:sndbuf`
`:tos` | `non_neg_integer` | `(NONE)` | `:tos`
`:tclass` | `non_neg_integer` | `(NONE)` | `:tclass`
#### `:bind_to_device`
Binds a socket to a specific network interface. This option must be
used in a function call that creates a socket, that is,
`:gen_tcp.connect/3,4`, `:gen_tcp.listen/2`, `:gen_udp.open/1,2`, or
`:gen_sctp.open/0,1,2`.
Unlike `getifaddrs/0`, Ifname is encoded a binary. In the unlikely
case that a system is using non-7-bit-ASCII characters in network
device names, special care has to be taken when encoding this
argument.
This option uses the Linux-specific socket option `SO_BINDTODEVICE`,
such as in Linux kernel 2.0.30 or later, and therefore only exists
when the runtime system is compiled for such an operating system.
Before Linux 3.8, this socket option could be set, but could not
retrieved with getopts/2. Since Linux 3.8, it is readable.
The virtual machine also needs elevated privileges, either running
as superuser or (for Linux) having capability `CAP_NET_RAW`.
The primary use case for this option is to bind sockets into Linux VRF instances.
#### `:deliver`
When `active: true`, data is delivered on the form `port` :
`{socket_handle, {:data, [h1,..hsz | data]}}` or `term` : `{:tcp,
socket_handle, [h1..hsz | data]}`
#### `:dont_route?`
Enables/disables routing bypass for outgoing messages.
#### `:keepalive?`
Enables/disables periodic transmission on a connected socket when no
other data is exchanged. If the other end does not respond, the
connection is considered broken and an error message is sent to the
controlling process. Defaults to disabled.
#### `:linger`
Determines the time-out, in seconds, for flushing unsent data in the
`:gen_tcp.close/1` socket call. If the first component of the value
tuple is false, the second is ignored. This means that
`:gen_tcp.close/1` returns immediately, not waiting for data to be
flushed. Otherwise, the second component is the flushing time-out,
in seconds.
#### `:local_port`
Local port number to use for the outgoing socket.
#### `:network_interface`
If the host has many network interfaces, this option specifies which one to use.
#### `:network_namespace`
Sets a network namespace for the socket. Parameter s a filename
defining the namespace, for example, "/var/run/netns/example",
typically created by command `ip netns add example`. This option
must be used in a function call that creates a socket, that is,
`:gen_tcp.connect/3,4`, `:gen_tcp.listen/2`, `:gen_udp.open/1,2`, or
`:gen_sctp.open/0,1,2`.
This option uses the Linux-specific syscall `setns()`, such as in
Linux kernel 3.0 or later, and therefore only exists when the
runtime system is compiled for such an operating system.
The virtual machine also needs elevated privileges, either running
as superuser or (for Linux) having capability `CAP_SYS_ADMIN`
according to the documentation for `setns(2)`. However, during
testing also `CAP_SYS_PTRACE` and `CAP_DAC_READ_SEARCH` have proven
to be necessary.
Example:
```shell
setcap cap_sys_admin,cap_sys_ptrace,cap_dac_read_search+epi beam.smp
```
Notice that the filesystem containing the virtual machine executable
(`beam.smp` in the example) must be local, mounted without flag
`nosetuid`, support extended attributes, and the kernel must support
file capabilities. All this runs out of the box on at least Ubuntu
12.04 LTS, except that SCTP sockets appear to not support network
namespaces.
Namespace is a filename and is encoded and decoded as discussed in
module file, with the following exceptions:
- Emulator flag +fnu is ignored.
- `:inet.getopts/2` for this option returns a binary for the
filename if the stored filename cannot be decoded. This is only
to occur if you set the option using a binary that cannot be
decoded with the emulator's filename encoding:
`:file.native_name_encoding/0`.
#### `:nodelay?`
If `true`, option `TCP_NODELAY` is turned on for the socket, which
means that also small amounts of data are sent immediately.
#### `:packet_size`
Sets the maximum allowed length of the packet body. If the packet
header indicates that the length of the packet is longer than the
maximum allowed length, the packet is considered invalid. The same
occurs if the packet header is too large for the socket receive
buffer.
For line-oriented protocols (`line`, `http*`), option `packet_size`
also guarantees that lines up to the indicated length are accepted
and not considered invalid because of internal buffer limitations.
#### `:priority`
Sets the `SO_PRIORITY` socket level option on platforms where this is
implemented. The behavior and allowed range varies between different
systems. The option is ignored on platforms where it is not
implemented. Use with caution.
#### `:raw_fd`
If a socket has somehow been connected without using gen_tcp, use
this option to pass the file descriptor for it. If
`:network_interface` and/or `:port` options are combined with this
option, the fd is bound to the specified interface and port before
connecting. If these options are not specified, it is assumed that
the fd is already bound appropriately.
#### `:receive_buffer`
The minimum size of the receive buffer to use for the socket. You
are encouraged to use `:inet.getopts/2` to retrieve the size set by
your operating system.
#### `:receive_tclass?`
If set to true activates returning the received `TCLASS` value on
platforms that implements the protocol `IPPROTO_IPV6` option
`IPV6_RECVTCLASS` or `IPV6_2292RECVTCLASS` for the socket. The value
is returned as a `{:tclass,tclass}` tuple regardless of if the
platform returns an `IPV6_TCLASS` or an `IPV6_RECVTCLASS` `CMSG`
value.
For packet oriented sockets that supports receiving ancillary data
with the payload data (gen_udp and gen_sctp), the `TCLASS` value is
returned in an extended return tuple contained in an ancillary data
list. For stream oriented sockets (gen_tcp) the only way to get the
`TCLASS` value is if the platform supports the pktoptions option.
#### `:receive_tos?`
If set to true activates returning the received `TOS` value on
platforms that implements the protocol `IPPROTO_IP` option
`IP_RECVTOS` for the socket. The value is returned as a `{:tos,tos}`
tuple regardless of if the platform returns an `IP_TOS` or an
`IP_RECVTOS` `CMSG` value.
For packet oriented sockets that supports receiving ancillary data
with the payload data (`:gen_udp` and `:gen_sctp`), the `TOS` value
is returned in an extended return tuple contained in an ancillary
data list. For stream oriented sockets (`:gen_tcp`) the only way to
get the TOS value is if the platform supports the `pktoptions` option.
#### `:receive_ttl?`
If set to true activates returning the received `TTL` value on
platforms that implements the protocol `IPPROTO_IP` option
`IP_RECVTTL` for the socket. The value is returned as a `{:ttl,ttl}`
tuple regardless of if the platform returns an `IP_TTL` or an
`IP_RECVTTL` `CMSG` value.
For packet oriented sockets that supports receiving ancillary data
with the payload data (`:gen_udp` and `:gen_sctp`), the `TTL` value
is returned in an extended return tuple contained in an ancillary
data list. For stream oriented sockets (`;gen_tcp`) the only way to
get the `TTL` value is if the platform supports the `pktoptions`
option.
#### `:reuse_addr?`
Allows or disallows local reuse of port numbers. By default, reuse
is disallowed.
#### `:send_buffer`
The minimum size of the send buffer to use for the socket. You are
encouraged to use `getopts/2`, to retrieve the size set by your
operating system.
#### `:tos`
Sets `IP_TOS IP` level options on platforms where this is
implemented. The behavior and allowed range varies between different
systems. The option is ignored on platforms where it is not
implemented. Use with caution.
#### `:tclass`
Sets `IPV6_TCLASS IP` level options on platforms where this is
implemented. The behavior and allowed range varies between different
systems. The option is ignored on platforms where it is not
implemented. Use with caution.
"""
@enforce_keys [
:host,
:port,
:tcp_opts,
:receive_timeout,
:connect_timeout,
:bam_window,
:active
]
defstruct @enforce_keys
alias Bricks.{Connector, Options, Socket, Util}
alias Bricks.Connector.Tcp
alias Bricks.Error.{BadCombo, BadOption, Connect}
import Bricks.Guards
@default_connect_timeout 5000
@default_receive_timeout 5000
@default_send_timeout 5000
@default_tcp_opts []
@default_bam_window 10
@default_active false
## Types
@typedoc "Valid linger value. See docs for info"
@type linger :: {boolean(), non_neg_integer()}
@typedoc "Packet type for inbuilt message parsing facilities"
@type packet_type ::
:raw
| 0
| 1
| 2
| 4
| :asn1
| :cdr
| :sunrm
| :fcgi
| :tpkt
| :line
| :http
| :http_bin
| :httph
| :httph_bin
@typedoc "TCP Connector State"
@type t :: %Tcp{
host: Socket.host(),
port: Socket.port_num(),
tcp_opts: [term()],
receive_timeout: timeout(),
connect_timeout: timeout(),
bam_window: Socket.window(),
active: Socket.active()
}
@typedoc "Options for `create/1`"
@type create_opts :: %{
# Required
:host => binary(),
:port => pos_integer(),
# Optional Socket members
optional(:connect_timeout) => timeout(),
optional(:receive_timeout) => timeout(),
optional(:bam_window) => Socket.window(),
optional(:active) => Socket.active(),
# Optional non-Socket member `:gen_tcp`/`:inet` socket options
optional(:bind_to_device) => binary(),
optional(:buffer) => non_neg_integer(),
optional(:delay_send?) => boolean(),
optional(:deliver) => :port | :term,
optional(:dont_route?) => boolean(),
optional(:exit_on_close?) => boolean(),
optional(:header_size) => non_neg_integer(),
optional(:high_msgq_watermark) => pos_integer(),
optional(:high_watermark) => non_neg_integer(),
optional(:ipv4?) => boolean(),
optional(:ipv6?) => boolean(),
optional(:keepalive?) => boolean(),
optional(:line_delimiter) => char(),
optional(:linger) => {boolean(), pos_integer()},
optional(:local_port) => Socket.port_num(),
optional(:low_msgq_watermark) => pos_integer(),
optional(:low_watermark) => non_neg_integer(),
optional(:network_interface) => binary() | :inet.socket_address(),
optional(:network_namespace) => binary(),
optional(:nodelay?) => boolean(),
optional(:packet_type) => :raw | 1 | 2 | 4,
optional(:packet_size) => pos_integer(),
optional(:priority) => non_neg_integer(),
optional(:raw_fd) => non_neg_integer(),
optional(:receive_buffer) => non_neg_integer(),
optional(:receive_tclass?) => boolean(),
optional(:receive_tos?) => boolean(),
optional(:receive_ttl?) => boolean(),
optional(:reuse_addr?) => boolean(),
optional(:send_timeout) => timeout(),
optional(:send_timeout_close?) => boolean(),
optional(:show_econnreset?) => boolean(),
optional(:send_buffer) => non_neg_integer(),
optional(:tcp_module) => atom(),
optional(:tcp_opts) => [term()],
optional(:tos) => non_neg_integer(),
optional(:tclass) => non_neg_integer()
}
@typedoc "The errors that `create/1` may return"
@type option_error :: BadOption.t() | BadCombo.t()
@spec create(create_opts()) :: {:ok, Connector.t()} | {:error, option_error()}
@doc """
Creates a `Bricks.Connector` which uses this module as a callback
and the provided options to open and configure the socket.
See module documentation for more information about the options
"""
def create(opts) do
with {:ok, tcp_opts} <- tcp_options(opts) do
create_connector(opts, tcp_opts)
end
end
## behaviour impl: Connector
@spec connect(t()) :: {:ok, Socket.t()} | {:error, term()}
@doc false
def connect(%Tcp{host: host, port: port, tcp_opts: opts, connect_timeout: timeout} = tcp) do
case :gen_tcp.connect(host, port, opts, timeout) do
{:error, reason} -> {:error, Connect.new(reason)}
{:ok, socket} -> socket(socket, tcp)
end
end
## Internal helpers
@tcp_table_options [
bind_to_device: {:bind_to_device, &is_binary/1, [:binary]},
buffer: {:buffer, &non_neg_int?/1, [:non_neg_int]},
deliver: {:deliver, &deliver?/1, [:port, :term]},
delay_send?: {:delay_send, &is_boolean/1, [:bool]},
dont_route?: {:dontroute, &is_boolean/1, [:bool]},
exit_on_close?: {:exit_on_close, &is_boolean/1, [:bool]},
header_size: {:header, &non_neg_int?/1, [:non_neg_int]},
high_msgq_watermark: {:high_msgq_watermark, &pos_int?/1, [:pos_int]},
high_watermark: {:high_watermark, &non_neg_int?/1, [:non_neg_int]},
keepalive?: {:keepalive, &is_boolean/1, [:bool]},
line_delimiter: {:line_delimiter, &char?/1, [:char]},
linger: {:linger, &linger?/1, [:see_docs]},
local_port: {:port, &port?/1, [:non_neg_int]},
low_msgq_watermark: {:low_msgq_watermark, &pos_int?/1, [:pos_int]},
low_watermark: {:low_watermark, &non_neg_int?/1, [:non_neg_int]},
network_namespace: {:netns, &is_binary/1, [:binary]},
nodelay?: {:nodelay, &is_boolean/1, [:bool]},
packet_type: {:packet, &packet_type?/1, [:raw, 1, 2, 4]},
packet_size: {:packet_size, &pos_int?/1, [:pos_int]},
priority: {:priority, &non_neg_int?/1, [:non_neg_int]},
raw_fd: {:fd, &non_neg_int?/1, [:non_neg_int]},
receive_buffer: {:recbuf, &non_neg_int?/1, [:non_neg_int]},
receive_tclass?: {:recvtclass, &is_boolean/1, [:bool]},
receive_tos?: {:recvtos, &is_boolean/1, [:bool]},
receive_ttl?: {:recvttl, &is_boolean/1, [:bool]},
reuse_addr?: {:reuseaddr, &is_boolean/1, [:bool]},
send_timeout: {:send_timeout, &timeout?/1, [:infinity, :non_neg_int]},
send_timeout_close?: {:send_timeout_close, &is_boolean/1, [:bool]},
show_econnreset?: {:show_econnreset, &is_boolean/1, [:bool]},
send_buffer: {:sndbuf, &non_neg_int?/1, [:non_neg_int]},
tcp_module: {:tcp_module, &is_atom/1, [:atom]},
tos: {:tos, &non_neg_int?/1, [:non_neg_int]},
tclass: {:tclass, &non_neg_int?/1, [:non_neg_int]}
]
@tcp_custom_options [
:ipv4?,
:ipv6?,
:connect_timeout,
:send_timeout,
:receive_timeout,
:tcp_opts,
:active,
:binary?,
:host,
:port,
:bam_window,
:raw,
:network_interface
]
# network_interface: {:ip, &host?/1, },
@tcp_option_keys @tcp_custom_options ++ Keyword.keys(@tcp_table_options)
defp create_connector(opts, tcp_opts) do
with {:ok, conn_timeout} <-
Options.default_timeout(opts, :connect_timeout, @default_connect_timeout),
{:ok, receive_timeout} <-
Options.default_timeout(opts, :receive_timeout, @default_receive_timeout),
{:ok, bam_window} <-
Options.default(opts, :bam_window, @default_bam_window, &window?/1, [:once, :pos_int]),
{:ok, active} <-
Options.default(opts, :active, @default_active, &active?/1, [:bool, :integer, :once]),
{:ok, host} <- Options.required(opts, :host, &host?/1, [:binary, :ipv4, :ipv6]),
{:ok, port} <- Options.required(opts, :port, &port?/1, [:pos_int]) do
tcp = %Tcp{
host: host,
port: port,
tcp_opts: tcp_opts,
receive_timeout: receive_timeout,
connect_timeout: conn_timeout,
bam_window: bam_window,
active: active
}
{:ok, Connector.new(__MODULE__, tcp)}
end
end
defp tcp_options(opts) do
with {:ok, active} <-
Options.default(opts, :active, @default_active, &active?/1, [:bool, :integer, :once]),
{:ok, mode} <- mode(opts),
{:ok, send_timeout} <-
Options.default_timeout(opts, :send_timeout, @default_send_timeout),
{:ok, tcp_opts} <-
Options.default(opts, :tcp_opts, @default_tcp_opts, &is_list/1, [:proplist]),
:ok <- Options.check_extra_keys(opts, @tcp_option_keys),
{:ok, table} <- Options.table_options(opts, @tcp_table_options),
{:ok, ni} <- network_interface_options(opts),
{:ok, ip} <- ip_opts(opts),
{:ok, raw} <- raw_opts(opts) do
synthetic = [active: active, mode: mode, send_timeout: send_timeout]
{:ok, ip ++ ni ++ table ++ raw ++ synthetic ++ tcp_opts}
end
end
defp network_interface_options(opts) do
case Map.fetch(opts, :network_interface) do
{:ok, ni} ->
case host?(ni) do
true -> {:ok, [{:ip, Util.host_address(ni)}]}
_ -> {:error, BadOption.new(:network_interface, ni, [:binary, :ip])}
end
_ ->
{:ok, []}
end
end
@doc false
def mode(opts) do
Map.get(opts, :binary?, true)
|> case do
true ->
{:ok, :binary}
false ->
{:ok, :list}
binary? ->
{:error, BadOption.new(:binary?, binary?, [:bool])}
end
end
defp socket(socket, %Tcp{} = tcp) do
opts = Map.take(tcp, [:host, :port, :active, :receive_timeout, :bam_window])
try do
with {:error, reason} <- Socket.Tcp.create(socket, opts) do
:ok = :gen_tcp.close(socket)
{:error, reason}
end
rescue
e ->
:gen_tcp.close(socket)
{:error, e}
end
end
defp ip_opts(opts) do
ipv4? = Map.get(opts, :ipv4?, true)
ipv6? = Map.get(opts, :ipv6?, false)
case {ipv4?, ipv6?} do
{true, true} ->
{:ok, [:inet6]}
{true, false} ->
{:ok, [:inet]}
{false, true} ->
{:ok, [:inet6, {:ipv6_v6only, true}]}
{x, _} when not is_boolean(x) ->
{:error, BadOption.new(:ipv4?, ipv4?, [:bool])}
{_, x} when not is_boolean(x) ->
{:error, BadOption.new(:ipv6?, ipv6?, [:bool])}
{false, false} ->
{:error, BadCombo.new(%{ipv4?: ipv4?, ipv6?: ipv6?}, "May not both be false")}
end
end
@doc false
def raw_opts(opts) do
case Map.fetch(opts, :raw) do
{:ok, {protocol, optionnum, valuebin}} -> {:ok, [{:raw, protocol, optionnum, valuebin}]}
{:ok, other} -> {:error, BadOption.new(:raw, other, [:see_docs])}
:error -> {:ok, []}
end
end
end