Netty进阶
1.Netty问题
TCP协议都存在着黏包和半包问题,但是UDP没有
1.粘包现象
发送方分10次发送,接收方一次接受了10次发送的消息
2.半包现象
调整服务器的接受缓冲区大小(调小)
半包会导致服务器在1次接受时,无法接收到客户端1次的数据
3.TCP滑动窗口
粘包:
- 滑动窗口:假设一个报文为256字节,如果接收方处理不及时(没有及时把报文处理掉),并且滑动窗口的大小足够大,那么这256字节就会缓冲在窗口中,多个
- Nagle算法:攒够一批足够再发,也会造成粘包
- 应用层:ByteBuf设置很大,Netty默认时1024
半包:
- 滑动窗口:接收方窗口只有128字节,发送方发送256字节,放不下只能先发送前128,等待ack后发送剩余的部分,造成了半包
- MSS(链路层):(网卡/网络设备)发送数据超过MSS限制,会将数据切分发送,造成半包
- 应用层:接收方ByteBuf小于实际的发送数据量
4.代码案例
Server:
package com.yikolemon.P91Question;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
/**
* 黏包现象 : 不调 服务端 接收缓冲区 SO_RCVBUF
* 半包现象 : serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
* 影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍
*/
@Slf4j
public class HelloWorldServer {
void start(){
final NioEventLoopGroup boss = new NioEventLoopGroup();
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
// head<<<<<<<<<
// 接收缓冲区 设置字节 【使 发生半包】
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
// =============
// end>>>>>>>>>>
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
});
final ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
}catch (InterruptedException e)
{
log.error("server error", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new HelloWorldServer().start();
}
}
Client:
package com.yikolemon.P91Question;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorldClient {
static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// channel连接建立好之后 出发 channelActive() 时间
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// super.channelActive(ctx);
for (int i = 0; i < 10; i++) {
final ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.writeAndFlush(buf);
}
}
});
}
});
final ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e)
{
log.error("Client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
2.问题解决方案
1.短连接
能够解决粘包问题,但不能解决半包问题,当Server的接受缓冲区很小,就会产生半包问题
一次发送就断开连接,不会产生粘包的问题
Clinet代码:
package com.yikolemon.P91Question;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorldClient {
static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
//发送10次短连接
for (int i = 0; i < 10; i++) {
send();
}
}
public static void send() {
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// channel连接建立好之后 出发 channelActive() 时间
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// super.channelActive(ctx);
final ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.writeAndFlush(buf);
ctx.channel().close();
}
});
}
});
final ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e)
{
log.error("Client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
2.FixedLengthFrameDecoder定长帧解码器
FixedLengthFrameDecoder是Netty提供的一个解码器。发送方发送指定长度的消息,接收方如果接受不到定长,那么就会等到后面消息补齐最后拼接成为指定的长度。
Client:
package com.yikolemon.P91Question;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Random;
@Slf4j
public class SloveClient {
public static byte[] fill10Bytes(char c, int len) {
byte[] bytes = new byte[10];
Arrays.fill(bytes, (byte) '_');
for (int i = 0; i < len; i++) {
bytes[i] = (byte) c;
}
System.out.println(new String(bytes));
return bytes;
}
public static void main(String[] args) {
send();
}
public static void send() {
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// channel连接建立好之后 出发 channelActive() 时间
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//把所有的数据全都写到一个Buf中,让服务器去解码
ByteBuf buf = ctx.alloc().buffer();
char c='0';
for (int i = 0; i < 10; i++) {
byte[] bytes = fill10Bytes(c, new Random().nextInt(10) + 1);
buf.writeBytes(bytes);
c++;
}
ctx.writeAndFlush(buf);
}
});
}
});
final ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e)
{
log.error("Client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
Server代码:
package com.yikolemon.P91Question;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
/**
* 黏包现象 : 不调 服务端 接收缓冲区 SO_RCVBUF
* 半包现象 : serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
* 影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍
*/
@Slf4j
public class HelloWorldServer {
void start(){
final NioEventLoopGroup boss = new NioEventLoopGroup();
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//约定解码器,消息的长度为10
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
});
final ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
}catch (InterruptedException e)
{
log.error("server error", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new HelloWorldServer().start();
}
}
3.使用分割符作为解码器
1)LineBasedFrameDecoder
支持\n和\r\n,需要指定最大长度,如果在最大长度内没有找到分割符,就抛出异常
Client代码:
package com.yikolemon.P91Question;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j
public class SolveClient2 {
public static StringBuilder makeString(char c, int len) {
StringBuilder sb = new StringBuilder(len + 2);
for (int i = 0; i < len; i++) {
sb.append(c);
}
sb.append("\n");
return sb;
}
public static void main(String[] args) {
send();
}
public static void send() {
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// channel连接建立好之后 出发 channelActive() 时间
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//把所有的数据全都写到一个Buf中,让服务器去解码
ByteBuf buf = ctx.alloc().buffer();
char c='0';
for (int i = 0; i < 10; i++) {
int nextInt = new Random().nextInt(256+1);
StringBuilder str = makeString(c, nextInt);
byte[] bytes = str.toString().getBytes();
buf.writeBytes(bytes);
c++;
}
ctx.writeAndFlush(buf);
}
});
}
});
final ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e)
{
log.error("Client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
Server代码:
package com.yikolemon.P91Question;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HelloWorldServer {
void start(){
final NioEventLoopGroup boss = new NioEventLoopGroup();
final NioEventLoopGroup worker = new NioEventLoopGroup();
try{
final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//1024表示最大的检验长度,如果超过了长度仍然没有检测到换行符就会抛出异常
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
}
});
final ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
}catch (InterruptedException e)
{
log.error("server error", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new HelloWorldServer().start();
}
}
2)其他
其他的分割解码器,可以自行进行测试
4.LengthFieldBasedFrameDecoder帧解码器
四个构造参数的含义:
- lengthFieldOffset :长度字段偏移量
- lengthFieldLength :长度字段长度
- lengthAdjustment :长度字段为基准,还有几个字节是内容
- initialBytesToStrip :从头剥离几个字节
例子1:
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
例子2:最后剥离出长度字段
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
例子3:
cafe占据两个字节,offset代表长度的偏移量,length代表长度的长度
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
例子4:
lengthAdjustment代表,从length之后,需要跳过2字节(类似Http的Header),才是真正的内容
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
例子5:
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
代码测试:
package com.yikolemon.Decoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class LengthFieldDecoderTest {
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
//int length是4个字节
new LengthFieldBasedFrameDecoder(1024,0,4,4,0),
new LoggingHandler(LogLevel.INFO)
);
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
send(buf,"Fuck u");
send(buf,"Shit");
channel.writeInbound(buf);
}
public static void send(ByteBuf buf,String str) {
//4个字节的内容长度, 实际内容
byte[] bytes =str.getBytes();
int length = bytes.length;//实际内容长度
buf.writeInt(length);
//假装写入了一个版本号,所有需要adjustment对此(header)造成的影响进行修正
buf.writeBytes("why:".getBytes());
buf.writeBytes(bytes);
}
}
3.协议的设计和解析
1.Redis协议
根据Redis的协议,使用Netty根据协议发送命令
Redis网络传输协议:
set ke vvvv 对应的网络数据发送:
*3
$3
set
$2
ke
$4
vvvv
首先在本机上搭建Redis,然后Netty发送命令
FakeRedisClient代码:
package com.yikolemon.FakeRedis;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
public class FakeRedisClient {
public static void main(String[] args) throws InterruptedException {
final byte[] LINE={13,10};
NioEventLoopGroup worker = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
//第一次连接调用
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("set".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$2".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("ke".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$4".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("vvvv".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 6379).sync();
channelFuture.channel().closeFuture().sync();
}
}
2.Http协议
使用Netty提供的HttpServerCodec(),此Handler能够解码Http请求。(它既是入站处理器也是出站处理器)
解码后,会生成为HttpRequest和HttpContent两个类,有两种方式对消息内容进行处理
方式一
使用ChannelInboundHandlerAdapter()类处理,此方式处理需要对msg类型进行instance判断,需要if else,比较麻烦
方式二
使用SimpleChannelInboundHandler,这里的T泛型类即此Handler会处理的消息,如果发送来的msg不为此类型,那么不会处理
DefaultFullHttpResponse
作为HttpResponse,发送给Server,写入content和在header中的length
代码:
package com.yikolemon.FakeProtocol;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
@Slf4j
public class FakeHttp {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss=new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss,worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new HttpServerCodec());
/*ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("{}",msg.getClass());
if (msg instanceof HttpRequest){
//请求行,请求头
}
else if (msg instanceof HttpContent){
//请求体
}
}
});*/
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception {
String uri = httpRequest.getUri();
log.info(uri);
DefaultFullHttpResponse response =
new DefaultFullHttpResponse(httpRequest.getProtocolVersion(), HttpResponseStatus.OK);
byte[] bytes = "<h1>Fuck this</h1>".getBytes();
response.content().writeBytes(bytes);
//告诉浏览器,content长度,防止浏览器一直读取
response.headers().setInt(CONTENT_LENGTH,bytes.length);
channelHandlerContext.writeAndFlush(response);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
}
}
3.自定义协议的要素
- 魔数:判断包是否无效:例如CAFEBABE
- 版本号:可以支持协议的升级
- 序列化算法:消息正文采用哪种序列化/反序列化方式,由此拓展,如json,protobuf,(hessian,jdk这俩为二进制)
- 指令类型:登录,注册,单聊,群聊...
- 请求序号:为了双工通信,提供异步的能力
- 正文长度
- 消息正文:使用json或者xml,或者对象流
4.聊天室消息协议
MessageCodec:
package com.yikolemon.Codec;
import com.yikolemon.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 自定义编解码器
*/
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
//魔数写入
out.writeBytes(new byte[]{1,2,3,4});
//版本
out.writeByte(1);
//序列化算法 0:jdk 1:json
out.writeByte(0);
//指令类型
out.writeByte(msg.getMessageType());
//4个字节的请求序号
out.writeInt(msg.getSequenceId());
//对齐填充用
out.writeByte(0xff);
//长度
//内容字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
//长度
out.writeInt(bytes.length);
out.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes=new byte[length];
in.readBytes(bytes,0,length);
if (serializerType==0){
//使用jdk反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Message message = (Message)objectInputStream.readObject();
log.info("{},{},{},{},{},{}",magicNum,version,serializerType,messageType,sequenceId,length);
log.info("{}",message);
out.add(message);
}
}
}
测试类:
package com.yikolemon.Codec;
import com.yikolemon.message.LoginRequestMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class MessageCodecTest {
public static void main(String[] args) throws Exception {
EmbeddedChannel embeddedChannel = new EmbeddedChannel(
//使用帧解码器能够解决半包和粘包问题
new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
new LoggingHandler(LogLevel.INFO),
new MessageCodec()
);
//encode
LoginRequestMessage message = new LoginRequestMessage("yiko", "111");
embeddedChannel.writeOutbound(message);
//decode
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null,message,buf);
//入站
embeddedChannel.writeInbound(buf);
}
}
4.Handler线程安全
如果你使用的Handler没有被标注为Shareable,在addLast时不能共用Handler,否则会出错抛出异常
加上了@Shareable注解的Handler是线程安全的
这些编码和解码器能够公共使用
没有加的时候需要注意线程安全问题(编码解码器)
ByteToMessage的子类不能被标注为@Sharable
将3中的MessageCodec改为可以标注为Sharable的类:
package com.zko0.protocol;
import com.zko0.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 在前面必须添加LengthFieldBasedFrameDecoder处理器
* 确保接到的ByteBuf消息完整
*/
@Slf4j
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> list) throws Exception {
ByteBuf out = ctx.alloc().buffer();
//魔数写入
out.writeBytes(new byte[]{1,2,3,4});
//版本
out.writeByte(1);
//序列化算法 0:jdk 1:json
out.writeByte(0);
//指令类型
out.writeByte(msg.getMessageType());
//4个字节的请求序号
out.writeInt(msg.getSequenceId());
//对齐填充用
out.writeByte(0xff);
//长度
//内容字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
//长度
out.writeInt(bytes.length);
out.writeBytes(bytes);
list.add(out);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes=new byte[length];
in.readBytes(bytes,0,length);
if (serializerType==0){
//使用jdk反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Message message = (Message)objectInputStream.readObject();
log.info("{},{},{},{},{},{}",magicNum,version,serializerType,messageType,sequenceId,length);
log.info("{}",message);
out.add(message);
}
}
}
5.连接异常断开
channelInactive和exceptionCaught,分别处理channel连接断开和异常捕捉,重新实现该两个方法,可以实现客户端退出,服务端的对应处理
package com.zko0.server.handler;
import com.zko0.server.session.SessionFactory;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ChannelHandler.Sharable
public class QuitHandler extends ChannelInboundHandlerAdapter {
// 当连接断开时触发 inactive 事件
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
SessionFactory.getSession().unbind(ctx.channel());
log.info("{} 已经断开", ctx.channel());
}
// 当出现异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
SessionFactory.getSession().unbind(ctx.channel());
log.info("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage());
}
}
6.心跳机制
连接假死问题:
- 当设备出现故障,底层TCP连接已经断开,但是应用程序没有感知到连接已经端口,会保持资源的占用
- 公网不稳定,丢包。连续丢包体现为:客户端数据无法发送,服务端无法接收到消息
- 应用线程阻塞
解决方式:使用心跳机制,一段事件内没有发送/接收到消息,触发事件,进行心跳检测
new IdleStateHandler()
为Netty提供的Handler,能够在Channel长时间没有数据读/写的情况下,触发IdleState XXX
事件
服务端检测:
在init内加入Handler
//判断是否读空闲时间过长,或者写空闲时间过长
//5s内没有收到channel数据,会出发IdleState#Read_IDLE事件
ch.pipeline().addLast(new IdleStateHandler(5,0,0));
// ChannelDuplexHandler可以作为入站或者出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler(){
//用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
//触发读共享事件
if (event.state()== IdleState.READER_IDLE){
log.info("5s没有读到数据了");
ctx.channel().close();
}
}
});
客户端检测:
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
//触发读共享事件
if (event.state()== IdleState.WRITER_IDLE){
log.info("3s没有写数据了");
ctx.writeAndFlush(new PingMessage());
}
}
});
7.序列化算法拓展
package com.zko0.protocol;
import com.google.gson.Gson;
import java.io.*;
import java.nio.charset.StandardCharsets;
public interface Serializer {
<T> T deserialize(Class<T> clazz,byte[] bytes);
//序列化
<T> byte[] serialize(T object);
enum Algorithm implements Serializer{
Java{
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
T t = (T) objectInputStream.readObject();
return t;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public <T> byte[] serialize(T object) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
},
Json{
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
String json=new String(bytes,StandardCharsets.UTF_8);
return new Gson().fromJson(json,clazz);
}
@Override
public <T> byte[] serialize(T object) {
String json=new Gson().toJson(object);
return json.getBytes(StandardCharsets.UTF_8);
}
}
}
}
配置类:
package com.zko0.config;
import com.zko0.protocol.Serializer;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public abstract class Config {
static Properties properties;
static {
try (InputStream in = Config.class.getResourceAsStream("/application.properties")) {
properties = new Properties();
properties.load(in);
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
}
public static int getServerPort() {
String value = properties.getProperty("server.port");
if(value == null) {
return 8080;
} else {
return Integer.parseInt(value);
}
}
public static Serializer.Algorithm getSerializerAlgorithm() {
String value = properties.getProperty("serializer.algorithm");
if(value == null) {
return Serializer.Algorithm.Java;
} else {
return Serializer.Algorithm.valueOf(value);
}
}
}
配置序列化算法:
序列化:
枚举类.ordinal()能够获取它的序列(从0开始)
out.writeByte(Config.getSerializerAlgorithm().ordinal());
反序列化:
需要获取反序列化的class,在Message类中设置,通过序号获取class
package com.zko0.message;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
public abstract class Message implements Serializable {
/**
* 根据消息类型字节,获得对应的消息 class
* @param messageType 消息类型字节
* @return 消息 class
*/
public static Class<? extends Message> getMessageClass(int messageType) {
return messageClasses.get(messageType);
}
private int sequenceId;
private int messageType;
public abstract int getMessageType();
public static final int LoginRequestMessage = 0;
public static final int LoginResponseMessage = 1;
public static final int ChatRequestMessage = 2;
public static final int ChatResponseMessage = 3;
public static final int GroupCreateRequestMessage = 4;
public static final int GroupCreateResponseMessage = 5;
public static final int GroupJoinRequestMessage = 6;
public static final int GroupJoinResponseMessage = 7;
public static final int GroupQuitRequestMessage = 8;
public static final int GroupQuitResponseMessage = 9;
public static final int GroupChatRequestMessage = 10;
public static final int GroupChatResponseMessage = 11;
public static final int GroupMembersRequestMessage = 12;
public static final int GroupMembersResponseMessage = 13;
public static final int PingMessage = 14;
public static final int PongMessage = 15;
/**
* 请求类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
/**
* 响应类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_RESPONSE = 102;
private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();
static {
messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
messageClasses.put(ChatRequestMessage, ChatRequestMessage.class);
messageClasses.put(ChatResponseMessage, ChatResponseMessage.class);
messageClasses.put(GroupCreateRequestMessage, GroupCreateRequestMessage.class);
messageClasses.put(GroupCreateResponseMessage, GroupCreateResponseMessage.class);
messageClasses.put(GroupJoinRequestMessage, GroupJoinRequestMessage.class);
messageClasses.put(GroupJoinResponseMessage, GroupJoinResponseMessage.class);
messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
messageClasses.put(GroupChatRequestMessage, GroupChatRequestMessage.class);
messageClasses.put(GroupChatResponseMessage, GroupChatResponseMessage.class);
messageClasses.put(GroupMembersRequestMessage, GroupMembersRequestMessage.class);
messageClasses.put(GroupMembersResponseMessage, GroupMembersResponseMessage.class);
messageClasses.put(RPC_MESSAGE_TYPE_REQUEST, RpcRequestMessage.class);
messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE, RpcResponseMessage.class);
}
}
解码:
Serializer.Algorithm algorithm = Serializer.Algorithm.values()[serializerAlgorithm];
Class<? extends Message> messageClass = Message.getMessageClass(messageType);
Message deserialize = algorithm.deserialize(messageClass, bytes);
out.add(deserialize);
测试:
package com;
import com.zko0.config.Config;
import com.zko0.message.LoginRequestMessage;
import com.zko0.message.Message;
import com.zko0.protocol.MessageCodecSharable;
import com.zko0.protocol.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class TestSerializer {
public static void main(String[] args) {
MessageCodecSharable CODEC = new MessageCodecSharable();
LoggingHandler LOGGING = new LoggingHandler(LogLevel.INFO);
EmbeddedChannel channel = new EmbeddedChannel(LOGGING, CODEC, LOGGING);
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123");
//channel.writeOutbound(message);
ByteBuf buf = messageToByteBuf(message);
channel.writeInbound(buf);
}
public static ByteBuf messageToByteBuf(Message msg) {
int algorithm = Config.getSerializerAlgorithm().ordinal();
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
out.writeBytes(new byte[]{1, 2, 3, 4});
out.writeByte(1);
out.writeByte(algorithm);
out.writeByte(msg.getMessageType());
out.writeInt(msg.getSequenceId());
out.writeByte(0xff);
byte[] bytes = Serializer.Algorithm.values()[algorithm].serialize(msg);
out.writeInt(bytes.length);
out.writeBytes(bytes);
return out;
}
}
8.参数调优
1.调优方式
1.客户端
对BootStrap进行参数配置
通过.childOption()
来配置参数:给SocketChannel配置参数
2.服务端
- 通过
.option()
来配置参数:给ServerSocketChannel配置参数 - 通过
.childOption()
来配置参数:给SocketChannel配置参数
2.连接超时
SO_TIMEOUT用于阻塞IO,阻塞IO中accept,read无限等待。如果不希望永远阻塞,使用该参数进行超时时间的调整
//ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000
//1000毫秒没有连接抛出异常
Bootstrap bootstrap = new Bootstrap()
.group(group)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler());
源码解析:
在主线程和AbstractNioChannel使用同一个Promise对面。
Netty通过设置一个时间为延时时间
的定时任务,通过Promise设置结果,返还给主线程,主线程通过catch捕获返还的异常。
3.backlog
在TCP三次握手中,当服务器收到最后一次ACK时,不会立马进行Accept,而是放入全连接队列中,依次进行Accept。
- 在Linux2.2之前,backlog大小包含了两个队列的大小,在2.2之后,分别使用下面两个参数来控制
- sync queue 半连接队列
- 大小通过
/proc/sys/net/ipv4/tcp_max_syn_backlog
指定,在syncookies
启动的情况下,逻辑上没有最大限制,该设置可以被忽略
- 大小通过
- accept queue全连接队列
- 其大小通过
/proc/sys/net/core/somaxconn
指定,在使用listen函数的时候,内核会根据传入的backlog参数(Netty)和系统参数(Linux系统配置),取两者的较小值 - 如果accept queue队列满了,server将发送一个拒绝连接的错误给client
- 其大小通过
Server设置全连接队列大小为2:
package com.zko0;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
public class TestBacklogServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.option(ChannelOption.SO_BACKLOG, 2) // 全队列满了
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler());
}
}).bind(8080);
}
}
Client:
package com.zko0;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TestBacklogClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("hello!".getBytes()));
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
在EventLoop中打断点,使得accept阻塞,但是能放入全连接队列:
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {//该处打断点
unsafe.read();
}
将Server以debug模式运行,client以run模式运行,启动三个client,在第三个启动时报错:
4.ulimit-n
属于操作系统参数,为允许一个进程打开的最大文件描述符的数量
5.TCP_NODELAY
false 默认开启nagle算法,设置TCP_NODELAY为true,关闭nagle
属于SocketChannel参数,需要childOption()设置
6.SO_SNDBUF&SO_RCVBF
不建议调整,操作系统会根据情况去调整发送缓冲区和接受缓冲区
7.ALLOCATOR
属于SocketChannel参数
用于分配ByteBuf, ctx.alloc()拿到的就是该对象
在VM Option中添加netty参数配置:
-Dio.netty.allocator.type=unpooled
控制是否池化
-Dio.netty.noPreferDirect=true
控制直接内存还是堆内存
package com.zko0;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;
public class TestByteBuf {
public static void main(String[] args) {
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf.getClass());
System.out.println(buf.maxCapacity());
log(buf);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 32; i++) {
sb.append("a");
}
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
public static void log(ByteBuf buffer) {
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buffer.readerIndex())
.append(" write index:").append(buffer.writerIndex())
.append(" capacity:").append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf, buffer);
System.out.println(buf.toString());
}
}
8.RCVBUF_ALLOCATOR
- 属于SocketChannel参数
- 控制netty接受缓冲区的大小
Netty进阶的更多相关文章
- Netty进阶和实战
实现UDP单播和广播 UDP 这样的无连接协议中,并没有持久化连接这样的概念,并且每个消息(一个UDP 数据报)都是一个单独的传输单元.此外,UDP 也没有TCP 的纠错机制. 通过类比,TCP 连接 ...
- Netty 进阶
1. 粘包与半包 1.1 粘包现象 服务端代码 public class HelloWorldServer { static final Logger log = LoggerFactory.getL ...
- Netty学习路线
预研时间170517-170519 投入时间:约10h 理解度:入门①前置基础:了解基本网络协议和通信方式[图解HTTP]http://download.csdn.net/detail/niehanm ...
- 《Netty Redis Zookeeper 高并发实战》声明
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 这里, 对疯狂创客圈 <Netty Redis Zookeeper 高并发实战> 一书,进行一些必要说明. ...
- Netty 面试题 (史上最全、持续更新)
文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...
- Java开发者职业生涯要看的200+本书
作者:老刘链接:https://www.zhihu.com/question/29581524/answer/684872838来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...
- 2019年阿里java面试题
一.JVM与性能优化 描述一下 JVM 加载 Class 文件的原理机制? 什么是类加载器? 类加载器有哪些? 什么是tomcat类加载机制? 类加载器双亲委派模型机制? Java 内存分配? Jav ...
- Java 书单
Java 基础 <Head First Java> 有人说这本书不适合编程新手阅读?其实本书还是很适合稍微有一点点经验的新手来阅读的,当然也适合我们用来温故 Java 知识点. ps:刚入 ...
- Java学到什么程度能找到一份还不错的工作
我的读者里有很多 Java 新人,新人是指正在学 Java 的.以及工作时间不长的年轻人,他们经常问我一个问题: Java 学到什么程度才能找到一份还不错的工作? 今天我就从我自己面试新人的角度来回答 ...
- websocket(三) 进阶!netty框架实现websocket达到高并发
引言: 在前面两篇文章中,我们对原生websocket进行了了解,且用demo来简单的讲解了其用法.但是在实际项目中,那样的用法是不可取的,理由是tomcat对高并发的支持不怎么好,特别是tomcat ...
随机推荐
- C温故补缺(十四):内存管理
内存管理 stdlib库中有几个内存管理相关的函数 序号 函数和描述 1 void *calloc(int num, int size);在内存中动态地分配 num 个长度为size 个字节 的连续空 ...
- C温故补缺(七):函数指针与回调函数
函数指针与回调函数 函数指针就是指向函数调用栈地址的指针,定义时须和函数的返回值类型,参数类型相同 如: #include<stdio.h> int max(int x,int y){ r ...
- 关于js更改编码问题
前言 前几天调试喜马拉雅的js加密算法,找到固定一段加密算法后调试,发现结果与实际不一致,后来发现是js显示的编码不一致,而我用的密钥是直接通过 chrome控制台复制下来的,这就导致最后结果不一致. ...
- 运用领域模型——DDD
模型被用来描述人们所关注的现实或想法的某个方面.模型是一种简化.它是对现实的解释 -- 把与解决问题密切相关的方面抽象出来,而忽略无关的细节. 每个软件程序是为了执行用户的某项活动,或是满足客户的某种 ...
- Java9-17新特性一览,了解少于3个你可能脱节了
前言 Java8出来这么多年后,已经成为企业最成熟稳定的版本,相信绝大部分公司用的还是这个版本,但是一眨眼今年Java19都出来了,相信很多Java工程师忙于学习工作对新特性没什么了解,有的话也仅限于 ...
- 【Shell案例】【awk map计数&sort按指定列排序】9、统计每个单词出现的个数
描述写一个 bash脚本以统计一个文本文件 nowcoder.txt 中每个单词出现的个数. 为了简单起见,你可以假设:nowcoder.txt只包括小写字母和空格.每个单词只由小写字母组成.单词间由 ...
- 动手实验查看MySQL索引的B+树的高度
一:文中几个概念 h:统称索引的高度: h1:主键索引的高度: h2:辅助索引的高度: k:非叶子节点扇区个数. 二:索引结构 叶子节点其实是双向链表,而叶子节点内的行数据是单向链表,该图未体现. 磁 ...
- intel Pin:动态二进制插桩的安装和使用,以及如何开发一个自己的Pintool
先贴几个你可能用得上的链接 intel Pin的官方介绍Pin: Pin 3.21 User Guide (intel.com) intel Pin的API文档Pin: API Reference ( ...
- Python全栈工程师之从网页搭建入门到Flask全栈项目实战(5) - Flask中的ORM使用
1.理解ORM ORM是MTV模型里面的Model模型 ORM(Object Relational Mapping),对象关系映射 举例:学生选课 学生和课程这两个实体,一个学生可以选择多门课程,一个 ...
- 说说真实Java项目的开发流程,以及面试前的项目准备说辞
介绍项目是必不可少的Java面试环节,求职者需要借此证明自己真实Java项目的经验,如果再做的好的话,需要借此展开自己的亮点说辞. 不过之前如果只有学习项目经验,比如是自己跑通一个项目,或者是在培训班 ...