Just An Application

July 26, 2014

Rust Revisted: Ch…ch…ch…changes Or No More Sockets Anymore

Looking at Swift and some of its more Rust-like features, e.g., tuples, reminded me that its been over a year since I last looked at Rust.

In that time its gone from version 0.07 to 0.11 and there have been lots of changes including the disappearance of all the socket functions.

In their place we have the structs

  • TcpListener

  • TcpAcceptor

  • TcpStream

These are all in the std::io::net::tcp module.

Listening For Connections

To listen for incoming connections you call the static method

    TcpListener::bind

passing it an address and a port, like so (this and the other code snippets are all from examples given in the documentation)

    ...

    let listener = TcpListener::bind("127.0.0.1", 80);

    ...

This creates a TcpListener instance and binds it to the given address and port.

You can then listen for incoming connections by calling the listen method

    ...
    
    let mut acceptor = listener.listen();

    ...

This creates a TcpAcceptor instance which can be used to accept incoming connections

Accepting Connections

Once you have a TcpAcceptor you can accept a single connection by calling the method accept.

Alternatively, you can call the method incoming which returns an iterator which can be used to loop over incoming connections as they are made.

    ...
    
    for stream in acceptor.incoming() {
        match stream {
            Err(e) => { /* connection failed */ }
            Ok(stream) => spawn(proc() {
                // connection succeeded
                handle_client(stream)
            })
        }
    }

    ...

The IoResult<T> Type And IO Traits

There is one thing about the example given in the documentation which is at first glance rather puzzling.

The method

    TcpListener::bind

is declared to return

    IOResult<TcpListener>

The type IoResult is declared as

    type IoResult<T> = Result<T, IoError>

and

    Result<T, E>

is a variant enum.

So the method TcpListener::bind actually returns an enum and yet in the example the method listen is called directly on the return value, i.e. the enum.

The same is true for the method listen which is defined to return

    IOResult<TcpAcceptor>

So why does the code even compile ?

The answer is that IoResult implements all the methods defined for the traits

  • Acceptor
  • Listener
  • Reader
  • Seek
  • Writer

For example, this is the definition of the IOResult implementation of the Listener trait

    impl<T, A: Acceptor<T>, L: Listener<T, A>> Listener<T, A> for IoResult<L> {
        fn listen(self) -> IoResult<A> {
            match self {
                Ok(listener) => listener.listen(),
                Err(e) => Err(e),
            }
        }
    }

which can be found in the file

    $(RUST_SRC)/src/libstd/io/result.rs

As you can see if the IOResult contains a Listener the result of calling the method listen on it is returned, otherwise the IOResult must contain an error and that is returned.

Whilest not having to explicitly unwrap the result of each method which returns an IOResult is certainly convenient there can be a potential drawback in some cases.

If you run the following, which is based on the example code from the documentation, under Mac OS X

    fn main()
    {
        // bind the listener to the specified address
        
        let listener = TcpListener::bind("127.0.0.1", 80);
    
        match listener
        {
            Ok(_) =>
            {
                println!("bind: have listener");
            }
            Err(_) =>
            {
                println!("bind: have error");
            }
        }
    
        let mut acceptor = listener.listen();
    
        match acceptor
        {
            Ok(_) =>
            {
                println!("listen: have acceptor");
            }
            Err(e) =>
            {
                println!("listen: have error");
            }
        }
    
    
        // accept connections and process them, spawning a new tasks for each one
    
        for stream in acceptor.incoming()
        {
            match stream
            {
                Err(e) =>
                {
                    /* connection failed */
                    println!("incoming: have error {}", e);
                }
    
                Ok(stream) => spawn(proc()
                                    {
                                       // connection succeeded
                                       handle_client(stream)
                                    })
            }
        }
    
        // close the socket server
        drop(acceptor);
    }

you get

    bind: have error
    listen: have error

because by default the program cannot open port 80, followed by

    incoming: have error permission denied (Permission denied)

repeated ad infinitum, or at least until the point that you hit Ctrl-C, as the program screams around in a tight loop doing absolutely nothing.

In non-example code the failure case within the loop would probably cause the loop to exit or the task to terminate so it wouldn’t loop indefinitely but even so it would probably be better to check whether the initial call to bind actually worked before doing anything else.

Explicit Error Checking

The problem with explicit error checking is that it can get quite messy quite fast.

For example

    fn main()
    {
        match TcpListener::bind("127.0.0.1", 80)
        {
            Ok(listener) =>
            {
                println!("bind: have listener");
                match listener.listen()
                {
                    Ok(mut acceptor) =>
                    {
                        println!("listen: have acceptor");
                        for stream in acceptor.incoming()
                        {
                            match stream
                            {
                                Err(e) =>
                                {
                                    /* connection failed */
                                    println!("incoming: have error");
                                }

                                Ok(stream) => spawn(proc()
                                                    {
                                                        // connection succeeded
                                                        handle_client(stream)
                                                    })
                            }
                        }
                        // close the socket server
                        drop(acceptor);
                    }
                    Err(_) =>
                    {
                        println!("listen: have error");
                    }
                }
            }
            Err(_) =>
            {
                println!("bind: have error");
            }
        }
    }

Whilest not completely impenetrable its not the kind of code you would want to have to maintain if you could possibly avoid it.

The try Macro

There is a utility macro

    try

which is intended to help in this situation.

It is defined like this

    macro_rules! try(
        ($e:expr) => (match $e { Ok(e) => e, Err(e) => return Err(e) })
    )

The definition can be found in the file

    $(RUST_SRC)/rust-0.11.0/src/libcore/macros.rs

As you can see, it works by returning from the function immediately if an error occurs, so you can do something like this

    ...
    
    let listener = try!(TcpListener::bind("127.0.0.1", 80));
    
    ...

and if the call to TcpListener::bind fails the calling function returns immediately

We can then do the same thing for the call to listen

    ...
    
    let mut acceptor = try!(listener.listen());
    
    ...

so we end up with something like this

    fn run(host:&str, port: u16) -> IoResult<????>
    {
        // bind the listener to the specified address
    
        let listener = try!(TcpListener::bind(host, port));
    
        println!("listen: have listener");
    
        let mut acceptor = try!(listener.listen());
    
        println!("listen: have acceptor");
    
        // accept connections and process them, spawning a new tasks for each one
    
        for stream in acceptor.incoming()
        {
            match stream
            {
                Err(e) =>
                {
                     /* connection failed */
                    println!("incoming: have error");
                }
    
                Ok(stream) => spawn(proc()
                                    {
                                        // connection succeeded
                                        handle_client(stream)
                                    })
            }
        }
        // close the socket server
        drop(acceptor);
        return ????;
    }

Note that the function has to have a non-void return type and in this case it has to be

    IoResult<????>

or

    Result<????, IoError>

if you prefer, where ???? is the type of whatever the function returns in the non-error case.

Since there really isn’t anything to return from the function unless something goes wrong the easiest thing to do is reflect that in the return type and declare it to be

    IoResult<()>

i.e., the empty tuple type, so we now have

    fn run(host:&str, port: u16) -> IoResult<()>
    {
        // bind the listener to the specified address
    
        let listener = try!(TcpListener::bind(host, port));
    
        println!("listen: have listener");
    
        let mut acceptor = try!(listener.listen());
    
        println!("listen: have acceptor");
    
        // accept connections and process them, spawning a new tasks for each one
    
        for stream in acceptor.incoming()
        {
            match stream
            {
                Err(e) =>
                {
                    /* connection failed */
                    println!("incoming: have error");
                }
    
                Ok(stream) => spawn(proc()
                                    {
                                        // connection succeeded
                                        handle_client(stream)
                                    })
            }
        }
    
        // close the socket server
        drop(acceptor);
        return ();
    }

which is definitely an improvement upon the deeply nested version above.


Copyright (c) 2014 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 3, 2014

So Swift Then: Tuples – One More Thing

A tuple value is an example of a parenthesized expression the syntax for which is as follows

    parenthesized-expression : '(' expression-element-list? ')'
    
    expression-element-list : expression-element
                            | expression-element ',' expression-element-list
                            
    expression-element      : expression
                            | identifier ':' expression

In the case of a tuple value, if the expression-element is always an expression you get an ordinary tuple,

    ("cod", "dab", "eel")

if the expression-element is always an identifier a colon and an expression you get a tuple with named values,

    (first:"cod", second:"dab", third:"eel")

but the syntax at least does not preclude having a mixture of both.

This would seem to imply that you should be able to do something like this

    let mixedTuple = (first:"cod", "dab", third:"eel")

and interestingly enough a little experimentation reveals that you can do exactly that.

At the moment I can’t think why you might want to do this but if you can think of a reason then currently at least you will be able to.


Copyright (c) 2014 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

June 29, 2014

So Swift Then: Tuples

A Swift tuple is a composite value comprised of zero or more values.

An instance of a tuple is written as a parenthesis delimited list of comma separated values, like so

    (1, 2, 3)

The values in a tuple do not have to be of the same type, for example,

    (true, 2, ("three"))

The mutability of a tuple in Swift is determined by whether it has been assigned to a constant or a variable.

1.0 Types

The type of a tuple is a function of the number of values and their types and is written in the same way as a tuple but with type names rather than values.

1.1 Zero Values

The type of an empty tuple is written as an empty tuple, like so

    let t0 : ()  = ()

In Swift the type Void is simply an alias for the type of an empty tuple, so you can also write

    let tv : Void  = ()

1.2 One Value

The type of a single value tuple is simply the type of the value [1], so this

    let t1 : (String)  = ("malin")

can also be written like this

    let t1 : String  = ("malin")

or like this

    let t1 : (String)  = "malin"

or, if you are feeling really adventurous, like this

    let t1 : String  = "malin"

1.3 More Than One Value

The type of a tuple with more than one value is more straight-forward.

For example, the type of the tuple

    (true, 2, ("three"))

is

    (Bool, Int, (String))

2.0 Value Access

2.1 The Index Notation

The values comprising a tuple can be accessed using an index-like notation like this

    let triple  = (1, 2, 3)

    let first   = triple.0

The same notation can be use to modify a mutable tuple, for example

    var mutableTriple = (1, 2, 3)

    mutableTriple.2 = 5

2.1 Pattern Matching

The values comprising a tuple can also be accessed using pattern matching like this

    let triple    = (1, 2, 3)
    let (x, y, z) = triple

which results in x having the value 1, y having the value 2, and z having the value 3.

Values within a tuple can be ignored in a pattern by using an underscore (‘_‘) like so

    let triple    = (1, 2, 3)
    let (_, y, _) = triple

As before, y has the value 2.

Pattern matching can also be used in an assignment statement like this

    let fishes    = ("cod", "dab", "eel")
    var oneFish   = ""
    var twoFish   = ""
    var threeFish = ""

    (oneFish, twoFish, threeFish) = fishes

    (oneFish, _, _) = fishes

3.0 Assignment

Assignment of a tuple results in a copy being made, so

    let fishes        = ("cod", "dab", "eel")
    var mutableFishes = fishes

    mutableFishes.0 = "saithe"

    println(fishes)
    println(mutableFishes)

prints

    (cod, dab, eel)
    (saithe, dab, eel)

4.0 Named Values

The values in a tuple can also be named, like so

    var fishes = (first:"cod", second:"dab", third:"eel")

