目录

一、什么是netty

二、BIO &NIO &AIO的概念

BIO

NIO

NIO的三个组件

AIO

三者区别

三、为什么使用Netty

四、代码

BIO实现服务器&客户端

NIO实现服务器&客户端

Netty实现服务器&客户端

四、粘包和拆包


一、什么是netty

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

二、BIO &NIO &AIO的概念

BIO

所谓IO即input和output的缩写,是对数据的流入和流出的一种抽象。我们平时说的IO大多指的是BIO。BIO(BlockingIO)是一个同步阻塞的IO,是面向流传输的。阻塞的IO当没有获取到数据时(没有读写操作)会一直处于等待阻塞状态,占用系统资源。虽然可以使用多线程解决,但是频繁(来一个请求创建一个线程)的创建线程对我们的CPU就很不友好,于是NIO就应运而出。

NIO

NIO是同步非阻塞IO(NonBlocking IO),是面向缓存区,非阻塞的IO并不会等待直到获取到数据之后返回,而是不管有没有数据都会立即响应结果。

NIO的三个组件

Channel(通道):通常NIO的操作都是由通道开始的,channel类似于流,每个channel对应一个buffer缓冲区,buffer底层是个数组。Channel会注册到Selector(选择器),由Selector根据channel读写事件的发生将其交给空闲的线程处理。

Buffer(缓冲区):buffer就是一个数组,本质上相当于一个内存区,读取数据时并不会一个字节一个字节的读取,而是将数据先写入到buffer(缓存区),再统一的写到硬盘上。

Selector(选择器):选择器也可以叫做多路复用器(选择那个线程执行),可以对应一个或多个线程。NIO 的Buffer和Channel都是既可以读也可以写的。

AIO

AIO是一个异步非阻塞IO,由操作系统完成后回调通知服务端程序启用线程去处理,一般使用于连接数较多且连接时间长的应用。

三者区别

老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

老张把水壶放到火上,立等水开。(同步阻塞BIO) 老张觉得自己有点傻
老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞NIO) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞AIO) 老张觉得自己聪明了。

三、为什么使用Netty

  1. Jdk的Nio API复杂,而且还有空轮训的bug。
  2. Netty封装了NIO的API,API简单易用,开发门槛低。
  3. 内置很多编解码功能,支持多种主流协议。
  4. 性能高,社区活跃。

四、代码

BIO实现服务器&客户端

package com.xiaojie.bio;import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @Description: bio服务器模拟* @author: xiaojie* @date: 2021.07.29*/
public class BioServer {static int count = 0;public static void main(String[] args) throws Exception {//创建一个线程池ExecutorService executorService = Executors.newCachedThreadPool();//创建serversocketServerSocket serverSocket = new ServerSocket();//绑定端口号serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 9999));System.out.println("等待发送消息。。。。。进入阻塞状态。。。。。");while (true) {final Socket accept  = serverSocket.accept();final PrintWriter printWriter = new PrintWriter(accept.getOutputStream());executorService.submit(() -> {count++;System.out.println("count..............." + count);//获取OutputStreamtry {byte[] bytes = new byte[1024];if (accept.getInputStream().read(bytes) > 0) {System.out.println("接收到客户端的消息是" + new String(bytes).trim());}//服务端响应String responMsg = "我是服务端响应信息:hello client";printWriter.write(responMsg);printWriter.flush();} catch (IOException e) {e.printStackTrace();} finally {/* try {serverSocket.close();accept.close();printWriter.close();} catch (IOException e) {e.printStackTrace();}*/}});}}
}
package com.xiaojie.bio;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;/*** @Description:客户端* @author: xiaojie* @date: 2021.07.29*/
public class BioClient {public static void main(String[] args) throws Exception {//创建socketSocket socket = new Socket();//连接socket.connect( new InetSocketAddress(InetAddress.getLocalHost(),9999));//创建PrintWriterPrintWriter printWriter = new PrintWriter(socket.getOutputStream());//读回数据BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//发送的请求数据String requeMsg="我是客户端发送请求:hello server";//客户端写出数据printWriter.write(requeMsg);printWriter.flush();//读回数据
//        String readLine = bufferedReader.readLine();byte[] bytes = new byte[1024];int read = socket.getInputStream().read(bytes);if (read>0){System.out.println("服务端返回消息是:"+new String(bytes).trim());}while(true){}//关闭连接
//        socket.close();
//        printWriter.close();
//        bufferedReader.close();}
}

NIO实现服务器&客户端

