python의 soocket 통신시 send()sendall()의 차이에 대해서


Linux 시스템이 구동되는 임베디드 장비의 python과 Windows PC의 C#간 Socket통신을 하면서 임베디드 장비에서 binary 파일(이미지 파일)을 읽어서 Ethernet을 통해 PC로 전송하는 프로그램을 개발하다 보니 묘하게도 전송된 파일의 사이즈가 원본과 약간의 차이가 나는 것을 발견하게 되었다.

몇 byte에서 몇 십 byte까지...


로그를 출력해보면 데이터를 전송하는 쪽(python)에서 뭔가 모르게 데이터를 다 전송하지 못하는것 같았다. 이게 말이 되는지 모르지만 아무튼 전송된 파일의 파일 사이즈가 약간 적게 전송되는 기현상이 계속되었다.


결국은 python socket의 send()와 sendall()이 차이가 있음을 보게된다. 황당함~

Stackoverflow에 다음과 같은 설명이 있다.


https://stackoverflow.com/questions/34252273/what-is-the-difference-between-socket-send-and-socket-sendall


socket.send is a low-level method and basically just the C/syscall method send(3) / send(2). It can send less bytes than you requested, but returns the number of bytes sent.


socket.sendall is a high-level Python-only method that sends the entire buffer you pass or throws an exception. It does that by calling socket.send until everything has been sent or an error occurs.


If you're using TCP with blocking sockets and don't want to be bothered by internals (this is the case for most simple network applications), use sendall.


send()

 -. low-level에서 작동되는 시스템콜 형태

 -. 요청한 데이터보다 더 적게 전송할수도 있다.

 -. 그러나 요청한 만큼 전송한 걸로 return해 준다.


sendall()

 -. high-level단의 메소드로 python 메소드이다

 -. 요청한 데이터의 모든 버퍼 내용을 모두 전송한다. 그렇게 되지않을시 Exception발생

 -. sendall()도 내부적으로는 send()를 이용하는데 단지 모두 전송할 때까지 send()를 호출한다.


황당하지만 이렇다고 한다.

테스트해보면 5Mb정도의 이미지 파일을 전송해 볼때 sendall()이 send()보다는 확실히 속도가 좀 떨어지는 것 같다.




임베디드 장비이든 혹은 웹 서버이든 IP를 고정해야 할 경우들이 있다. 

DHCP로 IP를 자동 할당 받는 경우 IP가 고정이 아닌 유동적으로 바뀔수가 있기 때문에 고정 IP(static IP) 설정하는 법은 여러 모로 필요한 작업이다.

본 포스트에서는 Linux Ubuntu 상에서 IP를 어떻게 고정으로 할당할수 있을 것인지를 정리하고자 한다.

IP를 고정시키기 위해서는 다음의 정보가 필요하다.


-. 고정시킬 IP Address

-. Net mask

-. Default gateway

-. DNS Server


여기서 외부로 나갈 필요가 없는 상황이라면 즉 인터넷을 할 필요가 없이 임베디드 장비와 PC간의 통신만 되면 된다거나 아무튼 외부 통신을 필요로 하지 않는다면 gateway 정보나 DNS server 정보는 없어도 된다.


우선 현재의 네트워크 자원들에 대해 확인부터 해 보자.


# ip link show

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000

    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000

    link/ether 08:00:27:ff:a4:58 brd ff:ff:ff:ff:ff:ff


# ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

    inet 127.0.0.1/8 scope host lo

       valid_lft forever preferred_lft forever

    inet6 ::1/128 scope host 

       valid_lft forever preferred_lft forever

2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether 08:00:27:ff:a4:58 brd ff:ff:ff:ff:ff:ff

    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3

       valid_lft 83702sec preferred_lft 83702sec

    inet6 fe80::c33f:ad86:8225:3597/64 scope link 

       valid_lft forever preferred_lft forever


위의 정보를 보면 2개의 네트워크 interface가 보인다. 하나는 lo인데 이는 loop back으로 자기 자신을 나타내는 interface이다. 

우리가 고정 IP를 설정할 네트워크는 enp0s3로 명명되어진 이 interface이다.

link/ether는 이 장비가 이더넷 장비라는 뜻이고 IP 주소는 10.0.2.15이다.

참고로 위의 정보는 Virtual Box에서 실행되는 Ubuntu이다.

