socket编程--粘包

流协议与粘包

流协议与粘包
首先说明的是发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据包排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

假设主机A send了两条消息M1和M2各10k给主机B,由于主机B一次接收的字节数是不确定的,接收方收到数据的情况可能是:
• 一次性收到20k 数据
• 分两次收到,第一次5k,第二次15k
• 分两次收到,第一次15k,第二次5k
• 分两次收到,第一次10k,第二次10k
• 分三次收到,第一次6k,第二次8k,第三次6k
• 其他任何可能

粘包产生的原因

产生的原因主要有3个:一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数
1、用于程序写入的大小大于套接字发送缓冲区大小
2、进行MSS大小的TCP分段;
3、以太网帧的payload大于MTU进行IP分片时。
粘包处理方案

粘包本质是要在应用层维护消息与消息的边界 — 在传输层没有维护消息与消息的边界

* 定长包
* 包尾加\r\n(ftp采用这种方式--如果消息中本身就要\r\n,系统就无法区分)
* 包头加上包体长度
* 更复杂的应用层协议

定包长
readn、writen
以write函数为例,解释为何用readn和writen函数:
ssize_t write(int fd, const void*buf,size_t nbytes);
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两可能.
1)write的返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.
如果错误为EINTR表示在写的时候出现了中断错误.
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).
为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.

readn、writen—写入确切数目写操作、读取确切数目读操作

/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count)  //参数和read相同-- ssize_t 有符号整数 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) // 信号中断--不认为出错
                    countinue;
               return -1;
          }
          else if(nread == 0)     //对等方关闭
               return count - nleft;   //返回已经读取的字节数
          /*读到数据*/
          bufp += nread;  //偏移
          nleft -= nread;   //剩余还要读取的数据
     }
     return count;
}

说明:
EINTR — Linux中函数的返回状态,在不同的函数中意义不同
-》write—表示:由于信号中断,没写成功任何数据
-》read—表示:由于信号中断,没读到任何数据

包头加上包体长度
需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。

比较好的解决办法,其实也可以算是自定义的一种简单应用层协议–即封包,然后解包。比如我们可以自定义一个包体结构

struct packet {
int len;
char buf[1024];
};
先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。
更改—-socket编程–TCP客户/服务器模型 (c/s)—服务器端函数中的do_serverce程序

 /* 回射服务器,客户端不停从标准输入接收数据
* 发送给服务器端,然后服务器端接收回射回去*/
void do_severce(int conn)
{
                char recvbuf[1024];
                while(1){
                        memset(recvbuf, '0', sizeof(recvbuf));
                        int ret = readn(conn, recvbuf, sizeof(recvbuf));
                        if(ret == 0){
                                printf("client close\n");
                                break;
                        }
                        else if(ret == -1){
                        fputs(recvbuf, stdout);
                        writen(conn, recvbuf, ret);
                }
}

可看到无法读取客户端发过来的数据,因为在于do_serverce中char recvbuf[1024];–读取为1024个字节。对方如果发送的数据不足1024个字节,就会阻塞,一直在readn函数中循环。
解决方案为发送定长包。
回射客户/服务器
记录:

/*服务器*/


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

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

/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count)  //参数和read相同-- ssize_t 有符号整数 size_t 无符号整数
{
     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;
}
/*writen函数包装了write函数,用于写入定长包*/
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)//没有写满buf缓冲区,继续写入数据
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }
    return count;
}



/* 回射服务器,客户端不停从标准输入接收数据,
* 发送给服务器端,然后服务器端接收回射回去*/
void do_severce(int conn)
{
          struct packet recvbuf;
          int n;
          while(1){
               memset(&recvbuf, '0', sizeof(recvbuf));  

               int ret = readn(conn, &recvbuf.len, 4);

               /*ret = 0是可判断客户端关闭*/
               if(ret == -1)
                    exit(EXIT_FAILURE);
               else if(ret < 4){
                    printf("client close\n");
                    break;
               }
               n = ntohl(recvbuf.len);
               ret = readn(conn, recvbuf.buf, n);
               if(ret == -1){
                    exit(EXIT_FAILURE);
               }
               else if(ret < n){
                    printf("client close\n");
                    break;
               }

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

int main()
{
     int listenfd ;
     if((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0){  //小于0表示创建失败
          exit(EXIT_FAILURE);
          printf("socket failed\n");
     }
    /*初始化*/
     struct sockaddr_in seraddr;
     memset(&seraddr, 0, sizeof(seraddr));   //
     seraddr.sin_family = AF_INET;
     seraddr.sin_port = htons(5188);
     seraddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 第一种方式:INADDR_ANY表示本机的任意地址
     /*seraddr.sin_addr.a_addr = inet_addr("127.0.0.1");*/  //第二种方式
     int on = 1;
     if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0){
          exit(EXIT_FAILURE);
          printf("setsockopt fialed");
     }

     /*绑定*/
     if(bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) < 0){ //sockaddr_in强制转换为socketaddr
          exit(EXIT_FAILURE);
          printf("bind failed\n");
     }
     if(listen(listenfd, SOMAXCONN)){  //SOMAXCONN表示队列的最大值
          exit(EXIT_FAILURE);
          printf("listen failed\n");
     }

     struct sockaddr_in peeraddr;//定义对方地址
     socklen_t peerlen = sizeof(peeraddr); //对方的地址长度,必须有初始值,否则accept会失败
     int conn;
     pid_t pid;
     while(1){ 
          /*父进程accept其他客户端,子进程处理连接*/
          if((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0){
               exit(EXIT_FAILURE);
               printf("listen failed\n");
          }
          printf("ip = %s port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
          pid = fork();
          if(pid == -1){
               exit(EXIT_FAILURE);
               printf("fork failed\n");              
          }
          if(pid == 0){//子进程 
               close(listenfd);   //子进程不需要监听
               do_severce(conn);
               exit(EXIT_SUCCESS);//do_serverce一旦返回,这个进程就没有用---销毁进程
          }
          else{            //父进程
               close(conn);
          }
     }

     return 0;

}
/*客户端*/



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

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

/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count)  //参数和read相同-- ssize_t 有符号整数 size_t 无符号整数
{
     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;
}
/*writen函数包装了write函数,用于写入定长包*/
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)//没有写满buf缓冲区,继续写入数据
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }
    return count;
}


int main()
{
     int sock ;
     if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0){  //小于0表示创建失败
          exit(EXIT_FAILURE);
          printf("socket failed\n");
     }
    /*初始化*/
     struct sockaddr_in seraddr;
     memset(&seraddr, 0, sizeof(seraddr));   //
     seraddr.sin_family = AF_INET;
     seraddr.sin_port = htons(5188);
     seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器地址 

     if(connect(sock, (struct sockaddr*)&seraddr, sizeof(seraddr)) < 0){
          exit(EXIT_FAILURE);
          printf("connect failed");
     }
     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(sock, &sendbuf, 4 + n);

          int ret = readn(sock, &recvbuf.len, 4);
          if(ret == -1){
               exit(EXIT_FAILURE);
          }
          else if(ret < 4){
               printf("client close\n");
               break;
          }
          n = ntohl(recvbuf.len);
          ret = readn(sock, recvbuf.buf, n);         
          if(ret == -1){
               exit(EXIT_FAILURE);
          }
          else if(ret < n){
               printf("client close\n");
               break;
          }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值