^(코딩캣)^ = @"코딩"하는 고양이;
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 V..
API/COM
2020. 10. 4. 13:16

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)

API/COM
2020. 10. 4. 13:16

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가?

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
  2. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
  3. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
  4. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
  5. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
  6. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (6)
  7. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (7)
  8. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (8) [完]

 

기본 인터페이스 – IUnknown

모든 COM 인터페이스는 IUnknown으로부터 파생됩니다. 이 인터페이스의 이름은 다소 오해의 소지가 있는데, 사실 이것은 ‘알 수 없는(unknown)’ 인터페이스라는 뜻이 아닙니다. 모든 COM 객체는 IUnknown을 구현하고 있기 때문에 여러분이 어떤 COM 객체에 대해 IUnknown 포인터를 가지고 있다면, 여러분은 그 포인터가 정확히 어떤 형식의 객체를 가리키고 있는지 알 수 없다는 것을 이 이름이 의미하고 있습니다.

IUnknown은 세 가지 메소드들을 갖습니다.

AddRef
COM 객체에게 레퍼런스 카운트를 증가할 것을 알려줍니다. 여러분이 인터페이스 포인터의 복사본을 만들고, 원래의 포인터와 복사된 포인터를 함께 사용하려 할 때 이 메소드를 호출할 수 있습니다. 그러나 본 글에서 우리는 AddRef를 굳이 사용할 필요는 없습니다.
Release
COM 객체에게 레퍼런스 카운트를 감소할 것을 알려줍니다. 앞서 언급한 Release 호출 예제를 참고하시기 바랍니다.
QueryInterface
COM 객체에게 인터페이스 포인터를 요청합니다. 여러분은 coclass가 하나 이상의 인터페이스를 구현하고 있을 때 이 메소드를 사용할 수 있습니다.

우리는 이미 예제 코드에서 Release의 사용을 보았습니다. 그런데 QueryInterface는 무엇일까요? 여러분이 CoCreateInstance를 사용하여 COM 객체를 만들 때, 여러분은 인터페이스 포인터를 얻게 됩니다. COM 객체가 IUnknown을 제외하고 하나 이상의 인터페이스를 구현하고 있을 때, 여러분은 QueryInterface를 사용하여 여러분이 필요로 하는 추가적인 인터페이스 포인터를 얻을 수 있습니다. QueryInterface의 원형은 다음과 같습니다.

HRESULT IUnknown::QueryInterface(
    REFIID iid,
    void ** ppv);

파라미터의 의미는 다음과 같습니다.

iid
여러분이 요청하게 될 인터페이스에 대한 IID입니다.
ppv
인터페이스 포인터에 대한 포인터입니다. COM 객체가 iid 파라미터로 지정한 인터페이스를 구현하고 있다면, 그에 대한 인터페이스 포인터가 이 매개변수를 통해 반환됩니다.
앞서 언급한 쉘 바로가기 예제를 봅시다. 쉘 바로가기를 만들 수 있는 coclass는 IShellLinkIPersistFile을 구현하고 있습니다. 여러분이 이미 해당 COM 객체에 대해 IShellLink형의 인터페이스 포인터를 가지고 있다면, 같은 COM 객체에게 IPersistFile형의 인터페이스 포인터도 얻을 수 있습니다. 그 방법은 다음과 같습니다.
HRESULT hr;
IShellLink * pISL;
IPersistFile * pIPF;

hr = pISL->QueryInterface(IID_IPersistFile, (void **)&pIPF);

그 다음 QueryInterface가 정상적으로 작동하였는지 확인하기 위하여 SUCCEEDED 또는 FAILED 매크로를 사용하여 hr의 값을 검사합니다. 다른 형식의 인터페이스 포인터를 얻는데 성공하였다면, 여러분은 이 새로운 인터페이스 포인터도 사용할 수 있습니다. 단, 다른 인터페이스처럼 사용이 끝났을 때 여러분은 필히 pIPF->Release()를 호출하여야 합니다.

카테고리 “API/COM”
more...
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 V..
API/COM
2020. 10. 4. 13:06

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)

API/COM
2020. 10. 4. 13:06

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가?

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
  2. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
  3. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
  4. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
  5. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
  6. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (6)
  7. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (7)
  8. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (8) [完]

 

COM 객체로 작업하기

모든 언어는 객체를 다루기 위한 고유한 방법을 가지고 있습니다. 예를 들어 C++에서 여러분은 객체를 스택에 정적할당할 수도 있고, new 키워드를 사용해 동적할당할 수도 있습니다. COM은 언어 중립적이기 때문에 COM 라이브러리는 자체적인 객체 관리 기능을 제공하고 있습니다. COM 객체와 C++ 객체는 다음과 같이 비교 가능합니다.

《객체를 새로 만들 때》

- C++에서는 new 연산자로 동적할당 하거나, 스택에 정적할당 합니다.

- COM에서는 COM 라이브러리에 있는 API를 호출합니다.

《객체를 삭제할 때》

- C++에서는 delete 연산자로 할당을 해제하거나, 스택에 정적할당된 객체에 대해서는 스코프(scope) 바깥으로 프로그램의 흐름을 이동합니다.

- COM에서 모든 객체는 자신의 레퍼런스 카운트를 가지고 있습니다. 호출자는 객체에게 언제 호출자가 실행되었는지를 반드시 알려주어야 합니다. COM 객체는 레퍼런스 카운트가 0이 되었을 때 스스로를 할당 해제합니다.

이제, 객체를 생성하고 파괴하는 두 단계 사이에 독자 여러분은 실질적으로 이 COM 객체를 사용할 수 있습니다. 독자 여러분이 COM 객체를 생성할 때, 여러분은 COM 라이브러리에게 어떤 인터페이스가 필요한지를 알려주게 됩니다. 또한 독자 여러분은 보통의 C++ 객체처럼 포인터를 사용하여 메소드들을 호출할 수 있습니다.

 

COM 객체를 생성하기

COM 객체를 생성하고 객체로부터 인터페이스를 얻기 위하여, 독자 여러분은 COM 라이브러리의 API인 CoCreateInstance를 호출하게 됩니다. CoCreateInstance의 원형은 다음과 같습니다.

HRESULT CoCreateInstance(
    REFCLSID rclsid,
    LPUNKNOWN pUnkOuter,
    DWORD dwClsContext,
    REFIID riid,
    LPVOID * ppv);

 

각 파라미터들의 의미는 다음과 같습니다.

rclsid
coclass의 CLSID입니다. 예를 들어 바로가기 아이콘을 만들 수 있는 COM 객체를 생성하기 위하여 여러분은 CLSID_ShellLink를 전달할 수 있습니다.
pUnkOuter
이 파라미터는 COM 객체들을 결합시킬 때 사용합니다. 여기서 결합은 이미 존재하는 coclass에 새로운 메소드들을 덧붙이는 작업을 의미합니다. 객체 결합을 하지 않을 것이므로 우리는 여기에 NULL을 전달할 것입니다.
dwClsContext
우리가 사용하고자 하는 COM 서버의 종류를 지정합니다. 본 글에서 우리는 가장 단순한 형태의 서버인 ‘인 프로세스 DLL(in-process DLL)’만을 사용할 것이기 때문에, 우리는 여기에 CLSCTX_INPROC_SERVER를 전달합니다. 한가지 주의할 것은, 독자 여러분은 ATL에서는 기본값으로 쓰이는 CLSCTX_ALL 속성을 사용해서는 안 된다는 것입니다. 왜냐하면 DCOM이 설치되지 않은 Windows 95 운영체제에서는 실패할 것이기 때문입니다.
riid
독자 여러분이 얻고자 하는 인터페이스의 IID입니다. 예를 들어, IShellLink 인터페이스의 포인터를 얻고자 할 때 독자 여러분은 IID_IShellLink를 전달할 수 있습니다.
ppv
인터페이스 포인터의 주소입니다. COM 라이브러리는 이 파라미터를 통해 요청한 인터페이스를 전달할 것입니다.

독자 여러분이 CoCreateInstance를 호출할 때, 이 함수는 레지스트리에서 CLSID를 탐색할 것입니다. 그리고 서버의 위치를 읽을 것이고 메모리로 서버를 적재하여 여러분이 요청한 coclass에 해당하는 인스턴스를 만들 것입니다.

아래 코드는 호출 예제입니다. CLSID_ShellLink에 해당하는 객체를 생성 및 초기화하고, 그 객체의 IShellLink 인터페이스 포인터를 요청하게 됩니다.

HRESULT hr;
IShellLink * pISL;

hr = CoCreateInstance(CLSID_ShellLink, // coclass의 CLSID
    NULL, // 통합과 관련된 것이므로 사용하지 않음
    CLSCTX_INPROC_SERVER, // 서버의 종류
    IID_IShellLink, // 인터페이스의 IID
    (void **)&pISL); // 우리가 얻고자 하는 인터페이스 포인터에 대한 포인터

if (SUCCEEDED(hr)) {
    // pISH을 사용한 메소드 호출
} else {
    // COM 객체를 생성할 수 없는 경우 hr은 오류 코드를 갖습니다
}

먼저 우리는 CoCreateInstance로부터 반환되는 값을 보관하기 위한 HRESULT를 선언하고, 또한 IShellLink 형식의 포인터도 선언하였습니다.

새로운 COM 객체를 생성하기 위해 CoCreateInstance를 호출합니다. hr이 성공을 나타내는 값을 갖고 있다면 SUCCEEDED 매크로는 TRUE 값을 반환할 것이고, 그렇지 않을 경우 SUCCEEDED 매크로는 FALSE 값을 반환할 것입니다. 코드가 실패했는지 여부를 판단하기 위하여 FAILED 매크로가 또한 제공됩니다.

 

COM 객체를 해제하기

앞서 설명한 바와 같이 독자 여러분은 COM 객체를 직접 해제할 수는 없습니다. 대신 여러분은 이 객체의 사용을 끝냈다고 알려주기만 합니다. 모든 COM 객체가 구현하고 있는 IUnknown 인터페이스는 Release 메소드를 가지고 있습니다. 독자 여러분은 COM 객체에 이 메소드를 호출함으로써 더 이상 사용하지 않음을 알려주면 됩니다. 독자 여러분이 일단 Release를 호출하였다면, COM 객체가 언제든지 메모리에서 사라질 수 있으므로 이후로 절대 인터페이스 포인터를 사용해서는 안됩니다.

만일 여러분의 어플리케이션이 여러가지 다양한 COM 객체를 사용하고 있다면, 인터페이스의 사용을 끝낼 때마다 Release를 호출하는 것이 매우 중요합니다. 여러분이 인터페이스의 사용을 해제하지 않는다면 COM 객체와 이들의 코드를 가지고 있는 DLL들도 메모리에 계속 남게 되어 여러분의 어플리케이션 실행에 불필요하게 붙어있게 됩니다. 여러분의 어플리케이션이 장시간 실행되고 있을 경우 여러분은 유휴시간동안 CoFreeUnusedLibraries를 실행할 것이 권장됩니다. 이 API는 더 이상 참조되지 않는 COM 서버를 메모리 적재 해제하여 여러분의 어플리케이션의 메모리 사용량을 줄여줍니다.

앞의 예제에 이어서 Release를 이렇게 사용하면 됩니다.

// 앞에서 적은 바와 같이 COM 객체를 생성한 후
if (SUCCEEDED(hr)) {
    // pISL을 사용하는 메소드 호출
    // COM 객체에게 우리가 사용을 끝냈음을 알림
    pISL->Release();
}

IUnknown 인터페이스에 대해서는 다음 절에서 충분히 설명하겠습니다.

카테고리 “API/COM”
more...
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 V..
API/COM
2020. 10. 4. 13:03

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)

API/COM
2020. 10. 4. 13:03

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가?

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
  2. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
  3. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
  4. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
  5. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
  6. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (6)
  7. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (7)
  8. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (8) [完]

 

기본 요소들의 정의