They can then be accessed by their names

    let first = fishes.first

and if the tuple is mutable also modified

    fishes.first = "saithe"

It is also possible to use the index notation and pattern matching to access the values in a named tuple but this is not necessarily a good idea for reasons that will become apparent.

4.1 Named Values And Tuple Types

It is not entirely clear what the type of a tuple with named values is.

It is apparent that if declared explicitly it must include the names so if you try this

    let namedFishes: (String, String, String) = (first:"cod", second:"dab", third:"eel")

you get a compiler error.

On the other hand this works

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    let namedFishes: namedFishesType = ("cod", "dab", "eel")

    ...

as does this

     ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    var namedFishes: namedFishesType = (first:"cod", second:"dab", third:"eel")
    
    ...
    
    var unnamedFishes: (String, String, String) = ("flounder", "gudgeon", "hake")

    namedFishes = unnamedFishes

    ...

and this

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    var namedFishes: namedFishesType = (first:"cod", second:"dab", third:"eel")
    
    ...
    
    var unnamedFishes: (String, String, String) = ("flounder", "gudgeon", "hake")
    
    unnamedFishes = namedFishes

so for the purposes of assignment unnamed and named tuple values whose elements are of the same type seem to be interchangeable.

In addition for the purposes of assignment the order of the names in a named tuple value does not seem to be significant, so

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    let namedFishes: namedFishesType = (first:"cod", second:"dab", third:"eel")

    println(namedFishes)

    ...

prints

    (cod, dab, eel)

and so does

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    let namedFishes: namedFishesType = (third:"eel", second:"dab", first:"cod")
    
    println(namedFishes)

    ...

However, just to make things confusing, the order of the names in a named tuple type does seem to be significant, at least for the purposes of using the index notation and pattern matching, so

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    let namedFishes: namedFishesType = (first:"cod", second:"dab", third:"eel")

    println("namedFishes.0 == \(namedFishes.0)")

    let (first, second, third) = namedFishes

    println("\(first) \(second) \(third)")

    ...

prints

    namedFishes.0 == cod
    cod dab eel

but

    ...
    
    typealias reversedFishesType = (third:String, second:String, first:String)
    
    let reversedFishes: reversedFishesType = (first:"cod", second:"dab", third:"eel")

    println("reversedFishes.0 == \(reversedFishes.0)")

    let (first, second, third) = reversedFishes

    println("\(first) \(second) \(third)")

    ...

prints

    reversedFishes.0 == eel
    eel dab cod

which is why you might want to think twice about acessing named values in tuples using the index notation and/or pattern matching.

Finally, as already noted, the types of named values are significant when the names are the same, so you cannot do something like this

    ...
    
    typealias namedFishesType = (first:String, second:String, third:String)
    
    // DOES NOT COMPILE: names match, types do not match
    
    let fishes: namedFishesType = (first:"cod", second:2, third:true)

    ...

as are the names when the types are the same, so you cannot do something like this either

    ...
    
    typealias  namedFishesType = (first:String, second:String, third:String)
    
    // DOES NOT COMPILE: types match, names do not match

    let namedFishes: namedFishesType = (one:"cod", two:"dab", three:"eel")

    ...

Notes

  1. At first glance this seems a trifle odd but there is a possible explanation.

    It may have to do with the function return value type in Swift.

    The clue is the fact that Void is an alias for the type of an empty tuple.


Copyright (c) 2014 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

June 28, 2014

So Swift Then: Optionals Or Wot No nil ?

One of the more interesting features of Objective-C is the ability to send messages to non-existent objects, so

    ...
    
    NSString* path      = nil;
    BOOL      isTxtFile = [[path lastPathComponent] hasSuffix:@".txt"];
    
    if (isTxtFile)
    {
        printf("is text file\n");
    }
    else
    {
        printf("is not text file\n");
    }
    
    ...

prints

    is not text file

Depending upon your language of choice this kind of thing usually results in an NPE or the abrupt termination of your program, but in Objective-C nothing happens.

The reason nothing happens in Objective-C is because the semantics of sending a message to nil are well defined.

If you send a message which returns nothing, i.e., void, nothing happens.

if you send a message which returns a reference to object to nil then you get back nil.

If you send a message which returns something other than an object to nil you get back 0 or its equivalent

In the example above the call to lastPathComponent on nil returns nil.

The call to hasSuffix on nil returns false.

The downside to this is that you can, on occasion, spend quite a bit of time wondering why nothing is happening only to eventually discover that some variable had the value nil when it ought not to have.

The designers of Swift have decided to be helpful and eliminate this downside by making it both impossible to forget to initialize variables and impossible to initialize them to nil in the Objective-C style.

1.0 Constants And Variables

You can declare a constant in Swift like this

    let constantCod: String = "a fish"

Swift supports type inference so you can omit the type if you wish

    let constantCod = "a fish"

As you might expect given that it is a constant you cannot assign another value to constantCod once it has been initialized.

In addition if the constant refers to a compound value, an instance of a struct for example, the value itself is immutable.

You declare a variable in Swift in the same way but using var instead of let.

    var cod: String = "a fish"

and without the type.

    var cod = "a fish"

You can assign another value to a variable in the usual way

    cod = "another fish"

If you are declaring a constant or a global variable you must specify a value in the declaration, but if you are declaring a local variable you can omit the initializer if you wish in which case you must specify the type.

If you declare a local variable without an inititializer you cannot access that variable until it has been explicitly initialized.

So doing this

    var cod: String

    ...

    // compiler error here: Variable 'cod' used before being initialized 
    var uppercaseCod = cod.uppercaseString 
    
    
    println("uppercaseCod == \(uppercaseCod)")

gets you a compiler error, whereas, doing this

    var cod: String

    cod = "a fish"

    var uppercaseCod = cod.uppercaseString

    println("uppercaseCod == \(uppercaseCod)")

prints

    uppercaseCod == A FISH

as you might expect.

2.0 The Optional Type

While this is all very helpful, what if you really, really need to declare a variable which has a value except when it doesn’t ?

The answer is to declare the variable to have the type

    Optional<T>

where T is the type of the variable when it does have a value.

For example

    var optionalCod: Optional<String>

The generic type Optional<T> is defined in the Swift library.

Swift provides syntactic sugar for this in the form of question mark (‘?’) suffix to the type of the variable so you can write the above as

    var optionalCod: String?

3.0 Assignment

You can assign three types of thing to a variable of type Optional<T>

  • a value of type T

  • nil which indicates that the variable has no value, or

  • another value of type Optional<T>

Omitting the initializer when declaring an Optional variable

    var optionalCod: String?

is equivalent to explicitly initializing it to nil

    var optionalCod: String? = nil

If you assign one Optional<T> value to another the value will be copied. so

    var cod : String? = "a fish"
    var dab : String? = cod
    
    println("cod == \(cod)")
    
    cod = nil
    
    println("cod == \(cod)")
    println("dab == \(dab)")

prints

    cod == a fish
    cod == nil
    dab == a fish

4.0 Accessing The Value

Now you have succeeded in declaring a variable which may or may not have a value how do you get at the value, assuming there is one ?

In Swift parlance this is termed unwrapping and there are multiple ways of doing it.

4.1 Forced Unwrapping

If you are absolutely certain that there is a value in the variable you can use forced unwrapping which simply involves adding an exclamation mark (!) as a suffix

    optionalCod!

This is definitely the way to go if you like your programs to terminate abruptly when something goes wrong because if there is no value in the variable that is exactly what happens

So doing this

    var optionalCod: String?

    ...
    
    var cod : String = optionalCod!

gets you this at runtime

    fatal error: Can't unwrap Optional.None

and everything stops.

4.2 Look Before You Leap

Fortunately for those of us of a more nervous disposition it is possible to check whether an Optional variable contains a value before potentially exploding it using the bang operator !

For example

    var optionalCod: String?
    
    ...

    if optionalCod
    {
        println("uppercase optionalCod == \(optionalCod!.uppercaseString)")
    }
    else
    {
        println("optionalCod is nil")
    }

4.3 Optional Binding

Alternatively you can combine the checking and the unwrapping like so

    var optionalCod: String?

    if let cod = optionalCod
    {
        println("uppercase cod == \(cod.uppercaseString)")
    }
    else
    {
        println("optionalCod is nil")
    }

4.4 Optional Chaining

If you are invoking a method on the value in an Optional variable you can add a question mark (‘?’) suffix to the variable and then invoke the method, so given

    var path: String?

you can do

    path?.componentsSeparatedByString("/")

If the variable contains a value then the method will be invoked. If it is nil then nothing happens.

The value of this expression will of necessity be of type Optional<T> where T is the type of the value returned by the method being invoked

    var path: String?

    ...
    
    let parts: Optional<String[]> = path?.componentsSeparatedByString("/")

    ...

Given that the type of the expression is of type Optional<T> you might think that if you want to call a method on the result you need to use the ‘?’ suffix again like so

    path?.componentsSeparatedByString("/")?.reverse()

but if you try this you get a compiler error, specifically

    path?.componentsSeparatedByString("/")?.reverse()
    Operand of postfix '?' should have optional type: type is 'String[]'

which is a bit puzzling at first.

In fact while it is true that in isolation the type of

    path?.componentsSeparatedByString("/")

is

    Optional<String[]>

in the context of a chain of method calls if the Optional variable has no value then the ? operator shortcuts the entire chain and returns immediately.

So at runtime at the point the method reverse is being called the ? operator has already opened the box and found a
String[] value in it rather than nil and it has called componentsSeparatedByString on it and the type of

    path?.componentsSeparatedByString("/")

really is

    String[]

Another way of looking at it is that adding a postfix ? is equivalent to doing this

    let path  : String?
    var result: (String[])?
    
    // ...
    
    if let p : String = path
    {
        let parts   : String[] = p.componentsSeparatedByString("/")
        let reversed: String[] = parts.reverse()
    
        result = reversed
    }
    else
    {
        result = nil
    }

from which it is clear that the result of calling componentsSeparatedByString really is a String[].

Which is all a very long and round about way of you do not actually need the second postfix ? at all. You can simply do this

    path?.componentsSeparatedByString("/").reverse()

You can chain as many methods together as you can find and you still only need the initial ‘?’ assuming of course that none of the methods themselves explicitly return Optional values.

If the last method in the chain returns a value of type Optional<T>, for example

    path?.componentsSeparatedByString("/").reverse()[0].toInt()

where the method toInt returns

    Optional<Int>

you might think that the result of the method chain expression would be

    Optional<Optional<T>>

but it is in fact

    Optional<T>

which has the merit of consistency and simplicity since you only ever have to unwrap the resulting value once

4.5 Implicit Unwrapping

If you declare an Optional variable then, as we have seen, you need to unwrap it one way or another each time you want to use the value.

If you are in a position where you are certain that the Optional variable will definitely have a value each time you use it then you can avoid the need to explicitly unwrap it at each point of use
by declaring it like this

    var optionalDab: String!

adding a postfix exclamation mark (!) to the type rather than a question mark (?).

You can then refer to it directly when using without explicitly unwrapping it.

It is still being unwrapped implicitly using forced unwrapping so if your model of the variable’s usage is incorrect and there is in fact some point at which it can be used before it has been initialized you will find out all about it at runtime.

5.0 Optional Constants

You can, if you want, declare constants with Optional types but after the novelty has worn off they turn out not to be very interesting at all.

