from tkinter import * from tkinter.filedialog import asksaveasfilename import threading import socket # GLOBALS username = "나" connectionSocket = 0 # ----------------------------------------------------------------- # 메뉴 관련 함수 # 파일 대화상자 사용하여 채팅 내용 저장 함수 def saveHistory(): # 파일 저장 대화상자 생성 file_name = asksaveasfilename( title="채팅 내용 저장", filetypes=[('Plain text', '*.txt'), ('Any File', '*.*')]) try: filehandle = open(file_name + ".txt", "w") except IOError: print("채팅 내용 저장 실패.") return # 메인 스크린 텍스트 위젯 내용 읽기 contents = main_body_text.get(1.0, END) # 각 라인 읽어들여 파일에 저장 for line in contents: filehandle.write(line) filehandle.close() # 사용자 이름 지정 옵션 윈도우 생성 함수 def username_options_window(master): top = Toplevel(master) top.title("사용자 이름 옵션") top.grab_set() # 모든 이벤트를 현재 윈도우(Toplevel)에 한정 Label(top, text="사용자 이름:").grid(row=0) name = Entry(top) name.focus_set() # 키보드 입력 포커스 name.grid(row=0, column=1) go = Button(top, text="변경", command=lambda:username_options_go(name.get(), top)) go.grid(row=1, column=0, columnspan=2, sticky=W+E) # 1행 중앙에 배치 # 사용자 이름 지정 이벤트 함수 def username_options_go(name, window): for letter in [name]: if letter == " " or letter == "\n": error_window(root, "잘못된 사용자 이름. 공백문자 허용 안됨.") # 에러 표시 윈도우 생성 return global username username = name writeToScreen("변경된 사용자 이름: " + name, "시스템") # 메인 스크린 텍스트 위젯에 출력 window.destroy() #---------------------------------------------------------------------------- # 서버 연결 윈도우 # 서버 포트 입력 윈도우 생성 함수 def server_options_window(master): top = Toplevel(master) top.title("서버 포트 연결 옵션") top.grab_set() # 발생되는 이벤트를 현재 윈도우(Toplevel)에 한정 top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top)) Label(top, text="포트:").grid(row=0) port = Entry(top) port.grid(row=0, column=1) port.focus_set() go = Button(top, text="시작", command=lambda: server_options_go(port.get(), top)) go.grid(row=1, column=1) # 서버 연결 처리 이벤트 함수 def server_options_go(port, window): if options_sanitation(port): # 포트 입력 올바름 조사 window.destroy() Server(int(port)).start() # 서버 스레드 실행 시작 # 윈도우 닫기 시 처리되는 함수 def optionDelete(window): connecter.config(state=NORMAL) # 연결 버튼 활성화 window.destroy() # 채팅 종료 함수 def exitChat(window): if connectionSocket != 0: connectionSocket.close() # 연결 소켓 종료 if clientType == 1: # 서버 종료 serv.close() # 서버 소켓 종료 window.destroy() #---------------------------------------------------------------------------- # 클라이언트 연결 윈도우 # 클라이언트의 서버 주소 입력 윈도우 생성 함수 def client_options_window(master): top = Toplevel(master) top.title("클라이언트 연결 옵션") top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top)) top.grab_set() # 발생되는 이벤트를 현재 윈도우(Toplevel)에 한정 Label(top, text="서버 IP:").grid(row=0) location = Entry(top) location.grid(row=0, column=1) location.focus_set() Label(top, text="포트:").grid(row=1) port = Entry(top) port.grid(row=1, column=1) go = Button(top, text="시작", command=lambda: client_options_go(location.get(), port.get(), top)) go.grid(row=2, column=1) # 클라이언트 연결 처리 이벤트 함수 def client_options_go(dest, port, window): if options_sanitation(port, dest): window.destroy() Client(dest, int(port)).start() # 클라이언트 스레드 실행 시작 # 포트와 IP 주소의 올바름 검사 함수 def options_sanitation(por, loc=""): if not por.isnumeric(): error_window(root, "포트 번호를 입력해주세요.") return False if int(por) < 0 or 65555 < int(por): error_window(root, "0 과65555 사이 번호를 입력해주세요.") return False if loc != "": if not ip_process(loc.split(".")): error_window(root, "올바른 IP 주소를 입력해주세요.") return False return True # 유효한 IP 주소인지를 검사 함수 def ip_process(ipArray): if len(ipArray) != 4: return False for ip in ipArray: if not ip.isnumeric(): return False t = int(ip) if t < 0 or 255 < t: return False return True # 에러 표시 윈도우 생성 함수 def error_window(master, texty): window = Toplevel(master) window.title("에러") window.grab_set() Label(window, text=texty).pack() go = Button(window, text="OK", command=window.destroy) go.pack() go.focus_set() # -------------------------------------------------------- # 연결 버튼 및 라디오 버튼 이벤트 관련 함수 # 연결 버튼 이벤트 함수 def connects(clientType): connecter.config(state=DISABLED) # 버튼 비활성화 if clientType == 0: # 클라이언트 client_options_window(root) if clientType == 1: # 서버 server_options_window(root) # 클라이언트 라디오 버튼 이벤트 함수 def toOne(): global clientType clientType = 0 # 서버 라디오 버튼 이벤트 함수 def toTwo(): global clientType clientType = 1 # ----------------------------------------------------------------- # 텍스트 입출력 관련 함수 # 텍스트를 입력 def processUserText(event): data = text_input.get() # 엔트리 메시지 입력 placeText(data) # 메인 스크린에 출력하고 전송 text_input.delete(0, END) # 입력 엔트리 클리어 # 텍스트를 스크린에 출력하고 상대방에게 전송 def placeText(text): writeToScreen(text, username) # 메인 스크린에 출력 netThrow(connectionSocket, text) # 상대방에 메시지 전송 # 텍스트를 메인 스크린에 "[username] text" 형식으로 출력 def writeToScreen(text, username=""): main_body_text.config(state=NORMAL) # 메인 스트린 비활성화 main_body_text.insert(END, '\n') # 개행문자 출력 if username: main_body_text.insert(END, "[" + username + "] ") # 사용자 이름 출력 main_body_text.insert(END, text) # 메시지 출력 main_body_text.yview(END) # 수직 방향 뷰 변경 표시 main_body_text.config(state=DISABLED) # 메인 스크린 비활성화 # ----------------------------------------------------------------- # 메시지 송수신 관련 함수 # 상대방(conn 소켓)에 메시지를 전송 def netThrow(conn, message): try: # 문자열 인코드하여 데이터 송신 conn.send(message.encode("utf8")) except socket.error: writeToScreen("메시지 송신 실패.", "시스템") # conn 소켓에서 메시진 수신 def netCatch(conn): try: message = conn.recv(80) return message.decode("utf8") except socket.error: writeToScreen("메시지 수신 실패.", "시스템") #------------------------------------------------------------------------- # Server 클래스: Thread 클래스 상속 class Server (threading.Thread): # 생성자 def __init__(self, port): threading.Thread.__init__(self) # 부모 클래스 생성자 호출 self.port = port # 포트 지정 def run(self): global serv # 서버 소켓 생성 serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serv.bind(('', self.port)) # 주소와 소켓 결합 serv.listen(1) # 연결 요청 청취 global connectionSocket writeToScreen("포트 " + str(self.port) + "에서 연결 대기 중 ....", "시스템") # 연결 요청 수락, 연결 소켓 생성(클라이언트 IP 주소 받음) connectionSocket, addr = serv.accept() writeToScreen(str(addr[0]) + " 연결됨.", "시스템") # 메시지 수신 스레드 생성 threading.Thread(target=Runner, args=(connectionSocket,str(addr[0]))).start() #------------------------------------------------------------------------- # Client 클래스: Thread 클래스 상속 class Client (threading.Thread): # 생성자 def __init__(self, host, port): threading.Thread.__init__(self) # 부모 클래스 생성자 호출 self.port = port # 포트 지정 self.host = host # IP 주소 지정 def run(self): global connectionSocket # 소켓 생성 connectionSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 서버에 연결 요청 connectionSocket.connect((self.host, self.port)) writeToScreen("IP 주소 " + self.host + ", 포트 " + str(self.port) + " 에 연결됨.", "시스템") # 메시지 수신 스레드 생성 threading.Thread(target=Runner, args=(connectionSocket, self.host)).start() # 메시지 수신 스레드 함수 def Runner(conn, addr): while 1: data = netCatch(conn) # 메시지 수신 writeToScreen(data, addr) # -------------------------------------------------------- # -------------------------------------------------------- # 루트 애플리케이션 윈도우 생성 root = Tk() root.title("GUI 채팅") # 메뉴바 생성 menubar = Menu(root) root.config(menu=menubar) # 파일 메뉴 생성 및 메뉴바 연결 file_menu = Menu(menubar, tearoff=0) # tearoff = 0, 점선표시 제거 menubar.add_cascade(label="파일", menu=file_menu) # 파일 메뉴 항목 추가 file_menu.add_command(label="저장", command=lambda: saveHistory()) file_menu.add_command(label="사용자 이름", command=lambda: username_options_window(root)) file_menu.add_command(label="종료", command=lambda: exitChat(root)) # 메인 디스플레이 프레임 생성 main_body = Frame(root) # 메인 스크린 텍스트 위젯 생성 main_body_text = Text(main_body) #main_body_text.focus_set() main_body_text.pack(side=LEFT, fill=Y) # 스크롤바 위젯 객체 생성 body_text_scroll = Scrollbar(main_body) body_text_scroll.pack(side=RIGHT, fill=Y) # 스크롤바와 텍스트 위젯 연동 body_text_scroll.config(command=main_body_text.yview) main_body_text.config(yscrollcommand=body_text_scroll.set) main_body.pack() # 텍스트에 환영 인사 출력 main_body_text.insert(END, "GUI 채팅, 환영합니다!") main_body_text.config(state=DISABLED) # 텍스트 비활성화 # 메시지 입력 엔트리 위젯 생성 text_input = Entry(root, width=60) # 너비 60(텍스트) text_input.bind("", processUserText) text_input.pack() # 서버/클라이언트 지정 라디오 버튼 위젯 객체 생성 clientType = 1 Radiobutton(root, text="Client", variable=clientType, value=0, command=toOne).pack(anchor=E) Radiobutton(root, text="Server", variable=clientType, value=1, command=toTwo).pack(anchor=E) # 연결 버튼 위젯 생성 statusConnect = StringVar() statusConnect.set("연결") connecter = Button(root, textvariable=statusConnect, command=lambda: connects(clientType)) connecter.pack() root.mainloop()