Netty是什么:

  1. 异步事件驱动框架,用于快速开发高i性能服务端和客户端
  2. 封装了JDK底层BIO和NIO模型,提供高度可用的API
  3. 自带编码解码器解决拆包粘包问题,用户只用关心业务逻辑
  4. 精心设计的Reactor线程模型支持高并发海量连接
  5. 自带协议栈,无需用户关心
  Netty 是一款提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,使用 Netty 可以确保你快速和简单地开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 socket 服务开发。

Netty具有如下特性:

  • 设计:统一的API,支持多种传输类型,阻塞和非阻塞的,简单而强大的线程模型,真正的无连接数据报套接字支持,链接逻辑组件以支持复用。
  • 易于使用:详实的 Javadoc 和大量的示例集不需要超过JdK 1.6+的依赖。
  • 性能:拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟,得益于池化和复用,拥有更低的资源消耗,最少的内存复制。
  • 健壮性:不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError ,消除在高速网络中 NIO 应用程序常见的不公平读/写比率。
  • 安全性:完整的 SSL/TLS 以及 StartTLs 支持,可用于受限环境下,如 Applet 和 OSGI。
  • 社区驱动:发布快速而且频繁。

Netty核心组件:

  为了后期更好地理解和进一步深入 Netty,有必要总体认识一下 Netty 所用到的核心组件以及他们在整个 Netty 架构中是如何协调工作的。Nettty 有如下几个核心组件:

  • Bootstrap 和 ServerBootstrap
  • Channel
  • ChannelHandler
  • ChannelPipeline
  • EventLoop
  • ChannelFuture

1.Bootstrap或者ServerBootstrap,一个Netty应用通常由一个Bootstrap开始,它主要作用是配置整个Netty程序,串联起各个组件。

2.Channel:Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel的 EventLoop。在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而Netty 的 Channel 则提供的一系列的 API :它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势:

  1. 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。

  2. Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。

  3. 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,有 Channel 统一负责和调度,功能实现更加灵活。

  Channel 与 socket 的关系:

  在 Netty 中 Channel 有两种,对应客户端套接字通道NioSocketChannel,内部管理java.nio.channels.SocketChannel 套接字,对应服务器端监听套接字通道NioServerSocketChannel,其内部管理自己的 java.nio.channels.ServerSocketChannel 套接字。也就是 Channel 是对 socket 的装饰或者门面,其封装了对socket 的原子操作。

3.ChannelHandler:ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。

4.ChannelPipeline:ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。

  当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler 。当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext,它代表了 ChannelHandler 和 ChannelPipeline之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下:

  1. 一个 ChannelInitializer 的实现被注册到了 ServerBootStrap中

  2. 当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler

  3. ChannelInitializer 将它自己从 ChannelPipeline 中移除

5.EventLoop:Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。下图是Channel、EventLoop、Thread、EventLoopGroup之间的关系(摘自《Netty In Action》):

  • 一个 EventLoopGroup(Boos线程池,work线程池的分组概念) 包含一个或多个 EventLoop。
  • 一个 EventLoop 在它的生命周期内只能与一个Thread绑定。
  • 所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理。
  • 一个 Channel 在它的生命周期内只能注册与一个 EventLoop。
  • 一个 EventLoop 可被分配至一个或多个 Channel 。

  当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。

6.ChannelFuture:Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

  通过了解相应的组件,接下去先简单看一下Netty的基本使用,同样的市服务端与客户端的交互。

  服务端:
public class NettyServer  {

