TCP/UDP 멀티스레드 서버 프로그래밍
서버의 종류
- 반복 서버 (iterative server)
- 클라이언트 요청을 하나씩 서비스하는 서버
- 여러 클라이언트 동시 요청 시 앞의 클라이언트 서비스 종료까지 대기해야 됨
- 대기 시간이 길어지는 단점
- 병행 서버 (concurrent server)
- 반복 서버의 단점 극복
- 클라이언트들을 동시에 서비스하는 서버
- 방식 종류
- 스레드 방식 : 클라이언트 마다 별도의 스레드 사용 → 멀티 스레드
- 이벤트 구동 방식 : 이벤트가 발생하면 처리하는 방식
TCP 멀티스레드 서버
- 스레드 : 운영체제에 의해 시간이 배분되고 관리되는 프로그램의 실행 단위
- 스택, 데이터 메모리 등을 공유
- 스레드를 생성하고 스레드에게 함수의 실행을 맡기면 사용자의 개입이 필요 없음
- 메인 스레드는 클라이언트를 연결하고 데이터 처리(함수)는 스레드를 생성하여 서브 스레드에게 맡김
[ 멀티 스레드를 이용한 네트워크 프로그램 순서 ]
소켓 생성 및 결합 (메인 스레드 내에서)
socket(), bind()
무한루프를 돌며 외부 클라이언트 요청 대기 → 접속
while True: # 멀티 유저라면 listen() accept() create thread(recv_handler, args) # 서브 스레드 생성
접속 요청 시 서브 스레드로 실행 → 데이터 처리
def recv_handler(): while True: ~ 수신 데이터 처리 ~
b에서는 접속 관리, c에서는 데이터 처리를 담당하며, 동시성을 제공할 수 있다.
[ 멀티 스레드 구현 방법 (python) ]
_thread 모듈 사용
메인 스레드로 실행되는 main 함수에서 서브 스레드를 생성하고 인자와 함께 handler() 함수를 지정
서브 스레드생성과 실행
_thread.start_new_thread ( handler, (clientsock, addr)) # 서브 스레드 생성하고 실행 # 인자 1 : 서브 스레드로 실행할 함수 : 함수 포인터값 (함수명) # 인자 2 : 함수로 전달할 인자 (클라이언트 소켓, 주소)
구현
start_new_thread() 함수를 통해 서브 스레드가 생성되고, 서브 스레드는 이 함수이 인자인 handler() 를 실행한다.
from socket import * import _thread def handler(clientsock, addr): # 서브 스레드가 수행할 함수 (데이터 수신 무한 루프) while True: # 데이터 수신 및 출력 # 응답 전송 if __name__ == "__main__": # 메인 스레드 while True: # 소켓을 생성하고 클라이언트의 접속을 대기 clientsock, addr = serversock.accept() # accept() 함수는 client 이름과 주소를 반환 _thread.start_new_thread(handler, (clientsock, addr))
# 클라이언트에게 메세지를 받으면 출력하고 응답 메세지 송신하는 # TCP 멀티스레드 서버 프로그램 from socket import * import _thread BUFF = 1024 def response(key): return "서버 응답 메시지" def handler(clientsock, addr): #핸들러 함수 while True: data = clientsock.recv(BUFF) print('data: ' + repr(data)) #repr : Byte 데이터를 문자열 데이터로 표현해주는 함수 if not data: break clientsock.send(response("").encode()) #encode : 문자열 -> Byte 데이터로 print('sent: ' + repr(respons(""))) if __name__ == "__main__": ADDR = (HOST, PORT) serversock = socket(AF_INET, SOCK_STREAM) # socket 매개변수 (family, TCP/UDP 유형) serversock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 이미 사용되고 있는 주소나 포트 번호 바인딩 허용 serversock.bind(ADDR) serversock.listen(5) # 총 5개의 Client까지 접속 허용 while True: print('waiting for connection') clientsock, addr = serversock.accept() print('connected from:', addr) _thread.start_new_thread(handler, (clientsock, addr))
핸들러 함수 : 클라이언트로부터 수신한 데이터를 화면에 표시하고 응답을 송신함.
핸들러 함수의 실행을 스레드에게 맡기면 사용자 프로그램에서는 데이터 수신을 관리할 필요가 없음
threading.Thread() 함수 사용
handler 함수를 정의하고 스레드로 실행하는 방법
메인 스레드에서 실행할 함수를 정의하고, threading.Thread 클래스를 사용하여 스레드 객체를 생성한 다음, start() 메서드를 사용하여 서브 스레드를 생성함
threading.Thread로 스레드 객체를 생성할 때 실행할 함수 이름과 인수를 지정함
# 스레드 객체 생성 t = threading.Thread(target=ftn, args=(arguments)) # 스레드가 실행할 함수 이름, 함수로 전달할 인수 t.start() # 스레드 시작 (메인 스레드와 독립적으로 서브 스레드 실행) def ftn(arguments): # 메인 스레드에서 실행할 함수
[ threading 모듈과 함수를 사용한 TCP 채팅 프로그램 ]
특징
- 데이터를 수신하고 처리하는 함수를 정의하고, 서브 스레드로 실행
- 서버 는 수신 메시지를 모든 클라이언트에게 전송
- 메인 스레드와 서브 스레드의 역할
- 메인 스레드: 소켓 생성과 서브 스레드 생성 및 함수 실행
- 서브 스레드: handler() 함수 실행
메인 스레드 서브 스레드 (handler) 소켓 생성 데이터 수신 서브 스레드 생성 및 시작 소켓 리스트의 모든 소켓에게 데이터 송신 연결 요청을 받아 새로운 소켓 생성 데이터가 없으면 소켓 제거 새로운 소켓을 소켓리스트 추가 import socket import threading # 소켓 생성 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("", 2500) sock.listen(1) connections = [] # 서버와 연결된 클라이언트 소켓 목록 # 서브 스레드의 생성과 실행 while True: c, a = sock.accept() cThread = threading.Thread(target=handler, args = (c,a)) # 서브 스레드 생성 cThread.daemon = True cThread.start() # 서브 스레드 실행 connections.append(c) # 새로운 클라이언트를 목록에 추가 print(connections) # 데이터를 수신하고 처리하는 함수 def handler(c, a): global connections # 전역 변수, 서버와 연결된 클라이언트 목록 저장 while True: data = c.recv(2014) for connection in connections: # connections내 모든 클라이언트에게 데이터 전송 connection.send(bytes(data)) if not data: connections.remove(c) c.close() break
threading + subclass 사용
파생 클래스를 생성하고 메서드를 재정의하는 방법
threading.Thread의 파생 클래스를 정의하고 인수와 함께 파생 클래스 객체를 생성
스레드에서 처리할 내용을 run() 메서드에 정의하며 파생 클래스 객체의 start() 메서드를 사용하여 스레드 시작
객체의 daemon 속성 True로 → 메인 스레드가 종료될 때 서브 스레드도 종료 |속성이 False → 메인 스레드가 종료되어도 서버 스레드는 후순위로 진행
# 파생 클래스 (이름 커스텀) class sub_class(threading.Thread): def _init_(self, args): # 클래스 객체를 생성할 때 전달받은 인수 threading.Thread_init(self) # 스레드가 시작되면 실행되는 run 함수 재정의 def run(self): t = sub_class(args) # 클래스 객체를 생성하고 인수 전달 t.daemon = True # 메인 스레드가 종료되면 서브 스레드도 종료됨 t.start()
[ threading 모듈의 파생 클래스를 이용한 TCP 에코 서버 프로그램 ]
import socket import threading # 자식 클래스에서는 초기화를 위한 '__init__' 함수와 실행을 위한 'run()' 함수가 정의되어야 함 class ClientThread(threading.Thread): # 자식 클래스 def __init__(self, clientAddress, clientsocket): threading.Thread.__init__(self) # 부모 클래스 초기화 함수 실행 self.csocket = clientsocket # 인스턴스 변수 정의 print("New Connection Added: ", clientAddress) def run(self): # 스레드 객체가 생성되면 자동실행 함수. 데이터를 수신하여 출력 (handler 함수와 유사) print("Connection from: ", clientAddress) msg = "" while True: data = self.csocket.recv(2048) msg = data.decode() if msg == "quit" : break print("from client": msg) self.csocket.send(bytes(msg, "UTF-8")) print("Client at", clientAddress, "disconnected..") # 메인 스레드 LOCALHOST = "127.0.0.1" PORT = 2500 # 서버내에 소켓 생성 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsocketopt(socket.SOL_SOCKET, socket.SO_REUSERADDR, 1) # 주소 재사용 server.bind((LOCALHOST, PORT)) print("Server Started") print("Waiting for client request..") while True: server.listen(1) clientsock, clientAddress = server.accept() newthread = ClientThread(clientAddress, clientsock) # 서브 스레드 생성후 시작 (run() 함수) 호출 newthread.start()
concurrent.future 모듈 사용
[ ThreadPoolExecutor 클래스 ]
멀티스레드 구현 가능
Executor의 subclass이므로 Executor의 submit() 메서드를 이용하여 실행 함수를 등록할 수 있음
import concurrent.future as cf with cf.ThreadpoolExecutor(max_worker = 10) as cthread # 스레드 생성 cthread.submit(receive_message, sock, addr) # 실행 함수와 인자 전달 def receive_message(sock, addr): # 스레드로 실행할 함수
imort concurrent.futures as cf from socket import socket def receive_message(sock, addr): #thread로 실행할 함수 while True: r_msg = sock.recv(1024) if not r_msg: break print("Received: ", r_msg.decode()) sock.sendall(r_msg) sock.close() s_sock = socket(AF_INET, SOCK_STREAM) s_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) s_sock.bind(("`127.0.0.1", 2500)) # threadpool에 등록 with cf.ThreadPoolExecutor(max_workers = 10) as cthread: #thread 생성 s_sock.listen(1) c_sock, addr = s_sock.accept() # 마지막 submit 메서드 실행줘야 등록이 완료된다.
무한 루프의 개수는 스레드의 개수이다.
GUI 프로그래밍
[ threading 모듈을 이용한 GUI 서버/클라이언트 프로그램]
- 클라이언트로부터 섭씨 온도를 받아 화씨 온도로 변환하여 전송하는 서버 프로그램
- 서버 : 텍스트 모드
- 클라이언트 : GUI
- 클라이언트에서 수신 데이터를 사용자 화면에 표시하기 위해 스레드 필요
- GUI 프로그래밍→ 유저 친화적인 프로그래밍
[서버]
1) 소켓을 생성하고 클라이언트와 연결한다
import socket
socket = socket.socket
s = socket(AF_INET, SOCK_STREAM)
s.setsockopt(SOL_SOCKET, SO_REUSERADDR, 1)
s.bind(("", port))
s.listen(1)
conn, (remotehost, remoteport) = s.accept()
# conn : client 소켓 정보
# remotehost : client address
# remoteport : port num
2) 섭씨 온도를 화씨 온도로 변환하여 전송
while True:
data = conn,rec(BUFSIZE) # data에 섭씨온도 저장 (Byte 형태)
if not data:
break
data = float(data.decode()) # 수신 데이터를 float형으로 변환 (Byte -> 문자열 -> 실수)
data = 9.0/5.0*data + 32.0 # 화씨 온도 계산
data = '{:.1f}'.format(data) # 소숫점 1자리까지 표시
conn.send(data.send())
conn.close()
[클라이언트] - GUI 클라이언트 프로그램
- 사용자로부터 섭씨 온도를 입력 받아 서버로 전송하고, 서버로부터 화씨 온도를 수신하여 표시
- Label : 문자열 표현하는 객체
- Entry : 입력받을 수 있는 칸
- Button : 클릭할 수 있는 버튼
calculate(): 버튼 callback 함수
# 섭씨 온도를 서버로 전송 def calculate(): global temp temp = float(entry1.get()) # Read a temp # entry1.delete(0, END) sock.send(str(temp).encode()) # send the temp in C to server (섭씨 온도 창의 값을 읽어 서버로 전송)
사용자 화면 구성
import tkinter root = Tk() message_label = Label(text = 'Enter a temperature(C)', font = ("Verdana", 16)) entry1 = Entry(font=('Verdana', 16), width = 5) recv_label = Label(text = 'Temperature in F', font = ("Verdana", 16)) entry2 = Entry(font=('Verdana', 16), width = 5) calc_button = Button(text= '전송', font = ("Verdana", 12), command = calculate) message_label.grid(row = 0, column = 0, sticky = W) recv_label.grid(row = 1, column = 0, sticky = W) entry1.grid(row = 0, column = 1) entry2.grid(row = 1, column = 1) calc_button.grid(row = 0, column = 2, padx = 10)
소켓 생성과 메세지를 수신하는 메인 스레드
def handler(sock): while True: try: r_msg = sock.recv(1024) except: pass else: entry2.delete(0, END) entry2.insert(r_msg.decode()) entry1.delete(0, END) sock = socket(AF_INET, SOCK_STREAM) sock.connect(("localhost", 2500)) cThread = threading.Thread(target = handler, args = (sock,)) cThread.start()
'CS > 네트워크' 카테고리의 다른 글
[네트워크 프로그래밍] Ch10. 사물인터넷을 위한 프로그래밍 (0) | 2023.06.06 |
---|---|
[네트워크 프로그래밍] Ch6. 소켓 네트워크 프로그래밍 (0) | 2023.06.05 |