TCP三次握手、四次挥手+多线程并发处理

目录

一、三次握手建立连接

[1.1 标记位](#1.1 标记位)

[1.2 三次握手的过程](#1.2 三次握手的过程)

二、四次挥手断开连接

三、模拟服务器和客户端收发数据

四、多线程并发处理

五、TCP粘包问题

[5.1 什么是TCP粘包?](#5.1 什么是TCP粘包?)

[5.2 TCP粘包会有什么问题?](#5.2 TCP粘包会有什么问题?)

[5.3 TCP粘包的解决方法?](#5.3 TCP粘包的解决方法?)


一、三次握手建立连接

1.1 标记位

SYN:用于建立连接的初始握手。发送方发送一个SYN报文段给接收方,请求建立连接。

ACK:用于确认数据的传输。当成功接收到数据后,接收方发送一个带有ACK标记的报文段回复发送方,确认已经收到了数据。

FIN:用于关闭连接。当发送方发送完所有数据后,会发送一个带有FIN标记的报文段,请求关闭连接。接收方在收到FIN报文段后,发送一个带有ACK标记的报文段进行确认,并使用一个定时器在一段时间后关闭连接。

1.2 三次握手的过程

三次握手是TCP协议中用于建立连接的过程,具体步骤如下:

  1. 客户端向服务器发送一个**SYN(同步)**的数据包,表示客户端请求建立连接。该数据包中还包含客户端随机生成的初始序列号(Sequence Number,seq),比如 seq = x。
  2. 服务器收到客户端发送的SYN数据包后,向客户端发送一个ACK(确认)和SYN的组合数据包,服务器将自己的初始序列号(seq = y)发送给客户端,同时将客户端的序列号加 1 作为确认号(Acknowledgment Number,ack = x + 1),表示服务器同意建立连接,并向客户端发送确认信息。
  3. 客户端收到服务器发送的ACK和SYN的数据包后,向服务器发送一个ACK确认数据包,该包的确认号为服务器的序列号加 1(ack = y + 1),而序列号为客户端在第一次握手中发送的序列号加 1(seq = x + 1),表示客户端也同意建立连接。

经过以上三个步骤,客户端和服务器就成功建立了连接,可以开始进行数据传输。这个过程就是TCP协议中三次握手的过程。

图解如下:

二、四次挥手断开连接

四次挥手是TCP协议中用于关闭连接的过程,具体步骤如下:

  1. 第一次挥手:当客户端确定自己已经没有数据要发送时,向服务器发送一个FIN(结束)数据包,其中包含自己的序列号(seq = u),表示客户端关闭数据传输。
  2. 第二次挥手:服务器接收到客户端发送的FIN后,向客户端发送一个ACK确认数据包,确认号为客户端的序列号加 1(ack = u + 1),表示服务器收到了关闭请求。此时,服务器可能还有数据需要继续发送,所以连接不会立即关闭,而是进入半关闭状态。
  3. 第三次挥手:当服务器确定自己没有数据要发送时,向客户端发送一个FIN数据包,其中包含自己的序列号(seq = v),表示服务器也准备关闭连接。
  4. 第四次挥手:客户端接收到服务器发送的FIN后,向服务器发送一个ACK确认数据包,确认号为服务器的序列号加 1(ack = v + 1),序列号为之前发送 FIN 包时的序列号加 1(seq = u + 1),表示客户端收到了关闭请求,连接正式关闭。

通过以上四个步骤,客户端和服务器完成了关闭连接的过程。值得注意的是,四次挥手中的每一次挥手都需要对方发送确认,确保双方都能安全地关闭连接。

图解如下:

那么,挥手能不能是3次呢?答案是可以的,第二次挥手和第三次挥手是可以合并在一起的。

三、模拟服务器和客户端收发数据

服务器ser

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

int socket_init();

int main()
{
    int sockfd=socket_init();
    if(sockfd==-1)
    {
        exit(1);
    }

    while(1)
    {
        struct sockaddr_in caddr;//记录客户端地址ip,port
        int len=sizeof(caddr);
        int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//caddr存放客户端ip,port,可能会阻塞
        if(c<0)
        {
            continue;
        }
        
        printf("accept c=%d\n",c);
        while(1)
        {
            char buff[128]={0};
            int n=recv(c,buff,127,0);//有可能阻塞,如果返回-1则失败,是0则对方关闭了
            if(n<=0)
            {
                break;
            }
            printf("recv:%s\n",buff);
            send(c,"ok",2,0);//write(c,"ok",2);
        }
        close(c);
        printf("cilent close\n");
    }
}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//TCP,创建套接字
    if(sockfd==-1)
    {
        return -1;
    }

    struct sockaddr_in saddr;//ipv4地址族,对应套接字的地址
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定ip,port,指定
    if(res==-1)
    {
        printf("bind err\n");
        return -1;
    }

    res=listen(sockfd,5);//创建监听队列
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}
  1. 先调用socket_init()函数初始化服务器端的套接字,绑定IP和端口,并开始监听连接请求。
  2. 不断循环accept()函数接受客户端的连接请求,一旦有客户端连接则创建一个新的套接字来处理与客户端的通信。
  3. 在客户端连接成功后,进入一个无限循环,不断接收客户端发送的消息,并打印消息内容。
  4. 如果接收到的消息长度小于等于0,则说明客户端关闭了连接,跳出循环,关闭与客户端的连接。
  5. 如果收到消息,则回复客户端消息为"ok", 继续接受下一条消息。

客户端cil

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

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        exit(1);
    }

    struct sockaddr_in saddr;//服务器的地址
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//发起连接,三次握手
    if(res==-1)
    {
        printf("connet err\n");
        exit(1);
    }

    while(1)
    {
        printf("input:\n");
        char buff[128]={0};
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }

        send(sockfd,buff,strlen(buff)-1,0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("buff=%s\n",buff);
    }
    close(sockfd);
    exit(0);
}
  1. 创建了一个套接字socket,并指定为AF_INET和SOCK_STREAM,表示使用IPv4和TCP协议。
  2. 初始化服务器的地址saddr,包括IP地址为"127.0.0.1"、端口号为6000等信息。
  3. 调用connect()函数连接到服务器端,进行三次握手建立连接。
  4. 进入一个循环,不断接收用户输入的消息,将消息发送给服务器,并接收服务器的回复。
  5. 如果用户输入为"end",则跳出循环,关闭套接字,结束程序。

运行结果:

那么send后会直接将数据发送出去吗?答案不然,会将数据先发送到发送缓冲区,然后将数据发给接收缓冲区,最后才会接收到数据。

我们来验证一下:

把ser.c中的int n = recv(c,buff,127,0)改为int n = recv(c,buff,1,0),执行后结果:

每输一个应该输出五个ok,但此时只输出一个ok,剩下四个ok在recv的缓冲区中,4个ok八个字符所以缓冲区有8个字符。

通过netstat -natp 命令可以显示

再输入一个a,输出四个ok

此时缓冲区中有一个ok,2个字符

TCP字节流服务

四、多线程并发处理

服务器ser

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

int socket_init();
void* fun(void* arg)
{
    int* p=(int*)arg;
    int c=*p;
    free(p);

        while(1)
        {
            char buff[128]={0};
            int n=recv(c,buff,127,0);//有可能阻塞,如果返回-1则失败,是0则对方关闭了
            if(n<=0)
            {
                break;
            }
            printf("recv:%s\n",buff);
            send(c,"ok",2,0);//write(c,"ok",2);
        }
        close(c);
        printf("cilent close\n");
}
int main()
{
    int sockfd=socket_init();
    if(sockfd==-1)
    {
        exit(1);
    }

    while(1)
    {
        struct sockaddr_in caddr;//记录客户端地址ip,port
        int len=sizeof(caddr);
        int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//caddr存放客户端ip,port,可能会阻塞
        if(c<0)
        {
            continue;
        }
        
        printf("accept c=%d\n",c);
        int* p=(int*)malloc(sizeof(int));
        if(p==NULL)
        {
            close(c);
            continue;
        }

        *p=c;
        pthread_t id;
        pthread_create(&id,NULL,fun,(void*)p);
    }
}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//TCP,创建套接字
    if(sockfd==-1)
    {
        return -1;
    }

    struct sockaddr_in saddr;//ipv4地址族,对应套接字的地址
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定ip,port,指定
    if(res==-1)
    {
        printf("bind err\n");
        return -1;
    }

    res=listen(sockfd,5);//创建监听队列
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

客户端cil

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

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        exit(1);
    }

    struct sockaddr_in saddr;//服务器的地址
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//发起连接,三次握手
    if(res==-1)
    {
        printf("connet err\n");
        exit(1);
    }

    while(1)
    {
        printf("input:\n");
        char buff[128]={0};
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }

        send(sockfd,buff,strlen(buff)-1,0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("buff=%s\n",buff);
    }
    close(sockfd);
    exit(0);
}

