各位同学,请注意TCP粘包解包原因可自行百度。本篇文章适合有一定概念基础的同学,但网上例子五花八门,总感觉讲不到解决方案的具体措施,故本人写了这篇短文超详细的为大家提供一种解决TCP粘包拆包的方案。
首先解决方案大致有如下三种:
- 长度编码:发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
- 特殊字符分隔符协议:可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
- 定长协议:发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
对于以上三种方案来说,第一种方案也是最实际,最实用的方案。大家普遍使用这种解决方案。
首先在数据的发送端我们需要有包头表示本次发送的消息总长度。比如可以采用2位int类型值表示消息长度如06+msg。其中06代表本次总共发送六个字节的数据。后面的msd就是消息正文。
那么到底在数据接收端我们该怎么做呢?
首先我们有一个cacheBuffer用于存储发生拆包的半包。这个包数据包括2个字节的包头以及部分包数据。
接收消息第一步是判断这个cacheBuffer里面有没有数据,如果有说明上一次发生了粘包解包现象,那就用一个新的buffer直接把这次获得的包拼接在cacheBuffer后面。并清除cacheBuffer里的内容.然后把这个包进行解析。解析的结果有两种一种是本次收到的消息刚刚好等于消息长度,未发生拆包粘包现象,那么就正常给业务返回就行。如果本次的包不完整那么把不完整的包保存到cacheBuffer里,等下一次消息来了我们还是一样先判断cacheBuffer里有无内容,如果有内容则取出内容进行组包动作。以上是解决思路,下面是代码部分。大家从readMsg这个方法开始,跟着思路就行。同时这个代码也是nio做客户端的聊天后台demo。
package com.example.demo.client;
import com.example.demo.swing.MsgHandler;
import com.example.demo.utils.ContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author mark
* @describe socket通信客户端
* @date 2022/8/2 11:12
*/
@Slf4j
@Component
public class Client {
/**
* key是客户端id,value是信道
*/
private final ConcurrentHashMap<String, SocketChannel> channels = new ConcurrentHashMap<>();
/**
* 选择器
*/
private Selector selector;
private static final String LOGIN = "login";
/**
* 连接服务端
*
* @param ip 服务端IP
* @param clientId 客户端ID
* @param port 服务端端口
* @throws IOException IO异常
*/
public void connect(String ip, String clientId, Integer port) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(100);
// 得到一个网络通道