Voip.ms latency measurement, Part 2

Contents

Indices and tables

Background

This week, the goal is to ping the voip servers. To learn about ping, open up Konsole and type man ping. You can try to ping the local host as an example, ping -c 4 localhost

However, you won’t be able to ping externally (on a Wescam asset) due to the firewall. So, we will use a web server to do the pinging for us.

The server is at http://ec2-35-168-148-68.compute-1.amazonaws.com/, to see an example return try querying it with a host like python.org

Exercise

For this week, the goal is to create a Ping Request class that will do the following:

  • It should store the ip or dns address of the server.

  • It should contain a method to send a ping request:

    • The method should accept a parameter that is the host name to ping

    • The method should send a http GET request to the server

      • This request should only include the desired ping target, in our case the host name
    • The method should check that the server response has a status of 200

    • The method should return the server response.

This will require using the requests library again. You will likely want to use requests.get()

On completion of this exercise, you should be able to run your stub program and have it do the following:

  • Print a list of voip.ms server host names
  • For each host name, print the response recieved from the server after sending a request with the host name.

Hints

Here is the class definition, as well as signatures for the constructor and function to send the http requests

class PingRequest:
    """
    Class to deal with the web server. For now this just store the connection object, it
    will make more sense in the final version
    """

    def __init__(self, address):
        """
        Constructor. Open the connection on ip:port
        :param address: The IP or DNS address of the server to connect to
        """
    def ping_request(self, host_name):
        """
        Send an http GET request on the provided connection to ping the specified host.
        :param host_name: Host name or IP address to ping
        :return: The response from the ping
        """

For reference, the code running on the web server is below:

#!/usr/bin/env python3
# The code that will go in the cloud
import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
import re
import json
from enum import Enum


class Access(Enum):
    """
    Enum for clarity of accessing the previous pings
    """
    last_time = 0
    last_value = 1


class PingServer(BaseHTTPRequestHandler):
    """
    Server class, accepts get requests and pings the host name within
    """
    pings = dict()
    target = re.compile("time=[0-9]*\.?[0-9]*")

    # GET request
    def do_GET(self):
        # Send response status code
        self.send_response(200)

        # Send headers
        self.send_header('Content-type', 'text/html')
        self.end_headers()

        # Send message to client
        address = self.requestline.split()
        # /? is added by requests when it makes the "GET" request
        message = self.ping_target(address[1].strip('/?'))

        # Need to encode to bytes before trying to write out
        self.wfile.write(json.dumps(message).encode("utf8"))
        return

    def ping_target(self, host_name):
        """
        Ping an address and return the response. If it has been pinged in the last
        second return the stored value. Otherwise make a new ping
        :param host_name: Hostname to ping
        :return: JSON result or the error, as bytes
        """
        try:
            # This will wait up to 1 second for the ping response.
            now = time.time()
            if host_name in self.pings:
                # Setting the interval to 1 second for now
                if now - self.pings[host_name][Access.last_time.value] < 1:
                    # Return the existing value
                    return self.pings[host_name][Access.last_value.value]
            # In other cases do the ping again, and save (time, result)
            result = subprocess.check_output(["ping", "-c 1", "{}".format(host_name)],
                                             timeout=1)
            # Extract the ping time and create a json structure
            res = self.target.search(result.decode("utf-8"))
            if res is None:
                ping_result = {'status': 'failed', 'host': host_name, 'ping': None}
            else:
                this_ping = float(res.group(0).split("=")[1])
                ping_result = {'status': 'success', 'host': host_name, 'ping': this_ping}

            now = time.time()
            self.pings[host_name] = (now, ping_result)
            return ping_result
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err:
            # Don't throw, return the error to the caller
            return {'status': 'error', 'host': host_name, 'ping': str(err)}


def run(port=80):
    """
    Method to start up the server on the specified port. Once this is called you need to
    use a keyboard interrupt to stop the server (ctrl+c on most systems)
    :param int port: The port to run the server on. Defaults to 80
    """
    print('Server starting up...')
    print("Port set as {}".format(port))
    # Server settings, if changing the port use a large number to avoid issues.
    # You will also need to change ec2 security to allow incoming requests on the port
    server_address = ("0.0.0.0", port)
    httpd = HTTPServer(server_address, PingServer)
    print('Server running...')
    httpd.serve_forever()


if __name__ == '__main__':
    # Run the server as a script
    try:
        run()
    except KeyboardInterrupt:
        print("Keyboard interrupt received, shutting down.")
  • The server is located at IP: 35.168.148.68 or DNS: ec2-35-168-148-68.compute-1.amazonaws.com. It is on the http port (80), which means you don’t have to worry about including the number.
  • You can provided requests with either the IP or DNS address, however using the DNS can be significantly slower to access. (Give it a try!)
  • The server response will be a json object, you will need to convert it.

Solution

When you are ready to see one possible solution, try this file