资讯动态

libevent 从零搭建通信 Demo:动态库路径问题排查与实战

发布时间:2026/10/20 15:47:55 来源:尧图企业网站定制
1. 从零搭建 libevent 通信 Demo一个踩坑无数的实战记录libevent 这个名字做 C/C 网络编程的人应该都不陌生。它是一个轻量级、高性能的事件驱动库核心作用就是帮我们管理大量并发连接把 epoll、kqueue、select 这些不同平台的 I/O 多路复用机制统一封装成一套接口。说白了你写一套代码在 Linux 上跑 epoll在别的 Unix 系统上跑 kqueue不用改业务逻辑。这次我拿它做了一个最基础的服务端和客户端通信 Demo从下载安装到编译运行中间踩了好几个坑尤其是编译之后运行时报 “error while loading shared libraries: libevent-2.1.so.7: cannot open shared object file” 这个问题卡了我不少时间。这篇文章就把整个过程完整记录下来包括安装步骤、代码实现、编译命令、以及那个 so 找不到的问题怎么一步步排查解决的。如果你也是刚接触 libevent或者正在被动态库路径问题折磨这篇内容应该能帮你省下不少时间。先说一下这个 Demo 能做什么服务端监听一个端口客户端连上来发消息服务端收到后回显给客户端。功能很简单但麻雀虽小五脏俱全涉及了 libevent 的event_base、evconnlistener、bufferevent这几个核心组件。适合有一定 C 语言基础、想入门网络编程或者刚接触 libevent 的读者。下面我按实际操作的顺序来写尽量把每一步为什么这么做也讲清楚。2. 环境准备与 libevent 安装为什么源码编译比包管理更靠谱2.1 为什么我选择源码编译而不是 apt 安装在 Linux 上装 libevent最省事的方式当然是包管理器比如apt-get install libevent-dev或者yum install libevent-devel。但我这次选择了源码编译原因有三个。第一包管理器里的版本往往偏旧比如某些发行版自带的是 2.0.x而 2.1.x 在 bufferevent 的稳定性和 API 上有一些改进。第二源码编译可以自定义安装路径方便我把依赖库统一放在一个目录下管理不至于和系统库混在一起。第三也是最重要的一点我想搞清楚编译出来的.so文件到底放在哪里这样后面排查动态库找不到的问题时心里有底。当然如果你只是想快速跑通 Demo用包管理器装也完全没问题后面动态库路径那部分可以跳过。但如果你想深入理解 libevent 的安装结构建议跟着我走一遍源码编译。2.2 下载与解压选对版本很关键libevent 的官方发布地址在 GitHub 上直接下载 release 包就行。我选的是libevent-2.1.12-stable这个版本稳定分支bug 少社区用得最多。下载命令如下wget https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz下载完成后解压tar -zxvf libevent-2.1.12-stable.tar.gz cd libevent-2.1.12-stable提示不要用 master 分支的代码那个是开发版API 可能随时变而且没有经过充分测试。认准-stable后缀的 release 包。2.3 配置、编译、安装三个命令背后的逻辑进入解压后的目录依次执行./configure --prefix/usr/local/libevent make -j$(nproc) sudo make install这里重点说下--prefix参数。如果不指定默认会装到/usr/local下面头文件在/usr/local/include库文件在/usr/local/lib。我习惯单独指定一个目录比如/usr/local/libevent这样以后想卸载或者换版本直接删掉这个目录就行不会污染系统目录。make -j$(nproc)里的-j是并行编译$(nproc)会自动获取 CPU 核心数能显著加快编译速度。libevent 代码量不算大但并行编译还是能省几秒。make install需要 sudo 权限因为要往/usr/local写文件。安装完成后你可以去/usr/local/libevent下面看看应该能看到include、lib、bin这几个目录。lib目录里就是编译出来的动态库和静态库比如libevent-2.1.so.7.0.1、libevent_core-2.1.so.7.0.1等等。2.4 验证安装别急着写代码先确认库能用安装完之后先别急着写代码。我建议先验证一下 libevent 是否真的装好了。最简单的方法是写一个几行的小程序调用event_get_version()打印版本号#include event2/event.h #include stdio.h int main() { printf(libevent version: %s\n, event_get_version()); return 0; }编译gcc test_version.c -o test_version -I/usr/local/libevent/include -L/usr/local/libevent/lib -levent如果编译通过并且运行输出了版本号说明安装没问题。但如果运行时报错error while loading shared libraries: libevent-2.1.so.7: cannot open shared object file那就说明动态库路径没配好这个问题我们后面专门讲。3. 服务端与客户端代码实现核心组件拆解3.1 服务端设计思路evconnlistener bufferevent 的组合拳libevent 提供了一套高层封装叫bufferevent它把 socket 的读写缓冲、事件回调都包好了用起来比裸的event舒服很多。服务端的核心流程是这样的创建event_base这是 libevent 的事件循环核心所有事件都注册在它上面。用evconnlistener_new_bind创建一个监听器绑定端口设置新连接到来时的回调。在回调里用bufferevent_socket_new为每个新连接创建一个 bufferevent并设置读回调。在读回调里用bufferevent_read读取数据然后用bufferevent_write把数据回写。最后调用event_base_dispatch进入事件循环。这个流程比传统的acceptrecvsend要简洁得多因为 libevent 帮你处理了非阻塞 I/O 和事件分发。3.2 服务端完整代码与关键注释#include event2/listener.h #include event2/bufferevent.h #include event2/buffer.h #include arpa/inet.h #include string.h #include stdio.h #include stdlib.h #define PORT 9999 #define MAX_LINE 1024 // 读回调有数据到达时触发 static void read_cb(struct bufferevent *bev, void *ctx) { char line[MAX_LINE]; int n; struct evbuffer *input bufferevent_get_input(bev); struct evbuffer *output bufferevent_get_output(bev); // 从输入缓冲中按行读取 while ((n evbuffer_readln(input, NULL, EVBUFFER_EOL_LF)) ! NULL) { printf(Server received: %s\n, n); // 回显给客户端 evbuffer_add(output, n, strlen(n)); evbuffer_add(output, \n, 1); free(n); } } // 事件回调连接出错或断开时触发 static void event_cb(struct bufferevent *bev, short events, void *ctx) { if (events BEV_EVENT_ERROR) { perror(Error from bufferevent); } if (events (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { printf(Client disconnected\n); bufferevent_free(bev); } } // 新连接回调 static void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) { struct event_base *base evconnlistener_get_base(listener); struct bufferevent *bev bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL); bufferevent_enable(bev, EV_READ | EV_WRITE); printf(New client connected\n); } // 监听器错误回调 static void accept_error_cb(struct evconnlistener *listener, void *ctx) { struct event_base *base evconnlistener_get_base(listener); int err EVUTIL_SOCKET_ERROR(); fprintf(stderr, Got an error %d (%s) on the listener.\n, err, evutil_socket_error_to_string(err)); event_base_loopexit(base, NULL); } int main() { struct event_base *base; struct evconnlistener *listener; struct sockaddr_in sin; base event_base_new(); if (!base) { fprintf(stderr, Could not initialize libevent!\n); return 1; } memset(sin, 0, sizeof(sin)); sin.sin_family AF_INET; sin.sin_port htons(PORT); sin.sin_addr.s_addr htonl(INADDR_ANY); listener evconnlistener_new_bind(base, accept_conn_cb, NULL, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr *)sin, sizeof(sin)); if (!listener) { fprintf(stderr, Could not create a listener!\n); return 1; } evconnlistener_set_error_cb(listener, accept_error_cb); printf(Server listening on port %d...\n, PORT); event_base_dispatch(base); evconnlistener_free(listener); event_base_free(base); return 0; }这段代码里几个关键点值得说明。LEV_OPT_REUSEABLE对应SO_REUSEADDR避免服务端重启时端口被占用。BEV_OPT_CLOSE_ON_FREE表示 bufferevent 释放时自动关闭底层 socket省得手动 close。evbuffer_readln是按行读取适合文本协议如果是二进制协议就得用evbuffer_remove。3.3 客户端代码连接、发送、接收客户端比服务端简单一些核心是创建一个 bufferevent然后调用bufferevent_socket_connect去连接服务端。#include event2/event.h #include event2/bufferevent.h #include event2/buffer.h #include string.h #include stdio.h #include stdlib.h #define SERVER_IP 127.0.0.1 #define PORT 9999 #define MAX_LINE 1024 static void read_cb(struct bufferevent *bev, void *ctx) { char line[MAX_LINE]; int n; struct evbuffer *input bufferevent_get_input(bev); while ((n evbuffer_readln(input, NULL, EVBUFFER_EOL_LF)) ! NULL) { printf(Client received: %s\n, n); free(n); } } static void event_cb(struct bufferevent *bev, short events, void *ctx) { if (events BEV_EVENT_CONNECTED) { printf(Connected to server\n); // 连接成功后发送一条消息 const char *msg Hello, libevent!\n; bufferevent_write(bev, msg, strlen(msg)); } else if (events BEV_EVENT_ERROR) { perror(Error); } else if (events BEV_EVENT_EOF) { printf(Server closed connection\n); bufferevent_free(bev); event_base_loopexit((struct event_base *)ctx, NULL); } } int main() { struct event_base *base event_base_new(); if (!base) { fprintf(stderr, Could not initialize libevent!\n); return 1; } struct bufferevent *bev bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, read_cb, NULL, event_cb, base); bufferevent_enable(bev, EV_READ | EV_WRITE); struct sockaddr_in sin; memset(sin, 0, sizeof(sin)); sin.sin_family AF_INET; sin.sin_port htons(PORT); sin.sin_addr.s_addr inet_addr(SERVER_IP); if (bufferevent_socket_connect(bev, (struct sockaddr *)sin, sizeof(sin)) 0) { fprintf(stderr, Error starting connection\n); bufferevent_free(bev); return 1; } event_base_dispatch(base); bufferevent_free(bev); event_base_free(base); return 0; }客户端的event_cb里BEV_EVENT_CONNECTED事件表示连接建立成功这时候才能发数据。如果连接失败会触发BEV_EVENT_ERROR。收到服务端回显后read_cb打印出来。4. 编译与运行动态库路径问题的完整排查过程4.1 编译命令-I 和 -L 参数不能少编译服务端和客户端时因为 libevent 装在自定义目录/usr/local/libevent所以必须显式指定头文件和库文件路径gcc server.c -o server -I/usr/local/libevent/include -L/usr/local/libevent/lib -levent gcc client.c -o client -I/usr/local/libevent/include -L/usr/local/libevent/lib -levent-I告诉编译器去哪里找event2/event.h这些头文件-L告诉链接器去哪里找libevent.so。-levent表示链接 libevent 库。如果编译时报cannot find -levent说明-L路径不对或者 libevent 没装好。如果报event2/event.h: No such file or directory说明-I路径不对。4.2 运行时报错libevent-2.1.so.7 找不到编译通过后运行./server结果报错./server: error while loading shared libraries: libevent-2.1.so.7: cannot open shared object file: No such file or directory这个问题的本质是编译时链接器知道库在哪里但运行时动态加载器不知道。Linux 的动态库搜索路径由/etc/ld.so.conf和LD_LIBRARY_PATH环境变量决定。我们装的 libevent 在/usr/local/libevent/lib这个路径不在默认搜索范围内所以运行时报错。4.3 解决方案一设置 LD_LIBRARY_PATH临时生效最简单的方法是设置环境变量export LD_LIBRARY_PATH/usr/local/libevent/lib:$LD_LIBRARY_PATH然后再运行./server应该就能正常启动了。但这个方法是临时的只对当前终端会话有效关掉终端就失效了。适合快速测试。4.4 解决方案二配置 ld.so.conf永久生效如果想永久生效可以把库路径加到/etc/ld.so.conf.d/下面echo /usr/local/libevent/lib | sudo tee /etc/ld.so.conf.d/libevent.conf sudo ldconfigldconfig会刷新动态库缓存让系统重新扫描所有配置的库路径。执行完之后再运行./server就不会报错了。这是推荐的做法一次配置永久有效。4.5 解决方案三编译时指定 rpath最省心还有一种方法是在编译时通过-Wl,-rpath把库路径写进可执行文件gcc server.c -o server -I/usr/local/libevent/include -L/usr/local/libevent/lib -levent -Wl,-rpath/usr/local/libevent/lib这样可执行文件在运行时会自动去/usr/local/libevent/lib找库不需要设置任何环境变量。缺点是如果库路径变了需要重新编译。适合打包发布时使用。4.6 三种方案对比与选择建议方案生效范围是否需要 root适用场景LD_LIBRARY_PATH当前终端会话否临时测试、快速验证ld.so.conf ldconfig全局永久是开发机长期使用-Wl,-rpath单个可执行文件否打包发布、避免环境依赖我个人的习惯是开发阶段用LD_LIBRARY_PATH快速验证确认没问题后配置ld.so.conf永久生效。如果是给别人交付可执行文件就用-Wl,-rpath把路径写死省得对方还要配环境。5. 常见问题与排查技巧实录5.1 编译期问题速查表报错信息原因解决方法event2/event.h: No such file or directory头文件路径没指定加-I/usr/local/libevent/includecannot find -levent库路径没指定或库没装加-L/usr/local/libevent/lib确认 lib 目录下有 libevent.soundefined reference to event_base_new链接顺序问题把-levent放在源文件后面configure: error: openssl not found缺少 openssl 开发库装libssl-dev或者./configure --disable-openssl5.2 运行期问题速查表报错信息原因解决方法libevent-2.1.so.7: cannot open shared object file动态库路径未配置设置 LD_LIBRARY_PATH 或配置 ld.so.confbind: Address already in use端口被占用加LEV_OPT_REUSEABLE或换端口Connection refused服务端没启动或端口不对检查服务端是否在监听端口是否一致客户端连上后立即断开bufferevent 没启用 EV_READ检查bufferevent_enable是否调用5.3 几个容易忽略的细节第一个坑是bufferevent_enable必须调用。我一开始忘了加这行结果客户端连上后收不到任何数据排查了半天才发现是没启用读事件。libevent 的 bufferevent 默认是不启用任何事件的必须显式调用bufferevent_enable(bev, EV_READ | EV_WRITE)。第二个坑是evbuffer_readln的内存管理。它返回的字符串是 malloc 出来的用完必须 free否则内存泄漏。我见过不少人忘了 free跑久了内存一直涨。第三个坑是event_base_dispatch的退出。服务端一般是死循环不会主动退出。客户端在收到服务端关闭事件后需要调用event_base_loopexit让事件循环退出否则程序会一直挂着。第四个坑是编译时的链接顺序。-levent必须放在源文件后面因为链接器是从左到右解析符号的。如果写成gcc -levent server.c就会报 undefined reference。5.4 调试技巧用 strace 和 ldd 定位问题如果遇到奇怪的运行时问题可以用ldd查看可执行文件依赖哪些动态库ldd ./server如果某个库显示not found就说明路径有问题。还可以用strace跟踪系统调用看程序到底在哪里失败strace ./server这两个工具在排查动态库和网络问题时非常有用建议熟练掌握。6. 个人实操体会与扩展建议这套 Demo 跑通之后我又做了几个小扩展。一个是把服务端改成多线程每个线程一个 event_base利用多核性能。另一个是加了简单的协议解析用evbuffer_remove处理二进制数据。还有一个是用bufferevent_pair做线程间通信替代传统的 pipe。libevent 的学习曲线其实不算陡核心概念就那几个event_base、event、bufferevent、evconnlistener。把这四个东西搞清楚大部分网络编程场景都能应付。我建议新手先从 bufferevent 入手因为它封装得最完善不用自己管理缓冲区。等熟悉了之后再去看底层 event 的用法理解会更深刻。最后再分享一个小技巧如果你在容器里跑 libevent 程序记得把库路径也挂载进去或者直接在容器里重新编译安装。容器里的动态库搜索路径和宿主机是隔离的宿主机配好的 ld.so.conf 在容器里不生效。这个坑我在部署的时候踩过一次排查了好久才反应过来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