01.pythonネットワーク電子ホワイトボード雛形

27646 ワード

1.ユーザー登録機能の実現
1.新しいクライアントが接続されると、文字列「HLO」の暗号コネクタが送信される.2.他のすべてのオンラインクライアントのニックネームを新しいクライアントに送信し、新しいクライアントがユーザーが入力したニックネームが合法かどうかを確認する.3.新しいクライアントからニックネームが送られてくるのを待って、ニックネームを受け取った後、Clientsリストに新しいクライアントを追加する.4.新しいクライアントuseridを他のクライアントに送信し、他のクライアントuseridを新しいクライアントに送信する.クライアントが切断され、他のクライアントに通知されます.
server.py
import socket
import threading
import time

# Here we have the global variables
# The Clients consists of the list of thread objects clients
# The logs consists of all the messages send through the server, it is used to redraw when someone new connects
Clients = []
Logs = {}


# -------------------------------SERVER ----------------------------------------
# This is the Server Thread, it is responsible for listening to connexions
# It opens new connections as it is a thread constantly listening at the port for new requests
class Server:
    ID = 1

    def __init__(self, host, port):
        self.host = host
        self.port = port

        # Initialize network
        self.network = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.network.bind((self.host, self.port))
        self.network.listen(10)
        print("The Server Listens at {}".format(port))

        # Start the pinger
        threading.Thread(target=self.pinger).start()

    # Here we have the main listener
    # As somebody connects we send a small hello as confirmation
    # Also we give him an unique ID that will be able to differentiate them from other users
    # We send the server logs so the new user can redraw at the same state in the board
    # We send the list of connected users to construct the permission system
    def start(self):
        while True:
            connexion, infos_connexion = self.network.accept()
            print("Sucess at " + str(infos_connexion))
            connexion.send('HLO'.encode())
            time.sleep(0.1)

            # Send all ID's so user cannot repeat any id's
            msg = " "
            for client in Clients:
                msg = msg + " " + client.clientID
            connexion.sendall(msg.encode())
            time.sleep(0.1)

            # Here we start a thread to wait for the users nickname input
            # We do this so a server can wait for a nickname input and listen to new connections
            threading.Thread(target=self.wait_for_user_nickname, args=[connexion]).start()

    # This function was created just to wait for the users input nickname
    # Once it's done if sends the logs so the user can be up to date with the board
    # And finally it creates the Client Thread which will be responsible for listening to the user messages
    def wait_for_user_nickname(self, connexion):
        # Receive the chosen ID from user
        try:
            new_user_id = connexion.recv(1024).decode()

            for log in Logs:
                connexion.send(Logs[log])

            new_client = Client(connexion, new_user_id)
            new_client.load_users()
            Clients.append(new_client)
            Server.ID = Server.ID + 1
            new_client.start()
        except ConnectionResetError:
            pass
        except ConnectionAbortedError:
            pass


    # Function used by pinger
    # Sends a removal message to alert all users of the disconnection
    def announce_remove_user(self, disconnectedClient):
        msg = 'RE' + ' ' + str(disconnectedClient.clientID) + ' ' + 'Ø'
        msg = msg.encode('ISO-8859-1')
        print(threading.enumerate())
        for client in Clients:
            client.connexion.sendall(msg)

    # This is the pinger function, it is used to check how many users are currently connected
    # It pings all connections, if it receives a disconnection error, it does the following things:
    # 1.Sends a removal message to alert all users of the disconnection
    # 2.Removes client from list of clients to avoid sending messages to it again
    # 3.Sends the permission to delete the disconnected user stuff from the board!
    def pinger(self):
        while True:
            time.sleep(0.1)
            for client in Clients:
                try:
                    msg = "ß".encode('ISO-8859-1')
                    print('ß')
                    client.connexion.send(msg)
                except ConnectionResetError:
                    client.terminate()
                    Clients.remove(client)
                    self.announce_remove_user(client)
                except ConnectionAbortedError:
                    client.terminate()
                    Clients.remove(client)
                    self.announce_remove_user(client)


