Статья
Версия для печати
Обсудить на форуме
Построение клиент-серверного приложения на основе класса CAsyncSocket (MFC).
Часть 1.


Автор: Алексей1153.
Дата написания: 27.12.2009.
Права на статью принадлежат автору и Клубу программистов «Весельчак У».

Содержание.


Предисловие.

Каждый раз, создавая приложение с клиент-серверной архитектурой, я был вынужден повторять одни и теже действия, приводящие к появлению классов сервера и клиента — каждый раз со своими особенностями, но по структуре похожими. Появилось желание написать набор классов, где будет собрана вся рутина. Таким образом, не будет тратиться время (которое есть деньги) на повторение каждый раз общей части данной архитектуры. Кроме того, код классов клиента и сервера будет не так загружен, ведь рутина остаётся в родительских классах.
В этой части статьи будет реализована только структура клиент-сервер, работа же по дрессировке передаваемых и принимаемых данных будет проведена во второй части. Пока для передачи данных можно будет пользоваться прямыми вызовами методов CAsyncSocket::Send и CAsyncSocket::Receive. (Us_Send() и Us_Receive() соответственно). Но такой неструктурированный поток данных будет приниматься (а иногда и передаваться) нестабильно. Файлы классов (ASocket1153.cpp и ASocket1153.h) в том виде, в каком они сделаны в рамках первой части статьи, можно найти здесь.

Клиент-серверная архитектура приложения.

Для начала глянем, что пишет энциклопедия о данной архитектуре:

Цитата: Материал из Википедии — свободной энциклопедии
Клиент-сервер (англ. Client-server) — сетевая архитектура, в которой устройства являются либо клиентами, либо серверами. Клиентом (front end) является запрашивающая машина (обычно ПК), сервером (back end) — машина, которая отвечает на запрос. Оба термина (клиент и сервер) могут применяться как к физическим устройствам, так и к программному обеспечению.

При построении клиент-серверной архитектуры приложения неизменно используются следующие элементы создаваемой системы:
  • Клиент (обозначение далее — 1CL). Объект, который подключается к серверу.
  • Сервер (обозначение далее — SRV). Объект, осуществляющий ожидание запроса на подключение от Клиента (1CL) и подключающий его к одному из элементов серверного массива пАр для Клиентов.
  • Серверный клиент (обозначение далее — sCL). Этот объект является элементом серверного массива клиентов. В этом массиве содержатся «точки» (или «клиентские пАры») для подключения Клиентов (1CL).



Рисунок 1.


Установление постоянного соединения Клиента с Сервером типично для протокола TCP/IP, который и будет использоваться в наших классах.

Сокеты.

В API и сервер, и клиент представлены одним объектом: Windows — SOCKET. И название у него, собственно, сокет. Сокет — это виртуальный «разъём» для подключения по сети. Впрочем, оба «конца» сокета (клиент и сервер) могут находиться и на одной и той же машине.
Работа с объектом сокета на уровне API — довольно трудоёмкое занятие. Но этого и не потребуется, так как мы используем библиотеку MFC, а в ней имеется класс-обёртка CAsyncSocket. Этот класс инкапсулирует объект SOCKET, а также предоставляет уже готовые виртуальные функции-обработчики сообщений Windows, отправляемых сокету.:

Код: (C++)
        //сообщение WM_SOCKET_NOTIFY, lParam==FD_READ
        //1CL или sCL получил новые данные от пАры — нужно их принять
        virtual void OnReceive(int nErrorCode);

        //сообщение WM_SOCKET_NOTIFY, lParam==FD_WRITE
        //1CL или sCL отправляет данные через Send
        virtual void OnSend(int nErrorCode);

        //сообщение WM_SOCKET_NOTIFY, lParam==FD_OOB
        //1CL или sCL отправляет данные out-of-band (данные с высоким приоритетом) получил
        virtual void OnOutOfBandData(int nErrorCode);

        //сообщение WM_SOCKET_NOTIFY, lParam==FD_ACCEPT
        //SRV получил («подслушал»)запрос на соединение от 1CL
        virtual void OnAccept(int nErrorCode);

        //сообщение WM_SOCKET_NOTIFY, lParam==FD_CONNECT
        //1CL получил извещение о том, что сервер подключил его к sCL методом Accept
        virtual void OnConnect(int nErrorCode);

        //сообщение WM_SOCKET_NOTIFY, lParam==FD_CLOSE
        //1CL или sCL получил извещение о том, что его (сокет) закрыли
        virtual void OnClose(int nErrorCode);

