네트워크 응용 설계 소켓 API

곽태욱·2020년 4월 17일
0

강의 노트

목록 보기
17/22
post-custom-banner

네트워크 소켓

소켓은 인터넷 5계층 중 Transport 계층과 Application 계층 사이의 API에 해당한다.

Transport 계층은 데이터를 어느 프로세스에게 전달할지, ...를 정의한다.

UDP

UDP(User Datagram Protocol)는 보낼 데이터를 목적지 포트에 있는 소켓에 전송 할 뿐이다. 해당 소켓으로 데이터가 모두 잘 도착했는지, 순서대로 도착했는지는 확인하지 않는다. 데이터 전송 중 데이터 손실이 일어날 수도 있다는 뜻이다.

TCP와 다르게 UDP는 사전 연결을 하지 않기 때문에 데이터를 보낼 때마다 목적지의 주소(IP 주소, 포트 번호)를 명시해야 한다. 그리고 데이터를 수신할 때도 항상 데이터와 데이터 발신지 주소를 같이 받는다.

서버

서버는 항상 켜져있는 상태로 클라이언트의 연결을 기다리는, 네트워크에 연결된 기기다(host = end system = network edge). 먼저 클라이언트가 서버로 데이터를 전송하는 형태이기 때문에 서버는 일반적으로 고정 IP 주소를 사용한다.

1. 소켓 생성

# UDPServer.py

from socket import *

# IPv4를 따르는 UDP 소켓을 내 컴퓨터의 11758번 포트에 할당한다.
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverPort = 11758
serverSocket.bind(('', serverPort))

AF_INET은 인터넷 규약 중 IPv4를 따른다는 것을 의미하고, SOCK_DGRAM은 UDP를 의미한다. UDP는 데이터 전송 시 datagram 단위로 전송하기 때문에 저런 명칭이 붙었다.

bind() 함수는 소켓 프로세스를 내 컴퓨터의 11758번 포트에 생성한다는 것을 의미한다. 이 함수의 파라미터는 IP 주소와 포트 번호의 튜플인데 여기서 ''0.0.0.0을 의미하고 포트 번호는 0~65535번 중 임의로 설정할 수 있다. 이미 실행 중인 프로세스의 포트와 겹칠 수 있으므로 포트 번호는 4자리 수 이상으로 높게 설정하는 것이 좋다.

네트워크로부터 데이터가 수신되면 운영체제는 이 데이터를 해당하는 프로세스에게 전달해줘야 하는데 이때 전달의 편의를 위해 포트라는 개념이 생겼다. 데이터에 목적지 프로세스의 포트 번호를 명시하고 프로세스 별로 포트 번호를 부여하면, 운영체제는 네트워크로부터 데이터를 받아 해당 포트로 메시지를 전달하는 일만 하면 된다. 이렇게 프로세스 간 데이터 송수신이 간단해지기 때문에 포트라는 개념을 정의한 것이다.

한 포트에 여러 소켓이 있으면 그 포트로 데이터가 수신될 때 어떤 소켓에서 처리할지 애매해지기 때문에 기본적으로 한 포트엔 하나의 소켓만 생성할 수 있다. 그래서 프로세스가 운영체제에게 bind() 함수로 특정 포트를 사용하겠다는 요청을 하는 것이다.

2. 메시지 수신 및 전송

# UDPServer.py

try:
    while True:
        # 2048 bytes의 버퍼에 메시지와 메시지 발신지 주소를 수신한다.
        req, clientAddress = serverSocket.recvfrom(2048)
        # 수신한 문장을 모두 대문자로 바꿈
        res = req.decode().upper()
        # 메시지를 보낸 곳의 IP주소와 포트로 응답을 보낸다.
        serverSocket.sendto(res.encode(), clientAddress)
        
except KeyboardInterrupt:
    serverSocket.close()

소켓이 생성되면 네트워크로부터 11758번 포트로 데이터가 전송될 때까지 recvfrom 줄에서 대기한다. recvfrom()의 파라미터는 버퍼 크기로서 한번에 수신할 수 있는 데이터 크기를 의미한다. 버퍼 크기보다 큰 데이터가 수신되면 나머지 데이터는 어떻게 될까...?

그리고 클라이언트의 sendto() 함수로 서버의 11758번 포트로 데이터가 전송되면 데이터와 데이터 발신지 정보를 반환는다. 그리고 데이터를 전부 대문자로 바꾼 후 클라이언트와 동일하게 sendto() 함수를 통해 데이터 발신지로 응답을 보낸다.

while 문을 사용하면 클라이언트로부터 데이터를 여러 번 수신할 수 있고, 프로그램 실행 도중 Ctrl+C 등이 입력돼서 KeyboardInterrupt가 발생하면 서버 소켓을 닫고 종료한다.

클라이언트

서버와 달리 클라이언트는 항상 켜져있지 않으며, 여러 기기를 통해 서버에 접속할 수 있으므로 유동적인 IP 주소를 가진다.

