面试问题整理之TCP/IP和网络编程

本文为本人面试问题中关于TCP/IP和网络编程的整理汇总。

(1)常见问题

1.TCP和UDP的区别?

TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达; UDP尽最大努力交付,即不保证可靠交付

TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低

每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

TCP首部开销20字节;UDP的首部开销小,只有8个字节

TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道


2.TCP三次握手?

TCP提供的可靠数据传输服务,是依靠接收端TCP软件按序号对收到的数据分组进行逐一确认实现的。

三次握手协议指的是在发送数据的准备阶段,服务器端和客户端之间需要进行三次交互:

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的syn(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

连接建立后,客户端和服务器就可以开始进行数据传输了。

为什么客户端需要再发送一次确认?

client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用三次握手,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用三次握手的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。


3.TCP的四次挥手协议?

(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。

(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。

(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。

(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。

为什么TCP连接是三次,挥手确是四次?

在TCP连接中,服务器端的SYN和ACK向客户端发送是一次性发送的,而在断开连接的过程中,B端向A端发送的ACK和FIN是是分两次发送的。因为在B端接收到A端的FIN后,B端可能还有数据要传输,所以先发送ACK,等B端处理完自己的事情后就可以发送FIN断开连接了。

为什么在第四次挥手后会有2个MSL的延时?

MSL是Maximum Segment Lifetime,最大报文段生存时间,2个MSL是报文段发送和接收的最长时间。
假定网络不可靠,那么第四次发送的ACK可能丢失,即B端无法收到这个ACK,如果B端收不到这个确认ACK,B端会定时向A端重复发送FIN,直到B端收到A的确认ACK。所以这个2MSL就是用来处理这个可能丢失的ACK的。而且能确保下一个新的连接中没有这个旧连接的报文。


4.Linux进程间的通信方式?

linux下进程间通信的几种主要手段:
1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期 信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上, 该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,sigaction函数重新实现了signal函数);
3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点;
4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针其他通信机制运行效率较低设计的。往往与其它通信机制,如信号量结合使用, 来达到进程间的同步及互斥。
5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
6. 套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix 系统上:Linux和System V的变种都支持套接字。


5.用UDP协议通讯时怎样得知目标机是否获得了数据包?

在UDP之上自定义一个通讯协议:每个数据包中包含一个唯一标识,可以用编号也可以用时间;接收端收到数据包后回发一个数据包,包含收到的这个唯一标识;发送端在预定时间内没有收到回执则自动重发,重发一定次数后仍未收到回执则认为发送失败。


6.列出常见的信号,信号怎么处理?

SIGHUP:本信号在用户终端结束时发出,通常是在终端的控制进程结束时,通知同一会话期内的各个作业,这时他们与控制终端不在关联。比如,登录linux时,系统会自动分配给登录用户一个控制终端,在这个终端运行的所有程序,包括前台和后台进程组,一般都属于同一个会话。当用户退出时,所有进程组都将收到该信号,这个信号的默认操作是终止进程。此外对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
SIGINT:程序终止信号。当用户按下CRTL+C时通知前台进程组终止进程。
SIGQUIT:Ctrl+\控制,进程收到该信号退出时会产生core文件,类似于程序错误信号。
SIGILL:执行了非法指令。通常是因为可执行文件本身出现错误,或者数据段、堆栈溢出时也有可能产生这个信号。
SIGTRAP:由断点指令或其他陷进指令产生,由调试器使用。
SIGABRT:调用abort函数产生,将会使程序非正常结束。
SIGBUS:非法地址。包括内存地址对齐出错。比如访问一个4个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法地址的非法访问触发。
SIGFPE:发生致命的算术运算错误。
SIGKILL:用来立即结束程序的运行。
SIGUSR1:留给用户使用,用户可自定义。
SIGSEGV:访问未分配给用户的内存区。或操作没有权限的区域。
SIGUSR2:留给用户使用,用户可自定义。
SIGPIPE:管道破裂信号。当对一个读进程已经运行结束的管道执行写操作时产生。
SIGALRM:时钟定时信号。由alarm函数设定的时间终止时产生。
SIGTERM:程序结束信号。shell使用kill产生该信号,当结束不了该进程,尝试使用SIGKILL信号。
SIGSTKFLT:堆栈错误。
SIGCHLD:子进程结束,父进程会收到。如果子进程结束时父进程不等待或不处理该信号,子进程会变成僵尸进程。
SIGCONT:让一个停止的进程继续执行。
SIGSTOP:停止进程执行。暂停执行。
SIGTSTP:停止运行,可以被忽略。Ctrl+z。
SIGTTIN:当后台进程需要从终端接收数据时,所有进程会收到该信号,暂停执行。
SIGTTOU:与SIGTTIN类似,但在写终端时产生。
SIGURG:套接字上出现紧急情况时产生。
SIGXCPU:超过CPU时间资源限制时产生的信号。
SIGXFSZ:当进程企图扩大文件以至于超过文件大小资源限制时产生。
SIGVTALRM:虚拟使用信号。计算的是进程占用CPU调用的时间。
SIGPROF:包括进程使用CPU的时间以及系统调用的时间。
SIGWINCH:窗口大小改变时。
SIGIO:文件描述符准备就绪,表示可以进行输入输出操作。
SIGPWR:电源失效信号。
SIGSYS:非法的系统调用。


7.守护进程?

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。


8.select服务器端的实现,select与epoll的区别?

select的几大缺点:
1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3. select支持的文件描述符数量太小了,默认是1024

epoll解决了以上确定:
1. 支持的FD上限是最大可以打开文件的数目;
2. IO效率不随FD数目增加而线性下降,只会对”活跃”的socket进行操作;
3. epoll在epoll_ctl函数中,每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
4. 使用mmap加速内核与用户空间的消息传递.


9.什么是滑动窗口?

  滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

  TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。

  滑动窗口机制为端到端设备间的数据传输提供了可靠的流量控制机制。然而,它只能在源端设备和目的端设备起作用,当网络中间设备(例如路由器等)发生拥塞时,滑动窗口机制将不起作用。


10.connect会阻塞,怎么解决?

  • 非阻塞

步骤1: 设置非阻塞,启动连接

实现非阻塞 connect ,首先用Fcntl()函数把 sockfd 设置成非阻塞的。这样调用connect 可以立刻返回,根据返回值和 errno 处理三种情况:

(1) 如果返回 0,表示 connect 成功。

(2) 如果返回值小于 0, errno 为 EINPROGRESS, 表示连接
建立已经启动但是尚未完成。这是期望的结果,不是真正的错误。

(3) 如果返回值小于0,errno 不是 EINPROGRESS,则连接出错了。

步骤2:判断可读和可写

然后把 sockfd 加入 select 的读写监听集合,通过 select 判断 sockfd 是否可写,

(1) 如果连接建立好了,那么 sockfd 是可写的

(2) 如果连接发生错误,sockfd 也是可读和可写的。

步骤3:使用 getsockopt 函数检查错误

getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len)

在 sockfd 都是可读和可写的情况下,我们使用 getsockopt 来检查连接是否出错。

步骤4:重新将套接字设置为阻塞

  • 用信号设置超时

定义信号处理函数:


sigset(SIGALRM, u_alarm_handler);
alarm(2);
code = connect(socket_fd, (struct sockaddr*)&socket_st, sizeof(struct sockaddr_in));
alarm(0);
sigrelse(SIGALRM);

  首先定义一个中断信号处理函数u_alarm_handler,用于超时后的报警处理,然后定义一个2秒的定时器,执行connect,当系统connect成功,则系统正常执行下去;如果connect不成功阻塞在这里,则超过定义的2秒后,系统会产生一个信号,触发执行u_alarm_handler函数,当执行完u_alarm_handler后,程序将继续从connect的下面一行执行下去。

其中,处理函数可以如下定义,也可以加入更多的错误处理。

void u_alarm_handler()
{
}

11.TCP的connect函数与UDP的connect函数区别?

  在网络编程中,connect函数通常用于客户端建立tcp连接。tcp连接的建立实际上就是三次“握手”的过程。

  udp协议提供的是面向非连接的服务,通信双方不需要建立连接。一方只需要建立好套接字,并显式或由系统绑定地址和端口号后就可以发送/接收数据包。和tcp不同的是,使用udp协议的数据报套接字(SOCK_DGRAM)并不限定唯一的通信方。既可以发送(sendto)数据给任意的接受方,也可以从任意的发送方接收(recvfrom)数据。

   如果希望为一个数据报套接字指定唯一的通信方时,可以使用connect来实现这一功能。需要注意的是,在数据报套接字上使用connect并不是建立连接,不存在“握手”的过程。仅仅是为这个套接字指定一个通信方,一旦指定了对方的地址,就可以通过send/recv来发送/接收数据了。而且可以在这个数据报套接字上多次调用connect函数来指定不同的通信方。
在udp中使用connect的方法和tcp中类似,只需在创建套接字时,把套接字的类型由SOCK_STREAM换成SOCK_DGRAM即可。


14.tcp头多少字节?哪些字段?


15.socket什么情况下可读?

套接字准备好读的条件:

a)该套接字接受缓冲区中的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于tcp和udp套接字而言,其默认值为1。

b)该套接字的读半部关闭(也就是接受了FIN的tcp连接)。对这样的套接字的读操作将不阻塞并返回0.(也就是返回EOF)

c)该套接字是一个监听套接字且已完成的连接数不为0。对这样的套接字的accept通常不阻塞。

