파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

안녕하세요 멍개입니다. 이번글에서는 파이썬을 이용하여 채팅프로그램을 만들어보도록 하겠습니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

[그림 1]

다른 자료들을 통해서 만들게 되면 [그림 1]처럼 CLI 형태로만 구현이 되있습니다.

물론 저게 핵심 기능이긴 하지만 공부하고 만드는 입장에선 저게머지? 라는 생각을 할 수 있기 때문에 저는 GUI 형태로 만들어 보도록 하겠습니다.

· 사용할 라이브러리

채팅 프로그램을 만들기 위해사용할 대표적인 라이브러리입니다.

● UI 만들기

GUI 프로그램을 만드는 만큼 가장먼저 해야할 작업은 UI를 만들어야 합니다. pyqt는 qt designer라는 프로그램을 이용하여 UI를 만들 수 있습니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

qt designer 프로그램을 이용하여 UI를 설계해보겠습니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

UI 설계시 중요한 점은 object_name을 잘 다뤄줘야 합니다. object_name을 이용하여 이벤트를 다룰 수 있기 때문입니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

저장을 하면 *.ui 이름으로 XML 파일을 생성합니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

UI는 다 만들었으니 이제 코드를 작성해보도록 하겠습니다.

이제 실질적인 코드를 작성해야 하는데...

서버코드를 먼저 작성해도 되고 클라이언트 코드를 작성해도 되지만 여기서는 클라이언트 코드를 먼저 작성하겠습니다.

일단 서버가 없기 때문에 메시지를 받는것은 안되기 때문에 일단 메시지를 입력하면 메시지가 뜨고 랜덤하게 메시지를 받는것처럼 구현한 후 서버완성되면 연동을 하는 형태로 진행하겠습니다.

● 클라이언트 구현 1

클라이언트 구현을 위해 앞에서 만든 *.ui파일을 띄워보겠습니다.

· *.ui 파일 연결

import sys from PyQt5.QtWidgets import QMainWindow, QFileDialog, QTableWidgetItem, QMessageBox, QApplication from PyQt5.QtCore import QThread, pyqtSlot from PyQt5 import QtCore from PyQt5 import uic ui_form = uic.loadUiType("main.ui")[0] class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) if __name__ == "__main__": app = QApplication(sys.argv) myWindow = ChatWindow() myWindow.setWindowTitle('멍개의 채팅 프로그램') myWindow.show() app.exec_()

해당 코드를 실행하면 다음과 같이 실행 결과를 볼 수 있습니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

화면에 잘 뜨는것을 확인할 수 있습니다.

다음으로 할 작업은 입력을 누르면 입력한 메시지를 전송하기 전에 화면에 띄워주겠습니다.

· 입력한 메시지 출력

코드를 짜기전에 생각해야할 점은 다음과 같습니다.

1. 입력버튼에 클릭 이벤트를 건다

2. 입력한 메시지를 가져온다.

3. 메시지를 화면에 뿌려준다.

차근차근 짜보겠습니다.

class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) # 입력 버튼 클릭시 send_message 호출 def send_message(self): print(self.input_message.toPlainText()) # object_name이 input_message에서 입력값 가져오기

ChatWindow 클래스는 우리가 만든 UI 파일을 상속받았기 때문에 object_name으로 접근가능합니다.

self.[object_name]으로 우리가 만든 UI 파일에서 각 요소에 접근할 수 있으며 clicked.connect는 클릭이 발생하면 첫 번째 인자로 전달한 send_message 메서드를 호출합니다.

첫 번째, 두 번째 과정이 끝났으니 다음 과정으로 입력된 값을 화면에 뿌려보겠습니다.

def send_message(self): msg = self.input_message.toPlainText() self.add_chat('[나] %s'%(msg)) def add_chat(self, msg): self.chats.appendPlainText(msg)

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

입력 버튼을 누르면 입력한 메시지를 화면에 띄워줍니다. 음.... 보다보니깐 입력 버튼보단 전송 버튼이 날 것 같습니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

