本文转载自:史上最详细的网络编程实战教程
仓库:https://github.com/ithewei/libhv
libhv教程01–介绍与体验
名称由来
libhv
是一个类似于libevent、libev、libuv
的跨平台网络库,提供了带非阻塞IO和定时器的事件循环。 libhv的名称也正是继承此派,寓意高性能的事件循环High-performance event loop library
。
libhv能干什么
编写跨平台C/C++程序;
基于TCP/UDP/SSL开发自定义协议网络程序;
编写HTTP客户端/服务端程序;
编写WebSocket客户端/服务端程序;
学习实践网络编程;
libhv和libevent、libev、libuv有什么不同
libevent最为古老、有历史包袱,bufferevent虽为精妙,却也难以理解使用;
libev可以说是libevent的简化版,代码极为精简,但宏定义用的过多,代码可读性不强,且在Windows上实现不佳;
libuv是nodejs的c底层库,最先也是由libevent+对Windows IOCP支持,后来才改写自成一体,同时实现了管道、文件的异步读写,很强大,但结构体比较多,封装比较深,uv_write
个人感觉难用;
libhv本身是参考了libevent、libev、libuv的实现思路,它们的核心都是事件循环(即在一个事件循环中处理IO、定时器等事件),但提供的接口最为精简,API接近原生系统调用,最容易上手;
具体这几个库的写法比较见echo-servers ,可见libhv是最简单的;
此外libhv
集成了SSL/TLS
加密通信,支持心跳、转发、拆包、多线程安全write
和close
等特性,实现了HTTP
、WebSocket
等协议;
当然这几个库的性能是接近的,都将非阻塞IO多路复用用到了极致;
体验
linux与mac下的用户可直接执行./getting_started.sh
脚本,即可体验使用libhv编写的http服务端httpd
与http客户端curl
的便利之处。
运行效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv$ ./getting_started.sh Welcome to libhv! Press any key to run ... make httpd curl [config.mk] PREFIX=/usr/local INSTALL_INCDIR=$(PREFIX)/include/hv INSTALL_LIBDIR=$(PREFIX)/lib WITH_PROTOCOL=no WITH_EVPP=yes WITH_HTTP=yes WITH_HTTP_SERVER=yes WITH_HTTP_CLIENT=yes WITH_MQTT=no ENABLE_UDS=no ENABLE_WINDUMP=no USE_MULTIMAP=no WITH_CURL=no WITH_NGHTTP2=no WITH_OPENSSL=no WITH_GNUTLS=no WITH_MBEDTLS=no WITH_KCP=no CONFIG_DATE=20220224 checking for compiler... CC = gcc CXX = g++ gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 checking for os... HOST_OS = Linux HOST_ARCH = x86_64 TARGET_PLATFORM = x86_64-linux-gnu TARGET_OS = Linux TARGET_ARCH = x86_64 >> hconfig.h checking for stdbool.h... yes checking for stdint.h... yes checking for stdatomic.h... yes checking for sys/types.h... yes checking for sys/stat.h... yes checking for sys/time.h... yes checking for fcntl.h... yes checking for pthread.h... yes checking for endian.h... yes checking for sys/endian.h... no checking for gettid... no checking for strlcpy... no checking for strlcat... no checking for clock_gettime... yes checking for gettimeofday... yes checking for pthread_spin_lock... yes checking for pthread_mutex_timedlock... yes checking for sem_timedwait... yes checking for pipe... yes checking for socketpair... yes checking for eventfd... yes checking for setproctitle... no checking for WITH_OPENSSL=no checking for WITH_GNUTLS=no checking for WITH_MBEDTLS=no checking for ENABLE_UDS=no checking for USE_MULTIMAP=no checking for WITH_KCP=no configure done . make -f Makefile.in clean SRCDIRS=". base ssl event event/kcp util cpputil evpp protocol http http/client http/server mqtt" make[1]: 进入目录“/home/sv/pengeHome/libhv” cc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 OS = Linux ARCH = x86_64 MAKE = make CC = cc CXX = g++ CFLAGS = -O2 -fPIC -std=c99 CXXFLAGS = -O2 -fPIC -std=c++11 CPPFLAGS = -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Ievent/kcp -Iutil -Icpputil -Ievpp -Iprotocol -Ihttp -Ihttp/client -Ihttp/server -Imqtt LDFLAGS = -Llib -L3rd/lib -L3rd/lib/x86_64-linux-gnu -lstdc++ -lpthread -lm -ldl -lrt TARGET = test TARGET_TYPE = EXECUTABLE TARGET_PLATFORM = x86_64-linux-gnu BUILD_TYPE = RELEASE SRCS=base/htime.c base/rbtree.c base/hlog.c base/hsocket.c base/hversion.c base/herr.c base/hmain.c base/hbase.c ssl/nossl.c ssl/appletls.c ssl/openssl.c ssl/gnutls.c ssl/mbedtls.c ssl/hssl.c ssl/wintls.c event/unpack.c event/evport.c event/poll.c event/rudp.c event/nlog.c event/hloop.c event/iocp.c event/epoll.c event/hevent.c event/kqueue.c event/nio.c event/noevent.c event/select.c event/overlapio.c event/kcp/hkcp.c event/kcp/ikcp.c util/sha1.c util/md5.c util/base64.c cpputil/hasync.cpp cpputil/hdir.cpp cpputil/hstring.cpp cpputil/RAII.cpp cpputil/ThreadLocalStorage.cpp cpputil/ifconfig.cpp cpputil/hpath.cpp cpputil/hurl.cpp cpputil/iniparser.cpp protocol/dns.c protocol/ftp.c protocol/smtp.c protocol/icmp.c http/wsdef.c http/multipart_parser.c http/http_parser.c http/httpdef.c http/websocket_parser.c http/HttpParser.cpp http/Http1Parser.cpp http/http_content.cpp http/WebSocketChannel.cpp http/WebSocketParser.cpp http/HttpMessage.cpp http/Http2Parser.cpp http/client/WebSocketClient.cpp http/client/HttpClient.cpp http/client/AsyncHttpClient.cpp http/server/HttpMiddleware.cpp http/server/HttpHandler.cpp http/server/FileCache.cpp http/server/HttpResponseWriter.cpp http/server/HttpServer.cpp http/server/http_page.cpp http/server/HttpService.cpp mqtt/mqtt_client.c mqtt/mqtt_protocol.c OBJS=base/htime.o base/rbtree.o base/hlog.o base/hsocket.o base/hversion.o base/herr.o base/hmain.o base/hbase.o ssl/nossl.o ssl/appletls.o ssl/openssl.o ssl/gnutls.o ssl/mbedtls.o ssl/hssl.o ssl/wintls.o event/unpack.o event/evport.o event/poll.o event/rudp.o event/nlog.o event/hloop.o event/iocp.o event/epoll.o event/hevent.o event/kqueue.o event/nio.o event/noevent.o event/select.o event/overlapio.o event/kcp/hkcp.o event/kcp/ikcp.o util/sha1.o util/md5.o util/base64.o cpputil/hasync.o cpputil/hdir.o cpputil/hstring.o cpputil/RAII.o cpputil/ThreadLocalStorage.o cpputil/ifconfig.o cpputil/hpath.o cpputil/hurl.o cpputil/iniparser.o protocol/dns.o protocol/ftp.o protocol/smtp.o protocol/icmp.o http/wsdef.o http/multipart_parser.o http/http_parser.o http/httpdef.o http/websocket_parser.o http/HttpParser.o http/Http1Parser.o http/http_content.o http/WebSocketChannel.o http/WebSocketParser.o http/HttpMessage.o http/Http2Parser.o http/client/WebSocketClient.o http/client/HttpClient.o http/client/AsyncHttpClient.o http/server/HttpMiddleware.o http/server/HttpHandler.o http/server/FileCache.o http/server/HttpResponseWriter.o http/server/HttpServer.o http/server/http_page.o http/server/HttpService.o mqtt/mqtt_client.o mqtt/mqtt_protocol.o rm -r 2>/dev/null base/htime.o base/rbtree.o base/hlog.o base/hsocket.o base/hversion.o base/herr.o base/hmain.o base/hbase.o ssl/nossl.o ssl/appletls.o ssl/openssl.o ssl/gnutls.o ssl/mbedtls.o ssl/hssl.o ssl/wintls.o event/unpack.o event/evport.o event/poll.o event/rudp.o event/nlog.o event/hloop.o event/iocp.o event/epoll.o event/hevent.o event/kqueue.o event/nio.o event/noevent.o event/select.o event/overlapio.o event/kcp/hkcp.o event/kcp/ikcp.o util/sha1.o util/md5.o util/base64.o cpputil/hasync.o cpputil/hdir.o cpputil/hstring.o cpputil/RAII.o cpputil/ThreadLocalStorage.o cpputil/ifconfig.o cpputil/hpath.o cpputil/hurl.o cpputil/iniparser.o protocol/dns.o protocol/ftp.o protocol/smtp.o protocol/icmp.o http/wsdef.o http/multipart_parser.o http/http_parser.o http/httpdef.o http/websocket_parser.o http/HttpParser.o http/Http1Parser.o http/http_content.o http/WebSocketChannel.o http/WebSocketParser.o http/HttpMessage.o http/Http2Parser.o http/client/WebSocketClient.o http/client/HttpClient.o http/client/AsyncHttpClient.o http/server/HttpMiddleware.o http/server/HttpHandler.o http/server/FileCache.o http/server/HttpResponseWriter.o http/server/HttpServer.o http/server/http_page.o http/server/HttpService.o mqtt/mqtt_client.o mqtt/mqtt_protocol.omake[1]: [Makefile.in :285:clean] 错误 1 (已忽略) make[1]: 离开目录“/home/sv/pengeHome/libhv” rm -r 2>/dev/null examples/*.o examples/*/*.omake: [Makefile:78:clean] 错误 1 (已忽略) rm -r 2>/dev/null include/hvmake: [Makefile:79:clean] 错误 1 (已忽略) make clean done . mkdir -p 2>/dev/null binrm -r 2>/dev/null examples/httpd/*.omake -f Makefile.in TARGET=curl SRCDIRS=". base ssl event util cpputil evpp http http/client" SRCS="examples/curl.cpp" make[1]: 警告: jobserver 不可用: 正使用 -j1。添加 “+” 到父 make 的规则。 make: [Makefile:163:httpd] 错误 1 (已忽略) make -f Makefile.in TARGET=httpd SRCDIRS=". base ssl event util cpputil evpp http http/client http/server examples/httpd" make[1]: 进入目录“/home/sv/pengeHome/libhv” make[1]: 警告: jobserver 不可用: 正使用 -j1。添加 “+” 到父 make 的规则。 make[1]: 进入目录“/home/sv/pengeHome/libhv” cc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0cc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 OS = LinuxOS = Linux ARCH = x86_64 MAKE = make CC = cc CXX = g++ CFLAGS = -O2 -fPIC -std=c99 CXXFLAGS = -O2 -fPIC -std=c++11 CPPFLAGS = -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client ARCH = x86_64 MAKE = make CC = cc CXX = g++ CFLAGS = -O2 -fPIC -std=c99 CXXFLAGS = -O2 -fPIC -std=c++11 CPPFLAGS = -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd LDFLAGS = -Llib -L3rd/lib -L3rd/lib/x86_64-linux-gnu -lstdc++ -lpthread -lm -ldl -lrt TARGET = curl TARGET_TYPE = EXECUTABLE LDFLAGS = -Llib -L3rd/lib -L3rd/lib/x86_64-linux-gnu -lstdc++ -lpthread -lm -ldl -lrt TARGET = httpd TARGET_TYPE = EXECUTABLE TARGET_PLATFORM = x86_64-linux-gnu BUILD_TYPE = RELEASE SRCS=examples/curl.cpp base/htime.c base/rbtree.c base/hlog.c base/hsocket.c base/hversion.c base/herr.c base/hmain.c base/hbase.c ssl/nossl.c ssl/appletls.c ssl/openssl.c ssl/gnutls.c ssl/mbedtls.c ssl/hssl.c ssl/wintls.c event/unpack.c event/evport.c event/poll.c event/rudp.c event/nlog.c event/hloop.c event/iocp.c event/epoll.c event/hevent.c event/kqueue.c event/nio.c event/noevent.c event/select.c event/overlapio.c util/sha1.c util/md5.c util/base64.c cpputil/hasync.cpp cpputil/hdir.cpp cpputil/hstring.cpp cpputil/RAII.cpp cpputil/ThreadLocalStorage.cpp cpputil/ifconfig.cpp cpputil/hpath.cpp cpputil/hurl.cpp cpputil/iniparser.cpp http/wsdef.c http/multipart_parser.c http/http_parser.c http/httpdef.c http/websocket_parser.c http/HttpParser.cpp http/Http1Parser.cpp http/http_content.cpp http/WebSocketChannel.cpp http/WebSocketParser.cpp http/HttpMessage.cpp http/Http2Parser.cpp http/client/WebSocketClient.cpp http/client/HttpClient.cpp http/client/AsyncHttpClient.cpp OBJS=examples/curl.o base/htime.o base/rbtree.o base/hlog.o base/hsocket.o base/hversion.o base/herr.o base/hmain.o base/hbase.o ssl/nossl.o ssl/appletls.o ssl/openssl.o ssl/gnutls.o ssl/mbedtls.o ssl/hssl.o ssl/wintls.o event/unpack.o event/evport.o event/poll.o event/rudp.o event/nlog.o event/hloop.o event/iocp.o event/epoll.o event/hevent.o event/kqueue.o event/nio.o event/noevent.o event/select.o event/overlapio.o util/sha1.o util/md5.o util/base64.o cpputil/hasync.o cpputil/hdir.o cpputil/hstring.o cpputil/RAII.o cpputil/ThreadLocalStorage.o cpputil/ifconfig.o cpputil/hpath.o cpputil/hurl.o cpputil/iniparser.o http/wsdef.o http/multipart_parser.o http/http_parser.o http/httpdef.o http/websocket_parser.o http/HttpParser.o http/Http1Parser.o http/http_content.o http/WebSocketChannel.o http/WebSocketParser.o http/HttpMessage.o http/Http2Parser.o http/client/WebSocketClient.o http/client/HttpClient.o http/client/AsyncHttpClient.o TARGET_PLATFORM = x86_64-linux-gnumkdir -p 2>/dev/null bin lib BUILD_TYPE = RELEASE SRCS=base/htime.c base/rbtree.c base/hlog.c base/hsocket.c base/hversion.c base/herr.c base/hmain.c base/hbase.c ssl/nossl.c ssl/appletls.c ssl/openssl.c ssl/gnutls.c ssl/mbedtls.c ssl/hssl.c ssl/wintls.c event/unpack.c event/evport.c event/poll.c event/rudp.c event/nlog.c event/hloop.c event/iocp.c event/epoll.c event/hevent.c event/kqueue.c event/nio.c event/noevent.c event/select.c event/overlapio.c util/sha1.c util/md5.c util/base64.c cpputil/hasync.cpp cpputil/hdir.cpp cpputil/hstring.cpp cpputil/RAII.cpp cpputil/ThreadLocalStorage.cpp cpputil/ifconfig.cpp cpputil/hpath.cpp cpputil/hurl.cpp cpputil/iniparser.cpp http/wsdef.c http/multipart_parser.c http/http_parser.c http/httpdef.c http/websocket_parser.c http/HttpParser.cpp http/Http1Parser.cpp http/http_content.cpp http/WebSocketChannel.cpp http/WebSocketParser.cpp http/HttpMessage.cpp http/Http2Parser.cpp http/client/WebSocketClient.cpp http/client/HttpClient.cpp http/client/AsyncHttpClient.cpp http/server/HttpMiddleware.cpp http/server/HttpHandler.cpp http/server/FileCache.cpp http/server/HttpResponseWriter.cpp http/server/HttpServer.cpp http/server/http_page.cpp http/server/HttpService.cpp examples/httpd/router.cpp examples/httpd/handler.cpp examples/httpd/httpd.cpp OBJS=base/htime.o base/rbtree.o base/hlog.o base/hsocket.o base/hversion.o base/herr.o base/hmain.o base/hbase.o ssl/nossl.o ssl/appletls.o ssl/openssl.o ssl/gnutls.o ssl/mbedtls.o ssl/hssl.o ssl/wintls.o event/unpack.o event/evport.o event/poll.o event/rudp.o event/nlog.o event/hloop.o event/iocp.o event/epoll.o event/hevent.o event/kqueue.o event/nio.o event/noevent.o event/select.o event/overlapio.o util/sha1.o util/md5.o util/base64.o cpputil/hasync.o cpputil/hdir.o cpputil/hstring.o cpputil/RAII.o cpputil/ThreadLocalStorage.o cpputil/ifconfig.o cpputil/hpath.o cpputil/hurl.o cpputil/iniparser.o http/wsdef.o http/multipart_parser.o http/http_parser.o http/httpdef.o http/websocket_parser.o http/HttpParser.o http/Http1Parser.o http/http_content.o http/WebSocketChannel.o http/WebSocketParser.o http/HttpMessage.o http/Http2Parser.o http/client/WebSocketClient.o http/client/HttpClient.o http/client/AsyncHttpClient.o http/server/HttpMiddleware.o http/server/HttpHandler.o http/server/FileCache.o http/server/HttpResponseWriter.o http/server/HttpServer.o http/server/http_page.o http/server/HttpService.o examples/httpd/router.o examples/httpd/handler.o examples/httpd/httpd.o mkdir -p 2>/dev/null bin libg++ -O2 -fPIC -std=c++11 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -c -o examples/curl.o examples/curl.cpp cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/htime.o base/htime.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/rbtree.o base/rbtree.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/hlog.o base/hlog.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/hsocket.o base/hsocket.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/hversion.o base/hversion.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/herr.o base/herr.c cc -O2 -fPIC -std=c99 -DNDEBUG -Iinclude -I3rd -I3rd/include -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -Iexamples/httpd -c -o base/hmain.o base/hmain.c base/hmain.c: In function ‘getpid_from_pidfile’: base/hmain.c:392:5: warning: ignoring return value of ‘fscanf’, declared with attribute warn_unused_result [-Wunused-result] 392 | fscanf(fp, "%d" , &pid); | ^~~~~~~~~~~~~~~~~~~~~~ base/hmain.c: In function ‘main_ctx_init’: base/hmain.c:86:41: warning: ‘/logs’ directive output may be truncated writing 5 bytes into a region of size between 1 and 260 [-Wformat-truncation=] 86 | snprintf(logdir, sizeof(logdir), "%s/logs" , g_main_ctx.run_dir); | ^~~~~ In file included from /usr/include/stdio.h:867, from base/hplatform.h:262, from base/hmain.h:5, from base/hmain.c:1: /usr/include/x86_64-linux-gnu/bits/stdio2.h:67:10: note: ‘__builtin___snprintf_chk’ output between 6 and 265 bytes into a destination of size 260 67 | return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1, | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 | __bos (__s), __fmt, __va_arg_pack ()); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ base/hmain.c:88:65: warning: ‘/etc/’ directive output may be truncated writing 5 bytes into a region of size between 1 and 260 [-Wformat-truncation=] 88 | snprintf(g_main_ctx.confile, sizeof(g_main_ctx.confile), "%s/etc/%s.conf" , g_main_ctx.run_dir, g_main_ctx.program_name); | ^~~~~ In file included from /usr/include/stdio.h:867, from base/hplatform.h:262, from base/hmain.h:5, from base/hmain.c:1: /usr/include/x86_64-linux-gnu/bits/stdio2.h:67:10: note: ‘__builtin___snprintf_chk’ output between 11 and 529 bytes into a destination of size 260 67 | return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1, | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 | __bos (__s), __fmt, __va_arg_pack ()); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ base/hmain.c:89:65: warning: ‘/logs/’ directive output may be truncated writing 6 bytes into a region of size between 1 and 260 [-Wformat-truncation=] 89 | snprintf(g_mai ...
httpd与curl代码均可在examples 目录下找到,是完整的命令行程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv$ bin/httpd -h Usage: httpd [hvc:ts:dp:] Options: -h|--help Print this information -v|--version Print version -c|--confile <confile> Set configure file, default etc/{program}.conf -t|--test Test configure file and exit -s|--signal <signal> Send <signal> to process, <signal>=[start,stop,restart,status,reload] -d|--daemon Daemonize -p|--port <port> Set listen port (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv$ bin/curl -h Usage: curl [hVvX:H:r:d:F:n:] [METHOD] url [header_field:header_value] [body_key=body_value] Options: -h|--help Print this message. -V|--version Print version. -v|--verbose Show verbose infomation. -X|--method Set http method. -H|--header Add http header, -H "Content-Type: application/json" -r|--range Add http header Range:bytes=0-1023 -d|--data Set http body. -F|--form Set http form, -F "name=value" -F "file=@filename" -n|--count Send request count, used for test keep-alive --http2 Use http2 --grpc Use grpc over http2 --http-proxy Set http proxy --https-proxy Set https proxy --no-proxy Set no proxy --retry Set fail retry count --timeout Set timeout , unit(s) Examples: curl -v GET httpbin.org/get curl -v POST httpbin.org/post user=admin pswd=123456 curl -v PUT httpbin.org/put user=admin pswd=123456 curl -v localhost:8080 curl -v localhost:8080 -r 0-9 curl -v localhost:8080/ping curl -v localhost:8080/query?page_no=1\&page_size=10 curl -v localhost:8080/echo hello,world! curl -v localhost:8080/kv user=admin\&pswd=123456 curl -v localhost:8080/json user=admin pswd=123456 curl -v localhost:8080/form -F file=@filename curl -v localhost:8080/upload @filename curl version 1.0.0
libhv教程02–编译与安装
libhv提供了原生Makefile
(这里仅指适用于类unix系统的Makefile)和cmake
两种构建方式。
Makefile命令行
CLI
即Command Line Interface
命令行界面。鄙人强烈推荐使用的一种,特别是对于服务端开发人员,必备技能。
对于类Unix系统平台来说,推荐使用Makefile三部曲
1 2 3 ./configure make sudo make install
1 2 3 4 5 6 7 8 (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv$ sudo make install [sudo] sv 的密码: mkdir -p 2>/dev/null /usr/local/include/hvmkdir -p 2>/dev/null /usr/local/libcp -r 2>/dev/null include/hv/* /usr/local/include/hvcp -r 2>/dev/null lib/libhv.* /usr/local/libldconfig 2>/dev/null make install done .
Windows平台编译libhv请使用cmake先生成VS工程,各平台具体编译步骤见BUILD.md
cmake命令行
1 2 cmake -B build cmake --build build
编译产物
头文件
类unix系统默认安装在/usr/local/include/hv
目录下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 . ├── Buffer.h 缓存类 ├── Callback.h 回调定义 ├── Channel.h IO通道类 ├── Event.h 事件类 ├── EventLoop.h 事件循环类 ├── EventLoopThread.h 事件循环线程类 ├── EventLoopThreadPool.h 事件循环线程池类 ├── HttpMessage.h HTTP消息类 ├── HttpParser.h HTTP解析类 ├── HttpServer.h HTTP服务类 ├── HttpService.h HTTP业务类 ├── Status.h 状态类 ├── TcpClient.h TCP客户端类 ├── TcpServer.h TCP服务端类 ├── ThreadLocalStorage.h 线程本地存储类 ├── UdpClient.h UDP客户端类 ├── UdpServer.h UDP服务端类 ├── WebSocketChannel.h WebSocket通道类 ├── WebSocketClient.h WebSocket客户端类 ├── WebSocketParser.h WebSocket解析类 ├── WebSocketServer.h WebSocket服务端类 ├── base64.h BASE64编解码 ├── grpcdef.h grpc定义 ├── hatomic.h 原子操作 ├── hbase.h 基本函数 ├── hbuf.h 缓存buffer ├── hconfig.h configure生成配置 ├── hdef.h 常见宏定义 ├── hdir.h 目录(ls 实现) ├── hendian.h 大小端 ├── herr.h 错误码定义 ├── hexport.h DLL导出宏 ├── hfile.h 文件类 ├── hlog.h 日志 ├── hloop.h 事件循环 ├── hmain.h 命令行解析 ├── hmath.h 数学函数 ├── hmutex.h 互斥锁 ├── hobjectpool.h 对象池 ├── hplatform.h 平台相关宏 ├── hproc.h 进程 ├── hscope.h 作用域 ├── hsocket.h 套接字 ├── hssl.h SSL/TLS加密 ├── hstring.h 字符串操作 ├── hsysinfo.h 系统信息 ├── hthread.h 线程操作 ├── hthreadpool.h 线程池类 ├── htime.h 日期时间 ├── http2def.h http2定义 ├── http_client.h HTTP客户端 ├── http_content.h HTTP Content-Type ├── httpdef.h http定义 ├── hurl.h URL操作 ├── hv.h hv总头文件 ├── hversion.h 版本 ├── ifconfig.h ifconfig实现 ├── iniparser.h INI解析类 ├── json.hpp JSON解析 ├── md5.h MD5数字摘要 ├── nlog.h 网络日志 ├── nmap.h 主机发现 ├── requests.h 模拟python requests api ├── sha1.h SHA1安全散列算法 └── singleton.h 单例模式宏
库文件
静态库libhv.a
或libhv_static.a
windows
动态库hv.dll
linux
动态库libhv.so
macosx
动态库libhv.dylib
示例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ├── hmain_test 命令行解析测试程序 ├── hloop_test 事件循环测试程序 ├── htimer_test 定时器测试程序 ├── http_client_test HTTP客户端测试程序 ├── http_server_test HTTP服务端测试程序 ├── websocket_client_test WebSocket客户端测试程序 ├── websocket_server_test WebSocket服务端测试程序 ├── curl HTTP客户端 ├── httpd HTTP服务端 ├── nc 网络客户端 ├── nmap 主机发现 ├── tcp_chat_server TCP聊天服务 ├── tcp_echo_server TCP回显服务 ├── tcp_proxy_server TCP代理服务 └── udp_echo_server UDP回显服务
另外,仓库通过 Github Actions 确保master
分支在linux、windows、macosx
三个平台编译通过,大家再也不用担心编译不过了。
libhv教程03–链库与使用
在上一篇中,我们已经生成了头文件与库文件,接下来我们写个测试程序链库验证下。
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "hv/hv.h" int main () { char exe_filepath[MAX_PATH] = {0 }; char run_dir[MAX_PATH] = {0 }; const char * version = hv_compile_version (); get_executable_path (exe_filepath, sizeof (exe_filepath)); get_run_dir (run_dir, sizeof (run_dir)); printf ("exe_filepath=%s\n" , exe_filepath); printf ("run_dir=%s\n" , run_dir); LOGI ("libhv version: %s" , version); return 0 ; }
编译运行:
1 2 3 4 5 6 (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv/test$ gcc -std=c99 test.c -o test -lhv (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv/test$ ./test exe_filepath=/home/sv/pengeHome/libhv/test/test run_dir=/home/sv/pengeHome/libhv/test (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv/test$ cat libhv.20240524.log 2024-05-24 11:27:43.920 INFO libhv version: 1.24.5.24 [test.c:19:main]
libhv教程04–编写一个完整的命令行程序
首先,一个完整的命令行程序应该包含哪些功能?
命令行参数解析
配置文件解析
打印帮助信息和版本信息
信号处理
日志、pid文件
如果是服务端长时间运行后台程序,还需要看门狗(崩溃自动重启)
看看libhv是如何提供这些功能的,参考示例代码见examples/hmain_test.cpp
编译运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 $ c++ -std=c++11 examples/hmain_test.cpp -o bin/hmain_test -I/usr/local/include/hv -lhv $ bin/hmain_test -h Usage: hmain_test [hvc:ts:dp:] Options: -h|--help Print this information -v|--version Print version -c|--confile <confile> Set configure file, default etc/{program}.conf -t|--test Test configure file and exit -s|--signal <signal> Send <signal> to process, <signal>=[start,stop,restart,status,reload] -d|--daemon Daemonize -p|--port <port> Set listen port $ bin/hmain_test -v hmain_test version 1.21.1.31 $ bin/hmain_test -c etc/hmain_test.conf -t Test confile [etc/hmain_test.conf] OK! $ bin/hmain_test -d $ bin/hmain_test -s restart -d hmain_test stop/waiting hmain_test start/running $ bin/hmain_test -s status hmain_test start/running, pid=27766 $ cat logs/hmain_test.pid 27776 $ cat logs/hmain_test*.log 2021-02-06 12:18:53.509 INFO hmain_test version: 1.21.1.31 [hmain_test.cpp:94:parse_confile] 2021-02-06 12:18:53.509 DEBUG worker_processes=ncpu=2 [hmain_test.cpp:103:parse_confile] 2021-02-06 12:18:53.509 INFO parse_confile('/home/hw/github/libhv/etc/hmain_test.conf') OK [hmain_test.cpp:129:parse_confile] 2021-02-06 12:18:53.509 INFO create_pidfile('/home/hw/github/libhv/logs/hmain_test.pid') pid=27766 [hmain.cpp:317:create_pidfile] 2021-02-06 12:18:53.509 INFO workers[0] start/running, pid=27767 [hmain.cpp:611:master_workers_run] 2021-02-06 12:18:53.509 INFO workers[1] start/running, pid=27768 [hmain.cpp:611:master_workers_run] 2021-02-06 12:18:53.509 INFO master start/running, pid=27766 [hmain.cpp:614:master_workers_run] 2021-02-06 12:18:53.509 INFO worker_thread pid=27767 tid=27767 [hmain.cpp:539:worker_thread] 2021-02-06 12:18:53.510 INFO worker_thread pid=27768 tid=27768 [hmain.cpp:539:worker_thread] 2021-02-06 12:18:53.510 INFO worker_thread pid=27767 tid=27769 [hmain.cpp:539:worker_thread] 2021-02-06 12:18:53.510 INFO worker_thread pid=27768 tid=27770 [hmain.cpp:539:worker_thread] $ ps aux | grep hmain_test hw 27776 0.0 0.0 18000 2084 ? Ss 12:20 0:00 hmain_test: master process hw 27777 0.0 0.0 91732 240 ? Sl 12:20 0:00 hmain_test: worker process hw 27778 0.0 0.0 91732 240 ? Sl 12:20 0:00 hmain_test: worker process $ sudo kill -9 27778 $ ps aux | grep hmain_test hw 27776 0.0 0.0 18000 2084 ? Ss 12:20 0:00 hmain_test: master process hw 27777 0.0 0.0 91732 240 ? Sl 12:20 0:00 hmain_test: worker process hw 27796 0.0 0.0 91732 244 ? Sl 12:27 0:00 hmain_test: worker process
可以看到,hmain_test
提供了打印帮助信息、打印版本信息、测试配置文件、后台运行、创建pid文件、查看进程状态、开始|停止|重启进程、master-workers
多进程模式、崩溃自动重启等功能。
流程图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 st=>start: main e=>end: 结束 main_ctx_init=>operation: main_ctx_init main入口初始化 parse_opt=>operation: parse_opt 解析命令行参数 parse_confile=>operation: parse_confile 解析配置文件 hlog_set_xxx=>operation: hlog_set_xxx 日志设置 signal_init=>operation: signal_init 信号初始化 signal_handle=>operation: signal_handle 信号处理 daemon=>operation: daemon 后台运行 create_pidfile=>operation: create_pidfile 创建pid文件 master_workers_run=>operation: master_workers_run 扩展多进程|多线程模式 run=>operation: worker_fn 长时间运行... st->main_ctx_init->parse_opt->parse_confile->hlog_set_xxx->signal_init->signal_handle->daemon->create_pidfile->master_workers_run->run
libhv教程05–事件循环以及定时器的简单使用
事件循环简介
很多同学不理解事件循环的概念,所以这里有必要前置说明一下。 对于大多数长时间运行程序来说,都会有主循环的存在。
如窗口界面程序,就是等待键盘、鼠标等外设的输入,界面做出相应的变化。 我们以windows窗口消息机制
举例说明:
1 2 3 4 5 6 MSG msg; while (GetMessage(&msg, NULL , 0 , 0 )) { TranslateMessage(&msg); DispatchMessage(&msg); }
此循环所在的线程我们称之为GUI线程
(即窗口所在线程),MFC、WPF、Qt
等界面框架不过是将此过程给封装了。
理解了窗口消息循环的存在,其实就不难理解windows下老生常谈的问题:SendMessage
与PostMessage
的区别。 SendMessage
和PostMessage
都是windows提供的用来向窗口线程发送消息的API。 不同之处是SendMessage
是同步的,如果SendMessage
调用线程和窗口线程位于同一线程,则直接调用窗口过程处理此消息;如果不是同一线程,则会阻塞等待窗口线程处理完此消息再返回。 PostMessage
是异步的,将消息投递到窗口消息队列中就返回了,所以使用PostMessage
传递参数时需要注意不能使用栈上的局部变量。
IO多路复用简介
我们都知道IO可分为阻塞BIO
与非阻塞NIO
。 libhv的头文件hsocket.h 中提供了跨平台的设置阻塞与非阻塞的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifdef OS_WIN static inline int blocking (int sockfd) { unsigned long nb = 0 ; return ioctlsocket(sockfd, FIONBIO, &nb); } static inline int nonblocking (int sockfd) { unsigned long nb = 1 ; return ioctlsocket(sockfd, FIONBIO, &nb); } #else #define blocking(s) fcntl(s, F_SETFL, fcntl(s, F_GETFL) & ~O_NONBLOCK) #define nonblocking(s) fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK) #endif
对于BIO,伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 while (1 ) { readbytes = read(fd, buf, len); if (readbytes <= 0 ) { close(fd); break ; } ... writebytes = write(fd, buf, len); if (writebytes <= 0 ) { close(fd); break ; } }
因为读写都是阻塞的,所以一个IO线程只能处理一个fd,对于客户端尚可接受,对于服务端来说,每accept
一个连接,就创建一个IO线程去读写这个套接字,并发达到几千就需要创建几千个线程,线程上下文的切换开销都会把系统占满。
所以IO多路复用机制应运而生,如最早期的select
、后来的poll
,linux
的epoll
、windows
的iocp
、bsd
的kqueue
、solaris
的port
等,都属于IO多路复用机制。非阻塞NIO搭配IO多路复用机制就是高并发的钥匙
。
关于select、poll、epoll
的区别,可自行百度,这里就不展开说了。仅以select为例,写下伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 while (1 ) { int nselect = select(max_fd+1 , &readfds, &writefds, &exceptfds, timeout); if (nselect == 0 ) continue ; for (int fd = 0 ; fd <= max_fd; ++fd) { if (FD_ISSET(fd, &readfds)) { ... read(fd, buf, len); } if (FD_ISSET(fd, &writefds)) { ... write(fd, buf, len); } } }
通过IO多路复用机制,一个IO线程就可以同时监听多个fd了,以现代计算机的性能,一个IO线程即可处理几十万数量级别的IO读写。
libhv下的event模块 正是封装了多种平台的IO多路复用机制,提供了统一的事件接口
,是libhv的核心模块。
libhv中的事件包括IO事件
、timer定时器事件
、idle空闲事件
、自定义事件
(见hloop_post_event
接口,作用类似于windows窗口消息机制的PostMessage
)。
使用libhv创建一个事件循环
c版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "hv/hloop.h" static void on_timer (htimer_t * timer) { printf ("time=%lus\n" , (unsigned long )time(NULL )); } int main () { hloop_t * loop = hloop_new(0 ); htimer_add(loop, on_timer, 1000 , INFINITE); hloop_run(loop); hloop_free(&loop); return 0 ; }
事件循环测试代码examples/hloop_test.c 定时器测试代码见examples/htimer_test.c
c++版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "hv/EventLoop.h" using namespace hv;int main () { EventLoopPtr loop (new EventLoop) ; loop->setInterval (1000 , [](TimerID timerID){ printf ("time=%lus\n" , (unsigned long )time (NULL )); }); loop->run (); return 0 ; }
evpp模块 被设计成只包含头文件,不参与编译。 hloop.h中的c接口被封装成了c++的类,参考了muduo和evpp。 类设计如下:
1 2 3 4 5 6 7 8 9 10 ├── Buffer.h 缓存类 ├── Channel.h 通道类,封装了hio_t ├── Event.h 事件类,封装了hevent_t、htimer_t ├── EventLoop.h 事件循环类,封装了hloop_t ├── EventLoopThread.h 事件循环线程类,组合了EventLoop和thread ├── EventLoopThreadPool.h 事件循环线程池类,组合了EventLoop和ThreadPool ├── TcpClient.h TCP客户端类 ├── TcpServer.h TCP服务端类 ├── UdpClient.h UDP客户端类 └── UdpServer.h UDP服务端类
示例代码位于evpp目录下 - evpp/EventLoop_test.cpp - evpp/EventLoopThread_test.cpp - evpp/EventLoopThreadPool_test.cpp
多说两句: - EventLoop
中实现了muduo
有的两个接口,runInLoop
和queueInLoop
,我觉得命名不错,也直接采用了。runInLoop
对应SendMessage
,queueInLoop
对应PostMessage
,这么解释大家是不是更理解文章开头的铺垫了; - EventLoopThreadPool
的核心思想就是one loop per thread
;
libhv教程06–创建一个简单的TCP服务端
下文以TCP echo server
为例,使用libhv创建TCP服务端。
c版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include "hv/hloop.h" void on_close (hio_t * io) {} void on_recv (hio_t * io, void * buf, int readbytes) { hio_write(io, buf, readbytes); } void on_accept (hio_t * io) { hio_setcb_close(io, on_close); hio_setcb_read(io, on_recv); hio_read(io); } int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: cmd port\n" ); return -10 ; } int port = atoi(argv[1 ]); hloop_t * loop = hloop_new(0 ); hio_t * listenio = hloop_create_tcp_server(loop, "0.0.0.0" , port, on_accept); if (listenio == NULL ) { return -20 ; } hloop_run(loop); hloop_free(&loop); return 0 ; }
编译运行:
1 2 $ cc examples/tcp_echo_server.c -o bin/tcp_echo_server -I/usr/local/include/hv -lhv $ bin/tcp_echo_server 1234
类unix系统可使用nc作为客户端测试:
1 2 3 $ nc 127.0.0.1 1234 < hello > hello
windows端可使用telnet作为客户端测试:
更多TCP服务端示例参考: - TCP回显服务 - TCP聊天服务 - TCP代理服务
c++版本
代码示例参考evpp/TcpServer_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include "hv/TcpServer.h" using namespace hv;int main (int argc, char * argv[]) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); TcpServer srv; int listenfd = srv.createsocket (port); if (listenfd < 0 ) { return -20 ; } printf ("server listen on port %d, listenfd=%d ...\n" , port, listenfd); srv.onConnection = [](const SocketChannelPtr& channel) { std::string peeraddr = channel->peeraddr (); if (channel->isConnected ()) { printf ("%s connected! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); } else { printf ("%s disconnected! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); } }; srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("< %.*s\n" , (int )buf->size (), (char *)buf->data ()); channel->write (buf); }; srv.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("> %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; srv.setThreadNum (4 ); srv.start (); while (1 ) sleep (1 ); return 0 ; }
编译运行:
1 2 (base) sv@sv-NF5280M5:/home/sv/pengeHome/libhv$ c++ -std=c++11 evpp/TcpServer_test.cpp -o bin/TcpServer_test -I/usr/local/include/hv -lhv -lpthread $ bin/TcpServer_test 5678
TcpServer
更多实用接口
setThreadNum
:设置IO线程数
setMaxConnectionNum
:设置最大连接数
setUnpack
:设置拆包
withTLS
:SSL/TLS加密通信
libhv教程07–创建一个简单的TCP客户端
c版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "hv/hloop.h" #include "hv/htime.h" void on_timer (htimer_t * timer) { char str[DATETIME_FMT_BUFLEN] = {0 }; datetime_t dt = datetime_now(); datetime_fmt(&dt, str); printf ("> %s\n" , str); hio_t * io = (hio_t *)hevent_userdata(timer); hio_write(io, str, strlen (str)); } void on_close (hio_t * io) {} void on_recv (hio_t * io, void * buf, int readbytes) { printf ("< %.*s\n" , readbytes, (char *)buf); } void on_connect (hio_t * io) { hio_setcb_close(io, on_close); hio_setcb_read(io, on_recv); hio_read(io); htimer_t * timer = htimer_add(hevent_loop(io), on_timer, 1000 , INFINITE); hevent_set_userdata(timer, io); } int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: cmd port\n" ); return -10 ; } int port = atoi(argv[1 ]); hloop_t * loop = hloop_new(0 ); hio_t * listenio = hloop_create_tcp_client(loop, "127.0.0.1" , port, on_connect); if (listenio == NULL ) { return -20 ; } hloop_run(loop); hloop_free(&loop); return 0 ; }
完整TCP/UDP客户端程序可参考examples/nc.c
c++版本
示例代码见:evpp/TcpClient_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include "hv/TcpClient.h" #include "hv/htime.h" using namespace hv;int main (int argc, char * argv[]) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); TcpClient cli; int connfd = cli.createsocket (port); if (connfd < 0 ) { return -20 ; } printf ("client connect to port %d, connfd=%d ...\n" , port, connfd); cli.onConnection = [](const SocketChannelPtr& channel) { std::string peeraddr = channel->peeraddr (); if (channel->isConnected ()) { printf ("connected to %s! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); setInterval (3000 , [channel](TimerID timerID){ if (channel->isConnected ()) { char str[DATETIME_FMT_BUFLEN] = {0 }; datetime_t dt = datetime_now (); datetime_fmt (&dt, str); channel->write (str); } else { killTimer (timerID); } }); } else { printf ("disconnected to %s! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); } }; cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("< %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; cli.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("> %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; ReconnectInfo reconn; reconn.min_delay = 1000 ; reconn.max_delay = 10000 ; reconn.delay_policy = 2 ; cli.setReconnect (&reconn); cli.start (); while (1 ) sleep (1 ); return 0 ; }
TcpClient
更多实用接口
setConnectTimeout
:设置连接超时
setReconnect
:设置重连
setUnpack
:设置拆包
withTLS
:SSL/TLS加密通信
libhv教程08–创建一个简单的UDP服务端
下文以UDP echo server
为例,使用libhv创建UDP服务端。
c版本
代码示例参考examples/udp_echo_server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include "hv/hloop.h" #include "hv/hsocket.h" static void on_recvfrom (hio_t * io, void * buf, int readbytes) { printf ("on_recvfrom fd=%d readbytes=%d\n" , hio_fd(io), readbytes); char localaddrstr[SOCKADDR_STRLEN] = {0 }; char peeraddrstr[SOCKADDR_STRLEN] = {0 }; printf ("[%s] <=> [%s]\n" , SOCKADDR_STR(hio_localaddr(io), localaddrstr), SOCKADDR_STR(hio_peeraddr(io), peeraddrstr)); printf ("< %.*s" , readbytes, (char *)buf); printf ("> %.*s" , readbytes, (char *)buf); hio_write(io, buf, readbytes); } int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi(argv[1 ]); hloop_t * loop = hloop_new(0 ); hio_t * io = hloop_create_udp_server(loop, "0.0.0.0" , port); if (io == NULL ) { return -20 ; } hio_setcb_read(io, on_recvfrom); hio_read(io); hloop_run(loop); hloop_free(&loop); return 0 ; }
编译运行:
1 2 $ cc examples/udp_echo_server.c -o bin/udp_echo_server -I/usr/local/include/hv -lhv $ bin/udp_echo_server 1234
可使用nc作为客户端测试:
1 2 3 $ nc -u 127.0.0.1 1234 < hello > hello
c++版本
代码示例参考evpp/UdpServer_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include "hv/UdpServer.h" using namespace hv;int main (int argc, char * argv[]) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); UdpServer srv; int bindfd = srv.createsocket (port); if (bindfd < 0 ) { return -20 ; } printf ("server bind on port %d, bindfd=%d ...\n" , port, bindfd); srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("< %.*s\n" , (int )buf->size (), (char *)buf->data ()); channel->write (buf); }; srv.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("> %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; srv.start (); while (1 ) sleep (1 ); return 0 ; }
编译运行:
1 2 $ c++ -std=c++11 evpp/UdpServer_test.cpp -o bin/UdpServer_test -I/usr/local/include/hv -lhv $ bin/UdpServer_test 5678
libhv教程09–创建一个简单的UDP客户端
c版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include "hv/hloop.h" #include "hv/htime.h" void on_timer (htimer_t * timer) { char str[DATETIME_FMT_BUFLEN] = {0 }; datetime_t dt = datetime_now(); datetime_fmt(&dt, str); printf ("> %s\n" , str); hio_t * io = (hio_t *)hevent_userdata(timer); hio_write(io, str, strlen (str)); } void on_recvfrom (hio_t * io, void * buf, int readbytes) { printf ("< %.*s\n" , readbytes, (char *)buf); } int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: cmd port\n" ); return -10 ; } int port = atoi(argv[1 ]); hloop_t * loop = hloop_new(0 ); hio_t * io = hloop_create_udp_client(loop, "127.0.0.1" , port); if (io == NULL ) { return -20 ; } hio_setcb_read(io, on_recvfrom); hio_read(io); htimer_t * timer = htimer_add(hevent_loop(io), on_timer, 1000 , INFINITE); hevent_set_userdata(timer, io); hloop_run(loop); hloop_free(&loop); return 0 ; }
完整TCP/UDP客户端程序可参考examples/nc.c
c++版本
示例代码见:evpp/UdpClient_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "hv/UdpClient.h" #include "hv/htime.h" using namespace hv;int main (int argc, char * argv[]) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); UdpClient cli; int sockfd = cli.createsocket (port); if (sockfd < 0 ) { return -20 ; } printf ("client sendto port %d, sockfd=%d ...\n" , port, sockfd); cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("< %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; cli.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) { printf ("> %.*s\n" , (int )buf->size (), (char *)buf->data ()); }; cli.start (); cli.loop ()->setInterval (3000 , [&cli](TimerID timerID) { char str[DATETIME_FMT_BUFLEN] = {0 }; datetime_t dt = datetime_now (); datetime_fmt (&dt, str); cli.sendto (str); }); while (1 ) sleep (1 ); return 0 ; }
libhv教程10–创建一个简单的HTTP服务端
HTTP协议作为本世纪最通用的应用层协议,本文就不加以介绍了,不熟悉的自行阅读 awesome-http
简单的HTTP服务端示例
示例代码参考 examples/http_server_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include "hv/HttpServer.h" int main () { HttpService router; router.GET ("/ping" , [](HttpRequest* req, HttpResponse* resp) { return resp->String ("pong" ); }); router.GET ("/data" , [](HttpRequest* req, HttpResponse* resp) { static char data[] = "0123456789" ; return resp->Data (data, 10 ); }); router.GET ("/paths" , [&router](HttpRequest* req, HttpResponse* resp) { return resp->Json (router.Paths ()); }); router.POST ("/echo" , [](const HttpContextPtr& ctx) { return ctx->send (ctx->body (), ctx->type ()); }); http_server_t server; server.port = 8080 ; server.service = &router; http_server_run (&server); return 0 ; }
编译运行:
1 2 c++ -std=c++11 examples/http_server_test.cpp -o bin/http_server_test -lhv bin/http_server_test
测试使用curl
或浏览器输入以下url
:
1 2 3 4 curl -v http: curl -v http: curl -v http: curl -v http:
完整的HTTP服务端示例
完整的http服务端示例代码参考 examples/httpd测试步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 git clone https://github.com/ithewei/libhv.git cd libhv make httpd curl bin/httpd -h bin/httpd -d #bin/httpd -c etc/httpd.conf -s restart -d ps aux | grep httpd # http web service bin/curl -v localhost:8080 # http indexof service bin/curl -v localhost:8080/downloads/ # http api service bin/curl -v localhost:8080/ping bin/curl -v localhost:8080/echo -d "hello,world!" bin/curl -v localhost:8080/query?page_no=1\&page_size=10 bin/curl -v localhost:8080/kv -H "Content-Type:application/x-www-form-urlencoded" -d 'user=admin&pswd=123456' bin/curl -v localhost:8080/json -H "Content-Type:application/json" -d '{"user":"admin","pswd":"123456"}' bin/curl -v localhost:8080/form -F "user=admin pswd=123456" bin/curl -v localhost:8080/upload -F "file=@LICENSE" bin/curl -v localhost:8080/test -H "Content-Type:application/x-www-form-urlencoded" -d 'bool=1&int=123&float=3.14&string=hello' bin/curl -v localhost:8080/test -H "Content-Type:application/json" -d '{"bool":true,"int":123,"float":3.14,"string":"hello"}' bin/curl -v localhost:8080/test -F 'bool=1 int=123 float=3.14 string=hello' # RESTful API: /group/:group_name/user/:user_id bin/curl -v -X DELETE localhost:8080/group/test/user/123
压力测试
使用apache
的ab
、或者wrk
都可以用来做压力测试,一般服务器单线程QPS
可轻松达到3w
1 2 3 4 5 ab -c 100 -n 100000 http://127.0.0.1:8080/ wrk -c 100 -t 4 -d 10s http://127.0.0.1:8080/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 (base) sv@sv-NF5280M5:/home/sv$ ab -c 100 -n 100000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision : 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 10000 requests Completed 20000 requests Completed 30000 requests Completed 40000 requests Completed 50000 requests Completed 60000 requests Completed 70000 requests Completed 80000 requests Completed 90000 requests Completed 100000 requests Finished 100000 requests Server Software: libhv/1.3.2 Server Hostname: 127.0.0.1 Server Port: 8080 Document Path: / Document Length: 182 bytes Concurrency Level: 100 Time taken for tests: 14.869 seconds Complete requests: 100000 Failed requests: 0 Total transferred: 43800000 bytes HTML transferred: 18200000 bytes Requests per second: 6725.23 [ Time per request: 14.869 [ms] (mean) Time per request: 0.149 [ms] (mean, across all concurrent requests) Transfer rate: 2876.61 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 7 1.4 7 13 Processing: 1 8 1.6 8 15 Waiting: 0 6 1.6 5 13 Total: 9 15 1.2 15 23 Percentage of the requests served within a certain time (ms) 50% 15 66% 15 75% 16 80% 16 90% 17 95% 17 98% 18 99% 18 100% 23 (longest request)
libhv教程11–创建一个简单的HTTP客户端
简单的同步HTTP客户端示例
同步http客户端接口模拟实现了python
的requests
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "requests.h" int main () { auto resp = requests::get ("http://www.example.com" ); if (resp == NULL ) { printf ("request failed!\n" ); } else { printf ("%d %s\r\n" , resp->status_code, resp->status_message ()); printf ("%s\n" , resp->body.c_str ()); } resp = requests::post ("127.0.0.1:8080/echo" , "hello,world!" ); if (resp == NULL ) { printf ("request failed!\n" ); } else { printf ("%d %s\r\n" , resp->status_code, resp->status_message ()); printf ("%s\n" , resp->body.c_str ()); } return 0 ; }
简单的异步HTTP客户端示例
示例代码参考examples/http_client_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include "requests.h" #include "hthread.h" static void test_http_async_client (int * finished) { printf ("test_http_async_client request thread tid=%ld\n" , hv_gettid ()); HttpRequestPtr req (new HttpRequest) ; req->method = HTTP_POST; req->url = "127.0.0.1:8080/echo" ; req->headers["Connection" ] = "keep-alive" ; req->body = "this is an async request." ; req->timeout = 10 ; http_client_send_async (req, [finished](const HttpResponsePtr& resp) { printf ("test_http_async_client response thread tid=%ld\n" , hv_gettid ()); if (resp == NULL ) { printf ("request failed!\n" ); } else { printf ("%d %s\r\n" , resp->status_code, resp->status_message ()); printf ("%s\n" , resp->body.c_str ()); } *finished = 1 ; }); } int main () { int finished = 0 ; test_http_async_client (&finished); while (!finished) { hv_delay (100 ); } printf ("finished!\n" ); return 0 ; }
完整的HTTP客户端示例
完整的http客户端示例代码参考examples/curl.cpp ,模拟实现了curl
命令行程序。
测试步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 git clone https://github.com/ithewei/libhv.git cd libhv make httpd curl bin/httpd -h bin/httpd -d #bin/httpd -c etc/httpd.conf -s restart -d ps aux | grep httpd # http web service bin/curl -v localhost:8080 # http indexof service bin/curl -v localhost:8080/downloads/ # http api service bin/curl -v localhost:8080/ping bin/curl -v localhost:8080/echo -d "hello,world!" bin/curl -v localhost:8080/query?page_no=1\&page_size=10 bin/curl -v localhost:8080/kv -H "Content-Type:application/x-www-form-urlencoded" -d 'user=admin&pswd=123456' bin/curl -v localhost:8080/json -H "Content-Type:application/json" -d '{"user":"admin","pswd":"123456"}' bin/curl -v localhost:8080/form -F "user=admin pswd=123456" bin/curl -v localhost:8080/upload -F "file=@LICENSE" bin/curl -v localhost:8080/test -H "Content-Type:application/x-www-form-urlencoded" -d 'bool=1&int=123&float=3.14&string=hello' bin/curl -v localhost:8080/test -H "Content-Type:application/json" -d '{"bool":true,"int":123,"float":3.14,"string":"hello"}' bin/curl -v localhost:8080/test -F 'bool=1 int=123 float=3.14 string=hello' # RESTful API: /group/:group_name/user/:user_id bin/curl -v -X DELETE localhost:8080/group/test/user/123
libhv教程12–创建一个简单的WebSocket服务端
示例代码参考 examples/websocket_server_test.cpp]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include "WebSocketServer.h" #include "EventLoop.h" #include "htime.h" using namespace hv;int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); WebSocketServerCallbacks ws; ws.onopen = [](const WebSocketChannelPtr& channel, const std::string& url) { printf ("onopen: GET %s\n" , url.c_str ()); setInterval (1000 , [channel](TimerID id) { if (channel->isConnected ()) { char str[DATETIME_FMT_BUFLEN] = {0 }; datetime_t dt = datetime_now (); datetime_fmt (&dt, str); channel->send (str); } else { killTimer (id); } }); }; ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { printf ("onmessage: %s\n" , msg.c_str ()); }; ws.onclose = [](const WebSocketChannelPtr& channel) { printf ("onclose\n" ); }; websocket_server_t server; server.port = port; server.ws = &ws; websocket_server_run (&server); return 0 ; }
编译运行:
1 2 c++ -std=c++11 examples/websocket_server_test.cpp -o bin/websocket_server_test -I/usr/local/include/hv -lhv bin/websocket_server_test 8888
libhv教程13–创建一个简单的WebSocket客户端
WebSocket简介
WebSocket 产生背景
在 WebSocket 协议出现以前,创建一个和服务端进行双通道通信的 web 应用,需要依赖HTTP协议进行不停的轮询,这会导致一些问题:
服务端被迫维持来自每个客户端的大量不同的连接
大量的轮询请求会造成高开销,比如会带上多余的header,造成了无用的数据传输
所以,为了解决这些问题,WebSocket 协议应运而生。
WebSocket 的定义
WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
WebSocket 握手过程
客户端请求
1 2 3 4 5 6 7 GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: example.com Origin: http://example.com Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
服务器回应
1 2 3 4 5 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s= Sec-WebSocket-Location: ws://example.com/
WebSocket 通信协议
WebSocket 通信协议本文居于篇幅,就不展开说明,感兴趣的推荐阅读下面这篇博文:
先说说为什么会产生WebSocket协议 ?
在早期的web应用中,HTTP协议是主要的通信方式。然而,HTTP协议是一种请求-响应模式的协议,即客户端发送请求,服务器返回响应。这种模式在传统的web应用中工作得很好,但在需要实时通信的应用中就显得力不从心。因为HTTP协议不能让服务器主动向客户端发送数据,每次通信都需要客户端先发送请求,这就导致无法实时地推送数据到客户端。
为了解决这个问题,WebSocket协议被引入。WebSocket协议支持全双工通信,即服务器和客户端可以在任何时候向对方发送数据,而不需要等待对方的请求。这使得服务器可以实时地向客户端推送数据,满足了诸如在线游戏、实时消息推送等应用的需求。
示例代码
js示例代码
见 html/websocket_client.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function WebSocketTest (url ) { var ws = new WebSocket (url); ws.onopen = function ( ) { alert ("连接已建立" ); ws.send ("hello" ); }; ws.onmessage = function (ev ) { var received_msg = ev.data ; console .log ("received websocket message: " + received_msg); }; ws.onclose = function ( ) { alert ("连接已关闭" ); }; }
c++示例代码
libhv提供的WebSocketClient
类使用起来与JS的WebSocket
一样简单。
示例代码见 examples/websocket_client_test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include "WebSocketClient.h" using namespace hv;int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: %s url\n" , argv[0 ]); return -10 ; } const char * url = argv[1 ]; WebSocketClient ws; ws.onopen = [&ws]() { printf ("onopen\n" ); ws.send ("hello" ); }; ws.onclose = []() { printf ("onclose\n" ); }; ws.onmessage = [](const std::string& msg) { printf ("onmessage: %s\n" , msg.c_str ()); }; ReconnectInfo reconn; reconn.min_delay = 1000 ; reconn.max_delay = 10000 ; reconn.delay_policy = 2 ; ws.setReconnect (&reconn); ws.open (url); while (1 ) hv_delay (1000 ); return 0 ; }
编译运行:
1 2 c++ -std=c++11 examples/websocket_client_test.cpp -o bin/websocket_client_test -I/usr/local/include/hv -lhv bin/websocket_client_test ws://127.0.0.1:8888/
libhv教程14–200行实现一个纯C版jsonrpc框架
使用libhv可以在200行内实现一个完整的jsonrpc框架,这得益于libhv新提供的一个接口 hio_set_unpack
设置拆包规则,支持固定包长、分隔符、头部长度字段
三种常见的拆包方式,调用该接口设置拆包规则后,内部会根据拆包规则处理粘包与分包,保证回调上来的是完整的一包数据,大大节省了上层处理粘包与分包的成本,该接口具体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 typedef enum { UNPACK_BY_FIXED_LENGTH = 1 , UNPACK_BY_DELIMITER = 2 , UNPACK_BY_LENGTH_FIELD = 3 , } unpack_mode_e; #define DEFAULT_PACKAGE_MAX_LENGTH (1 << 21) #define PACKAGE_MAX_DELIMITER_BYTES 8 typedef enum { ENCODE_BY_VARINT = 1 , ENCODE_BY_LITTEL_ENDIAN = LITTLE_ENDIAN, ENCODE_BY_BIG_ENDIAN = BIG_ENDIAN, } unpack_coding_e; typedef struct unpack_setting_s { unpack_mode_e mode; unsigned int package_max_length; unsigned int fixed_length; unsigned char delimiter[PACKAGE_MAX_DELIMITER_BYTES]; unsigned short delimiter_bytes; unsigned short body_offset; unsigned short length_field_offset; unsigned short length_field_bytes; unpack_coding_e length_field_coding; #ifdef __cplusplus unpack_setting_s() { mode = UNPACK_BY_LENGTH_FIELD; package_max_length = DEFAULT_PACKAGE_MAX_LENGTH; fixed_length = 0 ; delimiter_bytes = 0 ; body_offset = 5 ; length_field_offset = 1 ; length_field_bytes = 4 ; length_field_coding = ENCODE_BY_BIG_ENDIAN; } #endif } unpack_setting_t ; HV_EXPORT void hio_set_unpack (hio_t * io, unpack_setting_t * setting) ;
以ftp
为例(分隔符方式)可以这样设置:
1 2 3 4 5 6 7 unpack_setting_t ftp_unpack_setting;memset (&ftp_unpack_setting, 0 , sizeof (unpack_setting_t ));ftp_unpack_setting.package_max_length = DEFAULT_PACKAGE_MAX_LENGTH; ftp_unpack_setting.mode = UNPACK_BY_DELIMITER; ftp_unpack_setting.delimiter[0 ] = '\r' ; ftp_unpack_setting.delimiter[1 ] = '\n' ; ftp_unpack_setting.delimiter_bytes = 2 ;
以mqtt
为例(头部长度字段方式)可以这样设置:
1 2 3 4 5 6 7 8 unpack_setting_t mqtt_unpack_setting = { .mode = UNPACK_BY_LENGTH_FIELD, .package_max_length = DEFAULT_PACKAGE_MAX_LENGTH, .body_offset = 2 , .length_field_offset = 1 , .length_field_bytes = 1 , .length_field_coding = ENCODE_BY_VARINT, };
具体实现代码在event/unpack.c 中,在内部readbuf
的基础上直接原地拆包与组包,基本做到零拷贝,比抛给上层处理更高效,感兴趣的可以研究一下。
示例代码
见examples/jsonrpc
关键函数
hloop_new:创建事件循环
hloop_run: 运行事件循环
hloop_create_tcp_server:创建TCP服务
hio_set_unpack:设置拆包规则
hio_read:开始接收数据
hio_write: 发送数据
jsonrpc_unpack:拆包
jsonrpc_pack:组包
cJSON_xxx:json编解码
测试步骤
1 2 3 4 5 6 7 8 git clone https://github.com/ithewei/libhv cd libhv make jsonrpc # mkdir build && cd build && cmake .. && cmake --build . --target jsonrpc bin/jsonrpc_server 1234 bin/jsonrpc_client 127.0.0.1 1234 add 1 2 bin/jsonrpc_client 127.0.0.1 1234 div 1 0 bin/jsonrpc_client 127.0.0.1 1234 xyz 1 2
结果如下: 服务端:
1 2 3 4 5 6 $ bin/jsonrpc_server 1234 listenfd=4 on_accept connfd=7 > {"id":1,"method":"add","params":[1,2]} < {"id":1,"result":3} on_close fd=7 error=0
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ bin/jsonrpc_client 127.0.0.1 1234 add 1 2 on_connect fd=4 > {"id":1,"method":"add","params":[1,2]} < {"id":1,"result":3} on_close fd=4 error=0 $ bin/jsonrpc_client 127.0.0.1 1234 div 1 0 on_connect fd=4 > {"id":1,"method":"div","params":[1,0]} < {"id":1,"error":{"code":400,"message":"Bad Request"}} on_close fd=4 error=0 $ bin/jsonrpc_client 127.0.0.1 1234 xyz 1 2 on_connect fd=4 > {"id":1,"method":"xyz","params":[1,2]} < {"id":1,"error":{"code":404,"message":"Not Found"}} on_close fd=4 error=0
libhv教程15–200行实现一个C++版protorpc框架
在上篇教程中,我们200行实现了一个纯C版的jsonrpc框架 ,使用的event模块+cJSON
实现,本篇中我们将介绍200行实现一个C++版的protorpc框架,使用evpp模块+protobuf
实现。
evpp模块是event模块的c++封装,具体介绍见evpp/README.md
protobuf是google出品的序列化/反序列化结构化数据存储格式,具体介绍可参考我的另一篇博客protobuf ,也可参考protobuf官方文档
protobuf安装
1 2 3 4 5 6 7 8 9 10 git clone https://github.com/protocolbuffers/protobuf cd protobuf ./autogen.sh ./configure make sudo make install sudo ldconfig which protoc protoc -h
protorpc代码
见exmaples/protorpc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include "TcpServer.h" using namespace hv;#include "protorpc.h" #include "router.h" #include "handler/handler.h" #include "handler/calc.h" #include "handler/login.h" protorpc_router router[] = { {"add" , calc_add}, {"sub" , calc_sub}, {"mul" , calc_mul}, {"div" , calc_div}, {"login" , login}, }; #define PROTORPC_ROUTER_NUM (sizeof(router)/sizeof(router[0])) class ProtoRpcServer : public TcpServer {public : ProtoRpcServer () : TcpServer () { onConnection = [](const SocketChannelPtr& channel) { std::string peeraddr = channel->peeraddr (); if (channel->isConnected ()) { printf ("%s connected! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); } else { printf ("%s disconnected! connfd=%d\n" , peeraddr.c_str (), channel->fd ()); } }; onMessage = handleMessage; unpack_setting_t protorpc_unpack_setting; memset (&protorpc_unpack_setting, 0 , sizeof (unpack_setting_t )); protorpc_unpack_setting.mode = UNPACK_BY_LENGTH_FIELD; protorpc_unpack_setting.package_max_length = DEFAULT_PACKAGE_MAX_LENGTH; protorpc_unpack_setting.body_offset = PROTORPC_HEAD_LENGTH; protorpc_unpack_setting.length_field_offset = 1 ; protorpc_unpack_setting.length_field_bytes = 4 ; protorpc_unpack_setting.length_field_coding = ENCODE_BY_BIG_ENDIAN; setUnpack (&protorpc_unpack_setting); } int listen (int port) { return createsocket (port); } private : static void handleMessage (const SocketChannelPtr& channel, Buffer* buf) { protorpc_message msg; memset (&msg, 0 , sizeof (msg)); int packlen = protorpc_unpack (&msg, buf->data (), buf->size ()); if (packlen < 0 ) { printf ("protorpc_unpack failed!\n" ); return ; } assert (packlen == buf->size ()); protorpc::Request req; protorpc::Response res; if (req.ParseFromArray (msg.body, msg.head.length)) { printf ("> %s\n" , req.DebugString ().c_str ()); res.set_id (req.id ()); const char * method = req.method ().c_str (); bool found = false ; for (int i = 0 ; i < PROTORPC_ROUTER_NUM; ++i) { if (strcmp (method, router[i].method) == 0 ) { found = true ; router[i].handler (req, &res); break ; } } if (!found) { not_found (req, &res); } } else { bad_request (req, &res); } memset (&msg, 0 , sizeof (msg)); msg.head.length = res.ByteSizeLong (); packlen = protorpc_package_length (&msg.head); unsigned char * writebuf = NULL ; HV_ALLOC (writebuf, packlen); packlen = protorpc_pack (&msg, writebuf, packlen); if (packlen > 0 ) { printf ("< %s\n" , res.DebugString ().c_str ()); res.SerializeToArray (writebuf + PROTORPC_HEAD_LENGTH, msg.head.length); channel->write (writebuf, packlen); } HV_FREE (writebuf); } }; int main (int argc, char ** argv) { if (argc < 2 ) { printf ("Usage: %s port\n" , argv[0 ]); return -10 ; } int port = atoi (argv[1 ]); ProtoRpcServer srv; int listenfd = srv.listen (port); if (listenfd < 0 ) { return -20 ; } printf ("protorpc_server listen on port %d, listenfd=%d ...\n" , port, listenfd); srv.setThreadNum (4 ); srv.start (); while (1 ) hv_sleep (1 ); return 0 ; }
流程很清晰,启动一个TcpServer
,监听指定端口,通过setUnpack
接口设置拆包规则,onMessage
回调上来就是完整的一包数据,回调里调用protorpc_unpack
拆包、Request::ParseFromArray
反序列化得到结构化的请求,通过请求里的method
字段查找注册好的router路由表
,调用对应的handler
处理请求、填充响应,然后Response::SerializeToArray
序列化响应+protorpc_pack
加上头部封包后,最后调用Channel::write
发送出去。
base.proto 定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 syntax = "proto3" ; package protorpc; message Error { int32 code = 1 ; string message = 2 ; } message Request { uint64 id = 1 ; string method = 2 ; repeated bytes params = 3 ; } message Response { uint64 id = 1 ; optional bytes result = 2 ; optional Error error = 3 ; }
执行该目录下的protoc.sh
会调用protoc
根据proto
定义文件自动生成对应代码。
测试步骤
1 2 3 4 5 6 7 git clone https://github.com/ithewei/libhv cd libhvmake protorpc bin/protorpc_server 1234 bin/protorpc_client 127.0.0.1 1234 add 1 2 bin/protorpc_client 127.0.0.1 1234 div 1 0 bin/protorpc_client 127.0.0.1 1234 xyz 1 2
结果如下: 服务端:
1 2 $ bin/protorpc_server 1234 protorpc_server listen on port 1234, listenfd=3 ...
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 $ bin/protorpc_client 127.0.0.1 1234 add 1 2 connected to 127.0.0.1:1234! connfd=4 id : 1method: "login" params: "\n\005admin\022\006123456" login success! user_id: 123456 token: "admin:123456" id : 2method: "add" params: "\010\001" params: "\010\002" calc success! 1 add 2 = 3 disconnected to 127.0.0.1:1234! connfd=4 $ bin/protorpc_client 127.0.0.1 1234 div 1 0 connected to 127.0.0.1:1234! connfd=4 id : 1method: "login" params: "\n\005admin\022\006123456" login success! user_id: 123456 token: "admin:123456" id : 2method: "div" params: "\010\001" params: "" RPC error: code: 400 message: "Bad Request" calc failed! disconnected to 127.0.0.1:1234! connfd=4 $ bin/protorpc_client 127.0.0.1 1234 xyz 1 2 connected to 127.0.0.1:1234! connfd=4 id : 1method: "login" params: "\n\005admin\022\006123456" login success! user_id: 123456 token: "admin:123456" id : 2method: "xyz" params: "\010\001" params: "\010\002" RPC error: code: 404 message: "Not Found" calc failed! disconnected to 127.0.0.1:1234! connfd=4