1. 소켓 생성

# UDPClient.py

from socket import *

# IPv4를 따르는 UDP 소켓을 내 컴퓨터의 21758번 포트에 할당한다.
clientSocket = socket(AF_INET, SOCK_DGRAM)
clientPort = 21758
clientSocket.bind(('', clientPort))

# 응답을 기다리는 시간을 최대 10초로 설정한다.
clientSocket.settimeout(10)

# 클라이언트 소켓의 포트 번호를 출력한다. # 21758
print("The client is running on port", clientSocket.getsockname()[1])

연결 지연 시간(timeout)을 설정한 것 외엔 UDP 서버에서 소켓을 생성하는 과정과 동일하다. 연결 지연 시간은 서버로 데이터를 보낸 후 서버 응답을 기다리는 시간을 의미한다. 그리고 getsockname() 함수를 통해 소켓이 생성된 기기의 IP 주소와 포트 번호를 알 수 있다.

2. 메시지 전송 및 수신

# UDPClient.py

# 해당 주소(localhost:11758)로 접속한다.
serverName = 'localhost'
serverPort = 11758

while True:
    try:
        # 목적지로 전송할 데이터를 생성한다.
        request = input('Input lowercase sentence: ')
        
        # 서버 응답 시간(Round Trip Time) 측정
        begin = time() * 1000
        # 해당 목적지로 데이터를 전송한다.
        clientSocket.sendto(request.encode(), (serverName, serverPort))
        # 2048 bytes의 버퍼에 21758번 포트로 전송된 데이터와 발신지 주소를 수신한다.
        response, serverAddress = clientSocket.recvfrom(2048)
        # 서버 응답 시간(Round Trip Time) 측정
        end = time() * 1000

        # 서버로부터 온 응답 출력
        print('Reply from server:', response.decode())
        # 서버 응답 시간(Round Trip Time) 출력
        print('Response time:', end - begin, 'ms')

    # 해당 주소에 소켓이 존재하지 않거나 종료되어 있을 때
    except ConnectionResetError:
        print('Connection is reset by remote host')
    # 서버로부터 응답이 10초 안에 오지 않을 때
    except timeout:
        print('Connection timed out')
    # Ctrl+C가 입력되면
    except KeyboardInterrupt:
        break
    # 디버깅을 위한 기타 예외처리
    except Exception as e:
        print(e)

# 할 일이 다 끝나면 소켓을 닫아준다.
clientSocket.close()

serverName엔 목적지의 IP 주소를, serverPort엔 목적지 프로세스의 포트 번호를 적어주고 sendto() 함수를 통해 데이터를 목적지로 전송한다. 그리고 recvfrom() 함수를 통해 네트워크에서 21758번 포트로 들어오는 데이터를 수신할 수 있다. 이 함수의 파라미터로 데이터를 수신하는 버퍼의 크기를 설정할 수 있다. 즉, 파라미터의 숫자는 한번에 수신할 수 있는 데이터의 최대 크기를 의미한다.

앞서 timeout을 10초로 설정했기 때문에 서버 응답을 최대 10초까지 기다리고, 10초 안에 서버로부터 응답이 안 오면 timeout 예외가 발생한다.

while문을 사용하면 서버로 데이터를 여러 번 전송할 수 있고, 프로그램 실행 도중 Ctrl+C 등이 입력돼서 KeyboardInterrupt가 발생하면 클라이언트 소켓을 닫고 종료한다.

TCP

TCP(Transmission Control Protocol)

서버

1. 소켓 생성

# TCPServer.py

from socket import *

# IPv4를 따르는 TCP 소켓을 10825번 포트에 생성한다.
serverSocket = socket(AF_INET, SOCK_STREAM)
serverPort = 10825
serverSocket.bind(('', serverPort))

# 생성된 소켓의 포트 번호를 출력한다.
print("The server socket was created on port", serverSocket.getsockname()[1])

SOCK_STREAM은 TCP를 의미한다. TCP는 데이터 전송 시 byte stream 형태로 전송하기 때문에 저런 명칭이 붙었다. 그 외 과정은 UDP랑 비슷하다.

2. 사전 연결

# TCPServer.py

try:
    while True:
        serverSocket.listen(1)
        print("The server is listening to port", serverPort)

        (connectionSocket, clientAddress) = serverSocket.accept()
        print('Connection requested from', clientAddress)

        # (데이터 수신 및 전송)...
            
        connectionSocket.close()
        
# Ctrl+C가 입력되면
except KeyboardInterrupt:
    pass
# 클라이언트와의 연결이 끊어졌을 때
except ConnectionResetError:
    print('ConnectionResetError: Connection is reset by remote host')
    
connectionSocket.close()
serverSocket.close()

listen() 함수는 서버의 10825번 포트에 생성된 TCP 소켓으로 들어오는 TCP 연결 요청을 기다리는 함수이다. listen(1)은 클라이언트 연결을 동시에 최대 1개까지 받겠다는 의미이다.

