天天看点

JAVA Socket编程学习9--Netty入门之Hello World!

内容来自(做了微小的改动):

http://www.cnblogs.com/zou90512/p/3492878.html

http://www.cnblogs.com/zou90512/p/3507729.html

在中国程序界。我们都是学着Hello World !慢慢成长起来的。逐渐从一无所知到熟悉精通的。

首先创建一个Java项目。引入一个Netty 框架的包。这个步骤我在本系列教程的后面就不在重复了。先上一张我示例的项目工程图给大家看一下:

JAVA Socket编程学习9--Netty入门之Hello World!

1.下载并为项目添加Netty框架

Netty的包大家可以从Netty官网:http://netty.io/downloads.html下载

JAVA Socket编程学习9--Netty入门之Hello World!

    如图所示(原作者写作时间是2013-12-26,我这里的截图时间是2017-12-05): Netty提供了三个主要版本的框架包给大家下载。

    3.10.6版本Final说明这个版本是3.x版本中最新的版本。final意味着功能不再继续添加更新。仅为修改bug等提供继续的更新。

    5.x版本由于是开始。不能排除是否稳定运行等问题。加上5.x在4.x的版本上略微修改的。在5.x稳定之前。不推荐大家学习使用。(我截图的时候根本就没有5.x版本,难道是不稳定删掉了?)

    本教程是基于Netty4.x(我这里下载的是稳定版的netty-4.0.53)版本的。

解压netty-4.0.53.Final.tar.bz2后,大家可以看到这样一个目录结构,非常的清晰。

JAVA Socket编程学习9--Netty入门之Hello World!

    第一个文件夹jar是jar包的文件夹。第二个javadoc是API文档。第三个license文件夹是开源的授权文件(可以直接无视)。

    javadoc文件夹下面是一个jar包。可以直接解压缩出来。解压缩之后的文件夹就是api文档(以网页的形势展现)。

    jar文件夹里面有很多的jar包和一个all-in-one文件夹。都是Netty框架的组成部分。all-in-one里面有两个文件一个是jar包,另一个是对应的source源代码包。这样做的目的是为了给程序员有选择的添加自己所需要的包。

    假如读者是初学者的话。推荐直接套用all-in-one里面的jar包。假如你熟悉Netty的话可以根据自己的项目需求添加不同的jar包。

2.创建Server服务端

    Netty创建全部都是实现自AbstractBootstrap(http://netty.io/5.0/api/io/netty/bootstrap/AbstractBootstrap.html)。客户端的是Bootstrap,服务端的则是ServerBootstrap。

2.1创建一个HelloServer

package NettyTest;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class HelloServer {
    
    /**
     * 服务端监听的端口地址
     */
    private static final int portNumber = 7878;
    
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup);
            b.channel(NioServerSocketChannel.class);
            b.childHandler(new HelloServerInitializer());

            // 服务器绑定端口监听
            ChannelFuture f = b.bind(portNumber).sync();
            // 监听服务器关闭监听
            f.channel().closeFuture().sync();

            // 可以简写为
            /* b.bind(portNumber).sync().channel().closeFuture().sync(); */
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
           

    EventLoopGroup 是在4.x版本中提出来的一个新概念。用于channel的管理。服务端需要两个。和3.x版本一样,一个是boss线程一个是worker线程。

    b.childHandler(new HelloServerInitializer());    //用于添加相关的Handler

    服务端简单的代码,真的没有办法在精简了感觉。就是一个绑定端口操作。

2.2创建和实现HelloServerInitializer

    在HelloServer中的HelloServerInitializer在这里实现。

    首先我们需要明确我们到底是要做什么的。很简单。Hello World!。我们希望实现一个能够像服务端发送文字的功能。服务端假如可以最好还能返回点消息给客户端,然客户端去显示。

    需求简单。那我们下面就准备开始实现。

    DelimiterBasedFrameDecoder Netty在官方网站上提供的示例显示 有这么一个解码器可以简单的消息分割。

    其次 在decoder里面我们找到了String解码编码器。着都是官网提供给我们的。

package NettyTest;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class HelloServerInitializer extends ChannelInitializer<SocketChannel> {

	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
		ChannelPipeline pipeline = ch.pipeline();

		// 以("\n")为结尾分割的 解码器
		pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));

		// 字符串解码 和 编码
		pipeline.addLast("decoder", new StringDecoder());
		pipeline.addLast("encoder", new StringEncoder());

		// 自己的逻辑Handler
		pipeline.addLast("handler", new HelloServerHandler());
	}
}
           

    上面的三个解码和编码都是Netty自带的。

    另外我们自己的Handler怎么办呢。在最后我们添加一个自己的Handler用于写自己的处理逻辑。

