Linux环境下如何恰当的使用TCP协议编写网络传输程序

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

Linux环境下如何恰当的使用TCP协议编写网络传输程序【转】Linux环境下如何恰当的使用TCP协议编写网络传输程序2010-05-1108:55转载自xiangpengmeng最终编辑xiangpengmeng声明本文属于转载!学习中!!!摘要:本文介绍了Linux环境下TCP协议编写网络传输程序的几点考虑。作者提出了在使用此协议时,构造消息边界、区分程序控制数据与实际数据的设计思路。作者将客户与服务器之间的交互信息抽象成不同的命令,实现了一个文件传输协议和基于命令式交互的网络服务器、客户的架构模型。关键词:Linux,TCP,socket一、背景在2008年夏季小学期里,作者学习了Linux的下的并发网络服务器的设计理论,根据老师的要求,编制了并发文件传输的服务器、客户端程序。在编写网络程序的过程中,作者考虑到了TCP协议的原理和一般网络环境的特点,提出了平时大家不太注意的几个问题,并给出了解决方案,实现了自己的文件传输协议。二、使用TCP协议编写文件传输程序的三个问题1、无法区分消息边界平时在谈论TCP时,我们都能够注意到它与UDP的一点区别,那就是TCP的传输是流式的,而UDP的传输是面向数据报的,但是一般的同学在第一次编写传输程序时,却没有意识到这一点。实际上,TCP的流式传输,体现在编程上,就是TCP不为应用程序保留消息边界。也就是说在一端调用send(),发送一定的字节数,譬如2K个字节,在另一端调用recv()时,也许只收到其中的前1K个字节,需要再调用一次recv()才能把所有的字节收完整。这是因为在接收端的TCP程序收到合适数量的字节数之后或者缓存满之后,就会把缓冲区中的数据提交给应用程序,这时recv()函数返回,但实际上还有一些字节在网络上传送。若发送端调用两次send()分别发送了0.5K和2K个字节,接收端第一次接收可能会接收到1K个字节,第二次会接受到剩下的1.5K个字节。如果发送的0.5K和2K个字节分别代表服务器发送给客户的两个文件,接收方就无法区分两次收到的数据分别是哪个文件中的内容了。这种特点在客户与服务器需要进行一定数量的交互应答时显得非常不方便,传送的消息全部杂糅在一起,区分不出边界,需要程序员自己处理边界的问题。2、不恰当的使用发送函数另外一个问题是一些同学使用循环,按一个字节一个字节的发送文件的内容,这样做是非常有害的。虽然程序把数据推送到缓冲区后,TCP会尽量把数据集合成MSS长度发送出去,但是若数据到达的速率比较慢,即使不到一个MSS的长度,TCP等待超过一段时间后,也会导致一次发送。另外,在关闭了Nagle算法后,这种一次要求TCP发送一个字节的函数调用的效率会更低,如果文件比较大,速度会慢得不能忍受。消费IP和TCP的较长头部,却只带走很少数量字节的净荷,对带宽的利用率就变低了。3、控制信息与数据无法区分最后一个问题是,在文件较大的时候,无法一次全部读入内存,也无法一次就发送完毕,需要边读边发,这样就导致了多次send()函数的调用,客户与服务器进行请求应答会话时,传输的控制信息也要使用send(),由于没有消息边界,这些内容在recv()时就无法区分。在本次任务中,程序的要求比较简单,因此几乎没有控制信息,在更复杂的场合,这个问题是不可回避的。4、为什么平时都没有发现问题这些问题大家平时都不容易考虑到,是因为每次编写网络程序都比较简单,实验时也只发送小文件,看不到错误的发生。为了引发这些错误,我编写了一个程序,其逻辑很简单,先把文件全部读到一个一维字节数组中,再一次调用send()函数全部发出去,在接收方调用一次recv()函数收取数据,再将数据写到文件中。这个程序在发送小文件时工作得很好,在发送一个2.1K的文件时,接收方只调用一次recv(),仅收到了1.4K,剩下的数据还留在接收方缓冲区中。三、文件传输程序的设计思路1、区分消息边界在链路层的协议中,区分消息边界用的是位填充与字符填充法。在本程序中,使用另外一种办法。不使用填充法是因为无论是字符填充还是位填充,都要扫描一遍所有的数据,寻找出与边界相冲突的模式。这在链路层是比较合理的,因为要计算桢检验信息,必然要做全部的扫描,这时就可以顺便找出冲突并进行填充。但是在应用层的程序中,如果每次都要把所有的数据都扫描一遍,会减慢处理数据的速度。作者的办法是先发4字节定长的数据表示将来要发送的消息的长度,再一次把整个消息都推送给send()函数,放到缓冲区中,让TCP协议自己决定何时从缓冲区中取出数据、取多少数据发送到网络上。接收方每次先接收4字节的数据,确定将来接收数据的长度,根据这个信息,再不停的调用recv()函数,并累积已经收到的字节数,直到达到发送方一开始通告的数量为止。这期间收到的数据视为一个消息,下次再接收数据时还是先收4个字节。这个方法不需要做额外的计算,就解决了区分消息边界的问题,在发送大量数据(G以上)时,效率要比把数据全扫描一遍并填充的效率高。2、恰当的使用发送函数在调用发送函数时,不是一个字节一个字节的发送,而是一次发送一块数据。数据块的大小只要适当即可,推送到缓冲区后,TCP协议会自行决定最佳的发送策略。3、区分控制信息和数据在这之前,我们已经解决了区分消息边界的问题。一个消息实际上是一块数据,我们可以在数据的头部划分出一定的空间存放控制信息,在其余的部分存放数据。在本程序中,作者提出了“命令”的概念。一个命令包含命令的名字和命令的数据。命令名既用来区分不同的命令,又用来表示各命令特定的语义;命令的数据根据命令的不同,其含义不同。在本程序中,有如下三个命令:命令REQ_FILE,表示客户向服务器请求一个文件,命令名为“REQ_FILE”,数据部分是要请求文件的NULL结尾的字符串路径;命令RESP_DATA,表示服务器向客户发送的文件的一块数据,命令名为“RESP_DATA”,数据部分是文件的数据块;命令RESP_FIN,这个命令在服务器发送完一个文件的所有数据后发送,表示发送文件结束,命令名为“RESP_FIN”,数据部分为空。4、文件传输协议作者设计的传输协议的基础就是这些命令。每个命令被分解成有两个消息在网络上传送,首先传送包含命令名的消息,接着传送命令的数据消息。客户端连接到服务器后,向服务器传送一个REQ_FILE命令,服务器打开对应的文件,并向客户发送一系列的RESP_DATA命令,从文件里读一块数据,就把这块数据放到RESP_DATA命令的数据区域中发送出去,最后发送一个RESP_FIN命令表示传送完毕。在此,由于任务比较简单,所以命令的种类比较少。事实上,可以很容易拓展这种机制,比如增加一个RESP_NOFILE命令表示服务器对客户端通知请求的文件不存在。再增加一些登录、退出、谈话命令和线程同步机制,就可以很容易的实现一个多用户聊天程序了。这个架构可以作为一个实用的Internet程序的基础。5、任务要求客户端向服务器发送一个文件名(服务器存有该文件),服务器收到报文后进行解析,从本地读取该文件,把该文件的内容发送给客户端,客户端将接收到的数据存在本地的硬盘上。在实验中,并发服务器要接受2个以上客户端的连接。每个客户端要求的文件名应该不同。四、文件传输的实现1、公共部分首先以符号常量的形式,定义几个命令的名字:#defineREQ_FILEREQ_FILE#defineRESP_DATARESP_DATA#defineRESP_FINRESP_FIN接着定义一次处理数据块的大小:#defineMAXSIZE1024再用一个结构体定义命令:typedefstruct{//'\0'结尾的字符串形式的命令名字char*cmd;//不同的命令对应的数据的意思可能不同char*data;//数据的长度intdatalength;}Command;结构体中的变量都是指针变量,其包含的数据在运行时动态加载,分配的内存空间在使用完后释放。接着是实现发送和接收消息的两个函数://用TCP协议,发送一个消息,并且保留消息边界fd的socket描述符,D是字节数组,n是数组长度,返回的是已发送的字节数externintsndmsg(intfd,char*D,intn);//从网络上获取一个消息,返回一个指针指向收到的消息,fd是socket描述符,n是输出参数,代表收到的数据的大小externchar*getmsg(intfd,int*n);再接着是发送、接收命令的函数,这些函数通过恰当的调用发送、接收消息函数来实现自己的功能://发送一个命令,名字和数据分开成两个消息发送externintsndcmd(intfd,Commandcmd);//将接收到的数据解析成程序可理解的命令externCommandhandlecmd(intfd);最后是发送和接收文件的两个函数,这两个函数通过操作发送、接收命令的函数来完成自己的任务:externintsndbigf(intfd,char*filename);externintrcvbigf(intfd,char*filename);以上的定义包含在fileoperations.h文件中,在fileoperations.c实现其功能。2、服务器的结构有了fileoperations.c中的实现,服务器的结构就很简单了,首先建立一个socket,接着绑定到某一端口并开始监听,在accept()到客户端连接后,利用fork()函数建立子进程处理客户的请求,主进程则继续监听。方便叙述起见,省略了错误处理代码。其结构可以用自然语言+伪代码的形式表示如下:skid=socket(AF_INET,SOCK_STREAM,0);bind(skid,填写好的本机地址结构,地址结构大小);listen(skid,1);while(1){newid=accept(skid,&对方地址,地址结构体大小))pid=fork();if(pid==0){我们在子进程中,调用sendbigf(new_fd);}elseif(pid0){我们在父进程里,打印客户和子进程信息;}else{打印错误;}close(newfd);}close(skid);3、客户端的结构方便叙述起见,省略了错误处理代码。//建立套接字sockfd=socket(AF_INET,SOCK_STREAM,0);//连接服务器connect(sockfd,&对方地址,地址结构体大小);//调用下面自定义函数向服务器请求一个文件,此函数向服务器发送REQ_FILE命令reqfile(sockfd,getf);//getf是需要的文件的文件名或者路径//调用自定义函数接收文件,savef是要存放的文件名或者路径rcvbigfile(sockfd,savef);//关闭套接字close(sockfd);4、其他建立套接字和获取网络信息要用到gethostname()、gethostbyname()、inet_ntoa()等函数和hostent结构体,构造地址信息要用sockaddr_in结构体,转换字节顺序要用到htons等函数,这些都是socket编程的基础,在此并不进行讨论。5、实验在本机和机房都进行了实验,实验多次,第一次传送了两个100MB以上的文件,第二次传送了两个几十MB的RAR文件并在传送完毕后对数据进行了检验。以下是在本机上运行程序的截图。图1服务器图2客户端1图3客户端2我们看到,服务器的传输是并发的,如若不然,假设传输先后进行的,必然显示的是这样的结果:Client1请求文件传送完毕Client2请求文件传送完毕而实际显示的结果是:Client1请求Client2请求文件传送完毕文件传送完毕所以程序的传输是并发的。六、小结在小学期学习的过程中,作者进一步的实践了Linux下编程的技术,对Linux有了更深入的了解。在编写网络程序时,注意到了大家平时没有注意到的三个问题并提出了自己的解决方案。

1 / 9
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功