Chapter 2. 동기 I/O 방식의 TCP/IP Echo 서버, 클라이언트

Date:     Updated:

카테고리:

태그:

최흥배님의 책 Boost.Asio를 사용한 네트워크 프로그래밍 을 공부하고 정리한 필기입니다. 😀

Chapter 2. 동기 I/O 방식의 TCP/IP Echo 서버, 클라이언트 프로그램 만들기

동기 방식 👉 요청 후 답변을 받을 때까지 더 이상 진행하지 않고 기다리는 방식. 요청한 답변을 받아야만 다음 단계를 진행한다.

아래 코드를 이해한 후 외울 것을 추천!!!

  • 서버, 클라이언트 아예 각각 별개의 프로젝트로 만들어야 한다.
  • 실행시 서버를 먼저 실행해야 한다.
    • 서버가 먼저 실행된 후 클라이언트의 접속을 기다린다.

🔔 서버

#include <iostream>
#include <boost/asio.hpp>

using namespace std;

const char SERVER_IP[] = "127.0.0.1";  // 서버에선 사실 필요 없다.
const unsigned short PORT_NUMBER = 3100;

int main()
{
	boost::asio::io_service io_service;
	boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), PORT_NUMBER);
	boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint);

	boost::asio::ip::tcp::socket socket(io_service);
	acceptor.accept(socket);
	
	cout << "클라이언트 접속" << endl;

	for (;;)
	{
		char buf[128] = { 0 };
		boost::system::error_code error;
		size_t len = socket.read_some(boost::asio::buffer(buf), error);

		if (error)
		{
			if (error == boost::asio::error::eof)
				cout << "클라이언트와 연결이 끊어졌습니다" << endl;
			else
				cout << "error No: " << error.value() << " error Message: " << error.message() << endl;
			break;
		}

		cout << "클라이언트에서 받은 메세지: " << &buf[0] << endl;
		char szMessage[128] = { 0, };
		sprintf_s(szMessage, 128 - 1, "Re:%s", &buf[0]);
		int nMsgLen = strnlen_s(szMessage, 128 - 1);

		boost::system::error_code ignored_error;
		socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);
	}
	getchar();

	return 0;
}

1️⃣ 클라이언트의 접속을 준비하기

boost::asio::io_service io_service;

  • io_service
    • Boost.Asio의 핵심으로 커널에서 발생한 I/O 이벤트를 디스패치 해준다.
      • 디스 패치 👉 멀티 태스킹 환경에서 우선 순위가 높은 작업이 수행될 수 있도록 자원을 할당하는 것
    • 네트워크 상의 접속 받기(accept), 접속 하기(connect), 데이터 받기(receitve), 데이터 보내기(send) 등등 I/O 이벤트를 알 수 있다.
    • 그래서 socket과 같은 객체를 생성할 때 io_service를 인자로 넘겨주어야 한다.

boost::asio::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), PORT_NUMBER);

  • endpoint
    • 네트워크 주소를 설정한다. 이 주소로 클라이언트가 접속한다.
    • 서버는 IP 주소 체계 (IPv4 or IPv6)와 포트 번호를 사용한다. 이 둘을 인수로 넘긴다.
      • 클라이언트와 서버는 endpoint 설정 방식이 다름.
      • v4() 여기선 IPv4 주소 체계를 사용했다.
      • 서버 스스로 자신의 주소(ip)를 알고 있기 때문에 서버 시작을 준비하는 아래 과정에선 ip다루는 부분 없다.
        • char 배열 SERVER_IP를 사용하지 않음.

boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint)

  • acceptor
    • 클라이언트 접속을 받아들이는 역할
      • 그러니 io_service를 인수로 넘겨주어야 함!
      • endpoint에 접속이 됐는지를 알기 위해 endpoint도 넘기기
    • 인수 👉 io_service, endpoint


2️⃣ 클라이언트의 접속 받기

boost::asio::ip::tcp::socket socket(io_service)

  • socket
    • 접속한 클라이언트에 할당할 소켓을 만든다.
    • 만든 socket을 통해 클라이언트가 보낸 메세지를 주고 받는다.
      • 메세지를 주고 받기 위하려면 io_service가 필요하므로 인수로 넘겨주기

acceptor.accept(socket);

  • 클라이언트의 접속을 받아들인다.
    • 접속한 클라이언트에 할당될 socket을 appect 한다.


3️⃣ 클라이언트가 보낸 메세지 받기 👉 read_some