2.3 增加自己的逻辑HelloServerHandler

     自己的Handler我们这里先去继承extends官网推荐的SimpleChannelInboundHandler<C>。在这里C,由于我们需求里面发送的是字符串,这里的C改写为String。

package NettyTest;

import java.net.InetAddress;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class HelloServerHandler extends SimpleChannelInboundHandler<String> {

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
		// 收到消息直接打印输出
		System.out.println(ctx.channel().remoteAddress() + " Say : " + msg);

		// 返回客户端消息 - 我已经接收到了你的消息
		ctx.writeAndFlush("Received your message !\n");
	}

	/*
	 * 覆盖channelActive方法在channel被启用的时候触发 (在建立连接的时候)
	 * channelActive和channelInActive在后面的内容中讲述,这里先不做详细的描述
	 * */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("RamoteAddress : " + ctx.channel().remoteAddress() + " active !");
		ctx.writeAndFlush( "Welcome to " + InetAddress.getLocalHost().getHostName() + " service!\n");
		super.channelActive(ctx);
	}
}
           

    在channelHandlerContent自带一个writeAndFlush方法。方法的作用是写入Buffer并刷入。

注意:在3.x版本中此处有很大区别。在3.x版本中write()方法是自动flush的。在4.x版本的前面几个版本也是一样的。但是在4.0.9之后修改为WriteAndFlush。普通的write方法将不会发送消息。需要手动在write之后flush()一次

    这里channeActive的意思是当连接活跃(建立)的时候触发.输出消息源的远程地址。并返回欢迎消息。

    channelRead0 在这里的作用是类似于3.x版本的messageReceived()。可以当做是每一次收到消息是触发。

    我们在这里的代码是返回客户端一个字符串"Received your message !".

注意:字符串最后面的"\n"是必须的。因为我们在前面的解码器DelimiterBasedFrameDecoder是一个根据字符串结尾为“\n”来结尾的。假如没有这个字符的话。解码会出现问题。

3.Client客户端

     类似于服务端的代码。我们不做特别详细的解释。

     直接上示例代码:

package NettyTest;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class HelloClient {

	public static String host = "127.0.0.1";
	public static int port = 7878;

	/**
	 * @param args
	 * @throws InterruptedException 
	 * @throws IOException 
	 */
	public static void main(String[] args) throws InterruptedException, IOException {
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioSocketChannel.class).handler(new HelloClientInitializer());

			// 连接服务端
			Channel ch = b.connect(host, port).sync().channel();

			// 控制台输入
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			for (;;) {
				String line = in.readLine();
				if (line == null) {
					continue;
				}
				/*
				 * 向服务端发送在控制台输入的文本并用"\r\n"结尾
				 * 之所以用\r\n结尾是因为我们在handler中添加了DelimiterBasedFrameDecoder帧解码。
				 * 这个解码器是一个根据\n符号位分隔符的解码器。所以每条消息的最后必须加上\n否则无法识别和解码
				 * */
				ch.writeAndFlush(line + "\r\n");
			}
		} finally {
			// The connection is closed automatically on shutdown.
			group.shutdownGracefully();
		}
	}
}
           

下面的是HelloClientInitializer代码貌似是和服务端的完全一样。我没注意看。其实编码和解码是相对的。服务端和客户端都是解码和编码,才能通信。

