• Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)


    一、Linux发送网络消息的过程

          (1) 应用程序调用write()将消息发送到内核中

           ( 2)内核中的缓存达到了固定长度数据后,一般是SO_SNDBUF,将发送到TCP协议层

          (3)IP层从TCP层收到数据,会加上自己的包头然后发送出去。一般分片的大小是MTU(含IP包头),而IPV4下IP的包头长度为40,而IPV6下为60,因此,TCP中分片后,有效的数据长度为MSS = MTU - 40 或 MSS = MTU -60

           (4)最终经过其他层的包装,发送到公网上,跑来跑去,这时候,你的数据可能几段连为一条,一条可能分为几段。

            

    二、粘包问题

         上一篇文章中,我们用write()系统调用来读取数据,但是这个调用需要指定长度,例如上文中的1024,那么问题来了:

         (1)报文有效数据长1025怎么办 ? 对方发“Hi,I like you!” 你期望收到“Hi, I like ”吗

         (2)报文有效数据长度300怎么办? 对方发“Hi, I like you!” "You are befutiful" 你期望收到“Hi, I like you!You are”么? 你不想知道 you are 什么,还有,明明对方发送了两条消息,而你。。。收到了一条半,还当作了一条。

    三、解决

         3.1主要有两种解决方案,分别为

         (1)认为的加边界 例如以RN为界限,FTP协议就是用的这种方法。

          (2)建立一个数据结构,如下:

          

    1 struct packet
    2 {
    3     int len;
    4     char buff[1024];
    5 };

    发送前,将packet.len设置好,然后将该数据结构的一个实例发送过去,读的时候先读取int长度即4个字节的数据,获得buff的有效长度,然后循环读,直到读够len字节的数据为止。

         本文主要介绍第二种设定数据结构的方案。该方案的一个小缺点是,单次写不会超过buff[1024]的大小限制。。。  

        3.2  readn函数:

           

     1 ssize_t readn(int sock, void *recv, size_t len)
     2 {
     3     size_t nleft = len;
     4     ssize_t nread;
     5     char *bufp = (char*)recv;  // 辅助指针变量,记录位置的。
     6     while(nleft > 0){
     7         if((nread = read(sock,bufp,nleft)) < 0){ //read error    读len,当然可能被中断读不够len,所以继续
     8             if(errno == EINTR){ // 被信号中断到
     9                 continue;
    10             }
    11             return -1;
    12         }
    13         else if(nread == 0){ // 若对方已关闭,返回已读字数。
    14             return len - nleft;
    15         }
    16         bufp += nread; // mov point
    17         nleft -= nread;
    18     }
    19     return len;
    20 }
    readn

       3.3  writen函数;

        

     1 ssize_t writen(int sock,const void *buf, size_t len)
     2 {
     3     size_t nleft = len;
     4     ssize_t nwrite;
     5     char *bufp = (char*)buf;
     6 
     7     while(nleft > 0){
     8         if((nwrite = write(sock,bufp,nleft)) < 0){
     9             if(errno == EINTR){ // 信号中断
    10                 continue;
    11             }
    12             return -1;
    13         }
    14         else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
    15             continue;
    16         }
    17         bufp += nwrite;
    18         nleft -= nwrite;
    19     }
    20     return len;
    21 }
    writen

       3.4利用这两个函数,即可完成读写。下文将介绍利用这两个函数完成的一个P2P程序,服务端与客户端互相发送用户输入的数据:程序的架构如下:

            

           3.4.1 服务端:

            

      1 #include <unistd.h>
      2 #include <sys/stat.h>
      3 #include <sys/wait.h>
      4 #include <sys/types.h>
      5 #include <fcntl.h>
      6 
      7 #include <stdlib.h>
      8 #include <stdio.h>
      9 #include <errno.h>
     10 #include <string.h>
     11 #include <signal.h>
     12 
     13 #include <arpa/inet.h>
     14 #include <sys/socket.h>
     15 #include <netinet/in.h>
     16 #include <string.h>
     17 
     18 #define ERR_EXIT(m) 
     19         do { 
     20             perror(m);
     21             exit(EXIT_FAILURE);
     22         }while(0)
     23 
     24 struct packet
     25 {
     26     int len;
     27     char buff[1024];
     28 };
     29 
     30 ssize_t readn(int sock, void *recv, size_t len)
     31 {
     32     size_t nleft = len;
     33     ssize_t nread;
     34     char *bufp = (char*)recv;  // 辅助指针变量,记录位置的。
     35     while(nleft > 0){
     36         if((nread = read(sock,bufp,nleft)) < 0){ //read error    读len,当然可能被中断读不够len,所以继续
     37             if(errno == EINTR){ // 被信号中断到
     38                 continue;
     39             }
     40             return -1;
     41         }
     42         else if(nread == 0){ // 若对方已关闭,返回已读字数。
     43             return len - nleft;
     44         }
     45         bufp += nread; // mov point
     46         nleft -= nread;
     47     }
     48     return len;
     49 }
     50 ssize_t writen(int sock,const void *buf, size_t len)
     51 {
     52     size_t nleft = len;
     53     ssize_t nwrite;
     54     char *bufp = (char*)buf;
     55 
     56     while(nleft > 0){
     57         if((nwrite = write(sock,bufp,nleft)) < 0){
     58             if(errno == EINTR){ // 信号中断
     59                 continue;
     60             }
     61             return -1;
     62         }
     63         else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
     64             continue;
     65         }
     66         bufp += nwrite;
     67         nleft -= nwrite;
     68     }
     69     return len;
     70 }
     71 void handle(int sig)
     72 {
     73     printf("recv sig = %d
    ", sig);
     74     exit(0);
     75 }
     76 int main(void)
     77 {
     78     signal(SIGUSR1,handle);
     79 
     80     int sockfd;
     81     // 创建一个Socket
     82     sockfd = socket(AF_INET,SOCK_STREAM,0);
     83     if(sockfd == -1){
     84         perror("error");
     85         exit(0);
     86     }
     87 
     88 
     89     ///////////////////////////////////////////////////////////
     90 //    struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
     91     struct sockaddr_in sockdata;
     92     sockdata.sin_family = AF_INET;
     93     sockdata.sin_port = htons(8001);
     94     sockdata.sin_addr.s_addr = inet_addr("192.168.59.128");
     95     
     96     int optval = 1;
     97     if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
     98     {
     99         perror("error");
    100         exit(0);
    101     }
    102     if(bind(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) < 0){
    103         perror("error");
    104         exit(0);
    105     }
    106     
    107     ////////////////////////////////////////////////////////////
    108     if(listen(sockfd,SOMAXCONN) == -1){ //变成被动侦听套接字。
    109         perror("error");
    110         exit(0);
    111     }
    112     
    113     //////////////////////////////////////////////////////////
    114     struct sockaddr_in peeradr;
    115     socklen_t peerlen = sizeof(peeradr); // 得有初始值
    116 
    117 
    118     /////////////////////////////////////////////////////////
    119     int conn = 0;
    120     conn = accept(sockfd,(struct sockaddr *)&peeradr,&peerlen);
    121     if(conn == -1){            
    122         perror("error");
    123         exit(0);
    124     }    
    125                 
    126     printf("收到的IP %s
     客户端端口是:%d
    ,conn == %d
    ",inet_ntoa(peeradr.sin_addr),ntohs(peeradr.sin_port),conn);
    127 
    128     pid_t twopid;
    129     twopid = fork();
    130 
    131     if(twopid == -1){
    132         perror("error");
    133         exit(0);
    134     }
    135     if(twopid > 0){ // father , 接受数据
    136         struct packet recvBuff;
    137         memset(&recvBuff,0,sizeof(recvBuff));
    138         int ret = 0;
    139         int rn;
    140         while(1){
    141             ret = readn(conn,&recvBuff,4); // 先获得长度
    142             if(ret == -1){
    143                 ERR_EXIT("READ");
    144             }
    145             if(ret < 4){
    146                 printf("client close
    ");
    147                 break;
    148             }
    149             rn = ntohl(recvBuff.len);
    150             ret = readn(conn,recvBuff.buff,rn);
    151             if(ret == -1){
    152                 ERR_EXIT("READ");
    153             }
    154             if(ret < rn){
    155                 printf("client close
    ");
    156                 break;
    157             }
    158             fputs(recvBuff.buff,stdout);
    159             memset(&recvBuff,0,sizeof(recvBuff));
    160         }
    161         printf("client closed"); // may this create a guer process
    162         // send signal to child
    163         kill(twopid, SIGUSR1);
    164         close(conn);
    165         close(sockfd);
    166         sleep(2);
    167         exit(0);
    168     }
    169     if(twopid == 0){ // child send data
    170         close(sockfd);
    171         int n;
    172         struct packet sendBuff;
    173         memset(&sendBuff,0,sizeof(sendBuff));
    174         while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){
    175             n = strlen(sendBuff.buff);
    176             sendBuff.len = htonl(n);
    177             writen(conn,&sendBuff,4+n);
    178             memset(&sendBuff,0,sizeof(sendBuff));
    179         }
    180         exit(0);
    181     }
    182     return 0;
    183 }
    server.c

           3.4.2 客户端:

           

      1 #include <unistd.h>
      2 #include <sys/stat.h>
      3 #include <sys/wait.h>
      4 #include <sys/types.h>
      5 #include <fcntl.h>
      6 
      7 #include <stdlib.h>
      8 #include <stdio.h>
      9 #include <errno.h>
     10 #include <string.h>
     11 #include <signal.h>
     12 
     13 #include <arpa/inet.h>
     14 #include <sys/socket.h>
     15 #include <netinet/in.h>
     16 #include <string.h>
     17 #define ERR_EXIT(m) 
     18         do { 
     19             perror(m);
     20             exit(EXIT_FAILURE);
     21         }while(0)
     22 
     23 struct packet
     24 {
     25     int len;
     26     char buff[1024];
     27 };
     28 
     29 ssize_t readn(int sock, void *recv, size_t len)
     30 {
     31     size_t nleft = len;
     32     ssize_t nread;
     33     char *bufp = (char*)recv;  // 辅助指针变量,记录位置的。
     34     while(nleft > 0){
     35         if((nread = read(sock,bufp,nleft)) < 0){ //read error    读len,当然可能被中断读不够len,所以继续
     36             if(errno == EINTR){ // 被信号中断到
     37                 continue;
     38             }
     39             return -1;
     40         }
     41         else if(nread == 0){ // 若对方已关闭,返回已读字数。
     42             return len - nleft;
     43         }
     44         bufp += nread; // mov point
     45         nleft -= nread;
     46     }
     47     return len;
     48 }
     49 ssize_t writen(int sock,const void *buf, size_t len)
     50 {
     51     size_t nleft = len;
     52     ssize_t nwrite;
     53     char *bufp = (char*)buf;
     54 
     55     while(nleft > 0){
     56         if((nwrite = write(sock,bufp,nleft)) < 0){
     57             if(errno == EINTR){ // 信号中断
     58                 continue;
     59             }
     60             return -1;
     61         }
     62         else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
     63             continue;
     64         }
     65         bufp += nwrite;
     66         nleft -= nwrite;
     67     }
     68     return len;
     69 }
     70 int main(void)
     71 {
     72     int sockfd;
     73     // 创建一个Socket
     74     sockfd = socket(AF_INET,SOCK_STREAM,0);
     75     if(sockfd == -1){
     76         perror("error");
     77         exit(0);
     78     }
     79 
     80 
     81     ///////////////////////////////////////////////////////////
     82 //    struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
     83     struct sockaddr_in sockdata;
     84     sockdata.sin_family = AF_INET;
     85     sockdata.sin_port = htons(8001);
     86     sockdata.sin_addr.s_addr = inet_addr("192.168.59.128");
     87     if(connect(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) == -1){
     88         perror("error");
     89         exit(0);
     90     }
     91     pid_t pid = 0;
     92     pid = fork();
     93     if(pid == -1){         perror("error");
     94         exit(0);
     95     }
     96     if(pid > 0){ // father     // ccept data from keyboad
     97         struct packet sendBuff;
     98         memset(&sendBuff,0,sizeof(sendBuff));
     99         int n;
    100         while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){
    101             
    102             n = strlen(sendBuff.buff);
    103             // 设置发送消息到长度。
    104             sendBuff.len = htonl(n);
    105             // 将结构体实例写入。
    106             writen(sockfd,&sendBuff,4+n);
    107 
    108             // 清零
    109             memset(&sendBuff,0,sizeof(sendBuff));
    110         }
    111 
    112     }
    113     if(pid == 0){ // child recv data
    114         struct packet recvBuff;
    115         memset(&recvBuff,0,sizeof(recvBuff));
    116         // 从服
    117         int ret;
    118         int rn;
    119         while(1){
    120             // 首先获得要读取到长度,前4个字节
    121             ret = readn(sockfd,&recvBuff.len,4);
    122             if(ret == -1){
    123                 ERR_EXIT("READ");
    124             }
    125             if(ret < 4){
    126                 printf("server close
    ");
    127                 break;
    128             }
    129 
    130             // 读取4个字节开始到数据。
    131             rn = ntohl(recvBuff.len);
    132             ret = readn(sockfd,recvBuff.buff,rn);
    133             if(ret == -1){
    134                 ERR_EXIT("read error");
    135             }
    136             if(ret < rn ){
    137                 printf("server close
    ");
    138                 break;
    139             }
    140             // put it to screen
    141             fputs(recvBuff.buff,stdout);
    142             // 清零
    143             memset(&recvBuff,0,sizeof(recvBuff));
    144         }
    145 
    146     }
    147     
    148     close(sockfd);
    149     return 0;    
    150 }
    client.c

    后记:

       由于上文中获得len的大小的方式是 测试buff[1024]中有效数据的长度的,所以实际上len每一个不会超过1024。

       但是,当fgets函数接受的一行长度大于1023的时候,它会将剩下的(1023以后的)字符串作为下一次的输入。然后发送端会发送两个Packet实例。

        而接收端接收到的两个pocket都有正确的长度,所以可以安全的接受,但是不幸的是,会将一条报文分成多条。。。

        该程序是单进程的,读者可以自行改成多进程的。

  • 相关阅读:
    Java面试系列05(static、JVM内存模、final、abstract、interface)
    Java面试系列04(抽象、实例化、类、多态、对象、特殊对象、权限封装)
    第一个spring冲刺团队贡献分(80分满分)
    第二个Sprint冲刺第一天
    第一阶段的事后诸葛亮
    第一个Sprint冲刺成果
    第一个Sprint冲刺第十天
    第一个Sprint冲刺第九天
    第一个Sprint冲刺第八天
    第一个Sprint冲刺第七天
  • 原文地址:https://www.cnblogs.com/tntboom/p/4491414.html
Copyright © 2020-2023  润新知