网络编程中的粘包处理

流协议与粘包

  • TCP是基于字节流传输的,只维护发送出去多少,确认了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题。
  • 粘包问题解决方法是在应用层维护消息边界。
    这里写图片描述

粘包原因

这里写图片描述

  • tcp 字节流 无边界
  • udp消息、数据报 有边界
  • 对等方,一次读操作,不能保证完全把消息读完。
  • 对方接受数据包的个数是不确定的。

产生粘包问题的原因
1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)
2、tcp传送的端 mss大小限制
3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割。
4、tcp的流量控制和拥塞控制,也可能导致粘包
5、tcp延迟发送机制 等等
结论:tcp/ip协议,在传输层没有处理粘包问题。

粘包解决方案

本质上是要在应用层维护消息与消息的边界
* 定长包
* 包尾加\r\n(ftp)
* 包头加上包体长度
* 更复杂的应用层协议

包头加上包体长度

  • 发报文时,前四个字节长度(转成网络字节序)+包体
  • 收报文时,先读前四个字节,求出长度;根据长度读数据。

  • server

/*server04*/

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>

#define MAX_CLIENT 10
#define MAX_BUF  1024

//自定义封包的结构体
typedef struct _my_pkt
{
  int len;//自定义的包头,包含了包的大小
  char buf[MAX_BUF];//包存放的数据
}my_pkt;

void handle(int sig)//辅助杀死子进程或者父进程
{
  printf("recv : %d\n",sig);
  exit(0);
}

//@ssize_t:返回读的长度 若ssize_t<count 读失败
//@buf:接受数据内存首地址
//@count:接受数据长度
ssize_t readn(int fd,void *buf,size_t cnt)
{
  size_t nleft = cnt;//定义剩余没有读取的个数
  ssize_t nread = 0;//读取的个数
  char * bufp = (char *)buf;//将参数接过来

  while(nleft > 0)//当剩余需要读取的个数>0
  {
    if((nread  = read(fd,bufp,nleft)) < 0)//成功读取的个数小于0,则判断出错的原因
    {
      //如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
            //不是信号中断则退出。
      if(errno == EINTR)
      {
        continue;
      }  
      perror("write");
      exit(-1);
    }
    else if(nread == 0)//若对方已关闭
    {
      return cnt - nleft;
    }

    bufp += nread;//将 字符串指针向后移动已经成功读取个数的大小。
    nleft -= nread;//需要读取的个数=需要读取的个数-以及成功读取的个数
  }

  return cnt;
}


//@ssize_t:返回写的长度 -1失败
//@buf:待写数据首地址
//@count:待写长度
ssize_t writen(int fd,const void *buf,size_t cnt)
{
  size_t nleft = cnt;//需要写入的个数
  ssize_t nwritten = 0;//已经成功写入的个数
  char * bufp = (char *)buf;//接参数

  while(nleft > 0)//如果需要写入的个数>0
  {
    //如果写入成功的个数<0 判断是否是被信号打断
    if((nwritten  = write(fd,bufp,nleft)) < 0)
    {
      if(errno == EINTR)//信号打断,则继续
      {
        continue;
      }  
      perror("write");
      exit(-1);
    }
    //需要写入的数据个数>0
        //如果成功写入的个数为0 则继续
    else if(nwritten == 0)
    {
      continue;
    }

    bufp += nwritten;//将bufp指针向后移动已经
    nleft -= nwritten;//剩余个数
  }

  return cnt;
}

void do_service(int fd)
{
  my_pkt rcv_pkt;//定义了封包结构体
  int num = 0;//数据包长度--封包的包头
  int ret = 0;
  while(1)
  {
    memset(&rcv_pkt,0,sizeof(rcv_pkt));//清空结构体
    ret = readn(fd,&(rcv_pkt.len),sizeof(rcv_pkt.len));//读包头 4个字节
    if(-1 == ret)
    {
      perror("readn for len");
      exit(-1);
    }
    else if(ret < 4)//如果读取的个数小于4,则对方已经关闭
    {
      printf("client close");
      break;
    }

    num = ntohl(rcv_pkt.len);//将网络数据转换为本地数据结构,比如网络数据为大端,而本地数据为小端
    ret = readn(fd,rcv_pkt.buf,num);//根据包头里包含的大小读取数据
    if(-1 == ret)
    {
      perror("readn for buf");
      exit(-1);
    }
    else if(ret < num)//如果读取的数据的大小小于封包包头中包的大小,那么对方已经关闭
    {
      printf("client close");
      break;
    }
    fputs(rcv_pkt.buf,stdout);//将数据打印出
    //将接受到的数据再直接发出去。
    writen(fd,&rcv_pkt,sizeof(rcv_pkt.len)+num);  //注意写数据的时候,多加包头长度(len)部分 
  }

}

