Netty框架实战篇 - 实现UDP单播和广播

文章目录

  • UDP简介
    • 什么是UDP协议
    • UDP报文头
    • UDP与TCP的区别
    • 认知UDT
  • Netty中UDP相关类
  • 实现UDP单播
  • 实现UDP广播
  • 总结

UDP简介

什么是UDP协议

UDP(用户数据报协议)是一种无连接的传输层协议,它主要用于不要求分组顺序到达的传输中,分组传输顺序的检查与排序由应用层完成,提供面向事务的简单不可靠信息传送服务。UDP协议基本上是IP协议与上层协议的接口,适用端口分别运行在同一台设备上的多个应用程序,同时UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等

UDP报文头

Netty框架实战篇 - 实现UDP单播和广播_第1张图片

UDP报文头由4个域组成,每个域各占用2个字节,其中包括目的端口号和源端口号信息,数据报的长度域是指包括报头和数据部分在内的总字节数,校验值域来保证数据的安全。由于通讯不需要连接,所以可以实现广播发送

UDP与TCP的区别

TCP作为面向连接的传输协议,负责管理两个网络端点之间的连接的建立,在连接的生命周期内的有序和可靠的消息传输,以及最后,连接的有序终止。而相比之下,UDP无连接的协议,并没有持久化连接的概念,并且每个消息(一个 UDP数据报)都是一个单独的传输单元。此外,UDP也没有TCP的纠错机制,其中每个节点都将确认它们所接收到的包,而没有被确认的包将会被发送方重新传输

通过类比,TCP连接就像打电话,其中一系列的有序消息将会在两个方向上流动。相反, UDP则类似于往邮箱中投入一叠明信片。你无法知道它们将以何种顺序到达它们的目的地, 或者它们是否所有的都能够到达它们的目的地

并且UDP相对TCP其本身作为无连接的不可靠的传输协议,在传输过程中不会对数据包进行合并发送(TCP通常情况下存在Nagle算法会合并数据包),而是直接发送,因此既然不会对数据进行合并,每个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了

认知UDT

基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称 UDT)是一种互联网数据传输协议。UDT 的主要目的是支持高速广域网上的海量数据传输,最典型的例子就是建立在光纤广域网上的网格计算,一些研究所在这样的网络上运行他们的分布式的数据密集程序,例如,远程访问仪器、分布式数据挖掘和高分辨率的多媒体流

而互联网上的标准数据传输协议 TCP 在高带宽长距离网络上性能很差。顾名思义,UDT建于UDP 之上,并引入新的拥塞控制和数据可靠性控制机制。UDT 是面向连接的双向的应用层协议

UDT 的特性主要包括在以下几个方面:

  1. 面向连接的协议:面向连接意味着两个使用协议的应用在彼此交换数据之前必须先建立一个连接,当然 UDT 是逻辑上存在的连接通道。这种连接的维护是基于握手、Keep-alive(保 活)以及关闭连接
  2. 可靠的协议:依靠包序号机制、接收者的 ACK 响应和丢包报告、ACK 序号机制、重传机 制(基于丢包报告和超时处理)来实现数据传输的可靠性
  3. 双工的协议:每个 UDT 实例包含发送端和接收端的信息
  4. 新的拥塞算法,并且具有可扩展的拥塞控制框架:新的拥塞控制算法不同于基于窗口的 TCP 拥塞控制算法(慢启动和拥塞避免),是混合的基于窗口的、基于速率的拥塞控制算法。 可扩展的拥塞控制框架开源的代码和拥塞控制的 C++类架构,可支持开发者派生专用的拥塞 控制算法
  5. 带宽估计:UDT使用对包(PP – Packet pair)的机制来估计带宽值。即每 16 个包为一组, 最后一个是对包。即发送方不用等到下一个发送周期内再发送。接收方接收到对包后对其到达时间进行记录,可结合上次记录的值计算出链路的带宽(计算的方法称为中值过滤法), 并在下次 ACK 中进行反馈

