openc3/lib/openc3/io/udp_sockets.rb
# encoding: ascii-8bit
# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# Modified by OpenC3, Inc.
# All changes Copyright 2022, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
require 'socket'
require 'ipaddr'
require 'timeout' # for Timeout::Error
# Define needed constants for Windows
Socket::IP_MULTICAST_IF = 9 unless Socket.const_defined?('IP_MULTICAST_IF')
Socket::IP_MULTICAST_TTL = 10 unless Socket.const_defined?('IP_MULTICAST_TTL')
module OpenC3
class UdpReadWriteSocket
HOST_0_0_0_0 = '0.0.0.0'
# @param bind_port [Integer[ Port to write data out from and receive data on (0 = randomly assigned)
# @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
# @param external_port [Integer] External port to write to
# @param external_address [String] External host to send data to
# @param multicast_interface_address [String] Local outgoing interface to send multicast packets from
# @param ttl [Integer] Time To Live for outgoing multicast packets
# @param read_multicast [Boolean] Whether or not to try to read from the external address as multicast
# @param write_multicast [Boolean] Whether or not to write to the external address as multicast
def initialize(
bind_port = 0,
bind_address = HOST_0_0_0_0,
external_port = nil,
external_address = nil,
multicast_interface_address = nil,
ttl = 1,
read_multicast = true,
write_multicast = true
)
@socket = UDPSocket.new
# Basic setup to reuse address
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
# Bind to local address and port - This sets recv port, write_src port, recv_address, and write_src_address
@socket.bind(bind_address, bind_port) if bind_address and bind_port
# Default send to the specified address and port
@socket.connect(external_address, external_port) if external_address and external_port
# Handle multicast
if UdpReadWriteSocket.multicast?(external_address, external_port)
if write_multicast
# Basic setup set time to live
@socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl.to_i)
# Set outgoing interface
@socket.setsockopt(
Socket::IPPROTO_IP,
Socket::IP_MULTICAST_IF,
IPAddr.new(multicast_interface_address).hton
) if multicast_interface_address
end
# Receive messages sent to the multicast address
if read_multicast
multicast_interface_address = HOST_0_0_0_0 unless multicast_interface_address
membership = IPAddr.new(external_address).hton + IPAddr.new(multicast_interface_address).hton
@socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
end
end
end
# @param data [String] Binary data to send
# @param write_timeout [Float] Time in seconds to wait for the data to send
def write(data, write_timeout = 10.0)
num_bytes_to_send = data.length
total_bytes_sent = 0
bytes_sent = 0
data_to_send = data
loop do
begin
bytes_sent = @socket.write_nonblock(data_to_send)
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
result = IO.fast_select(nil, [@socket], nil, write_timeout)
if result
retry
else
raise Timeout::Error, "Write Timeout"
end
end
total_bytes_sent += bytes_sent
break if total_bytes_sent >= num_bytes_to_send
data_to_send = data[total_bytes_sent..-1]
end
end
# @param read_timeout [Float] Time in seconds to wait for the read to
# complete
def read(read_timeout = nil)
data = nil
begin
data, _ = @socket.recvfrom_nonblock(65536)
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
result = IO.fast_select([@socket], nil, nil, read_timeout)
if result
retry
else
raise Timeout::Error, "Read Timeout"
end
end
data
end
# Defer all methods to the UDPSocket
def method_missing(method, *args, &block)
@socket.__send__(method, *args, &block)
end
# @param host [String] Machine name or IP address
# @param port [String] Port
# @return [Boolean] Whether the hostname is multicast
def self.multicast?(host, port)
return false if host.nil? || port.nil?
Addrinfo.udp(host, port).ipv4_multicast?
end
end
# Creates a UDPSocket and implements a non-blocking write.
class UdpWriteSocket < UdpReadWriteSocket
# @param dest_address [String] Host to send data to
# @param dest_port [Integer] Port to send data to
# @param src_port [Integer[ Port to send data out from
# @param multicast_interface_address [String] Local outgoing interface to send multicast packets from
# @param ttl [Integer] Time To Live for outgoing packets
# @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
def initialize(
dest_address,
dest_port,
src_port = nil,
multicast_interface_address = nil,
ttl = 1,
bind_address = HOST_0_0_0_0
)
super(
src_port,
bind_address,
dest_port,
dest_address,
multicast_interface_address,
ttl,
false,
true)
end
end
# Creates a UDPSocket and implements a non-blocking read.
class UdpReadSocket < UdpReadWriteSocket
# @param recv_port [Integer] Port to receive data on
# @param multicast_address [String] Address to add multicast
# @param multicast_interface_address [String] Local incoming interface to receive multicast packets on
# @param bind_address [String] Local address to bind to (0.0.0.0 = All local addresses)
def initialize(
recv_port = 0,
multicast_address = nil,
multicast_interface_address = nil,
bind_address = HOST_0_0_0_0
)
super(
recv_port,
bind_address,
nil,
multicast_address,
multicast_interface_address,
1,
true,
false)
end
end
end