引子

现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客讲得很不错,因此转载过来并稍作修改与大家分享,也留作自己时常温习和查阅,文章的版权归haifeiWu博主所有。

作者: haifeiWu

出处: http://www.hchstudio.cn/

关于作者:专注大后端,分布式,高并发等领域,请多多赐教!

原文链接:https://www.cnblogs.com/haifeiWu/p/9358499.html

TCP协议的简单介绍

TCP是面向连接的运输层协议

简单来说,在使用TCP协议之前,必须先建立TCP连接,就是我们常说的三次握手。在数据传输完毕之后,必须是释放已经建立的TCP连接,否则会发生不可预知的问题,造成服务的不可用状态。

每一条TCP连接都是可靠连接,且只有两个端点

TCP连接是从Server端到Client端的点对点的,通过TCP传输数据,无差错,不重复不丢失。

TCP协议的通信是全双工的

TCP协议允许通信双方的应用程序在任何时候都能发送数据。TCP 连接的两端都设有发送缓冲区和接收缓冲区,用来临时存放双向通信的数据。发送数据时,应用程序把数据传送给TCP的缓冲后,就可以做自己的事情,而TCP在合适的时候将数据发送出去。在接收的时候,TCP把收到的数据放入接收缓冲区,上层应用在合适的时候读取数据。

TCP协议是面向字节流的

TCP中的流是指流入进程或者从进程中流出的字节序列。所以向Java,golang等高级语言在进行TCP通信是都需要将相应的实体序列化才能进行传输。还有就是在我们使用Redis做缓存的时候,都需要将放入Redis的数据序列化才可以,原因就是Redis底层就是实现的TCP协议。

TCP并不知道所传输的字节流的含义,TCP并不能保证接收方应用程序和发送方应用程序所发出的数据块具有对应大小的关系(这就是TCP传输过程中产生的粘包问题)。但是应用程序接收方最终受到的字节流与发送方发送的字节流是一定相同的。因此,我们在使用TCP协议的时候应该制定合理的粘包拆包策略。

下图是TCP的协议传输的整个过程:

下面这个图是从老钱的博客里面取到的,非常生动

TCP粘包问题复现

理论推敲

如下图所示,出现的粘包问题一共有三种情况

第一种情况:
如上图中的第一根bar所示,服务端一共读到两个数据包,每个数据包都是完成的,并没有发生粘包的问题,这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,每次服务端读取到的消息都是完成的,并不会出现数据不正确的情况。

第二种情况:
服务端仅收到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于第一种情况的逻辑实现的服务端就蒙了,因为服务端并不能很好的处理这个数据包,甚至不能处理,这种情况其实就是TCP的粘包问题。

第三种情况:
服务端收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆包问题,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。

为什么会发生TCP粘包、拆包

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

  3. 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

如何处理粘包、拆包

通常会有以下一些常用的方法:

  1. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。

  2. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符。

  3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容,一般使用‘\n’。

  4. 更为复杂的协议,例如楼主最近接触比较多的车联网协议808,809协议。

TCP粘包拆包的代码实践

下面代码楼主主要演示了使用规定消息头,消息体的方式来解决TCP的粘包,拆包问题。

server端代码: server端代码的主要逻辑是接收客户端发送过来的消息,重新组装出消息,并打印出来。


import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author wuhf
* @Date 2018/7/16 15:50
**/
public class TestSocketServer {
public static void main(String args[]) {
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8089));
while (true) {
Socket socket = serverSocket.accept();
new ReceiveThread(socket).start(); }
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} static class ReceiveThread extends Thread {
public static final int PACKET_HEAD_LENGTH = 2;//包头长度
private Socket socket;
private volatile byte[] bytes = new byte[0]; public ReceiveThread(Socket socket) {
this.socket = socket;
} public byte[] mergebyte(byte[] a, byte[] b, int begin, int end) {
byte[] add = new byte[a.length + end - begin];
int i = 0;
for (i = 0; i < a.length; i++) {
add[i] = a[i];
}
for (int k = begin; k < end; k++, i++) {
add[i] = b[k];
}
return add;
} @Override
public void run() {
int count = 0;
while (true) {
try {
InputStream reader = socket.getInputStream();
if (bytes.length < PACKET_HEAD_LENGTH) {
byte[] head = new byte[PACKET_HEAD_LENGTH - bytes.length];
int couter = reader.read(head);
if (couter < 0) {
continue;
}
bytes = mergebyte(bytes, head, 0, couter);
if (couter < PACKET_HEAD_LENGTH) {
continue;
}
}
// 下面这个值请注意,一定要取2长度的字节子数组作为报文长度,你懂得
byte[] temp = new byte[0];
temp = mergebyte(temp, bytes, 0, PACKET_HEAD_LENGTH);
String templength = new String(temp);
int bodylength = Integer.parseInt(templength);//包体长度
if (bytes.length - PACKET_HEAD_LENGTH < bodylength) {//不够一个包
byte[] body = new byte[bodylength + PACKET_HEAD_LENGTH - bytes.length];//剩下应该读的字节(凑一个包)
int couter = reader.read(body);
if (couter < 0) {
continue;
}
bytes = mergebyte(bytes, body, 0, couter);
if (couter < body.length) {
continue;
}
}
byte[] body = new byte[0];
body = mergebyte(body, bytes, PACKET_HEAD_LENGTH, bytes.length);
count++;
System.out.println("server receive body: " + count + new String(body));
bytes = new byte[0];
} catch (Exception e) {
e.printStackTrace();
}
}
}
} }

client端代码:客户端代码主要逻辑是组装要发送的消息,确定消息头,消息体,然后发送到服务端。


