파이썬으로 구현한 채팅 클라이언트앱

이전 채팅 서버에 이어 이번에는 채팅 클라이언트(Chat Client) 입니다.

서버측 프로그래밍은 이전 게시물을 참조하기 바랍니다.

[채팅 클라이언트 화면]

Python + PyQt5를 이용해 만들어 보았습니다.

채팅 클라이언트는 client.py, window.py 두개의 파일로 구성되어 있습니다.


1. client.py 파일 분석

이 파일은 클라이언트 소켓 관련된 클래스이며, 서버 연결, 데이터 송수신과 관련된 처리를 담당합니다.

전체 소스코드를 살펴보겠습니다.

from threading import *
from socket import *
from PyQt5.QtCore import Qt, pyqtSignal, QObject

class Signal(QObject):  
    recv_signal = pyqtSignal(str)
    disconn_signal = pyqtSignal()   

class ClientSocket:

    def __init__(self, parent):        
        self.parent = parent                
       
        self.recv = Signal()        
        self.recv.recv_signal.connect(self.parent.updateMsg)
        self.disconn = Signal()        
        self.disconn.disconn_signal.connect(self.parent.updateDisconnect)

        self.bConnect = False
        
    def __del__(self):
        self.stop()

    def connectServer(self, ip, port):
        self.client = socket(AF_INET, SOCK_STREAM)           

        try:
            self.client.connect( (ip, port) )
        except Exception as e:
            print('Connect Error : ', e)
            return False
        else:
            self.bConnect = True
            self.t = Thread(target=self.receive, args=(self.client,))
            self.t.start()
            print('Connected')

        return True

    def stop(self):
        self.bConnect = False        
        if hasattr(self, 'client'):            
            self.client.close()
            del(self.client)
            print('Client Stop') 
            self.disconn.disconn_signal.emit()

    def receive(self, client):
        while self.bConnect:            
            try:
                recv = client.recv(1024)                
            except Exception as e:
                print('Recv() Error :', e)                
                break
            else:                
                msg = str(recv, encoding='utf-8')
                if msg:
                    self.recv.recv_signal.emit(msg)
                    print('[RECV]:', msg)

        self.stop()

    def send(self, msg):
        if not self.bConnect:
            return

        try:            
            self.client.send(msg.encode())
        except Exception as e:
            print('Send() Error : ', e)

서버 대비 코드가 짧고 간단합니다.

1~3번 라인은 Thread, Socket, PyQt5 관련 모듈을 불러오는 부분입니다.

5번 라인의 Signal Class는 소켓 클래스가 연결 끊김, 데이터 수신 시그널 발생 시 window.py 파일이 생성하는 윈도우 창으로 사용자 정의 시그널을 전송하기 위함입니다.

9번 라인부터 ClientSocket Class에 대한 클래스 코드입니다.

클라이언트 소켓은 서버의 경우처럼 접속대기(Listen)가 불 필요하므로 코드가 짧습니다.

서버 접속(Connect), 수신(Receive), 송신(Send) 처리를 담당하게 됩니다.

11번 라인 생성자 함수에서, 부모 윈도우를 저장해둘 self.parent 변수 선언 및 데이터 수신, 연결 끊김시 사용할 시그널을 선언하고 부모 윈도우의 슬롯(함수)과 연결합니다.

21번 라인 소멸자 함수는 self.stop() 함수를 호출해 소켓을 닫고, 부모 윈도우창에 연결끊김을 알리는 신호를 보내도록 합니다.

24번 라인은 부모 윈도우창에서 '접속' 버튼을 눌렀을때 호출되는 함수이며, 소켓을 생성하고 해당 IP 주소의 포트번호로 연결을 시도합니다.


이 과정에서 서버가 대기 상태가 아니거나, 네트워크 상황에 따라 다양한 오류가 발생 할 수 있는데 이 경우  try, except 구문을 이용한 오류처리가 필요합니다.

일단 접속을 시도(try)한 후 오류가 있다면 except 구문에서 처리, 정상적으로 연결이 이루어지면 쓰레드를 생성하고 연결된 소켓의 데이터 수신을 준비합니다.

