上一篇我们讲了《Socket粘包问题的3种解决方案》,但没想到评论区竟然炸了。介于大家的热情讨论,以及不同的反馈意见,本文就来做一个扩展和延伸,试图找到问题的最优解,以及消息通讯的最优解决方案。
在正式开始之前,我们先对上篇评论中的几个典型问题做一个简单的回复,不感兴趣的朋友可直接划过。
问题一:TCP存在粘包问题吗?先说答案:TCP 本身并没有粘包和半包一说,因为 TCP 本质上只是一个传输控制协议(Transmission Control Protocol,TCP),它是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。
所谓的协议本质上是一个约定,就好比 Java 编程约定使用驼峰命名法一样,约定的意义是为了让通讯双方,能够正常的进行消息互换的,那粘包和半包问题又是如何产生的呢?
这是因为在 TCP 的交互中,数据是以字节流的形式进行传输的,而“流”的传输是没有边界的,因为没有边界所以就不能区分消息的归属,从而就会产生粘包和半包问题(粘包和半包的定义,详见上一篇)。所以说 TCP 协议本身并不存在粘包和半包问题,只是在使用中如果不能有效的确定流的边界就会产生粘包和半包问题。
问题二:分隔符是最优解决方案?坦白的说,经过评论区大家的耐心“开导”,我也意识到了以结束符作为最终的解决方案存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性。
问题三:Socket 高效吗?这个问题的答案是否定的,其实上文在开头已经描述了应用场景:「传统的 Socket 编程」,学习它的意义就在于理解更早期更底层的一些知识,当然作为补充本文会提供更加高效的消息通讯方案——Netty 通讯。
聊完了以上问题,接下来咱们先来补充一下上篇文章中提到的,将消息分为消息头和消息体的代码实现。
一、封装消息头和消息体在开始写服务器端和客户端之前,咱们先来编写一个消息的封装类,使用它可以将消息封装成消息头和消息体,如下图所示:
消息头中存储消息体的长度,从而确定了消息的边界,便解决粘包和半包问题。 1.消息封装类
消息的封装类中提供了两个方法:一个是将消息转换成消息头 + 消息体的方法,另一个是读取消息头的方法,具体实现代码如下:
/** * 消息封装类 */ class SocketPacket { // 消息头存储的长度(占 8 字节) static final int HEAD_SIZE = 8; /** * 将协议封装为:协议头 + 协议体 * @param context 消息体(String 类型) * @return byte[] */ public byte[] toBytes(String context) { // 协议体 byte 数组 byte[] bodyByte = context.getBytes(); int bodyByteLength = bodyByte.length; // 最终封装对象 byte[] result = new byte[HEAD_SIZE + bodyByteLength]; // 借助 NumberFormat 将 int 转换为 byte[] NumberFormat numberFormat = NumberFormat.getNumberInstance(); numberFormat.setMinimumIntegerDigits(HEAD_SIZE); numberFormat.setGroupingUsed(false); // 协议头 byte 数组 byte[] headByte = numberFormat.format(bodyByteLength).getBytes(); // 封装协议头 System.arraycopy(headByte, 0, result, 0, HEAD_SIZE); // 封装协议体 System.arraycopy(bodyByte, 0, result, HEAD_SIZE, bodyByteLength); return result; } /** * 获取消息头的内容(也就是消息体的长度) * @param inputStream * @return */ public int getHeader(InputStream inputStream) throws IOException { int result = 0; byte[] bytes = new byte[HEAD_SIZE]; inputStream.read(bytes, 0, HEAD_SIZE); // 得到消息体的字节长度 result = Integer.valueOf(new String(bytes)); return result; } } 2.编写客户端接下来我们来定义客户端,在客户端中我们添加一组待发送的消息,随机给服务器端发送一个消息,实现代码如下:
/** * 客户端 */ class MySocketClient { public static void main(String[] args) throws IOException { // 启动 Socket 并尝试连接服务器 Socket socket = new Socket("127.0.0.1", 9093); // 发送消息合集(随机发送一条消息) final String[] message = {"Hi,Java.", "Hi,SQL~", "关注公众号|Java中文社群."}; // 创建协议封装对象 SocketPacket socketPacket = new SocketPacket(); try (OutputStream outputStream = socket.getOutputStream()) { // 给服务器端发送 10 次消息 for (int i = 0; i < 10; i++) { // 随机发送一条消息 String msg = message[new Random().nextInt(message.length)]; // 将内容封装为:协议头+协议体 byte[] bytes = socketPacket.toBytes(msg); // 发送消息 outputStream.write(bytes, 0, bytes.length); outputStream.flush(); } } } } 3.编写服务器端