int main()
{

  int serv_fd,con_fd;//服务器端至少要有两个套接字文件描述符--一个用来监听,一个/其余多个用来和客户端通信
  struct sockaddr_in serv_addr;//IPV4套接字结构体--服务器
  struct sockaddr_in clt_addr;//IPV4套接字结构体--客户端

  int optvar;//地址复用使用的参数
  pid_t pid;//子进程PID

  socklen_t addr_len;

  signal(SIGUSR1,handle);//注册新号和处理函数

  serv_fd = socket(AF_INET,SOCK_STREAM,0);//建立套接字
  if(-1 == serv_fd)
  {
    perror("socket");
    exit(-1);
  }

  if(setsockopt(serv_fd, SOL_SOCKET,SO_REUSEADDR,&optvar,sizeof(optvar))  == -1 )//地址复用
  {
    perror("setsockopt");
    exit(-1);
  }

  /*设置地址*/
  bzero(&serv_addr,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(8001);
  serv_addr.sin_addr.s_addr = htons(INADDR_ANY);

  if(bind(serv_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) == -1)//绑定端口
  {
    perror("bind");
    exit(-1);
  }

  /*一旦调用listen函数--套接字就会变成被动套接字--用来监听客户端,让客户端连接他
  被动套接字--只能接受连接,不能主动发送连接
  做了两个队列:
          一个已经完成三次握手,建立连接的队列--客户端发connect请求被响应,已经成功完成连接
          一个是未完成成三次握手的队列--正在握手
  */
  if(listen(serv_fd,MAX_CLIENT)== -1)//开始监听
  {
    perror("listen");
    exit(-1);
  }

  addr_len = sizeof(clt_addr);

  printf("Accepting connections ...\n");

  while(1)
  {
    if((con_fd = accept(serv_fd,(struct sockaddr *)&clt_addr,&addr_len)) == -1)//可以支持多并发访问
    {
      perror("accept");
      exit(-1);
    }

    printf("received from %s at PORT %d\n",inet_ntoa(clt_addr.sin_addr),ntohs(clt_addr.sin_port));

    pid = fork();//创建子进程

    if(pid == -1)
    {
      perror("fork");
      close(serv_fd);
      exit(-1);
    }
    else if(pid == 0)//子进程负责接收客户端数据
    {
      close(serv_fd);
      do_service(con_fd);
      close(con_fd);
      kill(getppid(),SIGUSR1);//发送信号给父进程--通知他死期已到
      exit(1);
    }
    else//父进程负责监听客户端连接请求
    {
      close(con_fd);
    }
  }

  return 0;
}
  • client
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>

#define MAX_BUF  1024
//自定义封包的结构体
typedef struct _my_pkt
{
  int len;//自定义的包头,包含了包的大小
  char buf[MAX_BUF];//包存放的数据
}my_pkt;

void handle(int sig)
{
  printf("recv : %d\n",sig);
  exit(0);
}

//@ssize_t:返回读的长度 若ssize_t<count 读失败
//@buf:接受数据内存首地址
//@count:接受数据长度
ssize_t readn(int fd,void *buf,size_t cnt)
{
  size_t nleft = cnt;//定义剩余没有读取的个数
  ssize_t nread = 0;//读取的个数
  char * bufp = (char *)buf;//将参数接过来

  while(nleft > 0)//当剩余需要读取的个数>0
  {
    if((nread  = read(fd,bufp,nleft)) < 0)//成功读取的个数小于0,则判断出错的原因
    {
      //如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
            //不是信号中断则退出。
      if(errno == EINTR)
      {
        continue;
      }  
      perror("write");
      exit(-1);
    }
    else if(nread == 0)//若对方已关闭
    {
      return cnt - nleft;
    }

    bufp += nread;//将 字符串指针向后移动已经成功读取个数的大小。
    nleft -= nread;//需要读取的个数=需要读取的个数-以及成功读取的个数
  }

  return cnt;
}


//@ssize_t:返回写的长度 -1失败
//@buf:待写数据首地址
//@count:待写长度
ssize_t writen(int fd,const void *buf,size_t cnt)
{
  size_t nleft = cnt;//需要写入的个数
  ssize_t nwritten = 0;//已经成功写入的个数
  char * bufp = (char *)buf;//接参数

  while(nleft > 0)//如果需要写入的个数>0
  {
    //如果写入成功的个数<0 判断是否是被信号打断
    if((nwritten  = write(fd,bufp,nleft)) < 0)
    {
      if(errno == EINTR)//信号打断,则继续
      {
        continue;
      }  
      perror("write");
      exit(-1);
    }
    //需要写入的数据个数>0
        //如果成功写入的个数为0 则继续
    else if(nwritten == 0)
    {
      continue;
    }

    bufp += nwritten;//将bufp指针向后移动已经
    nleft -= nwritten;//剩余个数
  }

  return cnt;
}

int main()
{

  int clt_fd;
  struct sockaddr_in serv_addr;

  char addr_dst[INET_ADDRSTRLEN] = {0};
  ssize_t ret;

  my_pkt sendbuf;
    my_pkt recvbuf;
  int num = 0;

  memset(&sendbuf,0,sizeof(sendbuf));//清空结构体
  memset(&recvbuf,0,sizeof(recvbuf));//清空结构体

  signal(SIGUSR1,handle);

  clt_fd = socket(AF_INET,SOCK_STREAM,0);
  if(-1 == clt_fd)
  {
    perror("socket");
    exit(-1);
  } 

  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(8001);
  serv_addr.sin_addr.s_addr = inet_addr("192.168.1.110");

  if(connect(clt_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1)
  {
    perror("connect");
    exit(-1);
  }
  printf("Connect successfully\t%s at PORT %d\n",inet_ntop(AF_INET,&serv_addr.sin_addr,addr_dst,sizeof(addr_dst)),ntohs(serv_addr.sin_port));

  while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL)
  {
    num = strlen(sendbuf.buf);
    sendbuf.len = htonl(num);

    writen(clt_fd,&sendbuf,sizeof(sendbuf.len)+num);


    ret = readn(clt_fd,&(recvbuf.len),sizeof(recvbuf.len));//读包头 4个字节
    if(-1 == ret)
    {
      perror("readn for len");
      exit(-1);
    }
    else if(ret < 4)//如果读取的个数小于4,则对方已经关闭
    {
      printf("client close");
      break;
    }

    num = ntohl(recvbuf.len);//将网络数据转换为本地数据结构,比如网络数据为大端,而本地数据为小端
    ret = readn(clt_fd,recvbuf.buf,num);//根据包头里包含的大小读取数据
    if(-1 == ret)
    {
      perror("readn for buf");
      exit(-1);
    }
    else if(ret < num)//如果读取的数据的大小小于封包包头中包的大小,那么对方已经关闭
    {
      printf("client close");
      break;
    }
    fputs(recvbuf.buf,stdout);//将数据打印出

    memset(&sendbuf,0,sizeof(sendbuf));//清空结构体
    memset(&recvbuf,0,sizeof(recvbuf));//清空结构体
  }

  return 0;
}

包尾加上\n编程实践

  • `ssize_t recv(int s, void *buf, size_t len, int flags);
  • 与read相比,只能用于套接字文件描述符;
  • 多了一个flags
       MSG_OOB
   This  flag requests receipt of out-of-band data that would not be received in the normal data stream.  Some protocols place expedited data at thehead of the normal data queue, and thus this flag cannot be used with such protocols.
带外数据 紧急指针
       MSG_PEEK
This flag causes the receive operation to return data from the beginning of the receive queue without removing that data from the queue.  Thus, asubsequent receive call will return the same data.
        可以读数据,不从缓存区中读走,利用此特点可以方便的实现按行读取数据。 

一个一个字符的读,方法不好–多次调用系统调用read方法.

#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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*) buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        } else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;//需要写入的数据个数
    ssize_t nwritten;//
    char *bufp = (char*) buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        } else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}
//从指定的socket中读取指定大小的数据但 不取出,封装后不被信号中断
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        //MSG_PEEK 读取队列中指定大小的数据,但不取出
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        //如果被信号中断,则继续
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

//maxline 一行最大数
//先提前peek一下缓冲区,如果有数据从缓冲区的读数据,
//1、缓冲区数据中带\n
//2 缓存区中不带\n
//读取读取包直到\n
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//成功预读取的数据的个数
    char *bufp = buf;//读取数据存放的数组,在外分配内存
    int nleft = maxline;//封包最大值
    while (1)
    {
        //看一下缓冲区有没有数据,并不移除内核缓冲区数据
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0) //失败 
            return ret;
        else if (ret == 0) //对方已关闭
            return ret;

        nread = ret;
        int i;
        for (i = 0; i < nread; i++)
        {
            if (bufp[i] == '\n') //若缓冲区有\n
            {
                ret = readn(sockfd, bufp, i + 1); //读走数据
                if (ret != i + 1)
                    exit(EXIT_FAILURE);
                return ret; //有\n就返回,并返回读走的数据
            }
        }

        if (nread > nleft) //如果读到的数大于 一行最大数 异常处理
            exit(EXIT_FAILURE);

        nleft -= nread;  若缓冲区没有\n, 把剩余的数据读走
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);
        bufp += nread; //bufp指针后移后,再接着偷看缓冲区数据recv_peek,直到遇到\n
    }

    return -1;
}

void do_service(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }

        fputs(recvbuf, stdout);
        //将读到的数据再发送,发送没有对\n处理
        writen(conn, recvbuf, strlen(recvbuf));
    }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        /*  if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

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

    int on = 1;
    //设置socket 地址复用
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*) &peeraddr, &peerlen))
                < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%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
            close(conn);
    }

    return 0;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值