40번 라인의 stop()함수는 위에서 설명한 대로 소켓을 닫고, 부모에게 알리는 역할입니다.

48번 라인의 recevie() 함수는 클라이언트 소켓 연결이 정상적으로 이루어지면 생성되는 쓰레드에 의해 호출되는 함수로 무한루프를 통해 소켓의 데이터 수신을 대기합니다.

소켓의 recv()함수는 호출시 데이터를 수신하기 전까지 블록되어, 다음 코드는 수행되지 않습니다.

수신 대기중 데이터 수신시 함수를 탈출하여, 수신된 바이트 타입의 데이터를 utf-8 문자열로 인코딩 한 후 부모 윈도우로 보내 화면에 출력하도록 합니다.

따라서 반드시 쓰레드로 구성되어야 하며, 또 다른 데이터가 언제 수신될 지 알 수 없으므로 무한 루프로 구성해 계속 대기할 수 있어야 합니다.

63번 라인 send()함수는 부모 윈도우의 '보내기' 버튼을 누르면 호출되는 함수이며, 보낼 메시지 내용을 복사해 연결된 소켓으로 전송하는 역할을 담당합니다.


다음은 실제 윈도우 창을 만드는 windows.py 파일을 살펴보겠습니다.


2. windows.py 파일 분석

이 파일은 실제 윈도우 창을 생성하는 클래스와 메인함수로 구성되어 있습니다.

전체 코드를  살펴보겠습니다.

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import client

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

port = 5614

class CWidget(QWidget):
    def __init__(self):
        super().__init__()  
        
        self.c = client.ClientSocket(self)
       
        self.initUI()

    def __del__(self):
        self.c.stop()

    def initUI(self):
        self.setWindowTitle('클라이언트')
        
        # 클라이언트 설정 부분
        ipbox = QHBoxLayout()

        gb = QGroupBox('서버 설정')
        ipbox.addWidget(gb)

        box = QHBoxLayout()

        label = QLabel('Server IP')
        self.ip = QLineEdit()
        self.ip.setInputMask('000.000.000.000;_')
        box.addWidget(label)
        box.addWidget(self.ip)

        label = QLabel('Server Port')
        self.port = QLineEdit(str(port))
        box.addWidget(label)
        box.addWidget(self.port)

        self.btn = QPushButton('접속')       
        self.btn.clicked.connect(self.connectClicked)
        box.addWidget(self.btn)

        gb.setLayout(box)       

        # 채팅창 부분  
        infobox = QHBoxLayout()      
        gb = QGroupBox('메시지')        
        infobox.addWidget(gb)

        box = QVBoxLayout()
        
        label = QLabel('받은 메시지')
        box.addWidget(label)

        self.recvmsg = QListWidget()
        box.addWidget(self.recvmsg)

        label = QLabel('보낼 메시지')
        box.addWidget(label)

        self.sendmsg = QTextEdit()
        self.sendmsg.setFixedHeight(50)
        box.addWidget(self.sendmsg)

        hbox = QHBoxLayout()

        box.addLayout(hbox)
        self.sendbtn = QPushButton('보내기')
        self.sendbtn.setAutoDefault(True)
        self.sendbtn.clicked.connect(self.sendMsg)
        
        self.clearbtn = QPushButton('채팅창 지움')
        self.clearbtn.clicked.connect(self.clearMsg)

        hbox.addWidget(self.sendbtn)
        hbox.addWidget(self.clearbtn)
        gb.setLayout(box)

        # 전체 배치
        vbox = QVBoxLayout()
        vbox.addLayout(ipbox)       
        vbox.addLayout(infobox)
        self.setLayout(vbox)
        
        self.show()

    def connectClicked(self):
        if self.c.bConnect == False:
            ip = self.ip.text()
            port = self.port.text()
            if self.c.connectServer(ip, int(port)):
                self.btn.setText('접속 종료')
            else:
                self.c.stop()
                self.sendmsg.clear()
                self.recvmsg.clear()
                self.btn.setText('접속')
        else:
            self.c.stop()
            self.sendmsg.clear()
            self.recvmsg.clear()
            self.btn.setText('접속')

    def updateMsg(self, msg):
        self.recvmsg.addItem(QListWidgetItem(msg))

    def updateDisconnect(self):
        self.btn.setText('접속')

    def sendMsg(self):
        sendmsg = self.sendmsg.toPlainText()       
        self.c.send(sendmsg)        
        self.sendmsg.clear()

    def clearMsg(self):
        self.recvmsg.clear()

    def closeEvent(self, e):
        self.c.stop()       


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    sys.exit(app.exec_())

