📜 ⬆️ ⬇️

Cross-platform https server with non-blocking sockets. Part 3

In this article, I continue to improve single-threaded https server on non-blocking sockets. Previous articles with links to source code can be found here:
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

At the end of this article there will be a link to the source code of the server, which I tested in Visual Studio 2012 (Windows 8 64bit), g ++ 4.4 (Linux 32bit), g ++ 4.6 (Linux 64bit). The server accepts connections from any number of clients and sends request headers in response.
But I begin the article perhaps with answers to some of the comments to the previous ones.


Firstly, having received a lot of negative feedback about the unusualness of my code, from now on I decided to place my articles in the “Abnormal Programming” hub.
Secondly, I decided not to tick the “tutorial” anymore: someone will find something new in my articles, and to someone they seem amateurish. I'm not against…
')
Now about my programming style:
1. I will continue to write code in the header files for a number of reasons:
a) I want to know the full number of lines of code without additional gestures and therefore it is more convenient for me.
b) At any moment I may want to screw the template to the client or server, and I would not like to rewrite all the code for this.
Those who are sure that since I can’t do it, you can teach the creators of stl and boost programming first, and then rename the server.h file to server.cpp and it will be good for everyone ...

2. I will leave an infinite loop in the constructor for one reason: I think this approach is correct. If a class does nothing more than changing its internal variables, then the most correct thing is to leave this class a single public function: its constructor.
You can of course in this case without a class at all, but I’m somehow more familiar with the class, and I don’t need global functions from scratch either.

3. I will not use std :: copy instead of memcpy for one reason: std :: copy - a brake!

Finally I want to thank everyone who was not too lazy to compile the source and point out some errors. I tried to take them into account and correct.

Now about the main thing.
In order for the server from the previous article to finally prepare to parse the request headers and distribute files, it remains to make one small addition: to start instead of an endless loop, use functions specially designed for passively waiting for network events.
There are several such functions in Windows and Linux, I suggest using select on Windows and epoll on Linux.

There is a problem that the epoll function in Windows does not exist. To make the code look consistent on all systems, let's write the server code as if there is an epoll in Windows!

A simple implementation of epoll for Windows with select
1. Add to the Visual Studio project two empty files from the same directory as “server.h”. Files: “epoll.h” and “epoll.cpp”.
2. Transfer the definitions of constants, structures and functions from the epoll documentation to the epoll.h file:
#ifndef __linux__ enum EPOLL_EVENTS { EPOLLIN = 0x001, #define EPOLLIN EPOLLIN EPOLLPRI = 0x002, #define EPOLLPRI EPOLLPRI EPOLLOUT = 0x004, #define EPOLLOUT EPOLLOUT EPOLLRDNORM = 0x040, #define EPOLLRDNORM EPOLLRDNORM EPOLLRDBAND = 0x080, #define EPOLLRDBAND EPOLLRDBAND EPOLLWRNORM = 0x100, #define EPOLLWRNORM EPOLLWRNORM EPOLLWRBAND = 0x200, #define EPOLLWRBAND EPOLLWRBAND EPOLLMSG = 0x400, #define EPOLLMSG EPOLLMSG EPOLLERR = 0x008, #define EPOLLERR EPOLLERR EPOLLHUP = 0x010, #define EPOLLHUP EPOLLHUP EPOLLRDHUP = 0x2000, #define EPOLLRDHUP EPOLLRDHUP EPOLLONESHOT = (1 << 30), #define EPOLLONESHOT EPOLLONESHOT EPOLLET = (1 << 31) #define EPOLLET EPOLLET }; /* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */ #define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */ #define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */ #define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */ typedef union epoll_data { void *ptr; int fd; unsigned int u32; unsigned __int64 u64; } epoll_data_t; struct epoll_event { unsigned __int64 events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); #endif 


3. In the epoll.cpp file we add headers, as well as a global variable in which sockets and their states will be stored:
 #include "epoll.h" #include <map> #ifndef WIN32 #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #else #include <io.h> #include <Winsock2.h> #pragma comment(lib, "ws2_32.lib") #endif std::map<int, epoll_event> g_mapSockets; 


4. Add the code for the first function:
 int epoll_create(int size) { return 1; } 


What's going on here?
As far as I can tell from the documentation: the original Linux code on every call to epoll_create creates a file that stores the states of the sockets. Apparently this is necessary in multi-threaded processes.
We have a single-threaded process and we do not need more than one structure for storing sockets. Therefore, epoll_create is a stub.

