diff --git a/exercises/6/Makefile b/exercises/6/Makefile new file mode 100644 index 0000000..b00eed1 --- /dev/null +++ b/exercises/6/Makefile @@ -0,0 +1,38 @@ +# +# Makefile +# web_server +# +# created by changkun at labex.io +# + +CXX = g++ +EXEC_HTTP = server.http +EXEC_HTTPS = server.https + +SOURCE_HTTP = main.http.cpp +SOURCE_HTTPS = main.https.cpp + +OBJECTS_HTTP = main.http.o +OBJECTS_HTTPS = main.https.o + +LDFLAGS_COMMON = -std=c++11 -O3 -pthread -lboost_system +LDFLAGS_HTTP = +LDFLAGS_HTTPS = -lssl -lcrypto + +LPATH_COMMON = -I/usr/include/boost +LPATH_HTTP = +LPATH_HTTPS = -I/usr/include/openssl + +LLIB_COMMON = -L/usr/lib + +all: + make http + make https + +http: + $(CXX) $(SOURCE_HTTP) $(LDFLAGS_COMMON) $(LDFLAGS_HTTP) $(LPATH_COMMON) $(LPATH_HTTP) $(LLIB_COMMON) $(LLIB_HTTP) -o $(EXEC_HTTP) +https: + $(CXX) $(SOURCE_HTTPS) $(LDFLAGS_COMMON) $(LDFLAGS_HTTPS) $(LPATH_COMMON) $(LPATH_HTTPS) $(LLIB_COMMON) $(LLIB_HTTPS) -o $(EXEC_HTTPS) + +clean: + rm -f $(EXEC_HTTP) $(EXEC_HTTPS) *.o diff --git a/exercises/6/handler.hpp b/exercises/6/handler.hpp new file mode 100644 index 0000000..daf455b --- /dev/null +++ b/exercises/6/handler.hpp @@ -0,0 +1,96 @@ +// +// handler.hpp +// web_server +// created by changkun at changkun.de/modern-cpp +// + +#include "server.base.hpp" +#include + +using namespace std; +using namespace LabexWeb; + +template +void start_server(SERVER_TYPE &server) { + // resources request + + // processing POST /string, return the string from POST + server.resource["^/string/?$"]["POST"] = [](ostream& response, Request& request) { + // fetch string from istream (*request.content) + stringstream ss; + *request.content >> ss.rdbuf(); // read request to stringstream + string content=ss.str(); + + // return response + response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; + }; + + // process GET request from /info, return response + server.resource["^/info/?$"]["GET"] = [](ostream& response, Request& request) { + stringstream content_stream; + content_stream << "

Request:

"; + content_stream << request.method << " " << request.path << " HTTP/" << request.http_version << "
"; + for(auto& header: request.header) { + content_stream << header.first << ": " << header.second << "
"; + } + + // get the length of content_stream (use content.tellp() to get) + content_stream.seekp(0, ios::end); + + response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" << content_stream.rdbuf(); + }; + + // process GET request for /match/[digit+numbers], e.g. GET request is /match/abc123, will return abc123 + server.resource["^/match/([0-9a-zA-Z]+)/?$"]["GET"] = [](ostream& response, Request& request) { + string number=request.path_match[1]; + response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; + }; + + // peocess default GET request; anonymous function will be called if no other matches + // response files in folder web/ + // default: index.html + server.default_resource["^/?(.*)$"]["GET"] = [](ostream& response, Request& request) { + string filename = "www/"; + + string path = request.path_match[1]; + + // forbidden use `..` access content outside folder web/ + size_t last_pos = path.rfind("."); + size_t current_pos = 0; + size_t pos; + while((pos=path.find('.', current_pos)) != string::npos && pos != last_pos) { + current_pos = pos; + path.erase(pos, 1); + last_pos--; + } + + filename += path; + ifstream ifs; + // folder inspection across platform + if(filename.find('.') == string::npos) { + if(filename[filename.length()-1]!='/') + filename+='/'; + filename += "index.html"; + } + ifs.open(filename, ifstream::in); + + if(ifs) { + ifs.seekg(0, ios::end); + size_t length=ifs.tellg(); + + ifs.seekg(0, ios::beg); + + // copy file to response-stream , shouldn't use for large files + response << "HTTP/1.1 200 OK\r\nContent-Length: " << length << "\r\n\r\n" << ifs.rdbuf(); + + ifs.close(); + } else { + // return unable to open if file doesn't exists + string content="Could not open file "+filename; + response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; + } + }; + + // start HTTP(S) server + server.start(); +} diff --git a/exercises/6/main.http.cpp b/exercises/6/main.http.cpp new file mode 100644 index 0000000..6fc0d8b --- /dev/null +++ b/exercises/6/main.http.cpp @@ -0,0 +1,19 @@ +// +// main_http.cpp +// web_server +// created by changkun at changkun.de/modern-cpp +// + +#include +#include "server.http.hpp" +#include "handler.hpp" + +using namespace LabexWeb; + +int main() { + // HTTP server runs in port 12345 HTTP, enable 4 threads + Server server(12345, 4); + std::cout << "Server starting at port: 12345" << std::endl; + start_server>(server); + return 0; +} diff --git a/exercises/6/main.https.cpp b/exercises/6/main.https.cpp new file mode 100644 index 0000000..fd73727 --- /dev/null +++ b/exercises/6/main.https.cpp @@ -0,0 +1,18 @@ +// +// main_https.cpp +// web_server +// created by changkun at changkun.de/modern-cpp +// +#include +#include "server.https.hpp" +#include "handler.hpp" +using namespace LabexWeb; + +int main() { + // HTTPS server runs in port 12345, enable 4 threads + // Use certificates for security + Server server(12345, 4, "server.crt", "server.key"); + std::cout << "Server starting at port: 12345" << std::endl; + start_server>(server); + return 0; +} diff --git a/exercises/6/server.base.hpp b/exercises/6/server.base.hpp new file mode 100644 index 0000000..5cc6f9b --- /dev/null +++ b/exercises/6/server.base.hpp @@ -0,0 +1,197 @@ +// +// server_base.hpp +// web_server +// created by changkun at changkun.de/modern-cpp +// + +#ifndef SERVER_BASE_HPP +#define SERVER_BASE_HPP + +#include + +#include +#include +#include + +namespace LabexWeb { + struct Request { + // request method, POST, GET; path; HTTP version + std::string method, path, http_version; + // use smart pointer for reference counting of content + std::shared_ptr content; + // hash container, key-value dict + std::unordered_map header; + // use regular expression for path match + std::smatch path_match; + }; + + // use typedef simplify resource type + typedef std::map>> resource_type; + + // socket_type is HTTP or HTTPS + template + class ServerBase { + public: + resource_type resource; + resource_type default_resource; + + // construct server, initalize port, default: 1 thread + ServerBase(unsigned short port, size_t num_threads = 1) : + endpoint(boost::asio::ip::tcp::v4(), port), + acceptor(m_io_service, endpoint), + num_threads(num_threads) {} + + void start() { + // default resource in the end of vector, as response method + for(auto it = resource.begin(); it != resource.end(); it++) { + all_resources.push_back(it); + } + for(auto it = default_resource.begin(); it != default_resource.end(); it++) { + all_resources.push_back(it); + } + + // socket connection + accept(); + + // if num_threads>1, then m_io_service.run() + // it will start (num_threads-1) threads as thread pool + for(size_t c = 1;c < num_threads; c++) { + threads.emplace_back([this](){ + m_io_service.run(); + }); + } + + // main thread + m_io_service.run(); + + // wait for other threads finish + for(auto& t: threads) + t.join(); + } + protected: + // io_service is a dispatcher in asio library, all asynchronous io events are dispatched by it + // in another word, constructor of IO object need a io_service object as parameter + boost::asio::io_service m_io_service; + // IP address, port and protocol version as a endpoint, and generated on serverside + // tcp::acceptor object, wait for connection + boost::asio::ip::tcp::endpoint endpoint; + // thus, a acceptor object requires io_service and endpoint as parameters + boost::asio::ip::tcp::acceptor acceptor; + + // server side threads + size_t num_threads; + std::vector threads; + + // all resource will be append to the end of vector, and created in start() + std::vector all_resources; + + // requires to implement this method for different type of server + virtual void accept() {} + + void process_request_and_respond(std::shared_ptr socket) const { + // created cache for async_read_untile() + // shared_ptr will use for passing object to anonymous function + // the type will be deduce as std::shared_ptr + auto read_buffer = std::make_shared(); + + boost::asio::async_read_until(*socket, *read_buffer, "\r\n\r\n", + [this, socket, read_buffer](const boost::system::error_code& ec, size_t bytes_transferred) { + if(!ec) { + // Note: read_buffer->size() always equal to bytes_transferred, the document of Boost indicates: + // after async_read_until operation, streambuf contains some extra data out of delimiter + // thus, the best way is to read and parse the content from the left of read_buffer, then append the content of async_read + size_t total = read_buffer->size(); + + // convert istream to string-lines + std::istream stream(read_buffer.get()); + + // deduce the type of std::shared_ptr + auto request = std::make_shared(); + *request = parse_request(stream); + + size_t num_additional_bytes = total-bytes_transferred; + + // if satisfy then also read + if(request->header.count("Content-Length")>0) { + boost::asio::async_read(*socket, *read_buffer, + boost::asio::transfer_exactly(stoull(request->header["Content-Length"]) - num_additional_bytes), + [this, socket, read_buffer, request](const boost::system::error_code& ec, size_t bytes_transferred) { + if(!ec) { + // pointer as istream object stored in read_buffer + request->content = std::shared_ptr(new std::istream(read_buffer.get())); + respond(socket, request); + } + }); + } else { + respond(socket, request); + } + } + }); + } + + Request parse_request(std::istream& stream) const { + Request request; + + std::regex e("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$"); + + std::smatch sub_match; + + // read method, path and http version from the frist line + std::string line; + getline(stream, line); + line.pop_back(); + if(std::regex_match(line, sub_match, e)) { + request.method = sub_match[1]; + request.path = sub_match[2]; + request.http_version = sub_match[3]; + + bool matched; + e="^([^:]*): ?(.*)$"; + // parse head information + do { + getline(stream, line); + line.pop_back(); + matched=std::regex_match(line, sub_match, e); + if(matched) { + request.header[sub_match[1]] = sub_match[2]; + } + + } while(matched==true); + } + return request; + } + + void respond(std::shared_ptr socket, std::shared_ptr request) const { + // response after search requested path and method + for(auto res_it: all_resources) { + std::regex e(res_it->first); + std::smatch sm_res; + if(std::regex_match(request->path, sm_res, e)) { + if(res_it->second.count(request->method)>0) { + request->path_match = move(sm_res); + + // will be deduce to std::shared_ptr + auto write_buffer = std::make_shared(); + std::ostream response(write_buffer.get()); + res_it->second[request->method](response, *request); + + // capture write_buffer in lambda, make sure it can be destroyed after async_write + boost::asio::async_write(*socket, *write_buffer, + [this, socket, request, write_buffer](const boost::system::error_code& ec, size_t bytes_transferred) { + // HTTP 1.1 connection + if(!ec && stof(request->http_version)>1.05) + process_request_and_respond(socket); + }); + return; + } + } + } + } + + }; + + template + class Server : public ServerBase {}; +} +#endif /* SERVER_BASE_HPP */ diff --git a/exercises/6/server.http.hpp b/exercises/6/server.http.hpp new file mode 100644 index 0000000..9e53f28 --- /dev/null +++ b/exercises/6/server.http.hpp @@ -0,0 +1,38 @@ +// +// server_http.hpp +// web_server +// created by changkun at changkun.de/modern-cpp +// + +#ifndef SERVER_HTTP_HPP +#define SERVER_HTTP_HPP + +#include "server.base.hpp" + +namespace LabexWeb { + typedef boost::asio::ip::tcp::socket HTTP; + template<> + class Server : public ServerBase { + public: + // use port, thread number to construct web server + // http server is much simple than https since it doesn't need to initial config file + Server(unsigned short port, size_t num_threads=1) : + ServerBase::ServerBase(port, num_threads) {}; + private: + // implement accept() method + void accept() { + // create a new socket for current connection + // shared_ptr is used for passing temporal object to anonymous function + // socket will be deduce as type of std::shared_ptr + auto socket = std::make_shared(m_io_service); + + acceptor.async_accept(*socket, [this, socket](const boost::system::error_code& ec) { + // establish a connection + accept(); + // if no error + if(!ec) process_request_and_respond(socket); + }); + } + }; +} +#endif /* SERVER_HTTP_HPP */ diff --git a/exercises/6/server.https.hpp b/exercises/6/server.https.hpp new file mode 100644 index 0000000..380ae01 --- /dev/null +++ b/exercises/6/server.https.hpp @@ -0,0 +1,65 @@ +// +// server_https.hpp +// web_server +// created by changkun at changkun.de/modern-cpp +// + +#ifndef SERVER_HTTPS_HPP +#define SERVER_HTTPS_HPP + +#include "server.http.hpp" +#include + +namespace LabexWeb { + + // define HTTPS type + typedef boost::asio::ssl::stream HTTPS; + + // define HTTPS service, template type is HTTPS + template<> + class Server : public ServerBase { + public: + // a HTTPS server requires two more parameters: certificate file and private key file + Server(unsigned short port, size_t num_threads, + const std::string& cert_file, const std::string& private_key_file) : + ServerBase::ServerBase(port, num_threads), + context(boost::asio::ssl::context::sslv23) { + // use certificate file + context.use_certificate_chain_file(cert_file); + // use private key file, we need pass a new parameter to specify the format + context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); + } + + private: + // compare to HTTP server, we must define ssl context object + boost::asio::ssl::context context; + + // the difference between HTTPS and HTTP server + // is the construct difference of socket object + // HTTPS will encrypt the IO stream socket + // thus, accept() method must initialize ssl context + void accept() { + // create a new socket for current connection + // shared_ptr is used for passing temporal object to anonymous function + // socket will be deduce as std::shared_ptr + auto socket = std::make_shared(m_io_service, context); + + acceptor.async_accept( + (*socket).lowest_layer(), + [this, socket](const boost::system::error_code& ec) { + // accept a new connection + accept(); + + // if no error + if(!ec) { + (*socket).async_handshake(boost::asio::ssl::stream_base::server, + [this, socket](const boost::system::error_code& ec) { + if(!ec) process_request_and_respond(socket); + }); + } + }); + } + }; +} + +#endif /* SERVER_HTTPS_HPP */ diff --git a/exercises/6/www/index.html b/exercises/6/www/index.html new file mode 100644 index 0000000..2686da0 --- /dev/null +++ b/exercises/6/www/index.html @@ -0,0 +1,8 @@ + + + LabEx Web Server Test + + + Hello world in index.html. + + diff --git a/exercises/6/www/test.html b/exercises/6/www/test.html new file mode 100644 index 0000000..e1fac1b --- /dev/null +++ b/exercises/6/www/test.html @@ -0,0 +1,8 @@ + + + LabEx Web Server Test + + + Hello world in test.html. + + diff --git a/exercises/7/ThreadPool.hpp b/exercises/7/ThreadPool.hpp new file mode 100644 index 0000000..1b3eff9 --- /dev/null +++ b/exercises/7/ThreadPool.hpp @@ -0,0 +1,141 @@ +#ifndef THREAD_POOL_H +#define THREAD_POOL_H + +#include // std::vector +#include // std::queue +#include // std::make_shared + +#include // std::thread +#include // std::mutex, std::unique_lock +#include // std::condition_variable +#include // std::future, std::packaged_task + +#include // std::function, std::bind +#include // std::runtime_error +#include // std::move, std::forward + +class ThreadPool { +public: + + // initialize the number of concurrency threads + ThreadPool(size_t); + + // enqueue new thread task + template + auto enqueue(F&& f, Args&&... args) + -> std::future::type>; + + // destroy thread pool and all created threads + ~ThreadPool(); +private: + + // thread list, stores all threads + std::vector< std::thread > workers; + // queue task, the type of queue elements are functions with void return type + std::queue< std::function > tasks; + + // for synchonization + std::mutex queue_mutex; + // std::condition_variable is a new feature from c++11, + // it's a synchronization primitives. it can be used + // to block a thread or threads at the same time until + // all of them modified condition_variable. + std::condition_variable condition; + bool stop; +}; + +// constructor initialize a fixed size of worker +inline ThreadPool::ThreadPool(size_t threads): stop(false) +{ + // initialize worker + for(size_t i = 0;i task; + + // critical section + { + // get mutex + std::unique_lock lock(this->queue_mutex); + + // block current thread + this->condition.wait(lock, + [this]{ return this->stop || !this->tasks.empty(); }); + + // return if queue empty and task finished + if(this->stop && this->tasks.empty()) + return; + + // otherwise execute the first element of queue + task = std::move(this->tasks.front()); + this->tasks.pop(); + } + + // execution + task(); + } + } + ); +} + +// Enqueue a new thread +// use variadic templates and tail return type +template +auto ThreadPool::enqueue(F&& f, Args&&... args) + -> std::future::type> +{ + // deduce return type + using return_type = typename std::result_of::type; + + // fetch task + auto task = std::make_shared< std::packaged_task >( + std::bind(std::forward(f), std::forward(args)...) + ); + + std::future res = task->get_future(); + + // critical section + { + std::unique_lock lock(queue_mutex); + + // avoid add new thread if theadpool is destroied + if(stop) + throw std::runtime_error("enqueue on stopped ThreadPool"); + + // add thread to queue + tasks.emplace([task]{ (*task)(); }); + } + + // notify a wait thread + condition.notify_one(); + return res; +} + +// destroy everything +inline ThreadPool::~ThreadPool() +{ + // critical section + { + std::unique_lock lock(queue_mutex); + stop = true; + } + + // wake up all threads + condition.notify_all(); + + // let all processes into synchronous execution, use c++11 new for-loop: for(value:values) + for(std::thread &worker: workers) + worker.join(); +} + +#endif diff --git a/exercises/7/main.cpp b/exercises/7/main.cpp new file mode 100644 index 0000000..324a30e --- /dev/null +++ b/exercises/7/main.cpp @@ -0,0 +1,40 @@ +#include // std::cout, std::endl + +#include // std::vector +#include // std::string +#include // std::future +#include // std::this_thread::sleep_for +#include // std::chrono::seconds + +#include "ThreadPool.hpp" + +int main() +{ + // create a thread pool with max. 4 concurrency threads + ThreadPool pool(4); + // create execution results list + std::vector< std::future > results; + + // start eight thread task + for(int i = 0; i < 8; ++i) { + // add all task to result list + results.emplace_back( + // ass print task to thread pool + pool.enqueue([i] { + std::cout << "hello " << i << std::endl; + // wait a sec when the previous line is out + std::this_thread::sleep_for(std::chrono::seconds(1)); + // keep output and return the status of execution + std::cout << "world " << i << std::endl; + return std::string("---thread ") + std::to_string(i) + std::string(" finished.---"); + }) + ); + } + + // outputs + for(auto && result: results) + std::cout << result.get() << ' '; + std::cout << std::endl; + + return 0; +}