Linux下c语言快速实现web服务器

    科技2024-08-11  25

    1. 前言

    最近整理了整理,把之前好玩的东西翻一下,这期就到了http了。 那么如何快速写一个http服务器?方法当然五花八门了,python、go几行就能实现出来了,那么c\c++如何实现呢?

    2. 相关接口

    libevent内置了http相关的处理方法(之前早期版本会有一些不稳定,现在event2比较靠谱了),同时支持openssl的方法,可以比较方便的从http升级到https。

    2.1 绑定接口

    指定一个地址和端口,绑定上http服务

    int evhttp_bind_socket(struct evhttp *http, const char * address, ev_uint16_t port); struct evhttp_bound_socket *evhttp_bind_socket_with_handle(struct evhttp *http, const char *address, ev_uint16_t port);

    2.2 请求处理

    请求的处理,使用的回调函数的注册

    void evhttp_set_gencb(struct evhttp *http, void(*cb)(struct evhttp_request *, void *), void *arg);

    2.3 响应回复

    支持直接回复、回复chunk、回复encoding的chunk,,,

    void evhttp_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *databuf); void evhttp_send_reply_chunk(struct evhttp_request *req, struct evbuffer *databuf); void evhttp_send_reply_chunk_with_cb(struct evhttp_request *, struct evbuffer *, void(*cb)(struct evhttp_connection *, void *), void *arg) void evhttp_send_reply_end(struct evhttp_request *req); void evhttp_send_reply_start(struct evhttp_request *req, int code, const char *reason);

    2.4 常用错误码:

    enum evhttp_request_error { EVREQ_HTTP_TIMEOUT, EVREQ_HTTP_EOF, EVREQ_HTTP_INVALID_HEADER, EVREQ_HTTP_BUFFER_ERROR, EVREQ_HTTP_REQUEST_CANCEL, EVREQ_HTTP_DATA_TOO_LONG }

    3. 实现

    https的实现参考了这位老哥的代码:https://github.com/ppelleti/https-example.git 主要目标是完成一个静态的http服务。

    重新包装整理了一下到mod_server类中,基本思路就是初始化、mod_server::load_cert、mod_server::dispatch

    #ifndef _MOD_SERVER_H_ #define _MOD_SERVER_H_ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <limits.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <event2/bufferevent.h> #include <event2/bufferevent_ssl.h> #include <event2/event.h> #include <event2/http.h> #include <event2/buffer.h> #include <event2/util.h> #include <event2/keyvalq_struct.h> #include <mutex> #include <string> #include "common.h" class mod_server { private: struct evhttp *_http; struct event_base *_base; struct evhttp_bound_socket *_handle; bool _use_ssl; SSL_CTX *_ssl_ctx; std::once_flag _flag_init; std::string _root; public: mod_server(const std::string &root = "/tmp/") : _http(NULL) , _base(NULL) , _handle(NULL) , _use_ssl(false) , _ssl_ctx(NULL) { _root = root; _base = event_base_new(); _http = evhttp_new(_base); _ssl_ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX_set_options(_ssl_ctx, SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_SSLv2); } ~mod_server() { evhttp_free(_http); event_base_free(_base); } void set_root(const std::string &root) { _root = root; printf("Set root: %s\n", root.c_str()); } int dispatch(const char *listen, uint16_t port); int load_cert(const char *cert_chain, const char *private_key); private: static void on_send_document(struct evhttp_request *req, void *arg) { ((class mod_server *)arg)->__on_send_document(req); } /** * This callback is responsible for creating a new SSL connection * and wrapping it in an OpenSSL bufferevent. This is the way * we implement an https server instead of a plain old http server. */ static struct bufferevent *on_https_new(struct event_base *base, void *arg) { struct bufferevent *bev; SSL_CTX *ctx = (SSL_CTX *)arg; printf("New https...\n"); bev = bufferevent_openssl_socket_new(base, -1, SSL_new(ctx), BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE); return bev; } private: void __on_send_document(struct evhttp_request *req); void __do_request_get(struct evhttp_request *req, const char *uri); void __do_request_post(struct evhttp_request *req, const char *uri); int __do_website_show(const char *file, struct evbuffer *evb, struct evhttp_request *req); }; #endif // #ifndef _MOD_SERVER_H_

    然后第一步看大循环dispatch,三步走:绑定接口、设置回调、启动循环。

    int mod_server::dispatch(const char *listen, uint16_t port) { if (_use_ssl) { /* This is the magic that lets evhttp use SSL. */ (void)evhttp_set_bevcb(_http, on_https_new, _ssl_ctx); } /* This is the callback that gets called when a request comes in. */ (void)evhttp_set_gencb(_http, on_send_document, this); /* Now we tell the evhttp what port to listen on */ _handle = evhttp_bind_socket_with_handle(_http, listen, port); if (!_handle) { printf("evhttp_bind_socket_with_handle: %s:%hu\n", listen, port); return -1; } printf("Listen: %s:%hu\n", listen, port); return event_base_dispatch(_base); }

    https会涉及到加载服务器证书,代码如下:

    int mod_server::load_cert(const char *cert_chain, const char *private_key) { std::call_once(_flag_init, &__ssl_init); /* Cheesily pick an elliptic curve to use with elliptic curve ciphersuites. * We just hardcode a single curve which is reasonably decent. * See http://www.mail-archive.com/openssl-dev@openssl.org/msg30957.html */ EC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); if (!ecdh) { printf("EC_KEY_new_by_curve_name"); goto err; } if (TRUE != SSL_CTX_set_tmp_ecdh(_ssl_ctx, ecdh)) { printf("SSL_CTX_set_tmp_ecdh"); goto err; } if (TRUE != SSL_CTX_use_certificate_chain_file(_ssl_ctx, cert_chain)) { printf("SSL_CTX_use_certificate_chain_file"); goto err; } if (TRUE != SSL_CTX_use_PrivateKey_file(_ssl_ctx, private_key, SSL_FILETYPE_PEM)) { printf("SSL_CTX_use_PrivateKey_file"); goto err; } if (TRUE != SSL_CTX_check_private_key(_ssl_ctx)) { printf("SSL_CTX_check_private_key"); goto err; } _use_ssl = true; return 0; err: ERR_print_errors_fp(stderr); return -1; }

    然后里面有个ssl初始化的动作,按github那个老哥的说法,早期的openssl还有个内存脏数据的问题,重设了一下内存接口。

    #ifdef __cplusplus extern "C" { #endif #if OPENSSL_VERSION_NUMBER < 0x1010000fL /* OpenSSL has a habit of using uninitialized memory. (They turn up their * nose at tools like valgrind.) To avoid spurious valgrind errors (as well * as to allay any concerns that the uninitialized memory is actually * affecting behavior), let's install a custom malloc function which is * actually calloc. */ static void *malloc2calloc(size_t howmuch) { return calloc(1, howmuch); } #endif static void __ssl_init() { signal(SIGPIPE, SIG_IGN); #if OPENSSL_VERSION_NUMBER < 0x1010000fL CRYPTO_set_mem_functions(malloc2calloc, realloc, free); #endif SSL_library_init(); SSL_load_error_strings(); OpenSSL_add_all_algorithms(); printf("Initialize: OpenSSL <%s>, Libevent <%s>\n", SSLeay_version(SSLEAY_VERSION), event_get_version()); } #ifdef __cplusplus } #endif

    下来看看处理请求的部分,on_send_document就是注册的请求处理函数,每一个请求都会调用;

    /* This callback gets invoked when we get any http request that doesn't match * any other callback. Like any evhttp server callback, it has a simple job: * it must eventually call evhttp_send_error() or evhttp_send_reply(). */ void mod_server::__on_send_document(struct evhttp_request *req) { const char *uri = evhttp_request_get_uri(req); switch (evhttp_request_get_command(req)) { case EVHTTP_REQ_GET: __do_request_get(req, uri); break; #if HAVE_POST case EVHTTP_REQ_POST: __do_request_post(req, uri); break; #endif default: evhttp_send_reply(req, HTTP_OK, "OK", NULL); break; } }

    GET方法,这块我们的主要目的是处理GET类型的请求,将静态文件反馈回去:

    void mod_server::__do_request_get(struct evhttp_request *req, const char *uri) { int res = -1; char fname[SIZE_NAME_LONG]; struct evbuffer *evb = evbuffer_new(); if (!evb) { printf("evbuffer_new\n"); return; } __get_website_path(_root.c_str(), uri, fname, sizeof(fname)); printf("%s\n", fname); res = __do_website_show(fname, evb, req); if (0 != res) { evhttp_send_reply(req, 404, "Not found", evb); } else { evhttp_send_reply(req, HTTP_OK, "OK", evb); } //free_evb: evbuffer_free(evb); return; }

        静态页面的显示,主要就是找到文件,然后evbuffer_add_file方法读取文件内容进行回显;     由于涉及到拼路径问题,所以在__get_website_path里面,处理了包含../../..的请求,避免任意路径访问;

    int mod_server::__do_website_show(const char *file, struct evbuffer *evb, struct evhttp_request *req) { int fd = -1; int res = -1; struct stat st = {0}; res = lstat(file, &st); if (0 != res) { printf("lstat: %s\n", file); return -1; } else if (!S_ISREG(st.st_mode)) { printf("%s not file\n", file); return -1; } else if (st.st_size == 0) { printf("skip x-empty\n"); return -1; } fd = open(file, O_RDONLY); if (fd < 0) { printf("open: %s\n", file); } evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", __get_content_type(file)); /* mmap to evbuffer, close-fd inside */ evbuffer_add_file(evb, fd, 0, st.st_size); return 0; } static inline void __get_website_path(const char *root, const char *uri, char *fname, size_t fmax) { while (uri[0] == '/' || uri[0] == '.') { uri++; } if (uri[0]) { snprintf(fname, fmax, "%s/%s", root, uri); } else { snprintf(fname, fmax, "%s/index.html", root); } }

    然后回显文件的时候,有个http头部信息content-type,这里按照后缀名进行转换处理

    static const struct table_entry { const char *extension; const char *content_type; } content_type_table[] = { { "txt", "text/plain" }, { "c", "text/plain" }, { "h", "text/plain" }, { "html", "text/html" }, { "htm", "text/htm" }, { "css", "text/css" }, { "gif", "image/gif" }, { "jpg", "image/jpeg" }, { "jpeg", "image/jpeg" }, { "png", "image/png" }, { "pdf", "application/pdf" }, { "ps", "application/postscript" }, { "js", "application/javascript" }, { NULL, NULL }, }; /* Try to guess a good content-type for 'path' */ static const char * __get_content_type(const char *path) { const char *last_period = NULL, *extension = NULL; const struct table_entry *ent = NULL; last_period = strrchr(path, '.'); if (!last_period || strchr(last_period, '/')) { goto not_found; /* no exension */ } extension = last_period + 1; for (ent = &content_type_table[0]; ent->extension; ++ent) { if (!evutil_ascii_strcasecmp(ent->extension, extension)) { return ent->content_type; } } not_found: return "application/misc"; }

    最后剩下一个POST方法的处理,这块没啥处理,仅打印了调试信息

    void mod_server::__do_request_post(struct evhttp_request *req, const char *uri) { char response[SIZE_LINE_LONG]; // = {0} struct evhttp_uri *decoded = NULL; struct evbuffer *evb = evbuffer_new(); if (!evb) { printf("evbuffer_new\n"); return; } printf("Got a POST request for <%s>\n", uri); /* Decode the URI */ decoded = evhttp_uri_parse(uri); if (!decoded) { printf("It's not a good URI. Sending BADREQUEST\n"); evhttp_send_error(req, HTTP_BADREQUEST, 0); goto free_evb; } /* Decode the payload */ { struct evkeyvalq kv; memset(&kv, 0, sizeof(kv)); struct evbuffer *buf = evhttp_request_get_input_buffer(req); evbuffer_add(buf, "", 1); /* NUL-terminate the buffer */ char *payload = (char *)evbuffer_pullup(buf, -1); if (0 != evhttp_parse_query_str(payload, &kv)) { printf("Malformed payload. Sending BADREQUEST\n"); evhttp_send_error(req, HTTP_BADREQUEST, 0); goto free_decode; } /* Determine peer */ char *peer_addr = NULL; ev_uint16_t peer_port; struct evhttp_connection *con = evhttp_request_get_connection(req); evhttp_connection_get_peer(con, &peer_addr, &peer_port); /* Extract passcode */ const char *passcode = evhttp_find_header(&kv, "passcode"); evutil_snprintf(response, sizeof(response), "Hi %s! I %s your passcode.\n", peer_addr, (0 == strcmp(passcode, "123456") ? "liked" : "didn't like")); evhttp_clear_headers(&kv); /* to free memory held by kv */ } /* This holds the content we're sending. */ evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "application/x-yaml"); evbuffer_add(evb, response, strlen(response)); evhttp_send_reply(req, HTTP_OK, "OK", evb); free_decode: evhttp_uri_free(decoded); free_evb: evbuffer_free(evb); return; }

    最后看一下main函数的调用,这里通过传参控制,如果传证书了,那么就走https方式,否则就走http

    int main(int argc, char *argv[]) { int res = -1; class mod_server https; if (argc < 2) { printf("http:\n"); printf("%s\n", argv[0]); printf("https:\n"); printf("%s <cert.pem> <key.pem> [webroot]\n", argv[0]); } else { res = https.load_cert(argv[1], argv[2]); if (0 != res) { goto out; } } if (argv[3]) { https.set_root(argv[3]); } res = https.dispatch("0.0.0.0", argv[2] ? 443 : 80); if (0 != res) { goto out; } out: printf("Result:\t\t\t\t[%s]\n", res ? "Failure" : "Success"); exit(res ? EXIT_FAILURE : EXIT_SUCCESS); }

    编译时需要连接一下-levent -levent_openssl -lssl -lcrypto, 直接指定了工作路径/usr/share/doc/libevent-2.1.8/api,运行效果如下:

    4. 结论

    一开始想着web服务还比较好实现,libevent封装了好多http的细节在接口里,但实际c写起来还是比python、go复杂一些呢。

    Processed: 0.014, SQL: 8