d)其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这样待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

套接字准备好写的条件:

a)该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如udp套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于tcp和udp而言,其默认值通常为2048。

b)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。

c) 使用非阻塞connect的套接字已建立连接,或者connect已经以失败告终。

d) 其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。


16.写一个c程序辨别系统是大端or小端字节序。

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

#include <stdio.h>
int main(int argc, char* argv[]){
    union {
        short s;
        char c[sizeof(short)];
    } un;

    un.s = 0x0102;  // 使un为2字节,一节为1,一节为2

    if (sizeof(short) == 2) {
        if (un.c[0] == 1 && un.c[1] == 2) printf("big-endian\n");
        else if (un.c[0] == 2 && un.c[1] == 1) printf("little-endian\n");
        else printf("unknown\n");
    } else printf("sizeof(short) = %d\n", sizeof(short));
    return 0;
}

17.路由表的网段怎么存储、查找

存储:
根据RIP协议报文(包含目的网络,距离以及下一跳),若目的网络在原路由表中不存在,则直接加入路由表中;若存在,且距离比原路由表比较小,则更新距离,否则忽略。

查找:
路由表中有目的地址网络,子网掩码,下一跳这些数据;
先从数据包中获取目的地址,让该地址与路由表中的子网掩码逐个进行与操作,若结果和子网掩码对应的目的地址相匹配,则从下一跳指向的端口进行转发,否则查找下一个路由表项;