CAsyncSocket, однако, предоставляет достаточно низкий уровень работы с SOCKET, что делает возможным организовать более гибкую работу с сокетом, чем, к примеру, в ещё одном классе MFC — CSocket:public CAsyncSocket.
Класс CAsyncSocket я выбрал в качестве родительского для набора классов, которые будут описаны в данной статье.

Структура набора классов.

Все свои классы я с некоторых пор стал размещать в пространстве имён ALX1153, и эти тоже не будут исключением. Набор объявлений классов имеет следующую структуру:

Код: (C++)
namespace ALX1153
{
        //константы и вспомогательные структуры и классы
        namespace ASocketH
        {
                //класс пользовательских данных клиента,
                //используемый по умолчанию
                class VoidUSERDATA
                {
                };
                ...
                ...
        };

        //родитель для сервера и клиента
        class ASocketParent:public CAsyncSocket
        {
                ...
                ...
        };

        //класс для 1CL
        class ASocketClient:public ASocketParent
        {
                ...
                ...
        };

        //класс для SRV
        template<class USERDATA=ASocketH::VoidUSERDATA>
        class ASocketServer:public ASocketParent
        {
                //класс для sCL
                class CsCL:public ASocketClient
                {
                        friend class ASocketServer;

                        USERDATA m_UserData;            //пользовательские данные
                        ASocketServer* m_pParentServer;//указатель на сервер

                        public:
                        //добыть указатель на пользовательские данные
                        USERDATA* Us_GetUserData() const
                        {
                                return (USERDATA*)&m_UserData;
                        }
                        ...
                        ...
                };

                private:
                //класс-контейнер указателя на CsCL для помещения в std::map
                struct CsCL_container
                {
                        private:
                        CsCL* m_p;
                        ...
                        ...
                        CsCL_container(ASocketServer* pParentServer)
                        {
                                m_p=new CsCL(pParentServer);
                        }

                        void operator=(const CsCL_container& src)
                        {
                                m_p=src.m_p;
                                ((CsCL_container*)&src)->m_p=0;
                        }

                        ~CsCL_container()
                        {
                                if(m_p)
                                {
                                        delete m_p;
                                        m_p=0;
                                }
                        }
                        ...
                        ...
                };

                private:
                //ServerClientsMap — массив для хранения серверных клиентов
                typedef std::map<int,CsCL_container> td_ClientsMap;
                td_ClientsMap m_ServerClientsMap;
                ...
                ...
        };
};

Все три класса (ASocketClient, ASocketServer и ASocketServer::CsCL) имеют общего родителя, произведённого, в свою очередь, от CAsyncSocket. Также,  из данного кода видно, что серверный клиент (sCL) во многом будет похож на одиночный клиент (1CL). Оно и понятно, ведь 1CL и sCL — это два конца одного соединения. Различаются они только тем, что один из них «живёт» сам по себе, а другой содержится в массиве сервера.
Обычно в классе клиента содержатся пользовательские данные (самые простые примеры: название подключения и его идентификатор). С одиночным клиентом всё понятно — можно (и нужно) от него произвести потомка и там спокойно объявить любые пользовательские данные в виде членов класса. С серверным же клиентом такой номер не пройдёт: мало того, что напрямую его использовать нельзя, ведь им управляет сервер, так и сам класс объявлен приватным внутри класса сервера. По этой причине класс сервера ASocketServer сделан шаблонным — можно указать класс используемых пользовательских данных, и будет объявлена член-переменная нужного типа внутри sCL (можно также не указывать тип вообще, тогда применится пустой тип по умолчанию — ASocketH::VoidUSERDATA ). Доступ к переменной всегда есть из виртуальных функций ASocketServer (о них далее). На тип USERDATA накладываются некоторые условия: в нём должны быть определены
  • Конструктор по умолчанию;
  • Оператор =;
  • Конструктор копирования.
Вспомогательный класс CsCL_container позволит размещать объекты CsCL в массиве, в качестве которого выбран класс

Код: (C++)
typedef std::map<int,CsCL_container> td_ClientsMap;

Без вспомогательного контейнера объект класса CsCL мог быть бесконтрольно пересоздан при манипуляциях с массивом, а значит SOCKET в его составе может внезапно разрушиться. А такой бардак нам не нужен. Назначение CsCL_container также видно из его оператора присваивания — при присваивании происходит не копирование указателя на клиент, а его передача.

Описание пространства имён ALX1153::ASocketH.

