Introduction to network programming with Python #0002 TCP (2020 Tutorial)

 


Table of Contents

1. Introduction to TCP
2. TCP client
3. TCP server
4. TCP application file download

1. Introduction to TCP

You have probably heard the TCP/IP protocol suite. TCP stands for Transmission Control Protocol; IP stands for Internet Protocol. TCP/IP provides apps a way to deliver an ordered and error-checked stream of information packets over the internet.  While UDP provides apps a way to deliver a fast stream of information packets over the internet. UDP is faster than TCP, but it may lose data during data transmission.

Both TCP and UDP are built on top of IP.  No matter which protocol is used, data packets are delivered to the IP address. Packets sent to the routers and then to the hosts. 

TCP and UDP are not the only protocols that built on top of IP protocol, but they are the most widely used.

For networking, TCP is the most widely used protocol across the internet. When you download a movie from a Bitorrent webserver, your Bitorrent client receives the data packets from the server and sends back a response telling the server that packet received successfully.

TCP guarantees a reliable transmission of data packets. TCP orders every packet, so no packet is lost or corrupt during transmission; and also it error checks by having the recipient send back a response saying it has received the packet correctly.

As for UDP, it does not do the error checking work; data lost during transit are just lost. Because back and forth transmission of data reduces speed and increase the latency, so UDP is used when speed is at priority and error-checking is unnecessary, such as live broadcast, USB data transmission, online gaming.

2. TCP client

When use TCP, we clearly separate the client and server. A client is just like your Bitorrent client on your computer, whereas a server is the Bitorrent server from that we retrieve data. But for UDP in the previous topic, we didn't mention anything about the UDP client and UDP server. When you receive data, it is a UDP client and when you send data, it is a UDP server. 

2.1 How to write a TCP client

To create a TCP client, you have the four basic steps:

  • create the TCP socket
  • establish the connection
  • send and receive data packets
  • close the socket and connection
Let's look at a simple example:

# tcp_basic_client.py
import socket

def main():
      # create socket object
      tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # bind local address
      local_ip = '' # leave it empty
      local_port = 7878
      tcp_socket.bind((local_ip, local_port))
      
      # destination IP and port
      dest_ip = input('Enter remote IP: ')
      dest_port = int(input('Enter remote port: '))
      # establish the connection
      tcp_socket.connect((dest_ip, dest_port))

      # send data
      send_data = input('Enter your message: ')
      tcp_socket.send(send_data.encode('utf-8'))

      # close the socket and connection
      tcp_socket.close()

if __name__ == '__main__':
      main()

As always, we import socket. We use if __name__ == "__main__" in this program, if you don't know its usage, please check my blog article

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

We create a socket object by passing socket.SOCK_STREAM as the second argument indicating it is a TCP socket.

local_ip = '' # leave it empty
local_port = 7878
tcp_socket.bind((local_ip, local_port))

Then we bind the tcp_socket to a  local address, if we do not bind the local address, a random port will be picked for this socket. Usually, we do not bind, you could omit this step.

dest_ip = input('Enter remote IP: ')
dest_port = int(input('Enter remote port: '))

Next, we ask the user to enter destination IP and port.

tcp_socket.connect((dest_ip, dest_port))

Then, before we want to send message to the server, we need to establish the connection to the server first by using socket.connect() method which takes the destination address as a tuple.

send_data = input('Enter your message: ')
tcp_socket.send(send_data.encode('utf-8'))

After establishing the connection, we just need to send the data whatever we want. Remember, we could only send bytes instead of pure string.

Finnaly, we close the socket.

Before we run the program, we open our Net Assistant and start a TCP server on port 8080.


Then we run our program, type in TCP server's IP and port number. Once you have done that, connection is established if there's no errors happend.


Next, we just need to enter the message and return. Ther server will receive the message.


Let's move on to TCP server

3. TCP server

As for TCP server, it is more complex than a TCP client. But it also has a basic structure.

  • create a socket object
  • bind socket to local address (important)
  • turn the socket into listening mode
  • waiting for the connection from clients
  • receive data from clients and response
  • close connection and socket
Let's see a simple example:

# tcp_basic_server
import socket

def main():
      # create a socket object
      tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # bind to a local address
      local_addr = ('', 7878)
      tcp_server_socket.bind(local_addr)
      
      # turn socket into listening mode
      tcp_server_socket.listen(128)

      # waiting for incoming connections from client
      client_socket, client_addr = tcp_server_socket.accept()

      # receive data from the client using client_socket
      data_recv = client_socket.recv(1024)
      print(data_recv)
      
      # respond to client saying ok indicating message received
      client_socket.send(b'OK')

      # close both sockets
      client_socket.close()
      tcp_server_socket.close()

