Using Sockets

Sockets are used for interprocess communication.

Communication between Programs

A client/server interaction is similar to a telephone exchange.

  1. dial company on 1-312-666-9000 \(\sim\) connect to IP address 127.0.0.1
  2. call answered by reception \(\sim\) connection established to remote host
  3. ask for computer center \(\sim\) route using specified port (8732)
  4. call answered by computer center \(\sim\) server handles request from client
  5. hang up the phone \(\sim\) close sockets

The most commonly used methods on socket objects are listed in Table 13.

Table 13 methods on socket objects
method description
accept() accepts connection and returns new socket
bind() binds a socket to an address
close() closes the socket
connect(a) connects to address a
listen(c) listen for c connections
shutdown(flag) shut down for reading, writing, or both
recv(b) receives data in buffer of size b
send(d) sends data in d

Remote Server and Client

With ifconfig we can get the IP address of a computer.

  • On a Mac: open System Preferences -> Network.
  • On Unix: ifconfig configures network interface parameters. Without parameters, ifconfig displays current configuration.

To select what we need, we type at the prompt $

$ ifconfig | grep "inet " | grep -v 127.0.0.1 | cut -d\  -f2

The firewall on the server computer must allow incoming connections for Python.

On Mac, see System Preferences -> Security.

On Unix, the nslookup returns the IP address and the full name of a computer.

$ nslookup people.uic.edu
Server:      128.248.171.50
Address:     128.248.171.50#53

Name:    people.uic.edu
Address: 128.248.156.140
$

To allow tcp communications via a port number on Linux, one must modify the Firewall Configuration and add a port number.

The code for remote_client.py is listed below.

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM
SERVER = '131.193.178.183'  # IP address
PORTNUMBER = 41267          # port number
BUFFER = 80                 # buffer size

SERVER_ADDRESS = (SERVER, PORTNUMBER)
CLIENT = Socket(AF_INET, SOCK_STREAM)
try:
    CLIENT.connect(SERVER_ADDRESS)
    print('client is connected')
    DATA = input('Give message : ')
    CLIENT.send(DATA.encode())
except OSError:
    print('connection failed')
CLIENT.close()

The code in the corresponding remove_server.py is

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM, SHUT_RDWR

HOSTNAME = ''      # blank for any address
PORTNUMBER = 41267 # number for the port
BUFFER = 80        # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
SERVER = Socket(AF_INET, SOCK_STREAM)
SERVER.bind(SERVER_ADDRESS)
SERVER.listen(1)

print('server waits for connection')
CLIENT, CLIENT_ADDRESS = SERVER.accept()

if CLIENT_ADDRESS[0] == '131.193.41.130':
    print('server accepted connection from ',\
        CLIENT_ADDRESS)
    print('server waits for data')
    DATA = CLIENT.recv(BUFFER).decode()
    print('server received ', DATA)
else:
    print('server does not accept data from ',\
        CLIENT_ADDRESS)
    CLIENT.shutdown(SHUT_RDWR)

SERVER.close()

Two Clients and One Talk Host

In the following example, we will use a server to swap information between two clients. Often the server acts as moderator between clients. Suppose two clients want to exchange information.

Protocol:

  1. The server listens to two incoming clients.
  2. Both clients connect to the server and the server accepts two connections. Both clients send their data to the server.
  3. The server sends the data of the first client to the second and the data of the second client to the first.

The same program will be used for both clients.

The server script is called talk_host.py. In a terminal window at the server, we see:

$ python talk_host.py
talk host waits for first client
server accepted connection from ('127.0.0.1', 49158)
talk host waits for second client
server accepted connection from ('127.0.0.1', 49159)
talk host waits for first client
talk host received "this is alpha"
talk host waits for second client
talk host received "this is beta"
talk host sends "this is beta" to first client
talk host sends "this is alpha" to second client
$

What appears in the terminal window for the first client is

