Connecting to services in a DHCP-configured network

Over the past 15 years, communication infrastructure improvements have allowed datacenter technologies to expand out into small remote computing environments, such as, hamburger stands, salons, and small businesses, in general.  Historically (say, pre-1995) one would tolerate such infrastructure investments for large facilities like big-box retailers and large remote office locations, since the investment was small relative to the facility, inventory, and staffing costs.  As more and more IT services have been deployed to improve efficiency (at least in the US) and to enhance customer experience, however, developers have tended to promote solutions based on enterprise infrastructures and operating practices.

Obviously, such an approach can be justified by noting Moore’s Law and the ever decreasing cost of hardware and bandwidth coupled with the argument that development groups can just leverage their existing enterprise development patterns and practices.  What is often overlooked is the associated help desk and remote management costs as well as the deployment and field support costs associated with supporting datacenter-like infrastructures remotely.  Whereas remote-site applications used to be designed to run in a lights-out production environment; now, much product development takes a “lazy” approach and just adds layers of management software and remote monitoring.  These are, however, real costs that must be borne by the customer — probably in excess of having a resilient solution designed for minimum configuration management and support.  This is one area that the Go programming language is uniquely positioned to help address: simplifying the delivery of service-based applications in remote business environments.

One common challenge when supporting applications deployed in remote locations — i.e., outside of a managed datacenter environment — is network configuration management.  Deploying network applications typically requires that servers, workstations and devices be deployed with static iP configurations, and if we have multiple remote locations, say, several thousand or more, with different device requirements, considerable effort can be spent developing and managing network IP address schemes.  One work-around to this problem involves using IP multicast and requiring the applications and services to find each other through the multicast pool; while this works, implementing this is definitely a distraction from getting on with the business of writing business logic and delivering functionality.

Ideally, we’d like to be able to simplify network configuration — say, use default DHCP services — and let the application loop through a prescribed range of addresses in order to find the particular service of interest. The only problem is that if we just try looping through candidate IP addresses using Dial(), we’ll end with long latencies while the application blocks waiting for net.Dial() to timeout.

Go allows us to build and deploy compact service-based applications.  For services-based applications deployed to remote computing environments, I would like to minimize dependencies on hardware and network configuration as much as possible.  Essentially, I’d like to be able to deploy solutions that are self configuring on a minimal infrastructure.  Using goroutines and channels we can wrap the relevant Dial() function and let the application loop through the candidate addresses using a specified timeout period so that the application isn’t blocking for the TCP/IP timeout.  The basic programming pattern has been discussed by Andrew Gerrard in the Go Programming Language Blog; here I’ll apply the pattern to testing whether a websocket connection can be established.

The idiomatic Go pattern for testing to see if we can establish a websocket connection on a specific IP addresses should be:

  • wrap the websocket.Dial() function in a function, say, ‘dialWebsocConn()’, that passes the websocket.Dial() return values on a channel;
  • after invoking ‘dialWebsocConn()’ as a go routine, the calling function, say, ‘GetWebsocConn()’, enters a timing loop that repeatedly checks the channel for the Dial() result until the prescribed time interval expires;
  • if the prescribed time has expired, then assume the connection cannot be established — close the channel and return a relevant error value so that the application can continue, perhaps trying another address on the network.

Some code to implement this pattern is as follows.

// websoc struct{ }
// Just a data type to hold websocket.Dial() return values, for channel communication. //
//
type websoc struct {
       c *websocket.Conn
       err os.Error
}

// GetWebsocConn()
// Looks like websocket.Dial() but includes the number of seconds to wait before timing out
// If the call times out, it returns (nil, os.ECONNREFUSED)
// otherwise it returns the values from websocket.Dial() received from dialWebsocConn().
func GetWebsocConn(wsString, destString string, timeOut int) (*websocket.Conn, os.Error) {
     var ws websoc
     var ok bool

       ch := make(chan websoc,1)
      // invoke the dialer in a goroutine
       go dialWebsocConn(wsString, destString, ch)

       tic := time.Seconds()                               // start the timer
       for {
           ws, ok = <-ch                            // use non-blocking channel read
           if ok == true { break }                       // if a result is received, return it
           toc := time.Seconds()
           if int(toc-tic) > timeOut {                   // if the timeout interval has expired
              close(ch)                                        // close the channel (don’t need it anymore)
              ws.err = os.ECONNREFUSED                  // and set up err for return
           break
           }
          time.Sleep(2e8)                            // take a breath
       }
       return ws.c, ws.err
}

// dialWebsocConn()
// Call the websocket.Dial() function and
// return the connection handle or (possible) error on the channel
//
func dialWebsocConn(wsString, destString string, ch chan websoc) {
       var ws websoc

       c, err := websocket.Dial(wsString,””,destString)
       ws.c = c
       ws.err = err
       ch <- ws
}

A call using this websocket dialer function and a two second timeout value might look something like this.

c, _ := mypkg.GetWebsocConn(“ws://192.168.1.104:12345/alerts”,”http://192.168.1.104:12345/”,2)
if c == nil {
       fmt.Println(“No ‘alerts’ service available on 192.168.1.104:12345”)
}

It is a trivial exercise, now, to loop through a range of IP addresses (and/or port values) looking for a valid connection.

In my R&D project I wanted to be able to attach specific monitors and devices to particular named services.  This is easy to do using websockets by implementing a handshake protocol in the service handler that provides a connecting application with the service instance ID.  If the handshake fails, the application can just keep looping through the specified address/port ranges until it reaches one that succeeds.

In a production setting we would, of course, like to avoid unnecessary startup work.  So, it’s a useful addition to save the valid address:port value once the application and service are connected so that the application can try that address first when it restarts.

Posted in Go Programming Language | Tagged , , , | Leave a comment