보다 구체적으로 살펴봅시다. ‘인터페이스(interface)’는 쉽게 말하면 함수들의 집합입니다. 또한 인터페이스에 속한 함수들을 ‘메소드(method)’라고 부릅니다. 인터페이스 이름은 대문자 ‘I’로 시작하는데, 예를 들어 IShellLink가 있습니다. C++에서 인터페이스는 순수 가상함수들만을 포함하는 추상 클래스로서 작성됩니다.

인터페이스는 다른 인터페이스에서 상속할 수 있습니다. 상속은 C++의 단일상속과 비슷하게 작동합니다. 인터페이스에서 다중상속은 허용되지 않습니다.

‘coclass(component object class의 약어)’는 DLL 또는 EXE 등에 포함되어 있습니다. 또한 하나 이상의 인터페이스에 대한 코드 비하인드(code behind)를 가지고 있습니다. 때문에 ‘coclass’는 이러한 인터페이스를 “구현한다(implement)”고도 부릅니다. ‘COM 객체(COM object)’는 메모리에서 ‘coclass’의 인스턴스입니다. 종종 COM 클래스의 구현체가 C++ 클래스이기는 하지만, COM 클래스가 C++의 클래스와 같지 않음을 숙지하시기 바랍니다.

‘COM 서버(COM server)’는 하나 이상의 ‘coclass’를 포함하고 있는 DLL, ELE 등의 바이너리입니다.

‘등록(registration)’은 Windows 운영체제에서 COM 서버의 위치를 알려주는 레지스트리 진입점을 생성하는 과정입니다.

‘등록 해제(unregistration)’는 그 반대로서, 레지스트리 진입점을 제거합니다.

‘GUID(glocal unique identifier의 약어)’는 128비트 숫자입니다. GUID는 COM이 대상을 식별하기 위한 언어 독립적인 방법입니다. 각각의 인터페이스와 ‘coclass’는 GUID를 갖습니다. 독자 여러분이 GUID를 생성할 때 COM API를 사용하는 한, GUID는 전세계에서 유일하기 때문에 이름 충돌을 회피할 수 있습니다. 또한 독자 여러분은 ‘UUID(universally unique identifier의 약어)’라는 용어도 접할 수 있는데, 일반적으로 UUIDGUID는 같습니다.

‘클래스 ID, 또는 ‘CLSID’는 ‘coclass’를 지명하는 GUID입니다. 인터페이스 ID, 또는 ‘IID’는 인터이스를 지명하는 GUID입니다.

COM에서 GUID를 적극적으로 사용하는 이유는 다음과 같이 두 가지로 정리할 수 있습니다.

1. GUID는 내부적으로 숫자에 불과합니다. 따라서 어떤 프로그래밍 언어라도 이를 다룰 수 있습니다.

2. 누가 어떤 장치로 GUID를 생성하든, 모든 GUID는 적절하게 생성되기만 한다면 유일합니다. 그러므로 COM 개발자들은 서로 같지 않은 유일한 GUID를 얻을 수 있습니다. 이는 GUID를 발급하는 중앙 기관이 필요하지 않게 합니다.

HRESULT’는 COM이 오류 또는 성공 코드를 반환하기 위해 사용되는 통합된 자료형입니다. 자료형 명칭에 ‘H’ 접두어가 있지만, 어떤 대상과 관계된 ‘핸들’이 아닙니다. 이후에 HRESULT와 이를 어떻게 사용하는지에 대해 충분히 설명하겠습니다.

‘COM 라이브러리(COM library)’는 COM 관련 작업을 수행할 때 독자 여러분이 상호작용하는 운영체제의 일부입니다. 종종 COM 라이브러리를 줄여서 COM이라 부르기도 하지만, 여기에서는 혼동을 피하기 위해 COM 라이브러리라고만 부르겠습니다.

카테고리 “API/COM”
more...
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 V..
API/COM
2020. 10. 4. 12:59

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)

API/COM
2020. 10. 4. 12:59

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가?

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
  2. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
  3. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
  4. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
  5. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
  6. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (6)
  7. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (7)
  8. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (8) [完]

 

COM – 이것은 정확히 무엇인가?

COM은, 간단히 말해서, 서로 다른 어플리케이션과 프로그래밍 언어 사이에 바이너리 코드를 공유하는 방법입니다. 이것은 소스 코드의 재사용을 추구하는 C++ 방식의 접근법과는 다릅니다. 소스코드의 재사용을 추구하는 완벽한 예로는 ATL이 있습니다. 다만 ATL은 소스코드 단계에서의 재사용이 원활하다고 해도 C++ 언어에서만 사용할 수 있습니다. 이는 또한 중복되는 이름 때문에 충돌할 가능성이 있고 독자 여러분의 프로젝트에서 코드 중복을 일으켜 프로그램 크기가 커지게 할 수도 있습니다.

Windows는 DLL을 사용하여 바이너리 단계에서 코드를 공유할 수 있도록 하였습니다. 바로 Windows 어플리케이션이 kernel32.dll, user32.dll 등을 재사용하여 구동되는 방식입니다. 그러나 이러한 DLL은 C의 인터페이스로 작성되었기 때문에 C 언어 또는 C 호출 규약을 지원하는 언어들에서만 사용이 가능합니다. 이러한 제약은 DLL 그 자체보다도 그 프로그래밍 언어로 작성하는 구현체에 부담을 가져오게 됩니다.

MFC는 ‘MFC 확장 DLL(MFC Extension DLL)’이라는 다른 방식의 바이너리 공유 메커니즘을 도입하였습니다. 그러나 이것은 더욱 큰 제약사항이 존재하는데, 바로 독자 여러분이 MFC 어플리케이션을 개발할 때에만 사용 가능하다는 것입니다.

COM은 ‘바이너리 스탠더드(binary standard)’를 정의함으로써 이러한 문제를 해결하고 있습니다. 이것은 DLL이나 EXE 등의 바이너리 모듈이 특별한 구조에 맞추어 컴파일되어야 한다는 뜻입니다. 그리고 이 표준은 COM 객체가 메모리에서 어떻게 구성되어야 하는지도 정의합니다. 또한 바이너리들은 특정 프로그래밍 언어의 기능에 의존해서도 안 됩니다(예를 들면 C++의 네임 데코레이션 같은 기능). 일단 위와 같은 조건이 만족하면 해당 모듈은 어느 프로그래밍 언어에서도 쉽게 접근이 가능합니다. 바이너리 스탠더드는 바이너리를 생성할 수 있는 컴파일러에게 호환성을 지킬 것을 요구하는데, 이렇게 하면 나중에 입문하는 프로그래머 또는 바이너리를 사용하고자 하는 프로그래머들의 작업이 훨씬 쉬워집니다.

메모리에서 COM 객체의 구조는 가상함수가 사용된 C++ 객체와 같습니다. 이는 대다수의 COM 코드가 C++로 작성되는 이유이기도 합니다. 그러나 꼭 기억하시기 바랍니다. COM 모듈을 작성하는데 어떤 언어가 사용되는지는 중요하지 않습니다. 왜냐하면 출력되는 바이너리는 모든 언어에서 사용 가능하기 때문입니다.

한편, COM은 Win32 전용이 아닙니다. 이론적으로 COM은 유닉스나 기타 운영체제로 포팅이 가능합니다. 그러나 필자는 Windows 이외의 환경에서 COM이 언급된 사실을 본 적이 없습니다.

 

카테고리 “API/COM”
more...
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 V..
API/COM
2020. 10. 4. 12:54

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)

API/COM
2020. 10. 4. 12:54

COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가?

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM - What It Is and How to Use It.’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (1)
  2. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (2)
  3. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (3)
  4. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (4)
  5. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (5)
  6. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (6)
  7. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (7)
  8. COM의 소개(파트 1) – COM이 무엇이며, 어떻게 사용하는가? (8) [完]

첨부파일:

COMIntro_src.zip
0.01MB

 

이 게시물의 목적

필자는 COM에 이제 막 입문하여 기초를 이해하는 데 도움이 필요한 프로그래머를 위하여 이 튜토리얼을 작성하였습니다. 본 글에서는 COM의 기술적인 사항을 간략하게 다루고, COM 용어들에 대해 설명한 다음, 이미 존재하는 COM 구성요소들을 어떻게 재사용하는지에 대해 설명하겠습니다.

 

도입

COM(Component Object Model)은 가장 유명한 TLA(three-letter acronym, 3글자 약어)로서 오늘날까지 Windows 세계의 어디에나 있는 것처럼 보입니다. 새롭게 출시되는 수 많은 기술들은 COM에 기반하여 존재합니다. 그러한 기술 문서에서는 COM 객체(COM object), 인터페이스(interface), 서버(server) 등의 다양한 용어 설명은 빼고, 독자 여러분이 COM이 어떻게 작동하고 어떻게 사용하는지에 대해 잘 알고 있다고 가정합니다.

본 글에서는 COM을 기초부터 소개하면서, 잠재되어 있는 메커니즘까지 포함하여 설명하겠습니다. 또한 다른 기술(특히 Windows Shell)이 제공하는 COM 객체를 어떻게 사용하는지도 보여드리고자 합니다. 이 글의 마지막에서 여러분은 Windows에 내장되었거나 서드파티가 제공하는 COM 객체를 사용할 수 있게 될 것입니다.

또한 이 글에서는 독자 여러분이 C++을 습득하였을 것이라고 가정합니다. 필자는 본 글에서 약간의 MFC와 ATL 예제 코드를 사용 할 것입니다만, 또한 그 코드들에 대해 상세히 설명해드릴 것입니다. 그러므로 여러분은 MFC와 ATL에 익숙하지 않더라도 이미 숙지한 C++ 언어에 대한 배경지식을 활용하여 필자의 내용에 따라오실 수 있어야 합니다.

이 글에서는 다음과 같은 절(section)이 포함됩니다.

《COM – 이것은 정확히 무엇인가?》

COM 표준을 간략이 소개하고 이것이 만들어지게 된 배경이 되는 문제를 소개할 것입니다. 물론 독자 여러분은 COM을 사용하는데 이러한 내용들을 알아야 될 필요는 없습니다만, 왜 COM을 사용하여 그러한 문제가 해결되는지에 대해 이해할 수 있도록, 이 절을 읽기를 필자는 권장합니다.

《기본 요소의 정의》

COM에서 쓰이는 용어와 그 의미를 소개합니다.

《COM 객체로 작업하기》

COM 객체를 생성하고, 사용하고 파괴하는 방법에 대해 전반적으로 설명합니다.

《기본 인터페이스 – IUnknown》

기본 인터페이스인 IUnknown에 있는 메소드들을 설명합니다.

《주의 깊게 보세요 – 문자열 취급》

COM 코드에서 문자열을 어떻게 다루는지에 대해 설명합니다.

《함께 해 봅시다 – 예제 코드》

본 글에서 다뤄본 개념들을 활용할 수 있는 두 가지의 예제 코드입니다.

《HRESULT 다루기》

HRESULT 타입의 설명과 성공 및 실패 코드를 어떻게 확인하는지에 대해 설명합니다.

《참고자료》

한 번 사서 볼만 한 책들에 대한 소개입니다.

카테고리 “API/COM”
more...
[PowerShell] OpenFileDialog / SaveFileDialog 사용하기
PowerShell 본 시리즈에서는 PowerShell의 구체적인 사용에 대해 정리한다. 이전 게시글: [PowerShell] C#의 using 키워드 구현하기 OpenFileDialog / SaveFileDialog 사용하기 PowerShell에서는 .NET Framework의 WinForm 구성 요소인 System.Windows.Forms.OpenFileDialog과 System.Windows.Forms.SaveFileDialog를 가져와서 연동할 수 있다. OpenFileDialog를 사용하여 파일 열기 대화상자를 띄우는 코드는 다음과 같다. SaveFileDialog도 이와 다르지 않다. [System.Reflection.Assembly]::LoadWithPartialName("System.Win..
Language/PowerShell
2020. 9. 15. 13:37

