轻量级RPC框架开发

内容安排:

1、掌握RPC原理

2、掌握nio操作

3、掌握netty简单的api

4、掌握自定义RPC框架

  1. RPC原理学习

    1. 什么是RPC

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

  1. RPC原理

运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:

1.调用客户端句柄;执行传送参数

2.调用本地系统内核发送网络消息

3.消息传送到远程主机

4.服务器句柄得到消息并取得参数

5.执行远程过程

6.执行的过程将结果返回服务器句柄

7.服务器句柄返回结果,调用远程系统内核

8.消息传回本地主机

9.客户句柄由内核接收消息

10.客户接收句柄返回的数据

  1. hadoopRPC演示

见代码

  1. nio原理学习(nio的优势不在于数据传送的速度)

    1. 简介

nio 是New IO 的简称,在jdk1.4 里提供的新api 。

Sun 官方标榜的特性如下:

为所有的原始类型提供(Buffer)缓存支持。

字符集编码解码解决方案。

Channel :一个新的原始I/O 抽象。

支持锁和内存映射文件的文件访问接口。

提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。

  1. 传统socketsocket nio代码

基本编程模型:

服务端

核心API    ServerSocket

流程: 先创建一个服务,然后绑定在服务器的IP地址和端口

等待客户端的连接请求

收到连接请求后,接受请求,建立了一个TCP连接

从建立的连接中获取到socket输入、输出流(两个流都是同步阻塞的)

通过两个流进行数据的交互

客户端

核心API    Socket

流程: 先向服务端请求连接

一旦被服务器接受,连接就创建好了

从tcp连接中获取socket输入、输出流

通过两个流进行数据的交互

见代码

  1. socket /nio原理

1)阻塞和非阻塞:

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式。

当数据没有准备好的时候,

阻塞:往往需要等待缓冲区中的数据准备好之后才处理,否则一直等待。

非阻塞:当我们的进程访问我们的数据缓冲区的时候,数据没有准备好的时候,直接返回,不需要等待。有数据的时候,也直接返回

2)同步和异步

同步和异步都是基于应用程序和操作系统处理IO事件所采用的方式:

同步:应用程序要直接参与IO事件的操作;

异步:所有的IO读写事件交给操作系统去处理;

同步的方式在处理IO事件的时候,必须阻塞在某个方法上面等待我们的IO事件完成(阻塞在IO事件或者通过轮询IO事件的方式);对于异步来说,所有的IO读写都交给了操作系统,这个时候,我们可以去做其他的事情,并不需要去完成真正的IO操作。当操作系统完成IO之后,给我们的应用程序一个通知就可以了。

同步有两种实现模式:

1)阻塞到IO事件 阻塞到read 或者 write 方法上,这个时候我们就完全不能做自己的事情。(在这种情况下,我们只能把读写方法放置到线程中,然后阻塞线程的方式来实现并发服务,对线程的性能开销比较大)

2)IO事件的轮询  --在linux c语言编程中叫做多路复用技术(select模式)

读写事件交给一个专门的线程来处理,这个线程完成IO事件的注册功能,还有就是不断地去轮询我们的读写缓冲区(操作系统),看是否有数据准备好,然后通知我们的相应的业务处理线程。这样的话,我们的业务处理线程就可以做其他的事情。在这种模式下,阻塞的不是所有的IO线程,而是阻塞的只是select线程

比喻说明:

Client                Selector 管家                     BOSS

当客人来的时候,就给管家说,我来了(注册),管家得到这个注册信息后,就给BOSS说,我这里有一个或者多个客人。BOSS就说你去给某人A这件东西(IO数据),给另外一个人B另一件东西。这个时候,客人是可以去做自己的事情(比如看看花园等等),当管家知道BOSS给他任务后,他就会去找对应的某人(根据客人的注册信息),告诉他BOSS给他了某样东西。

JAVA  IO模型

基于以上4中IO模型,JAVA对应的实现有:

BIO--同步阻塞: JDK1.4以前我们使用的都是BIO

阻塞到我们的读写方法,阻塞到线程来提高并发性能,但是效果不是很好

NIO--同步非阻塞:JDK1.4  linux多路复用技术(select模式) 实现IO事件的轮询方式:同步非阻塞的模式,这种方式目前是主流的网络通信模式

mina  netty   ——网络通信框架,比自己写NIO要容易些,并且代码可读性更好

AIO:JDK1.7(NIO2)真正的异步非阻塞IO(基于linux的epoll模式)

AIO目前使用的还比较少

小结:1)BIO阻塞的IO

2)NIO select多路复用+非阻塞   同步非阻塞

3)AIO异步非阻塞IO