18.服务器和客户端都是怎么工作的(从创建socket到工作的流程)

TCP:
服务器端:socket(SOCK_STREAM)、bind、listen、accept
客户端: socket(SOCK_STREAM)、connect

UDP:
服务器端:socket(SOCK_DGRAM)、bind
客户端:socket(SOCK_DGRAM)


19.listen的两个参数分别是什么

int listen(int sockfd, int backlog);

backlog是为sockfd套接字维护的 未完成连接(SYN-RECV) 和 已完成连接(ESTAVBLISHED) 两个队列的大小之和。


20.当并发量很大时,可以进行哪些处理解决这个问题

服务器集群;

I/O复用

多进程:(单服务器)
收到连接就fork一个子进程处理连接;
建立进程池。

多线程:(单服务器)
收到连接就创建一个线程处理连接;
建立线程池。


21.如果select在监听的时候,描述符对端被关闭了会发生什么

如果有客户端连接请求,select函数将监听socket描述符设置为可读。注意:如果监听socket为阻塞模式,那么,当使用select进行多路分离时,可能造成select返回可读但是调用accept会被阻塞住的情况,原因是在调用accept之前客户端可能主动关闭连接或者发送RST异常关闭连接.


22.send、recv都是阻塞的,怎么控制在一定时间内完成send、recv,如果超时就不执行,怎么跟select联系起来

套接字超时:
1. 调用alarm(),超时时产生SIGALRM信号
2. 在select中阻塞等待IO,select内置定时器
3. 使用SO_RCVTIMEO和SO_SNDTIMEO套接字选项

对于recv,在select的读文件描述符集参数中打开对应的描述符的位,然后把select内置的定时器设为想要的时间就行;同理对于send,则在写文件描述符集中打开对应的位。


23.epoll底层使用了红黑树,说一下红黑树的特点和优点

红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。

满足下列条件的二叉搜索树是红黑树:
* 每个结点要么是“红色”,要么是“黑色”
* 所有的叶结点都是空结点,并且是“黑色”的
* 如果一个结点是“红色”的,那么它的两个子结点都是“黑色”的
* 如果結點是黑色的,那么它的子節點可以是紅色或者是黑色的
* 结点到其子孙结点的每条简单路径都包含相同数目的“黑色”结点
* 根结点永远是“黑色”的


