Java Socket 短链接 自定义报文

最近对接的一个项目,使用 TCP Socket 短链接自定义报文格式,不是传统的 HTTP 接口方式对接,踩了一些坑,特此记录一下。特别是 TCP 是流式传输,会出现只收到前几个字节,后面的字节过一会才会收到的情况。

我这边的报文格式是,前4个字节是后面报文主体的长度,然后后面才是报文主体

SocketMessageReader.java 报文读取

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

// Socket 读报文,报文格式: 4字节报文长度+报文主体
public class SocketMessageReader implements AutoCloseable {

  private final Socket socket;
  private final InputStream in;

  public SocketMessageReader(Socket socket) throws IOException {
    this.socket = socket;
    this.in = socket.getInputStream();
  }

  /**
   * 读取一条完整报文
   */
  public String readMessage() throws IOException {
    // 1. 先读取4个字节,代表正文长度
    byte[] headerBytes = readFully(4);
    int contentLength = ByteBuffer.wrap(headerBytes).getInt(); // 默认大端序

    // 2. 再根据长度读取正文
    byte[] bodyBytes = readFully(contentLength);
    return new String(bodyBytes, StandardCharsets.UTF_8);
  }

  /**
   * 从输入流读取指定字节数,直到满为止(处理TCP分片问题)
   */
  private byte[] readFully(int length) throws IOException {
    byte[] buffer = new byte[length];
    int offset = 0;
    while (offset < length) {
      int read = in.read(buffer, offset, length - offset);
      if (read == -1) {
        throw new IOException("连接已关闭,未能完整读取数据");
      }
      offset += read;
    }
    return buffer;
  }

  @Override
  public void close() {
    try {
      in.close();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        socket.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

SocketMessageWriter.java 写入报文

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

// Socket 写报文,报文格式: 4字节报文长度+报文主体
public class SocketMessageWriter implements AutoCloseable {

  private final Socket socket;
  private final OutputStream out;

  private int length = -1;

  public SocketMessageWriter(Socket socket) throws IOException {
    this.socket = socket;
    this.out = socket.getOutputStream();
  }

  // 设置报文长度,只需调用一次,需搭配 writePartMsg 一起使用
  public void setLength(int length) throws IOException {
    this.length = length;
    byte[] headerBytes = ByteBuffer.allocate(4).putInt(length).array();
    out.write(headerBytes);
    out.flush();
  }

  // 发送部分消息,需提前调用  setLength 设置整体报文长度
  public void writePartMsg(String msg) throws IOException {
    if (length < 0) {
      throw new RuntimeException("请先设置length");
    }
    byte[] bodyBytes = msg.getBytes(StandardCharsets.UTF_8);
    out.write(bodyBytes);
    out.flush();
  }

  // 消息整体发送
  public void writeAllMsg(String body) throws IOException {
    byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
    byte[] headerBytes = ByteBuffer.allocate(4).putInt(bodyBytes.length).array();
    out.write(headerBytes);

    out.write(bodyBytes);
    out.flush();
  }


  @Override
  public void close() {
    try {
      out.close();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        socket.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

}

TcpServer 服务端

java 复制代码
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpServer {

  public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    try (ServerSocket serverSocket = new ServerSocket(9000)) {
      System.out.println("服务端启动,等待连接...");
      while (true) {
        Socket socket = serverSocket.accept();
        executor.submit(() -> {
          String uuid = UUID.randomUUID().toString();
          try (SocketMessageReader reader = new SocketMessageReader(socket)) {
            while (true) {
              String message = reader.readMessage();
              System.out.println(uuid + " ==> 收到消息: " + message);
              try (SocketMessageWriter writer = new SocketMessageWriter(socket)) {
                writer.writeAllMsg(uuid + "服务端回复:" + message);
              }
            }
          } catch (IOException e) {
            System.out.println("客户端 " + uuid + " 已断开: " + e.getMessage());
          }
        });
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

TcpClient 客户端

java 复制代码
import java.net.Socket;

public class TcpClient {

  public static void main(String[] args) {
    try (Socket socket = new Socket("127.0.0.1", 9000)) {
      SocketMessageWriter writer = new SocketMessageWriter(socket);

      writer.setLength(10);
      // 故意延时500ms,生产环境需要去掉
      Thread.sleep(500);

      writer.writePartMsg("hello");
      // 故意延时500ms,生产环境需要去掉
      Thread.sleep(500);

      writer.writePartMsg("world");

      SocketMessageReader socketMessageReader = new SocketMessageReader(socket);

      String s = socketMessageReader.readMessage();
      System.out.println("客户端接收:" + s);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
相关推荐
凤凰院凶涛QAQ6 小时前
《Java版数据结构 & 集合类剖析》栈与队列:“push/pop 是栈的灵魂,offer/poll 是队列的骨架——四组 API,两种人生”
java·开发语言·数据结构
掘金_答案7 小时前
上线那天,一个 ConcurrentHashMap 差点送走我的 AI 客服——3 天排查 JVM 血泪史
java·后端·架构
hehelm7 小时前
Linux网络编程—TCP字典翻译系统
linux·开发语言·网络·c++·tcp/ip
猿与禅7 小时前
CosId 分布式 ID 生成器完全教程:从架构原理到生产落地
java·shardingsphere·雪花算法·分布式id·高性能·cosid·号段模式
MindUp8 小时前
企业网盘权限模型解析:多层级访问控制与审计能力选型指南
java·linux·运维
NG4778 小时前
【软件设计与体系结构】-策略设计模式
java·设计模式·软件工程
程序员天天困8 小时前
优雅记录操作日志:从注解到 SpEL 的全链路实践与开源方案对比
java·后端
战血石LoveYY9 小时前
Integer类型超限,导致数据异常
java·算法
乐观的Terry10 小时前
1、为什么要自己造发布系统
java·spring boot·后端·spring cloud·架构
兰令水10 小时前
hot100【acm版】【2026.7.13打卡-java版本】
java·开发语言·数据结构·算法·leetcode·面试