C#과 Python 간의 소켓 통신

김혜성·2022년 8월 30일
1
post-thumbnail

Socket Programming

서버와 클라이언트는 소켓이라는 보관함을 만든다. 이들은 서로 데이터를 이 보관함으로 주고받으며 통신을 할 수 있다. 이 중 OSI 7 Layers의 네번째 계층의 TCP 계층에서 동작하는 소켓을 쓴다

Server Socket & Client Socket

TCP 특성상 일방적인 요청으로 통신이 이루어지지 않고, 한쪽이 요청하면 한쪽이 받아줘야 한다. 이 때 요청을 받는 쪽이 Server, 요청을 하는 쪽이 Client가 되겠다.

  • 그치만 서버소켓과 클라이언트소켓은 다르지 않다! 같은 종류의 소켓이나 역할에 따라 다르게 부르는 것 뿐
  • 또한 서버소켓과 클라이언트소켓이 직접 왔다갔다를 하는 게 아니다! 왔다갔다 직접 데이터를 송수신하는 소켓은 따로 있다! - 서버소켓의 요청수락시 새로 생성되는 소켓.

Socket API 흐름

Client Socket
1. create socket
2. connect to server socket
3. send & receive
4. close

Server Socket
1. create socket
2. bind - 사용할 IP 주소와 포트번호를 소켓에 결합
3. listen - 클라이언트로부터 수신요청이 오는지 주시
4. accept - 요청이 수신되면 받아들임 + 데이터 송수신을 위한 소켓 생성
5. sned & receive
6. close

Python 2 Python

Python과 C#간의 통신 이전에 파이썬끼리 소켓 프로그래밍을 먼저 해보면서 감을 좀 익혔다.

한번씩 메세지 주고받기

server.py

from socket import *

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 25001))
serverSock.listen(1)

connectionSock, addr = serverSock.accept()

print('Connection from ',str(addr))

data = connectionSock.recv(1024)
print('Received Data : ', data.decode('utf-8'))

connectionSock.send('Yo I\'m Server'.encode('utf-8'))
print('Message sent successfully')

serverSock.close()
connectionSock.close()
  • socket(AF_INET, SOCK_STREAM)
    소켓 객체 생성
    인자는 AF(Address Family), Socket Type
    AF는 주소 체계로, 거의 AF_INET 사용 AF_INET은 IPv4, AF_INET6은 IPv6를 의미
  • bind(('', 25001))
    튜플 입력! 소켓과 AF((ip, port)) 연결하는 과정
    ''는 AF_INET에서 모든 인터페이스와 연결한다는 의미
  • listen(1)
    인자는 동시접속 허용 개수
  • accept()
    client가 접속 요청할 때 결과값이 return됨
    accept() 실행시 새로운 소켓이 생성됨
    기존에 생성한 serverSock이 아닌 새로운 connectionSock를 통해서 데이터 주고받음!!!
  • close()
    server의 경우 총 2개의 소켓(serverSock, connectionSock)이 생성되었으므로 이 둘 모두를 닫아줘야 함

client.py

from socket import *

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect(('127.0.0.1', 25001))

print('connection has been made!')
clientSock.send('I am Client'.encode('utf-8'))

print('Message sent')

data = clientSock.recv(1024)
print('Received Data : ', data.decode('utf-8'))

clientSock.close()
  • connect(('127.0.0.1', 25001))
    AF가 튜플로 인자로 들어감

  • send(msg.encode('utf-8'))
    데이터를 전송하는 함수
    * encode() - 문자열을 byte로 변환(문자열은 파이썬 내부에서 작동하는 객체로, 이를 바로 통신에 쓸 수는 없음)

  • recv(1024)
    recv()실행시 소켓에 메세지가 수신될 때까지 대기하게 됨
    1024: 소켓에서 1024 bytes만큼 가져오겠다는 의미
    * decode('utf-8'): encode와 마찬가지로 byte로 데이터를 수신 후 문자열로 변환해야 함

계속 메세지 주고받기

  • while문으로 계속 데이터 주고받게 하기
  • 사용자가 입력한 문자열을 보내게 하기

쓰레드를 이용하여 데이터 송수신을 순서없이 할 수 있도록 한다

server.py

from socket import *
import threading
import time

