Форум программистов, компьютерный форум, киберфорум
C++: Сети
Войти
Регистрация
Восстановить пароль
 
Рейтинг 4.67/6: Рейтинг темы: голосов - 6, средняя оценка - 4.67
8 / 3 / 0
Регистрация: 11.08.2016
Сообщений: 21
1

Winsock и цикл сообщений

07.09.2020, 02:19. Просмотров 1084. Ответов 6

В общем, делаю лабу универскую, пытаюсь разобраться с Winsock, но никак не могу понять, в чем проблема. Сначала скину код.
Сервер:
Кликните здесь для просмотра всего текста

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#define _WINSOCK_DEPRECATED_NO_WARNINGS
 
#include "Winsock2.h"                // заголовок  WS2_32.dll
#pragma comment(lib, "WS2_32.lib")   // экспорт  WS2_32.dll
 
#include <iostream>
#include <string>
 
std::string SetErrorMsgText(std::string msgText, int code);
std::string itos(int i);
 
union Converter // для перевода int в char[4] и обратно
{
    int number;
    char bytes[4];
};
 
int main()
{
    WSADATA wsaData;
    try
    {
        // инициализация библиотеки
        if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
            throw SetErrorMsgText("Startup:", WSAGetLastError());
 
        // создаем СОКЕТ ДЛЯ ПРОСЛУШИВАНИЯ ПОРТА и получаем его дескриптор
        SOCKET serverSocket = socket(
            AF_INET,        //[in]  формат адреса: TCP/IP - AF_INET, не должен быть = null!!!
            SOCK_STREAM,    //[in]  тип сокета: ориентированный на сообщения (UDP, SOCK_DGRAM) или на поток (TCP, SOCK_STREAM)
            IPPROTO_TCP     //[in]  протокол транспортного уровня. для tcp можно указать null
        );
        if (serverSocket == INVALID_SOCKET)
            throw SetErrorMsgText("socket:", WSAGetLastError());
 
        SOCKADDR_IN serv;                     // параметры  сокета sS
        serv.sin_family = AF_INET;            // используется IP-адресация  
        serv.sin_port = htons(2000);          // порт 2000
        serv.sin_addr.s_addr = INADDR_ANY;    // ответ присылать на IP-адрес: любой собственный IP-адрес 
 
        if (bind(serverSocket, (LPSOCKADDR)&serv, sizeof(serv)) == SOCKET_ERROR)
            throw SetErrorMsgText("bind:", WSAGetLastError());
        // запуск прослушивания сокетом порта
        if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) // SOMAXCONN - максимально возможная длина очереди подключений
            throw SetErrorMsgText("socket can't listen to port: ", WSAGetLastError());
        // теперь клиентская прог может вызывать connect() для подключения
 
        // переменные для работы с клиентом
        SOCKADDR_IN clientSocketInfo;       // параметры сокета клиента
        int size = sizeof(SOCKADDR_IN);     // размер структуры этих параметров
        SOCKET clientSocket;                // СОКЕТ ДЛЯ ОБМЕНА ДАННЫМИ
 
        memset(&clientSocketInfo, 0, sizeof(clientSocketInfo)); // обнулить память
        
 
        std::cout << "Server is listening to port 2000...\n";
        // получаем дескриптор сокета для обмена данными по КАНАЛУ
        clientSocket = accept(serverSocket,                 // [in]  дескриптор связанного сокета
                            (sockaddr*)&clientSocketInfo,   // [out] указатель на sockaddr
                            &size);             // [out] указатель на длину структуры sockaddr
 
        if (clientSocket == INVALID_SOCKET)
            throw SetErrorMsgText("socket: ", WSAGetLastError());
 
        // служебная инфа
        std::cout << "IP address: " << inet_ntoa(clientSocketInfo.sin_addr) << std::endl << "Port: " << htons(clientSocketInfo.sin_port) << std::endl;
 
        Converter dataSize;
        char* buffer;
 
        for(int i = 0; i < 1000; i++) {
            // принимаем размер будущих данных
            char* cSize = new char[4]; // 4 байта = int
            memset(cSize, 0, 4);
            int byteCounter = 0;
            byteCounter = recv(clientSocket, cSize, sizeof(int), NULL); // 4-й аргумент: NULL - после получения входной буфер очищается, MSG_PEEK - входной буфер не очищается
            //byteCounter = recv(clientSocket, cSize, sizeof(int), NULL); // 4-й аргумент: NULL - после получения входной буфер очищается, MSG_PEEK - входной буфер не очищается
            if (byteCounter == SOCKET_ERROR)
                throw SetErrorMsgText("recv: ", WSAGetLastError());
            std::cout << "MESSAGE SIZE = " << byteCounter << std::endl;
            
            // конвертируем
            dataSize.bytes[0] = cSize[0];
            dataSize.bytes[1] = cSize[1];
            dataSize.bytes[2] = cSize[2];
            dataSize.bytes[3] = cSize[3];
 
 
            buffer = new char[dataSize.number];
            memset(buffer, 0, dataSize.number);
            std::cout << "Getting " << dataSize.number << " bytes...\n";
 
            byteCounter = 0;
            while (byteCounter < dataSize.number)
            {
                byteCounter += recv(clientSocket, buffer, dataSize.number, MSG_PEEK); // 4-й аргумент: NULL - после получения входной буфер очищается, MSG_PEEK - входной буфер не очищается
                if (byteCounter == SOCKET_ERROR)
                    throw SetErrorMsgText("recv: ", WSAGetLastError());
            }
            std::cout << buffer << std::endl << "Bytes got: " << byteCounter << std::endl;
            delete[] buffer;
            delete[] cSize;
        }
 
        // закрываем сокеты
        if (closesocket(clientSocket) == SOCKET_ERROR)
            throw SetErrorMsgText("closing client socket error:", WSAGetLastError());
        if (closesocket(serverSocket) == SOCKET_ERROR)
            throw SetErrorMsgText("closing server socket error:", WSAGetLastError());
 
        // выгрузка библиотеки
        if (WSACleanup() == SOCKET_ERROR)
            throw SetErrorMsgText("Cleanup:", WSAGetLastError());
    }
    catch (std::string errorMsgText)
    {
        std::cout << std::endl << std::endl << errorMsgText;
        getchar();
    }
    std::cout << "DONE.";
    getchar();
    return 0;
}
 
