Network programming for PyS60 (VII)

by Marcelo Barros

Everything is about “protocols” in computer networks, doesn’t it ? In fact they are very important. A protocol is a kind of agreement where rules and roles are definged. They are essential for computer networks, controlling the communication between computer endpoints in all levels (from hardware to applications).

In this post, we will create a simple “homebrew” protocol to receive files over WiFi network using a mono thread server (our first server!). Our protocol has the following format:

protocolo

So, we have three fields, separated by “\n”:

  • An string representing the file name (maximum of 32 characters).
  • The size of the file, represented in big endian (most significant byte comes first in memory). When exchanging data between machines with different alignments, endianness matters. While PCs are little endian, machines with MIPS processor are big endian and if you do not pack properly your data it will be misundertood. Size may be useful for checking if all bytes were received or even if we will have enough space for it.
  • File contents.

TCP servers have a specific flow. Basically, you need the following sequence of calls to socket API:

  • socket(): it is used to create the socket, our “access point” to the socket API
  • bind(): if you are creating a server, it is necessary to specify exactly where your server will run or your clients won’t find it. Bind does this task, saying to the operating system that we want to create a new server at (IP,PORT). For multi-homed machines (a machine with several IPs), its possible to use “0.0.0.0″ to say to operating system (OS) that we want to have our server in all available local IPs (but same port).
  • listen(): frequently misunderstood, listen is used to reserve resources in your OS. It tell to OS how many simultaneous connection requests must be handled by it at the same time for this socket. Once the connection is accepted by your server, related resources are released, allowing additional connection requests. listen does not limit the amount of connected clients that you may have, this is done by your application. Which number to put in listen ? The answer is application dependent. If you expects few connections per minute, use an small number, like 5 or 10. Many connections per second ? It better to try to estimate the amount of simultaneous connection requests you may have and use this number in listen.
  • accept(): everything is defined, your server is up and running. Time to take a breath and wait for your clients, using accept. Your server code will block when accept is called, waiting for possible incoming connections. When a connection arrives, the program continues its normal execution flow just after accept call. It is possible not to block in a call to accept, using some socket options (we will talk about socket option in future posts).

accept() returns two important parameters:

  • a socket for describing the incoming connection. Any further communication with the client must be done using this socket.
  • client IP address and port, as a tupple.

From this point, you can use recv(), send() to exchange data with your client. Multi thread servers will probably create a new thread, passing the socket to it. This way, it can call accept again and wait for more clients.

As cited in network programming III, since TCP is a byte oriented stream, you need to wait until a complete name and size arrive, so it is necessary some additional work when receiving. As we will see in a future post, the “makefile” function may helps a lot.

Our server code is below:

# -*- coding: utf-8 -*-
import sys
try:
    # http://discussion.forum.nokia.com/forum/showthread.php?p=575213
    # Try to import 'btsocket' as 'socket' - ignored on versions < 1.9.x
    sys.modules['socket'] = __import__('btsocket')
except ImportError:
    pass
 
from appuifw import *
import socket
import os
import e32
import struct
 