    private static final String IP = "127.0.0.1";
private static final int port = 6666;
private static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2;
private static final int BIZTHREADSIZE = 100;
//创建两个EventLoopGroup对象,创建boss线程组 ⽤于服务端接受客户端的连接
private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE);
//创建 worker 线程组 ⽤于进⾏ SocketChannel 的数据读写
private static final EventLoopGroup workGroup = new NioEventLoopGroup(BIZTHREADSIZE); public static void start() throws Exception { //启动类初始化
ServerBootstrap serverBootstrap = initServerBootstrap();
// 绑定端⼝,并同步等待成功,即启动服务端
ChannelFuture channelFuture = serverBootstrap.bind(IP, port).sync();
//成功绑定到端口之后,给channel增加一个 管道关闭的监听器并同步阻塞,直到channel关闭,线程才会往下执行,结束进程。
channelFuture.channel().closeFuture().sync();
System.out.println("server start"); } private static ServerBootstrap initServerBootstrap() {
//一个Netty应用通常由一个Bootstrap开始
ServerBootstrap serverBootstrap = new ServerBootstrap();
//添加两个组,设置使⽤的EventLoopGroup
serverBootstrap.group(bossGroup,workGroup)
//初始化 channel,设置要被实例化的为 NioServerSocketChannel 类
.channel(NioServerSocketChannel.class)
//初始化channelHandler,设置连⼊服务端的 Client 的 SocketChannel 的处理器
.childHandler(new ChannelInitializer<Channel>() {
            //我们再来设置下相应的过滤条件。 这⾥需要继承Netty中ChannelInitializer 类,
            //然后重写 initChannel 该⽅法,进⾏添加相应的设置,传输协议设置,以及相应的业务实现类
@Override
protected void initChannel(Channel ch) throws Exception {
//配置pipeline相关属性
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
// 相关处理 Handler
pipeline.addLast(new TcpServerHandler());
}
});
return serverBootstrap;
} protected static void shutdown(){
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
} public static void main(String[] args) throws Exception {
System.out.println("启动Server...");
NettyServer.start();
}
}

  服务相关的设置的代码写完之后,我们再来编写主要的业务代码。 使⽤Netty编写 [业务层 ]的代码,我们需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,在这⾥说下它们两的区别吧。

  继承 SimpleChannelInboundHandler 类之后,会在接收到数据后会⾃动 release 掉数据占⽤的 Bytebuffer 资源。并且继承该类需要指定数据格式。

  ⽽继承ChannelInboundHandlerAdapter 则不会⾃动释放,需要⼿动调⽤ReferenceCountUtil.release() 等⽅法进⾏释放。继承该类不需要指定数据格式。 所以在这⾥,个⼈推荐服务端继承 ChannelInboundHandlerAdapter ,⼿动进⾏释放,防⽌数据未处理完就⾃动释放了。⽽且服务端可能有多个客户端进⾏连接,并且每⼀个客户端请求的数据格式都不⼀致,这时便可以进⾏相应的处理。

  客户端根据情况可以继承 SimpleChannelInboundHandler 类。好处是直接指定好传输的数据格式,就不需要再进⾏格式的转换了。

  TcpServerHandler :

public class TcpServerHandler extends ChannelInboundHandlerAdapter {
//建⽴连接时,发送⼀条庆祝消息
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("chanelActive>>>>>>>");
}
//业务逻辑处理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server receive message:" + msg);
ctx.channel().writeAndFlush("accept message "+ msg);
ctx.close();
}
//异常相关处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("get server exception :"+cause.getMessage());
}
}

  客户端:客户端过滤其这块基本和服务端⼀致。不过需要注意的是,传输协议、编码和解码应该⼀致.

public class NettyClient implements Runnable {

    @Override
public void run() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("handler", new MyClient());
}
});
for (int i=0;i<10;i++){
ChannelFuture f = bootstrap.connect("127.0.0.1",6666).sync();
f.channel().writeAndFlush("hello service !" + Thread.currentThread().getName()+ ":---->"+i);
f.channel().closeFuture().sync();
}
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
} } public static void main(String[] args) {
for (int i = 0;i < 3 ;i++ ){
new Thread(new NettyClient(),">>> this thread "+i).start();
}
}
}

  MyClient :这⾥有个注解, 该注解 Sharable 主要是为了多个handler可以被多个channel安全地共享,也就是保证线程安全。