В пространстве имён ALX1153::ASocketH содержатся константы, вспомогательные функции и структуры. Роль пространства имён ASocketH — избавить пространство ALX1153 от «замусоривания» (но это уже совсем другая статья, то есть, тема). Назначение элементов (здесь перечислено не всё):

Константы.

e_srv_clients_max_count — определяем максимальное возможное количество одновременно подключенных к серверу клиентов. В самом классе сервера можно динамически менять число разрешённых клиентов от 0 до этой константы.
e_net_MaxOnceSendLen — максимальный размер данных одного пакета, отправляемого через CAsyncSocket::Send. Менять это значение с 0x1000 на другое настоятельно НЕ РЕКОМЕНДУЮ, но в теории можно установить от 1 до 0xffff-1.

Классы.

CTimeOut позволяет отсчитать необходимый таймаут в миллисекундах. В классе используется функция GetTickCount(), возвращающая количество миллисекунд, прошедших с момента запуска системы Windows). Это значение максимально покрывает интервал в примерно 49 дней 17 часов (0xffffffff мс). Этого более чем достаточно для наших задач передачи и приёма данных. После переполнения счётчик начинается с нуля — это в CTimeOut тоже учитывается при подсчёте прошедшего времени. Однако если счётчик переполнится два и более раз, то интервал будет посчитан с ошибкой (остаток от деления на 49 дней 17 часов), но это уже совсем фантастика для нашего случая.
VoidUSERDATA — тип пользовательских данных по умолчанию в серверном клиенте.

Описание класса ALX1153::ASocketParent.

Класс производится от CAsyncSocket. Роль класса ASocketParent — быть родительским для классов всех трёх элементов архитектуры — сервера (SRV), клиента(1CL) и серверного клиента (sCL). Назначение элементов (здесь перечислено не всё):

Константы.

e_sendTOdefault — таймаут по умолчанию при попытке отправить данные.
e_FlashTO — период задержки горения экранчика трафика, если не было передачи/приёма новых данных.

Переменные.

m_LastSetEventMask хранит последнюю установленную маску событий (через функцию CAsyncSocket::AsyncSelect).
m_DataWereSentTO и m_DataWereRecvTO — таймауты, запускаемые после отправки/приёма данных. Используются в функции клиента, рисующей экранчики трафика.

Описание класса ALX1153::ASocketClient.

Класс Клиента (1CL). Роль класса: от него производится «свой» пользовательский класс — клиент. Класс ASocketClient является абстрактным, некоторые его виртуальные функции должны быть определены в классе пользовательского клиента. Назначение элементов (здесь перечислено не всё):

Переменные.

m_bIsConnected — показывает, что клиент подключен.
m_bBadDisconnect — показывает, что произошёл «нештатный» разрыв связи с сервером (то есть, не по желанию одной из сторон, иначе это был бы «штатный» разрыв связи.)
m_Statistics хранит статистику о передаваемых и принимаемых данных.;//статистика передачи и приёма данных.

Функции интерфейса пользователя («Us_...»).



Рисунок 2.


Код: (C++)
//вспомогательная процедура для рисования текущего состояния мониторчиков трафика
Us_CalcMonitorsRect(CDC& destDC,const CRect& LayoutRect)const;

CalcMonitorsRect рисует в указанном прямоугольнике текущее состояние в виде классических моргающих мониторчиков, расположенных один за другим. Как видите, для рисования мониторчиков делать ничего дополнительно не требуется — только вызывать данную функцию с определённым периодом. «Инертность» моргания мониторчиков задаётся константой e_FlashTO. Перерисовывать экранчики, следовательно, имеет смысл примерно с интервалом в e_FlashTO/2.

Код: (C++)
//определение того, что сокет подключен
bool Us_IsClient__Connected();

//подключение к серверу по IP адресу (в виде строки) или доменному имени
bool Us_StartWorkingClient(const char* pchAddress,WORD wServerPort,bool bPauseEventsBeforeWorking);

//отключение от сервера и закрытие сокета
void Us_StopWorking() const;

//проверка, включено ли получение события приёма данных
bool Us_GetIsEventOn_OnReceive();

//приостановить получение событий приёма/передачи/подключения
void Us_PauseEvents();

//возобновить получение событий приёма/передачи/подключения
void Us_StartEvents();

//получить адрес клиента-пАры
bool Us_GetPeerIP(DWORD& dwdIP,WORD& wPort)const;
bool Us_GetPeerIP(CString& csIP,WORD& wPort)const;

//возвращает константу ASocketH::e_net_MaxOnceSendLen
static int Us_GetMaxOneSendLen();

//проверка того, что сокет был некорректно отключен
//(связь разорвалась по непонятным причинам)
bool Us_IsClient__BadDisconnectedAfterLastConnect()const;

//проверить объект SOCKET (хендл сокета) на валидность
bool Us_IsWorking_checkJustHandle()const;

//обычные функции отсылки и приёма
int Us_Send(const void* lpBuf, int nBufLen,int nFlags=0)const;
int Us_Receive(void* lpBuf,const int nBufLen,int nFlags=0);

О функциях подкачки сообщений сокета (Us_Polling_...) расскажу позднее, сразу с примерами.

Чистые виртуальные функции («VF_...»).



Рисунок 3.


Код: (C++)
//вызывается из Send при каждой неудачной попытке послать данные
//(вернее, когда (CAsyncSocket::Send()==SOCKET_ERROR и
//this->GetLastError()==WSAEWOULDBLOCK)
//dwdTicksPassed — мс, примерно сколько прошло с начала попытки отправить
virtual bool VF_FeedBackFromSend_RetTrueIfNeedCancel(DWORD dwdTicksPassed)=0;

//вызывается после закрытия клиента
virtual void VF_OnAfterClose()=0;

//вызывается после подключения клиента сервером (когда сервер вызывает Accept)
virtual void VF_OnConnected()=0;

//вызывается, когда имеются данные для приёма (но ещё не приняты -
//это надо сделать в этом обработчике)
virtual void VF_OnReceive()=0;

Описание класса ALX1153::ASocketServer<>.

Класс Сервера.  Назначение элементов (здесь перечислено не всё):

Функции интерфейса пользователя («Us_...»).



Рисунок 4.


Код: (C++)
//проверить соответствие индекса и указателя на серверный клиент в массиве клиентов
bool Us_CheckClientPointerEqualsTheIndex(DWORD Index_zb,const CsCL* cli_test);

//проверка сокета на валидность хендла
bool Us_IsWorking_checkJustHandle()const;

//проверка сокета на подключенность
bool Us_IsClient__Connected(const CsCL* cli);
bool Us_IsClient__Connected(DWORD Index_zb);

//определение факта «нештатного» разрыва связи
bool Us_IsClient__BadDisconnectedAfterLastConnect(DWORD Index_zb);

//получить указатель на серверный клиент по индексу
const CsCL* Us_GetClientByIndex(DWORD Index_zb);

//задать максимальное количество клиентов
void Us_SetClientCount(DWORD ClientsMax_in);

//достать текущее максимальное количество клиентов
DWORD Us_GetClientsCount();

//запуск сервера
bool Us_StartWorkingServer(
        WORD wServerPort,
        int nClientsCountLimit,
        bool bPauseEventsBeforeWorking
        );

//остановка сервера
void Us_StopWorking();

Чистые виртуальные функции («VF_...»).



Рисунок 5.


Код: (C++)
//вызывается, когда клиент отключился
virtual void VF_OnAfterClose(const CsCL* sCL)=0;

//вызывается, когда клиент подключился
virtual void VF_Accepted(const CsCL* sCL)=0;

//вызывается, сервер запущен
virtual void VF_SRV_OnAfterServerCreated()=0;

//вызывается из Send при каждой неудачной попытке послать данные
//(вернее, когда (CAsyncSocket::Send()==SOCKET_ERROR и
//this->GetLastError()==WSAEWOULDBLOCK)
//dwdTicksPassed — мс, примерно сколько прошло с начала попытки отправить
virtual bool VF_FeedBackFromSend_RetTrueIfNeedCancel(
        const CsCL* sCL,
        DWORD dwdTicksPassed
        )=0;

//вызывается, когда имеются данные для приёма (но ещё не приняты -
//это надо сделать в этом обработчике)
virtual void VF_OnReceive(const CsCL* sCL)=0;

Описание класса ALX1153::ASocketServer<>::CsCL.

Класс cерверного клиента (sCL). Описан, как приватный, внутри класса сервера, однако указатели на серверные клиенты передаются в виртуальные функции сервера. Назначение элементов (здесь перечислено не всё):

Функции интерфейса пользователя («Us_...»).

Набор функций класса CsCL (дополнительно  к функциям, унаследованным от ASocketClient):



Рисунок 6.


Код: (C++)
//добыть указатель на пользовательские данные
USERDATA* Us_GetUserData() const;

