Cокеты и особенности их реализации в библиотеке wxWidgets

Здравствуй, дружок!
Сегодня я расскажу тебе сказку про сокеты и особенности их реализации в библиотеке wxWidgets. Ты, наверное, уже знаешь что такое сокеты и, уверен, знаешь о том, что с их помощью можно творить чудеса.

Итак, сказка начинается…
Жили были юзвери и была у них локаль домовая… только локаль была у них непродвинутая… без ICQшного или Jabber’овского сервера…. по сему общались юзвери исключительно net send’ом…. (чего уж тут поделаешь)

Был у них еще не то админ, не то программер…. кто его разберет… за длинным хаером (волосами от англ. hair) и бородой все-равно ничего не видно.

В общем надоел юзверям net send и начали они к своему админу приставать:
- Мы тебя, - говорять, - пивом кормим? Кормим. Давай ставь нам софтину чтобы чатиться можно было, а то как-то не весело, когда посреди кина или в самый разар игры в сапёра или Mario выскакиваеть надоедливое окошко и говорить «NET SEND» smile.gif

Ну, админ почесал репу, подумал, потом раздал юзверям по подзатыльнику, по ходу назвав маздайными ламерами и сказал «Черт с вами, будеть вам чат»

Пошел админ к себе в админку, закрылся и начал думать… как бы это так поставить чатилку чтобы поменьше системных ресурсов ела и чтобы еще под Линухом работала и чтобы писать поменьше да настраивать не надо было.

А про Линух это я чего сказал… хоть юзвери и маздайные были, но у некоторых на машинках Линух стоял (к слову сказать. они в Линухе все-равно не шарили, но зато как игрушку или какую-нить программулину запустить знали)

В общем думал наш админ-думал и решил… «А забабахаю я им чат на wxWidgets» (гы-ы-ы-ы) «Оно потом и настройки не будт требовать, а wx у меня все-равно уже установлен»
Сбегал он в гамазин (энто магазин по-нашенски, все-равно в сказке всё не как у людей. по сему пусть так называется), затарился пивом и печеньком, закрылся у себя в админке, отключил телефон и начал програмить…

Первым делом принялся наш герой за сервак… т.к. энто чудо на его же машинке и должно было стоять…. а себе любимому надо все в первую очередь делать… юзвери подождут.

<!--c1-->
CODE
<!--ec1-->
#include <wx/wx.h>
#include <wx/splitter.h>
#include <wx/socket.h>
#include <wx/list.h>
#include <wx/valgen.h>
#include <wx/listimpl.cpp>
#include <math.h>

/// Создаем новый тип списков – список указателей на сокеты
WX_DECLARE_LIST(wxSocketBase, wxSocketList);
WX_DEFINE_LIST(wxSocketList);

class MyFrame : public wxFrame
{    
    // Здесь у нас будут отображаться сообщения
    wxTextCtrl * m_LOGTextCtrl;    
    // Здесь будет список юзверей
wxListBox * m_ClientListBox;
// Переменная для отправки сообщения (временный буфер)
    wxString m_MessageStr;
    // Список клиентских сокетов
    wxSocketList m_Clients;
    // Серверный сокет
    wxSocketServer * m_SocketServer;
public:
    MyFrame(wxWindow * parent);    
    ~MyFrame();
    // Этот метод выводит список клиентов
    void ShowClientList();
    DECLARE_EVENT_TABLE()    
    void OnExit(wxCommandEvent & event);
    void OnMessageInput(wxCommandEvent & event);
    void OnServerSocketEvent(wxSocketEvent & event);
    void OnClientSocketEvent(wxSocketEvent & event);
};

enum
{
    ID_LOG_TEXT_CTRL,
    ID_MESSAGE_TEXT_CTRL,
    ID_CLIENT_LIST_BOX,
    ID_SOCKET_SERVER,
    ID_SOCKET_CLIENT
};

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(wxID_EXIT, MyFrame::OnExit)
EVT_TEXT_ENTER(ID_MESSAGE_TEXT_CTRL, MyFrame::OnMessageInput)
EVT_SOCKET(ID_SOCKET_SERVER, MyFrame::OnServerSocketEvent)
EVT_SOCKET(ID_SOCKET_CLIENT, MyFrame::OnClientSocketEvent)
END_EVENT_TABLE()

