Netty
什么是TCP协议?
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。
三次握手,建立TCP连接
所谓三次握手即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
tcp三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的:
- 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器 进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
Socket API
TCP socket的API调用流程:
- socket函数创建一个套接字;
- bind绑定套接字的本地 IP地址和端口号;
- listen将服务端TCP套接字设置为监听模式,并设置队列大小;
- accept 接收一个连接请求,创建新的套接字,如果没有连接则会一直阻塞直到有连接进来。得到客户端的fd(套接字)之后,就可以调用read, write函数和客户端通讯,读写方式和其他I/O类似;
- read 从fd读数据,socket默认是阻塞模式的,如果对方没有写数据,read会一直阻塞着;
- write 写fd写数据,socket默认是阻塞模式的,如果对方没有写数据,write会一直阻塞着;
- connect 客户端向服务器发起连接;
Socket应用之BIO
同步阻塞IO,阻塞整个步骤,如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
public static void start(int port) throws IOException {
// 1. 创建服务器端实例,接受客户端请求,转发给Socket对象 socket
ServerSocket serverSocket = new ServerSocket();
// 2. 指定服务器的监听端口,阻塞 bind & listen
serverSocket.bind(new InetSocketAddress(port), 2);//backlog, accept queue was created in listen()
while(true) {
// 3. 创建Socket进行请求转发, blocking(阻塞操作) 因为没有请求过来的话一直处于等待状态 accept
final Socket clientSocket = serverSocket.accept();//block!
System.out.println("accept!");
new Thread(()-> {// or user thread pool
try {
// 4. 获取客户端意图
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 5. 给出客户端响应
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String line = in.readLine();//block
while(line != null) {
out.println(line);
out.flush();
line = in.readLine();//block
}
// 6. 关闭资源
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ee) {
e.printStackTrace();
}
}
}).start();
}
}
public static void main( String[] args ) throws InterruptedException, IOException{
start(8084);
}
}
BIO同步阻塞多线程时的缺点:
- 线程的创建和销毁成本很高;
- 线程本身占用较大内存;
- 线程的上下文切换成本很高;
- 容易造成锯齿状的系统负载(因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大)。
NIO
同步非阻塞IO,阻塞业务处理但不阻塞数据接收,适用于高并发且处理简单的场景,比如聊天软件。
解决了BIO上下文切换耗内存的问题(使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销)。
- ServerSocketChannel:监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel;
- SocketChannel :能通过TCP读写网络中的数据;
- Buffer:Buffer用于和通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的;
- Selector:,是阻塞的,能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。(ON_ACCEPT、ON_CONNECT、ON_READ、ON_WRITE)
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;
import java.util.Set;
public class NIOServer {
public static void start(int port) throws IOException {
// 1. 创建ServerSocketChannel,绑定监听端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 2. 设置Channel为非阻塞模式
serverChannel.configureBlocking(false);//nonblocking
// 3. 为Channel通道绑定监听端口bind & listen
serverChannel.bind(new InetSocketAddress(port));
// 4. 创建Selector
Selector selector = Selector.open();
// 5. 将Channel通道注册到Selector上,监听连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待新接入的连接
while(true) {
// 获取可用Channel数量
selector.select();//scan
// 6. 获取可用Channel的集合
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> it = readyKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
/*
* 根据就绪状态,调用对应方法处理业务逻辑
*/
// 7. 如果是链接事件 处理。accept
if (key.isAcceptable()) {
// 第一步:从SelectedKey中获取已经就绪的Channel
ServerSocketChannel server = (ServerSocketChannel)key.channel();
// 第二步:创建ServerSocketChannel
SocketChannel socket = server.accept();
System.out.println("Accept !");
// 第三步:将SocketChannel设置为非阻塞工作模式
socket.configureBlocking(false);
// 第四步:将Channel注册到Selector上,监听可读事件
socket.register(selector, SelectionKey.OP_READ );//tricky
}
// 8. 如果是读写事件 处理
if (key.isReadable()) {
// 第一步:从SelectedKey中获取已经就绪的Channel
SocketChannel socket = (SocketChannel) key.channel();
// 第二步:创建buffer
final ByteBuffer buffer = ByteBuffer.allocate(64);
// 第三步:读取客户端请求信息,非阻塞
final int bytesRead = socket.read(buffer);//also nonblock
if (bytesRead > 0) {
// 第四步:切换buffer为读模式
buffer.flip();
// 第五步:将信息发送到客户端
int ret = socket.write(buffer);
if (ret <=0) {
// 第六步:将Channel再次注册到Selector上,监听它的可读事件 register op_write
socket.register(selector, SelectionKey.OP_WRITE);
}
buffer.clear();
} else if (bytesRead < 0) {//means connection closed
key.cancel();
socket.close();
System.out.println("Client close");
}
}
// if (key.isWritable()) {
// SocketChannel socket = (SocketChannel) key.channel();
// final ByteBuffer buffer = ByteBuffer.allocate(64);
// socket.write(buffer);
// //remove
// }
// 第七步:remove
it.remove();// 处理完了,需要remove掉。
}
}
}
public static void main( String[] args ) throws InterruptedException, IOException{
start(8084);
}
}
缺点:
- 存在空轮询BUG,会让CPU飙到100%。
NIO的Reactor模式(多路复用)
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;
import java.util.Set;
public class Reactor {
interface ChannelHandler{
public void onRead(SocketChannel channel) throws Exception;
public void onAccept();
}
private static ChannelHandler echo = new ChannelHandler() {
public void onRead(SocketChannel socket) throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(256);
final int bytesRead = socket.read(buffer);
if (bytesRead > 0) {
buffer.flip();
socket.write(buffer);
buffer.clear();
} else if (bytesRead < 0) {
socket.close();
System.out.println("Client close");
}
}
public void onAccept() {
}
};
public static void start(int port) throws Exception {
final ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetSocketAddress address = new InetSocketAddress(port);
//bind & listen
serverChannel.bind(address);
final Selector selector = Selector.open();
SelectionKey sk = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new ChannelHandler() {//Acceptor
public void onRead(SocketChannel channel) {}
public void onAccept() {
try {
SocketChannel socket = serverChannel.accept();
System.out.println("Accept !");
socket.configureBlocking(false);
SelectionKey sk = socket.register(selector, 0);//register op_read here is also ok in single thread
sk.attach(echo);
sk.interestOps(SelectionKey.OP_READ);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
while (true) {
selector.select();
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> it = readyKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
ChannelHandler handler = (ChannelHandler) key.attachment();
//accept
if (key.isAcceptable()) {
handler.onAccept();
}
if (key.isReadable()) {
handler.onRead((SocketChannel) key.channel());
}
it.remove();
}
}
}
public static void main( String[] args ) throws Exception{
start(8084);
}
}
缺点:
- 处理op_write事件:写忙时需要注册op_write,不忙时注销op_write
- 处理byteBuffer: byteBuffer API 非常难用
- 处理粘包:需要记录接收到的字节状态
- 处理多个selector
- 提供优雅的业务编程模型
- 根据环境运行参数
总结
异步和非阻塞的区别?
Netty
Netty 是一个基于NIO(非阻塞IO)的客户端、服务器端编程框架,Netty提供异步的,事件驱动的网络应用程序框架和工具,可以快速开发高可用的客户端和服务器。
优点:
- 并发高
- 传输快
- 封装好
Netty核心概念
Channel:表示一个连接,可以理解为每一个请求,就是一个Channel。相当于SocketChannel。
EventLoop:负责处理注册到Channel上的处理 I/O 操作。 相当于Selector。
ChannelPipeline:用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。
ChannelHandler:核心处理业务就在这里,用于处理业务请求。
Bootstrap:启动器main。
ByteBuf:相当于ByteBuffer。
public class Server {
private static void start(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
// 自定义ChannelInboundHandler
ch.pipeline().addLast(new PrintInboundHandler("id1"));
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
start(8084);
}
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
public class PrintInboundHandler implements ChannelInboundHandler {
private final String id;
public PrintInboundHandler(String id) {
this.id = id;
}
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerAdded " + id);
}
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved " + id);
}
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelRegistered " + id);
ctx.fireChannelRegistered();
}
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelUnregistered " + id);
ctx.fireChannelUnregistered();
}
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive " + id);
ctx.fireChannelActive();
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive " + id);
ctx.fireChannelInactive();
}
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("channelRead " + id);
ctx.fireChannelRead(msg);
//ctx.channel().pipeline().fireChannelRead(msg);
}
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelReadComplete " + id);
ctx.fireChannelReadComplete();
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("userEventTriggered " + id);
}
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelWritabilityChanged " + id);
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("exceptionCaught " + id);
}
}
EventLoop
ChannelHandler & ChannelPipeline
粘包&拆包
参数说明:
- lengthFieldOffset
Length Field在帧的什么位置 - lengthFieldLength
Length Field 自身长度多少 - lengthAdjustment
Length Field中的长度值具体指哪部分 - initialBytesToStripe
Frame Decode后希望获得的内容(想要丢弃的帧部分)
服务端:
import com.tim.nettttty.handler.IProtocalHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
public class Server {
private static void start(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
/*
* 参数说明:
* 第一个参数:字节大小
* 第二个参数:lengthFieldOffset
* 第三个参数:lengthFieldLength
* 第四个参数:lengthAdjustment
* 第五个参数:initialBytesToStripe
*/
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 2, 2, -2, 0));
ch.pipeline().addLast(new DefaultEventExecutorGroup(16), new IProtocalHandler());
ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
start(8084);
}
}
/**
* 自定义业务处理
*/
import java.util.Random;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class IProtocalHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, final Object msg) throws Exception {
int sleep = 500 * new Random().nextInt(5);
System.out.println("sleep:" + sleep);
Thread.sleep(sleep);
final ByteBuf buf = (ByteBuf) msg;
char c1 = (char) buf.readByte();
char c2 = (char) buf.readByte();
if (c1 != 'C' || c2 != 'D') {
ctx.fireExceptionCaught(new Exception("magic error"));
return;
}
buf.readShort();//skip length
String outputStr = buf.toString(CharsetUtil.UTF_8);
System.out.println(outputStr);
ctx.channel().writeAndFlush(outputStr + "\n");
}
}
客户端:
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class OIOClient {
public static final String[] commands = new String[]{
"nihao",
"wo shi ke hu duan",
"hello world",
"java and netty"
};
public static void main(String[] args) throws IOException {
int concurrent = 1;
Runnable task = () -> {
try {
Socket socket = new Socket("127.0.0.1", 8084);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
/**
* HEADER(2)|LENGTH(2)|BODY
* LENGTH = (self(2) + BODY), not include header
*/
for (String str : commands) {
out.writeByte('C');
out.writeByte('D');
int length = str.length();
out.writeShort(length * 2 + 2);//why *2 here?
out.writeChars(str);
}
out.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while (!((line = br.readLine()) == null)) {
System.out.println(line);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
};
for (int i = 0; i < concurrent; i++) {
new Thread(task).start();
}
}
}
Netty和Tomcat有什么区别?
Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。