24.dns怎么解析的?

主机通过ISP接入了互联网,那么ISP就会分配一个DNS服务器;
主机向ISP DNS发起查询www.baidu.com请求;
ISP DNS收拿到请求后,先检查一下自己的缓存中有没有这个地址,有的话就直接返回,如果缓存中没有的话,ISP DNS会从配置文件里面读取13个根域名服务器的地址。并向其中一台发起请求。
根服务器拿到这个请求后,知道他是com.这个顶级域名下的,所以就会返回com域中的NS记录;
ISP DNS向其中一台再次发起请求,com域的服务器发现你这请求是baidu.com这个域的,一查发现了这个域的NS,那我就返回给你,你再去查;
ISP DNS不厌其烦的再次向baidu.com这个域的权威服务器发起请求,baidu.com收到之后,查了下有www的这台主机,就把这个IP返回给你了;
然后ISPDNS拿到了之后,将其返回给了客户端,并且把这个保存在高速缓存中。


25.ipv4与ipv6:

一、==IPv6具有更大的地址空间==。IPv4中规定IP地址长度为32,最大地址个数为2^32;而IPv6中IP地址的长度为128,即最大地址个数为2^128。与32位地址空间相比,其地址空间增加了2^128-2^32个。

二、==IPv6使用更小的路由表==。IPv6的地址分配一开始就遵循聚类(Aggregation)的原则,这使得路由器能==在路由表中用一条记录(Entry)表示一片子网,大大减小了路由器中路由表的长度,提高了路由器转发数据包的速度。==

三、IPv6增加了增强的组播(Multicast)支持以及对流的控制(Flow Control),这使得网络上的多媒体应用有了长足发展的机会,为服务质量(QoS,Quality of Service)控制提供了良好的网络平台。

四、IPv6加入了对自动配置(Auto Configuration)的支持。这是对DHCP协议的改进和扩展,使得网络(尤其是局域网)的管理更加方便和快捷。

五、IPv6具有更高的安全性。==在使用IPv6网络中用户可以对网络层的数据进行加密并对IP报文进行校验,在IPV6中的加密与鉴别选项提供了分组的保密性与完整性。极大的增强了网络的安全性。==

六、允许扩充。如果新的技术或应用需要时,IPV6允许协议进行扩充。

七、更好的头部格式。IPV6使用新的头部格式,其选项与基本头部分开,如果需要,可将选项插入到基本头部与上层数据之间。这就简化和加速了路由选择过程,因为大多数的选项不需要由路由选择。

八、新的选项。IPV6有一些新的选项来实现附加的功能。


(2)知识补充

通信的真正端点不是主机而是主机中的进程。端到端之间的通信是应用进程之间的通信。


服务器端使用的端口号: 熟知端口号 (0~1023)和登记端口号 (1024~49151)
客户端使用的端口号:49152~65535


用户数据报协议UDP

UDP主要特点 :
无连接 发送前不需要建立连接
尽最大努力交付
面向报文 : 应用层交下来的报文直接加上UDP头部就往IP层扔, 不合并也不拆分
没有拥塞控制
支持一对一, 一对多, 多对一和多对多的交互通信
首部开销小, 只有8个字节


传输控制协议TCP

TCP主要特点 :
面向连接的运输层协议,使用TCP协议前,必须先建立连接。
每一条TCP连接只能有2个端点, TCP是点对点的。
提供可靠交付,无差错,不丢失,不重复,并且按序到达。
全双工通信,TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
面向字节流,TCP把应用程序交下来的数据看成仅仅是一连串的无结构字节流。


源端口和目的端口 :