Since the only thing you can do with a constant is initialize it or use it you can either have a constant which is guaranteed to be nil

    let constantDab: String?

or one that is guaranteed to have a value

    let constantDab: String? = "an optional fish"

and thats it.

6.0 Optional Globals

You can declare a global variable with an Optional value.

This can be useful if you do not want to or cannot initialize it at the point it is declared but it is guaranteed to have been initialized before it is used.

This case also allows you to take advantage of implicit unwrapping as described above but the caveat about runtime exceptions also applies.

    // declaration
    
    var globalOptionalEel: String!
    
    ...
    
    // initialization
    
    globalOptionalEel = "global eel"
    
    ...
    
    // usage with implict unwrapping plus chance of runtime exception
    
    let constantEel: String = globalOptionalEel

Copyright (c) 2014 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

June 27, 2014

So Swift Then ? Or Wot No Brackets ?

So Swift is essentially Objective-C without the C and without the bracket message sending syntax and with some new stuff and some changed stuff.

Well the C is no loss but the brackets are the best bit so what new stuff do we get to compensate for their disappearance ?

In no particular order

  • tuples

  • a first class enum type

  • a first class struct type

  • operator functions and custom operators

  • range operators

  • subscript operators

  • optional values

  • generics

  • a super switch

  • a plethora of options for parameter lists

and

    much much more

as it always says on the posters.


Copyright (c) 2014 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 22, 2013

Service Discovery In Android And iOS: Part Nine – iOS Take Five

And if none of the previous choices are to your liking then there is also the theoretical possibility of doing the whole thing from scratch as in the Java case.

For this to be feasible it must be possible to send and receive multicast UDP datagrams at the application level. There is nothing in Foundation or Core Foundation that supports this directly but it is possible at the POSIX level.

1.0 Opening A Multicast UDP Socket Using POSIX System Calls

Opening a multicast UDP socket at the POSIX level is feasible if a little bit fiddly as befits something that needs to be done in C/C++.

It requires three system calls.

1.1 socket

First we need to open a socket using the socket system call which is declared in the header file <sys/socket.h>
like this

    int socket(int domain, int type, int protocol);

In our case the domain argument is AF_INET because we want to use the socket to communicate using IP over the network.

The type argument is SOCK_DGRAM because we want to use the socket to send and receive datagrams.

The protocol argument is 0 which means we want to use the default protocol given the socket domain and type which should be UDP.

So our code to open the socket looks like this.

    ...
    
    s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s == -1)
    {
        perror("socket");
        return -1;
    }

    ...

1.2 bind

Having opened the socket we need to associate it with a local address and port using the bind system call which is declared in the header file <sys/socket.h> like this

    int bind(int socket, const struct sockaddr *address, socklen_t address_len);

In our case we need to specify an IP address using an instance of struct sockaddr_in which is defined in the header file
<netinet/in.h> and then cast appropriately.

On the assumption that there is only one network interface so we do not need to be bound to a specific one we specify the address as INADDR_ANY.

As we do not need to send and receive on a specific port we specify the port as 0 so the system will allocate a port number for us.

    ...
    
    struct sockaddr_in inetAddress;
    
    memset(&inetAddress, 0, sizeof(inetAddress));
    
    inetAddress.sin_family      = AF_INET;
    inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    inetAddress.sin_port        = htons(0);
    
    
    if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
    {
        perror("bind");
        close(s);
        return -1;
    }

    ...

Note

The address and port in the sockaddr_in struct should be in network byte order hence the use of htonl and htons.

As it happens in this case it is not necessary to convert either the address or port from host to network byte order but doing so in all cases helps ensure that you don’t forget in a case where it is necessary.

1.3 setsockopt

Having bound the socket to a local address and port number we need to associate it with the multicast group we wish to send to and receive from.

We do this using the setsockopt system call which is defined like this in the header file <sys/socket.h>

    int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

In our case level is IPPROTO_IP as we are setting an IP level option.

The option_name is IP_ADD_MEMBERSHIP indicating that we want to add the socket to the membership of a multicast group.

The option_value is a pointer to an instance of the struct ip_mreq which specifies the group address,
imr_multiaddr, and the network interface, imr_interface.

    ...

    struct ip_mreq imr;
    
    imr.imr_multiaddr.s_addr = group;
    imr.imr_interface.s_addr = htonl(INADDR_ANY);
    
    if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
    {
        perror("setsockopt");
        close(s);
        return -1;
    }
    
    ...

where group is the multicast group address in network byte order.

As before we assume there is only a single network interface and specify INADDR_ANY.

Note

The imr_multiaddr and imr_interface fields are both of type struct in_addr and as in the bind case above the addresses must be in network byte order.

2.0 Making A POSIX Socket More iOS Like

Putting to one side for the moment the issue of whether it is actually possible to open a multicast socket there is the question of how it could be used in the context of an iOS application.

Although a POSIX socket is perfectly functional, receiving datagrams using the recvfrom system call is by default synchronous so using it as is would require at least one separate non-standard thread.

Fortunately the function CFSocketCreateWithNative can turn a theoretical POSIX socket into a theoretical CFSocket.

Using CFSocketCreateWithNative we can specify a function to be called whenever the socket is readable and in return we will get a CFSocketRef

Once we have a theoretical CFSocketRef we can obtain a CFRunLoopSource using the function CFSocketCreateRunLoopSource and then add it to the CFRunLoop of our choice.

The end result will be that our function is called when the socket is readable so recvfrom will not block and this will be done in the context of a standard CFRunLoop.

3.0 But Does It Work ?

Perhaps surprisingly the answer is yes, at least on an iPad running iOS 7.0.4.

DNS messages can be multicast, responses received and services duly discovered.

4.0 The MulticastSocket Class

Here is an initial attempt at encapsulating the multicast socket.

    //
    //  MulticastSocket.m
    //  XperTakeFive
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
    
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    
    #import "MulticastSocket.h"
    
    @interface MulticastSocket()
    
    - (void)read;
    
    @end
    
    @implementation MulticastSocket
    {
        CFSocketRef                 socketRef;
        CFRunLoopSourceRef          runLoopSourceRef;
    
        in_addr_t                   group;
        int                         s;
    
        struct sockaddr_in          from;
        unsigned char               buffer[4096];
    }
    
    static void socketCallback(
                    CFSocketRef          theSocketRef,
                    CFSocketCallBackType theCallbackType,
                    CFDataRef            theAddress,
                    const void*          theData,
                    void*                theInfo)
    {
        switch (theCallbackType)
        {
            case kCFSocketReadCallBack:
    
                [(__bridge MulticastSocket*)theInfo read];
                break;
    
            default:
    
                NSLog(@"socketCallback: %lu !", theCallbackType);
        }
    }
    
    - (id)init:(NSString*)theGroup
    {
        self = [super init];
        if (self != nil)
        {
            group = inet_addr([theGroup UTF8String]);
        }
        return self;
    }
    
    - (void)dealloc
    {
        if (s != -1)
        {
            close(s);
            CFRelease(socketRef);
            CFRelease(runLoopSourceRef);
        }
    }
    
    - (BOOL)open
    {
        s = socket(AF_INET, SOCK_DGRAM, 0);
        if (s == -1)
        {
            perror("socket");
            return NO;
        }
    
        struct sockaddr_in inetAddress;
    
        memset(&inetAddress, 0, sizeof(inetAddress));
    
        inetAddress.sin_family      = AF_INET;
        inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
        inetAddress.sin_port        = htons(0);
    
    
        if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
        {
            perror("bind");
            return NO;
        }
    
        struct ip_mreq imr;
    
        imr.imr_multiaddr.s_addr = group; // in network byte order courtesy of inet_addr
        imr.imr_interface.s_addr = htonl(INADDR_ANY);
    
        if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
        {
            perror("setsockopt");
            return NO;
        }
    
        CFSocketContext context;
    
        memset(&context, 0, sizeof(context));
    
        context.info = (__bridge void*)self;
    
        socketRef = CFSocketCreateWithNative(NULL, s, kCFSocketReadCallBack, socketCallback, &context);
        if (socketRef == NULL)
        {
            return NO;
        }
        runLoopSourceRef = CFSocketCreateRunLoopSource( NULL, socketRef, 0);
        if (runLoopSourceRef == NULL)
        {
            return NO;
        }
        CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSourceRef, kCFRunLoopDefaultMode);
        return YES;
    }
    
    - (void)send:(unsigned char*)theData ofLength:(ssize_t)theLength toPort:(uint16_t)thePort
    {
        struct sockaddr_in inetAddress;
    
        memset(&inetAddress, 0, sizeof(inetAddress));
    
        inetAddress.sin_family      = AF_INET;
        inetAddress.sin_addr.s_addr = group;
        inetAddress.sin_port        = ntohs(thePort);
    
        ssize_t nBytes = sendto(s, theData, theLength, 0, (const struct sockaddr*)&inetAddress, sizeof(inetAddress));
    
        if (nBytes == -1)
        {
            perror("sendto");
        }
    }
    
    // internal
    
    - (void)read
    {
        socklen_t length = sizeof(from);
    
        memset(&from, 0, sizeof(from));
    
        ssize_t nBytes = recvfrom(s, buffer, sizeof(buffer), 0, (struct sockaddr*)&from, &length);
    
        if (nBytes != -1)
        {
            [self.delegate received:buffer ofLength:nBytes from:&from];
        }
        else
        {
            perror("recvfrom");
        }
    }
    
    @end

5.0 And The Rest

The rest is just reading and writing messages which is much the same in Objective-C as it is in Java.

For example, here is the method for reading a message.

    ...
    
    - (BOOL)readRecord:(Category)theCategory
    {
        NSString* name = [self readNodeName];
    
        if (name == nil)
        {
            return NO;
        }
        if ((offset + HEADER_LENGTH) > length)
        {
            return NO;
        }
    
        uint16_t type     = UNSIGNED_SHORT;
        uint16_t class    = UNSIGNED_SHORT;
        uint32_t ttl      = UNSIGNED_INT;
        uint16_t rdlength = UNSIGNED_SHORT;
    
        [responseHandler
             record:
                 [[ResourceRecord alloc]
                       init:
                           name
                       type:
                           type
                       class:
                           class
                       ttl:
                           ttl
                       data:
                           [[RecordData alloc]
                                 init:
                                     message
                                 offset:
                                     offset
                                 length:
                                     offset + rdlength]]
             category:
                 theCategory];
        offset += rdlength;
        return YES;
    }

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 20, 2013

Service Discovery In Android And iOS: Part Eight – iOS Take Four

If DNSServiceBrowse and friends are not to your liking there is always the function DNSServiceQueryRecord which enables us to obtain DNS records directly.

1.0 DNSServiceQueryRecord

The DNSServiceQueryRecord function declaration follows the common function pattern and looks like this

    DNSServiceErrorType DNSServiceQueryRecord(
                            DNSServiceRef*             sdRef,
                            DNSServiceFlags            flags,
                            uint32_t                   interfaceIndex,
                            const char*                fullname,
                            uint16_t                   rrtype,
                            uint16_t                   rrclass,
                            DNSServiceQueryRecordReply callBack,
                            void*                      context);

The fullname should be the absolute name of the node for which the record or records are being requested.

The rrtype should be the type of the record or records being requested.

The rrclass should be the class of the record or records being requested.

