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

知识点1【TCP编程概述】

1、TCP的概述

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

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

这里重复TCP和UDP特点

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

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

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

1、创建TCP套接字

        SOCK_STREAM

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

2、connect函数连接服务器

   #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发送消息

#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接收数据(默认阻塞)

   #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监听函数

   #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提取客户端的连接(阻塞

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

函数功能

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

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

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

参数

        sockfd:监听套接字

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

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

返回值

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

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

失败:返回-1

注意

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

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

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

TCP服务器代码演示

#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、状态转换

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

下面是状态转换流程图

介绍

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

结束

代码重在练习!

代码重在练习!

代码重在练习!

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值