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);
    }
  }
}
相关推荐
丸码10 分钟前
Java异常体系全解析
java·开发语言
v***885611 分钟前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
q***494511 分钟前
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
java·ubuntu·centos
IMPYLH18 分钟前
Lua 的 require 函数
java·开发语言·笔记·后端·junit·lua
曾经的三心草19 分钟前
基于正倒排索引的Java文档搜索引擎1-实现索引模块-实现Parser类
java·开发语言·搜索引擎
vx_bscxy32243 分钟前
告别毕设焦虑!Python 爬虫 + Java 系统 + 数据大屏,含详细开发文档 基于web的图书管理系统74010 (上万套实战教程,赠送源码)
java·前端·课程设计
字节拾光录1 小时前
Java工具库三足鼎立:Hutool、Apache Commons、Guava深度测评与场景化选型指南
java·apache·guava
爱学习的小可爱卢1 小时前
Java UDP编程实战:UDP数据报套接字编程DatagramPacket、DatagramSocket 、InetSocketAddress
java·udp·udp数据报
未来之窗软件服务1 小时前
幽冥大陆(三十五)S18酒店门锁SDK go语言——东方仙盟筑基期
java·前端·golang·智能门锁·仙盟创梦ide·东方仙盟·东方仙盟sdk
r***93481 小时前
【Redis】在Java中以及Spring环境下操作Redis
java·redis·spring