[Python]소켓통신 - 서버 구축

Inung_92·2023년 7월 4일
2

Python

목록 보기
3/4
post-thumbnail

포스팅 목적

📖소켓통신을 위한 서버 구축 간 기본적인 구성을 알아보고, 주의 또는 신경써야 할 부분들은 어떤 것들이 있는지 알아보자.

서버 구성

⚡️ 실행환경

  • OS : Linux(ubuntu)
  • 데몬 서비스

⚡️ 통신규칙

  • 클라이언트 통신 주기 0.2초
  • 데이터 형식 : String(구분자 : ',')
  • 복수 클라이언트 통신으로 서버는 멀티 프로세스 기반으로 구축 필요

⚡️ 흐름도


실습

⚡️ 코드 구성

클래스를 사용하여 코드를 작성 할 예정이며, SocketServer는 서버를 가동시키고 클라이언트 소켓의 데이터만 수령 및 전달하는 역할을 수행할 예정이다.

세부적인 비즈니스 로직은 DataHandler라는 객체에 데이터를 전달하여 가공 및 DB 적재를 할 계획이다.

  • SocketServer 클래스 정의
    • 인스턴스 필드
      _config : ConfigParser 변수
      _host : Server IP 주소
      _port : Server Port 번호
    • 인스턴스 메소드
      def __init__(self): 인스턴스 생성자 메서드
      def run_server(self): 서버 실행
      def _handle_client(self): 클라이언트 소켓 데이터 수령 및 비즈니스 로직 수행 지시
      def _close_client(self): 클라이언트 소켓 종료

⚡️ 코드 예제

🖥️ 클래스 정의

class SocketServer():
	def __init__(self):
    	# 서버 구동에 필요한 정보 세팅
    
    def run_server(self):
    	# 서버 실행 및 멀티 프로세스 할당
    
    def _handle_client(self):
    	# 클라이언트 데이터 수령 및 전달
    
    def _close_client(self):
    	# 클라이언트 소켓 및 해당 프로세스 종료

🖥️ 생성자

생성자에서는 서버 연결 정보 등 민감 정보가 포함되는 것을 방지하기 위하여 config.ini 파일에 작성한 내용을 ConfigParser를 통해 세팅하여 사용한다.

init()

def __init__(self):
	self._config = configparser.ConfigParser()
    self._config.read(CONFIG, encoding='utf-8') # definition.py에 등록된 config.ini
    self._host = self._config['SOCKET']['SERVER_HOST'] # Server IP
    self._host = int(self._config['SOCKET']['SERVER_PORT']) # Server Port

definition.py

import os
from pathlib import Path

ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG = Path(ROOT_DIR) / 'config.ini' # config.ini 설정

config.ini

[SOCKET]
SERVER_HOST = localhost
SERVER_PORT = 5005

# 정보를 노출하기 싫은 경우 암호화하여 저장하고 사용할 때 decode를 하는 것도 좋은 방법이다.

🖥️ 서버 실행

run_server()

def run_server(self):
	# 소켓 객체 생성
    # AF_INET : IP와 PORT를 통한 연결(TCP)
	server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 소켓 연결 정보 세팅
    server_address = (self._host, self._port)
    
    print('Strat up on {} port {}'.format(*server_address))
    # 소켓 연결 정보 바인딩
    server_socket.bind(server_address)
    
    # 가동
    server_socket.listen()
    
    # 무한루프
    while True:
    	print('accept wait')
        # 클라이언트 접속 대기
        client_socket, client_address = server_socket.accept()
        
        # 클라이언트 접속 시 멀티 프로세스를 위한 fork() 수행
        # fork() 리턴 : 0 = 자식, -1 = 실패, 0 이상 = 부모 프로세스
        pid = os.fork()
        
        # 자식 프로세스
        if pid == 0:
        	print(f'Child process :: {os.getpid()}')
        	# 데이터 수령 및 전달 메소드 호출
        	self._handle_client(client_socket)
        
        # 실패
        elif pid == -1:
        	print('Child process fail')
        	# 필요시 추가적인 예외처리 로직 구현....
            
            # 클라이언트 소켓 종료
        	client_socket.close()
                
        # 부모프로세스
        else:
        	time.sleep(1)
            # 자식 프로세스에 client_socket 접속 할당 후 부모프로세스에서 종료
            client_socket.close()

