Toy_Project/Python multi thread TCP server

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

Joo-Topia 2019. 11. 20. 11:59

N사 최종 면접 후 멀티 스레드에 대한 이론은 있지만 실전에 약하다는 느낌을 받았다.

아무래도 멀티 스레드 환경에서 프로그래밍을 할 경험이 없어서 더 그런 것 같다.

그! 래! 서! 혼자서라도 멀티 스레드 채팅 서버를 구축해보는 프로젝트를 진행하려고 한다.

 

기본 코드


Python 게시판에 올려두었던 서버, 클라이언트 코드를 조금 변조해서 뼈대가 될 코드를 작성하였다.

 

- 서버 코드

import socket
import argparse
import threading
import time

host = "127.0.0.1"
port = 4000
user_list = {}
notice_flag = 0
def handle_receive(client_socket, addr, user):
    while 1:
        data = client_socket.recv(1024)
        string = data.decode()

        if string == "/종료" : break
        string = "%s : %s"%(user, string)
        print(string)
        for con in user_list.values():
            try:
                con.sendall(string.encode())
            except:
                print("연결이 비 정상적으로 종료된 소켓 발견")

    del user_list[user]
    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)
        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()

 

- 클라이언트 코드

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

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

def handle_send(num, client_socket):
    while 1:
        data = input()
        client_socket.sendall(data.encode())
        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.sendall(user.encode())

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

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

    send_thread.join()
    receive_thread.join()

 

두 코드의 스타일이 살짝 다른데, 추후에 정리하면서 진행할 예정이다.

현재 기능은 단순하게 채팅 기능에 조금 더 현실감 있는 기능을 하나 추가했다.

보통 예제로 사용되는 채팅 코드를 실행시켜보면, 내가 입력한 채팅이 cmd창에 다시 출력되어 뭔가 거슬렸던 부분이 있었다. (나만 그런가)

직점 Front-end 개발을 하는 게 아니니까! cmd창에서라도 보기 좋게 하기 위해 서버에서 내가 보낸 메시지에 대한 데이터가 수신되면 무시하는 방향으로 개발을 진행했다.

 

실행 결과


왼쪽부터 Client1, Client2, Server 순서로 실행 화면을 캡쳐했다.

서버에는 모든 이용자들의 대화가 남고, 각 각의 클라이언트는 자신이 보낸 메시지는 이름이 없이, 다른 사람이 보낸 메세지는 유저의 이름과 메시지가 같이 출력된다.

 

데이터를 주고받는 방식과 전체적인 코드 스타일을 수정하고 다양한 기능을 추가로 구현할 예정이다.