流式协议

搞清楚TCP流式协议的概念

  假设应用层通过TCP发送数据"HelloWorld",发送方可能分两次发送,比如"Hello"和"World",但是接收方可能一次就收到"HelloWorld!",或者有可能分多次收到,比如先收到"Hell",再收到"oWorld"。这说明TCP传输的数据是没有消息边界的,应用层需要自己处理这些数据的拆分和组合,这就是流式协议。故在应用层协议设计的时候需要考虑如何界定消息的结束,比如用特定的分隔符,或者固定长度,或者在消息头声明长度。例如HTTP协议中使用content-length或者分块传输编码。

为什么TCP要设计成流式协议呢?

  可以更灵活地处理数据,不需要维护消息边界,从而更高效地利用网络资源。比如,TCP可能会将多个小的数据包合并成一个大的数据包发送,或者将一个大的数据包拆分成多个小的数据包传输,这取决于网络状况和拥塞控制算法。这样的话,发送方和接收方的数据块可能不一致,所以应用层需要自己处理这些情况。

如何处理TCP这种流式协议呢?

  如果我们使用socket编程的时候,recv可能返回任意长度的数据,所以一般我们需要循环读取,并将每次读取的数据追加到缓冲区中,然后检查是否有完整的消息,处理完后再将剩余的数据保存在缓冲区中,供与下次读取的数据组合,解析成完整的数据消息。
  另外,常见的粘包问题也是由于TCP的流式特性导致的。粘包是指多个应用层的数据包被合并成一个TCP段发送,或者一个应用层的数据包被拆分成多个TCP段。这时候,接收方可能一次读取到多个包的数据,或者需要多次读取才能得到一个完整的包。解决粘包问题的方法通常是在应用层添加消息边界,比如前面提到的长度前缀、分隔符或者使用自描述格式如JSON、XML等,但需要确保能够正确解析。
  举个例子,比如客户端发送两个消息:"Hello"和"World",服务端可能先收到"HelloWorld",如果我们没有处理机制,此时我们是无法区分收到的这两个消息的,所以我们需要约定一种消息界定方法,比如,我们固定每次发送消息前,先发送4字节大小的长度信息,再发送实际内容,这样接收方先读取4字节的长度长度信息,解析出长度N,再去取接下来的N字节长度的消息内容,这样我们就可以处理流式协议了。
  具体现实应用的例子就是HTTP协议,它使用Content-Length来制定正文的长度,甚至使用Transfer-Encoding: chunked来分块传输,每块前面有长度信息,这样接收方可以正确解析。

深刻理解TCP流式协议的关键点

1.流式协议的本质

  TCP是无消息边界的协议,数据就像水流一样连续传输。发送方多次写入的数据可能被接收方一次读取,反之亦然

1.发送端的多次send()调用可能被TCP合并为一个数据发送。
2.接收端的recv()调用可能返回任意长度的数据,无法保证与发送端发送的数据块一一对应。

2.设计流式协议的优点

灵活性:TCP根据网络状况进行动态调整数据包大小(MSS),合并小数据包(nagle算法)或拆分大数据报文,提升传输效率。
可靠性:通过序列号、确认应答、重传等机制保证数据按序到达,但无需维护应用层消息边界。

3.“粘包”与“拆包”问题

粘包:多个应用消息被合并为一个TCP报文接收
拆包:单个应用层消息被拆分为多个TCP报文传输

4.应用层处理TCP“粘包”的方案

  因为TCP不维护消息边界,应用层需要自行处理数据的组装和拆分。常见方案如下:

定义长度前缀:在消息头部定义固定字节的字段声明消息长度(HTTP的Content-Length)
分隔符:使用特许字符(如\r\n)标记消息结束(SMTP协议)。
自描述格式: Json/XML等格式通过语法标记边界,但需要解析器支持。
  通过上述拆包的方法,再结合缓冲区管理,逐步解析出完整的TCP消息。

5.编程实践示例

例程1:发送端发送TCP消息
//假设tcp socket是非阻塞的
//发送方循环发送消息,其中buf是缓冲区,缓冲区中有多个tcp消息。
bool send_data(char *buf, size_t buf_len, int fd) {
    int send_len = 0;
    while(true) {
      int n = send(fd, buf + ret, buf_len);
      if (n == 0) {
          return false;
      } else if (n < 0) {
          if (errno == EINTR) {
              continue;
          } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
              //发送缓冲区已满,暂时发送不出去
              return true;
          }
          printf("send error.\n");
          return false;               
      }
      send_len += n;
      if (send_len == buf_len) {
          return true;
      }
   }
}
例程2:接收方接收消息
//假设TCP socket是非阻塞的。
//接收端循环接收TCP消息至缓冲区
bool recv_data(std::string buf, int fd) {
    char tmp_buf[1024] = {0};
    while (true) {
        int n = recv(fd, buf, 1024, 0);
        if (n == 0) {
            return false;
        } else if (n < 0) {
            if (errno == EINTR) {
                continue;
            } else if (errno == EAGAIN || EWOULDBLOCK) {
                return true;
            }
            return false;
        }
        buf.append(tmp_buf, n);
    }
}
例程3.解析消息
void on_package(int fd) {
    std::string buf;
    bool success = recv_data(buf, fd);
    if (success) {
        decode_package(buf);
        return;
    }
    //关闭连接
    close(fd);
}

//解析消息
//我们假设应用消息的头部有4字节的长度信息
void decode_package(std::string& buf) {
    int body_len = 0;
    while (true) {
        //检查缓冲区buf是否足够一个包头长度
        if (buf.size() < 4) {
            //不够,退出
            return;
        }
        //解析出包头长度
        memcpy(&body_len, buf.c_str(), 4);
        body_len = htonl(body_len);
        //检查buf足够一个包的情况
        if (buf.size() < body_len + 4) {
            //不足,退出
            return;
        }
        buf.erase(0, 4);
        package_handle(buf.substr(0, body_len));
        buf.erase(0, body_len);
    }
}

void package_handle(std::string &buf) {
    std::cout << "tcp message: " << buf << std::endl;
}

posted @ 2025-02-20 03:50  ydqun  阅读(131)  评论(0)    收藏  举报