[Python] Python과 Java에서의 소켓 통신


Development note/Python  2020. 1. 17. 09:00

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


이 글은 Python과 Java와의 소켓 통신입니다.


이전에 Python과 C#과의 소켓 통신에 대해 설명했었습니다.

링크 - [Python] Python과 C#에서의 소켓 통신


그럼 뭐 Java와도 Socket 통신에 대해 작성해야하지 않나 싶어서 작성합니다.

저의 경우는 Java는 거의 100의 99는 톰켓을 이용한 웹 프로젝트입니다. 그런데 톰켓으로 작성하니 웹 프로젝트이기는 합니다만, 내부에서 Thread하나 만들어서 소켓 서버를 두고 안드로이드나 IOS의 소켓 서버던가 각종 어플케이션 서버(게임 등등)를 개발하기도 합니다.

그니깐 내부 주요 프로그램 흐름은 소켓 서버입니다만 그 소켓 서버를 제어하기 위한 프로그램은 웹 서비스로 개발합니다.


그러므로 Java에서도 소켓을 사용하는 경우가 많이 있으니 그에 대한 모의 서버 혹은 클라이언트가 필요하는 경우가 있습니다.

python에서의 소켓 서버는 똑같습니다.

참조 - [Python] Socket 통신

# 소켓을 사용하기 위해서는 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 big 엔디언으로 byte에서 int형식으로 변환한다.	
      # C#의 BitConverter는 big엔디언으로 처리된다.	
      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();	
    th = threading.Thread(target=binder, args = (client_socket,addr));	
    # 쓰레드를 이용해서 client 접속 대기를 만들고 다시 accept로 넘어가서 다른 client를 대기한다.	
    th.start();	
except:	
  print("server");	
finally:	
   # 에러가 발생하면 서버 소켓을 닫는다.	
  server_socket.close();

데이터 전송 바이트 배열은 little 엔디언을 사용합니다.