Netty中UDP相关类

Netty中提供了大量的类来支持UDP应用程序的编写,主要包括:

名称 描述
interface AddressedEnvelopeextends ReferenceCounted 定义一个消息,其包装了另一个消息并带有发送者和接收者地址。其中 M 是消息类型; A 是地址类型
class DefaultAddressedEnvelopeimplements AddressedEnvelope 提供了 interface AddressedEnvelope 的默认实现
class DatagramPacket extends DefaultAddressedEnvelope implements ByteBufHolder 扩展了 DefaultAddressedEnvelope 以使用 ByteBuf 作为消息数据容器,其中存在比较重要的方法:通过content()来获取消息内容、通过sender()来获取发送者的消息、通过recipient()来获取接收者的消息
interface DatagramChannel extends Channel 扩展了 Netty 的 Channel 抽象以支持 UDP 的多播组管理
class NioDatagramChannnel extends AbstractNioMessageChannel implements DatagramChannel 定义了一个能够发送和接收 Addressed-Envelope 消息的 Channel 类型

Netty 的 DatagramPacket 是一个简单的消息容器,DatagramChannel 实现用它来和远程节点通信。类似于在我们先前的类比中的明信片,它包含了接收者(和可选的发送者)的地址以及消息的有效负载本身

实现UDP单播

所谓的单播的传输模式,是定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。而面向连接的协议和无连接协议都是支持这种模式的

首先,配置发送方,发送方将传输方式设置为NioDatagramChannel即通过UDP传输,然后将发送的UDP报文信息打包成DatagramPacket发送到指定的地址和端口

public class MyUdpQuestionSide {

	private static final int PORT = 8761;
	private static final String HOST = "127.0.0.1";
	
	private static EventLoopGroup  eventLoopGroup =new NioEventLoopGroup();
	private static Bootstrap bootstrap = new Bootstrap();
	

	public static void startQuestion(){
		try {
			bootstrap.group(eventLoopGroup).
			   channel(NioDatagramChannel.class).//指定UDP传输方式
			    handler(new MyQuestionHandler());
			//由于UDP是无连接的,因此不需要建立连接
			Channel channel= bootstrap.bind(0).sync().channel();
			//将请求的UDP报文打包成DatagramPacket发送到接收方
			channel.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("question message!",CharsetUtil.UTF_8),new InetSocketAddress(HOST, PORT))).sync();
			//由于不确定接收方是否能够收到报文,并且当前能否收到应答报文也不确定
			//因此需要为channel设置等待10s,超时10s就关闭连接
			if(channel.closeFuture().await(10000)){
				System.out.println("请求结束");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}
	
	public static void main(String[] args) {
		startQuestion();
	}
}

定义发送方的业务Handler,用于接收应答方的应答报文

/**
 * 发送方的handler,用于接收接收方的应答报文
 *
 */
public class MyQuestionHandler extends SimpleChannelInboundHandler<DatagramPacket> {

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
		//获取应答信息
		String content=msg.content().toString(CharsetUtil.UTF_8);
		System.out.println("发送方接收的应答信息为:"+content);
	}
}

接下来,配置应答方,由于UDP是无连接的传输协议,因此应答方只需要监听本地端口即可

public class MyUdpAnswerSide {
	
	private static final int PORT = 8761;
	
	private static EventLoopGroup  eventLoopGroup =new NioEventLoopGroup();
	//由于UDP是无连接的,因此接收方也是使用Bootstrap
	private static Bootstrap bootstrap = new Bootstrap();
	
	public static void startAnswer(){
		try {
			bootstrap.group(eventLoopGroup).
			   channel(NioDatagramChannel.class).//指定UDP传输方式
			    handler(new MyAnswerHandler());
			//没有接受客户端连接的过程,监听本地端口即可
			ChannelFuture future=bootstrap.bind(PORT).sync();
			System.out.println("启动应答服务.....");
			future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}
	public static void main(String[] args) {
		startAnswer();
	}
}

定义应答方的业务handler,用于接收发送方的请求报文,然后再向发送方回复应答报文

public class MyAnswerHandler extends SimpleChannelInboundHandler<DatagramPacket>{

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
		//获取发送方的udp报文数据
		String content = msg.content().toString(CharsetUtil.UTF_8);
		System.out.println("接收方接收到请求:"+content);
		//应答,由于UDP报文头是包含了目的端口和源端口号,所以可以直接通过DatagramPacket中获得要发送方信息
		ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("answer message!",CharsetUtil.UTF_8), msg.sender()));
	}
}

测试

首先,开启应答服务
在这里插入图片描述
接下来,开启发送方服务
在这里插入图片描述
然后,应答服务,接收到了发送报文
Netty框架实战篇 - 实现UDP单播和广播_第2张图片

实现UDP广播

定义广播的日志实例,主要包括:消息内容、消息id、消息发送时间和分隔符

/**
 * 日志实体
 */
public class LogEvent {
	public static final byte SEPARATOR = (byte) ':';//分隔符
	private final InetSocketAddress source;
	private final String msg;//消息内容
	private final long msgId;//消息id
	private final long time;//消息发送时间
	
	//用于传入消息的构造函数
    public LogEvent(InetSocketAddress source,String msg) {
        this(source, msg,-1,System.currentTimeMillis());
    }

    //用于传出消息的构造函数
    public LogEvent(InetSocketAddress source, long msgId,
                  String msg) {
        this(source,msg,msgId,System.currentTimeMillis());
    }
	
	public LogEvent(InetSocketAddress source, String msg, long msgId, long time) {
		this.source = source;
		this.msg = msg;
		this.msgId = msgId;
		this.time = time;
	}
    ...
}

通过数组来模拟要发送的日志内容

public class LogEventConst {
   
	 public final static int MONITOR_SIDE_PORT = 9998;
	    private static final String[] LOG_INFOS = {
	            "20180912:admin:Send sms to 10001",
	            "20180912:user1:Send email to [email protected]",
	            "20180912:user2:Happen Exception",
	            "20180912:user3:Send email to [email protected]", };

	    private final static Random r = new Random();
	    public static String getLogInfo(){
	        return LOG_INFOS[r.nextInt(LOG_INFOS.length-1)];
	    }
}

定义广播方,广播方通过向255.255.255.255广播地址及对应的端口上,进行发送广播

public class UdpBroadCastQuestionSide {

	private static final int PORT = 8761;
	private static EventLoopGroup  eventLoopGroup =new NioEventLoopGroup();
	private static Bootstrap bootstrap = new Bootstrap();
	
	public static void start(){
		try {
			bootstrap.group(eventLoopGroup).
			   channel(NioDatagramChannel.class).//指定UDP传输方式
			   option(ChannelOption.SO_BROADCAST, true) //设置开启广播
			    .handler(new UdpBroadCastQuestionSideEncoder());
			//由于UDP是无连接的,因此不需要建立连接
			Channel channel= bootstrap.bind(0).sync().channel();
			InetSocketAddress inetSocketAddress =new InetSocketAddress("255.255.255.255", PORT);
			System.out.println("广播服务启动......");
			int count = 0;
			while(true){
				channel.writeAndFlush(new LogEvent(inetSocketAddress, ++count,LogEventConst.getLogInfo()));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}
	
	public static void main(String[] args) {
		start();
	}
}

定义广播方的编码器,将每条日志消息实例,经过编码打包成DatagramPacket并发送到广播地址和对应端口上

public class UdpBroadCastQuestionSideEncoder extends MessageToMessageEncoder<LogEvent>{

	@Override
	protected void encode(ChannelHandlerContext ctx, LogEvent msg, List<Object> out) throws Exception {
		//获得日志内容
		byte[] msgByte= msg.getMsg().getBytes();
		//创建ByteBuf(2个long类型+消息内容的字节长度+1一个分隔符字节)
		ByteBuf byteBuf = ctx.alloc().buffer(8*2+msgByte.length+1);
		//将信息写入到byteBuf中
		byteBuf.writeLong(msg.getMsgId());
		byteBuf.writeLong(msg.getTime());
		byteBuf.writeByte(msg.SEPARATOR);
		byteBuf.writeBytes(msgByte);
		//向后续handler传递
		out.add(new DatagramPacket(byteBuf,msg.getSource()));
	}
}

定义广播订阅方,广播订阅方通过监听指定端口,然后对接收到的广播报文进行解码并输出对应报文信息

public class UdpBroadCastAnswerSide {
 
    private static final int PORT = 8761;
	
	private static EventLoopGroup  eventLoopGroup =new NioEventLoopGroup();
	//由于UDP是无连接的,因此接收方也是使用Bootstrap
	private static Bootstrap bootstrap = new Bootstrap();
	
	public static void startAnswer(){
		try {
			bootstrap.group(eventLoopGroup).
			   channel(NioDatagramChannel.class).//指定UDP传输方式
			   option(ChannelOption.SO_BROADCAST, true).//设置广播
			   option(ChannelOption.SO_REUSEADDR, true) //端口复用
			    .handler(new ChannelInitializer<DatagramChannel>() {

					@Override
					protected void initChannel(DatagramChannel ch) throws Exception {
						//先添加解码器
						ch.pipeline().addLast(new UdpBroadCastAnswerSideDecoder());
						//添加业务handler
						ch.pipeline().addLast(new UdpBroadCastAnswerSideHandler());
					}
				});
			//没有接受客户端连接的过程,监听本地端口即可
			ChannelFuture future=bootstrap.bind(PORT).sync();
			System.out.println("启动应答广播服务.....");
			future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}
	public static void main(String[] args) {
		startAnswer();
	}
}

定义广播订阅方的解码器,负责将收到的广播报文进行解码,解码成日志实例并传给后续业务handler进行处理

public class UdpBroadCastAnswerSideDecoder extends MessageToMessageDecoder<DatagramPacket>{

	@Override
	protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
		ByteBuf data= msg.content();
		//获得消息id
		long msgId=data.readLong();
		//消息发送时间
		long time = data.readLong();
		//分隔符
		byte sep = data.readByte();
		//获得消息内容
		//先获得分隔符的读索引位置,然后从该索引位置开始读,之后的才是消息内容
		String msgContent = data.slice(data.readerIndex(), data.readableBytes()).toString(CharsetUtil.UTF_8);
		//构造LogEvent
		LogEvent logEvent = new LogEvent(msg.sender(), msgContent, msgId, time);
		//解码成功,将logEvent向后面的handler传递
		out.add(logEvent);
	}
}

定义广播订阅方业务handler,负责接收解码后的报文信息

public class UdpBroadCastAnswerSideHandler extends SimpleChannelInboundHandler<LogEvent>{

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, LogEvent msg) throws Exception {
		System.out.println("收到广播消息:"+ msg.toString());
	}
}

测试

首先,启动广播方服务
在这里插入图片描述
接下来,开启广播订阅方服务,可以看到一些收到广播方发送的日志信息
在这里插入图片描述
再启动一个新的广播订阅方服务,也可以看到一些收到广播方发送的日志信息
Netty框架实战篇 - 实现UDP单播和广播_第3张图片

总结

本文介绍了关于UDP无连接协议相关的知识,并基于Netty实现了UDP的单播和广播,从实现上来就可以看出通过Netty我们可以很简单、方便的开发UDP协议相关的应用程序

你可能感兴趣的:(网络编程,netty,网络通信,udp,广播,udt)