접속한 클라이언트가 있다면 서버를 무한 루프 for(;;;)로 돌린다.

  • 클라이언트와 메세지 주고 받는 것을 무한 루프로 돌림.
  • 클라이언트와 연결이 끊어지거나 에러가 발생할 때만 무한 루프를 빠져나와 서버를 종료시킬 수 있다.

char buf[128] = { 0 };

  • buf
    • 클라이언트가 보낸 메세지를 담을 버퍼
    • char 배열 타입으로 설정했다.
      • string이나 vector로도 가능하다.

boost::system::error_code error

  • error_code
    • 시스템에서 발생하는 에러 코드를 wrapping한 클래스
    • 에러가 발생하면 에러 코드와 에러 메세지를 얻을 수 있다.

size_t len = socket.read_some(boost::asio::buffer(buf), error)

  • 클라이언트가 보낸 데이터(메세지) 받기
    • socketread_some 함수
      • 클라이언트가 보낸 메세지를 boost::asio::buffer 타입으로 buf에 받는다.
      • 에러 코드가 발생할 것을 대비하여 error도 넘긴다. 만약 클라이언트의 메세지를 받는데 실패한다면 error에 에러코드가 담길 것이다.
      • 성공적으로 클라이언트가 보낸 데이터를 받으면 받은 데이터 크기를 리턴한다.
  • 동기 방식이므로 데이터를 다 받을 때까지 대기 상태에 들어간다.
		if (error)
		{
			if (error == boost::asio::error::eof)
				cout << "클라이언트와 연결이 끊어졌습니다" << endl;
			else
				cout << "error No: " << error.value() << " error Message: " << error.message() << endl;
			break;
		}
  • 클라이언트에서 보낸 데이터를 받으면 에러가 발생했는지를 조사해야 한다.
    • error가 null이 아니라는 것은 read_some 함수로 클라이언트의 메세지를 받을 때 문제가 발생하여 인수로 넘겼던 error에 에러 코드가 담겼다는 뜻
  • 에러 코드가 boost::asio::error::eof 라면 클라이언트의 접속이 끊어졌다는 뜻
  • 그런건 아니지만 에러 코드가 존재한다면 error.value()error.message()를 통해 에러 값과 에러 메세지를 출력한다.
  • break
    • 👉 이렇게 클라이언트의 메세지를 받는 과정 (read_some) 에서 문제가 발생하여 error != null 이라면 무한루프 for(;;)를 빠져나와 서버를 종료한다.


4️⃣ 클라이언트에게 메세지 보내기 👉 write_some

		cout << "클라이언트에서 받은 메세지: " << &buf[0] << endl;
		char szMessage[128] = { 0, };
		sprintf_s(szMessage, 128 - 1, "Re:%s", &buf[0]);
		int nMsgLen = strnlen_s(szMessage, 128 - 1);

		boost::system::error_code ignored_error;
		socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);

클라이언트로부터 받은 메세지는 buf에 담겨 있다.

  • &buf[0]buf나 마찬가지다. 첫 원소의 주소는 곧 배열의 주소! (=배열 이름)
  • szMessage
    • 클라이언트에게 보낼 메세지를 담을 버퍼
    • char 배열이기는 하지만 마찬가지로 string이나 vector도 사용 가능하다.
    • Echo 채팅 서버를 만들 것이므로 클라이언트로부터 받은 메세지를 그대로 돌려 보내주자.
      • “Re: “ 와 함께 클라이언트로부터 받은 메세지를 그대로 보내줄 것임.
      • sprintf 함수를 통해 szMessage에 클라이언트에게 보낼 메세지를 담았다.
        • “Re: 블라블라”
        • 마지막 글자는 char 배열 특성상 ‘\0’이어야 해서 127글자만.
  • nMsgLen
    • 클라이언트에게 보낼 메세지의 길이
  • write_some 함수를 통해 클라이언트에게 메세지 szMessage를 보낸다.
    • 클라이언트에게 보낼 메세지인 szMessageboost::asio::buffer 타입으로 보낸다.
    • 단, 받을 때와 다르게 보낼 때는 보낼 데이터의 양을 미리 지정해주어야 한다.
      • 따라서 nMsgLen도 인수로 넘겨야 하며 szMessage의 모든 데이터를 보내지 않고 nMshLen 길이만큼만 보낸다.
    • 마찬가지로 이 과정에서 에러가 발생한다면 ignored_error에 에러 코드가 담길 것이다.


