Network

[네트워크] 파이썬 소켓 프로그래밍

아윤_ 2023. 7. 24. 18:20

소켓 프로그래밍

 

 

켓(Socket)이란 무엇인가

 

하나의 프로세스로부터 다른 프로세스로 보내는 메시지는 네트워크를 통해 움직이며, 프로세스는 소켓(socket)을 통해 네트워크로 메시지를 보내고 받는다. 소켓은 호스트의 애플리케이션 계층과 트랜스포트 계층 간의 인터페이스로, 애플리케이션과 네트워크 사이의 API(Application Programming Interface)라고도 한다. 즉, 소켓이란, TCP나 UDP와 같은 트랜스포트 계층을 이용하는 API를 말한다.

 

소켓은 네트워크에서 프로세스로 데이터를 전달하며, 프로세스로부터 네트워크로 데이터를 전달하는 출입구 역할을 한다. 

 

 

 

클라이언트(Client)와 서버(Server) 

 

일반적인 네트워크 애플리케이션은 2개의 서로 다른 종단 시스템에 존재하는 클라이언트 프로그램과 서버 프로그램으로 구성된다.

 

두 프로세스 간의 통신 세션에서 통신을 초기화(다른 프로세스와 세션을 시작하려고 접속을 초기화)하는 프로세스클라이언트(Client)라 하고, 세션을 시작하기 위해 접속을 기다리는 프로세스서버(Server)라고 한다.

 

클라이언트 프로그램과 서버 프로그램을 수행하면, 클라이언트와 서버 프로세스가 생성되고, 두 프로세스가 소켓으로부터 읽고(read), 소켓에 쓰기(write)를 통해서 서로 통신한다.

 

 

 

소켓 프로그래밍 종류

 

클라이언트와 서버 간의 소켓 프로그래밍은 TCP를 이용한 프로그래밍과 UDP를 이용한 프로그래밍 두 가지가 있다.

 

TCP

  • 연결지향형 서비스
  • 신뢰적인 바이트 스트림 채널을 제공
  • 해당 채널을 통해 데이터가 두 종단 시스템 사이를 흐름

 

UDP

  • 비연결지향형 서비스
  • 전송에 대한 보장이 없어 비신뢰적
  • 한 종단 시스템에서 다른 곳으로 데이터를 독립적인 패킷으로 만들어 전송

 

 

 

UDP를 이용한 소켓 프로그래밍

 

먼저, UDP를 이용한 클라이언트-서버 간의 소켓 프로그래밍을 살펴보고, TCP를 이용한 소켓 프로그래밍에 대해 살펴보고자 한다. 이제 UDP 소켓을 이용하는 두 통신 프로세스들 간의 상호작용을 더 자세히 살펴보도록 하자.

 

UDP를 사용할 때에는 먼저 패킷에 목적지 주소를 붙여 넣어야 한다. 이 패킷이 송신자의 소켓을 통과한 후 인터넷은 이 목적지 주소를 이용하여 그 패킷을 인터넷을 통해 수신 프로세스에 있는 소켓으로 라우트 할 것이다. 패킷이 수신 소켓에 도착하면 수신 프로세스는 소켓을 통해 그 패킷을 추출하고, 다음에 패킷의 콘텐츠를 조사하고 적절한 동작을 취한다.

 

그렇다면 패킷에 붙여지는 목적지 주소에는 무엇이 들어갈까?

바로 목적지 호스트의 IP 주소가 목적지 주소의 일부가 된다. 패킷에 목적지 주소를 포함함으로써 인터넷의 라우터는 목적지 호스트로 인터넷을 통해 패킷을 라우트 할 수 있다. 그러나, 호스트는 하나 혹은 그 이상의 소켓을 갖는 많은 네트워크 애플리케이션 프로세스를 수행하고 있을 수 있기 때문에 목적지 호스트 내의 특정한 소켓을 식별할 필요가 있다.

 

소켓이 생성될 때 포트 번호(port number)라고 하는 식별자가 소켓에 할당되며, 패킷의 목적지 주소는 소켓의 포트 번호도 포함한다. 

 

포트(port) : 운영체제 통신의 종단점으로 네트워크 서비스나 특정 프로세스를 식별하는 논리 단위

 

 

요약하면, 송신 프로세스는 목적지 호스트의 IP 주소와 목적지 소켓의 포트 번호로 구성된 목적지 주소를 패킷에 붙인다. 더 나아가, 송신자의 소스(source) 주소-소스 호스트의 IP 주소와 소스 소켓의 포트 번호도 패킷에 붙여진다.

 

 

 

 

UDP(비연결형) 소켓 사용 절차

 

TCP는 연결형으로, 일대일 통신에만 사용되지만 UDP는 비연결형으로 TCP와 달리 일대일 통신에만 사용되지 않는다.

 

비연결형 소켓

  • Connect() 시스템 콜을 사용할 필요가 없다
  • 소켓 개설 후 바로 상대방과 데이터를 송수신한다.

 

