文章目录
Netty权威指南读书笔记(☆☆☆)
Java的I/O演进之路
IO基础入门
java1.4之前的IO缺点:
没有数据缓冲区,IO性能存在问题
没有C中的Channel的概念,只有输入和输出流
同步阻塞式IO通信,通常会导致通信线程被长时间阻塞
支持的字符集有限,硬件可移植性不好
Linux的IO模型简介
Linux的内核将所有的外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个fd,对socket的操作也会有相应的fd,它指向内存中的一个结构体。UNIX提供了5中模型:
阻塞IO模型:对于套接字来说,进程空间调用recvfrom命令,系统直到有数据时才会返回
非阻塞IO:recvfrom从应用层到内核的时候,如果没有数据直接返回一个错误,一般非阻塞IO模型都会轮询这个状态,看是否有数据
IO复用模型:对应select/poll模型,注意此处的select和NIO的Selector不是一个东西,select/poll通过顺序扫描的方式侦测多个fd是否处于就绪状态。注意select命令是阻塞的,当select有返回的时候再调用recvfrom命令读取数据。后面有发展处epoll模型,采用事件驱动的方式代替顺序扫描
信号驱动IO模型:它是有异步的,当有数据时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据。
异步IO:告知内核启动某个操作,并让内核在整个完成操作后通知应用程序(底层就不是使用recvfrom命令了,因为该命令仍然是同步的)
IO多路复用技术
IO多路复用技术通过把多个IO的阻塞复用到一个select的阻塞上(单个阻塞效率低,多个一起阻塞,降低阻塞的概率),应用场景:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
服务器需要同时处理多种网络协议的套接字
epoll的改进:
支持一个进程打开的socket描述符不受限制
IO效率不会随着FD数目的增加而线性下降
使用mmap加速内核与用户空间的消息传递
epoll的API更简单
Java的IO演进
- jdk1.4引用NIO
- jdk1.7引入NIO2
NIO入门
传统的BIO编程
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后每个客户端创建一个新的线程进行连接处理。采用BIO的服务器随着并发的上升会开启大量的线程,导致系统性能急剧下降,甚至线程堆栈溢出、创建新线程失败等问题。采用BIO的客户端和服务端代码(客户端发送指令,服务端返回当前时间,客户端打印返回的消息):
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("server is start in port:" + port);
Socket socket;
while (true) {
socket = server.accept();
//此处可以不用异步线程处理
new Thread(new TimeServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (server != null) {
System.out.println("time server close");
server.close();
}
}
}
}
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime;
String body;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("Time server receive order:" + body);
currentTime = "QUERY TIME".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
out.println(currentTime);
out.flush();
System.out.println("end");
}
} catch (IOException e) {
if (in != null) {
try{
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
if (out != null) {
out.close();
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
}
public class TimeClient {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME");
System.out.println("send order 2 server succeed.");
String resp = in.readLine();
System.out.println("Now is: " + resp);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
if (socket != null) {
socket.close();
}
}
}
}
伪异步IO
BIO的代码可以优化一下,那就是采用线程池来减少线程的数量,防止资源耗尽,伪异步IO的服务端代码:
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port: " + port);
Socket socket = null;
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000);
while (true) {
socket = server.accept();
singleExecutor.execute(new TimeServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
public class TimeServerHandlerExecutePool {
private ExecutorService executor;
public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));
}
public void execute(Runnable task) {
executor.execute(task);
}
}
伪异步IO底层仍然使用的是同步IO,也就是其中的read方法,write方法都是同步阻塞的,当出现以下的情况,很可能会出问题:
消息接收方接收缓慢,导致发送TCP的发送窗口不断减少,直到为0,那么此时write就会被无限期阻塞
返回应答过长,所有的可用线程都被故障服务器阻塞,后续所有的IO消息都将在队列中排队,后续入队列的操作将会被阻塞,队列满后,接入的连接将会被拒绝
NIO编程
NIO采用多路复用的机制,通过监听事件判断Channel的状态,从而进行相关的操作:
public class TimeServer {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
MultiplexerTimerServer timeServer = new MultiplexerTimerServer(port);
new Thread(timeServer,"NIO SERVER").start();
}
}
public class MultiplexerTimerServer implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
public MultiplexerTimerServer(int port) {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//第二个参数backlog为请求队列的最大数量
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
//表示程序非正常退出
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
//每隔1s返回一次
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
//监听accept事件
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
//读数据
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
if (readBytes > 0) {
//为了读取数据
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order: " + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc, currentTime);
}
}
}
private void doWrite(SocketChannel sc, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
//写完之后调用flip
writeBuffer.flip();
sc.write(writeBuffer);
}
}
}
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new Thread(new TimeClientHandle("127.0.0.1", port),
"TimeClient").start();
}
}
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
key = iterator.next();
iterator.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
//判断是否可以连接,因为连接是异步的,连接的时候不一定连接上
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
System.out.println("connected");
}else{
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int read = sc.read(readBuffer);
if (read > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is: " + body);
this.stop = true;
} else if (read < 0) {
key.cancel();
sc.close();
} else {
System.out.println("get zero bytes");
}
}
}
}
private void doConnect() throws IOException {
//连接异步的
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}else {
//如果没有连接成功,处理的时候再进行连接
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void doWrite(SocketChannel socketChannel) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
socketChannel.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("Send order to server succeed.");
}
}
}
AIO编程
NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现,异步通道提供了一下两种方式获取操作结果:
通过Future类来表示异步操作的结果
在执行异步操作(包括accept, connect, read, write)的时候传入一个CompletionHandler接口的实现类作为操作完成的回调
服务端和客户端代码:
public class TimeServer {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
AsynTimeServerHandler handler = new AsynTimeServerHandler(port);
new Thread(handler, "AIO SERVER").start();
}
}
public class AsynTimeServerHandler implements Runnable {
private int port;
CountDownLatch latch;
AsynchronousServerSocketChannel channel;
public AsynTimeServerHandler(int port) {
this.port = port;
try {
channel = AsynchronousServerSocketChannel.open();
channel.bind(new InetSocketAddress(port));
System.out.println("The time server is start in port: " + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAccept() {
//通过回调接口完成后续处理,传入的attachment即使本类
channel.accept(this, new AcceptCompletionHander());
}
public class AcceptCompletionHander implements CompletionHandler<AsynchronousSocketChannel,AsynTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result, AsynTimeServerHandler attachment) {
//每当接收一个连接成功后,再异步接收新的客户端连接
attachment.channel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//又交由读的回调类处理
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsynTimeServerHandler attachment) {
exc.printStackTrace();
//失败后结束程序
attachment.latch.countDown();
}
}
}
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] bytes = new byte[attachment.remaining()];
attachment.get(bytes);
try {
String req = new String(bytes, "UTF-8");
System.out.println("The time sever receive ordder: " + req);
String currentTime = "QUERY TIME".equalsIgnoreCase(req) ?
new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String currentTime) {
if (currentTime != null && currentTime.length() > 0) {
byte[] bytes = currentTime.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (attachment.hasRemaining()) {
channel.write(attachment, attachment, this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
new Thread(new AsynTimeClientHandler("127.0.0.1", port), "AIO CLIENT").start();
}
}
public class AsynTimeClientHandler implements CompletionHandler<Void, AsynTimeClientHandler>, Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public AsynTimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
}
catch (IOException e) {
e.printStackTrace();
}
}
@Override public void run() {
latch = new CountDownLatch(1);
//attachment是this
client.connect(new InetSocketAddress(host, port), this, this);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
client.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
@Override public void completed(Void result, AsynTimeClientHandler attachment) {
//连接上服务器的回调方法
byte[] req = "QUERY TIME".getBytes();
final ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(req);
buffer.flip();
client.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer result, ByteBuffer byteBuffer) {
//写的回调方法,因为可能一次不能写完,所以要判断下
if (byteBuffer.hasRemaining()) {
String name = this.getClass().getName();
System.out.println(name);
//继续写
client.write(byteBuffer, byteBuffer, this);
}else {
final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer result, ByteBuffer byteBuffer) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String body;
try {
body = new String(bytes, "UTF-8");
System.out.println("Now is:" + body);
}catch (UnsupportedEncodingException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
}
@Override public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
}
});
}
}
@Override public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
}
});
}
@Override public void failed(Throwable exc, AsynTimeClientHandler attachment) {
try {
client.close();
}
catch (IOException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
}
}
选择Netty的理由
原生NIO的缺点
API复杂
对异常情况的处理能力不够
不稳定,曾出现过Bug
netty的缺点:
API简单
功能强大,内置多种编解码能力
定制能力强
稳定,社区成熟
Netty 入门应用
这里采用Netty来实现之前的客户端和服务端,注意,本书采用的Netty5.0已经被废弃,因此代码有所变动。
代码的解析:
NioEventLoopGroup是一个线程组,包含了一组NIO线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接收客户端的连接,另一个用于进行SocketChannel的网络读写。
ServerBootstrap对象,它是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度
设置创建的Channel是NioServerSocketChannel,它的功能对应于JDK NIO类库中的ServerSocketChannle类
最后绑定IO事件的处理类是ChildChannelHandler,主要用于处理网络IO事件
使用f.channel().closeFuture().sync()方法进行阻塞,等待服务链路关闭之后Main退出
public class TimeServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//backlog是阻塞队列
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler());
ChannelFuture f = bootstrap.bind(port).sync();
//绑定端口,同步等待成功
f.channel().closeFuture().sync();
}finally {
//优雅退出
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeServer().bind(port);
}
}
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The Time server receive order: " + body);
String cuurentTime = "QUERY TIME".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD QUERY";
ByteBuf resp = Unpooled.copiedBuffer(cuurentTime.getBytes());
//不是写到channel,而是写到buffer,通过下面的flush真正写到SocketChannel
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
客户端的代码:
当客户端和服务端TCP链路建立成功后,Netty的NIO线程会调用channelActive方法,发送查询时间的指令给服务端
public class TimeClient {
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeClient().connect(port, "127.0.0.1");
}
}
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf firstMessage;
public TimeClientHandler() {
byte[] req = "QUERY TIME".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//链路建立的时候发送第一条消息
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is :" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("Exception ");
cause.printStackTrace();
ctx.close();
}
}
TCP粘包/拆包问题的解决之道
粘包:就两条消息被当成一个包发送了,导致解析出问题
拆包:数据包包含完整的消息和不完整的部分消息
解决粘包问题
解决策略:该问题只能在应用层解决,TCP传输的都是字节流,不感知消息内容
消息定长
在包尾加上回车换行进行分割
将消息分为消息头和消息尾,消息头包含消息的总长度
使用更复杂的应用层消息
为了验证粘包问题,可以让客户端发100条命令给服务端,然后让服务端打印收到的消息数,如果基于上面的代码来实现这个验证,会发现服务端收到的消息数仅为两条
代码实现
这里我们采用消息发送方和接收方都已换行符来分割消息体:
LineBasedFrameDecoder的工作原理是它一次遍历ByteBuf中的可读字节,判断是否有换行符,如果有就以此为结束
StringDecoder将受到的对象转成字符串,然后继续调用后面的Handler
服务端代码:
public class TimeServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//backlog是阻塞队列
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler());
ChannelFuture f = bootstrap.bind(port).sync();
f.channel().closeFuture().sync();
}finally {
//优雅退出
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeServer().bind(port);
}
}
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("The Time server receive order: " + body + " the counter is: " + ++counter);
String cuurentTime = "QUERY TIME".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD QUERY";
cuurentTime = cuurentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(cuurentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
public class TimeClient {
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeClient().connect(port, "127.0.0.1");
}
}
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private int counter;
private byte[] req;
public TimeClientHandler() {
req = ("QUERY TIME" + System.getProperty("line.separator"))
.getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Now is " + body + "; the counter is :" + ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("Exception ");
cause.printStackTrace();
ctx.close();
}
}
分隔符和定长解码器的应用
上面的LineBasedFrameDecoder是利用换行符进行分割消息,同时netty也给我们提供了另外两种解码器:
以特定的分隔符分割
定长分割
DelimiterBasedFrameDecoder应用开发
这里只截取部分,剩余部分参考之前的代码
//EchoServer.java
try {
//LoggingHandler会打印netty的日志
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).
childHandler(new ChildChannelHandler());
ChannelFuture f = bootstrap.bind(port).sync();
f.channel().closeFuture().sync();
}finally {
//优雅退出
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//以$_为分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
}
//EchoServerHandler
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处消息已经解码
String body = (String) msg;
System.out.println("Server received times: "+ ++counter);
body = body + "$_";
ByteBuf resp = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(resp);
}
//EchoClient
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//以$_为分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
//EchoClientHandler
private int counter;
//需要加上分割符
static final String REQ = "Hi,nice to meet you.$_";
public EchoClientHandler() {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
ByteBuf byteBuf = Unpooled.copiedBuffer(REQ.getBytes());
ctx.writeAndFlush(byteBuf);
}
}
FixedLengthFrameDecoder固定长度的消息解析
只需要简单的修改为:
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(20));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
}
此时如果发送数据,底层会截取20个字节的数据作为一个消息
编解码技术
Java序列化技术
Java序列化技术的目的主要有两个
网络传输
对象持久化
缺点:
无法跨语言
序列化后的码流太大
MessagePack编解码
MessagePack的特点:
编解码高效
码流小
支持跨语言
下面是采用MessagePack辩解码的实例,先需要定义Bean类,注意的是需要在类上添加注解@Message:
@Message
public class UserInfo {
private int age;
private String name;
//其他方法省略
}
编解码的类:
public class MsgpackDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
MessagePack messagePack = new MessagePack();
byte[] array = new byte[msg.readableBytes()];
msg.getBytes(msg.readerIndex(), array, 0, array.length);
out.add(messagePack.read(array));
}
}
public class MsgpackEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
MessagePack messagePack = new MessagePack();
byte[] raw = messagePack.write(msg);
out.writeBytes(raw);
}
}
服务器端代码:这里已经考虑了粘包问题
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//解决粘包问题
/*相关参数的解释:
maxFrameLength: 结构的最大长度
lengthFieldOffset:长度属性的位置
lengthFieldLength:长度属性的长度
lengthAdjustment:长度属性的补偿(填充)值
initialBytesToStrip:解码时需要提取的字节数*/
socketChannel.pipeline().addLast("frameDecoder",
new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
socketChannel.pipeline().addLast("msgpack decoder", new MsgpackDecoder());
//在ByteBuf消息之前增加2个字节的消息长度字段
socketChannel.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
socketChannel.pipeline().addLast("msgpack encoder", new MsgpackEncoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
}
//handler代码
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处消息已经解码
System.out.println("Server received : " + msg);
ctx.write(msg);
}
客户端代码:
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline().addLast("frameDecoder",
new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
socketChannel.pipeline().addLast("msgpack decoder", new MsgpackDecoder());
socketChannel.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
socketChannel.pipeline().addLast("msgpack encoder", new MsgpackEncoder());
socketChannel.pipeline().addLast(new EchoClientHandler(100));
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
//handler代码
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
UserInfo[] infos = getUserInfos();
for (UserInfo info : infos) {
ctx.write(info);
}
ctx.flush();
System.out.println("Client have sent msgs");
}
private UserInfo[] getUserInfos() {
UserInfo[] userInfos = new UserInfo[sendNumber];
UserInfo userInfo;
for (int i = 0; i < sendNumber; i++) {
userInfo = new UserInfo();
userInfo.setAge(i);
userInfo.setName("ABCDEFG--->" + i);
userInfos[i] = userInfo;
}
return userInfos;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Client received msg: " + msg);
}
Protobuf编解码
Protobuf的最大优点是编码后消息很小,且编解码的性能非常高。和MessagePack不同的是,Protobuf的类使用的不是原生的bean,需要自己定义.proto文件然后通过命令生成相关的类。这里采用一个商品订购的程序来说明其使用
服务端的代码:
注意下面采用ProtobufVarint32FrameDecoder进行分包,也可以换成前面用过的LengthFieldBasedFrameDecoder或者继承ByteToMessageDecoder类,自己处理半包消息
public class SubReqServer {
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, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//分包
ch.pipeline().addLast(
new ProtobufVarint32FrameDecoder());
//获取解码器,解码和指定类有关
ch.pipeline().addLast(
new ProtobufDecoder(
SubscribeReqProto.SubscribeReq
.getDefaultInstance()));
ch.pipeline().addLast(
new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new SubReqServerHandler());
}
});
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new SubReqServer().bind(port);
}
}
public class SubReqServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
SubscribeReqProto.SubscribeReq req = (SubscribeReqProto.SubscribeReq) msg;
if ("Lilinfeng".equalsIgnoreCase(req.getUserName())) {
System.out.println("Service accept client subscribe req : ["
+ req.toString() + "]");
ctx.writeAndFlush(resp(req.getSubReqID()));
}
}
private SubscribeRespProto.SubscribeResp resp(int subReqID) {
SubscribeRespProto.SubscribeResp.Builder builder = SubscribeRespProto.SubscribeResp
.newBuilder();
builder.setSubReqID(subReqID);
builder.setRespCode(0);
builder.setDesc("Netty book order succeed, 3 days later, sent to the designated address");
return builder.build();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();// 发生异常,关闭链路
}
}
客户端代码:
public class SubReqClient {
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
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 ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(
new ProtobufDecoder(
SubscribeRespProto.SubscribeResp
.getDefaultInstance()));
ch.pipeline().addLast(
new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new SubReqClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 当代客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new SubReqClient().connect(port, "127.0.0.1");
}
}
public class SubReqClientHandler extends ChannelInboundHandlerAdapter {
/**
* Creates a client-side handler.
*/
public SubReqClientHandler() {
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
ctx.write(subReq(i));
}
ctx.flush();
}
private SubscribeReqProto.SubscribeReq subReq(int i) {
SubscribeReqProto.SubscribeReq.Builder builder = SubscribeReqProto.SubscribeReq
.newBuilder();
builder.setSubReqID(i);
builder.setUserName("Lilinfeng");
builder.setProductName("Netty Book For Protobuf");
List<String> address = new ArrayList<>();
address.add("NanJing YuHuaTai");
address.add("BeiJing LiuLiChang");
address.add("ShenZhen HongShuLin");
builder.addAllAddress(address);
return builder.build();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Receive server response : [" + msg + "]");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Jboss Marshalling编解码
Marshalling能够兼容JDK的序列化接口,下面是其案例(本人未跑通):
首先定义编码器和解码器,这里通过一个工厂来获取:
public final class MarshallingCodeCFactory {
/**
* 创建Jboss Marshalling解码器MarshallingDecoder
*
* @return
*/
public static MarshallingDecoder buildMarshallingDecoder() {
//serial表示创建的是Java序列化工厂
final MarshallerFactory marshallerFactory = Marshalling
.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(
marshallerFactory, configuration);
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024);
return decoder;
}
/**
* 创建Jboss Marshalling编码器MarshallingEncoder
*
* @return
*/
public static MarshallingEncoder buildMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling
.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
MarshallerProvider provider = new DefaultMarshallerProvider(
marshallerFactory, configuration);
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}
主要的服务端和客户端代码,需要注意的是Marshalling编解码器支持半包和粘包处理
//服务端
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, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(
MarshallingCodeCFactory
.buildMarshallingDecoder());
ch.pipeline().addLast(
MarshallingCodeCFactory
.buildMarshallingEncoder());
ch.pipeline().addLast(new SubReqServerHandler());
}
});
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
//handler
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
ctx.write(subReq(i));
}
ctx.flush();
}
private SubscribeReq subReq(int i) {
SubscribeReq req = new SubscribeReq();
req.setAddress("NanJing YuHuaTai");
req.setPhoneNumber("138xxxxxxxxx");
req.setProductName("Netty Book For Marshalling");
req.setSubReqID(i);
req.setUserName("Lilinfeng");
return req;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Receive server response : [" + msg + "]");
}
//客户端
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
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(
MarshallingCodeCFactory
.buildMarshallingDecoder());
ch.pipeline().addLast(
MarshallingCodeCFactory
.buildMarshallingEncoder());
ch.pipeline().addLast(new SubReqClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 当代客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
//handler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
SubscribeReq req = (SubscribeReq) msg;
if ("Lilinfeng".equalsIgnoreCase(req.getUserName())) {
System.out.println("Service accept client subscrib req : ["
+ req.toString() + "]");
ctx.writeAndFlush(resp(req.getSubReqID()));
}
}
private SubscribeResp resp(int subReqID) {
SubscribeResp resp = new SubscribeResp();
resp.setSubReqID(subReqID);
resp.setRespCode(0);
resp.setDesc("Netty book order succeed, 3 days later, sent to the designated address");
return resp;
}
多协议开发和应用
Http协议
文件服务器
下面是一个用http协议实现文件服务器的案例:
public class HttpFileServer {
private static final String DEFAULT_URL = "/src/com/phei/netty/";
public void run(final int port, final String url) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch)
throws Exception {
// 请求消息解码器
ch.pipeline().addLast("http-decoder",
new HttpRequestDecoder());
// 目的是将多个消息转换为单一的request或者response对象
ch.pipeline().addLast("http-aggregator",
new HttpObjectAggregator(65536));
//响应解码器
ch.pipeline().addLast("http-encoder",
new HttpResponseEncoder());
////目的是支持异步大文件传输
ch.pipeline().addLast("http-chunked",
new ChunkedWriteHandler());
// 业务逻辑
ch.pipeline().addLast("fileServerHandler",
new HttpFileServerHandler(url));
}
});
ChannelFuture future = b.bind("192.168.1.102", port).sync();
System.out.println("HTTP文件目录服务器启动,网址是 : " + "http://192.168.1.102:"
+ port + url);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
String url = DEFAULT_URL;
if (args.length > 1)
url = args[1];
new HttpFileServer().run(port, url);
}
}
注意代码中的ChannelProgressiveFutureListener和ChannelFutureListener的使用
public class HttpFileServerHandler extends
SimpleChannelInboundHandler<FullHttpRequest> {
private final String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
public void channelRead0(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
//判断解码是否失败
if (!request.decoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
//只允许GET
if (request.method() != GET) {
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
final String uri = request.uri();
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {
if (uri.endsWith("/")) {
sendListing(ctx, file);
} else {
sendRedirect(ctx, uri + '/');
}
return;
}
if (!file.isFile()) {
sendError(ctx, FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
} catch (FileNotFoundException fnfe) {
sendError(ctx, NOT_FOUND);
return;
}
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
HttpUtil.setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (HttpUtil.isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,
fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future,
long progress, long total) {
if (total < 0) { // total unknown
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + " / "
+ total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = ctx
.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!HttpUtil.isKeepAlive(request)) {
//关闭future
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, INTERNAL_SERVER_ERROR);
}
}
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String sanitizeUri(String uri) {
try {
//解析成URI
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException e1) {
throw new Error();
}
}
//判断是否是合法的URL
if (!uri.startsWith(url)) {
return null;
}
if (!uri.startsWith("/")) {
return null;
}
//转换成文件路径
uri = uri.replace('/', File.separatorChar);
if (uri.contains(File.separator + '.')
|| uri.contains('.' + File.separator) || uri.startsWith(".")
|| uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
return null;
}
//工程目录+URI构造绝对路径返回
return System.getProperty("user.dir") + File.separator + uri;
}
private static final Pattern ALLOWED_FILE_NAME = Pattern
.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
//如果是目录列出所有的文件
private static void sendListing(ChannelHandlerContext ctx, File dir) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append(" 目录:");
buf.append("</title></head><body>\r\n");
buf.append("<h3>");
buf.append(dirPath).append(" 目录:");
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
for (File f : dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
buf.append("<li>链接:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx,
HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
status, Unpooled.copiedBuffer("Failure: " + status.toString()
+ "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,
mimeTypesMap.getContentType(file.getPath()));
}
}
Http+xml的订购案例
一个订购系统的案例,代码较多,参考github代码,原理本身都一样,关键在于xml的编解码和业务逻辑处理:
https://github.com/guanhang89/nio/tree/master/src/main/java/com/guanhang/nettyguide/protocol/xml
WebSocket协议开发
Http协议的弊端:
半双工,不能双方同时传输
HTTP消息冗长
容易被黑客攻击
WebSocket的特点:
单一的TCP连接,采用全双工模式通信,双方可以同时发消息
对代理、防火墙和路由器透明
无头部信息、Cookie和身份验证
无安全开销
通过"ping/pong"帧保持链路激活
服务器可以主动传递消息给客户端,不需要客户端轮询
WebSocket的连接建立:
建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求的附加头中携带“Upgrade: websocket”信息,表示要建立websocket信息
基于netty的WebSocket协议的开发
本案例中客户端HTML通过内嵌的js脚本创建WebSocket连接,如果握手成功,服务端返回成功的消息
服务端代码:
可以看到,Server的代码和上线http server的代码非常类似,仅缺少了HttpResponseEncoder。对Websocet协议的处理在handler类中
public class WebSocketServer {
public void run(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch)
throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http-codec",
new HttpServerCodec());
pipeline.addLast("aggregator",
new HttpObjectAggregator(65536));
//用来发送HTML文件
ch.pipeline().addLast("http-chunked",
new ChunkedWriteHandler());
pipeline.addLast("handler",
new WebSocketServerHandler());
}
});
Channel ch = b.bind(port).sync().channel();
System.out.println("Web socket server started at port " + port
+ '.');
System.out
.println("Open your browser and navigate to http://localhost:"
+ port + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
new WebSocketServer().run(port);
}
}
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = Logger
.getLogger(WebSocketServerHandler.class.getName());
private WebSocketServerHandshaker handshaker;
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 传统的HTTP接入
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
}
// WebSocket接入
else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handleHttpRequest(ChannelHandlerContext ctx,
FullHttpRequest req) throws Exception {
// 如果HTTP解码失败,返回HHTP异常
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
BAD_REQUEST));
return;
}
// 构造握手响应返回,本机测试
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://localhost:8080/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
private void handleWebSocketFrame(ChannelHandlerContext ctx,
WebSocketFrame frame) {
// 判断是否是关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(),
(CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程仅支持文本消息,不支持二进制消息
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(String.format(
"%s frame types not supported", frame.getClass().getName()));
}
// 返回应答消息
String request = ((TextWebSocketFrame) frame).text();
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("%s received %s", ctx.channel(), request));
}
ctx.channel().write(
new TextWebSocketFrame(request
+ " , 欢迎使用Netty WebSocket服务,现在时刻:"
+ new java.util.Date().toString()));
}
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req, FullHttpResponse res) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
私有协议栈开发
相关细节
私有协议栈能够用于跨进程、跨主机数据交换的非标准协议。下面是私有协议栈的案例,它支持:
使用netty进行服务端客户端通信
提供消息的编解码
提供基于IP地址的白名单机制
链路有效性校验:ping/pong
链路的断连重连
消息的定义:
消息包含:消息头和消息体
netty的消息头定义:
crcCode:netty消息的校验码,分为三部分:第一部分是一个固定值0xABEF,表示是netty协议消息,第二部分是版本号,第三部分是次版本号
length:整个消息的长度
sessionID:集群节点内全局唯一,由会话ID生成器生成
type:表示请求和响应的类别
priority:消息优先级
attachment:可选字段,用于扩展消息头,是一个map
注意:消息的body通过JBoss Marshalling将其序列化为byte数组
可靠性的设计:
心跳机制
重连机制
重复登录保护
消息缓存重发
具体代码
详见:私有协议开发
服务端创建
服务端创建步骤解释
-
ServerBootstrap
ServerBootstrap是服务端的启动辅助类,提供了一系列的方式设置启动相关的参数
-
EventLoopGroup
EventLoopGroup是一个接口,NioEventLoopGroup是它的底层的一个实现类,EventLoopGroup实际是Reactor模式中的线程池,因此它实现了ScheduledExecutorService接口。在NioEventLoopGroup的父类MultithreadEventExecutorGroup(也是EventLoopGroup的实现类)中,有代码:
private final EventExecutor[] children;
EventLoop是EventExecutor的子接口
EventExecutor的父接口是EventExecutorGroup,又继承了ScheduledExecutorService,因此也可以理解为线程池
综合来看,EventLoopGroup实际就是EventLoop的数组,它的主要功能:
处理注册到本线程多路复用器Selector上的Channel,Selector的轮询操作由绑定的EventLoop线程方法驱动。注意的是EventLoop的职责不仅仅是处理网络IO事件,用户自定的Task和定时任务Task也统一由EventLoop负责处理
-
绑定服务端Channel。服务端使用的是NioServerSocketChannel,注意我们创建的时候传入的是class,代码里面是使用反射生成class类
-
链路建立的时候创建并初始化ChannelPipeline,网络事件以流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行
-
初始化ChannelPipeline后,添加并设置ChannelHandler
-
绑定并监听端口
-
挡轮询到就绪的Channel后,就由Reactor线程NioEventLoop执行ChannelPipeline的方法,最终调度并执行ChannelHandler
相关细节
-
服务端的ServerBootstrap一般传两个参数,第一个参数是父线程组,第二个参数是子线程组
-
backlog参数的含义
对于给定的监听套接字,内核要维护两个队列:未连接队列和已连接队列,backlog制定了内核为此socket排队的最大连接个数,这个个数是这两个队列之和
-
TCP参数设置后,可以为启动辅助类和其父类分别制定handler,子类的handler是NioServerSocket对应的ChannelPipeline的Handler,父类中的Handler是客户端新接入的连接SocketChannel对应的ChannelPipeline的Handler。
-
负责处理网络读写、连接和客户端请求接入的Reactor线程就是NioEventLoop
##客户端创建
客户端的创建与服务端类似,对于客户端来说,使用的是Bootstrap,而且只需要一个处理IO读写的线程组即可。
Netty功能介绍和源码分析
ByteBuf和相关辅助类
ByteBuffer的缺点:
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收敛
只有一个标志位置的指针position,需要手动调用flip
API功能受限,一些高级和实用的特性不支持
而ByteBuf的特点:
缓冲区自身的copy和slice
ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex(简写r),写操作使用writerIndex(简写w),r和w一开始是0,随着读的增加,r会递增,随着写的增加,w会递增,但是r不会超过w。读取之后,0-r就被视为discard,调用discardReadBytes可以释放这部分空间。w-capacity之间的空间是可以写的
ByteBuf可以动态扩展。ByteBuffer为了防止溢出,每进行一次put操作,都需要对可用空间进行校验,这导致代码冗余。ByteBuf对write操作进行了封装,有ByteBuf的write操作负责进行剩余可用空间校验,如果缓冲区不够会自动扩展
使用细节
-
read开头和write开头的方法是都是顺序读写的方法,也就是会操作上面的w,r,skip方法也是参考的r
-
调用discardReadBytes操作可以重用已读的空间,提高内存的使用率,但是会导致内存复制,降低性能下降
-
调用clear操作不会清空缓冲区,它会令r=w=0
-
由于某种原因需要对之前的读写进行回滚,ByteBuf也提供了rest和mark接口,因为有r和w,所以总共有2个mark方法,2个reset方法
-
ByteBuf提供了很多查找byte的方法
-
ByteBuf的复制方法:
duplicate:复制ByteBuf,维护自己的读写索引,但是共享缓冲区内容
copy:复制ByteBuf,不共享内容,
copy(int index, int length):从指定的索引开始复制
slice:返回ByteBuf的可读子缓冲区, 共享内容,维护独立的读写索引
slice(int index, int length):返回带起始位置的可读子缓冲区,同样共享内容
-
当通过NIO的SocketChannel进行网络读写时,操作的对象是JDK标准的ByteBuffer,因此需要支持ByteBuf到ByteBuffer的互相转换
-
ByteBuf的get和set方法主要用来支持随机读和写,需要注意的是,set和write不一样,它不支持动态扩展缓冲区,所以使用者必须保证当前的缓冲区字节数大于需要写入的字节长度
-
ByteBuf的实现类划分:
从内存划分角度:分为HeapByteBuf和DirectByteBuf,前者使用堆内存,由JVM进行垃圾回收,但是需要将数据从缓冲区拷贝到内核Channel中,后者采用堆外内存,回收速度较慢,但是不需要内存复制,速度较快
从内存回收角度来说:基于对象池的ByteBuf和普通ByteBuf
常用的是UnpooledHeapByteBuf
相关源码简析
AbstractByteBuf:
一些公共的操作放在这个类中,需要注意的是缓冲区的实现不在该类中,因为缓冲区有多种实现方式,所以具体实现放在子类中
类中leakDetector变量用来检测是否有内存泄漏
ByteBuf的扩容策略:开始是double,当申请的数量大于4M是,采用4 M递增
Netty的ByteBuffer和JDK的ByteBuffer一个不同点是前者可以动态扩展
AbstractReferenceCountedByteBuf:
AbstractByteBuf的子类,主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,做自动内存回收,每调用一次retain计数器就会加一
UnpooledHeapByteBuf:
基于堆内存分配的字节缓冲区,它聚合了一个ByteBufAllocator,用于UnpooledHeapByteBuf的内存分配
PooledByteBuf:
主要是内存池的实现:
PoolArena:Arena本身是指一块连续区域,Netty的PoolArena由多个Chunk组成,每个Chunk由多个Page组成,并形成一个多层的二叉树,最底层是一个page,使用的时候采用深度优先的遍历,选择在合适的层进行分配。
无论是Chunk和Page,都通过状态位来标识内存是否可用
辅助类:
-
ByteBufHolder
是ByteBuf的容器,它包含了一个ByteBuf,另外还提供其他实用的方法,使用者继承ByteBufHolder接口后便可以按需封装自己的实现
-
ByteBufAllocator
字节缓冲分配器,有两种:基于内存池的字节缓冲区分配器和普通的字节缓冲区分配器
-
CompositeByteBuf
它允许将多个ByteBuf的实例组装到一起,形成一个统一的试图,类似于数据库将多个表字段组装到一起统一用视图表示
-
ByteBufUtil
非常有用的工具类,用于操作ByteBuf对象,比如提供了对字符串编码和解码的功能
Channel和Unsafe
###功能说明
Channel是netty网络操作的抽象类,它聚合了一组功能,想读、写、客户端发起的连接、链路关闭等,同时也能获取到
为什么netty会选择实现自己的Channel:
JDK的channel没有同意的Channel接口提供业务开发者使用
JDK的channel主要职责就是网络IO操作,由于它们是SPI类接口,由具体的虚拟机厂家提供,所以通过集成SPI功能类来扩展其功能难度很大
netty的channel需要提供netty本身特有的功能
设计理念:
在Channel接口层,采用Facade模式进行统一的封装
Channel的接口定义大而全
具体实现采用聚合而非包含的方式,功能实现更灵活
Channel的几个重要的API功能:
一个比较重要的方法是eventLoop方法,通过该方法可以获取到Channel注册的EventLoop,EventLoop本质上就是处理网络读写事件的Reactor线程,也可以用来处理自定的任务
第二个是metadata(),它表示TCP的参数
第三个方法是parent(),对于ServiceSocketChannel而言,它的父channel为空,对于SocketChannel而言,父channel是ServiceSocketChannel
第四个方法是id(),表示channel的id
源码简析
AbstractChannel:
Channel在进行IO操作时,会产生对应的事件,然后驱动事件在ChannelPipeLine中传播,由对应的ChannelHandler对事件拦截和处理,不关心的事件可以直接忽略
网络的IO操作,包括connect、disconnect、close、flush等方法都是通过efaultChannelPipeline调用的
AbstractNioChannel源码分析:
定义了SelectableChannel
定义了readInterestOp,对应JDK的OP_READ
定义了SelectionKey,是Channel注册到EventLoop后返回的选择键
定义了ChannelPromise,代表连接操作结果
下面是register的代码:
用selected标识是否操作成功,注意这里注册的事件是0,标识对任何事件都不感兴趣 ,仅仅完成注册操作,后期在执行指定时间时可以再次注册感兴趣的事件
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
//该key已经被取消
if (!selected) {
// Force the Selector to select now as the "canceled" SelectionKey may still be
// cached and not removed because no Select.select(..) operation was called yet.
//调用该方法将取消的key从selector中删除掉
eventLoop().selectNow();
selected = true;
} else {
// We forced a select operation on the selector before but the SelectionKey is still cached
// for whatever reason. JDK bug ?
//还是出错就抛出异常
throw e;
}
}
}
}
AbstractNioByteChannel:
最主要的方法就是doWrite方法,从发送消息环形数组ChannelOutboundBuffer弹出一条消息,判断该消息是否为空,如果为空表示消息发送完毕,清除半包标志
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = config().getWriteSpinCount();
do {
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
//清除写标志
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
}
//处理发送的消息
writeSpinCount -= doWriteInternal(in, msg);
} while (writeSpinCount > 0);
//发送之前还没有发送完的半包
incompleteWrite(writeSpinCount < 0);
}
AbstractNioMessageChannel:
主要的方法也是doWrite,只不过上面发送的的是ByteBuf,而对于AbstractNioMessageChannel发送的是POJO
AbstractNioMessageServerChannel:
定义了一个EventLoopGroup,用于给新接入的客户端分配EventLoop
NioServerSocketChannel:
定义了一个ChannelMetadata,以及ServerSocketChannelConfig(用于配置ServerSocketChannel的TCP的参数)
对于NioServerSocketChannel,它的读取操作就是接收客户端的连接,创建NioSocketChannel对象
NioSocketChannel:
主要有连接以及读写操作,需要关注半包的处理方式
Unsafe
内部调用接口,设计底层实现
AbstractUnsafe:
register方法主要用于当前Unsafe对应的Channel注册到EventLoop的多路复用器上
bind用于绑定指定的端口,对于服务端,用于绑定监听端口;对于客户端,主要用于指定客户端Channel的本地绑定Socket地址
disconnect方法:用于客户端或者服务端主动关闭连接
close:关闭链路
write:将消息添加到环形发送数组中
AbstractNioUnsafe:
AbstractUnsafe的子类,主要实现了connect和finishConnect方法
ChannelPipeline和ChannelHandler
功能说明
Netty的ChannelPipeline和ChannelHandler机制类似于Servlet和Filter过滤器,前者将Channel的数据冠道抽象为ChannelPipeline,消息在该pipeline中流动和传递,内部维护了ChannelHandler的链表,由这些handler对IO事件进行拦截和处理
ChannelPipeline的事件处理
底层的SocketChannel read方法会触发ChannelRead事件,有NioEventLoop调用ChannelPIpeline的fireChannelRead方法,就像消息传送到ChannelPipeline
消息被多个handler依次处理
调用ChannelHandlerContext的write方法,同样会依次经过handler,不过和上面的顺序相反
netty中的事件分为inbound和outbound事件,前者通常由IO线程触发,例如TCP连接的建立、链路关闭等,后者是由用户发起的网络IO事件,例如用户发起的连接操作、绑定操作、消息发送等操作
自定拦截器
通过ChannelHandler接口(或子接口)来实现事件的拦截和处理
构建pipeline
不需要用户自己创建,netty会为每个Channel连接创建一个独立的pipeline。handler存在先后顺序,比如在MessageToMessage之前,往往需要一个ByteToMessageDecoder。
ChannelPipeline支持运行动态的添加或者而删除handler
源码简析
ChannelPipeline:
该类是线程安全的,但是handler不是线程安全点的
handler会进行重名校验,校验后会创建DefaultChannelHandlerContext,并添加到pipeline中
pipeline中以fire开头的方法都是从IO线程流向用户业务Handler的inbound事件
由用户线程或者代码发起的IO操作被称为outbound事件
pipeline本身不直接进行IO操作,有Channel和Unsafe进行
ChannelHandler:
ChannelHandler支持注解,目前有两种:
Sharable:表示多个pipeline可以共用handler
Skip:skip的方法不会调用
ChannelHandlerAdapter:
或者说ChannelInboundHandlerAdapter,都是ChannelHandler的实现类,用户只需要继承这些类,然后覆盖自己感兴趣的方法,不用直接实现ChannelHandler接口
ByteToMessageDecoder:
提供将ByteBuf解码成POJO对象的能力
MessageToMessageDecoder:
实际上这个Decoder是二次解码器,因为从Channel读取到的数据是在ByteBuffer中,因此一般先转为Message,再能转为其他消息
LengthFieldBasedFrameDecoder:
通过长度进行消息区分,可以组合不同的参数对消息进行解码,这几个参数的说明:
lengthFieldOffset:长度字段的偏移,消息头有可能在消息的中间
lengthFieldLength:长度字段的长度
lengthAjustment:有时候长度还包含了其他消息头的长度,因此会做修正,表示向前向后推多少个字节
initialBytesToStrip:跳过忽略的字节
LengthFieldPrepender:
在待发送的消息前添加该消息的长度,占用两个字节
EventLoop和EventLoopGroup
Reactor的线程模型:
单线程模型:一个线程处理所有的IO操作
Reactor多线程模式:有一个专门的Acceptor线程,读写由另一个IO线程池负责
主从多线程模型:Acceptor也采用一个独立的线程池
netty的线程模型:
通过设置不同的参数可以达到上面的三种线程模型 ,我们之前的案例中在服务器端程序需要传两个线程池,它们的作用:
用于接收请求的线程池:接收TCP连接,初始化Channel,将链路状态通知给ChannelPipeline
用于处理IO操作的线程池:异步读取通信对端的数据报,发送读事件到ChannelPipeline;异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;执行系统调用Task;执行定时任务Task,例如空闲状态监测定时任务
为了尽可能的提升性能,netty在很多地方进行了无锁化的设计:netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead,只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换
最佳实践:
创建两个NioEventLoop
尽量不要再ChannelHandler中启动用户线程
解码要放在NIO线程调用的解码Handler中进行,不要在用户线程中执行
如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池由业务线程执行,以保证NIO线程尽快释放,处理其他的IO操作
源码简析
NioEventLoop除了负责IO的读写之外,还要处理以下两类任务:
处理系统的Task
定时任务
NioEventLoop内部维护了一个selector变量,用来select,netty对selectedKeys进行了优化,可以设置为打开,默认为不打开;由于JDK的select有bug,因此netty会对空的select计数,超过一定次数,重新创建Selector
Future和Promise
Future:
Future就是future模式中的Future类,用来获取异步操作的结果,在netty中,因为操作的都是Channel,因此命名为ChannelFuture,它有两种状态:uncompleted,completed,以及是三种可能:成功,失败,取消。ChannelFuture提供了API用来添加事件监听器,并建议通过监听器的方式获取结果并进行相关的操作。
需要注意的是,不要在ChannelHandler中调用ChannelFuture的await方法,会导致死锁
Promise:
是可写的future,future本身没有写操作的相关接口,通过promise可以对future扩展,用于设置IO操作的结果
其他
架构质量属性
高性能
可靠性:链路有效性检测,内存保护机制,优雅停机
可定制型
可扩展性
多线程
Java内存模型:
工作内存和主内存
Java的线程:
java采用单进程多线程的实现,目前线程实现的三种方式:
内核线程实现:由内核完成线程切换,内核通过线程调度器对线程进行调度,并负责将线程任务映射到不同的处理器上
用户线程实现:建立在用户空间线程库上的线程,不需要内核的帮助,执行性能较高
混合实现
volatile的作用:
线程可见
禁止重排序:避免编译上的优化改变实际逻辑