🔔 클라이언트

#include <iostream>
#include <boost/asio.hpp>

using namespace std;

const char SERVER_IP[] = "127.0.0.1";
const unsigned short PORT_NUMBER = 3100;

int main()
{
	boost::asio::io_service io_service;
	boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::address::from_string(SERVER_IP), PORT_NUMBER);

	boost::system::error_code connect_error;
	boost::asio::ip::tcp::socket socket(io_service);
	socket.connect(endpoint, connect_error);

	if (connect_error)
	{
		cout << "연결 실패. error No: " << connect_error.value() << " , Message: " << connect_error.message() << endl;
		getchar();
		return 0;
	}
	else
		cout << "서버에 연결 성공!" << endl;

	for (int i = 0; i < 7; i++)
	{
		char szMessage[128] = { 0, };
		sprintf_s(szMessage, 128 - 1, "%d - Send Message", i);
		int nMsgLen = strnlen_s(szMessage, 128 - 1);

		boost::system::error_code ignored_error;
		socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);

		cout << "서버에 보낸 메세지 : " << szMessage << endl;

		char buf[128] = { 0 };
		boost::system::error_code error;
		size_t len = socket.read_some(boost::asio::buffer(buf), error);

		if (error)
		{
			if (error == boost::asio::error::eof)
				cout << "서버와 연결이 끊어졌습니다" << endl;
			else
				cout << "error No: " << error.value() << " error Message: " << error.message().c_str() << endl;
			break;
		}

		cout << "서버로부터 받은 메세지 : " << &buf[0] << endl;
	}

	if (socket.is_open())
		socket.close();

	getchar();
	return 0;
}
  • 서버랑 메세지 주고 받는 것을 7 번만 하고 서버랑 연결 끊을거라서 7 번 도는 for문 예제 !

1️⃣ 서버에 접속하기

boost::asio::tcp::endpoint endpoint(boost::asio::ip::address::from_string(SERVER_IP), PORT_NUMBER);

  • endpoint
    • 네트워크 주소를 설정한다. 접속할 서버의 IP 주소를 지정한다.
    • 서버에서의 endpoint 설정과는 조금 다르다
      • 서버의 endpoint 설정과 비교 👉 서버는 Ipv4 주소 체계를 인수로 넘기고 있다. 클라이언트는 서버 주소를 인수로 넘김
        boost::asio::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), PORT_NUMBER);
        
    • boost::asio::ip::address::from_string 클래스를 사용하여 문자열로 된 서버 IP 주소를 Boost.Asio에서 사용하는 IP 주소로 변환해주어야 한다.
	boost::system::error_code connect_error;
	boost::asio::ip::tcp::socket socket(io_service);
	socket.connect(endpoint, connect_error);

	if (connect_error)
	{
		cout << "연결 실패. error No: " << connect_error.value() << " , Message: " << connect_error.message() << endl;
		getchar();
		return 0;
	}
	else
		cout << "서버에 연결 성공!" << endl;

socket.connect(endpoint, connect_error);

  • socket에 클라이언트인 자기 자신을 할당하기
    • connect 함수를 사용하여 서버(endpoint에 서버 주소와 포트 넘버를 지정했었다.)에 접속을 시도한다.
      • 실패하면 connect_error에 에러 코드가 담긴다.
      • 동기 방식이므로 접속이 성공하거나 실패할 때까지 대기 상태가 된다.


2️⃣ 서버에게 메세지 보내기 👉 write_some

		char szMessage[128] = { 0, };
		sprintf_s(szMessage, 128 - 1, "%d - Send Message", i);
		int nMsgLen = strnlen_s(szMessage, 128 - 1);

		boost::system::error_code ignored_error;
		socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);

		cout << "서버에 보낸 메세지 : " << szMessage << endl;


3️⃣ 서버가 보낸 메세지 받기 👉 read_some

		char buf[128] = { 0 };
		boost::system::error_code error;
		size_t len = socket.read_some(boost::asio::buffer(buf), error);

		if (error)
		{
			if (error == boost::asio::error::eof)
				cout << "서버와 연결이 끊어졌습니다" << endl;
			else
				cout << "error No: " << error.value() << " error Message: " << error.message().c_str() << endl;
			break;
		}
  • read_some 과정에서 error 체크