If the kDNSServiceFlagsTimeout bit is set in the flags argument then the function will timeout after a system dependent amount of time.

2.0 The DNSServiceQueryRecord Callback Function

The callback function will be invoked

  • once for each record that is received in response to the query

  • once for each record that is received in response to the query that subsequently expires

  • if an error occurs

  • if a timeout occurs

3.0 DNSServiceQueryRecordReply

The DNSServiceQueryRecordReply function type declaration follows the common function type pattern and looks like this

    typedef void (*DNSServiceQueryRecordReply)(
                       DNSServiceRef       sdRef,
                       DNSServiceFlags     flags,
                       uint32_t            interfaceIndex,
                       DNSServiceErrorType errorCode,
                       const char*         fullname,
                       uint16_t            rrtype,
                       uint16_t            rrclass,
                       uint16_t            rdlen,
                       const void*         rdata,
                       uint32_t            ttl,
                       void*               context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • fullname is the absolute name of the node with the record is associated

  • rrtype is the type of the record

  • rrclass is the class of the record.

  • rdlen is the length of the record data

  • rdate is the record data

  • ttl is the time in seconds for which the record is valid

The flags argument will have the kDNSServiceFlagsAdd bit set if the callback is being invoked when a record is received in response to the query.

If kDNSServiceFlagsAdd bit is clear then callback is being invoked because the record has expired,
in which case the ttl argument will be 0.

If a timeout occurs the value of the errorCode argument will be kDNSServiceErr_Timeout.

3.0 The Query Class

We can encapsulate the call to DNSServiceQueryRecord and its associated callback function in a class
like so

    //
    //  Query.m
    //  XperTakeFour
    //
    //  Created by Simon Lewis on 08/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <dns_sd.h>
        
    #import "QueryDelegate.h"
    #import "Record.h"
        
    #import "Query.h"
        
    @interface Query()
        
    @property NSString* name;
    @property uint16_t  type;
    @property uint16_t class;
        
    - (void)record:(const Record*)theRecord onInterface:(uint32_t)theIndex;
        
    - (void)recordExpired:(const Record*)theRecord;
        
    - (void)failed:(DNSServiceErrorType)theName;
        
    @end
        
    @implementation Query
    {
        DNSServiceRef   ref;
    }
        
    - (Query*)init:(NSString*)theName type:(uint16_t)theType class:(uint16_t)theClass
    {
        self = [super init];
        if (self != nil)
        {
            self.name  = theName;
            self.type  = theType;
            self.class = theClass;
        }
        return self;
    }
    
    static void queryCallback(
                    DNSServiceRef       sdRef,
                    DNSServiceFlags     theFlags,
                    uint32_t            theInterfaceIndex,
                    DNSServiceErrorType theErrorCode,
                    const char*         theName,
                    uint16_t            theType,
                    uint16_t            theClass,
                    uint16_t            theDataLength,
                    const void*         theData,
                    uint32_t           theTTL,
                    void*               theContext)
    {
        NSLog(@"queryCallback: flags == %d error code == %d", theFlags, theErrorCode);
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            [(__bridge Query*)theContext failed:theErrorCode];
        }
        else
        {
            NSLog(@"theName == %s theType == %u", theName, theType);
        
            Record rr = {
                            theName,
                            theType,
                            theClass,
                            theTTL,
                            theDataLength,
                            theData
                        };
        
            if ((theFlags & kDNSServiceFlagsAdd) != 0)
            {
                [(__bridge Query*)theContext record:&rr onInterface:theInterfaceIndex];
            }
            else
            {
                [(__bridge Query*)theContext recordExpired:&rr];
            }
        }
    }
        
        
    - (void)start:(DNSServiceRef)theServiceRef interface:(uint32_t)theInterfaceIndex timeout:(BOOL)timeout
    {
        ref = theServiceRef;
        
        DNSServiceErrorType error;
        DNSServiceFlags     flags;
        
        flags = kDNSServiceFlagsShareConnection;
        if (timeout)
        {
            flags |= kDNSServiceFlagsTimeout;
        }
        error = DNSServiceQueryRecord(
                    &ref,
                    flags,
                    theInterfaceIndex,
                    [self.name UTF8String],
                    self.type,
                    self.class,
                    queryCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"DNSServiceQueryRecord: %d", error);
            [self.delegate queryDidFail:self withError:error];
        }
    }
        
    - (void)record:(const Record*)theRecord onInterface:(uint32_t)theIndex
    {
        [self.delegate query:self didGetResponse:theRecord onInterface:theIndex];
    }
        
    - (void)recordExpired:(const Record *)theRecord
    {
        [self.delegate query:self recordDidExpire:theRecord];
    }
        
    - (void)failed:(DNSServiceErrorType)theErrorCode
    {
        if (theErrorCode != kDNSServiceErr_Timeout)
        {
            [self.delegate queryDidFail:self withError:theErrorCode];
        }
        else
        {
            [self.delegate queryDidTimeout:self];
        }
    }
        
    @end

4.0 Using The Query Class

The start method of FindServices v4 starts the search by querying for PTR records associated with the service type node.

    ...
    
    - (BOOL)start
    {
        DNSServiceErrorType error = DNSServiceCreateConnection(&dnsServiceRef);
        
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceCreateConnection %d", error);
            return NO;
        }
        error = DNSServiceSetDispatchQueue(dnsServiceRef, dispatch_get_main_queue());
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceSetDispatchQueue %d", error);
            return NO;
        }
        self.ptrQuery = [[Query alloc] init:self.type type: kDNSServiceType_PTR class:kDNSServiceClass_IN];
        self.ptrQuery.delegate = self;
        [self.ptrQuery start:dnsServiceRef interface:kDNSServiceInterfaceIndexAny timeout:NO];
        return YES;
    }
    
    ...

5.0 Examples

I will forgo the output from the examples as there is only so many console log messages anybody can be expected to find interesting.

Needless to say everything works in pretty much the same way as it did in the three preceding incarnations of the
FindServices class.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 18, 2013

Service Discovery In Android And iOS: Part Seven – iOS Take Three: Down At C Level

For anyone who considers both Foundation and Core Foundation too rarified there is always a C function API for performing DNS/mDNS based service discovery and registration.

This is declared in the header file

    /usr/include/dns_sd.h

1.0 DNSServiceRefs And Connection Sharing

By default each function in the API which requires DNS functionality establishes a separate connection to the DNS service which it returns as a DNSServiceRef.

In this context the name DNSServiceRef can be a bit confusing.

A DNSServiceRef is a reference to the DNS Service, that is, the thing doing the search for services using DNS, not a reference to a service found using DNS.

There is an alternative to having a per function call connection and DNSServiceRef.

A connection to the DNS service can be established separately and then shared by passing it to each function that would otherwise create a new connection.

2.0 The Function Pattern

All the functions in the API which access the DNS service are declared using a common pattern

    DNSServiceErrorType <function-name>(
                            DNSServiceRef*           sdRef,
                            DNSServiceFlags          flags,
                            uint32_t                 interfaceIndex,
        
                            ... <function specific arguments> ...
    
                            <function specific type> callBack,
                            void*                    context);

2.1 The sdRef Argument

    DNSServiceRef*  sdRef

The sdRef argument is either

  • a pointer to an uninitalized DNSServiceRef which will be initialized to a valid DNSServiceRef if the call succeeds, or

  • a pointer to a shared DNSServiceRef which should be used by the function

2.2 The flags argument

    DNSServiceFlags flags

The flags argument unsurprisingly specifies zero or more flags. If an initialized DNSServiceRef is being passed via the sdRef argument then the flag

    kDNSServiceFlagsShareConnection

must be set.

2.3 The interfaceIndex Argument

    uint32_t   interfaceIndex

The index of the network interface to use to perform the requested DNS operation(s).

If the specific network interface is not important then when starting the search for services the constant

    kDNSServiceInterfaceIndexAny

can be used.

When a service is found the index of the network interface it is associated with is reported and subsequent calls can use this value.

2.4 The callBack Argument

The type of the callBack argument is specific to the function to which it is being passed but in each case it specifies the function to be invoked when a a result is available or an error occurs.

2.5 The info Argument

    void*  context

The value of the context argument will be passed as an argument to the callback function specified by
the callBack argument.

3.0 The Callback Function Pattern

All the callback function types are declared using a common pattern.

    typedef void (*<type-name>)(
                        DNSServiceRef       sdRef,
                        DNSServiceFlags     flags,
                        uint32_t            interfaceIndex,
                        DNSServiceErrorType errorCode,
    
                        ... <function specific arguments> ...

                        void*               context);

3.1 The sdRef Argument

The DNSServiceRef which was passed to the function which invoked this callback.

3.2 The flags Argument

The flags argument is used to pass general status information, e.g., if the

    kDNSServiceFlagsMoreComing

flag is set then this callback will be invoked again.

3.3 The interfaceIndex Argument

The index of the network interface on which the result was obtained.

3.4 The errorCode Argument

If the errorCode argument is not

    kDNSServiceErr_NoError

then an error has occurred.

3.5 The context Argument

The value of the context argument passed to the function which invoked this callback.

4.0 Searching For Services

4.1 DNSServiceBrowse

We can start the search for services of a given type by using the function DNSServiceBrowse
which is declared like this

    DNSServiceErrorType DNSServiceBrowse(
                            DNSServiceRef*        sdRef,
                            DNSServiceFlags       flags,
                            uint32_t              interfaceIndex,
                            const char*           regtype,
                            const char*           domain,
                            DNSServiceBrowseReply callBack,
                            void*                 context);

The regtype argument should be the domain relative type name, e.g.,

    "_ipp.tcp."

The domain argument should be the absolute name of the domain to search in, e.g.,

    "local."

4.2 The DNSServiceBrowse Callback Function

The function passed as the callBack argument to DNSServiceBrowse will be called once for each service of the given type that is found.

If the information about a service that was found becomes invalid, implying that it has ‘disappeared’, then the callback function will called again.’

4.3 The DNSServiceBrowseReply Function Type

The function type DNSServiceBrowseReply is declared like this

    typedef void (*DNSServiceBrowseReply)(
                       DNSServiceRef       sdRef,
                       DNSServiceFlags     flags,
                       uint32_t            interfaceIndex,
                       DNSServiceErrorType errorCode,
                       const char*         serviceName,
                       const char*         regtype,
                       const char*         replyDomain,
                       void*               context);

When a function of this type is invoked, then if the errorCode argument is kDNSServiceErr_NoError

  • serviceName is the type relative name of the service.

  • regtype is the domain relative name of the service type

  • replyDomainis the absolute name of the domain the service is registered in

The kDNSServiceFlagsAdd flag will be set in the flags argument if the service has been found, and clear if the service has been ‘lost’.

5.0 Resolving Services

Resolving a service involves two functions

    DNSServiceResolve

which obtains the service’s SRV and TXT records, and

and

    DNSServiceGetAddrInfo.

which obtains the address or addresses of the host on which the service is running

5.1 DNSServiceResolve

We can obtain the information contained in the SRV and TXT records associated with a given service by using the function
DNSServiceResolve which is declared like this

    DNSServiceErrorType DNSServiceResolve(
                            DNSServiceRef*         sdRef,
                            DNSServiceFlags        flags,
                            uint32_t               interfaceIndex,
                            const char*            name,
                            const char*            regtype,
                            const char*            domain,
                            DNSServiceResolveReply callBack,
                            void*                  context);

