네트워크를 통해 파일을 전송하는 방법은 많은 애플리케이션에서 필요한 기본적인 기능입니다. 본 기사에서는 Python을 사용한 소켓 프로그래밍의 기초부터 실제 파일 전송 방법, 에러 처리, 응용 예제, 보안 대책까지 자세히 설명합니다. 초보자부터 중급자까지 누구나 이해할 수 있도록 친절하게 설명합니다.
소켓 프로그래밍의 기초
소켓 프로그래밍은 네트워크 통신을 구현하기 위한 기본적인 방법입니다. 소켓이란 통신의 끝점을 나타내며 데이터의 송수신을 가능하게 하는 인터페이스입니다. 소켓을 사용하면 다른 컴퓨터 간에 데이터를 주고받을 수 있습니다.
소켓의 종류
소켓에는 주로 2가지 종류가 있습니다:
- 스트림 소켓 (TCP): 신뢰성 높은 데이터 전송을 제공합니다.
- 데이터그램 소켓 (UDP): 빠르지만 신뢰성은 TCP에 비해 떨어집니다.
소켓의 기본 작업
소켓을 사용할 때의 기본 작업은 다음과 같습니다:
- 소켓 생성
- 소켓 바인딩 (서버 측)
- 연결 설정 (클라이언트 측)
- 데이터 송수신
- 소켓 닫기
Python에서의 기본 작업 예시
다음은 Python에서 소켓을 생성하고 기본 작업을 수행하는 예제입니다:
import socket
# 소켓 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 서버 측 설정
server_socket.bind(('localhost', 8080))
server_socket.listen(1)
# 클라이언트 측 연결
client_socket.connect(('localhost', 8080))
# 연결 수락
conn, addr = server_socket.accept()
# 데이터 송수신
conn.sendall(b'Hello, Client')
data = client_socket.recv(1024)
# 소켓 닫기
conn.close()
client_socket.close()
server_socket.close()
이 예제에서는 로컬 호스트에서 서버와 클라이언트가 통신하는 기본 절차를 보여줍니다. 실제 파일 전송은 이를 기반으로 이루어집니다.
Python에서의 소켓 기본 설정
Python에서 소켓을 사용하려면 먼저 소켓을 생성하고 기본 설정을 해야 합니다. 여기서는 그 구체적인 절차를 설명합니다.
소켓 생성
Python의 socket
모듈을 사용하여 소켓을 생성합니다. 다음 코드는 TCP 소켓을 생성하는 예입니다.
import socket
# 소켓 생성
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
소켓 파라미터
소켓 생성 시 지정하는 파라미터는 다음과 같습니다:
AF_INET
: IPv4 주소를 사용SOCK_STREAM
: TCP 프로토콜을 사용
소켓 바인딩 및 리슨 (서버 측)
서버 측에서는 소켓을 특정 주소와 포트에 바인딩하고, 연결 요청을 대기하도록 설정합니다.
# 서버 측 설정
server_address = ('localhost', 8080)
sock.bind(server_address)
sock.listen(1)
print(f'Listening on {server_address}')
소켓 연결 (클라이언트 측)
클라이언트 측에서는 서버에 연결을 시도합니다.
# 클라이언트 측 설정
server_address = ('localhost', 8080)
sock.connect(server_address)
print(f'Connected to {server_address}')
데이터 송수신
소켓이 연결되면 데이터를 송수신합니다.
# 데이터 송신 (클라이언트 측)
message = 'Hello, Server'
sock.sendall(message.encode())
# 데이터 수신 (서버 측)
data = sock.recv(1024)
print(f'Received {data.decode()}')
주의 사항
- 송수신하는 데이터는 바이트 배열로 처리해야 합니다. 문자열을 보낼 경우
encode()
를, 받은 바이트 배열을 문자열로 변환할 경우decode()
를 사용합니다.
소켓 닫기
통신이 끝나면 소켓을 닫고 리소스를 해제합니다.
sock.close()
이로써 기본적인 소켓 설정과 작업이 완료되었습니다. 다음은 실제 파일 전송을 위한 서버와 클라이언트의 구체적인 구현으로 넘어갑니다.
서버 측 구현
파일을 수신하는 서버 측 코드를 자세히 설명합니다. 여기서는 Python을 사용하여 파일을 수신하는 서버를 구축하는 방법을 설명합니다.
서버 소켓 설정
먼저, 서버 소켓을 생성하고 특정 주소와 포트에 바인딩한 후 연결 요청을 기다립니다.
import socket
# 소켓 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 주소와 포트 지정
server_address = ('localhost', 8080)
server_socket.bind(server_address)
# 연결 요청을 기다림
server_socket.listen(1)
print(f'Server listening on {server_address}')
연결 수락
클라이언트로부터 연결 요청을 수락하고 연결을 확립합니다.
# 연결 요청을 수락
connection, client_address = server_socket.accept()
print(f'Connection from {client_address}')
파일 수신
이제 클라이언트에서 전송된 파일을 수신합니다. 여기서는 수신된 데이터를 파일에 기록하는 작업을 합니다.
# 수신된 파일 저장 위치
file_path = 'received_file.txt'
with open(file_path, 'wb') as file:
while True:
data = connection.recv(1024)
if not data:
break
file.write(data)
print(f'File received and saved as {file_path}')
수신 루프 세부 사항
recv(1024)
: 1024바이트씩 데이터를 수신합니다.- 수신 데이터가 없으면 (
not data
) 루프를 종료합니다. - 수신한 데이터를 지정된 파일에 기록합니다.
연결 닫기
통신이 끝나면 연결을 닫고 리소스를 해제합니다.
# 연결 닫기
connection.close()
server_socket.close()
서버 측 전체 코드
다음은 앞서 설명한 모든 단계를 포함한 서버 측 전체 코드입니다.
import socket
def start_server():
# 소켓 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 주소와 포트 지정
server_address = ('localhost', 8080)
server_socket.bind(server_address)
# 연결 요청을 기다림
server_socket.listen(1)
print(f'Server listening on {server_address}')
# 연결 요청을 수락
connection, client_address = server_socket.accept()
print(f'Connection from {client_address}')
# 수신된 파일 저장 위치
file_path = 'received_file.txt'
with open(file_path, 'wb') as file:
while True:
data = connection.recv(1024)
if not data:
break
file.write(data)
print(f'File received and saved as {file_path}')
# 연결 닫기
connection.close()
server_socket.close()
if __name__ == "__main__":
start_server()
이 코드를 실행하면 서버 측에서 클라이언트로부터 파일을 수신하고 지정된 위치에 저장합니다. 다음은 클라이언트 측 구현에 대해 설명합니다.
클라이언트 측 구현
파일을 전송하는 클라이언트 측 코드를 자세히 설명합니다. 여기서는 Python을 사용하여 서버에 파일을 전송하는 클라이언트를 구축하는 방법을 설명합니다.
클라이언트 소켓 설정
먼저 클라이언트 소켓을 생성하고 서버에 연결합니다.
import socket
# 소켓 생성
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 서버 주소와 포트 지정
server_address = ('localhost', 8080)
client_socket.connect(server_address)
print(f'Connected to server at {server_address}')
파일 전송
다음으로 지정한 파일을 서버에 전송합니다. 여기서는 파일을 읽고 서버에 데이터를 전송하는 작업을 수행합니다.
# 전송할 파일 경로
file_path = 'file_to_send.txt'
with open(file_path, 'rb') as file:
while True:
data = file.read(1024)
if not data:
break
client_socket.sendall(data)
print(f'File {file_path} sent to server')
전송 루프 세부 사항
read(1024)
: 1024바이트씩 파일을 읽습니다.- 읽은 데이터가 없으면 (
not data
) 루프를 종료합니다. - 읽은 데이터를 서버에 전송합니다.
연결 닫기
통신이 끝나면 연결을 닫고 리소스를 해제합니다.
# 연결 닫기
client_socket.close()
클라이언트 측 전체 코드
다음은 앞서 설명한 모든 단계를 포함한 클라이언트 측 전체 코드입니다.
import socket
def send_file(file_path, server_address=('localhost', 8080)):
# 소켓 생성
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 서버에 연결
client_socket.connect(server_address)
print(f'Connected to server at {server_address}')
# 파일 전송
with open(file_path, 'rb') as file:
while True:
data = file.read(1024)
if not data:
break
client_socket.sendall(data)
print(f'File {file_path} sent to server')
# 연결 닫기
client_socket.close()
if __name__ == "__main__":
file_path = 'file_to_send.txt'
send_file(file_path)
이 코드를 실행하면 클라이언트 측에서 지정한 파일을 서버에 전송합니다. 이로써 서버와 클라이언트 간에 파일을 전송하는 기본적인 구조가 완성되었습니다. 다음은 파일 전송 구조에 대해 더 자세히 설명합니다.
파일 전송 구조
파일 전송은 클라이언트와 서버 간에 데이터를 송수신하는 과정입니다. 이곳에서는 실제로 파일이 어떻게 전송되는지 그 구조에 대해 자세히 설명합니다.
데이터 분할 및 전송
파일을 전송할 때 큰 파일은 한 번에 전송할 수 없습니다. 따라서 파일은 작은 청크(데이터 블록)로 나누어져 차례로 전송됩니다. 아래는 클라이언트 측 데이터 전송 흐름입니다.
# 파일 전송
with open(file_path, 'rb') as file:
while True:
data = file.read(1024) # 1024바이트씩 읽기
if not data:
break
client_socket.sendall(data) # 읽은 데이터를 전송
세부 흐름
- 파일 열기
- 파일에서 1024바이트씩 읽기
- 읽은 데이터가 없을 때까지 루프
- 루프 내에서 데이터를 서버에 전송
데이터 수신 및 저장
서버 측에서는 클라이언트로부터 전송된 데이터를 수신하고, 수신된 데이터를 파일에 기록하여 파일을 재구성합니다.
# 수신 파일 저장 위치
with open(file_path, 'wb') as file:
while True:
data = connection.recv(1024) # 1024바이트씩 수신
if not data:
break
file.write(data) # 수신된 데이터를 파일에 기록
세부 흐름
- 파일을 열기 (쓰기 모드)
- 클라이언트로부터 1024바이트씩 데이터 수신
- 수신 데이터가 없을 때까지 루프
- 루프 내에서 수신된 데이터를 파일에 기록
파일 전송 흐름도
다음은 파일 전송의 전체 흐름을 보여주는 도식입니다.
클라이언트 서버
| |
|-- 소켓 생성 -----------------> |
| |
|-- 서버에 연결 ---------------> |
| |
|-- 파일 읽기 시작 -----------> |
| |
|<--- 연결 수락 ---------------- |
| |
|<--- 데이터 수신 --------------- |
|-- 데이터 전송 (청크 단위) ---> |
| |
|-- 파일 전송 완료 -------------> |
| |
|-- 연결 닫기 -----------------> |
| |
신뢰성과 데이터 무결성
TCP 프로토콜을 사용함으로써 데이터의 순서와 무결성이 보장됩니다. TCP는 신뢰성이 높은 통신 프로토콜로, 전송된 데이터가 정확히 수신되도록 패킷 순서 및 오류를 검사하고 필요에 따라 재전송을 수행합니다.
이로 인해 클라이언트에서 전송된 파일이 서버 측에서 정확히 재구성되는 것이 보장됩니다. 다음은 파일 전송 중 발생할 수 있는 오류와 그 처리 방법에 대해 설명합니다.
에러 처리
파일 전송 중에는 다양한 오류가 발생할 수 있습니다. 여기서는 대표적인 오류와 그 처리 방법에 대해 설명합니다.
연결 오류
서버가 다운되었거나 네트워크가 불안정하거나 지정한 포트가 이미 사용 중인 경우 등 여러 이유로 연결이 실패할 수 있습니다. 이러한 경우의 처리 방법은 다음과 같습니다.
import socket
try:
# 소켓 생성 및 서버 연결
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 8080)
client_socket.connect(server_address)
except socket.error as e:
print(f'Connection error: {e}')
데이터 송신 오류
데이터 송신 중 오류가 발생하면 재시도하거나 송신을 중단해야 할 수 있습니다. 송신 오류의 예로는 네트워크 일시적인 끊김이 있습니다.
try:
with open(file_path, 'rb') as file:
while True:
data = file.read(1024)
if not data:
break
client_socket.sendall(data)
except socket.error as e:
print(f'Sending error: {e}')
client_socket.close()
데이터 수신 오류
데이터 수신 중에도 오류가 발생할 수 있습니다. 이 경우 적절한 에러 처리가 필요합니다.
try:
with open(file_path, 'wb') as file:
while True:
data = connection.recv(1024)
if not data:
break
file.write(data)
except socket.error as e:
print(f'Receiving error: {e}')
connection.close()
타임아웃 오류
네트워크 통신에서 타임아웃이 설정되어 있을 수 있으며, 지정된 시간 내에 데이터 송수신이 완료되지 않으면 타임아웃 오류가 발생합니다. 타임아웃을 설정하여 장시간 대기하지 않고 다음 작업으로 진행할 수 있습니다.
# 소켓 타임아웃 설정
client_socket.settimeout(5.0) # 5초 타임아웃 설정
try:
client_socket.connect(server_address)
except socket.timeout:
print('Connection timed out')
에러 로그 기록
오류가 발생했을 때 에러 메시지를 로그로 기록하는 것이 중요합니다. 이를 통해 나중에 문제의 원인을 쉽게 파악할 수 있습니다.
import logging
# 로그 설정
logging.basicConfig(filename='file_transfer.log', level=logging.ERROR)
try:
client_socket.connect(server_address)
except socket.error as e:
logging.error(f'Connection error: {e}')
print(f'Connection error: {e}')
정리
에러 처리를 적절하게 수행함으로써 파일 전송 프로세스의 신뢰성과 견고성을 높일 수 있습니다. 특히 네트워크 통신에서는 예기치 않은 오류가 자주 발생하므로, 오류 처리 구현이 매우 중요합니다. 다음은 응용 예제로 여러 파일을 전송하는 방법에 대한 구체적인 예시를 소개합니다.
응용 예제: 여러 파일 전송
한 번에 여러 파일을 전송하는 방법에 대해 설명합니다. 여기서는 여러 파일을 송수신하기 위한 서버와 클라이언트 구현 예시를 소개합니다.
여러 파일 전송 (클라이언트 측)
여러 파일을 전송하기 위해 파일 리스트를 작성하고 각 파일을 차례대로 전송합니다.
import socket
import os
def send_files(file_paths, server_address=('localhost', 8080)):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(server_address)
print(f'Connected to server at {server_address}')
for file_path in file_paths:
file_name = os.path.basename(file_path)
client_socket.sendall(file_name.encode() + b'\n') # 파일명 전송
with open(file_path, 'rb') as file:
while True:
data = file.read(1024)
if not data:
break
client_socket.sendall(data)
client_socket.sendall(b'EOF\n') # 파일 종료 마커 전송
print(f'File {file_path} sent to server')
client_socket.close()
if __name__ == "__main__":
files_to_send = ['file1.txt', 'file2.txt']
send_files(files_to_send)
중요한 포인트
- 파일명을 먼저 전송하여 서버가 수신할 파일을 식별할 수 있도록 합니다.
- 각 파일 전송이 끝난 후
EOF
마커를 보내어 파일 종료를 알립니다.
여러 파일 수신 (서버 측)
서버 측에서는 클라이언트로부터 전송된 파일명과 데이터를 수신하고, 적절한 파일에 저장합니다.
import socket
def start_server(server_address=('localhost', 8080)):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(server_address)
server_socket.listen(1)
print(f'Server listening on {server_address}')
connection, client_address = server_socket.accept()
print(f'Connection from {client_address}')
while True:
# 파일명 수신
file_name = connection.recv(1024).strip().decode()
if not file_name:
break
print(f'Receiving file: {file_name}')
with open(file_name, 'wb') as file:
while True:
data = connection.recv(1024)
if data.endswith(b'EOF\n'):
file.write(data[:-4]) # 'EOF' 제외하고 기록
break
file.write(data)
print(f'File {file_name} received')
connection.close()
server_socket.close()
if __name__ == "__main__":
start_server()
중요한 포인트
- 파일명을 수신하여 새로운 파일로 엽니다.
EOF
마커가 나타날 때까지 데이터를 수신하여 파일에 기록합니다.EOF
마커를 찾으면 해당 파일 수신을 종료합니다.- 서버 측에서는 인증서와 비밀 키를 로드하여 소켓을 감쌉니다.
- 클라이언트 측에서는 서버 인증서를 검증하고 소켓을 감쌉니다.
- 클라이언트는 연결 시 인증 정보를 전송합니다.
- 서버는 수신한 인증 정보를 검증하여 맞으면 연결을 유지하고, 틀리면 연결을 끊습니다.
- 파일의 해시 값을 계산하여 송신 측과 수신 측에서 일치하는지 확인합니다.
- 해시 값이 일치하지 않으면 파일이 변조된 것일 수 있습니다.
- 서버는 특정 포트에서 대기하고, 클라이언트의 연결을 수락합니다.
- 클라이언트는 서버에 연결하여 지정된 텍스트 파일을 전송합니다.
- 서버는 수신한 파일을 저장합니다.
- 서버 측은 파일을 수신하여
received_file.txt
라는 이름으로 저장합니다. - 클라이언트 측은
file_to_send.txt
라는 파일을 전송합니다. - 클라이언트는 여러 파일명을 서버에 전송합니다.
- 서버는 파일명을 수신하여 각 파일을 저장합니다.
- 각 파일 전송이 끝날 때
EOF
마커를 사용하여 파일의 종료를 표시합니다. - 파일명 전송과 수신에 주의하며
EOF
마커를 올바르게 처리합니다. - SSL/TLS를 사용하여 안전한 연결을 확립합니다.
- 클라이언트는 암호화된 데이터를 전송합니다.
- 서버는 암호화된 데이터를 수신하여 복호화하여 저장합니다.
ssl
모듈을 사용하여 암호화된 소켓을 생성합니다.- 서버 인증서와 비밀 키를 사용하여 안전한 통신을 구현합니다.
- 클라이언트는 파일의 SHA-256 해시 값을 계산하여 파일과 함께 전송합니다.
- 서버는 수신한 파일의 해시 값을 재계산하여 클라이언트에서 전송된 해시 값과 비교합니다.
- 해시 값이 일치하지 않으면 에러 메시지를 표시합니다.
hashlib
모듈을 사용하여 해시 값을 계산합니다.- 해시 값 송수신을 정확하게 처리합니다.
정리
여러 파일을 전송할 때는 파일명과 데이터의 구분을 명확히 하기 위해 특별한 처리가 필요합니다. 위 방법을 통해 여러 파일을 효율적으로 전송할 수 있습니다. 다음은 파일 전송 시의 보안 대책에 대해 설명합니다.
보안 대책
파일 전송 시의 보안 대책은 매우 중요합니다. 불법 접근이나 데이터 유출을 방지하기 위해 여러 가지 기본적인 보안 대책을 강구해야 합니다. 여기에서는 대표적인 보안 대책을 설명합니다.
데이터 암호화
데이터 송수신 시 암호화를 하면 제3자의 도청을 방지할 수 있습니다. Python에서는 SSL/TLS를 사용하여 통신을 암호화할 수 있습니다. 아래는 SSL을 사용한 예를 보여줍니다.
import socket
import ssl
# 서버 측 설정
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8080))
server_socket.listen(1)
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
secure_socket = context.wrap_socket(server_socket, server_side=True)
connection, client_address = secure_socket.accept()
# 클라이언트 측 설정
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context.load_verify_locations('server.crt')
secure_socket = context.wrap_socket(client_socket, server_hostname='localhost')
secure_socket.connect(('localhost', 8080))
중요한 포인트
인증 및 접근 제어
인증을 통해 신뢰할 수 있는 클라이언트만 연결할 수 있도록 합니다. 기본적인 사용자명과 비밀번호 인증 예제를 보여줍니다.
# 클라이언트 측에서 인증 정보 전송
username = 'user'
password = 'pass'
secure_socket.sendall(f'{username}:{password}'.encode())
# 서버 측에서 인증 정보 검증
data = connection.recv(1024).decode()
received_username, received_password = data.split(':')
if received_username == 'user' and received_password == 'pass':
print('Authentication successful')
else:
print('Authentication failed')
connection.close()
중요한 포인트
데이터 무결성 확보
데이터가 변조되지 않았는지 확인하기 위해 해시 값을 사용합니다. 전송할 파일의 해시 값을 계산하고, 수신 측에서 이를 다시 계산하여 비교합니다.
import hashlib
# 파일의 해시 값 계산
def calculate_hash(file_path):
hasher = hashlib.sha256()
with open(file_path, 'rb') as file:
while chunk := file.read(1024):
hasher.update(chunk)
return hasher.hexdigest()
# 클라이언트 측에서 해시 값 전송
file_hash = calculate_hash('file_to_send.txt')
secure_socket.sendall(file_hash.encode())
# 서버 측에서 해시 값 비교
received_file_hash = connection.recv(1024).decode()
if received_file_hash == calculate_hash('received_file.txt'):
print('File integrity verified')
else:
print('File integrity compromised')
중요한 포인트
정리
파일 전송 시의 보안 대책으로 데이터 암호화, 인증 및 접근 제어, 데이터 무결성 확보가 중요합니다. 이러한 대책을 구현함으로써 안전하고 신뢰할 수 있는 파일 전송을 실현할 수 있습니다. 다음은 독자가 직접 시도해볼 수 있는 연습 문제를 제공합니다.
연습 문제
여기서는 지금까지의 내용을 실습할 수 있는 연습 문제를 제공합니다. 이 문제들을 통해 소켓 프로그래밍과 파일 전송에 대한 이해를 깊게 할 수 있습니다.
연습 문제 1: 기본적인 파일 전송
서버와 클라이언트를 생성하고 텍스트 파일을 전송하는 프로그램을 구현하세요. 아래 요구 사항을 충족해야 합니다:
힌트
연습 문제 2: 여러 파일 전송
여러 파일을 한 번에 전송하는 프로그램을 구현하세요. 아래 요구 사항을 충족해야 합니다:
힌트
연습 문제 3: 데이터 암호화
데이터를 암호화하여 전송하는 프로그램을 구현하세요. 아래 요구 사항을 충족해야 합니다:
힌트
연습 문제 4: 파일 무결성 확인
파일의 해시 값을 계산하여 데이터의 무결성을 확인하는 프로그램을 구현하세요. 아래 요구 사항을 충족해야 합니다:
힌트
정리
이 연습 문제를 통해 소켓 프로그래밍과 파일 전송의 기초부터 응용까지 실습할 수 있습니다. 각 문제에 도전하여 네트워크 통신의 이해를 깊게 하고 안전하고 효율적인 파일 전송 기술을 습득할 수 있습니다. 마지막으로, 이번 내용을 정리합니다.
정리
본 기사에서는 Python을 사용한 소켓 프로그래밍을 통한 파일 전송 방법에 대해 설명했습니다. 먼저 소켓의 기본 개념과 설정 방법을 배우고, 이어서 서버 측과 클라이언트 측 구현을 진행했습니다. 또한 파일 전송 구조와 에러 처리, 보안 대책에 대해서도 자세히 설명했습니다. 나아가 여러 파일 전송 방법과 실습 문제를 통해 이해를 깊게 했습니다.
소켓 프로그래밍은 네트워크 통신의 기반이 되는 중요한 기술입니다. 이를 마스터하면 다양한 네트워크 애플리케이션 개발이 가능해집니다. 이번 내용을 바탕으로 더욱 복잡한 시스템이나 프로젝트에 도전해 보세요. 소켓을 사용한 파일 전송은 그 첫걸음입니다.