$ python talk_client.py
client is connected
Give message : this is alpha
client waits for reply
client received "this is beta"
$

And the terminal window for the second client looks as

$ python talk_client.py
client is connected
Give message : this is beta
client waits for reply
client received "this is alpha"
$

The code for the client in talk_client.py is listed below.

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM
HOSTNAME = 'localhost'  # on same host
PORTNUMBER = 11267      # same port number
BUFFER = 80             # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
CLIENT = Socket(AF_INET, SOCK_STREAM)
CLIENT.connect(SERVER_ADDRESS)

print('client is connected')
ALPHA = input('Give message : ')
CLIENT.send(ALPHA.encode())

print('client waits for reply')
BETA = CLIENT.recv(BUFFER).decode()
print('client received \"' + BETA + '\"')
CLIENT.close()

The code for starting the server in talk_host.py is

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM

HOSTNAME = ''      # blank for any address
PORTNUMBER = 11267 # number for the port
BUFFER = 80        # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
SERVER = Socket(AF_INET, SOCK_STREAM)
SERVER.bind(SERVER_ADDRESS)
SERVER.listen(2)

The code for accepting connections in the server script is

print('talk host waits for first client')
FIRST, FIRST_ADDRESS = SERVER.accept()
print('server accepted connection from ', FIRST_ADDRESS)

print('talk host waits for second client')
SECOND, SECOND_ADDRESS = SERVER.accept()
print('server accepted connection from ', SECOND_ADDRESS)

Then there is the code for exchanging data:

print('talk host waits for first client')
ALPHA = FIRST.recv(BUFFER).decode()
print('talk host received \"' + ALPHA + '\"')
print('talk host waits for second client')
BETA = SECOND.recv(BUFFER).decode()
print('talk host received \"' + BETA + '\"')

print('talk host sends \"' + BETA + '\"' + ' to first client')
FIRST.send(BETA.encode())
print('talk host sends \"' + ALPHA + '\"' + ' to second client')
SECOND.send(ALPHA.encode())

SERVER.close()

Monte Carlo Simulations

As an application of Monte Carlo methods, consider the estimation of \(\pi\).

The estimation can be summarized as throwing darts into a square and count the ratio of darts that landed inside the disk versus the total number of darts that landed in the square. That the area of the unit disk (shown in Fig. 85) equals \(\pi\) justifies this method.

_images/figareadisk.png

Fig. 85 The area of the unit disk equals \(\pi\).

In the simulation, we generate random uniformly distributed points with coordinates \((x,y) \in [-1,+1] \times [-1,+1]\). We count a success when \(x^2 + y^2 \leq 1\).