[PowerShell] OpenFileDialog / SaveFileDialog 사용하기

Language/PowerShell
2020. 9. 15. 13:37

PowerShell


본 시리즈에서는 PowerShell의 구체적인 사용에 대해 정리한다.

이전 게시글: [PowerShell] C#의 using 키워드 구현하기

 

OpenFileDialog / SaveFileDialog 사용하기


PowerShell에서는 .NET Framework의 WinForm 구성 요소인 System.Windows.Forms.OpenFileDialogSystem.Windows.Forms.SaveFileDialog를 가져와서 연동할 수 있다.

OpenFileDialog를 사용하여 파일 열기 대화상자를 띄우는 코드는 다음과 같다. SaveFileDialog도 이와 다르지 않다.

[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [System.String]::Empty
$openFileDialog.Filter = "All Files (*.*)|*.*"
$openFileDialog.ShowDialog()

 

위 코드를 보다 자세히 분해하여 보겠다.

 

[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

이것은 컴퓨터에 설치된 .NET Framework 어셈블리 중에서 System.Windows.Forms라는 이름의 어셈블리를 찾아서 로드하라는 뜻이다. 자세한 내용은 MSDN을 참고하며, 성공하면 현재의 프로그램에 어셈블리가 로드되어 이를 사용할 수 있고 해당 어셈블리에 대해 System.Reflection.Assembly형 객체를 반환한다. 반환값에 대해서 여기에서는 사용하지 않고 그냥 버린다. 만일 실패할 경우 null이 반환될 것이다.

 

$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog

C# 방식으로는 var openFileDialog = new System.Windows.Forms.OpenFileDialog() 정도로 이해할 수 있다. 파일 열기 대화상자 객체를 새로 만든다.

 

$openFileDialog.InitialDirectory = [System.String]::Empty
$openFileDialog.Filter = "All Files (*.*)|*.*"
$openFileDialog.ShowDialog()

나머지는 OpenFileDialog의 reference에 따라 구성된 내용이다. 특히 InitialDirectory 프로퍼티는 대화상자가 맨 처음 열릴 때 보여질 폴더의 경로를 지정하는데, 기본값은 빈 문자열이다. 특정 경로를 보일 것이라면, 그 경로명을 지정하면 된다.

카테고리 “Language/PowerShell”
more...
[PowerShell] C#의 using 키워드 구현하기
PowerShell 본 시리즈에서는 PowerShell의 구체적인 사용에 대해 정리한다. 다음 게시글: OpenFileDialog / SaveFileDialog 사용하기 C#의 using 키워드 구현하기 C#은 System.IDisposable 인터페이스를 구현하는 객체에 대해 using 구문을 사용하여 블록을 벗어날 때 자동으로 관리되지 않는 리소스들을 해제할 수 있다. 예를 들어, using (FileStream fileStream = new FileStream()) { // Do Something } 와 같은 구문을 통해 파일 스트림 작업이 끝나고 블록을 벗어나면 자동으로 fileStream 내부의 리소스들이 해제될 수 있다. .NET Framework의 구성 요소들을 사용할 수 있는 PowerSh..
Language/PowerShell
2020. 9. 15. 12:11

[PowerShell] C#의 using 키워드 구현하기

Language/PowerShell
2020. 9. 15. 12:11

PowerShell


본 시리즈에서는 PowerShell의 구체적인 사용에 대해 정리한다.

다음 게시글: OpenFileDialog / SaveFileDialog 사용하기

 

C#의 using 키워드 구현하기


C#은 System.IDisposable 인터페이스를 구현하는 객체에 대해 using 구문을 사용하여 블록을 벗어날 때 자동으로 관리되지 않는 리소스들을 해제할 수 있다. 예를 들어,

using (FileStream fileStream = new FileStream()) {
    // Do Something
}

와 같은 구문을 통해 파일 스트림 작업이 끝나고 블록을 벗어나면 자동으로 fileStream 내부의 리소스들이 해제될 수 있다.

.NET Framework의 구성 요소들을 사용할 수 있는 PowerShell에는 기본적으로 using statement가 포함되어 있지는 않지만 다음과 같이 간단하게 함수를 하나 선언하여 이를 구현할 수는 있다

 

전체적인 코드는 다음과 같다.

function Using-Disposable {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [AllowNull()]
        [System.IDisposable]
        $disposable,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $scriptBlock
    )
    
    begin {
        
    } process {
        .$scriptBlock
    } end {
        if ($disposable -ne $null) {
            $disposable.Dispose()
        }
    }
}

#usage
Using-Disposable($fileStream = New-Object System.IO.FileStream("hello.txt")) {
    // Do Something
    $fileStream.Flush()
}

 

위 코드를 분해하여 이해해본다.

 

function Using-Disposable {

함수의 이름이다. Using-Disposable이외의 다른 이름을 얼마든지 지정 가능하다.

 

[CmdletBinding()]

함수에 부여할 수 있는 특성(attribute)이다. 이 특성이 부여되면 마치 PowerShell에 기본으로 내장된 명령인 cmdlet처럼 코딩하여 호출 가능하게 된다.

 

param (

이 함수가 받는 매개변수들을 선언한다.

 

[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[AllowEmptyCollection()]
[AllowNull()]
[System.IDisposable]
$disposable,

첫 번째 매개변수는 using statement에 사용될 IDisposable 객체이다. 여기에 부여되는 특성은 이름에서 볼 수 있듯,

생략할 수 없는 필요 매개변수([Parameter(Mandatory = $true)])이고,

"" 또는 [System.String]::Empty 같은 빈 문자열이 전달될 수 있고([AllowEmptyString()]),

포함하고 있는 원소의 개수가 0개인 빈 콜렉션이 전달될 수 있고([AllowEmptyCollection()]),

아예 null 객체가 전달될 수도 있다([AllowNull()]).

다만, 이 매개변수의 데이터타입은 반드시 System.IDisposable이다.

$disposable은 매개변수의 이름이므로 자유롭게 수정 가능하다.

 

[Parameter(Mandatory = $true)]
[scriptblock]
$scriptBlock

Using-Disposable이 블록 { }으로 감싸게 될 많은 실행 구문들이 "매개변수"의 형태로 이 곳에 전달된다. 그러므로 이 매개변수의 데이터 타입은 scriptblock이다. C#의 방식으로는 람다식? 정도로 이해할 수 있다. 스크립트 언어이기에 가능한 기능이다.

이 매개변수는 직관적으로 보아도 당연히 생략될 수 없다([Parameter(Mandatory = $true)]]).

$scriptBlock은 매개변수의 이름이므로 자유롭게 수정 가능하다.

 

PowerShell의 함수는 다음과 같은 구조로 선언할 수 있다.

function 함수이름 {
    param(매개변수, 매개변수, ...)
    
    begin { 준비작업 }
    process { 본문 }
    end { 정리작업 }
    
    반환할 값 또는 객체
}

PowerShell 함수에서는 값을 반환할 때 return과 같은 키워드가 없고 그냥 값 그 자체를 함수 끝에 적어주면 되는 것이 인상적이다. 또한 함수를 작성할 때 준비작업과 본문 및 정리작업을 각각 블록으로 구분하여서 적을 수 있는 것도 인상적인데, C++이나 C#에는 이러한 구문이 없지만 굳이 빗대자면 예외처리를 할 때 try, finally를 사용하는 것 정도로 이해해할 수 있다. 이보다 더 좋은 비유가 있다면 댓글로 남겨주시길...

실행되는 순서는 당연하게도 begin 블록 안의 내용이 실행되고 나서 process 블록 안의 내용이 실행이 될 것이고, 마지막으로 end 블록 안의 내용이 실행될 것이다.

 

begin {
    
} process {
    .$scriptBlock
} end {
    if ($disposable -ne $null) {
        $disposable.Dispose()
    }
}

어쨌든, 이 함수는 사전 작업 할 것이 딱히 없으므로 begin 블록은 비워 두고 process 블록에서 앞서 매개변수로 전달받은 블록인 $scriptBlock을 실행한다.

스크립트 블록의 실행이 끝나면 Dispose()를 호출한다. PowerShell의 조건 연산자는 C#, C++과는 판이하게 다르며 오히려 perl과 가깝다. -ne!= 연산자와 같다. 매개변수로 받은 $disposablenull이 아니라면 Dispose 메소드를 호출한다.

 


Using-Disposable($fileStream = New-Object System.IO.FileStream("hello.txt")) {
    // Do Something
    $fileStream.Flush()
}

이제 실제로 사용해 본다.

첫 번째 매개변수였던 $disposable에는 System.IO.FileStream형 객체가 전달된다. 그리고 블록으로 감싼 코드는 두 번째 매개변수였던 $scriptBlock으로 전달된다.

$fileStream.Flush()까지 실행이 끝나고 블록을 벗어날 때, Using-Disposableend 블록에 적었던 Dispose 메소드가 비로소 호출되고 메모리가 정리된다.

카테고리 “Language/PowerShell”
more...
Debian / Ubuntu에서 방화벽 설정 방법
Debian 계열의 방화벽 다루는 방법 본 명령어는 ubuntu에도 유효하다. 방화벽 서비스 자체의 켜기/끄기 및 기본 조작 방법 방화벽이 켜져 있는지 꺼져 있는지 확인하는 방법은... $ sudo ufw status 방화벽을 켜는 방법은... $ sudo ufw enable 방화벽을 끄는 방법은... $ sudo ufw disable 설정 파일인 /etc/default/ufw을 다시 로드하여 방화벽을 적용하는 방법은... $ sudo ufw reload 방화벽 설정을 초기 설정으로 되돌리는 방법은... $ sudo ufw reset 방화벽의 기본 설정 사항을 확인하는 방법은... $ sudo ufw show raw 기본 설정 허용/차단 기본 설정을 "차단" 상태로 만들려면... $ sudo ufw de..
Operating System/Unix × Debian
2020. 9. 15. 10:15

Debian / Ubuntu에서 방화벽 설정 방법

Operating System/Unix × Debian
2020. 9. 15. 10:15

Debian 계열의 방화벽 다루는 방법

본 명령어는 ubuntu에도 유효하다.

방화벽 서비스 자체의 켜기/끄기 및 기본 조작 방법


방화벽이 켜져 있는지 꺼져 있는지 확인하는 방법은...

$ sudo ufw status

방화벽을 켜는 방법은...

$ sudo ufw enable

방화벽을 끄는 방법은...

$ sudo ufw disable

설정 파일인 /etc/default/ufw을 다시 로드하여 방화벽을 적용하는 방법은...

$ sudo ufw reload

방화벽 설정을 초기 설정으로 되돌리는 방법은...

$ sudo ufw reset

방화벽의 기본 설정 사항을 확인하는 방법은...

$ sudo ufw show raw

 

기본 설정 허용/차단


기본 설정을 "차단" 상태로 만들려면...

$ sudo ufw default deny

밖으로 나가는 패킷에 대해 기본적으로 "차단" 상태로 만들려면...

$ sudo ufw default deny outgoing

밖에서 들어오는 패킷에 대해 기본적으로 "차단" 상태로 만들려면...

$ sudo ufw default deny incoming

기본 설정으로 "허용" 상태로 만들려면...

$ sudo ufw default allow

밖으로 나가는 패킷에 대해 기본적으로 "허용" 상태로 만들려면...

$ sudo ufw default allow outgoing

밖에서 들어오는 패킷에 대해 기본적으로 "허용" 상태로 만들려면...

$ sudo ufw default allow incoming

 

특정 포트의 개방/차단


포트를 여는 방법은...

$ sudo ufw allow <TODO: 포트번호>

특정 프로토콜에 의한 통신에 대해 포트를 여는 방법은...

$ sudo ufw allow <TODO: 포트번호>/<TODO: 프로토콜>

예를 들어 65535번 포트를 TCP 프로토콜로 접속하는 것을 허용하고자 할 때...

$ sudo ufw allow 65535/tcp

 

광범위하게 포트를 여는 방법은...

$ sudo ufw allow <TODO: 시작포트번호>:<TODO: 최종포트번호>

특정 프로토콜에 의한 통신에 대해 광범위하게 포트를 여는 방법은...

$ sudo ufw allow <TODO: 시작포트번호>:<TODO: 최종포트번호>/<TODO: 프로토콜>

예를 들어 60000 ~ 65535번의 포트를 모두 개방하고자 할 때...

$ sudo ufw allow 60000:65535/tcp

 

포트를 닫는 방법은...

$ sudo ufw deny <TODO: 포트번호>

특정 프로토콜에 의한 통신에 대해 포트를 닫는 방법은...

$ sudo ufw deny <TODO: 포트번호>/<TODO: 프로토콜>

예를 들어 65535번 포트를 UDP 프로토콜로 접속하는 것을 차단하고자 할 때...

$ sudo ufw deny 65535/udp

 

광범위하게 포트를 닫는 방법은...

$ sudo ufw deny <TODO: 시작포트번호>:<TODO: 최종포트번호>

특정 프로토콜에 의한 통신에 대해 광범위하게 포트를 닫는 방법은...

$ sudo ufw deny <TODO: 시작포트번호>:<TODO: 최종포트번호>/<TODO: 프로토콜>

예를 들어 60000 ~ 65535번의 포트를 모두 차단하고자 할 때...

$ sudo ufw deny 60000:65535/tcp

 

특정 아이피의 허용/거부


특정 아이피의 접속을 허용하는 방법은...

$ sudo ufw allow from <TODO: 아이피주소>

특정 아이피의 접속을 차단하는 방법은...

$ sudo ufw deny from <TODO: 아이피주소>

 

넓은 범위의 아이피 주소를 차단하는 방법은 다음과 같다.

$ sudo ufw deny from <TODO: 시작아이피주소>/<TODO: 마스크비트수>

예를 들어 192.168.211.0부터 192.168.211.1까지 2개의 아이피를 지정하고자 할 때...

  • 192.168.211.0 = 11000000 10101000 11010011 00000000
  • 192.168.211.1 = 11000000 10101000 11010011 00000001

이므로 이 범위를 추출할 수 있는 마스크는 11111111 11111111 11111111 11111110으로서 1이 31개 있다.

따라서 해당 범위를 식별할 수 있는 아이피 주소 패턴은 192.168.211.0/31이다. 즉 시작아이피주소/마스크비트수이다.

좀 더 살펴본다. 192.168.211.0부터 192.168.211.128까지 129개의 아이피를 지정하고자 할 때...

  • 192.168.211.0 = 11000000 10101000 11010011 00000000
  • 192.168.211.128 = 11000000 10101000 11010011 10000000
  • Mask = 11111111 11111111 11111111 00000000 (1이 24개)

이므로 아이피 주소 패턴은 192.168.211.0/24이다.

이 규칙을 적용하여 192.168.0.0부터 192.168.0.15까지 총 15개의 아이피 주소를 차단하고자 하면...

  • 192.168.0.0 = 11000000 10101000 11010011 00000000
  • 192.168.0.15 = 11000000 10101000 11010011 00001111
  • Mask = 11111111 11111111 11111111 11110000 (1이 28개)
$ sudo ufw deny from 192.168.0.0/28

반대로 허용하고자 할 때는...

$ sudo ufw allow from 192.168.0.0/28

 

아이피, 포트, 프로토콜 동시 지정


접속할 수 있는(또는 접속할 수 없는) 아이피, 포트, 프로토콜을 동시에 지정하는 방법은...

$ sudo ufw allow from <TODO: 아이피주소> port <TODO: 포트번호> proto <TODO: 프로토콜>
$ sudo ufw deny from <TODO: 아이피주소> port <TODO: 포트번호> proto <TODO: 프로토콜>

 

서비스의 허용/거부


특정 서비스를 허용할 때는...

$ sudo ufw allow <TODO: 서비스이름>

특정 서비스를 거부할 때는...

$ sudo ufw deny <TODO: 서비스이름>

사용 가능한 서비스 목록은 /etc/services 파일에 있다. vi, cat, less 등으로 확인할 수 있다. 이 파일을 참고하여 예를 들어 ftp서비스를 거부하고자 할 때는...

$ sudo ufw deny ftp

반대로 허용 할 때는...

$ sudo ufw allow ftp

 

로그 활성화/비활성화


로그를 남기기 위해서는...

$ sudo ufw logging on

로그를 끄기 위해서는...

$ sudo ufw logging off

 

썸네일 이미지
[MS-DOS] MS-DOS 6.2 (한국어 버전)
MS-DOS 6.2 (한국어 버전) 설치 디스크 이미지 아래 5개의 디스크 이미지는 MS-DOS 6.2의 한국어 버전이다. 체크섬(MD5): 2532451EB62D9E98A9B7A70125E02869 체크섬(MD5): 1ADC83589DB7D5C66C4688D7C6A6F591 체크섬(MD5): 0AFA60359477B17059EA5C28E8196028 체크섬(MD5): 936CE93E1311F25A42A4D41EDAC6E0D0 체크섬(MD5): F7D5E74BD21C8B55228635CE4ADC12EC 설치 과정 캡처 제1단계 하드디스크를 포맷하지 않은 상태에서 1번 디스크를 삽입하고 부팅한다. MS-DOS 6.2는 파일 시스템이 FAT16까지만 지원된다. 드라이브 1개당 최대 용량은 2GiB이다. 제2..
Operating System/MS-DOS & Windows 3.x
2020. 9. 6. 21:43

[MS-DOS] MS-DOS 6.2 (한국어 버전)

Operating System/MS-DOS & Windows 3.x
2020. 9. 6. 21:43

MS-DOS 6.2 (한국어 버전)

설치 디스크 이미지

아래 5개의 디스크 이미지는 MS-DOS 6.2의 한국어 버전이다.

체크섬(MD5): 2532451EB62D9E98A9B7A70125E02869
체크섬(MD5): 1ADC83589DB7D5C66C4688D7C6A6F591
체크섬(MD5): 0AFA60359477B17059EA5C28E8196028
체크섬(MD5): 936CE93E1311F25A42A4D41EDAC6E0D0
체크섬(MD5): F7D5E74BD21C8B55228635CE4ADC12EC

 

설치 과정 캡처

제1단계

하드디스크를 포맷하지 않은 상태에서 1번 디스크를 삽입하고 부팅한다. MS-DOS 6.2는 파일 시스템이 FAT16까지만 지원된다. 드라이브 1개당 최대 용량은 2GiB이다.

 

제2단계

마이크로소프트 한글 MS-DOS 6.2 설치

마이크로소프트 한글 MS-DOS 6.2 설치 프로그램입니다.
설치 프로그램은 한글 MS-DOS 6.2를 컴퓨터에 설치합니다.
* 한글 MS-DOS를 설치하려면 Enter 키를 누르십시오.
* 설치 프로그램에 대해서 알아보려면 F1키를 누르십시오.
* 한글 MS-DOS를 설치하지 않고 종료하려면 F3키를 누르십시오.
주의: 백업을 하려면 한글 MS-DOS를 설치하기 전에 하십시오.
파일을 백업하려면 F3키를 눌러 설치를 종료한 다음
백업 프로그램을 사용하여 파일을 백업합니다.
설치 프로그램을 계속하려면 Enter키를 누르십시오.

Enter=계속 F1=도움말 F3=종료 F5=색상 제거 F7=플로피디스크로 설치

설치 첫 화면에서 Enter를 누르면 하드디스크로 설치가 되고, F7을 누르면 플로피디스크로 설치되어 부팅 디스크를 만들 수 있다. 여기서는 Enter를 눌러 하드디스크에 설치한다.

 

제3단계

마이크로소프트 한글 MS-DOS 6.2 설치

설치 프로그램은 한글 MS-DOS가 사용할 할당이 안된 하드디스크 공간을 구성할 수 있습니다. 기존 파일에는
영향을 미치지 않습니다.
설치 프로그램이 공간을 구성하도록 하려면 추천된
옵션을 선택하십시오.
할당이 안된 디스크 공간 구성 (추천함)
설치 프로그램 종료
현재 옵션을 실행하려면 Enter키를 누르십시오.
다른 옵션을 실행하려면 위, 아래화살표키로 원하는 옵션을
선택한 후 Enter키를 누르십시오.
Enter=계속 F1=도움말 F3=종료

포맷되지 않은 하드디스크일 경우 자동으로 C 드라이브에 대해서만 파티션을 잡고 포맷을 수행한다. 하드디스크의 물리적 용량이 얼마나 크든, FAT16에서는 드라이브 하나당 2GiB까지만 지원된다.

 

제4단계

마이크로소프트 한글 MS-DOS 6.2 설치

설치 프로그램이 컴퓨터를 재시동합니다.
디스크 1이 A 드라이브에 있는지 확인하십시오.
* 계속하려면 Enter키를 누르십시오.
Enter=계속

C: 드라이브에 대한 파티션을 설정한 후 재부팅을 한다.

 

제5단계

마이크로소프트 한글 MS-DOS 6.2 설치

하드디스크 드라이브를 포맷 중 입니다.
한글 MS-DOS가 사용할 디스크 공간을 확보하기
위하여 하드디스크 드라이브를 포맷 합니다.
C: 드라이브를 포맷합니다
**%가 포맷되었습니다.

재부팅 후 C: 드라이브를 포맷한다.

 

제6단계

마이크로소프트 한글 MS-DOS 6.2 설치

설치 프로그램은 다음과 같은 시스템 설정을 사용합니다.
날짜/시간: **-**-** **:**
국가: 한국
키보드 형태: 한국

설정이 올바릅니다.

모든 설정이 올바르면 Enter키를 누르십시오.
설정을 변경하려면 위,아래화살표키로 설정을 선택하십시오.
그런 후 Enter키를 누르면 선택사항이 나타납니다.

Enter=계속
F1=도움말
F3=종료

포맷 후 간단한 로케일(locale) 설정을 한다. 현재의 Windows 운영체제에 비하면 MS-DOS의 로케일은 상당히 심플하다.

 

제7단계

마이크로소프트 한글 MS-DOS 6.2 설치

설치 프로그램은 한글 MS-DOS 파일을 다음 디렉토리에 복사합니다.
C:\DOS
한글 MS-DOS 파일을 이 디렉토리에 복사하려면 Enter키를 누르십시오.
다른 디렉토리에 복사하려면 새로운 경로를 입력한 후 Enter키를
누르십시오.

Enter=계속
F1=도움말
F3=종료

MS-DOS가 설치될 디렉토리 경로를 지정한다. 특이 사항이 없으면 C:\DOS를 그대로 둔다.

 

제8단계

마이크로소프트 한글 MS-DOS 6.2 설치

등록 카드를 작성한 다음 마이크로소프트로 보내 주시기 바랍니다.
* 마이크로소프트 제품에 대한 정보를 알려 줍니다.
* 최근의 제품 업그레이드 정보를 알려 줍니다.

계속하여 '다음 디스크'를 넣어주면서 Enter키를 누른다. 간혹 나오는 홍보문구들이 당시의 컴퓨터 환경을 잘 말해주는듯 하다. 무엇보다도 하드 용량과 램 용량 확보에 사활을 걸었던 시대였다.

DBLSPACE는 드라이브를 통째로 하나의 압축파일로 만들어놓고 파일이 편집될 때마다 그 파일을 꺼내서 수정하고 다시 압축파일로 넣는 원리이다. 드라이브의 빈 공간이 다소 늘어나는 효과는 있었지만, 압축을 수시로 풀고 다시 압축하니까 오버헤드가 좀 있었을 것이다. 하드디스크가 잠시 '삐끗'해서 통짜 압축파일이 깨지기라도 하면 저장되어 있던 모든 파일들이...

MEMMAKER는 x86 리얼모드의 태생적 한계인 기본 메모리 문제(응용 프로그램이 아무 거리낌없이 사용할 수 있었던 메모리 범위가 640KB였고 그 마저도 도스가 구동을 유지하기 위해 필수적으로 예약하고 차지하던 범위를 빼면 남는 공간이 거의없던 문제)를 조금이나마 덜기 위한 도구였다. 뭐 지금은 가상 메모리에 선형주소니까 아무 의미 없음.

 

제9단계

한국어 입출력을 위해서는 별도의 드라이버가 필요했다. 드라이버라고 해 봤자, 확장 아스키코드 범위의 문자를 한글로 출력해 줄 수 있는 폰트와, 키보드 입력을 KS 코드로 해석해주는 도구였다. "한글" MS-DOS니까 당연히 한국어 입출력을 위한 드라이버를 내장하고 있기에 'N' 키를 누른다. 당시 또 어떤 부류는 태백한글과 같은 서드파티 한글 드라이버를 쓰기도 했던 모양이다.

MS-DOS 설치가 완료되었다. 구글이 없던 시대에서 MS-DOS를 활용하기 위해서는 help 명령어와 매뉴얼 책자가 전부였을듯.

 

제10단계

MS-DOS 부팅 완료.

카테고리 “Operating System/MS-DOS & Windows 3.x”
more...
썸네일 이미지
[BSD Socket] WinSock 버전 TCP(UDP) 클라이언트측 코드
BSD Socket 사용법 정리 WinSock 버전 TCP 클라이언트측 코드 BSD Socket 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 클라이언트측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP(UDP) 클라이언트 코드를 작성하는 방법에 대해 정리한다. *nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로..
BSD Socket
2020. 6. 7. 08:18

[BSD Socket] WinSock 버전 TCP(UDP) 클라이언트측 코드

BSD Socket
2020. 6. 7. 08:18

BSD Socket 사용법 정리


WinSock 버전 TCP 클라이언트측 코드


BSD Socket 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 클라이언트측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP(UDP) 클라이언트 코드를 작성하는 방법에 대해 정리한다.

*nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로그램을 작성하는 경우가 더 많을 것이므로 프로그램의 작동 과정도 표준 출력이 아니라 디버그 출력을 통해 나타낼 것이다. 변수 또는 버퍼의 값을 간편하게 출력하기 위해 OutputFormattedDebugString이라는 사용자 정의 함수를 사용하였다. 이 함수에 대한 선언과 정의는 다른 포스트([Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기)에 자세히 적어 두었다.

 

포팅된 코드


#pragma comment(lib, "ws2_32.lib")

#include <WinSock2.h>
#include <Windows.h>

#define SCK_PORT 9999
#define SCK_ADDR "192.168.204.169"
#define SCK_BUFF 512
#define SCK_MESG "Hello, TCP Server!"

void OutputFormattedDebugString(LPCTSTR format, ...);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	DWORD dwThreadId;

	WSADATA wsaData;

	SOCKET fdserver;
	SOCKADDR_IN addrserver;
	int cbaddrserver;

	int cbbuffserver, cbbuffclient;
	char buffserver[SCK_BUFF], buffclient[SCK_BUFF];
	int rwresult;

	int sockresult;

	dwThreadId = GetCurrentThreadId();

	ZeroMemory(&wsaData, sizeof(WSADATA));
	sockresult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (sockresult != 0) {
		OutputFormattedDebugString(TEXT("[FAILURE] WSAStartup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSAStartup() @ dwThreadId = %p\n"), dwThreadId);

	fdserver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (fdserver == INVALID_SOCKET) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] socket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSAStartup() @ dwThreadId = %p\n"), dwThreadId);

	cbaddrserver = sizeof(SOCKADDR_IN);
	ZeroMemory(&addrserver, cbaddrserver);
	addrserver.sin_family = AF_INET;
	addrserver.sin_addr.S_un.S_addr = inet_addr(SCK_ADDR);
	addrserver.sin_port = htons(SCK_PORT);

	sockresult = connect(fdserver, (SOCKADDR *)&addrserver, cbaddrserver);
	if (sockresult < 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] connect(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] connect() @ dwThreadId = %p\n"), dwThreadId);
	
	cbbuffserver = sizeof(buffserver);
	ZeroMemory(buffserver, cbbuffserver);

	rwresult = recv(fdserver, buffserver, cbbuffserver, 0);
	cbbuffserver = strlen(buffserver) * sizeof(char);
	if (rwresult < 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] recv(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] recv(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT(">> %hs\n"), buffserver);

	cbbuffclient = sizeof(buffclient);
	ZeroMemory(buffclient, cbbuffclient);

	sprintf(buffclient, "%s @ dwThreadId = %p", SCK_MESG, dwThreadId);
	cbbuffclient = strlen(buffclient) * sizeof(char);
	rwresult = send(fdserver, buffclient, cbbuffclient, 0);
	if (rwresult < 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] send(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] send(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT("<< %hs\n"), buffclient);

EXITPROC_WINMAIN:
	if (fdserver != INVALID_SOCKET) {
		sockresult = closesocket(fdserver);
		if (sockresult != 0) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		}
		OutputFormattedDebugString(TEXT("[SUCCESS] closesocket() @ dwThreadId = %p\n"), dwThreadId);
	}

	sockresult = WSACleanup();
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] WSACleanup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSACleanup() @ dwThreadId = %p\n"), dwThreadId);

	return 0;
}

WSAStartup, WSACleanup, socket, closesocket은 이전 포스트(WinSock 버전 TCP 서버측 코드)와 동일하므로 생략한다. 클라이언트 측에서 사용하는 함수는 connect이다.

 

connect


*nix버전 connect 함수의 선언은 다음과 같다.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sys/types.h, sys/socket.h 순으로 include

WinSock 버전 connect 함수의 선언은 다음과 같다.

int connect(SOCKET s, const struct sockaddr FAR *name, int namelen); // WinSock2.h

이 함수에서도 인터넷 주소를 나타내는 구조체인 struct sockaddr_in(SOCKADDR_IN)이 사용된다. 관련 내용은 이미 이전 포스트 (WinSock 버전 TCP 서버측 코드)에서 소개하였다.

접속에 성공하면 두 함수 모두 0을 반환한다. 그렇지 않은 경우 *nix 버전은 -1을 반환하고 errno에 오류 내용을 설정한다. WinSock 버전은 SOCKET_ERROR를 반환한다. 오류 내용은 WSAGetLastError로 확인할 수 있다.

 

recv, recvfrom, send, sendto


클라이언트와 서버 양측이 데이터를 송수신할 때 사용하는 함수이다. 이미 살펴 보았듯이 TCP에서는 recv, send를 사용하고 UDP에서는 recvfrom, sendto를 사용한다. 4가지 함수의 *nix 선언과 비교하여 WinSock에서 선언된 것과 비교한다.

먼저 *nix에서 4가지 함수 선언은 다음과 같았다.

ssize_t recv(int sockfd, void *buf, size_t len, int flags); // sys/types.h, sys/socket.h 순으로 include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // sys/types.h, sys/socket.h 순으로 include
ssize_t send(int sockfd, const void *buf, size_t len, int flags); // sys/types.h, sys/socket.h 순으로 include
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // sys/types.h, sys/socket.h 순으로 include

또한 *nix에서 sockfd는 파일 디스크립터의 일종이므로 운영체제 입출력 함수인 read, write를 사용하면 flags = 0인 호출과 동일하다고 하였다.

ssize_t read(int fd, void *buf, size_t count); // unistd.h
ssize_t write(int fd, const void *buf, size_t count); // unistd.h

size_t는 unsigned형 정수이지만 ssize_t는 signed형 정수이다. 송수신에 성공하면 바이트 수를 반환하고 그렇지 않으면 -1을 반환하고 errno에 그 내용이 기록된다. 이에 비해 WinSock 버전에서는 read, write 같은 함수는 사용할 수 없고 기본 소켓 함수만 호출 가능하다.

int recv(SOCKET s, char FAR *buf, int len, int flags); // WinSock2.h
int recvfrom(SOCKET s, char FAR *buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen);
int send(SOCKET s, const char FAR *buf, int len, int flags);
int sendto(SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen);

WinSock의 위 함수들은 송수신에 성공하면 바이트 수를 반환하고 그렇지 않으면 SOCKET_ERROR를 반환한다. 오류 내용은 WSAGetLastError로 확인할 수 있다.

 

실행 결과 보기


소스 코드를 실행한 결과는 다음과 같다.

서버와 문자열을 주고받은 TCP 클라이언트측 프로그램

여기서 0xFFF6242D는 클라이언트 측 Thread ID이다. 서버로부터는 0xFFF6A755라는 서버측 Thread ID를 수신하였고 이것으로 WinSock 서버 및 클라이언트 프로그램이 정상적으로 작동함을 확인할 수 있다.

 

카테고리 “BSD Socket”
more...
썸네일 이미지
[BSD Socket] WinSock 버전 TCP 서버측 코드
BSD Socket 사용법 정리 WinSock 버전 TCP 서버측 코드 BSD Socket 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 서버측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP 서버 코드를 작성하는 방법에 대해 정리한다. *nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로그램을 작성하는 경우가 더..
BSD Socket
2020. 6. 5. 23:54

[BSD Socket] WinSock 버전 TCP 서버측 코드

BSD Socket
2020. 6. 5. 23:54

BSD Socket 사용법 정리


WinSock 버전 TCP 서버측 코드


BSD Socket 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 서버측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP 서버 코드를 작성하는 방법에 대해 정리한다.

*nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로그램을 작성하는 경우가 더 많을 것이므로 프로그램의 작동 과정도 표준 출력이 아니라 디버그 출력을 통해 나타낼 것이다. 변수 또는 버퍼의 값을 간편하게 출력하기 위해 OutputFormattedDebugString이라는 사용자 정의 함수를 사용하였다. 이 함수에 대한 선언과 정의는 다른 포스트([Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기)에 자세히 적어 두었다.

 

포팅된 코드

#pragma comment(lib, "ws2_32.lib")

#include <WinSock2.h>
#include <Windows.h>

#define SCK_PORT 9999
#define SCK_BUFF 512
#define SCK_MESG "Hello, TCP Client!"

VOID OutputFormattedDebugString(LPCTSTR format, ...);
DWORD WINAPI ClientThreadProc(LPVOID lpParameter);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	WSADATA wsaData;
	DWORD dwThreadId, dwClientThreadId;
	int cbaddrserver, cbaddrclient;
	SOCKADDR_IN addrserver, addrclient;
	SOCKET fdserver, fdclient;

	int sockresult;

	dwThreadId = GetCurrentThreadId();

	sockresult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (sockresult != 0) {
		OutputFormattedDebugString(TEXT("[FAILURE] WSAStartup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSAStartup() @ dwThreadId = %p\n"), dwThreadId);

	fdserver = fdclient = INVALID_SOCKET;
	fdserver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (fdserver == INVALID_SOCKET) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] socket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] socket() @ dwThreadId = %p\n"), dwThreadId);

	cbaddrserver = sizeof(SOCKADDR_IN);
	ZeroMemory(&addrserver, cbaddrserver);
	addrserver.sin_family = AF_INET;
	addrserver.sin_port = htons(SCK_PORT);
	addrserver.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	sockresult = bind(fdserver, (SOCKADDR *)&addrserver, cbaddrserver);
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] bind(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] bind() @ dwThreadId = %p\n"), dwThreadId);

	sockresult = listen(fdserver, SOMAXCONN);
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] listen(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_WINMAIN;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] listen() @ dwThreadId = %p\n"), dwThreadId);

	while (TRUE) {
		cbaddrclient = sizeof(SOCKADDR_IN);
		ZeroMemory(&addrclient, cbaddrclient);

		fdclient = accept(fdserver, (SOCKADDR *)&addrclient, &cbaddrclient);
		if (fdclient == INVALID_SOCKET) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] accept(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			goto EXITPROC_WINMAIN;
		}
		OutputFormattedDebugString(TEXT("[SUCCESS] accept() @ dwThreadId = %p\n"), dwThreadId);

		dwClientThreadId = 0;
		if (CreateThread(NULL, 0, ClientThreadProc, (LPVOID)((INT_PTR)fdclient), 0, &dwClientThreadId) == NULL) {
			DWORD dwLastError = GetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] CreateThread(): 0x%08x @ dwThreadId = %p\n"), dwLastError, dwThreadId);
		}
		OutputFormattedDebugString(TEXT("[SUCCESS] CreateThread(): dwClientThreadId = %p @ dwThreadId = %p\n"), dwClientThreadId, dwThreadId);
	}