# -----------------------------------CLIENTS -------------------------------------
# This is the client thread, it is responsible for dealing with the messages from all different clients
# There is one thread for every connected client, this allows us to deal with them all at the same time
class Client():
    MessageID = 0

    def __init__(self, connexion, clientID):
        self.connexion = connexion
        self.clientID = clientID
        self._run = True

    def load_users(self):
        for client in Clients:
            msg = 'A' + ' ' + str(client.clientID) + ' ' + 'Ø'
            self.connexion.send(msg.encode('ISO-8859-1'))
            msg = 'A' + ' ' + str(self.clientID) + ' ' + 'Ø'
            client.connexion.send(msg.encode('ISO-8859-1'))

    def terminate(self):
        self._run = False

    def start(self):
        while self._run:
            try:
                # Here we start by reading the messages
                # Split according to the protocol
                msg = ""
                while True:
                    data = self.connexion.recv(1).decode('ISO-8859-1')
                    if data == "Ø":
                        break
                    msg = msg + data

                splitted_msg = msg.split()

                # We do not want to keep the logs
                if splitted_msg[0] in ['TA']:
                    self.echoesAct3(msg)
                    continue
            # We pass the Connection Reset Error since the pinger will deal with it more effectivelly
            except ConnectionResetError:
                pass
            except ConnectionAbortedError:
                pass

    # Main echoes function!
    # This is responsible for echoing the message between the clients
    def echoesAct3(self, msg):
        msg = msg + " Ø"
        msg = msg.encode('ISO-8859-1')
        for client in Clients:
            client.connexion.sendall(msg)

if __name__ == "__main__":
    host = ''
    port = 5000
    server = Server(host, port)
    server.start()


graphical_widgets.py ExternalWindowsクラスは、ユーザがipポートを入力するインタフェースとニックネームを入力するインタフェースを実現する
from tkinter import *
import tkinter.font as font

class ExternalWindows:

    def __init__(self):
        pass

    # Default ip and port for debbuging
    _IP = "127.0.0.1"
    _Port = 5000
    # Text for the drawing text part!
    _Text = "WOW"
    _Nickname = "lol"

    # This temporary variable is used to get any other things we might need from the user
    # A little bit confusing but it works
    _Temp = ""

    # A flag to check whether you press the default exit button
    _Flag = False

    # This method is used to show error boxes
    # Everytime an error message we show a box with the given message
    @classmethod
    def show_error_box(cls, msg):
        master = Tk()
        Label(master, text= msg).grid(row=0)
        Button(master, text='OK', command= master.destroy ).grid(row=1,  pady=4)
        master.mainloop()

    # This is the method that is used to get the ip and the port from the user!
    # It sets the protected class variable _Ip and _port to the values given by our user
    # This value is set by inputing the value in the widgets
    @classmethod
    def getValuesFromUser(cls):
        def show_entry_fields():
            try:
                cls._IP = e1.get()
                cls._Port = int(e2.get())
                cls._Flag = True
            except:
                pass
            master.destroy()

        def exit_program():
            exit()

        cls._Flag = False
        master = Tk()
        Label(master, text="Please type the host information").grid(row=0)
        Label(master, text="IP:").grid(row=1)
        Label(master, text="Port:").grid(row=2)

        e1 = Entry(master)
        e2 = Entry(master)

        e1.grid(row=1, column=1)
        e2.grid(row=2, column=1)

        # Button(master,text='Start',command=master.quit).grid(row=3,column=1,sticky=W,pady=4)
        #Button(master, text='Set', command=show_entry_fields).grid(row=3, column=0, sticky=W, pady=4)
        #button_set = Button(master, text="Set", command=show_entry_fields).grid(row=3, column=0, sticky=W, pady=4)
        button = Button(master)
        button.config(text="Set", command=show_entry_fields)
        button.grid(row=3, column=0, sticky=W, pady=4)
        Button(master, text='Exit Program', command=exit_program).grid(row=4, column=0, sticky=W, pady=4)
        master.bind('', lambda event=None: button.invoke())
        master.mainloop()

        cls.check_ip_and_port()


    # This method checks using regular expressions to see if the IP and port number
    # are within valid parameters
    @classmethod
    def check_ip_and_port(cls):
        if cls._Flag == False:
            exit()
        expression = r"^(?=.*[^\.]$)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.?){4}$"
        if re.search(expression, cls._IP) is None:
            cls.show_error_box("Please type a valid IP address")
            cls.getValuesFromUser()

        if (type(cls._Port) != int or cls._Port <= 1024 or cls._Port > 50000):
            cls.show_error_box("Please type a valid port number")
            cls.getValuesFromUser()

        print("Information received IP: {} Port: {}".format(cls._IP, cls._Port))


    # Method for getting text from user
    # This is used to print the text on the drawing board!
    @classmethod
    def get_text_from_user(cls):

        def get_text():
            temp = e1.get()
            master.destroy()
            if( "Ø" in temp):
                cls.show_error_box("Invalid character in expression")
            else:
                cls._Text = temp

        master = Tk()
        Label(master, text="What do you want to write?").grid(row=0)
        e1 = Entry(master)
        Label(master, text="Text: ").grid(row=1)
        e1.grid(row=1, column=1)

        button = Button(master)
        button.config(text='Set', command=get_text)
        button.grid(row=2, column=0, sticky=W, pady=4)
        master.bind('', lambda event=None: button.invoke())
        master.mainloop()

    # This class is used to retrieve the user's selected nickname
    @classmethod
    def get_nickname_from_user(cls):
        def get_text():
            try:
                cls._Nickname = e1.get()
                cls._Flag = True
            except:
                pass
            master.destroy()
        cls._Flag = False
        master = Tk()
        Label(master, text="Choose a Nickname").grid(row=0)
        e1 = Entry(master)
        Label(master, text="Text: ").grid(row=1)
        e1.grid(row=1, column=1)

        button = Button(master)
        button.config(text='Set', command=get_text)
        button.grid(row=2, column=0, sticky=W, pady=4)
        master.bind('', lambda event=None: button.invoke())
        Button(master, text='Exit', command= exit).grid(row=2, column=0, pady=4)
        master.mainloop()

        cls.check_nickname()

    # This function is used to check to see if the nickname is within valid parameters
    # We only allow letters and 6 characters long
    # Why 6? Cause i wanted that way, it's a nickname for God sake
    @classmethod
    def check_nickname(cls):
        if cls._Flag == False:
            exit()
        if (len(cls._Nickname) > 6):
            ExternalWindows.show_error_box("Please choose a shorter nickname. 6 characters long")
            cls.get_nickname_from_user()

        expression = r"^[a-zA-Z]+$"
        if re.search(expression, cls._Nickname) is None:
            cls.show_error_box("Only letters")
            cls.get_nickname_from_user()

    # This method was created for getting general data from the user
    # It's not currently used, but it was implemented due to it's potential
    # It may be necessary to get things for the user within new updates
    # Therefore a flexible method that allows us to get anything we want serves it purpose
    @classmethod
    def get_anything_from_user(cls, msg):

        def get_text():
            cls._Temp = e1.get()
            master.destroy()

        master = Tk()
        Label(master, text = msg).grid(row=0)
        e1 = Entry(master)
        Label(master, text="Text: ").grid(row=1)
        e1.grid(row=1, column=1)

        Button(master, text='Set', command=get_text).grid(row=2, column=0, sticky=W, pady=4)
        master.mainloop()

    # Return methods for the protected variables!
    @classmethod
    def return_ip(cls):
        return cls._IP

    @classmethod
    def return_port(cls):
        return cls._Port

    @classmethod
    def return_text(cls):
        return cls._Text

    @classmethod
    def return_nickname(cls):
        return cls._Nickname

    @classmethod
    def return_temp(cls):
        return cls._Temp

