class EOFError(Exception):
pass
# 서버는 한번에 하나씩 처리, 클라이언트 세션상태 유지하는 클래스
class ConnectionBase:
# 3
def __init__(self, connection):
self.connection = connection
self.file = connection.makefile('rb')
def send(self, command):
line = command + '\\n'
data = line.encode()
self.connection.send(data)
def receive(self):
line = self.file.readline()
if not line:
raise EOFError('Connection closed')
return line[:-1].decode()
# Example 2
import random
WARMER = 'Warmer'
COLDER = 'Colder'
UNSURE = 'Unsure'
CORRECT = 'Correct'
class UnknownCommandError(Exception):
pass
# handle_connection 1
class Session(ConnectionBase):
def __init__(self, *args):
super().__init__(*args)
self._clear_state(None, None)
def _clear_state(self, lower, upper):
self.lower = lower
self.upper = upper
self.secret = None
self.guesses = []
# 클라이언트 들어오는 메시지를 처리해 명령에 맞는 메서드 호출
def loop(self):
while command := self.receive():
parts = command.split(' ')
if parts[0] == 'PARAMS':
self.set_params(parts)
elif parts[0] == 'NUMBER':
self.send_number()
elif parts[0] == 'REPORT':
self.receive_report(parts)
else:
raise UnknownCommandError(command)
# 서버가 추측할 값의 상한과 하한 설정
def set_params(self, parts):
assert len(parts) == 3
lower = int(parts[1])
upper = int(parts[2])
self._clear_state(lower, upper)
# 클라이언트가 해당하는 Session 인스턴스에 저장된 이전ㄴ 상태 바탕으로 새로운 수 추측.
# 서버가 파라미터 설정된 시점 이후에 같은 수를 두번 반복해 추측하지 않도록 보장
def next_guess(self):
if self.secret is not None:
return self.secret
while True:
guess = random.randint(self.lower, self.upper)
if guess not in self.guesses:
return guess
def send_number(self):
guess = self.next_guess()
self.guesses.append(guess)
self.send(format(guess))
# 서버 추측이 상태에 대해 클라이언트가 보낸 결과를 받은 후 Session 상태를 적절히 바꿈
def receive_report(self, parts):
assert len(parts) == 2
decision = parts[1]
last = self.guesses[-1]
if decision == CORRECT:
self.secret = last
print(f'Server: {last} is {decision}')
# Example 7
import contextlib
import math
class Client(ConnectionBase):
def __init__(self, *args):
super().__init__(*args)
self._clear_state()
def _clear_state(self):
self.secret = None
self.last_distance = None
# 파라미터를 with 문을 통해 설정함으로써 서버측에 상태를 제대로 관리.
# 첫ㅅ 명령어를 서버에게 보냄
@contextlib.contextmanager
def session(self, lower, upper, secret):
print(f'Guess a number between {lower} and {upper}!'
f' Shhhhh, it\\'s {secret}.'')
self.secret = secret
self.send(f'PARAMS {lower} {upper}')
try:
yield
finally:
self._clear_state()
self.send('PARAMS 0 -1')
# 구현하는 다른 메서드를 사용해 새로운 추측 서버에 요청
def request_numbers(self, count):
for _ in range(count):
self.send('NUMBER')
data = self.receive()
yield int(data)
if self.last_distance == 0:
return
# 세번째 명령 구현한 마지막 메서드를 통해 서버가 돌려준 추측이 마지막 결과를 알려준 추측보다
# 차갑거나 뜨거운지 알려줌
def report_outcome(self, number):
new_distance = math.fabs(number - self.secret)
decision = UNSURE
if new_distance == 0:
decision = CORRECT
elif self.last_distance is None:
pass
elif new_distance < self.last_distance:
decision = WARMER
elif new_distance > self.last_distance:
decision = COLDER
self.last_distance = new_distance
self.send(f'REPORT {decision}')
return decision
# 소켓에 리슨하는 스레드 하나 사용하고 새 연결 들어올떄 마다 스레드 추가로 시작하는 방식으로 서버실행
import socket
from threading import Thread
def handle_connection(connection):
with connection:
session = Session(connection)
try:
session.loop()
except EOFError:
pass
def run_server(address):
with socket.socket() as listener:
# Allow the port to be reused
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(address)
listener.listen()
while True:
connection, _ = listener.accept()
thread = Thread(target=handle_connection,
args=(connection,),
daemon=True)
thread.start()
# 클라이언트는 주 스레드에 실행되며 추측게임 결과를 호출한 쪽에 돌려줌
# 이 코드는 명시적 다양한 파이썬 언어 기능 활용
def run_client(address):
with socket.create_connection(address) as connection:
client = Client(connection)
results = []
with client.session(1, 5, 3):
results = [(x, client.report_outcome(x))
for x in client.request_numbers(5)]
with client.session(10, 15, 12):
for number in client.request_numbers(5):
outcome = client.report_outcome(number)
results.append((number, outcome))
return results
# Example 13
def main():
address = ('127.0.0.1', 1234)
server_thread = Thread(
target=run_server, args=(address,), daemon=True)
server_thread.start()
results = run_client(address)
for number, outcome in results:
print(f'Client: {number} is {outcome}')
main()
위 코드로 Thread와 동시에 코루틴으로 리팩토링하는 과정을 설명해주는 목차였는데
심각한 것은 소켓통신을 하는 과정 이후에 작동순서들이 머릿 속에서 그려지지가 않았다.
그래서 위 코드를 하나하나 살펴보면서 알아갔던 것들을 정리해보려고 한다.
import socket
def start_client():
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 12345))
# 소켓을 파일 객체로 변환
client_file = client_socket.makefile('rw', buffering=1)
client_file.write("안녕, 서버!\\n")
client_file.flush()
line = client_file.readline().strip()
print(f"받은 데이터: {line}")
client_file.close()
client_socket.close()
start_client()
소켓통신 예제를 살펴보던 중, 두 예제에서 소켓을 사용하는 방식이 좀 상이했다.
맨 위의 코드는 서버에 통신하기 위해서 send() 라는 메소드를 사용했고,
위의 코드는 write() 에 전송할 문자를 담고, flush()를 하여 서버에 통신하였다.
왜 이런 차이점이 발생한 것 일까?
결론은 큰 차이점은 존재하지 않는다 이다. 다만 write() 를 하게 되면 내부버퍼에 데이터가 쌓이게 되고,
flush() 를 호출하게 되면 내부버퍼의 데이터가 송신버퍼로 넘어가게 되는 차이인데,
send() 는 내부버퍼에 데이터를 쌓는 과정을 무시하고 바로 송신버퍼로 보내버린 것이다
makefile() 는 왜 쓰는 걸까?
그 이유는 소켓를 파일객체처럼 쓰기 위해서 라고 한다.
위 코드들을 보면 readline()를 써서 소켓에 통신한 데이터를 읽을 수 있게 구현됨을 볼 수 있는데,
혹시나 나처럼 C → B → A 순으로 데이터를 보내게 되면 A , AB, ABC 로 받는 거라고 생각하면 안된다.
readline() 를 하게 되면 Queue.pop() 과 유사한 효과를 나타내어서 데이터를 읽는 과정에서 읽혀진 데이터는 삭제된다고 한다
송신버퍼, 수신버퍼에 대한 이해
소켓에서 클라이언트에게 통신할 때, 거치는 버퍼가 총 3개의 버퍼가 있다 내부버퍼, 송신버퍼, 수신버퍼 이렇게 총 3가지가 있다.
여기 궁금한 부분이 송신버퍼에 데이터가 꽉차버리면 어떻게 될까였다.
이 부분을 테스트하긴 어렵고 이론적으로는 내부버퍼 쪽에 블로킹을 걸어 송신버퍼의 공간이 확보되면
수신버퍼의 데이터를 통신하고 자리가 남으면 받는 원리로 작동한다고 한다.
그렇다면 내부버퍼가 꽉 차버리는 경우는 어떻게 될까?
파이썬 기준으로 내부버퍼가 꽉차면 flush()를 하여 공간을 확보하는 경우도 있으며, 블로킹이 걸릴 수 도 있다고 한다
만일 내부버퍼에 ‘바보’, ‘말미잘’, ‘멍청이’ 가 들어있는 상태인데
송신버퍼에는 당장에 2개의 글자수만 가져올 수 있는 상황이라고 가정해봤을 때
나는 통신버퍼에 2자리 밖에 남지 않아, 바보가 쌓일 줄 알았다.
하지만 그렇지 않고 ‘멍청’ 까지만 쌓고 ‘바보말미잘멍청’ 은 내부버퍼에 남아있는 상태가 되는 것이다
그리고 위와 같은 상태가 되었을 떄, 서로에 대한 경계가 없어져버린 것이 아닌가? 했지만
바이트(멍청이)길이/바이트(멍청이) 이런식으로 데이터가 형성되어 쭉 쌓여있는 모습이여서 수신 측에서는 바이트길이를 읽고 데이터를 읽을 수 있는 원리로 돌아간다고 한다
'프로그래밍 언어 > Python' 카테고리의 다른 글
memoryview 성능 테스트 ( feat.timeit, 버퍼프로토콜 ) (0) | 2023.09.23 |
---|---|
asyncio를 활용해 간단하게 서버 구현한 코드 리뷰하기 (0) | 2023.09.22 |
with와 @contextlib.contextmanager (0) | 2023.09.20 |
컨텍스트 스위칭으로부터의 예상치 못한 결과 ( feat.GIL ) (0) | 2023.09.20 |
Thread와 Queue를 활용한 파이프라인 구현하기( feat. 콘웨이 생명게임 ) (0) | 2023.09.19 |