package NettyTest;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class HelloClientInitializer extends ChannelInitializer<SocketChannel> {

	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
		ChannelPipeline pipeline = ch.pipeline();

		/*
		 * 这个地方的 必须和服务端对应上。否则无法正常解码和编码
		 * 解码和编码 我将会在下一张为大家详细的讲解。再次暂时不做详细的描述
		 * */
		pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
		pipeline.addLast("decoder", new StringDecoder());
		pipeline.addLast("encoder", new StringEncoder());

		// 客户端的逻辑
		pipeline.addLast("handler", new HelloClientHandler());
	}
}
           

HellClientHandler:

package NettyTest;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class HelloClientHandler extends SimpleChannelInboundHandler<String> {

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
		System.out.println("Server say : " + msg);
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("Client active ");
		super.channelActive(ctx);
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("Client close ");
		super.channelInactive(ctx);
	}
}
           

下面上几张成果图:

客户端在连接建立是输出了Client active 信息,并收到服务端返回的Welcome消息。

输入Hello World!回车发送消息。服务端响应返回消息已接受。

JAVA Socket编程学习9--Netty入门之Hello World!

客户端控制台截图

JAVA Socket编程学习9--Netty入门之Hello World!

服务端控制台截图

4.Hello World代码详解

4.1HelloServer详解

    HelloServer首先定义了一个静态终态的变量---服务端绑定端口7878。至于为什么是这个7878端口,纯粹是笔者个人喜好。大家可以按照自己的习惯选择端口。当然了。常用的几个端口(例如:80,8080,843(Flash及Silverlight策略文件请求端口等等),3306(Mysql数据库占用端口))最好就不要占用了,避免一些奇怪的问题。

    HelloServer类里面的代码并不多。只有一个main函数,加上内部短短的几行代码。

    Main函数开始的位置定义了两个工作线程,一个命名为WorkerGroup,另一个命名为BossGroup。都是实例化NioEventLoopGroup。这一点和3.x版本中基本思路是一致的。Worker线程用于管理线程为Boss线程服务。

    讲到这里需要解释一下EventLoopGroup,它是4.x版本提出来的一个新概念。类似于3.x版本中的线程。用于管理Channel连接的。在main函数的结尾就用到了EventLoopGroup提供的便捷的方法,shutdownGraceFully(),翻译为中文就是优雅的全部关闭。感觉是不是很有意思。作者居然会如此可爱的命名了这样一个函数。查看相应的源代码。我们可以在DefaultEventExecutorGroup的父类MultithreadEventExecutorGroup中看到它的实现代码。关闭了全部EventExecutor数组child里面子元素。相比于3.x版本这是一个比较重大的改动。开发者可以很轻松的全部关闭,而不需要担心出现内存泄露。

    在try里面实例化一个ServerBootstrap b。设置group。设置channel为NioServerSocketChannel。

设置childHandler,在这里使用实例化一个HelloServerInitializer类来实现,继承ChannelInitializer<SocketChannel>。内部的代码我们可以在前文的注视中大致了解一下,主要作用是设置相关的字节解码编码器,和代码处理逻辑。Handler是Netty包里面占很大一个比例。可见其的作用和用途。Handler涉及很多领域。HTTP,UDP,Socket,WebSocket等等。详细的部分在本章的第三节解释。

    设置好Handler绑定端口7878,并调用函数sync(),监听端口(等待客户端连接和发送消息)。并监听端口关闭(为了防止线程停止)。

    最后finally我们要优雅的全部关闭服务端。^_^

4.2HelloClient详解

    相比于服务端的代码。客户端要精简一些。

    客户端仅仅只需要一个worker的EventLoopGroup。其次是类似于ServerBootstrap的HandlerInitializer。

    唯一不同的可能就是客户端的connect方法。服务端的绑定并监听端口,客户端是连接指定的地址。Sync().channel()是为了返回这个连接服务端的channel,并用于后面代码的调用。

    BufferedReader这个是用于控制台输入的。不做详细的解释了就。大家都懂的。

    当用户输入一行内容并回车之后。循环的读取每一行内容。然后使用writeAndFlush向服务端发送消息。