if __name__ == '__main__':

    ExternalWindows.getValuesFromUser()
    print(ExternalWindows.return_ip())


    ExternalWindows.get_nickname_from_user()
    print(ExternalWindows.return_nickname())


whiteboard.py電子ホワイトボードインタフェースの実現
from tkinter import *

class Whiteboard:

    # Here we initiate with the line drawing tool, this is the tool currently used to draw
    drawing_tool = "line"
    # Here we have the dictionary with the used colors to paint!
    Colors = {'b': 'blue', 'r': 'red', 'g': 'green', 'o': 'orange', 'y': 'yellow', 'c': 'cyan', 'p': 'purple1',
              'd': 'black', 's': 'snow'}

    # Here we initiate the whiteboard by calling all the functions necessary to construct it
    # And also initiate the parent class!
    # We call the save and load and permission classes here, we need them to instantiate the buttons
    def __init__(self):
        self._init_whiteboard()
        self._init_item_button()
        # self._init_user_button()
        self._init_color_button()
        self._init_drawing_area()
        self.color = 'b'

    # Here we have the main loop of the Whiteboard when it is closed it executes the code just bellow
    # Which raises an exception that closes the software
    def show_canvas(self):
        mainloop()
        raise Exception("Board Closed Ending Execution")

    # Here we initiate the whiteboard with Tk() and set it's dimensions
    def _init_whiteboard(self):
        self.myWhiteBoard = Tk()
        self.myWhiteBoard.geometry('2000x1100')

    # ---------------------------------- Button functions ------------------------------------------
    # Here we have the buttons on the top of the whiteboard
    # Those buttons are responsible for changing the drawing tool as their name indicates
    # Every button pressed is a different drawing tool
    def _init_item_button(self):
        Button(self.myWhiteBoard, text='line', height=1, width=5, bg='dark goldenrod', font='Arial',
               command=lambda: self.set_drawing_tool('line')).place(x=70, y=0)
        Button(self.myWhiteBoard, text='rect', height=1, width=5, bg='saddle brown', font='Arial',
               command=lambda: self.set_drawing_tool('rectangle')).place(x=140, y=0)
        Button(self.myWhiteBoard, text='oval', height=1, width=5, bg='NavajoWhite4', font='Arial',
               command=lambda: self.set_drawing_tool('oval')).place(x=210, y=0)
        Button(self.myWhiteBoard, text='text', height=1, width=5, bg='SteelBlue4', font='Arial',
               command=self.get_text_from_user).place(x=280, y=0)
        Button(self.myWhiteBoard, text='pencil', height=1, width=5, bg='DeepSkyBlue2', font='Arial',
               command=lambda: self.set_drawing_tool('pencil')).place(x=350, y=0)
        Button(self.myWhiteBoard, text='circle', height=1, width=5, bg='Turquoise2', font='Arial',
               command=lambda: self.set_drawing_tool('circle')).place(x=420, y=0)
        Button(self.myWhiteBoard, text='square', height=1, width=5, bg='CadetBlue1', font='Arial',
               command=lambda: self.set_drawing_tool('square')).place(x=490, y=0)
        Button(self.myWhiteBoard, text='eraser', height=1, width=5, bg='purple1', font='Arial',
               command=lambda: self.set_drawing_tool('eraser')).place(x=560, y=0)
        Button(self.myWhiteBoard, text='drag', height=1, width=5, bg='green', font='Arial',
               command=lambda: self.set_drawing_tool('drag')).place(x=630, y=0)
        # Button(self.myWhiteBoard, text='delALL', height=1, width=5, bg='snow', font='Arial',
        #        command=self.erase_all).place(x=700, y=0)

    # This is the own user button, it is used mostly as a display of the user name
    def _init_user_button(self):
        Button(self.myWhiteBoard, text=self.my_connexion.ID, height=1, width=5, bg='snow').place(x=1100, y=0)

    # This are the color buttons, they are responsible for changing the colors of the corresponding drawings
    def _init_color_button(self):

        Button(self.myWhiteBoard, height=1, width=5, bg='red',
               command=lambda: self.set_color('red')).place(x=1010,y=50)
        Button(self.myWhiteBoard, height=1, width=5, bg='orange',
               command=lambda: self.set_color('orange')).place(x=1010, y=100)
        Button(self.myWhiteBoard, height=1, width=5, bg='yellow',
               command=lambda: self.set_color('yellow')).place(x=1010, y=150)
        Button(self.myWhiteBoard, height=1, width=5, bg='green',
               command=lambda: self.set_color('green')).place(x=1010, y=200)
        Button(self.myWhiteBoard, height=1, width=5, bg='cyan',
               command=lambda: self.set_color('cyan')).place(x=1010, y=250)
        Button(self.myWhiteBoard, height=1, width=5, bg='blue',
               command=lambda: self.set_color('blue')).place(x=1010, y=300)
        Button(self.myWhiteBoard, height=1, width=5, bg='purple1',
               command=lambda: self.set_color('purple1')).place(x=1010, y=350)
        Button(self.myWhiteBoard, height=1, width=5, bg='black',
               command=lambda: self.set_color('black')).place(x=1010, y=400)
        Button(self.myWhiteBoard, height=1, width=5, bg='snow',
               command=lambda: self.set_color('snow')).place(x=1010, y=450)
        # Button(self.myWhiteBoard, height=1, width=5, bg='snow', text="Save", command=self.save_and_load.save).place(x=1010,
        #                                                                                                    y=500)
        # Button(self.myWhiteBoard, height=1, width=5, bg='snow', text="Load", command=self.save_and_load.load).place(x=1010,
        #                                                                                                    y=550)



    # Here we initiate the drawing area, which is a canvas
    # and place it accordingly
    def _init_drawing_area(self):
        self.drawing_area = Canvas(self.myWhiteBoard, width=1000, height=1000, bg='white')
        self.drawing_area.place(y=50)

    # ---------CHANGE DRAWING TOOL---------------
    # Here we change the drawing tools according to the widget that was pressed on the top
    def set_drawing_tool(self, tool):
        self.drawing_tool = tool

    # This functions are called when the user presses color button!
    # They set the byte that will be send on the message to send the color to be used to draw
    def set_color(self, color):
        color_to_set = [k for k, v in self.Colors.items() if v == color]
        if len(color_to_set) == 1:
            self.color = color_to_set[0]
        else:
            print("Unknown color, check the code!")

    #################################GETIING THE TEXT##################################################################
    # This part gets text from the user before printing it!
    # It refers to the text functionality of the text button widget on the top
    def get_text_from_user(self):
        self.drawing_tool = 'text'


    # ----------------------------- Erase All Function -----------------------------------------------------------------
    # Since this is an extra functionality i will explain it more extensively
    # This function finds every object tagged with the user nickname (user ID)
    # And also every single object tagged with an user which is in his list of permissions
    # Since every user is in it's own list of permissions, we only need to check the list of permissions
    # Disconnected users loose their privileges!
    # Them it sends a delete message for every one of them!