std::string GetErrorMsgText(int code)    // cформировать текст ошибки 
{
    std::string msgText;
    switch (code)                      // проверка кода возврата  
    {
    case WSAEINTR: msgText = "WSAEINTR"; break;
    case WSAEACCES: msgText = "WSAEACCES"; break;
    case WSAEFAULT: msgText = "WSAEFAULT"; break;
    case WSAEINVAL: msgText = "WSAEINVAL"; break;
        //..........коды WSAGetLastError ..........................
    case WSASYSCALLFAILURE: msgText = "WSASYSCALLFAILURE"; break;
    default: msgText = "UNKNOWN ERROR. CODE: " + itos(code); break;
    };
    return msgText;
};
 
std::string SetErrorMsgText(std::string msgText, int code) { return  msgText + GetErrorMsgText(code); };
 
#include <sstream>
std::string itos(int i) // convert int to string
{
    std::stringstream s;
    s << i;
    return s.str();
}

Клиент:
Кликните здесь для просмотра всего текста

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
 
#include "Winsock2.h"                // заголовок  WS2_32.dll
#pragma comment(lib, "WS2_32.lib")   // экспорт  WS2_32.dll
 
#include <iostream>
#include <string>
 
std::string SetErrorMsgText(std::string msgText, int code);
// Преобразование int в string
std::string itos(int i);
 
 
union Converter // для перевода int в char[4] и обратно
{
    int number;
    char bytes[4];
};
 