if __name__ == "__main__":
      main()

tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind to a local address
local_addr = ('', 7878)
tcp_server_socket.bind(local_addr)

We first create a tcp socket and bind to a local address which is very import for TCP servers. This step could not be omitted otherwise client will not find the correct server.

tcp_server_socket.listen(128)

Then we put our socket into listening mode by invoking socket.listen() method. Remember this listening socket could only listen the incoming connection and it is not responsible for receiving from and sending data to clients. 

client_socket, client_addr = tcp_server_socket.accept()

Next, our listening socket waits for incoming client connections. After running the code, our program or server will block here waiting for incoming connections. Once a client connected to the server, socket.accept() method returns a tuple consisting the client socket object which we use to receive data and send data back to client and the client's address.

data_recv = client_socket.recv(1024)
print(data_recv)

Then, we invoke client_socket.recvfrom() method to receive data and print the data.

client_socket.send(b'OK')

In order to let the client know that our server has received the message, our server respond by sending 'OK' back to the client using the client_sockt.send() method.


client_socket.close()
tcp_server_socket.close()

Finnaly, we close both sockets.

After running this program (we must run this server first), we lauch the Net Assistant, open a TCP client connected to our server.


Then we type 'hello' into the right bottom section and click send.



Our server received the message 'hello' and send back 'OK' to the client.

Our basic TCP server could only accept one client and quit, let's build a TCP sever that could serve multiple clients one by one.

# tcp_basic_server_serving_multiple_clients
import socket

def main():
      # create a socket object
      tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # bind to a local address
      local_addr = ('', 7878)
      tcp_server_socket.bind(local_addr)
      
      # turn socket into listening mode
      tcp_server_socket.listen(128)

      while True:
            # waiting for incoming connections from client
            print('Waiting for clients:')
            client_socket, client_addr = tcp_server_socket.accept()
            print(str(client_addr)+' connected')
            # receive data from the client using client_socket
            data_recv = client_socket.recv(1024)
            print(data_recv)
            
            # respond to client saying ok indicating message received
            client_socket.send(b'OK')

            # close both sockets
            client_socket.close()
            
      tcp_server_socket.close()

if __name__ == "__main__":
      main()

We just added a while True loop, the server will constantly listen to incoming connections and serve each client one at a time.

Then run this program first, lauch two Net Assistant TCP clients.


Both clients will be connected to our server, but only the first one will be served. After serving the first one, the second client is served.


First client on port 50523 will be served first.


Then second client on port 50524 will be served next.

The program above could only serve one client once, namely our client could only send message to the server once. So we could revise the program and it could serve our client multiple times until we close the client.

# tcp_basic_server_serving_multiple_clients_revised
import socket

def main():
      # create a socket object
      tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # bind to a local address
      local_addr = ('', 7878)
      tcp_server_socket.bind(local_addr)
      
      # turn socket into listening mode
      tcp_server_socket.listen(128)

      while True:
            # waiting for incoming connections from client
            print('Waiting for clients:')
            client_socket, client_addr = tcp_server_socket.accept()
            print(str(client_addr)+' connected')
            while True:
                  # receive data from the client using client_socket
                  data_recv = client_socket.recv(1024)

                  if data_recv:
                        print(data_recv)
                        # respond to client saying ok indicating message received
                        client_socket.send(b'OK')
                  else:
             print('Client closed!')
break # close both sockets client_socket.close() tcp_server_socket.close() if __name__ == "__main__": main()

while True:
      # receive data from the client using client_socket
      data_recv = client_socket.recv(1024)

      if data_recv:
            print(data_recv)
            # respond to client saying ok indicating message received
            client_socket.send(b'OK')
       else:
        print('Client closed!')
            break

We just added another while True to contain socket.recv() method. If your client send message to the server, it wil receive the message and respond. If your client is closed, data_recv will be None and breaks out of the loop.

Let's run this server first, and then lauch the Net Assistant as a TCP client.


As you could see, our client could constantly send message to the TCP server. Once we close the client, Our server close the connection, waiting for another incoming client.


4. TCP Application File Download

So far we have touched the topics about TCP client and server.  Now we could write an TCP client that retrieve a file from a TCP server. Let's write the client first.

# tcp_client_file_downloader
import socket

