import socket 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# This creates a socket

# AF_INET => family ipv4. 
# SOCK_STREAM => TCP protocol.

"""
Explanation: Network Programming

Sockets


We have talked about varoius ways of getting networking. One way all of us are familiar are is HTTP, even FTP. Infact even bluetooth is a way of networking.


At the heart of networking is sockets. They form the lowest level of networking.
End of explanation
"""


import socket 


def client_handler(client_sock):
    # Do things here

    # .send() Takes byte type object
    # b' it indicates that the literal should become a bytes literal in Python 3
    client_sock.send(b'Thank you for connecting') 

    # Close the connection with the client
    client_sock.close()


# creates the socket
server = socket.socket()

PORT = 1234

# bind the socket to the port. 
server.bind(('localhost',PORT))   # takes a tuple

# at most 5 connections in the queue
server.listen(5)   

print("Listening for clients to connect")

while True:
 
    # Establish connection with client.
    client, addr = server.accept()     # waits until a connection arrives i.e (blocking).
    print('Got connection from', addr)
    client_handler(client)
   

"""
Explanation: Sockets are the endpoints of a bidirectional communications channel. 

Sockets can be used to communicate within a process, between processes on the same machine, or even between processes on different computers.


One socket(node) listens on a particular port at an IP, while other socket reaches out to the other to form a connection. 

Server forms the listener socket while client reaches out to the server.



TCP protocol(Transmission Control Protocol ): 
TCP is a connection-oriented, UDP isnt.
TCP is highly reliable, as in a packet is lost in trasnmission, it is resent. Also gaurentees the order of packets
TCP is slow and UDP is fast



We will first try to make a simple TCP server and client
End of explanation
"""


from socket import *

PORT = 1234
client = socket()

address=("localhost", PORT)

# connect to the given address
client.connect(address)

print(client.recv(1024))

client.close()

"""
Explanation: To check if the socket is actually created one can check using the command netstat -nlp on the command line.
End of explanation
"""


from threading import *

def lift_off(number):
    for i in range(3,0, -1):
        print("#"+ str(i) + "("+ str(number) + ") ")

for x in range(7):
    Thread(target=lift_off, args=(x,)).start()


"""
Explanation: Blocking Execution : These functions/constructs stop main execution until its own execution ends. Also called synchronous. 
Non-Blocking : These functions return immediately and continue execution sperately. Also called asynchronous. 
Can be obtained by the use of async/await
Or by using threads.



Threads

Threads are constructs that enable parallel (actually concurrent) execution of codes.
Thread class is available in the threading module.
One has to override the run() method of Thread class to inherit it.

An example:
End of explanation
"""


# Inheriting from the Thread class

from threading import *

class lift_off(Thread):
    def __init__(self, number):
        Thread.__init__(self)
        self.number = number
    def run(self):
        for i in range(3,0, -1):
            print("#"+ str(i) + "("+ str(self.number) + ") ")

for x in range(7):
    lift_off(5).start()

"""
Explanation: So the threads execute concurrently independent of each other.
End of explanation
"""


