UNIX网络编程-卷1_TCP粘包问题解决方法

news2024/10/1 15:10:57

这篇文件记录流协议粘包问题。首先记录什么是粘包,其次介绍粘包产生的原因,最后给出粘包的解决方法。

1 流协议与粘包

在这里插入图片描述
第一种情况:主机B一次读一个M消息 ,不会产生粘包;
第二种情况:主机B一次读M1+M2个消息
第三种情况:主机B第一次读M1消息,第二次读m2前半段消息;第二M2后半段消息
第四种情况:主机B第一次读M1前半段,第二次读M1后半段,第三次读M2消息
后三种情况都会产生粘包。

2 粘包产生原因

在这里插入图片描述
1 应用层通过套接字发送到TCP缓冲区,如果消息缓冲区不能容纳这一次消息,也就是消息被分割了,产生了粘包。
2 如果TCP传输的段,MSS,又会对缓冲区的数据进行分割;
3 在IP层进程分片,可能产生粘包。
4 TCP 流量控制,拥塞控制。

3 粘包解决方案

定包长
包尾加\r\n(ftp)
包头加上包体长度:包头 + 包体长度,使用一个结构体,前面4个字节存放包头,后面存放消息。
本篇笔记包头加上包体长度方式解决粘包问题。
下面的代码实现的readn和writen.

服务端:


/**
 * 支持多个客户端链接
 *  每次监听到链接,都会创建一个新的进程
 *
 */

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
// Andy : 自定义发送数据的协议
struct packet
{
    int len;
    char buf[1024];
};

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;     // 剩余要读取的字节数
    ssize_t nread;            // 表示接收到的字节数
    char *bufp = (char *)buf; // 按字节,继续向后读

    while (nleft > 0)
    {
        nread = read(fd, bufp, nleft);
        if (nread < 0)
        {
            if (errno == EINTR) // 被中断
            {
                continue;
            }
            else
            {
                return -1;
            }
        }
        else if (nread == 0) // Andy: 如果对方关闭链接,返回读取到的字节数量
        {
            return count - nleft;
        }

        bufp += nread;  // 从新位置开始读;
        nleft -= nread; // 剩余要读取的字节数
    }

    return count;
}

// 向缓冲区中写入固定count数量的字节
ssize_t writen(int fd, const void *buf, size_t count) // const void *buf 要写入内存的数据
{
    size_t nleft = count; // 剩余要写入的字节数
    ssize_t nwritten;     // 已写入到缓冲区中的字节数量
    char *bufp = (char *)buf;

    while (nleft > 0)
    {
        nwritten = write(fd, bufp, nleft);
        if (nwritten < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            else
            {
                return -1;
            }
        }
        else if (nwritten == 0) // 缓冲区满了,继续写
        {
            continue;
        }

        bufp += nwritten;  // 从字符串+nwritten位置继续写
        nleft -= nwritten; // 剩余要写入的
    }

    return count;
}

void do_service(int conn)
{
    // Andy: 分两次接收,先接受定长4字节,再接收固定数量的
    struct packet recvbuf;
    while (1)
    {
        memset(&recvbuf, 0, sizeof(recvbuf));
        // Andy: 先读取4个字节的包长度
        int ret = readn(conn, &recvbuf.len, 4); // 将读取到的字节放入recvbuf.len
        if (ret == -1)
        {
            ERR_EXIT("read");
        }
        else if (ret < 4)
        {
            printf("client close\n");
            break;
        }
        int n = ntohl(recvbuf.len); // Andy: 转为网络字节序
        // 第二次读
        ret = readn(conn, recvbuf.buf, n);
        if (ret == -1)
        {
            ERR_EXIT("read");
        }
        else if (ret < n)
        {
            printf("client close\n");
            break;
        }

        fputs(recvbuf.buf, stdout);
        writen(conn, &recvbuf, 4 + n);
    }
}

int main()
{
    // 第一步, 创建套接字
    int listenfd;
    int ret = 0;
    listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenfd < 0)
    {
        ERR_EXIT("socket");
    }

    // 第二步:将ip绑定到套接字
    // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5888);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr 将 ip 转为 uint
    ret = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if (ret < 0)
    {
        ERR_EXIT("bind");
    }
    // 第三步:将主动文件描述符变为被动文件描述符
    // int listen(int sockfd, int backlog);
    // SOMAXCONN 半链接状态最大长度
    if (listen(listenfd, SOMAXCONN) < 0)
    {
        ERR_EXIT("listen");
    }

    // 第四步,从已完成队列中得到一个链接,并将已完成队列中删除这条链接,如果已链接队列是空,则阻塞
    // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    struct sockaddr_in peeraddr; // 存放对端Ip地址。
    socklen_t peerlen = sizeof(peeraddr);

    int conn;

    // 第五步,获取数据

    pid_t pid;
    //
    while (1)
    {
        // 循环监听,当有一个客户端链接时候,创建一个子进程,与客户端建立链接,并发送数据。
        conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen);
        if (conn < 0)
        {
            ERR_EXIT("connect");
        }
        printf("%s,%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
        {
            ERR_EXIT("fork");
        }
        if (pid == 0)
        {
            // 子进程不需要监听 被动套接字
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS); // 退出,并关闭文件描述符
        }
        else // 父进程不需要链接conn
            close(conn);
    }

    return 0;
}

