Chapter 2. 동기 I/O 방식의 TCP/IP Echo 서버, 클라이언트
카테고리: Cpp Network
태그: Network Programming Cpp
최흥배님의 책 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의 핵심으로 커널에서 발생한 I/O 이벤트를 디스패치 해준다.
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
를 사용하지 않음.
- char 배열
- 클라이언트와 서버는
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)
- 클라이언트가 보낸 데이터(메세지) 받기
socket
의 read_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(;;)
를 빠져나와 서버를 종료한다.
- 👉 이렇게 클라이언트의 메세지를 받는 과정 (read_some) 에서 문제가 발생하여
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
를 보낸다.- 클라이언트에게 보낼 메세지인
szMessage
을boost::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
에 에러 코드가 담긴다. - 동기 방식이므로 접속이 성공하거나 실패할 때까지 대기 상태가 된다.
- 실패하면
- connect 함수를 사용하여 서버(
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()
- 서버와의 연결을 끊는다.
🔔 전반적인 흐름 : 서버-클라이언트
서버 | 클라이언트 | |
---|---|---|
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);
- 서버가 이 생성자를 사용했다. 서버는 스스로 자신의 IP 주소를 알고있기 때문에 IP 주소는 인수로 넘길 필요 없이 주소 체계만 지정해주면 된다.
- 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);
- 1️⃣ 인수 없는
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)
- 연결된 곳과의 접속을 끊는다
- 인수를 아무것도 넘기지 않은 경우 에러가 발생하면 예외가 발생한다.
- 에러코드를 인수로 넘겨진 경우 에러가 발생하면 이곳에 어떤 에러인지에 대한 내용이 담긴다.
🔔 다른 코드와 비교
- 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);
- acceptor.accept(*stream.rdbuf(), ec);
- 메세지 받기
- socket.read_some(boost::asio::buffer(buf), error);
- 위 코드와 비교) std::getline(stream, line);
- socket.read_some(boost::asio::buffer(buf), error);
- 메세지 보내기
- socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);
- 위 코드와 비교) std::cout « line « std::endl;
- socket.write_some(boost::asio::buffer(szMessage, nMsgLen), ignored_error);
- Boost.Asio 에서 오버로딩된
서버
#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;
}
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글남기기