Thread 기반 TCP Server/Client 예제

|

Thread 기반의 TCP Server/Client

Thread 기반으로 동작하는 TCP Server/Client 예제입니다.


Common.constant.py

IP_ADDRESS = '127.0.0.1'
PORT = 10034


server_main.py

from TcpServer import tcpServer

if __name__ == '__main__':
    print('TCP Server starts.')

    while True:
        print('<Menu>')
        print('   1. Start Server')
        print('   2. Stop Server')
        print('   99. Finish')

        menu = input('Input the menu: ')

        # print('   --> Your input is {}'.format(menu))

        if menu == '1':
            tcpServer.start_server()
        elif menu == '2':
            tcpServer.stop_server()
        elif menu == '99':
            print("Exit !!!")
            tcpServer.stop_server()
            break


TcpServer.tcpServer.py

import threading
import socketserver
import Common.constant as constant

TAG = 'TcpServer'

stdout_lock = threading.Lock()


class TcpServerHandler(socketserver.BaseRequestHandler):
    def handle(self):
        with stdout_lock:
            print('Client({}) is connected.'.format(self.client_address[0]))
        sock = self.request

        while True:
            buffer = sock.recv(1024).strip()

            if not buffer:
                sock.close()
                with stdout_lock:
                    print('Client({}) is disconnected.'.format(self.client_address[0]))
                break

            received_message = str(buffer, encoding='utf-8')
            with stdout_lock:
                print('Received: {}'.format(received_message))


class TcpServerThread(threading.Thread):
    socketserver.TCPServer.allow_reuse_address = True
    server = None

    def __init(self):
        pass

    def run(self):
        if not self.server is None:
            self.server.shutdown()
            self.server = None

        self.server = socketserver.TCPServer(('', constant.PORT), TcpServerHandler)
        with stdout_lock:
            print('TcpServerThread is started.')
            print('Listening on port({})'.format(constant.PORT))
        self.server.serve_forever()
        with stdout_lock:
            print('TcpServerThread is terminated.')

    def stop(self):
        self.server.shutdown()
        self.server = None


server_thread = None


def start_server():
    stop_server()

    global server_thread
    server_thread = TcpServerThread()
    server_thread.start()


def stop_server():
    global server_thread
    if not server_thread is None:
        server_thread.stop()
        server_thread.join()
        server_thread = None


tcp_client.py

from TcpClient import tcpClient

if __name__ == '__main__':
    print('TCP Client starts.')

    while True:
        print('<Menu>')
        print('   1. Connect to Server')
        print('   2. Send a message to Server')
        print('   9. Disconnect from Server')

        print('   99. Finish')

        menu = input('Input the menu: ')

        # print('   --> Your input is {}'.format(menu))

        if menu == '1':
            tcpClient.connect()
        elif menu == '2':
            tcpClient.send_message('hello. snowdeer')
        elif menu == '9':
            tcpClient.disconnect()
        elif menu == '99':
            break


TcpClient.tcpClient.py

import threading
import time
import socket
import Common.constant as constant
from Common.log import Log as log

TAG = 'TcpClient'


class TcpClientThread(threading.Thread):
    sock = None

    def __init(self):
        pass

    def run(self):
        log.i(TAG, 'Start TcpClientThread.')
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(('127.0.0.1', 0))
        self.sock.connect((constant.IP_ADDRESS, constant.PORT))

    def stop(self):
        self.sock.close()

    def send_message(self, message):
        buffer = bytes(message, encoding='utf-8')
        self.sock.send(buffer)


client_thread = None


def connect():
    disconnect()

    global client_thread
    client_thread = TcpClientThread()
    client_thread.daemon = True
    client_thread.start()


def disconnect():
    global client_thread
    if not client_thread is None:
        client_thread.stop()
        client_thread.join()
        client_thread = None


def send_message(message):
    try:
        client_thread.send_message(message)
    except:
        print("Error to send a message !!")

Custom Logging 클래스 예제

|

Custom Logging 클래스

리눅스에서 사용하기 위한 Custom Logging 클래스를 간단하게 작성해보았습니다. 안드로이드에서의 로그 메소드들과 비슷하게 사용할 수 있도록 변경했습니다.

log.py

import logging
import logging.handlers

class Log:
    __logger = logging.getLogger('SnowLog')
    __logger.setLevel(logging.DEBUG)

    formatter = logging.Formatter('%(asctime)s > [%(levelname)s] %(message)s')
    fileHandler = logging.FileHandler('./log.txt')
    fileHandler.setFormatter(formatter)

    __logger.addHandler(fileHandler)

    @classmethod
    def d(cls, tag, message):
        cls.__logger.debug('[' + tag + '] ' + message)

    @classmethod
    def i(cls, tag, message):
        cls.__logger.info('[' + tag + '] ' + message)

    @classmethod
    def w(cls, tag, message):
        cls.logger.warning('[' + tag + '] ' + message)

    @classmethod
    def e(cls, tag, message):
        cls.logger.error('[' + tag + '] ' + message)

    @classmethod
    def c(cls, tag, message):
        cls.logger.critical('[' + tag + '] ' + message)


리눅스에서 로그 확인 할 때

사실 로그를 콘솔창이 아닌 파일로 출력하도록 한 건 로그 백업의 목적도 있지만, 파이썬의 경우 콘솔창으로 출력을 하게 될 경우 콘솔에서의 입력인 input() 메소드와 매끄럽게 동작하지 않아서였습니다. 단일 Thread의 경우는 문제가 없지만 복수의 Thread에서 로그를 출력할 경우 출력시마다 input() 버퍼의 내용을 지워버리는 문제가 있었습니다.

그래서 파일로 로그를 남길 경우 새로운 터미널을 하나 열고 다음 명령어를 입력해서 로그를 실시간으로 확인할 수 있습니다.

tail -f [파일명]

ex) tail -f log.txt

Logging 모듈 사용 방법

|

logging 모듈

Python에서는 로그 출력을 위한 logging 모듈을 제공합니다. 아주 간단히 사용할 수 있으며, print 함수 등을 통해 콘솔창에 지저분하게 출력하는 것보다 logging 모듈을 사용하는 것을 추천합니다.


로그 레벨

Python에서 로그 레벨은 다음과 같습니다. 안드로이드 등에서와 거의 동일합니다.

  • DEBUG
  • INFO
  • WARNING
  • ERROR
  • CRITICAL


간단한 logging 모듈 사용 예제

import logging as log

log.basicConfig(filename='./log.txt', level=log.DEBUG)

if __name__ == '__main__':
    log.debug('debug')
    log.info('info')
    log.warning('warning')
    log.error('error')
    log.critical('critical')

출력 결과는 다음과 같습니다.

DEBUG:root:debug
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical

basicConfig 함수를 통해서 로그 파일의 이름을 지정할 수 있고, filemode 속성의 값을 a, w 등으로 지정할 수 있습니다. a의 경우 기존 파일에 이어서 기록(Append)하며, w의 경우 기존 파일 내용을 삭제하고 새로 작성합니다.


콘솔창과 파일에 동시에 로그 남기기

import logging
import logging.handlers

log = logging.getLogger('snowdeer_log')
log.setLevel(logging.DEBUG)

fileHandler = logging.FileHandler('./log.txt')
streamHandler = logging.StreamHandler()

log.addHandler(fileHandler)
log.addHandler(streamHandler)

if __name__ == '__main__':
    log.debug('debug')
    log.info('info')
    log.warning('warning')
    log.error('error')
    log.critical('critical')


Formatter 적용하기

import logging
import logging.handlers

log = logging.getLogger('snowdeer_log')
log.setLevel(logging.DEBUG)

formatter = logging.Formatter('[%(levelname)s] (%(filename)s:%(lineno)d) > %(message)s')

fileHandler = logging.FileHandler('./log.txt')
streamHandler = logging.StreamHandler()

fileHandler.setFormatter(formatter)
streamHandler.setFormatter(formatter)

log.addHandler(fileHandler)
log.addHandler(streamHandler)

if __name__ == '__main__':
    log.debug('debug')
    log.info('info')
    log.warning('warning')
    log.error('error')
    log.critical('critical')

출력 결과는 다음과 같습니다.

[DEBUG] (log_test.py:19) > debug
[INFO] (log_test.py:20) > info
[WARNING] (log_test.py:21) > warning
[ERROR] (log_test.py:22) > error
[CRITICAL] (log_test.py:23) > critical