보통 공유기 하위에 물려 있는 경우라면 192.168.x.x와 같은 형태가 될 것이다.


이제 IP정보를 설정해 보자. 아래 파일을 열어 보면 대체로 이런 정보가 들어 있다.


# vi /etc/network/interfaces


# interfaces(5) file used by ifup(8) and ifdown(8)

auto lo

iface lo inet loopback


위의 파일 내용에 고정 IP 할당에 대한 정보를 아래와같이 입력하고 파일을 저장한다.


auto enp0s3

iface enp0s3 inet static

address 192.168.0.77

netmastk 255.255.255.0

gateway 10.0.2.2

dns-nameserver 8.8.8.8 8.8.4.4


gateway는 현재의 gateway를 그대로 사용했고 dns-nameserver는 구글 것을 사용했다.

현재의 gateway 정보를 알려면 다음 명령으로 확인할 수 있다.


root@joe-VirtualBox:/etc/network# ip route

default via 10.0.2.2 dev enp0s3  proto static  metric 100 

10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15  metric 100 

169.254.0.0/16 dev enp0s3  scope link  metric 1000 


현재의 gateway(인터넷을 할수 있는 출입문, 통신이 외부로 나갈수 있는 출입문 IP)정보는

default via 10.0.2.2 dev enp0s3로 되어 있는 10.0.2.2이다.

아래는 또 다른 경우에 대한 정보이고 192.168.0.1이 현재의 gateway이다.


root@localhost:/etc/network# ip route

default via 192.168.0.1 dev eth0  metric 100

169.254.0.0/16 dev eth0  scope link  metric 1000

192.168.0.0/24 dev eth0  proto kernel  scope link  src 192.168.0.88


위의 정보만으로 네임서버가 안된다면 다음 정보를 추가하도록 하자.

네임서버란 www.google.com, www.naver.com과 같이 숫자로 된 IP가 아닌 사람이 쉽게 파악할수 있는 형태의 IP 지정 방식이다.

아래의 경로에 tail이라는 파일을 생성해서(만일 없다면 새로 만든다) nameserver 8.8.8.8을 입력하고 파일을 저장한다.


# vi /etc/resolvconf/resolv.conf.d/tail

nameserver 8.8.8.8


이상으로 설정을 완료되었고 네트워크 장비를 재시작 한다.


ip addr flush enp0s3 && systemctl restart networking.service


만일 구버전 리눅스라면 위의 명령이 실행되지 않을수 있다. 그럴 경우는 아래와 같이 하자.


# ip addr flush enp0s3 && /etc/init.d/networking restart


여기까지가 Ubuntu 시스템 상에서의 고정 IP 설정하는 법이다.

정상적으로 설정이 되었다면 터미널 창에서 ping이 정상적으로 송수신될 것이다.


# ping www.google.com


위 명령이 정상적으로 수행된다면 IP 설정과 nameserver 설정이 제대로 된 것이다.

그런데 ping 172.217.24.4과 같이 IP로는 실행되나 도메인 네임으로는 수행되지 않는다면 이건 nameserver 설정이 잘못되었다는 뜻이다.



delegate는 C#이 가진 독특한 개념인데 따라서 기본적으로 생소한 개념으로 다가온다.

아래는 delegate에 대한 기본적인 개념 소개와 간단한 예제를 통해 개념을 이해해 보고자 한다.


using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;


namespace EXdelegate1

{

    class Program

    {

        //delegate란 

        // (1) 일종의 데이터 type과 같다. 

        // (2) 어떤 메소드를 encapsulate할수 있다. 즉 어떤 메소드를 가리킬수 있고 이를 통해 그 메소드를 실행할수 있다.

        // (3) similar to function pointer in C, C++ 

        // (C, C++에서 어떤 함수를 포인터로 가리키고 이 포인터를 이용해서 해당 함수를 실행하는 방식)

        //아래는 새로운 데이터 타입과 같은 delegate를 선언하는데 이 새로운 데이터 타입의 이름은 Del이고

        //이 데이터 타입이 가리킬수 있는 메소드는 argument로 string을 하나 받고 반환 값은 void인

        //메소드가 있다면 그들 메소드들은 모두 delegate 이름이 Del인 새로운 데이터 타입으로 객체를 선언하고