def main():

      # create a socket object
      tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # enter server address
      server_ip = input('Enter server IP: ')
      server_port = int(input('Enter server port: '))
      
      # connect to the server
      tcp_client_socket.connect((server_ip, server_port))

      # enter file name that you want to retrieve
      file_name = input('Enter the file name that you want to download: ')
      
      # send the file_name to the server
      tcp_client_socket.send(file_name.encode('utf-8'))

      # receive file data from the client if any
      data_recv = tcp_client_socket.recv(1024)
      if data_recv:
            with open('new_'+file_name, 'wb') as file:
                  file.write(data_recv)

      # close the socket
      tcp_client_socket.close()

if __name__ == '__main__':
      main()

file_name = input('Enter the file name that you want to download: ')

As always, we create a TCP socket, this time we ask the user to type in the name of the file to be downloaded.

tcp_client_socket.send(file_name.encode('utf-8'))

Then send the file name to the server. After that our client will wait for the response from the server by invoking socket.recv() method.

data_recv = tcp_client_socket.recv(1024)
if data_recv:
     with open('new_'+file_name, 'wb') as file:
     file.write(data_recv)

# close the socket
tcp_client_socket.close()

If there is data sent by the server, we create a file and write all the data into the file. Finally close the socket.

Let's test this client using Net Assistant.


We start a TCP server and then run the client; type in the server ip and port number; then our client is connected to the server.

We enter the file name we want to download and then our server will receive the file name. For our Net Assistant is not designed to fetch the file and send it back  to the client, we just put some text 'this is the content of info.txt' into the response and click send. 

Then, our client receive the data sent by the server and create a file named 'new_info.txt' and write all the data into it.

If there is nothing wrong with the client above, now let's write another TCP server that could retrieve the file on the server's local machine and send it back to the client.

# tcp_server_file_downloader
import socket

def open_file_and_send_to_client(client_socket, client_addr):
      # receive data from
      client_file_name = client_socket.recv(1024).decode('utf-8')

      # make sure file exists, otherwise return nothing
      file_content = None
      try:
            file = open(client_file_name, 'rb')
            file_content = file.read()
      except Exception as exc:
            print('Error on opening file: ', client_file_name)
      
      if file_content:
            client_socket.send(file_content)
      
def main():

      # create a socket object
      tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # bind local address
      local_ip = ''
      local_port = 7878
      tcp_server_socket.bind((local_ip, local_port))
      
      # put the tcp_client_socket into listening mode
      tcp_server_socket.listen(128)

      while True:
            # wait for incoming connections
            print('Waiting for clients...')
            client_socket, client_addr = tcp_server_socket.accept()
            print(str(client_addr)+' connected...')


            # open the file and send back to client
            open_file_and_send_to_client(client_socket, client_addr)

            # close the client socket
            client_socket.close()
            
      # close the listening mode socket
      tcp_server_socket.close()
if __name__ == '__main__':
      main()

As always for writing a TCP server, we must bind it to a local address; put the TCP socket into listening mode.

# bind local address
local_ip = ''
local_port = 7878
tcp_server_socket.bind((local_ip, local_port))
      
# put the tcp_client_socket into listening mode
tcp_server_socket.listen(128)

Then we constantly accept connections from clients and handle all the requests one by one. 

 client_socket, client_addr = tcp_server_socket.accept()

The core of this server is the open_file_and_send_to_client() function.

def open_file_and_send_to_client(client_socket, client_addr):
      # receive data from
      client_file_name = client_socket.recv(1024).decode('utf-8')

      # make sure file exists, otherwise return nothing
      file_content = None
      try:
            file = open(client_file_name, 'rb')
            file_content = file.read()
      except Exception as exc:
            print('Error on opening file: ', client_file_name)
      
      if file_content:
            client_socket.send(file_content)


It receive the filename from the client and search the file in the current working directory. If anything go wrong, exception will be raised, nothing will be sent back to the client.

client_socket.close()

Then we close the client_socket object.

Let's test our TCP client and server together. Run the server first and then run the client.


Then we type the server IP and port in client terminal and server and client are connected.


Next, we type in the file name we want to download from the server. If server could find the file, it will send its content back to the client; after that server close the connection waiting for next connection.


If server could not find the file, errors will be printed on server's terminal, and nothing will be sent. Client will not create the file.


In summary, we could use both UDP and TCP to transmitt data over networks. UDP is fast but not as reliable as TCP. For most web applications, we always use TCP.

TCP server and client takes their own specific role in transmitting data over networks. TCP server must bind a local address while a TCP client may or may not bind a local port. Pay attention to the socket that receive and send data on the server side; it is the socket returned by the accept() method not the server socket itself.










Comments

Popular posts from this blog

How to create a memory puzzle game with Python and Pygame (#005)

How to write a slide puzzle game with Python and Pygame (2020 tutorial)

Introduction to multitasking with Python #003 gevent (2020 tutorial)