EXITPROC_WINMAIN:
	if (fdserver != INVALID_SOCKET) {
		sockresult = closesocket(fdserver);
		if (sockresult != 0) {
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			ExitThread(sockresult);
		}
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] closesocket() @ dwThreadId = %p\n"), dwThreadId);

	sockresult = WSACleanup();
	if (sockresult != 0) {
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] WSACleanup(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		ExitThread(sockresult);
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] WSACleanup() @ dwThreadId = %p\n"), dwThreadId);

	return 0;
}

DWORD WINAPI ClientThreadProc(LPVOID lpParameter) {
	DWORD dwThreadId;

	int fdclient;
	int sockresult;

	int cbbuffserver, cbbuffclient;
	char buffserver[SCK_BUFF], buffclient[SCK_BUFF];
	int rwresult;

	dwThreadId = GetCurrentThreadId();
	fdclient = (int)((INT_PTR)lpParameter);

	cbbuffserver = sizeof(buffserver);
	ZeroMemory(buffserver, cbbuffserver);
	sprintf(buffserver, "%s @ dwThreadId = %p", SCK_MESG, dwThreadId);

	cbbuffserver = (strlen(buffserver) + 1) * sizeof(char);
	rwresult = send(fdclient, buffserver, cbbuffserver, 0);
	if (rwresult < 0) {
		// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] send(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_THREAD;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] send(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT("<< %hs\n"), buffserver);

	cbbuffclient = sizeof(buffclient);
	ZeroMemory(buffclient, cbbuffclient);
	rwresult = recv(fdclient, buffclient, cbbuffclient, 0);
	if (rwresult < 0) {
		// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
		sockresult = WSAGetLastError();
		OutputFormattedDebugString(TEXT("[FAILURE] recv(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
		goto EXITPROC_THREAD;
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] recv(): %d byte(s) @ dwThreadId = %p\n"), rwresult, dwThreadId);
	OutputFormattedDebugString(TEXT(">> %hs\n"), buffclient);

EXITPROC_THREAD: // Thread가 종료되기 전 실행되어야 할 내용
	if (fdclient != INVALID_SOCKET) {
		sockresult = closesocket(fdclient);
		if (sockresult < 0) {
			// 오류가 발생하면 오류 내용을 출력 후 Thread를 종료시킨다.
			sockresult = WSAGetLastError();
			OutputFormattedDebugString(TEXT("[FAILURE] closesocket(): %d @ dwThreadId = %p\n"), sockresult, dwThreadId);
			ExitThread(sockresult);
		}
	}
	OutputFormattedDebugString(TEXT("[SUCCESS] closesocket() @ dwThreadId = %p\n"), dwThreadId);
	
	return 0;
}

전체적인 구조는 *nix버전과 크게 차이가 없다. socket, bind, listen 순서로 서버 소켓을 구성하고, 무한 루프 내에서 accept가 클라이언트를 기다리다가, 클라이언트 접속이 확인되면 새로운 thread를 생성하고, 새로운 thread에서 클라이언트와의 통신을 수행한다. 그러나 몇 가지 다른 부분을 살펴보면,

 

WSAStartup, WSACleanup


WSAStartupWSACleanup은 WinSock을 사용하기 전에 리셋하고, 사용 후 정리하는 함수이다. 이는 Unix/Linux의 Socket에는 없는 Windows만의 과정이다.

int WSAStartup(
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);
int WSACleanup(void);

 

함수의 실행 결과는 정수로 리턴한다. 성공하면 0을 반환하고 그렇지 않으면 다른 값을 반환한다. 오류의 상세한 종류는 WSAGetLastError의 반환값으로 확인 가능하다. 단, WSAStartup이 실패할 경우 WinSock이 아예 적용되지 않은 상태(WinSock의 오류 코드 변수를 사용할 수 없는 상태)이기 때문에 WSAStartup이 반환하는 값이 곧 상세한 오류 내용이다.

int WSAGetLastError(void);

 

다음은 Windows 프로그램에서 WinSock을 시작하고 종료하는 예이다. 윈도우 운영체제는 Windows 95/NT를 전후로 하여 매우 크게 변하였고, WinSock역시 이 때를 기준으로 WinSock 1과 WinSock 2로 나뉜다. Windows 10을 쓰는 지금까지도 WinSock 2가 이어져 내려온다.

먼저 WinSock을 사용하려면 별도로 라이브러리(ws2_32.lib)와 헤더(ws2_32.h)를 끌어와야 한다.

#pragma comment(lib, "ws2_32.lib")
#include <WinSock2.h>

 

그리고 다음과 같은 뼈대를 바탕으로 변용해서 사용하면 된다. 이후의 함수 호출은 Unix/Linux와 같다.

WORD wVersionRequested;
WSADATA stWSAData;

/* using WinSock 2.2 */
wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &stWSAData) == 0) {
	/* TODO: WinSock */
} else {
	/* TODO: 오류 처리 */
}

WSACleanup();

그리고 DWORD와 같은 자료형 및 각종 운영체제 함수들을 사용하기 위하여 윈도우 헤더를 가져온다.

#include <Windows.h>

 

SOCKET, SOCKADDR_IN


*nix에서 파일 디스크립터의 일종으로서 int형으로 다뤄지던 소켓 객체가 WinSock에서는 의도를 약간 변형시켜 SOCKET이라는 별도의 자료형으로 다뤄진다(어쨌거나 정수인 것은 마찬가지임). 또한 인터넷 주소를 지정하는 struct sockaddr_in 구조체는 SOCKADDR_IN으로 typedef되었다. 몰론 전자를 사용해도 된다.

 

그 외 선언의 차이


그 외 소켓 함수의 선언에서 세세한 차이가 있다.

*nix의 socket 선언

int socket(int domain, int type, int protocol);

WinSock의 socket 선언

SOCKET socket(int af, int type, int protocol);

 

*nix의 bind 선언

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

WinSock의 bind 선언

int bind(SOCKET s, const struct sockaddr FAR *name, int namelen);

이 부분에서 특히 struct sockaddr_in의 멤버 이름에 차이가 있음을 확인할 수 있다. AF_INET 모드로 인터넷 주소를 지정할 때 *nix에서는

struct sockaddr_in {
	sa_family_t    sin_family; /* address family: AF_INET */
	in_port_t      sin_port;   /* port in network byte order */
	struct in_addr sin_addr;   /* internet address */
};

와 같이 선언되었다면 WinSock에서는,

struct sockaddr_in {
	short          sin_family;
	unsigned short sin_port;
	struct in_addr sin_addr;
	char           sin_zero[8];
};

와 같이 sin_zero 멤버가 추가됨을 확인할 수 있다. 그러나 이 멤버는 어차피 사용되지 않고 0으로 리셋할 멤버이므로 무시한다 하더라도, sin_addr으로 명명된 struct in_addr 멤버는 *nix와 WinSock에서 중대한 차이가 있다. *nix와 WinSock 사이에 포팅을 할 때는 이 부분에서 "정의되지 않은 식별자" 오류가 높은 확률로 발생할 것이다.

*nix의 struct in_addr 선언은 다음과 같다.

struct in_addr {
	uint32_t       s_addr;     /* address in network byte order */
};

그러나 WinSock의 struct in_addr 선언은 다음과 같다.

struct in_addr {
	union {
		struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
		struct { u_short s_w1,s_w2; } S_un_w;
		u_long S_addr;
	} S_un;
}

struct in_addr 구조체 안에 S_un 공용체가 있다.

이 공용체 안에 xxx.xxx.xxx.xxx 형태의 4바이트 아이피 주소를 직접 적을 수 있는 S_un_b이 있고, 딱히 비중있는 용도는 없어 보이는 S_un_w가 있고, 또한 *nix의 s_addr과 같은 방식으로 접근하는 S_addr이 있다.

따라서 *nix에서 주소 지정을 위해 코딩했던 (struct sockaddr_in).sin_addr.s_addr의 접근은 WinSock에서 (struct sockaddr_in).sin_addr.S_un.S_addr이 된다.

 

*nix의 listenaccept 선언

int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

WinSock의 listenaccept 선언

int listen(SOCKET s, int backlog);
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);

 