public class MyClient extends ChannelInboundHandlerAdapter {
//@Sharable
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("client receieve message: "+msg);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("get client exception :"+cause.getMessage());
}
}

  启动服务器,客户端即可看到演示效果。

Netty线程模型:

  了解了Netty 基础服务的构建,我们对Netty服务有了一定的认识,最后来一张线程模型图(Reactor主从多线程模型):

Netty实现简易版Tomcat:

  之前我们通过手写springMvc,实现了自己的Mvc的调用流程,其中最本质的东西是通过Servlert,将我们对应的Controller对应的请求路径及controller给映射缓存起来,Tomcat我们称之为Servlet容器,所以我们将自己实现的Servlet交由其管理是理所当然的,既然现在我们自己有映射关系,同时现在也有了Netty这么强大的通信框架,也了解了他的基本使用,那么我们如何将其与我们的程序关联起来,实现自己 容器呢?

  在手写之前,我们需要明白的是在这个过程中非常重要的几个对象,Servlet是必不可少的,Request,Response,另外一个就是我们的容器本身,我们按照我们的思路,就是通过Netty对外暴露一个端口,同时在启动的时候初始化映射关系,在有请求进来的时候调用对应的Servlet进行业务处理,最后进行响应。

  主类:

//Netty就是一个同时支持多协议的网络通信框架
public class WuzzTomcat {
//打开Tomcat源码,全局搜索ServerSocket private int port = ; private Map<String, WuzzServlet> servletMapping = new HashMap<String, WuzzServlet>(); private Properties webxml = new Properties(); private void init() {
//加载web.xml文件,同时初始化 ServletMapping对象
try {
String WEB_INF = this.getClass().getResource("/").getPath();
FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");
       //加载配置文件
webxml.load(fis);
for (Object k : webxml.keySet()) {
String key = k.toString();
if (key.endsWith(".url")) {
String servletName = key.replaceAll("\\.url$", "");
String url = webxml.getProperty(key);
String className = webxml.getProperty(servletName + ".className");
WuzzServlet obj = (WuzzServlet) Class.forName(className).newInstance();
servletMapping.put(url, obj);
}
}
} catch (Exception e) {
e.printStackTrace();
}
} public void start() {
init();
//Netty封装了NIO,Reactor模型,Boss,worker
// Boss线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
// Worker线程
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// Netty服务
//ServetBootstrap ServerSocketChannel
ServerBootstrap server = new ServerBootstrap();
// 链路式编程
server.group(bossGroup, workerGroup)
// 主线程处理类,看到这样的写法,底层就是用反射
.channel(NioServerSocketChannel.class)
// 子线程处理类 , Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
// 客户端初始化处理
protected void initChannel(SocketChannel client) throws Exception {
// 无锁化串行编程
//Netty对HTTP协议的封装,顺序有要求
// HttpResponseEncoder 编码器
client.pipeline().addLast(new HttpResponseEncoder());
// HttpRequestDecoder 解码器
client.pipeline().addLast(new HttpRequestDecoder());
// 业务逻辑处理
client.pipeline().addLast(new WuzzTomcatHandler());
} })
// 针对主线程的配置 分配线程最大数量 128
.option(ChannelOption.SO_BACKLOG, )
// 针对子线程的配置 保持长连接
.childOption(ChannelOption.SO_KEEPALIVE, true); // 启动服务器
ChannelFuture f = server.bind(port).sync();
System.out.println("Wuzz Tomcat 已启动,监听的端口是:" + port);
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}public static void main(String[] args) {
new WuzzTomcat().start();
}
}

  配置文件:web.properties主要是模仿web工程中webxml中对Servlet的映射关系的配置:

servlet.one.url=/firstServlet.do
servlet.one.className=com.wuzz.demo.netty.tomcat.servlet.FirstServlet servlet.two.url=/secondServlet.do
servlet.two.className=com.wuzz.demo.netty.tomcat.servlet.SecondServlet

  所以我们这里需要定义自己的Servlet,这里主要定义自己一个抽象的Servlet类,采用模板方法的模式来编写代码。:

public abstract class WuzzServlet {
  public void service(WuzzRequest request, WuzzResponse response) throws Exception{ //由service方法来决定,是调用doGet或者调用doPost
if("GET".equalsIgnoreCase(request.getMethod())){
doGet(request, response);
}else{
doPost(request, response);
}
} public abstract void doGet(WuzzRequest request, WuzzResponse response) throws Exception; public abstract void doPost(WuzzRequest request, WuzzResponse response) throws Exception;
}
public class FirstServlet extends WuzzServlet {
@Override
public void doGet(WuzzRequest request, WuzzResponse response) throws Exception {
this.doPost(request, response);
}
@Override
public void doPost(WuzzRequest request, WuzzResponse response) throws Exception {
response.write("This is First Serlvet");
}
}
public class SecondServlet extends WuzzServlet {
@Override
public void doGet(WuzzRequest request, WuzzResponse response) throws Exception {
this.doPost(request, response);
}
@Override
public void doPost(WuzzRequest request, WuzzResponse response) throws Exception {
response.write("This is Second Serlvet");
}
}

  到目前为止,从初始化工作到接受请求的流程已经都可以了,那么现在就是处理这个请求的过程,那么这里需要定义Request ,Response.在Netty中进行响应的类是需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,我们采用前者,那么我们就可以定义出这样的两个类:

  Request:

public class WuzzRequest {

    private ChannelHandlerContext ctx;

    private HttpRequest req;

    public WuzzRequest(ChannelHandlerContext ctx, HttpRequest req) {
this.ctx = ctx;
this.req = req;
} public String getUrl() {
return req.uri();
} public String getMethod() {
return req.method().name();
}
}

  Response:

public class WuzzResponse {
//SocketChannel的封装
private ChannelHandlerContext ctx; private HttpRequest req; public WuzzResponse(ChannelHandlerContext ctx, HttpRequest req) {
this.ctx = ctx;
this.req = req;
} public void write(String out) throws Exception {
try {
if (out == null || out.length() == ) {
return;
}
// 设置 http协议及请求头信息
FullHttpResponse response = new DefaultFullHttpResponse(
// 设置http版本为1.1
HttpVersion.HTTP_1_1,
// 设置响应状态码
HttpResponseStatus.OK,
// 将输出值写出 编码为UTF-8
Unpooled.wrappedBuffer(out.getBytes("UTF-8"))); response.headers().set("Content-Type", "text/html;");
// 当前是否支持长连接
// if (HttpUtil.isKeepAlive(r)) {
// // 设置连接内容为长连接
// response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
// }
ctx.write(response);
} finally {
ctx.flush();
ctx.close();
}
}
}

  最后我们需要定义自己的业务处理类,这里为了方便,我们直接在主类中新建一个内部类来处理:

 //业务处理handler
public class WuzzTomcatHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
// 转交给我们自己的request实现
WuzzRequest request = new WuzzRequest(ctx, req);
// 转交给我们自己的response实现
WuzzResponse response = new WuzzResponse(ctx, req);
// 实际业务处理
String url = request.getUrl(); if (servletMapping.containsKey(url)) {
servletMapping.get(url).service(request, response);
} else {
response.write("404 - Not Found");
}
}
}
     //异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { }
}

  这样子就完成了我们整个容器的编写,启动容器,通过 http://localhost:8080/firstServlet.do 访问可以看到拿到响应:

Netty核心组件介绍及手写简易版Tomcat的更多相关文章

  1. 手写简易版RPC框架基于Socket

    什么是RPC框架? RPC就是远程调用过程,实现各个服务间的通信,像调用本地服务一样. RPC有什么优点? - 提高服务的拓展性,解耦.- 开发人员可以针对模块开发,互不影响.- 提升系统的可维护性及 ...

  2. 手写简易版Promise

    实现一个简易版 Promise 在完成符合 Promise/A+ 规范的代码之前,我们可以先来实现一个简易版 Promise,因为在面试中,如果你能实现出一个简易版的 Promise 基本可以过关了. ...

  3. mybatis(八)手写简易版mybatis

    一.画出流程图 二.设计核心类 二.V1.0 的实现 创建一个全新的 maven 工程,命名为 mebatis,引入 mysql 的依赖. <dependency> <groupId ...

  4. 【Tomcat】手写迷你版Tomcat

    目录 源码地址 一,分析 Mini版Tomcat需要实现的功能 二,开发--准备工作 2.1 新建Maven工程 2.2 定义编译级别 2.3 新建主类编写启动入口和端口 三,开发--1.0版本 3. ...

  5. 手写简易的Mybatis

    手写简易的Mybatis 此篇文章用来记录今天花个五个小时写出来的简易版mybatis,主要实现了基于注解方式的增删查改,目前支持List,Object类型的查找,参数都是基于Map集合的,可以先看一 ...

  6. JDK动态代理深入理解分析并手写简易JDK动态代理(下)

    原文同步发表至个人博客[夜月归途] 原文链接:http://www.guitu18.com/se/java/2019-01-05/27.html 作者:夜月归途 出处:http://www.guitu ...

  7. JDK动态代理深入理解分析并手写简易JDK动态代理(上)

    原文同步发表至个人博客[夜月归途] 原文链接:http://www.guitu18.com/se/java/2019-01-03/27.html 作者:夜月归途 出处:http://www.guitu ...

  8. 【教程】手写简易web服务器

    package com.littlepage.testjdbc; import java.io.BufferedReader; import java.io.FileReader; import ja ...

  9. 手写简易SpringMVC

    手写简易SpringMVC 手写系列框架代码基于普通Maven构建,因此在手写SpringMVC的过程中,需要手动的集成Tomcat容器 必备知识: Servlet相关理解和使用,Maven,Java ...

随机推荐

  1. C# System.Web.Caching.Cache类 缓存 各种缓存依赖

    原文:https://www.cnblogs.com/kissdodog/archive/2013/05/07/3064895.html Cache类,是一个用于缓存常用信息的类.HttpRuntim ...

  2. 云中沙箱学习笔记2-ECS之初体验

    1.1 背景知识 云服务器(Elastic Compute Service, 简称ECS),是一种简单高效,处理能力可以弹性伸缩的计算服务.ECS的相关术语说明如下: --实例(Instance):是 ...

  3. linux安装 inotify

    [root@rsync-client-inotify ~]# yum install make gcc gcc-c++ [root@rsync-client-inotify ~]# wget http ...

  4. Sql在Group by的select中包含多列

    SELECT A , B , COUNT(Id) AS '数量' FROM dbo.[Table] GROUP BY A , B

  5. LINUX VSFTP配置及安装

    ------------------转载:亲身实践,确实好用(http://www.cnblogs.com/jack-Star/p/4089547.html) 1.VSFTP简介 VSFTP是一个基于 ...

  6. 22pygame 安装

    实战步骤 pygame 快速体验 飞机大战 实战 确认模块 --pygame pygame 就是一个 Python 模块, 专为电子游戏设计 提示 : 学习第三方模块, 通常最好的参考资料就在官方网站 ...

  7. tac 反向显示文件内容

    1.命令功能 tac是cat的反向拼写,功能是反向显示文件内容. 2.语法格式 tac  option  file 3.使用范例 [root@localhost chu]# cat test.txt ...

  8. tf.matmul / tf.multiply

    import tensorflow as tfimport numpy as np 1.tf.placeholder placeholder()函数是在神经网络构建graph的时候在模型中的占位,此时 ...

  9. count(*),count(1),count(列名)的区别

    count(*)和count(1)无任何差别,永远优于count其他字段只要存在普通索引,count就会使用普通索引,只存在主键时,count(*)和或count(1)会使用主键索引 count(a) ...

  10. 【leetcode】1026. Maximum Difference Between Node and Ancestor

    题目如下: Given the root of a binary tree, find the maximum value V for which there exists different nod ...