2013.07.05 06:58


제목/목차

1. 소개
2. 클라이언트와 서버간 통신 개요
3. 간단한 서버와 클라이언트 구현
3.1 서버 - 기다리는 소켓 만들기
3.2 클라이언트 - 서버로 연결
3.3 서버 - 클라이언트의 연결 시도 받아들이기
3.4 클라이언트와 서버 - 자료 주고받기
4 만든 클라이언트와 서버를 컴파일하고 테스트
4.1 파일 목록
4.2 컴파일과 테스트
5. 결론

1. 소개

소켓은 프로세스간에 자료를 교환하는 수단이다. 프로세스는 같은 컴퓨터에 있거나 네트웍으로 연결된 서로 다른 컴퓨터에 있을 수 있다. 소켓이 연결되면 한쪽이 연결을 닫을 때까지 양편 모두 자료를 보낼 수 있다.

나는 작업중인 프로젝트에 소켓이 필요해서 소켓 API 함수를 감싸는 C++ 클래스를 개발하고 다듬었다. 일반적으로 자료를 요청하는 프로그램을 클라이언트, 요청에 응답하는 프로그램을 서버라고 한다. 내가 만든 ClientSocket과 ServerSocket, 주된 두 클래스를 사용하여 클라이언트와 서버가 서로 자료를 교환할 수 있다.

이 글의 목표는 프로그램에서 ClientSocket과 ServerSocket 클래스를 사용하는 방법을 알리는 것이다. 먼저 클라이언트와 서버간 통신에대해 간단히 다룬 다음, 이 두 클래스를 사용하여 간단한 서버와 클라이언트 예제를 만들 것이다.

2. 클라이언트와 서버간 통신 개요

코드로 들어가기전에 전형적인 클라이언트와 서버간 연결 단계를 간단히 살펴보자. 다음 표는 단계들을 보여준다.

서버클라이언트
1. 기다리는 소켓(listening socket)을 만들고 클라이언트에서 연결을 기다린다.
2. 클라이언트 소켓을 만들고 서버로 연결을 시도한다.
3. 클라이언트의 연결 시도를 받아들인다.
4. 자료를 주고받는다.4. 자료를 주고받는다.
5. 연결을 닫는다.5. 연결을 닫는다.

기본적으로는 이렇다. 먼저 서버가 기다리는 소켓을 만들고 클라이언트의 연결 시도를 기다린다. 클라이언트가 자체 소켓을 만들고 서버와 연결을 시도한다. 서버는 연결을 받아들이고, 자료 교환이 시작된다. 소켓 연결을 통해 모든 자료가 전달되면 양쪽 중 하나가 연결을 닫는다.

3. 간단한 서버와 클라이언트 구현

이제 코드로 들어갈 시간이다. 이제부터 개요에서 다룬 단계를 모두 수행하는 클라이언트와 서버를 만든다. 우리는 일반적으로 일어나는 순서대로 - 예를 들어 먼저 소켓을 기다리는 서버 부분을 만든 다음 서버로 연결하는 클라이언트 부분을 만드는 등 - 구현한다. 전체 코드는 simple_server_main.cppsimple_client_main.cpp에서 볼 수 있다.

미리 소스코드를 살펴보고 실행해보려면 여기를 읽어봐라. 프로젝트 파일 목록과 어떻게 컴파일하고 테스트하는지를 설명한다.

3.1 서버 - 기다리는 소켓 만들기

첫번째 할 일은 클라이언트에서 들어오는 요청을 기다리는 간단한 서버를 만드는 것이다. 다음은 서버 소켓을 만드는 코드이다.

목록 1 : 서버 소켓 만들기 ( simple_server_main.cpp의 일부 )
#include "ServerSocket.h"
#include "SocketException.h"
#include 

int main ( int argc, int argv[] )
{
  try
    {
      // Create the server socket
      ServerSocket server ( 30000 );

      // rest of code -
      // accept connection, handle request, etc...

    }
  catch ( SocketException& e )
    {
      std::cout << "Exception was caught:" << e.description() << "\nExiting.\n";
    }

  return 0;
}


