Linux高级编程——网络通信实现TCP(2)

本文详细解释了TCP协议中的粘包现象,由于TCP为提高效率会将数据暂存并一起发送,可能导致接收方数据混淆。为解决此问题,提出了四种方法:添加定长包头和包尾、采用应答式通信、增加发送间隔和禁用Nagle算法。文中还提供了客户端和服务器端的C语言代码示例,展示了如何在实际应用中防止粘包。
摘要由CSDN通过智能技术生成

为解决TCP协议中的粘包问题,提供以下实例说明:

TCP 协议不允许传送空数据包

通信协议(Communication Protocol):为了实现正常通信,参与通信的双方或多方必须共同遵守的规则,比如通信的流程、数据格式等。

世界知名的文件传输协议就是 FTP 协议,它也是基于 TCP 协议的应用层协议

TCP 协议内部实现了很多机制以保障传输可靠:

  • 不会发生乱序问题,因为 TCP 协议内部会根据发送方的发送顺序在接收方缓冲区中进行重新排序。(按发送顺序来)。

  • 不会发生丢包问题,内部具有拥塞控制和流量控制机制。

  • 差错校验、出错重传。

粘包问题

TCP 协议为了提升传输效率,内部实现了一些优化算法,发送方调用send/write 发送一份数据时,TCP 协议并不会立即将这份数据打包送走,而是将这份数据存放在发送缓冲区中,并等待一小段时间,如果等待期间发送方又调用了 send/write 发送数据,TCP 协议会进行同样的处理。如果等待期间发送方没有再发送数据或者发送缓冲区满了,那么TCP协议会将发送缓冲区中现有数据直接打包送走,这样就导致了”粘包“问题发生。

当数据包到达接收方后,系统会解包后将数据按发送顺序存放在接收缓冲区中,如果接收方没有及时调用 recv/read 从接收缓冲区中收取数据,那么多个数据包可能都在接收缓冲区中连在一起,这样也会导致粘包。(两个缓冲区面向当前套接字)

解决粘包问题的方法:

  1. 给要发送的数添加定长的包头(比如一个按 1 字节对齐的结构体数据,用于描述后续数据的特性)和特殊的包尾。
  2. 采用应答式通信。
  3. 增加连续两次发送数据操作的时间间隔(无法根本解决问题,不太建议使用!)。
  4. 设置套接字属性,禁用 TCP 协议内部 Nagle算法(还是无法完全解决粘包问题,很无脑,不太建议使用!)。

开发经验:不要通过 send 函数返回值轻易判断对方成功接收到数据,因为 send 函数只是将要发送的数据拷贝到网络发送缓冲区中,它就成功返回。最可靠的做法就是通过接收对方的回复消息来判断是否成功。

客户端

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>


#pragma pack(1)

struct str_info
{
    int size;
    char type;
};

#pragma pack()


int main(int argc, char** argv)
{
    //socket 函数:创建一个新套接字
    // 参数意义:
    // 第一个参数:地址家族,通常是 AF_INET
    // 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
    // 第三个参数:通常为 0, 表示使用默认协议。
    // 返回值为新套接字的文件描述符,如果失败则为 -1

    // 第一步:创建一个新的套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 == sock)
    {
        perror("socket");
        return 1;
    }

    // // 第二步:绑定地址

    // // 指定地址信息
    // struct sockaddr_in myaddr;
    // myaddr.sin_family = AF_INET;            // 指定地址家族为 Internet 地址家族
    // myaddr.sin_addr.s_addr = INADDR_ANY;    // 指定 IP 地址为本机任意地址
    // //myaddr.sin_addr.s_addr = inet_addr("192.168.0.56");    // 指定 IP 地址为本机某个确定的 IP 地址
    // myaddr.sin_port = htons(8888);          // 指定端口号为 8888
    // // htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
    // // inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序

    // // 将上面指定的地址和套接字绑定
    // if(-1 == bind(sock, (struct sockaddr*)&myaddr, sizeof(myaddr)))
    // {
    //     perror("bind");
    //     return 1;
    // }

    // 第三步:连接服务器

    // 指定服务器的地址
    struct sockaddr_in srv_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    srv_addr.sin_port = htons(atoi(argv[2]));

    if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
    {
        perror("connect");
        return 1;
    }

    // 第四步:收发数据

    // 发送数据
    char msg[100];
    int ret;
    struct str_info si;


    strcpy(msg, "你是傻子!");
    si.size = strlen(msg);
    si.type = 'a';

    ret = send(sock, &si, sizeof(si), 0);
    ret = send(sock, msg, strlen(msg), 0);

    // 接收对方回复
    // ret = recv(sock, msg, sizeof(msg), 0);
    // msg[ret] = '\0';

    // if(strcmp(msg, "ok") != 0)
    // {
    //     goto end;
    // }
        

    strcpy(msg, "hello,world...");
    si.size = strlen(msg);
    si.type = 'b';

    ret = send(sock, &si, sizeof(si), 0);
    ret = send(sock, msg, strlen(msg), 0);

    //usleep(10000);

    // 接收对方回复
    // ret = recv(sock, msg, sizeof(msg), 0);
    // msg[ret] = '\0';

    // if(strcmp(msg, "ok") != 0)
    // {
    //     goto end;
    // }    

    strcpy(msg, "你好,中国!");
    si.size = strlen(msg);
    si.type = 'a';

    ret = send(sock, &si, sizeof(si), 0);
    ret = send(sock, msg, strlen(msg), 0);

end:
    // 第五步:端开连接
    close(sock);
    
    return 0;
}