int main()
{
    WSADATA wsaData;
    try
    {
        // инициализация библиотеки
        if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
            throw SetErrorMsgText("Startup:", WSAGetLastError());
 
        // создаем СОКЕТ ДЛЯ ОТПРАВЛЕНИЯ и получаем его дескриптор
        SOCKET clientSocket = socket(
            AF_INET,        //[in] формат адреса: TCP/IP - AF_INET, не должен быть = null!!!
            SOCK_STREAM,    //[in] тип сокета: ориентированный на сообщения (UDP, SOCK_DGRAM) или на поток (TCP, SOCK_STREAM)
            IPPROTO_TCP     //[in] протокол транспортного уровня. для tcp можно указать null
        );
        if (clientSocket == INVALID_SOCKET)
            throw SetErrorMsgText("socket:", WSAGetLastError());
 
        SOCKADDR_IN serverInfo; // параметры сокета сервера
        memset(&serverInfo, 0, sizeof(serverInfo)); // обнулить память
        serverInfo.sin_family = AF_INET;                        // тип сети
        serverInfo.sin_port = htons(2000);                      // порт для подключения
        serverInfo.sin_addr.s_addr = inet_addr("127.0.0.1");    // IP-адрес сервера для подключения
        //serverInfo.sin_addr.s_addr = inet_addr("192.168.56.103"); // IP-адрес сервера для подключения
 
        std::cout << "Connecting...\n";
        if (connect(clientSocket, (LPSOCKADDR)&serverInfo, sizeof(serverInfo)) == SOCKET_ERROR)
            throw SetErrorMsgText("can't connect: ", WSAGetLastError());
 
        for (int i = 0; i < 1000; i++)
        {
            std::string message = "Hello from Client " + itos(i);
            
            const char* m = message.c_str();
 
            std::cout << m << std::endl;
 
            Converter size;
            size.number = strlen(m);
 
            // отправляем размер
            std::cout << "Sending " << size.number << " bytes...\n";
 
            int code = send(clientSocket, size.bytes, sizeof(int), NULL);
            if (code == SOCKET_ERROR)
                throw SetErrorMsgText("send: ", WSAGetLastError());
 
            // теперь отправляем сами данные
            code = send(clientSocket, m, size.number, NULL);
            if (code == SOCKET_ERROR)
                throw SetErrorMsgText("send: ", WSAGetLastError());
            
            getchar();
        }
 
        // закрываем сокеты
        if (closesocket(clientSocket) == SOCKET_ERROR)
            throw SetErrorMsgText("closesocket:", WSAGetLastError());
 
        // выгрузка библиотеки
        if (WSACleanup() == SOCKET_ERROR)
            throw SetErrorMsgText("Cleanup:", WSAGetLastError());
    }
    catch (std::string errorMsgText)
    {
        std::cout << std::endl << std::endl << errorMsgText;
    }
    std::cout << "\n\nDONE.";
    getchar();
    return 0;
}
 
std::string GetErrorMsgText(int code)    // cформировать текст ошибки 
{
    std::string msgText;
    switch (code)                      // проверка кода возврата  
    {
    case WSAEINTR: msgText = "WSAEINTR"; break;
    case WSAEACCES: msgText = "WSAEACCES"; break;
    case WSAEFAULT: msgText = "WSAEFAULT"; break;
    case WSAEINVAL: msgText = "WSAEINVAL"; break;
        //..........коды WSAGetLastError ..........................
    case WSASYSCALLFAILURE: msgText = "WSASYSCALLFAILURE"; break;
    default: msgText = "UNKNOWN ERROR. CODE: " + itos(code); break;
    };
    return msgText;
};
 
std::string SetErrorMsgText(std::string msgText, int code) { return  msgText + GetErrorMsgText(code); };
 
#include <sstream>
std::string itos(int i) // convert int to string
{
    std::stringstream s;
    s << i;
    return s.str();
}

