一.知识回顾
【0.Netty相关的知识专栏都帮你整理好了,根据自己的情况,自行选择学习,点击我即可快速跳转】
【1.初识Netty&使用Netty实现简单的客户端与服务端的通信操作&Netty框架中一些重要的类以及方法的解析】
二.基于Netty实现Http通信
2.1 Netty内置的编解码器和ChannelHandler
Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,减少了我们在繁琐的事务上花费的时间。
2.2 Netty中HTTP请求通信
- HTTP 是基于请求/响应模式的:客户端向服务器发送一个HTTP 请求,然后服务器将会返回一个HTTP 响应。Netty 提供了多种编码器和解码器以简化对这个协议的使用。
- 一个HTTP 请求/响应可能由多个数据部分组成,DefaultFullHttpRequest和DefaultFullHttpResponse消息是特殊的子类型,分别代表了完整的请求和响应。
2.3 案例演示
客户端
将我们的浏览器作为客户端,发送消息,此处我们就写服务端的代码:
服务端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* An HTTP server that sends back the content of the received HTTP request
* in a pretty plaintext form.
*/
public final class HttpHelloWorldServer {
public static void main(String[] args) throws Exception {
//主从多线程Reactor模式
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpServerCodec()); //netty针对http编解码的处理类
p.addLast(new HttpServerExpectContinueHandler());//netty针对http编解码的处理类
p.addLast(new HttpHelloWorldServerHandler());//自己的业务处理逻辑
}
});
Channel ch = b.bind(8080).sync().channel();
System.err.println("Open your web browser and navigate to " + "http://127.0.0.1:8080");
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务端自己定义的业务处理逻辑
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private static final byte[] CONTENT = "Hello Netty!".getBytes();
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK,
Unpooled.wrappedBuffer(CONTENT));
response.headers()
.set(CONTENT_TYPE, TEXT_PLAIN)
.setInt(CONTENT_LENGTH, response.content().readableBytes());
ChannelFuture f = ctx.write(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
2.4 运行结果
三.基于Netty实现UDP单播和广播通信
3.0 补充UDP相关知识
UDP协议
- 面向无连接的通讯协议。
- 通讯时不需要接收方确认,属于不可靠的传输。
- 因为不需要建立连接,所以传输速度快,但是容易丢失数据。
UDP报文组成
UDP是面向无连接的通讯协议,UDP报头由4个域组成,其中每个域各占用2个字节,其中包括目的端口号和源端口号信息,数据报的长度域是指包括报头和数据部分在内的总字节数,校验值域来保证数据的安全。由于通讯不需要连接,所以可以实现广播发送。
源端口:源端口号,在需要对方回信时选用,不需要时可用全0。
目的端口:目的端口号,这在终点交付报文时必须要使用到。
长度:UDP用户数据包的长度,其最小值是8(仅有首部)。
校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃。
3.1 基于Netty实现UDP单播通信
单播的传输模式:定义为发送消息给一个由唯一的地址所标识的单一的网络目的地
同学端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
/**
* 类说明:学生端
*/
public class UdpAnswerSide {
public final static String ANSWER = "老师,我,我,我,我想!!!";
public void run(int port) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
/*和tcp的不同,udp没有接受连接的说法,所以即使是接收端,
也使用Bootstrap*/
Bootstrap b = new Bootstrap();
/*由于我们用的是UDP协议,所以要用NioDatagramChannel来创建*/
b.group(group)
.channel(NioDatagramChannel.class)
.handler(new AnswerHandler());
//没有接受客户端连接的过程,监听本地端口即可
ChannelFuture f = b.bind(port).sync();
System.out.println("同学们等待老师的提问:。。。。。");
//System.out.println("请问同学们谁想成为架构师?.....");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String [] args) throws Exception{
int port = 8080;
new UdpAnswerSide().run(port);
}
}
同学端自定义的处理逻辑
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;
import java.util.Random;
/**
* 类说明:应答Handler
*/
public class AnswerHandler extends
SimpleChannelInboundHandler<DatagramPacket> {
/*应答的具体内容从常量字符串数组中取得,由nextQuote方法随机获取*/
private static final String[] DICTIONARY = {
"小刘",
"小张",
"小王",
"小宋",
"小黄" };
private static Random r = new Random();
private String nextQuote(){
return DICTIONARY[r.nextInt(DICTIONARY.length-1)];
}
@Override
protected void channelRead0(ChannelHandlerContext ctx,
DatagramPacket packet)
throws Exception {
//获得请求
String req = packet.content().toString(CharsetUtil.UTF_8);
if(UdpQuestionSide.QUESTION.equals(req)){
String name=nextQuote();
String answer = UdpAnswerSide.ANSWER+name;
System.out.println(name+"同学回答:"+answer);
/**
* 重新 new 一个DatagramPacket对象,我们通过packet.sender()来获取发送者的消息。重新发送出去!
*/
ctx.writeAndFlush(
new DatagramPacket(
Unpooled.copiedBuffer(
answer,
CharsetUtil.UTF_8),
packet.sender()));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
cause.printStackTrace();
}
}
老师端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
/**
* 类说明:老师端
*/
public class UdpQuestionSide {
public final static String QUESTION = "请问同学们谁想成为架构师";
public void run(int port) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
/*由于我们用的是UDP协议,所以要用NioDatagramChannel来创建*/
.channel(NioDatagramChannel.class)
.handler(new QuestoinHandler());
//不需要建立连接
Channel ch = b.bind(0).sync().channel();
//将UDP请求的报文以DatagramPacket打包发送给接受端
System.out.println(QUESTION);
ch.writeAndFlush(
new DatagramPacket(
Unpooled.copiedBuffer(QUESTION,
CharsetUtil.UTF_8),
new InetSocketAddress("127.0.0.1",
port)))
.sync();
//不知道接收端能否收到报文,也不知道能否收到接收端的应答报文
// 所以等待15秒后,不再等待,关闭通信
if(!ch.closeFuture().await(30000)){
System.out.println("无人回答!");
}
} catch (Exception e) {
group.shutdownGracefully();
}finally {
group.shutdownGracefully();
}
}
public static void main(String [] args) throws Exception{
int answerPort = 8080;
new UdpQuestionSide().run(answerPort);
}
}
服务端业务逻辑处理类
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;
/**
* 类说明:提问端的Handler,读取服务器的应答
*/
public class QuestoinHandler extends
SimpleChannelInboundHandler<DatagramPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg)
throws Exception {
//获得应答,DatagramPacket提供了content()方法取得报文的实际内容
String response = msg.content().toString(CharsetUtil.UTF_8);
if (response.startsWith(UdpAnswerSide.ANSWER)) {
//System.out.println(response);
System.out.println("接收到同学回答:"+response);
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
运行结果
同学端
教师端
3.2 基于Netty实现UDP广播通信
特别说明:案例来自马士兵教育
广播:传输到网络(或者子网)上的所有主机。
日志实体类
import java.net.InetSocketAddress;
/**
* 类说明:日志实体类
*/
public final class LogMsg {
public static final byte SEPARATOR = (byte) ':';
/*源的 InetSocketAddress*/
private final InetSocketAddress source;
/*消息内容*/
private final String msg;
/*消息id*/
private final long msgId;
/*消息发送的时间*/
private final long time;
//用于传入消息的构造函数
public LogMsg(String msg) {
this(null, msg,-1,System.currentTimeMillis());
}
//用于传出消息的构造函数
public LogMsg(InetSocketAddress source, long msgId,
String msg) {
this(source,msg,msgId,System.currentTimeMillis());
}
public LogMsg(InetSocketAddress source, String msg, long msgId, long time) {
this.source = source;
this.msg = msg;
this.msgId = msgId;
this.time = time;
}
//返回发送 LogMsg 的源的 InetSocketAddress
public InetSocketAddress getSource() {
return source;
}
//返回消息内容
public String getMsg() {
return msg;
}
//返回消息id
public long getMsgId() {
return msgId;
}
//返回消息中的时间
public long getTime() {
return time;
}
}
日志信息
import java.util.Random;
/**
* 类说明:日志信息,用String数组代替
*/
public class LogConst {
public final static int MONITOR_SIDE_PORT = 9998;
private static final String[] LOG_INFOS = {
"20220715:machine-1:Send sms to 10001",
"20220715:machine-3:Send email to ***@qq.com",
"20220715:machine-4:Happen Exception",
"20220715:machine-5:人生不能象做菜,把所有的料都准备好了才下锅",
"20220715:machine-6:牵着你的手,就象左手牵右手没感觉,但砍下去也会痛!",
"20220715:machine-7:我听别人说这世界上有一种鸟是没有脚的," +
"它只能一直飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次," +
"那一次就是它死亡的时候.",
"20220715:machine-2:多年以后我有个绰号叫西毒,任何人都可以变得狠毒," +
"只要你尝试过什么叫妒嫉.我不介意其他人怎么看我," +
"我只不过不想别人比我更开心.我以为有一些人永远不会妒嫉," +
"因为他太骄傲 . 在我出道的时候,我认识了一个人," +
"因为他喜欢在东边出没,所以很多年以后,他有个绰号叫东邪.",
"20220715:machine-3:做人如果没有梦想,那和咸鱼有什么区别",
"20220715:machine-5:世界上最远的距离不是生和死," +
"而是我站在你的面前却不能说:我爱你",
"20220715:machine-6:成功的含义不在于得到什么," +
"而是在于你从那个奋斗的起点走了多远.",
"20220715:machine-7:一个人杀了一个人,他是杀人犯.是坏人," +
"当一个人杀了成千上万人后,他是英雄,是大好人",
"20220715:machine-2:世界在我掌握中,我却掌握不住对你的感情",
"20220715:machine-3:我害怕前面的路,但是一想到你,就有能力向前走了。",
"20220715:machine-5:如果你只做自己能力范围之内的事情,就永远无法进步。" +
"昨天已成为历史,明天是未知的,而今天是上天赐予我们的礼物," +
"这就是为什么我们把它叫做现在!"};
private final static Random r = new Random();
public static String getLogInfo(){
return LOG_INFOS[r.nextInt(LOG_INFOS.length-1)];
}
}
日志的广播端
import com.msb.netty.udp.broadcast.LogConst;
import com.msb.netty.udp.broadcast.LogMsg;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import java.net.InetSocketAddress;
/**
* 类说明:日志的广播端
*/
public class LogEventBroadcaster {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventBroadcaster(InetSocketAddress remoteAddress) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
//引导该 NioDatagramChannel(无连接的)
bootstrap.group(group).channel(NioDatagramChannel.class)
//设置 SO_BROADCAST 套接字选项
.option(ChannelOption.SO_BROADCAST, true)
.handler(new LogEventEncoder(remoteAddress));
}
public void run() throws Exception {
//绑定 Channel
Channel ch = bootstrap.bind(0).sync().channel();
long count = 0;
//启动主处理循环,模拟日志发送
for (;;) {
ch.writeAndFlush(new LogMsg(null, ++count,
LogConst.getLogInfo()));
try {
//休眠 2 秒,如果被中断,则退出循环;
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.interrupted();
break;
}
}
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
//创建并启动一个新的 UdpQuestionSide 的实例
LogEventBroadcaster broadcaster = new LogEventBroadcaster(
//表明本应用发送的报文并没有一个确定的目的地,也就是进行广播
new InetSocketAddress("255.255.255.255",
LogConst.MONITOR_SIDE_PORT));
try {
broadcaster.run();
}
finally {
broadcaster.stop();
}
}
}
实际的日志实体类编码
import com.msb.netty.udp.broadcast.LogMsg;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
import java.util.List;
/**
* 类说明:编码,将实际的日志实体类编码为DatagramPacket
*/
public class LogEventEncoder extends MessageToMessageEncoder<LogMsg> {
private final InetSocketAddress remoteAddress;
//LogEventEncoder 创建了即将被发送到指定的 InetSocketAddress
// 的 DatagramPacket 消息
public LogEventEncoder(InetSocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
LogMsg logMsg, List<Object> out) throws Exception {
byte[] msg = logMsg.getMsg().getBytes(CharsetUtil.UTF_8);
//容量的计算:两个long型+消息的内容+分割符
ByteBuf buf = channelHandlerContext.alloc()
.buffer(8*2 + msg.length + 1);
//将发送时间写入到 ByteBuf中
buf.writeLong(logMsg.getTime());
//将消息id写入到 ByteBuf中
buf.writeLong(logMsg.getMsgId());
//添加一个 SEPARATOR
buf.writeByte(LogMsg.SEPARATOR);
//将日志消息写入 ByteBuf中
buf.writeBytes(msg);
//将一个拥有数据和目的地地址的新 DatagramPacket 添加到出站的消息列表中
out.add(new DatagramPacket(buf, remoteAddress));
}
}
日志的接受端
import com.msb.netty.udp.broadcast.LogConst;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import java.net.InetSocketAddress;
/**
* 类说明:日志的接受端
*/
public class LogEventMonitor {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventMonitor(InetSocketAddress address) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
//引导该 NioDatagramChannel
bootstrap.group(group)
.channel(NioDatagramChannel.class)
//设置套接字选项 SO_BROADCAST
.option(ChannelOption.SO_BROADCAST, true)
//允许重用
.option(ChannelOption.SO_REUSEADDR,true)
.handler( new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LogEventDecoder());
pipeline.addLast(new LogEventHandler());
}
} )
.localAddress(address);
}
public Channel bind() {
//绑定 Channel。注意,DatagramChannel 是无连接的
return bootstrap.bind().syncUninterruptibly().channel();
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
//构造一个新的 UdpAnswerSide并指明监听端口
LogEventMonitor monitor = new LogEventMonitor(
new InetSocketAddress(LogConst.MONITOR_SIDE_PORT));
try {
//绑定本地监听端口
Channel channel = monitor.bind();
System.out.println("UdpAnswerSide running");
channel.closeFuture().sync();
} finally {
monitor.stop();
}
}
}
日志的业务处理类
import com.msb.netty.udp.broadcast.LogMsg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* 类说明:日志的业务处理类,实际的业务处理,接受日志信息
*/
public class LogEventHandler
extends SimpleChannelInboundHandler<LogMsg> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
//当异常发生时,打印栈跟踪信息,并关闭对应的 Channel
cause.printStackTrace();
ctx.close();
}
@Override
public void channelRead0(ChannelHandlerContext ctx,
LogMsg event) throws Exception {
//创建 StringBuilder,并且构建输出的字符串
StringBuilder builder = new StringBuilder();
builder.append(event.getTime());
builder.append(" [");
builder.append(event.getSource().toString());
builder.append("] :[");
builder.append(event.getMsgId());
builder.append("] :");
builder.append(event.getMsg());
//打印 LogMsg 的数据
System.out.println(builder.toString());
}
}
将DatagramPacket解码为实际的日志实体类
import com.msb.netty.udp.broadcast.LogMsg;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.util.CharsetUtil;
import java.util.List;
/**
* 类说明:解码,将DatagramPacket解码为实际的日志实体类
*/
public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext ctx,
DatagramPacket datagramPacket, List<Object> out)
throws Exception {
//获取对 DatagramPacket 中的数据(ByteBuf)的引用
ByteBuf data = datagramPacket.content();
//获得发送时间
long sendTime = data.readLong();
System.out.println("接受到"+sendTime+"发送的消息");
//获得消息的id
long msgId = data.readLong();
//获得分隔符SEPARATOR
byte sepa = data.readByte();
//获取读索引的当前位置,就是分隔符的索引+1
int idx = data.readerIndex();
//提取日志消息,从读索引开始,到最后为日志的信息
String sendMsg = data.slice(idx ,
data.readableBytes()).toString(CharsetUtil.UTF_8);
//构建一个新的 LogMsg 对象,并且将它添加到(已经解码的消息的)列表中
LogMsg event = new LogMsg(datagramPacket.sender(),
msgId, sendMsg);
//作为本handler的处理结果,交给后面的handler进行处理
out.add(event);
}
}
运行结果
发送端接收消息
接收端接收消息
好了,到这里【基于Netty实现Http通信、实现UDP单播和广播通信、代码案例实战学习】,下次我们更新学习WebSocket通信的代码案例。