The name argument should be the type relative name of the service.

The regtype argument should be the domain relative name of the service type.

The domain argument should be the absolute name of the domain in which the service is registered.

5.1.1 The DNSServiceResolve Callback Function

The function passed as the callBack argument to DNSServiceResolve will be called once, either with the results of with an error.

5.1.1 The DNSServiceResolveReply Function Type

The function type DNSServiceResolveReply is declared like this

    typedef void (*DNSServiceResolveReply)(
                       DNSServiceRef        sdRef,
                       DNSServiceFlags      flags,
                       uint32_t             interfaceIndex,
                       DNSServiceErrorType  errorCode,
                       const char*          fullname,
                       const char*          hosttarget,
                       uint16_t             port,
                       uint16_t             txtLen,
                       const unsigned char* txtRecord,
                       void*                context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • fullname is the absolute name of the service, e.g., "ipp_server_1._ipp._tcp.local.".

  • hosttarget is the name of the host the service is running on.

  • port is the port, in network byte order, the service is listening on.

  • txtLen is is the length of the TXT record data.

  • txtRecord is a pointer to the TXT record data itself.

5.2 DNSServiceGetAddrInfo

We can obtain the address of a host using the function DNSServiceGetAddrInfo which is declared like this

    DNSServiceErrorType DNSServiceGetAddrInfo(
                            DNSServiceRef*             sdRef,
                            DNSServiceFlags            flags,
                            uint32_t                   interfaceIndex,
                            DNSServiceProtocol         protocol,
                            const char*                hostname,
                            DNSServiceGetAddrInfoReply callBack,
                            void*                      context);

If the

    kDNSServiceFlagsTimeout

is set in the flags argument then the operation may timeout after a system defined amount of time.

The protocol argument specifies the type of the address requested.

The value should be one of

  • 0

  • kDNSServiceProtocol_IPv4

  • kDNSServiceProtocol_IPv6

  • kDNSServiceProtocol_IPv4|kDNSServiceProtocol_IPv6

A value of 0 (zero) is usually equivalent to requesting both the IPv4 and IPv6 addresses.

The hostname argument should be the absolute name of the host.

5.2.1 The DNSServiceGetAddrInfo Callback Function

The function passed as the callBack argument to DNSServiceGetAddrInfo will be called once for each address type that was requested and is found

It will also be called if the address of the host becomes invalid, e.g., because the host has been turned off.

5.2.2 The DNSServiceGetAddrInfoReply Function Type

The function type DNSServiceGetAddrInfoReply is declared like this

    typedef void (*DNSServiceGetAddrInfoReply)(
                       DNSServiceRef          sdRef,
                       DNSServiceFlags        flags,
                       uint32_t               interfaceIndex,
                       DNSServiceErrorType    errorCode,
                       const char*            hostname,
                       const struct sockaddr* address,
                       uint32_t               ttl,
                       void*                  context);

When a function of this type is invoked, then, if the errorCode argument is kDNSServiceErr_NoError

  • hostname is the name of the host whose address this is

  • address is a pointer to its address, and

  • ttl is the time in seconds for which the given address is valid

The kDNSServiceFlagsAdd flag will be set in the flags argument if the address has been ‘found’, and clear if the address is no longer valid.

The kDNSServiceFlagsMoreComing will be set in the flags argument if there are more addresses, and will be clear if this is the last address.

The type of the address will of course depend upon what what specified as the protocol argument in the call to DNSServiceGetAddrInfo.

If both IPv4 and IPv6 addresses were requested then it will be necessary to examine the sa_family field of the sockaddr struct to find out which one it is.

6.0 The Care And Maintenance Of Your DNSServiceRef

Something that is not perhaps immediately apparent is that the shared DNSServiceRef or one created by a function like DNSServiceBrowse has to be actively handled on the client side.

There are two ways to do this, either

  • by obtaining the file descriptor associated with the connection by calling DNSServiceRefSockFD and doing it all yourself, or

  • by calling the function DNSServiceSetDispatchQueue which will result in the connection being handled ‘automatically’ on the dispatch queue of your choice.

6.1 DNSServiceRefSockFD

The function DNSServiceRefSockFD is declared like this

    int DNSSD_API DNSServiceRefSockFD(DNSServiceRef sdRef);

It takes a DNSServiceRef and returns the file descriptor of the underlying connection to the DNS service.

Once you have obtained your file descriptor you will need to determine when it is readable.

To do this you are going to need either

  • an fd_set and a system call, or

  • a pollfd struct and a different system call

6.1.1 Using select

To use the select system call and assuming sr is the DNSServiceRef you will need to do something like this.

    ...
    
    int fd = DNSServiceRefSockFD(sr);
    
    if (fd == -1)
    {
        fprintf(stderr, "fd == -1 !");
        return;
    }
    
    fd_set         readFDs;
    struct timeval tv;
    
    while (true)
    {
        FD_ZERO(&readFDs);
        FD_SET(fd, &readFDs);
    
        tv.tv_sec  = 1000000;
        tv.tv_usec = 0;
    
        int status = select(fd + 1, &readFDs, NULL, NULL, &tv);
    
        if (status == -1)
        {
            fprintf(stderr, "status == -1\n");
            break;
        }
        else
        if (status == 0)
        {
            fprintf(stderr, "status == 0\n");
        }
        else
        if (FD_ISSET(fd, &readFDs))
        {
            int error = DNSServiceProcessResult(sr);
    
            if (error != kDNSServiceErr_NoError)
            {
                fprintf(stderr, "DNSServiceProcessResult: error == %d\n", error);
                break;
            }
        }
    }

    ...
    

When the file descriptor is readable the function DNSServiceProcessResult is invoked to handle the input. It is this call that results in callback functions being invoked.

This assumes that you are sharing a single DNSServiceRef if not then you are going to end up knee deep in file descriptors and its all going to get very messy very fast.

6.1.2 Using poll

If the use of select is too retro for you you can always use the new-fangled poll system call.

The code looks very similar because poll is just select with unlimited [1] file descriptors.

    ...

    int fd = DNSServiceRefSockFD(sr);
    
    if (fd == -1)
    {
        NSLog(@"fd == -1 !");
        return;
    }
    
    struct pollfd   pollFD;
    
    while (true)
    {
        pollFD.fd = fd;
        pollFD.events = POLL_IN;
        
        int status = poll(&pollFD, 1 , 1000000);
    
        if (status == -1)
        {
            fprintf(stderr, "status == -1\n");
            break;
        }
        else
        if (status == 0)
        {
            fprintf(stderr, "status == 0\n");
        }
        else
        if ((pollFD.revents & POLL_IN) != 0)
        {
            int error = DNSServiceProcessResult(sr);

            if (error != kDNSServiceErr_NoError)
            {
                fprintf(stderr, "DNSServiceProcessResult: error == %d\n", error);
                break;
            }
        }
    }

    ...

6.2 DNSServiceSetDispatchQueue

The alternative to wrestling with file descriptors is the function DNSServiceSetDispatchQueue
which is declared like this

    DNSServiceErrorType DNSServiceSetDispatchQueue(
                            DNSServiceRef    service,
                            dispatch_queue_t queue);

The DNSServiceRef can be associated with a shared connection or with a per function connection.

See below for an example of its use.

7.0 Creating A DNSServiceRef For A Shared Connection

The only way to create a DNSServiceRef for a connection which can be shared is by using the function
DNSServiceCreateConnection which is declared like this

   DNSServiceErrorType DNSServiceCreateConnection(DNSServiceRef* sdRef);

A copy of the initialized DNSServiceRef that results should be passed to each function that is going to share the connection.

See below for an example of its use.

8.0 DNSServiceBrowse In Action

This is the start method of the third version of the FindServices class.

    - (BOOL)start
    {
        DNSServiceErrorType error = DNSServiceCreateConnection(&dnsServiceRef);
    
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceCreateConnection %d", error);
            return NO;
        }
        error = DNSServiceSetDispatchQueue(dnsServiceRef, dispatch_get_main_queue());
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceSetDispatchQueue %d", error);
            return NO;
        }
        browseRef = dnsServiceRef;
        error = DNSServiceBrowse(
                    &browseRef,
                    kDNSServiceFlagsShareConnection,
                    kDNSServiceInterfaceIndexAny,
                    [self.type UTF8String],
                    [self.domain UTF8String],
                    browseCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"Error: DNSServiceBrowse %d", error);
            return NO;
        }
        return YES;
    }
    static void browseCallback(
                    DNSServiceRef       sdRef,
                    DNSServiceFlags     theFlags,
                    uint32_t            theInterfaceIndex,
                    DNSServiceErrorType theErrorCode,
                    const char*         theName,
                    const char*         theType,
                    const char*         theDomain,
                    void*               theContext)
    {
        NSLog(@"browseCallback:  error == %d flags == %s", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode == kDNSServiceErr_NoError)
        {
            ServiceIdentifier si = { theName, theType, theDomain };
        
            if ((theFlags & kDNSServiceFlagsAdd) != 0)
            {
                [(__bridge FindServices*)theContext serviceFound:&si onInterface:theInterfaceIndex];
            }
            else
            {
                [(__bridge FindServices*)theContext serviceLost:&si];
            }
        }
        else
        {
            [(__bridge FindServices*)theContext browseFailed:theErrorCode];
        }
    }

9.0 DNSServiceResolve In Action

This is the resolve:onInterface: method of the ServiceResolver class

    - (void)resolve:(ServiceIdentifier*)theServiceId onInterface:(uint32_t)theInterfaceIndex
    {
        DNSServiceErrorType error;
        
        error = DNSServiceResolve(
                    &resolveRef,
                    kDNSServiceFlagsShareConnection,
                    theInterfaceIndex,
                    theServiceId->name,
                    theServiceId->type,
                    theServiceId->domain,
                    resolveCallback,
                    (__bridge void*)self);
        if (error != kDNSServiceErr_NoError)
        {
            NSLog(@"DNSServiceResolve: %d", error);
            [self.delegate serviceResolver:self didFail:error];
        }
    }

and this is the associated callback function.

    static void resolveCallback(
                    DNSServiceRef        theRef,
                    DNSServiceFlags      theFlags,
                    uint32_t             theInterfaceIndex,
                    DNSServiceErrorType  theErrorCode,
                    const char*          theFullName,
                    const char*          theTarget,
                    uint16_t             thePort,
                    uint16_t             theTXTRecordLength,
                    const unsigned char* theTXTRecord,
                    void*                theContext)
    {
        NSLog(@"resolveCallback: error == %d flags == %s", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            NSLog(@"resolveCallback: error !");
            [(__bridge ServiceResolver*)theContext resolveFailed:theErrorCode];
        }
        else
        if (theFlags == 0)
        {
            ServiceInfo si =
                {
                    theFullName,
                    theTarget,
                    ntohs(thePort),
                    theTXTRecordLength,
                    theTXTRecord
                };
        
            NSLog(@"%s %s %u", theFullName, theTarget, thePort);
        
            [(__bridge ServiceResolver*)theContext resolved:&si onInterface:theInterfaceIndex];
        }
        else
        {
            NSLog(@"resolveCallback: flags set !");
            [(__bridge ServiceResolver*)theContext internalError];
        }
    }

