서버를 짜다보면 간단한 제어 인터페이스를 노출할 경우가 많은데 보통 웹을 붙이거나 shell 을 흉내낸다. 이하는 asio 로 간단히 shell 흉내를 내는 코드를 만들어본것.
그냥 시간이 남아 간단히 만들어본건데 async_read_until 때문에 조금 고생했다.
이놈이 받는 버퍼는 다른 async 류 함수들과 달리 streambuf 더라.
나는 평소 C++ 을 쓰더라도 stream 쪽은 전혀 쓰질 않아서 이쪽은 하나도 모르는데.. 그래서 좀 애먹었다. 급한대로 streambuf 로부터 한글자씩 꺼내서 처리해봤는데 이건 좀 아닌거 같고 좀더 나은 방법을 뒤져봐야겠다.
이하는 main 을 담고있는 코드.
한줄을 받아서 한줄을 내놓는 최소한의 로직만 담고있는 클래스 echo 를 정의하고 이놈으로 console<echo> 인스턴스를 만들어 io_service 에 붙였다. 당연히 그냥 asio 공부하려고 만들어본 코드니 제구실하기엔 부족한 코드.
main.cpp..
#include "console.hpp"
// 걍 간단한 예제니까 operator() 하나만 요구하도록 했다.
// 좀더 써먹으려면 프롬프트제어나.. 등등 많은 기능이 이쪽으로 들어와야겠지?
struct echo
{
string operator()(const string& input)
{
// 간단히 만들다 보니 그만 끝내라는 제어를 예외로 하게됐다.
if(input == "quit") throw runtime_error("damn shit");
// 여기서 적절한 처리를 하면 되겠지.
return input;
}
};
int main()
{
try
{
asio::io_service io_service;
console<echo> con(io_service, tcp::endpoint(tcp::v4(), 4321), "sucks> ");
io_service.run();
}
catch(exception& e)
{
cerr << e.what() << endl;
}
}
아래는 console, session 을 담고있는 코드
console<T> 는 그냥 acceptor 라고 보면 된다. 특정 포트로 tcp 받아서 session<T> 를 까주는 놈. session<T> 는.. 결국 session<echo> 로 인스턴스가 만들어지는데 echo 가 가진 함수를 부르기 위한 소켓관련 작업들을 해주는 놈이다.
console.hpp..
// 몇줄 안되는 코드지만 async_read_until 때문에 고생했다. 우선 이놈은
// 버퍼로 받는 타입이 다른 어싱크 함수들과는 다르더라. basic_streambuf
// 라.. 문서상에는 std::streambuf 확장이라고 되어있던데.. 헌데 난 C++
// 을 쓰더라도 가능하면 stream 라이브러리쪽을 쓰지 않는 주의(C 쪽
// FILE* 또는 win32 api 선호)라 streambuf 를 다뤄보는건 처음이었다.
//
// 결론적으로 streambuf 는 잘 모른다는 소리. 예제에서는 streambuf 에서
// istream 만들어서 이 스트림을 가지고 버퍼를 읽던데.. 이 무슨
// 삽질인가? 버퍼면 그냥 꺼낼수 있어야지. 그런데 적절히 둘러봐도 멋지게
// 꺼낼 메서드를 못찾아서(몰라서) 결국 한글자씩 뽑아내는 무식한짓을
// 했다... 헐....
//
// 여유나면 C++ stream 쪽도 공부를 좀 해두자.
//
#ifndef CONSOLE_HPP
#define CONSOLE_HPP
#include "boostasio.hpp"
template<class T>
class session
{
public:
// 최초에 프롬프트 찍어주고
session(socket_ptr sock, const string& pr)
: sock_(sock),
prompt_(pr)
{
prompt();
}
private:
socket_ptr sock_;
T proc_;
asio::streambuf buf_;
string prompt_;
// 프롬프트 날려준다.
void prompt()
{
asio::async_write(*sock_,
asio::buffer(prompt_),
bind(&session<T>::handle_prompt,
this,
asio::placeholders::error));
}
// 프롬프트 날아갔으면 한줄 읽을 차례
void handle_prompt(const error_code& error)
{
if(error) return die(error);
readline();
}
// 한줄 읽는다.
void readline()
{
asio::async_read_until(*sock_,
buf_,
"\r\n",
bind(&session<T>::handle_readline,
this,
asio::placeholders::error,
asio::placeholders::bytes_transferred));
}
// 한줄 읽었으면 처리할 차례. 단 빈 문자열이면 프롬프트나 날린다.
void handle_readline(const error_code& error, size_t readed)
{
if(error) return die(error);
// 아래 istream 이 문서상에 나온 사용법. 그런데 이런식의
// 사용법은 좀 거시기하네.
// istream i(&buf_);
// string line;
// getline(i, line);
// 으음. streambuf 로부터 직접 값을 꺼냈다. 이런식의 접근도
// 정상은 아닌거 같은데.. 어쨌거나 마지막에 \r\n 이 들어있으니
// 적절히 처리를 해줘야 했는데 코드를 깔끔하게 하려고 모두
// 읽은후 트리밍해줬다. 릴리즈할 코드였다면 readed-2 만큼
// 읽었겠지. 아니 애초에 한글자씩 꺼내는 이짓을 하지 않고 다른
// 방법을 찾았겠지.
string line;
for(int i = 0; i < readed; ++i)
line.push_back(buf_.sbumpc());
boost::trim(line);
// 빈입력이 들어오면 바로 다시 프롬프트찍도록 했다. 이거 빠지면
// 엔터 마구칠때 보기 흉해진다.
if(line.empty()) return prompt();
proc(line);
}
// 처리.. 처리하는 코드를 템플릿 인자로 받도록 했다. 별의미는 없고 그냥..
void proc(const string& input)
{
try
{
string output(proc_(input));
writeline(output+"\r\n");
}
catch(exception& e)
{
die(e);
}
}
// 처리결과 쏴주고
void writeline(const string& output)
{
asio::async_write(*sock_,
asio::buffer(output),
bind(&session<T>::handle_writeline,
this,
asio::placeholders::error));
}
// 다시 프롬프트 쏠 차례
void handle_writeline(const error_code& error)
{
if(error) return die(error);
prompt();
}
// 에러나면 걍 delete this
void die(const error_code& error)
{
cerr << error.message() << endl;
delete this;
}
void die(exception& e)
{
cerr << e.what() << endl;
delete this;
}
};
// 템플릿 인자로 받은건 괜히 쑈한것. 그나저나 이 T 로 인스턴스를 직접
// 만드는게 아니고 session 에 전달만 하네.. 이거 좀 모양이 별로군.
template <class T>
class console
{
public:
console(asio::io_service& io_service, const tcp::endpoint& endpoint, const string& prompt)
: acceptor_(io_service, endpoint),
prompt_(prompt)
{
start_accept();
}
private:
tcp::acceptor acceptor_;
string prompt_;
void start_accept()
{
socket_ptr childsocket(new tcp::socket(acceptor_.io_service()));
acceptor_.async_accept(*childsocket,
bind(&console<T>::handle_accept,
this,
childsocket,
asio::placeholders::error));
}
void handle_accept(socket_ptr childsocket, const error_code& error)
{
if(error)
{
cerr << error.message() << endl;
return;
}
new session<T>(childsocket, prompt_);
start_accept();
}
};
#endif
이건 include 모음집. 네임스페이스 처리가 짱나서 하나에 몰아두고 쓰는게 좋지.
boostasio.hpp..
#ifndef BOOSTASIO_HPP
#define BOOSTASIO_HPP
#include <iostream>
#include <boost/algorithm/string.hpp>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
using namespace std;
namespace asio = boost::asio;
using boost::asio::ip::tcp;
using boost::lexical_cast;
using boost::array;
using boost::bind;
using boost::shared_ptr;
using boost::system::error_code;
typedef shared_ptr<tcp::socket> socket_ptr;
#endif
음.
좀더 뒤져서 asio::streambuf 에서 값을 꺼내는 방법을 하나 더 찾았다.
asio::const_buffer data = buf_.data();
const char* b = asio::buffer_cast<const char*>(data);
const char* e = b + readed;
뭐 대략 이런식인데 이것도 통쾌한 방법은 아닌거 같다. 문서상 그리고 메일링에서도 istream 을 통해 꺼내오는것을 권장하는거 같은데.. iostream 쪽 코드는 영 손대기 그렇구만..
그리고 이런식으로 뽑아쓸경우 consume 으로 버퍼 비워주는것도 잊지말자.
추가.
ReadHandler 같은경우 error 와 readed 를 인자로 받는데 나는 error 나면 readed 무시하고 바로 에러를 내도록 했지만 경우에 따라 readed 를 먼저 처리하고 error 확인해야 할때도 있을거 같은데.. 이건 나중에 asio 를 정말 쓰게되면 다시 만날 문제겠지.
댓글 없음:
댓글 쓰기