[Python] Socket 통신


Study/Python  2020. 1. 13. 09:00

안녕하세요. 명월입니다.


이 글은 Python에서 Socket를 다루는 방법에 대한 글입니다.

소켓이란 프로그램에서 다른 프로그램과 네트워크 통신을 하는 것을 말합니다. 그 통신은 내부 로컬에서 프로그램 간의 통신이 될 수도 있고 다른 PC간에 통신이 될 수도 있습니다.

Socket은 이 Python만 있는 것이 아니고 네트워크 통신의 규약이기 때문에 모든 프로그램 언어에서 지원을 합니다.

링크 - [Java강좌 - 23] 소켓 통신 (Socket)

링크 - [C# 강좌 - 32] 소켓 통신 - 1


기본적인 소켓의 흐름은 다음과 같습니다.

출처 - http://jkkang.net/unix/netprg/chap2/net2_1.html


크게 서버와 클라이언트가 존재하며 서버는 Listen을 대기하는 주체에 client가 서버에 접속하는 형태입니다.

listen상태의 서버에서 client가 connection을 하면 서버에서는 accept가 발생하고 서로 send와 receive로 데이터를 주고 받습니다.

이때 데이터를 주고 받는 것은 바이너리(byte)형식의 데이터가 됩니다.

# 소켓을 사용하기 위해서는 socket을 import해야 한다.
import socket, threading

# binder함수는 서버에서 accept가 되면 생성되는 socket 인스턴스를 통해 client로 부터 데이터를 받으면 echo형태로 재송신하는 메소드이다.
def binder(client_socket, addr):
  # 커넥션이 되면 접속 주소가 나온다.
  print('Connected by', addr);
  try:
    # 접속 상태에서는 클라이언트로 부터 받을 데이터를 무한 대기한다.
    # 만약 접속이 끊기게 된다면 except가 발생해서 접속이 끊기게 된다.
    while True:
      # socket의 recv함수는 연결된 소켓으로부터 데이터를 받을 대기하는 함수입니다. 최초 4바이트를 대기합니다.
      data = client_socket.recv(4);
      # 최초 4바이트는 전송할 데이터의 크기이다. 그 크기는 little 엔디언으로 byte에서 int형식으로 변환한다.
      length = int.from_bytes(data, "little");
      # 다시 데이터를 수신한다.
      data = client_socket.recv(length);
      # 수신된 데이터를 str형식으로 decode한다.
      msg = data.decode();
      # 수신된 메시지를 콘솔에 출력한다.
      print('Received from', addr, msg);
      
      # 수신된 메시지 앞에 「echo:」 라는 메시지를 붙힌다.
      msg = "echo : " + msg;
      # 바이너리(byte)형식으로 변환한다.
      data = msg.encode();
      # 바이너리의 데이터 사이즈를 구한다.
      length = len(data);
      # 데이터 사이즈를 little 엔디언 형식으로 byte로 변환한 다음 전송한다.
      client_socket.sendall(length.to_bytes(4, byteorder="little"));
      # 데이터를 클라이언트로 전송한다.
      client_socket.sendall(data);
  except:
    # 접속이 끊기면 except가 발생한다.
    print("except : " , addr);
  finally:
    # 접속이 끊기면 socket 리소스를 닫는다.
    client_socket.close();

# 소켓을 만든다.
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
# 소켓 레벨과 데이터 형태를 설정한다.
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1);
# 서버는 복수 ip를 사용하는 pc의 경우는 ip를 지정하고 그렇지 않으면 None이 아닌 ''로 설정한다.
# 포트는 pc내에서 비어있는 포트를 사용한다. cmd에서 netstat -an | find "LISTEN"으로 확인할 수 있다.
server_socket.bind(('', 9999));
# server 설정이 완료되면 listen를 시작한다.
server_socket.listen();

try:
  # 서버는 여러 클라이언트를 상대하기 때문에 무한 루프를 사용한다.
  while True:
    # client로 접속이 발생하면 accept가 발생한다.
    # 그럼 client 소켓과 addr(주소)를 튜플로 받는다.
    client_socket, addr = server_socket.accept();
    # 쓰레드를 이용해서 client 접속 대기를 만들고 다시 accept로 넘어가서 다른 client를 대기한다.
    th = threading.Thread(target=binder, args = (client_socket,addr));
    th.start();
except:
  print("server");
finally:
  # 에러가 발생하면 서버 소켓을 닫는다.
  server_socket.close();

위 소스는 socket 서버 소스입니다.

기본적으로 bind를 하고 listen으로 클라이언트를 대기한 다음에 accept가 발생하면 쓰레드로 클라이언트로 부터 메시지 대기를 합니다.

binder 함수에서의 처리는 전송 받을 데이터 사이즈, 데이터 수신 , echo를 붙힌 데이터의 데이터 사이즈 송신, 데이터 송신 순으로 흐르도록 만들었습니다.

thread 환경으로 만들었기 때문에 다시 클라이언트를 접속 받을 수 있겠네요.

