또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
모든 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의 원형은 다음과 같습니다.
인터페이스 포인터에 대한 포인터입니다. COM 객체가 iid 파라미터로 지정한 인터페이스를 구현하고 있다면, 그에 대한 인터페이스 포인터가 이 매개변수를 통해 반환됩니다.
앞서 언급한 쉘 바로가기 예제를 봅시다. 쉘 바로가기를 만들 수 있는 coclass는 IShellLink와 IPersistFile을 구현하고 있습니다. 여러분이 이미 해당 COM 객체에 대해 IShellLink형의 인터페이스 포인터를 가지고 있다면, 같은 COM 객체에게 IPersistFile형의 인터페이스 포인터도 얻을 수 있습니다. 그 방법은 다음과 같습니다.
그 다음 QueryInterface가 정상적으로 작동하였는지 확인하기 위하여 SUCCEEDED 또는 FAILED 매크로를 사용하여 hr의 값을 검사합니다. 다른 형식의 인터페이스 포인터를 얻는데 성공하였다면, 여러분은 이 새로운 인터페이스 포인터도 사용할 수 있습니다. 단, 다른 인터페이스처럼 사용이 끝났을 때 여러분은 필히 pIPF->Release()를 호출하여야 합니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 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의 원형은 다음과 같습니다.
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();
}
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
보다 구체적으로 살펴봅시다. ‘인터페이스(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의 약어)’라는 용어도 접할 수 있는데, 일반적으로 UUID와 GUID는 같습니다.
‘클래스 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 라이브러리라고만 부르겠습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 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이 언급된 사실을 본 적이 없습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
필자는 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을 사용하여 그러한 문제가 해결되는지에 대해 이해할 수 있도록, 이 절을 읽기를 필자는 권장합니다.
이것은 컴퓨터에 설치된 .NET Framework 어셈블리 중에서 System.Windows.Forms라는 이름의 어셈블리를 찾아서 로드하라는 뜻이다. 자세한 내용은 MSDN을 참고하며, 성공하면 현재의 프로그램에 어셈블리가 로드되어 이를 사용할 수 있고 해당 어셈블리에 대해 System.Reflection.Assembly형 객체를 반환한다. 반환값에 대해서 여기에서는 사용하지 않고 그냥 버린다. 만일 실패할 경우 null이 반환될 것이다.
C# 방식으로는 var openFileDialog = new System.Windows.Forms.OpenFileDialog() 정도로 이해할 수 있다. 파일 열기 대화상자 객체를 새로 만든다.
$openFileDialog.InitialDirectory = [System.String]::Empty
$openFileDialog.Filter = "All Files (*.*)|*.*"
$openFileDialog.ShowDialog()
나머지는 OpenFileDialog의 reference에 따라 구성된 내용이다. 특히 InitialDirectory 프로퍼티는 대화상자가 맨 처음 열릴 때 보여질 폴더의 경로를 지정하는데, 기본값은 빈 문자열이다. 특정 경로를 보일 것이라면, 그 경로명을 지정하면 된다.
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는 != 연산자와 같다. 매개변수로 받은 $disposable가 null이 아니라면 Dispose 메소드를 호출한다.
Using-Disposable($fileStream = New-Object System.IO.FileStream("hello.txt")) {
// Do Something
$fileStream.Flush()
}
이제 실제로 사용해 본다.
첫 번째 매개변수였던 $disposable에는 System.IO.FileStream형 객체가 전달된다. 그리고 블록으로 감싼 코드는 두 번째 매개변수였던 $scriptBlock으로 전달된다.
$fileStream.Flush()까지 실행이 끝나고 블록을 벗어날 때, Using-Disposable의 end 블록에 적었던 Dispose 메소드가 비로소 호출되고 메모리가 정리된다.
하드디스크를 포맷하지 않은 상태에서 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 명령어와 매뉴얼 책자가 전부였을듯.
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처럼 서식(포맷) 적용하여 사용하기)에 자세히 적어 두었다.
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 프로그램은 WinSock 기반의 Windows 프로그램으로 포팅이 가능하다. 본 포스팅에서는 이전 내용(FreeBSD(+*nix) 버전 TCP 서버측 코드)에서 작성한 *nix용 소스 코드를 Windows용 프로그램으로 옮긴 예를 통해 WinSock에서 TCP 서버 코드를 작성하는 방법에 대해 정리한다.
*nix용 소스코드는 프롬프트 상에서 작동되기 때문에 printf 함수에 의한 표준 출력(stdout)으로 프로그램의 작동 과정을 출력하였지만, Windows에서는 프롬프트 상에서 구동되는 프로그램을 작성하는 경우보다는 Windows API 또는 MFC 기반의 창(Window) 형태의 프로그램을 작성하는 경우가 더 많을 것이므로 프로그램의 작동 과정도 표준 출력이 아니라 디버그 출력을 통해 나타낼 것이다. 변수 또는 버퍼의 값을 간편하게 출력하기 위해 OutputFormattedDebugString이라는 사용자 정의 함수를 사용하였다. 이 함수에 대한 선언과 정의는 다른 포스트([Windows API] OutputDebugString을 printf처럼 서식(포맷) 적용하여 사용하기)에 자세히 적어 두었다.
전체적인 구조는 *nix버전과 크게 차이가 없다. socket, bind, listen 순서로 서버 소켓을 구성하고, 무한 루프 내에서 accept가 클라이언트를 기다리다가, 클라이언트 접속이 확인되면 새로운 thread를 생성하고, 새로운 thread에서 클라이언트와의 통신을 수행한다. 그러나 몇 가지 다른 부분을 살펴보면,
WSAStartup, WSACleanup
WSAStartup 및 WSACleanup은 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)를 끌어와야 한다.
그리고 다음과 같은 뼈대를 바탕으로 변용해서 사용하면 된다. 이후의 함수 호출은 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 */
};
이 공용체 안에 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의 listen과 accept 선언
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
WinSock의 listen과 accept 선언
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임을 확인할 수 있었다. 이 번호는 실행 할 때마다 달라질 수 있다.
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이다. printf와 wprintf를 TCHAR형으로 일반화한 명칭이 _tprintf이다.
printf, sprintf는 가변인수를 하드코딩으로 받는 함수이다. 서식과 가변인수가 몇 개가 있든, 동적으로 문자열을 출력하기 위하여 가변인수 벡터로 가변인수를 받는 함수가 vprintf와 vsprintf이다. 각각 표준 출력과 특정한 버퍼에 완성된 문자열을 출력한다.
vprintf의 와이드 문자 버전은 vwprintf이고, vprintf와 vwprintf를 TCHAR형으로 일반화한 명칭이 _vtprintf이다. 마찬가지로 특정 버퍼로 문자열을 출력하는 함수는 vsprintf, vswprintf이고 이를 일반화한 명칭이 _vstprintf이다. 본 함수에서 _vstprintf를 사용한 이유는 이와 같다.
사용 방법은 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;
}
본 포스팅에서는 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);
TCP, IPC와 같이 송신과 수신을 확실히 확인하는 프로토콜에서는 서버 측에서 호출하는 bind와 클라이언트 측에서 호출하는 connect를 사용한다. 두 함수는 모두 접속할 주소가 적혀있는 inaddr_XX 구조체를 필요로 한다.
parameter: sockfd
앞의 socket 함수로 얻은 file descriptor를 전달한다.
parameter: addr
sockaddr_in는 인터넷으로 접속하려는 주소를 구성하고 전달하는 구조체이다. sockaddr_un은 IPC로 접속할 때 그 위치를 구성하고 전달하는 구조체이다. 그 외에도 다른 구조체들이 사용될 수 있으나, 이들 구조체의 기본 형태는 모두 공통적으로 아래의 sockaddr 구조체의 변형이다.
sin_port는 포트 번호이고, sin_addr는 IPv4 주소이며, IPv4는 in_addr이라는 별도의 구조체 내의 32비트 정수로 전달된다. 두 변수의 endian은 모두 Big Endian이다. sin_addr.s_addr의 몇몇 특수한 IP 주소는 다음과 같은 상수로 정의되어 있다.
여기서 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에 기반한 통신을 하고자 할 때 연결 주소를 구조체에 다음과 같이 정형화할 수 있다.
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
listen과 accept는 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이 있다.
모두 클라이언트와 무언가를 주고 받는데 사용되는 함수이다. 여기서 read/write/recv/send는 TCP 및 UDP와 같이 송수신 여부를 확인하는 방식의 프로토콜로 통신할 때 사용하는 함수이고, recvfrom/sendto는 UDP와 같이 송수신여부를 확인하지 않고 일방적으로 전달하는 방식의 프로토콜로 통신할 때 사용하는 함수이다.
특히 read/write는 소켓함수라기 보다는 운영체제의 입출력 함수인데, socket에서 반환한 객체가 파일 디스크립터이기 때문에 이들 함수의 사용이 가능하다. 이 때는 내부적으로 recv, send가 호출되며 flags = 0으로 처리된다.