客户端:

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

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

struct packet
{
    int len;
    char buf[1024];
};

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;     // 剩余要读取的字节数
    ssize_t nread;            // 表示接收到的字节数
    char *bufp = (char *)buf; // 按字节,继续向后读

    while (nleft > 0)
    {
        nread = read(fd, bufp, nleft);
        if (nread < 0)
        {
            if (errno == EINTR) // 被中断
            {
                continue;
            }
            else
            {
                return -1;
            }
        }
        else if (nread == 0) // Andy: 如果对方关闭链接,返回读取到的字节数量
        {
            return count - nleft;
        }

        bufp += nread;  // 从新位置开始读;
        nleft -= nread; // 剩余要读取的字节数
    }

    return count;
}

// 向缓冲区中写入固定count数量的字节
ssize_t writen(int fd, const void *buf, size_t count) // const void *buf 要写入内存的数据
{
    size_t nleft = count; // 剩余要写入的字节数
    ssize_t nwritten;     // 已写入到缓冲区中的字节数量
    char *bufp = (char *)buf;

    while (nleft > 0)
    {
        nwritten = write(fd, bufp, nleft);
        if (nwritten < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            return -1;
        }
        else if (nwritten == 0) // 缓冲区满了,继续写
        {
            continue;
        }

        bufp += nwritten;  // 从字符串+nwritten位置继续写
        nleft -= nwritten; // 剩余要写入的
    }

    return count;
}