3、NIO原理

通过selector(选择器),管理所有的IO事件:

客户端的connection事件

服务端的accept事件

客户端和服务端的读写事件

selector如何进行事件管理?

当IO事件注册给我们的选择器的时候,选择器会给他们分配一个key(可以简单的理解成一个事件的标签)

当IO事件就绪后,可以通过key值来找到相应的管道channel,然后通过管道发送数据和接收数据等操作

数据缓冲区:

通过bytebuffer来实现,提供了很多读写的方法  put()   get()

2.1.4 NIO示例代码

<见代码工程>

  1. netty常用API学习

    1. netty简介

Netty是基于Java NIO的网络通信框架.

Netty是一个NIO client-server(客户端服务器)框架,使用Netty可以快速开发网络应用,例如服务器和客户端协议。Netty提供了一种新的方式来使开发网络应用程序,这种新的方式使得它很容易使用和有很强的扩展性。Netty的内部实现时很复杂的,但是Netty提供了简单易用的api从网络处理代码中解耦业务逻辑。Netty是完全基于NIO实现的,所以整个Netty都是异步的。

网络应用程序通常需要有较高的可扩展性,无论是Netty还是其他的基于Java NIO的框架,都会提供可扩展性的解决方案。Netty中一个关键组成部分是它的异步特性.

  1. nettyhelloworld
  2. 下载netty

•       下载netty包,下载地址http://netty.io/

  1. 服务端启动类

package com.netty.demo.server;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.Channel;

import io.netty.channel.ChannelFuture;

import io.netty.channel.ChannelInitializer;

import io.netty.channel.EventLoopGroup;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.nio.NioServerSocketChannel;

/**

* • 配置服务器功能,如线程、端口 • 实现服务器处理程序,它包含业务逻辑,决定当有一个请求连接或接收数据时该做什么

*

* @author wilson

*

*/

public class EchoServer {

private final int port;

public EchoServer(int port) {

this.port = port;

}

public void start() throws Exception {

EventLoopGroup eventLoopGroup = null;

try {

//创建ServerBootstrap实例来引导绑定和启动服务器

ServerBootstrap serverBootstrap = new ServerBootstrap();

//创建NioEventLoopGroup对象来处理事件,如接受新连接、接收数据、写数据等等

eventLoopGroup = new NioEventLoopGroup();

//指定通道类型为NioServerSocketChannel,设置InetSocketAddress让服务器监听某个端口已等待客户端连接。

serverBootstrap.group(eventLoopGroup).channel(NioServerSocketChannel.class).localAddress("localhost",port).childHandler(new ChannelInitializer<Channel>() {

//设置childHandler执行所有的连接请求

@Override

protected void initChannel(Channel ch) throws Exception {

ch.pipeline().addLast(new EchoServerHandler());

}

});

// 最后绑定服务器等待直到绑定完成,调用sync()方法会阻塞直到服务器完成绑定,然后服务器等待通道关闭,因为使用sync(),所以关闭操作也会被阻塞。

ChannelFuture channelFuture = serverBootstrap.bind().sync();

System.out.println("开始监听,端口为:" + channelFuture.channel().localAddress());

channelFuture.channel().closeFuture().sync();

} finally {

eventLoopGroup.shutdownGracefully().sync();

}

}

public static void main(String[] args) throws Exception {

new EchoServer(20000).start();

}

}

  1. 服务端回调方法

package com.netty.demo.server;

import io.netty.buffer.ByteBuf;

import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelFutureListener;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg)

throws Exception {

System.out.println("server 读取数据……");

//读取数据

ByteBuf buf = (ByteBuf) msg;

byte[] req = new byte[buf.readableBytes()];

buf.readBytes(req);

String body = new String(req, "UTF-8");

System.out.println("接收客户端数据:" + body);

//向客户端写数据

System.out.println("server向client发送数据");

String currentTime = new Date(System.currentTimeMillis()).toString();

ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());

ctx.write(resp);

}

@Override

public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

System.out.println("server 读取数据完毕..");

ctx.flush();//刷新后才将数据发出到SocketChannel

}

@Override

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)

throws Exception {

cause.printStackTrace();

ctx.close();

}

}

  1. 客户端启动类

package com.netty.demo.client;

import io.netty.bootstrap.Bootstrap;

import io.netty.channel.ChannelFuture;

import io.netty.channel.ChannelInitializer;

import io.netty.channel.EventLoopGroup;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.SocketChannel;

import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**

* • 连接服务器 • 写数据到服务器 • 等待接受服务器返回相同的数据 • 关闭连接

*

* @author wilson

*

*/

