Netty(二)

4 篇文章 0 订阅

Netty 二

Netty模块组件

  • ServerBootstrap& Bootstrap:
  1. 在 Netty, ServerBootstrap类是服务端的启动引导类, Bootstrap类是客户端的启动引导类. 主要作用是配置整个 Netty程序
  2. 常见方法:
    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup), 该方法用于服务器端设置两个 EventLoopGroup(一个Boss Group, 一个Worker Group)
    public B group(EventLoopGroup group), 该方法用于客户端设置一个 NioEventLoopGroup
    public B channel(Class<? extends C> channelClass),该方法用来设置 SocketChannel
    public B option(ChannelOption option, T value), 该方法用来设置服务端通道的的 ChannelOption配置
    public ServerBootstrap childOption(ChannelOption childOption, T value), 该方法用来设置给接收到的通道添加 ChannelOption配置
    public ServerBootstrap childHandler(ChannelHandler childHandler), 该方法用来设置自定义业务处理类
    public ChannelFuture bind(int inetPort), 该方法用于服务器端设置监控端口号
    public ChannelFuture connect(String inetHost, int inetPort), 该方法用于客户端连接服务器端
  • Future& ChannelFuture:
  1. Netty中所有的 IO操作都是异步的, 也就是不能立刻得知消息是否被正确处理, 而是需要通过监听获取结果
  2. 常见方法: sync()等待异步操作执行完毕
  • Channel:
  1. 通过 Channel可获得当前网络连接的通道的状态以及配置参数 如接收缓冲区大小
  2. Channel提供异步的网络 I/O操作 如建立连接, 读写, 绑定端口
  3. 调用后返回一个 ChannelFuture实例, 可以注册监听器来获得完成结果
  4. 支持关联 I/O操作与对应的处理程序
  5. 不同协议, 不同阻塞类型的连接都有不同的 Channel类型与之对应, 常用的 Channel类型:
    NioSocketChannel, 异步的客户端 TCP Socket连接
    NioServerSocketChannel, 异步的服务器端 TCP Socket连接
    NioDatagramChannel, 异步的 UDP连接
    NioSctpChannel, 异步的客户端 Sctp连接
    NioSctpServerChannel, 异步的 Sctp服务器端连接
  • Selector:
  1. Netty基于 Selector对象实现 I/O多路复用, 通过 Selector一个线程可以监听多个连接的 Channel事件
  2. 当向一个 Selector中注册 Channel后, Selector内部机制就可以自动不断地查询这些注册的 Channel是否有已就绪的 I/O事件 如可读, 可写, 网络连接完成等
  • ChannelHandler及其实现类:
  1. ChannelHandler是一个接口, 处理 I/O事件或拦截 I/O操作, 并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序
  2. ChannelHandler本身并没有提供很多方法, 可继承它的子类
  3. ChannelHandler 及其实现类一览图(后)
    ChannelInboundHandler用于处理入站 I/O事件
    ChannelOutboundHandler用于处理出站 I/O操作
    ChannelInboundHandlerAdapter(适配器), 用于处理入站 I/O事件
    ChannelOutboundHandlerAdapter(适配器), 用于处理出站 I/O操作
    ChannelDuplexHandler(适配器), 用于处理入站和出站事件

在这里插入图片描述

  1. 我们经常需要自定义一个 Handler类去继承 ChannelInboundHandlerAdapter, 然后通过重写相应方法实现业务逻辑

public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
	public ChannelInboundHandlerAdapter() {}

	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		ctx.fireChannelRegistered();
	}

	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		ctx.fireChannelUnregistered();
	}

	// 通道就绪事件
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		ctx.fireChannelActive();
	}

	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		ctx.fireChannelInactive();
	}

	// 通道读取数据事件
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ctx.fireChannelRead(msg);
	}
}

  • Pipeline& ChannelPipeline
  1. ChannelPipeline是一个 Handler的集合, 它负责处理和拦截 inbound或 outbound的事件和操作, 相当于一个贯穿 Netty的链(可以理解为 ChannelPipeline是保存 ChannelHandler的 List, 用于处理或拦截 Channel的入站事件和出站操作)
  2. ChannelPipeline实现了一种拦截过滤器模式, 使用户可以完全控制事件的处理方式, 以及 Channel中各个 ChannelHandler, 如何相互交互
  3. 在 Netty中每个 Channel都有且仅有一个 ChannelPipeline与之对应, 它们的组成关系如下