# 소켓을 사용하기 위해서는 socket을 import해야 한다.
import socket
# 로컬은 127.0.0.1의 ip로 접속한다.
HOST = '127.0.0.1'  
# port는 위 서버에서 설정한 9999로 접속을 한다.
PORT = 9999       
# 소켓을 만든다.
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# connect함수로 접속을 한다.
client_socket.connect((HOST, PORT))

# 10번의 루프로 send receive를 한다.
for i in range(1,10):
  # 메시지는 hello로 보낸다.
  msg = 'hello';
  # 메시지를 바이너리(byte)형식으로 변환한다.
  data = msg.encode();
  # 메시지 길이를 구한다.
  length = len(data);
  # server로 리틀 엔디언 형식으로 데이터 길이를 전송한다.
  client_socket.sendall(length.to_bytes(4, byteorder="little"));
  # 데이터를 전송한다.
  client_socket.sendall(data);

  # server로 부터 전송받을 데이터 길이를 받는다.
  data = client_socket.recv(4);
  # 데이터 길이는 리틀 엔디언 형식으로 int를 변환한다.
  length = int.from_bytes(data, "little");
  # 데이터 길이를 받는다.
  data = client_socket.recv(length);
  # 데이터를 수신한다.
  msg = data.decode();
  # 데이터를 출력한다.
  print('Received from : ', msg);
      
client_socket.close();

서버 결과 이미지

클라이언트 결과 이미지


클라이언트에서 서버로 접속해서 hello라는 메시지를 10번 루프로 전송했습니다. 서버에서는 10번의 hello를 받고 echo : 를 붙혀서 echo : hello를 클라이언트로 전송했습니다.

결과 client에서는 echo: hello가 10번 콘솔에 결과가 나왔습니다.


위 socket을 맏들 때의 옵션과 소켓 레벨과 옵션에 대한 설명입니다.

링크 - https://www.joinc.co.kr/w/Site/Network_Programing/AdvancedComm/SocketOption


소켓 생성할 때 socket.AF_INET와 socket.SOCK_STREAM를 사용했는데 socket.AF_INET는 IP4인터넷을 사용한다는 뜻이고 데이터를 바이너리(byte 스트림)식으로 사용한다는 뜻입니다.

AF_INET이외에 AF_INET6, AF_UNIX, AF_CAN, AF_PACKET, AF_RDS의 옵션을 사용할 수 있는데 사실 저도 사용해 본적이 없어서 구체적으로 어떤 것인지 잘 모릅니다. AF_INET6만이 IP6인터넷이다 라고 생각할 뿐입니다.

거의 인터넷 주소 계열의 옵션인 듯 한데 그냥 일반 프로그램이라면 socket.AF_INET로 충분합니다.

socket.SOCK_STREAM은 연결 지향성의 뜻으로 대체 될 수 있는 옵션으로는 socket.SOCK_DGRAM, socket.SOCK_RAW, socket.SOCK_RDM, socket.SOCK_SEQPACKET 가 있습니다.

SOCK_DGRAM은 비연결 지향성이고(UDP) SOCK_RAW은 소켓의 해더 정보까지 취득할 때 사용하는 방법입니다.


소켓 레벨로는 SOL_SOCKET, IPPROTO_TCP, IPPROTO_UDP를 사용할 수 있습니다.

특히 SOL_SOCKET의 경우는 레벨 옵션을 프로토콜에서 설정하는 방법에 대한 옵션입니다.

옵션값 데이터형 설명
SO_BROADCAST BOOL 브로드캐스트 메시지 전달이 가능하도록 한다.
SO_DEBUG BOOL 디버깅 정보를 레코딩 한다.
SO_DONTLINGER BOOL 소켓을 닫을때 보내지 않은 데이터를 보내기 위해서 블럭되지 않도록 한다.
SO_DONTROUTE BOOL 라우팅 하지 않고 직접 인터페이스로 보낸다.
SO_OOBINLINE BOOL OOB 데이터 전송을 설정할때, 일반 입력 큐에서 데이터를 읽을 수 있게 한다. 이 플래그를 켜면 recv(:12)나 send(:12)에서 MSG_OOB 플래그를 사용할 필요 없이 OOB 데이터를 읽을 수 있다.
SO_GROUP_PRIORITY int 사용하지 않음
SO_KEEPALIVE BOOL Keepalives를 전달한다.
SO_LINGER struct LINGER 소켓을 닫을 때 전송되지 않은 데이터의 처리 규칙
SO_RCVBUF int 데이터를 수신하기 위한 버퍼공간의 명시
SO_REUSEADDR BOOL 이미 사용된 주소를 재사용 (bind) 하도록 한다.
SO_SNDBUF int 데이터 전송을 위한 버퍼공간 명시

저의 경우는 특별한 일이 없으면 socket.AF_INET와 socket.SOCK_STREAM 설정에 SOL_SOCKET + SO_REUSEADDR, IPPROTO_TCP + TCP_NODELAY의 형태로 자주 사용합니다.


여기까지 Python에서 Socket를 다루는 방법에 관한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.