10.0 DNSServiceGetAddrInfo In Action

This is the resolved:onInterface: method of the ServiceResolver class

    - (void)resolved:(const ServiceInfo*)theServiceInfo onInterface:(uint32_t)theInterfaceIndex
    {
        DNSServiceErrorType error;
        
        error = DNSServiceGetAddrInfo(
                    &addressRef,
                    kDNSServiceFlagsShareConnection,
                    theInterfaceIndex,
                    kDNSServiceProtocol_IPv4|kDNSServiceProtocol_IPv6,
                    theServiceInfo->target,
                    addressInfoCallback,
                    (__bridge void*)self);
        if (error == kDNSServiceErr_NoError)
        {
            [self.builder serviceInfo:theServiceInfo];
        }
        else
        {
            NSLog(@"DNSServiceGetAddrInfo: %d", error);
            [self.delegate serviceResolver:self didFail:error];
        }
    }

and this is the associated callback function.

    static void addressInfoCallback(
                    DNSServiceRef          theServiceRef,
                    DNSServiceFlags        theFlags,
                    uint32_t               theInterfaceIndex,
                    DNSServiceErrorType    theErrorCode,
                    const char*            theHostname,
                    const struct sockaddr* theAddress,
                    uint32_t               theTTL,
                    void*                  theContext)
    {
        NSLog(@"addressInfoCallback: error == %d flags == %s ", theErrorCode, flagsToString(theFlags));
        
        if (theErrorCode != kDNSServiceErr_NoError)
        {
            NSLog(@"addressInfoCallback error");
            [(__bridge ServiceResolver*)theContext getAddrInfoFailed:theErrorCode];
        }
        else
        if ((theFlags & kDNSServiceFlagsAdd) != 0)
        {
            NSLog(@"theHostname == %s", theHostname);
            NSLog(@"theAddress->sa_family == %d", theAddress->sa_family);
        
            [(__bridge ServiceResolver*)theContext address:theAddress];
            if ((theFlags & kDNSServiceFlagsMoreComing) == 0)
            {
                [(__bridge ServiceResolver*)theContext done];
            }
        }
        else
        {
            NSLog(@"theHostname == %s", theHostname);
            NSLog(@"theAddress->sa_family == %d", theAddress->sa_family);
            // ignore
        }
    }

11.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1
running on a Mac and then being stopped.

