转自:https://www.cnblogs.com/qhyuan1992/p/5385289.html

其实最终讨论的是TCP通信过程中的粘包拆包(半包)问题。

在用socket写一个服务器时遇到了问题于是将主要的问题抽了出来,代码如下,由于代码很简单于是也没有注释。

public class Main {

private static ServerSocket serverSocket;

private final static ExecutorService exec = Executors.newFixedThreadPool(30);

public static void main(String[] args) {

try {

serverSocket = new ServerSocket(8888);

while (true) {

Socket socket = serverSocket.accept();

exec.execute(new ServerRunnable(socket));

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

class ServerRunnable implements Runnable {

private Socket socket;

private InputStream is;

private OutputStream out;

private String reqStr;

private String resContent;

public ServerRunnable(Socket socket) {

this.socket = socket;

}

@Override

public void run() {

handleSocket(socket);

}

private void handleSocket(Socket socket) {

try {

byte[] buffer = new byte[1024];

is = socket.getInputStream();

System.out.println(is);

out = socket.getOutputStream();

int len = 0;

StringBuilder sb = new StringBuilder();

while ((len = is.read(buffer)) != -1) {

String str = new String(buffer, 0, len);

sb.append(str);

}

reqStr = sb.toString();

System.out.println(reqStr);

resContent = "Welcome!";

StringBuilder resBuilder = new StringBuilder();

resBuilder.append("HTTP/1.1 200 OK").append("\r\n").

append("Date:").append(new Date()).append("\r\n").

append("Content-Type:").append("text/plain;charset=UTF-8").append("\r\n").

append("Content-Length:").append(resContent.getBytes().length).append("\r\n").

append("\r\n");

resBuilder.append(resContent);

out.write(resBuilder.toString().getBytes());

socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

代码很简单,就是写了一个Socket的服务器,通过浏览器来访问localhost:8888会返回Welcome!

可是在实际工作时,死活不能达到效果。

我想到过可能是out根本就没把数据写进去,然后断点调试,但就是因为断点调试才导致很长时间没能把错误找出来。

1.在测试的时候有这样一个现象一直没引起我的注意:服务器端打印的浏览器发过来的数据在点击停止加载网页/刷新时才会打印!!(知道真相后明白了是因为断开连接另一端就会跳出阻塞继续执行下去)

而我在测试的时候由于浏览器一直收不到服务器端发的数据而处于不停地等待状态,我就会再次刷新或者再访问一次,而恰恰由于这样愚蠢的操作,服务器端打印了数据,断点调试也进去了,于是我好长时间没有怀疑是因为压根就没走到这一步。而怀疑是我的电脑哪里或者浏览器哪里没设置好。

2.屏蔽了handleSocket里面接收客户端的输入代码,仅仅加上给客户端发的数据,发现可以收到数据,明确了数据没有写错,最后在发现上面的问题后在while循环处打断点,最终发现程序阻塞在那里。

刚开始感到很奇怪,大文件的复制不都是这样做的么,怎么还会出错,在网上搜了一下,socket在close后,才会发送给另一端结束符EOF,从而才会read到流结尾信息而返回-1。

以前写java聊天功能的时候其实遇到过这样的问题的,要退出聊天发一个特定的字符,然后在break出循环,接着会close掉socket,这样另一端的会由于这端的socket被close掉也跳出循环。只是现在由于只写服务端就没想到。

因为无法知道远程的socket是否还有没有东西要发送。所以read一直不会返回。

read的文档说明大致是:如果因已到达流末尾而没有可用的字节,则返回值 -1。在输入数据可用、检测到流的末尾或者抛出异常前,此方法一直阻塞。

socket和文件不一样,从文件中读,读到末尾就到达流的结尾了,所以会返回-1或null,循环结束,但是socket是连接两个主机的桥梁,一端无法知道另一端到底还有没有数据要传输。

socket如果不关闭的话,read之类的阻塞函数会一直等待它发送数据,就是所谓的阻塞。

当然这里我们可以将缓冲buffer调整的大一点,这样不用while循环,只读一次即可,然而其他的场景比如发送的数据很大一次读不完那么就只能while循环来处理了。这种场景下的解决方案方案见下面。

四种途径解决:

1.调用socke的shutdownOutput方法关闭输出流,该方法的文档说明为,将此套接字的输出流置于“流的末尾”,这样另一端的输入流上的read操作就会返回-1。不能调用socket.getInputStream().close()。这样会导致socket被关闭。

2.设置超时,会在设置的超时时间到达后抛出SocketTimeoutException异常而不再阻塞。

3.约定结束标志,当读到该结束标志时退出不再read。

4.约定数据长度,数据长度不够则补齐,每次read约定好的长度即可。

5.在头部约定好数据的长度。当读取到的长度等于这个长度时就不再继续调用read方法。

总之tcp方式会经常由于阻塞函数等read/readLine和流处理的函数如刷新缓冲导致代码出现问题。一定要小心!

方式1一般用在通信双方均由开发者掌控。方式2总感觉不好,超时应该用在其他更有意义的地方,如网络不好时的时间限制。方式3有一定的局限,并且双方还要沟通好标结束志。方式4由于补齐会造成浪费。方式5应该是最好的方式,并且大多数的情况都是这样做的。

显然我们这里不能使用方式1。

于是我立刻想到了一个问题:HTTP协议的结束标志是什么?

貌似就搜到了几个地方有人讨论该问题,见:

1.主题:学习Spring必学的Java基础知识(9)—-HTTP报文(系列全) 里面提到的结束标志我测试了也不对。

2.http包结束的标志

我没有研究过HTTP协议的具体细节,只知道它是对Socket的封装和一些协议的格式,其他的还不太清楚,不过就目前看到的来看应该没有让服务器端知道数据结束的标志。

于是另一个问题又在我脑海产生了:tomcat源代码是怎么解析HTTP协议的头信息呢?

我最初猜想应该是通过第5种方式因为包含了Content-Length字段,很容易能得到总的大小。大致翻看了一下源代码,貌似还不是这样,其采用的是NIO Socket实现的,

在解析HTTP的头时是一个字节一个字节解析的,不过代码太长,只是看了个大概,比较了解的可以和我交流学习,不胜感激。

最后讨论下半包和粘包问题

什么是TCP粘包半包?

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包(半包);

(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包(半包)。

解决粘包拆包(半包)问题

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

(1)在包尾增加分割符,比如回车换行符进行分割,例如FTP协议;如使用netty的LineBasedFrameDecoder和DelimiterBasedFrameDecoder,如果超过规定字节长度,会报错。

(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;如使用netty的FixedLengthFrameDecoder

(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第 一个字段使用int32来表示消息的总长度,如netty的LengthFieldBasedFrameDecoder。

参考

Netty4实战 - TCP粘包&拆包解决方案

Java包数据消息头消息尾_读Socket流时产生阻塞的解决方案(粘包拆包问题)相关推荐

  1. java tcp怎么拆包_Java网络编程基础之TCP粘包拆包

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

  2. java获取一个tcp包大小_Java网络编程之TCP粘包拆包

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

  3. tcp协议缓冲区溢出_关于TCP 粘包拆包,你了解吗?

    一.什么是粘包拆包? 粘包拆包是TCP协议传输中一种现象概念.TCP是传输层协议,他传输的是"流"式数据,TCP并不知道传输是哪种业务数据,或者说,并不关心.它只是根据缓冲区状况将 ...

  4. (Java)socket网络编程及处理socket粘包拆包问题

    目录 1.socket简介 2.TCP/IP协议 3.tcp三次握手 4.socket的一些接口函数原理 5.java socket 长连接粘包拆包问题 6.socket模拟服务端客户端发消息 7.U ...

  5. Java基础之《netty(28)—TCP粘包拆包原理》

    一.基本介绍 1.TCP是面向连接的,面向流的,提供高可靠性服务.收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Na ...

  6. socket接收时信号量阻塞了会丢数据吗_浅谈Java网络编程——非阻塞I/O

    文件描述符(descriptors) Unix中I/O的基本组成元素是字节序列.大多数程序应用于字节流或I/O流. 进程通过描述符引用I/O流,也被称作文件描述符.管道.文件.POSIX IPC's( ...

  7. java代码中出现黑色画线_从对象流中读取对象,请在画线处加入代码完成此程序【 】。 import java.util.*; import ja...

    从对象流中读取对象,请在画线处加入代码完成此程序[ ]. import java.util.*; import ja 更多相关问题 [问答题,简答题] "三好一满意"是什么? [单 ...

  8. java自带的字节码技术_读懂字节码-还原JAVA源码

    已知有两个类: public class Father extends GrandFather { public String name = "father"; public vo ...

  9. Netty详解(七):Netty 编解码以及消息头编解码器

    1. MessagePack 概述 MessagePack是一个高效的二进制序列化框架,像JSON一样支持不同语言间的数据交换,速度更快,序列化之后的码流更小. MessagePacke优点 编解码高 ...

最新文章

  1. 【快速入门系列】简述 for...in 和 for...of 区别
  2. JZOJ 5197. 【NOIP2017提高组模拟7.3】C
  3. 怎样利用细碎时间达到整体学习的效果
  4. Oracle11g不能导出空表问题
  5. 负载均衡Ribbon和Feign---SpringCloud
  6. Java内存泄露8种情况的总结
  7. 没有到主机的路由_网络基础知识:TCP协议之跟踪路由
  8. python基础语法(一)
  9. 全网首发:JDK绘制文字:七、使用字体图像进行绘制
  10. 转载:医疗保险,公积金、养老、生育、工伤、失业保险
  11. python文字处理dummy_python – 使用’dummy’变量创建数学函数,以便稍后进行评估...
  12. ASML的EUV光刻机卖不动?
  13. php做网站需要的技术,建网站需要什么技术
  14. 简洁好用的3个研发项目管理工具
  15. 图像的MAP-MRF模型
  16. vista磁盘使用100%_在Vista上运行的免费ISO磁盘映像刻录实用程序
  17. 3.Spring Boot使用Apache Curator实现leader选举「第四章 ZooKeeper Curator应用场景实战」「架构之路ZooKeeper理论和实战」
  18. Thread--线程创建的三种方式
  19. css中设置图片上下移动,CSS如何实现图片上下垂直居中
  20. 如何利用计算机管护,计算机数据库的管护研究论文

热门文章

  1. AIM Tech Round 4 (Div. 2)
  2. 递归,复制对象,找质数,判断数组
  3. mysql用shell建100多字段表并导入
  4. 什么是向量中断,什么是中断向量?
  5. Linux 账户信息显示和实现账户安全
  6. 获取结构体某成员偏移
  7. 计算机流水线作业员做什么,电子厂普通作业员的工作流程是什么
  8. java实现展示框,如何使用纯CSS实现蝴蝶标本的展示框效果
  9. 报告PPT|Python编程之美(45页)
  10. 微课|中学生可以这样学Python(5.8.1节):使用切片访问列表元素