在这里插入图片描述

(-) 一个 Channel包含了一个 ChannelPipeline, 而 ChannelPipeline中又维护了一个由 ChannelHandlerContext组成的双向链表, 并且每个 ChannelHandlerContext中又关联着一个 ChannelHandler
(-) 入站事件和出站事件在一个双向链表中, 入站事件会从链表 head往后传递到最后一个入站的 handler, 出站事件会从链表 tail往前传递到最前一个出站的 handler, 两种类型的 handler互不干扰
4) 常用方法:
ChannelPipeline addFirst(ChannelHandler… handlers), 把一个业务处理类(handler), 添加到链中的第一个位置
ChannelPipeline addLast(ChannelHandler… handlers), 把一个业务处理类(handler), 添加到链中的最后一个位置

  • ChannelHandlerContext
  1. 保存 Channel相关的所有上下文信息, 同时关联一个 ChannelHandler对象
  2. ChannelHandlerContext中包含一个具体的事件处理器 ChannelHandler, 同时 ChannelHandlerContext中也绑定了对应的 pipeline和 Channel的信息
  3. 常用方法:
    ChannelFuture close(), 关闭通道
    ChannelOutboundInvoker flush(), 刷新
    ChannelFuture writeAndFlush(Object msg), 将数据写到 ChannelPipeline中当前 ChannelHandler的下一个 ChannelHandler开始处理(出站)
  • ChannelOption
  1. Netty在创建 Channel实例后, 设置 ChannelOption参数
  2. ChannelOption参数如下:
    ChannelOption.SO_BACKLOG: 对应 TCP/IP协议 listen函数中的 backlog参数, 用来初始化服务器可连接队列大小. 服务端处理客户端连接请求是顺序处理的, 所以同一时间只能处理一个客户端连接. 多个客户端来的时候, 服务端将不能处理的客户端连接请求放在队列中等待处理, backlog参数指定了队列的大小
    ChannelOption.SO_KEEPALIVE: 一直保持连接活动状态
  • EventLoopGroup和其实现类 NioEventLoopGroup
  1. EventLoopGroup是一组 EventLoop的抽象, Netty为了更好的利用多核 CPU资源, 一般会有多个 EventLoop同时工作, 每个 EventLoop维护着一个 Selector实例
  2. EventLoopGroup提供 next接口, 可以从组里面按照一定规则获取其中一个 EventLoop来处理任务. 在 Netty服务器端编程中, 我们一般都需要提供两个 EventLoopGroup, 如 BossEventLoopGroup和 WorkerEventLoopGroup
  3. 通常一个服务端口即一个 ServerSocketChannel对应一个 Selector和一个 EventLoop线程. BossEventLoop负责接收客户端的连接并将 SocketChannel交给 WorkerEventLoopGroup来进行 IO处理, 如下图所示

在这里插入图片描述

(-) BossEventLoopGroup通常是一个单线程的 EventLoop, EventLoop维护着一个注册了 ServerSocketChannel的 Selector实例 BossEventLoop不断轮询 Selector将连接事件分离出来
(-) 通常是 OP_ACCEPT事件, 然后将接收到的 SocketChannel交给 WorkerEventLoopGroup
(-) WorkerEventLoopGroup会由 next选择其中一个 EventLoop来将这个 SocketChannel注册到其维护的 Selector并对其后续的 IO事件进行处理
4) 常用方法:
public NioEventLoopGroup(), 构造方法
public Future<?> shutdownGracefully(), 断开连接, 关闭线程

  • Unpooled类
  1. Netty 提供一个专门用来操作缓冲区(即 Netty 的数据容器)的工具类
  2. 常用方法:
    public static ByteBuf copiedBuffer(CharSequence string, Charset charset), 通过给定的数据和字符编码返回一个 ByteBuf对象

在这里插入图片描述