먼저 클라이언트를 자바로 작성하겠습니다. java의 소켓 통신에 대해서는 이전에 설명한 적이 있는데 참고하기 바랍니다.

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

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class Client {
  public static void main(String... args) {
    // 소켓을 선언한다.
    try (Socket client = new Socket()) {
      // 소켓에 접속하기 위한 접속 정보를 선언한다.
      InetSocketAddress ipep = new InetSocketAddress("127.0.0.1", 9999);
      // 소켓 접속!
      client.connect(ipep);
      // 소켓이 접속이 완료되면 inputstream과 outputstream을 받는다.
      try (OutputStream sender = client.getOutputStream(); InputStream receiver = client.getInputStream();) {
        // 메시지는 for 문을 통해 10번 메시지를 전송한다.
        for (int i = 0; i < 10; i++) {
          // 전송할 메시지를 작성한다.
          String msg = "java test message - " + i;
          // string을 byte배열 형식으로 변환한다.
          byte[] data = msg.getBytes();
          // ByteBuffer를 통해 데이터 길이를 byte형식으로 변환한다.
          ByteBuffer b = ByteBuffer.allocate(4);
          // byte포멧은 little 엔디언이다.
          b.order(ByteOrder.LITTLE_ENDIAN);
          b.putInt(data.length);
          // 데이터 길이 전송
          sender.write(b.array(), 0, 4);
          // 데이터 전송
          sender.write(data);
          
          data = new byte[4];
          // 데이터 길이를 받는다.
          receiver.read(data, 0, 4);
          // ByteBuffer를 통해 little 엔디언 형식으로 데이터 길이를 구한다.
          ByteBuffer b = ByteBuffer.wrap(data);
          b.order(ByteOrder.LITTLE_ENDIAN);
          int length = b.getInt();
          // 데이터를 받을 버퍼를 선언한다.
          data = new byte[length];
          // 데이터를 받는다.
          receiver.read(data, 0, length);
          
          // byte형식의 데이터를 string형식으로 변환한다.
          msg = new String(data, "UTF-8");
          // 콘솔에 출력한다.
          System.out.println(msg);
        }
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 예제를 보면 client(java)에서 10개의 메시지를 보낼 때, 끝에 index 번호를 포함해서 보냈는데 순서대로 잘 전송이 되네요. server에서는 메시지를 잘 받아서 콘솔에 출력하고 「echo :」를 붙혀서 client로 다시 재 전송했습니다.

client에서는 「echo : 」 가 붙은 메시지를 받고 콘솔에 출력하였는데 제대로 표시되네요.


이번에는 반대로 Java를 소켓 서버로 두고 python을 클라이언트로 접속하겠습니다.

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server {
  public static void main(String... args) {
    // 클라이언트 소켓을 받을 threadpool를 선언한다. 쓰레드 풀안에는 최대 10개의 쓰레드를 가동시킬 수 있다.
    ExecutorService clientService = Executors.newFixedThreadPool(10);
    // serverSocket를 선언한다.
    try (ServerSocket server = new ServerSocket()) {
      // 포트는 9999로 오픈한다.
      InetSocketAddress ipep = new InetSocketAddress(9999);
      server.bind(ipep);
      while (true) {
        // 클라이언트가 접속할 때까지 대기한다.
        Socket client = server.accept();
        // 클라이언트가 접속이 되면 쓰레드 풀에 쓰레드를 하나 생성하고 inputstream과 outputstream을 받는다.
        clientService.submit(() -> {
          try (OutputStream sender = client.getOutputStream();
              InputStream receiver = client.getInputStream();) {
            // 서버 무한 대기를 한다.
            while (true) {
              byte[] data = new byte[4];
              // 데이터 길이를 받는다.
              receiver.read(data, 0, 4);
              // ByteBuffer를 통해 little 엔디언 형식으로 데이터 길이를 구한다.
              ByteBuffer b = ByteBuffer.wrap(data);
              b.order(ByteOrder.LITTLE_ENDIAN);
              int length = b.getInt();
              // 데이터를 받을 버퍼를 선언한다.
              data = new byte[length];
              // 데이터를 받는다.
              receiver.read(data, 0, length);

              // byte형식의 데이터를 string형식으로 변환한다.
              String msg = new String(data, "UTF-8");
              // 콘솔에 출력한다.
              System.out.println(msg);
              // echo를 붙힌다.
              msg = "Java server echo : " + msg;
              // string을 byte배열 형식으로 변환한다.
              data = msg.getBytes();
              // ByteBuffer를 통해 데이터 길이를 byte형식으로 변환한다.
              b = ByteBuffer.allocate(4);
              // byte포멧은 little 엔디언이다.
              b.order(ByteOrder.LITTLE_ENDIAN);
              b.putInt(data.length);
              // 데이터 길이 전송
              sender.write(b.array(), 0, 4);
              // 데이터 전송
              sender.write(data);
            }
          } catch (Throwable e) {
            e.printStackTrace();
          } finally {
            try {
              // 에러가 발생하면 접속을 종료한다.
              client.close();
            } catch (Throwable e) {
              e.printStackTrace();
            }
          }
        });
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위는 java에서 socketServer클래스를 이용해서 소켓 통신 서버를 생성하였습니다.

이번에는 python에서 클라이언트를 작성하여 접속합니다.

# 소켓을 사용하기 위해서는 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):	
  # 메시지를 보낸다.	
  msg = 'java hello message';	
  # 메시지를 바이너리(byte)형식으로 변환한다.	
  data = msg.encode();	
  # 메시지 길이를 구한다.	
  length = len(data);	
  # server로 little 엔디언 형식으로 데이터 길이를 전송한다.	
  client_socket.sendall(length.to_bytes(4, byteorder="little"));	
  # 데이터를 전송한다.	
  client_socket.sendall(data);	
 	
  # server로 부터 전송받을 데이터 길이를 받는다.	
  data = client_socket.recv(4);	
  # 데이터 길이는 little 엔디언 형식으로 int를 변환한다.
  length = int.from_bytes(data, "big");	
  # 데이터 길이를 받는다.	
  data = client_socket.recv(length);	
  # 데이터를 수신한다.	
  msg = data.decode();	
  # 데이터를 출력한다.	
  print('Received from : ', msg);	
 	
client_socket.close();

client에서 메시지를 만들어서 10번의 루프로 서버로 보냅니다. 그럼 서버에서는 메시지에 echo의 문자열을 붙여서 client로 메시지를 보냅니다. java server - python client도 문제없이 잘 실행됩니다.

Java서버에서 에러가 표시되는 이유는 client에서 강제로 종료를 하게 되면 exception이 발생하게 됩니다. 자바의 60번째 줄의 의해 에러 메시지가 표시되고 콘솔에 에러 메시지가 나오는 것입니다.

그리고 Python - C#의 소켓 통신과 차이라고 한다면 엔디언의 설정 차이입니다. C#은 big 엔디안이 표준이고 java는 little엔디안이 표준입니다.


여기까지 Python과 Java와의 소켓 통신에 관한 설명이었습니다.


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