        //이 선언된 delegate 타입의 객체로 그들 메소드들을 실행할수 있다.

        private delegate void Del(string message);


        //아래는 새로운 데이터 타입인 MyDel이라는 이름의 데이터 타입을 선언하는데 이 데이터 타입은 

        //특별히 delegate형태의 데이터 타입이다.

        //MyDel라는 데이터 타입으로 선언된 어떤 객체(참조변수)는 argument로 string 타입 하나, int 타입 하나를

        //받고 반환 데이터는 string 타입을 반환하는 모든 메소드를 MyDel이라는 데이터 타입으로 모두 

        //가리키고 또 실행할수 있다는 개념이다.

        private delegate string MyDel(string info, int some);


        static void Main(string[] args)

        {

            //delegate 객체를 생성. 이 delegate 객체가 실행할 메소드는 DelegateMethod()이다.

            //handler는 delegate Del 타입의 object이다.

            //delegate 타입의 객체 생성은 Java나 기존의 객체 생성 방식과 약간 다르다.

            //Del이라는 delegate 타입의 객체 handler가 실행할 메소드 이름을 대입해 주면 

            //delegate Del 타입의 새로운 객체가 하나 생성된다.

            Del handler = DelegateMethod;

            handler("delegate object가 보낸 메시지");


            MyDel another = AnotherDelegateMethod;


            Console.WriteLine("\n\n 이름 : " + another("홍길동", 25));


            Del myDel = mDel;

            myDel("Del이라는 이름의 delegate가 실행하는 두~~ 번째 메소드임");



            //익명함수 방식으로 delegate 객체 생성

            //익명함수란 원래 객체 생성 시점에 함수(메소드)의 이름은 없이 막바로 함수 본체가

            //주어지면서 객체 생성되는 형태이다.

            //따라서 아래에서 MyDel의 조건인 매개인자로 string 하나, int형 하나를 받고

            //반환 데이터 타입이 string이라는 조건만 만족시켜주면 함수 본체는 

            //내용이 어떠하든지 상관이 없다.

            MyDel anonymMyDel = delegate (string addr, int sex)

            {

                string gender = "";


                if (sex == 1)

                {

                    gender = "남성";

                } else if (sex == 0)

                {

                    gender = "여성";

                }

                return "주소 : " + addr + ", 성별 : " + gender;

            };


            Console.WriteLine(anonymMyDel("서울특별시 강남구 아무개길 77", 1));



            //익명함수 방식의 delegate 객체 생성2

            Del anonymDel = delegate (string msg)

            {

                Console.WriteLine("\n\nDel의 익명함수 방식 객체 : "+msg);

            };


            anonymDel("오 이런 식으로 되는구나\n\n\n");

        }



        private static void DelegateMethod(string msg)

        {

            Console.WriteLine("\n여기는 delegate method\nmessage sent from delegate object : \""+msg+"\"");

        }


        private static string AnotherDelegateMethod(string name, int age)

        {

            return name + ", age : " + age;

        }


        private static void mDel(string info)

        {

            Console.WriteLine("\n\n여기는 " + info + "~~~\n\n");

        }


    }

}




리눅스 Shell command를 이용한 Socket 통신하기


리눅스의 cat 명령어는 쓰임새가 다양하다. 해보진 않았지만 cat을 이용해서 리눅스 시스템을 백업도 가능한듯 하다.

리눅스에 있는 kkk.txt라는 파일의 내용을 Socket을 이용해서 내보낼수 있는가?

netcat(혹은 줄여서 nc)와의 조합을 통해서 가능하다.

아래와 같이


cat kkk.txt | nc 192.168.0.7 8801


여기서 192.168.0.7은 서버 소켓의 IP 주소이고, 8801은 서버 소켓의 포트번호이다.

혹은 간단한 문자열 정도를 보낼려면 이렇게도 가능하다.


echo "This string is from Linux" | netcat 192.168.0.7 8801


통신상태 확인을 위해 간단히 사용할수 있을 것이다.



파이썬(python)에서의 문자열 비교에 대한 간단한 예제 코드이다.


#-*- coding: utf-8 -*-


from operator import eq  # 문자열 비교를 위한 함수 eq 추가


chk = "joe7"


if chk == "joe":

    print "같다."

else:

    print "다르다."


위의 코드는 C style 언어들의 보편적인 비교 법이다. 