# ByteBuf的基本使用
public class NettyByteBuf {
    public static void main(String[] args) {
        // 1. 创建 netty的 ByteBuf对象, 该对象包含一个数组 arr, 是一个 byte[10]
        // 2. ByteBuf不需要使用 flip()反转
        // 3. 底层维护了 readerindex和 writerIndex, 通过 readerindex, writerIndex和 capacity
        // readerindex 已经读取的区域
        // readerindex ~ writerIndex, 可读的区域
        // writerIndex ~ capacity, 可写的区域
        ByteBuf buffer = Unpooled.buffer(10);
        for (int i = 0; i < 8; i++) {
            buffer.writeByte(i);
        }
        System.out.println("capacity=" + buffer.capacity()); // 10
        System.out.println("readerIndex=" + buffer.readerIndex()); // 0
        System.out.println("writerIndex=" + buffer.writerIndex()); // 8

        for (int i = 0; i < 3; i++) {
            System.out.println(buffer.getByte(i)); // 此方式不累加 readerIndex
        }
        System.out.println("readerIndex=" + buffer.readerIndex()); // 0
        System.out.println("writerIndex=" + buffer.writerIndex()); // 8

        for (int i = 0; i < 5; i++) {
            System.out.println(buffer.readByte()); // 此方式会累加 readerIndex
        }
        System.out.println("readerIndex=" + buffer.readerIndex()); // 5
        System.out.println("writerIndex=" + buffer.writerIndex()); // 8
    }

}

# Unpooled获取 ByteBuf的基本使用:
public class NettyByteBuf {
    public static void main(String[] args) {
        // 创建 ByteBuf
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello, world!", CharsetUtil.UTF_8);
        // 使用相关的方法
        if (byteBuf.hasArray()) {
            byte[] content = byteBuf.array();
            // 将 content 转成字符串
            System.out.println(new String(content, CharsetUtil.UTF_8));
            System.out.println("byteBuf=" + byteBuf); // byteBuf=UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 13, cap: 39)
            System.out.println(byteBuf.arrayOffset()); // 0
            System.out.println(byteBuf.readerIndex()); // 0
            System.out.println(byteBuf.writerIndex()); // 13
            System.out.println(byteBuf.capacity()); // 39
            System.out.println(byteBuf.getByte(0)); // 104

            int len = byteBuf.readableBytes();
            System.out.println("len=" + len); // len=13
            // 使用 for取出各个字节
            for (int i = 0; i < len; i++) {
                System.out.println((char) byteBuf.getByte(i));
            }
            // 按照某个范围读取
            System.out.println(byteBuf.getCharSequence(0, 4, CharsetUtil.UTF_8)); // hell
            System.out.println(byteBuf.getCharSequence(4, 6, CharsetUtil.UTF_8)); // o, wor
        }
    }

}

  • Netty实例(群聊系统)

public class GroupChatServer {
    private int port;

    public GroupChatServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
          EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 向 pipeline加解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            // 向 pipeline加编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            // 加自己的业务处理(handler)
                            pipeline.addLast(new GroupChatServerHandler());
                        }
                    });
            ChannelFuture cf = b.bind(port).sync();
            // 监听关闭
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatServer(7000).run();
    }

}

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
    // 使用 map, 管理所有 channel; 或此处可以使用 db来管理
    //public static ConcurrentMap<String, Channel> channels = new ConcurrentHashMap<>();

    // 创建 ChannelGroup, 管理所有 channel;
    // GlobalEventExecutor.INSTANCE是全局的事件执行器, 是一个单例
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    // 一个用户首次建立连接, 第一个被执行的方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        // 该方法会将 ChannelGroup中所有的 channel遍历, 并发送消息
        channelGroup.writeAndFlush(LocalDateTime.now() + " [客户] " + channel.remoteAddress() + " 加入聊天!\n");
        // 将当前通道加到 ChannelGroup
        channelGroup.add(channel);
    }

    // 触发执行 channelInactive方法后, 触发此方法
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        // 发送消息给当前在线的所有用户
        channelGroup.writeAndFlush(LocalDateTime.now() + " [客户] " + channel.remoteAddress() + " 离开了!\n");
        System.out.println("User count=" + channelGroup.size());
    }

    // channel处于, 活动状态时(上线), 触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " 上线了! User count=" + channelGroup.size());
    }

    // channel处于, 不活动状态时(下线), 触发
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " 离线了! User count=" + channelGroup.size());
    }

    // 读取数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel currentChannel = ctx.channel();
        // 遍历 channelGroup
        channelGroup.forEach(ch -> {
            if (currentChannel == ch) {
                ch.writeAndFlush(LocalDateTime.now() + " [我] 发送了消息: " + msg);
            } else {
                ch.writeAndFlush(LocalDateTime.now() + " [客户] " + currentChannel.remoteAddress() + " 发送了消息: " + msg);
            }
        });
    }

    // 捕获异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close(); // 关闭通道
    }

}

