Авторские статьи Клиент-сервер на WinSock

Discussion in 'Статьи' started by --StraNger--, 20 Apr 2009.

  1. --StraNger--

    --StraNger-- Member

    Joined:
    4 Jan 2009
    Messages:
    63
    Likes Received:
    57
    Reputations:
    5
    Привет всем юным кодерам! Сегодня я расскажу о написании клиент серверного приложения на winapi библиотеке Winsock.
    Как известно клиент-серверное приложение состоит из клиента и сервера. Клиент у нас будет программой визуальной, т.е. это центр управление, с которого мы будет посылать команды не визуальному серверу, который будет из анализировать и выполнять.
    Что касается компилятора то я буду использовать Borland c++. (Я уже вижу, как в меня летят тухлые помидоры =)).
    Начнем с простого – клиент.
    Как ты помнишь это программа визуальная. На главной форме должны присутствовать такие компоненты:
    Текстовое поле для ввода IP адреса(Edit).
    Кнопки для подключение и отключения сервера(Button).
    Кнопки для управлением сервера (Button). (На ваш вкус)
    [​IMG]
    Да, и не забудь подключить библиотеку:
    Code:
    #include
    
    Первое что бы сделаем это подключение к серверу.
    Code:
    {
    WSADATA wsd;
    If (WSAStartup(MAKEWORD(1,1), &wsd) != 0) // загрузка библиотеки и проверка на //ошибки
    {
    // если ошибки есть, то проинформировать юзера
    Form1->Memo1->Lines->Add("Error load WinSock");
    return ;
    }
    // создание нового потока
    HANDLE hThread;
    DWORD idThread;
    hThread = CreateThread(NULL, 0, Thread, 0, 0, & idThread);
    }
    
    Что тут происходит? Да пока ничего. Мы просто загрузили библиотеке функцией
    WSAStartUp и создали новый поток, в нем мы уже будет создавать сокет и соединяться с сервером. В первом аргументе функции WSAStartUp мы указали MAKEWORD(1,1) т.е. версия библиотеки 1. Второй аргумент это указатель на структуру WSADATA. В ней будет храниться информация о библиотеки.
    Итак, поток создан, теперь можно работать с ним.
    Сначала нужно объявить глобально следующие переменные
    Code:
    SOCKET sClient;
    int ret, i;
    struct sockaddr_in server;
    struct hostent *host = NULL;
    bool status;
    
    По ходу дела мы разберемся, что к чему.
    А вот и сам поток, что бы программа правильно проинициализировала его, поток нужно добавить до того кода, что я привел выше.
    Code:
    
    DWORD WINAPI Thread(LPVOID lpParam)
    {
    
    char* szServerName= new char;
    szServerName="12345";
    szServerName=strcpy(szServerName,(Form1->Edit1->Text.c_str()));
    
    //создание сокета
    sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sClient == INVALID_SOCKET)
    {
    MessageBox(0, "Can't create socket", "Error", 0);
    return 1;
    }
    //заполненеие структуры sockaddr_in
    server.sin_family = AF_INET;
    server.sin_port = htons(7070);
    server.sin_addr.s_addr = inet_addr(szServerName);
    
    //само соеденение
    
    if (connect(sClient, (struct sockaddr *)&server,
    sizeof(server)) == SOCKET_ERROR)
    {
    
    Form1->Memo1->Lines->Add("connect failed");
    return 1;
    }
    char szRecvBuff[1024];
    //принимаем данные
    ret = recv(sClient, szRecvBuff, strlen(szRecvBuff), 0);
    if (ret == SOCKET_ERROR)
    {
    
    Form1->Memo1->Lines->Add("recv failed");
    }
    // выводим данные
    String q=szRecvBuff;
    Form1->Memo1->Lines->Add(q);
    status=true;
    
    }
    
    Перед основным кодом мы заносим в переменную szServerName значение текстового поля, в который пользователь введет ip адрес.
    Сначала создаем сокет функцией socket.
    Первый аргумент это семейство протоколов.
    Нам нужен TCP, так что указываем AF_INET.
    Второй аргумент этот спецификация для сокета, т.е. что мы с ним будем делать.
    Нам нужно установить соединение и передать данные, поэтому указываем SOCK_STREAM.
    Третий аргумент собственно сам протокол. IPPROTO_TCP соответствуют протоколу TCP.
    Теперь нам нужно заполнить структуру типа sockaddr_in. В ней храниться информация о созданном сокете. Напомню что переменую (server) данного типа (sockaddr_in) мы объявили глобально. В процессе заполнения структуры нам встречается функция htons.
    Она осуществляет перевод целого короткого числа из порядка байт, принятого на компьютере, в сетевой порядок байт.
    Ну и само соединение. Функция connect. Первый параметр это созданный сокет.
    Второй – указатель на структуру типа sockaddr_in третий размер этой структуры.
    Если все пройдет успешно, то можно принять данные функцией recv и вывести в текстовое поле. Первый аргумент функции recv это созданный сокет, второй – буфер который мы предварительно создали, третий аргумент это размер буфера (функция strlen возвращает размер строки). И, наконец, последний аргумент это флаги, определяющие метод получения. Они нам не нужны, поэтому указываем 0.
    В конце кода мы приводим логическую переменную status в значение true. Это нужно чтобы впоследствии избежать ненужного повторного подключения.
    Кстати, так как мы работаем с классно string в начале файла нужно добавить
    Code:
    #include
    
    Мы научились соеденяться с сервером, теперь нужно добавить функцию отправки команд.
    Для этого мы напишам функцию GetComand, ее единственным аргументом будет команда которую нужно отправить.
    Code:
    void GetCommand(char* str){ /
    //если мы подлючены то…
    if(status==true)
    {
    char* szMessage= new char;
    szMessage="0000";
    // копируем в буферв вписаную нами команду
    strcpy(szMessage, str);
    // отправка команды
    ret = send(sClient, szMessage, strlen(szMessage), 0);
    if (ret == SOCKET_ERROR)
    {
    } Form1->Memo1->Lines->Add("send failed");
    
    
    //получение ответа
    char szRecvBuff[1024];
    ret = recv(sClient, szRecvBuff, 1024, 0);
    if (ret == SOCKET_ERROR)
    {
    
    Form1->Memo1->Lines->Add("recv failed");
    }
    String q=szRecvBuff;
    //вывод ответа
    Form1->Memo1->Lines->Add(q);
    }
    else
    {
    //если мы не подключены то осведомить пользователя
    Form1->Memo1->Lines->Add("The server is not connected");
    
    }
    }
    
    Сначала происходит проверка переменной status. Если она равна true то мы подключены и можно выполнять дальнейший код.
    А далее мы копируем в буфер szMessage значение переменной str.
    И потом отправляем это значение серверу функцией send;
    Первый ее аргумент это сокет второй это буфер третий размер буфера.
    После этого мы получаем ответ функцией recv он аналогична send.
    После этого выводим ответ в текстовое поле.
    Ответы мы будем формировать сами при разработке сервера.
    Замечу, что в коде есть проверка на ошибки.
    Функция готова к использованию.
    Например при нажатии на кнопку «Перезагрузка» нужно выполнить такой код:
    Code:
    {
    GetCommand("RB");
    }
    
    Кстати смотри не запутайся в командах т.к. в сервера из нужно будет анализировать.
    Вот мои:
    «Поменять кнопки мыши» -«AMB»
    «Блокировать стол» - «BT»
    «Сообщение» - «MSG»
    Итак, простейший клиент готов. Подведем итог.
    Клиент может подключаться к серверу отправлять и принимать команды. Пока это все.

    Теперь можно приступить к разработке сервера.
    Как я уже говорил сервер это программа не визуальная.
    В нем, как и в клиенте нужно загрузить бибилиотеку создать сокет и подключиться.
    Так же мы встретимся с привязкой к локльному адресу (функция bind) и прослушиванию порта (функция listen).
    Опять же глобально нужно объявить следущие переменные:
    Code:
    SOCKET sServerListen, sClient;
    struct sockaddr_in localaddr,
    clientaddr;
    HANDLE hThread;
    DWORD dwThreadId;
    int iSize;
    WSADATA wsaData;
    
    
    Итак начнем вот код главной функции:
    Code:
    {
    //загрузка библиотеки
    WSAStartup(MAKEWORD(1, 1), &wsaData);
    Создание сокета
    sServerListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    заполнение струтуры адреса
    localaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    localaddr.sin_family = AF_INET;
    localaddr.sin_port = htons(7070);
    //привязка адреса к сокету
    bind(sServerListen, (struct sockaddr *)&localaddr,sizeof(localaddr));
    //простушиваниче порта
    listen(sServerListen, 2);
    while (1)
    {
    iSize = sizeof(clientaddr);
    //разрешение на соеденение
    sClient = accept(sServerListen, (struct sockaddr *)&clientaddr,&iSize);
    //создание потока для приема и отправки данных
    hThread = CreateThread(NULL, 0, ClientThread,(LPVOID)sClient, 0, &dwThreadId);
    CloseHandle(hThread);
    }
    closesocket(sServerListen);
    return 0;
    }
    
    До заполнения структуры адреса все идет так же. После этого вызываеться функция bind.
    Она служит для связи сокета с адресом.
    Т.е. первым аргументом мы передали сокет, вторым указатель на структуру с адресом, третим – размер структуры.
    Теперь сокет готов к работе и можно прослушивать порт на соеденение со стороны клиента.
    Функция listen. У нее два параметра. Сокет и кол-во клиентов которые могут подключиться к серверу. Т.к. сервер должен постоянно находиться в памяти и принимать соеденения, то мы запускаем бесконечный цикл.
    Функция accert проверяет подключающийся клиент и разрешает соеденения.
    Первый параметр это сокет. Второйуказатель на структуру типа sockaddr ну и третий размер структуры. sClient будет указателем на новый сокет.
    После этого создаем новый поток для обмена данными.
    Вот этот поток:

    Code:
    DWORD WINAPI ClientThread(LPVOID lpParam)
    {
    
    SOCKET sock=(SOCKET)lpParam;
    
    
    string a="";
    char szRecvBuff[1024],szSendBuff[1024] ;
    int ret;
    strcpy(szSendBuff,"Server connect!");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    
    
    while(1)
    {
    //Получаем данные
    ret = recv(sock, szRecvBuff, 1024, 0);
    
    if (ret == 0)
    break;
    else if (ret == SOCKET_ERROR)
    {
    MessageBox(0, "Recive data filed", "Error", 0);
    break;
    }
    szRecvBuff[ret] = '\0';
    //записываем в стринговую переменную для дальнейшей обработки
    a=szRecvBuff;
    //Проверка команд
    //если команда перезагрузки то…
    if(a=="RB")
    {
    ExitWindowsEx(EWX_REBOOT, 0);
    strcpy(szSendBuff,"Computer reboot =) ");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    
    }
    else
    //если команда поменять кнопки мышки то…
    if(a=="AMB")
    {
    
    SwapMouseButton(false);
    
    strcpy(szSendBuff,"The buttons on the mouse were displaced");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    }
    Else
    //если команда для вывода сообщения то…
    if(a=="MSG")
    {
    MessageBox(0, " It is time to you to be hung up", "Error", 0);
    strcpy(szSendBuff," The message is sent");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    
    }
    Else
    //если команда для блокирования стола
    if(a=="BT")
    {
    HWND h=GetDesktopWindow();
    EnableWindow(h, FALSE);
    strcpy(szSendBuff,"The working table is blocked");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    
    }
    
    
    }
    
    Сначала мы передаем клиенту сообщение о том, что сервер успешно подключен и готов к работе. Далее мы получаем команды и записываем полученную команду в стринговую переменную (не забудь добавить #include ) для того, что бы дальше ее проанализировать. Команд у нас не много поэтому здесь происходит проверка условием.
    Надеюсь, смысл команд понятен. Замечу, что после выполнения каждой команды мы посылаем клиенту уведомление об успешности выполнения. Например, вот участок кода:
    Code:
    if(a=="BT")
    {
    HWND h=GetDesktopWindow();
    EnableWindow(h, FALSE);
    strcpy(szSendBuff,"The working table is blocked");
    send(sock, szSendBuff, sizeof(szSendBuff), 0);
    }
    
    После того как стол заблокирован мы отправляем клиенту сообщение ,"The working table is blocked" – что означает рабочий стол заблокирован.
    Все мы подготовили простейший макет клиент – сервера и даже внедрили некоторые функции. Чтое еще можно зделать? Все информация которую мы передаем в winsock передаеться байтами поэтому можно передать любой файл. Например скриншот экрана.
    Если подумать, то можно организовать даже потоковое видео. Вот примерный алгоритм

    на стороне сервера храним текущий кадр и прошлый, сверяем разницу между ними, получаем битовую маску, она сжимается нефигово, потом передаем изменившиеся части(только их, сплошным потоком) клиент уже смотря по маске подставляет эти части в нужные места
    Правда программно я это не проверял. Может ты зделаешь?
    Это все. До свидания.

    ps в приведенных кодах есть несколько ошибок с точки зрения программирование, которые я так и не удосужился исправить
    ©StraNger
     
    #1 --StraNger--, 20 Apr 2009
    Last edited: 20 Apr 2009
    2 people like this.
  2. fucker"ok

    fucker"ok Elder - Старейшина

    Joined:
    21 Nov 2004
    Messages:
    580
    Likes Received:
    279
    Reputations:
    91
    Да вообщем то ничего нового. Просто пересказ того, чего уже итак полно в инете.