Какую дичь я только не пробовал (думаю, по коду вы это понимаете), все равно не работает.
Задание такое: отправить 1000 раз сообщение "Hello from Client XXX", где XXX - номер сообщения.
Т.к. сообщение у меня переменной длины, то сначала я отправляю его размер, а затем само сообщение. Вроде бы адекватное решение, верно? На сервере соответственно я сначала читаю размер будущего сообщения, затем само смс. Но почему-то на сервере во время 2-й итерации происходит вылет при инициализации массива buffer: size.number принимает аномально огромное значение. Глянув внимательно в консоль сервера и прогнав через отладчик, я понял, что при 1-й итерации массив имеет размер 19 символов (тут все верно), но в консоль каждый раз выводит одни и те же 23 (wtf?) символа: "Hello from Clientээээ". Что за бред? Первая глупость, приходящая в голову: 4-хбайтовый размер следующего СМС прилетел сюда, из-за чего появились эти символы и на 2-й итерации в size.number записываются уже другие 4 байта. Но т.к. клиент останавливается с помощью getchar(), это просто невозможно! Также заметил, что на 2-й итерации в массив cSize, что в начале цикла, постоянно записывается значение "Hellээээ*~Ъ\". Часть от отправленной фразы длиной в 4 байта?.. В общем, я окончательно запутался в этом вопросе. Я гуглил эту тему, но ответа так и не нашел. Плохо гуглил? Пожалуйста, объясните начинающему студенту, как правильно отправлять такие сообщения и исправить ошибки.
Заранее спасибо! Удачи всем и хорошего кода!
0
Лучшие ответы (1)
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
07.09.2020, 02:19
Ответы с готовыми решениями:

Цикл сообщений и функция их обработки
вечер добрый! я почти нулевой новичок в win api, в одном из первых примеров, которые я смотрел...

Чем отличается цикл сообщений в скрытом окне от бесконечного цикла?
Написал прогу под WinAPI, которая перебирает все окна в бесконечном цикле #include &lt;windows.h&gt;...

Отправка сообщений. Winsock, send и telnet
Приветствую. Пишу простенький сервер. Использую Delphi 6, winsock. Клиентом является стандартный...

Цикл обработки сообщений
Можно ли как то отобразить основной цикл обработки сообщений в WinForms c++ если нет то как...

6
Native x86
Эксперт Hardware
3478 / 2301 / 680
Регистрация: 13.02.2013
Сообщений: 7,581
07.09.2020, 03:55 2
О правильном использовании TServerSocket/TClientSocked и подобными компонентами, пункт 3 (на п.2 тоже обратите внимение).
Там про компоненты Delphi/Builder, но чистых сокетов это касается в той же мере.
0
Любитель чаепитий
3470 / 1589 / 495
Регистрация: 24.08.2014
Сообщений: 5,492
Записей в блоге: 1
07.09.2020, 08:43 3
Лучший ответ Сообщение было отмечено Ivanshka как решение

Решение

