//client端分10次每次发送16字节数据
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buf);
}
}
在服务端输出,可以看到一次就收到了160字节数据,而非10次接收。
//client端一次发送160字节数据
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
}
ctx.writeAndFlush(buffer);
//server端修改接收缓冲区
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10); //影响底层接收缓冲区(滑动窗口)大小,仅决定netty读取最小单位,实际读取为其整数倍
在服务端输出中可看到数据被分为两节,一节20字节,一节140字节
粘包:发送abc def,接收abcdef。原因:
本质都是因为TCP是流式协议,消息无边界
Nagle算法:为了提高网络利用率,发送足够多的数据,如果发送数据少的话,则进行延时发送:SO_SNDBUF达到MSS或含有FIN;TCP_NODELAY=TRUE,收到ACK,超时时发送。除了以上几种情况则延时发送。
MSS限制:不同设备的MTU不同,以太网MTU是1500,FDDI的MTU是4352,本地回环地址的MTU是65535本地不走网卡,
MSS :是最大段长度,它是MTU刨去 tcp和ip 头后剩余能够作为数据传输的字节数,ipv4tcp头占用 20,ip头占用 20,因此以太网 MSS 的值为1500- 40=1460,TCP在传递大量数据时,会按照 MSS 大小将数据进行分割发送,MSS的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为MSS。
TCP以段(Segment)为单位发送一次数据就需要却仍应答ack处理,但是往返时间长性能差,因此引了了窗口概念,窗口大小决定了无需等待应答而可以继续发送数据最大值。
滑动窗口起到一个缓冲区的作用,也能进行流量控制。窗口内的数据才允许发送,当应答未到达前,窗口必须停止滑动,接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收。
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
// 发完即关
ctx.close();
}
//调整netty的接收缓冲区
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16,16,16));
//让所有数据包长度固定,服务端加入
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
//客户端什么时候 flush 都可以
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[8];
for (int j = 0; j < r.nextInt(8); j++) {
bytes[j] = (byte) c;
}
c++;
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
缺点:长度定的太大,浪费,长度定的太小,对某些数据包又显得不够
//服务端加入,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
//客户端发送+\n
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;
}
ByteBuf buf = ctx.alloc().buffer();
char c = '0';
Random r = new Random();
for (int i = 0; i < 10; i++) {
StringBuilder sb = makeString(c, r.nextInt(256) + 1);
c++;
buf.writeBytes(sb.toString().getBytes());
}
ctx.writeAndFlush(buf);
缺点,处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
//在发送消息前,先约定用定长字节表示接下来数据的长度
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));//最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
TCP/IP 中消息传输基于流的方式,没有边界。协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则。
//模拟客户端给本机redis发送set name aric命令
public static void main(String[] args) {
final byte[] LINE = {13,10};
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) {
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("$4".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("name".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$4".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aric".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
buf.toString(Charset.defaultCharset());
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 6379).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}
}
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest httpRequest) throws Exception {
log.debug(httpRequest.getUri());
//返回响应
DefaultFullHttpResponse response = new DefaultFullHttpResponse(httpRequest.protocolVersion(),HttpResponseStatus.OK);
byte[] bytes = "hello,world!
".getBytes();
response.headers().setInt(CONTENT_LENGTH, bytes.length)
response.content().writeBytes(bytes);
//写回响应
ctx.writeAndFlush(response);
}
});
public class MessageCodec extends ByteToMessageCodec<Message> {
//编码:出站前把msg编码成ByteBuf
@Override
protected void encode(ChannelHandlerContext ctx, Message message, ByteBuf out) throws Exception {
//1. 魔数4字节
out.writeBytes(new byte[]{1, 2, 3, 4});
//2. 版本号1字节
out.writeByte(1);
//3. 字节序列化算法 jdk 0, json 1
out.writeByte(0);
//4. 指令类型1字节
out.writeByte(message.getMessageType());
//5. 请求序号4字节
out.writeInt(message.getSequenceId());
out.writeByte(0xff); //用于对齐字节
//6. 获取内容字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(message);
byte[] bytes = bos.toByteArray();
//7. 长度
out.writeInt(bytes.length);
//8。 写入内容
out.writeBytes(bytes);
}
//解码:把ByteBuf转化为msg
@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();
byte length = in.readByte();
byte[] bytes = new byte[length];
in.readBytes(bytes, 0, length);
if (serializerType == 0) {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
out.add(message);
}
}
}
@Slf4j
@ChannelHandler.Sharable
/**
* 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的
*/
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {}
}
netty实现聊天室,可以登录,单聊,创建群聊,群聊,加群,退群,退出功能。
见ch.pipeline().addLast(LOGIN_HANDLER);方法。
服务器端将 handler 独立出来。ch.pipeline().addLast(CHAT_HANDLER);
ch.pipeline().addLast(GROUP_CREATE_HANDLER); //创建群聊
ch.pipeline().addLast(GROUP_JOIN_HANDLER); //加入群聊
ch.pipeline().addLast(GROUP_MEMBER_HANDLER); //查看群成员
ch.pipeline().addLast(GROUP_QUIT_HANDLER); //退出群聊
ch.pipeline().addLast(GROUP_CHAT_HANDLER); /群聊消息
ch.pipeline().addLast(QUIT_HANDLER);
原因:
//空闲状态检测器,5s没收客户端消息,会触发IdleState#READER_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) { //触发5s读写空闲事件,断开
ctx.channel().close();
}
super.userEventTriggered(ctx, evt);
}
});
客户端定时心跳
客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
https://gitee.com/xuyu294636185/netty-demo.git