📜 ⬆️ ⬇️

Cross-platform server with non-blocking sockets. Part 4

This article continues my previous ones:
The easiest cross-platform server with ssl support
Cross-platform https server with non-blocking sockets
Cross-platform https server with non-blocking sockets. Part 2
Cross-platform https server with non-blocking sockets. Part 3

In my articles, I gradually describe the process of creating a single-threaded cross-platform server on non-blocking sockets.
In all previous articles, the server received and sent messages only via the ssl protocol. In this article I will describe the addition to the server of support for the usual unencrypted tcp protocol and teach the server to send a graphic file to the browser.
But first, a little walk through the comments on previous articles.


1. I listened to tips to get rid of the printf function in favor of std :: cout.
2. Smart people have proven to me that std :: memcpy and std :: copy are the same for the compiler.
Memcpy is more convenient for me, so I will continue to use it.
3. I transferred all early releases and will transfer future ones to GitHub , although in my opinion the client for Windows is terrible.
4. Who believes the lines
const char on = 1; setsockopt(listen_sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) ); 

will help to avoid the “Address already in use” error in case of emergency restart of the server - they are cruelly mistaken. Will not help.
5. For those who believe that different classes always need to be spread across different files, I'll add some oil: I want to transfer the CClient class to the private section of the CServer class!
')
It was:
 CClient { *** }; CServer { *** }; 

It became:
 CServer { CClient { *** }; *** }; 


Now, if the server becomes a library, no one should have thought about using the CClient class: this is a utility class, designed exclusively for interacting with the CServer class.

6. And in my opinion, the function main () is an atavism, inherited by programmers from SI. In C ++, it is superfluous. But compilers do not yet know this unfortunately.
But I decided to “punish” this unnecessary function, taking away from her the ability to do something - changed the serv.cpp file as follows:

 #include "server.h" const server::CServer s(8085, 1111); int main() {return 0;} 


Now the main thing ...

Adding non-encrypted tcp connections to the server


Unencrypted and encrypted connections in servers are usually accepted on different ports, so the first thing to do is to change the server constructor and add variables for another listening socket.

Instead
 struct epoll_event m_ListenEvent; 


write in server class
 struct epoll_event m_ListenEventTCP, m_ListenEventSSL; 


In the server constructor, we add port numbers and code for Linux, which will not allow the server to crash if there is an error in TCP operations:
  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 


Let's write separate functions to initiate listening sockets and to add a new client:
  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, '\0', 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); } } 


Now, in the client, we will add the m_bIsSSL variable, which we will initiate in the constructor, and then we will change the callback functions so that they can work with TCP connections:
Instead
  const RETCODES AcceptSSL() { if (!m_pSSLContext) //     SSL return RET_ERROR; 

The time will be:
  const RETCODES AcceptSSL() { cout << "AcceptSSL\n"; if (!m_bIsSSL) return RET_READY; if (!m_pSSLContext) return RET_ERROR; 


As you can see, there is no place to be easier: the TCP accept function does not require any additional gestures in order to begin receiving and sending data.
No TCP certificates are needed, so the beginning of the corresponding function will now look like this:
  const RETCODES GetSertificate() { cout << "GetSertificate\n"; if (!m_bIsSSL) return RET_READY; 


In a function that reads data from the ContinueRead () client, you need instead
  unsigned char szBuffer[4096]; const int err = SSL_read (m_pSSL, szBuffer, 4096); //      


write code:
  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); 


In the same function, you now need to add error handling code for TCP connections. As with SSL, the error will be
if the message receive function returns a negative or zero value. But since we have non-blocking sockets,
then the WSAEWOULDBLOCK error on Windows and EWOULDBLOCK on Linux means that everything is fine, you just have to wait.
Add these macros:
 #ifndef _WIN32 #define S_OK 0 #define WSAEWOULDBLOCK EWOULDBLOCK #define WSAGetLastError() errno #endif 


And such code in the ContinueRead function:
  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; } 


and we define the CClient :: GetLastError function as
  private: int GetLastError(int err) const { if (m_bIsSSL) return SSL_get_error(m_pSSL, err); else return WSAGetLastError(); } 


In a completely similar way, we will correct the function of sending messages to ContinueWrite and our single-threaded cross-platform server is ready to accept tcp and ssl connections from clients to give them request headers.
Let's also teach our server today to give files to clients.
In principle, there is nothing special about this, except that in Linux there is a faster way to send a file than on other systems: the sendfile function.
To make the code uniform, I suggest that you do the same with sendfile as you did with epoll: write an emulator of this function for all systems except Linux.

Emulate sendfile function

1. Create empty files “sendfile.h”, “sendfile.cpp” and add them to the Visual Studio project.
2. In sendfile.h we place such code:
 #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. In sendfile.cpp we place this:

 #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. Add the necessary inclusions to the server class so that standard functions are used in Linux, and ours are used in other systems.
In addition, we will add an inclusion for working with files and define the path to the file that we will send to the client:
 #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" 


With emulation sendfile finished.

Sending a file

5. Add a file descriptor and current position to the client class.
  class CClient { int m_nSendFile; off_t m_nFilePos; unsigned long long m_nFileSize; 


6. Change the InitRead () function
  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. Add functions to send a file using the tcp and ssl protocols:
  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. Change the logic of the client’s callback function:
  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; } 


Our server is ready!
Now he can send not only a buffer, but also files. Maybe someone noticed that the variable “m_nLastSocketError” was added to the client class ...
The fact is that in previous versions of the server, we always waited for any events from the sockets, now the m_nLastSocketError variable will help us modify the CClient :: SetState function so that the epoll function
From sockets I waited only for those events that are needed at the moment.
  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; } 


All is ready!
To compile with Visual Studio 2012 , open the ssl_test.sln file and compile.
The epoll.h, epoll.cpp, sendfile.h and sendfile.cpp files are not needed for compiling Linux , so that everything works, just copy the files to one directory: serv.cpp, server.h, ca-cert.pem, create a wwwroot directory and copy the ./wwwroot/festooningloops.jpg file there, then type in the command line: "g ++ -std = c ++ 0x -L / usr / lib -lssl -lcrypto serv.cpp" Someone who wants to see the compiler warnings, add the -Wall option to it.

You can check the server on the local computer by starting the server and typing in the address bar of the browser
localhost:1111
or
localhost:8085

Source: https://habr.com/ru/post/213301/


All Articles