그런데 Java에서는 저런식으로 비교가 안된다는 약간이 당황스러움.

말이 나온김에 Java의 문자열 비교를 보자면

if (chk.equals("joe")) { ... }와 같이 해야 한다.


위 코드 실행 결과는 당연히 "다르다"가 출력될 것이다.

이번에는 문자열 비교 함수 eq()를 이용한 방식이다.


아래 코드에서 eq(chk, "joe")는 괄호 안의 2개의 매개인자가 같으면 true, 아니면 false가 된다.

결과는 당연히 "다름"이 출력된다.


if eq(chk, "joe"): 

    print "같음"

else:

    print "다름"


다음으로 문자열 비교에서 매우 유용하게 사용되는 특정 문자를 "포함하고 있는지"에 대한 파이썬에서의 용법이다.

find()라는 메소드를 활용하면 된다. find()메소의 syntax는 다음과 같다.


str.find(s, beg=0, end=len(str))


str이라는 문자열에서 문자열 s가 발견 되어지면 발견되어진 위치 값(index값)을 반환한다.

만일 발견되지 않으면 -1을 반환한다. 이때 문자열의 index는 0부터 시작한다.


beg : 문자열 str에서 s라는 문자열을 찾을 때 시작 Index 값이다. 지정하지 않으면 default로 0가 지정된다.


end : 문자열 str에서 s라는 문자열을 찾을 때 종료될 index 값이다. 지정하지 않으면 문자열 str의 length 값이 지정된다.


예를들어


str = "<EOF>"가 있다면 

index 0 : <

index 1 : E

index 2 : O

index 3 : F

index 4 : >

가 된다.


print str.find("EOF")를 하면 1이 반환된다.

print str.find("<EOF>")를 하면 0이 반환된다.

print str.find("efgh")를 하면 -1이 반환된다.

print str.find("OF")를 하면 2가 반환된다.

print str.find("F")를 하면 3이 반환된다.

print str.find("OF>")를 하면 2가 반환된다. OF>라는 전체 문자열이 str에서 발견되기 때문이다.

print str.find("OF)")를 하면 -1이 반환된다. OF까지는 문자가 포함되어 있지만 OF)라는 문자는 포함되어 있지 않기 때문이다. 


info = "Hello world<EOF>"

print info.find(str)를 하면 11이 반환된다.


print info.find(str, 5)를 하면 str을 찾는 위치를 5위치에서 찾지만 반환은 전체 위치에서의 index값을 반환하므로 11이 반환된다.


print info.find(str, 5, 7)를 하면 str을 info에서 찾을 때 시작 위치를 5에서 시작해서 7위치까지만 찾으므로 str이 발견되지 않는다. 따라서 -1이 반환된다.


아무튼 python에서 java의 contains()와 같은 기능을 find()를 이용해서 할수 있다는 얘기다.

java의 contains()와는 약간 개념이 다르지만 -1이 아니면 해당 문자열을 포함하고 있는 것이 되는 것이다. 예를 들어 data라는 변수에 소켓통신으로부터 데이터를 읽었을 경우 data "EOF"라는 문자열이 포함되어 있다면 소켓통신의 읽기 작업을 종료하는 코드를 작성할려면 다음과 같이 하면된다.


if data.find("EOF") != -1 :

    print "OK, Job completed"

    break;


과 같이하면 될 것이다.




C#의 .dll Library를 C++에서 활용하기


