DEV Community

loading...

NodeJS Portfinding - Three Approaches Compared

richardeschloss profile image Richard Schloss ・4 min read

TL;DR - At the heart of most NodeJS apps is the line of code that says "server.listen". Sounds simple enough, until the port you want is taken, resulting in the server failing to start. This article describes 3 approaches to portfinding in NodeJS, each with their own pros and cons. I wrote this because what appears to be the most popular Googled solution may not necessarily be the best solution for all cases.


Method 1: Try / fail / repeat

This method is probably the most popular one floating around on Google, Github and npm. The idea is, be optimistic. Try to listen to the port you provide. If it's open, fantastic, your server is up and running as fast as possible, but if it fails, it's waiting for the port to increment and try again. Why is this method so popular? Well, for one, the code is pretty simple and straightforward:

function startServer(server, port, host) {
  function onError(error) {
    server
      .removeListener('error', onError)
      .removeListener('listening', onSuccess)
    if (error.code === 'EADDRINUSE') {
      startServer(server, ++port, host)
    })
  }

  function onSuccess() {
    console.log(
      `listening at: (host = ${host}, port = ${_server.address().port})`
    )
  }
  server.listen(port, host)
  .on('error', onError)
  .on('listening', onSuccess)
}

const port = 8000
const host = 'localhost'
const server = http.createServer({...})

startServer(server, 8000, 'localhost')

In a lot of practical cases, we tend to get lucky. The machine usually has many ports available, and this is a pretty quick way to make the code work cross-platform. The developer doesn't have to be bothered parsing OS-specific responses from netstat. However, it may turn out that we'd still want to be more efficient. Maybe a machine would have many more services running and we'd want to listen on a free port asap. If that becomes the case, the above code can still be relatively slow. Each time we attempt to listen, we have to wait for either success or failure. Plus, on each error event, we have to remember to remove listeners for 'error' and 'listening' events, a clean-up step that's easy to forget. And that clean-up step has us writing just a little more code than we would want, because removing the listeners has to be done with named functions, not anonymous ones.

Method 2: Query the OS! (it already knows!)

When services listen on your machine, the OS maintains the list of listening ports. It has to. On Linux for example, it maintains /proc/net/tcp:

cat /proc/net/tcp

The output from that file might appear a bit cryptic, but the used ports are indeed there. The local addresses are formatted in hex as [hostname (Little-Endian)]:[port]. netstat is probably the utility most used to read this information.

Reading this information would clearly be a much more efficient approach, because then the algorithm would simply be: "is port in list? if so, use a random port not in the list, otherwise, use the port". It would not be try/cross-fingers/fail/repeat. However, it's likely this solution has been avoided in the past because not all netstats are created the same. netstat on Windows is different than netstat on Linux. Similar, but different. And parsing the netstat output wasn't always a fun task.

Fortunately, there are many generous people out there who have created netstat wrappers, with my personal favorite being node-netstat. That means we can write some very simple utilities like this: (but, if NodeJS core is reading, net.stat(...) is on my NodeJS wish list :))

import netstat from 'node-netstat' // Credit: [Roket](https://www.npmjs.com/~roket84)

const netstatP = (opts) => // Credit: [vjpr](https://github.com/vjpr)
  new Promise((resolve, reject) => {
    const res = []
    netstat(
      {
        ...opts,
        done: (err) => {
          if (err) return reject(err)
          return resolve(res)
        }
      },
      (data) => res.push(data)
    )
    return res
  })

async function findFreePort({ range = [8000, 9000] }) {
  const usedPorts = (await netstatP({ filter: { protocol: 'tcp' } })).map(
    ({ local }) => local.port
  )

  let [startPort, endPort] = range
  let freePort
  for (let port = startPort; port <= endPort; port++) {
    if (!usedPorts.includes(port)) {
      freePort = port
      break
    }
  }
  return freePort
}

async function portTaken({ port }) {
  const usedPorts = (await netstatP({ filter: { protocol: 'tcp' } })).map(
    ({ local }) => local.port
  )
  return usedPorts.includes(port)
}

export { findFreePort, portTaken }

If it is expected that more than one port will be occupied, this method should work much faster than the previous one. However, if you're feeling lucky or know for sure you won't have anything else listening at your specified port, the previous method will be faster.

Method 3: Let the OS assign you the port

This is probably the easiest method, and maybe even the fastest (but comes with a different kind of cost). The trick is to simply specify port 0, and the OS will assign the port to you:

Example:

const host = 'localhost'
const port = 0
server
  .listen(port, host)
  .on('listening', () => {
    console.log(
     `listening at: (host = ${host}, port = ${
        _server.address().port
     })` // listening at: (host = localhost, port = 37087)
    )
  })

This method is pretty cool, right? However, it does come with perhaps a pretty important caveat. In your development workflow, you may find yourself restarting your server for a variety of reasons. Each time you restart, you will get assigned a random port. Even though it is easy for you be made aware of that port, it may become frustrating to keep changing the port on your client.

Conclusion

Three ways to solve portfinding in NodeJS were presented here today. While the information may already be spread across the internet, I thought it would still be worth it to summarize the solutions here to help reduce the time spent Googling. I hope you found this helpful!

Discussion (0)

pic
Editor guide