Netty 学习(二):服务端与客户端通信
Netty 学习(二):服务端与客户端通信
作者: Grey
原文地址:
说明
Netty 中服务端和客户端通信,包括两个方面,一个是 IO 处理逻辑的配置,一个是通信载体的设置。
IO 处理逻辑
无论是客户端,还是服务端,都是通过 Bootstrap 的 handler()方法指定的。我们通过模拟一个简单的客户端发送消息给服务端,服务端回写消息给客户端的示例程序来说明
服务端代码如下(每个配置见注释说明)
package netty.v3;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
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.util.AttributeKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* Netty 自动绑定递增端口,增加了IO处理逻辑
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyServer {
public static void main(String[] args) {
// 引导服务端的启动
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 用于监听端口,接收新连接的线程组
NioEventLoopGroup boss = new NioEventLoopGroup();
// 表示处理每一个连接的数据读写的线程组
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap.group(boss, worker)
// 指定IO模型为NIO
.channel(NioServerSocketChannel.class)
// 可以给服务端的Channel指定一些属性,非必须
.attr(AttributeKey.newInstance("serverName"), "nettyServer")
// 可以给每一个连接都指定自定义属性,非必须
.childAttr(AttributeKey.newInstance("clientKey"), "clientValue")
// 使用option方法可以定义服务端的一些TCP参数
// 这个设置表示系统用于临时存放已经完成三次握手的请求的队列的最大长度,
// 如果连接建立频繁,服务器创建新的连接比较慢,则可以适当调大这个参数
.option(ChannelOption.SO_BACKLOG, 1024)
// 以下两个配置用于设置每个连接的TCP参数
// SO_KEEPALIVE: 表示是否开启TCP底层心跳机制,true表示开启
.childOption(ChannelOption.SO_KEEPALIVE, true)
// TCP_NODELAY:表示是否开启Nagle算法,true表示关闭,false表示开启
// 如果要求高实时性,有数据发送时就马上发送,就设置为关闭;
// 如果需要减少发送次数,减少网络交互,就设置为开启。
.childOption(ChannelOption.TCP_NODELAY, true)
// 定义后面每一个连接的数据读写
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 不管服务端还是客户端,收到数据后都会调用channelRead()方法
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(StandardCharsets.UTF_8));
// 服务端将读到的数据返回客户端
System.out.println(new Date() + ": 服务端写出数据");
ctx.channel().writeAndFlush(getByteBuf(ctx));
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
byte[] bytes = "hello world from server!".getBytes(StandardCharsets.UTF_8);
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(bytes);
return buffer;
}
});
}
});
// 本地绑定一个8000端口启动服务
bind(serverBootstrap, 8000);
}
public static void bind(final ServerBootstrap serverBootstrap, final int port) {
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("端口[" + port + "]绑定成功");
} else {
System.err.println("端口[" + port + "]绑定失败");
bind(serverBootstrap, port + 1);
}
});
}
}
客户端代码如下(关于每个配置的说明见注释)
package netty.v3;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.AttributeKey;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* Netty 可自动重连的客户端,增加了IO处理逻辑
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyClient {
static final int MAX_RETRY = 6;
static final String HOST = "localhost";
static final int PORT = 8000;
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap
// 指定线程模型
.group(group)
// 指定IO类型为NIO
.channel(NioSocketChannel.class)
// attr可以为客户端Channel绑定自定义属性
.attr(AttributeKey.newInstance("clientName"), "nettyClient")
// 连接的超时时间,如果超过这个时间,仍未连接到服务端,则表示连接失败
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
// 表示是否开启TCP底层心跳机制,true表示开启
.option(ChannelOption.SO_KEEPALIVE, true)
// 是否开启Nagle算法,如果要求高实时性,有数据就马上发送,则为true
// 如果需要减少发送次数,减少网络交互,就设置为false
.option(ChannelOption.TCP_NODELAY, true)
// IO处理逻辑
.handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 这个方法会在客户端连接建立成功之后被调用
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println(new Date() + ": 客户端写出数据");
// 包装成ByteBuf并发送到服务端
// 注:Netty中的数据是以 ByteBuf 为单位的。
ctx.channel().writeAndFlush(getByteBuf(ctx));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(new Date() + ": 客户端读取到的数据 -> " + byteBuf.toString(StandardCharsets.UTF_8));
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
ByteBuf buffer = ctx.alloc().buffer();
byte[] bytes = "hello world".getBytes(StandardCharsets.UTF_8);
buffer.writeBytes(bytes);
return buffer;
}
});
}
});
connect(bootstrap, HOST, PORT, MAX_RETRY);
}
private static void connect(final Bootstrap bootstrap, final String host, final int port, int retry) {
bootstrap.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数已经使用完毕");
} else {
// 第几次重试
int order = (MAX_RETRY - retry) + 1;
// 本次的重试间隔
int delay = 1 << order;
System.out.println(new Date() + ": 连接失败,第" + order + "次重连...");
bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
}
});
}
}
代码说明:
无论是服务端还是客户端
channelActive
方法会在客户端连接建立成功之后被调用。
channelRead
方法在收到数据后都会调用
先运行服务端,控制台输出
端口[8000]绑定成功
然后运行客户端,客户端打印
连接成功!
Wed Sep 14 19:58:50 CST 2022: 客户端写出数据
Wed Sep 14 19:58:50 CST 2022: 客户端读取到的数据 -> hello world from server!
服务端打印
端口[8000]绑定成功
Wed Sep 14 19:58:50 CST 2022: 服务端读到数据 -> hello world
Wed Sep 14 19:58:50 CST 2022: 服务端写出数据
数据载体
Netty 中的数据载体是 ByteBuf,ByteBuf 的结构如下
ByteBuf 和 java.nio.ByteBuffer
类似,但是提供了比 java.nio.ByteBuffer
更方便使用的 API。
关于 Java 的java.nio.ByteBuffer
的使用,参考:Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer
接下来使用一个示例来说明 ByteBuf 的使用
代码如下:
package bytebuf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
/**
* ByteBuf 示例
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/14
* @since
*/
public class ByteBufTest {
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(9, 100);
print("allocate ByteBuf(9, 100)", buffer);
// write 方法改变写指针,写完之后写指针未到 capacity 的时候,buffer 仍然可写
buffer.writeBytes(new byte[]{1, 2, 3, 4});
print("writeBytes(1,2,3,4)", buffer);
// write 方法改变写指针,写完之后写指针未到 capacity 的时候,buffer 仍然可写, 写完 int 类型之后,写指针增加4
buffer.writeInt(12);
print("writeInt(12)", buffer);
// write 方法改变写指针, 写完之后写指针等于 capacity 的时候,buffer 不可写
buffer.writeBytes(new byte[]{5});
print("writeBytes(5)", buffer);
// write 方法改变写指针,写的时候发现 buffer 不可写则开始扩容,扩容之后 capacity 随即改变
buffer.writeBytes(new byte[]{6});
print("writeBytes(6)", buffer);
// get 方法不改变读写指针
System.out.println("getByte(3) return: " + buffer.getByte(3));
System.out.println("getShort(3) return: " + buffer.getShort(3));
System.out.println("getInt(3) return: " + buffer.getInt(3));
print("getByte()", buffer);
// set 方法不改变读写指针
buffer.setByte(buffer.readableBytes() + 1, 0);
print("setByte()", buffer);
// read 方法改变读指针
byte[] dst = new byte[buffer.readableBytes()];
buffer.readBytes(dst);
print("readBytes(" + dst.length + ")", buffer);
}
private static void print(String action, ByteBuf buffer) {
System.out.println("after ===========" + action + "============");
System.out.println("capacity(): " + buffer.capacity());
System.out.println("maxCapacity(): " + buffer.maxCapacity());
System.out.println("readerIndex(): " + buffer.readerIndex());
System.out.println("readableBytes(): " + buffer.readableBytes());
System.out.println("isReadable(): " + buffer.isReadable());
System.out.println("writerIndex(): " + buffer.writerIndex());
System.out.println("writableBytes(): " + buffer.writableBytes());
System.out.println("isWritable(): " + buffer.isWritable());
System.out.println("maxWritableBytes(): " + buffer.maxWritableBytes());
System.out.println();
}
}
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(9, 100);
表示分配了一块ByteBuf
。相关的参数值如下图
buffer.writeBytes(new byte[]{1, 2, 3, 4});
写入四个 byte 类型的数据,ByteBuf
变化如下
buffer.writeInt(12);
由于int
类型是4个Byte,所以,写入后,buffer
变化如下
buffer.writeBytes(new byte[]{5});
写入一个byte类型的数据,此时,写空间已经满了。如下图
buffer.writeBytes(new byte[]{6});
继续写入,由于写空间已经满了,所以要进行扩容,扩容后的结构如下
buffer.getByte(3);
buffer.getShort(3);
buffer.getInt(3);
buffer.setByte(buffer.readableBytes() + 1, 0);
由于get/set
操作不改变读写指针,所以buffer
还是保持原样
byte[] dst = new byte[buffer.readableBytes()];
buffer.readBytes(dst);
read
方法会改变读指针,改变后结构如下
图例
本文所有图例见:processon: Netty学习笔记
代码
更多内容见:Netty专栏
参考资料
Netty 学习(二):服务端与客户端通信的更多相关文章
- C#Winform窗体实现服务端和客户端通信例子(TCP/IP)
Winform窗体实现服务端和客户端通信的例子,是参考这个地址 http://www.cnblogs.com/longwu/archive/2011/08/25/2153636.html 进行了一些异 ...
- C# Socket服务端与客户端通信(包含大文件的断点传输)
步骤: 一.服务端的建立 1.服务端的项目建立以及页面布局 2.各功能按键的事件代码 1)传输类型说明以及全局变量 2)Socket通信服务端具体步骤: (1)建立一个Socket (2)接收 ...
- (C#:Socket)简单的服务端与客户端通信。
要求:1.可以完成一对一的通信:2.实现服务端对客户端一对多的选择发送:3.可以实现服务端的群发功能:4.可以实现客户端文件的发送: 要点:服务器端:第一步:用指定的端口号和服务器的ip建立一个End ...
- java的服务端与客户端通信(2)
一.Socket连接与HTTP连接 1.1Socket套接字 套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元.它是网络通信过程中端点的抽象表示,包含进行网络通信 ...
- java的服务端与客户端通信(1)
一.理解socket 1.1什么是socket? socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄.应用程序通常通过"套接字"向网络 ...
- java网络编程-单线程服务端与客户端通信
该服务器一次只能处理一个客户端请求;p/** * 利用Socket进行简单服务端与客户端连接 * 这是服务端 */public class EchoServer { private ServerSoc ...
- 多线程服务端与客户端通信(IO是阻塞的)_02
下面是多线程的;每次服务端接受请求,会创建一个线程专门处理这个请求: 虽然是多线程的,但还是阻塞,相当于单线程处理模式 public class TimeServer { public static ...
- NIO服务端和客户端通信demo
代码转自 https://www.jianshu.com/p/a9d030fec081 服务端: package nio; import java.io.IOException; import jav ...
- python 学习笔记_2 模拟socket编程 服务端、客户端通信(参考核心编程2代码实现)
服务器端代码实现: #!/usr/bin/env python#coding=gbk'''接收客户端字符串,在字段串前面打上当前时间,然后返回server端采用 python2 linux下调试运行客 ...
随机推荐
- 扩展-PageHelper分页插件
1.PageHelper 分页插件简介 1) PageHelper是MyBatis中非常方便的第三方分页插件 2) 官方文档: https://github.com/pagehelper/Mybati ...
- javascript基本属性访问对象的属性和方法
var myName = "Shelley"; //字符串基本类型 alert(myName.length); //隐式创建String对象,数值与myName相同,并执行len ...
- 【最全】CSS盒子(div)水平垂直居中居然还有这种方式
最全的CSS盒子(div)水平垂直居中布局,对CSS 布局掌握程度决定你在 Web 开发中的开发页面速度. 相对于屏幕 方法一:利用定位 <div class="box"&g ...
- 【Unity基础知识】基础游戏单位GameObject中常用的属性和API
一.GameObject中的成员变量 主要思想:得到该脚本依附的GameObject的相关信息 现有: Lesson4的代码: using System.Collections; using Syst ...
- ActiveMQ、RabbitMQ、RocketMQ、Kafka四种消息中间件分析介绍
ActiveMQ.RabbitMQ.RocketMQ.Kafka四种消息中间件分析介绍 我们从四种消息中间件的介绍到基本使用,以及高可用,消息重复性,消息丢失,消息顺序性能方面进行分析介绍! 一.消息 ...
- Android开启OTG功能/USB Host API功能
Android USB 模式简介 设备模式 当计算机或其他USB主机需要连接安卓设备时,此时安卓设备是作为"USB设备"角色的,在计算机上显示为 USB 外设.现在的安卓设备已经被 ...
- salt stack学习笔记
saltstack运行模式: local master/minion salt ssh saltstack三大功能 远程执行命令 配置管理(状态管理) 云管理 安装: master salt-mas ...
- qbxt数学五一Day4
目录 1. 随机试验 2. 概率 1. 平凡 2. 条件概率 3. 期望 习题 1 2 3 4 1. 随机试验 定义: 不能预先确知结果 试验之前可以预测所有可能结果或范围 可以在相同条件下重复实验 ...
- Nuget打包并上传教程
一.准备 1 . 下载 Download NuGet.exe 2 . windows 系统下设置环境变量 path中 或者 在dos 命令窗口下cd转到 nuget.exe 所在目录,这里为了每次使用 ...
- Blazor和Vue对比学习(进阶2.2.3):状态管理之状态共享,Blazor的依赖注入和第三方库Fluxor
Blazor没有提供状态共享的方案,虽然依赖注入可以实现一个全局对象,这个对象可以拥有状态.计算属性.方法等特征,但并不具备响应式.比如,组件A和组件B,都注入了这个全局对象,并引用了全局对象上的数据 ...