본문 바로가기
전공 수업/컴퓨터 통신(Computer Communication)

[3주 차] - Windows 소켓, 간단한 서버 소켓 프로그램 작성

by TwoJun 2022. 9. 12.

    과목명 : 컴퓨터 통신(Computer communication)

수업일자 : 2022년 09월 12일 (월)

 

 

 

 

 

1. Windows socket (윈속)

1-1. Windows socket의 뜻

- 버클리 유닉스에서 개발한 네트워크 프로그래밍 인터페이스를 윈도우 환경에서 사용할 수 있게 만든 것입니다.

- Windows 95  버전부터 API에 정식적으로 포함하여 제공하게 되었습니다. 

 

 

1-2. Windows socket과 유닉스 socket의 차이점

- 윈도우 소켓은 DLL을 통해 대부분의 기능이 제공되므로 DLL 초기화 종료 작업을 위한 함수가 필요합니다.

 

- 윈도우 라이브러리는 일종의 함수들의 모임으로, Static(정적) 라이브러리, Dynamic(동적, Shared) 라이브러리로 나뉩니다.

 

예를 들어 간단한 C 프로그램 코드를 작성 후 빌드를 통해 실행파일을 만드는 과정 중 컴파일(Compile) > 링크(Link) 과정을 거치며 빌드가 진행됩니다. 이에 따라 링크 과정에선 일반적으로 Dynamic Library를 링크를 하게 됩니다. 

 

- 윈도우 프로그램은 대부분 GUI 방식을 갖추고 메시지 구동 방식으로 동작하므로 이를 위한 확장함수가 존재합니다.

 

- 윈도우는 운영체제 차원에서 멀티스레드를 지원하므로 멀티스레드 환경에서 안정적으로 동작하는 구조와 이를 위한 함수가 필요합니다. 

 

 

 

1-3 Windows socket의 장점

- 유닉스 소켓과 소스 코드 수준에서 호환성이 높으므로 기존 코드를 이식하여 활용하기 쉽습니다.

 

- 가장 널리 사용하는 네트워크 프로그래밍 인터페이스이므로 한 번 배우면 여러 운영체제(윈도우, 리눅스 등)에서 사용 가능합니다.

 

- TCP/IP 외의 포로토콜도 지원하므로 최소 코드 수정으로 응용 프로그램이 사용할 프로토콜 변경이 가능합니다

 

- 비교적 저수준 프로그래밍(Low-level programming) 인터페이스이므로 세부 제어가 가능하며 고성능 네트워크 프로그램 개발이 가능합니다.

 

 

 

 

1-4. Windows socket의 단점

- 응용 프로그램 수준의 프로토콜을 개발자가 직접 설계해야 합니다.

주고받는 데이터의 형식이나 전송 절차 등을 고려해 개발자가 설계해야 하며, 설계 변경 시에는 코드 수정이 불가피하게 됩니다.

 

- 서로 다른 바이트 정렬 방식을 사용하거나 데이터 처리 단위가 서로 다른 호스트끼리 통신하는 경우, 응용 프로그램 수준에서 데이터 변환을 처리해야 합니다. 

 

 

 

1-5. Windows socket(윈속)의 구조

 

Windows socket의 구조

 

 

* 시스템 호출(System call)

- 운영체제에서 시스템 호출, 즉 시스템 콜(System call)이란 간단히 설명하면 운영체제의 커널이 제공하는 서비스에 대해 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스를 의미합니다. 시스템 호출에 대해서는 운영체제에 관한 내용을 다룰 때 자세히 포스팅하도록 하겠습니다.

 

시스템 호출(System call)을 설명하기 위한 대략적인 이미지

 

 

 

 

 

2. Visual C++ 2010 express를 사용하여 간단한 소켓 프로그램 작성하기

- 간단한 소켓 프로그램을 작성하기 위해 Microsoft Visual C++ 2010 Express를 설치합니다.

 

 

