И начал я натыкаться на ошибки разные, до этого чуждые мне, да на синтаксис непонятный. И в итоге, сейчас делюсь с Вами тем, как надо и как не надо писать даже самый простой сервер.
И так начнём.
Начнём с самого главного элемента — класс сервера SServer:
#pragma once
#include "includes.h"
class SServer
{
public:
SServer();
~SServer();
void startServer();
void closeServer();
void handle();
unsigned short port;
private:
SOCKET this_s;
WSAData wData;
};
Тут думаю сложно не будет. SOCKET создаём как сокет для сервера, он будет слушать.
WSAData нужен для активации использования сокетов в Windows.
Это был заголовок. Перейдём к CPP:
#include "SServer.h"
#include "includes.h"
SServer::SServer(){
}
SServer::~SServer(){
}
void SServer::startServer(){
if (WSAStartup(MAKEWORD(2, 2), &wData) == 0)
{
printf("WSA Startup succes\n");
}
SOCKADDR_IN addr;
int addrl = sizeof(addr);
addr.sin_addr.S_un.S_addr = INADDR_ANY;
addr.sin_port = htons(port);
addr.sin_family = AF_INET;
this_s = socket(AF_INET, SOCK_STREAM, NULL);
if (this_s == SOCKET_ERROR) {
printf("Socket not created\n");
}
if (bind(this_s, (struct sockaddr*)&addr, sizeof(addr)) != SOCKET_ERROR) {
printf("Socket succed binded\n");
}
if (listen(this_s, SOMAXCONN) != SOCKET_ERROR){
printf("Start listenin at port%u\n", ntohs(addr.sin_port));
}
handle();
}
void SServer::closeServer() {
closesocket(this_s);
WSACleanup();
cout << "Server was stoped. You can close app" << endl;
}
void SServer::handle() {
while (true)
{
SOCKET acceptS;
SOCKADDR_IN addr_c;
int addrlen = sizeof(addr_c);
if ((acceptS = accept(this_s, (struct sockaddr*)&addr_c, &addrlen)) != 0) {
printf("send\n");
printf("sended Client connected from 0 %u.%u.%u.%u:%u\n",
(unsigned char)addr_c.sin_addr.S_un.S_un_b.s_b1,
(unsigned char)addr_c.sin_addr.S_un.S_un_b.s_b2,
(unsigned char)addr_c.sin_addr.S_un.S_un_b.s_b3,
(unsigned char)addr_c.sin_addr.S_un.S_un_b.s_b4,
ntohs(addr_c.sin_port));
SClient* client = new SClient(acceptS, addr_c);
}
Sleep(50);
}
}
А теперь по-порядку. Сначала активируем WSA вызовом WSAStartup(). MAKEWORD задаёт версию библиотек, которые будут подключаться и ссылка на объект WSAData. Обычно всегда срабатывает успешно, только ели у вас не *unix, хе-хе. Дальше. структура SOCKADDR_IN помогает нам определить заранее порт и доступность нашего сервера. А теперь поясню:
- INETADDR_ANY говорит о том, что сервер будет доступен с любой пользовательской машины. Если указать как пишут inet_aadr(«127.0.0.1»), то это не даст вам даже в локальной сети тестировать
- htons() — функция, которая превращает hardware to network short при этом используется ushort. Дальше будет функция ntohs() — действует наоборот
- AF_INET — константа, отвечающая за то, что устройство использует глобальную сеть по протоколу IPv4, AF_INET6 — IPv6
Дальше происходит
SOCK_STREAM говорит об использовании TCP протокола, SOCK_DGRAM — UDP соответственно.
Вызываем обработчик входящих соединений handle() и в бесконечном цикле начинаем проверку.
Создаём сокет для входящего подключения, структуру для заполнения адреса и длину структуры.
Производим проверку на то, что не является ли наш созданный сокет подключённым по адресу такому-то от компьютера того-то. И если всё хорошо, то выводим сообщение и подключении с адреса нашего клиента и создаём клиента, который уже в дальнейшем будет сам всё обрабатывать.
Теперь же код клиента. Заголовок и имплементация вместе:
#pragma once
#include "includes.h"
class SClient
{
public:
SClient(SOCKET s, SOCKADDR_IN sock_in);
~SClient();
void handle();
private:
SOCKET c_sock;
SOCKADDR_IN c_addr;
char buffer[1024];
};
SClient::SClient(SOCKET s, SOCKADDR_IN sock_in)
{
c_sock = s;
c_addr = sock_in;
printf("Client created\n");
handle();
}
SClient::~SClient()
{
}
void SClient::handle()
{
while (true)
{
int k = recv(c_sock, buffer, sizeof(buffer), NULL);
if(k>0){
printf(buffer);
}
Sleep(30);
}
}
Тут тоже как видите, ничего сложного нет. главное это handle() функция, которую мы сразу запускаем в конструкторе. Функцию recv() предпочтительно использовать с TCP, а recvfrom() с UDP протоколом соответственно.
И на последок. В main.cpp Всё выглядит вот так:
#include "SServer.h"
int main()
{
SServer server ;
server.port = 3487;//порт это ushort - так что cin.get() и т.п. функции тут работаю некорректно. Лучше задать фвручную
server.startServer();
return 0;
}
А файлик, который вы так часто видели includes.h. Вот и он:
#pragma once
#pragma comment(lib, "ws2_32.lib")
#pragma warning(disable: 4996)
#include <iostream>
#include <WinSock2.h>
#include <winsock.h>
#include "SClient.h"
using namespace std;
Итог: Мы научились создавать простой TCP сервер на C++, прочитав хорошую статью на хабре
Будет так же и UDP версия, но отличия очень малы и думаю скоро тоже будет здесь.
Так же всё есть на GitHub
Комментарии (18)
zloiia
28.04.2017 14:35+3Не в обиду автору, но за старания 5, за реализацию 2. Даже для начала обучения, возьмите за основу готовые красивые и практичные библиотеки. Хотя бы то-же Boost::Asio. Даже базовых примеров хватит с лихвой, чтобы начать делать что-то, что будет уже работать чуть стабильнее и адекватнее, чем вышеизложенное.
iassasin
29.04.2017 05:52Мне с вами сложно согласиться, что boost::asio подходит для новичка. Когда только начинал знакомиться с сокетами в C++, конечно, гуглил библиотеки и буст рассматривал. Но у меня была простая задача: передать файл по сети, т.е. простая утилита по типу netcat, и я совсем не понимал, зачем буст предлагает мне какие-то io-сервисы и acceptor-ы.
В итоге накопал в интернете примеры работы с сокетами с помощью API ОС и сделал свою небольшую обертку над ним (кому интересно, репозиторий у меня на гитхабе — cppstreams). Файлы хотелось передавать между виндой и линуксом своим приложением, поэтому сделал ее кроссплатформенной, научил ее работать с C++-овым stream-ом и только после этого уже понял, для чего нужны io-сервисы, select-ы и как это вообще работает.
Но вот так с наскоку разобраться в boost::asio у меня не вышло.
Serge78rus
30.04.2017 01:13Если цель — поучиться, покопавшись в исходниках библиотеки, то я бы предложил посмотреть еще Poco — вполне работоспособно, но внутренняя реализация читается легче, чем boost
insafonov
29.04.2017 02:08Если чуток подробнее про htons(), то она нужна для смены (при необходимости) порядка байтов в словах (big/little-endian), на разных архитектурах он может отличаться, а в сети используется big-endian.
JegernOUTT
29.04.2017 02:09server.port = 3487;//порт это ushort — так что cin.get() и т.п. функции тут работаю некорректно. Лучше задать фвручную
Чем вас не устроил стандартный способ?
std::cin << server.port;
А про остальной код можно промолчать. Смешение стримов и сишных обёрток для stdio уже говорит о многом. Зря сюда его выложили, для новичков он не годится, а для ветеранов только на потеху
si1v3r
30.04.2017 21:23Будет так же и UDP версия, но отличия
скорее всего до вас не дойдут. (с) анекдот
Tujh
02.05.2017 09:32Я бы промолчал…
но вот за такое:
А файлик, который вы так часто видели includes.h. Вот и он:
нужно линейкой по пальцам бить, до осознания…
…
using namespace std;
oleg-m1973
02.05.2017 10:02Бесконечный цикл в конструкторе — это круто. Типа — наш сервер никто не остановит!
Второй раз приконнектиться тоже никто не сможет. Да и сокет вроде блокирующий, так что отправить меньше 1024 байт также будет нельзя.
aquamakc
Ок, а при чём тут C# из заголовка?
Публичное поле port в класса сервера — фу-фу-фу.
Обработка подключений в один поток.
Это что? Студенческая лабораторная?