服务器端

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>


#pragma pack(1)

struct str_info
{
    int size;
    char type;
};

#pragma pack()


int main()
{
    //socket 函数:创建一个新套接字
    // 参数意义:
    // 第一个参数:地址家族,通常是 AF_INET
    // 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
    // 第三个参数:通常为 0, 表示使用默认协议。
    // 返回值为新套接字的文件描述符,如果失败则为 -1

    // 第一步:创建一个新的监听套接字
    int sock_listen = socket(AF_INET, SOCK_STREAM, 0);

    if(-1 == sock_listen)
    {
        perror("socket");
        return 1;
    }

    // 第二步:绑定地址

    // 指定地址信息
    struct sockaddr_in myaddr;
    myaddr.sin_family = AF_INET;            // 指定地址家族为 Internet 地址家族
    myaddr.sin_addr.s_addr = INADDR_ANY;    // 指定 IP 地址为本机任意地址
    //myaddr.sin_addr.s_addr = inet_addr("192.168.0.56");    // 指定 IP 地址为本机某个确定的 IP 地址
    myaddr.sin_port = htons(8888);          // 指定端口号为 8888
    // htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
    // inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序

    // 将上面指定的地址和套接字绑定
    if(-1 == bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
    {
        perror("bind");
        return 1;
    }

    // 第三步:监听
    // listen 函数的第二个参数为监听等待队列的长度
    if(-1 == listen(sock_listen, 5))
    {
        perror("listen");
        return 1;
    }

    while(1)
    {
        // 第四步:接受客户端连接请求

        //accept(sock_listen, NULL, NULL);  // 如果对客户端地址信息不感兴趣

        // 如果想获取当前客户端的地址信息,就使用下面的方式
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);

        // 调用 accept 函数接收一个客户端连接请求(队头请求)
        // 如果成功,返回值为一个套接字描述符,这个套接字是和该客户端一一对应的
        // 这个套接字专门用于和对应的客户端通信,所以通常称它为连接套接字。
        // 如果当前没有任何客户端连接请求到来,accept 函数将会阻塞当前线程,直到成功接收到一个连接请求或出错
        int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len);

        if(-1 == sock_conn)
        {
            perror("accept");
        }

        // 第五步:收发数据

        // 发送数据
        char msg[101] = "";
        char buf[101] = "";
        int ret, cnt;
        struct str_info si;

        recv(sock_conn, &si, sizeof(si), 0);

        // 最稳妥做法
        cnt = 0;
        // 接收数据,如果当前没有任何数据,recv 函数会阻塞当前线程,直到成功接收到数据或对方断开连接(返回 0)或出错(返回 -1)。
        while(cnt < si.size && (ret = recv(sock_conn, buf, si.size - cnt, 0)) > 0)
        {
            memcpy(msg + cnt, buf, ret);
            cnt += ret;
        }

        msg[cnt] = '\0';
        printf("%s\n", msg);

        // send(sock_conn, "ok", 2, 0);

        // 简单做法,不一定可靠!
        recv(sock_conn, &si, sizeof(si), 0);
        ret = recv(sock_conn, msg, si.size, 0);
        msg[ret] = '\0';
        printf("%s\n", msg);

        // send(sock_conn, "ok", 2, 0);

        // 简单做法,不一定可靠!
        recv(sock_conn, &si, sizeof(si), 0);
        ret = recv(sock_conn, msg, si.size, 0);
        msg[ret] = '\0';
        printf("%s\n", msg);

        // send(sock_conn, "ok", 2, 0);

        // 第六步:断开客户端连接
        close(sock_conn);
    }

    // 第七步:关闭监听套接字
    close(sock_listen);
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值