侧边栏壁纸
博主头像
soulballad博主等级

技术文章记录及总结

  • 累计撰写 169 篇文章
  • 累计创建 26 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

3.Netty多协议开发和运用

soulballad
2022-03-06 / 0 评论 / 0 点赞 / 70 阅读 / 19,622 字
温馨提示:
本文最后更新于 2022-03-06,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1. HTTP协议开发

HTTP (超文本传输协议)协议是建立在TCP传输协议之上的应用层协议,它的发展是万维网协会和Internet工作小组IETF合作的结果。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990 年提出,经过多年的使用和发展,得到了不断地完善和扩展。

由于HTTP协议是目前Web开发的主流协议,基于HTTP的应用非常广泛,因此,掌握HTTP的开发非常重要,本节将重点介绍如何基于Netty的HTTP协议栈进行HTTP服务端和客户端开发。由于Netty的HTTP协议栈是基于Netty的NIO通信框架开发的,因此,Netty的HTTP协议也是异步非阻塞的。

1.1 HTTP协议介绍

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。

HTTP协议的主要特点如下:

  • 支持 Client/Server模式;
  • 简单————客户向服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;
  • 灵活————HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type加以标记;
  • 无状态————HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要之前的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快,负载较轻。

1.2 HTTP服务端开发

Netty的HTTP协议栈无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用,相比于传统的Tomeat、Jetty 等Web容器,它更加轻量和小巧,灵活性和定制性也更好。

HttpFileServer

public class HttpFileServer {

    private static final String DEFAULT_URL = "/src/main/java/com/soulballad/book/netty/d01";

    public static void main(String[] args) throws Exception {
        String url = DEFAULT_URL;
        int port = 8080;
        new HttpFileServer().run(port, url);
    }

    public void run(final int port, final String url) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 请求消息解码器
                            ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                            // 目的是将多个消息转换为单一的request或者response对象
                            ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                            //响应解码器
                            ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                            //目的是支持异步大文件传输()
                            ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                            // 业务逻辑
                            ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
                        }
                    });
            ChannelFuture f = b.bind("192.168.31.121", port).sync();
            System.out.println("HTTP 文件服务器启动,网址是: " + "http://192.168.31.121:" + port + url);
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

HttpFileServerHandler

