UDP Multicast Example

This example demonstrates use of the GIO low-level socket API to send and receive a string using UDP/IP via a multicast group.

The application behaves as the sender if it is invoked with an argument and otherwise behaves as a receiver. A message is sent once per second containing the signed decimal representation of an integer that increments for each message sent. The initial value of the integer is the argument given to the sender, represented as a signed decimal literal. The sender terminates after it has sent a message representing zero. The receiver terminates after it has received a message representing zero. Therefore, the sender and receivers terminate only if the sender is invoked with a non-positive argument.

Note that the SML Basis Library (as it stands on 2020-11-10) does not provide support for joining a multicast group so only the sender can be implemented using the SML Basis Library.

This can be tested without an internet connection, for example running the sender and multiple receivers on the same machine or on multiple machines connected to a portable hotspot on a mobile phone!

Download

Multicast.tar.gz

See Using Make for build instructions.

Library dependencies

  • GLib 2.0 (GLib, GObject, Gio)

File listings

multicast.sml

val localInterface = NONE
val localPort = 6600
val multicastAddr = "239.255.201.1"

fun fmtInetSocketAddress inetSockAddr =
  let
    open Gio

    val addr = InetSocketAddress.getAddress inetSockAddr
    val port = InetSocketAddress.getPort inetSockAddr

    val addrStr = InetAddress.toString addr
    val portStr = LargeInt.toString port
  in
    concat [addrStr, ":", portStr]
  end

fun main () : unit =
  let
    val () = GObject.typeInit ()
    open Gio

    (* create UDP socket *)
    val socket = Socket.new (SocketFamily.IPV_4, SocketType.DATAGRAM, SocketProtocol.DEFAULT)

    (* get multicast internet address *)
    val multicastInetAddr =
      case InetAddress.newFromString multicastAddr of
        SOME address => address
      | NONE         => Giraffe.error 1 ["address \"", multicastAddr, "\" not valid\n"]
    val multicastInetAddrStr = InetAddress.toString multicastInetAddr

    (* get multicast socket address *)
    val multicastInetSockAddr = InetSocketAddress.new (multicastInetAddr, localPort)
    val multicastInetSockAddrStr = fmtInetSocketAddress multicastInetSockAddr
  in
    (* behave as the sender or the receiver depending on the arguments *)
    case CommandLine.arguments () of
      num :: _ =>
      (* one or more aguments: sender *)
      let
        (* convert the first argument to an integer *)
        val n0 =
          case Int.fromString num of
            SOME n => n
          | NONE   => Giraffe.error 1 ["first argument is not a signed integer\n"]

        val delay = Time.fromSeconds 1

        (* send integers as strings to the socket, increasing by 1 until "0" is sent *)
        fun log msg = app print ["sending to ", multicastInetSockAddrStr, ": \"", msg, "\"\n"]

        fun send n =
          let
            val msg = Int.toString n
            val () = log msg
            val buffer =
              GUInt8CArrayN.tabulate
                (String.size msg, fn i => Byte.charToByte (String.sub (msg, i)))
            val _ =
              Socket.sendTo socket (SOME multicastInetSockAddr, buffer, NONE)
                handle GLib.Error _ => Giraffe.error 1 ["failed to send message\n"]
            val () = GC.full ()
          in
            if n <> 0
            then (Posix.Process.sleep delay; send (n + 1))
            else ()
          end

        val () = send n0
      in
        ()
      end

    | [] =>
      (* no arguments: receiver *)
      let
        (* bind the socket to the multicast address *)
        val () = app print ["binding to ", multicastInetSockAddrStr, "\n"]
        val () = Socket.bind socket (multicastInetSockAddr, true)

        (* join the multicast group *)
        val () =
          app (app print) [
            ["joining multicast group ", multicastInetAddrStr, " via "],
            case localInterface of
              SOME iface => ["local interface ", iface]
            | NONE       => ["default local interface"],
            ["\n"]
          ]
        val () =
          Socket.joinMulticastGroup socket (multicastInetAddr, false, localInterface)
            handle
              GLib.Error _ => Giraffe.error 1 ["failed to join multicast group\n"]

        (* create a buffer to receive data *)
        val maxRecvSize = 128

        (* receive from the socket until a string that represents zero is received *)
        fun log inetSockAddr msg =
          app print ["received from ", fmtInetSocketAddress inetSockAddr, ": \"", msg, "\"\n"]

        fun receive () =
          let
            val (n, fromSockAddr, buffer) = Socket.receiveFrom socket (maxRecvSize, NONE)
            val fromInetSockAddr =
              case SocketAddress.getFamily fromSockAddr of
                SocketFamily.IPV_4 =>
                  SocketAddressClass.toDerived InetSocketAddressClass.t fromSockAddr
              | _ => Giraffe.error 1 ["message received from non-inet socket address\n"]
            val msg = CharVector.tabulate (n, Byte.byteToChar o GUInt8CArrayN.get buffer)
            val () = log fromInetSockAddr msg
            val () = GC.full ()
          in
            case Int.fromString msg of
              SOME 0 => ()
            | _      => receive ()
          end

        val () = receive ()

        (* leave the multicast group *)
        val () =
          app (app print) [
            ["leaving multicast group ", multicastInetAddrStr, " via "],
            case localInterface of
              SOME iface => ["local interface ", iface]
            | NONE       => ["default local interface"],
            ["\n"]
          ]
        val () =
          Socket.leaveMulticastGroup socket (multicastInetAddr, false, localInterface)
            handle
              GLib.Error _ => Giraffe.error 1 ["failed to leave multicast group\n"]
      in
        Giraffe.exit 0
      end
  end
    handle e => Giraffe.error 1 ["Uncaught exception\n", exnMessage e, "\n"]