1번 라인부터 GUI 구성을 위해 PyQt5 모듈을 불러옵니다.

11번 라인에서 윈도우 창을 만드는 CWidget 클래스를 QWidget으로 부터 상속받아 만듭니다.

12번 라인의 생성자에서 앞서 만든 SocketClient의 객체를 self.c 라는 이름으로 생성합니다. 생성자의 전달인자로 자신(윈도우)을 넘겨주는 부분이 보입니다.

22번 라인부터 화면을 구성하는 컨트롤을 생성합니다.

25~46번 라인은 IP주소, 포트번호, 접속 버튼 컨트롤을 생성하는 코드입니다.

[서버 설정 관련 컨트롤]

IP 주소창은 QLineEdit 컨트롤이며, setInputMask() 함수를 통해 IP주소를 받는 형식으로 만듭니다.

MFC에는 CEdit와는 별도로 IP Address 컨트롤이 존재하는데 Qt는 IP 컨트롤이 따로 존재하지 않아 마스킹을 통해 처리해 보았습니다.

50~82번 라인은 수신 메시지 출력창, 보낼 메시지를 작성창, 버튼 들을 생성하는 코드입니다.

[송수신 메시지 관련 컨트롤]

받은 메시지는 QListWidget 컨트롤, 보낼메시지는 여러줄을 한번에 보낼 수 있도록 QTextEdit 컨트롤을 사용합니다.

생성된 컨트롤들은 BoxLayout을 이용해 배치합니다.

92번 라인은 접속 버튼을 누른 경우(시그널) 실행되는 슬롯함수이며, 앞서 작성한 ClientSocket 클래스의 connectServer() 함수에 ip, port를 넘겨 소켓연결을 시도합니다.

109번 라인은 소켓 클래스에서 수신된 문자열을 받은 메시지에 표시하는 부분입니다.

112번 updateDisconnect()함수는 소켓의 연결이 끊어진 경우 호출되는 함수입니다.

115번 라인은 보낼 메시지에 작성한 내용을 소켓 클래스로 전달해 데이터를 송신하는 역할을 담당합니다.

120번 clearMsg() 함수는 채팅창 지움 버튼을 누른 경우 호출되며, 받은 메시지를 전체 삭제합니다.

123번 closeEvent()는 QWidget의 재정의 함수로 윈도우 창 종료시 소켓클래스를 닫는 역할입니다.

127번 라인은 이젠 설명하지 않아도 아실 것 같지만, 메인함수로 윈도우 창을 생성하고, 프로그램을 생성하는 역할을 담당합니다.

이상으로 채팅 클라이언트 소스코드 분석을 마칩니다.

앞서 제작한 채팅 서버와 클라이언트를 연결해 통신이 잘 이루어지는지 확인해 보시기 바랍니다. (한 대의 컴퓨터에서 서버, 클라이언트를 둘 다 실행해도 확인가능)

참조로 내 컴퓨터의 IP 주소는 시작 -> cmd 입력 후 명령프롬프트에 ipconfig라고 입력하면 확인 가능합니다.

[내 컴퓨터 IP 찾기]

pyinstaller 로 제작한 실행파일 링크 : 채팅클라이언트실행파일

만약 서버가 정상적으로 대기(리슨) 중인 상태에서 클라이언트의 접속이 이루어지지 않는다면, 방화벽 해지 or 예외로 등록후 시도하기 바랍니다.
  • 개발 환경
  1. 운영체제 : MS Windows 10 Pro
  2. 개발 언어 : Python 3.7 (32bit), PyQt5 (5.11.3)
  3. 개발 도구 : MS Visual Studio 2017 Pro

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기