이것만으로 끝났다. ServerSocket 클래스의 생성자는 기다리는 소켓을 만드는데 필요한 소켓 API를 부른다. 자세한 사항을 감추기때문에 로컬 포트에 기다리기위해 이 클래스 객체를 생성하기만 하면 된다.

try/catch 문을 주목하라. ServerSocket과 ClientSocket 클래스는 C++의 예외처리 기능을 사용한다. 클래스 함수가 어떤 이유에서건 문제가 생기면 SocketException.h에 정의된 SocketException 형의 예외를 발생한다. 이 예외를 처리하지 않으면 프로그램이 끝나기때문에 처리해주는 것이 좋다. 위에서처럼 SocketException의 description() 함수를 사용하여 오류문을 얻을 수 있다.

3.2 클라이언트 - 서버로 연결

전형적인 클라이언트와 서버간 연결의 두번째 단계는 클라이언트가 서버로 연결을 시도하는 일이다. 코드는 방금 전에 본 서버 코드와 비슷하다.

목록 2 : 클라이언트 소켓 만들기 ( simple_client_main.cpp의 일부 )
#include "ClientSocket.h"
#include "SocketException.h"
#include 
#include 

int main ( int argc, int argv[] )
{
  try
    {
      // Create the client socket
      ClientSocket client_socket ( "localhost", 30000 );

      // rest of code -
      // send request, retrieve reply, etc...

    }
  catch ( SocketException& e )
    {
      std::cout << "Exception was caught:" << e.description() << "\n";
    }

  return 0;
}


ClientSocket 클래스 객체를 만들기만하면 리눅스 소켓을 만들어서 생성자에 주어진 호스트와 포트로 연결한다. ServerSocket 클래스와 같이 생성자가 어떤 이유에서건 문제가 생기면 오류가 발생한다.

3.3 서버 - 클라이언트의 연결 시도 받아들이기

클라이언트와 서버간 연결의 다음 단계는 서버에서 일어난다. 서버가 클라이언트의 연결 시도를 받아들이면, 두 소켓 양단간 통신 채널이 열린다.

우리는 간단한 서버에 이 기능을 추가해야 한다. 다음은 수정된 버전이다.

목록 3 : 클라이언트 연결 받아들이기 ( simple_server_main.cpp의 일부 )
#include "ServerSocket.h"
#include "SocketException.h"
#include 

int main ( int argc, int argv[] )
{
  try
    {
      // Create the socket
      ServerSocket server ( 30000 );

      while ( true )
	{
	  ServerSocket new_sock;
	  server.accept ( new_sock );

	  // rest of code -
	  // read request, send reply, etc...

	}
    }
  catch ( SocketException& e )
    {
      std::cout << "Exception was caught:" << e.description() << "\nExiting.\n";
    }

  return 0;
}

연결을 받아들이기위해 accept 함수를 호출하면 된다. 이 함수는 연결 시도를 받아들이고, 연결에 대한 소켓 정보를 new_sock에 채운다. 우리는 다음 절에서 어떻게 new_sock을 사용하는지 볼 것이다.

3.4 클라이언트와 서버 - 자료 주고받기

이제 서버는 클라이언트의 연결 요청을 받아들였고, 소켓 연결을 통해 자료를 주고받을 시간이다.

C++의 고급 기능중 하나가 연산자를 오버로딩하는 - 간단히 말해서, 연산자가 특정 작업을 하도록 만드는 - 능력이다. 나는 ClientSocket과 ServerSocket 클래스의 <<와 >> 연산자를 오버로딩하여, 연산자를 사용하여 소켓에서 자료를 읽고쓸 수 있게 만들었다. 다음은 간단한 서버의 수정된 버전이다.

목록 4 : 간단한 서버 구현 ( simple_server_main.cpp의 일부 )
#include "ServerSocket.h"
#include "SocketException.h"
#include 

int main ( int argc, int argv[] )
{
  try
    {
      // Create the socket
      ServerSocket server ( 30000 );

      while ( true )
	{

	  ServerSocket new_sock;
	  server.accept ( new_sock );

	  try
	    {
	      while ( true )
		{
		  std::string data;
		  new_sock >> data;
		  new_sock << data;
		}
	    }
	  catch ( SocketException& ) {}

	}
    }
  catch ( SocketException& e )
    {
      std::cout << "Exception was caught:" << e.description() << "\nExiting.\n";
    }

  return 0;
}