다음은 UDP 소켓 프로그래밍 절차를 나타낸 그림이다.

 

UDP 소켓 프로그래밍 절차

 

 

서버(Server)

  • socket() : 소켓을 생성한다.
  • bind() : ip 주소와 port 번호를 설정한다.
  • recvfrom() : 클라이언트로부터 전송된 데이터를 수신한다. 
  • sendto() :  클라이언트로 데이터를 전송한다.

 

클라이언트(Client)

  • socket() : 소켓을 생성한다.
  • sendto() : 서버로 데이터를 전송한다.
  • recvfrom() : 서버로부터 전송된 데이터를 수신한다.
  • close() : 소켓을 닫는다.

 

 

 

서버(Server) 소스 코드

 

다음은 UDP를 사용한 서버의 파이썬 소스 코드이다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000
cnt = 0

먼저, 소켓 프로그래밍을 사용하기 위해 socket 모듈을 import 한다. 서버의 ip 주소를 127.0.0.1(localhost) 주소로 설정하였으며, 포트 번호를 12000으로 설정해 주었다.

 

클라이언트로부터 수신한 메시지 번호를 출력하기 위해 cnt를 0으로 초기화해 주었다.

serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serverSocket.bind((serverIP, serverPort))

print("The server is ready to receive")

socket() 함수를 통해 serverSocket이라는 서버 소켓을 생성한다.

  • AF_INET : 네트워크가 IPv4를 사용하고 있음을 의미한다.
  • SOCK_DREAM : 소켓 타입을 나타내는 것으로 소켓이 UDP 소켓임을 의미한다.

 

bind() 함수를 통해 지정한 서버의 ip 주소와 포트 번호를 서버의 소켓에 할당한다. 클라이언트에서 서버 ip 주소의 12000 포트로 패킷을 보내면 해당 소켓으로 패킷이 전달된다.

while True:
    message, clientAddress = serverSocket.recvfrom(1024)
    message = message.decode()
    
    cnt += 1
    print('[',cnt,']' , "Received message:", message)
    serverSocket.sendto("Message received complete".encode(), clientAddress)

recvfrom() 함수를 통해 서버는 패킷이 도착하기를 기다리다가 패킷이 서버의 소켓에 도착하면, 패킷의 데이터는 message 변수에 할당되고, 패킷의 소스 주소는 변수 clientAddress에 할당된다. clientAddress는 클라이언트의 ip 주소와 포트 번호를 포함한다.

 

서버는 클라이언트로부터 받은 메시지를 출력하고, sendto() 함수를 통해 클라이언트로 메시지를 정상적으로 수신했다는 응답을 보낸다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000
cnt = 0

serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serverSocket.bind((serverIP, serverPort))

print("The server is ready to receive")

while True:
    message, clientAddress = serverSocket.recvfrom(1024)
    message = message.decode()
    
    cnt += 1
    print('[',cnt,']' , "Received message:", message)
    serverSocket.sendto("Message received complete".encode(), clientAddress)

 

 

 

클라이언트(Client) 소스 코드

 

다음은 UDP를 사용한 클라이언트의 파이썬 소스 코드이다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000

소켓 프로그래밍을 사용하기 위해 socket 모듈을 import 한다. 데이터를 전송하고자 하는 서버의 ip 주소와 포트 번호를 지정한다.

while True:

    clientSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    sendMessage = input("input message: ")
    sendMessage = sendMessage.encode()

    clientSocket.sendto(sendMessage, (serverIP, serverPort))

    recvMessage, serverAddress = clientSocket.recvfrom(1024)
    print(recvMessage.decode())

    clientSocket.close()

socket() 함수를 통해 clientSocket이라는 클라이언트 소켓을 생성한다. 인자는 서버와 동일하다.

 

서버로 전송할 메시지를 입력한 다음, sendto() 함수를 통해 서버로 입력한 메시지를 전송한다.

 

서버로 메시지가 정상적으로 전송되었다면, recvfrom() 함수를 통해 서버로부터 데이터 수신을 기다린다. 수신받은 메시지를 바이트에서 문자열로 변환한 다음, 이를 출력한다.

 

close() 함수를 통해 소켓을 닫는다. 이후, 프로세스가 종료된다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000


while True:

    clientSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    sendMessage = input("input message: ")
    sendMessage = sendMessage.encode()

    clientSocket.sendto(sendMessage, (serverIP, serverPort))

    recvMessage, serverAddress = clientSocket.recvfrom(1024)
    print(recvMessage.decode())

    clientSocket.close()

 

 

 

TCP를 이용한 소켓 프로그래밍

 

UDP와 달리 TCP는 연결지향형 프로토콜로, 클라이언트와 서버가 서로에게 데이터를 보내기 전에 TCP 연결을 설정할 필요가 있다. TCP 연결이 설정된 후, 한쪽에서 다른 쪽으로 데이터를 보내려면 소켓을 통해 데이터를 TCP 연결로 보내면 된다. 이것이 UDP와 다른 부분이며, UDP에서는 서버가 패킷을 소켓에 제공하기 전에 패킷에 목적지 주소를 붙여야 한다.

 

 

 