(C# 프로그램을 .dll Library로 만드는 방법은 여기를 클릭)


C#에서 생성한 .dll파일과 .tlb 파일을 C++의 해당 프로젝트 폴더안과 .exe 실행파일이 있는 곳에 복사해 넣는다.

이제 남은 것은 C++ 소스코드 작성법만 알면 C#에서 만든 .dll Library를 C++에서 사용할수 있게 된다.

샘플 예제 코드는 다음과 같다. 각 부분에 대한 설명은 아래 코드상에 있다.

아래 코드는 MFC에서 정보 확인 버튼 클릭시 C# .dll Library의 특정 메소드(함수)를 실행하고 C# .dll이 던져주는 정보(결과)를 보여주는 기능을 구현한 코드 조각이다.


이때 다음과 같이 .tlb 파일을 #include 아랫쪽에 import해 주어야 정상적으로 C# .dll 라이브러리에 있는 메소드들을 MFC에서 사용할수 있다.


#import "ExMakeClassLibSerialRead.tlb" no_namespace named_guids


//MFC에서 C#의 라이브러리 .dll을 이용한다.

void CMFC_UseCsharpDllDlg::OnBnClickedButton1()

{

// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.


//ICallClass는 C#의 interface이름이다.

//public interface ICallClass { .... }과 같이 C#에서 선언되어 있는 부분이다.

ICallClass *csharp = NULL;


//CoInitialize()

//==> Initializes the COM library on the current thread and identifies the concurrency model 

//==> as single-thread apartment (STA).

//COM이란, 컴포넌트 오브젝트 모델(Component Object Model, COM)은 마이크로소프트가 개발한 

//소프트웨어 구성 요소들의 응용 프로그램 이진 인터페이스이다. 

//COM을 이용해 개발된 프로그램들은 프로세스간 통신과 동적 오브젝트 생성이 가능하다. 

CoInitialize(NULL);


//typedef long HRESULT

//CoCreateInstance(CLSID_C#의클래스이름, NULL, CLSCTX_INPROC_SERVER, IID_C#의interface이름, 

//                 reinterpret_cast<void**>(&C#의interface이름type으로선언된참조변수명));

//아래의 CLSID_Class1에서 Class1은 C#에서 사용자가 필요로하는 기능을 담고 있는 클래스인데 

        //C#에서 다음과 같이 정의되어 있다.

//public class Class1 : ICallClass { ... } 자세한 것은 C#의 ExMakeClassLibSerialRead라는 이름의 프로젝트를 참조할 것

//아래에서 IID_ICallClass에서 ICallClass는 C#에서 public interface ICallClass { ... }와 같이 선언된 부분의 interface 이름이다.

HRESULT hr = CoCreateInstance(CLSID_Class1, NULL, CLSCTX_INPROC_SERVER, IID_ICallClass, reinterpret_cast<void**>(&csharp));

//SUCCEEDED macro

//Provides a generic test for success on any status value.

//BOOL SUCCEEDED(HRESULT hr);

//==> hr : The status code.This value can be an HRESULT or an SCODE.A non - negative number indicates success.

//==> Return value : TRUE if hr represents a success status value; otherwise, FALSE.

if (SUCCEEDED(hr))

{

//showInfo() 함수는 C#에서 public void showInfo() { ... }과 같이 정의되어 있는 C#용 메소드이다.

csharp->showInfo();


CString msg = csharp->getInfo();

MessageBox(msg);


}

else {

MessageBox(_T("실패~\n\nC++의 exe 실행파일이 있는 위치에 \nC#에서 생성한 .dll과 .tlb가 있어야 합니다."));

}

}



C#에서 .dll로 library를 만들고 이것을 C++에서 사용하기


(C++에서 C# .dll 라이브러리 사용하는 법)


C#은 MFC에 비해 여러면에서 개발자 편의적이다. 따라서 C#으로 만든 프로그램을 .dll 라이브러리로 만들고 이것을 C++ 혹은 MFC에서 사용하는 방법에 대해서 다루고자 한다.


전개순서는


-. C#을 클래스 라이브러리로 개발하기

-. C#을 .dll library로 만들기 위한 Visual Studio 2017에서 속성 설정하기

-. C# 프로그램이 .dll library로 MFC(혹은 C++)와 동작하기 위한 소스 코드 작성법

-. C# 소스코드 빌드하기

-. 빌드된 C#의 .dll을 레지스트리 등록 및 .tlb 파일 만들기

-. C++ 쪽에서 C#용 .dll 파일 사용하기 위한 소스 코드 작성법


의 순서로 진행이 된다.


1) C#을 클래스 라이브러리로 개발하기


Visual Studio 2017 파일 - 새로 만들기 - 프로젝트 - 클래스 라이브러리(.NET Framework)



이렇게해서 소스코드 창이 열리면 


2) C#을 .dll library로 만들기 위한 Visual Studio 2017에서 속성 설정하기
다음과 같이 해당 프로젝트의 속성을 C# 클래스 라이브러리(.dll) 용으로 설정해 준다.





3) C# 프로그램이 .dll library로 MFC(혹은 C++)와 동작하기 위한 소스 코드 작성법