4️⃣ 서버랑 접속 끊기

	if (socket.is_open())  // 네트워크 연결 상태일 때
		socket.close();  // 서버와의 연결을 끊는다. 
  • for문을 7 번 돌면서 서버랑 메세지를 7 번 주고 받은 후 for문을 빠져나오면 이제 서버랑 접속을 끊어야 한다.
  • socket.is_open()
    • 네트워크에 연결된 상태라면 true를 리턴하고 연결되지 않은 상태라면 false를 리턴한다.
  • socket.close()
    • 서버와의 연결을 끊는다.


🔔 전반적인 흐름 : 서버-클라이언트

image

서버 클라이언트  
1️⃣ io_service 생성
boost::asio::io_service io_service;
   
2️⃣ 접속 받을 준비
boost::asio::ip::tcp::endpoint endpoint(..)
boost::asio::ip::tcp::acceptor acceptor(..)
   
  3️⃣ io_service 생성
boost::asio::io_service io_service;
 
  4️⃣ 서버에 접속하기
boost::asio::ip::tcp::endpoint endpoint(..)
boost::asio::ip::tcp::socket socket(io_service);
socket.connect(endpoint, connect_error);
 
5️⃣ 클라이언트의 접속 받아들이기
boost::asio::ip::tcp::socket socket(io_service);
acceptor.accept(socket);
   
  6️⃣ 서버에게 메세지 보내기
socket.write_some(…)
 
7️⃣ 클라이언트로부터 메세지 받기
socket.read_some(…)
   
8️⃣ 클라이언트에게 메세지 보내기
socket.write_some(…)
   
  9️⃣ 서버로부터 메세지 받기
socket.read_some(…)
 
  1️⃣0️⃣ 서버와 접속 끊기
socket.close()
 
1️⃣1️⃣ 클라이언트로부터 메세지 받기
socket.read_some(…)
클라이언트로부터의 메세지를 받는 과정에서
클라이언트와 접속이 끊겨 `error`에 에러코드가 담기고 무한루프를 break 하여 서버도 종료된다
   


🔔 관련 Boost.Asio API

endpoint 클래스

boost::asio::ip::tcp::endpoint

  • 프로그램에서 사용할 네트워크 주소를 설정한다.
  • 생성자 종류
    • 1️⃣ 인수 없는
      using boost::asio::ip::tcp;
      
      tcp::endpoint endpoint();
      
    • 2️⃣ 인수 2 개 👉 주소 체계(프로토콜) + 포트 번호
      • 서버가 이 생성자를 사용했다. 서버는 스스로 자신의 IP 주소를 알고있기 때문에 IP 주소는 인수로 넘길 필요 없이 주소 체계만 지정해주면 된다.
        using boost::asio::ip::tcp;
        
        tcp::endpoint endpoint(tcp::v4(), 14567);
        
    • 3️⃣ 인수 2 개 👉 서버 주소 + 포트 번호
      • 클라이언트가 이 생성자를 사용했다. 접속할 서버 주소가 필요하다.
        using boost::asio::ip::tcp;
        
        tcp::endpoint endpoint(boost::asio::ip::address::from_sttring("127.0.0.1"), 14567);
        
    • 4️⃣ 인수 1 개 👉 다른 endpoint 로 복사 생성
      using boost::asio::ip::tcp;
      
      tcp::endpoint endpoint_2(endpoint);
      


acceptor 클래스

boost::asio::ip::tcp::acceptor

  • 클라이언트의 접속을 받아들이기 위한 클래스
  • 생성자 버전은 5 가지가 있는데 책 참고
    • 접속을 받아들일 주소와 만약 다른 프로그램에서 이미 사용중인 주소라면 재설정할 수 있도록 인수를 넘길 수 있다.

accept 함수

acceptor.accept(socket)

  • 클라이언트의 연결 요청을 받아들인다
    • 연결 요청이 있을 때까지 대기한다.
  • 인수로 클라이언트가 할당 되어 있는 소켓을 넘긴다. 에러코드를 넘겨줄 수도 있다.


socket 클래스

boost::asio::ip::tcp::socket

connect 함수

socket.connect(endpoint)

  • 서버와의 접속 요청
    • 접속 될 때까지 더 진행하지 않고 기다린다.
  • 서버 주소인 endpoint를 인수로 넘긴다. 에러 코드를 넘겨줄 수도 있다.

read_some 함수