2-1. 콘솔에 Hello World!를 출력하는 프로그램 작성하기

// HelloWorld.cpp : Defines the entry point for the console application.
//
// Standard Application Framework(stdafx)
#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
	// Hello world!를 출력하는 printf() 함수
	printf("Hello world!\n");

	return 0;
}

 

2-2. 메세지 박스를 출력하는 프로그램 작성하기

// HelloMessageBox.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <Windows.h>


int _tmain(int argc, _TCHAR* argv[])
{
	// Win32 API 
	// 해당 MessageBox()의 prototype?? -> header file을 선언해 줘야 한다.
	// 유니코드 문자열의 경우 L을 붙여준다.
	MessageBox(NULL, L"Hello World...", L"Hello Title", MB_OK);  // MessageBox() 함수 호출
	return 0;
}

- 간단히 메세지 박스를 출력할 수 있는 MessageBox() 함수를 사용하기 위해선 헤더 파일(컴파일러에 의해 다른 소스코드 파일에 자동으로 포함된 또 다른 소스파일 코드이다.) <Windows.h> 을 선언해 주어야 합니다.

 

- 유니코드 문자열을 출력하기 위해 문자열 옆에 L을 붙여줍니다.

다만 이 방식이 조금 번거로울 수 있기 때문에 #pragma comment(lib, "ws2_32") 해당 코드를 소스 코드 내부에 넣어 별도의 설정없이 문자열에 접두어를 붙이지 않고 자유롭게 출력할 수 있도록 합니다.

 

 

 

 

 

3. 통신 프로그램의 시나리오 작성하기

 

 

현재 위의 그림처럼 시나리오를 작성한 상황이며 서버 사이드를 열고 클라이언트가 Telnet을 통해 접속할 수 있도록 네트워크 프로그램을 아래와 같이 구현하였습니다.

// WelcomeServer.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

#define SERVERPORT 9000
#define BUFSIZE    512

// 소켓 함수 오류 출력 후 종료
void err_quit(char *msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}

// 소켓 함수 오류 출력
void err_display(char *msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	printf("[%s] %s", msg, (char *)lpMsgBuf);
	LocalFree(lpMsgBuf);
}