소스 코드는 크게 두 부분으로 구성이 된다. C++과 통신하기 위한 interface 하나와 필요한 사용자 기능을 위한 class 하나이다. 이 둘에 대한 Guid 값을 지정해 주어야 하는데 Guid 값을 얻기 위해서는 아래 그림과 같이 하면된다.




'복사' 버튼을 클릭하여 Guid 값을 소스코드에(interface와 사용자 클래스에) 붙여넣으면 된다.

소스 코드는 아래와 같다. Guid가 붙여진 곳을 확인해보자.

여기서 또 기억할 사항은 Guid값을 소스코드에서 사용할수 있기 위해서는 using System.Runtime.InteropServices를 추가해 주어야 한다.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Runtime.InteropServices;


namespace ExMakeClassLib

{

    //여기가 C++과 연동하기 위한 interface부분이다.

    [Guid("357CCEDB-44A1-481E-A42A-0D4DCA8C5EEA")]

    public interface ICallClass

    {

        //여기에 C++에서 사용할 메소드들의 프로토타입을 지정해 주면 된다.

        //C++에서 사용하게 될 public 형태의 메소드들이 여기에 해당된다.

        //private 모드의 메소드는 여기에 지정이 불가능하다. 

        //생성자는 여기서 지정해 줄 필요가 없다.

        //메소드들만 지정해 주면 된다.

        void setInfo(String _name, int age, String _phoneNum);

        void showInfo();

    }



    //여기가 사용자가 원하는 기능을 구현한 클래스이다.

    [Guid("D00C0769-26E7-4B4E-A7D0-8CAAE2AB3702")]

    public class Class2 : ICallClass

    {

        private String name;

        private int age;

        private String phoneNum;

        //여기서 주의 해야할 사항은 SerialPort나 Thread 같은경우 처음 사용 후에 

        //포트를 닫는 동작이 필요하거나 Thread를 종료시키는 동작이 필요할 경우 등

        //여러 메소드들에서 사용을 해야하는 멤버 변수들은 반드시 static으로

        //선언해 주어야 한다. 왜냐하면 비록 전역 변수 형태로 선언되었다 할지라도

        //처음 사용했던 메소드가 아닌 다른 메소드에서 또 사용되어질 경우는

        //해당 멤버 변수(Thread, SerialPort...)가 null 상태가 되어 버린다.

        //따라서 C++에서 계속해서 해당 멤버 변수를사용할수 있도록 하기 위해서는

        //반드시 static으로 선언해 주어야 한다.


        public Class2() { }


        public Class2(String _name, int _age, String _phoneNum)

        {

            name = _name;

            age = _age;

            phoneNum = _phoneNum;

        }


        public void showInfo()

        {

            Console.WriteLine("▶ name : " + name + "\n▶ age : " + age + "\n▶ 폰번호 : " + phoneNum);

            Console.WriteLine("---- This is C# Library for C++ from Joe");

        }


        public void setInfo(String _name, int _age, String _phoneNum)

        {

            name = _name;

            age = _age;

            phoneNum = _phoneNum;

        }

    }

}



4) C# 소스코드 빌드하기

Visual Studio 2017의 메뉴에서 '빌드' - '해당프로젝트명 빌드'를 클릭하여 소스 코드를 빌드한다.

(아래 이미지의 경우는 프로젝트 명이 ExMakeClassLibSerialRead라고 가정할 경우이다)



C#의 프로젝트명\bin\Debug\해당프로젝트명.dll 이 생성이 되어 있을 것이다.



5) 빌드된 C#의 .dll을 레지스트리 등록 및 .tlb 파일 만들기

.tlb 파일을 만드는 이유는 C#은 .NET 기반이기 때문에 이를 C++에서 사용가능하도록 하기 위해서이다.


"Developer Command Prompt for VS 2017"창을 관리자 권한으로 열고 C#의 해당 .dll이 있는 경로로 이동한다.

"Developer Command Prompt for VS 2017"창을 열어야 regasm을 정상적으로 이용할수 있다.

여는 방법은 윈도우즈의 시작 - Visual Studio 2017 하위 항목 열기 - Developer Command Prompt for VS 2017 메뉴 위에서 마우스 우측 클릭 - 관리자 권한으로 실행한다.

