本文转载自:史上最详细的网络编程实战教程

仓库: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加密通信,支持心跳、转发、拆包、多线程安全writeclose等特性,实现了HTTPWebSocket等协议;
  • 当然这几个库的性能是接近的,都将非阻塞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.o
make[1]: [Makefile.in:285:clean] 错误 1 (已忽略)
#-rm -r 2>/dev/null lib
#-rm -r 2>/dev/null bin
make[1]: 离开目录“/home/sv/pengeHome/libhv”
rm -r 2>/dev/null examples/*.o examples/*/*.o
make: [Makefile:78:clean] 错误 1 (已忽略)
rm -r 2>/dev/null include/hv
make: [Makefile:79:clean] 错误 1 (已忽略)
make clean done.
mkdir -p 2>/dev/null bin
rm -r 2>/dev/null examples/httpd/*.o
make -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 lib
g++ -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命令行

CLICommand 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/hv
mkdir -p 2>/dev/null /usr/local/lib
cp -r 2>/dev/null include/hv/* /usr/local/include/hv
cp -r 2>/dev/null lib/libhv.* /usr/local/lib
ldconfig 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.alibhv_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};

// 获取hv编译版本
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
// windows窗口消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

此循环所在的线程我们称之为GUI线程(即窗口所在线程),MFC、WPF、Qt等界面框架不过是将此过程给封装了。

理解了窗口消息循环的存在,其实就不难理解windows下老生常谈的问题:SendMessagePostMessage的区别。 SendMessagePostMessage都是windows提供的用来向窗口线程发送消息的API。 不同之处是SendMessage是同步的,如果SendMessage调用线程和窗口线程位于同一线程,则直接调用窗口过程处理此消息;如果不是同一线程,则会阻塞等待窗口线程处理完此消息再返回。 PostMessage是异步的,将消息投递到窗口消息队列中就返回了,所以使用PostMessage传递参数时需要注意不能使用栈上的局部变量。

IO多路复用简介

1
GUI线程具有主循环,网络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、后来的polllinuxepollwindowsiocpbsdkqueuesolarisport等,都属于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有的两个接口,runInLoopqueueInLoop,我觉得命名不错,也直接采用了。runInLoop对应SendMessagequeueInLoop对应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) {
// 设置close回调
hio_setcb_close(io, on_close);
// 设置read回调
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);
// 创建TCP服务
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作为客户端测试:

1
$ telent 127.0.0.1 1234

更多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) {
// echo
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);
// 获取userdata
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) {
// 设置close回调
hio_setcb_close(io, on_close);
// 设置read回调
hio_setcb_read(io, on_recv);
// 开始读
hio_read(io);

// 添加一个定时器
htimer_t* timer = htimer_add(hevent_loop(io), on_timer, 1000, INFINITE);
// 设置userdata
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);
// 创建TCP客户端
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());
// send(time) every 3s
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());
};
// reconnect: 1,2,4,8,10,10,10...
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);
// 创建UDP服务
hio_t* io = hloop_create_udp_server(loop, "0.0.0.0", port);
if (io == NULL) {
return -20;
}
// 设置read回调
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) {
// echo
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);
// 获取userdata
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);
// 创建UDP客户端
hio_t* io = hloop_create_udp_client(loop, "127.0.0.1", port);
if (io == NULL) {
return -20;
}
// 设置read回调
hio_setcb_read(io, on_recvfrom);
// 开始读
hio_read(io);
// 添加一个定时器
htimer_t* timer = htimer_add(hevent_loop(io), on_timer, 1000, INFINITE);
// 设置userdata
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();

// sendto(time) every 3s
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://127.0.0.1:8080/ping
curl -v http://127.0.0.1:8080/data
curl -v http://127.0.0.1:8080/paths
curl -v http://127.0.0.1:8080/echo -d "hello,world"

完整的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

压力测试

使用apacheab、或者wrk都可以用来做压力测试,一般服务器单线程QPS可轻松达到3w

1
2
3
4
5
# sudo apt install apache2-utils
ab -c 100 -n 100000 http://127.0.0.1:8080/

# sudo apt install wrk
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 [#/sec] (mean)
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客户端接口模拟实现了pythonrequests

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" // import hv_gettid

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);

// demo wait async ResponseCallback
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());
// send(time) every 1s
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());
};

// reconnect: 1,2,4,8,10,10,10...
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, // 根据分隔符拆包,如常见的“\r\n”
UNPACK_BY_LENGTH_FIELD = 3, // 根据头部长度字段拆包
} unpack_mode_e;

#define DEFAULT_PACKAGE_MAX_LENGTH (1 << 21) // 2M

// UNPACK_BY_DELIMITER
#define PACKAGE_MAX_DELIMITER_BYTES 8

// UNPACK_BY_LENGTH_FIELD
typedef enum {
ENCODE_BY_VARINT = 1, // varint编码
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; // 最大包长度限制
// UNPACK_BY_FIXED_LENGTH
unsigned int fixed_length; // 固定包长度
// UNPACK_BY_DELIMITER
unsigned char delimiter[PACKAGE_MAX_DELIMITER_BYTES]; // 分隔符
unsigned short delimiter_bytes; // 分隔符长度
// UNPACK_BY_LENGTH_FIELD
unsigned short body_offset; // body偏移量(即头部长度)real_body_offset = body_offset + varint_bytes - length_field_bytes
unsigned short length_field_offset; // 头部长度字段偏移量
unsigned short length_field_bytes; // 头部长度字段所占字节数
unpack_coding_e length_field_coding; // 头部长度字段编码方式,支持varint、大小端三种编码方式,通常使用大端字节序(即网络字节序)
#ifdef __cplusplus
unpack_setting_s() {
// Recommended setting:
// head = flags:1byte + length:4bytes = 5bytes
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;
// init protorpc_unpack_setting
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) {
// unpack -> Request::ParseFromArray -> router -> Response::SerializeToArray -> pack -> Channel::write
// protorpc_unpack
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());

// Request::ParseFromArray
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());
// router
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);
}

// Response::SerializeToArray + protorpc_pack
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 libhv
make 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: 1
method: "login"
params: "\n\005admin\022\006123456"

login success!
user_id: 123456
token: "admin:123456"

id: 2
method: "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: 1
method: "login"
params: "\n\005admin\022\006123456"

login success!
user_id: 123456
token: "admin:123456"

id: 2
method: "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: 1
method: "login"
params: "\n\005admin\022\006123456"

login success!
user_id: 123456
token: "admin:123456"

id: 2
method: "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