// TCP 서버(IPv4)
DWORD WINAPI TCPServer4(LPVOID arg)
{
	int retval;

	// socket()
	SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if(listen_sock == INVALID_SOCKET) err_quit("socket()");

	// bind()
	SOCKADDR_IN serveraddr;
	ZeroMemory(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = bind(listen_sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
	if(retval == SOCKET_ERROR) err_quit("bind()");

	// listen()
	retval = listen(listen_sock, SOMAXCONN);
	if(retval == SOCKET_ERROR) err_quit("listen()");

	// 데이터 통신에 사용할 변수
	SOCKET client_sock;
	SOCKADDR_IN clientaddr;
	int addrlen;
	char buf[BUFSIZE+1];

	while(1){
		// accept()
		addrlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen);
		if(client_sock == INVALID_SOCKET){
			err_display("accept()");
			break;
		}

		// 접속한 클라이언트 정보 출력
		printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
			inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

		// 클라이언트와 데이터 통신
		while(1){
			// 데이터 받기
			retval = recv(client_sock, buf, BUFSIZE, 0);
			if(retval == SOCKET_ERROR){
				err_display("recv()");
				break;
			}
			else if(retval == 0)
				break;

			// 받은 데이터 출력
			buf[retval] = '\0';
			printf("%s", buf);
		}

		// closesocket()
		closesocket(client_sock);
		printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
			inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
	}

	// closesocket()
	closesocket(listen_sock);

	return 0;
}

// TCP 서버(IPv6)
DWORD WINAPI TCPServer6(LPVOID arg)
{
	int retval;

	// socket()
	SOCKET listen_sock = socket(AF_INET6, SOCK_STREAM, 0);
	if(listen_sock == INVALID_SOCKET) err_quit("socket()");

	// bind()
	SOCKADDR_IN6 serveraddr;
	ZeroMemory(&serveraddr, sizeof(serveraddr));
	serveraddr.sin6_family = AF_INET6;
	serveraddr.sin6_addr = in6addr_any;
	serveraddr.sin6_port = htons(SERVERPORT);
	retval = bind(listen_sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
	if(retval == SOCKET_ERROR) err_quit("bind()");

	// listen()
	retval = listen(listen_sock, SOMAXCONN);
	if(retval == SOCKET_ERROR) err_quit("listen()");

	// 데이터 통신에 사용할 변수
	SOCKET client_sock;
	SOCKADDR_IN6 clientaddr;
	int addrlen;
	char buf[BUFSIZE+1];

	while(1){
		// accept()
		addrlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen);
		if(client_sock == INVALID_SOCKET){
			err_display("accept()");
			break;
		}

		// 접속한 클라이언트 정보 출력
		char ipaddr[50];
		DWORD ipaddrlen = sizeof(ipaddr);
		WSAAddressToString((SOCKADDR *)&clientaddr, sizeof(clientaddr),
			NULL, ipaddr, &ipaddrlen);
		printf("\n[TCP 서버] 클라이언트 접속: %s\n", ipaddr);

		// 클라이언트와 데이터 통신
		while(1){
			// 데이터 받기
			retval = recv(client_sock, buf, BUFSIZE, 0);
			if(retval == SOCKET_ERROR){
				err_display("recv()");
				break;
			}
			else if(retval == 0)
				break;

			// 받은 데이터 출력
			buf[retval] = '\0';
			printf("%s", buf);
		}

		// closesocket()
		closesocket(client_sock);
		printf("[TCP 서버] 클라이언트 종료: %s\n", ipaddr);
	}

	// closesocket()
	closesocket(listen_sock);

	return 0;
}

int main(int argc, char *argv[])
{
	// 윈속 초기화
	WSADATA wsa;
	if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)
		return 1;

	HANDLE hThread[2];
	hThread[0] = CreateThread(NULL, 0, TCPServer4, NULL, 0, NULL);

#ifdef DEBUG_IPV6
	hThread[1] = CreateThread(NULL, 0, TCPServer6, NULL, 0, NULL);

	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
#else
	WaitForSingleObject(hThread[0], INFINITE);
#endif

	// 윈속 종료
	WSACleanup();
	return 0;
}

 

 

 

 

 

위와 같이  소스코드를 입력하고 컴파일하는 과정에서 아래와 같은 오류가 발생했습니다.

 

1>c:\users\82104\desktop\2학년\컴퓨터 통신\3 주차\projects\helloworldsolution\welcomeserver\welcomeserver.cpp(24): error C2664: 'MessageBoxW' : cannot convert parameter 3 from 'char *' to 'LPCWSTR'
1>          Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
1>c:\users\82104\desktop\2학년\컴퓨터 통신\3 주차\projects\helloworldsolution\welcomeserver\welcomeserver.cpp(152): error C2664: 'WSAAddressToStringW' : cannot convert parameter 4 from 'char [50]' to 'LPWSTR'
1>          Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

 

 

 

 

 

 

 

 

 

이 부분은 아래와 같이 프로젝트의 속성에서 Character Set > Multi-byte code 사용으로 속성값을 변경해 줍니다.

Solution Explorer 경로에서 프로젝트를 우클릭하고 아래의 properties에 접근합니다.

 

 

 

 

 

 

 

 

아래와 같이 Properties를 클릭하여 프로젝트의 속성에 접근한 후

 

Configuration properties > General > 문자 집합(Character Set) > Use Multi-Byte Character Set으로 속성값을 변경합니다. 

 

 

 

 

 

 

이후 정상적으로 다시 컴파일하여 해당 프로그램을 실행하게 되면 아래와 같이 서버가 클라이언트의 접속을 기다리고 있는 콘솔 화면이 출력됩니다.

 