accept() 함수는 클라이언트의 connect() 요청을 받아들인다. 만약 클라이언트에서 서버로 connect() 요청이 들어오면 서버에 connectionSocket이 새로 생성되고 데이터 송수신 단계로 넘어간다. serverSocket은 단순히 클라이언트로부터 connect() 요청이 들어오는지만 확인하고, 실제 데이터 송수신은 connectionSocket에서 이뤄진다.

3. 데이터 수신 및 전송

# TCPServer.py

while True:
    req = connectionSocket.recv(1024).decode() 
    if(req == ''):
        break
               
    # convert text to UPPER-case letters
    modifiedMessage = req.upper()
    connectionSocket.send(modifiedMessage.encode())

UDP와는 다르게 TCP 서버는 사전에 클라이언트와 연결했기 때문에 recv() 함수로 데이터를 받거나 send() 함수로 데이터를 보낼 때 클라이언트의 주소가 필요하지 않다.

TCP 서버는 사전 연결된 클라이언트와 connectionSocket을 통해 데이터 송수신을 계속할 수 있다. 그래서 while문을 사용해 클라이언트로 데이터를 여러 번 송수신할 수 있고, 클라이언트로부터 ''의 응답이 오면 클라이언트와의 TCP 연결이 끊겼다는 의미이므로(?) 반복문을 종료한다.

클라이언트

1. 소켓 생성

# TCPClient.py

from socket import *
from sys import exit

# IPv4를 따르는 TCP 소켓을 20825번 포트에 생성한다.
clientPort = 20825
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.bind(('', clientPort))

# timeout을 10초로 설정한다.
clientSocket.settimeout(10)

# 클라이언트 소켓의 포트 번호를 출력한다. # 20825
print("The client is running on port", clientSocket.getsockname()[1])

UDP 클라이언트에서도 그랬듯이 소켓에 연결 지연 시간(timeout)을 설정한 것 외엔 TCP 서버에서 소켓을 생성하는 과정과 동일하다.

2. 사전 연결

# TCPClient.py

# 서버 주소
serverName = 'localhost'
serverPort = 10825

counter = 0
while True:
    # 해당 주소의 서버에 사전 연결을 요청한다.
    try:
        clientSocket.connect((serverName, serverPort))
        break
    # gaierror: 유효하지 않은 서버 주소
    except gaierror:
        print('gaierror: Invalid address of server')
        clientSocket.close()
        exit()
    # ConnectionRefusedError: 서버가 연결을 거부했거나 서버가 종료되어 있음
    except ConnectionRefusedError:
        print('ConnectionRefusedError: Connection is refused by remote host')
        counter += 1
    # ConnectionAbortedError: 위와 동일?
    except ConnectionAbortedError:
        print('ConnectionAbortedError: Connection is aborted by remote host')
        sleep(1)
        counter += 1
    # TimeoutError: ?
    except TimeoutError:
        print('TimeoutError: No response from server')
        clientSocket.close()
        exit()
    # timeout: 응답 대기시간이 timeout(10초)을 초과함
    except timeout:
        print('timeout: No response from server')
        clientSocket.close()
        exit()
    # 서버에 10번 넘게 사전 연결을 요청했으면 소켓을 닫고 프로그램을 종료한다.
    if(counter > 10):
        clientSocket.close()
        exit()

클라이언트는 서버에 connect() 함수를 통해 TCP 사전 연결을 요청할 수 있다. except 절은 그에 따른 예외를 처리하는 부분이다.

3. 데이터 전송 및 수신

# TCPClient.py

while True:
    try:
        # 서버로 전송할 데이터 생성
        message = input('Input lowercase sentence: ')
        
        # 서버 응답 시간(Round Trip Time) 측정
        begin = time() * 1000
        # 서버로 데이터 전송 및 수신
        clientSocket.send(request.encode())  # UDP와 다른 점
        response = clientSocket.recv(2048)  # UDP와 다른 점
        # 서버 응답 시간(Round Trip Time) 측정
        end = time() * 1000

        # 서버로부터 온 응답 출력
        print('Reply from server:', response.decode())
        # 서버 응답 시간(Round Trip Time) 출력
        print('Response time:', end - begin, 'ms')
        
    # 서버와의 TCP 연결이 끊어졌을 때
    except (ConnectionResetError, BrokenPipeError):
        print('Connection is reset by remote host')
        break
    # 서버로부터의 응답이 timeout(10초)을 초과했을 때
    except timeout:
        print('Connection timed out')
    # 'Ctrl-C’ 등을 눌렀을 때
    except KeyboardInterrupt:
        break
    # 디버깅을 위한 기타 예외처리
    except Exception as e:
        print(e)

    print()

# 할 일이 모두 끝나면 소켓을 닫아준다.
clientSocket.close()

데이터를 전송하고 수신하는 부분을 제외하고 UDP 클라이언트와 동일하다.

profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.
post-custom-banner

0개의 댓글