老师交给小明一个开发任务,实现一个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帮了大忙啊 ^_^
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则可以采用主调+回调模式。
以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。
使用者只需编写一个函数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目录里下可以找到
使用者编写一个函数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目录里下可以找到
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设置通讯数据会话结构)
函数名 | 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:发送通讯数据事件 |
tcpdaemon提供了多种服务模型和链接模式,旨在协助使用者快速构建TCP应用服务器,比如可以使用本人的另一个开源项目 HTTP解析器fasterhttp 以百行以内代码构建出一个完整的Web服务器,还有一个完整的应用案例可参阅本人的另一个开源项目 分布式发号器 ,经过tcpdaemon改造后应用代码缩短了一半。
tcpdaemon源码托管在 开源中国码云,你也可以通过 邮箱 联系到作者
能帮助到您是我的荣幸 ^_^