new_sock 변수는 모든 소켓 정보를 저장하고 있어서, 클라이언트와 자료를 교환하는데 사용한다. "new_sock >> data;" 줄은 "new_sock에서 자료를 읽어서 'data'라는 문자열 변수에 저장한다"는 뜻이다. 비슷하게 다음 줄은 'data'에 있는 자료를 소켓을 통해 클라이언트로 보낸다.

주의를 기울였다면 여기서 만든 것이 echo 서버임을 알 수 있을 것이다. 클라이언트가 보낸 자료는 모두 다시 클라이언트에게 그대로 되돌려진다. 우리는 자료를 보내고 서버의 응답을 출력하는 클라이언트를 작성할 수 있다.

목록 5 : 간단한 클라이언트 구현 ( simple_client_main.cpp의 일부 )
#include "ClientSocket.h"
#include "SocketException.h"
#include 
#include 

int main ( int argc, int argv[] )
{
  try
    {

      ClientSocket client_socket ( "localhost", 30000 );

      std::string reply;
      try
	{
	  client_socket << "Test message.";
	  client_socket >> reply;
	}
      catch ( SocketException& ) {}

      std::cout << "We received this response from the server:\n\"" << reply << "\"\n";;

    }
  catch ( SocketException& e )
    {
      std::cout << "Exception was caught:" << e.description() << "\n";
    }

  return 0;
}

우리는 문자열 "Test Message."를 서버로 보내고, 서버의 응답을 표준출력으로 출력한다.

4. 만든 클라이언트와 서버를 컴파일하고 테스트

우리는 ClientSocket과 ServerSocket 클래스의 기본적인 사용법을 살펴봤다. 이제 프로젝트 전체를 컴파일하여 테스트해보자.

4.1 파일 목록

예제는 아래 파일들로 구성된다.

기타:
Makefile - 이 프로젝트의 Makefile
Socket.h, Socket.cpp - 소켓 API 함수를 구현한 Socket 클래스
SocketException.h - SocketException 클래스
서버:
simple_server_main.cpp - 주파일
ServerSocket.h, ServerSocket.cpp - ServerSocket 클래스
클라이언트:
simple_client_main.cpp - 주파일
ClientSocket.h, ClientSocket.cpp - ClientSocket 클래스

4.2 컴파일과 테스트

컴파일은 간단하다. 먼저 모든 프로젝트 파일을 하위디텍로리에 저장하고, 명령행 프롬프트에 다음과 같이 입력한다.

prompt$ cd directory_you_just_created
prompt$ make

그러면 프로젝트의 모든 파일을 컴파일하여 simple_server와 simple_client 출력 파일을 만든다. 두 출력 파일을 테스트하기위해 한 명령행 프롬프트에서 서버를 실행하고, 다른 명령행 프롬프트에서 클라이언트를 실행한다.

첫번째 프롬프트:
prompt$ ./simple_server
running....



두번째 프롬프트:
prompt$ ./simple_client
We received this response from the server:
"Test message."
prompt$

클라이언트는 서버로 자료를 보내고, 응답을 읽어서 위에서처럼 표준출력으로 출력한다. 클라이언트를 원하는만큼 실행할 수 있다. 서버는 매 요청에 응답한다.

5. 결론

소켓은 프로세스간에 자료를 보내는 간단하고 효율적인 방법이다. 이 글에서 우리는 소켓 통신을 살펴보고 서버와 클라이언트 예제를 만들어봤다. 이제 당신의 프로그램에 소켓 통신을 추가할 수 있다!

신고

'Programming > CPP' 카테고리의 다른 글

멀티바이트와 유니코드  (0) 2013.07.27
BroadCasting 참고 코드  (0) 2013.07.09
Virtual Key Code  (0) 2013.07.06
키보드 이벤트 처리하기  (0) 2013.07.05
Visual Studio 2012 단축키  (0) 2013.07.05
소켓통신  (0) 2013.07.05


Posted by injunech

티스토리 툴바