//проверка сокета на валидность хендла
bool Us_IsWorking_checkJustHandle()const

Пример объявления своего класса клиента и сервера.

(Пример показан в файле «ASocket1153.h».)
Пусть «свой» клиент будет иметь имя класса CMyClient, а свой сервер — CMyServer:

Код: (C++)
#pragma once
#include "ASocket1153.h"

class CMyClient:public ALX1153::ASocketClient
{
        virtual void VF_OnReceive()
        {
                //вручную читаем напрямую из сокета
                //Us_Receive(...);//опционально.
        }

        virtual void VF_OnAfterClose(){}
        virtual void VF_OnConnected(){}
        virtual bool VF_FeedBackFromSend_RetTrueIfNeedCancel(DWORD dwdTicksPassed)
        {
                if(dwdTicksPassed>e_sendTOdefault)
                {
                        TRACE("\r\n\r\nVF_FeedBackFromSend_RetTrueIfNeedCancel has stoped sending\r\n\r\n");
                        return true;
                }

                Sleep(1);
                return false;
        }

public:

        CMyClient()
        {
        }

        ~CMyClient()
        {
        }
};

Тип пользовательских данных для серверного клиента:

Код: (C++)
class CMyClientData
{
        ...
};

При объявления сервера тип пользовательских данных в клиентах указываем в параметре шаблона (напомню, что если пользовательских данных нет, то при объявлении сервера можно оставить треугольные скобки шаблона пустыми.)

Код: (C++)
class CMyServer:public ALX1153::ASocketServer<CMyClientData> //опционально.
{
        virtual void VF_OnReceive(const CsCL* sCL)
        {
                //вручную читаем напрямую из сокета
                //Us_Receive(...);//опционально.
        }
        virtual void VF_OnAfterClose(const CsCL* sCL){}
        virtual void VF_Accepted(const CsCL*){}

        virtual bool VF_FeedBackFromSend_RetTrueIfNeedCancel(const CsCL* sCL,DWORD dwdTicksPassed)
        {
                if(dwdTicksPassed>e_sendTOdefault)
                {
                        TRACE("\r\n\r\nVF_FeedBackFromSend_RetTrueIfNeedCancel has stoped sending\r\n\r\n");
                        return true;
                }

                Sleep(1);
                return false;
        }

        //извещение для сервера
        virtual void VF_SRV_OnAfterServerCreated(){}

public:

        CMyServer()
        {
        }

        ~CMyServer()
        {
        }
};

Запуск сервера:

Код: (C++)
CMyServer Server;
Server.Us_StartWorkingServer(1234,50,false);

Подключение клиента к серверу:

Код: (C++)
CMyServer Client;
Client.Us_StartWorkingClient("5.6.7.8",1234,false);

Подкачка сообщений при синхронном обмене.

Обычно сообщения для класса CAsyncSocket приходят асинхронно (то есть, в любой момент), и это обеспечивается тем, что где-то в недрах приложения происходит разбор очереди сообщений и для выбранных сообщений вызывается нужный обработчик. А понадобилось, к примеру, в некой функции F произвести синхронный обмен между клиентом и сервером. Если F вызвана в том же потоке, где производится обработка очереди сообщений (основной поток обычного приложения, например), то, пока не выйдем из функции F, очередь сообщений стоит и не обрабатывается.
Тут на помощь приходят функции клиента для подкачки сообщений из очереди.

Код: (C++)
//применяется, когда очередь сообщений не обрабатывается, а поймать
//событие подключения клиента к серверу необходимо. Выход из функции
//происходит по приходу нужного сообщения WM_SOCKET_NOTIFY(FD_CONNECT) или
//по истечению таймаута (false). Функция возвращает значение Us_IsClient__Connected()
//если callback есть и возвращает true, то опрос прерывается и возвращает false
bool Us_WaitOneConnectEvent(
        DWORD dwdTO=5000,
        DWORD dwdTimeStep=50,
        bool (*WaitCancelCallBack)(int percent,void* pData)=0,
        void* pData=0
        );

//применяется, когда очередь сообщений не обрабатывается, а поймать
//событие прихода данных к серверу необходимо. Выход из функции
//происходит по приходу сообщения WM_SOCKET_NOTIFY(FD_READ) или
//по истечению таймаута. Функция ничего не возвращает
//если callback есть и возвращает true, то опрос прерывается
//подходит для цельного сообщения, но не для разбитого на части.
bool Us_WaitOneReceiveEvent(
        DWORD dwdTO=10000,
        DWORD dwdTimeStep=50,
        bool (*WaitCancelCallBack)(int percent,void* pData)=0,
        void* pData=0
        );