TCP(연결형) 소켓 사용 절차

 

다음은 TCP 소켓 프로그래밍 절차를 나타낸 그림이다.

 

TCP 소켓 프로그래밍 절차

 

 

서버(Server)

  • socket() : 소켓을 생성한다.
  • bind() : ip 주소와 port 번호를 설정한다.
  • listen() : 클라이언트의 소켓 연결 요청을 기다린다.
  • accept() : 클라이언트의 소켓 연결 요청을 수락한다.
  • send() / receive() :  데이터를 송수신한다.

 

클라이언트(Client)

  • socket() : 소켓을 생성한다.
  • connect() : 서버 소켓에 연결을 요청한다.
  • send() / receive() :  데이터를 송수신한다.
  • close() :  소켓을 닫는다.

 

 

 

서버(Server) 소스 코드

 

다음은 TCP를 사용한 서버의 파이썬 소스 코드이다.

 

serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

socket() 함수를 통해 serverSocket이라는 서버 소켓을 생성한다.

  • AF_INET : 네트워크가 IPv4를 사용하고 있음을 의미한다.
  • SOCK_STREAM : 소켓 타입을 나타내는 것으로 소켓이 TCP 소켓임을 의미한다.
serverSocket.bind((serverIP, serverPort))

TCP에서는 serverSocket이 대기하는 소켓이 된다.

serverSocket.listen(1)
print("The server is ready to receive")

출입문을 설정한 후, 임의의 클라이언트가 이 문을 두드리기를 기다린다.

해당 코드는 서버가 클라이언트로부터의 TCP 연결 요청을 듣도록 하는 문장으로, 파라미터는 큐 되는 연결의 최대 수를 나타낸다.(적어도 1개)

connectionSocket, addr = serverSocket.accept()

클라이언트가 이 문을 두드리면, 프로그램은 serverSocket을 위한 accept() 메서드를 시작해서 이 클라이언트에게 지정된 connectionSocket이라는 새로운 소켓을 서버에 생성한다.

그 뒤, 클라이언트와 서버는 핸드셰이킹을 완료해서 클라이언트의 clientSocket과 connectionSocket 간에 TCP 연결을 생성한다.

TCP 연결이 설정되었으므로 클라이언트와 서버는 이 연결을 통해 서로에게 바이트를 전송할 수 있다.

connectionSocket.close()

연결 소켓을 닫는다. 하지만, serverSocket이 열려 있기 때문에 다른 클라이언트가 출입문을 두드릴 수 있고, 서버에게 수정할 문장을 보낼 수 있다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000
cnt = 0

serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind((serverIP, serverPort))

serverSocket.listen(1)
print("The server is ready to receive")

while True:
    connectionSocket, addr = serverSocket.accept()
    message = connectionSocket.recv(1024).decode()
    
    cnt += 1
    print('[',cnt,']' , "Received message:", message)

    connectionSocket.send("Message received complete".encode())
    connectionSocket.close()

 

 

 

클라이언트(Client) 소스 코드

 

다음은 TCP를 사용한 클라이언트의 파이썬 소스 코드이다.

 

clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

clientSocket이라는 클라이언트의 소켓을 생성한다. 인자는 서버와 동일하다.

clientSocket.connect((serverIP, serverPort))

클라이언트가 TCP 소켓을 사용하여 서버로 (혹은 그 반대로) 데이터를 보내기 전에 TCP 연결이 먼저 클라이언트와 서버 사이에 설정되어야 한다. 위의 라인을 통해 클라이언트와 서버 간에 TCP 연결을 시작한다.

connect() 메서드의 파라미터는 연결의 서버 쪽 주소이다. 이 라인이 수행된 후에 세 방향 핸드셰이크가 수행되고 클라이언트와 서버 간에 TCP 연결이 설정된다.

sendMessage = input("input message: ")
sendMessage = sendMessage.encode()

clientSocket.send(sendMessage)

입력한 메시지를 클라이언트 소켓을 통해 TCP 연결로 보낸다. UDP 소켓처럼 프로그램이 패킷을 명시적으로 생성하지 않으며, 패킷에 목적지 주소를 붙이지 않는다.

 recvMessage = clientSocket.recv(1024)

서버로부터 온 문자를 문자열에 recvMessage에 모은다. 리턴 키로 끝날 때까지 문자가 계속해서 쌓인다.

 clientSocket.close()

소켓을 닫고 클라이언트와 서버 간의 TCP 연결을 닫는다.

import socket

serverIP = "127.0.0.1"
serverPort = 12000

while True:

    clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    clientSocket.connect((serverIP, serverPort))
    sendMessage = input("input message: ")
    sendMessage = sendMessage.encode()

    clientSocket.send(sendMessage)

    recvMessage = clientSocket.recv(1024)

    print(recvMessage.decode())

    clientSocket.close()