python 기반 채팅프로그램 - 3

김두현·2023년 7월 26일
0

python_chat

목록 보기
4/4

오랜만에 글을 채팅 프로그램 포스팅을 작성한다. 아마 이 포스팅을 마지막으로 한동안 파이썬 채팅 프로그램 포스팅은 중단할려고 한다...
(요즘 너무 바쁘다 ㅜㅜ... 할게 너무 많아)

일단 지금까지 작성한 채팅 프로그램의 포스팅으로 채팅 프로그램을 구현하기 위한 기본 지식들은 얼추 언급한거 같네요. 예를 들면 TCP/IP 통신, 소켓 통신, 쓰레드 등? 그래서 지금부터 작성하는 것은 기본 채팅 프로그램틀에서 기능들을 하나씩 추가하는 과정을 보여줄 예정입니당~

일단 이번 3번째 버전의 채팅 프로그램에 추가한 기능들을 짧게 요약하면 아래와 같네요...

  1. 유저 닉네임 등록
  2. 서버-클라이언트 접속 종료
  3. 메세지 송신자 파악
  4. 귓속말 기능

뭐 자세한 기능들은 아래에 하나씩 설명하겠습니다~~ 마지막엔 에러로그 하나 정도 추가적으로...?


추가 기능

1. 유저 닉네임

클라이언트가 서버에 연결하면 사용할 닉네임을 묻는 창이 열리고, 닉네임을 설정할 수 있다. 이미 존재하는 중복 닉네임을 처리하는 기능도 추가하였습니다.

if nickname in nickname_dic.keys():
        send_to_msg(["Server", "해당 닉네임은 중복입니다.", "Fail"], client)
        print(f'닉네임 중복: ({client[1]} -> "{nickname}")')
        register_user(client)

💡 nickname_dic 딕셔너리에 클라이언트가 입력한 닉네임이 존재하면 클라이언트에게 닉네임 중복 에러를 전송하고 재귀를 이용해 다시 닉네임 등록 메소드를 호출

2. 서버-클라이언트 연결 종료

서버 프로그램은 항상 구동 중이라 종료하는 기능이 필요없다고 생각하지만 클라이언트 측은 서버와 접속한 후 정상적으로 종료를 마치는 기능이 필요할 거 같아서 추가했다.

처음에는 프로그램인 CIL 기반이니깐 ctrl+c 를 눌러 강제종료를 하니 뭐 당연히 클라이언트야 종료되지만 서버측에서는 소켓 연결이 정상적으로 종료되지 않으니 서버 프로그램이 먹통이 됐다.

그래서 먼저 클라이언트가 접속 종료 신호를 서버로 전송하면 각 측에서 서로 연결된 소켓을 끊은 다음에 클라이언트 프로그램을 종료하는 과정으로 구현했다.

그런데 종료 기능을 구현하는 과정에서 두 가지의 문제점을 직면했는데 각각 클라이언트 측 에러와, 서버 측 에러다.

<문제점>

1. Thread 종료 실패 (클라이언트 문제)

msg = client_sock.recv(1024)
위 코드에서 소켓 버퍼에 메세지가 들어올 때까지 대기 상태이므로 thread가 종료 X

(해결방안)

  1. socket 객체에 settimeout 메소드를 설정해 일정 시간 동안 메세지를 수신 못 받으면 다음 코드로 건너뛰기
  2. 메인 쓰레드 종료시 자동으로 무조건 종료하는 데몬 쓰레드를 이용
recv_thread.daemon = True

2. 클라이언트가 강제로 프로그램 종료 시 서버에서 연결 종료 감지 실패 ( 서버)

(해결방안)

  1. try-catch 문을 통해 ConnectionError 예외 처리 (서버 코드 수정)
  • 하지만 ctrl+c 와 같이 클라이언트 측에서 강제로 프로그램을 종료하면 서버는 connection error를 감지 실패
  1. 클라이언트에서 프로그램 종료 전에 서버로 접속을 끊는다는 메세지를 무조건 보내도록 구현 (클라이언트 코드 수정)
finally:  ## 프로그램이 종료하기 전 서버로 연결을 종료하는 메세지를 전송(crtl+c 도 감지)
    disconnect_with_server()

3. 메세지 송신자 확인