public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String url;

    public HttpFileServerHandler(String url) {
        this.url = url;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {

        if (!req.decoderResult().isSuccess()) {
            sendError(ctx, BAD_REQUEST);
            return;
        }
        if (req.method() != GET) {
            sendError(ctx, METHOD_NOT_ALLOWED);
            return;
        }
        final String uri = req.uri();
        final String path = sanitizeUri(uri);
        if (path == null) {
            sendError(ctx, FORBIDDEN);
            return;
        }
        File file = new File(path);
        if (file.isHidden() || !file.exists()) {
            sendError(ctx, NOT_FOUND);
            return;
        }
        if (file.isDirectory()) {
            if (uri.endsWith("/")) {
                sendListing(ctx, file);
            } else {
                sendRedirect(ctx, uri + '/');
            }
            return;
        }
        if (!file.isFile()) {
            sendError(ctx, FORBIDDEN);
            return;
        }
        // 以只读的方式打开文件
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
        setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        if (isKeepAlive(req)) {
            response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        ctx.write(response);
        ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                if (total < 0) {
                    System.err.println("Transfer progress : " + progress);
                } else {
                    System.err.println("Transfer progress : " + progress + "/" + total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture future) throws Exception {
                System.out.println("Transfer complete.");
            }
        });
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if (!isKeepAlive(req)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            sendError(ctx, INTERNAL_SERVER_ERROR);
        }
    }

    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

    private String sanitizeUri(String uri) {
        try {
            uri = URLDecoder.decode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            try {
                uri = URLDecoder.decode(uri, "ISO-8859-1");
            } catch (UnsupportedEncodingException e1) {
                throw new Error();
            }
        }
         if (!uri.startsWith(url)) {
            return null;
        }
        if (!uri.startsWith("/")) {
            return null;
        }
        uri = uri.replace('/', File.separatorChar);
        if (uri.contains(File.separator + '.')
                || uri.contains('.' + File.separator) || uri.startsWith(".")
                || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
            return null;
        }
        String path = this.getClass().getResource("/").getPath();
        return path.replace("target/classes", "") + uri;
//        return System.getProperty("user.dir") + File.separator + uri;
    }

    private static final Pattern ALLOWED_FILE_NAME = Pattern
            .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

    private static void sendListing(ChannelHandlerContext ctx, File dir) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
        response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
        StringBuilder buf = new StringBuilder();
        String dirPath = dir.getPath();
        buf.append("<!DOCTYPE html>\r\n");
        buf.append("<html><head><title>");
        buf.append(dirPath);
        buf.append(" 目录:");
        buf.append("</title></head><body>\r\n");
        buf.append("<h3>");
        buf.append(dirPath).append(" 目录:");
        buf.append("</h3>\r\n");
        buf.append("<ul>");
        buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
        for (File f : dir.listFiles()) {
            if (f.isHidden() || !f.canRead()) {
                continue;
            }
            String name = f.getName();
            if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                continue;
            }
            buf.append("<li>链接:<a href=\"");
            buf.append(name);
            buf.append("\">");
            buf.append(name);
            buf.append("</a></li>\r\n");
        }
        buf.append("</ul></body></html>\r\n");
        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
        response.headers().set(LOCATION, newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void sendError(ChannelHandlerContext ctx,
                                  HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
                status, Unpooled.copiedBuffer("Failure: " + status.toString()
                + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(CONTENT_TYPE,
                mimeTypesMap.getContentType(file.getPath()));
    }
}

1.3 运行结果

启动HTTP文件服务器,通过浏览器进行访问,运行结果如下。

image-20200419205231924

我们首先进行异常场景的测试,输入错误的URL网址:

http://192.168.31.121:8080/abcde/get?123

运行结果如图所示:

image-20200419205231924

我们继续测试正常场景,在浏览器中输入正确的网址:

http://192.168.31.121:8080/src/main/java/com/soulballad/book/netty/

运行结果如下图:

image-20200419205231924

2. WebSocket协议开发

2.1 HTTP协议的弊端

将HTTP协议的主要弊端总结如下:
(1) HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输。它意味着在同一时刻,只有一个方向上的数据传送;
(2) HTTP消息冗长而繁琐。HTTP消息包含消息头、消息体、换行符等,通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长而繁琐;
(3)针对服务器推送的黑客攻击。例如长时间轮询。

现在,很多网站为了实现消息推送,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出 HTTP request,然后由服务器返回最新的数据给客户端浏览器。这种传统的模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,然而HTTP request的header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。

比较新的一种轮询技术是Comet,使用了Ajax。这种技术虽然可达到双向通信,但依然需要发出请求,而且在Comet中,普遍采用了长连接,这也会大量消耗服务器带宽和资源。

为了解决HTTP协议效率低下的问题,HTML5 定义了WebSocket 协议,能更好地节省服务器资源和带宽并达到实时通信,下个小节让我们一起来学习WebSocket的入门知识。

2.2 WebSocket入门

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通信的网络技术,WebSocket 通信协议于2011年被IETF定为标准RFC6455,WebSocket API被W3C定为标准。

在WebSocket API中,浏览器和服务器只需要做-一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。WebSocket 基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比于HTTP的半双工协议,性能得到很大提升。

下面总结一下WebSocket的特点:

  • 单一的TCP连接,采用全双工模式通信;
  • 对代理、防火墙和路由器透明;
  • 无头部信息、Cookie和身份验证;
  • 无安全开销;
  • 通过 “ping/pong” 帧保持链路激活;
  • 服务器可以主动传递消息给客户端,不再需要客户端轮询。

2.2.1 WebSocket背景

WebSocket设计出来的目的就是要取代轮询和Comet 技术,使客户端浏览器具备像C/S架构下桌面系统一样的实时通信能力。浏览器通过JavaSeript 向服务器发出建立WebSocket连接的请求,连接建立以后,客户端和服务器端可以通过TCP连接直接交换数据。因为WebSocket连接本质上就是一个TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及Comet 技术相比,具有很大的性能优势。Websocket.org 网站对传统的轮询方式和WebSocket调用方式作了一个详细的测试和比较,将一个简单的Web应用分别通过轮询方式和WebSocket 方式来实现,在这里引用一下测试结果, 如图11-1所示。

image-20200419205231924

通过对比图可以清楚地看出,在流量和负载增大的情况下,WebSocket方案相比传统的Ajax轮询方案有极大的性能优势。这也是为什么我们认为WebSocket是未来实时Web应用的首选方案的原因。

2.2.2 WebSocket连接建立

建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求消息示例如下所示。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXB3ZSBub25jZQ=-
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息 “Upgrade:WebSocket" 表明这是一个申请协议升级的HTTP请求。服务器端解析这些附加的头信息,然后生成应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由地传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动关闭连接。

服务端返回给客户端的应答消息如下 所示。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMB1TxaQ9kYGz zhZRbK+x0o=
Sec-WebSocket-Protocol: chat

请求消息中的 “Sec-WebSocket-Key" 是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要,把 “Sec-WebSocket-Key" 加上-一个魔幻字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,然后进行BASE-64编码,将结果做为 “Sec-WebSocket-Acept” 头的值,返回给客户端。

2.2.3 WebSocket生命周期

握手成功之后,服务端和客户端就可以通过 “messages" 的方式进行通信了,一个消息由一个或者多个帧组成,WebSocket的消息并不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合并。

帧都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据。从广义上讲,数据类型可以是文本数据(UTF-8[RFC3629]文字)、二进制数据和控制帧(协议级信令,如信号)。

WebSocket连接生命周期示意图如图11-5 所示。

image-20200419205231924

2.2.4 WebSocket连接关闭

为关闭WebSocket连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TLS会话。如果合适,丢弃任何可能已经接收的字节;必要时(比如受到攻击),可以通过任何可用的手段关闭连接。

底层的TCP连接,在正常情况下,应该首先由服务器关闭。在异常情况下( 例如在一个合理的时间周期后没有接收到服务器的TCP Close),客户端可以发起TCP Close。因此,当服务器被指示关闭WebSocket连接时,它应该立即发起一个TCP Close操作;客户端应该等待服务器的TCP Close。

WebSocket的握手关闭消息带有一个状态码和-一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。

2.3 Netty WebSocket协议开发

Netty基于HTTP协议栈开发了WebSocket 协议栈,利用Netty的WebSocket协议栈可以非常方便地开发出WebSocket客户端和服务端。

2.3.1 WebSocket服务端开发

首先对WebSocket服务端的功能进行简单地讲解。WebSocket服务端接收到请求消息之后,先对消息的类型进行判断,如果不是WebSocket握手请求消息,则返回HTTP 400BAD REQUEST响应给客户端。

客户端的握手请求消息如图11-8所示。

image-20200419205231924

服务端对握手请求消息进行处理,构造握手响应返回,双方的Socket连接正式建立,服务端返回的握手应答消息如图11-9所示。

image-20200419205231924

连接建立成功后,到被关闭之前,双方都可以主动向对方发送消息,这点跟HTTP的一请求一应答模式存在很大的差别。相比于HTTP,它的网络利用率更高,可以通过全双工的方式进行消息发送和接收。

WebSocketServer

public class WebSocketServer {

    public static void main(String[] args) throws Exception {
        int port = 8080;
        new WebSocketServer().run(port);
    }

    public void run(final int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast("http-codec", new HttpServerCodec());
                            ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                            ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                            ch.pipeline().addLast("handler", new WebSocketServerHandler());
                        }
                    });
            Channel ch = b.bind(port).sync().channel();
            System.out.println("Open your browser and navigate to http://localhost:" + port + "/");
            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

