配套视频教程地址
1.实现用户登录功能
1.当有新客户端连接上来时,发送字符串'HLO'暗号接头;
2.发送所有其他在线客户端的昵称给新客户端,以便新客户端确认用户输入的昵称是否合法;
3.等待新客户端发送过来其昵称,收到昵称后,在Clients列表中添加新客户端;
4.发送新客户端userid给其他客户端,发送其他客户端userid给新客户端
5.有客户端断开,通知其他客户端
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('<Return>', 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('<Return>', 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('<Return>', 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()
网友评论