TCP
目录
4、为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
TCP/IP 跟 TCP 是两个东西
TCP/IP 是一个概念
TCP 则是一个传输协议
这里所学的 tcp 和 udp 只负责在传输层进行数据的传输 其他层的内容我们不需要进行考虑
-- TCP是面向连接的协议,通常用于一对一的通信,即一个客户端与一个服务器之间建立连接。
引入
-- 传输层通信协议
传输层有两个主要协议:TCP 和 UDP
TCP 稳定 数据传输有应答的过程
UDP 不稳定 只管发送 不需要管对方有没有接收 数据的传递没有应答过程
-- TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次挥手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。
-- 传输消息稳定!!!
三次握手,四次挥手!!!
-- 在介绍三次握手与四次挥手之前我们先来了解一下TCP的头部结构是什么样的。
-- TCP头部的最大长度为60字节,其中包括TCP报文固定长度20字节+可变字节(最大40字节),具体结构如下:
--- 在认识三次握手与四次挥手之前我们必须要知道以下字段及含义:
- 序号:seq,占32位,用来标识从发送端到接收端发送的字节流。
- 确认号:ack,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。
- 标志位:
SYN:发起一个新连接。
FIN:释放一个连接。
ACK:确认序号有效。
-- 在学习三次握手四次挥手后我们需要能流畅描述三次握手的流程及其中的字段含义作用的同时还需要记住每次握手时接收端和发送端的状态。
TCP 三次握手过程
-- 在三次握手中发送端有CLOSED、SYN-SENT、ESTABLISHED三种状态,接收端有CLOSED、LISTEN、SYN-RCVD、ESTABLISHED四种状态。
- 三次握手过程如下图:
假设发送端为客户端,接收端为服务端。开始时客户端和服务端的状态都是CLOSE。
- 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位SYN=1,序列号seq=x。
第一次握手前客户端的状态为CLOSE,第一次握手后:客户端的状态为SYN-SENT(等待一个匹配的连接请求),此时服务端的状态为LISTEN(等待从任何远端TCP 和端口的连接请求)。 - 第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位SYN=1,ACK=1,序列号seq=y,确认号ack=x+1。
第二次握手前服务端的状态为LISTEN,第二次握手后服务端的状态为SYN-RCVD(等待连接请求确认),此时客户端的状态为 SYN-SENT。 - 第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位ACK=1,序列号seq=x+1,确认号ack=y+1。
第三次握手前客户端的状态为SYN-SENT,第三次握手后客户端和服务端的状态都为ESTABLISHED(表示一个打开的连接,接收到的数据可以被投递给用户)。
需要注意的一点是:
第一次握手,客户端向服务端发起建立连接报文,会占一个序列号。
但是第三次握手,同样是客户端向服务端发送报文,这次却不占序列号.
所以建立连接后,客户端向服务端发送的第一个数据的序列号为x+1。
四次挥手
客户端在四次挥手过程中有ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、TIME-WAIT、CLOSED等五个状态,服务端有ESTABLISHED、CLOSE-WAIT、LAST-ACK、CLOSED等四种状态。
-- 四次挥手过程如下图:
假设客户端首先发起的断开连接请求
- 第一次挥手:客户端向服务端发送的数据完成后,向服务端发起释放连接报文,报文包含标志位 FIN=1,序列号seq=u。此时客户端只能接收数据,不能向服务端发送数据。
- 第二次挥手:服务端收到客户端的释放连接报文后,向客户端发送确认报文,包含标志位ACK=1,序列号seq=v,确认号ack=u+1。此时客户端到服务端的连接已经释放掉,客户端不能像服务端发送数据,服务端也不能向客户端发送数据。但服务端到客户端的单向连接还能正常传输数据。
- 第三次挥手:服务端发送完数据后向客户端发出连接释放报文,报文包含标志位FIN=1,标志位ACK=1,序列号seq=w,确认号ack=u+1。
- 第四次挥手:客户端收到服务端发送的释放连接请求,向服务端发送确认报文,包含标志位ACK=1,序列号seq=u+1,确认号ack=w+1。
相关问题
1、为什么TCP连接的时候是三次?两次是否可以?
答:不可以,主要从以下两方面考虑(假设客户端是首先发起连接请求):
假设建立TCP连接仅需要两次握手,那么如果第二次握手时,服务端返回给客户端的确认报文丢失了,客户端这边认为服务端没有和他建立连接,而服务端却以为已经和客户端建立了连接,并且可能服务端已经开始向客户端发送数据,但客户端并不会接收这些数据,浪费了资源。如果是三次握手,不会出现双方连接还未完全建立成功就开始发送数据的情况。
如果服务端接收到了一个早已失效的来自客户端的连接请求报文,会向客户端发送确认报文同意建立TCP连接。但因为客户端并不需要向服务端发送数据,所以此次TCP连接没有意义并且浪费了资源。
2、为什么TCP连接的时候是三次,关闭的时候却是四次?
答:因为需要确保通信双方都能通知对方释放连接。
假设客户端发送完数据向服务端发送释放连接请求,当客户端并不知道,服务端是否已经发送完数据,所以此次断开的是客户端到服务端的单向连接。
服务端返回给客户端确认报文后,服务端还能继续单向给客户端发送数据。
当服务端发送完数据后还需要向客户端发送释放连接请求。 客户端返回确认报文,TCP连接彻底关闭。
所以断开TCP连接需要客户端和服务端分别通知对方并分别收到确认报文,一共需要四次。
3、TIME_WAIT和CLOSE_WAIT的区别在哪?
答:默认客户端首先发起断开连接请求,从上图可以看出:
CLOSE_WAIT是被动关闭形成的,当客户端发送FIN报文,服务端返回ACK报文后进入CLOSE_WAIT。
TIME_WAIT是主动关闭形成的,当第四次挥手完成后,客户端进入TIME_WAIT状态。
4、为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
答:其中MSL的意思是报文的最长寿命,可以从两方面考虑:
客户端发送第四次挥手中的报文后,再经过2MSL,可使本次TCP连接中的所有报文全部消失,不会出现在下一个TCP连接中。
考虑丢包问题,如果第四挥手发送的报文在传输过程中丢失了,那么服务端没收到确认ack报文就会重发第三次挥手的报文。如果客户端发送完第四次挥手的确认报文后直接关闭,而这次报文又恰好丢失,则会造成服务端无法正常关闭。
可靠的实现TCP全双工连接的终止; 允许老的重复分节在网络中消逝。
5、如果已经建立了连接,但是客户端突然出现故障了怎么办?
答: 如果TCP连接已经建立,在通信过程中,客户端突然故障,那么服务端不会一直等下去,过一段时间就关闭连接了。具体原理是TCP有一个保活机制,主要用在服务器端,用于检测已建立TCP链接的客户端的状态,防止因客户端崩溃或者客户端网络不可达,而服务器端一直保持该TCP链接,占用服务器端的大量资源(因为Linux系统中可以创建的总TCP链接数是有限制的)。
6、三次握手过程中可以携带数据吗?
答:第三次握手的时候可以携带数据的,第一次、第二次握手不可以携带数据。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。
第一次、第二次握手都有SYN报文所以第一次、第二次握手都不可以携带数据。
而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
7、各种状态的含义
- CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
- LISTEN:等待从任何远端TCP 和端口的连接请求。
- SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
- SYN_RCVD:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
- ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
- FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
- CLOSE_WAIT:等待本地用户的连接终止请求。
- FIN_WAIT_2:等待远端TCP 的连接终止请求。
- LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)。
- CLOSING:等待远端TCP 的连接终止请求确认。
- TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
服务器端的搭建流程
1、创建服务器套接字
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int socket(int domain, int type, int protocol);
-- 函数的作用
- 根据传入的参数来创建套接字
-- 函数的参数
- domain:指定通信的域。
AF_INET 表示在 ipv4 下进行通信
AF_INET6 在 ipv6 下进行通信
- type:指定通信的类型。
SOCK_STREAM 表示使用 TCP 协议进行通信
SOCK_DGRAM 表示使用 UDP 协议进行通信
- protocol:指定通信的协议。
0 表示使用默认协议
-- 如果我需要创建一个基于 TCP 的通信套节字
int fd = socket(AF_INET,SOCK_STREAM,0);
-- 函数的返回值
- 成功:返回套接字描述符
- 失败:返回 -1
2、绑定套接字
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-- 函数的作用
- 将套接字和 IP 地址以及端口号绑定在一起
- 主要的目的是为了方便客户端来查找到该服务器套节字
-- 函数的参数
- sockfd:套接字描述符
- socklen_t addrlen :填写第二个参数的结构体的大小即可
- struct sockaddr *addr:用来填写进行绑定的端口和 ip
struct sockaddr {
sa_family_t sa_family;
char
sa_data[14];
}
-- 但是该结构体不好用
- 我们可以使用该头文件中定义的结构体
#include <netinet/in.h>
struct sockaddr_in 类型的结构体来使用即可
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port;
/* Port number. */
struct in_addr sin_addr;
/* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
-- 注意:该结构体中的端口和 ip 地址不可以直接赋值 需要进行相应的转换
-- 端口需要从小端格式转化为大端格式(在网络上通信的地址是大端模式),使用下面找个函数可以进行小转大
- uint16_t htons(uint16_t hostshort)
-- 结构体中的 ip 地址可以选择
- 1 通过 ifconfig 查看获得的 ipv4 地址
- 2 127.0.0.1 本地回环
-- 填写的 ip 地址必须转化为二进制,使用下面的函数可以实现将
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
inet_addr(“192.168.219.129”);
inet_addr 函数的作用是将一个点分十进制的 IPv4 地址(例如 "192.168.1.1")转换为一个网络字节序的无符号长整型(unsigned long)值。这个函数通常用于网络编程中,以便将字符串形式的 IP 地址转换为可以在网络通信中使用的格式。
-- 函数的返回值
- 成功:返回 0
- 失败:返回 -1
3、监听套接字
-- 函数头文件
- -#include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int listen(int sockfd, int backlog);
-- 函数的作用
- 将套接字设置为监听状态,等待客户端的连接 (将服务器的套接字设置为监听状态 可以监听客户端的链接请求)
-- 函数的参数
- sockfd:填写服务器套接字
- backlog:设置监听队列的长度(等待队列的最大长度, 同一时刻可以处理的最大的链接请求个数)
-- 函数的返回值
- 成功:返回 0
- 失败:返回 -1
4、接受连接
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-- 函数的作用
- 接受客户端的连接请求
- 如果有客户端连接,则返回一个和客户端通信的套接字
- 如果没有客户端连接,则阻塞等待
-- 函数的参数
- sockfd:填写服务器套接字
- addr:用来存放链接上来的客户端的对应的 ip 和端口 如果不用直接给 NULL
- addrlen:填写存放了第二个结构体大小的变量的地址 如果第二个参数为 NULL 该参数也为 NULL
-- 函数的返回值
- 成功:链接上来的客户端的通信套接字(后续通过这个套接字进行通信)
- 失败:返回 -1
客户端的搭建流程
1、创建套接字
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int socket(int domain, int type, int protocol);
-- 函数的作用
- 根据传入的参数来创建套接字
-- 函数的参数
- domain:指定通信的域。
AF_INET 表示在 ipv4 下进行通信
AF_INET6 在 ipv6 下进行通信
- type:指定通信的类型。
SOCK_STREAM 表示使用 TCP 协议进行通信
SOCK_DGRAM 表示使用 UDP 协议进行通信
- protocol:指定通信的协议。
0 表示使用默认协议
-- 如果我需要创建一个基于 TCP 的通信套节字
int fd = socket(AF_INET,SOCK_STREAM,0);
-- 函数的返回值
- 成功:返回套接字描述符
- 失败:返回 -1
2、连接服务器
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-- 函数的作用
- 向服务器发起连接请求 (发送客户端的连接请求
将客户端的通信套接字连接到指定的 ip 和 port 上去)
-- 函数的参数
- sockfd:填写客户端套接字
- struct sockaddr *addr:填写想要连接的服务器的 ip 和 port
- 使用 struct sockaddr_in 找个结构体进行填充
- addrlen:填写第二个结构体的大小
-- 函数的返回值
- 成功:返回 0
- 失败:返回 -1
服务器和客户端连接成功之后可以进行数据的传递
-- 通过 read 可以读取数据
write 函数可以发送数据
注意 遵循先进先出原则
其次 没有数据可以接收 read 会阻塞
当 read 的套接字对象下线时 该套接字会失去作用 read 不会有阻塞性
也可以使用
send 和 recv 函数来实现数据的收发
1、发送数据
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-- 函数的作用
- 向指定的套接字发送数据
-- 函数的参数
- sockfd:填写通信套接字
- buf:填写发送数据的缓冲区
- len:填写发送数据的长度
- flags:填写 0
-- 函数的返回值
- 成功:返回发送数据的长度
- 失败:返回 -1
2、接收数据
-- 函数头文件
- #include <sys/types.h>
- #include <sys/socket.h>
-- 函数原型
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-- 函数的作用
- 从指定的套接字接收数据
-- 函数的参数
- sockfd:填写通信套接字
- buf:填写接收数据的缓冲区
- len:填写接收数据的长度
- flags:填写 0
-- 函数的返回值
- 成功:返回接收数据的长度
- 失败:返回 -1
例题:实现 TCP 1 对 1 全双工通信
-- 服务器
#include "stdio.h"
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "unistd.h"
#include <pthread.h>
char buff[30]={0};
int aa;
int fd;
void * func(void *arg)
{
while(1)
{
printf("请输入数据!\n");
scanf("%s",buff);
send(aa,buff,sizeof(buff),0);
//write(fd,buff,sizeof(buff));
}
}
int main(int argc, char const *argv[])
{
pthread_t thread;
int pp = pthread_create(&thread, NULL, func,NULL);
if(pp == -1)
{
perror("pthread_create");
return -1;
}
fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
printf("创建套节字成功\n");
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;//表示使用IPV4
addr.sin_port = htons(45612); //将小端模式转化为大端模式
addr.sin_addr.s_addr = inet_addr("192.168.33.129"); //将点分十进制转化为对应的格式S
int ret = bind(fd, (struct sockaddr *)&addr,sizeof(addr));
if(ret == -1)
{
perror("bind");
return -1;
}
int ll = listen(fd,5);
if(ll == -1)
{
perror("listen()");
return -1;
}
printf("设置监听成功\n");
aa = accept(fd,NULL,NULL);
if(aa == -1)
{
perror("accept");
return -1;
}
printf("处理客户端的链接请求\n");
while(1)
{
int rr = recv(aa,buff,sizeof(buff),0);
//ret = read(aa,buff,sizeof(buff));
if(rr == 0)
{
printf("对方下线\n");
close(aa);
break;
}
printf("read:%s\n",buff);
}
close(fd);
return 0;
}
-- 客户端
#include "stdio.h"
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "unistd.h"
#include <pthread.h>
char buff[30]={0};
int fd;
void * func(void *arg)
{
while(1)
{
printf("请输入数据!\n");
scanf("%s",buff);
send(fd,buff,sizeof(buff),0);
//write(fd,buff,sizeof(buff));
}
}
int main(int argc, char const *argv[])
{
pthread_t thread;
int pp = pthread_create(&thread, NULL, func,NULL);
if(pp == -1)
{
perror("pthread_create");
return -1;
}
fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
printf("创建套节字成功\n");
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;//表示使用IPV4
addr.sin_port = htons(45612);
addr.sin_addr.s_addr = inet_addr("192.168.33.129");
int cc = connect(fd, (struct sockaddr*)&addr,sizeof(addr));
if(cc == -1)
{
perror("connect");
return -1;
}
printf("客户端连接成功!\n");
while(1)
{
int rr = recv(fd,buff,sizeof(buff),0);
//ret = read(cli,buff,sizeof(buff));
if(rr == 0)
{
printf("对方下线\n");
close(fd);
break;
}
printf("read:%s\n",buff);
}
return 0;
}