同UDP端口作用
序号seq : 本报文段的数据的第一个字节的序号
确认号ack(注意和大写ACK区分) : 期望收到对方下一个报文段的第一个数据字节的序号
若确认号 = N, 则表明 : 到序号N-1为止的所有数据都已正常收到
数据偏移 : TCP报文段的首部长度
保留 : 以后用, 目前为0
紧急URG : 若URG = 1时, 说明紧急指针字段有效, 告诉系统这是紧急数据, 应尽快传送. 例如突然要中断传送
确认ACK : ACK = 1时确认号才有效, ACK = 0时确认号无效. TCP规定, 连接建立后所有传送的报文段都必须把ACK置1
推送PSH : 若PSH = 1, 则接收方收到报文段之后不再等到整个缓存满而是直接向上交付
复位RST : 当RST = 1, 说明TCP连接有严重错误, 必须释放连接再重连
同步SYN : 在连接建立时用来同步序号. 当SYN = 1, ACK = 0时表明这是一个连接请求报文段, 对方若同意建立连接, 则在响应的报文段中置SYN = 1, ACK = 1
终止FIN : 当FIN = 1, 表明此报文段的发送方数据已发送完毕, 并要求释放连接
窗口 : 告诉对方 : 从本报文段首部中的确认号算起, 接收方目前允许对方发送的数据量. 这是作为接收方让发送方设置其发送窗口的依据
检验和 : 同UDP, 检验首部和数据部分
紧急指针 : 当URG = 1时有效, 指出紧急数据的末尾在报文段的位置
选项 : 最大可40字节, 没有则为0。
a. 窗口扩大:窗口字段长度为16位,因此窗口最大为64K字节,窗口扩大将窗口值扩大至(16+S)位,S最大值规定是14.
b. 时间戳:用于计算RTT,也用于防止序号绕回。
c. 选择确认(SACK):选择确认某个连续的字节块,而不用只确认按序到达的块。
最大报文段长度MSS(Maximum Segment Size) : 每一个TCP报文段中数据字段的最大长度, 若不填写则为默认的536字节.因此所有在因特网上的主机都应该能接受的报文段长度是536+20(TCP首部长度)=556字节。


TCP的连接

每一条TCP连接唯一地被通信两端的两个端点(socket)所确定. 即 :
TCP连接 ::= {socket1, socket2} = {(IP1 : port1), (IP2 : port2)}


可靠传输的工作原理

1、停止等待协议:
(1)停止等待协议:每发送完一个分组就停止发送,等待对方确认。收到确认后再发送下一个分组。
无差错情况:收到确认再发送
出现差错:发送方超过一段时间仍没有收到确认就会重新发送(超时重传)。因此发送完必须保留分组副本,分组和确认分组都必须编号,超时计时器应该比平均往返时间长一点。
确认丢失和确认迟到:接收方如果重复收到某个分组,就说明确认信息丢失了。此时接收方会丢弃重复分组并向发送方再次发送确认。发送方重复收到确认,则把重复的确认丢弃就行。
(2)连续ARQ协议:发送方每收到一个按序到达的确认,就把发送窗口向前移动一个分组的位置。


TCP的数据编号与确认

  TCP将所要传送的整个报文(这可能包括许多个报文段)看成是一个个字节组成的数据流,并使每一个字节对应于一个序号。TCP的确认是对接收到的数据的最高序号(即收到的数据流中的最后一个序号)表示确认。但接收端返回的确认序号是已收到的数据的最高序号加1。也就是说,确认序号表示接收端期望下次收到的数据中的第一个数据字节的序号。


在发送端,TCP是怎样决定发送个报文段的时机呢

  TCP有三种基本机制来控制报文段的发送。
  第一种机制是TCP维持一个变量,它等于最大报文段长度MSS,只要发送缓存从发送进程得到的数据达到MSS字节时,就组装成—个TCP报文段,然后发送出去。
  第二种机制是发送端的应用进程指明要求发送报文段,即TCP支持的推送(push)操作。
  第三种机制是发送端的一个计时器时间到了,这时就把当前已有的缓存数据装入报文段发送出去。


TCP流量控制:

  接受方告诉发送方自己的接受窗口大小,发送方调整自己的发送窗口大小。窗口以字节为单位。
  在TCP的实现中广泛使用Nagle算法:Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。若发送端应用进程将欲发送的数据逐个字节地达到发送端的TCP缓存,则发送端就将第一个字符(—个字符的长度是一个字节)发送出去,将后面到达的字符将都缓存起来。当接收端收到对第一个字符的确认后,再将缓存中的所有字符装成一个报文段发送出去,同时继续对随后到达的字符进行缓存。只有在收到对前一个报文段的确认时才继续发送下一个报文段。当字符到达较快而网络速率较慢时,用这样的方法可明显的减少所用的网络带宽,算法还规定,当到达的字符已达到窗口大小的一半或己达到报文段的最大长度时,就立即发送一个报文段。


Nagle算法的规则:

(1)如果包长度达到MSS,则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出的小数据包(长度小于MSS)均被确认,则允许发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。