class FileUpload(object):
    """ File upload server class
    """
    def __init__(self):
        self.lock = e32.Ao_lock()
        self.dir = "e:\\file_upload"
        if not os.path.isdir(self.dir):
            os.makedirs(self.dir)
        self.apo = None
        self.port = 54321
        self.new_line = u"\u2029"
        app.title = u"File upload"
        app.screen = "normal"
        app.menu = [(u"About", self.about)]
        self.body = Text()
        app.body = self.body
        self.lock = e32.Ao_lock()
 
    def recv_file(self,cs,addr):
        """ Given a client socket (cs), receive a new file
            and save it at self.dir
        """
        data = ""
        name = ""
        size = 0
        # waiting for file name
        while True:
            n = data.find("\n")
            if n >= 0:
                name = data[:n]
                data = data[n+1:]
                break
            buf = cs.recv(1024)
            data = data + buf
 
        # waiting for file size (may be useful for limits checking)
        while True:
            n = data.find("\n")
            if n >= 0:
                # unpack one long (L) using big endian (>) endianness
                size = struct.unpack(">L",data[:n])[0]
                data = data[n+1:]
                break
            buf = cs.recv(1024)
            data = data + buf
 
        self.body.add(u"Uploading %s (%d bytes)" % (name,size) + self.new_line)
        # waiting for file contents
        fname = os.path.join(self.dir,name)
        f = open(fname,"wb")
        while True:
            f.write(data)
            data = cs.recv(1024)
            if not data:
                break
        self.body.add(u"Finished." + self.new_line)
        cs.close()
        f.close()
 
    def server(self,ip,port):
        """ Starts a mono thread server at ip, port
        """
        s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        s.bind((ip,port))
        s.listen(1)
 
        while True:
            (cs,addr) = s.accept()
            self.body.add(u"Connect to %s:%d" % (addr[0],addr[1]) + self.new_line)
            self.recv_file(cs,addr)
 
    def sel_access_point(self):
        """ Select and set the default access point.
            Return the access point object if the selection was done or None if not
        """
        aps = socket.access_points()
        if not aps:
            note(u"No access points available","error")
            return None
 
        ap_labels = map(lambda x: x['name'], aps)
        item = popup_menu(ap_labels,u"Access points:")
        if item is None:
            return None
 
        apo = socket.access_point(aps[item]['iapid'])
        socket.set_default_access_point(apo)
 
        return apo
 
    def about(self):
        note(u"File upload by Marcelo Barros (marcelobarrosalmeida@gmail.com)","info")
 
    def run(self):
        self.apo = self.sel_access_point()
        if self.apo:
            self.apo.start()
            self.body.add(u"Starting server." + self.new_line)
            self.body.add(u"IP = %s" % self.apo.ip() + self.new_line)
            self.body.add(u"Port = %d" % self.port + self.new_line)
            self.body.add(u"Repository = %s" % (self.dir) + self.new_line)
            self.server(self.apo.ip(),self.port)
            self.lock.wait()
        app.set_tabs( [], None )
        app.menu = []
        app.body = None
        app.set_exit()
 
if __name__ == "__main__":
    app = FileUpload()
    app.run()

Finally, this small PC code may be used to send the file. Run it from command line and use the sintaxe “script_name.py server_ip server_port file_to_be_send“.

import struct
import sys
import os
import socket
 
if len(sys.argv) < 4:
    print "%s server_addr server_port file_to_upload" % sys.argv[0]
    sys.exit(1)
 
ip = sys.argv[1]
port = int(sys.argv[2])
full_name = sys.argv[3]
base_name = os.path.basename(full_name)
size = os.path.getsize(sys.argv[3])
 
print "Sending %s to %s:%d ..." % (base_name,ip,port)
 
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((ip,port))
f = open(full_name,"rb")
header = "%s\n%s\n" % (base_name,struct.pack(">L",size))
s.sendall(header)
while True:
    data = f.read(1024)
    if not data:
        break
    s.sendall(data)
 
s.close()
f.close()

In the next two pictures we can see these programs working. I used the command ping to test the ad-hoc connection between PC (10.0.0.2) and mobile phone (10.0.0.1), before transferring files.

pc_send_file

file_upload

As homework, how about to port this PC client to S60 and exchange file between two phones (see posts IV and V ) ?

Related posts:

  1. Network programming for PyS60 (VIII) Did you do your homework ? So, I would like...
  2. Network programming for PyS60 (XV) As discussed in post III, TCP sockets have flow...
  3. Network programming for PyS60 (III) As I promised, today we will start with some programming,...
  4. Network programming for PyS60 (XIII) In our last post we talked about multicast, a special...
  5. Network programming for PyS60 (IX) A new element was presented in our last post: exception...

Related posts brought to you by Yet Another Related Posts Plugin.