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


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

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


이 글은 Python과 C#에서의 소켓 통신에 대한 글입니다.


사람마다 프로그램을 사용하는 방법이나 용도에서는 차이가 있습니다만, 저의 경우는 주력 언어는 java와 C#이고 그 외에 개발 보조 도구로 python을 사용하고 있습니다.

그 뜻은 C#의 프로젝트가 있고 CS(Client Server)프로그램을 만들 때, 보통은 Client나 Server의 한쪽을 맡아서 개발을 하게 됩니다. (뭐 프로젝트 크기에 따라 양쪽을 둘다 맡는 경우도 있죠..)

만약 제가 Client를 만든다고 하면 분명 Server와 Client의 통신 부분을 작성해야 하는 부분이 있습니다. 이 통신 부분은 HttpConnection을 이용할 수도 있고 Soap통신을 할 수도 있습니다.

그러나 가장 보편적으로 사용되는 것은 일반 Socket통신으로 프로토콜(일명 전문 규약(?))을 설정한 다음에 정보를 주고 받는 것이 가장 많겠네요.

그럴 때, Server도 Client와의 통신 부분을 작성해야 하지만 서로 간의 일정이 차이가 있는 경우, Client에서 Server의 통신 부분을 작성될 때까지 마냥 기다릴 수는 없기 때문에 저는 스크립트 언어(Python이나 node.js)로 모의 서버(?)를 만들어서 개발합니다.(반대의 경우도 server를 만들 경우, 모의 단말을 만들기도 합니다.)

개인적으로 javascritp(node.js)를 좋아하기는 하지만 로컬 스크립트 언어로는 node.js보다는 python이 더 편하기 때문에 python을 자주 사용합니다.


꼭 이런 목적이 아니고도, 스크립트로 다른 언어간의 통신을 할 수 있죠..

그럼 먼저 Python의 서버와 C#의 클라이언트 통신을 알아보겠습니다.

# 소켓을 사용하기 위해서는 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바이트는 전송할 데이터의 크기이다. 그 크기는 big 엔디언으로 byte에서 int형식으로 변환한다.
      # C#의 BitConverter는 big엔디언으로 처리된다.
      length = int.from_bytes(data, "big");
      # 다시 데이터를 수신한다.
      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);
      # 데이터 사이즈를 big 엔디언 형식으로 byte로 변환한 다음 전송한다.(※이게 버그인지 big을 써도 little엔디언으로 전송된다.)
      client_socket.sendall(length.to_bytes(4, byteorder='big'));
      # 데이터를 클라이언트로 전송한다.
      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();

위 소스는 이전에 작성한 글에서 python의 서버와 크게 다르지 않습니다.

링크 - [Python] Socket 통신

