什么是粘包/拆包
一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
粘包/拆包问题一般的处理方式有四种:
数据段定长处理,位数不足的空位补齐。
消息头+消息体,消息头中一般会包含消息体的长度,消息类型等信息,消息体为实际数据体。
特殊字符(如:回车符)作为消息数据的结尾,以实现消息数据的分段。
复杂的应用层协议,这种方式使用的相对较少,耦合了网络层与应用层。 上面的四种方式目的都是为了将数据在流中精确分开以便进一步解析处理,在自定义的协议中,第二种方式用的比较多,因为它更能满足定制化协议开发需求,比如自定义Netty协议时可以将具体数据报文放入消息体,消息头中根据需要放入其他变量(如:消息类型,此处可以具体对应到维护netty长链接的心跳消息、客户端请求消息、服务端处理结果消息等)。
短连接
连接->传输数据->关闭连接
比如HTTP是无状态的的短链接,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
因为连接后接收了数据就断开了,所以每次数据接受处理不会有联系。 这也是HTTP协议无状态的原因之一。
长连接
连接->传输数据->保持连接 -> 传输数据-> …->直到一方关闭连接,多是客户端关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,
这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都
不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果
用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网
站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成
千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频
繁操作情况下需用短连好。
单客户端实例
客户端:
public static void main(String[] args) throws InterruptedException {
System.out.println("客户端已经启动....");
// 创建负责接收客户端连接
NioEventLoopGroup pGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf = b.connect("127.0.0.1", 8080).sync();
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("itmayiedu".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("itmayiedu".getBytes()));
// 等待客户端端口号关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
}
public class ClientHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端消息:"+msg.toString());
}
}
服务端:
public static void main(String[] args) {
try {
System.out.println("服务器端启动");
NioEventLoopGroup pGroup=new NioEventLoopGroup();
NioEventLoopGroup pGroup2=new NioEventLoopGroup();
//创建铺助类
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(pGroup, pGroup2).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_SNDBUF,32*1024).option(ChannelOption.SO_RCVBUF, 32*1024)
.childHandler(new ChannelInitializer<io.netty.channel.socket.SocketChannel>() {
@Override
protected void initChannel(io.netty.channel.socket.SocketChannel soc) throws Exception {
soc.pipeline().addLast(new StringDecoder());
soc.pipeline().addLast(new ServerHandler());
};
});
ChannelFuture sync = serverBootstrap.bind(8080).sync();
sync.channel().closeFuture().sync();
pGroup.shutdownGracefully();
pGroup2.shutdownGracefully();
}catch(Exception e) {
e.printStackTrace();
}
}
public class ServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
System.out.println("服务端消息:"+msg.toString());
//ctx.channel().writeAndFlush("serverHandler"+System.currentTimeMillis());
//把消息往下一个Handler传
ctx.fireChannelRead(msg);
}
}
多客户端
服务端:
private Bootstrap bootstrap=new Bootstrap();
private final AtomicInteger index=new AtomicInteger();
//对话对象组
private List<Channel> channels=new ArrayList<Channel>();
public void init(int count){
bootstrap.group(new NioEventLoopGroup());
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ServerHandlerMuch());
}
});
for (int i = 0; i < count; i++) {
Channel channel=bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).channel();
channels.add(channel);
}
}
//获取可用的channel
private Channel next(){
return getChannel(0);
}
private Channel getChannel(int count) {
Channel channel = channels.get(Math.abs(this.index.getAndIncrement()%channels.size()));
if(count>=channels.size()) {
throw new RuntimeException("没有足够的管道");
}
if(!channel.isActive()) {
//重连
reconnect(channel);
//尝试获取下一个channel
return getChannel(++count);
}
return channel;
}
private void reconnect(Channel channel) {
synchronized (channel) {
int index=channels.indexOf(channel);
bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).channel();
channels.set(index, channel);
}
}
public static void main(String[] args) {
NetServerMuch netServerMuch=new NetServerMuch();
netServerMuch.init(10);
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("请输入:");
String next = scanner.next();
try {
Channel next2 = netServerMuch.next();
next2.writeAndFlush(next);
}catch (Exception e) {
e.printStackTrace();
}
}
}
序列化协议与自定义序列化协议
序列化定义
序列化(serialization)就是将对象序列化为二进制形式(字节数组),一般也将序列化称为编码(Encode),主要用于网络传输、数据持久化等;
反序列化(deserialization)则是将从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务的进行,一般也将反序列化称为解码(Decode),主要用于网络传输对象的解码,以便完成远程调用。
序列化协议“鼻祖”
我知道的第一种序列化协议就是Java默认提供的序列化机制,需要序列化的Java对象只需要实现 Serializable / Externalizable 接口并生成序列化ID,这个类就能够通过 ObjectInput 和 ObjectOutput 序列化和反序列化
java序列化的缺点
java自己提供序列化而且用起来也非常简单,但是在远程服务调用中很少用它,主要存在以下缺点:
无法跨语言:我认为这对于Java序列化的发展是致命的“失误”,因为Java序列化后的字节数组,其它语言无法进行反序列化。;
序列化后的码流太大::相对于目前主流的序列化协议,Java序列化后的码流太大;
序列化的性能差:由于Java序列化采用同步阻塞IO,相对于目前主流的序列化协议,它的效率非常差。
影响序列化性能的关键因素
序列化后的码流大小(网络带宽的占用);
序列化的性能(CPU资源占用);
是否支持跨语言(异构系统的对接和开发语言切换)。
netty序列化
对象
public class TransportObject implements Serializable {
/**
*
*/
private static final long serialVersionUID = -4128002851096643251L;
private String name;
private int id;
private List list = new ArrayList();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
@Override
public String toString() {
return "TransportObject{" +
"name='" + name + '\'' +
", id=" + id +
", list=" + list +
'}';
}
}
服务端
public class TranspoertObjectServer {
private static int port = 7777;
public static void start() {
ServerBootstrap bootstrap = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.option(ChannelOption.SO_BACKLOG, 2048);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
ch.pipeline().addLast(new ObjectServerHandler());
}
});
ChannelFuture channelFuture = bootstrap.bind(port);
System.out.println("server start");
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
start();
}
}
public class ObjectServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
ctx.writeAndFlush(msg);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getMessage());
ctx.close();
// super.exceptionCaught(ctx, cause);
}
}
客户端
public class TranspoertOjectClient {
private static int port = 7777;
private static String ip = "localhost";
public static void connect(){
Bootstrap bootstrap = new Bootstrap();
EventLoopGroup worker = new NioEventLoopGroup();
try{
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE,
ClassResolvers.cacheDisabled(null)));
ch.pipeline().addLast(new ObjectClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(ip, port).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
connect();
}
}
public class ObjectClientHandler extends ChannelInboundHandlerAdapter {
private TransportObject getTransportObject(){
TransportObject to = new TransportObject();
to.setId(10001);
to.setName("xiaowang");
to.setList(Arrays.asList("zhangsan","lisi","wangwu"));
return to;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("active");
super.channelActive(ctx);
//发送消息给服务端
ctx.writeAndFlush(getTransportObject());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//super.exceptionCaught(ctx, cause);
System.out.println(cause.getMessage());
ctx.close();
}
}
XML
(1)定义:
XML(Extensible Markup Language)是一种常用的序列化和反序列化协议, 它历史悠久,从1998年的1.0版本被广泛使用至今。
(2)优点
人机可读性好
可指定元素或特性的名称
(3)缺点
序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息。
类必须有一个将由 XmlSerializer 序列化的默认构造函数。
只能序列化公共属性和字段
不能序列化方法
文件庞大,文件格式复杂,传输占带宽
(4)使用场景
当做配置文件存储数据
实时数据转换
JSON
(1)定义:
JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集, JSON采用与编程语言无关的文本格式,但是也使用了类C语言(包括C, C++, C#, Java, JavaScript, Perl, Python等)的习惯,简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。
(2)优点
前后兼容性高
数据格式比较简单,易于读写
序列化后数据较小,可扩展性好,兼容性好
与XML相比,其协议比较简单,解析速度比较快
(3)缺点
数据的描述性比XML差
不适合性能要求为ms级别的情况
额外空间开销比较大
(4)适用场景(可替代XML)
跨防火墙访问
可调式性要求高的情况
基于Web browser的Ajax请求
传输数据量相对小,实时性要求相对低(例如秒级别)的服务
Fastjson
(1)定义
Fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致。
(2)优点
接口简单易用
目前java语言中最快的json库
(3)缺点
过于注重快,而偏离了“标准”及功能性
代码质量不高,文档不全
(4)适用场景
协议交互
Web输出
Android客户端
Thrift
(1)定义:
Thrift并不仅仅是序列化协议,而是一个RPC框架。它可以让你选择客户端与服务端之间传输通信协议的类别,即文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议。
(2)优点
序列化后的体积小, 速度快
支持多种语言和丰富的数据类型
对于数据字段的增删具有较强的兼容性
支持二进制压缩编码
(3)缺点
使用者较少
跨防火墙访问时,不安全
不具有可读性,调试代码时相对困难
不能与其他传输层协议共同使用(例如HTTP)
无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议
(4)适用场景
分布式系统的RPC解决方案
Avro
(1)定义:
Avro属于Apache Hadoop的一个子项目。 Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,Avro的产生解决了JSON的冗长和没有IDL的问题
(2)优点
支持丰富的数据类型
简单的动态语言结合功能
具有自我描述属性
提高了数据解析速度
快速可压缩的二进制数据形式
可以实现远程过程调用RPC
支持跨编程语言实现
(3)缺点
对于习惯于静态类型语言的用户不直观
(4)适用场景
在Hadoop中做Hive、Pig和MapReduce的持久化数据格式
Protobuf
(1)定义
protocol buffers 由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
(2)优点
序列化后码流小,性能高
结构化数据存储格式(XML JSON等)
通过标识字段的顺序,可以实现协议的前向兼容
结构化的文档更容易管理和维护
(3)缺点
需要依赖于工具生成代码
支持的语言相对较少,官方只支持Java 、C++ 、Python
(4)适用场景
对性能要求高的RPC调用
具有良好的跨防火墙的访问属性
适合应用层对象的持久化