int main()
{
    // 第一步,建立套接字
    int sockfd;
    if ((sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    // 第二步:connect 链接
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5888);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    // 第三步:
    struct packet sendbuf;
    struct packet recvbuf;
    memset(&sendbuf, 0, sizeof(sendbuf));
    memset(&recvbuf, 0, sizeof(recvbuf));

    int n;
    while (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
    {
        n = strlen(sendbuf.buf); //
        sendbuf.len = htonl(n);  // 字节序转换
        writen(sockfd, &sendbuf, 4 + n);

        // 分两次读取:先读4字节的字符串长度,然后再读字符串
        int ret = readn(sockfd, &recvbuf.len, 4);
        if (ret == -1)
        {
            ERR_EXIT("connect");
        }
        else if (ret < 4) // 链接关闭
        {
            printf("client close\n");
            break;
        }

        // 第二次读取
        n = ntohl(recvbuf.len);
        ret = readn(sockfd, recvbuf.buf, n);
        if (ret == -1)
        {
            ERR_EXIT("connect");
        }
        else if (ret < n)
        {
            printf("client close\n");
            break;
        }

        fputs(recvbuf.buf, stdout);
        memset(&sendbuf, 0, sizeof(sendbuf));
        memset(&recvbuf, 0, sizeof(recvbuf));
    }

    close(sockfd);
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据模型(下):反规范化和维度模型

接前文: 数据模型(上)_专治八阿哥的孟老师的博客-CSDN博客 数据模型(中):键和规范化_专治八阿哥的孟老师的博客-CSDN博客 5.反规范化 反规范化是选择性地违反规范化规则并在模型中重新引入冗余的过程,额外的冗余有助于降低数据检索的时间,且创建一个用户友好的模型。 数…

一文吃透 Spring 中的IOC和DI

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

【Redis应用】查询缓存相关问题解决(二)

&#x1f697;Redis应用学习第二站~ &#x1f6a9;起始站&#xff1a;【Redis应用】基于Redis实现共享session登录(一) &#x1f6a9;本文已收录至专栏&#xff1a;Redis技术学习 &#x1f44d;希望您能有所收获&#xff0c;底部附有完整思维导图 一.概述 本篇我们会一起来学习…

项目管理工具DHTMLX Gantt灯箱元素配置教程:只读模式

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

【深度探讨】公共部门在选择区块链平台时要考虑的6个方面

发表时间&#xff1a;2022年8月17日 信息来源&#xff1a;bsvblockchain.org 与私营企业相比&#xff0c;全球的公共部门组织在考虑升级软件解决方案时面临着一系列的全新挑战。公共部门的决策流程冗长而复杂&#xff0c;他们要不惜一切代价避免对现有业务造成干扰&#xff0c;…

ISP全流程简介

ISP的pipline总结 ISP(Image Signal Processor)&#xff0c;即图像处理&#xff0c;主要作用是对前端图像传感器输出的信号做后期处理&#xff0c;主要功能有线性纠正、噪声去除、坏点去除、内插、白平衡、自动曝光控制等&#xff0c;依赖于ISP才能在不同的光学条件下都能较好…

面试 - 软件工程体系

今天是我人生中的第二次面试&#xff0c;第一次面试到技术问题。 面试公司&#xff1a;无锡信捷电气股份有限公司 面试时间&#xff1a;2023 年 3 月 6 日 15:30 面试地点&#xff1a;西安工程大学&#xff08;临潼校区&#xff09;D-188 在技术面中&#xff0c;我表现的不是很…

外骨骼机器人(五):步态分析之正常步态

研究病理步态之前,需要了解正常步态,作为判断标准。但是需要记住两个问题:1.“正常”因人而异,性别、年龄、身体情况都需要考虑在内,因此,需要对不同的个体选择合适的正常标准;2.即使病人的步态与正常步态有某种不同,这也不能说明这是不可取的,也不能说明应该把它变成…

计算机网络【王道】

文章目录第一章 计算机网络体系结构计算机网络概述计算机网络的概念计算机网络的组成计算机网络的功能计算机网络的分类计算机网络的性能指标计算机网络体系结构与参考模型计算机网络分层结构计算机网络协议、接口、服务的概念ISO/OSI 参考模型和 TCP/IP模型第二章物理层基本概…

Codeforces Round 855 (Div. 3) A-E2

比赛链接&#xff1a;Dashboard - Codeforces Round 855 (Div. 3) - Codeforces A&#xff1a;模拟 题意&#xff1a;给定一个字符串&#xff0c;问这个字符串是不是猫叫。定义是猫叫得字符串&#xff1a; 1&#xff1a;必须由大写或小写得M&#xff08;m&#xff09;,E&…

【大数据是什么】

大数据是什么大数据是做什么的&#xff1f;大数据主要有哪些职位 &#xff1f;大数据运维工程师数据仓库开发工程师ETL工程师大数据开发工程师BI工程师算法工程师大数据平台开发工程师大数据架构师讲述一下自己的大数据学习之路大数据是做什么的&#xff1f; 2014年&#xff0c…

Pytorch语义分割网络的详细训练过程——以NYUv2数据集为例

目录一、构建数据集1. 对Dataset和DataLoader的理解2. torch.utils.data.Dataset3. torch.utils.data.DataLoader4. 代码分块解析5. 完整代码6. 可视化二、模型搭建三、定义损失函数和优化器四、迭代训练参考文章一、构建数据集 1. 对Dataset和DataLoader的理解 Pytorch提供了…

[ROC-RK3568-PC] [Firefly-Android] 10min带你了解RTC的使用

&#x1f347; 博主主页&#xff1a; 【Systemcall小酒屋】&#x1f347; 博主追寻&#xff1a;热衷于用简单的案例讲述复杂的技术&#xff0c;“假传万卷书&#xff0c;真传一案例”&#xff0c;这是林群院士说过的一句话&#xff0c;另外“成就是最好的老师”&#xff0c;技术…

c++11 标准模板(STL)(std::unordered_map)(六)

定义于头文件 <unordered_map> template< class Key, class T, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator< std::pair<const Key, T> > > class unordered…

【Transformers】IMDB 分类

安装 transformers 库。 !pip install transformersimport numpy as np import pandas as pd import tensorflow as tf import tensorflow_datasets as tfdsfrom transformers import BertTokenizer from sklearn.model_selection import train_test_split from transformers i…

IO学习、拓展贴

1. 字节流 1.1 FileInputStream import org.junit.jupiter.api.Test;import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;/*** 演示FileInputStream的使用(字节输入流 文件--> 程序)*/ public class FileInputStream_ {pu…

10款最佳项目管理工具推荐,总有一款适合你

为什么需要项目管理工具&#xff1f; 如今企业规模不断扩大&#xff0c;业务逐渐复杂化&#xff0c;项目管理已经成为现代企业管理中不可或缺的一环&#xff1b; 如果没有合适的项目管理工具&#xff0c;我们的项目管理和跟踪就会变得非常困难。这可能导致项目延期或者出现一…

免费Api接口汇总(亲测可用,可写项目)

免费Api接口汇总&#xff08;亲测可用&#xff09;1. 聚合数据2. 用友API3. 天行数据4. Free Api5. 购物商城6. 网易云音乐API7. 疫情API8. 免费Api合集1. 聚合数据 https://www.juhe.cn/ 2. 用友API http://iwenwiki.com/wapicovid19/ 3. 天行数据 https://www.tianapi.com…

RK356x U-Boot研究所(命令篇)3.9 scsi命令的用法

平台U-Boot 版本Linux SDK 版本RK356x2017.09v1.2.3文章目录 一、设备树与config配置二、scsi命令的定义三、scsi命令的用法3.1 scsi总线扫描3.2 scsi设备读写一、设备树与config配置 RK3568支持SATA接口,例如ROC-RK3568-PC: 原理图如下: 可以新建一个rk3568-sata.config配…