WebSocketServerHandler

public class WebSocketServerHandler extends SimpleChannelInboundHandler {

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            // 传统的HTTp接入
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            // WebSocket接入
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }


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

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        // 如果HTTP解码失败,返回HHTP异常
        if (!req.decoderResult().isSuccess() || !"websocket".equals(req.headers().get("Upgrade"))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
            return;
        }

        // 构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
            return;
        }
        // 判断是否是Ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 本例仅支持文本消息,不支持二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format("%s frame type not supported", frame.getClass().getName()));
        }
        // 返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        ctx.channel().write(new TextWebSocketFrame(request + ", 欢迎使用 Netty WebSocket 服务,现在时刻: " + new Date().toString()));
    }

    private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse resp) {
        if (resp.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(resp.status().toString(), CharsetUtil.UTF_8);
            resp.content().writeBytes(buf);
            buf.release();
            setContentLength(resp, resp.content().readableBytes());
        }
        // 如果不是Keep-Alive,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(resp);
        if (!isKeepAlive(req) || resp.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

2.3.2 WebSocket客户端

使用js来创建 websocket 连接

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    Netty WebSocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8080/websocket");
        socket.onmessage = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = event.data
        };
        socket.onopen = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
        };
        socket.onclose = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = "WebSocket 关闭!";
        };
    } else {
        alert("抱歉,您的浏览器不支持WebSocket协议!");
    }

    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("WebSocket连接没有建立成功!");
        }
    }
</script>
<form onsubmit="return false;">
    <input type="text" name="message" value="Netty最佳实践"/>
    <br><br>
    <input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/>
    <hr color="blue"/>
    <h3>服务端返回的应答消息</h3>
    <textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>

2.3.3 运行结果

服务端结果:

image-20200419205231924

浏览器端:

image-20200419205231924

点击 “ 发送WebSocket请求消息”

image-20200419205231924

3. UDP协议开发

UDP是用户数据报协议( User Datagram Protocol, UDP)的简称,其主要作用是将网络数据流量压缩成数据报形式,提供面向事务的简单信息传送服务。与TCP协议不同,UDP协议直接利用IP协议进行UDP数据报的传输,UDP提供的是面向无连接的、不可靠的数据报投递服务。当使用UDP协议传输信息时,用户应用程序必须负责解决数据报丢失、重复、排序,差错确认等问题。