4.3HandlerInitializer详解

    Handler在Netty中是一个比较重要的概念。有着相当重要的作用。相比于Netty的底层。我们接触更多的应该是他的Handler。在这里我将它剥离出来单独解释。

    ServerHandlerInitializer继承与ChannelInitializer<SocketChannel>需要我们实现一个initChannel()方法。我们定义的handler就是写在这里面。

    在最开始的地方定义了一个DelimiterBasedFrameDecoder。按直接翻译就是基于分隔符的帧解码器。再一次感觉框架的作者的命名,好直接好简单。详细的内容我们在后面的文章中在为大家详细的解释。目前大家知道他是以分隔符为分割标准的解码器就好了。

    也许有人会问分隔符是什么?我只能 !*_* :“纳尼 !!”。分隔符其实就是“\n”我们在学习C语言的时候最常用的的也许就是这个分隔符了吧。

    下面的则是StringDecoder和StringEncoder。字符串解码器和字符串编码器。

    最后面则是我们自己的逻辑。服务/客户端逻辑是在消息解码之后处理的。然后服务/客户端返回相关消息则是需要对消息进行相对应的编码。最终才是以二进制数据流的形势发送给服务/客户端的。

Netty的UDP实现(http://www.cnblogs.com/wade-luffy/p/6180770.html):

服务端开发

由于UDP通信双方不需要建立链路,所以,代码相对于TCP更加简单一些,代码如下:启动类ChineseProverbServer类

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;

//由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
public class ChineseProverbServer {
	public void run(int port) throws Exception{
		EventLoopGroup group = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		//由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
		b.group(group).channel(NioDatagramChannel.class)
		.option(ChannelOption.SO_BROADCAST, true)//支持广播
		.handler(new ChineseProverbServerHandler());//ChineseProverbServerHandler是业务处理类
		b.bind(port).sync().channel().closeFuture().await();
	}
	public static void main(String [] args) throws Exception{
		int port = 8080;
		new ChineseProverbServer().run(port);
	}
}
           

    由于使用UDP通信,在创建Channel的时候通过NioDatagramChannel来创建,随后设置Socket参数支持广播,最后设置处理handler ChineseProverbServerHandler. 

    相比于TCP通信,UDP不存在客户端和服务器端的实际连接,因此不需要为连接(channelPipeline)设置handler,对于服务端,只需设置启动辅助类的handler即可。

下面看ChineseProverbServerHandler的实现

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.ThreadLocalRandom;

/**
 * @author 作者 YYD
 * @version 创建时间:2016年11月18日 下午8:43:10
 * @function 未添加
 */
public class ChineseProverbServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
	//谚语列表
	private static final String[] DICTIONARY = { "只要功夫深,铁棒磨成针。",
		"旧时王谢堂前燕,飞入寻常百姓家。", "洛阳亲友如相问,一片冰心在玉壶。", "一寸光阴一寸金,寸金难买寸光阴。",
		"老骥伏枥,志在千里,烈士暮年,壮心不已" };
	private String nextQuote(){
		//返回0-DICTIONARY.length中的一个整数。
		int quoteId = ThreadLocalRandom.current().nextInt(DICTIONARY.length);
		return DICTIONARY[quoteId];//将谚语列表中对应的谚语返回
	}
	/**
	 * 在这个方法中,形参packet客户端发过来的DatagramPacket对象
	 * DatagramPacket 类解释
	 * 1.官网是这么说的:
	 * The message container that is used for {@link DatagramChannel} to communicate with the remote peer.
	 * 翻译:DatagramPacket 是消息容器,这个消息容器被 DatagramChannel使用,作用是用来和远程设备交流
	 * 2.看它的源码我们发现DatagramPacket是final类不能被继承,只能被使用。我们还发现DatagramChannel最终实现了AddressedEnvelope接口,接下来我们看一下AddressedEnvelope接口。
	 * AddressedEnvelope接口官网解释如下:
	 * A message that wraps another message with a sender address and a recipient address.
	 * 翻译:这是一个消息,这个消息包含发送者和接受者消息
	 * 3.那我们知道了DatagramPacket它包含了发送者和接受者的消息,
	 * 通过content()来获取消息内容
	 * 通过sender();来获取发送者的消息
	 * 通过recipient();来获取接收者的消息。
	 * 
	 * 4.public DatagramPacket(ByteBuf data, InetSocketAddress recipient) {}
	 * 这个DatagramPacket其中的一个构造方法,data 是发送内容;是发送都信息。
	 */
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
		String req = packet.content().toString(CharsetUtil.UTF_8);//上面说了,通过content()来获取消息内容
		System.out.println(req);
		if("谚语字典查询?".equals(req)){//如果消息是“谚语字典查询?”,就随机获取一条消息发送出去。
			/**
			 * 重新 new 一个DatagramPacket对象,我们通过packet.sender()来获取发送者的消息。
			 * 重新发达出去!
			 */
			ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("谚语查询结果:"+nextQuote(),CharsetUtil.UTF_8), packet.sender()));
		}
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		ctx.close();
		cause.printStackTrace();
	}
}
           