def send(sock):
    while True:
        sendingData = input('enter message: ')
        sock.send(sendingData.encode('utf-8'))

def receive(sock):
    while True:
        recvData = sock.recv(1024)
        print("client: ", recvData.decode('utf-8'))

port = 25001

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', port))
serverSock.listen(1)

print("%d port waiting for connection..."%port)

connectionSock, addr = serverSock.accept()

print('Connection from ',str(addr))

sender = threading.Thread(target=send, args=(connectionSock,))
receiver = threading.Thread(target=receive, args=(connectionSock,))
# target: 실제로 쓰레드가 실행할 함수
# args: 그 함수에게 전달할 인자 - 튜플같이 iterable한 변수만 입력가능! but 인자가
# 하나면 튜플이 아닌 var로 인식하기에 (soc,)로 입력해야 함

sender.start()
receiver.start()
# start()로 생성되었던 쓰레드가 실행됨, 전부 수행하면 소멸함
# -> 소멸하지 않게 하기위해 while True를 send, receive 함수에 넣음
# 그치만 이 프로세스가 종료되면 분신과 같은 쓰레드 역시 소멸

while True:
    time.sleep(1)
    pass

# 그냥 pass하면 쉬지않고 코드를 실행함 -> 1초씩 쉬어가며 반복하도록

connectionSock.close()
serverSock.close()

client.py

from socket import *
import threading
import time

def send(sock):
    while True:
        sendingData = input('enter message: ')
        sock.send(sendingData.encode('utf-8'))

def receive(sock):
    while True:
        recvData = sock.recv(1024)
        print("server: ", recvData.decode('utf-8'))

ip = '127.0.0.1'
port = 25001

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect((ip, port))

print('connection has been made!')

sender = threading.Thread(target=send, args=(clientSock,))
receiver = threading.Thread(target=receive, args=(clientSock,))

sender.start()
receiver.start()

while True:
    time.sleep(1)
    pass

    
clientSock.close()

여기서