由于UDP具有资源消耗小、处理速度快的优点,所以通常视频、音频等可靠性要求不高的数据传输一般会使用UDP,即便有一定的丢包率,也不会对功能造成严重的影响。

3.1 UDP协议简介

UDP是无连接的,通信双方不需要建立物理链路连接。在网络中它用于处理数据包,在OSI模型中,它处于第四层传输层,即位于IP协议的上一层。它不对数据报分组、组装、校验和排序,因此是不可靠的。报文的发送者不知道报文是否被对方正确接收。

UDP数据报格式有首部和数据两个部分,首部很简单,为8个字节,包括以下部分。

  • (1)源端口:源端口号,2个字节,最大值为65535;
  • (2)目的端口:目的端口号,2个字节,最大值为65535;
  • (3)长度: 2字节,UDP用户数据报的总长度;
  • (4)校验和: 2字节,用于校验UDP数据报的数字段和包含UDP数据报首部的“伪首部”。其校验方法类似于IP分组首部中的首部校验和。

伪首部,又称为伪包头(Pseudo Header):是指在TCP的分段或UDP的数据报格式中,在数据报首部前面增加源IP 地址、目的IP地址、IP分组的协议字段、TCP或UDP数据报的总长度等,共12字节,所构成的扩展首部结构。此伪首部是一个临时的结构,它既不向。上也不向下传递,仅仅是为了保证可以校验套接字的正确性。.

UDP协议数据格式如下图所示:

image-20200419205231924

UDP协议的特点如下:

  • (1) UDP传送数据前并不与对方建立连接,即UDP是无连接的。在传输数据前,发送方和接收方相互交换信息使双方同步;
  • (2)UDP对接收到的数据报不发送确认信号,发送端不知道数据是否被正确接收,也不会重发数据;
  • (3) UDP传送数据比TCP快速,系统开销也少:UDP比较简单,UDP头包含了源端口、目的端口、消息长度和校验和等很少的字节。由于UDP比TCP简单、灵活,常用于可靠性要求不高的数据传输,如视频、图片以及简单文件传输系统(TFTP) 等。TCP则适用于可靠性要求很高但实时性要求不高的应用,如文件传输协议FTP、超文本传输协议HTTP、简单邮件传输协议SMTP等。

在下面的章节我们一起学习如何通过Netty开发UDP协议客户端和服务端应用。

3.2 UDP服务端开发

由于UDP通信双方不需要建立链路,所以,代码相对于TCP更加简单-一些,下面来看服务端代码。

ChineseProverbServer

public class ChineseProverbServer {

    public static void main(String[] args) throws Exception {
        int port = 8080;
        new ChineseProverbServer().run(port);
    }

    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 ChineseProverbServerHandler());
            b.bind(port).sync().channel().closeFuture().await();
            System.out.println("Open your browser and navigate to http://localhost:" + port + "/");
        } finally {
            group.shutdownGracefully();
        }
    }
}

ChineseProverbServerHandler

public class ChineseProverbServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    private static final String[] DICTIONARY = {"只要功夫深,铁杵磨成针。", "旧时王谢堂前燕,飞入寻常百姓家。", "洛阳亲友如相问,一片冰心在玉壶。", "一寸光阴一寸金,寸金难买寸光阴。", "老骥伏枥,志在千里。烈士暮年,壮心不已。"};

    private String nextQuote() {
        int quote = ThreadLocalRandom.current().nextInt(DICTIONARY.length);
        return DICTIONARY[quote];
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
        String req = packet.content().toString(CharsetUtil.UTF_8);
        System.out.println(req);
        if ("谚语字典查询?".equals(req)) {
            ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("谚语查询结果: " + nextQuote(), CharsetUtil.UTF_8), packet.sender()));
        }
    }

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

3.3 UDP客户端开发

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

public class ChineseProverbClient {

    public static void main(String[] args) throws InterruptedException {
        new ChineseProverbClient().run(8080);
    }

    public void run(int port) throws InterruptedException {
        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("查询超时!");
            }
        } finally {
            group.shutdownGracefully();
        }
    }
}

ChineseProverbClientHandler

public class ChineseProverbClientHandler extends SimpleChannelInboundHandler<DatagramPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
        String resp = msg.content().toString(CharsetUtil.UTF_8);
        if (resp.startsWith("谚语查询结果: ")) {
            System.out.println(resp);
            ctx.close();
        }
    }

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

3.4 运行结果

首先启动UDP服务端,然后启动客户端(运行两次),查看运行结果。

服务端运行结果如图所示:

image-20200419205231924

客户端运行结果1如图所示:

image-20200419205231924

客户端运行结果2如图所示:

image-20200419205231924

0

评论区