当前位置: 首页 > news >正文

做的网站有广告图片网上接做网站的单子

做的网站有广告图片,网上接做网站的单子,网站开发技术培训,专业的医疗网站建设18.1 引言 TCP是一个面向连接的协议。无论哪一方向另一方发送数据之前#xff0c;都必须先在双方之间建立一条连接。本章将详细讨论一个TCP连接是如何建立的以及通信结束后是如何终止的。 这种两端间连接的建立与无连接协议如UDP不同。我们在第11章看到一端使用UDP向另一端发…18.1 引言 TCP是一个面向连接的协议。无论哪一方向另一方发送数据之前都必须先在双方之间建立一条连接。本章将详细讨论一个TCP连接是如何建立的以及通信结束后是如何终止的。 这种两端间连接的建立与无连接协议如UDP不同。我们在第11章看到一端使用UDP向另一端发送数据报时无需任何预先的握手。 18.2 连接的建立与终止 为了了解一个TCP连接在建立及终止时发生了什么我们在系统svr4上键入下列命令 telnet命令在与丢弃(discard)服务参见1.12节对应的端口上与主机bsdi建立一条TCP连接。这服务类型正是我们需要观察的一条连接建立与终止的服务类型而不需要服务器发起任何数据交换。 18.2.1tcpdump的输出 图18-1显示了这条命令产生TCP报文段的tcpdump输出。 这7个TCP报文段仅包含TCP首部。没有任何数据。 对于TCP段每个输出行开始按如下格式显示 源目的:标志 这里的标志代表TCP首部图17-2中6个标志比特中的4个。图18-2显示了表示标志的5个字符的含义。 在这个例子中我们看到了S、F和句点“.”标志符。我们将在以后看到其他的两个标志R和P。TCP首部中的其他两个标志比特—ACK和URG—tcpdump将作特殊显示。 图18-2所示的4个标志比特中的多个可能同时出现在一个报文段中但通常一次只见到一个。 在第1行中字段1415531521:1415531521(0)表示分组的序号是1415531521而报文段中数据字节数为0。tcpdump显示这个字段的格式是开始的序号、一个冒号、隐含的结尾序号及圆括号内的数据字节数。显示序号和隐含结尾序号的优点是便于了解数据字节数大于0时的隐含结尾序号。这个字段只有在满足条件1报文段中至少包含一个数据字节或者2SYN、FIN或RST被设置为1时才显示。图18-1中的第1、2、4和6行是因为标志比特被置为1而显示这个字段的在这个例子中通信双方没有交换任何数据。 在第2行中字段ack1415531522表示确认序号。它只有在首部中的ACK标志比特被设置1时才显示。 每行显示的字段win4096表示发端通告的窗口大小。在这些例子中我们没有交换任何数据窗口大小就维持默认情况下的4096我们将在20.4节中讨论TCP窗口大小。 图18-1中的最后一个字段表示由发端指明的最大报文段长度选项。发端将不接收超过这个长度的TCP报文段。这通常是为了避免分段见11.5节。我们将在18.4节讨论最大报文段长度而在18.10节介绍不同TCP选项的格式。 18.2.2 时间系列 图18-3显示了这些分组序列的时间系列在图6-11中已经首次介绍了这些时间系列的一些基本特性。这个图显示出哪一端正在发送分组。我们也将对tcpdump输出作一些扩展例如印出SYN而不是S。在这个时间系列中也省略窗口大小的值因为它和我们的讨论无关。 18.2.3 建立连接协议 现在让我们回到图18-3所示的TCP协议中来。为了建立一条TCP连接 请求端通常称为客户发送一个SYN段指明客户打算连接的服务器的端口以及初始序号ISN在这个例子中为1415531521。这个SYN段为报文段1。服务器发回包含服务器的初始序号的SYN报文段报文段2作为应答。同时将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认报文段3。 这三个报文段完成连接的建立。这个过程也称为三次握手three-way handshake。 发送第一个SYN的一端将执行主动打开active open。接收这个SYN并发回下一个SYN的另一端执行被动打开passive open在18.8节我们将介绍双方如何都执行主动打开。 当一端为建立连接而发送它的SYN时它为连接选择一个初始序号。ISN随时间而变化因此每个连接都将具有不同的ISN。RFC 793[Postel 1981c]指出ISN可看作是一个32比特的计数器每4ms加1。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送而导致某个连接的一方对它作错误的解释。 报文段3与报文段4之间4.1秒的时间间隔是建立TCP连接到向telnet键入quit命令来中止该连接的时间。 18.2.4 连接终止协议 建立一个连接需要三次握手而终止一个连接要经过4次握手。这由TCP的半关闭halfclose造成的。既然一个TCP连接是全双工即数据在两个方向上能同时传递因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。 收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的尽管在实际应用中只有很少的TCP应用程序这样做。正常关闭过程如图18-3所示。我们将在18.5节中详细介绍半关闭。 首先进行关闭的一方即发送第一个FIN将执行主动关闭而另一方收到这个FIN执行被动关闭。通常一方完成主动关闭而另一方完成被动关闭但我们将在18.9节看到双方如何都执行主动关闭。 图18-3中的报文段4发起终止连接它由Telnet客户端关闭连接时发出。这在我们键入quit命令后发生。它将导致TCP客户端发送一个FIN用来关闭从客户到服务器的数据传送。 当服务器收到这个FIN它发回一个ACK确认序号为收到的序号加1报文段5。和SYN一样一个FIN将占用一个序号。同时TCP服务器还向应用程序即丢弃服务器传送一个文件结束符。接着这个服务器程序就关闭它的连接导致它的TCP端发送一个FIN报文段6客户必须发回一个确认并将确认序号设置为收到序号加1报文段7。 图18-4显示了终止一个连接的典型握手顺序。我们省略了序号。在这个图中发送FIN将导致应用程序关闭它们的连接这些FIN的ACK是由TCP软件自动产生的。 连接通常是由客户端发起的这样第一个SYN从客户传到服务器。每一端都能主动关闭这个连接即首先发送FIN。然而一般由客户端决定何时终止连接因为客户进程通常由用户交互控制用户会键入诸如“quit”一样的命令来终止进程。在图18-4中我们能改变上边的标识将左方定为服务器右方定为客户一切仍将像显示的一样工作例如在14.4节中的第一个例子中就是由daytime服务器关闭连接的。 18.2.5 正常的tcpdump输出 对所有的数值很大的序号进行排序是很麻烦的因此默认情况下tcpdump只在显示SYN报文段时显示完整的序号而对其后的序号则显示它们与初始序号的相对偏移值为了得到图18-1的输出显示必须加上-S选项。对应于图18-1的正常tcpdump显示如图18-5所示 除非我们需要显示完整的序号否则将在以下的例子中使用这种形式的输出显示。 18.3 连接建立的超时 有很多情况导致无法建立连接。一种情况是服务器主机没有处于正常状态。为了模拟这种情况我们断开服务器主机的电缆线然后向它发出telnet命令。图18-6显示了tcpdump的输出。 在这个输出中有趣的一点是客户间隔多长时间发送一个SYN试图建立连接。第2个SYN与第1个的间隔是5.8秒而第3个与第2个的间隔是24秒。 图18-6中没有显示客户端在放弃建立连接尝试前进行SYN重传的时间。为了了解它我们必须对telnet命令进行计时 时间差值是76秒。大多数伯克利系统将建立一个新连接的最长时间限制为75秒。我们将在21.4节看到由客户发出的第3个分组大约在16:25:29超时客户在它第3个分组发出后48秒而不是75秒后放弃连接。 18.3.1 第一次超时时间 在图18-6中一个令人困惑的问题是第一次超时时间为5.8秒接近6秒但不准确相比之下第二个超时时间几乎准确地为24秒。运行十多次测试发现第一次超时时间在5.59秒~5.93秒之间变化。然而第二次超时时间则总是24.00秒精确到小数点后面两位。 这是因为BSD版的TCP软件采用一种500 ms的定时器。这种500 ms的定时器用于确定本章中所有的各种各样的TCP超时。当我们键入telnet命令将建立一个6秒的定时器12个时钟滴答tick但它可能在之后的5.5秒6秒内的任意时刻超时。图18-7显示了这一发生过程。尽管定时器初始化为12个时钟滴答但定时计数器会在设置后的第一个0~500 ms中的任意时秒刻减1。从那以后定时计数器大约每隔500 ms减1但在第1个500 ms内是可变的我们使用限定词“大约”是因为在TCP每隔500 ms获得系统控制的瞬间系统内核可能会优先处理其他中断。 当滴答计数器为0时6秒的定时器便会超时见图18-7这个定时器会在以后的24秒48个滴答重新复位。之后的下一个定时器将更接近24秒因为当TCP的500 ms定时器被内核调用时它就会被修改一次。 18.3.2 服务类型字段 在图18-6中出现了符号 [tos 0x10]。这是IP数据报内的服务类型TOS字段参见图3-2。BSD/386中的Telnet客户进程将这个字段设置为最小时延。 18.4 最大报文段长度 最大报文段长度MSS表示TCP传往另一端的最大块数据的长度。当一个连接建立时连接的双方都要通告各自的MSS。我们已经见过MSS都是1024。这导致IP数据报通常是40字节长20字节的TCP首部和20字节的IP首部。 在有些书中将它看作可“协商”选项。它并不是任何条件下都可协商。当建立一个连接时每一方都有用于通告它期望接收的MSS选项MSS选项只能出现在SYN报文段中。如果一方不接收来自另一方的MSS值则MSS就定为默认值536字节这个默认值允许20字节的IP首部和20字节的TCP首部以适合576字节IP数据报)。 一般说来如果没有分段发生MSS还是越大越好这也并不总是正确参见图24-3和图24-4中的例子。报文段越大允许每个报文段传送的数据就越多相对IP和TCP首部有更高的网络利用率。当TCP发送一个SYN时或者是因为一个本地应用进程想发起一个连接或者是因为另一端的主机收到了一个连接请求它能将MSS值设置为外出接口上的MTU长度减去固定的IP首部和TCP首部长度。对于一个以太网MSS值可达1460字节。使用IEEE 802.3的封装参见2.2节它的MSS可达1452字节。 在本章见到的涉及BSD/386和SVR4的MSS为1024这是因为许多BSD的实现版本需要MSS为512的倍数。其他的系统如SunOS 4.1.3、Solaris 2.2和AIX 3.2.2当双方都在一个本地以太网上时都规定MSS为1460。[Mogul 1993] 的比较显示了在以太网上1460的MSS在性能上比1024的MSS更好。 如果目的IP地址为“非本地的(nonlocal)”MSS通常的默认值为536。而区分地址是本地还是非本地是简单的如果目的IP地址的网络号与子网号都和我们的相同则是本地的如果目的IP地址的网络号与我们的完全不同则是非本地的如果目的IP地址的网络号与我们的相同而子网号与我们的不同则可能是本地的也可能是非本地的。大多数TCP实现版都提供了一个配置选项附录E和图E-1让系统管理员说明不同的子网是属于本地还是非本地。这个选项的设置将确定MSS可以选择尽可能的大达到外出接口的MTU长度或是默认值536。 MSS让主机限制另一端发送数据报的长度。加上主机也能控制它发送数据报的长度这将使以较小MTU连接到一个网络上的主机避免分段。 考虑我们的主机slip通过MTU为296的SLIP链路连接到路由器bsdi上。图18-8显示这些系统和主机sun。 从sun向slip发起一个TCP连接并使用tcpdump来观察报文段。图18-9显示这个连接的建立省略了通告窗口大小。 在这个例子中sun发送的报文段不能超过256字节的数据因为它收到的MSS选项值为256第2行。此外由于slip知道它外出接口的MTU长度为296即使sun已经通告它的MSS为1460但为避免将数据分段它不会发送超过256字节数据的报文段。系统允许发送的数据长度小于另一端的MSS值。 只有当一端的主机以小于576字节的MTU直接连接到一个网络中避免这种分段才会有效。如果两端的主机都连接到以太网上都采用536的MSS但中间网络采用296的MTU也将会出现分段。使用路径上的MTU发现机制参见24.2节是关于这个问题的唯一方法。 18.5 TCP的半关闭 TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。这就是所谓的半关闭。正如我们早些时候提到的只有很少的应用程序使用它。 为了使用这个特性编程接口必须为应用程序提供一种方式来说明“我已经完成了数据传送因此发送一个文件结束FIN给另一端但我还想接收另一端发来的数据直到它给我发来文件结束FIN”。 如果应用程序不调用close而调用shutdown且第2个参数值为1则插口的API支持半关闭。然而大多数的应用程序通过调用close终止两个方向的连接。 图18-10显示了一个半关闭的典型例子。让左方的客户端开始半关闭当然也可以由另一端开始。开始的两个报文段和图18-4是相同的初始端发出的FIN接着是另一端对这个FIN的ACK报文段。但后面就和图18-4不同因为接收半关闭的一方仍能发送数据。我们只显示一个数据报文段和一个ACK报文段但可能发送了许多数据报文段将在第19章讨论数据报文段和确认报文段的交换。当收到半关闭的一端在完成它的数据传送后将发送一个FIN关闭这个方向的连接这将传送一个文件结束符给发起这个半关闭的应用进程。当对第二个FIN进行确认后这个连接便彻底关闭了。 为什么要有半关闭一个例子是Unix中的rsh(1)命令它将完成在另一个系统上执行一个命令。 命令 将在主机bsdi上执行sort排序命令rsh命令的标准输入来自文件datafile。rsh将在它与在另一主机上执行的程序间建立一个TCP连接。rsh的操作很简单它将标准输入datafile复制给TCP连接并将结果从TCP连接中复制给标准输出我们的终端。图18-11显示了这个建立过程牢记TCP连接是全双工的。 在远端主机bsdi上rshd服务器将执行sort程序它的标准输入和标准输出都是TCP连接。第14章的[Stevens 1990]详细介绍了有关Unix进程的结构但这儿涉及的是使用TCP连接以及需要使用TCP的半关闭。 sort程序只有读取到所有输入数据后才能产生输出。所有的原始数据通过TCP连接从rsh客户端传送到sort服务器进行排序。当输入datafile到达文件尾时rsh客户端执行这个TCP连接的半关闭。接着sort服务器在它的标准输入这个TCP连接上收到一个文件结束符对数据进行排序并将结果写在它的标准输出上TCP连接。rsh客户端继续接收来自TCP连接另一端的数据并将排序的文件复制到它的标准输出上。 没有半关闭需要其他的一些技术让客户通知服务器客户端已经完成了它的数据传送但仍要接收来自服务器的数据。使用两个TCP连接也可作为一个选择但使用半关闭的单连接更好。 18.6 TCP的状态变迁图 我们已经介绍了许多有关发起和终止TCP连接的规则。这些规则都能从图18-12所示的状态变迁图中得出。 在这个图中要注意的第一点是一个状态变迁的子集是“典型的”。我们用粗的实线箭头表示正常的客户端状态变迁用粗的虚线箭头表示正常的服务器状态变迁。 第二点是两个导致进入ESTABLISH-ED状态的变迁对应打开一个连接而两个导致从ESTABLISHED状态离开的变迁对应关闭一个连接。ESTABLISHED状态是连接双方能够进行双向数据传递的状态。以后的章节将介绍这个状态。 将图中左下角4个状态放在一个虚线框内并标为“主动关闭”。其他两个状态(CLOSE_WAIT和LAST_ACK)也用虚线框住并标为“被动关闭”。 这个图中11个状态的名称关闭) (CLOSEDLISTENSYN_SENT等)是有意与netstat命令显示的状态名称一致。netstat对状态的命名几乎与在RFC793中的最初描述一致。CLOSED状态不是一个真正的状态而是这个状态图的假想起点和终点。 从LISTEN到SYN_SENT的变迁是正确的但伯克利版的TCP软件并不支持它。 只有当SYN_RCVD状态是从LISTEN状态正常情况进入而不是从SYN_SENT状态同时打开进入时从SYN_RCVD回到LISTEN的状态变迁才是有效的。这意味着如果我们执行被动关闭进入LISTEN收到一个SYN发送一个带ACK的SYN进入SYN_RCVD然后收到一个RST而不是一个ACK便又回到LISTEN状态并等待另一个连接请求的到来。 图18-13显示了在正常的TCP连接的建立与终止过程中客户与服务器所经历的不同状态。它是图18-3的再现不同的是仅显示了一些状态。 假定在图18-13中左边的客户执行主动打开而右边的服务器执行被动打开。尽管图中显示出由客户端执行主动关闭但和早前我们提到的一样另一端也能执行主动关闭。 可以使用图18-12的状态图来跟踪图18-13的状态变化过程以便明白每个状态的变化。 18.6.1 2MSL等待状态 TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSLMaximum Segment Lifetime。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的因为TCP报文段以IP数据报在网络内传输而IP数据报则有限制其生存时间的TTL字段。 RFC 793 [Postel 1981c]指出MSL为2分钟。然而实现中的常用值是30秒1分钟或2分钟。 从第8章我们知道在实际应用中对IP数据报TTL的限制是基于跳数而不是定时器。 对一个具体实现所给定的MSL值处理的原则是当TCP执行一个主动关闭并发回最后一个ACK该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失另一端超时并重发最后的FIN。 这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间定义这个连接的插口客户的IP地址和端口号服务器的IP地址和端口号不能再被使用。这个连接只能在2MSL结束后才能再被使用。 遗憾的是大多数TCP实现如伯克利版强加了更为严格的限制。在2MSL等待期间插口中使用的本地端口在默认情况下不能再被使用。我们将在下面看到这个限制的例子。 某些实现和API提供了一种避开这个限制的方法。使用插口API时可说明其中的SO_REUSEADDR选项。它将让调用者对处于2MSL等待的本地端口进行赋值但我们将看到TCP原则上仍将避免使用仍处于2MSL连接中的端口。 在连接处于2MSL等待时任何迟到的报文段将被丢弃。因为处于2MSL等待的、由该插口对(socket pair)定义的连接在这段时间内不能被再用因此当要建立一个有效的连接时来自该连接的一个较早替身incarnation的迟到报文段作为新连接的一部分不可能不被曲解一个连接由一个插口对来定义。一个连接的新的实例instance称为该连接的替身。 我们说图18-13中客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭不会进入TIME_WAIT状态。这暗示如果我们终止一个客户程序并立即重新启动这个客户程序则这个新客户程序将不能重用相同的本地端口。这不会带来什么问题因为客户使用本地端口而并不关心这个端口号是什么。 然而对于服务器情况就有所不同因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序并试图立即重新启动这个服务器程序服务器程序将不能把它的这个熟知端口赋值给它的端点因为那个端口是处于2MSL连接的一部分。在重新启动服务器程序前它需要在1~4分钟。 可以通过sock程序看到这一切。我们启动服务器程序从一个客户程序进行连接然后停止这个服务器程序。 当重新启动服务器程序时程序报告一个差错信息说明不能绑定它的熟知端口因为该端口已被使用即它处于2MSL等待。 运行netstat程序来查看连接的状态以证实它的确处于2MSL等待状态。 如果我们一直试图重新启动服务器程序并测量它直到成功所需的时间我们就能确定出2MSL值。对于SunOS 4.1.3、SVR4、BSD/386和AIX 3.2.2它需要1分钟才能重新启动服务器程序这意味着它们的MSL值为30秒。而对于Solaris 2.2它需要4分钟才能重新启动服务器程序这表示它的MSL值为2分钟。 如果一个客户程序试图申请一个处于2MSL等待的端口客户程序通常不会这么做就会出现同样的差错。 我们在第1次执行客户程序时采用-v选项来查看它使用的本地端口为11 62。第2次执行客户程序时则采用-b选项来选择端口11 62为它的本地端口。正如我们所预料的那样客户程序无法那么做因为那个端口是一个还处于2MSL等待连接的一部分。 需要再次强调2MSL等待的一个效果因为我们将在第27章的文件传输协议FTP中遇到它。和以前介绍的一样一个插口对即包含本地IP地址、本地端口、远端IP地址和远端端口的4元组在它处于2MSL等待时将不能再被使用。尽管许多具体的实现中允许一个进程重新使用仍处于2MSL等待的端口通常是设置选项SO_REUSEADDR但TCP不能允许一个新的连接建立在相同的插口对上。可通过下面的试验来看到这一点 第1次运行sock程序中我们将它作为服务器程序端口号为6666并从主机bsdi上的一个客户程序与它连接这个客户程序使用的端口为1098。我们终止服务器程序因此它将执行主动关闭。这将导致4元组140.252.13.33本地IP地址、6666本地端口号、140.252.13.35另一端IP地址和1098另一端的端口号在服务器主机进入2MSL等待。 在第2次运行sock程序时我们将它作为客户程序并试图将它的本地端口号指明为6666同时与主机bsdi在端口1098上进行连接。但这个程序在试图将它的本地端口号赋值为6666时产生了一个差错因为这个端口是处于2MSL等待4元组的一部分。 为了避免这个差错我们再次运行这个程序并使用选项-A来设置前面提到的SO_REUSEADDR。这将让sock程序能将它的本地端口号设置为6666但当我们试图进行主动打开时又出现了一个差错。即使它能将它的本地端口设置为6666但它仍不能和主机bsdi在端口1098上进行连接因为定义这个连接的插口对仍处于2MSL等待状态。 如果我们试图从其他主机来建立这个连接会如何首先我们必须在sun上以-A标记来重新启动服务器程序因为它需要的端口6666是还处于2MSL等待连接的一部分。 接着在2MSL等待结束前我们在bsdi上启动客户程序 不幸的是它成功了这违反了TCP规范但被大多数的伯克利版实现所支持。这些实现允许一个新的连接请求到达仍处于TIME_WAIT状态的连接只要新的序号大于该连接前一个替身的最后序号。在这个例子中新替身的ISN被设置为前一个替身最后序号与128000的和。附录的RFC 1185 [Jacobsan、Braden和Zhang 1990]指出了这项技术仍可能存在缺陷。 对于同一连接的前一个替身这个具体实现中的特性让客户程序和服务器程序能连续地重用每一端的相同端口号但这只有在服务器执行主动关闭才有效。我们将在图27-8中使用FTP时看到这个2MSL等待条件的另一个例子。也见习题18.5。 18.6.2 平静时间的概念 对于来自某个连接的较早替身的迟到报文段2MSL等待可防止将它解释成使用相同插口对的新连接的一部分。但这只有在处于2MSL等待连接中的主机处于正常工作状态时才有效。 如果使用处于2MSL等待端口的主机出现故障它会在MSL秒内重新启动并立即使用故障前仍处于2MSL的插口对来建立一个新的连接吗如果是这样在故障前从这个连接发出而迟到的报文段会被错误地当作属于重启后新连接的报文段。无论如何选择重启后新连接的初始序号都会发生这种情况。 为了防止这种情况RFC 793指出TCP在重启动后的MSL秒内不能建立任何连接。这就称为平静时间(quiet time)。 只有极少的实现版遵守这一原则因为大多数主机重启动的时间都比MSL秒要长。 18.6.3 FIN_WAIT_2状态 在FIN_WAIT_2状态我们已经发出了FIN并且另一端也已对它进行确认。除非我们在实行半关闭否则将等待另一端的应用层意识到它已收到一个文件结束符说明并向我们发一个FIN来关闭另一方向的连接。只有当另一端的进程完成这个关闭我们这端才会从FIN_WAIT_2状态进入TIME_WAIT状态。 这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态并一直保持这个状态直到应用层决定进行关闭。 许多伯克利实现采用如下方式来防止这种在FIN_WAIT_2状态的无限等待。如果执行主动关闭的应用层将进行全关闭而不是半关闭来说明它还想接收数据就设置一个定时器。如果这个连接空闲10分钟75秒TCP将进入CLOSED状态。在实现代码的注释中确认这个实现代码违背协议的规范。 18.7 复位报文段 我们已经介绍了TCP首部中的RST比特是用于“复位”的。一般说来无论何时一个报文段发往基准的连接referenced connection出现错误TCP都会发出一个复位报文段这里提到的“基准的连接”是指由目的IP地址和目的端口号以及源IP地址和源端口号指明的连接。这就是为什么RFC 793称之为插口。 18.7.1 到不存在的端口的连接请求 产生复位的一种常见情况是当连接请求到达时目的端口没有进程正在听。对于UDP我们在6.5节看到这种情况当一个数据报到达目的端口时该端口没在使用它将产生一个ICMP端口不可达的信息。而TCP则使用复位。 产生这个例子也很容易我们可使用Telnet客户程序来指明一个目的端口没在使用的情况 Telnet客户程序会立即显示这个差错信息。图18-14显示了对应这个命令的分组交换过程。 在这个图中需要注意的值是复位报文段中的序号字段和确认序号字段。因为ACK比特在到达的报文段中没有被设置为1复位报文段中的序号被置为0确认序号被置为进入的ISN加上数据字节数。尽管在到达的报文段中没有真正的数据但SYN比特从逻辑上占用了1字节的序号空间因此在这个例子中复位报文段中确认序号被置为ISN与数据长度0、SYN比特所占的1的总和。 18.7.2 异常终止一个连接 我们在18.2节中看到终止一个连接的正常方式是一方发送FIN。有时这也称为有序释放orderly release因为在所有排队数据都已发送之后才发送FIN正常情况下没有任何数据丢失。但也有可能发送一个复位报文段而不是FIN来中途释放一个连接。有时称这为异常释放abortive release。 异常终止一个连接对应用程序来说有两个优点1丢弃任何待发数据并立即发送复位报文段2RST的接收方会区分另一端执行的是异常关闭还是正常关闭。应用程序使用的API必须提供产生异常关闭而不是正常关闭的手段。 使用sock程序能够观察这种异常关闭的过程。Socket API通过“linger on close”选项SO_LINGER提供了这种异常关闭的能力。我们加上-L选项并将停留时间设为0。这将导致连接关闭时进行复位而不是正常的FIN。我们连接到处于服务器上的sock程序并键入一输入行 图18-15是这个例子的tcpdump输出显示在这个图中我们已经删除了所有窗口大小的说明因为它们与讨论无关。 第1~3行显示出建立连接的正常过程。第4行发送我们键入的数据行12个字符和Unix换行符第5行是对收到数据的确认。 第6行对应为终止客户程序而键入的文件结束符Control_D。由于我们指明使用异常关闭而不是正常关闭命令行中的-L0选项因此主机bsdi端的TCP发送一个RST而不是通常的FIN。RST报文段中包含一个序号和确认序号。需要注意的是RST报文段不会导致另一端产生任何响应另一端根本不进行确认。收到RST的一方将终止该连接并通知应用层连接复位。 我们在服务器上得到下面的差错信息 这个服务器程序从网络中接收数据并将它接收的数据显示到其标准输出上。通常从它的TCP上收到文件结束符后便将结束但这里我们看到当收到RST时它产生了一个差错。这个差错正是我们所期待的连接被对方复位了。 18.7.3 检测半打开连接 如果一方已经关闭或异常终止连接而另一方却还不知道我们将这样的TCP连接称为半打开Half-Open的。任何一端的主机异常都可能导致发生这种情况。只要不打算在半打开连接上传输数据仍处于连接状态的一方就不会检测另一方已经出现异常。 半打开连接的另一个常见原因是当客户主机突然掉电而不是正常的结束客户应用程序后再关机。这可能发生在使用PC机作为Telnet的客户主机上例如用户在一天工作结束时关闭PC机的电源。当关闭PC机电源时如果已不再有要向服务器发送的数据服务器将永远不知道客户程序已经消失了。当用户在第二天到来时打开PC机并启动新的Telnet客户程序在服务器主机上会启动一个新的服务器程序。这样会导致服务器主机中产生许多半打开的TCP连接在第23章中我们将看到使用TCP的keepalive选项能使TCP的一端发现另一端已经消失。 能很容易地建立半打开连接。在bsdi上运行Telnet客户程序通过它和svr4上的丢弃服务器建立连接。我们键入一行字符然后通过tcpdump进行观察接着断开服务器主机与以太网的电缆并重启服务器主机。这可以模拟服务器主机出现异常在重启服务器之前断开以太网电缆是为了防止它向打开的连接发送FIN某些TCP在关机时会这么做。服务器主机重启后我们重新接上电缆并从客户向服务器发送另一行字符。由于服务器的TCP已经重新启动它将丢失复位前连接的所有信息因此它不知道数据报文段中提到的连接。TCP的处理原则是接收方以复位作为应答。图18-16是这个例子的tcpdump输出显示已从这个输出中删除了窗口大小的说明、服务类型信息和MSS声明因为它们与讨论无关。 图18-16是这个例子的tcpdump输出显示(已从这个输出中删除了窗口大小的说明、服务类型信息和MSS声明,因为它们与讨论无关)。 第1~3行是正常的连接建立过程。第4行向丢弃服务器发送字符行“hithere”第5行是确认。 然后是断开svr4的以太网电缆重新启动svr4并重新接上电缆。这个过程几乎需要190秒。接着从客户端输入下一行即“another line”当我们键入回车键后这一行被发往服务器图18-16的第6行。这导致服务器产生一个响应但要注意的是由于服务器主机经过重新启动它的ARP高速缓存为空因此需要一个ARP请求和应答第7、8行。第9行表示RST被发送出去。客户收到复位报文段后显示连接已被另一端的主机终止Telnet客户程序发出的最后信息不再有什么价值。 18.8 同时打开 两个应用程序同时彼此执行主动打开的情况是可能的尽管发生的可能性极小。每一方必须发送一个SYN且这些SYN必须传递给对方。这需要每一方使用一个对方熟知的端口作为本地端口。这又称为同时打开simultaneous open。 例如主机A中的一个应用程序使用本地端口7777并与主机B的端口8888执行主动打开。主机B中的应用程序则使用本地端口8888并与主机A的端口7777执行主动打开。 这与下面的情况不同主机A中的Telnet客户程序和主机B中Telnet的服务器程序建立连接与此同时主机B中的Telnet客户程序与主机A的Telnet服务器程序也建立连接。在这个Telnet例子中两个Telnet服务器都执行被动打开而不是主动打开并且Telnet客户选择的本地端口不是另一端Telnet服务器进程所熟悉的端口。 TCP是特意设计为了可以处理同时打开对于同时打开它仅建立一条连接而不是两条连接其他的协议族最突出的是OSI运输层在这种情况下将建立两条连接而不是一条连接。 当出现同时打开的情况时状态变迁与图18-13所示的不同。两端几乎在同时发送SYN并进入SYN_SENT状态。当每一端收到SYN时状态变为SYN_RCVD如图18-12同时它们都再发SYN并对收到的SYN进行确认。当双方都收到SYN及相应的ACK时状态都变迁为ESTABLISHED。图18-17显示了这些状态变迁过程。 一个同时打开的连接需要交换4个报文段比正常的三次握手多一个。此外要注意的是我们没有将任何一端称为客户或服务器因为每一端既是客户又是服务器。 一个例子 尽管很难但仍有可能产生一个同时打开的连接。两端必须几乎在同时启动以便收到彼此的SYN。只要两端有较长的往返时间就能保证这一点。这样我们将一端设置在主机bsdi上另一端则设置在主机vangogh.cs.berkeley.edu上。由于两端之间有一条拨号链路SLIP它的往返时间对保证双方同步收到SYN是足够长的几百毫秒。 一端bsdi将本地端口设置为8888使用命令行选项-b并对另一端主机端口7777执行主动打开。 另一端也几乎在同一时间将本地端口设置为7777并对端口8888执行主动打开。 我们指明带-v标志的sock程序来验证连接两端的IP地址和端口号。这个选项也显示每一端的MSS值。为证实两端确实在相互交谈我们在每一端还输入一行字符看它们是否会被送到另一端并显示出来。 图18-18显示了这个连接的段交换过程我们删除了出现在来自vangogh第一个SYN中的一些新的TCP选项因为vangogh使用4.4BSD系统。将在18.10节介绍这些较新的选项。注意两个SYN第12行后跟着两个带ACK的SYN第34行。它们将执行同时打开。 第5行显示了由bsdi发送给vangogh的输入行“hello,world”第6行对此进行确认。第78行对应另一方向的输入行“and hi there”和确认。第9~12行显示正常的连接关闭。 许多伯克利版的TCP实现都不能正确地支持同时打开。在这些系统中如果能够进行SYN的同步接收你将经历极多的报文段交换过程才能关闭它们。每个报文段交换过程包括每个方向上的一个SYN和一个ACK。图18-12中从SYN_SENT到状态SYN_RCVD的变迁在许多TCP实现中很少测试过。 18.9 同时关闭 我们在以前讨论过一方通常但不总是客户方发送第一个FIN执行主动关闭。双方都执行主动关闭也是可能的TCP协议也允许这样的同时关闭simultaneous close。 在图18-12中当应用层发出关闭命令时两端均从ESTABLISHED变为FIN_WAIT_1。这将导致双方各发送一个FIN两个FIN经过网络传送后分别到达另一端。收到FIN后状态由FIN_WAIT_1变迁到CLOSING并发送最后的ACK。当收到最后的ACK时状态变化为TIME_WAIT。图18-19总结了这些状态的变化。 同时关闭与正常关闭使用的段交换数目相同。 18.10 TCP选项 TCP首部可以包含选项部分图17-2。仅在最初的TCP规范中定义的选项是选项表结束、无操作和最大报文段长度。在我们的例子中几乎每个SYN报文段中我们都遇到过MSS选项。 新的RFC主要是RFC 1323 [JacobsonBraden和Borman 1992]定义了新的TCP选项这些选项的大多数只在最新的TCP实现中才能见到我们将在第24章介绍这些新选项。图18-20显示了当前TCP选项的格式这些选项的定义出自于RFC 793和RFC 1323。 每个选项的开始是1字节kind字段说明选项的类型。kind字段为0和1的选项仅占1个字节。其他的选项在kind字节后还有len字节。它说明的长度是指总长度包括kind字节和len字节。 设置无操作选项的原因在于允许发方填充字段为4字节的倍数。如果我们使用4.4BSD系统进行初始化TCP连接tcpdump将在初始的SYN上显示下面TCP选项 MSS选项设置为512后面是NOP接着是窗口扩大选项。第一个NOP用来将窗口扩大选项填充为4字节的边界。同样10字节的时间戳选项放在两个NOP后占12字节同时使两个4字节的时间戳满足4字节边界。 其他kind值为4、5、6和7的四个选项称为选择ACK及回显选项。由于回显选项已被时间戳选项取代而目前定义的选择ACK选项仍未定论并未包括在RFC 1323中因此图18-20没有将它们列出。另外作为TCP事务第24.7节的T/TCP建议也指明kind为11,12和13的三个选项。 18.11 TCP服务器的设计 我们在1.8节说过大多数的TCP服务器进程是并发的。当一个新的连接请求到达服务器时服务器接受这个请求并调用一个新进程来处理这个新的客户请求。不同的操作系统使用不同的技术来调用新的服务器进程。在Unix系统下常用的技术是使用fork函数来创建新的进程。如果系统支持也可使用轻型进程即线程thread。 我们感兴趣的是TCP与若干并发服务器的交互作用。需要回答下面的问题当一个服务器进程接受一来自客户进程的服务请求时是如何处理端口的如果多个连接请求几乎同时到达会发生什么情况 18.11.1 TCP服务器端口号 通过观察任何一个TCP服务器我们能了解TCP如何处理端口号。我们使用netstat命令来观察Telnet服务器。下面是在没有Telnet连接时的显示只留下显示Telnet服务器的行。 -a标志将显示网络中的所有主机端而不仅仅是处于ESTABLISHED的主机端。-n标志将以点分十进制的形式显示IP地址而不是通过DNS将地址转化为主机名同时还要求显示端口号例如为23而不是服务名称如Telnet。-f inet选项则仅要求显示使用TCP或UDP的主机。 显示的本地地址为*.23星号通常又称为通配符。这表示传入的连接请求即SYN将被任何一个本地接口所接收。如果该主机是多接口主机我们将指定其中的一个IP地址为本地IP地址并且只接收来自这个接口的连接在本节后面我们将看到这样的例子。本地端口为23这是Telnet的熟知端口号。 远端地址显示为*.*表示还不知道远端IP地址和端口号因为该端还处于LISTEN状态正等待连接请求的到达。 现在我们在主机slip140.252.13.65启动一个Telnet客户程序来连接这个Telnet服务器。以下是netstat程序的输出行 端口为23的第1行表示处于ESTABLISHED状态的连接。另外还显示了这个连接的本地IP地址、本地端口号、远端IP地址和远端端口号。本地IP地址为该连接请求到达的接口以太网接口140.252.13.33。 处于LISTEN状态的服务器进程仍然存在。这个服务器进程是当前Telnet服务器用于接收其他的连接请求。当传入的连接请求到达并被接收时系统内核中的TCP模块就创建一个处于ESTABLISHED状态的进程。另外注意处于ESTABLISHED状态的连接的端口不会变化也是23与处于LISTEN状态的进程相同。 现在我们在主机slip上启动另一个Telnet客户进程并仍与这个Telnet服务器进行连接。以下是netstat程序的输出行 现在我们有两条从相同主机到相同服务器的处于ESTABLISHED的连接。它们的本地端口号均为23。由于它们的远端端口号不同这不会造成冲突。因为每个Telnet客户进程要使用一个外设端口并且这个外设端口会选择为主机slip当前未曾使用的端口因此它们的端口号肯定不同。 这个例子再次重申TCP使用由本地地址和远端地址组成的4元组目的IP地址、目的端口号、源IP地址和源端口号来处理传入的多个连接请求。TCP仅通过目的端口号无法确定那个进程接收了一个连接请求。另外在三个使用端口23的进程中只有处于LISTEN的进程能够接收新的连接请求。处于ESTABLISHED的进程将不能接收SYN报文段而处于LISTEN的进程将不能接收数据报文段。 下面我们从主机solaris上启动第3个Telnet客户进程这个主机通过SLIP链路与主机sun相连而不是以太网接口。 现在第一个ESTABLISHED连接的本地IP地址对应多地址主机sun中的SLIP链路接口地址140.252.1.29。 18.11.2 限定的本地IP地址 我们来看看当服务器不能任选其本地IP地址而必须使用特定的IP地址时的情况。如果我们为sock程序指明一个IP地址或主机名并将它作为服务器那么该IP地址就成为处于LISTEN服务器的本地IP地址。例如 使这个服务器程序的连接仅局限于来自SLIP接口140.252.1.29。netstat的显示说明了这一点 如果我们从主机solaris通过SLIP链路与这个服务器相连接它将正常工作。 但如果我们试图从以太网140.252.13中的主机与这个服务器进行连接连接请求将被TCP模块拒绝。如果使用tcpdump来观察这一切对连接请求SYN的响应是一个如图18-21所示的RST。 这个连接请求将不会到达服务器的应用程序因为它根据应用程序中指定的本地IP地址被内核中的TCP模块拒绝。 18.11.3 限定的远端IP地址 在11.12节我们知道UDP服务器通常在指定IP本地地址和本地端口外还能指定远端IP地址和远端端口。RFC 793中显示的接口函数允许一个服务器在执行被动打开时可指明远端插口等待一个特定的客户执行主动打开也可不指明远端插口等待任何客户。 遗憾的是大多数API都不支持这么做。服务器必须不指明远端插口而等待连接请求的到来然后检查客户端的IP地址和端口号。 图18-22总结了TCP服务器进行连接时三种类型的地址绑定。在三种情况中lport是服务器的熟知端口而localIP必须是一个本地接口的IP地址。表中行的顺序正是TCP模块在收到一个连接请求时确定本地地址的顺序。最常使用的绑定第1行如果支持的话将最先尝试最不常用的最后一行两端的IP地址都没有制定将最后尝试。 18.11.4 呼入连接请求队列 一个并发服务器调用一个新的进程来处理每个客户请求因此处于被动连接请求的服务器应该始终准备处理下一个呼入的连接请求。那正是使用并发服务器的根本原因。但仍有可能出现当服务器在创建一个新的进程时或操作系统正忙于处理优先级更高的进程时到达多个连接请求。当服务器正处于忙时TCP是如何处理这些呼入的连接请求 在伯克利的TCP实现中采用以下规则 正等待连接请求的一端有一个固定长度的连接队列该队列中的连接已被TCP接受即三次握手已经完成但还没有被应用层所接受。注意区分TCP接受一个连接是将其放入这个队列而应用层接受连接是将其从该队列中移出。 应用层将指明该队列的最大长度这个值通常称为积压值(backlog)。它的取值范围是0~5之间的整数包括0和5大多数的应用程序都将这个值说明为5。 当一个连接请求即SYN到达时TCP使用一个算法根据当前连接队列中的连接数来确定是否接收这个连接。我们期望应用层说明的积压值为这一端点所能允许接受连接的最大数目但情况不是那么简单。图18-23显示了积压值与传统的伯克利系统和Solaris2.2所能允许的最大接受连接数之间的关系。注意积压值说明的是TCP监听的端点已被TCP接受而等待应用层接受的最大连接数。这个积压值对系统所允许的最大连接数或者并发服务器所能并发处理的客户数并无影响。在这个图中Solaris系统规定的值正如我们所期望的。而传统的BSD系统将这个值由于某些原因设置为积压值乘3除以2再加1。 如果对于新的连接请求该TCP监听的端点的连接队列中还有空间基于图18-23TCP模块将对SYN进行确认并完成连接的建立。但应用层只有在三次握手中的第三个报文段收到后才会知道这个新连接时。另外当客户进程的主动打开成功但服务器的应用层还不知道这个新的连接时它可能会认为服务器进程已经准备好接收数据了如果发生这种情况服务器的TCP仅将接收的数据放入缓冲队列)。 如果对于新的连接请求连接队列中已没有空间TCP将不理会收到的SYN。也不发回任何报文段即不发回RST。如果应用层不能及时接受已被TCP接受的连接这些连接可能占满整个连接队列客户的主动打开最终将超时。 通过sock程序能了解这种情况。我们调用它并使用新的选项-O。让它在创建一个新的服务器进程后而没有接受任何连接请求之前暂停下来。如果在它暂停期间又调用了多个客户进程它将导致接受连接队列被填满通过tcpdump能够看到这一切。 -q1选项将服务器端的积压值置1。在这种情况下传统的BSD系统中的队列允许接受两个连接请求图18-23。-O30选项使程序在接受任何客户连接之前暂停30秒。在这30秒内我们可启动其他客户进程来填充这个队列。在主机sun上启动4个客户进程。 图18-24显示了tcpdump的输出首先是第1个客户进程的第1个SYN省略窗口大小和MSS声明。当TCP连接建立时将客户进程的端口号用粗体标出。 端口为1090的第一个客户连接请求被TCP接受报文段13。端口为1091的第2个客户连接请求也被TCP接受报文段4~6。而服务器的应用仍处于休眠状态还未接受任何连接。目前的一切工作都由内核中的TCP模块完成。另外两个客户进程已经成功地完成了它们的主动打开因为它们建立连接的三次握手已经完成。 我们接着在报文段7端口1092和报文段8端口1093启动第3和第4个客户进程。由于服务器的连接队列已满TCP将不理会两个SYN。这两个客户进程在报文段910111215重发它们的SYN。第4个客户进程的第3个SYN重传被接受了因为服务器程序的30秒休眠结束后它将已接受的两个连接从队列中移出使连接队列变空服务器程序接收连接的时间是28.19小于30的原因在于启动服务器程序后它需要几秒的时间来启动第1个客户进程(报文段1显示的就是启动时间)。第3个客户进程的第4个SYN重传这时将被接受报文段15~17。服务器程序先接受第4个客户连接端口1093的原因是服务器程序30秒休眠与客户程序重传之间的定时交互作用。 我们期望接收连接队列按先进先出顺序传递给应用层。如TCP接受了端口为1090和1091的连接我们希望应用层先接受端口为1090的连接然后再接受端口为1091的连接。但许多伯克利的TCP实现都出现按后进先出的传递顺序这个错误已存在了多年。产商最近已开始改正这个错误但在如SunOS 4.13等系统中仍存在这个问题。 当队列已满时TCP将不理会传入的SYN也不发回RST作为应答因为这是一个软错误而不是一个硬错误。通常队列已满是由于应用程序或操作系统忙造成的这样可防止应用程序对传入的连接进行服务。这个条件在一个很短的时间内可以改变。但如果服务器的TCP以系统复位作为响应客户进程的主动打开将被废弃如果服务器程序没有启动我们就会遇到。由于不应答SYN服务器程序迫使客户TCP随后重传SYN以等待连接队列有空间接受新的连接。 这个例子中有一个巧妙之处这在大多TCP/IP的具体实现中都能见到就是如果服务器的连接队列未满时TCP将接受传入的连接请求即SYN但并不让应用层了解该连接源于何处即不告知源IP地址和源端口。这不是TCP所要求的而只是共同的实现技术如伯克利源代码通常都这么做。如果一个API如TLI见1.15节向应用程序提供了解连接请求的到来的方法并允许应用程序选择是否接受连接。当应用程序假定被告知连接请求已经到来时TCP的三次握手已经结束其他运输层的实现可能将连接请求的到达与接受分开如OSI的运输层但TCP不是这样。 Solaris 2.2提供了一个选项使TCP只有在应用程序说可以接受tcp_eager_listeners见E.4才允许接受传入的连接请求。 这种行为也意味着TCP服务器无法使客户进程的主动打开失效。当一个新的客户连接传递给服务器的应用程序时TCP的三次握手就结束了客户的主动打开已经完全成功。如果服务器的应用程序此时看到客户的IP地址和端口号并决定是否为该客户进行服务服务器所能做的就是关闭连接发送FIN或者复位连接发送RST。无论哪种情况客户进程都认为一切正常因为它的主动打开已经完成并且已经向服务器程序发送过请求。 18.12 小结 两个进程在使用TCP交换数据之前它们之间必须建立一条连接。完成后要关闭这个连接。本章已经详细介绍了如何使用三次握手来建立连接以及使用4个报文段来关闭连接。 我们用tcpdump程序显示了TCP首部中的各个字段。也了解了连接建立是如何超时连接复位是如何发送使用半打开连接发生的情况以及TCP是如何提供半关闭、同时打开和同时关闭。 弄清TCP操作的关键在于它的状态变迁图。我们跟踪了连接建立与关闭的步骤以及它们的状态变迁过程。还讨论了在设计TCP并发服务器时TCP连接建立的具体实现方法。 一个TCP连接由一个4元组唯一确定本地IP地址、本地端口号、远端IP地址和远端端口号。无论何时关闭一个连接一端必须保持这个连接我们看到TIME_WAIT状态将处理这个问题。处理的原则是执行主动打开的一端在进入这个状态时要保持的时间为TCP实现中规定的MSL值的两倍。
http://www.hkea.cn/news/14541244/

相关文章:

  • 网站后台建设公司广州联雅网络科技有限公司
  • 如何用用dw做网站后台学网站开发技术
  • 怎么查看网站哪个公司做的装饰公司网站源码
  • 企业建设网站的价值扩展名 网站
  • 免费手机端网站模板下载工具兰州新区农投建设网站
  • 青岛做网站哪个公司好做任务领取礼品的网站
  • 什么是网站微商城的建设html网页制作表格代码
  • 微信h5制作网站开发wordpress文章样式插件
  • 北京网站优化服务商个人如何推广app
  • 保险网站建设公司深圳网站建设制作哪家口碑好
  • 游戏平台网站制作用毛做简单的网站
  • wps免费模板网站山西省建设工程信息网站
  • 网站建设的工作流程规划设计公司网站
  • 织梦婚纱网站模板凡客诚品失败的主要原因
  • 潍坊网站建设公司哪家好哈尔滨市住房和城乡建设局局网站
  • 谁有做任务网站wordpress 背景特效
  • 莱芜网站同城分类信息网站
  • 临桂建设局网站郑州做网站推广价格
  • 网站建设选哪个外贸网站建设要求
  • 手机网站建设推广软文营业执照 网站开发
  • 瑞安联科网站建设下载ps软件免费版2022
  • 在线培训网站怎么做wordpress分享文章插件
  • html5网站特点昆明网络营销
  • 做寂网站设计类专业电脑推荐
  • 外贸网站设计与推广wordpress顶部栏
  • 360做网站荆州西宁做网站的好公司
  • ps和dw怎么做网站室内设计主要是干什么的
  • 网站设计模板下载ps怎么制作网页效果图
  • 中国建设银行企业网站做招聘网站做服务器多少钱
  • 商城网站设计需要哪些技术广东省做网站推广公司