解释:请看channelRead0方法,netty对UDP进行了封装,接收到的是DatagramPacket对象,然后通过packet.content().toString(CharsetUtil.UTF_8)获取packet的内容。如果是“谚语字典查询?”字符串则随机取一个谚语返回。DatagramPacket有二个参数,第一个是发送的内容,另一个是接收者的相关信息,这个可以通过packet.sender()获取。

客户端开发

     UDP程序的客户端和服务器端代码非常相似,唯一不同是UDP客户端会主动构造请求消息,向本网段内的所有主机请求消息,对于服务端而言接收到广播消息之后向广播消息的发起方进行定点发送。

启动类ChineseProverbClient类

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
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.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.CharsetUtil;

import java.net.InetSocketAddress;

/**
 * @author 作者 YYD
 * @version 创建时间:2016年11月18日 下午9:00:11
 * @function 未添加
 */
public class ChineseProverbClient {
	public void run(int port) throws Exception{
		EventLoopGroup group  = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioDatagramChannel.class)
			.option(ChannelOption.SO_BROADCAST,true)//允许广播
			.handler(new ChineseProverbClientHandler());//设置消息处理器
			Channel ch = b.bind(0).sync().channel();
			//向网段内的所有机器广播UDP消息。
			ch.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("谚语字典查询?",CharsetUtil.UTF_8), new InetSocketAddress("255.255.255.255",port))).sync();
			if(!ch.closeFuture().await(15000)){
				System.out.println("查询超时!");
			}
		} catch (Exception e) {
			group.shutdownGracefully();
		}
	}
	public static void main(String [] args) throws Exception{
		int port = 8080;
		new ChineseProverbClient().run(port);
	}
}
           

解释:创建UDPChannel和设置支持广播属性等与服务端完全一致,由于不需要和服务端建立链路,UDP Channel 创建完成之后,客户端就要主动发送广播消息。而TCP客户端是在客户端和服务端链路建立成功之后由客户端的业务handler发送消息,这就是二者最大区别。

ChineseProverClientHandler类

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;

/**
 * @author 作者 YYD
 * @version 创建时间:2016年11月18日 下午9:09:18
 * @function 未添加
 */
public class ChineseProverbClientHandler extends SimpleChannelInboundHandler<DatagramPacket> {
	/**
	 * DatagramPacket的详细介绍,看服务器的代码注释,这里不重复了。
	 */
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
		String response = msg.content().toString(CharsetUtil.UTF_8);
		if (response.startsWith("谚语查询结果:")) {
			System.out.println(response);
			ctx.close();
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
}
           

    代码非常简单,接收到服务端的消息之后将其转成字符串然后判断是否以“谚语查询结果:”开头,如果没有发生丢包等问题,数据是完整的,就打印查询结果,然后释放资源。

运行结果:

先启动UDP服务端,然后启动客户端(运行二次),运行结果如下:

服务器运行结果:

JAVA Socket编程学习9--Netty入门之Hello World!

客户端1运行结果:

JAVA Socket编程学习9--Netty入门之Hello World!

客户端2运行结果:

JAVA Socket编程学习9--Netty入门之Hello World!

通过上图我们可以看出,客户端每次运行结果都不一样,说明UDP服务器的查询功能正确,并且客户端成功的接受到了服务器的应答,说明整个过程没有丢包和乱序问题。