Linux网络编程 深入解析Linux TCP:TCP实操,三次握手和四次挥手的底层分析

知识点1【TCP编程概述】

1、TCP的概述

客户端:主动连接服务器,和服务器进行通信

服务器:被动被客户端连接,启动新的线程或进程,服务器客户端(并发服务器)

这里重复TCP和UDP特点

TCP(传输控制协议):是一种靠谱的传输层协议,

买电话 买电话卡 开声音 按下接听

知识点2【TCP客户端编程】

1、创建TCP套接字

SOCK_STREAM

socket函数创建的TCP套接字,没有端口,且默认是主动连接特性

2、connect函数连接服务器

cpp 复制代码
   #include <sys/types.h>          
   #include <sys/socket.h>
   int connect(int sockfd, const struct sockaddr *addr,
               socklen_t addrlen);

功能介绍

客户端主动发出与TCP服务器的连接(三次握手)

参数

sockfd:客户端套接字

addr:只想服务器的地址结构体

addrlen:地址结构体的长度

返回值

成功:0

失败:返回-1

注意

TCP客户端通信之前,必须实现 建立和服务器之间的连接

connect连接成功一个服务器,不能再次连接其他服务器

inet_addr()(补充函数,平替pton)函数仅支持IPv4

如果socket没有固定端口,在调用connect时系统自动分配随机端口为源端口

connect实际上是带阻塞的,需要三次握手

3、send发送消息

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能介绍

通过已连接套接字发送数据

参数

sockfd:已连接套接字(下面accept函数中会介绍)

buf:带发送数据的缓冲区指针

len:要发送的字节数

flags:控制标志

  • MSG_OOB:发送带外数据(紧急数据)。
  • MSG_DONTWAIT:非阻塞发送(立即返回)。
  • MSG_NOSIGNAL:禁止发送SIGPIPE信号。

返回值

成功:返回实际发送的字节数

失败:返回-1

注意

TCP不能发出0长度报文,但是UDP可以。

4、recv接收数据(默认阻塞)

cpp 复制代码
   #include <sys/types.h>
   #include <sys/socket.h>
   ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能介绍

从已连接套接字接收数据

参数

sockfd:已连接套接字

buf:接收数据的缓冲区指针

len:缓冲区最大容量

falgs:控制标记

返回值

成功:返回实际接收到的字节数

失败:返回-1

注意

recv如果收到0长度报文,表明对方已经断开连接,因此我们使用send的时候,不能发送0长度报文

只要一方关闭,另外一方就会收到0长度报文

知识点3【TCP服务器编程】

我们先把服务器的过程形象化:

1、socket() 买手机

2、bind() 买电话卡

3、listen() 打开声音

4、accept() 接听

1、作为服务器的条件

1、需要为服务器绑定一个固定的端口、IP(连接作用)

2、让操作系统知道这是一个服务器,而不是客户端

