Простой и быстрый сервер на C/C++ с клиентом на C#: TCP версия

Всем привет. Потихоньку перебирая листы книг, занимаюсь я серверным программированием. И дошёл мой разум до того, что можно было бы и на C++ сервер написать. Ну и недолго думая(точнее вообще не думая и плохо зная плюсы), я пошёл писать сервер.

И начал я натыкаться на ошибки разные, до этого чуждые мне, да на синтаксис непонятный. И в итоге, сейчас делюсь с Вами тем, как надо и как не надо писать даже самый простой сервер.
И так начнём.

Начнём с самого главного элемента — класс сервера 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

Дальше происходит волшебство вызов трёх функций и проверка их значений на SOCKET_ERROR. Думаю тут более ли менее понятно. Проинициализировали, забиндили и заставили слушать.

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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 24

    +8
    Ок, а при чём тут C# из заголовка?
    Публичное поле port в класса сервера — фу-фу-фу.
    Обработка подключений в один поток.
    Это что? Студенческая лабораторная?
      +3
      Не в обиду автору, но за старания 5, за реализацию 2. Даже для начала обучения, возьмите за основу готовые красивые и практичные библиотеки. Хотя бы то-же Boost::Asio. Даже базовых примеров хватит с лихвой, чтобы начать делать что-то, что будет уже работать чуть стабильнее и адекватнее, чем вышеизложенное.
        0
        Мне с вами сложно согласиться, что boost::asio подходит для новичка. Когда только начинал знакомиться с сокетами в C++, конечно, гуглил библиотеки и буст рассматривал. Но у меня была простая задача: передать файл по сети, т.е. простая утилита по типу netcat, и я совсем не понимал, зачем буст предлагает мне какие-то io-сервисы и acceptor-ы.
        В итоге накопал в интернете примеры работы с сокетами с помощью API ОС и сделал свою небольшую обертку над ним (кому интересно, репозиторий у меня на гитхабе — cppstreams). Файлы хотелось передавать между виндой и линуксом своим приложением, поэтому сделал ее кроссплатформенной, научил ее работать с C++-овым stream-ом и только после этого уже понял, для чего нужны io-сервисы, select-ы и как это вообще работает.
        Но вот так с наскоку разобраться в boost::asio у меня не вышло.
          0
          Если цель — поучиться, покопавшись в исходниках библиотеки, то я бы предложил посмотреть еще Poco — вполне работоспособно, но внутренняя реализация читается легче, чем boost
            –1
            А если не совать монструозный буст в каждую дырку?
            +7
            Весна, студенты повылезали =)
              0
              Ну дык, лабы/курсовые/дипломы сделали, а потом смотрят и думают — чё ж добру пропадать, может ещё куда залить :)

              /me нервно посматривает на свою лабу по мультипоточности в JS
                0
                /me бы тоже взглянул =) м.б. на ЯДиск и ссылку в личку?
                  0
                  Да там ничего такого что потянет на пост. Заюзал вот эту библиотеку, и посчитал числовой ряд в несколько потоков. Это вроде как неполная реализация MPI протокола для ноды
              +1

              А порт в деструкторе закрывать не надо?

                0
                Если чуток подробнее про htons(), то она нужна для смены (при необходимости) порядка байтов в словах (big/little-endian), на разных архитектурах он может отличаться, а в сети используется big-endian.
                  0

                  Sleep там не к чему.

                    0
                    server.port = 3487;//порт это ushort — так что cin.get() и т.п. функции тут работаю некорректно. Лучше задать фвручную

                    Чем вас не устроил стандартный способ?


                    std::cin << server.port;

                    А про остальной код можно промолчать. Смешение стримов и сишных обёрток для stdio уже говорит о многом. Зря сюда его выложили, для новичков он не годится, а для ветеранов только на потеху

                      0
                      Судя по профилю — человек-оркестр. Оно и видно.
                        0
                        Отличный сервер на одного клиента.
                          0
                          Будет так же и UDP версия, но отличия
                          скорее всего до вас не дойдут. (с) анекдот
                            0
                            Я бы промолчал…
                            но вот за такое:
                            А файлик, который вы так часто видели includes.h. Вот и он:

                            using namespace std;
                            нужно линейкой по пальцам бить, до осознания…
                              0
                              Бесконечный цикл в конструкторе — это круто. Типа — наш сервер никто не остановит!
                              Второй раз приконнектиться тоже никто не сможет. Да и сокет вроде блокирующий, так что отправить меньше 1024 байт также будет нельзя.
                                0
                                А мне вот тоже интересно, как написать простой работающий сервер на C++ (без boost, а то я смотрел уже примеры на нём, как-то не очень понятно выглядит). Можно без многопоточности, требования по нагрузке — до 15-20 одновременных клиентов (для небольшого делового чата, например). И желательно с использованием неблокирующих сокетов. Кто-нибудь запилит такую статью? Было бы круто.
                                  0
                                  Серьёзно? Без многопоточности на пару клиентов — это задача лабораторной работы среднего ВУЗа. Почему лабораторной? Потому-что во взрослой жизни такое приложение неработоспособно. Добавляем многопоточность (или асинхронность) — получаем лабораторную работу хорошего ВУЗа. В сети реализаций полно, в гугл вроде пока пускают бесплатно.
                                    0
                                    Сервер на 15-20 клиентов с использованием С++ — это из пушки по воробьям. На такую «нагрузку» можно на чём угодно левой пяткой написать сервер, и будет хорошо работать, и поддерживать будет легче. Взять тот же nodejs.
                                      0
                                      Я писал небольшие сервера на Java. Насколько я помню, у меня так и не получилось принять новое соединение до завершения обработки уже поступившего (то есть если нужен LongPoll — беда). Возможно, я что-то делал не так, либо у Java проблема с неблокирующими сокетами.
                                        0
                                        А если надо будет 100 или 200? Или 500? Мне кажется, если есть хоть какой-то шанс, что понадобится масштабируемость — лучше сразу писать не левой пяткой, а как следует.
                                      0
                                      1. Код в статье не намного отличается от docs.microsoft.com/ru-ru/windows/desktop/WinSock/complete-server-code — думаю для начального уровня и обучения — вполне нормально.
                                      2. А при использовании Boost::Asio — не нужно забывать про то что указано в www.boost.org/LICENSE_1_0.txt — "....this restriction and the following disclaimer, must be included in all copies of the Software....."

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое