Linux下基于TCP传输组件的实现
2014-01-01苑晓芳刘志广
苑晓芳,刘志广
(中国电子科技集团公司第五十四研究所,河北石家庄050081)
0 引言
随着国产Linux操作系统的不断应用,原来在Windows平台下的信息系统也逐渐转向了Linux平台。在信息系统中非常重要的一个部分就是数据的共享,该功能通常由数据流转软件实现,而数据传输部分是该软件一个重要的底层功能支撑。根据对组件的多客户并发应用、保证数据传输的可靠性和时效性的要求特点,设计实现了一个在Linux系统下基于TCP协议的传输组件。
1 传输组件的工作原理
1.1 服务和客户的初始化
传输组件是一个对等传输,所以在使用时,要先初始化本地的传输服务端和客户端。
(1)初始化组件服务端
建立组件服务端的套接字,对其进行参数设置,然后将本地服务端口号绑定到套接字。
启动监听线程:该线程是为等待接收新客户端的连接请求。
启动接收线程:该线程是为接收已连接客户端发送的数据。
(2)初始化组件客户端
启动发送线程:该应用线程是为处理客户端提交发送的数据。
1.2 数据传输
(1)数据发送接收的准备过程
数据流转软件调用组件发送接口进行数据发送。组件客户端首先调用socket()函数创建新的通信套接字,通过connect()函数和目的地址服务端建立连接。连接建立成功后,向服务端发送本地的监听服务端口号。客户端将建立成功的连接套接字存放到套接字列表。建立新的发送数据结构,将待发送内容和数据类型添加到结构中,并将该数据结构添加到待发送数据列表。
服务端在监听端口调用accept()函数成功后,返回一个新的套接字表示与客户端的连接。调用recv()函数接收客户端发来的服务端口号。将返回的套接字和操作实例用map表进行保存,并将返回的套接字保存到套接字列表中。
客户端发送线程通过select()函数,遍历发送套接字列表,查找出已提交数据的套接字,然后从线程池取出线程用于对该目的地址进行数据发送。
(2)数据发送接收的具体过程
首先从发送数据列表中取出待发送数据,将当前发送状态置为NONE_SNDST,然后构建发送请求数据包,该数据包中包含数据请求标志、数据编号和待发送数据长度;(其中,待发送数据长度是重要的数据项,可以保证服务端接收数据时对即将接收到的数据长度的控制。)然后将发送状态置为WAITSNDACK_SNDST。
服务端的接收线程通过使用select()函数,对多个套接字描述符进行一定时间的等待,判定描述符是否有新数据到来。当有数据到来时,通过线程池启动的线程接收数据,对数据进行处理。先将接收状态置为NONE_RECVST,此时接收的数据是客户端发送的数据请求。对该请求进行处理,如果是数据发送请求,得到数据长度;如果是文件发送请求,得到文件名和文件长度。将发送状态置为READY_RECVST,构建发送请求响应包,包中包括响应标志和发送请求中的数据编号并发送请求响应。发送成功后,将接收状态置为RECVING_RECVST。
客户端接收到请求响应数据包,对响应进行验证,验证正确后,发送状态转为READY_SNDST。每次读取一定数量字节的数据,进行循环发送,直到发送数据的字节数等于待发送数据的字节数,表明此次数据完全发送结束,将发送状态置为SNDED_SNDST。
对等服务端循环接收客户端发送的数据。如果是内存数据,接收直到已接收数据的字节数大于等于预期的接收字节数,回调组件的接收数据接口函数,将接收到的完整数据通过回调接收函数发送给应用程序;如果是文件数据,先在当前目录创建该文件,然后将接收到的文件数据内容写入文件,直到写入的字节数等于预期接收的文件字节数。将接收状态置为RECVED_RECVST,此次数据接收完成。
对等传输组件的工作原理如图1所示。
图1 传输组件的工作原理
传输组件在客户和服务之间交互过程的状态控制,提高了组件传输的可靠性。
2 技术实现
2.1 IO多路复用服务模型
Socket的读写模型有3种:阻塞方式、非阻塞方式和 IO 多路复用[1-4]。
阻塞方式:应用进程调用read()或recv()函数时,若套接字缓冲区没有可读或可写的数据时,则函数将被阻塞,进程进入睡眠等待状态,直到有数据到来,进程才被唤醒。阻塞式IO模型的结构简单,但是进程的效率低,因为应用进程不能及时处理多个套接字的情况。
非阻塞方式:当进程调用read()或recv()函数时,若套接字缓冲区没有数据或有部分数据都会立即返回。非阻塞方式相比较阻塞方式虽然进程效率有些提高,但因为每个套接字都要读取多次,所以在多个套接字的情况下,需要对每个套接字进行轮询而占用大量的CPU时间。
IO多路复用:IO复用可以在进程睡眠的状态下实现对多个套接字的检测,任何一个套接字上发生事件时,由linux系统去唤醒进程,这样就能节省大量的CPU时间。
在该传输组件中,客户端的发送线程和服务端的接收线程是通过使用socket的IO多路复用模型,来实现对数据的发送和接收的。其实现方式是通过select()函数:函数定义形式为:
Int select(int maxfd,fd_set*read_set,fd_set*write_set,fd_set except_set,const struct timeval*timeout);
其中,maxfd是需要检测的文件描述符最大值加 1,read_set、write_set和 except_set参数分别对应于需要检测的可读描述符集,可写描述符集和异常描述符集,timeout是select函数的最大等待时间。
以发送端的发送线程为例:循环发送套接字列表,并依次调用FD_SET(*iterSock,&sockSet)函数将每个套接字描述符*iterSock在sockSet中的相应位开放,并通过
}来记录maxsock的最大值。调用INT iRes=select(maxsock,NULL,&sockSet,NULL,&tval),如果iRes>0,则在 sockSet集合中有一个或多个 socket可写数据。再次循环socket发送列表,并通过FD_ISSET((*iterSock),&sockSet))函数确定某个socket达到了写条件,然后对该socket中的数据进行发送操作。
Socket的IO多路复用综合了阻塞式与非阻塞式输入输出的优点,提高了传输组件的进程效率,增加了数据处理的时效性。
2.2 实现并应用线程池
传输组件的客户端会向多个不同地址的客户端发送数据,服务端也会接收多个不同地址客户端发来的数据。在这种多用户并发的情况下,为了能够及时响应并处理多个不同端地址的数据,需要使用多个线程。传统的多线程服务模型是当新数据请求到来时,就创建新线程,线程执行完任务后退出。尽管创建和销毁线程是轻量级的,但过于频繁的话,就会给服务造成很大的负担[5-7]。为了解决上述问题,传输组件实现并使用了自适应的线程池。
线程池可以统一任务接口,通过线程复用将创建和销毁线程的开销分摊到多个任务上。线程池的实现包括4部分:线程池管理器、工作线程、任务接口和任务队列。
线程池管理器:用于创建和管理池中的工作线程,并将工作任务按需分配到不同的线程中;
工作线程:执行实际任务的线程;
任务接口:执行任务函数和参数的地址;
队列:保存待执行任务的列表和工作线程的列表。
以客户端发送数据为例:当向新的客户端发送数据时,发送客户端将发送任务添加到线程池管理器的任务队列,线程池管理器找出空闲线程后,将该任务分配到线程中去执行。需要注意的是,为了保证数据的按序发送,需要对添加的每个任务设置是否完成标志,来保证每条数据按序正常的发送。以客户端为例,传输组件通过线程池执行任务的代码如下:
2.3 TCP粘包的处理
TCP是面向连接的并且提供了一个可靠的字节流信道,数据流可以通过这个信道在两个终端系统之间流动。由于TCP协议的传输特点,为了提高效率,TCP在发送方会将多个待发送的数据包一起发送。发送方发送的若干包数据到达接收方接收时粘成一包,即多个数据包首尾相连,称为粘包[8,9]。
通常解决粘包现象的方法是:接收方创建一个预处理线程,对接收到的数据包进行预处理,将粘包分开。
本传输组件因为是对等传输,所以可以通过在客户端和服务端之间的传输过程中加入控制机制,来避免对粘包的单独处理。采取的措施如下:
①在每一次数据传输之前,都会先进行一个发送数据请求和返回请求响应的交互过程。该交互过程主要是在发送方和接收方之间确定即将传输数据的大小,这样接收方就可以在数据传送之前确定本次应该接收的数据的确切大小;
②第1次数据传输结束之后,会将发送状态置为发送完毕,第2条数据就会开始发送数据请求。而由于TCP的传输机制,会将第1条的全部或部分数据和第2条的数据请求包粘在一起进行发送。因为发送方对于第2条数据没有收到请求响应之前,不会发送真正数据内容,所以粘在后面的数据部分只可能是第2个数据包的数据请求。接收方先基于第1条数据请求中携带的数据长度,从粘包数据中截取真正的数据内容,然后将剩下的数据作为第2包的数据请求报进行验证处理。
这种处理TCP粘包的方式比连续接收数据,并启动线程进行单独处理粘包数据的方式效率更高。
2.4 丰富的传输接口和回调接口
传输组件为了使上层应用能够更灵活地进行数据传输和得到更多的数据传输信息,组件提供了较为丰富的传输接口和回调接口[10-12]。
传输接口:提供了对数据和文件的异步和同步发送。如异步发送数据接口:
virtual LONG SendData(const BYTE* pBuf,const ULONG ulSndLen,const PeerAddress& peeraddr,const ULONG uiBlockNotifySize,UINT & uiSnd-Num)=0;
其中,const BYTE*pBuf为指向待发送数据的指针;const ULONG ulSndLen为待发送数据长度;const PeerAddress&peeraddr为待发送数据的目的地址,其中PeerAddress为自定义的地址结构,包括端口号和IP地址等;const ULONG uiBlockNotifySize是设置已传输数据多大时通知给应用;UINT&uiSnd-Num是数据编号,可以为空。组件还提供了传输文件的异步接口SendFile,可同时发送数据和文件的异步接口SendDataAndFiles。
回调接口:组件通过回调接口可以接收文件或数据,还可以接收在传输过程中接收数据或传输数据的大小等。如接收数据回调接口:
virtual LONG OnRecvData(const BYTE*pData,const ULONG ulLen,const DataTime& tmDataRecv,const PeerAddress& peeraddr)=0;//接收数据
其中,const BYTE*pData为传入的接收到的数据指针,const ULONG ulLen是接收到的数据长度,const DataTime&tmDataRecv是数据接收的时间,const PeerAddress&peeraddr是数据的发送地址。
组件还提供了接收文件的回调接口OnRecv-File,当前接收数据大小的回调接口OnRecvingData-Size,当前发送文件数据的大小 OnFileTransSize,发送结果OnSendResult。还有连接断开OnDisconnect函数,OnConnError连接错误等函数。通过这些回调函数接口,应用进程可以获得更多的传输状态,发送结果等信息。
3 组件测试
传输组件的功能是为信息系统中的数据流转软件提供及时和可靠的数据传输,并且在多用户并发的情况下也能够满足。针对这一特性,在测试阶段为传输组件设计了多种性能测试。测试内容主要是针对多用户并发、大数据块和大数据量等不同条件下,组件传输数据的时效性和可靠性。经过大量的测试实验后,表明该传输组件可以为数据流转软件提供及时和可靠的传输服务。
4 结束语
Linux下基于TCP的传输组件,可以为数据流转软件和其他上层应用软件提供底层的数据和文件传输,是信息系统中重要支撑功能;同时,数据传输的稳定性、可靠性和及时性,更是上层应用所关心的,所以性能成为了评价传输组件优劣非常关键的因素。
本文中所探讨的组件实现通过在组件中应用多种技术,提高了组件的传输性能。首先在TCP传输协议上增加自控机制提高了数据传输的稳定性和可靠性,也提高了粘包数据的处理效率;采用socket的多路复用与线程池技术,提高了组件传输数据的及时性;丰富的传输接口和回调接口可以满足不同上层应用软件的要求,也提供了更全面的传输信息。
[1] 天夜创作室.Linux网络编程技术[M].北京:人民邮电出版社,2001.
[2] 宋敬彬,孙海滨.Linux网络编程[M].北京:清华大学出版社,2010.
[3] 张斌.Linux网络编程[M].北京:清华大学出版社,2000.
[4] 陈娟,赵振平.TCP/IP高效编程[M].北京:人民邮电出版社,2011.
[5] 王雷,王子淘.基于Linux的Socket网络编程的性能优化[J].电子设计工程,2009,17(9):101 -103.
[6] 唐富强,于鸿洋,张萍.Linux下通用线程池的改进与实现[J].计算机工程与应用,2012 48(28):77-83.
[7] 郭东升,田秀华.Linux环境下基于socket的网络通信[J].软件导刊,2009,8(1):116 -118.
[8] 李慧霸,田甜,彭宇行,等.网络程序设计中的并发复杂性[J].软件学报,2011,22(1):132 -148.
[9] 刘新强,曾兵义.用线程池解决服务器并发请求的方案设计[J].现代电子技术,2011,34(15):141 -143.
[10]崔弘珂.一种空间环境下的TCP传输技术研究[J].无线电通信技术,2011,37(4):21 -24.
[11]宋广怡.超宽带高速数据传输技术研究[J].无线电工程,2014,44(5):23 -25
[12]李志高,周音,孙学斌,等.认知无线电网络中一种改进的传输层协议[J].无线电工程,2011,41(10):1 -3.