closesocket


*nix에서 socket이 반환하는 정수는 파일 디스크립터의 일종이므로, 운영체제 파일 입출력 함수인 close로 소켓 통신을 통료할 수 있다. 그러나 Windows 운영체제는 파일 디스크립터에 해당하는 개념이 없고, 입출력 대상에 따라 사용하는 함수가 파편화되어 있다. Windows에서는 socket으로 생성한 소켓은 closesocket으로 종료시킨다.

int close(int fd); // unistd.h
int closesocket(SOCKET s); // WinSock2.h

소켓 종료에 성공하면 두 함수 모두 0을 반환한다. 그렇지 않은 경우 *nix 버전은 -1을 반환하고 errno에 오류 내용을 설정한다. WinSock 버전은 SOCKET_ERROR를 반환한다. 오류 내용은 WSAGetLastError로 확인할 수 있다.

 

Thread 작업에서 나타나는 차이


*nix와 Windows 둘 다 다중 스레드를 지원하기는 하지만, 세밀한 부분에서 차이가 있다.

먼저 *nix에서는 thread를 종료할 때 자동으로 실행되는 내용을 기술할 수 있는 pthread_cleanup_push 함수가 있었다. 그러나 Windows API에는 Thread가 종료될 때 특정 작동을 기술할 수 있는 기능이 없으므로, 부득이하게 goto 문을 사용하였다. 이는 온전한 Windows 응용 프로그램에서 event에 의한 실행으로 대체될 수 있다.

 