public class EchoClient {

private final String host;

private final int port;

public EchoClient(String host, int port) {

this.host = host;

this.port = port;

}

public void start() throws Exception {

EventLoopGroup nioEventLoopGroup = null;

try {

//创建Bootstrap对象用来引导启动客户端

Bootstrap bootstrap = new Bootstrap();

//创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接、接受数据、发送数据

nioEventLoopGroup = new NioEventLoopGroup();

//创建InetSocketAddress并设置到Bootstrap中,InetSocketAddress是指定连接的服务器地址

bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host, port))

.handler(new ChannelInitializer<SocketChannel>() {

//添加一个ChannelHandler,客户端成功连接服务器后就会被执行

@Override

protected void initChannel(SocketChannel ch)

throws Exception {

ch.pipeline().addLast(new EchoClientHandler());

}

});

// • 调用Bootstrap.connect()来连接服务器

ChannelFuture f = bootstrap.connect().sync();

// • 最后关闭EventLoopGroup来释放资源

f.channel().closeFuture().sync();

} finally {

nioEventLoopGroup.shutdownGracefully().sync();

}

}

public static void main(String[] args) throws Exception {

new EchoClient("localhost", 20000).start();

}

}

  1. 客户端回调方法

package com.netty.demo.client;

import io.netty.buffer.ByteBuf;

import io.netty.buffer.ByteBufUtil;

import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.SimpleChannelInboundHandler;

public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

//客户端连接服务器后被调用

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.out.println("客户端连接服务器,开始发送数据……");

byte[] req = "QUERY TIME ORDER".getBytes();

ByteBuf  firstMessage = Unpooled.buffer(req.length);

firstMessage.writeBytes(req);

ctx.writeAndFlush(firstMessage);

}

//•        从服务器接收到数据后调用

@Override

protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {

System.out.println("client 读取server数据..");

//服务端返回消息后

ByteBuf buf = (ByteBuf) msg;

byte[] req = new byte[buf.readableBytes()];

buf.readBytes(req);

String body = new String(req, "UTF-8");

System.out.println("服务端数据为 :" + body);

}

//•        发生异常时被调用

@Override

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

System.out.println("client exceptionCaught..");

// 释放资源

ctx.close();

}

}

  1. nettyhandler的执行顺序

Handler在netty中,占据着非常重要的地位。Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、统一对错误进行处理、统一对请求进行计数、控制Handler执行与否。一句话,没有它做不到的只有你想不到的。

Netty中的所有handler都实现自ChannelHandler接口。

按照输入输出来分,分为两大类:

ChannelInboundHandler对接收到的报文进行处理,一般用来执行解码、读取数据、进行业务处理等;

ChannelOutboundHandler对发出去的报文进行处理,一般用来进行编码、发送报文到对端。

Netty中,可以注册多个handler。

ChannelInboundHandler按照注册的先后顺序执行;

ChannelOutboundHandler按照注册的先后逆序执行,

如下图所示:

  1. 代码

见代码

  1. 总结

在使用Handler的过程中,需要注意:

1、ChannelInboundHandler之间的传递,通过调用 ctx.fireChannelRead(msg) 实现;调用ctx.write(msg) 将传递到ChannelOutboundHandler。

2、ctx.write()方法执行后,需要调用flush()方法才能令它立即执行。

3、流水线pipeline中outhandler不能放在最后,否则不生效

如果使用addlast方法来组装handler,则为以下执行顺序:

// 注册两个InboundHandler,执行顺序为注册顺序,所以应该是InboundHandler1 InboundHandler2

// 注册两个OutboundHandler,执行顺序为注册顺序的逆序,所以应该是OutboundHandler2 OutboundHandler1

3.2 netty发送对象

3.2.1 简介

Netty中,通讯的双方建立连接后,会把数据按照ByteBuf的方式进行传输,例如http协议中,就是通过HttpRequestDecoder对ByteBuf数据流进行处理,转换成http的对象。基于这个思路,可自定义一种通讯协议:Server和客户端直接传输java对象。

实现的原理是通过Encoder把java对象转换成ByteBuf流进行传输,通过Decoder把ByteBuf转换成java对象进行处理,处理逻辑如下图所示:

3.2.2 代码

见代码

  1. SpringIOC/AOP)注解学习

    1. spring的初始化顺序

在spring的配置文件中配置bean,如下

在One类和Two类中,分别实现一个参数的构造如下

加载spring配置文件,初始化bean如下

那么。结果如何呢?

结论:spring会按照bean的顺序依次初始化xml中配置的所有bean

  1. 通过ApplicationContextAware加载Spring上下文环境

在One中实现ApplicationContextAware接口会出现如何的变换呢?

