【网络】(二)流协议粘包问题

本文深入探讨了TCP粘包问题产生的原因,并详细解释了如何通过定长包、包头指明数据长度等方法解决粘包问题,同时提供了实际的C语言代码实现案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、流协议与粘包

  TCP协议是基于字节流的,数据无边界,无边界反应在对方接收消息时无法保证一次性返回多少个字节,有可能收到的不是一个完整的消息,可能是半个消息、或者多于一个消息,这就是TCP的粘包问题

  UDP协议基于数据报,数据有边界,体现在对方一次收到的是一个完整的消息

产生粘包的因素

这里写图片描述

  如上图所示,应用层首先用write函数将要发送的数据拷贝到套接字发送缓冲区中,该缓冲区的大小由SO_SNDBUF来指定,如果应用程序缓冲区的大小超过了套接字发送缓冲区大小,这时候消息就会被分割,第一部分消息从发送缓冲区中发送出去后,后面的数据才进去发送缓冲区,这样对方在接收数据时,就先收到数据的一部分,另一部分就有延时,这样粘包问题就产生了;
  再者,TCP传输层协议传输的数据段有最大段MSS的限制,数据如果太大,那么会被分割为MSS大小的数据节,这也会导致粘包问题;
  或者,数据链路层有最大传输单元MTU的限制,如果数据超过了MTU,那么在IP就要对数据进行分组(分片),这也会造成粘包问题!另外,TCP的流量控制,拥塞控制,延迟发送机制,都可能会造成粘包问题!

 
2、解决粘包问题

既然TCP没有自己维护数据的边界,那么就需要我们自己在应用层维护!方法有几种:

  • 定长包
  • 包尾加上\r\n (FTP协议就这么干的)
  • 包头指明包的数据长度
  • 设计更复杂的应用层协议

一些问题:
如果采用定长包,那么两方通信会很浪费带宽,比如:定长定为1024字节,而实际的数据只有10个字节,而还是会填充成1024个字节去发送,这就浪费了1014个字节,因此最合适的方法还是在包头指明包的数据长度,这样灵活高效!

 
3、代码实现

公用函数的定义

#define handle_error(msg)   \
                    do{perror(msg);exit(EXIT_FAILURE);}while(0)

//包结构的定义
typedef struct package
{
    int len;            //包实际的数据长度
    char buf[1024];     //包的数据存储区
}Package;

//接收指定数目的数据
ssize_t readn(int fd, void *buf, size_t count)
{
  if((fd < 0) || (buf == NULL) || (count < 0))
        return -1;
  size_t nleft = count;   //剩余字节数
  ssize_t nread = 0;      //已读字节数
  char *pbuf = (char*)buf;
  while(nleft > 0)
  {
    if((nread = read(fd, pbuf, nleft)) < 0)
    {
        if(errno == EINTR)
            continue;
        return -1;
    }
    else if (nread == 0)
        return count - nleft;
    pbuf += nread;
    nleft -= nread;
  }
  return count;
}

//发送指定数目的数据
ssize_t writen(int fd, const void *buf, size_t count)
{
    if((fd < 0) || (buf == NULL) || (count < 0))
          return -1;
    size_t nleft = count;   //剩余字节数
    ssize_t nwritten = 0;      //已发送字节数
    char *pbuf = (char*)buf;
    while (nleft > 0)
    {
        if((nwritten = write(fd, pbuf, nleft)) < 0)
        {
            if(errno == EINTR)
                continue;
            return -1;
        }
        else if(nwritten == 0)
            continue;
        pbuf += nwritten;
        nleft -= nwritten;
    }
    return count;
}

服务端代码
server.c

编译命令:gcc -Wall -g -std=gnu99 server.c -o server

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


//忽略子进程的SIG_CHLD信号
void handle_SIGIGN(void)
{
  struct sigaction act_chld;
  act_chld.sa_handler = SIG_IGN;
  act_chld.sa_flags = 0;
  sigemptyset(&act_chld.sa_mask);

  if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕获终端中断信号
      handle_error("sigaction SIGCHLD");
}

void do_work(int sock);


int main(void)
{
    handle_SIGIGN();

    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");


    //使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
    int on = 1;
    if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        close(sk_fd);
        handle_error("setsockopt");
    }


    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("bind");
    }

    //被动套接字
    if(listen(sk_fd, SOMAXCONN) < 0)     //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
    {
        close(sk_fd);
        handle_error("listen");
    }


    struct sockaddr_in cl_addr;
    socklen_t cl_length = sizeof(cl_addr);
    pid_t pid;
    while (true)
    {
        memset(&cl_addr,0,sizeof(cl_addr));
        int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
        if(ac_sk < 0)
        {
            if(errno == EINTR)
                continue;
            close(sk_fd);
            handle_error("accept");
        }
        printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));

        //每个客户端对应一个子进程
        pid = fork();
        if(pid == -1)
            handle_error("fork");
        if(pid == 0)
        {
            close(sk_fd);
            do_work(ac_sk);
            close(ac_sk);
            exit(EXIT_SUCCESS);
        }
        else
            close(ac_sk);
    }
    close(sk_fd);
    return 0;
}


void do_work(int sock)
{
    Package recvbuf;
    while(true)
    {
        memset(&recvbuf,0,sizeof(recvbuf));
        int iret = readn(sock,&recvbuf.len,sizeof(recvbuf.len));    //获取包数据长度
        if(iret == -1)
            handle_error("read");
        else if(iret < sizeof(recvbuf.len))
        {
            printf("Client was closed!\n");
            break;
        }
        int n = ntohl(recvbuf.len);     //网络字节序转换为主机字节序
        iret = readn(sock, recvbuf.buf, n);     //正式接收数据
        if(iret == -1)
            handle_error("read");
        else if(iret < n)
        {
            printf("Client was closed!\n");
            break;
        }
        printf("recv = %d\n", n);
        fputs(recvbuf.buf,stdout);
        writen(sock, &recvbuf, sizeof(recvbuf.len) + n);    //回传数据
    }
}

客户端代码
client.c

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

int main(void)
{
    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");
    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    //sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("connect");
    }

    Package sendbuf;
    Package recvbuf;
    while (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
    {
        int n = strlen(sendbuf.buf);
        sendbuf.len = htonl(n);     //主机字节序转换为网络字节序
        writen(sk_fd, &sendbuf, sizeof(sendbuf.len) + n);   //发送数据

        memset(&sendbuf, 0, sizeof(sendbuf));
        memset(&recvbuf, 0, sizeof(recvbuf));
        int iret = readn(sk_fd, &recvbuf.len, sizeof(recvbuf.len)); //接收包数据长度
        if(iret == -1)
            handle_error("read");
        else if(iret < sizeof(recvbuf.len))
        {
            printf("Server was closed!\n");
            break;
        }
        n = ntohl(recvbuf.len);     //网络字节序转换为主机字节序
        iret = readn(sk_fd, recvbuf.buf, n);        //正式接收数据
        if(iret == -1)
            handle_error("read");
        else if(iret < n)
        {
            printf("Server was closed!\n");
            break;
        }
        printf("recv = %d\n", n);
        fputs(recvbuf.buf, stdout);
    }
    close(sk_fd);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值