본문 바로가기

Programming/Python

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

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

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

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

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

서버 측 코드입니다.

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

지난번과 다르게 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나 쉘에서 작동을 확인하시기 바랍니다.

  • jhjh 2019.03.14 14:57 댓글주소 수정/삭제 댓글쓰기

    ㅋㅋㅋㅋㅋㅋㅋㅋㅋ찰지게 재밌게쓰셨네요

  • jdg1023 2019.05.01 15:07 댓글주소 수정/삭제 댓글쓰기

    혹시 파이참 프로그램 2개를 동시에 실행이 가능한가요 ??

  • ㅋㅋㅋㅋㅋ 2019.05.07 03:47 댓글주소 수정/삭제 댓글쓰기

    개웃기다 ㅋㅋㅋㅋ 잘봤습니다

  • asdad 2019.05.24 22:26 댓글주소 수정/삭제 댓글쓰기

    ConnectionRefusedError: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다
    이게 뜨는데 혹시 이 문제를 해결할 방법이 있을까요??

    • 혹시 서버에서 연 포트와 클라이언트에서 접속하는 포트가 다르지 않은가요? 글 중간에 포트가 8080에서 8081로 바뀌어서 코드를 잘못 적으셨을 수도 있습니다. 혹은 localhost가 아니라 다른 컴퓨터와 접속을 시도하신 거라면 포트포워딩 등의 작업이 필요합니다. 마지막으로 서버 실행에서부터 socket이 잡히지 않는 것이라면, 포트번호를 바꿔서 시도해보는 것을 추천드립니다.

    • cpa 2020.03.26 16:51 댓글주소 수정/삭제

      클라이언트쪽 코드에 서버의 아이피 주소를 입력해 주세요

  • 방문자 2019.06.03 13:28 댓글주소 수정/삭제 댓글쓰기

    위 코딩은 아톰에서 하는건가요???
    실행하는 어플리케이션도 아톰인지 궁금합니다!!

    • 파이참에서 작성 및 실행했습니다. 커뮤니티 에디션은 무료고, 학교 메일이 있으면 1년간 정식버전 무료로 쓸 수 있으니 고려해보세요.

  • 방문자 2019.06.03 13:57 댓글주소 수정/삭제 댓글쓰기

    학교 실습실에서 하고 있는데
    하나의 컴퓨터로 서버를 연뒤 cmd 창을 여러개 띄어서 하는 건 가능한데
    두대 이상의 컴퓨터로 주고 받는 것은 안되나요?

    • 같은 네트워크에서라면 ipconfig에서 나오는 사설ip 주소를 가지고 통신이 가능합니다. 서로 다른 네트워크 상에서 통신하려면 서버 역할을 하는 곳에서 포트포워딩이 되어 있어야 합니다.

  • dia7691 2019.07.24 20:49 댓글주소 수정/삭제 댓글쓰기

    ㅋㅋㅋ
    파이썬으로 소켓 프로그래밍 배워서 채팅프로그램같은거 한번 만들어보고싶었는데..
    정말감사합니다!!

  • dia7691 2019.07.24 21:12 댓글주소 수정/삭제 댓글쓰기

    clientSock.connect(('127.0.0.1', port))
    ConnectionRefusedError: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다

    이렇게 나오면서 계속 실행이 안되네요 ㅠㅠ
    혹시 동시에 실행해야되나요?

  • sw94 2019.09.30 12:59 댓글주소 수정/삭제 댓글쓰기

    c에서는 getch 와 \r 을 사용해서 어떻게 말이 끊기지 않게 할수 있나요?
    구체적인 방법이 궁금합니다.

    • C를 하다 말아서 자세히는 모릅니다만..
      getch()는 echo 없이 키 입력만을 받으므로, getch()에서 키 입력 받을때마다 puts() 등의 함수를 통해 입력한 값을 그대로 echo하되, 출력하는 문장의 맨 앞에 \r 을 넣으면 되지 않을까 싶습니다. \r은 캐리지 리턴으로, 출력시에 해당 줄의 맨 처음부터 출력을 다시 합니다. char c; while (c = getch() != '\n') {} 형식으로 짜시고, 괄호 안에는 1. 변수 c를 별도로 선언된 char array에 넣기 2. char array를 puts 혹은 printf를 통해 출력하기. 3. 어느 쪽의 방법이건, 출력하는 문자열의 맨 앞은 \r이어야 합니다. C를 안 한지 오래되어서 문법이나 라이브러리의 구체적인 사용법은 잘 모릅니다.

  • soulzer 2019.10.19 07:15 댓글주소 수정/삭제 댓글쓰기

    진짜 이해하기 쉽고 재밌게 글을 잘 쓰시는 것 같습니다. 덕분에 어려웠던 소켓에 대해 잘 이해하고 갑니다. 감사합니다.

  • ss9 2020.02.08 22:22 댓글주소 수정/삭제 댓글쓰기

    잘 봤습니다. 그런데 클라이언트에서 IP를 루프백 아이피로 했을 땐 잘 됐지만
    제 컴퓨터의 외부 아이피로 하니 연결이 되지 않습니다. 어떤 아이피를 적어야 하나요?

    • 공유기를 사용한 환경이라 그런 것 같습니다. 사설IP/공인IP의 개념과 포트포워딩에 대해 찾아보세요. 포트포워딩을 통해 외부에서 내 IP와 연결될 수 있도록 해야 합니다.

    • ss9 2020.02.09 09:42 댓글주소 수정/삭제

      포트포워딩을 해야 하는군요. 해결되었습니다 감사합니다!

  • tastetherainbow 2020.02.10 17:14 댓글주소 수정/삭제 댓글쓰기

    좋은 글 감사합니다! 그런데 send 함수로 리스트를 보낼 수 도 있나요?

    • 소켓으로 보낼 수 있는 정보는 사실상 바이트밖에 없습니다. 리스트로 보내고 싶으시다면 어떤 규칙을 정해서 전송하시고, 받는 쪽에서 그 규칙에 따라 풀도록 해야할거 같네요. 단순 문자열 이상의 정보들을 주고받으려면 소켓에 정말 많은 규칙이 붙게 되고, 이를 프로토콜이라고 부릅니다. 직접 구현하는건 꽤 많은 노가다니 http나 웹소켓 통신 등을 고려해보시는 게 나을거 같아요

    • tastetherainbow 2020.02.11 22:23 댓글주소 수정/삭제

      감사합니다 참고하겠습니다!

  • Harry 2020.02.23 19:36 댓글주소 수정/삭제 댓글쓰기

    잘 보았습니다. 감사합니다.

  • harry 2020.03.09 20:09 댓글주소 수정/삭제 댓글쓰기

    혹시 main.py로 하나 실행시켜서 server.py와 client.py 를 같이 실행하고 싶은데
    main.py에 os.system써서 불러와서 실행시키려고 했는데 server가 종료되야 client가 실행되서요...
    동시에 실행되게 한 main에 나오게 할 수 없나요??

    • 질문의 의미를 잘 모르겠습니다. server.py와 client.py는 사실상 순서 차이만 있을뿐 파일 하나하나가 각각 채팅 프로그램인데요, 이 둘을 한 번의 프로세스에 같이 포함시켜서 켜는 이유가 있으신가요? 카카오톡 2개를 한꺼번에 켜서 한번 채팅에 2번을 전송할 수 있냐는 질문과 같아 보이네요.