Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Кроссплатформенный сервер с неблокирующими сокетами. Часть 4

Anna | 24.06.2014 | нет комментариев
Эта статья продолжает мои предыдущие:
Примитивный кросcплатформенный сервер с помощью ssl
Кроссплатформенный https сервер с неблокирующими сокетами 
Кроссплатформенный https сервер с неблокирующими сокетами. Часть 2
Кроссплатформенный https сервер с неблокирующими сокетами. Часть 3В своих статьях я поэтапно расписываю процесс создания однопоточного кроссплатформенного сервера на неблокирующих сокетах.
Во всех предыдущих статьях, сервер принимал и отправлял сообщения только по ssl протоколу. В этой статье я опишу добавление в сервер поддержки обыкновенного нешифрованного tcp протокола и обучу сервер отправлять в браузер графический файл.
Но вначале немножко пройдусь по комментариям к предыдущим статьям.

1. Я послушал советов избавиться от функции printf в пользу std::cout.
2. Мудрые люди подтвердили мне, что std::memcpy и std::copy для компилятора одно и то же.
Мне memcpy комфортней, следственно буду продолжать пользоваться ей.
3. Я перенес все ранние релизы и буду переносить грядущие на GitHub, правда заказчик для Windows у них, на мой взор, жуток.
4. Кто считает, что строчки

			const char on = 1;
			setsockopt(listen_sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );

помогут избежать ошибки «Address already in use» при аварийном перезапуске сервера — жестоко заблуждаются. Не помогут.
5. Тем, кто считает, что различные классы неизменно необходимо разносить по различным файлам, подолью масла: я хочу перенести класс CClient в приватную секцию класса CServer!

Было:

CClient
{
***
};
CServer
{
***
};

Стало:

CServer
{
	CClient
	{
	***
	};
***
};

Сейчас, если сервер станет библиотекой, ни у кого не должно появиться мысли об применении класса CClient: это рабочий класс, предуготовленный экстраординарно для взаимодействия с классом CServer.

6. И еще на мой взор, функция main() — атавизм, доставшийся программистам от СИ. В С она лишняя. Но компиляторы пока этого не знают к сожалению.
Но я решил «наказать» эту непотребную функцию, отобрав у нее вероятность что-либо сделать — изменил файл serv.cpp дальнейшим образом:

#include "server.h"

const server::CServer s(8085, 1111);

int main() {return 0;}

Сейчас о главном…

Добавление в сервер поддержки нешифрованных tcp соединений

Нешифрованные и шифрованные соединения в серверах традиционно принимаются на различные порты следственно первое, что необходимо сделать — это изменить конструктор сервера и добавить переменных, для еще одного слушающего сокета.

Взамен

struct epoll_event m_ListenEvent;

пишем в классе сервера

struct epoll_event m_ListenEventTCP, m_ListenEventSSL;

