빅데이터 프로그래밍/Python

[Python] 17. [Network] threading module, 채팅 서버/클라이언트 제작

밍글링글링 2017. 8. 2.
728x90

01.  threading module
- thread란 하나의 프로세스내에서 진행되는 하나의 실행단위를 뜻하며, 
  하나의 프로세스에서 여러 실행단위가 실행되는것을 멀티스레드라고 한다. 
  프로세스와 스레드는 모두 프로그램을 수행된다는 공통점을 가지고 있지만, 
  프로세스는 윈도우에서 여러 응용프로그램을 각각 실행시키는것처럼 독립적으로 실행되어
  독립된 메모리공간을 사용하지만, 멀티스레드는 하나의 프로세스내에서 여러 스레드들이
  프로세스공간의 메모리를 공유하여 사용할수 있다.
  파이썬에서는 threading이라는 멀티스레드 기능을 지원하는 모듈을 제공한다.
  threading모듈에서 자주 사용되는 객체를 살펴보고, 간단한 예제를 통해 threading모듈을 알아보자
 

1. Thread 객체
  - 스레드를 사용하기 위해서는 일반적으로 threading.Thread를 상속받은 클래스객체를
    생성하여 사용하며, 아래 나열된 메서드들을 주로 이용한다. 
    생성자를 재정의하는 경우 반드시 Thread.__init__()을 수행해야 한다.

1) Thread.start(): 
   스레드를 시작할때 사용함
 
2) Thread.run(): 스레드의 주요 동작을 정의함, 개발자가 주로 작업하는 함수
 
3) Thread.join([timeout]): 스레드가 종료되기를 기다린다.
   timeout이 정의된경우, 최대 정의된 시간(초)만큼 기다린다.

 

2.  Lock 객체
- 하나의 프로세스에서 2개 이상의 스레드가 동시에 수행되는 경우,
  스레드간에 프로세스 공간의 메모리를 공유한다. 
  만약 하나의 변수에 대해서 2개이상의 스레드가 변경을 가하고자 한다면, 
  어떤 스레드가 먼저 수행되느냐에 따라 결과가 달라질수 있다. 
  이러한 상태를 경쟁상태(race condition)라고 한다. 
  이러한 상황을 방지하기 위해서 파이썬에서는 Lock이라는 동기화 객체를 지원한다.
 
- Lock객체는 locked와 unlocked의 2가지 상태를 가지며, acquire()(잠금 걸기)와 release()(잠금 해제)의
  두가지 함수만을 제공한다.
  unlocked상태에서 acquire()(잠금 걸기)가 호출되면 locked상태로 바뀌고
  locked상태에서 release()(잠금 해제)가 호출되면 unlocked상태로 바뀌게 된다.
  locked상태에서 다른스레드가 acquire()를 호출하면 locked상태의 스레드가
  release()할때까지 멈추게 된다.
 
  
3. 동기화 실습
- acquire()(잠금 걸기)와 release()(잠금 해제) 메소드를 주석 처리하고 실행을 비교할 것.

[실행 화면]
- Thread 처리를 하지않은 경우

> name=usr1
count: 9
> name=usr2
count: 8
> name=usr3
count: 7
> name=usr3
> name=usr2
> name=usr1
count: 6
count: 5
count: 4
> name=usr1
> name=usr3
> name=usr2
count: 3
count: 2
count: 1
> name=usr3
> name=usr1
> name=usr2
count: 0
count: -1
count: -2
usr1 fixed 4
usr2 fixed 4
usr3 fixed 4
 

- Thread 처리를 한경우

> name=usr1
count: 9
> name=usr1
count: 8
> name=usr2
count: 7
> name=usr1
count: 6
> name=usr3
count: 5
> name=usr1
count: 4
> name=usr3
count: 3
> name=usr2
count: 2
> name=usr1
count: 1
> name=usr3
count: 0
usr1 fixed 5
usr2 fixed 2
usr3 fixed 3
 
▷ network.Thread1.py
-----------------------------------------------------------------------------------
# -*- coding: utf-8 -*-

from threading import Thread, Lock
import time

count=10
lock=Lock()

class Developer(Thread): # Thread 상속
    def __init__(self, name): # 생성자
        Thread.__init__(self)  # 부모 클래스의 생성자 호출
        self.name=name     # instance 변수
        self.fixed=0            # instance 변수 

    def run(self):             # 지정된 메소드 형식, start()에 호출됨.
        global count         # class 밖의 전역 변수 접근
        while 1:                # 무한 루틴
            lock.acquire()    # 잠금 얻기
            if count > 0:
                print ('> name=%s' % self.getName()) # self.getName은 Thread 이름 반환
                count -= 1       # 전역 변수 접근
                print('count: ' + str(count))
                
                lock.release()  # 잠금 해제
                self.fixed += 1  # 객체 멤버 변수, instance, attribute
                time.sleep(1)  # 1: 1초
            else:
                lock.release()   # 잠금 해제
                break  # while 문 종료 -> run() 메소드 종료

dev_list=[]  # list 선언, 사용자 저장
for name in ['usr1','usr2','usr3']:
    dev=Developer(name)
    dev_list.append(dev)  # Developer class의 객체를 list에 추가
    dev.start()    # 스레드 호출, run() 호출

for dev in dev_list:
    dev.join()     # 스레드가 모두 종료될때까지 대기  
    print(dev.name, 'fixed', dev.fixed) # 가변인자로 출력
    
    
    
-----------------------------------------------------------------------------------
 

 

02. 채팅 서버/클라이언트 제작

1. Server
[실행 화면]

C:/CD 201705_python/ws_python/oop/network
python ChatServer1.py
▷ network.ChatServer1.py
-----------------------------------------------------------------------------------
# -*- coding: utf-8 -*-

import socketserver, sys
import threading

lock = threading.Lock()

class UserManager:
    def __init__(self):
        # Dictionary, 변경 가능, 키, 값
        '''
        {"왕눈이":(conection 객체, "172.16.3.1"),
         "아로미":(conection 객체, "172.16.3.2"),
         "가길동":(conection 객체, "172.16.3.3"),
        }
        '''  
        self.users = {} 

    def addUser(self, username, conn, addr):
        if username in self.users:
            # 현재 접속자에게 문자열 송신
            conn.send('이미 등록된 사용자입니다.\n'.encode())
            return None

        # 새로운 사용자를 등록함
        lock.acquire()  # Lock
        self.users[username] = (conn, addr) # Connection, IP tuple 저장
        lock.release()   # Unlock

        # 모든 사용자에게 메시지 전송
        self.sendMessageToAll('[%s]님이 입장했습니다.' %username)
        print('▷ 대화 참여자 수 [%d]' %len(self.users))
        
        return username

    def removeUser(self, username):
        # 삭제하려는 ID가 없으면 아무일도 안함.
        if username not in self.users:
            return

        lock.acquire()  # Lock
        del self.users[username] # Dictionary에서 아이디에 해당하는 항목 삭제
        lock.release()   # Unlock

        self.sendMessageToAll('[%s]님이 퇴장했습니다.' %username)
        print('▷ 대화 참여자 수 [%d]' %len(self.users))

    def messageHandler(self, username, msg):
        if msg.strip() == '/exit':            # 접속자 접속 해제
            self.removeUser(username)  # 사용자 삭제
            return -1
        
        self.sendMessageToAll('[%s] %s' %(username, msg))


    def sendMessageToAll(self, msg):
        # Dictionary 값 2개 추출: Connection, IP로 구성된 tuple
        for conn, addr in self.users.values(): 
            conn.send(msg.encode()) # 각각의 사용자에게 메시지 전송
          

class MyTcpHandler(socketserver.BaseRequestHandler):  # 한번만 객체 생성됨
    userManager = UserManager()  # 사용자 관리 객체 생성
    
    def handle(self):   # 사용자 접속시마다 계속 자동 실행     
        print('▷ [%s] 연결됨' %self.client_address[0])

        try:
            username = self.registerUsername() # 사용자 id 처리
            msg = self.request.recv(1024) # 접속된 사용자로부터 입력대기
            while msg:
                print(msg.decode()) # 서버 화면에 출력
                if self.userManager.messageHandler(username, msg.decode()) == -1:
                    self.request.close() # Connection close
                    break   # 메시지 수신 대기 종료
                msg = self.request.recv(1024) # 메시지 수신 대기
                
        except Exception as e:
            print(e)

        print('▷ [%s] 접속종료' %self.client_address[0])
        self.userManager.removeUser(username)

    def registerUsername(self):  # 접속자의 이름 받기
        while True:
            self.request.send('로그인ID: '.encode()) # 신규 현재 접속자에게 전송
            username = self.request.recv(1024)     # 수신 대기
            username = username.decode().strip()  # strip(): 공백 제거
            if self.userManager.addUser(username, self.request, self.client_address):
                return username

# socketserver.ThreadingMixIn: 독립된 스레드로 처리하도록 접속시 마다 새로운 스레드 생성
# ThreadingMixIn, TCPServer class 상속
class ChatingServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass
        
def runServer(ip):
    print('▷ 채팅 서버를 시작합니다.')
    print('▷ 채텅 서버를 끝내려면 Ctrl-C를 누르세요.')

    try:
        server = ChatingServer((ip, 9009), MyTcpHandler)
        server.serve_forever()          # 무한 실행
    except KeyboardInterrupt:       # Ctrl + C 입력시 종료
        print('▷ 채팅 서버를 종료합니다.')
        server.shutdown()               # 서버 종료
        server.server_close()            # 메모리 해제

''' 
C:
CD 201705_python/ws_python/oop/network
python ChatServer1.py 172.16.7.100
'''
runServer(sys.argv[1])


-----------------------------------------------------------------------------------

 

2. Client
[실행 화면]

C:/CD 201705_python/ws_python/oop/network
python ChatClient1.py 172.16.7.100 9009
▷ network.ChatClient1.py
-----------------------------------------------------------------------------------
# -*- coding: utf-8 -*-

import sys, socket
from threading import Thread # Thread() 함수 import

def rcvMsg(sock):
    while True:   # 연속성 스레드
        try:
            data = sock.recv(1024)  # 서버로부터 문자열 수신
            if not data:  # 문자열 없으면 종료
                break
            print(data.decode())       
        except:
            pass  # 예외 원인 무시

def runChat(host, port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((host, port))  # 소켓 연결
        # rcvMsg 함수를 연속 실행하는 스레드 생성,
        # sock 소켓 객체를 tuple 형식으로 전달
        t = Thread(target=rcvMsg, args=(sock,))  
        t.daemon = True # 스레드를 생성한 메인 스레드가 종료되면 자동으로 종료됨.
        t.start()               # 스레드 시작

        while True:
            msg = input()        # 키보드 입력
            if msg == '/exit':    # 종료
                sock.send(msg.encode())
                break               # while 종료

            sock.send(msg.encode()) # 입력 메시지 전송

''' 
C:
CD 201705_python/ws_python/oop/network
python ChatClient1.py 172.16.7.100 9009 
'''            
runChat(sys.argv[1], int(sys.argv[2]))



-----------------------------------------------------------------------------------
 

728x90

댓글