Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制

三种非阻塞模式的方法: 

(1) fcntl函数

    int Mode = fcntl(sockfd, F_GETFL, 0);       //获取文件的Mode值
    fcntl(sockfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;

(2)recvfrom函数

int size = recvfrom(sockfd, (char *)recvbuffer, recvlength, MSG_DONTWAIT,(struct sockaddr*)&addr,(socklen_t*)&addlen);

参数flag不是设置为0,而是MSG_DONTWAIT

(3)ioctl()函数

        ioctl()函数,设置超时  还没细看

常见问题:

1. sendto返回值为-1

容易混淆的几点:

UDP

通过bind绑定本机地址(local addr)以及端口(local port), 实现从本机端口(local port)发送以及监听;

通过connect指定目的地址(dst addr)以及目的端口(dst port), 实现目标地址的绑定;

UDP服务器绑定,recvfrom

接收端使用 bind() 函数,来完成地址结构与 socket 套接字的绑定,这样 ip、port 就固定了,发送端在 sendto 函数中指定接收端的 ip、port,就可以发送数据了。

接收方的绑定是绑定本地任意IP,特定端口;发送方需要知道接收方的IP地址,和接收方用的哪个端口接收,这是UDP的,而TCP不是这样的。TCP建立连接后,就已经知道客户端地址了。

1. TCP服务器

服务器一直都是监听的任意IP地址,端口号用于区分接收哪个客户端发来的数据。客户端代码向服务器哪个端口发,服务器就绑定哪个端口。

2. TCP客户端

3. UDP发送端

发送端不需要绑定操作,只需要知道IP和端口号,所以会有一个struct sockaddr_in addr;

发送,写的是对面的地址。

4. UDP接收端

相反,接收端肯定是需要绑定的了。就是监听,和TCP监听一样,监听任意IP地址,绑定发送端发送地址的端口号

一·、UDP协议及其工作原理

(1) 特点

面向无连接的协议。

不保证数据一定能够到达对端,

不保证数据能够按照顺序到达对端

udp数据的可靠性需要应用层来保证。

UDP没有拥塞控制,网络出现的拥塞不会使源主机的发送速率降低

UDP支持一对一、一对多、多对一和多对多的交互通信

UDP消息头开销小,只有8个字节(TCP消息头共20个字节)

UDP相比较TCP更高效,牺牲效率

(2)udp和tcp的区别

Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制_第1张图片

udp协议的格式非常简单,它只有8个字节的首部,后面的全是数据。

UDP是面向数据包的,对应用层数据既不合并也不拆分(保留数据包边界,不粘包

二、UDP在Linux下的编程方式

2.1 参考链接:

(10条消息) UDP的编程流程_liqiao_418的博客-CSDN博客_udp流程

2.2 编程流程

阻塞,非阻塞模式

服务端:

1.创建套接字

2.初始化socket地址

3.设置阻塞或者非阻塞模式

4.绑定服务端端口

5.发送或者接收

6.关闭套接字


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define UDPPORT 60060
int main()
{
    int sockfd;
	struct sockaddr_in addr;
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(UDPPORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);//客户端IP
    //绑定服务器UDP端口
	int bret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
	if (bret < 0)
	{
		perror("bind fail");
		exit(1);
	}
    char recvbuffer[1024];
    int recvlength = 0;
    recvlength = 1;
    int sendlength=0,actual_send_length=0;
    unsigned char sendbuffer[1024];
    int Mode = fcntl(sockfd, F_GETFL, 0);	   //获取文件的Mode值。
	fcntl(sockfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;
    int addlen = sizeof(addr);
    while(1)
    {
        int size = recvfrom(sockfd, (char *)recvbuffer, recvlength, 0,(struct sockaddr*)&addr,(socklen_t*)&addlen);
        if(size<0)
            perror("recv error");
        else    
        {
            printf("%c\n", recvbuffer[0]);
            actual_send_length=sendto(sockfd,(char*)"I'm hear that you sending is:",29,0,(struct sockaddr*)&addr, sizeof(addr));
            //actual_send_length=sendto(sockfd,(char*)recvbuffer,size,0,(struct sockaddr*)&addr, sizeof(addr));
            if(actual_send_length<0)
                perror("send error");
            else
                printf("send success\n");
        }
        //usleep(500000);
    }
}

客户端:

1.创建套接字

2.初始化socket地址

3.设置阻塞或者非阻塞模式

4.发送或者接收

5.关闭套接字


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define UDPPORT 60060
int main()
{
    string cockpit_ip="127.0.0.1";
    int socketfd = 0;
    struct sockaddr_in addr;//服务器地址
    socketfd = socket(AF_INET, SOCK_DGRAM, 0);
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(UDPPORT);//服务器端口
    addr.sin_addr.s_addr = inet_addr("cockpit_ip.c_str()");//服务器IP

    int on=1;
    setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR | SO_BROADCAST,&on,sizeof(on));

    unsigned char sendbuffer[1024],recvbuffer[1024];
    int sendlength=0,actual_send_length=0,actual_recv_length=0,recvlength=0;
    sendbuffer[0]='A';
    sendlength = 1;
    recvlength = 1;
    //int Mode = fcntl(socketfd, F_GETFL, 0);	   //获取文件的Mode值。
	//fcntl(socketfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;
    int addlen = sizeof(addr);
    while (1)
    {
        actual_send_length = sendto(socketfd,(char*)sendbuffer,sendlength,0,(struct sockaddr*)&addr, sizeof(addr));
        if(actual_send_length<0)
            perror("send error");
        else
            printf("actual_send_length = %d\n",actual_send_length);
            
        actual_recv_length = recvfrom(socketfd, (char *)recvbuffer, 29, 0,(struct sockaddr*)&addr, (socklen_t*)&addlen);
        if(actual_recv_length>0)
            printf("%s\n", recvbuffer);
        else
            perror("recv error");
       //usleep(500000);
    }
}

2.3 相关函数

服务器端:socket(),  bind(),  recvfrom()/sendto(),  close();

客户端:socket(),  sendto()/recvfrom(),  close();

(1) socket()

#include
#include
int socket(int domain,int type,int protocol);//创建socket

domain  表示底层协议族。其中IPv4用AF_INET(Protocol Family of Internet),IPv6用AF_INET6。
type 指定服务类型。服务类型主要有SOCK_STREAM(字节流服务)服务和SOCK_DGRAM服务(数据报服务)。对于TCP/IP协议族来说,SOCK_STREAM表示传输层使用TCP协议SOCK_DGRAM表示传输层使用UDP协议。
protocol 在前两个参数构成的协议集合下,再选择一个具体的协议。一般设置为0,表示使用默认协议。

socket()系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

(2)绑定bind()

#include
#include
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addlen);//命名(绑定)socket

bind的作用是将未命名的sockfd文件描述符指向my_addr所指的socket地址。其中socket地址长度由参数addlen指出。

bind成功时返回0,失败则返回-1并设置errno。

其中struct sockaddr是通用的socket地址,而TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6,这里我们只介绍sockaddr_in:

struct sockaddr_in
{
    sa_family_t sin_family;  //地址族:AF_INET
    u_int16_t sin_port;      //端口号(要用网络字节序)
    struct in_addr sin_addr; //IPv4地址结构体
};
struct in_addr //IPv4地址结构体
{
    u_int32_t s_addr;  //IPv4地址(要用网络字节序)
};

(3) 转化函数

首先看端口号,两台主机之间要通过TCP/IP协议进行通信的时候需要调用相应的函数进行主机序 和网络序的转换。因为主机字节序一般为小端模式(Little-Endian),而网络字节序大端模式(Big-Endian),也就是说两者的存储方式不同。所以我们介绍4个函数来完成主机字节序和网络字节序之间的转换:

#include
unsigned long int htonl(unsigned long int hostlong);//主机字节序转网络字节序(32bit的长整型)
unsigned short int htonl(unsigned short int hostshort);//主机字节序转网络字节序(16bit的短整型)
unsigned long int ntohl(unsigned long int netlong);//网络字节序转主机字节序(32bit的长整型)
unsigned short int ntohs(unsigned short int netshort);//网络字节序转主机字节序(16bit的短整型)

(4)3个IP地址转换函数

#include
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。

inet_aton函数完成inet_addr函数同样的功能,但是将转化结果存储于参数inp指向的地址结构中。该函数成功时返回1,失败返回-1。

inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。

(5)数据读写

对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。UDP与TCP不同,UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址。所以UDP的读写函数比TCP的读写函数参数要多。

#include
#include
int recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);//读取sockfd上的数据

int sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t addrlen);//往sockfd上写入数据

Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制_第2张图片

recvfrom/sendto系统调用也可以用于面向连接的socket数据读写,只需要把最后两个参数都设置为NULL以忽略发送端、接收端的socket地址(因为我们已经建立了连接,所以已经知道其socket地址了)。

通过 setsockopt() /getsockopt() 可存取指定socket的属性值

三 、深入UDP 数据收发(上)

① 问题:如何进行一对多的UDP数据发送?
② UDP通信中的广播
广播是向同一网络中的所有主机传输数据的方法
广播类型
直接广播:IP地址中除网络地址外,其余主机地址均设置为1(eg:192.168.1.255【网络地址  主机地址】)
本地广播:无需知道网络地址,使用255.255.255.255作为IP地址使用
区别:
本地广播数据不经过路由器寻址,直接发送到本地主机

Linux 网络开发必学课程(七)UDP 数据收发实战、深入UDP 数据收发_Exp.Joker的博客-CSDN博客_linux udp 接收数据

四、TCP协议及其原理

网络原理——TCP协议_cervello的博客-CSDN博客_tcp网络

TCP的三次握手与四次挥手理解及面试题(很全面)_青柚_的博客-CSDN博客_三次握手

五、TCP编程流程

5.1 编程流程

Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制_第3张图片