  1. nio执行过程
  2. 创建一个ServerSocketChannel和Selector,将serverSocketChannel注册到Selector上
  3. selector通过select()方法监听channel事件,当客户端连接时selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey
  4. selectionKey通过channel()方法可以获取绑定的ServerSocketChannel
  5. ServerSocketChannel通过accept()方法得到SocketChannel
  6. 将SocketChannel注册到Selector上,关心read事件
  7. 注册后返回一个SelectionKey,会和该SocketChannel关联
  8. selector继续通过select()方法监听事件,当客户端发送数据给服务端,selector监听到read事件,获取到SocketChannel注册时绑定的selectionKey
  9. selectionKey通过channel()方法可以获取绑定的socketChannel
  10. 将socketChannel里的数据读取出来
  11. 用socketChannel将服务端数据写回客户端
package com.xiaojie.nio;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/*
nio执行过程
1、创建一个ServerSocketChannel和Selector,将serverSocketChannel注册到Selector上
2、selector通过select()方法监听channel事件,当客户端连接时selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey
3、selectionKey通过channel()方法可以获取绑定的ServerSocketChannel
4、ServerSocketChannel通过accept()方法得到SocketChannel
5、将SocketChannel注册到Selector上,关心read事件
6、注册后返回一个SelectionKey,会和该SocketChannel关联
7、selector继续通过select()方法监听事件,当客户端发送数据给服务端,selector监听到read事件,获取到SocketChannel注册时绑定的selectionKey
8、selectionKey通过channel()方法可以获取绑定的socketChannel
9、将socketChannel里的数据读取出来
10、用socketChannel将服务端数据写回客户端*/
/*** @Description:* @author: xiaojie* @date: 2021.07.29*/
public class NioServer {/** 初始化* @param port* @todo* @author xiaojie* @date 2021/7/29 14:17* @return void*/private Selector selector;public void initServer(int port) throws IOException {//创建ServerSocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//设置通道为非阻塞serverSocketChannel.configureBlocking(false);//绑定端口号serverSocketChannel.bind(new InetSocketAddress(port));//获取通道管理器this.selector = Selector.open();/*将通道管理器和通道绑定,并为通道注册SelectionKey.OP_ACCEPT事件,*当该事件到达时,selector.select()会返回,如果该事件没有到达selector.select()会一直阻塞*/serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);}/** 监听* @todo* @author xiaojie* @date 2021/7/29 14:26* @return void*/public void listen() throws IOException {while (true) {int select = selector.select();if (select == 0) {continue;}//获取选中的迭代器Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();//删除防止重复处理iterator.remove();if (key.isAcceptable()) {//客户端请求连接事件ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();//获取客户端连接的通道SocketChannel channel = serverSocketChannel.accept();//设置非阻塞channel.configureBlocking(false);//在与客户端连接成功之后,给通道设置权限channel.register(this.selector, SelectionKey.OP_READ);} else if (key.isReadable()) {read(key);}}}}/** 读取数据 并返回结果* @todo* @author xiaojie* @date 2021/7/29 14:41* @return void*/public void read(SelectionKey key) throws IOException {//服务器可读取消息,得到事件发生的通道SocketChannel socketChannel = (SocketChannel) key.channel();//创建缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(512);socketChannel.read(byteBuffer);byte[] array = byteBuffer.array();String msg = new String(array);System.out.println("服务端获取数据:" + msg.trim());String res = "我是服务端响应信息:hello client";ByteBuffer buffer = ByteBuffer.wrap(res.getBytes("utf-8"));//将消息发给客户端socketChannel.write(buffer);}public static void main(String[] args) throws IOException {NioTcpServer server = new NioServer();server.initServer(9999);server.listen();}
}
package com.xiaojie.nio;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class NioClient {Selector selector;public static void main(String[] args) throws IOException {NioClient nioClient = new NioClient();nioClient.initClient("127.0.0.1",9999);nioClient.connection();}public void initClient(String ip,int port) throws IOException {// 获取一个socket 通道SocketChannel socketChannel = SocketChannel.open();// 设置通道为非阻塞socketChannel.configureBlocking(false);// 获取一个通道管理器this.selector = Selector.open();// 客户端连接服务器,其实方法执行并没实现连接,需要在listen()方法中// 调用channel.finishConnection才能完成连接socketChannel.connect(new InetSocketAddress(ip,port));// 将管道管理器和通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件socketChannel.register(this.selector, SelectionKey.OP_CONNECT);}public void connection() throws IOException{// 轮询访问selectorwhile(true){// 选择一组可以进行I/O操作的事件,放在selector中,客户端该方法不会阻塞// 这里和服务端的方法不一样,查看api注释可以知道,服务端当至少一个通道被选中时// selector的wakeup方法被调用,方法返回,而对于客户端来说,通道是一直被选中的this.selector.select();Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();while (it.hasNext()){SelectionKey key = it.next();it.remove();// 连接事件发生if(key.isConnectable()){SocketChannel socketChannel =(SocketChannel) key.channel();// 如果正在连接则完成连接if(socketChannel.isConnectionPending()){socketChannel.finishConnect();}// 设置成非阻塞socketChannel.configureBlocking(false);// 向服务器发送信息ByteBuffer byteBuffer = ByteBuffer.wrap("我是客户端发送请求:hello server".getBytes());socketChannel.write(byteBuffer);// 连接成功之后注册读取服务器信息事件socketChannel.register(this.selector,SelectionKey.OP_READ);}else if(key.isReadable()){read(key);}}}}public void read(SelectionKey key) throws IOException{SocketChannel channel = (SocketChannel)key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int len = channel.read(byteBuffer);if(len!=-1){System.out.println("接收到服务端信息:"+new String(byteBuffer.array(),0,len));}}
}

Netty实现服务器&客户端

1、创建ServerBootStrap实例。
​​​​​​2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel。
3、设置并绑定服务端的channel。
4、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证。
5、绑定并启动监听端口。
6、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler。
package com.xiaojie.netty.server;import com.sun.webkit.EventLoop;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**1、创建ServerBootStrap实例2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel3、设置并绑定服务端的channel4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证6、绑定并启动监听端口7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler*/
/*** @Description:* @author: xiaojie* @date: 2021.07.28*/
public class Nettyserver {public static void main(String[] args) {//创建主线程池,接受请求NioEventLoopGroup bossGroup = new NioEventLoopGroup();//创建work线程池,处理请求NioEventLoopGroup workGroup = new NioEventLoopGroup();try {//创建serverbootstrapServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class).group(bossGroup, workGroup).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new LoggingHandler(LogLevel.INFO)).addLast(new ServerHandler());}});ChannelFuture future =serverBootstrap.bind(8090).sync();System.out.println("starting...................");//wait until server is closedfuture.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();}finally {//关闭线程池bossGroup.shutdownGracefully();workGroup.shutdownGracefully();}}
}
package com.xiaojie.netty.server;import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;/*** @Description:* @author: xiaojie* @date: 2021.07.28*/
public class ServerHandler extends SimpleChannelInboundHandler {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object o) throws Exception {// 接受我们的数据ByteBuf byteBuf = (ByteBuf) o;String request = byteBuf.toString(CharsetUtil.UTF_8);System.out.println("获取到客户端的请求:" + request);// 响应内容:ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端响应信息:hello client", CharsetUtil.UTF_8));}
}
package com.xiaojie.netty.client;import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;/*** @Description: netty客户端* @author: xiaojie* @date: 2021.07.29*/
public class NettyClient {public static void main(String[] args) {//创建线程池NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();//创建bootstrapBootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class).group(eventLoopGroup).remoteAddress("127.0.0.1",8090).handler(new ChannelInitializer<SocketChannel>() {//处理消息@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast( new ClientHandler());}});//开始连接try {ChannelFuture channelFuture = bootstrap.connect().sync();ChannelFuture sync = channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();}finally {//关闭eventLoopGroup.shutdownGracefully();}}
}
package com.xiaojie.netty.client;import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;/*** @Description: 客户端处理handler* @author: xiaojie* @date: 2021.07.29*/
public class ClientHandler  extends SimpleChannelInboundHandler {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf=(ByteBuf) msg;String respon = byteBuf.toString(CharsetUtil.UTF_8);System.out.println("获取到服务器端响应数据是:"+respon);}/*** @param ctx* @在建立连接并准备传输数据时调用* @author xiaojie* @date 2021/7/30 10:00* @return void*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {for (int i=0;i<10;i++){ctx.writeAndFlush(Unpooled.copiedBuffer("我是客户端发送请求:hello server", CharsetUtil.UTF_8));}}
}

四、粘包和拆包

在netty服务器的代码中运行后结果如下图,这种现象我们叫粘包/拆包。产生的原因是应用层面使用了Netty,但是对于操作系统来说,只认TCP协议,尽管我们的应用层是按照 ByteBuf 为 单位来发送数据,server按照Bytebuf读取,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的。因此,我们需要在客户端根据自定义协议来组装我们应用层的数据包,然后在服务端根据我们的应用层的协议来组装数据包,这个过程通常在服务端称为拆包,而在客户端称为粘包。拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开。说直白点就是ByteBuf(缓存区的锅),当我们发送的数据小于ByteBuf(缓存区)的大小时,就会发生粘包(发送的数据多条合并成一条),当发送的数据大于ByteBuf(缓存区)的大小时,就会发生拆包(一次发送的数据没有完全包含我们要发送的数据)。

解决粘包拆包Netty自带四中拆包器

  1. LineBasedFrameDecoder: 行拆包器,在发送数据时以换行符(\n)作为分隔,接收端通过LineBasedFrameDecoder 将粘包的byteBuf进行拆包。
  2. FixedLengthFrameDecoder:固定长度的拆包器,顾名思义就是针对每个数据包长度固定。
  3. DelimiterBasedFrameDecoder:分隔符拆包器,可以自定义分隔符。
  4. LengthFieldBasedFrameDecoder:基于长度域的拆包器。

代码如下:

参考:

https://baike.baidu.com/item/Netty/10061624?fr=aladdin

https://blog.csdn.net/Zev_java/article/details/113932466

https://blog.csdn.net/u012250875/article/details/78341874

https://www.cnblogs.com/shineman-zhang/articles/13884407.html
https://netty.io/

Netty框架入门(一)相关推荐

  1. Netty框架入门案例,代码示例

    Netty框架入门案例 pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns ...

  2. Netty框架之网络通信入门程序~helloWorld

    案例演示:创建客户端和服务端,实现客户端与服务端建立连接,并且发送消息给服务端 Netty框架之网络通信入门程序~helloWorld 1.服务端代码实现 2.客户端代码实现 请尝试阅读代码,并理解注 ...

  3. NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示

    2019独角兽企业重金招聘Python工程师标准>>> 前言 NIO框架的流行,使得开发大并发.高性能的互联网服务端成为可能.这其中最流行的无非就是MINA和Netty了,MINA目 ...

  4. netty框架及原理解析

    本篇首发于橙寂博客转载请加上此标示. 正式开始了netty的学习,netty是基于nio上的一个框架.期间翻阅了很多文档以及资料.先推荐给大家. 相关文档 Netty源码在线阅读: Netty-4.1 ...

  5. Netty框架的基本使用

    Netty概述 为什么使用Netty 前面了解过的NIO模型,它有可靠性高.吞吐量高的优点,但也存在编程复杂的问题,我们要掌握大量的API,如:各种Channel.Buffer.Selector,还要 ...

  6. Netty框架之概述及基本组件介绍

    概述 Netty是由JBOSS提供的一个java开源框架,是一个高性能.高可扩展性的异步事件 驱动的网络应用程序框架,它极大地简化了TCP和UDP客户端和服务器开发等网络编 程. NIO有下面的缺点: ...

  7. Netty框架架构解析+API+运行流程+网络编程文章集锦

    新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析 <!-- 作者区域 --><div class="author"><a class=& ...

  8. java netty教程_明哥教学 - Netty简单入门教程

    作为一个正在Java路上摸爬滚打的小菜鸡,之前在项目中也用过Netty,也因为Netty报名阿里的中间件大赛,但终究功力太浅,最终不了了之,最近工作中又遇到了Netty的小姐妹Mina.此时楼主觉得N ...

  9. 『Scrapy』爬虫框架入门

    框架结构 引擎:处于中央位置协调工作的模块 spiders:生成需求url直接处理响应的单元 调度器:生成url队列(包括去重等) 下载器:直接和互联网打交道的单元 管道:持久化存储的单元 框架安装 ...

最新文章

  1. 喊了好久的AI落地究竟卡在哪里了?
  2. 箭头函数参数和返回值
  3. SVN Files 的值“ .mine”无效。路径中具有非法字符。
  4. 大数据时代下的迁移学习_继深度学习后,下一个热点技术是迁移学习
  5. linux动态库ppt,LINUX系统中动态链接库创建与使用补充_区块链白皮书代写|市场计划书项目PPT设计_Tbleg...
  6. mysql的表导出er关系图_使用Navicat生成ER关系图并导出的方法
  7. 【英语学习】【Daily English】U14 Transportation L02 I'm afraid that you're going in the wrong direction.
  8. Oracle中的环境变量(ORACLE_HOME 和 ORACLE_SID)
  9. ios标签控制器怎么用_带故事板的iOS标签栏控制器
  10. Python学习笔记(六)—几个标准类型内建函数
  11. 循环控制体重C语言,减重名医王存川教授告诫:越早控制体重,肥胖导致身体的伤害越小...
  12. vue2.0_前端跨域解决方案之proxy代理
  13. KSO-.NETCore部署CentOS7.5中验证码绘图Bitmap类型报错“The type initializer for ‘Gdip‘ threw an exception ”
  14. maven-resources-plugin:2.6:resources (default-resources)
  15. 知乎热议国家何时整治程序员的高薪现象,网友:用命和头发换的钱都被人眼红...
  16. Openwrt开发笔记(3)—— 修改路由的网关地址和无线SSID 密码
  17. word图文设计:如何用图片水印功能制作日历画册
  18. Surfaceview调用Camera实现拍照
  19. UNL系列图——交互图
  20. Redis——过期时间/过期回收策略

热门文章

  1. 年度大赏 | 2018最受欢迎的UWA问答
  2. 同时使用jsp与thyme leaf
  3. Linux环境下移动一个文件夹下的所有文件到另一个文件夹
  4. 【MySQL系统学习专栏】- 数据库操作
  5. python机器学习基础之plt.grid(网格线设置)
  6. 银行业务知识学习2(银行资本管理)
  7. bootstrap table detailView 详细视图默认展开|选择展开
  8. 【EasyUI】DataGrid DetailView(数据表格详细展示)
  9. [人工智能]深度学习卷积神经网络的秒懂各种操作
  10. eclipse的web项目出现莫名红色感叹号和红叉的且项目中无任何错误的解决方法