(만일 DOS 창을 관리자 권한으로 열어서 사용할 경우는 regasm.exe를 C#의 .dll이 있는 폴더로 복사해서 아래 명령을 처리하면된다)


C#용 .dll이 있는 디렉토리로 이동해서 다음 명령을 한다. 

아래에서 ExMakeClassLib는 프로젝트 명이다.


regasm ExMakeClassLib.dll /tlb:ExMakeClassLib.tlb


그러면 다음과 같은 메시지가 보이면 성공한 것이다.


Microsoft .NET Framework 버전 4.7.2556.0용

Microsoft .NET Framework Assembly Registration Utility 버전 4.7.2556.0

Copyright (C) Microsoft Corporation.  All rights reserved.


형식이 등록되었습니다.

어셈블리를 'D:\Joe\CSharp\ExMakeClassLib\ExMakeClassLib\bin\Debug\ExMakeClassLib.tlb'(으)로 내보내고 형식 라이브러리를 등록했습니다.


.dll과 .tlb 생성은 C# 소스 코드를 수정할 때마다 항상 같이 만들어 주어야 한다.

이렇게 만들어진 .dll과 .tlb를 C++의 프로젝트가 있는 디렉토리와 C++의 실행파일이 있는 위치에 복사해 준다.

이렇게 만들어진 C#용 library를 C++(혹은 MFC)에서 어떻게 사용하는지는 아래 링크에서 확인할수 있다.

(C++에서 C# .dll 라이브러리 사용하는 법)



Visual Studio 2017에서 C#으로 프로그래밍 중 "참조" - "참조 추가" 작업시 아래와 같은 에러 발생했을 경우에 대한 해법이다.


"작업을 완료할수 없습니다. 해당 인터페이스를 지원하지 않습니다."

(The operation could not be completed, no such interface supported)



아래 경로로 이동한다.

cd C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\PublicAssemblies


원하는 것은 Microsoft.VisualStudio.Shell.Interop.11.0.dll 파일이 있는 곳으로 이동해야 한다.


다음 명령을 실행한다.


C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\PublicAssemblies>gacutil -i Microsoft.VisualStudio.Shell.Interop.11.0.dll


그러면 다음과 같은 결과가 나타나면 문제가 해결 된 것이다. 


『  Microsoft (R) .NET Global Assembly Cache Utility.  Version 3.5.30729.1

    Copyright (c) Microsoft Corporation.  All rights reserved.


   Assembly successfully added to the cache  』


Visual Studio 2017을 다시 실행한다.


그런데 gacutil.exe가 해당 경로에는 없다. 그런데 gacutil.exe가 여러 군데, 여러 버전이 있는 것 같다. 파일 사이즈가 상이한 동일 이름의 gacutil.exe가 여러 군데 있다. 내 경우는 아래 경로에 있는 gacutil.exe로 문제 해결되었다.


C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\gacutil.exe


자세한 내용은 아래 사이트 참조

https://developercommunity.visualstudio.com/content/problem/75105/cant-add-reference-error-pops-up-no-such-interface.html




Java나 C#을 사용하다가 MFC를 사용하면서 느끼는 짜증나는 불편함이 있다면 문자열 처리에 대한 것이다.

Java나 C#에서의 문자열 처리의 개념으로 MFC에서 시도해보면 당황 스러울 정도로 답답함이 느껴진다.

본 포스트는 MFC에서의 문자열을 사용할수 있는 나름 편리한 도구인 CString을 이용한 코드조각을 소개하고자 한다.


같은 CString끼리는 + 연산으로 쉽게 두 문자열을 합칠수 있으므로 여기서는 CString 타입과 UINT 타입을 합쳐서

하나의 문자열로 만드는 경우를 다뤄보고자 한다.


현재 사용가능한 Serial Port를 출력한다고 가정할 경우 COM이라는 문자열과 1,2,3...과 같은 숫자를 하나의 문자열로 합친다면 다음과 같이 처리하면 된다.


#include <iostream>

using namespace std;


CString str = "COM";

CString port;

//UINT k = 3;

//str += k; //불가능


for (UINT i = 0; i < 7; i++)

{

                //CString과 UINT가 막바로 port = str + i와 같이 합쳐지지 않으므로 

                //UINT를 CString형으로 변환하는 작업

port.Format("%d", i);

port = str + port;


cout << "Available Serial Ports : " << port << endl;

printf("Ports : %s\n", port);

}


그런데 여기서 또 답답한 건 이 소스 코드의 문자 집합이 유니코드 집합을 사용할 경우는 컴파일 단계에서 아예 에러를 뿜는다.


CString str = "COM"; 이 코드에서는 『"const char [4]"에서 "ATL::CStringT<wchar_t, StrTraitMFC_DLL<wchar_t, ATL::ChTraitsCRT<wchar_t>>>"(으)로 변환하기 위한 적절한 생성자가 없습니다라』는 에러를 뿜는다.


port.Format("%d", i); 이 코드에서는 

『인수 목록이 일치하는 오버로딩된 함수 ...의 인스턴스가 없습니다.

              인수 형식이(const char [3], UINT)입니다....』와 같은 에러가 발생한다.


이건 현재 Visual Studio에서 사용중인 문자 집합을 "멀티바이트 문자 집합 사용"으로 변경해 주어야 한다.

문자셋 인코딩 문제는 항상 모든 프로그래밍 언어들에서 신경쓰야 할 부분이지만 이건 정상적인 문법 자체가 잘못됐다고 에러를 내 보내니... 답답한 노릇이다.


문자 집합을 변경하는 방법은

해당 프로젝트 이름에 마우스 우측 클릭 ⇒ 팝업 메뉴에서 "속성" 클릭 ⇒ 해당 프로젝트의 속성 페이지 창에서 ⇒ 구성 속성의 하위 항목 중 "일반" ⇒ 우측 항목들 중 "문자 집합"의 드랍 다운 메뉴에서 "멀티바이트 문자 집합 사용"을 선택 ⇒ 확인




위의 코드를 실행하면 다음과 같을 결과를 내 보이게 될 것이다.


Available Serial Ports : COM0

Ports : COM0

Available Serial Ports : COM1

Ports : COM1

Available Serial Ports : COM2

Ports : COM2

Available Serial Ports : COM3

Ports : COM3

Available Serial Ports : COM4

Ports : COM4

Available Serial Ports : COM5

Ports : COM5

Available Serial Ports : COM6

Ports : COM6


파이썬에서 Linux의 shell command를 사용하는 방법이다.

이를 위해서는 파이썬의 os모듈과 sys모듈을 import해야 한다.

다음은 코드 조각이다.


import os

import sys


os.system('cat qqq.txt > /dev/ttyPS0')


위의 소스코드는 현재 디렉토리에 있는 qqq.txt라는 파일의 내용을 ttyPS0라는 장치(리눅스에서는 장치도 file로 취급)로 전송하는 기능을 수행한다.

여기서 ttyPS0는 현재 디바이스가 가지고 있는 Serial port이다. 만일 현재 디바이스가 임베디드 장비이고 이 장비의 USB UART에 USB 케이블로 PC와 연결되어 있다면 위의 코드를 실행하고 PC쪽에서 Serial 포트의 데이터를 읽는 프로그램을 만들면 임베디드 장비의 qqq.txt의 내용을 PC에서 수신할수 있게 될것이다.


혹은 반대로 하면


os.system('cat /dev/ttyPS0')


이상과 같이 하면 현재의 장비의 Serial port로 들어오는 데이터를 터미널 창에 표시해주는 리눅스의 shell command를 python에서 사용하는 방법이었다. 이 경우는 PC에서 Serial port로 전송하는 데이터를 임베디드 장비에서 수신할수 있는 쉘 명령어를 python에서 사용한 방식이 되겠다.


만일 shell command의 내용을 변수에 저장하고자 한다면 subprocess라는 것을 import해 주어야 한다.


import subprocess


aaa = subprocess.check_output(['cat', 'kkk.txt'])

print aaa


와 같이 하면 kkk.txt의 내용을 변수 aaa에 저장했다가 이것을 출력하는 코드가 되겠다.


혹은 


mylist = subprocess.check_output(['ls', '-l'])

print mylist


이렇게하면 현재 디렉토리에 있는 파일 목록을 변수 mylist에 담고 그것을 출력하는 기능을 수행하는 코드이다.


혹은 보다 간단한 방법은 shell 옵션을 True로 하면 터미널 창에서 입력하는 형태 그대로의 command를 사용할수 있다.


pid = subprocess.check_output('ps -elf | grep CRSViewer.py', shell=True)


이렇게 하면 CRSViewer.py의 실행 process id를 나타내 준다.



+ Recent posts