运行结果:

五、TCP粘包问题

5.1 什么是TCP粘包?

TCP粘包是指发送方连续发送的多个数据包,在传输过程中可能会被TCP协议合并成一个大的数据包,在接收方收到时无法正确区分这些数据包的边界,导致粘在一起,从而引起粘包现象。这样就会影响接收方对数据的解析和处理。简而言之,就是多次send发送数据,被对方一次recv收到了。

5.2 TCP粘包会有什么问题?

  1. 数据解析错误:如果接收方无法正确区分数据包的边界,可能导致数据解析错误,无法按照预期的方式处理数据。
  2. 数据错误:如果多个数据包粘在一起,可能导致数据包数据内容混杂,造成数据错误或丢失。
  3. 性能影响:数据粘包会增加解析数据的复杂性,影响系统性能。

5.3 TCP粘包的解决方法?

  1. 消息定长:在发送方和接收方约定固定的消息长度,每次发送和接收的数据长度相同,这样接收方可以根据固定长度来截取数据包。
  2. 使用特殊符号分隔:在数据包之间加入特殊符号作为分隔符,接收方根据分隔符来区分不同数据包。
  3. 增加消息头:在数据包头部添加额外的消息头信息,包括消息长度等,接收方通过消息头信息来解析数据包。
相关推荐
無炆_14 分钟前
Cline原理分析-prompt
linux·ai·prompt
薯条不要番茄酱17 分钟前
【网络原理】深入理解HTTPS协议
网络·网络协议·https
xiaolingting2 小时前
HTTP 快速解析
网络·网络协议·http·协商缓存·文件传输·206
望获linux3 小时前
北京亦庄机器人马拉松:人机共跑背后的技术突破与产业启示
linux·人工智能·机器人·操作系统·开源软件·rtos·具身智能
张槊哲5 小时前
字符和编码(python)
linux·数据库·python
herinspace8 小时前
管家婆易指开单如何设置零售开单
运维·服务器·数据库·软件工程·sass·零售
小镇敲码人8 小时前
【网络层】之IP协议
服务器·网络·tcp/ip
IT阳晨。8 小时前
【嵌入式Linux】基于ARM-Linux的zero2平台的智慧楼宇管理系统项目
linux·arm开发
_清风来叙8 小时前
【Linux】Linux内核模块开发
linux·arm开发
于齐龙8 小时前
pip 常用命令及配置
linux·python·pip