실행 결과 보기


다음은 소스 코드를 실행한 화면이다.

클라이언트와 문자열 송수신을 끝내고 다른 클라이언트의 연결을 기다리고 있는 TCP 서버 측 프로그램

현재 WinMain을 구동하고 있는 thread의 번호는 0xFFF688ED이다. 무한루프를 돌리면서 클라이언트를 기다리다가, 클라이언트 접속이 확인되면 새 스레드를 생성하여 클라이언트와 통신한다. 이 새로운 스레드의 번호는 0xFFF6A755이며 클라이언트가 접속할 때마다 전혀 다른 번호로 thread가 생성될 것이다. 또한 클라이언트로부터 문자열을 수신하여 클라이언트 측에서 구동하고 있는 thread의 번호가 0xFFF6242D임을 확인할 수 있었다. 이 번호는 실행 할 때마다 달라질 수 있다.

카테고리 “BSD Socket”
more...
썸네일 이미지
[Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기
Windows API 본 시리즈에서는 Windows API 개별 함수에 대해 간략한 사용법 및 응용에 대해 정리한다. 이전 게시글: 크리티컬 섹션을 사용하여 스레드간 변수 공유 OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기 OutputDebugString은 Windows 응용 프로프램이 실행되는 동안 디버그 출력 채널을 통하여 테스트에 필요한 문자열을 출력할 있는 함수이다. MSDN에 따르면 그 원형은 다음과 같이 선언되어 있다. VOID OutputDebugString( LPCTSTR lpOutputString // string to be displayed ); 위와 같이 OutputDebugString은 이미 완성된 문자열만을 출력할 뿐, printf처럼 서식과 가변..
Microsoft Windows/단편 Windows API 예제
2020. 6. 5. 15:03

[Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기

Microsoft Windows/단편 Windows API 예제
2020. 6. 5. 15:03

Windows API


본 시리즈에서는 Windows API 개별 함수에 대해 간략한 사용법 및 응용에 대해 정리한다.

이전 게시글: 크리티컬 섹션을 사용하여 스레드간 변수 공유

 

OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기


OutputDebugString은 Windows 응용 프로프램이 실행되는 동안 디버그 출력 채널을 통하여 테스트에 필요한 문자열을 출력할 있는 함수이다. MSDN에 따르면 그 원형은 다음과 같이 선언되어 있다.

VOID OutputDebugString(
	LPCTSTR lpOutputString // string to be displayed
);

위와 같이 OutputDebugString은 이미 완성된 문자열만을 출력할 뿐, printf처럼 서식과 가변인수를 전달하는 기능이 없다. 때문에 변수의 값을 출력하고자 할 때 별도의 버퍼를 마련해야 하고, 유니코드 또는 MBCS 매크로에 따른 TCHAR 처리 등 매번 출력할 때마다 다소 불편한 면이 있다. 물론 MFC로 가면 TRACE 매크로가 있어서 디버그 문자열 출력 시 변수 출력과 서식 지정이 용이하지만, Windows API만을 사용하여 코딩할경우 이상하게도 마땅한 공식적인 함수가 없다.

기존 함수의 불편을 덜고, 디버그 문자열 출력 시 변수의 값 출력을 용이하게 하기 위하여 새 함수를 다음과 같이 정의할 수 있다.

#define BUFFER_CONST 8192 // 내부 버퍼의 크기 (필요한만큼 조정 가능)

VOID OutputFormattedDebugString(LPCTSTR format, ...) {
	static TCHAR buffer[BUFFER_CONST] = { TEXT('\0'), }; // TCHAR형 문자 버퍼

	va_list arg; // 가변인수 벡터

	va_start(arg, format); // 가변인수 벡터 리셋
	_vstprintf(buffer, format, arg);
	va_end(arg);

	OutputDebugString(buffer);
	ZeroMemory(buffer, sizeof(buffer)); // 사용 후 버퍼 내용 지우기
}

이 함수의 작동은 비교적 간단한 편이다. 임시 버퍼에 서식대로 문자열을 출력하여 완성된 문자열을 얻고, 이것을 다시 OutputDebugString에 전달하는 것이다.

Windows API는 문자형으로 TCHAR를 사용한다. 임시 버퍼로 문자열을 출력할 때 이를 고려하여 함수를 선택할 필요가 있다.

printf는 지정된 서식을 따라 표준 출력에 문자열을 출력하는 함수이다. 이 함수의 와이드 문자 버전은 wprintf이다. printfwprintfTCHAR형으로 일반화한 명칭이 _tprintf이다.

/* stdio.h */
int printf(const char *format [, argument]... );
/* wchar.h */
int wprintf(const wchar_t *format [, argument]... );
/* tchar.h */
#ifdef _UNICODE
#define _tprintf wprintf
#else
#define _tprintf printf
#endif

sprintf는 지정된 서식을 따라 특정한 버퍼에 문자열을 출력하는 함수이다. 이 함수의 와이드 문자 버전은 swprintf이다. sprintfswprintfTCHAR형으로 일반화한 명칭이 _stprintf이다.

/* stdio.h */
int sprintf( char *buffer, const char *format [, argument] ... );
/* wchar.h */
int swprintf( wchar_t *buffer, const wchar_t *format [, argument] ... );
/* tchar.h */
#ifdef _UNICODE 
#define _stprintf swprintf
#else
#define _stprintf sprintf
#endif

printf, sprintf는 가변인수를 하드코딩으로 받는 함수이다. 서식과 가변인수가 몇 개가 있든, 동적으로 문자열을 출력하기 위하여 가변인수 벡터로 가변인수를 받는 함수가 vprintfvsprintf이다. 각각 표준 출력과 특정한 버퍼에 완성된 문자열을 출력한다.

vprintf의 와이드 문자 버전은 vwprintf이고, vprintfvwprintfTCHAR형으로 일반화한 명칭이 _vtprintf이다. 마찬가지로 특정 버퍼로 문자열을 출력하는 함수는 vsprintf, vswprintf이고 이를 일반화한 명칭이 _vstprintf이다. 본 함수에서 _vstprintf를 사용한 이유는 이와 같다.


/* stdio.h */
int vprintf( const char *format, va_list argptr );
int vsprintf( char *buffer, const char *format, va_list argptr ); 
/* wchar.h */
int vwprintf( const wchar_t *format, va_list argptr );
int vswprintf( wchar_t *buffer, const wchar_t *format, va_list argptr );
/* tchar.h */
#ifdef _UNICODE
#define _vtprintf vwprintf
#define _vstprintf vswprintf
#else
#define _vtprintf vprintf
#define _vstprintf vsprintf
#endif

사용 방법은 printf와 다르지 않다. 서식을 첫 번째 인수에 넣고, 다음 인수부터는 0개 이상의 변수들을 전달하면 된다. 다음은 그 예이다.

VOID OutputFormattedDebugString(LPCTSTR format, ...); // 새롭게 정의한 함수 원형

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	OutputFormattedDebugString(TEXT("Hello, World!\n")); // 일반 문자열 출력 테스트
	OutputFormattedDebugString(TEXT("%d + %d = %d\n"), 1, 2, 1 + 2); // 가변인수를 통한 값 출력 테스트
	OutputFormattedDebugString(TEXT("string: \"%s\"\n"), TEXT("DebugString")); // 문자열 서식 테스트
	OutputFormattedDebugString(TEXT("character: '%c'\n"), TEXT('A')); // 문자 서식 테스트
	return 0;
}

소스 코드를 디버그하였을 때 출력 창에 의도대로 출력된 것을 확인할 수 있다.

새롭게 정의한 OutputFormattedDebugString의 작동

[BSD Socket] FreeBSD(+*nix) 버전 소켓함수 정리
BSD Socket 사용법 정리 FreeBSD(+*nix) 버전 클라이언트측 코드 본 포스팅에서는 FreeBSD를 포함하여 *nix를 기준으로 Socket 함수들을 정리한다. 1. socket #include #include int socket(int domain, int type, int protocol); socket 객체(enpoint)를 생성하는 함수이다. 생성에 성공하면 file descriptor를 반환한다. 그러나 오류가 발생하면 -1를 반환하고, 자세한 사유는 errno를 통해 확인할 수 있다. parameter: domain sys/socket.h에 정의된 여러개의 상수들 중 하나를 선택할 수 있다. 이번 예제에서 쓰인 상수는 IPC를 구현할 때 사용된 AF_UNIX 및 TCP/UDP 통..
BSD Socket
2020. 6. 2. 21:51

[BSD Socket] FreeBSD(+*nix) 버전 소켓함수 정리

BSD Socket
2020. 6. 2. 21:51

BSD Socket 사용법 정리


FreeBSD(+*nix) 버전 클라이언트측 코드


본 포스팅에서는 FreeBSD를 포함하여 *nix를 기준으로 Socket 함수들을 정리한다.

 

1. socket


#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

socket 객체(enpoint)를 생성하는 함수이다. 생성에 성공하면 file descriptor를 반환한다. 그러나 오류가 발생하면 -1를 반환하고, 자세한 사유는 errno를 통해 확인할 수 있다.

 

parameter: domain


sys/socket.h에 정의된 여러개의 상수들 중 하나를 선택할 수 있다. 이번 예제에서 쓰인 상수는 IPC를 구현할 때 사용된 AF_UNIX 및 TCP/UDP 통신에 사용된 AF_INET이다. 참고로 AF_LOCAL도 있는데 이는 AF_UNIX와 같은 값이다. IPv6 기반의 인터넷 통신에 대해서는 AF_INET6라는 별도의 상수가 선언되어 있는데 이를 활용한 IPv6 Socket 통신은 추후에 확인한다.

 

parameter: type


이번 예제에서 쓰인 상수는 TCP 및 IPC 통신에 쓰이는 상수인 SOCK_STREAM, UDP 통신에 쓰이는 상수인 SOCK_DGRAM이다. 패킷의 내용을 직접 다룰 수 있는 상수인 SOCK_RAW도 있는데 이는 보안을 위해 운영체제에 따라서 지원을 안 할 수도 있다.

 

parameter: protocol


socket 함수가 반환하는 file descripter도 일종의 파일처럼 취급되는데, 이 파일에 적용될 옵션을 설정할 수 있다. 이번 예제에서는 0을 설정하였다. 즉 특수한 옵션은 부여하지 않았다.

 

정리하면 다음과 같이 정형화시킬 수 있다.

TCP (IPv4): int fd = socket(AF_INET, SOCK_STREAM, 0);

TCP (IPv6): int fd = socket(AF_INET6, SOCK_STREAM, 0);

UDP (IPv4): int fd = socket(AF_INET, SOCK_DGRAM, 0);

UDP (IPv6): int fd = socket(AF_INET6, SOCK_DGRAM, 0);

IPC: int fd = socket(AF_UNIX, SOCK_STREAM, 0);

 

2. bind와 connect


#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

TCP, IPC와 같이 송신과 수신을 확실히 확인하는 프로토콜에서는 서버 측에서 호출하는 bind와 클라이언트 측에서 호출하는 connect를 사용한다. 두 함수는 모두 접속할 주소가 적혀있는 inaddr_XX 구조체를 필요로 한다.

 

parameter: sockfd


앞의 socket 함수로 얻은 file descriptor를 전달한다.

 

parameter: addr


sockaddr_in는 인터넷으로 접속하려는 주소를 구성하고 전달하는 구조체이다. sockaddr_un은 IPC로 접속할 때 그 위치를 구성하고 전달하는 구조체이다. 그 외에도 다른 구조체들이 사용될 수 있으나, 이들 구조체의 기본 형태는 모두 공통적으로 아래의 sockaddr 구조체의 변형이다.

struct sockaddr {
	sa_family_t sa_family;
	char sa_data[14];
}

sa_family는 주소의 종류를 결정한다. AF_INET, AF_INET6, AF_UNIX, AF_LOCAL 등을 지정할 수 있다. 상수의 종류에 따라 sa_addr[14]에 들어갈 주소의 구조는 달라진다.

TCP, UDP 등의 인터넷(IPv4) 기반 통신은 다음과 같이 sin_portsin_addr로 세분화된다.

struct sockaddr_in {
	sa_family_t sin_family;
	in_port_t sin_port;
	struct in_addr sin_addr;
};

struct in_addr {
	uint32_t s_addr;
};

sin_port는 포트 번호이고, sin_addr는 IPv4 주소이며, IPv4는 in_addr이라는 별도의 구조체 내의 32비트 정수로 전달된다. 두 변수의 endian은 모두 Big Endian이다. sin_addr.s_addr의 몇몇 특수한 IP 주소는 다음과 같은 상수로 정의되어 있다.

INADDR_LOOPBACK
127.0.0.1이다.
INADDR_ANY
0.0.0.0이며 아무 단말기로부터 연결을 수용할 때 사용된다.
INADDR_BROADCAST
255.255.255.255이며 모든 단말기로 브로드캐스트할 때 사용된다.

IPC 기반의 통신은 다음과 같이 sun_path로 분화된다.

struct sockaddr_un {
	sa_family_t sun_family;
	char sun_path[108];
};

여기서 sun_path의 길이는 POSIX 표준에서는 정의되지 않았다. 꼭 108문자가 아니어도 되고, 구현에 따라 다른 크기로 선언될 수도 있다. 문자열의 실제 길이가 얼마가 되었든, 항상 NULL 문자로 끝나야 하며 런타임 시 동적으로 문자열 길이를 측정한다.

 

parameter: addrlen


addr로 지정한 구조체의 길이를 전달한다.

 

예를 들어, TCP, UDP에 기반한 통신을 하고자 할 때 IP 주소와 포트 번호를 구조체에 다음과 같이 정형화할 수 있다. IPv4 주소를 Big Endian 정수로 변환할 때 inet_pton 함수를 사용하고, 포트 번호를 나타내는 시스템 정수를 Big Endian 정수로 변환할 때는 htons를 사용한다. 구조체의 내용을 전부 0x00으로 리셋하기 위하여 bzero 함수를 사용한다.

struct sockaddr_in addr_server;

bzero(&addr_server, sizeof(struct sockaddr_in)); // #include <strings.h>
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(포트번호); // #include <arpa/inet.h>
switch (inet_pton(AF_INET, "000.000.000.000", &addr_server.sin_addr)) { // #include <arpa/inet.h>
	case 1: 
		// 정상인 경우
		break;
	case 0:
		// IP 주소가 잘못된 경우
		break;
	case -1:
		// AF_ 상수가 잘못 지정된 경우
		break;
}

IPC에 기반한 통신을 하고자 할 때 연결 주소를 구조체에 다음과 같이 정형화할 수 있다.

struct sockaddr_un addr_server;

bzero(&addr_server, sizeof(struct sockaddr_un)); // #include <strings.h>
addr_server.sun_family = AF_UNIX;
strcpy(addr_server.sun_path, "주소");

 

3. listen과 accept


#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

listenaccept는 TCP와 IPC 서버 프로그램과 같이 상대방의 연결 여부를 확인하는 프로토콜로 소켓을 사용할 때 서버가 클라이언트를 기다렸다가 연결을 허용하는 함수이다. listen은 연결 대기열에 클라이언트가 추가되기를 기다리는 함수이고, accept는 연결 대기열에서 클라이언트를 하나 꺼내와서 통신을 시작하는 함수이다. 사용 예는 다음과 같이 정형화할 수 있다.

struct sockaddr addr_client;
socklen_t addr_client_len;

if (listen(fd, SOMAXCONN) < 0) {
	// 오류 처리
}

while (1) {
	if (accept(fd, &addr_client, &addr_client_len) < 0) {
		// 오류 처리
	}
	// 클라이언트와 통신할 내용
}

 

parameter: sockfd


socket 함수로 얻은 파일 디스크립터이다.

 

parameter: backlog


클라이언트 대기열의 최대 크기이다. 시스템에서 기본으로 제공되는 상수로는 SOMAXCONN이 있다.

 

parameter: addr, addrlen

클라이언트 측 주소를 담게 될 구조체 및 그 구조체의 길이이다.

 

3. read, write와 recv, send 및 recvfrom, sendto


#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

모두 클라이언트와 무언가를 주고 받는데 사용되는 함수이다. 여기서 read/write/recv/send는 TCP 및 UDP와 같이 송수신 여부를 확인하는 방식의 프로토콜로 통신할 때 사용하는 함수이고, recvfrom/sendto는 UDP와 같이 송수신여부를 확인하지 않고 일방적으로 전달하는 방식의 프로토콜로 통신할 때 사용하는 함수이다.

특히 read/write는 소켓함수라기 보다는 운영체제의 입출력 함수인데, socket에서 반환한 객체가 파일 디스크립터이기 때문에 이들 함수의 사용이 가능하다. 이 때는 내부적으로 recv, send가 호출되며 flags = 0으로 처리된다.

카테고리 “BSD Socket”
more...
썸네일 이미지
[Xcode 오류 해결] Unable to log in with account '<Apple ID>'.
상황 Xcode로 소스 코드를 실행하는데 다음과 같은 오류가 나타난다. No signing certificate "Mac Developer" found. No "Mac Development" signing certificate matching team ID "XXXXXXXXXX" with a private key was found. The operation couldn't be completed. Unable to log in with account ''. The login details for account '' were rejected. Xcode Apple ID 로그인 오류 이런 오류가 뜨는 이유에는 여러가지가 있겠으나 이번 경우에는 Apple ID 로그인에 실패한 경우였다. 최근에 Apple ID..
API/Cocoa
2020. 4. 8. 16:17

[Xcode 오류 해결] Unable to log in with account '<Apple ID>'.

API/Cocoa
2020. 4. 8. 16:17

상황


Xcode로 소스 코드를 실행하는데 다음과 같은 오류가 나타난다.

No signing certificate "Mac Developer" found. No "Mac Development" signing certificate matching team ID "XXXXXXXXXX" with a private key was found.
The operation couldn't be completed. Unable to log in with account '<Apple ID>'. The login details for account '<Apple ID>' were rejected.
Xcode Apple ID 로그인 오류

이런 오류가 뜨는 이유에는 여러가지가 있겠으나 이번 경우에는 Apple ID 로그인에 실패한 경우였다. 최근에 Apple ID의 패스워드를 변경한 적이 있었는데, Xcode는 이전 암호로 로그인을 시도했기 때문에 Code Signing이 되지 않은 것이었다.

 

해결


[Xcode] 메뉴 옆 [Preferences...]를 클릭하여 환경 설정을 연다.

[Xcode] -> Preferences...

[Accounts]를 클릭하면 해당 Apple ID가 세션 만료된 것을 확인할 수 있다. 새로운 암호로 로그인하면 해결 완료.

암호가 바뀌어 세션 연장이 안 된 상태
카테고리 “API/Cocoa”
more...
썸네일 이미지
[BSD Socket] FreeBSD(+*nix) 버전 IPC 클라이언트측 코드
BSD Socket 사용법 정리 FreeBSD(+*nix) 버전 클라이언트측 코드 본 포스팅에서는 FreeBSD를 포함하여 *nix를 기준으로 한 IPC Socket 클라이언트측 코드의 사용 예를 정리한다. 전체 코드 먼저 전체적인 코드를 훑어본다. /* 표준 헤더 include */ #include // intptr_t #include // exit() #include // printf(), sprintf() #include // assert() #include // strerror() #include // errno /* 유닉스 헤더 include */ #include // pthread_create(), pthread_exit(), pthread_cleanup_push(), pthread_clean..
BSD Socket
2020. 4. 8. 16:05

[BSD Socket] FreeBSD(+*nix) 버전 IPC 클라이언트측 코드

BSD Socket
2020. 4. 8. 16:05

BSD Socket 사용법 정리


FreeBSD(+*nix) 버전 클라이언트측 코드


본 포스팅에서는 FreeBSD를 포함하여 *nix를 기준으로 한 IPC Socket 클라이언트측 코드의 사용 예를 정리한다.

 

전체 코드


먼저 전체적인 코드를 훑어본다.

/* 표준 헤더 include */
#include <stdint.h> // intptr_t
#include <stdlib.h> // exit()
#include <stdio.h> // printf(), sprintf()
#include <assert.h> // assert()
#include <string.h> // strerror()
#include <errno.h> // errno

/* 유닉스 헤더 include */
#include <pthread.h> // pthread_create(), pthread_exit(), pthread_cleanup_push(), pthread_cleanup_pop()
#include <strings.h> // bzero()
#include <unistd.h> // ssize_t, read(), write()

/* 소켓 헤더 include */
#include <arpa/inet.h> // htons()
#include <sys/types.h> // socket
#include <sys/socket.h> // socket
#include <sys/un.h> // struct sockaddr_un

#define SCK_FILE "/tmp/codingcat.ipc"
#define SCK_BUFF 512
#define SCK_MESG "Hello, IPC Server!"

void cleanup_routine(void * pfdclient); // 소켓 함수 실행 중 오류가 발생할 때 수행할 공통의 해제 작업이 기술된 함수이다.

int main(void) {
	pthread_t thself; // 현재 실행중인 스레드 번호를 보관한다.

	int fdserver; // 서버에 연결하는 소켓에 대한 파일 디스크럽터.
	socklen_t cbaddrserver; // 서버측 인터넷 주소의 길이를 보관한다.
	struct sockaddr_in addrserver; // 서버측 인터넷 주소를 보관한다.
	int sockresult; // 소켓 함수의 실행 결과를 보관한다.

	size_t cbbuffserver, cbbuffclient; // 서버에서 클라이언트로, 클라이언트에서 서버로 송수신하는 문자열의 길이를 보관한다.
	char buffserver[SCK_BUFF], buffclient[SCK_BUFF]; // 서버에서 클라이언트로, 클라이언트에서 서버로 송수신하는 문자열을 보관한다.
	ssize_t rwresult; // 서버와 클라이언트간 송수신된 문자열의 길이를 보관한다.

	thself = pthread_self(); // 현재 실행중인 스레드 번호를 가져온다.
	pthread_cleanup_push(cleanup_routine, &fdserver);
	
	fdserver = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fdserver < 0) {
		printf("[FAILURE] socket(): %d(%s) @ pthread_t = %p\n", errno, strerror(errno), thself);
		pthread_exit((void *)((intptr_t)errno));
	}
	printf("[SUCCESS] socket() @ pthread_t = %p\n", thself);

	cbaddrserver = sizeof(struct sockaddr_un);
    bzero(&addrserver, cbaddrserver);
    addrserver.sun_family = AF_UNIX;
	strncpy(addrserver.sun_path, SCK_FILE, (sizeof(addrserver.sun_path) / sizeof(addrserver.sun_path[1])));

	sockresult = connect(fdserver, (struct sockaddr *)&addrserver, cbaddrserver);
	if (sockresult < 0) {
		printf("[FAILURE] connect(): %d(%s) @ pthread_t = %p\n", errno, strerror(errno), thself);
		pthread_exit((void *)((intptr_t)errno));
	}
	printf("[SUCCESS] connect() @ pthread_t = %p\n", thself);

	cbbuffserver = sizeof(buffserver);
	bzero(buffserver, cbbuffserver);

	rwresult = read(fdserver, buffserver, cbbuffserver);
	cbbuffserver = strlen(buffserver) * sizeof(char);
	if (rwresult < 0) {
		printf("[FAILURE] read(): %d(%s) @ pthread_t = %p\n", errno, strerror(errno), thself);
		pthread_exit((void *)((intptr_t)errno));
	}
	printf("[SUCCESS] read(): %zd byte(s) @ pthread_t = %p\n", rwresult, thself);
	printf(">> %s\n", buffserver);

	cbbuffclient = sizeof(buffclient);
	bzero(buffclient, cbbuffclient);
	sprintf(buffclient, "%s @ pthread_t = %p", SCK_MESG, thself);

	cbbuffclient = strlen(buffclient) * sizeof(char);
	rwresult = write(fdserver, buffclient, cbbuffclient);
	if (rwresult < 0) {
		printf("[FAILURE] write(): %d(%s) @ pthread_t = %p\n", errno, strerror(errno), thself);
		pthread_exit((void *)((intptr_t)errno));
	}
	printf("[SUCCESS] write(): %zd byte(s) @ pthread_t = %p\n", rwresult, thself);
	printf("<< %s\n", buffclient);
	
	pthread_exit((void *)((intptr_t)0));
	pthread_cleanup_pop(0);
	
	return 0;
}

void cleanup_routine(void * pfdsocket) {
	pthread_t thself;

	int fdsocket;
	int sockresult;

	fdsocket = *((int *)pfdsocket);
	thself = pthread_self();

	if (fdsocket) {
		sockresult = close(fdsocket);
		if (sockresult < 0) {
			printf("[FAILURE] close(): %d(%s) @ pthread_t = %p\n", errno, strerror(errno), thself);
		}
		printf("[SUCCESS] close() @ pthread_t = %p\n", thself);
	}

	return;
}

실행 결과 보기


위 소스 코드를 FreeBSD에서는 다음과 같이 컴파일한 후 실행할 수 있다.

$ cc 소스파일.c -o출력파일 -lpthread

pthread 함수를 사용하기 위한 옵션으로 특별히 -lpthread가 추가되었다. 이 옵션이 없다면, pthread 함수를 호출하는 부분에서 '정의되지 않은 식별자' 오류를 낼 것이다.

서버 접속에 성공하면 다음과 같이 서버로부터 메시지를 수신한다. 여기서 0x800681500은 이 클라이언트와 통신하고 있는 서버 측 thread ID이다.

서버와 문자열 교환에 성공하였을 때 서버측에서 보낸 메시지가 출력된다.

카테고리 “BSD Socket”
more...

“분류 전체보기” (134건)