В конструкторе сервера добавим номера портов и код для линукса, тот, что не дозволит аварийно кончаться серверу в случае ошибки в TCP операциях:

		CServer(const int nPortTCP, const int nPortSSL)
		{
#ifndef WIN32
			struct sigaction sa;			
			memset(&sa, 0, sizeof(sa));		
			sa.sa_handler = SIG_IGN;		
			sigaction(SIGPIPE, &sa, NULL);
#else
			WSADATA wsaData;
			if ( WSAStartup( MAKEWORD( 2, 2 ), &wsaData ) != 0 )
			{
				cout << "Could not to find usable WinSock in WSAStartup\n";
				return;
			}
#endif

Напишем отдельные функции для инициации слушающих сокетов и для добавления нового заказчика:

	private:
		void InitListenSocket(const int nPort, struct epoll_event &eventListen)
		{
			SOCKET listen_sd = socket (AF_INET, SOCK_STREAM, 0);
			SET_NONBLOCK(listen_sd);

			const char on = 1;
			setsockopt(listen_sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );

			struct sockaddr_in sa_serv;
			memset (&sa_serv, '', sizeof(sa_serv));
			sa_serv.sin_family      = AF_INET;
			sa_serv.sin_addr.s_addr = INADDR_ANY;
			sa_serv.sin_port        = htons (nPort);          /* Server Port number */

			int err = ::bind(listen_sd, (struct sockaddr*) &sa_serv, sizeof (sa_serv));

			if (err == -1)
			{
				cout << "bind error = " << errno << "\n";
				return;
			}
			/* Receive a TCP connection. */

			err = listen (listen_sd, SOMAXCONN);

			eventListen.data.fd = listen_sd;
			eventListen.events = EPOLLIN | EPOLLET;
			epoll_ctl (m_epoll, EPOLL_CTL_ADD, listen_sd, &eventListen);
		}
		void AcceptClient(const SOCKET hSocketIn, const bool bIsSSL)
		{
			cout << "AcceptClient";
			struct sockaddr_in sa_cli;  
			size_t client_len = sizeof(sa_cli);
#ifdef WIN32
			const SOCKET sd = accept (hSocketIn, (struct sockaddr*) &sa_cli, (int *)&client_len);
#else
			const SOCKET sd = accept (hSocketIn, (struct sockaddr*) &sa_cli, (socklen_t *)&client_len);
#endif  
			if (sd != INVALID_SOCKET)
			{
				cout << "Accepted\n";
				//Добавляем нового заказчика в класс сервера
				m_mapClients[sd] = shared_ptr<CClient>(new CClient(sd, bIsSSL));

				auto it = m_mapClients.find(sd);
				if (it == m_mapClients.end())
					return;

				//Добавляем нового заказчика в epoll
				struct epoll_event ev = it->second->GetEvent();
				epoll_ctl (m_epoll, EPOLL_CTL_ADD, it->first, &ev);
			}					
		}

Сейчас в заказчике добавим переменную m_bIsSSL, которую будем инициировать в конструкторе, а потом изменим callback функции так, Дабы они могли трудиться с TCP соединениями:
Взамен

		const RETCODES AcceptSSL()
		{
			if (!m_pSSLContext) //Наш сервер предуготовлен только для SSL
				return RET_ERROR;

Тепрь будет:

			const RETCODES AcceptSSL()
			{
				cout << "AcceptSSL\n";
				if (!m_bIsSSL) return RET_READY;

				if (!m_pSSLContext)
					return RET_ERROR;

Как видим, проще некуда: TCP функция accept не требует никаких дополнительных телодвижений для того, Дабы начать принимать и отдавать данные.
Никаких сертификатов для TCP не необходимо, следственно предисловие соответствующей функции будет сейчас выглядеть так:

			const RETCODES GetSertificate()
			{
				cout << "GetSertificate\n";
				if (!m_bIsSSL) return RET_READY;

В функции, читающей данные от заказчика ContinueRead() необходимо взамен

			unsigned char szBuffer[4096];

			const int err = SSL_read (m_pSSL, szBuffer, 4096); //читаем данные от заказчика в буфер

написать код:

				static char szBuffer[4096];

				//читаем данные от заказчика в буфер
				int err;
				if (m_bIsSSL)
					err = SSL_read (m_pSSL, szBuffer, 4096);
				else
				{
					errno = 0;
					err = recv(m_hSocket, szBuffer, 4096, 0);
				}
				m_nLastSocketError = GetLastError(err);

В этой же функции необходимо сейчас добавить код обработки ошибок для TCP соединения. Как и в случае SSL, оплошностью будет
если функция приема сообщения вернет негативное либо нулевое значение. Но так как у нас неблокирующие сокеты,
то оплошность WSAEWOULDBLOCK в Windows и EWOULDBLOCK в Linux обозначает, что все типично, легко необходимо еще подождать.
Добавим такие макросы:

#ifndef _WIN32
#define S_OK				0
#define WSAEWOULDBLOCK			EWOULDBLOCK
#define WSAGetLastError()		errno
#endif

И такой код в функцию ContinueRead:

				if (!m_bIsSSL)
				{
					if ((err == 0) || ((m_nLastSocketError != WSAEWOULDBLOCK) && (m_nLastSocketError != S_OK)))
						return RET_ERROR;
				}
				else
				{
					if ((err == 0) || ((m_nLastSocketError != SSL_ERROR_WANT_READ) && (m_nLastSocketError != SSL_ERROR_WANT_WRITE)))
						return RET_ERROR;
				}

а функцию CClient::GetLastError мы определим так

		private:
			int GetLastError(int err) const
			{
				if (m_bIsSSL)
					return SSL_get_error(m_pSSL, err);
				else
					return WSAGetLastError();
			}

Абсолютно аналогичным образом поправим функцию отправки сообщений ContinueWrite и наш однопоточный кроссплатформенный сервер готов для приема tcp и ssl соединений от заказчиков, Дабы отдать им заголовки запроса.
Давайте еще обучим сегодня наш сервер отдавать заказчикам файлы.
В тезисе ничего в этом особенного нет если не считать, что в Linux для отправки файла есть больше стремительный метод чем в других системах: функция sendfile.
Дабы код был единообразным, я предлагаю поступить с sendfile так же, как мы поступали с epoll: написать эмулятор этой функции для всех систем помимо Linux.

Эмуляция функции sendfile

1. Сотворим пустые файлы «sendfile.h», «sendfile.cpp» и добавим их в план Visual Studio.
2. В sendfile.h разместим такой код:

#ifndef __linux__
#ifndef _SENDFILE_H
#define _SENDFILE_H
#include <sys/types.h>

unsigned long long sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

#endif
#endif

3. В sendfile.cpp разместим такой:

#include <io.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#endif

unsigned long long sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
{
	static unsigned char buffer[4096];

	if (count > 4096)
		count = 4096;

	off_t lPos = _lseek(in_fd, *offset, SEEK_SET);
	if (lPos == -1)
		return -1;

	const int nReaded = _read(in_fd, buffer, count);

	if (nReaded == 0)
		return nReaded;
	if (nReaded == -1)
		return -1;

	*offset  = nReaded;

	errno = 0;
	const int nSended = send(out_fd, (const char *)buffer, nReaded, 0);

	if (nSended != SOCKET_ERROR)
		return nSended;

	if (WSAGetLastError() != WSAEWOULDBLOCK)
		return -1;

	return 0;
}
#endif

4. Добавим в класс сервера нужные включения так, Дабы в Linux применялись типовые функции, а в остальных системах — наши.
Помимо этого, добавим включение для работы с файлами и определим путь к файлу, тот, что будем посылать заказчику:

#ifdef __linux__
#include <sys/epoll.h>
#include <sys/sendfile.h>
#define O_BINARY	0
#else
#include "epoll.h"
#include "sendfile.h"
#endif
#include <sys/stat.h>
#define SEND_FILE "./wwwroot/festooningloops.jpg"

C эмуляцией sendfile завершили.

Посылаем файл

5. Добавим в класс заказчика файловый дескриптор и нынешнюю позицию

	class CClient
	{
		int m_nSendFile;
		off_t m_nFilePos;
		unsigned long long m_nFileSize;

6. Изменяем функцию InitRead()

			const RETCODES InitRead()
			{
				if (m_bIsSSL && (!m_pSSLContext || !m_pSSL))
					return RET_ERROR;

				m_nSendFile = _open(SEND_FILE, O_RDONLY|O_BINARY);
				if (m_nSendFile == -1)
					return RET_ERROR;

				struct stat stat_buf;
				if (fstat(m_nSendFile, &stat_buf) == -1)
					return RET_ERROR;

				m_nFileSize = stat_buf.st_size;

				//Добавляем в предисловие результата http заголовок
 				std::ostringstream strStream;
				strStream << 
						"HTTP/1.1 200 OK\r\n"
						<< "Content-Type: image/jpeg\r\n"
						<< "Content-Length: " << m_nFileSize << "\r\n" <<
						"\r\n";

				//Запоминаем заголовок
				m_vSendBuffer.resize(strStream.str().length());
				memcpy(&m_vSendBuffer[0], strStream.str().c_str(), strStream.str().length());

				return RET_READY;
			}

7. Добавляем функции для посылки файла по протоколам tcp и ssl:

			const RETCODES SendFileSSL(const int nFile, off_t *offset)
			{
				if (nFile == -1 || m_vSendBuffer.size())
					return ContinueWrite();

				if (!m_bIsSSL || !m_pSSLContext || !m_pSSL)
					return RET_ERROR;

				static unsigned char buffer[4096];

				off_t lPos = _lseek(nFile, *offset, SEEK_SET);
				if (lPos == -1)
					return RET_ERROR;

				const int nReaded = _read(nFile, buffer, 4096);

				if (nReaded == -1)
					return RET_ERROR;

				if (nReaded > 0)
				{
					*offset  = nReaded;

					m_vSendBuffer.resize(nReaded);
					memcpy(&m_vSendBuffer[0], buffer, nReaded);
				}

				return RET_WAIT;
			}
			const RETCODES SendFileTCP(const int nFile, off_t *offset)
			{
				if (nFile == -1 || m_vSendBuffer.size())
					return ContinueWrite();

				const unsigned long long nSended = sendfile(m_hSocket, nFile, offset, 4096);
				if (nSended == (unsigned long long)-1)
					return RET_ERROR;

				m_nLastSocketError = WSAEWOULDBLOCK;
				return RET_WAIT;
			}
			const bool IsAllWrited() const
			{
				if (m_nSendFile == -1 && m_vSendBuffer.size())
					return true;

				if (m_nFileSize == (unsigned long long)m_nFilePos)
					return true;

				return false;
			}

8. Изменяем логику callback функции заказчика:

					case S_WRITING:
					{
						if (!m_bIsSSL && (SendFileTCP(m_nSendFile, &m_nFilePos) == RET_ERROR))
							return false;
						else if (m_bIsSSL && (SendFileSSL(m_nSendFile, &m_nFilePos) == RET_ERROR))
							return false;

						if (IsAllWrited())
							SetState(S_ALL_WRITED, pCurrentEvent);
						return true;
					}

Наш сервер готов!
Сейчас он может отправлять не только буфер, но и файлы. Допустимо кто-то подметил, что в классе заказчика добавилась переменная «m_nLastSocketError»…
Дело в том, что в предыдущих версиях сервера мы неизменно ожидали от сокетов всяких событий, сейчас переменная m_nLastSocketError поможет нам модифицировать функцию CClient::SetState так, Дабы функция epoll
от сокетов ожидала только тех событий, которые необходимы в данный момент.

			void SetState(const STATES state, struct epoll_event *pCurrentEvent) 
			{
				m_stateCurrent = state;

				pCurrentEvent->events = EPOLLERR | EPOLLHUP;
				if (m_bIsSSL)
				{
					if (m_nLastSocketError == SSL_ERROR_WANT_READ)
						pCurrentEvent->events |= EPOLLIN;
					if (m_nLastSocketError == SSL_ERROR_WANT_WRITE)
						pCurrentEvent->events |= EPOLLOUT;
					return;
				}

				if (m_nLastSocketError == WSAEWOULDBLOCK)
				{
					if (m_stateCurrent == S_READING)
						pCurrentEvent->events |= EPOLLIN;
					if (m_stateCurrent == S_WRITING)
						pCurrentEvent->events |= EPOLLOUT;
					return;
				}

				pCurrentEvent->events |= EPOLLIN | EPOLLOUT;
			}

Все готово!
Для компиляциив Visual Studio 2012, откройте файл ssl_test.sln и компилируйте.
Для компиляции в Linux файлы epoll.h, epoll.cpp, sendfile.h и sendfile.cpp не необходимы, Дабы все работало довольно скопировать в одну директорию файлы: serv.cpp, server.h, ca-cert.pem, сделать директорию wwwroot и скопировать туда файл ./wwwroot/festooningloops.jpg, потом в командной строке набрать: «g -std=c 0x -L/usr/lib -lssl -lcrypto serv.cpp» Кто для чего-то хочет видеть предупреждения компилятора, добавьте ему опцию -Wall.

Проверить работу сервера на локальном компьютере дозволено, запустив сервер и набрав в адресной строке браузера
https://localhost:1111
либо
http://localhost:8085

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB