目录
运输层端口号、多路复用、多路分解
1.1、运输层端口号
端口号的基本概念
TCP/IP体系的应用层常用协议所使用的熟知端口号
1.2、运输层的多路复用与多路分解
编辑面向连接的运输:TCP
2.1TCP连接
2.2TCP协议与UDP协议对比
2.3TCP报文段格式
2.4TCP三次握手(建立连接)
2.4.1TCP为了保证连接的可靠性,每次都会通过进行三次握手来建立可靠的连接
2.4.2使用tcpdump命令抓包查看建立过程
2.5两次握手是否可以?(面试题)
2.5TCP四次挥手(释放连接)
2.5.1TCP使用四次挥手进行断开连接;编辑
2.5.2使用tcpdump命令抓包查看断开连接过程
2.5.3TIME-WAIT时间是否多余,为什么?(面试问题)
2.5.4是否可以强制使用被处于TIME_WAIT状态的连接占用的socket地址?怎样实现?(面试)
2.6TCP连接时SYN泛洪攻击
2.7TCP中报活计时器作用
2.8用状态图表示TCP建立与断开
2.8TCP流量控制
2.9TCP的拥塞控制
2.9.1从三个方面来说明出现拥塞的原因以及拥塞的代价(书上讲解);
2.9.2拥塞控制方法
2.9.3慢开始,拥塞避免
2.9.4快重传和快恢复
2.10TCP超时重传的时间选择
2.10.1超时重传时间的极端情况
2.10.1往返时间的估计与超时
2.11TCP总结
TCP之socket编程(c++实现代码)
TCP编程流程
客户端
服务器端
参考文献
在网络编程中我们可以经常听到端口号这个概念,TCP协议也是不例外的,那什么是端口号?
举个简单的例子:如果 IP 是用来定位街区的,那么端口就是对应于该街区中每一户的门牌号。在通讯过程中,数据通过各种通讯协议最终抵达设备(如计算机)后,这里的设备就相当于一个街区,而在设备计算机内部有很多程序在跑,数据进来之后,必须要给它一个对应的门牌号(即端口号),程序才方便进行后续操作。端口号属于传输协议的一部分,因此我们可以说,数据通过 IP 地址发送对应的数据到指定设备上,而通过端口号把数据发送到指定的服务或程序上。
TCP/IP体系的运输层使用端口号来区分应用层的不同应用进程。
端口号使用16比特表示,因此取值范围为0~65535;
端口号根据范围分为三种,如下:
协议及服务器 | 端口号 |
HTTP |
80/8080/3128/8081/9098 |
HTTPS | 443/tcp 443/udp |
SOCKS代理协议服务器 | 1080 |
.FTP(文件传输)协议 | 21 |
Telnet(远程登录)协议 | 23 |
FTP | 21/tcp |
TFTP | 69/udp |
SSH(安全登录)、SCP(文件传输) | 22/tcp |
SMTP | 25/tcp |
POP3(E-mail) | 110/tcp |
Webshpere应用程序 | 9080 |
webshpere管理工具 | 9090 |
JBOSS | 8080 |
TOMCAT | 8080 |
WIN2003远程登录 | 3389 |
MS SQL*SERVER数据库server | 1433/tcp 1433/udp |
MS SQL*SERVER数据库monitor | 1434/tcp 1434/udp |
DNS | 53/udp |
Mysql数据库 | 3306 |
BGP | 179 |
为了解释分解和复用的过程,在这里举一个简单的例子:
用一个家庭类比;家庭的每一个孩子通过他们的名字来标识。假设Ann的家庭向Bill发送信件;当Bill从邮递员处收到一批信件,并通过查看收件人姓名将信件交付给他的兄弟姐妹是,他执行的操作就是分解;当Ann从兄弟姐妹们那里手机信件并将它们交给邮寄员是,她执行的操作就是多路复用;
现在给出多路复用和多路分解的概念:
如图表示多路复用及分解过程:
TCP被称为面向连接的(connection-oriented),这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须先进行相互“握手”,即他们必须相互发送某些预备报文段,以建立确保数据传输的参数。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。
TCP为每块客户数据配上一个TCP首部,从而形成多个TCP报文段( TCP segment)。这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中。然后这些IP数据报被发送到网络中。当TCP在另一端接收到一个报文段后,该报文段的数据就被放入该TCP连接的接收缓存中,如上图所示。应用程序从此缓存中读取数据流。该连接的每一端都有各自的发送缓存和接收缓存。
下图可以帮助记忆:
TCP 报文段由首部字段和一个数据字段组成。数据字段包含一块应用数据。MSS限制了报文段数据字段的最大长度。当TCP发送一个大文件,例如某Web页面上的一个图像时,TCP通常是将该文件划分成长度为MSS的若干块(最后一块除外,它通常小于MSS)。然而,交互式应用通常传送长度小于MSS的数据块。例如,对于像Telnet这样的远程登录应用,其TCP报文段的数据字段经常只有一个字节。由于TCP的首部一般是20字节(比UDP首部多12字节),所以Telnet发送的报文段也许只有21字节长。
在Ubuntu下使用命令 tcpdump 可以抓包观察 TCP 连接的建立与关闭。该命令需要管理员权限,格式如下(假设两个测试用的主机 IP 地址为 192.168.43.214 和 192.168.43.160 ) :
不可以
假定客户端向服务器发送了一个建立连接的请求(SYN报文段),但是由于某种原因没有按时到达服务器端;基于TCP协议超时重传机制,客户端第二次发送建立连接的SYN报文段,服务器收到之后,对客户端发送的SYN报文段进行确认,如上面说的三次握手的第二步;此时我们已经完成连接(两次握手)进行数据传输等等直到最后关闭连接;此时处于关闭状态的服务器收到了客户端发送的第一条SYN报文段(建立连接),服务器对其进行回应(进入连接已建立阶段)但是客户端已经关闭(服务器并不知道)所以服务器端一直进行重传期望得到客户端的数据;过程如图所示:
答案是不多多余;
如图,假设取消了TIME-WAIT时间,当客户端的发送确认报文段,因为没有TIME_WAIT时间,所以此时客户端直接关闭;如果客户端发送的确认报文段丢失,如图所示,则服务器端将一直等待客户端的确认报文,进行不停的重传,一直无法关闭;
可以;服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。
int sock = socket( PF_INET,SOCK_ STREAM,0 ) ;
assert( sock >= 0 );
int reuse = 1;
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) ;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr ) ;
address.sin port=htons(port );
int ret = bind( sock, ( struct sockaddr* ) &address, sizeof(address));
我们在TCP三次握手的讨论中已经看到,服务器为了响应一个收到的SYN,分配并初始化连接变量和缓存。然后服务器发送一个SYNACK进行响应,并等待来自客户的ACK报文段。如果某客户不发送ACK来完成该三次握手的第三步,最终(通常在一分多钟之后)服务器将终止该半开连接并回收资源。这种TCP连接管理协议为经典的DoS攻击即SYN洪泛攻击(SYN flood attack) 提供了环境。在这种攻击中,攻击者发送大量的TCP SYN报文段,而不完成第三次握手的步骤。随着这种SYN报文段纷至沓来,服务器不断为这些半开连接分配资源( 但从未使用),导致服务器的连接资源被消耗殆尽。这种SYN洪泛攻击是被记载的众多DoS 攻击中的第一种[CERTSYN 1996]。幸运的是,现在有一种有效的防御系统,称为SYNcookie [ RFC 4987],它们被部署在大多数主流操作系统中。SYN cookie 以下列方式工作:
如图所示 当TCP客户端出现故障时,TCP服务器如何发现?
使用保活计时器;
客户TCP开始时处于CLOSED (关闭)状态。客户的应用程序发起一个新的TCP连接。这引起客户中的TCP向服务器中的TCP发送一个SYN报文段。在发送过SYN报文段后,客户TCP进入了SYN_SENT状态。当客户TCP处在SYN_ SENT 状态时,它等待来自服务器TCP的对客户所发报文段进行确认且SYN比特被置为1的一个报文段。收到这样一个报文段之后,客户端进入TCESTABLISHED (已建立)状态。当处在ESTABLISHED状态时,TCP客户就能发送和接收包含有效载荷数据(即应用层产生的数据)的TCP报文段了。
假设客户应用程序决定要关闭该连接。(注意到服务器也能选择关闭该连接。)这引起客户TCP发送一个带有FIN比特被置为1的TCP报文段,并进入FIN_WAIT_1状态。当处在FIN_WAIT1状态时,客户TCP等待一个来自服务器的带有确认的TCP报文段。当它收到该报文段时,客户TCP进入FIN _WAIT 2状态。当处在FIN_WAIT 2状态时,客户等待来自服务器的FIN比特被置为1的另一个报文段;当收到该报文段后,客户TCP对服务器的报文段进行确认,并进入TIME_ WAIT状态。假定ACK丢失,TIME_WAIT 状态使TCP客户重传最后的确认报文。在TIME_ WAIT 状态中所消耗的时间是与具体实现有关的,而典型的值是30秒、1分钟或2分钟。经过等待后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。
我们知道一条TCP连接的每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、按序的字节后,它就将数据放入接收缓存。相关联的应用进程会从该缓存中读取数据,但不必是数据刚一到达就立即读取。事实上,接收方应用也许正忙于其他任务,甚至要过很长时间后才去读取该数据。如果某应用程序读取数据时相对缓慢,而发送方发送得太多、太快,发送的数据就会很容易地使该连接的接收缓存溢出。
因此TCP为它的应用程序提供了流量控制服务( flow-control service) 以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。TCP发送方也可能因为IP网络的拥塞而被遏制;这种形式的发送方的控制被称为拥塞控制,即使流量控制和拥塞控制采取的动作非常相似(对发送方的遏制),但是它们显然是针对完全不同的原因而采取的措施。
TCP通过让发送方维护一个称为接收窗口( receive window)的变量来提供流量控制。通俗地说,接收窗口用于给发送方一个指示该接收方还有多少可用的缓存空间。因为TCP是全双工通信,在连接两端的发送方都各自维护一个接收窗口。我们在文件传输的情况下研究接收窗口。假设主机A通过一条TCP连接向主机B发送一个大文件。主机B为该连接分配了一个接收缓存,并用RevBuffer来表示其大小。主机B上的应用进程不时地从该缓存中读取数据。我们定义以下变量:
由于TCP不允许已分配的缓存溢出,下面公式必须成立:
接收窗口用rwnd来表示,根据缓存可用空间的数量来设置:
由于时间和空间是变化的,所以说rwnd是动态的。
主机B通过把当前的rwnd值放人它发给主机A的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。开始时,主机B设定rwnd =RcvBuffer。为了实现这点,主机B必须跟踪几个与连接有关的变量主机A轮流跟踪两个变量,LastByteSent 和LastByteAcked,这两个变量的意义很明显。注意到这两个变量之间的差LastByteSent E LastByteAcked,就是主机A发送到连接中但未被确认的数据量。通过将未确认的数据量控制在值rwnd以内,就可以保证主机A不会使主机B的接收缓存溢出。因此,主机A在该连接的整个生命周期须保证:
那么假设主机B的接收缓存已经存满,使得rwnd=0。在将rwnd=0通告给主机A之后,还要假设主机B没有任何数据要发给主机A。此时,考虑会发生什么情况。因为主机B上的应用进程将缓存清空,TCP并不向主机A发送带有rwnd新值的新报文段;事实上,TCP仅当在它有数据或有确认要发时才会发送报文段给主机A。这样主机A不可能知道主机B的接收缓存已经有新的空间了,即主机A被阻塞而不能再发送数据!
为了解决这个问题,TCP规范中要求:当主机B的接收窗口为0时,主机A继续发送只送一个字节数据的报文段。这些报文段将会被接收方确认。最终缓存将开始清空,并且确认报文里将包含一个非0的rwnd值;
如图所示 假设主机A主机B发送如下报文段;
则按照上面我们所说的那样,有如下图所示的过程:
情况一:两个发送方和一台具有无穷大缓存的路由器;
我们假设主机A中的应用程序以λn字节/秒的平均速率将数据发送到连接中(例如、通过一个套接字将数据传递给运输层协议)。这些数据是初始数据,这意味着每个数据单元仅向套接字中发送一次。下面的运输层协议是一个简单的协议。数据被封装并发送;不执行差错恢复(如重传)、流量控制或拥塞控制。忽略由于添加运输层和较低层首部信息产生的额外开销,在第一 种情况下,主机A向路由器提供流量的速率是λn字节/秒。主机B也以同样的方式运行,为了简化问题,我们假设它也是以速率λin字节/秒发送数据。来自主机A和主机B的分组通过一台路由器,在一.段容量为R的共享式输出链路上传输。该路由器带有缓存,可用于当分组到达速率超过该输出链路的容量时存储“人分组”。在此第一种情况下,我们将假设路由器有无限大的缓存空间。
下图描绘出了第一种情况下主机A的连接性能。左边的图形描绘了每连接的吞吐量(per-connection throughput) (接收方每秒接收的字节数)与该连接发送速率之间的函数关系。当发送速率在0~R/2之间时,接收方的吞吐量等于发送方的发送速率,即发送方发送的所有数据经有限时延后到达接收方。然而当发送速率超过R/2时,它的吞吐量只能达R/2。这个吞吐量上限是由两条连接之间共享链路容量造成的。链路完全不能以超过R/2的稳定状态速率向接收方交付分组。无论主机A和主机B将其发送速率设置为多高,它们都不会看到超过R/2的吞吐量。
取得每连接R/2的吞吐量实际上看起来可能是件好事,因为在将分组交付到目的地的过程中链路被充分利用了。但是,图b的图形却显示了以接近链路容量的速率运行时产生的后果。当发送速率接近R/2时(从左至右),平均时延就会越来越大。当发送速率超过R/2时,路由器中的平均排队分组数就会无限增长,源与目的地之间的平均时延也会变成无穷大(假设这些连接以此发送速率运行无限长时间并且有无限量的缓存可用)。因此,虽然从吞吐量角度看,运行在总吞吐量接近R的状态也许是一个理想状态,但从时延角度看,却远不是一个理想状态。甚至在这种(极端)理想化的情况中,我们已经发现了拥塞网络的一种代价,即当分组的到达速率接近链路容量时,分组经历巨大的排队时延。
情况二:两个发送方和一台具有有限缓存的路由器
现在我们从下列两个方面对情况1稍微做一些修改。首先,假定路由器缓存的容量是有限的。这种现实世界的假设的结果是,当分组到达一个已满的缓存时会被丟弃。其次,我们假定每条连接都是可靠的。如果一个包含有运输层报文段的分组在路由器中被丢弃,那么它终将被发送方重传。由于分组可以被重传,所以我们现在必须更小心地使用发送速率这个术语。特别是我们再次以λin字节/秒表示应用程序将初始数据发送到套接字中的速率。运输层向网络中发送报文段( 含有初始数据或重传数据)的速率用入:字节/秒表示。λ有时被称为网络的供给载荷(ofered load)。
首先,考虑一种不真实的情况,即主机A能够以某种方式(不可思议地!)确定路由器中的缓存是否空闲,因而仅当缓存空闲时才发送一个分组。在这种情况下,将不会产生丢包,λin与λin'相等,并且连接的吞吐量就等于λin。下图a中描述了这种情况。从吞吐量的角度看,性能是理想的,即发送的每个分组都被接收到。注意到在这种情况下,平均主机发送速率不能超过R/2,因为假定不会发生分组丢失。
接下来考虑一种更为真实的情况,发送方仅当在确定了一个分组已经丢失时才重传。(同样,所做的假设有一些弹性。然而,发送主机有可能将超时时间设置得足够长,以无形中使其确信一个还没有被确认的分组已经丢失。)在这种情况下,性能就可能与下图b所示的情况相似。为了理解这时发生的情况,考虑一下供给载荷λ'in(初始数据传输加上重传的总速率)等于R/2的情况。根据下图b,在这一供给载荷值时,数据被交付给接收方应用程序的速率是R/3。因此,在所发送的0.5R单位数据当中,从平均的角度说,0. 333R字节/秒是初始数据,而0.166R字节/秒是重传数据。我们在此看到了另一种网络拥塞的代价,即发送方必须执行重传以补偿因为缓存溢出而丢弃(丢失)的分组。最后,我们考虑下面一种情况:发送方也许会提前发生超时并重传在队列中已被推迟但还未丢失的分组。在这种情况下,初始数据分组和重传分组都可能到达接收方。当然,接收方只需要一份这样的分组副本就行了,重传分组将被丢弃。在这种情况下,路由器转发重传的初始分组副本是在做无用功,因为接收方已收到了该分组的初始版本。而路由器本可以利用链路的传输能力去发送另一个分组。这里,我们又看到了网络拥塞的另一种代价,即发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽来转发不必要的分组副本。图c 显示了当假定每个分组被路由器转发(平均)两次时,吞吐量与供给载荷的对比情况。由于每个分组被转发两次,当其供给载荷接近R/2时,其吞吐量将渐近R/4。
情况三:4个发送方和具有有限缓存的多台路由器及多跳路径
有4台主机发送分组,每台都通过交叠的两跳路径传输,如图所示。我们再次假设每台主机都采用超时/重传机制来实现可靠数据传输服务,所有的主机都有相同的λ值,所有路由器的链路容量都是R字节/秒。我们考虑从主机A到主机C的连接,该连接经过路由器R1和R2。A-C连接与D-B连接共享路由器R1,并与B-D连接共享路由器R2。对极小的λ值,路由器缓存的溢出是很少见的(与拥塞情况1、拥塞情况2中的一样),吞吐量大致接近供给载荷。对稍大的λ值,对应的吞吐量也更大,因为有更多的初始数据被发送到网络中并交付到目的地,溢出仍然很少。因此,对于较小的λ日,λin的增大会导致λout的增大。
在考虑了流量很小的情况后,下面分析当λin (因此λin')很大时的情况。考虑路由器R2。不管λm的值是多大,到达路由器R2的A-C流量(在经过路由器R1转发后到达路由器R2)的到达速率至多是R,也就是从R1到R2的链路容量。如果λi对于所有连接(包括B-D连接)来说是极大的值,那么在R2上,B-D流量的到达速率可能会比A-C流量的到达速率大得多。因为A-C流量与B-D流量在路由器R2上必须为有限缓存空间而竞争,所以当来自B- D连接的供给载荷越来越大时,A-C连接上成功通过R2 (即由于缓存溢出而未被丢失)的流量会越来越小。在极限情况下,当供给载荷趋近于无穷大时,R2的空闲缓存会立即被B- D连接的分组占满,因而A-C连接在R2上的吞吐量趋近于0。这又一次说明
在重载的极限情况下,A-C端到端吞吐量将趋近于0。这些考虑引发了供给载荷与吞吐量之间
的权衡,如图所示。当考虑由网络所做的浪费掉的工作量时,随着供给载荷的增加而使吞吐量最终减少的原因是明显的。在上面提到的大流量的情况中,每当有一个分组在第二跳路由器上被丢弃时,第一跳路由器所做的将分组转发到第二跳路由器的工作就是“劳而无功”的。如果第一跳路由器只是丢弃该分组并保持空闲,则网络中的情况是幸运的(更准确地说是糟糕的)。需要指出的是,第一跳路由器所使用的将分组转发到第二跳路由器的传输容量用来传送不同的分组可能更有效益。
四种方法即 慢开始 拥塞避免 快重传 快恢复
当TCP连接开始时,cwnd的值通常初始置为一个MSS的较小值,这就使得初始发送速率大约为MSS/RTT。例如,如果MSS=500字节且RTT =200ms,则得到的初始发送速率大约只有20kbps。由于对TCP发送方而言,可用带宽可能比MSS/RTT大得多,TCP发送方希望迅速找到可用带宽的数量。
慢启动( slow-start):cwnd的值以1个MSS开始并且每当传输的报文段首次被确认就增加1个MSS。TCP向网络发送第一个报文段并等待一个确认。当该确认到达时,TCP发送方将拥塞窗口增加一个RMSS,并发送出两个最大长度的报文段。这两个报文段被确认,则发送方对每个确认报文段将拥塞窗口增加一个MSS,使得拥塞窗口变为4个MSS,并这样下去。这一过程每过一个RTT,发送速率就翻番。因此,TCP发送速率起始慢,但在慢启动阶段以指数增长。
ssthresh:慢启动阈值;当检测到拥塞时将ssthresh置为拥塞窗口的一半。
拥塞避免模式:继续使cwnd翻番可能有些鲁莽。因此,当cwnd的值等于ssthresh时,结束慢启动并且TCP转移到拥塞避免模式。
采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失。
快重传:当出现超时时,TCP的拥塞避免算法行为相同。与慢启动的情况一样,cwnd 的值被设置为1个MSS,当丢包事件出现时,ssthresh 的值被更新为cwnd值的一半。然而,前面讲过丢包事件也能由一个三个冗余ACK事件触发。在这种情况下,网络继续从发送方向接收方交付报文段( 就像由收到冗余ACK所指示的那样)。因此TCP对这种丢包事件的行为,相比于超时指示的丢包,应当不那么剧烈:TCP 将cwnd的值减半( 为使测量结果更好,计及已收到的3个冗余的ACK要加上3个MSS),并且当收到3个冗余的ACK,将ssthresh的值记录为cwnd的值的一半。 接下来进入快速恢复状态。
快恢复:在快速恢复中,对于引起TCP进人快速恢复状态的缺失报文段,对收到的每个冗余ACK, cwnd 的值增加一个MSS。最终,当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进人拥塞避免状态。如果出现超时事件,快速恢复在执行如同在慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:当丢包事件出现时,cwnd 的值被设置为1个MSS,并且ssthresh的值设置为cwnd值的一半。快速恢复是TCP推荐的而非必需的构件。一种称为TCPTahoe的TCP早期版本,不管是发生超时指示的丟包事件,还是发生3个冗余ACK指示的丢包事件,都无条件地将其拥塞窗口减至1个MSS,并进人慢启动阶段。TCP的较新版本TCP Reno,则综合了快速恢复。
我们知道TCP协议采用超时/重传机制来处理报文段的丢失问题。尽管这在概念上简单,但是当在如TCP这样的实际协议中实现超时/重传机制时还是会产生许多微妙的问题。
RTT:报文段的样本RTT就是从某报文段被发出(即交给IP)到对该报文段的确认被收到之间的时间量。
情况一:RTO(超时重传时间)小于RTT时间,如图所示,则会出现不必要重传报文;
情况二:RTO(超时重传时间)远大于RTT时间,如图所示,则会出现重传报文时间太长,使得网络空闲时间增大;
问题:这个时间间隔到底应该是多大呢?刚开始时应如何估计往返时间呢?是否应该为所有未确认的报文段各设一个定时器?
大多数TCP的实现仅在某个时刻做一次RTTs(样本RTT)测量,而不是为每个发送的报文段测量一个RTTs。这就是说,在任意时刻,仅为一个已发送的但目前尚未被确认的报文段估计RTTs,从而产生一个接近每个RTT的新RTTs值。另外,TCP 决不为已被重传的报文段计算RTTs;它仅为传输一次的报文段测量。
由于路由器的拥塞和端系统负载的变化,这些报文段的RTTs值会随之波动。由于这种波动,任何给定的RTTs值也许都是非典型的。因此,为了估计一个典型的RTT,采取某种对RTTs取平均的办法。TCP维持一个RTTs均值。一旦获得一个新RTTs时,TCP就会根据下列公式来更新RTT:
RTT的新值是由以前的RTT值与RTT新值加权组合而成的。在[ RFC 6298]中给出的a推荐值是α=0.125 (即1/8);
RFC6298建议使用下式计算超时重传时间RTO:
我们可以看出不管是RTTs还是RTTd都与新的RTT样本有关系;但是又出现了以下两种情况如图:
情况一:源主机若误将确认当做对原报文段的确认,计算出来的RTTs和RTO就会偏大,降低了传输效率;
情况二:源主机若误将确认当做是对重传报文段的确认;计算出来的RTTs和RTO就会偏小,导致报文段没必要的重传,增加网络负荷;
针对出现超时重传时无法测准往返时间RTT的问题,Karn提出了一个算法:
在加权平均往返时间RTTs时,只要报文段重传了,就不采用其往返时间RTT样本。也就是出现重传时,不重新计算RTTs,进而超时重传时间RTO也不会重新计算。
这又引起了新的问题。设想出现这样的情况:报文段的时延突然增大了很多,并且之后很长一段时间都会保持这种时延。因此在原来得出的重传时间内,不会收到确认报文段。于是就重传报文段。但根据Karn算法,不考虑重传的报文段的往返时间样本。这样,超时重传时间就无法更新。这会导致报文段反复被重传。
因此,要对Karn算法进行修正。方法是:
报文段每重传一次,就把超时重传时间RTO增大些。典型的做法是将新RTO的值取为旧RTO值的2倍。
虽然发送方的发送窗口是根据接收方的接收窗口设置的,但在同一时刻,发送方的发送窗口并不总是和接收方的接收窗口一样大。
对于不按序到达的数据应如何处理,TCP并无明确规定。
TCP要求接收方必须有累积确认和捎带确认机制,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带上。
TCP的通信是全双工通信。通信中的每一方都在发送和接收报文段。因此,每一方都有自己的发送窗口和接收窗口。在谈到这些窗口时,一定要弄清楚是哪一方的窗口。
tcpclient.h/tcpclient.cc
#ifndef __TCP_CLIENT__H__
#define __TCP_CLIENT__H__
#include
class TcpClient {
private:
int m_sfd; // server fd
public:
TcpClient(const IpAddressPort&);
~TcpClient();
int SendMsg(const std::string& msg);
Msg RecvMsg();
int GetSfd() const;
};
#endif
#include "tcpClient.h"
// linux api的
#include
#include
#include
#include
#include
// c标准库的
#include // #include
TcpClient::TcpClient(const IpAddressPort& ipAddressAndPort) {
// tcp
m_sfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == m_sfd) {
LOG_FATAL << strerror(errno);//自己实现的日志文件
}
struct sockaddr_in ser;
// 本机字节序 -》 网络字节序
// intel 小端模式 -》 网络字节序 大端模式
ser.sin_family = AF_INET;
ser.sin_port = htons(ipAddressAndPort.GetPort());
ser.sin_addr.s_addr = inet_addr(ipAddressAndPort.GetIpAddress().c_str());
if (-1 == connect(m_sfd, (struct sockaddr*)&ser, sizeof(ser))) {
LOG_FATAL << strerror(errno);
}
LOG_INFO << "connect success!";
}
TcpClient::~TcpClient() {
if (-1 == close(m_sfd)) {
// log_fatal
LOG_FATAL << strerror(errno);
}
}
int TcpClient::SendMsg(const std::string& msg) {
// send
int numberOfSendByte = send(m_sfd, msg.c_str(), strlen(msg.c_str()), 0);
if (-1 == numberOfSendByte) {
LOG_ERROR << strerror(errno);
}
return numberOfSendByte;
}
// 怎么返回多个返回值
Msg TcpClient::RecvMsg() {
// recv api
char buf[LARGE_BUF_LEN] = {0};
int numberOfRecvByte = recv(m_sfd, buf, LARGE_BUF_LEN, 0);
if (-1 == numberOfRecvByte) {
LOG_ERROR << strerror(errno);
}
// c风格
// struct Msg {
// std::string m_msg;
// int m_numberOfRecvBytes;
// Msg(std::string msg, int numberOfRecvBytes) : m_msg(msg),
// m_numberOfRecvBytes(numberOfRecvBytes) {}
// std::string ToString() const {
// std::stringstream ss;
// ss << "[recmsg:" << m_msg << "], [numberOfByte:" << m_numberOfRecvBytes << "]";
// return ss.str();
// }
// };
// return Msg{.m_msg = buf, .m_numberOfRecvBytes = numberOfRecvByte};
return Msg{buf, numberOfRecvByte};
}
int TcpClient::GetSfd() const {
return m_sfd;
}
tcpserver.h/tcpserver.cc
#ifndef __TCP_SERVER__H__
#define __TCP_SERVER__H__
#include
class TcpServer {
private:
int m_lfd;
public:
TcpServer(const IpAddressPort&);
~TcpServer();
int Accept();
int SendMsg(int cfd, const std::string& msg);
Msg RecvMsg(int cfd);
int GetLfd() const;
};
#endif
#include "tcpServer.h"
#include "msg.h"
#include
#include
#include
#include
#include
#include
#include
// socket bind listen fd->(ip+port)
TcpServer::TcpServer(const IpAddressPort& ipAddressAndPort) {
m_lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == m_lfd) {
LOG_FATAL << strerror(errno);
}
struct sockaddr_in ser;
ser.sin_family = AF_INET;
// 主机字节序 -》 网络字节序
ser.sin_addr.s_addr = inet_addr(ipAddressAndPort.GetIpAddress().c_str());
ser.sin_port = htons(ipAddressAndPort.GetPort());
if (-1 == bind(m_lfd, (struct sockaddr*)&ser, sizeof(ser))) {
LOG_FATAL << strerror(errno);
}
if (-1 == listen(m_lfd, 5)) {
LOG_FATAL << strerror(errno);
}
}
// close
TcpServer::~TcpServer() {
if (-1 == close(m_lfd)) {
LOG_FATAL << strerror(errno);
}
}
// 从全连接队列中获取一个完成三次握手的cfd
int TcpServer::Accept() {
struct sockaddr_in cli;
socklen_t len = sizeof(cli);
int cfd = accept(m_lfd, (struct sockaddr*)& cli, &len);
if (-1 == cfd) {
LOG_ERROR << strerror(errno);
return -1;
} else {
char clientIpBuf[SMALL_BUF_LEN] = {0};
if (!inet_ntop(AF_INET, &cli.sin_addr, clientIpBuf, SMALL_BUF_LEN)) {
LOG_ERROR << strerror(errno);
} else {
// 数据的转换 网络字节序 -》 本机字节序
std::string clientIp(clientIpBuf);
unsigned short clientPort = ntohs(cli.sin_port);
IpAddressPort ipAddressPort(clientIp, clientPort);
LOG_INFO << ipAddressPort.ToString();
}
}
return cfd;
}
int TcpServer::SendMsg(int cfd, const std::string& msg) {
int numberOfSendByte = send(cfd, msg.c_str(), strlen(msg.c_str()), 0);
if (-1 == numberOfSendByte) {
LOG_ERROR << strerror(errno);
}
return numberOfSendByte;
}
Msg TcpServer::RecvMsg(int cfd) {
char buf[LARGE_BUF_LEN] = {0};
int numberOfRecvByte = recv(cfd, buf, LARGE_BUF_LEN, 0);
if (-1 == numberOfRecvByte) {
LOG_ERROR << strerror(errno);
}
return Msg{buf, numberOfRecvByte};
}
int TcpServer::GetLfd() const {
return m_lfd;
}
linux高性能服务器开发——前四章
计算机网络自顶而下——第三章
tcp/ip详解卷
计算机网络第五版 谢希仁
计算机网络面经 汇总
参考课程