MyFrame::MyFrame(wxWindow * parent)
: wxFrame(parent, -1, _("Test Server"), wxDefaultPosition, wxSize(600, 450))
{        
    SetExtraStyle(wxWS_EX_VALIDATE_RECURSIVELY);

    wxMenuBar * menuBar = new wxMenuBar();
    SetMenuBar(menuBar);
    wxMenu * fileMenu = new wxMenu();
    fileMenu->Append(wxID_EXIT, _("Exit\tAlt+F4"));
    menuBar->Append(fileMenu, _("File"));
    wxBoxSizer * sizer = new wxBoxSizer(wxVERTICAL);
    SetSizer(sizer);        

    // Это у нас такой сплиттер. Позволяет изменять размеры поля сообщений и списка юзверей мышкой
    wxSplitterWindow * splitter_window = new wxSplitterWindow(this, wxID_ANY);
    sizer->Add(splitter_window, 1, wxEXPAND|wxALL, 5);
    m_LOGTextCtrl = new wxTextCtrl(splitter_window, ID_LOG_TEXT_CTRL, wxEmptyString,
        wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxTE_MULTILINE|wxNO_BORDER);
    m_LOGTextCtrl->SetBackgroundColour(*wxBLACK);
    m_LOGTextCtrl->SetForegroundColour(*wxGREEN);    
    wxArrayString dummy;
    m_ClientListBox = new wxListBox(splitter_window, ID_CLIENT_LIST_BOX, wxDefaultPosition,
        wxSize(150, -1), dummy, wxNO_BORDER);
    splitter_window->SplitVertically(m_LOGTextCtrl, m_ClientListBox, splitter_window->GetSize().GetWidth()-150);
    splitter_window->SetMinimumPaneSize(50);
    splitter_window->SetSashGravity(1.0);
    wxTextCtrl * message_text_ctrl = new wxTextCtrl(this, ID_MESSAGE_TEXT_CTRL, wxEmptyString,
        wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
    message_text_ctrl->SetValidator(wxGenericValidator(&m_MessageStr));
    sizer->Add(message_text_ctrl, 0, wxGROW|wxLEFT|wxRIGHT|wxBOTTOM, 5);
    // Ага... здесь мы создаем серверный сокет
    // Сначала настраиваем порт 3000
    wxIPV4address addr;
    addr.Service(3000);
    // Потом создаем новый объект сокета с привязкой к конкретному порту
    m_SocketServer = new wxSocketServer(addr);
    // Если создалось, то говорим что «всё ОК»
    if (!m_SocketServer->Ok())
    {
        m_LOGTextCtrl->AppendText(_("Не удалось запустить сервер...\r\n"));
        return;    
    }
    else
    {
        m_LOGTextCtrl->AppendText(_("Работаем...\r\n"));
    }
    // Указываем, что сообщения от сокета будут обрабатываться нашей формой
    m_SocketServer->SetEventHandler(*this, ID_SOCKET_SERVER);
    // Указываем что обрабатываться будет толкьо соединение нового клиента
    m_SocketServer->SetNotify(wxSOCKET_CONNECTION_FLAG);
    // Говорим что уже можно обрабатывать события
    m_SocketServer->Notify(TRUE);

    CreateStatusBar();
        
    Centre();
}

MyFrame::~MyFrame()
{
    // При закрытии формы чистим список клиентов
    for(wxSocketList::Node * node = m_Clients.GetFirst(); node; node = node->GetNext())
    {
        node->GetData()->Destroy();
    }
    // И закрываем серверный сокет
    m_SocketServer->Destroy();
}

void MyFrame::ShowClientList()
{
    m_ClientListBox->Clear();
    wxIPV4address addr;
    wxSocketBase * sock;
    // А здесь мы просто добавляем в список адреса всех наших клиентов
    for(wxSocketList::Node * node = m_Clients.GetFirst(); node; node = node->GetNext())
    {
        sock = node->GetData();
        sock->GetLocal(addr);
        m_ClientListBox->Append(addr.IPAddress());
    }

}

void MyFrame::OnMessageInput(wxCommandEvent & event)
{
    TransferDataFromWindow();
    // Если нам есть что переслать
    if(m_MessageStr.IsEmpty()) return;
    wxSocketBase * sock;
    // Дописываем что это сообшение от сервера
    m_MessageStr = _("Server> ")+m_MessageStr;
    // Отсылаем сообщение всем клиентам из списка
    for(wxSocketList::Node * node = m_Clients.GetFirst(); node; node = node->GetNext())
    {
        sock = node->GetData();
        if(!sock) continue;
        // Не забываем что для UNICODE-сборки размер символа может быть 2 байта, поэтому учитываем размер wxChar
        sock->Write(m_MessageStr.GetData(), m_MessageStr.Length()*sizeof(wxChar));
    }
    m_LOGTextCtrl->AppendText(m_MessageStr+wxString(wxT("\r\n")));
    m_MessageStr = wxEmptyString;
    TransferDataToWindow();
}

void MyFrame::OnServerSocketEvent(wxSocketEvent & event)
{
    wxSocketBase *sock;
    wxIPV4address addr;
    switch(event.GetSocketEvent())
    {
    case wxSOCKET_CONNECTION:
        // Зааццептить нового клиента
        sock = m_SocketServer->Accept(FALSE);
        if (!sock) return;  
        // Указать что обрабатывать сообшение от этого сокета будет наша форма
        sock->SetEventHandler(*this, ID_SOCKET_CLIENT);
        // Указать что обрабатываться будет получение данных и дисконнект
        sock->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
        // Говорим что уже можно обрабатывать события
        sock->Notify(TRUE);
        // Выводим в ЛОГ что новый клиент добавлен
        sock->GetLocal(addr);
        m_Clients.Append(sock);
        m_LOGTextCtrl->AppendText(wxString::Format(_("Новый клиент: %s\r\n"), addr.IPAddress()));
        // Выводим список клиентов
        ShowClientList();
        break;
    }
}

void MyFrame::OnClientSocketEvent(wxSocketEvent & event)
{
    // Пока здесь пусто… смотрим дальше
}

void MyFrame::OnExit(wxCommandEvent & event)
{
    Close();
}

class MyApp : public wxApp
{
public:
    virtual bool OnInit();
};

IMPLEMENT_APP(MyApp)

bool MyApp::OnInit()
{
    MyFrame * frame = new MyFrame(NULL);    
    frame->Show();    
    SetTopWindow(frame);
    return true;
}
<!--c2-->
<!--ec2-->

Жмакнул Build – скомпилилось… это хорошо… можно браться за клиента:

<!--c1-->
CODE
<!--ec1-->
#include <wx/wx.h>
#include <wx/socket.h>
#include <wx/valgen.h>

class MyFrame : public wxFrame
{    
    wxTextCtrl * m_LOGTextCtrl;        
    wxString m_MessageStr;    
    // Это наш клиентский сокет
    wxSocketClient * m_SocketClient;
public:
    MyFrame(wxWindow * parent);    
    ~MyFrame();    
    DECLARE_EVENT_TABLE()    
    void OnExit(wxCommandEvent & event);
    // Обработчик нажатия ENTER в поле ввода сообщения
    void OnMessageInput(wxCommandEvent & event);    
    // Обработчик событий от сокета
    void OnClientSocketEvent(wxSocketEvent & event);
    // Обработчики для установки параметров контролов в зависимости от состояния приложения
    void OnMessageTextCtrlUpdateUI(wxUpdateUIEvent & event);
    void OnConnectButtonUpdateUI(wxUpdateUIEvent & event);
    void OnDisconnectButtonUpdateUI(wxUpdateUIEvent & event);
    // Обработчик кнопки КОННЕКТ
    void OnConnect(wxCommandEvent & event);
    // Обработчик кнопки ДИСКОННЕКТ
    void OnDisconnect(wxCommandEvent & event);
};

enum
{
    ID_LOG_TEXT_CTRL,
    ID_MESSAGE_TEXT_CTRL,        
    ID_CONNECT,
    ID_DISCONNECT,
    ID_SOCKET_CLIENT
};

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(wxID_EXIT, MyFrame::OnExit)
EVT_TEXT_ENTER(ID_MESSAGE_TEXT_CTRL, MyFrame::OnMessageInput)
EVT_SOCKET(ID_SOCKET_CLIENT, MyFrame::OnClientSocketEvent)
EVT_UPDATE_UI(ID_MESSAGE_TEXT_CTRL, MyFrame::OnMessageTextCtrlUpdateUI)
EVT_UPDATE_UI(ID_CONNECT, MyFrame::OnConnectButtonUpdateUI)
EVT_UPDATE_UI(ID_DISCONNECT, MyFrame::OnDisconnectButtonUpdateUI)
EVT_BUTTON(ID_CONNECT, MyFrame::OnConnect)
EVT_BUTTON(ID_DISCONNECT, MyFrame::OnDisconnect)
END_EVENT_TABLE()

MyFrame::MyFrame(wxWindow * parent)
: wxFrame(parent, -1, _("Test Client"), wxDefaultPosition, wxSize(600, 450)), m_SocketClient(NULL)
{        
    SetExtraStyle(wxWS_EX_VALIDATE_RECURSIVELY);

    wxMenuBar * menuBar = new wxMenuBar();
    SetMenuBar(menuBar);
    wxMenu * fileMenu = new wxMenu();
    fileMenu->Append(wxID_EXIT, _("Exit\tAlt+F4"));
    menuBar->Append(fileMenu, _("File"));
    wxBoxSizer * sizer = new wxBoxSizer(wxVERTICAL);
    SetSizer(sizer);        
    
    m_LOGTextCtrl = new wxTextCtrl(this, ID_LOG_TEXT_CTRL, wxEmptyString,
        wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxTE_MULTILINE);
    sizer->Add(m_LOGTextCtrl, 1, wxEXPAND|wxALL, 5);
    m_LOGTextCtrl->SetBackgroundColour(*wxBLACK);
    m_LOGTextCtrl->SetForegroundColour(*wxGREEN);        
    wxTextCtrl * message_text_ctrl = new wxTextCtrl(this, ID_MESSAGE_TEXT_CTRL, wxEmptyString,
        wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
    message_text_ctrl->SetValidator(wxGenericValidator(&m_MessageStr));
    sizer->Add(message_text_ctrl, 0, wxGROW|wxLEFT|wxRIGHT|wxBOTTOM, 5);
    wxBoxSizer * buttonsizer = new wxBoxSizer(wxHORIZONTAL);
    wxButton * connect_btn = new wxButton(this, ID_CONNECT, _("Connect"));
    wxButton * disconnect_btn = new wxButton(this, ID_DISCONNECT, _("Disonnect"));
    buttonsizer->Add(connect_btn, 0, wxRIGHT, 5);
    buttonsizer->Add(disconnect_btn, 0);
    sizer->Add(buttonsizer, 0, wxALIGN_RIGHT|wxALL, 5);

    CreateStatusBar();
    Centre();
}

MyFrame::~MyFrame()
{
    // При удалении формы, закрываем наш сокет
    if(m_SocketClient)
    {
        m_SocketClient->Destroy();
    }
}

// Эти обработчики вызываются при любом обновлении ГУИ
// С помощью wxUpdateUIEvent можно настраивать внешний вид и состояние контролов в зависимости от определенных параметров
// Изменения вступают в силу для всех контролов с указанным идентификатором
void MyFrame::OnMessageTextCtrlUpdateUI(wxUpdateUIEvent & event)
{
    // Сделать поле ввода сообщения неактивным если нет соединения (смотрим в Event Table на идентификатор контрола для этого обработчика)
    event.Enable(m_SocketClient != NULL);
}

void MyFrame::OnDisconnectButtonUpdateUI(wxUpdateUIEvent & event)
{
    // кнопка ДИСКОННЕКТ активна толкько когда есть соединение
    event.Enable(m_SocketClient != NULL);
}

void MyFrame::OnConnectButtonUpdateUI(wxUpdateUIEvent & event)
{
    // кнопка КОННЕКТ активна толкько когда нет соединения
    event.Enable(m_SocketClient == NULL);
}

void MyFrame::OnMessageInput(wxCommandEvent & event)
{
    TransferDataFromWindow();
    if(m_MessageStr.IsEmpty()) return;    
    if(!m_SocketClient) return;
    // Отослать сообщение
    m_SocketClient->Write(m_MessageStr.GetData(), m_MessageStr.Length()*sizeof(wxChar));
    m_MessageStr = wxEmptyString;
    TransferDataToWindow();
}

void MyFrame::OnConnect(wxCommandEvent & event)
{
    // Если мы вдруг нажали на кнопку КОННЕКТ когда соединение уже установлено, то выход (вобще, таукого не должно случиться...)
    if(m_SocketClient) return;
    // Показываем диаложек для ввода адреса (или хоста) сервера
    wxString addr_str = wxGetTextFromUser(_("Введите адрес сервера:"), _("Соединение"), wxT("localhost"));
    // Настраиваем адрес для подключения
    wxIPV4address addr;
    addr.Service(3000);
    addr.Hostname(addr_str);
    // Создаем сокет
    m_SocketClient = new wxSocketClient;
    // Привязываем его к нашей форме
    m_SocketClient->SetEventHandler(*this, ID_SOCKET_CLIENT);
    m_SocketClient->SetNotify(wxSOCKET_CONNECTION_FLAG|wxSOCKET_INPUT_FLAG);
    m_SocketClient->Notify(TRUE);
    // Если все прошлоу дачно, то…
    if(m_SocketClient)
    {
        // Пытаемся сконнектиться с сервером
        m_SocketClient->Connect(addr, false);
        // Ожидание соединения 10 сек.
        m_SocketClient->WaitOnConnect(10);
        // Если соединение установлено...
        if(m_SocketClient->IsConnected())
        {
            // Говорим что все ОК
            m_LOGTextCtrl->AppendText(_("Соединение выполнено...\r\n"));
        }
        else
        {
            m_LOGTextCtrl->AppendText(_("Не удалось выполнить соединение...\r\n"));
        }
    }
}

void MyFrame::OnDisconnect(wxCommandEvent & event)
{
    // При нажатии на кнопку ДИСКОННЕКТ прерываем соединение и удаляем сокет
if(m_SocketClient)
    {
        if(m_SocketClient->IsConnected()) m_SocketClient->Close();
        m_SocketClient->Destroy();
    }
    m_SocketClient = NULL;
}

void MyFrame::OnClientSocketEvent(wxSocketEvent & event)
{    
    // Пока здесь пусто… смотрим дальше
}

void MyFrame::OnExit(wxCommandEvent & event)
{
    Close();
}

class MyApp : public wxApp
{
public:
    virtual bool OnInit();
};

IMPLEMENT_APP(MyApp)

bool MyApp::OnInit()
{
    MyFrame * frame = new MyFrame(NULL);    
    frame->Show();    
    SetTopWindow(frame);
    return true;
}
<!--c2-->
<!--ec2-->

Запустил… работает… и даже коннектится smile.gif

- БУГАГА!!!, - Наверное скажешь ты, дорогой друг, и будешь совершенно прав. Стабильный коннект – залог здоровья и душевного равновесия любого АйТишника… но коннект – это еще не всё. Надо же было еще организовать передачу данных smile.gif

Для этого наш админ в проекте сервера дописал обработчик событий от клиентских сокетов

<!--c1-->
CODE
<!--ec1-->
void MyFrame::OnClientSocketEvent(wxSocketEvent & event)
{    
    wxSocketBase *sock = event.GetSocket();
    wxIPV4address addr;
    wxChar buffer[1024];
    sock->GetLocal(addr);
    wxString message;
    switch(event.GetSocketEvent())
    {
        // Если к нам пришло сообщение
        case wxSOCKET_INPUT:
        {           
            // Прочитали его в буфер
            sock->Read(buffer, 1024*sizeof(wxChar));
            // Если ошибка - поругались
            if(sock->Error())
            {
                m_LOGTextCtrl->AppendText(_("Ошибка чтения данных\r\n"));
            }
            else
            {
                // Дописали в конец нолик
                buffer[(size_t)ceil((double)(sock->LastCount()/sizeof(wxChar)))] = 0;
                // А в начало – от кого пришло
                message = wxString::Format(wxT("%s> %s"), addr.IPAddress(), buffer);
                // Добавили в ЛОГ
                m_LOGTextCtrl->AppendText(message+wxString(wxT("\r\n")));
                // Раздали всем клиентам
                for(wxSocketList::Node * node = m_Clients.GetFirst(); node; node = node->GetNext())
                {
                    node->GetData()->Write(message.GetData(), message.Length()*sizeof(wxChar));
                }
            }
            break;
        }
        case wxSOCKET_LOST:
        {
            // Если дисконнект – удалили из списка
            for(wxSocketList::Node * node = m_Clients.GetFirst(); node; node = node->GetNext())
            {
                if(sock == node->GetData())
                {
                    m_Clients.DeleteNode(node);
                    break;
                }
            }        
            // Убили сокет    
            sock->Destroy();
            // Записали в ЛОГ
            m_LOGTextCtrl->AppendText(wxString::Format(_("Дисконнект: %s\r\n"), addr.IPAddress()));
            break;
        }
        default:;
  }
}
<!--c2-->
<!--ec2-->

Скомпилил – вроде работает… не ругается.. но чтобы проверить, надо еще добавить обработчик и в клиентское приложеньице… да… без этого никак…

<!--c1-->
CODE
<!--ec1-->
void MyFrame::OnClientSocketEvent(wxSocketEvent & event)
{    
    wxSocketBase *sock = event.GetSocket();
    wxIPV4address addr;
    wxChar buffer[1024];
    switch(event.GetSocketEvent())
    {
        // Если пришло сообщение
        case wxSOCKET_INPUT:
        {       
            // Прочитали
            sock->Read(buffer, 1024*sizeof(wxChar));
            // Поругались, если ошибка
            if(sock->Error())
            {
                m_LOGTextCtrl->AppendText(_("Ошибка чтения данных\r\n"));
            }
            else
            {
                // Дописали в конец нолик
                buffer[(size_t)ceil((double)(sock->LastCount()/sizeof(wxChar)))] = 0;
                // Отобразили
                m_LOGTextCtrl->AppendText(wxString::Format(wxT("%s\r\n"), buffer));
            }
            break;
        }
        case wxSOCKET_LOST:
        {
            // Если дисконнект
            sock->GetLocal(addr);
            // Обнулили наш сокет
            if(sock == m_SocketClient)
            {
                m_SocketClient = NULL;
            }
            // Удалили
            sock->Destroy();            
            // Сказали что произошел дисконнект
            m_LOGTextCtrl->AppendText(wxString::Format(_("Дисконнект: %s\r\n"), addr.IPAddress()));
            break;
        }
        default:;
  }
}
<!--c2-->
<!--ec2-->

Скомпилил – вроде работает… к тому же неплохо…


Отнес юзверям, заинсталил, попрыгал для виду с бубном вокруг компутеров, забрал у одного ящик пива, у другого рыбки, у третьего печенько (а как же, за все хорошее надо платить… да и юзвери вроде не против были) и вернулся к себе в админку спать (завтра ему еще предстоял тяжелый день.. т.к. глупость человеческая не знает границ, а юзвери, ведь, они тоже люди smile.gif)


Перепечатка или использование сего творения где-либо кроме www.realcoding.net без письменного согласия автора ЗАПРЕЩАЕТСЯ
Copyright © Vladimir (T-Rex) Tryapichko

Источник: "wxWidgets programming" by t_rex



Опубликовал admin
3 Окт, Вторник 2006г.



Программирование для чайников.