The algorithm can be summarized in the following steps:

  1. Generate \(n\) points \(P\) in \([0,1] \times [0,1]\).
  2. Let \(m := \# \{ \ (x,y) \in P: x^2 + y^2 \leq 1 \ \}\).
  3. The estimate is then \(4 \times m / n\).

On a dual core processor, we use two clients. Since this is a pleasingly parallel computation (no communication between processes during the computation), we expect an optimal speedup and obtain the estimate twice as fast.

In the basic setup, there will be one server script and two clients. The server ensures the distribution of the work and collects the results of the simulations, as shown in a terminal window below.

$ python mc4pi2.py
server waits for connections...
server waits for results...
approximation for pi = 3.141487

The clients do the computations, as illustrated below.

$ python mc4pi_client.py
client is connected
client received 1
client computes 0.785253200000

$ python mc4pi_client.py
client is connected
client received 2
client computes 0.785490300000

The client each receive a different seed for their random number generators. The code for the server in mc4pi2.py is below.

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM

HOSTNAME = ''       # blank for any address
PORTNUMBER = 11267  # number for the port
BUFFERSIZE = 80     # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
SERVER = Socket(AF_INET, SOCK_STREAM)
SERVER.bind(SERVER_ADDRESS)
SERVER.listen(2)

print('server waits for connections...')
FIRST, FIRST_ADDRESS = SERVER.accept()
SECOND, SECOND_ADDRESS = SERVER.accept()
FIRST.send('1'.encode())
SECOND.send('2'.encode())

print('server waits for results...')
NBR1 = FIRST.recv(BUFFERSIZE).decode()
NBR2 = SECOND.recv(BUFFERSIZE).decode()
RESULT = 2*(float(NBR1)+float(NBR2))
print('approximation for pi =', RESULT)
SERVER.close()

Code for the workers is defined in mc4pi_client.py.

import random
from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM

HOSTNAME = 'localhost'  # on same host
PORTNUMBER = 11267      # same port number
BUFFERSIZE = 80         # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
CLIENT = Socket(AF_INET, SOCK_STREAM)
CLIENT.connect(SERVER_ADDRESS)

print('client is connected')
SEED = CLIENT.recv(BUFFERSIZE).decode()
print('client received %s' % SEED)
random.seed(int(SEED))

NBR = 10**7
CNT = 0
for i in range(NBR):
    XPT = random.uniform(0, 1)
    YPT = random.uniform(0, 1)
    if XPT**2 + YPT**2 <= 1:
        CNT = CNT + 1
RESULT = float(CNT)/NBR
print('client computes %.12f' % RESULT)

CLIENT.send(str(RESULT).encode())

CLIENT.close()

The above example to estimate \(\pi\) with a client/server computation is cumbersome and serves mainly as an illustration of the basic use of sockets. With the multiprocessing module, we can setup a parallel computation much better.

The multiprocessing module provides two classes:

  1. Objects of the class Process represent processes:

    • We instantiate with a function the process will execute.
    • The method start starts the child process.
    • The method join waits till the child terminates.

    In UNIX, we say: the main process forks a child process.

  2. Objects of the class Queue are used to pass data from the child processes to the main process that forked the processes.

The two classes suffice for a simple manager-worker model:

  • The manager distributes the work with functions.
  • The workers are the child processes which run the functions.

The processes report back via a queue. For the Monte Carlo estimation of \(\pi\) the function that is executed by the processes is below.

from multiprocessing import Process, Queue
from math import pi

def monte_carlo4pi(nbr, nsd, result):
    """
    Estimates pi with nbr samples,
    using nsd as seed.
    Adds the result to the queue q.
    """
    from random import uniform as u
    from random import seed
    seed(nsd)
    cnt = 0
    for _ in range(nbr):
        (x, y) = (u(-1, 1), u(-1, 1))
        if x**2 + y**2 <= 1:
            cnt = cnt + 1
    result.put(cnt)

The main function defines, starts and joins the processes.

from multiprocessing import Process, Queue
from math import pi

def main():
    """
    Prompts the user for the number of samples
    and the number of processes.
    """
    nbr = int(input('Give the number of samples : '))
    npr = int(input('Give the number of processes : '))
    queues = [Queue() for _ in range(npr)]
    procs = []
    for k in range(1, npr+1):
        procs.append(Process(target=monte_carlo4pi, \
            args=(nbr, k, queues[k-1])))
    for process in procs:
        process.start()
    for process in procs:
        process.join()
    app = 4*sum([q.get()/nbr for q in queues])/npr
    print(app, 'error : %.3e' % abs(app - pi))

Exercises

  1. Explore the possibilities of a client/server interaction between people.uic.edu and computers in the labs, or between your laptop and home computer.
  2. Modify the code for talk_host.py, the user is prompted to enter the number of clients. Change the code for talk_client.py, the user enters the number of the client. Extend the data swapping protocol: data from the \(i\)-th client goes to the \((i+1)\)-st client, data from the last client is sent to the first one.
  3. The natural logarithm of 2 is \(\ln(2) = \int_1^2 \frac{1}{x} dx\). Adjust the code for estimating \(\pi\), to estimate \(\ln(2)\) with an interactive parallel client/server computation.