UDP(用户数据报协议)是一种无连接的传输层协议,它主要用于不要求分组顺序到达的传输中,分组传输顺序的检查与排序由应用层完成,提供面向事务的简单不可靠信息传送服务。UDP协议基本上是IP协议与上层协议的接口,适用端口分别运行在同一台设备上的多个应用程序,同时UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等
UDP报文头由4个域组成,每个域各占用2个字节,其中包括目的端口号和源端口号信息,数据报的长度域是指包括报头和数据部分在内的总字节数,校验值域来保证数据的安全。由于通讯不需要连接,所以可以实现广播发送
TCP作为面向连接的传输协议,负责管理两个网络端点之间的连接的建立,在连接的生命周期内的有序和可靠的消息传输,以及最后,连接的有序终止。而相比之下,UDP无连接的协议,并没有持久化连接的概念,并且每个消息(一个 UDP数据报)都是一个单独的传输单元。此外,UDP也没有TCP的纠错机制,其中每个节点都将确认它们所接收到的包,而没有被确认的包将会被发送方重新传输
通过类比,TCP连接就像打电话,其中一系列的有序消息将会在两个方向上流动。相反, UDP则类似于往邮箱中投入一叠明信片。你无法知道它们将以何种顺序到达它们的目的地, 或者它们是否所有的都能够到达它们的目的地
并且UDP相对TCP其本身作为无连接的不可靠的传输协议,在传输过程中不会对数据包进行合并发送(TCP通常情况下存在Nagle算法会合并数据包),而是直接发送,因此既然不会对数据进行合并,每个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了
基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称 UDT)是一种互联网数据传输协议。UDT 的主要目的是支持高速广域网上的海量数据传输,最典型的例子就是建立在光纤广域网上的网格计算,一些研究所在这样的网络上运行他们的分布式的数据密集程序,例如,远程访问仪器、分布式数据挖掘和高分辨率的多媒体流
而互联网上的标准数据传输协议 TCP 在高带宽长距离网络上性能很差。顾名思义,UDT建于UDP 之上,并引入新的拥塞控制和数据可靠性控制机制。UDT 是面向连接的双向的应用层协议
UDT 的特性主要包括在以下几个方面:
Netty中提供了大量的类来支持UDP应用程序的编写,主要包括:
名称 | 描述 |
---|---|
interface AddressedEnvelope |
定义一个消息,其包装了另一个消息并带有发送者和接收者地址。其中 M 是消息类型; A 是地址类型 |
class DefaultAddressedEnvelope |
提供了 interface AddressedEnvelope 的默认实现 |
class DatagramPacket extends DefaultAddressedEnvelope |
扩展了 DefaultAddressedEnvelope 以使用 ByteBuf 作为消息数据容器,其中存在比较重要的方法:通过content()来获取消息内容、通过sender()来获取发送者的消息、通过recipient()来获取接收者的消息 |
interface DatagramChannel extends Channel | 扩展了 Netty 的 Channel 抽象以支持 UDP 的多播组管理 |
class NioDatagramChannnel extends AbstractNioMessageChannel implements DatagramChannel | 定义了一个能够发送和接收 Addressed-Envelope 消息的 Channel 类型 |
Netty 的 DatagramPacket 是一个简单的消息容器,DatagramChannel 实现用它来和远程节点通信。类似于在我们先前的类比中的明信片,它包含了接收者(和可选的发送者)的地址以及消息的有效负载本身
所谓的单播的传输模式,是定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。而面向连接的协议和无连接协议都是支持这种模式的
首先,配置发送方,发送方将传输方式设置为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()));
}
}
测试
首先,开启应答服务
接下来,开启发送方服务
然后,应答服务,接收到了发送报文
定义广播的日志实例,主要包括:消息内容、消息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());
}
}
测试
首先,启动广播方服务
接下来,开启广播订阅方服务,可以看到一些收到广播方发送的日志信息
再启动一个新的广播订阅方服务,也可以看到一些收到广播方发送的日志信息
本文介绍了关于UDP无连接协议相关的知识,并基于Netty实现了UDP的单播和广播,从实现上来就可以看出通过Netty我们可以很简单、方便的开发UDP协议相关的应用程序