socket.read_some(buffer, errorcode)

  • 연결된 곳에서 보내는 데이터를 받으며 이를 buffer에 담는다.
  • 데이터를 받을 때까지 대기한다.
  • 에러코드를 넘겨줄 수도 있다.
    • 연결이 끊어졌다면 에러코드는 boost::asio::error::eof 이다.

write_some 함수

socket.write_some(buffer, errorcode)

  • 연결된 곳에 데이터를 보내며 이를 buffer로 보낸다.
  • 데이터를 다 보낼 때까지 대기한다.
  • 에러코드를 넘겨줄 수도 있다.

close 함수

socket.close() 혹은 socket.close(ec)

  • 연결된 곳과의 접속을 끊는다
  • 인수를 아무것도 넘기지 않은 경우 에러가 발생하면 예외가 발생한다.
  • 에러코드를 인수로 넘겨진 경우 에러가 발생하면 이곳에 어떤 에러인지에 대한 내용이 담긴다.


🔔 다른 코드와 비교

따라서 배우는 C++ 강의에서 필기한 코드이다.

  • try-catch 문을 사용하며 연결 에러는 accept에서 받고 있다. 이 accept 과정도 무한 루프 안에서 이루어짐.
  • ⭐ 이 코드에서는 socket 객체를 사용하지 않는다. 대신 소켓 위에 iostream을 구현한 boost::asio::ip::tcp::iostream을 사용한다.
    • Boost.Asio 에서 오버로딩된 iostream이다.
    • boost::asio::ip::tcp::iostream stream;
      • 위 코드와 비교) boost::asio::ip::tcp::socket socket(io_service);
    • 클라이언트 접속 받기
      • acceptor.accept(*stream.rdbuf(), ec);
        • 위 코드와 비교) acceptor.accept(socket);
    • 메세지 받기
      • socket.read_some(boost::asio::buffer(buf), error);
        • 위 코드와 비교) std::getline(stream, line);
    • 메세지 보내기
      • socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);
        • 위 코드와 비교) std::cout « line « std::endl;

서버

#include <iostream>	
#include <vector>
#include <utility>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

int main()
{
	try
	{
		boost::asio::io_service io_service;

		tcp::endpoint endpoint(tcp::v4(), 13);			// 통신을 하는 양 끝점 endpoint. 13은 port # 임의의 숫자 가능
		tcp::acceptor acceptor(io_service, endpoint);	// endpoint에서 io_service를 accept할 acceptor를 생성

		std::cout << "Server started" << std::endl;

		for (;;)	// 무한 루프
		{
			const std::string message_to_send = "Hello From Server";

			boost::asio::ip::tcp::iostream stream;	// 일반 iostream이 아니라 오버로드된 tcp::iostream이다.

			std::cout << "check 1" << std::endl;	// 네트워킹에서도 cout을 쓸 수 있다.

			boost::system::error_code ec;
			acceptor.accept(*stream.rdbuf(), ec);	// 클라이언트 접속을 받아들인다.

			std::cout << "check 2" << std::endl;

			if (!ec)	//TODO: How to take care of multiple clients? Multi-threading?
			{	// 클라이언트가 제대로 접속이 됐다면,

				// receive message from client 
				std::string line;
				std::getline(stream, line);			// 입출력 스트림에서 클라이언트로부터 메세지를 받는다.
				std::cout << line << std::endl;		// 클라이언트 입장에서는 여기서 데이터를 보내야 서버가 받는다.

				// send message to client
				stream << message_to_send;
				stream << std::endl;				// 클라이언트 입장에서는 여기서 데이터를 읽어야 서버가 보낸다.
			}
		}
	}
	catch (std::exception& e)						// 예외 발생시
	{
		std::cout << e.what() << std::endl;
	}
}

클라이언트

#include <iostream>
#include <string>	
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

int main(int argc, char** argv)
{
	try
	{
		if (argc != 2)
		{
			std::cerr << "Usage : Client <host>\n";
			return EXIT_FAILURE;
		}
		
		tcp::iostream stream(argv[1], std::to_string(int(13))); // port num = 13
		if (!stream)
		{
			std::cout << "No address. Unable to connect: " << stream.error().message() << std::endl;
			return EXIT_FAILURE;
		}

		// send message to server
		stream << "Hello from client";
		stream << std::endl;

		// receive message from server
		std::string line;
		std::getline(stream, line);
		std::cout << line << std::endl;
	}
	catch(std::exception & e)
	{
		std::cout << e.what() << std::endl;
	}
}


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp Network 카테고리 내 다른 글 보러가기

댓글 남기기