Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python 멀티스레딩으로 구현하는 소켓 프로그래밍: 다중 클라이언트 서버 만들기

멀티스레딩(Multithreading)의 핵심 개념

멀티스레딩은 현대 프로그래밍 언어 대부분이 지원하는 핵심 개념입니다. 특히 파이썬(Python)은 간결하고 직관적인 방식으로 스레드를 구현할 수 있어 멀티스레딩을 활용하기에 매우 적합한 언어입니다.

스레드(thread)란 하나의 프로그램 내부에서 코드의 다른 부분과 독립적으로 실행될 수 있는 하위 프로그램(실행 흐름)을 의미합니다. 각 스레드는 동일한 프로세스 안에서 메모리 등 프로그램의 실행 가능한 자원들을 공유하면서 동작합니다.

즉, 하나의 프로세스 안에서 여러 개의 스레드를 동시에 실행하는 것을 멀티스레딩(multithreading)이라고 부릅니다.

파이썬 스레드 구현을 위한 모듈

파이썬에서는 크게 두 가지 모듈을 사용해 스레드를 구현할 수 있습니다.

  • _thread 모듈 — 파이썬 2.x의 thread 모듈에 해당하며, 저수준(low-level) API를 제공합니다.
  • threading 모듈 — 객체 지향적인 방식으로 스레드를 생성하고 제어할 수 있는 고수준(high-level) API입니다.

_thread 모듈은 함수 호출 형태로 스레드를 생성하는 반면, threading 모듈은 클래스 기반의 객체 지향 접근 방식을 제공하기 때문에 더욱 유연하고 안전하게 스레드를 관리할 수 있습니다.

스레드 생성 문법

_thread.start_new_thread(func, args[, kwargs])

위 함수는 새로운 스레드를 시작하고 그 스레드의 식별자(identifier)를 반환합니다. 첫 번째 인자인 func는 스레드가 실행할 함수이며, 두 번째 인자는 위치 인자(positional arguments)를 담는 튜플입니다. 선택적으로 kwargs 인자를 통해 키워드 인자 딕셔너리를 전달할 수도 있습니다. 함수가 반환되면 해당 스레드는 조용히 종료됩니다.

멀티스레드 소켓 서버 예제

이번에는 기본적인 클라이언트-서버 애플리케이션을 살펴보겠습니다. 클라이언트는 소켓 연결을 열고 서버로 요청(메시지)을 전송하며, 서버는 이를 처리한 후 응답을 되돌려줍니다. 인자 없이 실행하면 이 프로그램은 127.0.0.1의 8000번 포트에서 연결을 대기하는 TCP 소켓 서버로 시작됩니다.

client_thread1.py

import socket
import sys
def main():
    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    host = "127.0.0.1"
    port = 8000
    try:
        soc.connect((host, port))
    except:
        print("Connection Error")
        sys.exit()
    print("Please enter 'quit' to exit")
    message = input(" -> ")
    while message != 'quit':
        soc.sendall(message.encode("utf8"))
        if soc.recv(5120).decode("utf8") == "-":
            pass # null operation
        message = input(" -> ")
    soc.send(b'--quit--')
if __name__ == "__main__":
    main()

서버 프로그램은 다음과 같습니다.

server_thread1.py

import socket
import sys
import traceback
from threading import Thread
def main():
    start_server()
def start_server():
    host = "127.0.0.1"
    port = 8000 # arbitrary non-privileged port
    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    print("Socket created")
    try:
        soc.bind((host, port))
    except:
        print("Bind failed. Error : " + str(sys.exc_info()))
        sys.exit()
    soc.listen(6) # queue up to 6 requests
    print("Socket now listening")
    # infinite loop- do not reset for every requests
    while True:
        connection, address = soc.accept()
        ip, port = str(address[0]), str(address[1])
        print("Connected with " + ip + ":" + port)
    try:
        Thread(target=client_thread, args=(connection, ip, port)).start()
    except:
        print("Thread did not start.")
        traceback.print_exc()
    soc.close()
def clientThread(connection, ip, port, max_buffer_size = 5120):
    is_active = True
    while is_active:
        client_input = receive_input(connection, max_buffer_size)
        if "--QUIT--" in client_input:
            print("Client is requesting to quit")
            connection.close()
            print("Connection " + ip + ":" + port + " closed")
            is_active = False
        else:
            print("Processed result: {}".format(client_input))
            connection.sendall("-".encode("utf8"))
def receive_input(connection, max_buffer_size):
    client_input = connection.recv(max_buffer_size)
    client_input_size = sys.getsizeof(client_input)
    if client_input_size > max_buffer_size:
        print("The input size is greater than expected {}".format(client_input_size))
    decoded_input = client_input.decode("utf8").rstrip()
    result = process_input(decoded_input)
    return result
def process_input(input_str):
    print("Processing the input received from client")
    return "Hello " + str(input_str).upper()
if __name__ == "__main__":
    main()

실행 및 결과 확인

위 스크립트를 작성했다면 먼저 터미널에서 서버 프로그램을 실행합니다.

python server_thread1.py
Socket created
Socket now listening

서버 창을 주시하면서 흐름을 확인해 보겠습니다. 이제 여러 개의 클라이언트 터미널을 열고 클라이언트 프로그램을 실행합니다.

python client_thread1.py
Enter 'quit' to exit
-> Zack
->

다른 터미널에서 또 다른 클라이언트 프로그램을 실행하고, 서버 터미널 창도 함께 지켜봅니다.

python client_thread1.py
Enter 'quit' to exit
-> Python
-> quit

마찬가지로 세 번째 터미널에서도 클라이언트를 실행해 보겠습니다.

python client_thread1.py
Enter 'quit' to exit
-> world!
-> Anothny
->

그러면 서버 창에는 아래와 같은 출력이 표시되는 것을 확인할 수 있습니다.

Socket created
Socket now listening
Connected with 127.0.0.1:50275
Processing the input received from client
Processed result: Hello ZACK
Connected with 127.0.0.1:50282
Processing the input received from client
Processed result: Hello PYTHON
Processing the input received from client
Client is requesting to quit
Connection 127.0.0.1:50282 closed
Connected with 127.0.0.1:50285
Processing the input received from client
Processed result: Hello WORLD!
Processing the input received from client
Processed result: Hello ANOTHNY

정리

이처럼 스레드를 활용하면 여러 소켓 연결과 다수의 클라이언트를 동시에 효율적으로 처리할 수 있습니다. 멀티스레딩은 다중 클라이언트 환경의 네트워크 서버를 구축할 때 가장 널리 사용되는 핵심 기법 중 하나이므로, 위 예제를 직접 실행해 보며 동작 원리를 익혀두면 실무에 큰 도움이 됩니다.