if __name__ == '__main__':
    wb = Whiteboard()
    wb.show_canvas()

network.pyプロキシクライアントメッセージ送受信
import socket
from graphical_widgets import ExternalWindows

# Here we have the connection class! It is responsible for starting the connection with the server
# It also does the message sending and receiving part (handles all messages)
class MConnection:

    # Here we have the first part, we start the program by using the getValuesFromUser function
    # This function is the main box that appears requesting the IP and the port from the user
    # From this class we recover the values that have been typed on the widget to start our connection!
    def __init__(self):
        ExternalWindows.getValuesFromUser()
        self._host = ExternalWindows.return_ip()
        self._port = ExternalWindows.return_port()

        # Here we attempt to establish a connection
        # We open a socket in the given port and IP
        # And start by checking to see if we received a greeting 'HLO'
        # Afterwards the server will send a list with all the names of the connected users
        # This is done to avoid repeating names when creating a new user
        # After the id has been chosen it responds to the server so it can add to the list of clients
        try:
            self.s = socket.socket()
            self.s.connect((self._host, self._port))
            data = self.s.recv(3).decode()
            if data == 'HLO':
                print('[Network]Connection with %s:%s established.' % (self._host, self._port))

            data = self.s.recv(1024).decode()
            UserNames = data.split()
            print(UserNames)

            while True:
                ExternalWindows.get_nickname_from_user()
                self._ID = ExternalWindows.return_nickname()
                if (self._ID in UserNames):
                    ExternalWindows.show_error_box("User name is taken")
                    continue
                break

            self.s.sendall(self._ID.encode())
            print("Received ID is : " + self._ID)
        except SystemExit:
            exit()
        except:
            ExternalWindows.show_error_box("Could not connect to server")
            raise Exception("Connection Failed")

    # Here we have the send message function
    # The messages are received in the form of a tuple (A,B,C)
    # And in here they are transformed in a binary message of the form b"A B C Ø"
    # The Ø indicates the end of the message! and the type and beginning of the message is indicated
    # with a set of specific letters.
    def send_message(self, msg):
        msg = ' '.join(map(str, msg))
        msg = msg + " Ø"
        try:
            msg = msg.encode('ISO-8859-1')
            self.s.send(msg)
        except UnicodeEncodeError:
            ExternalWindows.show_error_box("Invalid character!")

    # Here we receive the messages, we take in a message of the form b"A B C Ø" and transform it
    # We transform it in a tuple of format (A,B,C)
    # From that tuple each class will recover the essential information for all it's functions
    # Now the ß that is ignored is a ping from the server
    # It is used to detect if users are still connected and act accordingly to their connection
    # So we ignore it when receiving messages
    def receive_message(self):
        msg = ""
        while True:
            data = self.s.recv(1).decode('ISO-8859-1')
            if data == "Ø":
                break
            if data == "ß":
                # print('receive ß')
                continue
            msg = msg + data
        msg = msg.split()
        return msg


    # Encapsulation of the USER ID ##########################################################
    def get_user_id(self):
        return self._ID

    def set_user_id(self, ID):
        self._ID = ID

    ID = property(get_user_id, set_user_id)

if __name__ == '__main__':
    m = MConnection()
    print('bye')


client.pyマスタ
import time
from threading import Thread
from graphical_widgets import ExternalWindows
from network import MConnection
from whiteboard import Whiteboard


class Client(Thread,Whiteboard):


    def __init__(self):
        self.my_connexion = MConnection()
        Whiteboard.__init__(self)
        Thread.__init__(self)
        self.setDaemon(True)

        # This part refers to the class that allows user to exchange messages between themselves

    # The run handles the messages
    # As it recognizes the type it assigns to the proper class that will handle it!
    def run(self):
        while True:
            try:
                msg = self.my_connexion.receive_message()
                if msg[0] == "ß":
                    print(msg[0])
            except ValueError:
                pass
            except IndexError:
                pass
            except ConnectionResetError:
                ExternalWindows.show_error_box("Server down please save your work")
                self.save_and_load.save()
                self.myWhiteBoard.destroy()



if __name__ == '__main__':
    c = Client()
    c.start()
    c.show_canvas()