입력 버튼을 전송 버튼으로 바꿔주겠습니다.

테스트를 하다보니 조금 부자연스러운 부분이 있습니다. 전송 버튼을 누르고 입력창이 지워주면 편할것 같은 느낌이 들기 때문에 수정하도록 하겠습니다.

def send_message(self): msg = self.input_message.toPlainText() # 입력 메시지 가져오기 self.add_chat('[나] %s'%(msg)) # 화면에 출력 self.input_message.setPlainText('') # 입력창 지우기

음... 여기서 add_chat 메서드를 분리한 이유는 메시지를 수신할 때 self.chats.appendPlainText()를 직접호출하게 되면 해당 객체에 직접 접근을 해야하는데 만약, 방금처럼 *.ui 파일을 수정하면서 object_name 같이 특정 값이 바뀌었을 때 수정할 부분이 많이지기 때문입니다. 또한 메시지를 화면에 출력할 때 복잡한 로직이 들어갈 수 있는데 중복되는 코드를 방지하기 위함합니다.

다음으로 할 작업은 메시지 수신을 유사하게 랜덤으로 화면에 출력하도록 해보겠습니다.

· 메시지 수신 유사 코드작성 - 메시지 수신(생성) 쓰레드 분리

여기서는 쓰레드시그널/슬롯 개념이 등장합니다.

쓰레드를 생성하는 이유는 소켓 서버로부터 데이터를 수신하기 위해 대기중인 부분을 분리하기 위함입니다.

시그널/슬롯은 데이터 수신을 하는 쓰레드가 수신을 완료하면 시그널을 통해 화면을 띄우는 객체에 데이터를 전달할 수 있습니다.

먼저할 것은 쓰레드 생성입니다.

class SocketClient(QThread): add_chat = QtCore.pyqtSignal(str) def run(self): print('data receive listen')

QThread를 상속받는 클래스 하나 만들어 줍니다.

앞에서 만든 ChatWIndow 클래스의 생성자를 다음과같이 수정합니다.

def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) sc = SocketClient(self) # 쓰레드 객체 생성 sc.start(1) # 쓰레드 객체 실행(run() 메서드 실행)

쓰레드 객체를 생성하여 start() 메소드를 호출하면 해당 객체의 run() 메서드를 호출합니다. 자 이제 우리는 쓰레드 객체생성-실행을 연결 버튼을 눌렀을 때 동작하도록 수정하겠습니다.

class SocketClient(QThread): def run(self): print('data receive listen') class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) self.btn_connect.clicked.connect(self.socket_connection) def socket_connection(self): ip = self.input_ip.toPlainText() port = self.input_port.toPlainText() print(ip, port) self.sc = SocketClient(self) self.sc.start(1) def send_message(self): msg = self.input_message.toPlainText() self.add_chat('[나] %s'%(msg)) self.input_message.setPlainText('') def add_chat(self, msg): self.chats.appendPlainText(msg)

프로그램을 실행하고 연결 버튼을 누르면 print()가 실행되어 터미널에 data receive listen을 출력합니다.

여기서 보완할 점은 한번 연결이 되면 다시 연결이 되면 안됩니다. 중복적으로 연결이 되는 현상을 막아보겠습니다.

class SocketClient(QThread): def __init__(self, parent=None): super().__init__() self.main = parent self.is_run = False def run(self): self.is_run = not self.is_run print('data receive listen')

쓰레드 클래스의 동작 상태를 관리하는 변수를 생성자에서 만들어 줍니다.

run()이 호출되면 is_run을 True로 바꿔준 후 쓰레드.start() 하기 전에 is_run을 검사시킵니다.

class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) self.btn_connect.clicked.connect(self.socket_connection) self.sc = SocketClient(self) def socket_connection(self): ip = self.input_ip.toPlainText() port = self.input_port.toPlainText() print(ip, port) if not self.sc.is_run: self.sc.start(1)

채팅앱 클래스 생성자에서 쓰레드 객체를 생성한 후 is_run의 상태를 확인합니다.