(使用listen函数 让服务器具备监听功能,使套接字由主动变被动

3、等待客户端的连接到来,使用accept提取到来的客户端

2、listen监听函数

cpp 复制代码
   #include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int listen(int sockfd, int backlog);

功能介绍

将套接字设为被动监听模式,等待客户端连接

参数

sockfd:套接字

backlog:连接队列的最大长度

返回值

成功:0

失败:-1

函数功能详解(底层)

1、将sockfd由主动变被动,并且对sockfd进行监听客户端连接的到来

2、backlog是连接队列 的大小,表示客户端的最大个数

连接队列大小分析:

1、在listen后,在TCP服务器中,套接字改称为监听套接字,所有客户端想要连接,就需要向这个客户端发送连接请求

2、客户端发出连接请求(connect),TCP的server就会创建一个连接队列

3、连接队列又会分为两部分,但是总大小是上面对应的个数

(1)完成连接------三次握手之后

(2)未完成连接------三次握手完成之前

图形解析

这里说一个早期的一个攻击手段:SYN洪流攻击

就是一直发送低于三次握手信号(半成品),这样的信号全部在连接队列的未完成连接中存储,服务器也无法处理,当达到连接队列的最大值后,正常连接反而进不去连接队列。

这里

我们写监听函数,并阻塞,查看其网络状态

netstate -anp | grep a.out

可以看到此时处于监听状态(想系统说明此时是一个服务器)

3、accept提取客户端的连接(阻塞

cpp 复制代码
   #include <sys/socket.h>
   int accept(int socket, struct sockaddr *restrict address,
       socklen_t *restrict address_len);

函数功能

accept只能从 连接队列 中 处于 完成连接部分 中提取连接

将提取到的该客户端的信息 存到addr

将提取到的该客户端从连接队列中删除

参数

sockfd:监听套接字

addr:存放的是 客户端 的地址信息

addrlen:地址结构体的长度的地址

返回值

成功 :返回一个已连接套接字,这个套接字才代表服务器和该客户端的连接端点(真正和客户端的连接)

解释:如果服务器想要和该客户端通信,就需要向这个 已连接套接字 中读写

失败:返回-1

注意

调用一次 ,只能提取一个客户端对应的连接 ,如果连接队列没有客户端连接 ,将阻塞

因为是从 连接队列 中提取的原因,遵循先进先出的原则

验证一下:通过两个客户端,都连接我们写的服务器,会发现只有一个能发送

TCP服务器代码演示

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char const *argv[])
{
    //创建监听套接字
    int fd_sock_lis = socket(AF_INET,SOCK_STREAM,0);
    if(fd_sock_lis < 0)
    {
        perror("socket_lis");
        _exit(-1);
    }

    //绑定固定端口  这里我们设置为8000
    struct sockaddr_in addr_bind;
    bzero(&addr_bind,sizeof(addr_bind));
    addr_bind.sin_family = AF_INET;
    addr_bind.sin_port = htons(8000);
    addr_bind.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret_bind = bind(fd_sock_lis,(struct sockaddr *)&addr_bind,sizeof(addr_bind));
    if(ret_bind < 0)
    {
        perror("bind");
        _exit(-1);
    }

    //监听 参数:监听套接字,连接队列的个数
    int ret_listen = listen(fd_sock_lis,10);
    if(ret_listen == -1)
    {
        perror("listen");
        _exit(-1);
    } 

    //提取客户端连接,已连接套接字的创建,已连接套接字理解为通信端口的一端
    //服务器通过操作这个已连接套接字与客户端进行联系
    struct sockaddr_in addr_accept;
    bzero(&addr_accept,sizeof(addr_accept));
    int len_accept = sizeof(addr_accept);
    int fd_sock_connect = accept(fd_sock_lis,(struct sockaddr *)&addr_accept,&len_accept);
    if(fd_sock_connect < 0)
    {
        perror("accept");
        _exit(-1); 
    }

    //接收数据,从已连接套接字中接收
    char arr_recv[256] = "";
    recv(fd_sock_connect,arr_recv,sizeof(arr_recv),0);
    
    //处理 客户端端口 的信息
    int port_client = ntohs(addr_accept.sin_port);
    char ip_client[16] = "";
    inet_ntop(AF_INET,&addr_accept.sin_addr.s_addr,ip_client,sizeof(ip_client));
    //遍历接收到的信息
    printf("从IP:%s的%d端口,获取的数据是%s\\n",ip_client,port_client,arr_recv);

    //发送应答
    send(fd_sock_connect,"ok",sizeof("ok"),0);

    //关闭 所有套接字
    close(fd_sock_connect);
    close(fd_sock_lis);

    return 0;
}

代码运行结果

知识点4【close关闭套接字】

当客户端用完后会调用close套接字,服务器也要关闭套接字(监听套接字,已连接套接字)

1、作为客户端

close(套接字),断开当前的连接,导致服务器收到0长度报文

2、作为服务器

close(监听套接字),该服务器不能监听新的连接的到来 ,但是不影响已连接客户端的通信

close(已连接套接字),只是断开当前客户端的连接 ,不会影响监听套接字(服务器可以继续监听新的连接的到来)

这里我们形象化一下

形象化理解

媒婆 监听套接字

小帅 客户端套接字

小美 已连接套接字

小帅找媒婆介绍对象,媒婆有个名单,把小美介绍给了小帅,小美和小帅分还是合,最后不会影响媒婆招揽其他生意

如果媒婆不想干媒婆这一行了,去转学嵌入式了,也不会影响小帅和小美的关系

3、下面内容前提

握手,挥手都涉及到底层,我们需要用抓包的操作来了解这一过程,我这里用的抓包工具是 wireshark

这是 我使用的抓包工具获取的上面的发送过程

由于我的 图不太好识别

我用我老师当时的抓包图片 (很标准的三次握手,数据通信,四次挥手抓包过程

我们分为三个流程 1、三次握手,2、数据通信,3、四次挥手 这里大家先看一下总的流程图,流程图是当时老师画的,我感觉很牛,这里直接使用了

知识点4【三次握手】(重要 背!!)

客户端 调用connect 连接服务器底层 会完成三次握手 信号,此时客户端阻塞 ,当三次握手信号完成 ,connect才会解除阻塞往下执行

三次握手的发起者客户端

1、TCP的头部

这里先介绍一下 TCP的头部

1、SYN:置1,表示报文是 连接请求报文

2、FIN:置1,表示报文是 关闭请求报文

3、ACK:置1,表示报文是 回应(应答)报文

4、URG:置1,表明紧急指针字段有效,告诉操作系统此报文内有紧急数据,请尽快传送

5、PUSH:置1:推送报文

6、RST:置1:复位连接

注意

序列号:seq,当前报文的编号。

确认序号:当前报文希望 接下来对方发送的报文编号

ack(确认序号)数据分析

1、当无数据时,ack = 对方原编号 + 1

2、当 有数据时,ack = 对房源编号 + 接收到数据长度

2、三次握手

分析过程

1、当客户端调用connect后,底层发送SYN连接请求

此时seq = 0,ack = 0

2、服务器收到客户端发出的SYN请求,服务器给客户端回应SYN和ACK应答

此时seq = 1,ack = 1(无数据)

3、客户端接收到服务器发送的SYN请求后,向服务器发送ACK应答

此时seq = 1,ack = 1(原本服务器seq = 0,无数据+1后为1)

形象化理解

小帅给小美表白

小帅:我喜欢你(SYN)

小美:我知道(ACK),我也喜欢你(SYN)

小帅:其实我也知道(ACK)

3、分析通信过程

这里客户端发送的时"hello ack",服务器应答我们设置的时"ok"

现在我们分析一下(每个红线是一个步骤)

分析过程

1、客户端和服务器经过三次握手后已经 完成连接(连接列表),客户端向服务器发送"hello ack"

这里有一个技巧:连续 的线都是一个方向 时,线对应的seq和ack一样

此时seq = 1,ack = 1,len = 9

2、服务器收到数据后,及其长度,发出ACK应答

此时seq = 1(与上面的ack相对应),ack = 10( 1+len(9))

3、服务器发送数据"ok"后,客户端接收

此时seq = 1,ack = 10(线 连续且方向一样),len = 2

4、客户端接收数据,发出ACK应答

此时seq = 10,ack = 3

确认序号的作用

1、当发送端数据时500位的时候,接收时仅收到了256位

此时len = 500,但实际发送的时候 接受到的长度的时256

这时,ack的值位 对方原序号码 + 256

接收端收到后,用ack - 原序号吗算出 接收数据长度,发现与发送数据长度不同,底层会处理后,继续发送没有接收的数据

2、检验是否发生错误传输,这里不多介绍

4、四次挥手

这里补充一个概念:FIN标志位置1的前提是调用close函数

服务器和客户端都可以先退出,一般都是客户端先退出

分析过程

1、当客户端调用close(套接字)函数,触发底层发出FIN断开连接的请求。

2、服务器收到FIN关闭请求,做出ACK应答(服务器进入CLOSE_WAIT状态)。

CLOSE_WAIT状态是指对方套接字已经关闭,等待 服务器 已连接套接字关闭,即调用close(对应已连接套接字)

3、服务器应用层调用close函数后,触发底层发送FIN。

4、客户端收到FIN请求,回应ACK。

以上内容依次是四次挥手

下面我想补充说明一些知识点

补充(重要)

1、细心的同学已经发现了,在第三次回收后,客户端的状态时TIME_WAIT,它在等上面?

第四次回收 涉及到服务器到CLOSE的关闭状态的转换,因此需要保证其正确执行

TIME_WAIT等待是有时间时间限制的,在规定时间内 ,发现服务器没有重发FIN (即二次挥手),就意味着,ACK(第四次挥手)已经成功被服务器收到,客户端也会变为CLOSE状态

如果在规定时间内,客户端又收到了(第三次挥手),表明出现了问题,此时客户端会再次四次挥手,然后重复等待。

2、补充问题(技术面常问问题

(1)为什么要四次挥手,而不能向三次握手一样,在第二次握手中一起将ACK和SYN一起发送给客户端?

因为第三次挥手的触发条件是,等待服务器调用close(已连接套接字),有一个等待的过程,因此不能和ACK一起发送。

(2)客户端都已经调用了close为什么还能执行 四次挥手的操作?

因为close只是关闭客户端中的套接字,此时是半关闭状态,仅关闭这个套接字应用层的数据的收发,而将FIN,ACK这些标志位置1的操作,是在底层实现的,并不是close负责的,因此可以执行四次挥手的操作。

5、状态转换

在图中大家可以看到有很多状态,下面介绍一下这些状态都是什么

下面是状态转换流程图

介绍

红色是服务器的状态转换图,蓝色是客户端的状态转换图

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!

相关推荐
沐墨专攻技术23 分钟前
《空间复杂度(C语言)》
c语言·数据结构·算法·空间复杂度
想躺在地上晒成地瓜干1 小时前
树莓派超全系列教程文档--(32)config.txt常用音频配置
linux·音视频·树莓派·raspberrypi·树莓派教程
邪恶的贝利亚2 小时前
FFmpeg 硬核指南:从底层架构到播放器全链路开发实战 基础
linux·服务器·ffmpeg
YGGP3 小时前
【每日八股】复习计算机网络 Day1:TCP 的头部结构 + TCP 确保可靠传输 + TCP 的三次握手
网络·tcp/ip·计算机网络
涛ing3 小时前
【Linux “less“ 命令详解】
linux·运维·c语言·c++·人工智能·vscode·bash
ybdesire4 小时前
Jinja2模板引擎SSTI漏洞
网络·人工智能·安全·web安全·大模型·漏洞·大模型安全
屎到临头想搅便4 小时前
OSPF综合实验(HCIP)
网络·智能路由器
林木木木木木木木木木5 小时前
【随身WiFi】随身WiFi Debian系统优化教程
linux·运维·debian·随身wifi
临观_6 小时前
打靶日记 zico2: 1
linux·网络安全
EstrangedZ6 小时前
vcpkg缓存问题研究
c语言·c++·缓存·cmake·vcpkg