网络程序设计第3章.docx

上传人:b****3 文档编号:27224813 上传时间:2023-06-28 格式:DOCX 页数:53 大小:167.02KB
下载 相关 举报
网络程序设计第3章.docx_第1页
第1页 / 共53页
网络程序设计第3章.docx_第2页
第2页 / 共53页
网络程序设计第3章.docx_第3页
第3页 / 共53页
网络程序设计第3章.docx_第4页
第4页 / 共53页
网络程序设计第3章.docx_第5页
第5页 / 共53页
点击查看更多>>
下载资源
资源描述

网络程序设计第3章.docx

《网络程序设计第3章.docx》由会员分享,可在线阅读,更多相关《网络程序设计第3章.docx(53页珍藏版)》请在冰豆网上搜索。

网络程序设计第3章.docx

网络程序设计第3章

第3章TCP插口编程

 

3-1概述

上一章详细介绍了网络应用编程接口——插口API。

本章主要介绍基于插口API的TCP插口编程,即利用TCP插口访问TCP协议提供的服务来实现应用进程间的通信。

TCP是TCP/IP体系中的运输层协议,是面向连接的,因而可提供可靠的、按序传送数据的服务。

TCP提供的连接是双向的,即全双工的。

在介绍利用TCP插口编程之前,先简单介绍TCP协议中的一些关键运行机制。

对这些机制的了解有助于网络程序设计人员设计出良好的基于TCP的网络应用程序,并有助于对程序的调试。

然后,介绍TCP插口编程模式,并用一些具体的客户-服务器的例子来说明TCP插口编程过程。

本章还介绍了TCP插口程序对异常情况的处理方法,TCP带外数据的原理、发送和接收程序的设计。

3-2TCP协议机制

本节主要介绍TCP协议中的一些关键运行机制,包括:

TCP连接的建立和终止,TCP的有限状态机,TIME_WAIT状态,TCP的数据输出过程。

3-2-1TCP连接的建立和终止

TCP是面向连接的协议,连接的建立和释放是每一次面向连接的通信中必不可少的过程。

了解TCP连接的建立和终止过程对于编写基于TCP的网络应用程序是很有必要的。

TCP采用“三次握手(three-wayhandshake)”协议来建立连接,过程如下:

(1)假定主机B中运行一个服务器进程。

它首先向它的TCP协议层发出一个被动打开(passiveopen)命令(调用listen()),告诉它的TCP要准备好接受客户进程的连接请求。

然后服务器进程就处理于“监听”状态,不断检测是否有客户进程要发起连接请求。

如果有,即作出响应。

(2)假定主机A中运行着客户进程。

它先向其TCP协议层发出主动打开(activeopen)命令(调用connect()),表明要向某个IP地址的某个端口建立运输连接。

主机A的TCP向主机B的TCP发出连接请求报文段,其首部中的同步比特SYN应置为1,同时选择一个序号x,表明在后面传输数据时的第一个数据字节的序号是x。

(3)主机B的TCP收到连接请求报文段后,如同意,则发回确认。

在确认报文段中应将SYN置为1,确认序号应为x+1,同时也为自己选择一个序号y。

(4)主机A的TCP收到此报文段后,还要向B给出确认,其确认序号为y+1。

同时,运行客户进程的主机A的TCP协议层通知上层应用进程,连接已经建立。

(5)当运行服务器进程的主机B的TCP收到主机A的确认后,也通知其上层应用进程,连接已经建立。

当数据传输结束后,需要释放连接。

通信的双方均可以发出释放连接的请求。

具体过程如下(假定主机A的客户进程先向其TCP协议层发出连接释放请求,并且不再将数据传送给TCP协议层):

(1)主机A上的TCP协议层通知对方要释放从A到B这个方向的连接,将发往主机B的TCP报文段首部的终止比特FIN置1,其序号等于前面已传送过的数据的最后一个字节的序号加1。

(2)主机B的TCP协议层收到释放连接的通知后,即发出确认,其序号为u+1,同时通知上层的应用进程。

这时,从A到B的连接就释放了,连接处于半关闭(half-close)状态。

这种状态相当于主机A向主机B说:

“我已经没有数据要发送了,但你如果还发送数据,我仍可以接收”。

(3)此后,主机B不再接收主机A发来的数据。

但若主机B还有一些数据要发送主机A,则可继续发送。

主机A只要收到数据,仍应向主机B发送确认。

(4)当主机B再也没有数据可发送时,其应用进程就通知TCP协议层释放连接。

主机B发出连接释放报文段,除了将终止比特FIN置1,并使其序号v等于前面已传送过的数据的最后一个字节的序号加1,还必须重复上次已发送过的ACK=u+1。

主机A必须对此发出确认,给出ACK=v+1。

这样才将B到A的反方向连接释放掉。

主机A的TCP协议层再向其应用进程报告,整个连接已经全部释放。

上述过程如图3-1所示。

图中还标出了连接建立和关闭的各个阶段或状态下的插口函数调用及调用返回情况(用小写字母表示的插口函数名标在状态名的上面或下面,如客户方的ESTABLISHED状态下的“connect返回”)。

图3-1TCP的正常的连接建立和关闭

3-2-2TCP的有限状态机

网络通信中,一个健壮的插口应用程序必须能够处理网络通信中可能出现的各种状态。

因此,必须对TCP的有限状态机有所了解。

TCP连接从建立到终止整个过程中,存在11种状态,TCP的有限状态自动机(或称为状态转换图)给出了TCP连接从一个状态转换到另一个状态的规则,如图3-2所示。

在TCP的有限状态机图中,状态之间的箭头表示可能发生的状态变迁。

箭头旁边写上的字指明是状态变迁的原因或指明发生状态变迁后又出现什么动作。

粗线箭头表示对客户进程的正常状态转换,虚线粗箭头表示对服务器进程的正常状态转换,另一种细线箭头表示非正常状态转换。

图3-2TCP的有限状态机

下面,我们来分析一个从客户进程开始的状态转换过程。

假设一个主机的客户进程发起连接请求(主动打开),这时,客户进程所在的TCP实体就发送一个SYN置1的报文,因而由CLOSED状态进入SYN_SENT状态。

当收到来自进程的SYN和ACK时,TCP就发出三次握手中的最后一个ACK,接着就进入连接已经建立的状态ESTABLISHED。

这时就可以发送和接收数据了。

当应用进程结束数据传送时,就要释放已建立的连接。

我们还是假定客户进程首先发起断连请求(主动关闭),这时,客户进程的主机的本地TCP实体发送FIN置1的报文,等待确认ACK的到达,这时状态由ESTABLISHED变为FIN_WAIT_1。

当运行客户进程的主机收到确认ACK时,则一个方向的连接已经关闭了,状态转换到FIN_WAIT_2。

当运行客户进程的主机收到运行服务器的主机发送的FIN置1的报文后,应响应确认ACK。

收到确认ACK后,连接不是立即进入原来的CLOSED状态而是进入TIME_WAIT状态。

TCP还要在TIME_WAIT状态等待一段时间(此时间取为报文段在网络中的寿命的两倍,即2MSL),才删除原来建立的连接记录,返回到初始的CLOSED状态。

这样做的目的是为了保证原来连接上的所有分组都从网络中消失了(下一节将详细讨论TIME_WAIT状态的用途)。

对状态图的正确理解有助于网络编程人员理解插口中的一些调用,如connect,accept,close等函数。

另外,调试客户-服务器式的网络应用程序的一个有用的操作系统命令netstat中列出的连接状态名就是状态图中所列出的11种状态。

本章的3-3-3节将详细讨论用netstat显示的连接状态变化情况。

3-2-3TIME_WAIT状态

从图3-2中我们可以看出有一个TIME_WAIT状态。

为什么需要这么一个状态,而不是直接进入CLOSED状态呢?

主要有两个原因:

(1)实现终止TCP全双工连接的可靠性。

(2)允许老的重复的TCP报文段在网络中消失。

对于第

(1)个原因,解释如下:

参考图3-1,假设图中最后一个分组(ACK=v+1)在网络传输中丢失了,会发生什么情况。

客户进程已经释放了连接,再没有数据或控制信息要发送,但是服务器进程并不知道客户进程是否已正确地接收到了所有的数据,最后的ACK就是用来指示这个的。

因此,最后的ACK丢失的结果就是:

服务器进程将重发它的未被应答的分组(FIN,SEQ=v,ACK=u+1)。

如果客户进程直接进入CLOSED状态,则服务器将得到一个RST响应,服务器将它解释为错误。

从而导致TCP不能保证彻底终止某个连接上两个方向的数据流(全双工关闭)。

而有了TIME_WAIT状态后,客户进程就可以重发最后的ACK,从而保证连接的全双工释放。

对于第

(2)个原因,主要是防止来自某个连接的老的重复分组在连接终止后再出现,从而被误认为属于同一连接的化身(incarnation)。

可能出现这种情况的前提条件是:

同一IP地址和端口向另一IP地址和端口发起建立同样的连接。

要实现这一功能,TCP不能给处于TIME_WAIT状态的连接启动新的化身。

因为TIME_WAIT状态的持续时间是2MSL(最长分组生命期,maximumsegmentlifetime),这就足够让某个方向上的分组最多存活MSL秒即被丢弃,同时另一方向上的应答也最多存活MSL秒后被丢弃。

通过这一规则,TCP就能保证当成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消失。

每一个TCP实现都必须选择一个MSL值。

RFC1122的建议值是2分钟,而源自Berkeley的实现传统上使用的值为30秒。

这意味着TIME_WAIT状态的延迟在1分钟到4分钟之间。

3-2-4TCP的数据输出过程

本节介绍TCP的数据输出过程。

我们以阻塞方式为例来说明这一过程,如图3-3所示。

因为TCP提供的是可靠的数据传输服务,因此,每一个插口设置了一个发送缓存,我们可以用SO_SNDBUF插口选项来改变发送缓存的大小。

当应用进程调用write时,内核从应用进程的缓存中拷贝所有数据到插口的发送缓存。

如果可用的插口缓存大小小于应用进程要发送的数据的大小,应用进程将被挂起,直到插口缓存可以完全保存整个应用进程的数据。

因此,写一个TCP插口的write的成功返回仅仅表示可以重新使用应用进程的缓存,它并不能说明发送的数据已被对方的TCP或应用进程收到。

一旦插口层将应用进程的数据成功拷贝到插口缓存中,TCP就取插口发送缓存中的数据,将数据放入TCP的协议数据单元(PDU),然后经IP层(转换成IP层的PDU)、数据链路层(转换成数据链路层的PDU)、物理层(比特流)到达对方的物理层、数据链路屋、IP层,最后到达对方的TCP层。

如果在非阻塞方式下,且插口缓存中没有可用空间,write调用将直接返回,并返回相应的错误代码指示没有可用缓存;如果有部分缓存,则返回实际可用的缓存大小。

余下过程与阻塞过程是一样的。

我们将在3-3-4节中非阻塞方式下的客户程序的运行结果中观察到这一特征。

图3-3TCP的数据输出过程

3-3基本TCP插口编程

3-3-1TCP插口编程模式

TCP应用程序采用客户-服务器模式。

因为TCP提供的是面向连接的数据传输服务,因此,客户与服务器在通信之前需要建立连接。

在数据传送完成之后,需要关闭连接,释放网络资源。

一般来说,客户方要做的工作按时间顺序如下:

(1)打开一个插口(socket)。

(2)发起连接请求(connect)。

(3)如果连接成功,则进行数据交换(read,write,send,recv等)。

(4)数据交换完成,关闭连接(shutdow,close)。

而服务器方要做的工作则如下:

(1)打开一个插口(socket)。

(2)将插口绑定到服务器地址上(bind)。

(3)指定插口为服务器插口(listen),作好接收连接请求准备。

(4)等待连接请求(accept)。

(5)如果连接请求到则连接建立,进行数据交换(read,write,send,recv等)。

(6)数据交换完成,关闭连接(shutdown,close)。

如果服务器方支持多个客户,则在收到一个连接请求并建立连接后,需要产生一个客户服务子进程或线程服务刚建立连接的客户(数据交换,请求处理),而主程序仍需继续等待新的连接请求到达。

本章只介绍服务器只服务一个客户的情况,多客户支持服务器将在第6章中介绍。

上述过程以及每一步骤中用到的插口函数如图3-4所示。

我们在第2章中曾提到过,可以用一个五元组来标志一个通信过程,这个五元组是:

本地IP地址、本地端口、使用协议、远程IP地址、远程端口。

对于TCP通信而言,当连接建立完成,上述五元组即形成。

下面我们来说明,上述过程是如何建立这个五元组的,如表3-1所示。

图3-4只显示了一个简单的客户-服务器的处理过程。

实际的网络应用程序可能要比此过程复杂得多。

如,需要改变插口行为时需要调用setsockopt(),需要获取插口信息时需要调用getsockopt()等等,最多时可能要用到第2章中介绍的所有与插口编程有关的函数。

下面,我们就从这个最简单的客户-服务器模式开始介绍如何实现利用TCP的通信。

表3-1标志TCP连接的五元组的建立过程

客户

服务器

五元组项目

说明

socket

socket

<使用协议>

bind

bind

<本地IP地址,本地端口>

如果客户方没有调用bind(),或调用了bind()但没有指定具体地址或端口,则其IP地址和端口由内核自动分配

(续表3-1)

客户

服务器

五元组项目

说明

connect

accept

<远程IP地址,远程端口>

至此,五元组建立成功。

客户方的connect在接收到三次握手过程中的第二个报文段后就返回,而服务器方的accept在接收到三次握手过程中的第三个报文段才返回

图3-4基本TCP客户-服务器程序的工作过程

3-3-2实例

下面的例子完成的主要功能是测试两台主机间TCP的性能指标之一:

回程时延(RTT:

RoundTripTime)。

其中,服务器完成的主要功能是:

从客户端接收数据,并立即将接收到的数据原样返回给客户方。

客户端的功能是:

往服务器端发送数据,然后立即接收从服务器原样返回的数据。

每一种数据大小测试50次,取平均回程时延值。

共测试5种数据大小,RTT的值由客户端计算。

首先给出的是头文件common.h的代码,客户和服务器程序均需要包含这个头文件。

程序3-1公共头文件common.h。

#include#include#include#include#include#include#include#include#include#include

#include

#defineSERVER_PORT2090/*服务器端口号*/

#defineMAXBUFSIZE(1024*100)/*发送和接收缓存的最大长度*/

下面给出客户端的程序。

程序3-2客户端程序tcp_client.c。

1#include"common.h"23charbuff[MAXBUFSIZE];/*发送和接收缓存*/

4intsend_size[5]={10,100,1000,10000,100000};/*发送数据大小*/

5

6main(argc,argv)

7intargc;

8char*argv[];

9{

10intsockfd=0;/*插口描述符*/

11structsockaddr_inserveraddr;/*服务器地址*/

12intmode=0;/*阻塞模式*/

13intrcount=0;/*接收次数*/

14intslen,rlen,len;/*发送和接收长度变量*/

15intrdt;/*记录RoundTrip时间*/

16inti,j;/*循环变量*/

17structtimevaltv1,tv2;/*时间结构变量*/

18

19if(argc<2)

20{

21printf("使用方法:

tcp_client\n");

22exit(0);

23}

24

25for(;;)/*循环等待连接成功*/

26{

27/*产生TCP插口*/

28if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)

29{

30printf("产生插口失败,退出\n");

31exit

(1);

32}

33

34/*填写服务器地址结构*/

35bzero((char*)&serveraddr,sizeof(structsockaddr_in));

36serveraddr.sin_family=AF_INET;

37serveraddr.sin_port=htons(SERVER_PORT);

38inet_aton(argv[1],&serveraddr.sin_addr);

39

40/*发起连接请求*/

41if(connect(sockfd,&serveraddr,sizeof(serveraddr))<0)

42{

43printf("连接请求失败:

errno=%d\n",errno);

44close(sockfd);/*重新建连,必须重新产生新插口,关闭原插口*/

45}

46elsebreak;

47}

48

49ioctl(sockfd,FIONBIO,&mode);/*将插口置成阻塞模式*/

50

51for(i=0;i<5;i++)/*测试不同大小的报文的RoundTrip时间*/

52{

53slen=send_size[i];/*本次测试数据大小*/

54rcount=0;

55

56gettimeofday(&tv1,(structtimezone*)0);

57

58for(j=0;j<50;j++)/*每一种大小测试50次*/

59{

60if(send(sockfd,buff,slen,0)<=0)

61{

62printf("接收失败,程序退出,errno=%d\n",errno);

63close(sockfd);

64exit

(1);

65}

66len=slen;

67for(;;)/*接收数据*/

68{

69rlen=recv(sockfd,buff,len,0);

70if(rlen<=0)

71{

72printf("接收失败,程序退出,errno=%d\n",errno);

73close(sockfd);

74exit

(2);

75}

76len=len-rlen;

77rcount++;/*接收次数加1*/

78if(len==0)break;/*本次接收完毕,进行下一次发送*/

79}

80}

81gettimeofday(&tv2,(structtimezone*)0);

82rdt=((tv2.tv_sec-tv1.tv_sec)*1000000+tv2.tv_usec-tv1.tv_usec)/50;

83printf("报文大小=%d,RoundTripTime=%d微秒,发送次数=50,

84接收次数=%d\n",slen,rdt,rcount);

85sleep(3);

86}

87close(sockfd);

88}

第3~4行声明发送和接收数据用的缓存buff[]和定义发送数据大小的数组变量send_size[]。

发送大小的单位为字节。

第10~17行声明并初始化内部变量。

其中,slen指的是测试数据大小,它等于数组send_size[]的某一单元值。

rlen接收函数recv()的返回值。

len为函数recv()的第三个参数,指定每次调用recv()时要接收的数据的大小。

变量i,j为循环变量。

客户端程序共有两重循环,第一重循环的功能是循环测试不同数据大小下的RTT值,而第二重循环的功能是重复测试(50次)某一数据大小下的RTT值。

i为第一重循环变量,j为第二重循环变量。

第25~47行创建插口,并发起与服务器的连接请求。

第28行创建一个TCP插口,如果创建失败则程序退出。

第35到38行填写服务器地址结构变量,此处要注意的是服务器的IP地址和端口号要用网络字节顺序。

第41行发起连接请求,如果请求失败(如服务器未启动),则需要关闭插口,回到循环的开始。

不可以对连接失败的插口重新调用connect()发起连接请求。

如果连接成功,则退出建连循环。

第49行第49行的功能是将插口置为阻塞模式(mode等于0)。

实际上,默认的插口模式即为阻塞模式。

第51~58行第51行为第一重循环的开始。

第58行为第二重循环的开始。

在第二重循环开始之前需要调用gettimeofday来获取当前系统时间,在第二重循环结束后再次调用gettimeofday来获取当时的系统时间,两个时间值的差值除以次数(50)即为平均RTT值。

第67~79行这段代码的主要功能是接收指定大小(slen)的数据。

为什么要用一个循环来接收长度为slen的数据呢?

这是因为TCP是一个面向字节流的协议,报文之间没有边界定义。

一端发送的长度为slen的数据,另一端可能需要调用recv多次才能接收完成;或一端发送的多个报文,另一端只需调用recv一次即可接收完成。

在本例中,当传送的数据比较小时,一般调用recv一次,但当数据比较大时,就需要调用多次才能完成。

变量rcount中记录的就是总的接收次数。

第85行调用sleep(3)的目的是为了用netstat观察TCP连接的状态。

在实际的性能测试程序中不需要如此。

下面给出服务器程序tcp_server.c。

程序3-3服务器程序tcp_server.c。

1#include"common.h"

2

3charbuff[MAXBUFSIZE];

4

5main(argc,argv)

6intargc;

7char*argv[];

8{

9intsockfd;/*监听插口描述符*/

10intnewsockfd;/*连接插口描述符*/

11structsockaddr_inserveraddr;/*服务器地址*/

12structsockaddr_inclientaddr;/*客户方地址*/

13intclientaddrlen=sizeof(clientaddr);

14structhostent*he;

15intlen;

16intmode=0;/*阻塞模式*/

17

18/*产生TCP插口*/

19if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)

20{

21printf("产生插口失败,退出\n");

22exit

(1);

23}

24

25/*设置服务器地址结构*/

26bzero((char*)&serveraddr,sizeof(structsockaddr_in));

27serveraddr.sin_family=AF_INET;

28

29/*由系统自动绑定一个IP地址*/

30

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 人文社科 > 广告传媒

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1