Лекции (1171139), страница 23
Текст из файла (страница 23)
Когда сервер готов обслужить очередной запрос,он использует функцию accept.int accept(int sockfd, void *addr, int *addrlen);Функция accept создаёт для общения с клиентом новый сокет и возвращает его дескриптор.Параметр sockfd задаёт слушающий сокет. После вызова он остаётся в слушающем состоянии иможет принимать другие соединения. В структуру, на которую ссылается addr, записываетсяадрес сокета клиента, который установил соединение с сервером.
В переменную, адресуемуюуказателем addrlen, изначально записывается размер структуры; функция accept записываеттуда длину, которая реально была использована. Если вас не интересует адрес клиента, выможете просто передать NULL в качестве второго и третьего параметров.Обратите внимание, что полученный от accept новый сокет связан с тем же самым адресом, чтои слушающий сокет. Сначала это может показаться странным. Но дело в том, что адрес TCPсокета не обязан быть уникальным в Internet-домене. Уникальными должны быть толькосоединения, для идентификации которых используются два адреса сокетов, между которымипроисходит обмен данными.Структура приложения-сервера TCPWSAStartupsocketbindlistenacceptsendrecvclosesocketclosesocketWSACleanupЗАДАНИЯРазбиться на пары: один участник пары пишет сервер, второй – клиент.1. Написать клиентское приложение протокола echo.2.
Написать клиентское приложение протокола daytime.3. Написать сервер протокола echo под TCP.4. Написать сервер протокола daytime под TCP.Функции обратного вызоваCallback (обратный вызов) — передача исполняемого кода в качестве одного из параметров другого кода.Обратный вызов позволяет в функции исполнять код, который задается в аргументах при её вызове. Этот кодможет быть определён в других контекстах программного кода и быть недоступным для прямого вызова из этойфункции. Некоторые алгоритмические задачи в качестве своих входных данных имеют не только числа илиобъекты, но и действия (алгоритмы), которые естественным образом задаются как обратные вызовы.Об обратном вызове можно думать как о действии, передаваемом некоторой основной процедуре вкачестве аргумента. И это действие может рассматриваться как• подзадача и использоваться для обработки данных внутри этой процедуры,• «телефонная связь», используемая для того, чтобы «связываться» с тем, кто вызвал процедуру, принаступлении какого-то события (англ.
callback дословно переводится как «звонок обратно»).Для лучшего понимания причин использования обратного вызова рассмотрим простую задачу выполненияследующих операций над списком чисел: напечатать все числа, возвести все числа в квадрат, увеличить все числана 1, обнулить все элементы. Ясно что алгоритмы выполнения этих четырёх операций схожи — это циклы обходвсех элементов списка с некоторым действием в теле цикла, применяемым к каждому элементу. Это несложныйкод, и в принципе можно написать его 4 раза. Но давайте рассмотрим более сложный случай, когда список у насхранится не в памяти, а на диске, и со списком могут работать несколько процессов одновременно и необходиморешать проблемы синхронизации доступа к элементам (несколько процессов могут выполнять разные задачи —удаления некоторых элементов из списка, добавления новых, изменение существующих элементов в списке).
Вэтом случае задача обхода всех элементов списка будет довольно сложным кодом, который не хотелось быпереписывать несколько раз. Правильнее создать функцию общего назначения для обхода элементов списка и дать83возможность программистам абстрагироваться от того, как именно устроен алгоритм обхода и писать лишьфункцию обратного вызова для обработки отдельного элемента списка.Рассмотрим использование функций обратного вызова на примере.typedef int (__stdcall *CompareFunction)(const byte*, const byte*);Ниже описана функция, принимающая в качестве параметров указатель на массив, числоэлементов массива, размер каждого элемента и указатель на метод сравнения элементовмассива.
Метод сравнения, в данном случае, есть функция обратного вызова.void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);Использование callback функции позволяет использовать одну и ту же функцию (Bubblesort)для сортировки, к примеру, как массивов целочисленных данных, так и массивов, элементамикоторых являются строки.void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc){for(int i=0; i < size; i++){for(int j=0; j < size-1; j++){// make the callback to the comparison functionif(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size)){// the two compared elements must be interchangedbyte* temp = new byte[elem_size];memcpy(temp, array+j*elem_size, elem_size);memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);memcpy(array+(j+1)*elem_size, temp, elem_size);delete [] temp;}}}}Для примера приведем код функций сравнений двух элементов типа int и char*.
Обе функции в качествепараметров принимают два сравниваемых аргумента.int __stdcall CompareInts(const byte* velem1, const byte* velem2){int elem1 = *(int*)velem1;int elem2 = *(int*)velem2;if(elem1 < elem2)return -1;if(elem1 > elem2)return 1;return 0;}int __stdcall CompareStrings(const byte* velem1, const byte* velem2){const char* elem1 = (char*)velem1;const char* elem2 = (char*)velem2;return strcmp(elem1, elem2);84}В коде основной программы нужно заметить, что функции сортировки передаются в виде адреса на началофункции (&CompareInts).int main(int argc, char* argv[]){int i;int array[] = {5432, 4321, 3210, 2109, 1098};cout << "Before sorting ints with Bubblesort\n";for(i=0; i < 5; i++)cout << array[i] << '\n';Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);cout << "After the sorting\n";for(i=0; i < 5; i++)cout << array[i] << '\n';const char str[5][10] = {"estella","danielle","crissy","bo","angie"};cout << "Before sorting strings with Bubblesort\n";for(i=0; i < 5; i++)cout << str[i] << '\n';Bubblesort((byte*)str, 5, 10, &CompareStrings);cout << "After the sorting\n";for(i=0; i < 5; i++)cout << str[i] << '\n';return 0;}В нашем случае в callback функцию можно вынести работа сервера с клиентом, после чего вызывать этуфункцию в отдельном потоке с помощью WinAPI функции CreateThread.HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId);Параметры функции CreateThread.LpThreadAttributes - является указателем на структуру LPSECURITY_ATTRIBUTES.
Дляприсвоения атрибутов защиты по умолчанию, передавайте в этом параметре NULL.DwStackSize - параметр определяет размер стека, выделяемый для потока из общего адресногопространства процесса. При передаче 0 - размер устанавливается в значение по умолчанию.LpStartAddress - указатель на адрес входной функции потока.LpParameter - параметр, который будет передан внутрь функции потока.DwCreationFlags - принимает одно из двух значений: 0 - исполнение начинается немедленно,или CREATE_SUSPENDED - исполнение приостанавливается до последующих указаний.LpThreadId - Адрес переменной типа DWORD в который функция возвращает идентификатор,приписанный системой новому потоку.Понятие потока.85Под словом «поток» имеется в виду «поток команд», то есть последовательность инструкций, которыесчитывает и исполняет процессор.
С каждым из потоков связан свой набор значений регистров процессора,называемый контекстом потока (thread context). Но кроме этого система создает еще ряд других объектов,однозначно связанных с каждым потоком. Важнейший из них (после контекста) – конечно же, стек (созданиесвоего стека для каждого потока возможно благодаря тому, что в контексте потока сохраняется, в том числе, ирегистр указателя стека, ESP). Для нас это важно, поскольку именно в стеке создаются и хранятся все локальныеили автоматические переменные. Поскольку у каждого потока свой стек, то у каждого потока имеется свойсобственный экземпляр локальной переменной, доступный только ему. Процесс – это объединение несколькихпотоков.
А объединяет эти потоки единое виртуальное адресное пространство. В этом пространстве размещаютсякод и данные приложения (обычно это один exe- и несколько dll-модулей). Именно единство этого пространства иделает обмен данными между потоками приложения предельно простым.При запуске программы ОС всегда создает один поток – главный. Именно в нем программа начинает своевыполнение. В зависимости от типа программы и настроек компилятора в главном потоке может выполнятьсяфункция main, WinMain, _tmain или функция, заданная пользователем.