//применяется, чтобы очистить очередь от «ненужных» сообщений
// WM_SOCKET_NOTIFY(FD_WRITE) void Us_WaitAndRemoveSendEvents();

Пример использования функций подкачки сообщений: получаем «авторизацию» от сервера в одной функции при помощи искусственно синхронного обмена.

Код: (C++)
//некая функция получения авторизации
bool GetAutoriz(CMyClient& Client)
{
        //вспомогательные callback-и
        struct s_show
        {
                static bool WaitingConnect(int percent,void* pData)
                {
                        TRACE("Подключение к серверу. Таймаут: %d%%\r\n",percent);
                        return false;
                }

                static bool WaitingReceive(int percent,void* pData)
                {
                        TRACE("Ожидание данных. Таймаут: %d%%\r\n",percent);
                        return false;
                }
        };

        TRACE("Подключаемся к серверу\r\n");
        if(!Client.Us_StartWorkingClient("localhost",1234,false))return false;

        TRACE("Ожидаем ассепт\r\n");
        if(!Client.Us_WaitOneConnectEvent(5000,100,s_show::WaitingConnect,this))return false;
        if(!Client.Us_IsClient__Connected())return false;
        TRACE("Подключились\r\n");

        TRACE("Отправка данных авторизаци\r\n");
        BYTE Q[10]={0,1,2,3,4,5,6,7,8,9};
        if(!Client.Us_Send(Q,sizeof(Q)))return false;

        //Удаляем из очереди сообщение отправки
        //Если этого не сделать, то это сообщение помешает
        //"увидеть" сообщение получения данных
        Client.Us_WaitAndRemoveSendEvents();

        TRACE("Ожидаем ответ\r\n");
        //сбрасываем флаг принятия ответа
        //(Флаг установится в Client.VF_OnReceive(), если данные-ответ верны )
        Client.m_bAnswerGot=false;
        if(!Client.Us_WaitOneReceiveEvent(5000,100,s_show::WaitingReceive,this))return false;
       
        //Сервер прислал данные.
        //Проверяем, взвёлся ли флаг
        if(!Client.m_bAnswerGot)return false;

        TRACE("аторизация получена!\r\n");
        return true;
}

Организационные вопросы.

Совместимость c Visual Studio 6.

Весь код классов практически без изменений является рабочим в Visual Studio 6, кроме некоторых модификаторов видимости. Для обеспечения нормальной работы класса в 6-й студии добавьте в файл «stdafx.h» следующие строки (но убедитесь, что эти три include-а STL не встречаются в файле «stdafx.h» ранее):

Код:
//добавлено в "stdafx.h" для совместимости классов
//ALX1153::ASocketParent, ... со средой MS Visual Studio 6
#pragma warning (disable : 4786)
#define ASocketUsesVS6
#include <vector>
#include <map>
#include <algorithm>

#pragma warning (disable : 4786) будет подавлять предупреждения о слишком длинных отладочных именах переменных (эти сообщения заполоняют вывод), а определение ASocketUsesVS6 меняет некоторые модификаторы доступа более мягкими, иначе 6-я студия не понимает некоторых конструкций (например, friend или доступ из дочернего класса к protected-членам родителя).
Основным элементом же здесь является определение #define ASocketUsesVS6.

Инициализация сокетов.

Не забывайте, что для того, чтобы приложение могло работать с сокетами, необходимо вызвать функцию инициализации ::AfxSocketInit() (можно без параметров) и, желательно, в начале функции InitInstance() приложения. Иначе функции сокета будут всё время возвращать ошибку WSANOTINITIALISED.

Общие правила пользования интерфейсом классов.

Пользователь имеет право вызывать ТОЛЬКО функции, имеющие префикс «Us_», иначе работоспособность класса может нарушится (ведь в случае с VS6 будут открыты и некоторые другие функции; а protected-функции, в принципе, доступны в любой версии студии из производного класса).
В «своих» клиенте и сервере, произведённых от ASocketClient и ASocketServer, пользователь должен переопределить чистые виртуальные функции, имеющие префикс «VF_».

Заключение.

Во второй части я предложу один из способов стабильной передачи и приёма потока данных, дополнив код набора наших классов. Также будет написан тестовый проект с графической визуализацией результатов работы классов.
Версия для печати
Обсудить на форуме