TCP“粘包”问题

2022-09-05  本文已影响0人  牛奶言兼

接TCP链路,参考tcp/ip的相关资料,描述tcp数据包为无边界数据包协议,那么什么是无边界呢?

参考如下测试程序:

服务端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>

#include "unistd.h"
#include "netinet/in.h" // sockaddr_in
#include "arpa/inet.h" // inet_pton

#define MAXLINE 4096
#define LISTENQ 1024

void my_log(const char* file, int line, const char* fmt, ...) {
    va_list args;
    printf("file:%s, line:%d  ", file, line);

    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);

   printf("%s", "\n");
}

#define LOG(fmt, ...) my_log(__FILE__, __LINE__, fmt, ##__VA_ARGS__);

int main(int argc, char **argv) {
   LOG("==== server start ====");
   int listenfd, connfd;
   struct sockaddr_in servaddr;
   char buff[MAXLINE];
   time_t ticks;

   listenfd = socket(AF_INET, SOCK_STREAM, 0);
   bzero(&servaddr, sizeof(servaddr));
   servaddr.sin_family = AF_INET;
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servaddr.sin_port = htons(1024);

   if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == 0) {
       LOG("bind port success.");
   } else {
       LOG("bind port fail.");
       exit(-1);
   }

   listen(listenfd, LISTENQ); // 这里应该补充状态判断
   LOG("wait recv connect request.");

   for(;;) {
       LOG("block net i/o.");
       connfd = accept(listenfd, (struct sockaddr *)NULL, NULL);  // 服务端的accept会形成阻塞,这里也就涉及到了非阻塞模型,比如select、poll、epoll等API
       LOG("accept connect request.");
       for (int i = 0; i < 100; ++i) {
           ticks = time(NULL);
           snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
           write(connfd, buff, strlen(buff));
           LOG("send msg.");
           sleep(10);
       }
       close(connfd);
       // break; // 如果不想结束服务,注掉这里
   }
   LOG("==== server end ====");
   exit(0);
}

客户端示例

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

#include "unistd.h"
#include "netinet/in.h" // sockaddr_in
#include "arpa/inet.h" // inet_pton

#define MAXLINE 4096

void my_log(const char* file, int line, const char* fmt, ...) {
   va_list args;
   printf("file:%s, line:%d  ", file, line);

   va_start(args, fmt);
   vprintf(fmt, args);
   va_end(args);

   printf("%s", "\n");
}

#define LOG(fmt, ...) my_log(__FILE__, __LINE__, fmt, ##__VA_ARGS__);

int main(int argc, char **argv) {
   LOG("==== client start ====");
   int sockfd, n;
   char recvline[MAXLINE + 1];
   struct sockaddr_in servaddr;

   if (argc != 2) {
       LOG("usage: a.out <IPaddress>");
       exit(1);
   }

   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
       LOG("socket error");
       exit(1);
   }

   bzero(&servaddr, sizeof(servaddr));
   servaddr.sin_family = AF_INET;
   servaddr.sin_port = htons(1024);  // 注意:需要确保跟服务端监听的端口一致
   if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
       LOG("inet_pon error for %s", argv[1]);
       exit(1);
   }

   if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { // sock.h connect
       LOG("connect error");
       exit(1);
   }

   sleep(20); // 等待一会儿,触发tcp粘包现象

   while ((n = read(sockfd, recvline, MAXLINE)) > 0) { // 无差别读取内容
       recvline[n] = 0;
       LOG("recv content:");
       if (fputs(recvline, stdout) == EOF) {
           LOG("fputs error.");
           exit(1);
       }
   }

   if (n < 0) {
       LOG("read error");
       exit(1);
   }

   LOG("==== client end ====");
   exit(0);
}

编译配置完成后,执行服务,等待客户端连接。连接成功后,执行一段时间,观察日志输出。
服务端日志

% ./server
file:server.c, line:28  ==== server start ====
file:server.c, line:41  bind port success.
file:server.c, line:50  wait recv connect request.
file:server.c, line:53  block net i/o.
file:server.c, line:55  accept connect request.
file:server.c, line:60  send msg.
file:server.c, line:60  send msg.
file:server.c, line:60  send msg.
file:server.c, line:60  send msg.
file:server.c, line:60  send msg.
file:server.c, line:60  send msg.

客户端日志

% ./client 127.0.0.1      
file:client.c, line:26  ==== client start ====
file:client.c, line:58  recv content:
Tue Sep  6 09:53:46 2022
Tue Sep  6 09:53:51 2022
Tue Sep  6 09:53:56 2022
Tue Sep  6 09:54:01 2022
file:client.c, line:58  recv content:
Tue Sep  6 09:54:06 2022
file:client.c, line:58  recv content:
Tue Sep  6 09:54:11 2022

从客户端的日志可以看出,在连接成功后等待了20s的时间,足够服务端发送4条消息,当客户端开始读取socket套接字时,第一次读取时,直接读取到了4条消息,再随后,因为客户端是即时读取,则每次只有一条消息。

这就是tcp消息包的“粘包”问题,要解决此类问题,可以采用消息长度+消息负载的方式,或者更复杂的应用层控制。

什么是粘包,所谓粘包就是连续给对端发送两个或者两个以上的数据包,对端在一次收取中收到的数据包数量可能大于 1 个,当大于 1 个时,可能是几个(包括一个)包加上某个包的部分,或者干脆就是几个完整的包在一起。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫半包。

衍生出一个问题

tcp的“粘包”消息中是一个完整的tcp消息流吗?还是可能是残缺的?

上一篇 下一篇

猜你喜欢

热点阅读