파이썬 소켓 프로그래밍 - 2. 간단한 채팅 프로그램 구현(2)

2018. 11. 21. 22:13Programming/Python

지난번에 설명한게 많아서 이번엔 설명할 게 별로 없을 듯합니다.

지난 포스트에서는 소켓 통신을 이용해서 서버와 클라이언트가 각각 한번씩 메시지를 주고 받게 만들었습니다. 그렇다면 이걸로 채팅을 하려면 어떻게 해야할까요?

  • 메시지를 한 번만 주고 받는게 아니라, 계속해서 주고 받게 만들고
  • 보내는 메시지도 사용자가 직접 입력하게 만들면 되겠죠

이번 포스트에서는 반복적으로 주고 받는 채팅을 구현해보겠습니다.

서버 측 코드입니다.

클라이언트 측 코드입니다.

지난번과 다르게 while True문이 추가되었습니다. 즉, 프로그램을 강제종료하지 않으면 계속해서 실행된다는 이야기죠.

while문 안을 보면 서버와 클라이언트가 미묘하게 다르단 걸 알 수 있습니다. 서버측 코드는 보낸 후에 상대방으로부터 수신을 기다리고 클라이언트측 코드는 먼저 받은 다음에 상대방한테 송신합니다. 즉, 항상 서버 > 클라이언트 > 서버 > 클라이언트 > ... 순으로 메시지가 오고간다는 이야기죠.

실제 실행결과는 아래와 같습니다.

일단, 이 채팅에서 가장 불편한 점이 뭔지 말 안해도 바로 아실 것 같습니다. 마치 편지로 서신왕래하듯이, 혹은 페이스북에서 콕 찔러보기를 하듯이 순서가 무조건 정해져있고, 동시에 서로 대화하는 것이 불가능하단 점이죠. 무전기랑 다를 게 없습니다. 아니, 무전기는 그래도 순서는 자유로우니 이 채팅 프로그램이 더 안 좋다고 볼 수 있겠네요.

이 부분을 개선하기 전에, 코드에서 개선할 점을 개선하도록 합시다. while문 안에 보내고 받는 부분을 한꺼번에 써놓으니 보기에 불편합니다. 일단 육안으로 딱 봤을 때 어느게 보내는 것이고, 어느게 받는 것인지 파악이 바로 되지 않습니다. 앞으로도 기능을 점점 더 추가하게 될 텐데, 코드가 점점 길어지면 수정하기도 어려워지겠죠? 그러므로 보내는 기능과 받는 기능을 함수로 따로 분리시키도록 합시다.

send()와 receive()라는 함수로 따로 분리를 시켰습니다. 대신, 데이터를 소켓을 통해 주고 받아야하므로 인자로 sock이라는 인자를 받도록 했습니다. 여기에 clientSock이나 connectionSock을 입력하면 정상적으로 작동을 합니다.

실행결과는 똑같으니 첨부하지 않겠습니다.

이제 송수신 순서 상관없이 동시적으로 할 수 있도록 만들어봅시다. 이를 위해선 스레드(Thread)를 활용할 필요가 있습니다.

스레드는 그 자체만으로 몇 시간짜리 강의 분량을 뽑아먹고도 남으므로 자세히 언급하진 않겠습니다. 간단히 설명하면 프로세스 내부에서 병렬 처리를 하기 위해, 프로세스의 소스코드 내부에서 특정 함수만 따로 뽑아내어 분신을 생성하는 것입니다. 즉, 원래라면 하나의 절차를 따르며 해야하는 일들도, 스레드를 생성해서 돌릴 경우엔 동시 다발적으로 일을 할 수 있단 소리죠.

파이썬에서 스레드를 생성하기 위해선 threading을 불러와야 합니다.

import threading

def send(sock):
    while True:
        sendData = input('>>>')
        sock.send(sendData.encode('utf-8'))

def receive(sock):
    while True:
        recvData = sock.recv(1024)
        print('상대방 :', recvData.decode('utf-8'))

sender = threading.Thread(target=send, args=(connectionSock,))
receiver = threading.Thread(target=receive, args=(connectionSock,))

sender.start()
receiver.start()

위 코드는 개선된 코드의 일부분만 적은 것입니다. 다른 부분까지 같이 적으면 되레 산만해질 것 같아서 핵심 부분만 적어봤습니다. 일단, send()와 receive()부분이 바뀌었단 것을 알 수 있습니다. while True로 감쌌는데, 이는 아래에서 설명하겠습니다.