Цитата Сообщение от Ivanshka Посмотреть сообщение
Вроде бы адекватное решение, верно?
верно, но оно совсем не обязательно.
к тому же, у вас оно неправильное, т.к. в сеть принято отправлять байты только в прямом порядке байт, чтобы у других не возникало путаницы.
так же у std::string есть метод size().
код на клиенте должен выглядеть как-то так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (int i = 0; i < 1000; i++)
{
    std::string message = "Hello from Client " + itos(i);
 
    std::cout << message << std::endl;
 
    uint32_t size = message.size();
    size = htonl(size); // переводим в прямой порядок байт
 
    // отправляем размер
    std::cout << "Sending " << size << " bytes...\n";
 
    int code = send(clientSocket, (const char *)&size, sizeof(uint32_t), 0);
    if (code == SOCKET_ERROR)
        throw SetErrorMsgText("send: ", WSAGetLastError());
 
    // теперь отправляем сами данные
    code = send(clientSocket, message.data(), message.size(), 0);
    if (code == SOCKET_ERROR)
        throw SetErrorMsgText("send: ", WSAGetLastError());
    
    getchar();
}
Цитата Сообщение от Ivanshka Посмотреть сообщение
byteCounter += recv(clientSocket, buffer, dataSize.number, MSG_PEEK);
зачем здесь флаг MSG_PEEK?
к тому же последним аргументом send и recv принимают int, а не указатель, поэтому там должно быть не NULL, а 0.
в таком случае у вас буфер не вычитывается и вы постепенно продолжаете его вычитывать как size, у которого этот флаг не стоит.
если добавлять к этому исправление отправки размера из предыдущего шага, то получается как-то так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for(int i = 0; i < 1000; i++) {
    // принимаем размер будущих данных
    uint32_t cSize = 0;
    int byteCounter = 0;
    byteCounter = recv(clientSocket, (char *)&cSize, sizeof(uint32_t), 0); // 4-й аргумент: NULL - после получения входной буфер очищается, MSG_PEEK - входной буфер не очищается
    cSize = ntohl(cSize); // перевод из прямого в системный порядок байт
    if (byteCounter == SOCKET_ERROR)
        throw SetErrorMsgText("recv: ", WSAGetLastError());
    std::cout << "MESSAGE SIZE = " << byteCounter << std::endl;
 
 
    buffer = new char[cSize];
    memset(buffer, 0, cSize);
    std::cout << "Getting " << cSize << " bytes...\n";
 
    byteCounter = 0;
    while (byteCounter < cSize)
    {
        byteCounter += recv(clientSocket, buffer + byteCounter, cSize - byteCounter, 0); // 4-й аргумент: NULL - после получения входной буфер очищается, MSG_PEEK - входной буфер не очищается
        if (byteCounter == SOCKET_ERROR)
            throw SetErrorMsgText("recv: ", WSAGetLastError());
    }
    std::cout << buffer << std::endl << "Bytes got: " << byteCounter << std::endl;
    delete[] buffer;
}
1
8 / 3 / 0
Регистрация: 11.08.2016
Сообщений: 21
07.09.2020, 15:57  [ТС] 4
Цитата Сообщение от GbaLog- Посмотреть сообщение
верно, но оно совсем не обязательно.
Не обязательно? О_о Тогда как быть, если у меня информация, например, 1 КБ? TCP разобьет его на части (например, 200 + 300 + 500 байт), и мне прилетит первая часть, а остальные - с задержкой. А как в этом случае правильно читать данные? Просто я хочу понять, как правильно передавать данные динамического размера.
Касаемо вашего кода, то все заработало, а пояснения я понял) Большое спасибо!)
Остался лишь 1 момент, который я не могу объяснить и исправить. При выводе полученных данных на консоль сервера, он выводит "Hello frm Client XXX¤¤¤¤". Откуда взялись еще 4 символа?..