结果

  1. InitializingBean的作用

在One中实现InitializingBean接口呢?

结果:

  1. 如果使用注解@Component

使用@Component注入类,那么它的顺序是如何呢?

    1. 结论
  1. spring先检查注解注入的bean,并将它们实例化
  2. 然后spring初始化bean的顺序是按照xml中配置的顺序依次执行构造
  3. 如果某个类实现了ApplicationContextAware接口,会在类初始化完成后调用setApplicationContext()方法进行操作
  4. 如果某个类实现了InitializingBean接口,会在类初始化完成后,并在setApplicationContext()方法执行完毕后,调用afterPropertiesSet()方法进行操作
    1. 注解使用回顾

1、在spring中,用注解来向Spring容器注册Bean。需要在applicationContext.xml中注册<context:component-scan base-package=”pagkage1[,pagkage2,…,pagkageN]”/>。

2、如果某个类的头上带有特定的注解@Component/@Repository/@Service/@Controller,就会将这个对象作为Bean注册进Spring容器

3、在使用spring管理的bean时,无需在对调用的对象进行new的过程,只需使用@Autowired将需要的bean注入本类即可

  1. 自定义注解

    1. 解释

1、自定义注解的作用:在反射中获取注解,以取得注解修饰的“类、方法、属性”的相关解释。

2、java内置注解

@Target 表示该注解用于什么地方,可能的 ElemenetType 参数包括:

ElemenetType.CONSTRUCTOR   构造器声明

ElemenetType.FIELD   域声明(包括 enum 实例)

ElemenetType.LOCAL_VARIABLE   局部变量声明

ElemenetType.METHOD   方法声明

ElemenetType.PACKAGE   包声明

ElemenetType.PARAMETER   参数声明

ElemenetType.TYPE   类,接口(包括注解类型)或enum声明

@Retention 表示在什么级别保存该注解信息。可选的 RetentionPolicy 参数包括:

RetentionPolicy.SOURCE   注解将被编译器丢弃

RetentionPolicy.CLASS   注解在class文件中可用,但会被JVM丢弃

RetentionPolicy.RUNTIME   JVM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。

  1. 实现

定义自定义注解

@Target({ ElementType.TYPE })//注解用在接口上

@Retention(RetentionPolicy.RUNTIME)//VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息

@Component

public @interface RpcService {

String value();

}

2、将直接类加到需要使用的类上,我们可以通过获取注解,来得到这个类

@RpcService("HelloService")

public class HelloServiceImpl implements HelloService {

public String hello(String name) {

return "Hello! " + name;

}

}

3、类实现的接口

public interface HelloService {

String hello(String name);

}

4、通过ApplicationContext获取所有标记这个注解的类

@Component

public class MyServer implements ApplicationContextAware {

@SuppressWarnings("resource")

public static void main(String[] args) {

new ClassPathXmlApplicationContext("spring2.xml");

}

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

Map<String, Object> serviceBeanMap = ctx

.getBeansWithAnnotation(RpcService.class);

for (Object serviceBean : serviceBeanMap.values()) {

try {

Method method = serviceBean.getClass().getMethod("hello", new Class[]{String.class});

Object invoke = method.invoke(serviceBean, "bbb");

System.out.println(invoke);

} catch (Exception e) {

e.printStackTrace();

}

}

}

}

  1. 结合spring实现junit测试

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations = "classpath:spring2.xml")

public class MyServer implements ApplicationContextAware {

@Test

public void helloTest1() {

}

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

Map<String, Object> serviceBeanMap = ctx

.getBeansWithAnnotation(RpcService.class);

for (Object serviceBean : serviceBeanMap.values()) {

try {

Method method = serviceBean.getClass().getMethod("hello",

new Class[] { String.class });

Object invoke = method.invoke(serviceBean, "bbb");

System.out.println(invoke);

} catch (Exception e) {

e.printStackTrace();

}

}

}

}

  1. 轻量级RPC框架开发

    1. 轻量级RPC框架需求分析及原理分析

      1. netty实现的RPC的缺点

在我们平常使用的RPC中,例如webservice,使用的习惯类似于下图

但是netty的实现过于底层,我们不能够像以前一样只关心方法的调用,而是要关心数据的传输,对于不熟悉netty的开发者,需要了解很多netty的概念和逻辑,才能实现RPC的调用。

