음 제목짓기가 애매하네.
어떤 서버가 tcp 연결을 몇개까지 받는지 테스트해야 했는데 어쩌다 보니 C++ 로 짜게 됐다.(ghc 의 소켓제한 때문에..)
워낙 간단한 코드라 평소 안하던짓을 한번 해보고 코드를 적어둔다.
소켓을 다룰때 많은 세션을 다루려면 이 세션을 다루는 매니저 클래스가 있을법 한데 보통 이경우 세션 클래스가 매니저 클래스를 알아야만 한다(커플링). 이걸 bind 와 function 으로 좀 오버해서 만들어봤다.
물론 이외에도 방법은 많고.. 실무에 써야 한다면 아마도 당연히 그냥 의존성을 그냥 두고 가거나 아니면 signal 류 라이브러리를 썼겠지. 아래 코드는 재미삼아 만들어본것에 불과하다.
펼쳐두기..
// 테스트용도로 프로그램을 작성해야 했는데 boost 의 asio, function,
// bind 를 함 써봤다. asio 는 뭐 워낙 문서 잘되있고 쓰기 쉬운놈이라
// 그냥 문서 따라하면 되고..
//
// 잠깐 재미삼아 드러운 코드를 만들어봤는데 대강 적어둔다. killer 란
// 이름은 좀 장난스러운 건데 manager 정도로 지을걸 그랬구먼.
//
// killer 는 일종의 매니저 클래스로 필요에 따라 session 을 생성하면서
// sessions 타입으로 적절히 관리(걍 들고만 있는다)를 하며 session
// 클래스는 소켓이 끊어지면 killer 의 어떤 메서드를 불러서 자신이
// 종료된다는것을 알리고 delete this 로 죽는다.
//
// 평소대로라면 session 에 killer 의 레퍼런스를 넘겨서 들고있다가
// 필요할때 이 레퍼런스를 통해서 killer 의 멤버를 불렀겠지...
//
// 그런데 이번에는 그냥 재미삼아서 session 클래스와 killer 클래스의
// 커플링을 제거해보려고 했다..
//
// OOP 식으로 접근하자면 매니저클래스의 추상클래스를 하나 만들어서
// killer 가 이를 상속받고 session 에는 추상클래스 포인터를 넘겨서
// session 가 구체클래스인 killer 가 아니라 추상클래스를 보도록
// 하겠지...
//
// 그건 별 재미가 없고 이번에는 killer 가 session 이 부를 자신의
// 메서드를 하나의 함수객체로 포장해서 session 에 넘겨버리고 session 은
// 이놈만 부르면 되도록 해봤다. killer 입장에서는 session 을 만들때
// session 이 상황에 따라 부를 함수들을 지정할수 있는 장점이 있고
// session 입장에서는 함수객체 라는 아주 추상화된 타입만 바라보면 되니
// 쓸데없이 클래스 끼고 상속받는것보다는 좀더 깔끔하다 하겠다.
//
// 1. killer::insert_session 에서 새로운 session 인스턴스를 만들어야
// 겠지. 이때 생성자 안에 필요한 모든 정보를 때려박으려
// 했었다. 그런데 이 정보중에는 session 인스턴스의 포인터가
// 포함되어있고, 이는 생성자가 끝나야만 유효한 값이니 결국 생성자
// 말고 또 다른 함수 session::set_finalizer 를 만들수밖에 없었다.
//
// 2. 필요한 작업은 하나인데 함수가 둘(new, set_finalizer) 로
// 쪼개졌으니 이걸 하나로 묶어야 했다. session 의 생성자를 private
// 로 감춰버리고 make 라는 스태틱 멤버를 추가해서 이놈이
// 생성자역할을 대신 하도록 했다. 물론 set_finalizer 로 받을
// 함수객체도 이놈이 받아야했다.
//
// 3. 이거 좀 억지스러운 코드지만.. session::make 에
// killer::remove_session 를 넘기기 위해 바인딩을 할때 마찬가지로
// session 의 포인터가 필요한 문제는 여전히 남아있다. 다행이 잘난
// boost::bind 가 콤포징을 지원하기 때문에 간단히 해결. _1 를 일단
// 바인딩 해서 넘기고 make 안에서 포인터를 마저 바인딩 하도록
// 했다. 당연히 이런 식의 코드는 공유코드를 만들때는 주의하도록
// 해야겠지.
//
// 4. session::make 에서는 먼저 session 을 생성하고 그 포인터에 인자로
// 받은 반쪽함수를 콤포징 해서 하나의 완전한 함수객체를 만들어
// 기억해두고 소멸될때 부르도록 했다.
//
// 중요.
//
// session 을 shared_ptr 로 들고다니지 않은 이유는 session 을 내가
// 강제적으로 delete 하는 구조라 그런것. 이런식의 구조라면
// shared_ptr 가 객체가 이미 delete 된걸 모르게 되니 당연히 죽는다.
//
// delete this 를 버리고 소멸을 shared_ptr 에 의존하는 식으로
// 수정하거나 weak_ptr 을 쓰면 해결이 가능하다.
//
// 물론.. 위에서 언급한 모든 작업들은 그다지 복잡한 작업이 아니므로
// 이렇게 boost 질을 한다는것 자체가 오버헤드. asio 는 libevent 를
// 쓰는것에 비해 월등한 장점이 있으니 잘 활용을 해야겠지만
// bind,function 등의 과도한 사용은 피하도록 하자.
#include <iostream>
#include <string>
#include <list>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/function.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
using namespace std;
namespace asio = boost::asio;
namespace posix_time = boost::posix_time;
using boost::array;
using boost::asio::ip::tcp;
using boost::bind;
using boost::function;
using boost::lexical_cast;
using boost::shared_ptr;
using boost::system::error_code;
typedef shared_ptr<tcp::socket> socket_ptr;
// 연결된 소켓을 받아서 그냥 들고만 있는다. 좀더 정확히 말하면 소켓
// 끊어지는것을 감시해야 하니 async_read_some 을 걸어두고 읽히는
// 데이타는 그냥 무시한다. 만약 소켓이 끊어지면 killer 의 특정 메서드를
// 불러야 하는데 이때 killer 와 의존성을 완전히 없애기 위해서 function
// 을 써봤다.
class session
{
public:
static session* make(socket_ptr& sock, function<void(session*)> fun)
{
session* s = new session(sock);
s->set_finalizer(bind(fun,s));
return s;
}
~session()
{
finalizer_();
}
private:
function<void()> finalizer_;
socket_ptr sock_;
array<char, 8> buf_;
session(socket_ptr& sock) : sock_(sock)
{
readmore();
}
void set_finalizer(const function<void()>& fun)
{
// 이걸 원래 생성자에 넣었어야 모양이 더 좋은데 생성자가 끝나야
// 이놈의 포인터가 떨어지기 때문에 포인터까지 포함해서 바인딩을
// 할수가 없었다. 그렇다고 function<void(session)> 타입을
// 받아서 후에 finalizer_(this)) 뭐 이딴식으로 부르는건 별
// 재미없는 코드라... 별수없이 생성자와는 별도로 함수가
// 추가되었다.
//
// 물론 생성자와 set_finalizer 는 항상 붙어다녀야 하는 놈이므로
// 생성자에 해당하는 함수 make 를 만들어주고 생성자는 private
// 으로 빼돌렸다.
finalizer_ = fun;
}
void readmore()
{
sock_->async_read_some(asio::buffer(buf_),
bind(&session::handle_read_some,
this,
asio::placeholders::error,
asio::placeholders::bytes_transferred));
}
void handle_read_some(const error_code& error, size_t readed)
{
if(error)
{
// 소켓이 끊어지면 delete this
cerr << "handle_read_some: " << error.message() << endl;
delete this;
return;
}
cout << "(unexpected) incoming data" << endl;
readmore();
}
};
// `host:port` 에 `interval` 간격으로 tcp 연결을 최대 `count` 개까지
// 하는놈
class killer
{
typedef list<session*> sessions;
public:
killer(asio::io_service& io_service,
const string& host,
int port,
int count,
int interval) : timer_(io_service),
target_(asio::ip::address::from_string(host), port),
count_(count),
interval_(interval)
{
set_timer();
}
private:
asio::deadline_timer timer_;
tcp::endpoint target_;
int count_;
int interval_;
sessions sessions_;
// 타이머 세팅
void set_timer()
{
timer_.expires_from_now(posix_time::milliseconds(interval_));
timer_.async_wait(bind(&killer::handle_timer, this, asio::placeholders::error));
}
// 타이머핸들러. 새 연결을 시도한다.
void handle_timer(const error_code& error)
{
if(error)
{
cerr << "handle_timer: " << error.message() << endl;
return;
}
socket_ptr sock(new tcp::socket(timer_.io_service()));
sock->async_connect(target_,
bind(&killer::handle_connect,
this,
sock,
asio::placeholders::error));
}
// 컨넥션 핸들러. 떨어진 소켓으로 새 세션을 만들고 필요하다면 다시 타이머를 세팅한다
void handle_connect(socket_ptr& sock, const error_code& error)
{
if(error)
{
cerr << "handle_connect: " << error.message() << endl;
return;
}
insert_session(sock);
if(sessions_.size() < count_)
set_timer();
}
// 소켓으로 새 세션을 만들어 기억해둔다.
void insert_session(socket_ptr& sock)
{
session* ses = session::make(sock,
bind(&killer::remove_session,
this,
_1));
sessions_.push_back(ses);
dump_session_counts();
}
// 세션을 삭제한다.
void remove_session(session* ses)
{
sessions_.remove(ses);
dump_session_counts();
}
// `현재세션`/`최대세션` 출력
void dump_session_counts()
{
cout << "sessions: " << sessions_.size() << "/" << count_ << endl;
}
};
int main(int argc, char* argv[])
{
try
{
if(argc != 5)
{
cout << "Usage: foo <host> <port> <count> <interval>" << endl;
return 1;
}
string host(argv[1]);
int port(lexical_cast<int>(argv[2]));
int count(lexical_cast<int>(argv[3]));
int interval(lexical_cast<int>(argv[4]));
asio::io_service io_service;
killer k(io_service, host, port, count, interval);
io_service.run();
}
catch(exception& e)
{
cerr << e.what() << endl;
}
}
추가.
어쩌다보니 연결 받는놈도 필요해서 만들었다. 걍 같이 적어둔다.
펼쳐두기..
// tcpkiller 로 찔러볼수있는 놈. 즉 여러개의 소켓을 accept 만 해보는
// 간단한 프로그램. java 로 만들어진 서버가 컨넥션을 몇개 못받길래 OS
// 환경상의 문제가 아닌가 해서 만들어본것.
#include <iostream>
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/array.hpp>
#include <boost/bind.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;
// idle 세션이 몇개나 떴는지는 다른 클래스에 일일히 보고하지 않고 그냥
// 스태틱 멤버를 둬서 생성 소멸시 카운팅 하도록 했다. asio 에 thread 를
// 하나만 썼기때문에 따로 락을 걸지는 않았다.
class idle
{
public:
idle(socket_ptr sock) : sock_(sock)
{
++instances_;
dump_idle_counts();
readmore();
}
~idle()
{
--instances_;
dump_idle_counts();
}
private:
socket_ptr sock_;
array<char, 8> buf_;
static size_t instances_;
void readmore()
{
sock_->async_read_some(asio::buffer(buf_),
bind(&idle::handle_read_some,
this,
asio::placeholders::error,
asio::placeholders::bytes_transferred));
}
void handle_read_some(const error_code& error, size_t readed)
{
if(error)
{
cerr << error.message() << endl;
delete this;
return;
}
readmore();
}
void dump_idle_counts()
{
cout << "idle: " << instances_ << endl;
}
};
size_t idle::instances_ = 0;
template<class T>
class async_acceptorT
{
public:
async_acceptorT(asio::io_service& io_service, const tcp::endpoint& endpoint)
: acceptor_(io_service, endpoint)
{
start_accept();
}
private:
tcp::acceptor acceptor_;
void start_accept()
{
socket_ptr childsocket(new tcp::socket(acceptor_.io_service()));
acceptor_.async_accept(*childsocket,
bind(&async_acceptorT::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 T(childsocket);
start_accept();
}
};
int main(int argc, char* argv[])
{
try
{
int port = argc == 2 ? lexical_cast<int>(argv[1]) : 9413;
cout << "listen: " << port << endl;
asio::io_service io_service;
async_acceptorT<idle> target(io_service, tcp::endpoint(tcp::v4(), port));
io_service.run();
}
catch(exception& e)
{
cerr << e.what() << endl;
}
}