지난번에 소켓을 socket()으로 생성했듯이, 여기에선 스레드를 threading.Thread()로 생성할 수 있습니다. 만일 import threading이 아니라 from threading import *를 하셨다면 그냥 Thread()만 적으셔도 됩니다.

Thread() 생성자는 여러가지 인자를 받습니다만, 여기에서는 target과 args만 주목하면 됩니다. target은 실제로 스레드가 실행할 함수를 입력해주시면 되고, 그 함수에게 전달할 인자를 args에 입력하시면 됩니다.

주의하셔야 할 점은, args는 튜플같이 iterable한 변수만 입력될 수 있습니다. 그런데 인자가 하나일 경우, (var) 식으로 괄호로 감싸기만 하면 파이썬 인터프리터는 이를 튜플이 아니라 그냥 var로 인식합니다. 그러므로 인자가 하나라면 (var,) 식으로 입력해야만 튜플로 인식하므로 이 점을 유의하셔야 합니다.

이렇게 생성된 스레드는 start()를 실행했을 때 비로소 일을 시작합니다. 생성된 스레드는 본인의 일을 전부 끝내면 알아서 사라집니다. 그러나 우리는 지속적으로 계속 채팅을 할 것이므로 스레드가 영원히 일을 하도록 만들어야 합니다. 그래서 send와 receive에 while True가 들어간 것입니다. 이제 sender와 receiver는 프로세스가 종료되지 않는 한은 계속 실행될 것입니다.

그러나, 스레드는 특별한 경우가 아니고선 프로세스가 종료될 때 자기 자신들도 같이 종료가 됩니다. 어쩔 수 없는 분신의 비애입니다. 스레드가 아무리 while True로 영원히 돌게 만들어졌다 하더라도, 프로세스가 종료되면 스레드도 다 사라지므로 아무 의미가 없습니다. 그러므로 메인인 프로세스 본인도 계속해서 돌아가야만 합니다.

while True:
    pass

마지막에 이 두 줄을 추가하면 프로그램은 강제 종료를 하지 않는 이상 영원히 꺼지지 않습니다. 그러나, 이렇게 해놓고 실행하면 노트북의 쿨러가 미친듯이 돌기 시작하고, 배터리도 갑자기 쭉쭉 닳기 시작할 것입니다. 요즘 PC들은 1초에 10억번을 넘는 연산을 할 수 있는데, 저렇게 대책없이 while True를 달아놓으면 당연히 파이썬은 저 코드를 1초에 몇 억번이나 실행하고 있을 것입니다. 어차피 스레드를 생성하고 난 후의 프로세스의 목적은 단순히 꺼지지 않는 것뿐이므로, 각 while문마다 1초씩 쉬게만 해줘도 충분할 것입니다.

import time
while True:
    time.sleep(1)
    pass

time의 sleep()함수는 입력된 인자만큼 대기하는 함수입니다. 단위는 초이므로 이 코드는 1초씩 쉬고 다음 while문을 호출할 것입니다. 물론 그 다음 다시 1초를 쉬겠죠.

이렇게 스레딩 작업까지 해주고 나면 코드는 다음과 같아집니다.

실제로 실행해본 결과는 이렇습니다.

이제 채팅에 순서가 없어졌고, 동시에 대화하는 것도 가능해졌습니다. 그러나 한 가지 이상한 점이 있습니다. 바로 자기가 입력하는 도중에 상대방이 전송하면 자신의 말이 끊긴다는 것입니다.

이는 콘솔에서 실행하는 특성상 어쩔 수 없는 일입니다. C언어처럼 getch()같은게 있었다면 \r(캐리지 리턴)과 연계해서 자신의 말이 끊기지 않도록 만들 수 있을 것 같습니다만, 안타깝게도 검색 결과 파이썬에 자체적으로 getch를 사용하는 방법은 없는 듯합니다. 이후에 나오는 개선 버전에서도 이 문제만큼은 해결이 안 될 것 같습니다. GUI로 다시 만들지 않는 한은요.

그리고, 실험결과 파이썬에 기본적으로 설치되는 IDLE에서는 자신이 송신하고 나서야 그 사이에 상대방이 보낸 메시지를 확인할 수 있는 기묘한 현상이 있었습니다. 아무래도 IDLE 환경에서는 stdin을 사용 중일때 stdout을 막아놓는 듯 하니, 이 채팅 프로그램은 꼭 cmd나 쉘에서 작동을 확인하시기 바랍니다.