`
moor212
  • 浏览: 176113 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java基于TCP的socket数据包拆分方法

 
阅读更多
java基于TCP的socket数据包拆分方法
发表于129 天前 ⁄ Java  ⁄ 评论数4 ⁄ 被围观 916次+ 关键字:java socket tcp 分包 粘包
好了,现在轻松许多。话说看到falcom官方的《空轨》动画时间表,又看到崩坏的人设,我表示真的非常不能接受。当然了这个咱也管不着。


好了话归正题,前不久写的socket程序,服务器是java的,客户端是flex。一开始就想过所谓的拆分数据包的问题,因为数据包结构是自己定义的,也简单的写了几行数据包的验证。关键是测试中完全没有发生什么情况,但是发布到外网之后却出现一些非常奇怪的问题,典型的就是通信过一定时间之后,数据包验证那块就会出错然后就抛弃了数据包,这就是所谓的“丢包”吧,但是我的是TCP的socket,所谓因为网络问题导致的数据包没有发送与接收成功这种问题应该是不可能出现的。
于是看了几篇文正,发现这种现象被称作“粘包”,我觉得还是挺贴切的。经过一定时间的思考、和测试,大概了解了其中的原理,按照现在的此时情况来看,应该是没什么问题。于是在此总结一下,如果哪天我发现一些新问题或更好的方法,还是会来继续补充这篇文章的。当然各位路过的前辈觉得其中存在错误什么的也请指出。
首先在将程序之前,还是先说一下TCP的通信。TCP和UDP的最大区别就是TCP维护了连接状态,而这个状态我们可以理解为一个畅通的流通道,即stream,当然流的传输内容归根结底还是byte。于是将流的通信进行假设,假设存在一条引水管道,从远方输水过来,我们在这边等待水的到来,并使用容器接收流出来的水。
此时存在一下几种情况:
■假设这个输水管道在操作过程中不会断掉。
■先进先出,先流进管道的水一定是先到达。
■某一状态,(在输水管没有断掉是)流量无法保证,甚至某段时间没有水。
■我们的容器(缓冲区)大小固定,即每次接收的水量存在最大值,超多将无法接收。
在以上情况作为前提,再回归编码。TCP的socket可以通信的前提是连接没有断开,连接断开事件可以从两种情况进行判断。流断开,这read()为-1,SocketException或者IoException。分包的前提是socket可以正常通信,不论网络延时多么严重,这些TCP协议会去处理。我们仅仅关心通信既可。现在最优情况,即实验室环境,或者是内网,服务器与客户端延时不会大于一毫秒,此时只要我们接收输水的容器够大,基本就可以完成正常通信。
但是互联网情况就非常复杂,数据包要经过无数网络软硬件设备,延时不可避免,但是TCP协议会像输水管道那样,保障数据包的顺序和保证不会丢失。所以这时我们可以控制的只有接水的容器。查看一些简单的TCP通信的知识,网络数据在传输的时候存在缓存现象,简单的说,就是连续发送N个数据包。他们可能被缓存起来一起被发送。这种情况就是粘包,当然对于接收端来说,我们不能保证 每次都能正好的完整的接收数据包,更多时候是x.5个数据包。
再次回到输水的模型,我们的容器等待水的到来,现在存在超时时间即每次等待水来有一个最大时间,超过这个时间,即使没有接到一滴水我们也要处理这个水桶。所以我们得到水桶的水理论上是大于等于0,小于等于水桶的容量。我想这样说应该可以很清楚的表达清楚了吧。现在开始从代码角度来说。
现在我们有一个byte[] buffer = new byte[MAX_LEN],即数据包读取缓冲区,int len = connection.read(buffer)。read方法使用buffer读数据流,len为实际读出的数据长度,此长度大于等于0,小于等于MAX_LEN。等于0自然不去处理,等于-1认为连接断开,当然read方法会抛出异常,即当读取数据过程中,连接出错。
现在我们获得一个buffer,即缓冲区。里面存在len长度的可用数据。我们要做的就是根据自己的协议结构将这个buffer转化为遵循我们自己的协议的packet。进而交由后面的业务逻辑代码处理。
此时我们定义自己的通信协议一个byte的包头,用于数据吧合法性验证,两byte数据包长(一般用4byte,即一个int),剩下内容为可变长度的数据包体。现在我们拿到buffer,这时候就有分包(粘包),和组包(数据包没有接收完整)两种情况。感觉似乎比较头疼,但是实际上获得packet我们紧紧需要知道的是数据包的真实长度,即2byte的内容,转为short后假设为PACKET_LEN。然后我们只要拆分和等待PACKET_LEN个长度的byte即可,那才是我们班真正需要的东西。当然,这个过程我曾经陷入过误区,然后经人指点后才发现我关注了很多没用的东西,结果增加了代码的复杂度。之后就上代码了,现在我的结构是服务器使用nio,然后nio框架将buffer封装为java.nio.ByteBuffer。其底层实现还是固定长度的byte[],它做的仅仅是封装了一些byte操作的快捷方法而已。既然它封装了,我们就要利用一下。
■ByteBuffer.remaining(),此方法最给力,返回剩余的可用长度,此长度为实际读取的数据长度,最大自然是底层数组的长度。于是这样看来这个ByteBuffer更像是一个可标记的流。
■ByteBuffer.get(byte[]),从ByteBuffer中读取byte[]。
首先呢把ByteBuffer当做流来处理,即read(ByteBuffer)之后ByteBuffer.flip()。此时重置到流的前端。这个java代码是按照最原始的思路写的,写的比较难看,但是比较清晰。有时间再优化下算法,应该可以写的再漂亮一点。
public List<byte[]> getPacket(ByteBuffer buffer) throws Exception{
pLink.clear();
try{
while(buffer.remaining() > 0){
if(packetLen == 0){  //此时存在两种情况及在数据包包长没有获得的情况下可能已经获得过一次数据包
if(buffer.remaining() + _packet.length < 3){
byte[] temp = new byte[buffer.remaining()];
buffer.get(temp);
_packet = PacketUtil.joinBytes(_packet , temp);
break;  //保存包头
}else{if(_packet.length == 0){
buffer.get();
packetLen = PacketUtil.parserBuffer2ToInt(buffer);
}else if(_packet.length == 1){
packetLen = PacketUtil.parserBuffer2ToInt(buffer);
} else if(_packet.length == 2){
byte[] lenByte = new byte[2];
lenByte[0] = _packet[1];
lenByte[1] = buffer.get();
packetLen = PacketUtil.parserBytes2ToInt(lenByte);
} else{
packetLen = PacketUtil.parserBytes2ToInt(_packet , 1);
}

}
}

if(_packet.length <= 3){   //此时_packet 没有有用数据,所需数据都在缓冲区中
if(buffer.remaining() < packetLen){
_packet = new byte[buffer.remaining()];
buffer.get(_packet);
}else{
byte[] p = new byte[packetLen];
buffer.get(p);
pLink.add(p);
packetLen = 0;
_packet = new byte[0];
}
}else {
if(buffer.remaining() + _packet.length - 3 < packetLen){   //剩余数据包不足一个完整包,保存后等待写一个
        byte[] temp = new byte[buffer.remaining()];
buffer.get(temp);
_packet = PacketUtil.joinBytes(_packet , temp);break;
}else{ //数据包完整或者多出
byte[] temp = new byte[packetLen - ( _packet.length - 3) ];
buffer.get(temp);
pLink.add(PacketUtil.subPacket(PacketUtil.joinBytes(_packet , temp)));
_packet = new byte[0];
packetLen = 0;
}
}
}
}catch(Exception e){
System.out.println("..GETPACKET packetLen = " + packetLen + " _packet.length = " + _packet.length);
throw e;
}
return pLink;
}
如果觉得不好看,可以先看下面的Flex首先方法,思路是一样的,但是看起来非常简单

//接收到消息
private function socketDataHandler(event:ProgressEvent):void{

try{
while(true){
if(packet_len == 0){
if(socket.bytesAvailable < 3) return ;
var temp : ByteArray = new ByteArray();
socket.readBytes(temp , 0 , 3);
packet_len = PacketUtil.parserBytesToInt2(temp , 1);
}
if(socket.bytesAvailable < packet_len) return;
var buffer : ByteArray = new ByteArray();
socket.readBytes(buffer , 0 , packet_len);
packet_len = 0;
buffer.position = 0;
packetArrive(buffer);
}

}catch(e : Error){
trace(e.message);
}
}
果然贴代码太占用篇幅了。首先拿Flex说,Flex库和Flash实际是一样的。flex中的socket中有自己的缓冲区,所以自己只管按时读数据即可。所以我们就等packet的长度,等待长度之后等这个长度的字节,简明扼要。但是java就不同,java的底层缓冲区我们没办法控制,于是就需要自己写一个东西缓冲没有接收完整的数据。就是代码中的_packet,他是一个初始化长度为0的byte[]。思想就是等我们需要的东西,等到就读出来,剩下不完整的就存起来和下一次合并再判断。当然这种东西都是有规律的,我觉得还没有发现这个规律,如果发现的话,代码长度应该会像Flex那么简明吧。
规律这种东西真的很美妙,我们总结出规律之后就完全跳出了复杂和容易出错的步骤,进而去关注更重要的事情。就像我获得packet之后,刚开始算数组索引,由于是可变长度,里面的内容也是定义的可变数据,所以算数据索引算的非常痛苦。之后我后来发现了所以规律,简单的说就是index += packet[index] + n。然后就完全从数据结构里面摆脱出来。
嗯,差不多就是这个样子了。
分享到:
评论

相关推荐

    基于tcp的socket编程

    - **粘包/拆包**:由于TCP的流式特性,可能会导致多个小数据包合并成一个大包或一个大数据包被拆分成多个小包。解决办法是在数据包头部添加长度信息,或者使用固定长度的数据包。 - **半关闭状态**:TCP连接允许...

    用JAVA进行TCP编程(原创)

    首先,我们要了解Java中的Socket类和ServerSocket类,它们是TCP编程的基础。`ServerSocket`用于创建服务器端的监听套接字,等待客户端的连接请求。创建`ServerSocket`时,通常需要指定一个端口号,例如: ```java ...

    C#中TCP粘包问题的解决方法

    TCP粘包是指发送方发送的多个数据包在接收方接收时被合并成一个大包,使得接收方无法正确区分各个独立的数据包。这种问题通常是由于TCP的优化策略,例如Nagle算法,以及接收方的处理方式导致的。 TCP粘包的产生主要...

    C# TCP Socket 分包传送数据

    首先,TCP协议本身并不保证一次性完整地传输大数据,而是根据网络状况将大数据拆分成多个小的数据段进行传输,这些小的数据段在网络中可能按照不同的路径到达目的地,这就产生了分包现象。因此,接收端需要有一种...

    C#TCP\Socket粘包处理(加长度头)

    然而,TCP本身并不保证数据包的边界,也就是说,多个小的数据包可能会被合并成一个大的数据包发送,或者一个大的数据包可能会被拆分成多个小的数据包发送,这就是所谓的“粘包”或“拆包”问题。在C#中,如果使用...

    java文件传输,基于tcp/ip的文件传输

    本项目专注于利用Java实现基于TCP/IP的文件传输,特别适合于局域网内的通信,如简单的聊天程序。 首先,我们要理解TCP协议的基本原理。TCP是一种面向连接的、可靠的传输协议,它确保数据包按照正确的顺序到达目的地...

    tcp-udp.zip_java socket udp_java tcp udp

    本篇将深入探讨如何在Java环境中使用Socket进行TCP和UDP的文件传输。 TCP是一种面向连接的、可靠的传输协议,它通过三次握手建立连接,并确保数据包按顺序到达,如果数据包丢失,TCP会自动重传。在Java中,我们可以...

    非常高效的 TCP socket 协议异步大文件数据传输代码,包括 sever 端与 client 端两个部分

    接收端程序通过端口 6544 向发送端程序发送一个 TCP 请求数据包,发送端监听到 TCP 请求数据包后,按数据包格式,将加载的图像拆分为固定大小的多个数据包,并异步发送给接收端程序,当接收端接收到所有数据包后,将...

    C# 异步TCP Socket聊天室(1服务器,N客户端)

    本项目"**C# 异步TCP Socket聊天室(1服务器,N客户端)**"正是基于这样的背景,旨在创建一个能够同时服务于多个客户端的聊天服务器,通过异步TCP Socket实现数据的实时传输。 首先,我们要理解TCP(Transmission ...

    tcp 文件传输 p2p socket

    TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,广泛应用于文件传输、网页浏览等场景。在本项目中,"tcp 文件传输 p2p socket" 涉及到的技术点包括TCP协议的特性、...

    javaqq(私聊+群聊+截图+文件传送)

    JavaQQ 是一个基于Java编程语言实现的即时通讯软件,它具备了私聊、群聊、截图和文件传输等核心功能,旨在提供与QQ类似的用户体验。这个项目不仅展示了Java在开发复杂应用程序上的能力,还体现了面向对象设计原则和...

    socket tcp如何防止多次send的包被合成一个包(粘包)发送.zip

    - 文件"2使用TCP_NODELAY选项,告诉内核尽快将该数据包发送出去.txt"和"2连接进行TCP_NODELAY选项的设置,关闭Nagle算法,这样就会像telnet一样,不管有多少数据会立即发送,没有延迟.txt"提到了这个方法。`TCP_...

    Socket通信,通过异步,解决粘包问题

    不过,这并不能完全解决问题,因为数据包的合并与拆分是TCP协议栈内部的行为,不受应用层控制。 2. **固定长度报文**:在设计通信协议时,约定每个报文都有固定的长度。接收方在接收到数据后,根据预设的报文长度...

    C# Socket 客户端服务端封装 支持多连接处理 Tasks多线程 队列处理 大数据拆分包处理

    首先,Socket在C#中是.NET Framework提供的一个类库,它允许开发者构建基于TCP或UDP协议的网络应用程序。Socket可以被封装成易于使用的类,以便开发者无需深入理解底层网络协议的细节,就能实现客户端和服务端的通信...

    wince下使用tcp协议socket上传文件,计时上传时间

    在Windows CE(简称Wince)操作系统环境下,使用TCP协议通过Socket进行文件上传是一个常见的任务,特别是在嵌入式设备或移动设备开发中。本教程将详细解释如何实现这一过程,并着重关注计时上传时间以评估性能。 ...

    【QT】自定义协议解决TCP粘包和拆包问题

    TCP为了提高传输效率,会将较小的数据包合并成一个大包发送,或者将一个大的数据包拆分成多个小包分别发送。这使得接收方无法直接根据接收到的数据判断其原始边界,从而引发问题。 为了解决这个问题,我们可以采取...

    【六祎 -Java】TCPUDP网络编程-文件上传演示代码.zip

    1. 数据分块:由于TCP是字节流,文件数据可能需要拆分成多个数据块进行传输,以适应网络的限制。 2. 错误处理:需要处理网络中断、文件读写异常等情况,确保传输的健壮性。 3. 断点续传:在大文件上传时,可以实现...

    c# Socket 多线程 分包 发送/接受数据

    当需要发送的数据量较大时,一次发送可能会超过Socket的缓冲区大小,这时就需要将大数据拆分成多个小的数据包(也称为帧或消息),逐个发送。同样,服务端也需要将接收到的小数据包重新组合成原始的大数据。在TCP中...

    Socket_File_Send.rar_TCP 文件传输_socket 文件传输_socket+文件传输_tcp文件传输_文件

    在TCP文件传输中,Socket扮演着桥梁的角色,将文件拆分成数据包并确保它们正确地发送到目的地,然后在接收端重新组合成原始文件。 首先,让我们了解TCP文件传输的基础。在发送文件时,我们需要创建一个Server端...

    socket tcp同步文件夹

    4. 数据打包与解包:为了将文件内容通过网络传输,需要将文件内容拆分成适合网络传输的数据包,并在接收端重新组合还原。 5. 多线程或多进程:为了提高同步效率,可能需要用到多线程或多进程,以便同时处理多个文件...

Global site tag (gtag.js) - Google Analytics