糊涂窗口综合症(silly window syndrome)

  有时也会使TCP的性能变坏。
  设想这种情况:接收端的缓存已满,而交互的应用进程一次只从缓存中读取一个字符(这样就在缓存产生1个字节的空位,然后向发送端发送确认,并将窗口设置为1个字节(但发送的数据报是40字节长)。接着,发送端又传来1个字符(但发来的IP数据报是41字节长。接收端发回确认,仍然将窗口设置为一个字节。这样进行下去,网络的效率将会很低。
  要解决这个问题,可让接收端等待一段时间,使得或者缓存已能有足够的空间容纳—个最长的报文段,或者缓存已有一半的中间处于空的状态。只要出现这两种情况之一,就发出确认报文,并向发送端通知当前的窗口大小。此外,发送端也不要发送太小的报文段,而是将数掘积累成足够大的报文段,或达到接收端缓存的空间的—半大小。
  上述两种方法(nagle和糊涂窗口)可配合使用。使得在发送端不发送很小的报文段的同时,接收端也不要在缓存刚刚有了一点小的空位置就急忙将一个很小的窗口大小通知给发送端。


拥塞控制

  若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况叫做拥塞。所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制是一个全局性过程,设计所有主机,所有的路由器。而流量控制是个端到端的问题。
  四种拥塞控制的算法:慢开始、拥塞避免、快重传、快恢复
  慢开始和拥塞避免(以下cwnd的大小单位都是报文段)
  发送方维持一个拥塞窗口cwnd, 发送方让发送窗口 等于 拥塞窗口和 接收方 接收窗口的最小值。
  发送方控制拥塞窗口的原则是:只要没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
  慢开始 : 开始发送数据时,先探测一下,有小到大逐渐增大发送窗口。cwnd = 1, 然后每经过一个传输轮次就翻倍
  拥塞避免 : 让cwnd缓慢增大, 每经过一个传输轮次就+1
  慢开始门限ssthresh : 只要发送方判断网络出现拥塞(根据就是没有按时收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一般,然后把cwnd重新设定为1
  cwnd < ssthresh, 使用慢开始算法
  cwnd > ssthresh, 使用拥塞避免算法
  cwnd = ssthresh, 随意
  快重传和快恢复
  快重传 : 接收方及时发送确认, 而发送方只要一连收到三个重复确认, 马上重传而不等待重传计时器。(需要明确的是确认指的是确认收到的有序的最大分节序号)由于尽早重传未被确认的报文段,整的网络的吞吐量提高20%
  快恢复 : 当发送方一连收到三个重复确认时, ssthresh减半, cwnd设为ssthresh,然后执行拥塞避免算法。


TCP的运输连接管理
TCP的连接建立(三次握手)

  B的TCP服务器进程先创建传输控制块TCB,准备接受客户进程A的请求。然后服务器出于LISTEN状态,等待客户A的连接请求。
  A的客户端进程首先创建TCP,然后向B发送连接请求报文段。这时首部中的同步位SYN = 1,同时选择一个初始序号seq = x。TCP规定,SYN(SYN = 1)报文段不能携带数据,但是要消耗一个序号。这时客户机A进入同步已发送状态(SYN-SENT)。
B收到连接请求的报文段后,如果同意建立连接,则向A发送确认。在确认报文段中应当把SYN和ACK的值都置为1,确认号是ack = x + 1,同时也为自己初始化一个序号seq = y。注意该报文也不能携带数据,但是需要消耗掉一个序号。此时TCP服务器进程进入同步收到状态(SYN-RCVD)。
  TCP客户进程收到服务器端的确认后,还要向B发送确认。报文段的ACK置为1,确认号ack = y + 1,而自己的序号为seq = x + 1。TCP的标准规定,ACK报文段可以携带数据,如果不携带数据则不消耗序号,在这种情况下,下一个报文段的序号仍是seq = x + 1.这时TCP连接已经建立,此时A已经进入ESTABLISHED状态。当B收到确认后,也进入ESTABLISHED状态。
至此,A与B已经建立连接,我们称作“三次握手”或者“三次联络”。


为什么要三次握手, 两次不可以吗?

   试想一下, A第一次发送请求连接, 但是在网络某节点滞留了, A超时重传, 然后这一次一切正常, A跟B就愉快地进行数据传输了. 等到连接释放了以后, 那个迷失了的连接请求突然到了B那, 如果是两次握手的话, B发送确认, 它们就算是建立起了连接了. 事实上A并不会理会这个确认, 因为我压根没有要传数据啊. 但是B却傻傻地以为有数据要来, 苦苦等待. 结果就是造成资源的浪费.
  更加接地气的解释就是 : A打电话给B
  第一次握手 : 你好, 我是A, 你能听到我说话吗? 第二次握手 : 听到了, 我是B, 你能听到我说话吗? 第三次握手 : 听到了, 我们可以开始聊天了。 三次握手其实就是为了检测双方的发送和接收能力是否正常。


TCP的连接释放(TCP四次挥手)

数据传输结束后,通信的双发都可释放连接,现在A B 都处于ESTABLISHED状态。
A的应用进程先向其TCP发出连接释放报文段,然后停止发送数据,主动关闭TCP连接。A的连接释放报文段把FIN置为1,其序号为seq = u,它等于前面已传送过的最后一个字节的序号加1。此时A进入FIN-WAIT-1状态,等待B的确认。TCP规定,FIN不携带数据,但是要消耗掉一个序号。
B收到连接释放报文段后向A发出确认,确认号是ack = u + 1,这个报文段自己的序号是v,等于B前面已传送数据的最后一个字节的序号加1。然后B进入CLOSE-WAIT状态。TCP服务器进程这时通知高层应用进程,因而从A到B这个方向的连接就释放了,这时TCP的连接处于半关闭状态,即A已经没有数据向B发送了,但是若B仍要发送数据,A依旧要接受。也就是说从B到A这个方向的连接并未关闭。
A收到来自B的报文段后进入FIN-WAIT-2状态,等待B的连续释放报文。
如果B已经没有数据向A发送了,其应用进程就会通知TCP释放连接。这时B发送连续确认报文段必须使FIN = 1,现在B的序号为w(在半关闭状态,B可能又发送了一段数据)B还必须重复已经发送过的确认号ack = u + 1。这时B进入了LAST-ACK状态,等待A的确认。
A在收到B的报文段后进行确认,其确认号ack为w + 1(TCP规定,FIN报文段需要消耗一个序号),其自己的序号为seq = u + 1。然后进入到TIME-WAIT状态。这时需要注意的是TCP连接还没有释放掉,必须经过时间等待计时器(TIME-WAIT timer)设置的2MSL(Maximum Segment Lifetime),A才进入关闭状态。MSL叫做最大报文段寿命。
SYN=1和FIN=1都要消耗掉一个字节序号,且都不能携带数据。ACK=1可以携带数据,携带则消耗序号,否则不消耗。
Q : 为什么要四次挥手, 而不是两次, 三次?
首先, 由于TCP的全双工通信, 双方都能作为数据发送方. A想要关闭连接, 必须要等数据都发送完毕, 才发送FIN给B. (此时A处于半关闭状态)
然后, B发送确认ACK, 并且B此时如果要发送数据, 就发送(例如做一些释放前的处理)
再者, B发送完数据之后, 发送FIN给A. (此时B处于半关闭状态)
然后, A发送ACK, 进入TIME-WAIT状态
最后, 经过2MSL时间后没有收到B传来的报文, 则确定B收到了ACK了. (此时A, B才算是处于完全关闭状态)
PS : 仔细分析以上步骤就知道为什么不能少于四次挥手了.


为什么要等待2MSL(Maximum Segment Lifetime)时间, 才从TIME_WAIT到CLOSED?

这有两个理由:
(1)保证A发送的最后一个ACK报文能够到达B。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN+ACK片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。如果Client不等待一段时间,则有可能会对Server发来的FIN+ACK报文回以RST导致Server无法进入CLOSED状态。
(2)防止旧连接请求报文影响新的连接。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。Client在发送完ACK后,再经过2MSL,就可以使本连接持续时间内所产生的所有报文段从网络中消失。
更加接地气的解释 :
第一次挥手 : A告诉B, 我没数据发了, 准备关闭连接了, 你要发送数据吗第二次挥手 : B发送最后的数据第三次挥手 : B告诉A, 我也要关闭连接了第四次挥手 : A告诉B你可以关闭了, 我这边也关闭了


TCP的有限状态机

TCP状态及描述
CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
CLOSE_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT:另一边已初始化一个释放
LAST_ACK:等待所有分组死掉

猜你喜欢

转载自blog.csdn.net/u013354486/article/details/80588916