import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket; /**
* @author wuhf
* @Date 2018/7/16 15:45
**/
public class TestSocketClient {
public static void main(String args[]) throws IOException {
Socket clientSocket = new Socket();
clientSocket.connect(new InetSocketAddress(8089));
new SendThread(clientSocket).start(); } static class SendThread extends Thread {
Socket socket;
PrintWriter printWriter = null; public SendThread(Socket socket) {
this.socket = socket;
try {
printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} @Override
public void run() {
String reqMessage = "HelloWorld! from clientsocket this is test half packages!";
for (int i = 0; i < 100; i++) {
sendPacket(reqMessage);
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} } public void sendPacket(String message) {
try {
OutputStream writer = socket.getOutputStream();
writer.write(message.getBytes());
writer.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} }

小结

最近一直在写一些框架性的博客,专门针对某些问题进行原理性的技术探讨的博客还比较少,所以楼主想着怎样能在自己学到东西的同时也可以给一同在技术这条野路子上奋斗的小伙伴们一些启发,是楼主一直努力的方向。

参考文章

如果觉得本篇博客对您有帮助,可以扫码小小地鼓励下马三,马三会写出更多的好文章,支持微信和支付宝哟!

       

作者:马三小伙儿
出处:https://www.cnblogs.com/msxh/p/10822516.html 
请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案的更多相关文章

  1. TCP 粘包 - 拆包问题及解决方案

    目录 TCP粘包拆包问题 什么是粘包 - 拆包问题 为什么存在粘包 - 拆包问题 粘包 - 拆包 演示 粘包 - 拆包 解决方案 方式一: 固定缓冲区大小 方式二: 封装请求协议 方式三: 特殊字符结 ...

  2. 浅谈tcp粘包问题

    第一部分:简介tcp socket通信的底层原理 原理解析图: socket通信过程如图所示:首先客户端将发送内容通过send()方法将内容发送到客户端计算机的内核区,然后由操作系统将内容通过底层路径 ...

  3. Java网络编程基础之TCP粘包拆包

    TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想象河里的流水,他们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实 ...

  4. TCP粘包/拆包 ByteBuf和channel 如果没有Netty? 传统的多线程服务器,这个也是Apache处理请求的模式

    通俗地讲,Netty 能做什么? - 知乎 https://www.zhihu.com/question/24322387 谢邀.netty是一套在java NIO的基础上封装的便于用户开发网络应用程 ...

  5. Netty(三)TCP粘包拆包处理

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 粘包.拆包问题说明 假设客户端分别发送数据包D1和D ...

  6. Netty(二)——TCP粘包/拆包

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...

  7. Netty使用LineBasedFrameDecoder解决TCP粘包/拆包

    TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...

  8. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...

  9. TCP 粘包问题浅析及其解决方案

    最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有TCP,UDP,HTTP等各种网络传输协议,因此楼主想先从协议最基本的TCP粘包问题搞起,把计算机网络这部分基础夯实一下. TCP协 ...

随机推荐

  1. 什么是RMI?

    RMI(Remote Method Invocation,远程方法调用)是用Java在JDK1.2中实现的,它大大增强了Java开发分布式应用的能力.Java作为一种风靡一时的网络开发语言,其巨大的威 ...

  2. wc命令统计目录下所有文件行数

    想统计一下最近一个项目的代码行数,一个一个文件统计显然不是程序员的思维,wc命令可以统计一个文本的行数,结合find命令可以实现我的需求(注意符号):

  3. 深度剖析YOLO系列的原理

    深度剖析YOLO系列的原理 本文系作者原创,转载请注明出处:https://www.cnblogs.com/further-further-further/p/12072225.html 目录 1. ...

  4. CSS样式 解决文字过长显示省略号问题

    一.CSS样式 解决文字过长显示省略号问题 1.一般样式 一般 css 样式,当宽度不够时,可能会出现换行的效果.这样的效果在某些时候肯定是不行的,可以修改 css 样式来解决这个问题. <!D ...

  5. bossplayersCTF 1: Vulnhub Walkthrough

    主机扫描: http://10.10.202.130/ <!--WkRJNWVXRXliSFZhTW14MVkwaEtkbG96U214ak0wMTFZMGRvZDBOblBUMEsK--> ...

  6. Support URL

    如您有任何疑问或者建议,请通过以下方式与我们取得联系,我们会尽快响应您的反馈: 邮箱:eighteyes_cn@163.com

  7. openwrt_在PPPOE上网的同时_访问光猫

    openwrt_在PPPOE上网的同时_访问光猫 转载注明来源: 本文链接 来自osnosn的博客,写于 2019-11-14. 参考文章: 光猫桥接模式下,通过路由器访问光猫.简单设置 设置Open ...

  8. 自学Java编程,如何混到一个7k薪资实习生的岗位

    现在Java软件开发的专业一直都是热门,有很多专业的学生,比如电子.机械.会计.土木等等专业由于专业本身没有更高的提升空间,所以现在的年轻人更加喜欢做一些科技前沿的工作,毕竟现在接触的都是电子产品.而 ...

  9. Java基础语法04面向对象上-类-属性-方法-可变参数-重载-递归-对象数组

    类 面向对象是一种思想,一般指将事务的属性与方法抽出总结为模板(类/class),处理事务时通过类创建/new出对象由对象的功能/方法去完成所要计算处理的事情. 面向过程:POP:以过程,步骤为主,考 ...

  10. antdesign的input增加自定义校验规则

    rules: [ {required: true, message: '请输入姓名'}, {max: 16, message: '姓名过长'}, { validator: (rule, val, ca ...