Formatter Keywords

이름 포맷 설명
asctime %(asctime)s 날짜 시간, 밀리세컨드까지 출력. ex) 2017.11.17 12:31:45,342
created %(created)f 생성 시간 출력
filename %(filename)s 파일명
funcnName %(funcName)s 함수명
levelname %(levelname)s 로그 레벨(DEBUG, INFO, WARNING, ERROR, CRITICAL)
levelno %(levelno)s 로그 레벨을 수치화해서 출력(10, 20, 30, …)
lineno %(lineno)d 소스의 라인 넘버
module %(module)s 모듈 이름
msecs %(msecs)d 로그 생성 시간에서 밀리세컨드 시간 부분만 출력
message %(message)s 로그 메시지
name %(name)s 로그 이름
pathname %(pathname)s 소스 경로
process %(process)d 프로세스(Process) ID
processName %(processName)s 프로세스 이름
thread %(thread)d Thread ID
threadName %(threadName)s Thread Name


파일의 최대 용량 및 새로운 파일 생성하는 방법

RotatingFileHandler 함수를 이용해서 파일의 최대 크기 및 새로운 파일을 생성할 때 최대 개수를 지정해줄 수 있습니다. 아래와 같은 코드를 이용해서 최대 10MB씩, 총 20개의 로그 파일을 만들어서 순환하도록 할 수 있습니다.

log_max_size = 10 * 1024 * 1024
log_file_count = 20
fileHandler = logging.handlers.RotatingFileHandler(filename='./log.txt', maxBytes=log_max_size,
                                                   backupCount=log_file_count)


logging.handlers

위에서는 FileHandlerStreamingHandlerRotatingFileHandler 등을 사용했었는데 이외에도 다양한 핸들러들이 존재합니다.

핸들러 설명
SocketHandler 외부 로그 서버로 소켓을 통해 전송
DatagramHandler UDP 통신을 통해 외부 서버로 전송
SysLogHandler Unix 류의 syslog 데몬에게 로그 전송
SMTPHandler 메일로 로그 전송
HTTPHandler HTTP를 통해 로그 전송

일반적으로 사용하는 포트 번호

|

일반적으로 사용하는 포트 번호 및 프로토콜

일반적으로 사용하는 포트 번호 및 프로토콜은 다음과 같습니다.

프로토콜 명령어 포트 번호
Echo echo 7
Daytime daytime 13
File Transfer ftp 21/20
Secure Shell ssh 22
Telnet Terminal telnet 23
Simple Mail Transfer smtp 25
Domain Name Service domain 53
Trivial File Transfer tftp 69
Finger finger 79
HyperText transfer http 80/84/8000
NetNews nntp 119

라즈베리파이에서의 pyAudio 모듈 사용 예제

|

라즈베리파이에서 pyAudio 모듈 사용하기

라즈베리파이에서 pyAudio 모듈을 사용해서 오디오 녹음을 하고 재생하는 예제 코드입니다. 오디오 파일 녹음과 재생은 Streaming 형태로 이루어지며, 각각 wav 파일로 저장하고 재생합니다.


pyAudio 모듈 설치

먼저 터미널에서 다음 명령어를 입력해서 pyAudio를 설치합니다.

sudo apt install python3-pyaudio


pyaudio_recorder.py

import pyaudio
import wave

CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
RECORD_SECONDS = 5
WAVE_OUTPUT_FILENAME = "output.wav"

p = pyaudio.PyAudio()

stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)

print("Start to record the audio.")

frames = []

for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
    data = stream.read(CHUNK)
    frames.append(data)

print("Recording is finished.")

stream.stop_stream()
stream.close()
p.terminate()

wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b''.join(frames))
wf.close()


pyaudio_player.py

import pyaudio
import wave
import sys

CHUNK = 1024

if len(sys.argv) < 2:
    print("Plays a wave file.\n\nUsage: %s filename.wav" % sys.argv[0])
    sys.exit(-1)

wf = wave.open(sys.argv[1], 'rb')

p = pyaudio.PyAudio()

stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)

data = wf.readframes(CHUNK)

while data != '':
    stream.write(data)
    data = wf.readframes(CHUNK)

stream.stop_stream()
stream.close()

p.terminate()