5. Using stl, adding and removing sockets in memory is as simple as:
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) { switch(op) { case EPOLL_CTL_ADD: case EPOLL_CTL_MOD: g_mapSockets[fd] = *event; return 0; case EPOLL_CTL_DEL: if (g_mapSockets.find(fd) == g_mapSockets.end()) return -1; g_mapSockets.erase(fd); return 0; } return 0; } 


6. Finally, the main thing: we implement the waiting function through select

 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) { if ((!events) || (!maxevents)) return -1; //      select fd_set readfds, writefds, exceptfds; FD_ZERO(&readfds); FD_ZERO(&writefds); FD_ZERO(&exceptfds); //   int nFDS = 0; for (auto it=g_mapSockets.begin(); it != g_mapSockets.end(); ++it) { if (it->first == -1) continue; if (it->first > nFDS) nFDS = it->first; FD_SET(it->first, &readfds); FD_SET(it->first, &writefds); FD_SET(it->first, &exceptfds); } //   struct timeval tv; tv.tv_sec = timeout/1000; tv.tv_usec = timeout - tv.tv_sec*1000; //  nFDS++; select(nFDS, &readfds, &writefds, &exceptfds, &tv); //     ,     epoll int nRetEvents = 0; for (auto it=g_mapSockets.begin(); (it != g_mapSockets.end() && nRetEvents < maxevents); ++it) { if (it->first == -1) continue; if (!FD_ISSET(it->first, &readfds) && !FD_ISSET(it->first, &writefds) && !FD_ISSET(it->first, &exceptfds)) continue; memcpy(&events[nRetEvents].data, &it->second.data, sizeof(epoll_data)); if (FD_ISSET(it->first, &readfds)) events[nRetEvents].events |= EPOLLIN; if (FD_ISSET(it->first, &writefds)) events[nRetEvents].events |= EPOLLOUT; if (FD_ISSET(it->first, &exceptfds)) events[nRetEvents].events |= EPOLLERR; nRetEvents++; } return nRetEvents; } 


That's all. The epoll feature for Windows is implemented!

Adding epoll to the server

1. Add to headers:
 #ifdef __linux__ #include <sys/epoll.h> #else #include "epoll.h" #endif 


2. Add the following lines to the CServer class:
  private: //   struct epoll_event m_ListenEvent; //   vector<struct epoll_event> m_events; int m_epoll; 


3. In the CServer constructor, all that after calling the listen function is changed to:
  m_epoll = epoll_create (1); if (m_epoll == -1) { printf("error: epoll_create\n"); return; } m_ListenEvent.data.fd = listen_sd; m_ListenEvent.events = EPOLLIN | EPOLLET; epoll_ctl (m_epoll, EPOLL_CTL_ADD, listen_sd, &m_ListenEvent); while(true) { m_events.resize(m_mapClients.size()+1); int n = epoll_wait (m_epoll, &m_events[0], m_events.size(), 5000); if (n == -1) continue; Callback(n); } 


4. We change the old CServer :: Callback function to a new one:
  void Callback(const int nCount) { for (int i = 0; i < nCount; i++) { SOCKET hSocketIn = m_events[i].data.fd; if (m_ListenEvent.data.fd == (int)hSocketIn) { if (!m_events[i].events == EPOLLIN) continue; 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) { //      m_mapClients[sd] = shared_ptr<CClient>(new CClient(sd)); auto it = m_mapClients.find(sd); if (it == m_mapClients.end()) continue; //    epoll struct epoll_event ev = it->second->GetEvent(); epoll_ctl (m_epoll, EPOLL_CTL_ADD, it->first, &ev); } continue; } auto it = m_mapClients.find(hSocketIn); //    if (it == m_mapClients.end()) continue; if (!it->second->Continue()) // -   { //   false,     epoll     epoll_ctl (m_epoll, EPOLL_CTL_DEL, it->first, NULL); m_mapClients.erase(it); } } } 


With the server class finished, it remains to deal with the CClient class.
Add the following code to it:
  private: //   struct epoll_event m_ClientEvent; public: const struct epoll_event GetEvent() const {return m_ClientEvent;} 


And with this the addition of the epoll support code is complete!

Here is a project for Visual Studio: l0.3s3s.org
To compile to Linux, the epoll.h and epoll.cpp files are not needed, that is, everything is as usual: “copy the following files into one directory: serv.cpp, server.h, ca-cert.pem and type in the command line:“ g ++ -std = c ++ 0x -L / usr / lib -lssl -lcrypto serv.cpp »„

Continuation

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


All Articles