应上面的需求,我们需要基于netty实现一个我们熟悉的RPC框架。逻辑如下:

  1. 轻量级RPC框架开发(仿写dubbo

    1. zk在框架中的应用

在上面的框架中,server端存在着一个问题,就是单点问题,也就是说,当服务端“挂了”之后,框架的使用就造成了单点屏障。

我们可以通过zookeeper来实现服务端的负载均衡

项目结构:

工程之间的依赖关系

手动实现一个基于netty的RPC框架(模拟dubble)相关推荐

  1. Java编写基于netty的RPC框架

    一 简单概念RPC: ( Remote Procedure Call),远程调用过程,是通过网络调用远程计算机的进程中某个方法,从而获取到想要的数据,过程如同调用本地的方法一样.阻塞IO :当阻塞I/ ...

  2. 基于Netty的RPC框架

    概述 RPC(Remote Procedure Call),远程过程调用,是一个计算机通信协议,该协议允许运行一个进程调用另一个进程,而程序员无需额外为这个交互作用编程. 两个或者多个应用程序分布在不 ...

  3. Netty和RPC框架线程模型分析

    <Netty 进阶之路>.<分布式服务框架原理与实践>作者李林锋深入剖析Netty和RPC框架线程模型.李林锋已在 InfoQ 上开设 Netty 专题持续出稿,感兴趣的同学可 ...

  4. Netty 和 RPC 框架线程模型分析

    https://www.infoq.cn/article/9Ib3hbKSgQaALj02-90y 1. 背景 1.1 线程模型的重要性 对于 RPC 框架而言,影响其性能指标的主要有三个要素: I/ ...

  5. Dubbo面试 - 如何自己设计一个类似 Dubbo 的 RPC 框架?

    Dubbo面试 - 如何自己设计一个类似 Dubbo 的 RPC 框架? 面试题 如何自己设计一个类似 Dubbo 的 RPC 框架? 面试官心理分析 说实话,就这问题,其实就跟问你如何自己设计一个 ...

  6. 基于Netty的RPC简易实现

    代码地址如下: http://www.demodashi.com/demo/13448.html 可以给你提供思路 也可以让你学到Netty相关的知识 当然,这只是一种实现方式 需求 看下图,其实这个 ...

  7. 基于Netty的RPC架构实战演练

    基于Netty的RPC架构实战演练 NIO netty服务端 netty客户端 netty线程模型源码分析(一) netty线程模型源码分析(二) netty5案例学习 netty学习之心跳 prot ...

  8. SuperDog——一个基于netty的web服务器开发项目

      项目GitHub地址:https://github.com/HelloWorld-Ian/SuperDog   这是我在实习期间开发的一个项目demo,简单来说是一个基于netty框架的web服务 ...

  9. java编写一个框架_手把手教你写一个基于 RxJava 的扩展框架

    背景 现在 RxJava 在 Android 开发中可谓时炽手可热,其受欢迎程度不言而喻,也因此在 github 上出现了一系列的基于 RxJava 的框架,如 RxBinding.RxPermiss ...

最新文章

  1. ​横扫六大权威榜单后,达摩院开源深度语言模型体系 AliceMind
  2. 按钮点击_如何设置微信小程序按钮点击事件?
  3. 自然语言处理中的Attention Model:是什么以及为什么[二]
  4. 实例变量和局部变量区别
  5. 浏览器中跨域创建cookie的问题
  6. 瑞幸回应申请破产:这是一个让重生之路又进一步的好消息
  7. Java Swing Mysql学生选课系统
  8. 如何在ReactJS中使用FastReport Core Web Report
  9. 【卫星】卫星通信基本概念与知识
  10. 自己动手用麦咖啡(mcafee)打造自己的安全网站!安全系统(服务器)!
  11. vue 实现计算器功能
  12. CPU 工作原理(附详细图解)
  13. 单片机毕业设计 stm32万能红外遥控器
  14. 当代著名国际摄影师相关网站大集合
  15. php重载求圆锥体积,编写一函数文件,实现求一个圆锥体的体积。
  16. Unsupervised Learning: Deep Auto-encoder
  17. Hack The Box - Catch 利用let chat API查询信息,Cachet配置泄露漏洞获取ssh登录密码,apk代码注入漏洞利用获取root权限
  18. html背景透明图片固定,请问在HTML中如何把一张图片的背景设定为透明的?
  19. windows下vue项目启动步骤
  20. 页面分享到微博、qq、qqzone

热门文章

  1. 小谈苹果M1芯片性能
  2. css3 实现省略号动态效果
  3. Android自定义动画专题一
  4. 【GDUT】回文序列
  5. leetocde 518 零钱兑换II
  6. 《未成年人保护法》,请不要保护加害者【微博复刻】
  7. Windows 安装,配置Mysql
  8. Fiddler教程(Web调试工具)
  9. sql cast函数
  10. 无需重置手机更换wp7手机的LIVE帐号