一、Java传统IO与NIO
1. Java 传统 socket IO
对于传统的Java socket编程,即使使用了线程池,一个客户端请求对应一个线程处理。
Java socket服务端代码如下:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
/**
* 传统socket服务端
*/
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 创建socket服务,监听10101端口
ServerSocket server = new ServerSocket(10101);
System.out.println("服务器启动!");
while (true) {
// 获取一个套接字(阻塞)
final Socket socket = server.accept();
System.out.println("来个一个新客户端!");
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
// 业务处理
handler(socket);
}
});
}
}
/**
* 读取数据
*
* @param socket
* @throws Exception
*/
public static void handler(Socket socket) {
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true) {
// 读取数据(阻塞)
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
System.out.println("socket关闭");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
当上面的程序启动时,有2个地方会阻塞:
1)final Socket socket = server.accept(); // 此处等待客户端建立连接。
2)int read = inputStream.read(bytes); // 与客户端建立连接完成后,等待接收客户端请求数据。
测试上述代码:
1)当服务端程序启动是,会阻塞在第一点accept上。
2)启动个控制台,并执行:
telnet localhost 10101
此时会建立连接,执行过accept, 并阻塞在第二点read上。
然后输入数据
111
此时会通过第二点read, 服务端收到客户端的请求数据111。
3)假设在上述控制台建立连接后,未输入111请求前,启动另一个控制台,然后执行telnet localhost 10101, 服务端仍可收到请求。不过,服务端需要另外启动个线程来处理这个新控制台的请求。
综上,上述Java传统socket的编程模型,一个线程只能处理一个请求,如果把服务端比喻成餐厅,处理请求的线程比喻成服务员,客户端的请求比喻成吃饭的客人,那么饭店中每一个客人,都需要一个服务员来接待,这样当客人很多时,需要很多服务员,这样饭店会赔钱。
所以,java传统的socket,是不能用来做长连接服务器的,但可以用来做短连接服务器,如旧版本的Tomcat,对应的场景是一问一答快速响应的场景。
二、Java NIO
1. Java NIO的demo:
package netty_test.bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO服务端
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
*
* @param port 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
handler(key);
}
}
}
/**
* 处理请求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客户端请求连接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 获得了可读的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 处理连接请求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息哦
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 处理读的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
}
/**
* 启动服务端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
当客户端建立连接前或者建立完连接发消息前,会阻塞在
selector.select();
当有连接请求时或建立完连接发消息时,会执行过这行,
如果是建立连接的请求,则把channel注册到selector:
channel.register(this.selector, SelectionKey.OP_READ);
如果是发消息请求,则读取消息内容:
int read = channel.read(buffer);
2. Java NIO的API
ServerSocketChannel :对应传统Socket编程中的Server Socket
SocketChannel :对应传统Socket编程中的Socket
Selector :Java NIO的核心,用于监听ServerSocketChannel和SocketChannel
SelectionKey : 每个客户端请求的唯一标识。
NIO可实现单线程为多个客户端服务。
3. 传统socket IO图与NIO图
传统socket IO:
对于传统socket IO, 大门是ServerSocket, 一个线程(服务员)对应一个请求(客人)。
NIO:
NIO模型的大门是ServerSocketChannel, 其服务员是线程+selector, 且不只服务于一个请求,且在服务于当前客人的同时,还在监听新来的客人。
注意:selector.select(); 这个方法底层是用C语言实现的,是基于操作系统的。
NIO是一个基于select的多路复用的IO(具体参见Linux操作系统)
二、Netty入门
netty 使用的版本:netty 3
1. netty的应用领域
领域1. 分布式进程间的通信
例如:Hadoop、Dubbo、akka等具有分布式功能的框架,底层RPC通信都是基于netty实现的,这些框架使用的netty版本通常还在3.x
领域2. 游戏服务器开发
最新的游戏服务器有部分公司可能会采用netty 4.x或netty 5.x
2. netty服务端hello world例子
1)Server.java
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
/**
* netty服务端入门
*/
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
2)HelloHandler.java
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
/**
* 消息接受处理类
*/
public class HelloHandler extends SimpleChannelHandler {
/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
String s = (String) e.getMessage();
System.out.println(s);
//回写数据
ctx.getChannel().write("hi");
super.messageReceived(ctx, e);
}
/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}
/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}
/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}
/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}
这样在启动Server.java后,
1)用telnet localhost 10101时,会触发HelloHandler.java的channelConnected方法;
2)当客户端发送消息后,会触发messageReceived方法;
3)当客户端断开连接时,会先后触发channelDisconnected方法和channelClosed方法。
boss和worker
boss和worker都分配一个线程,对应一个selector。
boss的selector负责监听端口;
worker的selector负责channel的读写任务。
3. netty客户端HelloWorld例子
Client.java:
package com.client;
import java.net.InetSocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
/**
* netty客户端入门
*/
public class Client {
public static void main(String[] args) {
//服务类
ClientBootstrap bootstrap = new ClientBootstrap();
//线程池
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//socket工厂
bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
//管道工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("hiHandler", new HiHandler());
return pipeline;
}
});
//连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10101));
Channel channel = connect.getChannel();
System.out.println("client start");
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请输入");
channel.write(scanner.next());
}
}
}
HiHandler.java:
package com.client;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
/**
* 消息接受处理类
*/
public class HiHandler extends SimpleChannelHandler {
/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
String s = (String) e.getMessage();
System.out.println(s);
super.messageReceived(ctx, e);
}
/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}
/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}
/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}
/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}
总结:可以看到,客户端和服务端的代码非常类似:
1)主要处理消息的类是Handler
2)消息通过Channel传递
3)channelDisconnected与channelClosed的区别:
如果服务端没有启动,然后启动客户端,这时只有channelClosed,而没有channelDisconnected。
三、netty工作原理源码分析
1. 如何提高NIO的工作效率(即netty怎样优化的NIO)
传统NIO相当于一个餐厅只有一个服务员,当餐厅的规模越来越大,一个服务员不够,需要多招服务员(selector),这样就相当于从单线程(selector)改成多线程(多selector)。
1)思考问题:
1)一个NIO系统是不是只能有一个selector呢?
答:不是。一个NIO系统可以有多个selector。
2)selector是不是只能注册一个ServerSocketChannel?
答:不是。可以注册多个。
2)传统NIO的线程模型:
即一个餐厅只有一个服务员。
3)netty工作的线程模型:
每个服务员(selector)相当于一个线程。
2. netty核心源码解析
1)boss和worker的区别?
boss负责监听端口
worker为客户端服务,让客户端执行读写任务。
2)netty核心代码解析
- boss和worker都是由"线程+selector"的方式来处理用户连接或发消息,所以他们都可以继承自相同的父类AbstractNioSelector(继承自Runnable, 本身是一个线程)
AbstractNioSelector源码:
package com.cn;
import java.io.IOException;
import java.nio.channels.Selector;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import com.cn.pool.NioSelectorRunnablePool;
/**
* 抽象selector线程类
*/
public abstract class AbstractNioSelector implements Runnable {
/**
* 线程池
*/
private final Executor executor;
/**
* 选择器
*/
protected Selector selector;
/**
* 选择器wakenUp状态标记
*/
protected final AtomicBoolean wakenUp = new AtomicBoolean();
/**
* 任务队列
*/
private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<Runnable>();
/**
* 线程名称
*/
private String threadName;
/**
* 线程管理对象
*/
protected NioSelectorRunnablePool selectorRunnablePool;
AbstractNioSelector(Executor executor, String threadName, NioSelectorRunnablePool selectorRunnablePool) {
this.executor = executor;
this.threadName = threadName;
this.selectorRunnablePool = selectorRunnablePool;
openSelector();
}
/**
* 获取selector并启动线程
*/
private void openSelector() {
try {
this.selector = Selector.open();
} catch (IOException e) {
throw new RuntimeException("Failed to create a selector.");
}
executor.execute(this);
}
@Override
public void run() {
Thread.currentThread().setName(this.threadName);
while (true) {
try {
wakenUp.set(false);
select(selector);
processTaskQueue();
process(selector);
} catch (Exception e) {
// ignore
}
}
}
/**
* 注册一个任务并激活selector
*
* @param task
*/
protected final void registerTask(Runnable task) {
taskQueue.add(task);
Selector selector = this.selector;
if (selector != null) {
if (wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
} else {
taskQueue.remove(task);
}
}
/**
* 执行队列里的任务
*/
private void processTaskQueue() {
for (;;) {
final Runnable task = taskQueue.poll();
if (task == null) {
break;
}
task.run();
}
}
/**
* 获取线程管理对象
* @return
*/
public NioSelectorRunnablePool getSelectorRunnablePool() {
return selectorRunnablePool;
}
/**
* select抽象方法
*
* @param selector
* @return
* @throws IOException
*/
protected abstract int select(Selector selector) throws IOException;
/**
* selector的业务处理
*
* @param selector
* @throws IOException
*/
protected abstract void process(Selector selector) throws IOException;
}
该AbstractNioSelector类包括的成员变量:
- 线程池:Executor executor
- boss和worker都有selector: Selector selector
- 标记selector是否被激活:AtomicBoolean wakenUp
- 任务队列:Queue taskQueue
- 线程名:String threadName
- 线程管理对象:NioSelectorRunnablePool selectorRunnablePool
包含的方法有: - 获取selector并启动线程:openSelector()
- 线程的run方法(分别有boss和worker自己实现)
- 注册一个任务并激活selector:registerTask(Runnable task)
- 执行队列里的任务:processTaskQueue()
- 获取线程管理对象:NioSelectorRunnablePool getSelectorRunnablePool()
- select抽象方法:select(Selector selector)
- selector的业务处理:process(Selector selector)
- boss实现类(继承自AbstractNioSelector)
package com.cn;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Executor;
import com.cn.pool.Boss;
import com.cn.pool.NioSelectorRunnablePool;
import com.cn.pool.Worker;
/**
* boss实现类
*/
public class NioServerBoss extends AbstractNioSelector implements Boss{
public NioServerBoss(Executor executor, String threadName, NioSelectorRunnablePool selectorRunnablePool) {
super(executor, threadName, selectorRunnablePool);
}
@Override
protected void process(Selector selector) throws IOException {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (selectedKeys.isEmpty()) {
return;
}
for (Iterator<SelectionKey> i = selectedKeys.iterator(); i.hasNext();) {
SelectionKey key = i.next();
i.remove();
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 新客户端
SocketChannel channel = server.accept();
// 设置为非阻塞
channel.configureBlocking(false);
// 获取一个worker
Worker nextworker = getSelectorRunnablePool().nextWorker();
// 注册新客户端接入任务
nextworker.registerNewChannelTask(channel);
System.out.println("新客户端链接");
}
}
public void registerAcceptChannelTask(final ServerSocketChannel serverChannel){
final Selector selector = this.selector;
registerTask(new Runnable() {
@Override
public void run() {
try {
//注册serverChannel到selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
});
}
@Override
protected int select(Selector selector) throws IOException {
return selector.select();
}
}
该类实现了其父类中的抽象方法:
其中,process方法获取到用户连接请求后,获取一个worker,并把该客户端连接的channel注册到worker。
- worker实现类(继承自AbstractNioSelector)
package com.cn;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Executor;
import com.cn.pool.NioSelectorRunnablePool;
import com.cn.pool.Worker;
/**
* worker实现类
*/
public class NioServerWorker extends AbstractNioSelector implements Worker{
public NioServerWorker(Executor executor, String threadName, NioSelectorRunnablePool selectorRunnablePool) {
super(executor, threadName, selectorRunnablePool);
}
@Override
protected void process(Selector selector) throws IOException {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (selectedKeys.isEmpty()) {
return;
}
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 移除,防止重复处理
ite.remove();
// 得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 数据总长度
int ret = 0;
boolean failure = true;
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
try {
ret = channel.read(buffer);
failure = false;
} catch (Exception e) {
// ignore
}
//判断是否连接已断开
if (ret <= 0 || failure) {
key.cancel();
System.out.println("客户端断开连接");
}else{
System.out.println("收到数据:" + new String(buffer.array()));
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("收到\n".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}
}
}
/**
* 加入一个新的socket客户端
*/
public void registerNewChannelTask(final SocketChannel channel){
final Selector selector = this.selector;
registerTask(new Runnable() {
@Override
public void run() {
try {
//将客户端注册到selector中
channel.register(selector, SelectionKey.OP_READ);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
});
}
@Override
protected int select(Selector selector) throws IOException {
return selector.select(500);
}
}
其业务处理process方法,主要代码都是经典的java NIO代码,用于与客户端读写。
- selector线程管理者类NioSelectorRunnablePool
package com.cn.pool;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import com.cn.NioServerBoss;
import com.cn.NioServerWorker;
/**
* selector线程管理者
*/
public class NioSelectorRunnablePool {
/**
* boss线程数组
*/
private final AtomicInteger bossIndex = new AtomicInteger();
private Boss[] bosses;
/**
* worker线程数组
*/
private final AtomicInteger workerIndex = new AtomicInteger();
private Worker[] workeres;
public NioSelectorRunnablePool(Executor boss, Executor worker) {
initBoss(boss, 1);
initWorker(worker, Runtime.getRuntime().availableProcessors() * 2);
}
/**
* 初始化boss线程
* @param boss
* @param count
*/
private void initBoss(Executor boss, int count) {
this.bosses = new NioServerBoss[count];
for (int i = 0; i < bosses.length; i++) {
bosses[i] = new NioServerBoss(boss, "boss thread " + (i+1), this);
}
}
/**
* 初始化worker线程
* @param worker
* @param count
*/
private void initWorker(Executor worker, int count) {
this.workeres = new NioServerWorker[count];
for (int i = 0; i < workeres.length; i++) {
workeres[i] = new NioServerWorker(worker, "worker thread " + (i+1), this);
}
}
/**
* 获取一个worker
* @return
*/
public Worker nextWorker() {
return workeres[Math.abs(workerIndex.getAndIncrement() % workeres.length)];
}
/**
* 获取一个boss
* @return
*/
public Boss nextBoss() {
return bosses[Math.abs(bossIndex.getAndIncrement() % bosses.length)];
}
}
该类用于管理boss和worker的线程池
- boss接口
import java.nio.channels.ServerSocketChannel;
/**
* boss接口
*/
public interface Boss {
/**
* 加入一个新的ServerSocket
* @param serverChannel
*/
public void registerAcceptChannelTask(ServerSocketChannel serverChannel);
}
- worker接口
import java.nio.channels.SocketChannel;
/**
* worker接口
*/
public interface Worker {
/**
* 加入一个新的客户端会话
* @param channel
*/
public void registerNewChannelTask(SocketChannel channel);
}
- 启动类
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import com.cn.pool.NioSelectorRunnablePool;
/**
* 启动函数
*/
public class Start {
public static void main(String[] args) {
// 初始化线程
NioSelectorRunnablePool nioSelectorRunnablePool = new NioSelectorRunnablePool(Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
// 获取服务类
ServerBootstrap bootstrap = new ServerBootstrap(nioSelectorRunnablePool);
// 绑定端口
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start");
}
}
四、netty心跳
1. IdleStateHandler
IdleStateHandler用于检测Channel会话是否过期。
1)HelloHandler.java
package com.heart;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.timeout.IdleState;
import org.jboss.netty.handler.timeout.IdleStateEvent;
public class HelloHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
System.out.println(e.getMessage());
}
@Override
public void handleUpstream(final ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
if (e instanceof IdleStateEvent) {
if(((IdleStateEvent)e).getState() == IdleState.ALL_IDLE){
System.out.println("提玩家下线");
//关闭会话,踢玩家下线
ChannelFuture write = ctx.getChannel().write("time out, you will close");
write.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
ctx.getChannel().close();
}
});
}
} else {
super.handleUpstream(ctx, e);
}
}
}
在做channel close前,先给客户端发送一条消息,告诉客户端,你超时了,服务端踢你下线了。
2)Server.java:用于加载IdleStateHandler, 启动HelloHandler.
package com.heart;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
import org.jboss.netty.handler.timeout.IdleStateHandler;
import org.jboss.netty.util.HashedWheelTimer;
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
final HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("idle", new IdleStateHandler(hashedWheelTimer, 5, 5, 10));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
对于代码:
IdleStateHandler(hashedWheelTimer, 5, 5, 10))
表示:
hashedWheelTimer:定时器
readerIdleTimeSeconds:读超时时间,单位秒
writerIdleTimeSeconds:写超时时间,单位秒
allIdleTimeSeconds:无论读还是写的超时时间,单位秒。
五、自定义序列化协议
如果要对int进行序列化与反序列化,有下面的方法。
1. 通过传统的java.io包
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
public class Test1 {
public static void main(String[] args) throws IOException {
int id = 101;
int age = 21;
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
arrayOutputStream.write(int2bytes(id));
arrayOutputStream.write(int2bytes(age));
byte[] byteArray = arrayOutputStream.toByteArray();
System.out.println(Arrays.toString(byteArray));
//==============================================================
ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteArray);
byte[] idBytes = new byte[4];
arrayInputStream.read(idBytes);
System.out.println("id:" + bytes2int(idBytes));
byte[] ageBytes = new byte[4];
arrayInputStream.read(ageBytes);
System.out.println("age:" + bytes2int(ageBytes));
}
/**
* 大端字节序列(先写高位,再写低位)
* 百度下 大小端字节序列
* @param i
* @return
*/
public static byte[] int2bytes(int i){
byte[] bytes = new byte[4];
bytes[0] = (byte)(i >> 3*8);
bytes[1] = (byte)(i >> 2*8);
bytes[2] = (byte)(i >> 1*8);
bytes[3] = (byte)(i >> 0*8);
return bytes;
}
/**
* 大端
* @param bytes
* @return
*/
public static int bytes2int(byte[] bytes){
return (bytes[0] << 3*8) |
(bytes[1] << 2*8) |
(bytes[2] << 1*8) |
(bytes[3] << 0*8);
}
}
缺点:自己实现序列化需要用位运算,比较麻烦,如果对于long等其他类型,都需要自己手动写代码实现。
2. 通过Java NIO的API实现
import java.nio.ByteBuffer;
import java.util.Arrays;
public class Test2 {
public static void main(String[] args) {
int id = 101;
int age = 21;
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.putInt(id);
buffer.putInt(age);
byte[] array = buffer.array();
System.out.println(Arrays.toString(buffer.array()));
//====================================================
ByteBuffer buffer2 = ByteBuffer.wrap(array);
System.out.println("id:"+buffer2.getInt());
System.out.println("age:"+buffer2.getInt());
}
}
优点:对于不同的类型,不用自己写序列化与反序列化代码
缺点:必须手动自动固定长度ByteBuffer.allocate(8),若超出长度则报错。
3. 通过netty的ChannelBuffer来实现
import java.util.Arrays;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
public class Test3 {
public static void main(String[] args) {
ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
buffer.writeInt(101);
buffer.writeDouble(80.1);
byte[] bytes = new byte[buffer.writerIndex()];
buffer.readBytes(bytes);
System.out.println(Arrays.toString(bytes));
========================================
ChannelBuffer wrappedBuffer = ChannelBuffers.wrappedBuffer(bytes);
System.out.println(wrappedBuffer.readInt());
System.out.println(wrappedBuffer.readDouble());
}
}
好处:可动态分配空间,且不用自己写序列化反序列化。
所以, ChannelBuffer结合了ByteArrayInputStream和ByteBuffer的优点。
缺点:不能序列化String对象
4. 自定义序列化与反序列化
1)序列化父类 Serializer.java:
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.netty.buffer.ChannelBuffer;
/**
* 自定义序列化接口
*/
public abstract class Serializer {
public static final Charset CHARSET = Charset.forName("UTF-8");
protected ChannelBuffer writeBuffer;
protected ChannelBuffer readBuffer;
/**
* 反序列化具体实现
*/
protected abstract void read();
/**
* 序列化具体实现
*/
protected abstract void write();
/**
* 从byte数组获取数据
* @param bytes 读取的数组
*/
public Serializer readFromBytes(byte[] bytes) {
readBuffer = BufferFactory.getBuffer(bytes);
read();
readBuffer.clear();
return this;
}
/**
* 从buff获取数据
* @param readBuffer
*/
public void readFromBuffer(ChannelBuffer readBuffer) {
this.readBuffer = readBuffer;
read();
}
/**
* 写入本地buff
* @return
*/
public ChannelBuffer writeToLocalBuff(){
writeBuffer = BufferFactory.getBuffer();
write();
return writeBuffer;
}
/**
* 写入目标buff
* @param buffer
* @return
*/
public ChannelBuffer writeToTargetBuff(ChannelBuffer buffer){
writeBuffer = buffer;
write();
return writeBuffer;
}
/**
* 返回buffer数组
*
* @return
*/
public byte[] getBytes() {
writeToLocalBuff();
byte[] bytes = null;
if (writeBuffer.writerIndex() == 0) {
bytes = new byte[0];
} else {
bytes = new byte[writeBuffer.writerIndex()];
writeBuffer.readBytes(bytes);
}
writeBuffer.clear();
return bytes;
}
public byte readByte() {
return readBuffer.readByte();
}
public short readShort() {
return readBuffer.readShort();
}
public int readInt() {
return readBuffer.readInt();
}
public long readLong() {
return readBuffer.readLong();
}
public float readFloat() {
return readBuffer.readFloat();
}
public double readDouble() {
return readBuffer.readDouble();
}
public String readString() {
int size = readBuffer.readShort();
if (size <= 0) {
return "";
}
byte[] bytes = new byte[size];
readBuffer.readBytes(bytes);
return new String(bytes, CHARSET);
}
public <T> List<T> readList(Class<T> clz) {
List<T> list = new ArrayList<>();
int size = readBuffer.readShort();
for (int i = 0; i < size; i++) {
list.add(read(clz));
}
return list;
}
public <K,V> Map<K,V> readMap(Class<K> keyClz, Class<V> valueClz) {
Map<K,V> map = new HashMap<>();
int size = readBuffer.readShort();
for (int i = 0; i < size; i++) {
K key = read(keyClz);
V value = read(valueClz);
map.put(key, value);
}
return map;
}
@SuppressWarnings("unchecked")
public <I> I read(Class<I> clz) {
Object t = null;
if ( clz == int.class || clz == Integer.class) {
t = this.readInt();
} else if (clz == byte.class || clz == Byte.class){
t = this.readByte();
} else if (clz == short.class || clz == Short.class){
t = this.readShort();
} else if (clz == long.class || clz == Long.class){
t = this.readLong();
} else if (clz == float.class || clz == Float.class){
t = readFloat();
} else if (clz == double.class || clz == Double.class){
t = readDouble();
} else if (clz == String.class ){
t = readString();
} else if (Serializer.class.isAssignableFrom(clz)){
try {
byte hasObject = this.readBuffer.readByte();
if(hasObject == 1){
Serializer temp = (Serializer)clz.newInstance();
temp.readFromBuffer(this.readBuffer);
t = temp;
}else{
t = null;
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
throw new RuntimeException(String.format("不支持类型:[%s]", clz));
}
return (I) t;
}
public Serializer writeByte(Byte value) {
writeBuffer.writeByte(value);
return this;
}
public Serializer writeShort(Short value) {
writeBuffer.writeShort(value);
return this;
}
public Serializer writeInt(Integer value) {
writeBuffer.writeInt(value);
return this;
}
public Serializer writeLong(Long value) {
writeBuffer.writeLong(value);
return this;
}
public Serializer writeFloat(Float value) {
writeBuffer.writeFloat(value);
return this;
}
public Serializer writeDouble(Double value) {
writeBuffer.writeDouble(value);
return this;
}
public <T> Serializer writeList(List<T> list) {
if (isEmpty(list)) {
writeBuffer.writeShort((short) 0);
return this;
}
writeBuffer.writeShort((short) list.size());
for (T item : list) {
writeObject(item);
}
return this;
}
public <K,V> Serializer writeMap(Map<K, V> map) {
if (isEmpty(map)) {
writeBuffer.writeShort((short) 0);
return this;
}
writeBuffer.writeShort((short) map.size());
for (Entry<K, V> entry : map.entrySet()) {
writeObject(entry.getKey());
writeObject(entry.getValue());
}
return this;
}
public Serializer writeString(String value) {
if (value == null || value.isEmpty()) {
writeShort((short) 0);
return this;
}
byte data[] = value.getBytes(CHARSET);
short len = (short) data.length;
writeBuffer.writeShort(len);
writeBuffer.writeBytes(data);
return this;
}
public Serializer writeObject(Object object) {
if(object == null){
writeByte((byte)0);
}else{
if (object instanceof Integer) {
writeInt((int) object);
return this;
}
if (object instanceof Long) {
writeLong((long) object);
return this;
}
if (object instanceof Short) {
writeShort((short) object);
return this;
}
if (object instanceof Byte) {
writeByte((byte) object);
return this;
}
if (object instanceof String) {
String value = (String) object;
writeString(value);
return this;
}
if (object instanceof Serializer) {
writeByte((byte)1);
Serializer value = (Serializer) object;
value.writeToTargetBuff(writeBuffer);
return this;
}
throw new RuntimeException("不可序列化的类型:" + object.getClass());
}
return this;
}
private <T> boolean isEmpty(Collection<T> c) {
return c == null || c.size() == 0;
}
public <K,V> boolean isEmpty(Map<K,V> c) {
return c == null || c.size() == 0;
}
}
2)自定义序列化的使用
要实现序列化接口:
import java.util.ArrayList;
import java.util.List;
import com.cn.core.Serializer;
public class Player extends Serializer{
private long playerId;
private int age;
private List<Integer> skills = new ArrayList<>();
private Resource resource = new Resource();
public Resource getResource() {
return resource;
}
public void setResource(Resource resource) {
this.resource = resource;
}
public long getPlayerId() {
return playerId;
}
public void setPlayerId(long playerId) {
this.playerId = playerId;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List<Integer> getSkills() {
return skills;
}
public void setSkills(List<Integer> skills) {
this.skills = skills;
}
@Override
protected void read() {
this.playerId = readLong();
this.age = readInt();
this.skills = readList(Integer.class);
this.resource = read(Resource.class);
}
@Override
protected void write() {
writeLong(playerId);
writeInt(age);
writeList(skills);
writeObject(resource);
}
}
业务类:
Resource.java
package com.cn;
import com.cn.core.Serializer;
public class Resource extends Serializer {
private int gold;
public int getGold() {
return gold;
}
public void setGold(int gold) {
this.gold = gold;
}
@Override
protected void read() {
this.gold = readInt();
}
@Override
protected void write() {
writeInt(gold);
}
}
5. Protobuf的序列化与反序列化
相比netty的序列化与反序列化,protocol buffer的序列化与反序列化做的更好。
对比:
1)对于Player的序列化:
package com.serial;
import java.io.IOException;
import java.util.Arrays;
/**
* 自定义序列化协议
*/
public class Serial2Bytes {
public static void main(String[] args) throws Exception {
byte[] bytes = toBytes();
toPlayer(bytes);
}
/**
* 序列化
* @throws IOException
*/
public static byte[] toBytes() throws IOException{
Player player = new Player(320, 20, "peter");
player.getSkills().add(1001);
//获取 字节数组
byte[] byteArray = player.getBytes();
System.out.println(Arrays.toString(byteArray));
return byteArray;
}
/**
* 反序列化
* @param bs
* @throws Exception
*/
public static void toPlayer(byte[] bs) throws Exception{
Player player = new Player();
player.readFromBytes(bs);
//打印
System.out.println("playerId:" + player.getPlayerId());
System.out.println("age:" + player.getAge());
System.out.println("name:" + player.getName());
System.out.println("skills:" + (Arrays.toString(player.getSkills().toArray())));
}
}
结果为:
[0, 0, 0, 0, 0, 0, 1, 64, 0, 0, 0, 20, 0, 5, 112, 101, 116, 101, 114, 0, 1, 0, 0, 3, -23]
对于protobuf, 对player类的序列化:
package com.proto;
import java.util.Arrays;
import com.proto.PlayerModule.PBPlayer;
import com.proto.PlayerModule.PBPlayer.Builder;
/**
* protobuf学习
*/
public class PB2Bytes {
public static void main(String[] args) throws Exception {
byte[] bytes = toBytes();
toPlayer(bytes);
}
/**
* 序列化
*/
public static byte[] toBytes(){
//获取一个PBPlayer的构造器
Builder builder = PlayerModule.PBPlayer.newBuilder();
//设置数据
builder.setPlayerId(101).setAge(20).setName("peter").addSkills(1001);
//构造出对象
PBPlayer player = builder.build();
//序列化成字节数组
byte[] byteArray = player.toByteArray();
System.out.println(Arrays.toString(byteArray));
return byteArray;
}
/**
* 反序列化
* @param bs
* @throws Exception
*/
public static void toPlayer(byte[] bs) throws Exception{
PBPlayer player = PlayerModule.PBPlayer.parseFrom(bs);
System.out.println("playerId:" + player.getPlayerId());
System.out.println("age:" + player.getAge());
System.out.println("name:" + player.getName());
System.out.println("skills:" + (Arrays.toString(player.getSkillsList().toArray())));
}
}
结果为:
[8, 101, 16, 20, 26, 5, 112, 101, 116, 101, 114, 32, -23, 7]
总结:可以看出, protobuf的序列化后更小。
2)Protobuf的实现原理:位运算
看PB自动生成类PlayerModule.java中的writeTo方法:
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeInt64(1, playerId_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeInt32(2, age_);
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, getNameBytes());
}
for (int i = 0; i < skills_.size(); i++) {
output.writeInt32(4, skills_.get(i));
}
getUnknownFields().writeTo(output);
}
PB的配置文件:
option java_package = "com.proto";
option java_outer_classname = "PlayerModule";
message PBPlayer{
required int64 playerId = 1;
required int32 age = 2;
required string name = 3;
repeated int32 skills = 4;
}
message PBResource{
required int64 gold = 1;
required int32 energy = 2;
}
PB的源码:
public void writeRawVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
writeRawByte(value);
return;
} else {
writeRawByte((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
(value & ~0x7F) == 0解析:
其中,0x7F转成自己为 0111 1111, 对应7F, 取反后为:
1000 0000
如果int的值与1000 0000与运算的话,其1~7位全被置为0,之后判断其是否==0
假如 value > 0111 1111
value & 1000 0000
这句话实际在判断 value < 0x7F, 这样意味着他不需要占一个字节。
else表示 value > 0x7F, 则:
(value & 0x7F) | 0x80
其中,value & 0x7F表示获取到1~7位数据,再与0x80做或运算。
0x80表示1000 0000
做或运算后,得到
1XXX XXXX
所以,获取前7位数据,第8位(最左一位表示是否还有数据,1表示还有数据)。
所以,PB序列化后,不会像我们序列化那样正好占4个字节,而是会根据int数据的大小占用1~5个字节。
由于我们使用的int通常占用1~2个字节就够了,这样PB序列化就节省了空间。
同理,Long正常占8个字节,但在PB中占1~9个字节。但通常只需要占用2-3个字节。
所以,自己要序列化做优化节省空间时,不能使用用netty的writeToLong等方法,而是自己写位运算来做。
六、自定义数据包协议
1. 什么是粘包、分包
假设客户端向服务端发送了2条消息,分别为:
give me a coffee
give me a tea
粘包:
但实际上可能收到的消息为:
give me a coffeegive me a tea
即发生了粘包现象。
分包:
也有可能收到的消息为:
give me
a coffeegive me a tea
即收到了2部分消息,这叫做分包现象。
粘包和分包出现的原因是,没有一个稳定的数据结构,解决方案:
方法1. 采用分隔符方式:
give me a tea|give me a tea|
这样即便发生了粘包和分包的现象,仍然可以解析出原始的消息。
缺点:引入分隔符后,效率稍差。
方法2. 长度+数据
16give me a coffee
13give me a tea
这样即便发生了粘包和分包的现象,仍然可以解析出原始的消息。
2. 数据包
1)数据包格式定义
包头 | 模块号 | 命令号 | 数据长度 | 数据 |
---|---|---|---|---|
4字节(int) | 2字节(short) | 2字节(short) | 4字节(int) | 序列化后的消息内容 |
2) 自定义数据包及Encoder & Decoder
- 数据包的POJO类:
package com.cn.model;
/**
* 请求对象
*/
public class Request {
/**
* 请求模块
*/
private short module;
/**
* 命令号
*/
private short cmd;
/**
* 数据部分
*/
private byte[] data;
public short getModule() {
return module;
}
public void setModule(short module) {
this.module = module;
}
public short getCmd() {
return cmd;
}
public void setCmd(short cmd) {
this.cmd = cmd;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
public int getDataLength(){
if(data == null){
return 0;
}
return data.length;
}
}
- 数据包的RequestEncoder:把Request对象转成了ChannelBuffer对象,即把对象转成了二进制。
package com.cn.codc;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Request;
/**
* 请求编码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+
* | 包头 | 模块号 | 命令号 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*/
public class RequestEncoder extends OneToOneEncoder{
@Override
protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
Request request = (Request)(rs);
ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
//包头
buffer.writeInt(ConstantValue.FLAG);
//module
buffer.writeShort(request.getModule());
//cmd
buffer.writeShort(request.getCmd());
//长度
buffer.writeInt(request.getDataLength());
//data
if(request.getData() != null){
buffer.writeBytes(request.getData());
}
return buffer;
}
}
- 数据包的RequestDecoder:
package com.cn.codc;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Request;
/**
* 请求解码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+
* | 包头 | 模块号 | 命令号 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*/
public class RequestDecoder extends FrameDecoder{
/**
* 数据包基本长度
*/
public static int BASE_LENTH = 4 + 2 + 2 + 4;
@Override
protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {
//可读长度必须大于基本长度
if(buffer.readableBytes() >= BASE_LENTH){
//防止socket字节流攻击
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
//记录包头开始的index
int beginReader;
while(true){
beginReader = buffer.readerIndex();
buffer.markReaderIndex();
if(buffer.readInt() == ConstantValue.FLAG){
break;
}
//未读到包头,略过一个字节
buffer.resetReaderIndex();
buffer.readByte();
//长度又变得不满足
if(buffer.readableBytes() < BASE_LENTH){
return null;
}
}
//模块号
short module = buffer.readShort();
//命令号
short cmd = buffer.readShort();
//长度
int length = buffer.readInt();
//判断请求数据包数据是否到齐
if(buffer.readableBytes() < length){
//还原读指针
buffer.readerIndex(beginReader);
return null;
}
//读取data数据
byte[] data = new byte[length];
buffer.readBytes(data);
Request request = new Request();
request.setModule(module);
request.setCmd(cmd);
request.setData(data);
//继续往下传递
return request;
}
//数据包不完整,需要等待后面的包来
return null;
}
}
该类继承自FrameDecoder,FrameDecoder可帮我们解决粘包、分包问题。
decode方法return什么对象,下面的handler就会收到什么对象。
- 数据包的Response对象
package com.cn.model;
/**
* 返回对象
*/
public class Response {
/**
* 请求模块
*/
private short module;
/**
* 命令号
*/
private short cmd;
/**
* 状态码
*/
private int stateCode;
/**
* 数据部分
*/
private byte[] data;
public short getModule() {
return module;
}
public void setModule(short module) {
this.module = module;
}
public short getCmd() {
return cmd;
}
public void setCmd(short cmd) {
this.cmd = cmd;
}
public int getStateCode() {
return stateCode;
}
public void setStateCode(int stateCode) {
this.stateCode = stateCode;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
public int getDataLength(){
if(data == null){
return 0;
}
return data.length;
}
}
- ResponseEncoder
package com.cn.codc;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Response;
/**
* 请求编码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* | 包头 | 模块号 | 命令号 | 状态码 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*/
public class ResponseEncoder extends OneToOneEncoder{
@Override
protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
Response response = (Response)(rs);
ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
//包头
buffer.writeInt(ConstantValue.FLAG);
//module
buffer.writeShort(response.getModule());
//cmd
buffer.writeShort(response.getCmd());
//状态码
buffer.writeInt(response.getStateCode());
//长度
buffer.writeInt(response.getDataLength());
//data
if(response.getData() != null){
buffer.writeBytes(response.getData());
}
return buffer;
}
}
- ResponseDecoder
package com.cn.codc;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Response;
/**
* response解码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* | 包头 | 模块号 | 命令号 | 状态码 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*/
public class ResponseDecoder extends FrameDecoder{
/**
* 数据包基本长度
*/
public static int BASE_LENTH = 4 + 2 + 2 + 4;
@Override
protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {
//可读长度必须大于基本长度
if(buffer.readableBytes() >= BASE_LENTH){
//记录包头开始的index
int beginReader = buffer.readerIndex();
while(true){
if(buffer.readInt() == ConstantValue.FLAG){
break;
}
}
//模块号
short module = buffer.readShort();
//命令号
short cmd = buffer.readShort();
//状态码
int stateCode = buffer.readInt();
//长度
int length = buffer.readInt();
if(buffer.readableBytes() < length){
//还原读指针
buffer.readerIndex(beginReader);
return null;
}
byte[] data = new byte[length];
buffer.readBytes(data);
Response response = new Response();
response.setModule(module);
response.setCmd(cmd);
response.setStateCode(stateCode);
response.setData(data);
//继续往下传递
return response;
}
//数据包不完整,需要等待后面的包来
return null;
}
}
3. 使用上面写好的编码器Encoder与解码器Decoder
- 客户端代码
package com.client;
import java.net.InetSocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import com.cn.codc.RequestEncoder;
import com.cn.codc.ResponseDecoder;
import com.cn.model.Request;
import com.cn.module.fuben.request.FightRequest;
/**
* netty客户端入门
*/
public class Client {
public static void main(String[] args) throws InterruptedException {
//服务类
ClientBootstrap bootstrap = new ClientBootstrap();
//线程池
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//socket工厂
bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
//管道工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new ResponseDecoder());
pipeline.addLast("encoder", new RequestEncoder());
pipeline.addLast("hiHandler", new HiHandler());
return pipeline;
}
});
//连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10101));
Channel channel = connect.sync().getChannel();
System.out.println("client start");
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请输入");
int fubenId = Integer.parseInt(scanner.nextLine());
int count = Integer.parseInt(scanner.nextLine());
FightRequest fightRequest = new FightRequest();
fightRequest.setFubenId(fubenId);
fightRequest.setCount(count);
Request request = new Request();
request.setModule((short) 1);
request.setCmd((short) 1);
request.setData(fightRequest.getBytes());
//发送请求
channel.write(request);
}
}
}
HiHandler.java:
package com.client;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import com.cn.model.Response;
import com.cn.model.StateCode;
import com.cn.module.fuben.request.FightRequest;
import com.cn.module.fuben.response.FightResponse;
/**
* 消息接受处理类
*/
public class HiHandler extends SimpleChannelHandler {
/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Response message = (Response)e.getMessage();
if(message.getModule() == 1){
if(message.getCmd() == 1){
FightResponse fightResponse = new FightResponse();
fightResponse.readFromBytes(message.getData());
System.out.println("gold:" + fightResponse.getGold());
}else if(message.getCmd() == 2){
}
}else if (message.getModule() == 1){
}
}
/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}
/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}
/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}
/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}
- 业务FightRequest对象, 继承Serializer对象。
package com.cn.module.fuben.request;
import com.cn.serial.Serializer;
public class FightRequest extends Serializer{
/**
* 副本id
*/
private int fubenId;
/**
* 次数
*/
private int count;
public int getFubenId() {
return fubenId;
}
public void setFubenId(int fubenId) {
this.fubenId = fubenId;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
@Override
protected void read() {
this.fubenId = readInt();
this.count = readInt();
}
@Override
protected void write() {
writeInt(fubenId);
writeInt(count);
}
}
- 业务FightResponse对象
package com.cn.module.fuben.response;
import com.cn.serial.Serializer;
public class FightResponse extends Serializer{
/**
* 获取金币
*/
private int gold;
public int getGold() {
return gold;
}
public void setGold(int gold) {
this.gold = gold;
}
@Override
protected void read() {
this.gold = readInt();
}
@Override
protected void write() {
writeInt(gold);
}
}
- 服务端代码
package com.server;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
import com.cn.codc.RequestDecoder;
import com.cn.codc.ResponseEncoder;
/**
* netty服务端入门
*/
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new RequestDecoder());
pipeline.addLast("encoder", new ResponseEncoder());
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
HelloHandler.java:
package com.server;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import com.cn.model.Request;
import com.cn.model.Response;
import com.cn.model.StateCode;
import com.cn.module.fuben.request.FightRequest;
import com.cn.module.fuben.response.FightResponse;
/**
* 消息接受处理类
*/
public class HelloHandler extends SimpleChannelHandler {
/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Request message = (Request)e.getMessage();
if(message.getModule() == 1){
if(message.getCmd() == 1){
FightRequest fightRequest = new FightRequest();
fightRequest.readFromBytes(message.getData());
System.out.println("fubenId:" +fightRequest.getFubenId() + " " + "count:" + fightRequest.getCount());
//回写数据
FightResponse fightResponse = new FightResponse();
fightResponse.setGold(9999);
Response response = new Response();
response.setModule((short) 1);
response.setCmd((short) 1);
response.setStateCode(StateCode.SUCCESS);
response.setData(fightResponse.getBytes());
ctx.getChannel().write(response);
}else if(message.getCmd() == 2){
}
}else if (message.getModule() == 1){
}
}
/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}
/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}
/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}
/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}
七、粘包、分包分析
1. pipeline分析
1)问题:消息如何在管道中流转?即当前一个handler如何往下一个handler传递对象的?
每个Handler会加入到pipeline(管道)中,一个pipeline中会有多个handler。那么handler之间的对象是如何流转的呢?
当用户向netty发消息时,netty会把消息封装成channel event(事件对象),然后把事件对象传递到pipeline里。pipeline中的每个handler是双向链表的节点。
handler往下传递对象的方法是netty的sendUpstream(event)
2)pipeline例子
server.java:
package com.server;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("handler1", new MyHandler1());
pipeline.addLast("handler2", new MyHandler2());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
MyHandler1.java:
package com.server;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.channel.UpstreamMessageEvent;
public class MyHandler1 extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
ChannelBuffer buffer = (ChannelBuffer)e.getMessage();
byte[] array = buffer.array();
String message = new String(array);
System.out.println("handler1:" + message);
//传递
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), "abc", e.getRemoteAddress()));
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), "efg", e.getRemoteAddress()));
}
}
MyHandler2.java:
package com.server;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class MyHandler2 extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
String message = (String)e.getMessage();
System.out.println("handler2:" + message);
}
}
2. 粘包、分包现象的处理
要解决分包和粘包问题,要在客户端和服务端之间定义一个稳定的数据结构:length+data
1) 粘包与分包的问题复现:
ClientTest.java: 循环发送1000次hello字符串
package com.server;
import java.io.IOException;
import java.net.Socket;
public class ClientTest {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 10101);
for (int i = 0; i < 1000; i++) {
socket.getOutputStream().write("hello".getBytes());
}
socket.close();
}
}
ServerTest.java:
package com.server;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class ServerTest {
public static void main(String[] args) {
// 服务类
ServerBootstrap bootstrap = new ServerBootstrap();
// boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
// 设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
// 设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("handler1", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
HelloHandler.java:
package com.server;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
private int count = 1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// System.out.println(e.getMessage() + " " +count);
//
// count++;
ChannelBuffer message = (ChannelBuffer)e.getMessage();
byte[] array = message.array();
System.out.println(new String(array));
}
}
服务端收到客户端发来的消息:
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohell
ohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohe
llohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello
可以看到,发送了分包与粘包的现象。
2)解决分包与粘包
client.java:
package com.server;
import java.net.Socket;
import java.nio.ByteBuffer;
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10101);
String message = "hello";
byte[] bytes = message.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
buffer.putInt(bytes.length);
buffer.put(bytes);
byte[] array = buffer.array();
for(int i=0; i<1000; i++){
socket.getOutputStream().write(array);
}
socket.close();
}
}
Server.java:
package com.server;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new MyDecoder());
pipeline.addLast("handler1", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
MyDecoder.java:
package com.server;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
public class MyDecoder extends FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
if (buffer.readableBytes() > 4) {
// System.out.println("readable:" + buffer.readableBytes());
// 标记
buffer.markReaderIndex();
// 长度
// System.out.println("readable2:" + buffer.readableBytes());
int length = buffer.readInt();
// System.out.println("length:"+length);
if (buffer.readableBytes() < length) {
// System.out.println("缓存当前剩余的buffer数据,等待剩下数据包到来,length:" + length + ", readable:" + buffer.readableBytes());
buffer.resetReaderIndex();
// System.out.println("buffer.readableBytes():" + buffer.readableBytes());
// 缓存当前剩余的buffer数据,等待剩下数据包到来
return null;
}
// 读数据
byte[] bytes = new byte[length];
buffer.readBytes(bytes);
// 往下传递对象
return new String(bytes);
}
// 缓存当前剩余的buffer数据,等待剩下数据包到来
return null;
}
}
HelloHandler.java:
package com.server;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
private int count = 1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
System.out.println(e.getMessage() + " " +count);
count++;
}
}
问题:
- 为什么FrameDecoder decode方法return的对象就是往下传递的对象?
答:因为调用了sendUpstream(event)方法 - buffer里面数据未读取完怎么办?
答:由cumulation缓存 - 为什么return null就可以缓存buffer?
答:由FrameDecoder的ReceiveMessage方法调用了updateCumulation(ctx, input);来缓存buffer中的数据,由cumulation缓存。
3. FrameDecoder源码分析
netty FrameDecoder方法:
@Override
public void messageReceived(
ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object m = e.getMessage();
if (!(m instanceof ChannelBuffer)) {
ctx.sendUpstream(e);
return;
}
ChannelBuffer input = (ChannelBuffer) m;
if (!input.readable()) {
return;
}
if (cumulation == null) {
try {
// the cumulation buffer is not created yet so just pass the input to callDecode(...) method
callDecode(ctx, e.getChannel(), input, e.getRemoteAddress()); // 调用MyDecode的decode方法
} finally {
updateCumulation(ctx, input); //保存数据到缓存
}
} else {
input = appendToCumulation(input); // 把之前剩余的数据拼到新数据里面
try {
callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
} finally {
updateCumulation(ctx, input);
}
}
}
里面的cumulation实际就是换成的ChannelBuffer对象。如果return null, 则会callDecode调用decode(context, channel, cumulation),实际上会走到MyDecoder中的decode方法。
4. Socket字节流攻击
思考问题:
长度+数据的方式的弊端:如果客户端把长度设置为Integer.max时,会把服务端内存爆掉。
字节流攻击:把长度定义的很大,这种数据包,通常被称为socket攻击。
解决方案:如果长度>2048, 则清除buffer。
但一旦清除了,我们将不知道数据包的开始位置,所以我们要用:
包头+长度+数据
所以“包头+长度”是必须的字段。
这样我们即使清除了数据包长度,仍然能通过包头找到数据包的起始位置。
防Socket攻击又可安全解码的Decoder:
package com.cn.codc;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Request;
/**
* 请求解码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+
* | 包头 | 模块号 | 命令号 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*
* @author -琴兽-
*
*/
public class RequestDecoder extends FrameDecoder{
/**
* 数据包基本长度
*/
public static int BASE_LENTH = 4 + 2 + 2 + 4;
@Override
protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {
//可读长度必须大于基本长度
if(buffer.readableBytes() >= BASE_LENTH){
//防止socket字节流攻击
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
//记录包头开始的index
int beginReader;
while(true){
beginReader = buffer.readerIndex();
buffer.markReaderIndex();
if(buffer.readInt() == ConstantValue.FLAG){
break;
}
//未读到包头,略过一个字节
buffer.resetReaderIndex();
buffer.readByte();
//长度又变得不满足
if(buffer.readableBytes() < BASE_LENTH){
return null;
}
}
//模块号
short module = buffer.readShort();
//命令号
short cmd = buffer.readShort();
//长度
int length = buffer.readInt();
//判断请求数据包数据是否到齐
if(buffer.readableBytes() < length){
//还原读指针
buffer.readerIndex(beginReader);
return null;
}
//读取data数据
byte[] data = new byte[length];
buffer.readBytes(data);
Request request = new Request();
request.setModule(module);
request.setCmd(cmd);
request.setData(data);
//继续往下传递
return request;
}
//数据包不完整,需要等待后面的包来
return null;
}
}