서버에서 클라이언트 측으로 메세지를 전송할 때 메세지의 내용 + 메세지를 보낸 측(다른 클라이언트 or 서버) 을 함께 패킷에 담아 전송하도록 구현했다.

  • 구현의 편의성을 위해 리스트나 튜플로 (수신자, 메세지 내용)을 인덱스화해서 전송
  • python의 pickle 모듈을 사용해 비바이트 데이터를 바이트로 직렬화한 뒤에 전송
  • 클라이언트는 바이트로 된 데이터를 다시 원본 메세지로 복구 후에 사용
import pickle

message = [client_sockets[from_client].nickname, from_client[0].recv(1024).decode()]      # 메세지를 보낸 유저의 닉네임과 메시지 내용을 함께 보냄
data = pickle.dumps(message)

4. 귓속말 기능

클라이언트가 "@[닉네임]: [메세지 내용]" 의 명령어를 통해 해당 닉네임을 가진 특정 클라이언트에게만 메세지를 보내는 이른바 "귓속말" 기능을 구현했다.

귓속말 기능을 구현하기 위해 크게 두 가지 정도의 문제가 있었다.

  1. 기존의 서버 코드에서는 소켓을 통해 클라이언트를 참조가능 하지만 닉네임을 통해 해당 클라이언트의 소켓을 참조하는 데이터 구조가 없었다.

(닉네임을 key, socket 객체를 value로 가지는 'nickname_dic' 딕셔너리 객체를 생성해 닉네임으로 소켓을 추적을 가능하게 구현)

nickname_dic = {}       # nickname으로 소켓 찾기
.
.
.
user = Client(nickname)
client_sockets[client] = user
nickname_dic[nickname] = client     # socket 객체가 복사되는 것이 아닌 기존 socket의 참조가 저장됨 -> 실질적으로 닉네임 데이터만 추가적으로 저장!!
send_to_msg(["Server", "성공적으로 클라이언트가 등록 완료했습니다.", "Pass"], client)
  1. 귓속말 대상으로 요청한 닉네임을 가진 클라이언트가 존재(접속x)하지 않을 때 처리가 필요하다.

("nickname_dic"에 해당 닉네임이 존재하지 않으면 KeyError 예외를 발생시켜 클라이언트로 error message 반환)

   try
    	...
    	to_sock = nickname_dic[to_nickname]
    	...
   except KeyError:        # 클라이언트가 없는 유저에게 귓속말 보낼 경우 에러 메세지 반환
        send_to_msg(["Server", "해당 닉네임은 존재하지 않습니다."], from_client)
        

Error Log

일단 지금까지 작성한 내용으로 2번째 버전의 채팅 프로그램에 추가한 기능들을 정리했다.

이외에 추가적으로 구현 중에 만난 에러를 간단하게 소개하고 해결한 과정을 기술하며 이번 포스팅을 마무리...

메세지 전체 보내기 오류

(Server)

(Client: aa)

(Client: bb)

(Client: cc)

일단 위의 사진을 보면 무슨 에러인지 알겠나요?? 제목처럼 잘 되던 메세지 전체 보내기 기능이 갑자기 특정 유저에게만 보내지고 다른 유저들에겐 이상한 바이트 형식의 메시지가 전달되는 오류가 생겼습니다.

💡 한번 천천히 확인해보니 아래와 같은 이유로 에러가 발생했고 해결했습니당~

에러 내용

“cc” 유저가 전체 메세지를 보내면 서버와 “aa” 유저에게는 정상적으로 메세지가 도달하는데 “bb” 유저와 같이 다른 유저들에게는 바이너리 타입의 문자가 전달됐다!

(원인)

왜 그런지 계속 고민하다가 문제는 send_all_msg 메소드에서 메세지를 보내기 전에 메세지 원본을 dumps 메소드를 통해 바이트로 변환하는데 변환한 메세지를 또 dumps 하고 다른 클라이언트에게 전송!!!!

(해결)
메세지 원본을 수정한 값을 원본에 저장하니깐 계속 바이트로 변환
-> 원본 값은 변경하지 않은 채 바이트로 변환된 값을 클라이언트로 전송!

## msg = pickle.dumps(msg)
client_socket[0].sendall(pickle.dumps(msg))
profile
끄적끄적

0개의 댓글

관련 채용 정보