다른 점은 little엔디언을 사용하는 것이 아니고 big 엔디언을 사용합니다. 근데, to_bytes에서 byteorder를 big으로 설정해도 little 엔디언으로 전송이 되네요... from_bytes는 big을 쓰면 big엔디언으로 설정되는데...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace SocketTest
{
  class Program
  {
    static void Main(string[] args)
    {
      // 소켓을 생성한다.
      using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
      {
        // Connect 함수로 로컬(127.0.0.1)의 포트 번호 9999로 대기 중인 socket에 접속한다.
        client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));
        // 보낼 메시지를 UTF8타입의 byte 배열로 변환한다.
        var data = Encoding.UTF8.GetBytes("this message is sent from C# client.");
        
        // big엔디언으로 데이터 길이를 변환하고 서버로 보낼 데이터의 길이를 보낸다. (4byte)
        client.Send(BitConverter.GetBytes(data.Length));
        // 데이터를 전송한다.
        client.Send(data);

        // 데이터의 길이를 수신하기 위한 배열을 생성한다. (4byte)
        data = new byte[4];
        // 데이터의 길이를 수신한다.
        client.Receive(data, data.Length, SocketFlags.None);
        // server에서 big엔디언으로 전송을 했는데도 little 엔디언으로 온다. big엔디언과 little엔디언은 배열의 순서가 반대이므로 reverse한다.
        Array.Reverse(data);
        // 데이터의 길이만큼 byte 배열을 생성한다.
        data = new byte[BitConverter.ToInt32(data, 0)];
        // 데이터를 수신한다.
        client.Receive(data, data.Length, SocketFlags.None);
        // 수신된 데이터를 UTF8인코딩으로 string 타입으로 변환 후에 콘솔에 출력한다.
        Console.WriteLine(Encoding.UTF8.GetString(data));
      }

      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 클라이언트 소스는 C#의 소켓 통신에서 설명한 적이 있습니다.

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

위 예제를 보면 client(C#)에서 「this message is sent from C# client.」이라는 메시지를 보냈는데 server에서는 메시지를 잘 받아서 콘솔에 출력하고 「echo :」를 붙혀서 client로 다시 재 전송했습니다.

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


이번에는 반대의 경우 C#이 서버이고 python이 클라이언트로 접속하는 것을 작성하겠습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace SocketTest
{
  class Program
  {
    static void Main(string[] args)
    {
      // server 소켓을 생성한다.
      using (var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
      {
        // ip는 로컬이고 포트는 9999로 listen 대기한다.
        server.Bind(new IPEndPoint(IPAddress.Any, 9999));
        server.Listen(20);

        Console.WriteLine("Server Start... Listen port 9999...");

        try
        {
          while (true)
          {
            // 다중 접속을 허용하기 위해 Threadpool를 이용한 멀티 쓰레드 환경을 만들었다.
            ThreadPool.QueueUserWorkItem(c =>
            {
              Socket client = (Socket)c;
              try
              {
                // 무한 루프로 메시지를 대기한다.
                while (true)
                {
                  // 처음에 데이터 길이를 받기 위한 4byte를 선언한다.
                  var data = new byte[4];
                  // python에서 little 엔디언으로 값이 온다. big엔디언과 little엔디언은 배열의 순서가 반대이므로 reverse한다.
                  client.Receive(data, 4, SocketFlags.None);
                  Array.Reverse(data);
                  // 데이터의 길이만큼 byte 배열을 생성한다.
                  data = new byte[BitConverter.ToInt32(data, 0)];
                  // 데이터를 수신한다.
                  client.Receive(data, data.Length, SocketFlags.None);
                  
                  // byte를 UTF8인코딩으로 string 형식으로 변환한다.
                  var msg = Encoding.UTF8.GetString(data);
                  // 데이터를 콘솔에 출력한다.
                  Console.WriteLine(msg);
                  // 메시지에 echo를 문자를 붙힌다.
                  msg = "C# server echo : " + msg;
                  // 데이터를 UTF8인코딩으로 byte형식으로 변환한다.
                  data = Encoding.UTF8.GetBytes(msg);
                  // 데이터 길이를 클라이언트로 전송한다.
                  client.Send(BitConverter.GetBytes(data.Length));
                  // 데이터를 전송한다.
                  client.Send(data, data.Length, SocketFlags.None);
                }

              }
              catch (Exception)
              {
                // Exception이 발생하면 (예기치 못한 접속 종료) client socket을 닫는다.
                client.Close();
              }
            // server로 client가 접속이 되면 ThreadPool에 Thread가 생성됩니다.
            }, server.Accept());
          }
        }
        catch (Exception e)
        {
          Console.WriteLine(e);
        }
      }
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 소스는 python server와 같은 기능입니다만 C#으로 작성한 소스 코드입니다.

# 소켓을 사용하기 위해서는 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로 big 엔디언 형식으로 데이터 길이를 전송한다.
  client_socket.sendall(length.to_bytes(4, byteorder="big"));
  # 데이터를 전송한다.
  client_socket.sendall(data);

  # server로 부터 전송받을 데이터 길이를 받는다.
  data = client_socket.recv(4);
  # 데이터 길이는 big 엔디언 형식으로 int를 변환한다. (※이게 버그인지 big을 써도 little엔디언으로 전송된다.)
  length = int.from_bytes(data, "big");
  # 데이터 길이를 받는다.
  data = client_socket.recv(length);
  # 데이터를 수신한다.
  msg = data.decode();
  # 데이터를 출력한다.
  print('Received from : ', msg);

client_socket.close();

client에서는 hello 라는 문자를 10번 전송하는 것으로 루프를 만들었습니다. server에서는 10번의 메시지를 받고 10번의 echo 메시지를 client로 보냈습니다.

역시 client - server 간에도 송수신이 문제없이 되는 군요..


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


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