여기서 쓰레드 객체를 생성자에서 생성하는 이유는 해당 객체는 하나만 만들어져야 하기 때문입니다.

def socket_connection(self): ip = self.input_ip.toPlainText() port = self.input_port.toPlainText() print(ip, port) self.sc = SocketClient(self) if not self.sc.is_run: self.sc.start(1)

이렇게 된다면 새로운 객체를 매번 생성하기 때문에 해당 객체의 is_run은 매번 False가 됩니다.

이렇게 짤 경우 SocketClient 클래스를 싱글톤객체나 is_run을 클래스 변수로 취급하면 해당 방식으로 작성해도 됩니다.

지금까지 작성된 코드는 다음과 같습니다.

import sys from PyQt5.QtWidgets import QMainWindow, QFileDialog, QTableWidgetItem, QMessageBox, QApplication from PyQt5.QtCore import QThread, pyqtSlot from PyQt5 import QtCore from PyQt5 import uic ui_form = uic.loadUiType("main.ui")[0] class SocketClient(QThread): def __init__(self, parent=None): super().__init__() self.main = parent self.is_run = False def run(self): self.is_run = not self.is_run print('data receive listen') class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) self.btn_connect.clicked.connect(self.socket_connection) self.sc = SocketClient(self) def socket_connection(self): ip = self.input_ip.toPlainText() port = self.input_port.toPlainText() print(ip, port) if not self.sc.is_run: self.sc.start(1) def send_message(self): msg = self.input_message.toPlainText() self.add_chat('[나] %s'%(msg)) self.input_message.setPlainText('') def add_chat(self, msg): self.chats.appendPlainText(msg) if __name__ == "__main__": app = QApplication(sys.argv) myWindow = ChatWindow() myWindow.setWindowTitle('멍개의 채팅 프로그램') myWindow.show() app.exec_()

다음 작업은 해당 쓰레드에서 메시지가 발생했을 때 ChatWindow의 add_chat 메서드에게 알려야 합니다. 이때 사용하는 기능이 시그널/슬롯 입니다.

class SocketClient(QThread): add_chat = QtCore.pyqtSignal(str) def __init__(self, parent=None): super().__init__() self.main = parent self.is_run = False def run(self): self.is_run = not self.is_run self.add_chat.emit('채팅 서버와 접속 완료했습니다.')

SocketClient 클래스에 add_chat 시그널을 만들어줍니다. 시그널은 신호를 보내주는 이벤트 객체이며 슬롯은 시그널이 보낸 신호를 받는 부분입니다.

add_chat은 str 타입을 전달할 수 있는 시그널입니다. 시그널을 발생할 땐 emit을 이용합니다.

class ChatWindow(QMainWindow, ui_form) : def __init__(self): super().__init__() self.setupUi(self) self.btn_send.clicked.connect(self.send_message) self.btn_connect.clicked.connect(self.socket_connection) self.sc = SocketClient(self) self.sc.add_chat.connect(self.add_chat) ... 중략 ... @pyqtSlot(str) def add_chat(self, msg): self.chats.appendPlainText(msg)

앱 객체 생성자에 SocketClient가 가지고 있는 시그널에 add_chat을 연결해줍니다. emit이 발생할 때 호출할 메서드를 전달하면 됩니다.

시그널에 이해 호출되는 add_chat()엔 pyqtSlot() 데코레이터를 달아줍니다. 사실 이 데코레이터는 달지 않아도 되지만 시각적으로 시그널에 의해 데이터를 받는다를 한눈에 보기 위해 필자는 달아주는 편입니다.

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi

· 전송버튼 개선

전송버튼 개선작업으로 플로우를 다음과 같이 수정하겠습니다.

1. 전송버튼 클릭

2. 입력 메시지 쓰레드로 전달

3. 쓰레드에서 시그널을 이용하여 화면에 출력

class SocketClient(QThread): ... 중략... def send(self, msg): self.add_chat.emit(msg)