여기서 중요한 부분은 os.fork()이다. os 모듈은 기본적으로 모든 OS의 종류에서 동작하지만 fork()의 경우 윈도우에서는 동작하지 않는다.

이유는 다음과 같다.

  • fork는 스레드가 존재하지 않던 시절의 기능으로 저비용의 멀티태스킹을 구현하기 위한 API
  • fork는 기본적으로 스레드와 무관한 '프로세스 복제'
  • 윈도우 운영체제에는 '프로세스 복제' 개념이 없음

이러한 이유로 os.fork()는 Linux 기반에서만 사용이 가능하다. 재미있는 부분은 os.fork()를 하면 부모와 자식 프로세스가 이후 코드에 대하여 동일한 수행결과를 나타낸다. 이러한 부분을 해결하기 위해 위와 같이 pid를 반환 값으로 받아 조건처리를 수행하는 것이다.

🖥️ 데이터 수령 및 전달

handle_client()

def _handle_client(self, client_socket):
	# 데이터 처리 객체 생성
    # 멀티 프로세스 기반으로 복수의 클라이언트가 접속하기 때문에
    # 데이터의 무결성 유지 및 비즈니스 로직 수행 시 충돌을 막기 위하여
    # 메소드 호출시마다 객체 생성
    data_handler = DataHandler()
    
    while True:
    	try:
        	BUFF_SIZE = 1024
            LIMIT_TIME = 10
            
            # 타임아웃 지정
            client_socket.settimeout(LIMIT_TIME)
            # 데이터 수령(버퍼 크기 지정)
            client_data = client_socket.recv(BUFF_SIZE)
            
            # 데이터가 없을 경우 예외 처리
            if not client_data:
            	raise SocketError('Empty Data Error')
            
            # 문자열 치환
            client_data = client_data.decode()
            # 클라이언트 데이터 전달
            data_handler.save_client_data(client_data)
            
            print('Client data save success')
            
		except socket.timeout as err:
        	print('client_socket Timeout Error')
            
            # 추가적인 예외처리 로직 구성....
            
            time.sleep(5)
            self._close_client(client_socket)
            break
            
		except SocketError as err:
        	print(err)
        	
            # 추가적인 예외처리 로직 구성....
            
            time.sleep(5)
            self._close_client(client_socket)
            break

해당 메소드에서는 클라이언트의 접속이 정상적인지 판단하고, 데이터를 전달하는 역할만을 수행한다. 데이터에 대한 세부적인 비즈니스 로직 수행은 해당 책임을 부여받은 객체가 수행하도록 할당한다.

🖥️ 클라이언트 소켓 종료

def _close_client(self, client_socket):
	# 소켓 연결 종료
	client_socket.close()
    # 자식 프로세스 종료
    os.exit(0)

여기서 os.exit(0)를 호출하지 않으면 부모 프로세스에서 추가적인 로직을 작성하여 관리를 해주어야한다. 해당 프로세스가 작업을 마쳤거나 예외가 발생했는데도 불구하고 연결을 끊어주지 않을 경우 시스템 자원의 누수가 발생 할 수 있으니 반드시 사용을 완료했거나 예외가 발생했다면 닫아주고 재접속을 유도하자.


마무리

여기까지가 소켓통신을 위한 서버를 구축한 코드가 되겠다. 크게 어려운 코드가 없었다. 하지만 최초에는 데이터를 처리하는 객체를 생서하지도 않았고, 서버 정보를 세팅하는 부분도 같은 클래스내에 정의해서 사용했다.

자바 개발을 하면서도 느끼지만 구조화가 되어있지 않다면 수정 사항이 발생한 경우 생각보다 많은 부분을 수정해야하는 수고스러움이 발생하니 가급적이면 최소한의 기능 단위, 정보 등을 고려하여 구조화하고 조합하여 사용하는 것이 좋다는 생각을 한다.

다음 게시글에서는 소켓 통신을 통해 전달된 데이터를 DB로 저장하는 과정에 대해서 포스팅 할 예정이다.

그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

2개의 댓글

comment-user-thumbnail
2023년 7월 4일

좋은 정보 감사합니다!!

1개의 답글