mlton-main.sml

val () = main ()

mlton.mlb

local
  $(SML_LIB)/basis/basis.mlb
  $(SML_LIB)/basis/mlton.mlb
  $(GIRAFFE_SML_LIB)/general/mlton.mlb
  $(GIRAFFE_SML_LIB)/glib-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gobject-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gio-2.0/mlton.mlb
in
  multicast.sml
  mlton-main.sml
end

polyml-libs.sml

use "$(GIRAFFE_SML_LIB)/general/polyml.sml";
use "$(GIRAFFE_SML_LIB)/ffi/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gir/polyml.sml";
use "$(GIRAFFE_SML_LIB)/glib-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gobject-2.0/polyml.sml";
use "$(GIRAFFE_SML_LIB)/gio-2.0/polyml.sml";

polyml-app.sml

(* For each line of the form
 *
 *   use "<file>";
 *
 * <file> is taken as a build dependency.
 *)

use "multicast.sml";

app.mk

################################################################################
# Application-specific values

NAME := multicast


# MLton target
#
# Define:
#   SRC_MLTON       - the SML source files for MLton
#   TARGET_MLTON    - the binary to be built with MLton

ifdef MLTON_VERSION

SRC_MLTON := $(shell $(MLTON_MLTON) -mlb-path-var 'GIRAFFE_SML_LIB $(GIRAFFE_SML_LIBDIR)' -stop f mlton.mlb)

TARGET_MLTON := $(NAME)-mlton

endif


# Poly/ML target
#
# Define:
#   SRC_POLYML      - the SML source files for Poly/ML
#   TARGET_POLYML   - the binary to be built with Poly/ML

ifdef POLYML_VERSION

SRC_POLYML := $(shell cat polyml-app.sml | sed -n 's|^use "\([^"]*\)";$$|\1|p')

TARGET_POLYML := $(NAME)-polyml

endif


# Library dependencies
#
# Define:
#   LIB_NAMES       - list of the libraries that the application references

LIB_NAMES := \
	glib-2.0 \
	gobject-2.0 \
	gio-2.0

# Note that LIB_NAMES does _not_ contain pkg-config names but GIR namespace
# names, which are also the directory names in $(GIRAFFEHOME)/lib/sml.
#
# One method to determine the list is as follows: for each instance of
#
#   $(GIRAFFE_SML_LIB)/$(LIB_NAME)/mlton.mlb
#
# in mlton.mlb, the list should include $(LIB_NAME).