class ChatWindow(QMainWindow, ui_form) : ... 중략 ... def send_message(self): if not self.sc.is_run: self.add_chat('서버와 연결 상태가 끊겨 있어 메시지를 전송할 수 없습니다.') return msg = self.input_message.toPlainText() self.sc.send('[나]:%s'%(msg)) self.input_message.setPlainText('')

SocketClient를 통해서 화면에 출력하는 이유는 추후에 소켓 서버에 데이터를 전달해야 하는데 소켓 서버에 전달하는 작업을 하는 쓰레드가 SocketClinet이기 때문입니다.

다음으로 서버와 연결 상태가 아니라면 화면에 연결상태가 아니라는 메시지를 출력합니다.

클라이언트가 얼추 완성 되었으니 이제 서버를 작업하겠습니다.

서버를 작업하고 클라이언트와 연동작업을 하도록 합니다.

● 서버생성

import eventlet import socketio sio = socketio.Server() app = socketio.WSGIApp(sio, static_files={ '/': {'content_type': 'text/html', 'filename': 'index.html'} }) @sio.event def connect(sid, environ): print('connect ', sid) print() @sio.event def my_message(sid, data): print('message ', data) print() @sio.event def disconnect(sid): print('disconnect ', sid) print() @sio.on('send') def send(sid, data): print('send message ', data) sio.emit('receive', str(data), skip_sid=sid) if __name__ == '__main__': eventlet.wsgi.server(eventlet.listen(('', 5000)), app)

해당 코드를 실행하면 소켓서버를 실행합니다.

send로 이벤트를 발생하면 @sio.on('send')의 함수를 실행하고 receive 이벤트를 브로드캐스팅합니다.

브로드캐스팅이란 이벤트를 발생한 대상을 제외하고 나머지 대상에게 해당 이벤트를 발생시킵니다.

● 소켓서버 연동

class SocketClient(QThread): add_chat = QtCore.pyqtSignal(str) sio = socketio.Client() def __init__(self, parent=None): super().__init__() self.main = parent self.is_run = False self.ip = 5000 self.localhost = 'localhost' def set_host(self, ip, port): self.ip = ip self.port = port def run(self): host = 'http://%s:%s'%(self.ip, self.port) self.connect(host) self.is_run = not self.is_run def connect(self, host): SocketClient.sio.on('receive', self.receive) SocketClient.sio.connect(host) self.add_chat.emit('채팅 서버와 접속 완료했습니다.') def send(self, msg): SocketClient.sio.emit('send', msg) self.add_chat.emit('[나]:%s'%(msg)) def receive(self, msg): self.add_chat.emit('[상대방] %s'%(msg))

ip와 port를 기본값으로 localhost:5000을 연결하도록 하며 set_host()를 호출하여 ip와 port를 수정할 수 있도록 합니다.

해당 쓰레드가 생성되는 run() 메소드에선 소켓 서버와 연결을 합니다.이때 receive 이벤트를 받아 receive() 메서드를 호출하여 전달받은 메시지를 add_chat 시그널을 발생하여 화면에 출력합니다.

소켓에서 on은 이벤트 리스닝 핸들러를 등록합니다.

해당 쓰레드에 맞춰서 앱 객체도 수정합니다.

class ChatWindow(QMainWindow, ui_form) : ... 중략 ... def socket_connection(self): ip = self.input_ip.toPlainText() port = self.input_port.toPlainText() if (not ip) or (not port): self.add_chat('ip 또는 port 번호가 비었습니다.') return self.sc.set_host(ip, port) if not self.sc.is_run: self.sc.start()

지금까지 간단한 채팅앱을 만들어 보았습니다. 다음 영상은 시뮬레이션 영상입니다.

원래는 파일전송이나 귓속말, 그룹채팅까지 하려고 했는데... 쓰다보니 팔목이 아파서 일단은 여기까지만 작성하겠습니다.

요즘 손목이 말썽이네요 ㅠㅠ

파이썬 메신저 만들기 - paisseon mesinjeo mandeulgi