网络编程-NETTY
【基础篇】
1、TCP、UDP的区别?
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
- TCP提供可靠的服务,UDP尽最大努力交付,即不保证可靠交付
- UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
- .每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的交互通信。
- TCP对系统资源要求较多,UDP对系统资源要求较少。
2、TCP协议如何保证可靠传输?-(校 序 重 流 拥)
- 校验和:发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
- 编号排序:TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 超时重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
- 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
- 拥塞控制: 当网络拥塞时,减少数据的发送。
3、TCP的握手、挥手机制?
我们需要掌握哪些标志量
- SYN:请求同步标志,为1的时候为有效
- ACK:应答标志,表示接受到所发的数据,1为有效
- FIN:结束请求标志,1为有效
- ack:应答,值为告诉对方下一次所发数据地址
- seq:值为所发数据地址
TCP的三次握手:建立连接
(1)第一次握手,客户机给服务器发送SYN报文,请求连接(其中有一个序列号为x)
(2)第二次握手,服务器接收到这个SYN包,反馈客户机ACK(这个ACK有两个序列号,第一个是用来回答客户机的,第二个是用来校验第三次握手)
(3)第三次握手:客户机接收到服务器的报文,反馈给服务器成功接受的信息。然后这个TCP连接就建立了,通信可以开始。
TCP的四次挥手:关闭连接。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。
问题: 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的.
4、TCP的粘包/拆包原因及其解决方法是什么?
发生TCP粘包、拆包主要是由于下面一些原因:
- 1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
- 2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
- 3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
- 4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。
粘包、拆包解决办法
TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
- 消息定长。报文大小固定长度,不够空格补全。发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
- 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
- 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
5、Netty的粘包/拆包是怎么处理的,有哪些实现?
Netty的解决之道
LineBasedFrameDecoder
废话不多说直接上代码
服务端
public class PrintServer {
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new LineBasedFrameDecoder(1024)); //1
arg0.pipeline().addLast(new StringDecoder()); //2
arg0.pipeline().addLast(new PrintServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
new TimeServer().bind(port);
}
}
服务端Handler
public class PrintServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req); //将缓存区的字节数组复制到新建的req数组中
String body = new String(req, "UTF-8");
System.out.println(body);
String response= "打印成功";
ByteBuf resp = Unpooled.copiedBuffer(response.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
}
客户端
public class PrintClient {
public void connect(int port, String host) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new LineBasedFrameDecoder(1024)); //3
ch.pipeline().addLast(new StringDecoder()); //4
ch.pipeline().addLast(new PrintClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 8080;
new TimeClient().connect(port, "127.0.0.1");
}
}
客户端的Handler
public class PrintClientHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger
.getLogger(TimeClientHandler.class.getName());
private final ByteBuf firstMessage;
/**
* Creates a client-side handler.
*/
public TimeClientHandler() {
byte[] req = "你好服务端".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("服务端回应消息 : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 释放资源
System.out.println("Unexpected exception from downstream : "
+ cause.getMessage());
ctx.close();
}
}
上诉代码逻辑与上一章代码逻辑相同,客户端接受服务端数据答应,并回复客户端信息,客户端接受到数据后打印数据。
我们观察代码可以发现,要想Netty解决粘包拆包问题,只需在编写服务端和客户端的pipeline上加上相应的解码器即可,上诉注释 1,2,3,4处。其余代码无需做任何修改。
LineBasedFrameDecoder+StringDecoder的组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。
原理为:如果连续读取到最大长度后任然没有发现换行符,就会抛出异常,同时忽略掉之前督导的异常码流。
DelimiteBasedFrameDecoder
该解码器的可以自动完成以分割符作为码流结束标识的消息解码。
(其实上一个解码器类似,如果指定分隔符为换行符,那么与上一个编码器的作用基本相同)
使用也很简单:
只需要修改服务端和客户端对应代码中的initChannel代码即可
public void initChannel(SocketChannel ch) {
ByteBuf delimiter = Unpooled.copiedBuffer("_".getBytes());//1首先创建分隔符缓冲对象ByteBuf,并指定以"_"作为分隔符。
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));//2:将分隔符缓冲对象ByteBuf传入DelimiterBasedFrameDecoder,并指定最大长度。
ch.pipeline().addLast(new StringDecoder()); //3指定为字符串字节流
ch.pipeline().addLast(new PrintHandler());
}
FixedLengthFrameDecoder
该解码器为固定长度解码器,它能够按照指定的长度对详细进行自动解码。
使用同样也很简单:
同样只需要修改服务端和客户端对应代码中的initChannel代码即可
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new PrintHandler());
}
});
这样我们就指定了,每接收20个字符大小的字符串字节流就将其看作一个包来经行处理。
6、同步与异步、阻塞与非阻塞的区别?
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
同步阻塞、同步非阻塞(轮训机制)、异步非阻塞
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞:轮训机制)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。
7、说说网络IO模型?
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
下图是几种常见I/O模型的对比:
以socket.read()为例子:
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
【netty篇】
1.Netty 是什么?
Netty 是一款基于 NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。难能可贵的是,在保证快速和易用性的同时,并没有丧失可维护性和性能等优势。
2.Netty 的特点是什么?
- 高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
- 传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
- 封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。
3.什么是 Netty 的零拷贝?
Netty 的零拷贝主要包含三个方面:
- Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
- Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
4.Netty 的优势有哪些?
- 使用简单:封装了 NIO 的很多细节,使用更简单。
- 功能强大:预置了多种编解码功能,支持多种主流协议。
- 定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。
- 性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。
- 稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。
- 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。
5.Netty 的应用场景有哪些?
典型的应用有:阿里分布式服务框架 Dubbo,默认使用 Netty 作为基础通信组件,还有 RocketMQ 也是使用 Netty 作为通讯的基础。
6.Netty 高性能表现在哪些方面?
- IO 线程模型:同步非阻塞,用最少的资源做更多的事。
- 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
- 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
- 串形化处理读写:避免使用锁带来的性能开销。
- 高性能序列化协议:支持 protobuf 等高性能序列化协议。
7.Netty 和 Tomcat 的区别?
- 作用不同:Tomcat 是 Servlet 容器,可以视为 Web 服务器,而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。
- 协议不同:Tomcat 是基于 http 协议的 Web 服务器,而 Netty 能通过编程自定义各种协议,因为 Netty 本身自己能编码/解码字节流,所有 Netty 可以实现,HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。
8.Netty 中有那种重要组件?
- Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。
- EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。
- ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。
- ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
- ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。
9.Netty 发送消息有几种方式?
Netty 有两种发送消息的方式:
- 直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动;
- 写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。
10.默认情况 Netty 起多少线程?何时启动?
Netty 默认是 CPU 处理器数的两倍,bind 完之后启动。
11.Netty 支持哪些心跳类型设置?
readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
allIdleTime:所有类型的超时时间。
12.项目中使用Netty使用的功能
服务的心跳报警,和监控中心通过netty服务传输心跳信息,一但收不到心跳信息,则发送报警email。
发送异常报告包,监控中心用来统计异常。