주의사항

  • Server 먼저 실행 후 Client 실행
  • [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다
    1. 서버 ip주소나 포트번호가 잘못됨
    2. 서버를 실행하지 않음
    3. 같은 Idle에서 실행함!! - 같은 idle.exe에서 실행하면 먼저 실행한 서버 스크립트가 닫히게 된다...
  • 두번째 코드로 실행했을 때 입력 도중 전송을 받으면 끊기는 현상 발생
    python 기본 IDLE에서는 stdin을 사용할 때 stdout을 막아서 내가 송신을 해야 그 사이에 상대가 보낸 데이터를 확인할 수 있게 된다고 한다.. GUI로 만들면 이를 방지할 수도..?

이제 문제는 C#과의 소켓 통신과 리스트를 어떻게 send/recv할 것이냐이다. 소켓으로 보내는 형식은 무조건 byte여야 한다. 리스트를 특정 규칙에 따라 전송하고 receive할 때에도 해당 규칙에 따라 풀어서 받아야 할 듯 하다...

아니 잠만 구체적으로 내가 보내려고 하는 게 뭐지? 이렇게 쓸 시간에 생각을 하는 게 낫지않나? 쓴다고 해결이 되나? 쓰는동안에 이 쓰는 거에 집중을 하게되면서 오히려 생각은 못하고 시간낭비만 하고있는데ㅋㅋㅋ 언제까지 이러나 보자
그만

우선 무조건적으로 보내야 하는 건 "좌표"이다. 이건 문자열로 전송이니까 ㄱㅊ
근데 좌표만 보내는 게 아니지? 그때그때 프레임도 보내야될까? 아니면 영상의 위치와 영상 중 몇번째 프레임인지 숫자를 보내면 되려나?

  1. 좌표 + 프레임(array)
  2. 좌표 + 파일명 + 프레임 번호

그래서 준비했습니다...

TCP image socket

이미지 송수신시 해당 이미지 데이터의 크기를 같이 보내야 한다!
TCP socket에서 한번에 보낼 수 있는 데이터 크기가 제한적이라 이미지를 string으로 변환하여 보낼 때 이미지 크기를 받고 그 크기만큼 socket에서 데이터를 받아서 다시 이미제 데이터로 변환하는 과정이 필요

근데 나는 이미지 데이터 크기가 다 일정하쥬? 다 2400x496이니까 미리 소켓에서 이만큼으로 받거나 크다 싶으면 잘라서 받아주면 되쥬?

server.py

class ServerSocket:

    def __init__(self, ip, port):
        self.TCP_IP = ip
        self.TCP_PORT = port
        self.socketOpen()
        self.receiveThread = threading.Thread(target=self.receiveImages)
        self.receiveThread.start()

    def socketClose(self):
        self.sock.close()
        print(u'Server socket [ TCP_IP: ' + self.TCP_IP + ', TCP_PORT: ' + str(self.TCP_PORT) + ' ] is close')

    def socketOpen(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.bind((self.TCP_IP, self.TCP_PORT))
        self.sock.listen(1)
        print(u'Server socket [ TCP_IP: ' + self.TCP_IP + ', TCP_PORT: ' + str(self.TCP_PORT) + ' ] is open')
        self.conn, self.addr = self.sock.accept()
        print(u'Server socket [ TCP_IP: ' + self.TCP_IP + ', TCP_PORT: ' + str(self.TCP_PORT) + ' ] is connected with client')

    def receiveImages(self):

        try:
            while True:
                length = self.recvall(self.conn, 64)
                length1 = length.decode('utf-8')
                stringData = self.recvall(self.conn, int(length1))
                stime = self.recvall(self.conn, 64)
                print('send time: ' + stime.decode('utf-8'))
                now = time.localtime()
                print('receive time: ' + datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
                data = numpy.frombuffer(base64.b64decode(stringData), numpy.uint8)
                decimg = cv2.imdecode(data, 1)
                cv2.imshow("image", decimg)
                cv2.waitKey(1)
        except Exception as e:
            print(e)
            self.socketClose()
            cv2.destroyAllWindows()
            self.socketOpen()
            self.receiveThread = threading.Thread(target=self.receiveImages)
            self.receiveThread.start()

    def recvall(self, sock, count):
        buf = b''
        while count:
            newbuf = sock.recv(count)
            if not newbuf: return None
            buf += newbuf
            count -= len(newbuf)
        return buf

def main():
    server = ServerSocket('localhost', 8080)

if __name__ == "__main__":
    main()

client.py - local video 송신

class ClientVideoSocket:
    def __init__(self, ip, port, video_path):
        self.TCP_SERVER_IP = ip
        self.TCP_SERVER_PORT = port
        self.video_path = video_path
        self.connectCount = 0
        self.connectServer()

    def connectServer(self):
        try:
            self.sock = socket.socket()
            self.sock.connect((self.TCP_SERVER_IP, self.TCP_SERVER_PORT))
            print(u'Client socket is connected with Server socket [ TCP_SERVER_IP: ' + self.TCP_SERVER_IP + ', TCP_SERVER_PORT: ' + str(self.TCP_SERVER_PORT) + ' ]')
            self.connectCount = 0
            self.sendImages()
        except Exception as e:
            print(e)
            self.connectCount += 1
            if self.connectCount == 10:
                print(u'Connect fail %d times. exit program'%(self.connectCount))
                sys.exit()
            print(u'%d times try to connect with server'%(self.connectCount))
            time.sleep(1)
            self.connectServer()

    def sendImages(self):
        cnt = 0
        capture = cv2.VideoCapture(self.video_path)
        capture.set(cv2.CAP_PROP_FRAME_WIDTH, 480)
        capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 315)
        try:
            while capture.isOpened():
                ret, frame = capture.read()
                resize_frame = cv2.resize(frame, dsize=(480, 315), interpolation=cv2.INTER_AREA)

                now = time.localtime()
                stime = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')

                encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]
                result, imgencode = cv2.imencode('.jpg', resize_frame, encode_param)
                data = numpy.array(imgencode)
                stringData = base64.b64encode(data)
                length = str(len(stringData))
                self.sock.sendall(length.encode('utf-8').ljust(64))
                self.sock.send(stringData)
                self.sock.send(stime.encode('utf-8').ljust(64))
                print(u'send images %d'%(cnt))
                cnt+=1
                time.sleep(0.02)
        except Exception as e:
            print(e)
            self.sock.close()
            time.sleep(1)
            self.connectServer()
            self.sendImages()

def main():
    TCP_IP = 'localhost'
    TCP_PORT = 8080
    video_path = './big_buck_bunny_720p_10mb.mp4'
    client = ClientVideoSocket(TCP_IP, TCP_PORT, video_path)

if __name__ == "__main__":
    main()
  • capture.set으로 프레임의 width나 height를 resize할 수 있다
  • openCV로 image를 byte로 변환하는 것이 가능하다!
  • TCP Socket 버퍼 크기는 기본 8KB, 최대 크기는 8MB(8096KB)라고 한다. 저 코드로 큰 바이트가 보내지는지 테스트해봐야게쓰...

Unity C# & Python Server 코드

https://www.youtube.com/watch?v=VeIiU9qTsGI

여기 코드가 있다. python과 c#에서 socket을 사용해서 서로 데이터를 실시간으로 주고받는다는 것. 이 사람은 Unity에서의 스크립트와 파이썬을 연동했고, String으로 주고받았는데, 일단 주고받는 게 된다는 점을 참고하면 좋을 것 같다. 나는 numpy? array? list? 를 Mat으로 주는 것과 좌표정도는 String으로 넘기는 거, 그렇게 둘을 해보면 될 것 같다.

이건 C#의 코드

using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using System.Threading;
public class CSharpForGIT : MonoBehaviour
{
    Thread mThread;
    public string connectionIP = "127.0.0.1";
    public int connectionPort = 25001;
    IPAddress localAdd;
    TcpListener listener;
    TcpClient client;
    Vector3 receivedPos = Vector3.zero;
    bool running;
    private void Update()
    {
        transform.position = receivedPos; //assigning receivedPos in SendAndReceiveData()
    }
    private void Start()
    {
        ThreadStart ts = new ThreadStart(GetInfo);
        mThread = new Thread(ts);
        mThread.Start();
    }
    void GetInfo()
    {
        localAdd = IPAddress.Parse(connectionIP);
        listener = new TcpListener(IPAddress.Any, connectionPort);
        listener.Start();
        client = listener.AcceptTcpClient();
        running = true;
        while (running)
        {
            SendAndReceiveData();
        }
        listener.Stop();
    }
    void SendAndReceiveData()
    {
        NetworkStream nwStream = client.GetStream();
        byte[] buffer = new byte[client.ReceiveBufferSize];
        //---receiving Data from the Host----
        int bytesRead = nwStream.Read(buffer, 0, client.ReceiveBufferSize); //Getting data in Bytes from Python
        string dataReceived = Encoding.UTF8.GetString(buffer, 0, bytesRead); //Converting byte data to string
        if (dataReceived != null)
        {
            //---Using received data---
            receivedPos = StringToVector3(dataReceived); //<-- assigning receivedPos value from Python
            print("received pos data, and moved the Cube!");
            //---Sending Data to Host----
            byte[] myWriteBuffer = Encoding.ASCII.GetBytes("Hey I got your message Python! Do You see this massage?"); //Converting string to byte data
            nwStream.Write(myWriteBuffer, 0, myWriteBuffer.Length); //Sending the data in Bytes to Python
        }
    }
    public static Vector3 StringToVector3(string sVector)
    {
        // Remove the parentheses
        if (sVector.StartsWith("(") && sVector.EndsWith(")"))
        {
            sVector = sVector.Substring(1, sVector.Length - 2);
        }
        // split the items
        string[] sArray = sVector.Split(',');
        // store as a Vector3
        Vector3 result = new Vector3(
            float.Parse(sArray[0]),
            float.Parse(sArray[1]),
            float.Parse(sArray[2]));
        return result;
    }
    /*
    public static string GetLocalIPAddress()
    {
        var host = Dns.GetHostEntry(Dns.GetHostName());
        foreach (var ip in host.AddressList)
        {
            if (ip.AddressFamily == AddressFamily.InterNetwork)
            {
                return ip.ToString();
            }
        }
        throw new System.Exception("No network adapters with an IPv4 address in the system!");
    }
    */
}

이건 Python의 코드

import socket
import time

host, port = "127.0.0.1", 25001
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

startPos = [0, 0, 0] #Vector3   x = 0, y = 0, z = 0
while True:
    time.sleep(0.5) #sleep 0.5 sec
    startPos[0] +=1 #increase x by one
    posString = ','.join(map(str, startPos)) #Converting Vector3 to a string, example "0,0,0"
    print(posString)

    sock.sendall(posString.encode("UTF-8")) #Converting string to Byte, and sending it to C#
    receivedData = sock.recv(1024).decode("UTF-8") #receiveing data in Byte fron C#, and converting it to String
    print(receivedData)

WinForm C# & Python Server 코드

https://www.youtube.com/watch?v=_k-E5GbOeQg

C#

//Imports libraries
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;

namespace SocketClient
{
    public partial class Form1 : Form
    {
        // Main Variables
        int port; //Port number of server
        string message; //Message to send
        int byteCount; //Raw bytes to send
        NetworkStream stream; //Link stream
        byte[] sendData; //Raw data to send
        TcpClient client; //TCP client variable

        public Form1()
        {
            InitializeComponent();
        }
        
        private void Form1_Load(object sender, EventArgs e)
        {

        }

        //toolStripButton1 -- Open Connection
        private void toolStripButton1_Click(object sender, EventArgs e)
        {
            //Convert port number to text with error handling 
            if(!int.TryParse(textBox1.Text, out port))
            {
                //Adds debug to list box and shows message box
                MessageBox.Show("Port number not valied");
                listBox1.Items.Add("Port number invalied");
            }
            
            //Attemps to make connection
            try
            {
                client = new TcpClient(textBox3.Text, port);
                //Adds debug to list box and shows message box
                MessageBox.Show("Connection Made");
                listBox1.Items.Add("Made connection with " + textBox3.Text);
            }
            catch (System.Net.Sockets.SocketException)
            {
                //Adds debug to list box and shows message box
                MessageBox.Show("Connection Failed");
                listBox1.Items.Add("Connection failed");
            }
            
           
        }

        //toolStripButton2 -- Send
        private void toolStripButton2_Click(object sender, EventArgs e)
        {
            try
            {
                message = textBox2.Text; //Set message variable to input
                byteCount = Encoding.ASCII.GetByteCount(message); //Measures bytes required to send ASCII data
                sendData = new byte[byteCount]; //Prepares variable size for required data
                sendData = Encoding.ASCII.GetBytes(message); //Sets the sendData variable to the actual binary data (from the ASCII)
                stream = client.GetStream(); //Opens up the network stream
                stream.Write(sendData, 0, sendData.Length); //Transmits data onto the stream
                listBox1.Items.Add("Sent Data " + message); //Adds debug to the list box
            }
            catch (System.NullReferenceException) //Error if socket not open
            {
                //Adds debug to list box and shows message box
                MessageBox.Show("Connection not installised");
                listBox1.Items.Add("Failed to send data");
            }
        }

        //toolStripButton3 -- Close Connection
        private void toolStripButton3_Click(object sender, EventArgs e)
        {
            stream.Close(); //Closes data stream
            client.Close(); //Closes socket
            listBox1.Items.Add("Connection terminated"); //Adds debug message to list box
        }
    }
}

파이썬

#Imports modules
import socket
import RPi.GPIO as GPIO
import time

listensocket = socket.socket() #Creates an instance of socket
Port = 8000 #Port to host server on
maxConnections = 999
IP = socket.gethostname() #IP address of local machine

listensocket.bind(('',Port))

#Starts server
listensocket.listen(maxConnections)
print("Server started at " + IP + " on port " + str(Port))

#Accepts the incomming connection
(clientsocket, address) = listensocket.accept()
print("New connection made!")

running = True

#Sets up the GPIOs --Can only be used on Raspberry Pi
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7,GPIO.OUT)

while running:
    message = clientsocket.recv(1024).decode() #Gets the incomming message
    print(message)
    if not message == "":
        GPIO.output(7,True)
        time.sleep(5)
        GPIO.output(7,False)

Marshalling

Serialization? 직렬화? 와 유사한 개념이라고 한다. 한 프로세스에서 데이터를 마샬링해서 다른 서버나 프로세스로 내보내면 그걸 받는 곳에서 언마샬링을 해서 쓰는 식으로 통신하는 것이다. 각 프로세스나 프로그램마다 데이터를 적용하는 방식이 다르니 데이터를 주고받을 때에는 표준에 맞게 데이터를 가공해주는? 그런 거라고 한다.

소켓으로 데이터가 일단 넘어가도록 구현한 다음에 생각해 볼 문제!

Reference

profile
똘멩이

0개의 댓글