Toy_Project/Python multi thread TCP server

[Python] 파이썬 멀티 스레드 채팅 서버 만들기 - 2

Joo-Topia 2019. 11. 23. 13:12

기본 코드에 조금 더 살을 붙여서 CLI환경에서 단채 채팅방 환경을 구축했다.

아직 최적화시킬 부분도 남았고, slack처럼 늦게 들어와도 채팅방의 기록을 볼 수 있게 구축하고 싶기 때문에 시간이 남으면 세 번째 작업도 진행할 예정이다.

 

서버 코드


import socket
import argparse
import threading
import time

host = "127.0.0.1"
port = 4000
user_list = {}
notice_flag = 0

def msg_func(msg):
    print(msg)
    for con in user_list.values():
        try:
            con.send(msg.encode('utf-8'))
        except:
            print("연결이 비 정상적으로 종료된 소켓 발견")

def handle_receive(client_socket, addr, user):
    msg = "---- %s님이 들어오셨습니다. ----"%user
    msg_func(msg)
    while 1:
        data = client_socket.recv(1024)
        string = data.decode('utf-8')

        if "/종료" in string:
            msg = "---- %s님이 나가셨습니다. ----"%user
            #유저 목록에서 방금 종료한 유저의 정보를 삭제
            del user_list[user]
            msg_func(msg)
            break
        string = "%s : %s"%(user, string)
        msg_func(string)
    client_socket.close()

def handle_notice(client_socket, addr, user):
    pass

def accept_func():
    #IPv4 체계, TCP 타입 소켓 객체를 생성
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #포트를 사용 중 일때 에러를 해결하기 위한 구문
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    #ip주소와 port번호를 함께 socket에 바인드 한다.
    #포트의 범위는 1-65535 사이의 숫자를 사용할 수 있다.
    server_socket.bind((host, port))

    #서버가 최대 5개의 클라이언트의 접속을 허용한다.
    server_socket.listen(5)

    while 1:
        try:
            #클라이언트 함수가 접속하면 새로운 소켓을 반환한다.
            client_socket, addr = server_socket.accept()
        except KeyboardInterrupt:
            for user, con in user_list:
                con.close()
            server_socket.close()
            print("Keyboard interrupt")
            break
        user = client_socket.recv(1024).decode('utf-8')
        user_list[user] = client_socket

        #accept()함수로 입력만 받아주고 이후 알고리즘은 핸들러에게 맡긴다.
        notice_thread = threading.Thread(target=handle_notice, args=(client_socket, addr, user))
        notice_thread.daemon = True
        notice_thread.start()

        receive_thread = threading.Thread(target=handle_receive, args=(client_socket, addr,user))
        receive_thread.daemon = True
        receive_thread.start()


if __name__ == '__main__':
    #parser와 관련된 메서드 정리된 블로그 : https://docs.python.org/ko/3/library/argparse.html
    #description - 인자 도움말 전에 표시할 텍스트 (기본값: none)
    #help - 인자가 하는 일에 대한 간단한 설명.
    parser = argparse.ArgumentParser(description="\nJoo's server\n-p port\n")
    parser.add_argument('-p', help="port")

    args = parser.parse_args()
    try:
        port = int(args.p)
    except:
        pass
    accept_func()

 

기본 코드에서 살을 더 붙여 좀 더 채팅방 느낌이 날 수 있도록 개발을 진행했다.

전송에 필요한 로직을 묶어 하나의 함수로 정의하여 조금 더 깔끔하게 코드를 짜려고 노력했다. (msg_func)

handle_notice함수는 추후에 채팅 서버에서 공지를 할 수 있는 기능을 추가하기 위해 만들었다.

 

딕셔너리 user_list를 계속 갱신하며 접속중인 유저들의 이름과 socket정보를 유지해준다.

 

클라이언트 코드


import socket
import argparse
import threading
#접속하고 싶은 ip와 port를 입력받는 클라이언트 코드를 작성해보자.
# 접속하고 싶은 포트를 입력한다.
port = 4000

def handle_receive(lient_socket, user):
    while 1:
        try:
            data = client_socket.recv(1024)
        except:
            print("연결 끊김")
            break
        data = data.decode('utf-8')
        if not user in data:
            print(data)

def handle_send(client_socket):
    while 1:
        data = input()
        client_socket.send(data.encode('utf-8'))
        if data == "/종료":
            break
    client_socket.close()


if __name__ == '__main__':
    #parser와 관련된 메서드 정리된 블로그 : https://docs.python.org/ko/3/library/argparse.html
    #description - 인자 도움말 전에 표시할 텍스트 (기본값: none)
    #help - 인자가 하는 일에 대한 간단한 설명.
    #nargs - 소비되어야 하는 명령행 인자의 수. -> '+'로 설정 시 모든 명령행 인자를 리스트로 모음 + 없으면 경고
    #required - 명령행 옵션을 생략 할 수 있는지 아닌지 (선택적일 때만).
    parser = argparse.ArgumentParser(description="\nJoo's client\n-p port\n-i host\n-s string")
    parser.add_argument('-p', help="port")
    parser.add_argument('-i', help="host", required=True)
    parser.add_argument('-u', help="user", required=True)

    args = parser.parse_args()
    host = args.i
    user = str(args.u)
    try:
        port = int(args.p)
    except:
        pass
    #IPv4 체계, TCP 타입 소켓 객체를 생성
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 지정한 host와 prot를 통해 서버에 접속합니다.
    client_socket.connect((host, port))


    client_socket.send(user.encode('utf-8'))

    receive_thread = threading.Thread(target=handle_receive, args=(client_socket, user))
    receive_thread.daemon = True
    receive_thread.start()

    send_thread = threading.Thread(target=handle_send, args=(client_socket,))
    send_thread.daemon = True
    send_thread.start()

    send_thread.join()
    receive_thread.join()

 

클라이언트 코드 또한 조금 더 채팅방 느낌이 날 수 있도록 개발을 진행했다.

서버에서 최대한 많은 일을 수행하고 클라이언트는 가볍게 유지하려고 노력했다.

 

결과


좌측 상단 - 서버
우측 상단 - 클라이언트1
좌측 하단 - 클라이언트2
우측 하단 - 클라이언트3

서버에는 항상 채팅 기록을 보여주고, 각 클라이언트는 자신의 메시지를 제외한 다른 사람의 메시지만 받게 된다.

클라이언트들의 접속과 종료를 알려주는 기능도 추가했다.