Добавлено через 16 минут
Цитата Сообщение от GbaLog- Посмотреть сообщение
к тому же последним аргументом send и recv принимают int, а не указатель, поэтому там должно быть не NULL, а 0.
И, кстати, какая разница между 0 и NULL, если NULL - это всего лишь
C++
1
#define NULL 0
? Это ведь не указатель.
0
2019 / 607 / 220
Регистрация: 10.02.2018
Сообщений: 1,387
07.09.2020, 16:32 5
Цитата Сообщение от Ivanshka Посмотреть сообщение
Откуда взялись еще 4 символа?
Вы со строками работаете, а завершающий строку 0 не передаёте и ручками не добавляете. Поэтому мусор разный в конце строки появляется.
1
Любитель чаепитий
3470 / 1589 / 495
Регистрация: 24.08.2014
Сообщений: 5,492
Записей в блоге: 1
07.09.2020, 16:54 6
Цитата Сообщение от Ivanshka Посмотреть сообщение
Не обязательно?
я имел ввиду, что это не единственный способ.
например, в HTTP конец строки обозначается последовательностью \r\n, а конец заголовка \r\n\r\n.
размер заголовка нигде не передаётся, но благодаря этим правилам подходит для потокового декодирования.
Цитата Сообщение от Ivanshka Посмотреть сообщение
При выводе полученных данных на консоль сервера, он выводит "Hello frm Client XXX¤¤¤¤". Откуда взялись еще 4 символа?..
как вам и сказали выше, у вас не хватает в строке завершающего строку символа: '\0'.
поэтому std::cout выводит строку, пока не встретит этот символ.
для того, чтобы этого избежать вам нужно создавать буфер размера cSize + 1. и memset так же поправить.
Цитата Сообщение от Ivanshka Посмотреть сообщение
какая разница между 0 и NULL, если NULL - это всего лишь
это не обязательно.
NULL может быть реализован через nullptr, а nullptr имеет тип std::nullptr_t.
вероятно, у вас скомпилируется код и с ним, но так просто не стоит делать.
NULL следует использовать только для указателей, а лучше использовать nullptr.
1
8 / 3 / 0
Регистрация: 11.08.2016
Сообщений: 21
08.09.2020, 08:48  [ТС] 7
Цитата Сообщение от GbaLog- Посмотреть сообщение
я имел ввиду, что это не единственный способ.
например, в HTTP конец строки обозначается последовательностью \r\n, а конец заголовка \r\n\r\n.
размер заголовка нигде не передаётся, но благодаря этим правилам подходит для потокового декодирования.
Ух ты) Теперь стало яснее)
Цитата Сообщение от GbaLog- Посмотреть сообщение
как вам и сказали выше, у вас не хватает в строке завершающего строку символа: '\0'.
поэтому std::cout выводит строку, пока не встретит этот символ.
для того, чтобы этого избежать вам нужно создавать буфер размера cSize + 1. и memset так же поправить.
Да, еще вчера, как прочел то СМС, сразу так сделал и исправил)
Цитата Сообщение от GbaLog- Посмотреть сообщение
это не обязательно.
NULL может быть реализован через nullptr, а nullptr имеет тип std::nullptr_t.
вероятно, у вас скомпилируется код и с ним, но так просто не стоит делать.
NULL следует использовать только для указателей, а лучше использовать nullptr.
О_о. nullptr я использовал по назначению и ранее, но вот про такой подводный камень не знал. Теперь все стало ясно.
Большое спасибо за помощь!)
1
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
08.09.2020, 08:48

Заказываю контрольные, курсовые, дипломные и любые другие студенческие работы здесь.

Как реализовать цикл ввода сообщений?
Программа должна выпольняться пока пользователь не введет слово Exit. Программа спрашивает...

Может так случится, что процесс приема новых сообщений и их прорисовки превратится в бесконечный цикл?
Допустим у меня есть приложение, которое добавляет в себя поступающие сообщения. А эти сообщения...

Используйте цикл for для вывода сообщений об именах и id десяти тегов <span> при нажатии кнопки.
Помогите пожалуйста. Используйте цикл for для вывода сообщений об именах и id деся-ти тегов...

Создать программу по всем 3 видам циклов...цикл с параметром,цикл с условием,цикл,и цикл с предусловием...
Найти сумму чисел 1 в квадрате до 10 c квадрате...операцию возведению в степень не использовать...

Система личных сообщений. Вывод списка сообщений.
Доброе время суток! Возникла проблема с почтой, а конкретно не могу вывести всю переписку...

Вычислить и вывести сумму чётных целых чисел в интервале от 1 до n: 1) цикл «ДО» 2) цикл «ПОКА» 3) цикл «ДЛЯ»
Вычислить и вывести сумму чётных целых чисел в интервале от 1 до n: 1. цикл «ДО» 2. цикл «ПОКА»...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
7
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2020, vBulletin Solutions, Inc.