public class GroupChatClient {
    private final String host;
    private final int port;

    public GroupChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 加相关 handler
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            // 加自定义 handler
                            pipeline.addLast(new GroupChatClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            // 取得 channel
            Channel channel = channelFuture.channel();
            System.out.println("localAddress: " + channel.localAddress());
            // 创建标准输入流, 用于客户端输入信息
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                // 通过 channel发信息到服务器端
                channel.writeAndFlush(msg + "\r\n");
            }
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatClient("127.0.0.1", 7000).run();
    }

}

public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    // 客户端读取信息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg.trim());
    }

}

  • Netty实例(心跳检测机制)

public class MyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)); // 配置服务器日志方式
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    /*
                    1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
                    2. long readerIdleTime: 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
                    3. long writerIdleTime: 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
                    4. long allIdleTime: 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
                    6. 当 IdleStateEvent触发后, 就会传递给管道的下一个 handler去处理
                        通过调用(触发)下一个 handler的 userEventTiggered, 处理 IdleStateEvent(读空闲,写空闲,读写空闲)*/
                    pipeline.addLast(new IdleStateHandler(8, 11, 5, TimeUnit.SECONDS));
                    // 加空闲检测处理器(自定义 handler)
                    pipeline.addLast(new MyServerHandler());
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

public class MyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * @param ctx 上下文
     * @param evt 事件
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            String eventType = null;
            switch (event.state()) {
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
                case ALL_IDLE:
                    eventType = "读写空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress() + " " + eventType);
            // 当发生空闲, 关闭通道
            // ctx.channel().close();
        }
    }

}

  • Netty实例(通过 WebSocket实现服务器和客户端的长连接)

public class MyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    // 因为基于 http协议, 需要配置 http的编解码器
                    pipeline.addLast(new HttpServerCodec());
                    // 是以块的方式写, 添加 ChunkedWriteHandler处理器(它解决大文件或码流传输过程中可能发生的内存溢出问题)
                    pipeline.addLast(new ChunkedWriteHandler());
                    /* 当浏览器发送大量数据时, 就会发出多次 http请求:
                    - http数据在传输过程中是分段, HttpObjectAggregator可以将多个段聚合(将数据解码成 FullHttpRequest)*/
                    pipeline.addLast(new HttpObjectAggregator(8192));
                    /*
                    1. 对应 websocket, 它的数据是以帧(frame)形式传递
                    2. 可以看到 WebSocketFrame下面有六个子类
                    4. WebSocketServerProtocolHandler核心功能是将 http协议升级为 ws协议, 保持长连接
                    5. 会将状态码升级为 101*/
                    pipeline.addLast(new WebSocketServerProtocolHandler("/abc"));
                    // 配置 TextWebSocketFrame处理器(自定义 handler)
                    pipeline.addLast(new MyTextWebSocketFrameHandler());
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

// TextWebSocketFrame类型, 表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    // 读数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("服务器收到消息 " + msg.text());
        // 回复消息
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间 " + LocalDateTime.now() + " " + msg.text()));
    }

    // 一个用户首次建立连接, 第一个被执行的方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id表示通道的唯一编号, LongText唯一, ShortText较短可能重复
        System.out.println("handlerAdded.channel.long.id=" + ctx.channel().id().asLongText()); // 84ef18fffe1640e0-000021d8-00000001-0721075f4dddb688-96c3f291
        System.out.println("handlerAdded.channel.short.id=" + ctx.channel().id().asShortText()); // 96c3f291
    }

    // 客户端离线时触发
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved.channel.long.id=" + ctx.channel().id().asLongText());
    }

    // 发生异常时触发
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println(cause.getMessage());
        ctx.close(); //关闭连接
    }

}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
<script>
    var socket;
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:7000/abc");
        // 收到服务器端回送的消息时触发
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + ev.data;
        }

        // 感知到连接开启时触发
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "连接已开启."
        }

        // 感知到连接关闭时触发
        socket.onclose = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "连接已关闭."
        }
    } else {
        alert("当前浏览器不支持 Websocket!")
    }

    // 发送消息到服务器
    function send(message) {
        if (!window.socket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message)
        } else {
            alert("连接未开启!");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name="message" style="height: 200px; width: 400px"></textarea>
    <input type="button" value="发生消息" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="height: 200px; width: 400px"></textarea>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值