tutorial-09-http_file_server.cc
http_file_server is a web server. You can start a web server after specifying the startup port and the root path (the default setting is the current path).
You can also specify a certificate file and a key file in PEM format to start an HTTPS web server.
The program mainly demonstrates how to use disk IO tasks. In the Linux system, we use the aio interface in the kernel of Linux, and the file reading is completely asynchronous.
For starting a server, the steps are almost the same as those when starting an echo server or an HTTP proxy. There is one more way to start an SSL server here:
class WFServerBase
{
...
int start(unsigned short port, const char *cert_file, const char *key_file);
...
};
In other words, you can specify a cert file and a key file in PEM format to start an SSL server.
In addition, when you define a server, you can use std::bind() to bind a root parameter to the process. The root parameter means the root path of the service.
void process(WFHttpTask *server_task, const char *root)
{
...
}
int main(int argc, char *argv[])
{
...
const char *root = (argc >= 3 ? argv[2] : ".");
auto&& proc = std::bind(process, std::placeholders::_1, root);
WFHttpServer server(proc);
// start server
...
}
Similar to http_proxy, no threads are occupied in file reading. Instead, an asynchronous task is generated to read files, and a reply to the request is generated after the reading is completed.
Please note again that the complete reply data should be read into the memory before the reply message is sent. Therefore, it is not suitable for transferring very large files.
void process(WFHttpTask *server_task, const char *root)
{
// generate abs path.
...
int fd = open(abs_path.c_str(), O_RDONLY);
if (fd >= 0)
{
size_t size = lseek(fd, 0, SEEK_END);
void *buf = malloc(size); /* As an example, assert(buf != NULL); */
WFFileIOTask *pread_task;
pread_task = WFTaskFactory::create_pread_task(fd, buf, size, 0,
pread_callback);
/* To implement a more complicated server, please use series' context
* instead of tasks' user_data to pass/store internal data. */
pread_task->user_data = resp; /* pass resp pointer to pread task. */
server_task->user_data = buf; /* to free() in callback() */
server_task->set_callback([](WFHttpTask *t){ free(t->user_data); });
series_of(server_task)->push_back(pread_task);
}
else
{
resp->set_status_code("404");
resp->append_output_body("<html>404 Not Found.</html>");
}
}
Unlike http_proxy that generates a new HTTP client task, here a pread task is generated by the factory.
WFAlgoTaskFactory.h contains the definitions of relevant interfaces.
struct FileIOArgs
{
int fd;
void *buf;
size_t count;
off_t offset;
};
...
using WFFileIOTask = WFFileTask<struct FileIOArgs>;
using fio_callback_t = std::function<void (WFFileIOTask *)>;
...
class WFTaskFactory
{
public:
...
static WFFileIOTask *create_pread_task(int fd, void *buf, size_t count, off_t offset,
fio_callback_t callback);
static WFFileIOTask *create_pwrite_task(int fd, void *buf, size_t count, off_t offset,
fio_callback_t callback);
...
};
Both pread and pwrite return WFFileIOTask. We do not distinguish between sort and psort, and we do not distinguish between client and server task. They all follow the same principle.
In addition to these two interfaces, preadv and pwritev return WFFileVIOTask; fsync and fdsync return WFFileSyncTask. You can see the details in the header file.
Currently, our interface requires users to open and close fd by themselves. We are developing a set of file management interfaces. In the future, users only need to pass file names, which is more friendly to cross-platform applications.
The example uses the user_data field of the task to save the global data of the service. For larger services, we recommend to use series context. You can see the proxy examples for details.
using namespace protocol;
void pread_callback(WFFileIOTask *task)
{
FileIOArgs *args = task->get_args();
long ret = task->get_retval();
HttpResponse *resp = (HttpResponse *)task->user_data;
close(args->fd);
if (ret < 0)
{
resp->set_status_code("503");
resp->append_output_body("<html>503 Internal Server Error.</html>");
}
else /* Use '_nocopy' carefully. */
resp->append_output_body_nocopy(args->buf, ret);
}
Use get_args() of the file task to get the input parameters. Here it is a FileIOArgs struct.
Use get_retval() to get the return value of the operation. If ret < 0, the task fails. Otherwise, the ret is the size of the read data.
In the file task, ret < 0 and task->get_state()! = WFT_STATE_SUCCESS are completely equivalent.
The memory of the buf domain is managed by ourselves. You can use append_output_body_nocopy() to pass that memory to resp.
After the reply is completed, we will free() this block of memory with this line in the process:
server_task->set_callback([](WFHttpTask *t){ free(t->user_data); });
Linux operating system supports a set of asynchronous IO system calls with high efficiency and very little CPU occupation. If you use our framework in a Linux system, this set of interfaces are used by default.
We have implemented a set of posix aio interfaces to support other UNIX systems, and used the sigevent notification method of threads, but it is no longer in use because of its low efficiency.
Currently, for non-Linux systems, asynchronous IO is always simulated by multi-threading. When an IO task arrives, a thread is created in real time to execute IO tasks, and then a callback is used to return to the handler thread pool.
Multi-threaded IO is also the only choice in macOS, because macOS does not have good sigevent support and posix aio will not work in macOS.
Multi-threaded IO does not support preadv and pwritev tasks. When these two tasks are created and run, you will get an ENOSYS error in the callback.
Some UNIX systems do not support fdatasync. In this case, an fdsync task is equivalent to an fsync task.