面向字节流的TCP

7 篇文章 0 订阅
4 篇文章 0 订阅

内容

TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。从这方面来说,读取TCP数据就像从串行端口读取数据一样–无法预先得知在一次指定的读调用中会返回多少字节(也就是说能知道总共要读多少,但是不知道具体某一次读多少)。

为了说明这一点,我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A上的应用程序向主机B发送一条报文。进一步假设主机A有两条报文要发送,并两次调用send来发送,每条报文调用一次。很自然就会想到从主机A向主机B发送的两条报文是作为两个独立实体,在各自的分组中发送的,如图 2-25所示。
这里写图片描述

但 不幸的是,实际的数据传输过程很可能不会遵循这个模型。主机A上的应用程序会调用send,我们假设这条写操作的数据被封装在一个分组中传送给B。实际 上,send通常只是将数据复制到主机A的TCP/IP栈中,就返回了。由TCP来决定(如果有的话)需要立即发送多少数据。做这种决定的过程很复杂,取决于很多因素,比如发送窗口(当时主机B能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(沿着主机A和B之间的网络路径一次可 以传输的最大数据量),以及连接的输出队列中有多少数据。

下图只显示了主机A的TCP封装数据时可能使用的诸多方法中的4种。在图2-26中,M11和M12表示M1的第一和第二部分,M21和M22与之类似。如图2-26所示,TCP不一定会将一条报文的全部内 容都放在一个分组(一个包)中传送出去。
这里写图片描述

现在,我们从主机B应用程序的角度来看这种情形。总的来说,主机B应用程序任意一次调用recv时,都不会对TCP发送给它的数据量做任何假设。比如,当主机B应用程序读取第一条报文时,可能会出现下列4种结果。

实际上,可能的结果不止4种,但我们忽略了出错和EOF之类的结果。我们还假设应用程序读取了所有可读的数据。

  1. 没有数据可读,应用程序阻塞,或者recv返回一条指示说明没有数据可读。到底会发生什么情况取决于套接字是否标识为阻塞,以及主机B的操作系统为系统调用recv指定了什么样的语义。

  2. 应用程序获取了报文M1中的部分而不是全部数据。比如,发送端TCP像图2-26D那样对数据进行分组就会发生这种情况。

  3. 应用程序获取了报文M1中所有的数据,除此之外没有任何其他内容。如果像图2-26A那样对数据分组就会发生这种情况。

  4. 应用程序获取了报文M1的所有数据,以及报文M2的部分或全部数据。如果像图2-26B或图2-26C那样对数据进行分组就会发生这种情况。

注意,这里还有一个定时问题。如果主机B的应用程序在主机A发送了第二条报文之后一段时间内都没有读取第一条报文,那么这两条报文都会成为可读的。这就和图2-26B所示情况相同了。这些描述说明,通常,在任意指定时刻,可读的数据量都是不确定的。

需要再次说明的是,TCP是一个流协议(stream protocol),尽管数据是以IP分组的形式传输的,但分组中的数据量与send调用中传送给TCP多少数据并没有直接关系。而且,接收程序也没有什么可靠的方法可以判断数据是如何分组的,因为在两次recv调用之间可能会有多个分组到来。即使接收端应用程序的响应非常及时,也可能会发生这种情况。例如,一个分组丢失了,而且后继分组都安全到达,TCP会将后继分组中的数据保存起来,直到重传第一个分组并正确收到为止。此时,所有数据对应用程序都是可用的。

TCP会记录它发送了多少字节,以及确认的字节,但它不会记录这些字节是如何分组的。实际上,有些实现在重传丢失分组的时候传送的数据可能比原来的多一些或少一些。

对TCP应用程序来说,就没有”分组(包)”这种概念。如果应用程序的设计与TCP对数据的分组方式有所关联,就应该考虑重新设计这个应用程序了。

既然任意一次指定的读操作中返回的数据量都是不可预测的,就必须在应用程序中做好应对这种情况的准备,这些情况下边界都是由应用程序级维护的。(交给上层处理)

最简单的情况就是定长报文。在这种情况下,只需要读取报文中固定数量的字节就可以了。

实现

在socket机制中,应用层的程序以send()函数将数据首先发送到本机系统的发送缓存中,我们称之为SendQ,意指这是一个FIFO(先进先出)的队列。这个缓存是系统决定的,并不是在我们的程序中指定的。然后socket机制负责将SendQ中的数据以字节为单位,按照顺序发送给对方的接收缓存RecvQ中。

RecvQ也是一个属于系统的FIFO缓存队列。从程序员的角度看,send()函数只负责把数据送入SendQ,而SendQ何时将数据发送则是不可控的。所以,send()通常不会阻塞,只有在不能立即将数据发送给SendQ的时候才会阻塞,这往往是因为SendQ缓存已满。

另外,SendQ并不负责统计每次send()所发送来的字节流的长度,事实上这个长度在TCP中没有意义,因为所有数据都以字节为单位按照FIFO的形式排列在队列中,而并不在乎来自于哪一次的send()

这也就是所谓的TCP无边缘保证,TCP的send()并不在乎每次传送的数据有多少,而只是致力于将数据以字节为单位按照FIFO的形式排列在SendQ队列中。我们看一下TCPServerSock和TCPClientSock的TCPSend()方法:

int TCPServerSock::TCPSend(const char* send_data,  
                           const int& data_length) const  
{  
    if (data_length > preBufferSize) {  
        sockClass::error_info("Data is too large, resize preBufferSize.");  
    }  

    int sent_length = send(    sockFD,  
                            send_data,  
                            data_length,  
                            0);  
    if (sent_length < 0) {  
        sockClass::error_info("send() failed.");  
    } else if (sent_length != data_length) {  
        sockClass::error_info("sent unexpected number of bytes.");  
    }  

    return sent_length;  
}  

int TCPClientSock::TCPSend(const char* send_data,  
                           const int& data_length) const  
{  
    if (data_length > preBufferSize) {  
        sockClass::error_info("Data is too large, resize preBufferSize.");  
    }  

    int sent_length = send(    sockFD,  
                            send_data,  
                            data_length,  
                            0);  
    if (sent_length < 0) {  
        sockClass::error_info("send() failed.");  
    } else if (sent_length != data_length) {  
        sockClass::error_info("sent unexpected number of bytes.");  
    }  

    return sent_length;  
}  

可以看到,这两个方法除了分属于不同的类名字不一样,其他都是一样的。send()的返回值是实际发送的字节长度。

在收信息的另外一边,当RecvQ没有数据时,recv()就会阻塞(默认情况下),每当有数据可接收,recv()就会返回实际接收到的数据长度。recv()同样不在乎每次接收的数据有多少,其参数只有一个最大长度限制,这个限制是应用程序分配给每次recv()储存数据的缓存大小。

所以TCP的send()和recv()不是一一对应的:send()只负责将数据写入本机的SendQ,而recv()只负责把本机RecvQ中的数据读出来。

假设send()传送了m+n字节,但是第一次到达远程目的地的RecvQ中只有m字节,于是这里的recv()就会马上返回m字节;剩下的n字节第二次才姗姗来迟,那么就需要第二次调用recv()来接收。

int TCPServerSock::TCPReceive() const  
{  
    preReceivedLength = recv(    sockFD,  
                                preBuffer,  
                                preBufferSize,  
                                0);  
    if (preReceivedLength < 0) {  
        sockClass::error_info("recv() failed.");  
    } else if (preReceivedLength == 0) {  
        std::cout << "Client has been disconnected.\n";  
        return 0;  
    }  
    return preReceivedLength;  
}  
int TCPClientSock::TCPReceive() const  
{  
    preReceivedLength = recv(    sockFD,  
                                preBuffer,  
                                preBufferSize,  
                                0);  
    if (preReceivedLength < 0) {  
        sockClass::error_info("recv() failed.");  
    } else if (preReceivedLength == 0) {  
        std::cout << "Disconnected from server.\n";  
        return 0;  
    }  
    return preReceivedLength;  
}  

可以看到这2个方法也几乎是一模一样——除了名字和对异常信息的描述。因为我们这里并不知道需要recv()的确切长度,所以这里的TCPReceive()也跟recv()一样,有数据就返回。需要验证数据长度的,比如echo服务,我们另外写验证长度的代码。

最后需要说明的是,虽然SYN和FIN都会占用一个字节的数据,但是对于应用层的send()和recv()来说是不可见的。FIN会让recv()返回0,表示连接正常断开。

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值