# tcpdaemon **Repository Path**: calvinwilliams/tcpdaemon ## Basic Information - **Project Name**: tcpdaemon - **Description**: tcpdaemon是一个TCP通讯服务端平台/库,它封装了众多常见服务端进程/线程管理和TCP连接管理模型(Forking、Leader-Follow、IO-Multiplex、WindowsThreads Leader-Follow),使用者只需加入TCP通讯数据收发和应用逻辑代码就能快速构建出完整的TCP应用服务器。 - **Primary Language**: C - **License**: LGPL-2.1 - **Default Branch**: release - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 91 - **Forks**: 40 - **Created**: 2014-07-05 - **Last Updated**: 2025-06-20 ## Categories & Tags **Categories**: web-dev-toolkits **Tags**: None ## README tcpdaemon ========= # 0.快速开始 # 老师交给小明一个开发任务,实现一个TCP网络迭代并发服务器,用于回射任何接收到的通讯数据。小明很懒,他在开源中国项目库里搜到了开源库tcpdaemon来帮助他快速完成任务。首先他安装好tcpdaemon,然后写了一个C程序文件test_callback_echo.c $ vi test_callback_echo.c #include "tcpdaemon.h" _WINDLL_FUNC int tcpmain( struct TcpdaemonServerEnvironment *p_env , int sock , void *p_addr ) { char buffer[ 4096 ] ; int len ; len = recv( sock , buffer , sizeof(buffer) , 0 ) ; if( len == 0 ) return TCPMAIN_RETURN_CLOSE; else if( len < 0 ) return TCPMAIN_RETURN_ERROR; len = send( sock , buffer , len , 0 ) ; if( len < 0 ) return TCPMAIN_RETURN_ERROR; return TCPMAIN_RETURN_WAITINGFOR_NEXT; } 他编译链接成动态库test_callback_echo.so,最后用tcpdaemon直接挂接执行 $ tcpdaemon -m IF -l 0:9527 -s test_callback_echo.so -c 10 --tcp-nodelay --logfile $HOME/log/test_callback_echo.log OK,总共花了五分钟,圆满完成老师作业。老师说这个太简单了,小明你给我改成像Apache经典的Leader-Follow服务端模型,小明说没问题,他把启动命令参数`-m IF`改成了`-m LF`,再次执行,完成老师要求,总共花了五秒钟。老师问你怎么这么快就改好了,小明说全靠开源项目tcpdaemon帮了大忙啊 ^_^ # 1.概述 # tcpdaemon是一个TCP通讯服务端平台/库,它封装了众多常见服务端进程/线程管理和TCP连接管理模型(Forking、Leader-Follow、IO-Multiplex、WindowsThreads Leader-Follow),使用者只需加入TCP通讯数据收发和应用逻辑代码就能快速构建出完整的TCP应用服务器。 | 服务模型 | 模型说明 | | ---- | ---- | | Forking | 单进程主守护,每当一条TCP新连接进来后,接受之,创建子进程进入回调函数tcpmain处理之。一条连接对应一个子进程(短生命周期) | | Leader-Follow | 单进程管理进程,预先创建一组子进程(长生命周期)并监控其异常重启。子进程等待循环争抢TCP新连接调用回调函数tcpmain处理之 | | IO-Multiplex | 单进程主守护,IO多路复用等待TCP新连接进来事件、TCP数据到来事件,TCP数据可写事件,调用回调函数tcpmain处理之 | | WindowsThreads Leader-Follow | 同Leader-Follow,区别在于预先创建一组子线程而非子进程 | tcpdaemon提供了三种与使用者代码对接方式:(注意:.exe只是为了说明自己是可执行文件,在UNIX/Linux中可执行文件一般没有扩展名) | 链接模式 | 链接关系 | 说明 | | ---- | ----- | -- | | 回调模式 | tcpdaemon.exe+user.so(tcpmain) | 可执行程序tcpdaemon通过启动命令行参数挂接用户动态库,获得动态库中函数tcpmain指针。当建立TCP连接后 或 IO多路复用模式下当可读可写事件发生时 调用回调函数tcpmain | | 主调模式 | user.exe(main,tcpmain)+libtcpdaemon.a(tcpdaemon) | 用户可执行程序user.exe隐式链接库libtcpdaemon.a。用户函数main(user.exe)初始化tcpdaemon参数结构体,并设置回调函数tcpmain,调用函数tcpdaemon(libtcpdaemon.so)。当建立TCP连接后 或 IO多路复用模式下当可读可写事件发生时 调用回调函数tcpmain | | 主调+回调模式 | user.exe(main)+libtcpdaemon.a(tcpdaemon) + user.so(tcpmain) | 同上,区别在于用户函数main不直接设置回调函数tcpmain而设置user.so文件名。函数tcpdaemon负责挂接动态库user.so并获得函数tcpmain指针 | 一般简单情况下,使用者采用回调模式即可,只要编写一个动态库user.so(内含回调函数tcpmain)被可执行程序tcpdaemon挂接上去运行。如果使用者想订制一些自定义处理,如初始化环境,可以采用主调模式,实现函数main里把自定义参数传递给tcpdaemon穿透给tcpmain。如果想实现运行时选择回调函数tcpmain则可以采用主调+回调模式。 # 2.编译安装 # 以Linux操作系统为例,下载到最新源码安装包tcpdaemon-x.y.z.tar.gz到某目录,解压之 $ tar xvzf tcpdaemon-x.y.z.tar.gz ... $ cd tcpdaemon $ cd src $ make -f makefile.Linux install rm -f LOGC.o rm -f tcpdaemon_lib.o rm -f tcpdaemon_main.o rm -f tcpdaemon rm -f libtcpdaemon.a gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include -c tcpdaemon_lib.c gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include -c LOGC.c ar rv libtcpdaemon.a tcpdaemon_lib.o LOGC.o ar: 正在创建 libtcpdaemon.a a - tcpdaemon_lib.o a - LOGC.o gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include -c tcpdaemon_main.c gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -o tcpdaemon tcpdaemon_main.o tcpdaemon_lib.o LOGC.o -L. -L/home/calvin/lib -lpthread -ldl cp -rf tcpdaemon /home/calvin/bin/ cp -rf libtcpdaemon.a /home/calvin/lib/ cp -rf tcpdaemon.h /home/calvin/include/tcpdaemon/ 可以看到可执行程序tcpdaemon安装到$HOME/bin,静态库libtcpdaemon.a安装到$HOME/lib,开发用的头文件安装到$HOME/include/tcpdaemon。 # 3.使用示例 # ## 3.1.服务模型Forking,链接模式1,接收HTTP请求报文然后发送HTTP响应报文 ## 使用者只需编写一个函数tcpmain,实现同步的接收HTTP请求报文然后发送HTTP响应报文回去 $ vi test_callback_http_echo.c #include "tcpdaemon.h" _WINDLL_FUNC int tcpmain( struct TcpdaemonServerEnvironment *p_env , int sock , void *p_addr ) { char http_buffer[ 4096 + 1 ] ; long http_len ; long len ; /* 接收HTTP请求 */ memset( http_buffer , 0x00 , sizeof(http_buffer) ); http_len = 0 ; while( sizeof(http_buffer)-1 - http_len > 0 ) { len = RECV( sock , http_buffer + http_len , sizeof(http_buffer)-1 - http_len , 0 ) ; if( len == 0 ) return TCPMAIN_RETURN_CLOSE; if( len == -1 ) return TCPMAIN_RETURN_ERROR; if( strstr( http_buffer , "\r\n\r\n" ) ) break; http_len += len ; } if( sizeof(http_buffer)-1 - http_len <= 0 ) { return TCPMAIN_RETURN_ERROR; } /* 发送HTTP响应 */ memset( http_buffer , 0x00 , sizeof(http_buffer) ); http_len = 0 ; http_len = sprintf( http_buffer , "HTTP/1.0 200 OK\r\nContent-length: 17\r\n\r\nHello Tcpdaemon\r\n" ) ; SEND( sock , http_buffer , http_len , 0 ); return TCPMAIN_RETURN_CLOSE; } 编译链接成test_callback_http_echo.so $ gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include/tcpdaemon -c test_callback_http_echo.c $ gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -shared -o test_callback_http_echo.so test_callback_http_echo.o -L. -L/home/calvin/lib -lpthread -ldl 用tcpdaemon直接挂接即可 $ tcpdaemon -m IF -l 0:9527 -s test_callback_http_echo.so -c 10 --tcp-nodelay --logfile $HOME/log/test_callback_http_echo.log --loglevel-debug 可执行程序tcpdaemon所有命令行参数可以不带参数的执行而得到 $ tcpdaemon USAGE : tcpdaemon -m IF -l ip:port -s so_pathfilename [ -c max_process_count ] -m LF -l ip:port -s so_pathfilename -c process_count [ -n max_requests_per_process ] other options : -v [ --daemon-level ] [ --work-path work_path ] [ --work-user work_user ] 执行后可在$HOME/log下可以看到tcpdaemon的日志。 通过curl发测试请求 $ curl "http://localhost:9527/" Hello Tcpdaemon 测试成功! **所有代码在源码安装包的test目录里下可以找到** ## 3.2.服务模型IO-Multiplex,链接模式2,非堵塞的接收HTTP请求报文然后发送HTTP响应报文 ## 使用者编写一个函数main $ test_main_IOMP.c #include "tcpdaemon.h" extern int tcpmain( struct TcpdaemonServerEnvironment *p_env , int sock , void *p_addr ); int main() { struct TcpdaemonEntryParameter ep ; memset( & ep , 0x00 , sizeof(struct TcpdaemonEntryParameter) ); snprintf( ep.log_pathfilename , sizeof(ep.log_pathfilename) , "%s/log/test_main_IOMP.log" , getenv("HOME") ); ep.log_level = LOGLEVEL_DEBUG ; strcpy( ep.server_model , "IOMP" ); ep.timeout_seconds = 60 ; strcpy( ep.ip , "0" ); ep.port = 9527 ; ep.tcp_nodelay = 1 ; ep.process_count = 1 ; ep.pfunc_tcpmain = & tcpmain ; ep.param_tcpmain = NULL ; return -tcpdaemon( & ep ); } 结构体TcpdaemonEntryParameter所有成员说明在tcpdaemon.h里可以找到 struct TcpdaemonEntryParameter { int daemon_level ; /* 是否转化为守护服务 1:转化 0:不转化(缺省) */ char log_pathfilename[ 256 + 1 ] ; /* 日志输出文件名,不设置则输出到标准输出上 */ int log_level ; /* 日志等级 */ char server_model[ 10 + 1 ] ; /* TCP连接管理模型 LF:领导者-追随者预派生进程池模型 for UNIX,Linux IF:即时派生进程模型 for UNIX,Linux WIN-TLF:领导者-追随者预派生线程池模型 for win32 IOMP:进程池+多路复用模型 for UNIX,Linux 回调函数内应分支事件处理 */ int process_count ; /* 当为领导者-追随者预派生进程池模型时为工作进程池进程数量,当为即时派生进程模型时为最大子进程数量,当为IO多路复用模型时为工作进程池进程数量 */ int max_requests_per_process ; /* 当为领导者-追随者预派生进程池模型时为单个工作进程最大处理应用次数 */ char ip[ 20 + 1 ] ; /* 本地侦听IP */ int port ; /* 本地侦听PORT */ char so_pathfilename[ 256 + 1 ] ; /* 用绝对路径或相对路径表达的应用动态库文件名 */ char work_user[ 64 + 1 ] ; /* 切换为其它用户运行。可选 */ char work_path[ 256 + 1 ] ; /* 切换到指定目录运行。可选 */ func_tcpmain *pfunc_tcpmain ; /* 当函数调用模式时,指向把TCP连接交给应用入口函数指针 */ void *param_tcpmain ; /* 当函数调用模式时,指向把TCP连接交给应用入口函数的参数指针。特别注意:自己保证线程安全 */ int tcp_nodelay ; /* 启用TCP_NODELAY选项 1:启用 0:不启用(缺省)。可选 */ int tcp_linger ; /* 启用TCP_LINGER选项 >=1:启用并设置成参数值 0:不启用(缺省)。可选 */ int timeout_seconds ; /* 超时时间,单位:秒;目前只对IO-Multiplex模型有效 */ /* 以下为内部使用 */ int install_winservice ; int uninstall_winservice ; } ; 使用者再编写一个函数tcpmain,实现非堵塞的接收HTTP请求报文然后发送HTTP响应报文回去 $ vi test_callback_http_echo_nonblock.c #include "tcpdaemon.h" struct AcceptedSession { int sock ; /* socket描述字 */ struct sockaddr addr ; /* socket地址 */ char http_buffer[ 4096 + 1 ] ; /* HTTP收发缓冲区 */ int read_len ; /* 读了多少字节 */ int write_len ; /* 将要写多少字节 */ int wrote_len ; /* 写了多少字节 */ } ; _WINDLL_FUNC int tcpmain( struct TcpdaemonServerEnvironment *p_env , int sock , void *p_addr ) { struct AcceptedSession *p_accepted_session = NULL ; int len ; switch( TDGetIoMultiplexEvent(p_env) ) { /* 接受新连接事件 */ case IOMP_ON_ACCEPTING_SOCKET : /* 申请内存以存放已连接会话 */ p_accepted_session = (struct AcceptedSession *)malloc( sizeof(struct AcceptedSession) ) ; if( p_accepted_session == NULL ) return TCPMAIN_RETURN_ERROR; memset( p_accepted_session , 0x00 , sizeof(struct AcceptedSession) ); p_accepted_session->sock = sock ; memcpy( & (p_accepted_session->addr) , p_addr , sizeof(struct sockaddr) ); /* 设置已连接会话数据结构 */ TDSetIoMultiplexDataPtr( p_env , p_accepted_session ); /* 等待读事件 */ return TCPMAIN_RETURN_WAITINGFOR_RECEIVING; /* 关闭连接事件 */ case IOMP_ON_CLOSING_SOCKET : /* 释放已连接会话 */ p_accepted_session = (struct AcceptedSession *) p_addr ; free( p_accepted_session ); /* 等待下一任意事件 */ return TCPMAIN_RETURN_WAITINGFOR_NEXT; /* 通讯接收事件 */ case IOMP_ON_RECEIVING_SOCKET : p_accepted_session = (struct AcceptedSession *) p_addr ; /* 非堵塞接收通讯数据 */ len = RECV( p_accepted_session->sock , p_accepted_session->http_buffer+p_accepted_session->read_len , sizeof(p_accepted_session->http_buffer)-1-p_accepted_session->read_len , 0 ) ; if( len == 0 ) return TCPMAIN_RETURN_CLOSE; else if( len == -1 ) return TCPMAIN_RETURN_ERROR; /* 已接收数据长度累加 */ p_accepted_session->read_len += len ; /* 如果已收完 */ if( strstr( p_accepted_session->http_buffer , "\r\n\r\n" ) ) { /* 组织响应报文 */ p_accepted_session->write_len = sprintf( p_accepted_session->http_buffer , "HTTP/1.0 200 OK\r\n" "Content-length: 17\r\n" "\r\n" "Hello Tcpdaemon\r\n" ) ; return TCPMAIN_RETURN_WAITINGFOR_SENDING; } /* 如果缓冲区收满了还没收完 */ if( p_accepted_session->read_len == sizeof(p_accepted_session->http_buffer)-1 ) return TCPMAIN_RETURN_ERROR; /* 等待下一任意事件 */ return TCPMAIN_RETURN_WAITINGFOR_NEXT; /* 通讯发送事件 */ case IOMP_ON_SENDING_SOCKET : p_accepted_session = (struct AcceptedSession *) p_addr ; /* 非堵塞发送通讯数据 */ len = SEND( p_accepted_session->sock , p_accepted_session->http_buffer+p_accepted_session->wrote_len , p_accepted_session->write_len-p_accepted_session->wrote_len , 0 ) ; if( len == -1 ) return TCPMAIN_RETURN_ERROR; /* 已发送数据长度累加 */ p_accepted_session->wrote_len += len ; /* 如果已发完 */ if( p_accepted_session->wrote_len == p_accepted_session->write_len ) return TCPMAIN_RETURN_CLOSE; /* 等待下一任意事件 */ return TCPMAIN_RETURN_WAITINGFOR_NEXT; default : return TCPMAIN_RETURN_ERROR; } } 编译成test_main_IOMP gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include/tcpdaemon -c test_main_IOMP.c gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -I. -I/home/calvin/include/tcpdaemon -c test_callback_http_echo_nonblock.c gcc -g -fPIC -O2 -Wall -Werror -fno-strict-aliasing -o test_main_IOMP test_main_IOMP.o test_callback_http_echo_nonblock.o -L. -L/home/calvin/lib -lpthread -ldl -ltcpdaemon 执行test_main_IOMP $ ./test_main_IOMP 执行后可在$HOME/log下可以看到test_main_IOMP的日志。 通过curl发测试请求 $ curl "http://localhost:9527/" Hello Tcpdaemon 测试成功! **所有代码在源码安装包的test目录里下可以找到** # 4.参考 # ## 4.1.回调函数tcpmain ## tcpdaemon会在合适时机回调用户层指定函数tcpmain。 当服务模型为Forking或Leader-Follow或WindowsThreads Leader-Follow时,回调函数tcpmain在连接被接受后调用。返回TCPMAIN_RETURN_CLOSE或TCPMAIN_RETURN_ERROR都将关闭通讯连接。 当服务模型为IO-Multiplex时,回调函数tcpmain在通讯连接被接受时、通讯连接被关闭时、通讯数据可接收/发送时调用,因为是非堵塞处理,返回TCPMAIN_RETURN_CLOSE或TCPMAIN_RETURN_ERROR会关闭通讯连接,返回TCPMAIN_RETURN_WAITINGFOR_RECEIVING会切换等待可读事件,返回TCPMAIN_RETURN_WAITINGFOR_SENDING会切换等待可写事件,返回TCPMAIN_RETURN_WAITINGFOR_NEXT会继续等待其它事件。(注意:通讯连接被接受时的tcpmain里必须调用TDSetIoMultiplexDataPtr设置通讯数据会话结构) ## 4.2.TcpdaemonServerEnvironment环境函数集 ## | 函数名 | TDGetTcpmainParameter | | --:|:-- | | 函数原型 | void *TDGetTcpmainParameter( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 调用tcpmain时传入TcpdaemonEntryParameter的param_tcpmain地址 | | 函数名 | TDGetListenSocket | | --:|:-- | | 函数原型 | int TDGetListenSocket( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 侦听端口描述字 | | 函数名 | TDGetListenSocketPtr | | --:|:-- | | 函数原型 | int *TDGetListenSocketPtr( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 侦听端口描述字的地址 | | 函数名 | TDGetListenAddress | | --:|:-- | | 函数原型 | struct sockaddr_in TDGetListenAddress( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 侦听端口网络地址 | | 函数名 | TDGetListenAddressPtr | | --:|:-- | | 函数原型 | struct sockaddr_in *TDGetListenAddressPtr( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 侦听端口网络地址的地址 | | 函数名 | TDGetProcessCount | | --:|:-- | | 函数原型 | int TDGetProcessCount( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 配置最大并发度或静态进程池进程数量 | | 函数名 | TDGetEpollArrayBase | | --:|:-- | | 函数原型 | int *TDGetEpollArrayBase( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | IO多路复用epoll数组第一个元素的地址 | | 函数名 | TDGetThisEpoll | | --:|:-- | | 函数原型 | int TDGetThisEpoll( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 当前IO多路复用epoll描述字 | | 函数名 | TDGetIoMultiplexEvent | | --:|:-- | | 函数原型 | int TDGetIoMultiplexEvent( struct TcpdaemonServerEnvironment *p_env ); | | 输入参数 | struct TcpdaemonServerEnvironment *p_env tcpdaemon环境结构指针 | | 返回值 | 当前IO多路复用epoll事件 IOMP_ON_ACCEPTING_SOCKET:接受新连接事件 IOMP_ON_CLOSING_SOCKET:关闭连接事件 IOMP_ON_RECEIVING_SOCKET:接收通讯数据事件 IOMP_ON_SENDING_SOCKET:发送通讯数据事件 | # 5.总结 # tcpdaemon提供了多种服务模型和链接模式,旨在协助使用者快速构建TCP应用服务器,比如可以使用本人的另一个开源项目 [HTTP解析器fasterhttp](http://git.oschina.net/calvinwilliams/fasterhttp) 以百行以内代码构建出一个完整的Web服务器,还有一个完整的应用案例可参阅本人的另一个开源项目 [分布式发号器](http://git.oschina.net/calvinwilliams/coconut) ,经过tcpdaemon改造后应用代码缩短了一半。 tcpdaemon源码托管在 [开源中国码云](http://git.oschina.net/calvinwilliams/tcpdaemon),你也可以通过 [邮箱](calvinwilliams@163.com) 联系到作者 能帮助到您是我的荣幸 ^_^