이를 통해 위의 시나리오에서 구상했던 서버의 실행을 완료하였습니다.

클라이언트의 접속을 기다리고 있는 서버 사이드의 콘솔 화면

 

 

 

 

 

 

이제는 클라이언트가 서버에 접속할 차례입니다.

 

- 클라이언트는 Telnet을 이용하여 서버에 접속할 수 있도록 설정해야 합니다.

- Command prompt를 아래와 같이 실행한 후 telnet 127.0.0.1 9000을 입력합니다.

- telnet 뒤엔 접속할 서버의 주소를 입력, 접속 시 사용할 포트번호를 입력합니다.

 

- 그러나 윈도우에서 기본적으로 telnet을 활성화시키지 않았기에 아래와 같은 메시지가 출력됩니다.

 

 

 

 

 

 

- 따라서 telnet을 활성화시키기 위해 appwiz.cpl 커맨드를 실행합니다.

 

 

 

 

 

 

 

- appwiz.cpl 커맨드라인을 실행하면 아래와 같이 프로그램 / 기능 화면이 나타납니다.

- 좌측의 Windows 기능 켜기 / 끄기를 클릭합니다.

- 나타나는 상자에서 텔넷 클라이언트 항목에 체크하고 확인을 누릅니다.

 

- 해당 과정에서 확인 이후 나타나는 컴퓨터 재시작 메시지에서 바로 재시작해주셔야 하는 점 참고하시어 컴퓨터를 재부팅하시면 Telnet 클라이언트가 활성화됩니다.

 

 

 

 

 

 

이후 Command prompt를 재실행하고 telnet 127.0.0.1 9090을 입력하고 실행하면 아래와 같이 서버 콘솔화면에 클라이언트가 접속했다는 문구가 출력됩니다. 

클라이언트가 서버에 접속 성공한 콘솔 화면

 

 

 

 

 

 

이 상태에서 클라이언트 콘솔에서 키보드를 통해 특정 데이터를 입력하면 서버 콘솔화면에 클라이언트 사이드에서 입력했던 데이터들이 모두 표시됩니다.

 

 

 

 

처음 실행한 상태라면 Client 콘솔에선 키보드로 어떠한 값을 입력해도 입력되는 값이 노출되지 않고 서버 콘솔 화면에서만 출력됩니다. 클라이언트에서 입력한 값을 확인하고자 한다면 특정 명령을 실행해 주어야 합니다

 

- 키보드에서 Ctrl + ] 를 입력하면 입력을 받는 과정을 잠시 멈추고 Telnet 명령어를 줄 수 있는 화면으로 넘어가게 되며 명령어 help를 입력하면 사용 가능한 Telnet의 명령어들의 리스트가 노출됩니다

 

 

 

 

 

- 클라이언트에서 입력되는 값을 사용자가 보기 위해 set이라는 명령어를 사용할 것이며 set localecho를 입력하면 아래와 같이 로컬 에코가 작동된다는 메시지가 출력됩니다 이후 키보드의 엔터키를 눌러서 명령어 입력 콘솔을 빠져나옵니다.

 

 

 

 

이후 아래와 같이 클라이언트 콘솔에서 데이터를 입력함과 동시에 콘솔에 입력한 데이터들이 노출되는 것을 볼 수 있습니다.

 

 

 

클라이언트의 접속을 종료하고자 한다면 키보드에서 Ctrl + ]을 한 번 더 누르고 quit 명령어를 통해 서버를 빠져나올 수 있습니다.

 

- 클라이언트의 접속이 종료된다면 서버 사이드 콘솔에서 클라이언트의 접속이 종료되었다는 문구가 출력됩니다.

 

- 서버의 경우 소스코드에서 while문을 통해 무한 루프를 돌고 있으므로 서버는 종료되지 않기에 클라이언트의 접속을 종료시켜 주었습니다.

 

 

 

 

- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.

- 내용 중에서 오타 또는 잘못된 내용이 있을 시 지적해 주시기 바랍니다.

댓글