In this case the addressInfoCallback function is called twice, first with the IPv6 address then with the IPv4 address.

    ...
        
    2013-11-18 14:53:20.304 XperTakeThree[334:60b] browseCallback:  error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:53:20.307 XperTakeThree[334:60b] serviceFound: ipp_server_1._ipp._tcp.local.
    2013-11-18 14:53:20.309 XperTakeThree[334:60b] resolveCallback: error == 0 flags == <none>
    2013-11-18 14:53:20.310 XperTakeThree[334:60b] ipp_server_1._ipp._tcp.local. Simons-Computer.local. 56088
    2013-11-18 14:53:20.312 XperTakeThree[334:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlags{Add,MoreComing}
    2013-11-18 14:53:20.313 XperTakeThree[334:60b] theHostname == Simons-Computer.local.
    2013-11-18 14:53:20.314 XperTakeThree[334:60b] theAddress->sa_family == 30
    2013-11-18 14:53:20.315 XperTakeThree[334:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:53:20.316 XperTakeThree[334:60b] theHostname == Simons-Computer.local.
    2013-11-18 14:53:20.317 XperTakeThree[334:60b] theAddress->sa_family == 2
    2013-11-18 14:54:18.545 XperTakeThree[334:60b] browseCallback:  error == 0 flags == <none>
    2013-11-18 14:54:18.547 XperTakeThree[334:60b] serviceLost: name == ipp_server_1._ipp._tcp.local.
        
    ...

11.2 A Single Printer

A printer being turned on and then turned off a couple of minutes later.

In this case we only get a single address, the IPV4 one, but we do get a second call to the function addressInfoCallback
function when the printer is turned off

    ...
        
    2013-11-18 14:55:55.137 XperFS_DNS_SD[351:60b] browseCallback:  error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:55:55.140 XperFS_DNS_SD[351:60b] serviceFound: Canon MG6200 series._ipp._tcp.local.
    2013-11-18 14:55:55.141 XperFS_DNS_SD[351:60b] resolveCallback: error == 0 flags == <none>
    2013-11-18 14:55:55.142 XperFS_DNS_SD[351:60b] Canon32MG620032series._ipp._tcp.local. 7D300C000000.local. 30466
    2013-11-18 14:55:55.144 XperFS_DNS_SD[351:60b] addressInfoCallback: error == 0 flags == kDNSServiceFlagsAdd
    2013-11-18 14:55:55.145 XperFS_DNS_SD[351:60b] theHostname == 7D300C000000.local.
    2013-11-18 14:55:55.146 XperFS_DNS_SD[351:60b] theAddress->sa_family == 2
    2013-11-18 15:02:18.835 XperFS_DNS_SD[351:60b] addressInfoCallback: error == 0 flags == <none>
    2013-11-18 15:02:18.837 XperFS_DNS_SD[351:60b] theHostname == 7D300C000000.local.
    2013-11-18 15:02:18.839 XperFS_DNS_SD[351:60b] theAddress->sa_family == 2
    2013-11-18 15:02:19.936 XperFS_DNS_SD[351:60b] browseCallback:  error == 0 flags == <none>
    2013-11-18 15:02:19.938 XperFS_DNS_SD[351:60b] serviceLost: name == Canon MG6200 series._ipp._tcp.local.

    ...

Notes

  • Subject to terms and conditions. The number of file descriptors may be subject to limits.

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 14, 2013

Service Discovery In Android And iOS: Part Six – iOS Take Two

At the Core Foundation level we have the CFNetServices API which supports the registration and discovery of services using DNS/mDNS.

1.0 CFNetServiceBrowser

A CFNetServiceBrowser is the CFNetServices equivalent of an NSNetServiceBrowser.

2.0 CFNetService

A CFNetService is the CFNetServices equivalent of an NSNetService.

3.0 Synchronous vs. Aynchronous

Unlike the NSNetServiceBrowser and NSNetService methods the equivalent CFNetServiceBrowser and CFNetService functions can be used synchronously or asynchronously.

By default the functions are synchronous.

To make them asynchronous for a given CFNetServiceBrowser or CFNetService it must be added to a CFRunLoop.

4.0 Searching For Services Asynchronously

To search for services of a given type using the CFNetworkServices API we need to

  1. create a CFNetServiceBrowser

  2. make it asynchronous

  3. start the search

4.1 CFNetServiceBrowserCreate

We can create a CFNetServiceBrowser using the function CFNetServiceBrowserCreate which is declared like this

    CFNetServiceBrowserRef CFNetServiceBrowserCreate (
                               CFAllocatorRef                    alloc,
                               CFNetServiceBrowserClientCallBack clientCB,
                               CFNetServiceClientContext*        clientContext);

The alloc argument is a reference to an allocator to use when creating the CFNetServiceBrowser or more likely the constant

    kCFAllocatorDefault

4.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceBrowserClientCallBack.

This function will be invoked when a service is found or if an error occurs.

The function type CFNetServiceBrowserClientCallBack is declared like this

    typedef void (*CFNetServiceBrowserClientCallBack) (
                       CFNetServiceBrowserRef browser,
                       CFOptionFlags          flags,
                       CFTypeRef              domainOrService,
                       CFStreamError*         error,
                       void*                  info);;

The browser argument is the CFNetServiceBrowserRef that was returned from the call to CFNetServiceBrowserCreate.

The callback function is also invoked when searching for domains so the flags argument is used to distinguish between the two uses.

It is also used to distinguish between the discovery of a service and the disappearance of a previously discovered service.

When a service is discovered the value of the flags argument will be zero (0).

When a previously discovered service disappears the value of the flags argument will be

   kCFNetServiceFlagRemove

When being invoked on the discovery or disappearance of a service the domainOrService argument will be a
CFNetServiceRef.

If an error has occurred, the error and domain fields of error argument will be set.

The info argument is the value of the info field of the CFNetServiceClientContext passed to the call to CFNetServiceBrowserCreate.

4.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which is declared like this.

    struct CFNetServiceClientContext {
        CFIndex                            version;
        void*                              info;
        CFAllocatorRetainCallBack          retain;
        CFAllocatorReleaseCallBack         release;
        CFAllocatorCopyDescriptionCallBack copyDescription;
    };
        
    typedef struct CFNetServiceClientContext CFNetServiceClientContext;

The info field can be used to store a pointer to an ‘object’ which will be passed to the callback function passed via the clientCB argument.

If the retain and release fields are not NULL then the functions specified will be invoked when the implementation wishes to retain and release the ‘object’ specified in the info field.

It is not clear from the documentation whether the contents of the CFNetServiceClientContext are copied.

A const qualifier is sometimes a clue, but a little experimentation shows that they are, so it is safe to stack allocate.

4.2 CFNetServiceBrowserScheduleWithRunLoop

We can add our newly created CFNetServiceBrowser to a CFRunLoop using the function CFNetServiceBrowserScheduleWithRunLoop which is declared like this

    void CFNetServiceBrowserScheduleWithRunLoop(
             CFNetServiceBrowserRef browser,
             CFRunLoopRef           runLoop,
             CFStringRef            runLoopMode);

The easiest way to use this is to add the CFNetServiceBrowser to the main CFRunLoop in the default mode like so

    CFNetServiceBrowserScheduleWithRunLoop(
        browser, 
        CFRunLoopGetMain(), 
        kCFRunLoopDefaultMode);

4.3 CFNetServiceBrowserSearchForServices

Once we have made our CFNetServiceBrowse asynchronous we can start the search for services by calling the function CFNetServiceBrowserSearchForServices which is declared like this

    Boolean CFNetServiceBrowserSearchForServices (
                CFNetServiceBrowserRef browser,
                CFStringRef            domain,
                CFStringRef            serviceType,
                CFStreamError*         error);

The browser argument should be the CFNetServiceBrowserRef returned from the call to CFNetServiceBrowserCreate.

The domain argument should be the absolute name of the domain in which to search, e.g.,

    "local."

The serviceType argument should be the domain relative type of the services to search for, e.g.,

    "_ipp._tcp."

The documentation for the error argument is a tad confusing.

It states

A pointer to a CFStreamError structure, that, if an error occurs, will be set to the error and the error’s domain and passed to your callback function.

The

… and passed to your callback

bit does not appear to be true.

If it is not possible to start the search then the function returns false immediately and the domain and error fields of the CFStreamError are set.

5.0 Resolving A Service Asynchronously

Once a service has been found we need to resolve it …

To do this asynchronously we need to

  1. make it possible to get the result asynchronously

  2. make the CFNetService asynchronous

  3. start the resolution

5.1 CFNetServiceSetClient

To get the results from CFNetService functions when running asynchronously we must associate a callback function and a context with the CFNetService first.

We do this using the function CFNetServiceSetClient which is declared like this

    Boolean CFNetServiceSetClient(
                CFNetServiceRef            theService,
                CFNetServiceClientCallBack clientCB,
                CFNetServiceClientContext* clientContext);

5.1.1 The clientCB Argument

The clientCB argument is a pointer to a function of type CFNetServiceClientCallBack

This function will be invoked when the service is resolved or an error occurs.

The function type CFNetServiceClientCallBack is declared like this

    typedef void (*CFNetServiceClientCallBack) (
                      CFNetServiceRef theService,
                      CFStreamError*  error,
                      void*           info);

The info argument is the value of the info field of the CFNetServiceClientContext passed as the clientContext argument in the call to the CFNetServiceSetClient

If an error has occurred, the error and domain fields of error argument will be set.

5.1.2 The clientContext Argument

The clientContext argument is a pointer to a CFNetServiceClientContext which we have already seen used with the function CFNetServiceBrowserCreate.

5.2 CFNetServiceScheduleWithRunLoop

We can add our CFNetService instance to a CFRunLoop using the function CFNetServiceScheduleWithRunLoop which is declared like this.

    void CFNetServiceScheduleWithRunLoop(
             CFNetServiceRef theService,
             CFRunLoopRef    runLoop,
             CFStringRef     runLoopMode);

The easiest way to use this is to add the CFNetService to the main CFRunLoop in the default mode like so

    CFNetServiceScheduleWithRunLoop(service, CFRunLoopGetMain(), kCFRunLoopDefaultMode);

5.3 CFNetServiceResolveWithTimeout

Once we have made our CFNetService asynchronous we can start the resolution bey calling the function
CFNetServiceResolveWithTimeout which is declared like this

    Boolean CFNetServiceResolveWithTimeout(
                CFNetServiceRef theService,
                CFTimeInterval  timeout,
                CFStreamError*  error);

The timeout argument specifies the amount of time in seconds that the implementation should wait for the resolution to complete If the value is less than or equal to zero the implementation will wait indefinitely.

If it not possible to start the resolution the function returns false immediately and the domain and error fields of the CFStreamError argument are set.

6.0 The FindServices Class

Here is the FindServices class re-written to use the CFNetServices API.

    //
    //  FindServices.m
    //  XperTakeTwo
    //
    //  Created by Simon Lewis on 12/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
    
    #import "Service.h"
    
    #import "FindServices.h"
    
    @interface FindServices ()
    
    @property NSString*             type;
    @property NSString*             domain;
    
    @property NSMutableDictionary*  services;
    
    
    - (void)serviceFound:(CFNetServiceRef)theService;
    
    - (void)serviceLost:(CFNetServiceRef)theService;
    
    
    - (void)resolved:(CFNetServiceRef)theService;
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError*)theError;
    
    
    - (void)stopBrowser;
    
    - (void)stopService:(CFNetServiceRef)theService;
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService;
    
    @end
    
    @implementation FindServices
    {
        CFNetServiceBrowserRef      browser;
    }
    
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain
    {
        self = [super init];
        if (self != nil)
        {
            self.type     = theType;
            self.domain   = theDomain;
            self.services = [NSMutableDictionary dictionaryWithCapacity:8];
        }
        return self;
    }
    
    
    
    static void browserCallBack(
                    CFNetServiceBrowserRef  theBrowser,
                    CFOptionFlags           theFlags,
                    CFTypeRef               theDomainOrService,
                    CFStreamError*          theError,
                    void*                   theInfo)
    {
        NSLog(@"browserCallBack");
    
        if ((theError->error) != 0)
        {
            NSLog(@"error: %d\n", (int)theError->error);
        }
        else
        if ((theFlags & kCFNetServiceFlagIsDomain) != 0)
        {
            NSLog(@"domain !\n");
        }
        else // service
        if ((theFlags & kCFNetServiceFlagRemove) == 0)
        {
            [(__bridge FindServices*)theInfo serviceFound:(CFNetServiceRef)theDomainOrService];
        }
        else
        {
            [(__bridge FindServices*)theInfo serviceLost:(CFNetServiceRef)theDomainOrService];
        }
    }
    
    - (void)start
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        browser = CFNetServiceBrowserCreate(kCFAllocatorDefault, browserCallBack, &context);
    
        CFNetServiceBrowserScheduleWithRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceBrowserSearchForServices(
                     browser,
                     (__bridge CFStringRef)self.domain,
                     (__bridge CFStringRef)self.type,
                     &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
            [self stopBrowser];
        }
    }

    static void resolveCallBack(
                    CFNetServiceRef theService,
                    CFStreamError*  theError,
                    void*           theInfo)
    {
        NSLog(@"resolveCallback");

        if (theError->error == 0)
        {
            [(__bridge FindServices*)theInfo resolved:theService];
        }
        else
        {
            [(__bridge FindServices*)theInfo resolveFailed:theService withError:theError];
        }
    }
    
    - (void)serviceFound:(CFNetServiceRef)theService
    {
        [self log:@"service found: %@.%@%@" service:theService];
    
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
        context.info = (__bridge void *)(self);
    
        CFNetServiceSetClient(theService, resolveCallBack, &context);
    
        CFNetServiceScheduleWithRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    
        Boolean       status;
        CFStreamError error;
    
        status = CFNetServiceResolveWithTimeout(theService, 0.0, &error);
        if (status == 0)
        {
            NSLog(@"error.error == %d\n", (int)error.error);
    
           [self stopService:theService];
        }
    }
    
    - (void)serviceLost:(CFNetServiceRef)theService
    {
        [self log: @"service lost: %@.%@%@" service:theService];
    
        NSString* name   = [NSString stringWithFormat:
                                @"%@.%@%@",
                                CFNetServiceGetName(
                                    theService),
                                CFNetServiceGetType(
                                    theService),
                                CFNetServiceGetDomain(
                                    theService)];
        Service* service = [self.services objectForKey:name];
    
        if (service != nil)
        {
            [service lost];
            [self.services removeObjectForKey:name];
            [self.delegate findServices:self didLoseService:service];
        }
        else
        {
            [self log: @"service lost but was not found: %@.%@%@" service:theService];
        }
    }
    
    - (void)resolved:(CFNetServiceRef)theService
    {
        [self log:@"service resolved: %@.%@%@" service:theService];
    
        CFArrayRef addresses = CFNetServiceGetAddressing(theService);
    
        if (CFArrayGetCount(addresses) != 0)
        {
            Service* service = [[Service alloc] init:theService];
    
            [self.services setObject:service forKey:service.name];
            [self.delegate findServices:self didFindService:service];
        }
        else
        {
            NSLog(@"service has 0 addresses: lost ?");
        }
    }
    
    - (void)resolveFailed:(CFNetServiceRef)theService withError:(CFStreamError *)theError
    {
        [self log:@"service resolvedFailed: %@.%@%@" service:theService];
        [self stopService:theService];
    }
    
    - (void)stopBrowser
    {
        CFNetServiceBrowserUnscheduleFromRunLoop(browser, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceBrowserInvalidate(browser);
        CFNetServiceBrowserStopSearch(browser, NULL);
    }
    
    - (void)stopService:(CFNetServiceRef)theService
    {
        CFNetServiceClientContext   context;
    
        memset(&context, 0, sizeof(context));
    
        CFNetServiceUnscheduleFromRunLoop(theService, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
        CFNetServiceSetClient(theService, NULL, &context);
        CFNetServiceCancel(theService);
    }
    
    - (void)log:(NSString*)theMessage service:(CFNetServiceRef)theService
    {
        NSLog(
            theMessage,
            CFNetServiceGetName(
                theService),
            CFNetServiceGetType(
                theService),
           CFNetServiceGetDomain(
               theService));
    }
    
    @end

7.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1 running on a Mac and then being stopped.

    ...

    2013-11-14 14:07:07.561 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:07.564 XperTakeTwo[665:60b] service found: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.593 XperTakeTwo[665:60b] resolveCallback
    2013-11-14 14:07:07.595 XperTakeTwo[665:60b] service resolved: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:07.597 XperTakeTwo[665:60b] findServices:didFindService:<Service: 0x14e86990>
    2013-11-14 14:07:13.931 XperTakeTwo[665:60b] browserCallBack
    2013-11-14 14:07:13.933 XperTakeTwo[665:60b] service lost: ipp_server_1._ipp._tcp.local.
    2013-11-14 14:07:13.935 XperTakeTwo[665:60b] findServices:didLoseService::<Service: 0x14e86990>

    ...

7.2 A Single Printer

A printer being turned on and then turned a couple of minutes later.

Note the lines shown in bold for emphasis.

When the printer is turned off the resolveCallback function is being called for a second time but this time the CFNetService has no addresses.

This is the same thing that happened in this case when using the NSNetServiceBrowser/NSNetService API. (see here)

    ...

    2013-11-14 14:07:55.177 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:07:55.180 XperTakeTwo[671:60b] service found: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.250 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:07:55.252 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:07:55.255 XperTakeTwo[671:60b] findServices:didFindService:<Service: 0x1452e060>

    2013-11-14 14:09:05.838 XperTakeTwo[671:60b] resolveCallback
    2013-11-14 14:09:05.840 XperTakeTwo[671:60b] service resolved: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:05.843 XperTakeTwo[671:60b] service has 0 addresses: lost ?

    2013-11-14 14:09:06.910 XperTakeTwo[671:60b] browserCallBack
    2013-11-14 14:09:06.912 XperTakeTwo[671:60b] service lost: Canon MG6200 series._ipp._tcp.local.
    2013-11-14 14:09:06.915 XperTakeTwo[671:60b] findServices:didLoseService::<Service: 0x1452e060>

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

November 12, 2013

Service Discovery In Android And iOS: Part Five – iOS Take One

When it comes to iOS we are spoilt for choice. There are no less than three APIs available for service discovery.

Starting at the Foundation level we have the Objective-C class NSNetServiceBrowser.

1.0 Instance Creation

The NSNetServiceBrowser class defines a single no argument init method so to create one we simply do this

    browser = [[NSNetServiceBrowser alloc] init];

2.0 Starting A Search

The search for services of a given type is asynchronous.

To start the search we call the method

    - (void)searchForServicesOfType:(NSString*)type inDomain:(NSString*)domainString;

The type argument should be the domain relative name of the service type to search for, e.g.,

    "_ipp._tcp."

Note the dot (‘.’) at the end.

The domainString should be the absolute name of the domain to search, e.g.,

    "local."

Note the dot (‘.’) at the end.

Once the search has started it will continue indefinitely unless it is explicitly stopped.

3.0 NSNetServiceBrowserDelegate

NSNetServiceBrowser uses the standard delegate pattern to return its results.

The delegate is required to implement the NSNetServiceBrowserDelegate protocol.

3.1 netServiceBrowserWillSearch:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance is going to perform the search it calls the delegate’s implementation of the method

    - (void)netServiceBrowserWillSearch:(NSNetServiceBrowser*)netServiceBrowser

3.2 netServiceBrowser:didNotSearch:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance is not going to perform the search it calls the delegate’s implementation of the method

    - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didNotSearch:(NSDictionary*)errorInfo

The easiest way to see this method in action is to get the type name wrong in the call to searchForServicesOfType:inDomain:, e.g.,

    "._ipp.tcp."

in which case the NSDictionary passed as the errorInfo argument will look like this

    {
        NSNetServicesErrorCode   = "-72004";
        NSNetServicesErrorDomain = 10;
    }

You can find the error codes in NSNetServices.h.

3.3 netServiceBrowser:didFindService:moreComing:

Following the call to the method searchForServicesOfType:inDomain: if the NSNetServiceBrowser instance finds a service of the given type it callsthe delegate’s implementation of the method

    - (void)netServiceBrowser:
                (NSNetServiceBrowser*)netServiceBrowser
            didFindService:
                (NSNetService*)netService
            moreComing:
                (BOOL)moreServicesComing

3.4 netServiceBrowser:didRemoveService:moreComing:

If a service that has previously been found is no longer available the NSNetServiceBrowser instance
calls the delegate’s implementation of the method

    - (void)netServiceBrowser:
                (NSNetServiceBrowser*)netServiceBrowser
            didRemoveService:
                (NSNetService*)netService
            moreComing:
                (BOOL)moreServicesComing

4.0 NSNetService

As discovered, the service as represented by the NSNetService instance is still in a nascent state.
Neither its address nor the key/value pairs in its TXT record are available.

To be useful it has to be resolved.

The resolution of a service is performed asynchronously. It is started by invoking the NSNetService
method resolveWithTimeout: which is declared like this

    - (void)resolveWithTimeout:(NSTimeInterval)timeout

Once started the resolution of a service will either complete successfully ot time out after the interval specified by the
timeout argument.

5.0 NSNetServiceDelegate

The success or failure of the resolution of a NSNetService instance is reported to the delegate of that instance.

The delegate must implement the NSNetServiceDelegate protocol.

5.1 netServiceWillResolve:

Following the call to the resolveWithTimeout: method the NSNetService instance will call its delegate’s implementation of the method

    - (void)netServiceWillResolve:(NSNetService*)sender

5.2 netService:didNotResolve:

If the resolution of the service fails the NSNetService instance calls its delegate’s implementation of the method

    - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict

5.3 netServiceDidResolveAddress:

If the resolution of the service succeeds the NSNetService instance calls its delegate’s implementation of the method

    - (void)netServiceDidResolveAddress:(NSNetService*)sender;t

There is a caveat however. As we shall see ‘success’ in this context does not always mean what you might expect.’

6.0 The FindServices Class

To make things slightly simpler we can wrap up the classes and their delegates in a single class FindServices
as we did in the Java case.

FindServicesDelegate.h

    //
    //  FindServicesDelegate.h
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <Foundation/Foundation.h>
        
    @class FindServices;
    @class Service;
        
    @protocol FindServicesDelegate <NSObject>
        
    - (void)findServices:(FindServices*)theFindServices didFindService:(Service*)theService;
        
    - (void)findServices:(FindServices*)theFindServices didLoseService:(Service*)theService;
        
    @end

FindServices.h

    //
    //  FindServices.h
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import <Foundation/Foundation.h>
        
    #import "FindServicesDelegate.h"
        
    @interface FindServices : NSObject<NSNetServiceBrowserDelegate, NSNetServiceDelegate>
        
    @property (weak) id<FindServicesDelegate>   delegate;
        
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain;
        
    - (void)start;
        
    @end

FindServices.m

    //
    //  FindServices.m
    //  XperTakeOne
    //
    //  Created by Simon Lewis on 03/11/2013.
    //  Copyright (c) 2013 Simon Lewis. All rights reserved.
    //
        
    #import "Service.h"
        
    #import "FindServices.h"
        
    @interface FindServices ()
        
    @property NSString*             type;
    @property NSString*             domain;
    @property NSNetServiceBrowser*  browser;
    @property NSMutableArray*       resolving;
    @property NSMutableDictionary*  services;
        
    @end
        
    @implementation FindServices
        
    - (FindServices*)initWithType:(NSString*)theType andDomain:(NSString*)theDomain
    {
        self = [super init];
        if (self != nil)
        {
            self.type      = theType;
            self.domain    = theDomain;
            self.browser   = [[NSNetServiceBrowser alloc] init];
            self.resolving = [NSMutableArray arrayWithCapacity:8];
            self.services  = [NSMutableDictionary dictionaryWithCapacity:8];
        
            self.browser.delegate = self;
        }
        return self;
    }
        
    - (void)start
    {
        [self.browser searchForServicesOfType:self.type inDomain:self.domain];
    }
        
    // NSNetServiceBrowserDelegate
        
    - (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)theBrowser
    {
        NSLog(@"netServiceBrowserWillSearch:\n");
    }
        
    - (void)netServiceBrowser:(NSNetServiceBrowser *)theBrowser didNotSearch:(NSDictionary *)theErrors
    {
        NSLog(@"netServiceBrowser:didNotSearch: %@", theErrors);
    }
        
    - (void)netServiceBrowser:
                (NSNetServiceBrowser *)aNetServiceBrowser
            didFindService:
                (NSNetService *)theService
            moreComing:
                (BOOL)moreComing
    {
        NSLog(@"netServiceBrowser:didFindService: %@", theService);
        
        [self.resolving addObject:theService];
        theService.delegate = self;
        [theService resolveWithTimeout:0.0];
    }
        
    - (void)netServiceBrowser:
                (NSNetServiceBrowser *)aNetServiceBrowser
            didRemoveService:
                (NSNetService *)theService
            moreComing:
                (BOOL)moreComing
    {
        NSLog(@"netServiceBrowser:didRemoveService: %@", theService);
        
        Service* service = [self.services objectForKey:theService.name];
        
        if (service != nil)
        {
            [self.services removeObjectForKey:theService.name];
            [self.delegate findServices:self didLoseService:service];
        }
        else
        {
            NSLog(@"%@ removed without being found ?", theService.name);
        }
    }
        
    // NSNetServiceDelegate
        
    - (void)netServiceWillResolve:(NSNetService *)theService
    {
        NSLog(@"netServiceWillResolve");
    }
        
    - (void)netServiceDidResolveAddress:(NSNetService *)theService
    {
        NSUInteger nAddresses = [[theService addresses] count];
        
        NSLog(@"netServiceDidResolveAddress: %@ nAddresses == %lu", theService, (unsigned long)nAddresses);
        
        if (nAddresses != 0)
        {
            Service* service = [[Service alloc] init:theService];
        
            [self.resolving removeObject:theService];
            [self.services setObject:service forKey:theService.name];
            [self.delegate findServices:self didFindService:service];
        }
        else
        {
            Service* service = [self.services objectForKey:theService.name];
        
            if (service != nil)
            {
                NSLog(@"service %@ now has 0 addresses !", theService.name);
            }
            else
            {
                NSLog(@"resolve failed ? %@ has 0 addresses", theService.name);
            }
        }
    }
        
    - (void)netService:(NSNetService *)theService didNotResolve:(NSDictionary *)theErrors
    {
        NSLog(@"netServiced:didNotResolve: %@ %@", theService, theErrors);
        
        [self.resolving removeObject:theService];
    }
        
    @end

7.0 Examples

In each case FindServices is looking for services of type

    "_ipp._tcp."

in the domain

    "local."

In each case the log output is from FindServices and its delegate running on an iPad running iOS 7.0.

7.1 A Single IPPServer

A single instance of the CUPS test server IPPServer with the name ipp_server_1 running on a Mac and then being stopped.

    ...

    2013-11-12 08:26:05.516 XperTakeOne[163:60b] netServiceBrowserWillSearch:
    2013-11-12 08:26:12.500 XperTakeOne[163:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x1464bd60> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:26:12.504 XperTakeOne[163:60b] netServiceWillResolve
    2013-11-12 08:26:12.533 XperTakeOne[163:60b] netServiceDidResolveAddress: \
        <NSNetService 0x1464bd60> local. _ipp._tcp. ipp_server_1 nAddresses == 2
    2013-11-12 08:26:12.535 XperTakeOne[163:60b] findServices:didFindService: <Service: 0x14631890>
    2013-11-12 08:27:56.204 XperTakeOne[163:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x14522f50> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:27:56.207 XperTakeOne[163:60b] findServices:didLoseService: <Service: 0x14631890>

    ...

7.2 Two IPPServers

Two instances of the CUPS test server IPPServer with the names ipp_server_1 and ipp_server_2 running on a Mac and then being stopped.

    ...

    2013-11-12 08:29:54.404 XperTakeOne[171:60b] netServiceBrowserWillSearch:
    2013-11-12 08:29:54.525 XperTakeOne[171:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x1757ac10> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:29:54.528 XperTakeOne[171:60b] netServiceWillResolve
    2013-11-12 08:29:54.531 XperTakeOne[171:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x17578140> local. _ipp._tcp. ipp_server_2
    2013-11-12 08:29:54.533 XperTakeOne[171:60b] netServiceWillResolve
    2013-11-12 08:29:55.553 XperTakeOne[171:60b] netServiceDidResolveAddress:\
        <NSNetService 0x17578140> local. _ipp._tcp. ipp_server_2 nAddresses == 2
    2013-11-12 08:29:55.556 XperTakeOne[171:60b] findServices:didFindService: \
        <Service: 0x17673570>
    2013-11-12 08:29:55.558 XperTakeOne[171:60b] netServiceDidResolveAddress: \
        <NSNetService 0x1757ac10> local. _ipp._tcp. ipp_server_1 nAddresses == 2
    2013-11-12 08:29:55.559 XperTakeOne[171:60b] findServices:didFindService: <Service: 0x176b25f0>
    2013-11-12 08:30:57.454 XperTakeOne[171:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x1757e7d0> local. _ipp._tcp. ipp_server_2
    2013-11-12 08:30:57.457 XperTakeOne[171:60b] findServices:didLoseService: <Service: 0x17673570>
    2013-11-12 08:31:02.678 XperTakeOne[171:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x17581730> local. _ipp._tcp. ipp_server_1
    2013-11-12 08:31:02.680 XperTakeOne[171:60b] findServices:didLoseService: <Service: 0x176b25f0>
    
    ...

7.3 A Single Printer

A printer being turned on and then turned off five minutes later.

Note the lines shown in bold for emphasis.

When the printer is turned off the netServiceDidResolveAddress: method is being called for a second time but this time the NSNetService instance has no addresses.

This is the reason for the convoluted code in the FindServices implementation of the netServiceDidResolveAddress:.

Note also that this does not happen when an IPPServer instance is shutdown.

    ...

    2013-11-12 08:32:14.253 XperTakeOne[179:60b] netServiceBrowserWillSearch:
    2013-11-12 08:32:14.976 XperTakeOne[179:60b] netServiceBrowser:didFindService: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series
    2013-11-12 08:32:14.979 XperTakeOne[179:60b] netServiceWillResolve
    2013-11-12 08:32:15.008 XperTakeOne[179:60b] netServiceDidResolveAddress: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series nAddresses == 1
    2013-11-12 08:32:15.011 XperTakeOne[179:60b] findServices:didFindService: <Service: 0x14681460>

    2013-11-12 08:37:10.250 XperTakeOne[179:60b] netServiceDidResolveAddress: \
        <NSNetService 0x146b3a50> local. _ipp._tcp. Canon MG6200 series nAddresses == 0
    2013-11-12 08:37:10.252 XperTakeOne[179:60b] service Canon MG6200 series now has 0 addresses !

    2013-11-12 08:37:11.325 XperTakeOne[179:60b] netServiceBrowser:didRemoveService: \
        <NSNetService 0x146b21d0> local. _ipp._tcp. Canon MG6200 series
    2013-11-12 08:37:11.327 XperTakeOne[179:60b] findServices:didLoseService: <Service: 0x14681460>

    ...

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Older Posts »

The WordPress Classic Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: