Linux_IO复用_select_poll

内容

  1. 基本概念
  2. 了解接口socket
  3. 了解协议tcp/udp
  4. io复用
  5. select
  6. poll/epoll

可以参考学习的文章:

  1. 深入浅出理解select、poll、epoll的实现
  2. linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。

I/O复用

  1. select
  2. poll
  3. epoll

select

先观察其API

1
2
#include<sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  1. 参数
    1. nfds参数通常被设置为select监听的所有文件描述符中的最大值加1,表示在fd_set集合中,我们关心的描述符的总数。为什么加1呢?因为文件描述符是从0开始计数的。nfdsfd_set容量大小不一样,容量大小指的是FD_SETSIZE,即fd_set容量大小fd_set可容纳描述符的最大大小。
    2. readfds参数是select关心的读事件的集合;
    3. writefds参数是select关心的写事件的集合;
    4. exceptfds参数select关心的异常事件的集合;
    5. timeout参数设置select的超时时间。
  2. 返回值
    1. 集合中有事件就绪的描述符的个数
    2. 但是并没有告诉你具体是哪一个描述符就绪

fd_set结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<typesizes.h>
#define __FD_SETSIZE 1024

#include<sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
}fd_set;
-------------------
#define FD_SETSIZE 1024
#define NFDBITS (8*(int)sizeof(long int))
typedef struct
{
long int fds_bits[FD_SETSIZE / NFDBITS];
}fd_set;

其中,__FD_SETSIZE指出select可以关注的最大文件描述符个数,默认为1024。

__fd_mask被定义为long int的类型别称,long int在32位机占8个字节。

__NFDBITS计算的是1个__fd_mask元素所占用的位数,一个字节占8位,sizeof算出__fd_mask的字节数,相乘得其占用的bit大小。

接着,定义fds_bits,其是long int型的数组,数组大小为__FD_SETSIZE除以__NFDBITS。比如SETSIZE为1024位,NFDBITS是64位,则数组大小位1024/64=16。这里的计算主要是为了计算出数组的大小,以确定多大的数组可以正好容纳1024个位数,来记录文件描述符信息。

用到的宏函数

fd_set集合对于文件描述符的管理是按位进行的,而位只有0和1两种状态。

假如SETSIZE=1024,则可管理1024个文件描述符,如果文件描述符7有效,我们需要对位操作,使其位变为1

由于位操作过于繁琐,select API中提供了一系列宏函数来方便我们访问、操作fd_set集合状态。

1
2
3
4
5
#include<sys/select.h>
FD_ZERO(fd_set *fdset); /*清除fdset的所有位*/
FD_SET(int fd, fd_set *fdset); /*设置fdset的位fd*/
FD_CLR(int fd, fd_set *fdset); /*清除fdset的位fd*/
int FD_ISSET(int fd, fd_set *fdset);/*测试fdset的位fd是否被设置*/

select编程思路

最好另外定义一个整型数组,其大小为我们预测将要出现的描述符的最多数目。用作我们存放描述符的容器。初始化时将数组值一律设为-1,表示容器中该位置还没有存放描述符。如果在某一时刻有一个描述符有了消息,我们就将该描述符数值覆盖到这个容器中第一个为-1的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define MAX 10
void fds_init(int fds[])
{
for(int i = 0;i<MAX;++i)
{
fds[i] = -1;
}
}
void fds_add(int fd, int fds[])//向fds容器中添加描述符fd
{
if(fd<0)
{
printf("无效的描述符\n");
return;
}
for(int i = 0;i<MAX;++i)
{
if(fds[i]==-1)
{
fds[i] = fd;
return;
}
}
printf("容器已满,无法添加该描述符\n");
}
void fds_del(int fd, int fds[])
{
for(int i = 0;i<MAX;++i)
{
if(fds[i]==fd)
{
fds[i] = -1;
return;
}
}
printf("没有找到该描述符\n");
}
int main()
{
int fds[MAX];
fds_init(fds);
}

示例-TCP服务使用select处理多个套接字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
int main()
{
int sockfd = socket_init();//socket_init封装了bind(ip:port)的操作,还封装了对sockfd进行listen的操作,并设置了监听队列大小。
assert(sockfd!=-1);

int fds[MAX];
fds_init(fds);

fds_add(sockfd, fds);

fd_set fdset;//此处的fd_set即<sys/select.h>库中API提供的fd_set结构体,实际上是一个有1024个“二进制位”的数组。
while(1)//将fds[MAX]中所有有效的(即>=0)描述符全部“加入”fdset中,
//即把fdset中与某有效描述符对应的[位]的状态设为1。
{
FD_ZERO(&fdset); //把1024位全清零。
int maxfd = -1;//记录当前最大的描述符数值是多少,方便过后调用select传入参数nfds。
for(int i = 0; i<MAX; ++i)
{
if(fds[i] == -1)
{
continue;
}
FD_SET(fds[i],fdset); //fds[i] != -1, 说明有效
if(maxfd<fds[i])//寻找最大描述符数值
{
maxfd = fds[i];
}
}//end for

//此时,我们已经把开始创建的sockfd添加到了fdset中。
//下面就可以用select来监测该套接字是否有消息了。
//比如,sockfd监听到了客户端的connect信息,
//则select就可以探测到fdset中对应的sockfd位处于消息就绪态,
//则select就可以不再阻塞,立马返回。
struct timeval tv = {5,0};
//返回在fdset集合中有信息的描述符的个数。
int n = select(maxfd+1, &fdset, NULL, NULL, &tv);
if(n < 0)
{
printf("select err\n");
}
else if(n == 0)
{
printf("time out\n");
}
else
{
for(int i = 0; i<MAX; ++i)//依然需要根据fdset查询:
//目前是哪个描述符有事件产生,
//fdset的过滤又需要根据fds的记录进行遍历。
{
if(fds[i] == -1)continue;
if(FD_ISSET(fds[i], &fdset))//此处判断ISSET
//即是判断我们关注的描述符是否有事件产生。
//为什么此时标志位为1一定有事件产生?
//因为在这之前我们进行了select,
//select不仅说明有事件产生,它还做了更多的工作:
//将我们关心的描述符却在其上没有事件产生的标志位置0。
//因此目前所有标志位为1的描述符均有事件。
{
//以下才是核心业务代码,抓住了有事件产生的描述符,
//对这些描述符我们的处理流程,对于不同类型的描述符
//需要不同的处理流程。比如sockfd用accept处理,
//accept返回1个新描述符c,则先将其加入fds容器,
//下一轮再用recv处理描述符c的消息。
if(fds[i] == sockfd)//处理监听套接字sockfd
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n", c);
//此处的fds_add是用户自定义的函数,添加的是fds自定义数组
fds_add(c, fds);//只是收到fds容器,下次while扫描才将c加到fdset
}
else//处理收发套接字,在此程序,除了sockfd皆为收发套接字c
{
char buff[128] = {0};
int num = recv(fds[i], buff, 127, 0);
if(num <= 0)
{
printf("client close\n");
close(fds[i]);
fds_del(fds[i], fds);
}
else//num > 0,读到了数据
{
printf("recv(c = %d) = %s\n",fds[i],buff);
send(fds[i], "ok", 2, 0);
}
}
}//end if(ISSET(fds[i], &fdset))
}//end for(int i = 0; i<MAX; ++i)
}//end if(n > 0)
}//while end
}

场景情况:如果客户端与select服务端已建立连接,而客户端进程结束,select会一直阻塞、未感知吗?–不会。

因为客户端的进程结束,也算是一种读事件,相当于通知服务端该套接字连接结束了。那么服务端recv会返回0,达到关闭该套接字的条件,关闭后,别忘了在fds容器中删除掉该描述符。

如果忘记了close该套接字,且忘了fds_del该描述符,那么如果客户端结束进程,服务端就会一直打印"client close",因为select一直在探测此描述符有无读事件,若该套接字连接关闭,那么此描述符一直有读事件,recv返回0,由于没有fds_del,每次都会关注,所以每次都会打印"client close"。

poll

可以理解为加强版的select。

先观察其API

1
2
#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
  1. fds参数是一个pollfd结构类型的指针,可指向一段连续空间(数组),因此很灵活,大小可按需声明。它可以指定我们感兴趣的文件描述符上发生的可读、可写和异常等事件。定义如下

    1
    2
    3
    4
    5
    6
    struct pollfd
    {
    int fd; //文件描述符
    short events; //注册的事件类型,按位标志
    short revents; //实际发生的事件,按位标志,由内核填充
    }
    1. 其中,fd成员指定文件描述符。
    2. events成员告诉poll监听fd上的哪些事件类型,他可以是一系列事件类型的按位或。常见的事件类型有:POLLIN(数据可读)、POLLOUT(数据可写)。
    3. revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。

poll编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
void poll_fds_init(struct pollfd* fds)
{
for(int i = 0;i<MAX;++i)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void poll_fds_add(int fd, struct pollfd* fds)
{
for(int i = 0;i<MAX;++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = POLLIN;//只关注读事件
fds[i].revents = 0;
break;
}
}
}
void poll_fds_del(int fd, struct pollfd* fds)
{
for(int i = 0;i<MAX;++i)
{
if(fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
#define MAX 10
int main()
{
int sockfd = socket_init();
assert(sockfd != -1);
struct pollfd poll_fds[MAX];
poll_fds_init(poll_fds);
poll_fds_add(sockfd,poll_fds);
while(1)
{
int n = poll(poll_fds,MAX,5000);//5000ms timeout
if(n < 0)printf("poll error\n");
else if(n == 0)printf("time out \n");
else
{
for(int i = 0;i<MAX;++i)
{
if(poll_fds[i].fd == -1)continue;
//short has 16bits,POLLIN is 10000000 ...,
//when revents is 10000000 ...,then the read event is going
if(poll_fds[i].revents & POLLIN)//revents & POLLIN 不为0 则代表有读事件产生
{
if(poll_fds[i].fd == sockfd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if(c < 0)
{
continue;
}
printf("accept:%d\n", c);
poll_fds_add(c, poll_fds);
}
else
{
char buff[128] = {0};
int num = recv(poll_fds[i].fd, buff, 127, 0);
if(num <= 0)
{
close(poll_fds[i].fd);
poll_fds_del(poll_fds[i].fd, poll_fds);
printf("client close\n");
}
else
{
printf("recv(%d):%s\n", poll_fds[i].fd, buff);
send(poll_fds[i].fd, "ok", 2, 0);
}
}

}
if(poll_fds[i].revents & POLLOUT)
{
//...
}
}
}
}
}

与select的一处细节区别:

每次select除了监测fd_set有效描述符上有无事件,其次还将没有事件的描述符从fd_set移除(将该描述符对应在fd_set上的位进行置0操作)(这样就得每次select之前都要重新注册一遍我们关注的描述符(即用户和内核共同操作FD_SET、FD_CLR等)),然后下面过滤有事件的描述符时,只要找到fd_set集合哪个位是1状态即就找到了有事件产生的描述符。

而poll的用法是:用户只管注册events,实际上的有无事件由内核来进行对revents的填充,以此来更好地区别该描述符是否有事件产生。这样,就不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events。我们只要把要关心的描述符的fd置成非-1,以及管理好要关心的哪些事件类型events即可。

与select相比的优点

  1. 可以监听的描述符的最大数目可以超过1024个,大小按需自拟。
  2. 可以监听的事件类型数目变多、变细了,更强大了。
  3. 不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events

总结

select和poll的总体实现流程:

  • 用户程序

select用fd_set结构体(默认是1024个bit位的数组);poll用struct pollfd fds[MAX]结构体。

select和poll通常被循环调用。每调用一次,就拷贝一次结构体数组给内核。

linux在系统调用进入内核时,为什么要将参数从用户空间拷贝到内核空间?不能直接访问,或是使用memcpy吗?非要使用copy_from_user才行吗? - 针对为什么select/poll调用一次拷贝一次数组到内核态空间。

  • 内核轮询
    内核对若干个文件描述符进行轮询扫描。查看之上是否有事件发生。
    时间复杂度,为O(n)
  • select/poll返回后
    返回值仅仅告诉用户程序发生了事件的描述符个数,并未告诉具体哪个描述符。
    用户程序还需要再次轮询一遍。O(n)
    select

网络_TCP

四次挥手

TCP

特点:

  1. 面向连接的
    1. 三次握手–在客户端connect()时
      1. 必须是三次
    2. 四次挥手–在任一方close时
      1. 有时可以三次
  2. 可靠的
    1. 应答确认
    2. 超时重传
    3. 乱序重排
    4. 去重
    5. 滑动窗口进行流量控制
  3. 流式服务
    1. 发送和接收的次数可能不一致。
      1. 连续多次发送的数据可能会被对方一次性收到。
    2. 起始末尾加标记
    3. send之后recv隔开
  4. tcp是有状态的
    1. 开始closed
    2. listenconnecting(三次握手中)
    3. established(已完成握手)
    4. FIN_WAIT_1/FIN_WAIT_2
    5. TIME_WAIT
      1. 可靠地终止TCP的连接
      2. 让迟来的报文在这一段时间被识别,即收集后丢弃,以防止误传给下一个使用该端口的连接。

问题:1、TCP/IP协议详解 卷一;2、UNIX网络编程 卷一

  1. 三次握手,四次挥手
  2. 应答确认、超时重传机制
  3. 乱序重排、去重、滑动窗口进行流量控制
  4. 什么是粘包?怎么解决?
  5. 中间转换状态的意义?TIME_WAIT状态的意义?

TCP首部

image-20220325091330300

  • 序列号(Sequence Number)字段标识了TCP发送端到TCP接收端的数据流的一个字节,该字节代表着这个报文段对应的数据中的第一个字节。如果我们考虑在两个应用程序之间的一个方向上流动的数据流,TCP给每个字节赋予一个序列号。这个序列号是一个32位的无符号数,到达23212^{32}-1后再循环回到0。
  • **确认号(Acknowledgment Number)**是该发送方期待接收的下一个序列号。
    • TCP可以被描述为一种“带累积正向确认的滑动窗口协议”。ACK号字段被构建用于指明在接收方已经顺序收到的最大字节(加1)。例如,如果字节11024已经接收成功,而下一个报文段包含字节20493072,那么接收方不能使用规则的ACK号字段去发信告诉发送方它接收到了这个新报文段(先保留)。然而,现代TCP有一个选择确认(Selective ACKnowledgment, SACK)选项,可以允许接收方告诉发送方它正确地接收到了次序杂乱的数据。当与一个具有选择重发(selective repeat)能力的TCP发送方搭配时,就可以实现性能的显著改善。我们将会看到TCP是如何使用重复确认(duplicate acknowledgments)以帮助它的拥塞控制和差错控制过程的。
  • 头部长度(Header Length)字段给出了头部的长度,此字段只占4位,计算时,以32位字(4字节)为单位,比如该字段是1111,则头部长度为15(32/8)=6015 * (32 / 8) = 60字节。它是必需的,因为选项(Options)字段的长度是可变的。
  • 接下来是预留位(Resv)和8个标志位,旧版本的TCP头部的标志位只用到了后6个;它们中的1个或多个可被同时启用。其中除了后6个旧版本已有的,新的2个标志位为:
    1. CWR:拥塞窗口减(发送方降低它的发送速率)
    2. ECE:ECN回显(发送方接收到了一个更早的拥塞通告)
  • 窗口大小(Window Size)字段。TCP的流量控制由每个端口使用这个字段来通告一个窗口大小来完成。这个窗口大小是字节数,从ACK号指定的,也是接收方想要接收的那个字节开始。这是一个16位的字段,限制了窗口大小到65535字节,从而限制了TCP的吞吐量性能。窗口缩放(Window Scale)选项可允许对这个值进行缩放,给高速和大延迟网络提供了更大的窗口和改进性能。
  • TCP校验和(TCP Checksum)字段覆盖了TCP的头部、数据以及IP头部中的一些字段;是一个强制性的字段,由发端计算和存储,并由收端进行验证。TCP检验和的计算与UDP检验和的计算相似,使用一个伪首部。
  • 紧急指针(Urgent Pointer)字段。紧急指针是一个正偏移量,和序列号字段中的值相加表示紧急数据最后一个字节的序列号。这种TCP紧急机制是发送端向另一端提供特殊标志数据的方法。
  • 选项字段。最常见的就是“最大段大小”选项。连接的每个端点一般在它发送的第一个报文段(为了建立该连接,SYN位字段被设置的那个报文段)上指定这个选项。MSS指定该选项的发送者在相反方向上希望接收到的报文段的最大值。
  • TCP报文段的数据部分是可选的。当一个连接被建立和终止时,交换的报文段只包含TCP头部(可能带选项)而没有数据。
    • 如果这个方向上没有数据被传输,那么一个不带任何数据的头部也会用于ACK接收到的数据(称为一个pure ACK),同时通知通信方改变窗口大小(称为一个窗口更新(window update))。
    • 当一个报文段可不带数据发送时,超时操作会因此而产生一些新情况。

标志位

URG

URG,全拼是urgent。表示紧急。此标志表示:紧急指针有效

ACK

表示确认序号字段有效。需要注意,确认标志位和确认序号字段不一样,确认序号和序列号不一样。

  • 既然每个传输的字节都被计数,确认序号会填写该端所期望收到的下一个序号。因此确认序号应当是上次已成功收到数据字节序号+1。
  • 发送一个ACK与发送任何一个TCP报文段的开销是一样的,因为那个32位的ACK号字段一直都
    是头部的一部分,ACK位字段也一样。
    TCP/IP详解_卷1_第2版_英语原文如下:Sending an ACK costs nothing more than sending any other TCP segment because the 32-bit ACK Number field is always part of the header, as is the ACK bit field.
  • 只有ACK标志为1时,确认序号字段才有效;一旦一个连接建立起来,确认序号字段总是被设置, ACK标志也总是被设置为1;这个ACK位字段几乎用于所有报文段,除了初始和末尾报文段

PSH

接收方应该尽快将这个报文段交给应用层。

RST

重建连接

SYN

同步序号用来发起一个连接。

  • 当建立一个新的连接时,SYN标志变1。序列号字段随即会包含由这个主机选择的该连接的初始序列号ISN(Initial Sequence Number)
  • 该主机要发送数据的第一个字节序列号为ISN+1,因为SYN标志消耗了一个序号。消耗一个序列号也意味着使用重传进行可靠传输。因此,SYN和应用程序字节(还有FIN,稍后我们将会见到)是被可靠传输的。不消耗序列号的ACKs则不是。

FIN

FIN发端完成发送任务。

如何保证数据可靠传输

那些自身不包含可靠传递数据机制的协议。它们可能会使用一种像校验和或CRC这样的数学函数来检测接收到的有差错的数据,但是它们不尝试去纠正差错。比如对于IP和UDP则根本没有实现差错纠正。对于以太网和基于其上的其他协议,协议提供一定次数的重试,如果还是不成功则放弃。

通信媒介可能会丢失或改变被传递的消息,在这种环境下的通信问题已经被研究了多年。关于这个课题的一些最重要的理论工作由香农在1948年给出。这些工作普及了术语“比特”,并成为信息理论(information theory)领域的基础,帮助我们理解了在一个有损(可能会删除或改变比特)信道里可通过的信息量的根本限制。信息理论与编码理论(coding theory)的领域密切相关,编码理论提供不同的信息编码手段,从而使得信息能在通信信道里尽量免于出错。其中有两种手段:

  1. 使用差错校正码(基本上是添加一些冗余的比特,使得即使某些比特被毁,真实的信息也可以被恢复过来)来纠正通信问题是处理差错的一种非常重要的方法。
  2. 另一种方法是简单地“尝试重新发送”,直到信息最终被接收。这种方法,称为自动重复请求(Automatic Repeat Request, ARQ),构成了许多通信协议的基础,包括TCP在内。

ARQ和重传

如果我们考虑的不只是单个通信信道,而是几个的多跳级联,我们会发现不只会碰到前面提到的那几种差错类型(分组比特差错),而且还会有更多其他的类型。这些问题可能发生在中间路由器上,是几种在讨论IP时会遇到的问题:分组重新排序,分组复制,分组泯灭(丢失)。为在多跳通信信道(例如IP)上使用而设计的带纠错的协议必须要处理这些问题。

现在让我们来探讨能处理这些问题的协议机制。在概括性地讨论这些之后,我们会探究它们是如何被TCP在互联网上使用的。

一个直接处理分组丢失(和比特差错)的方法是重发分组直到它被正确接收。这需要种方法来判断:

  1. 接收方是否已收到分组;
  2. 接收方接收到的分组是否与之前发送方发送的一样。

接收方给发送方发信号以确定自己已经接收到一个分组,这种方法称为确认(acknowledgment, ACK)。最基本的形式是,发送方发送一个分组,然后等待一个ACK。当接收方接收到这个分组时,它发送对应的ACK。当发送方接收到这个ACK,它再发送另一个分组,这个过程就这样继续。这里会有一些有意思的问题:

  1. 发送方对一个ACK应该等待多长时间?
  2. 如果ACK丢失了怎么办?
  3. 如果分组被接收到了,但是里面有错怎么办?

第一个问题其实挺深奥的。决定去等待多长时间与发送方期待(expect)为一个ACK等待多长时间有关。现在确定这个时间可能比较困难,因此我们推迟对这个技术的讨论。

第二个问题的答案比容易:如果一个ACK丢失了,发送方不易区分ACK报文段丢失与原分组丢失的情况,所以它简单地再次发送原分组。当然,这样的话,接收方可能会接收到两个或更多的拷贝,因此它必须准备好处理这种情况(见下一段)。

至于第三个问题,我们可以借助某个编码技术来解决。使用编码来检测一个大的分组中的差错(有很大的概率)一般都很简单,仅使用比其自身小很多的一些比特即可纠正。更简单的编码一般不能纠正差错,但是能检测它们。这就是校验和与CRC会如此受欢迎的原因。然后,为了检测分组里的差错,我们使用一种校验和形式。当一个接收方接收到一个含有差错的分组时,它不发送ACK。最后,发送方重发完整到达的无差错的分组。

到目前为止即使这种简单的场景,接收方都可能接收到被传送分组的重复(duplicate)副本。这个问题要使用序列号(sequence number)来处理。基本上,在被源端发送时,每个唯一的分组都有一个新的序列号,这个序列号由分组自身一直携带着。接收方可以使用这个序列号来判断它是否已经见过这个分组,如果见过则丢弃它。

到目前为止介绍的协议是可靠的,但效率不太高。如果从发送方到接收方传递即使一个很小的分组都要用很长时间(推迟或延迟)的话(如一秒或两秒,对卫星链路来说并非不正常),考虑一下那会怎样。发送方可以注入一个分组到通信路径,然后停下来等待直到它收到ACK。这个协议因此被称为“停止和等待”。假设没有分组在传输中丢失或者这些分组没有被无可挽回地损害,该协议的吞吐量(throughput)性能(每单位时间发送的数据量)与M/RM/R成正比,MM是分组大小,RR是往返时间(RTT),即分组越小、往返时间越大则性能就越低。如果有分组丢失和损害的话,情况甚至更糟糕:“吞吐质(goodput)”(每单位时间传送的有用数据量)明显要比吞吐量要低。

对于不会损害和丢失太多分组的网络来说,低吞吐量的原因是网络经常没有处于繁忙状态。情况与使用装配流水线时不出一个完整产品就不准新的工作进入类似。流水线大部分时间是空闲的。我们进一步对比,很明显,如果我们允许同一时间有多个工作单元进人流水线,就可以做得更好。对网络通信来说也是一样的一—如果我们允许多个分组进入网络,就可以使它“更繁忙”,从而得到更高的吞吐量。
很明显,允许多个分组同时进人网络使事情变得复杂。现在发送方必须不仅要决定什么时间注入一个分组到网络中,还要考虑注入多少个。并且必须要指出在等待ACK时,怎样

TIME_WAIT

2倍的MSL时间(Maximum Segment Lifetime)。