服务端

1.创建socket

2.初始化socket地址

3.绑定

4.监听

5.设置是否非阻塞

6.接收客户端连接

7.接收或发送

8.关闭

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/*
步骤1:创建一个TCP的socket
步骤2:绑定socket和端口号
步骤3:监听端口号
步骤4:接受请求连接
步骤5:读取字符
步骤6:关闭
*/
using namespace std;
#define ServerPort 60050
string cockpit_ip ="127.0.0.1";
int main()
{
    int listenfd,connfd;//listenfd   socket描述符
	//创建一个TCP的socket
    if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
        printf(" create socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }
	struct sockaddr_in servaddr;
	 //先把地址清空,检测任意IP
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(ServerPort);
	//地址绑定到listenfd
    if ( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf(" bind socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }
    //监听端口号listenfd  最多10个等待
    if( listen(listenfd,10) == -1) {
        printf(" listen socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }
    printf("====waiting for client's request=======\n");
    //accept 和recv,注意接收字符串添加结束符'\0'

	char buff[4096];
    int n;    
    //获取文件的Mode值
    int Mode = fcntl(listenfd, F_GETFL, 0);
	fcntl(listenfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
    while(1)
    {
	//接受请求连接
        if( (connfd = accept(listenfd, (struct sockaddr *)NULL, NULL))  == -1) {
            //printf(" accpt socket error: %s (errno :%d)\n",strerror(errno),errno);
            //return 0;
        }
		//读取字符
        n = recv(connfd,buff,10,0);
        printf("recv msg from client:%s\n",buff);
    }
    close(connfd);
	//关闭连接
    close(listenfd);
    return 0;   
}

客户端

1.创建socket

2.初始化socket地址

3.设置是否非阻塞

4.接收或发送

5.关闭

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define ServerPort 60050
string cockpit_ip ="127.0.0.1";
int main()
{
    int len=0;		//总长度
	int buffer[65535];
    //1.建立套接字
	int TCP_Client_Sockfd;
	TCP_Client_Sockfd = socket(AF_INET, SOCK_STREAM, 0);//报式套接字 制定某个协议的特定类型为该TYPE的特定类型
	if (TCP_Client_Sockfd == -1)
	{
		printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
	}
    //2.建立连接
	//2.1设置要连接的服务器的地址
	struct sockaddr_in	Serveraddr;
	bzero(&Serveraddr, sizeof(struct sockaddr_in));
	Serveraddr.sin_family = AF_INET;//IPV4
	Serveraddr.sin_port = htons(ServerPort);//服务器应用程序端口
	Serveraddr.sin_addr.s_addr = inet_addr(cockpit_ip.c_str());//服务器IP
	//2.连接服务器  阻塞连接和非阻塞连接
	int Mode = fcntl(TCP_Client_Sockfd, F_GETFL, 0);        //获取文件的Mode值。
	fcntl(TCP_Client_Sockfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
    int Connect_Time=0,Connect_Flag = 0;
    //TCP不断尝试连接
    while(1)
	{
		Connect_Flag = connect(TCP_Client_Sockfd, (struct sockaddr*)(&Serveraddr), sizeof(sockaddr_in));
		if ( Connect_Flag == -1)
		{
			printf("connect error\n");
			Connect_Time++;
			usleep(400000);//400ms
			if(Connect_Time>=40)
				break;
		}
		else if(Connect_Flag == 0)
		{
			printf("connect success\n");
			break;
		}
	}
    struct tcp_info info; 
    int len1=sizeof(info); 
    while(1)
	{	
		getsockopt(TCP_Client_Sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len1); 
		if((info.tcpi_state!=TCP_ESTABLISHED))  // 则说明断开  else 未断开
		{
				close(TCP_Client_Sockfd);
				TCP_Client_Sockfd = socket(AF_INET, SOCK_STREAM, 0);//报式套接字 制定某个协议的特定类型为该TYPE的特定类型
				if (TCP_Client_Sockfd == -1)
				{
					printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
				}
				//2.建立连接
				//2.1设置要连接的服务器的地址
				struct sockaddr_in	Serveraddr;
				bzero(&Serveraddr, sizeof(struct sockaddr_in));
				Serveraddr.sin_family = AF_INET;//IPV4
				Serveraddr.sin_port = htons(ServerPort);//服务器应用程序端口
				Serveraddr.sin_addr.s_addr = inet_addr(cockpit_ip.c_str());//服务器IP
				//2.连接服务器  阻塞连接和非阻塞连接
				int Mode = fcntl(TCP_Client_Sockfd, F_GETFL, 0);        //获取文件的Mode值。
				fcntl(TCP_Client_Sockfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
				while(1)
				{
					connect(TCP_Client_Sockfd, (struct sockaddr*)(&Serveraddr), sizeof(sockaddr_in));
					getsockopt(TCP_Client_Sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len1);
					printf("getsockopt connect error\n"); 
                    usleep(100000);//100ms
					if((info.tcpi_state==TCP_ESTABLISHED))
						break;
				}	
		}
        memcpy(buffer,"asdfg",5);
		if((info.tcpi_state==TCP_ESTABLISHED)) 
		{
			len =send(TCP_Client_Sockfd,buffer,5,0);
			printf("len=%d\n",len);
			usleep(100000);//100ms
		}
	}
}

5.2 编程函数

(1) connect函数

 #include       
 int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen); 

(2) listen函数

 int listen(int sockfd,int backlog);  

listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。

由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。

(3) accept函数

accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。

 int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen); 

第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。

六、三次握手四次挥手

SYN:表示建立连接。同步标志位,用于建立会话连接,同步序列号;
ACK:表示响应。确认标志位,对已接受的数据包进行确认;
FIN:表示关闭连接。完成标志位,表示我已经没有数据要发送,即将关闭连接;
PSH:表示有 DATA数据传输。推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队。
RST:表示连接重置。重置标志位,用于连接复位、拒绝错误和非法的数据包;
URG:紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;

Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制_第4张图片

第一次握手:主机A发送位码为syn=1随机

生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;

第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功

在TCP/IP协议中,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状态,完成三次握手。完成三次握手,客户端与服务器开始传送数据.

完成三次握手,主机A与主机B开始传送数据。

第一次挥手:A->B,A向B发出释放连接请求的报文,其中FIN(终止位) = 1,seq(序列号)=u;在A发送完之后,A的TCP客户端进入FIN-WAIT-1(终止等待1)状态。此时A还是可以进行收数据的

第二次挥手:B->A:B在收到A的连接释放请求后,随即向A发送确认报文。其中ACK=1,seq=v,ack(确认号) = u +1;在B发送完毕后,B的服务器端进入CLOSE_WAIT(关闭等待)状态。此时A收到这个确认后就进入FIN-WAIT-2(终止等待2)状态,等待B发出连接释放的请求。此时B还是可以发数据的。

(如果 B 直接跑路,则 A 永远处与这个状态。TCP 协议里面并没有对这个状态的处理,但 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。)

第三次挥手:B->A:当B已经没有要发送的数据时,B就会给A发送一个释放连接报文,其中FIN=1,ACK=1,seq=w,ack=u+1,在B发送完之后,B进入LAST-ACK(最后确认)状态。

第四次挥手:A->B;当A收到B的释放连接请求时,必须对此发出确认,其中ACK=1,seq=u+1,ack=w+1;A在发送完毕后,进入到TIME-WAIT (时间等待)状态。B在收到A的确认之后,进入到CLOSED(关闭)状态。在经过时间等待计时器设置的时间之后,A才会进入CLOSED状态。

为什么需要四次挥手

其实是客户端和服务端的两次挥手,也就是客户端和服务端分别释放连接的过程。可以看到,客户端在发送完最后一次确认之后,还要等待2MSL的时间。主要有两个原因,一个是为了让B能够按照正常步骤进入CLOSED状态,二是为了防止已经失效的请求连接报文出现在下次连接中。

解释:

1)、由于客户端最后一个ACK可能会丢失,这样B就无法正常进入CLOSED状态。于是B会重传请求释放的报文,而此时A如果已经关闭了,那就收不到B的重传请求,就会导致B不能正常释放。而如果A还在等待时间内,就会收到B的重传,然后进行应答,这样B就可以进入CLOSED状态了。

2)、在这2MSL等待时间里面,本次连接的所有的报文都已经从网络中消失,从而不会出现在下次连接中。

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

【问题3】为什么不能用两次握手进行连接?

答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

       现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
 

你可能感兴趣的:(Linux,udp,linux,网络)