一文带你实现RPC框架
想要获取更多文章可以访问我的博客 - 代码无止境。
现在大部分的互联网公司都会采用微服务架构,但具体实现微服务架构的方式有所不同,主流上分为两种,一种是基于Http协议的远程调用,另外一种是基于RPC方式的调用。两种方式都有自己的代表框架,前者是著名的Spring Cloud,后者则是有阿里巴巴开源的Dubbo,二者都被广泛的采用。今天这篇文章,我们就一起来了解一下RPC,并且和大家一起动手实现一个简单的RPC框架的Demo。
什么是RPC
RPC是一种远程调用过程,是一种通过网络远程调用其他服务的协议。通俗的说就是,A通过打电话的方式让B帮忙办一件事,B办完事后将结果告知A。 我们下面通过一张图来大概了解一下在一个完整的RPC框架中存在的角色以及整个远程调用的过程。
通过上面的图可以看出来,在RPC框架中主要有以下4个角色:
- registry - 注册中心,当服务提供者启动时会向注册中心注册,然后注册中心会告知所有的消费者有新的服务提供者。
- provider - 服务提供者,远程调用过程中的被消费方。
- consumer - 服务消费者,远程调用过程中的消费方。
- monitor - 监视器,它主要负责统计服务的消费和调用情况。
启动服务提供者后,服务提供者会以异步的方式向注册中心注册。然后启动服务消费者,它会订阅注册中心中服务提供者列表,当有服务提供者的信息发生改变时,注册中心会通知所有的消费者。当消费者发起远程调用时,会通过动态代理将需要请求的参数以及方法签名等信息通过Netty发送给服务提供者,服务提供者收到调用的信息后调用对应的方法并将产生的结果返回给消费者,这样就完成了一个完整的远程调用。当然了这个过程中可能还会将调用信息异步发送给monitor用于监控和统计。
阅读过上面的内容后,你应该对RPC框架有了一个大概的认识。为了更好更深入的了解RPC框架的原理,下面我们就一起来动手实现一个简单的RPC框架吧。
框架核心部分
首先我们要实现的是整个RPC框架的核心部分,这部分的主要包含以下内容:
- RPC服务的注解的实现。
- 服务提供者初始化、注册、以及响应远程调用的实现。
- 服务消费者订阅注册中心、监听服务提供者的变化的实现。
- 动态代理的实现。
整个核心部分将以一个Spring Boot Starter
的形式实现,这样我们可以很方便的在Spring Boot项目中使用它。
注解
我们需要使用一个注解来标识服务提供者所提供服务的实现类,方便在初始化的时候将其交由Spring管理,也只有这样我们才可以在远程调用发生时可以找到它们。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {
Class<?> value();
}
value
属性用来标记这个服务的实现类对应的接口,RPC框架中服务提供者和消费者之间会共同引用一个服务接口的包,当我们需要远程调用的时候实际上只需要调用接口中定义的方法即可。
除了一个标识服务实现类的注解之外,我们还需要一个标识服务消费者注入服务实现的注解@RpcConsumer
,被其修饰的属性在初始化的时候都会被我们设置上动态代理,这一点在后面会详细讲到,我们先来看下它的具体实现吧。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcConsumer {
/**
* 服务名称
* @return
*/
String providerName();
}
服务提供者
服务提供者启动的时候,我们RPC框架需要做以下几件事情:
- 扫描服务提供者中所有提供服务的类(被
@RpcService
修饰的类),并将其交由BeanFactory管理。 - 启动Netty服务端,用来收到消费者的调用消息,并且返回调用结果。
- 向注册中心注册,本例中使用的注册中心是Zookeeper。
这部分我们定义了一个ProviderAutoConfiguration
类来实现这几个步骤,
@PostConstruct
public void init() {
logger.info("rpc server start scanning provider service...");
Map<String, Object> beanMap = this.applicationContext.getBeansWithAnnotation(RpcService.class);
if (null != beanMap && !beanMap.isEmpty()) {
beanMap.entrySet().forEach(one -> {
initProviderBean(one.getKey(), one.getValue());
});
}
logger.info("rpc server scan over...");
// 如果有服务的话才启动netty server
if (!beanMap.isEmpty()) {
startNetty(rpcProperties.getPort());
}
}
看上面的代码,首先我们获取到了所有被@RpcService
注解修饰的实体,并且调用了initProviderBean
方法逐一对其处理,然后我们启动了Netty。那么我们需要在initProviderBean
方法中做些什么呢?其实很简单,就是逐一将其交由BeanFactory
管理。
private void initProviderBean(String beanName, Object bean) {
RpcService rpcService = this.applicationContext
.findAnnotationOnBean(beanName, RpcService.class);
BeanFactory.addBean(rpcService.value(), bean);
}
将服务实现类交由Spring管理之后,我们还需要启动Netty用来接收远程调用信息,启动Netty的代码在这里我就不全部粘出来了,大家可以在源码中查看。在Netty启动成功之后,其实我们还执行了下面的代码,用来向ZK注册。
new RegistryServer(rpcProperties.getRegisterAddress(),
rpcProperties.getTimeout(), rpcProperties.getServerName(),
rpcProperties.getHost(), port)
.register();
整个注册的过程也非常容易理解,首先是创建了一个ZK连接,然后是判断是否有/rpc
的根节点,如果没有的话就创建一个,最后就是在根节点下创建一个EPHEMERAL_SEQUENTIAL
类型的节点,这种类型的节点在ZK重启之后会自动清除,这样可以保证注册中心重启后会自动清除服务提供者的信息。而在节点中会存储服务提供者的名称,IP地址以及端口号的信息,这样RPC框架就可以根据这些信息顺利的定位到服务提供者。
public void register() throws ZkConnectException {
try {
// 获取zk连接
ZooKeeper zooKeeper = new ZooKeeper(addr, timeout, event -> {
logger.info("registry zk connect success...");
});
if (zooKeeper.exists(Constants.ZK_ROOT_DIR, false) == null) {
zooKeeper.create(Constants.ZK_ROOT_DIR, Constants.ZK_ROOT_DIR.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
zooKeeper.create(Constants.ZK_ROOT_DIR + "/" + serverName,
(serverName + ","+ host + ":" + port).getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
logger.info("provider register success {}", serverName);
} catch (Exception e) {
throw new ZkConnectException("register to zk exception," + e.getMessage(), e.getCaus());
}
}
就这样我们RPC框架与服务提供者相关的内容就完成了,接下来要完成的是服务消费者部分。
服务消费者
对于服务消费者,我们框架需要对它的处理就是,为所有的RPC服务(被@RpcConsumer
修饰的属性)设置上动态代理。具体的设置代码如下所示(PS:这段代码写在ConsumerAutoConfiguration
类中哦):
@Bean
public BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class<?> objClz = bean.getClass();
for (Field field : objClz.getDeclaredFields()) {
RpcConsumer rpcConsumer = field.getAnnotation(RpcConsumer.class);
if (null != rpcConsumer) {
Class<?> type = field.getType();
field.setAccessible(true);
try {
field.set(bean, rpcProxy.create(type, rpcConsumer.providerName()));
} catch (IllegalAccessException e) {
e.printStackTrace();
} finally {
field.setAccessible(false);
}
}
}
return bean;
}
};
}
BeanPostProcessor
也称为Bean后置处理器,它是Spring中定义的接口,在Spring容器的创建过程中(具体为Bean初始化前后)会回调BeanPostProcessor中定义的两个方法。上面实现的postProcessBeforeInitialization
是在Bean初始化之前调用的,还有一个postProcessAfterInitialization
方法是在Bean初始化之后调用的。
如上面代码所示,我们会在每一个带有@RpcConsumer
的实例初始化之前利用反射机制为其设置一个RpcProxy
的代理,可以看到我们在创建这个动态代理的时候还需要服务提供者的名称,这是因为在动态代理的实现里面需要使用服务提供者的名称来查询服务提供者的地址信息。那么这个动态代理的实现又是怎样的呢?这就是我们下一步需要做的事情。
动态代理
在这个RPC框架里面动态代理主要实现的内容就是,当服务消费者调用服务提供者提供的接口时,将调用信息通过Netty发送给对应的服务调用者,然后由服务提供者完成相关的处理并且将处理结果返回给服务消费者。下面我们就一起来看一下RpcProxy
的是如何实现这部分功能的。
@Component
public class RpcProxy {
@Autowired
private ServiceDiscovery serviceDiscovery;
public <T> T create(Class<?> interfaceClass, String providerName) {
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[]{interfaceClass},
(proxy, method, args) -> {
// 通过netty向Rpc服务发送请求。
// 构建一个请求。
RpcRequest request = new RpcRequest();
request.setRequestId(UUID.randomUUID().toString())
.setClassName(method.getDeclaringClass().getName())
.setMethodName(method.getName())
.setParamTypes(method.getParameterTypes())
.setParams(args);
// 获取一个服务提供者。
ProviderInfo providerInfo = serviceDiscovery.discover(providerName);
// 解析服务提供者的地址信息,数组第一个元素为ip地址,第二个元素为端口号。
String[] addrInfo = providerInfo.getAddr().split(":");
String host = addrInfo[0];
int port = Integer.parseInt(addrInfo[1]);
RpcClient rpcClient = new RpcClient(host, port);
// 使用Netty向服务提供者发送调用消息,并接收请求结果。
RpcResponse response = rpcClient.send(request);
if (response.isError()) {
throw response.getError();
} else {
return response.getResult();
}
});
}
}
其实在代理里面首先我们会构造请求信息实体,然后会根据服务提供者的名称获取一个服务提供者的地址,最后再将请求信息发送给服务提供者并接收调用结果。获取服务提供者的方法会在后面消费者和提供者的通用配置里面讲解。我们在这里重点来看一下发送调用信息并接收调用结果的实现。
public class RpcClient extends SimpleChannelInboundHandler<RpcResponse> {
... 此处省略对象属性信息,可查看源码。
public RpcResponse send(RpcRequest request){
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
... 此处省略Netty相关配置,可查看源码。
// 连接服务器
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
channelFuture.channel().writeAndFlush(request).sync();
future = new CompletableFuture<>();
future.get();
if (response != null) {
// 关闭netty连接。
channelFuture.channel().closeFuture().sync();
}
return response;
} catch (Exception e) {
logger.error("client send msg error,", e);
return null;
} finally {
workerGroup.shutdownGracefully();
}
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext,
RpcResponse rpcResponse) throws Exception {
logger.info("client get request result,{}", rpcResponse);
this.response = rpcResponse;
future.complete("");
}
}
通过上面的代码可以看出向服务提供者发送消息是异步的,我们通过CompletableFuture
的get()
方法阻塞当前线程,直到接收到调用结果(PS:我们在channelRead0
方法中收到返回结果后会将其设置成完成状态)。看到这里,你可能会问服务提供者收到调用请求信息后如何处理的呢?具体的处理逻辑我们写在了ServerHandler
这个类中,可以看出在channelRead0
方法收到一条调用信息之后,调用handle
方法来处理具体的调用过程,在handle
方法中会使用反射机制找到所调用方法的具体实现,然后执行调用过程并获取结果,最后再使用Netty将结果返回给消费者服务。
public class ServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext,
RpcRequest request) throws Exception {
logger.info("provider accept request,{}", request);
// 返回的对象。
RpcResponse rpcResponse = new RpcResponse();
// 将请求id原路带回
rpcResponse.setRequestId(request.getRequestId());
try {
Object result = handle(request);
rpcResponse.setResult(result);
} catch (Exception e) {
rpcResponse.setError(e);
}
channelHandlerContext.writeAndFlush(rpcResponse).addListener(ChannelFutureListener.CLOSE);
}
private Object handle(RpcRequest request) throws Exception {
String className = request.getClassName();
Class<?> objClz = Class.forName(className);
Object o = BeanFactory.getBean(objClz);
// 获取调用的方法名称。
String methodName = request.getMethodName();
// 参数类型
Class<?>[] paramsTypes = request.getParamTypes();
// 具体参数。
Object[] params = request.getParams();
// 调用实现类的指定的方法并返回结果。
Method method = objClz.getMethod(methodName, paramsTypes);
Object res = method.invoke(o, params);
return res;
}
}
消费者和提供者的通用配置
除了ProviderAutoConfiguration
和ConsumerAutoConfiguration
两个配置类,我们还定义了一个RpcAutoConfiguration
类来配置一些其他的东西,如下所示。
public class RpcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean
public ServiceDiscovery serviceDiscovery() {
ServiceDiscovery serviceDiscovery =
null;
try {
serviceDiscovery = new ServiceDiscovery(rpcProperties.getRegisterAddress());
} catch (ZkConnectException e) {
logger.error("zk connect failed:", e);
}
return serviceDiscovery;
}
@Bean
@ConditionalOnMissingBean
public RpcProxy rpcProxy() {
RpcProxy rpcProxy = new RpcProxy();
rpcProxy.setServiceDiscovery(serviceDiscovery());
return rpcProxy;
}
}
在这个配置类里面,主要初始化了一个ServiceDiscovery
的对象以及一个RpcProxy
的对象。其中RpcProxy
是动态代理,在上面我们已经详细了解过了。那么这里就来着重了解一下ServiceDiscovery
是干啥的吧。
大家还记得我们在文章开始的时候贴出来的那张图片吗?在服务消费者初始化的时候会去订阅服务提供者内容的变化,ServiceDiscovery
的主要功能就是这个,其主要代码如下所示(如果你需要完整的代码,可以查看本文源码)。
public class ServiceDiscovery {
// 存储服务提供者的信息。
private volatile List<ProviderInfo> dataList = new ArrayList<>();
public ServiceDiscovery(String registoryAddress) throws ZkConnectException {
try {
// 获取zk连接。
ZooKeeper zooKeeper = new ZooKeeper(registoryAddress, 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
logger.info("consumer connect zk success!");
}
});
watchNode(zooKeeper);
} catch (Exception e) {
throw new ZkConnectException("connect to zk exception," + e.getMessage(), e.getCause());
}
}
/**
* 监听服务提供者的变化
*/
public void watchNode(final ZooKeeper zk) {
...
}
/**
* 获取一个服务提供者
*/
public ProviderInfo discover(String providerName) {
....
}
}
在这个类的构造方法里面,我们和ZK注册中心建立了一个连接,并且在watchNode
方法中监听服务提供者节点的变化,当有服务提供者信息有变化时会去修改dataList
里的内容,这样可以保证在服务本地维持一份可用的服务提供者的信息。而在远程调用发生的时候我们会通过discover
方法(PS:前面有见到过哦)去dataList
里面寻找一个可用的服务提供者来提供服务。
Starter的配置
我们还需要在resources
目录下新建一个META-INF
目录,然后在该目录下新建一个spring.factories
文件,里面的内容如下面代码所示。它主要是用来指定在Spring Boot项目启动的时候需要加载的其他配置。如果你有不明白的地方可以查询一下Spring Boot自定义Stater的相关内容。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.itweknow.sbrpccorestarter.config.RpcAutoConfiguration,\
cn.itweknow.sbrpccorestarter.config.ProviderAutoConfiguration,\
cn.itweknow.sbrpccorestarter.config.ConsumerAutoConfiguration
到这一步我们框架的核心部分就完成了,它将会以一个Spring Boot Stater
的形式提供给服务提供者和服务消费者使用,接下来我们就将分别定义一个服务提供者和一个消费者来测试我们自己实现的RPC框架。
创建服务提供者
在创建服务提供者之前,我们需要新建一个与服务消费者之间共享的服务接口。因为前面提到过,在服务消费者眼里的远程调用实际上就是调用本地的接口方法而已。在这个项目里我们就创建了一个HelloRpcService.java
的接口,如下所示:
public interface HelloRpcService {
String sayHello();
}
在接口定义完成之后,我们就来创建我们的服务提供者,并且实现上面定义的HelloRpcService
接口。在服务提供者服务里还需要依赖RPC框架的核心Starter以及服务接口包,我们需要在pom.xml
中添加下面的依赖。
<dependency>
<groupId>cn.itweknow</groupId>
<artifactId>sb-rpc-core-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.itweknow</groupId>
<artifactId>sb-rpc-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
添加完依赖后,我们就来看下HelloRpcService
的具体实现吧:
@RpcService(HelloRpcService.class)
public class HelloRpcServiceImpl implements HelloRpcService {
@Override
public String sayHello() {
return "Hello RPC!";
}
}
其实现很简单,主要是要需要在实现类上加上@RpcService
注解,这样在项目启动的时候RPC框架才会扫描到它,并将其交给BeanFactory
管理。接下来还需要配置的是一些RPC框架需要的配置项,包括服务名称,ZK的地址以及Netty启动的端口等信息。这些信息在框架是通过RpcProperties
这个配置类来读取的,有兴趣的同学可以在源码中找到它。
spring.rpc.host=localhost
# netty服务的端口号
spring.rpc.port=21810
# zk地址
spring.rpc.register-address=localhost:2181
spring.rpc.server-name=provider
# 连接zk的超时时间
spring.rpc.timeout=2000
创建服务消费者
服务消费者同样也需要RPC核心框架的Starter以及服务接口的依赖,和RPC框架的一些基础配置项,和服务提供者类似,这里就不粘出来了。这里需要说明的一点是,为了方便测试,服务消费者是一个Web服务,所以它还添加了spring-boot-starter-web
的依赖。下面我们就一起来看下服务消费者是如何调用远程服务的吧。
@RestController
@RequestMapping("/hello-rpc")
public class HelloRpcController {
@RpcConsumer(providerName = "provider")
private HelloRpcService helloRpcService;
@GetMapping("/hello")
public String hello() {
return helloRpcService.sayHello();
}
}
我们在消费者服务中写了一个hello的接口,在接口里面调用了HelloRpcService
接口里的sayHello()
方法,看过前面内容的同学应该知道,被@RpcConsumer
修饰的helloRpcService
属性在初始化的时候会为其设置一个动态代理,当我们调用这个接口里面的方法时,会通过Netty向服务提供者发送调用信息,然后由服务提供者调用相应方法并返回结果。
到这一步,我们可以说完成了一个简单的RPC框架以及其使用,下面我们就一起来验证一下结果吧。
测试
在测试之前我们需要在自己本地电脑上安装Zookeeper,具体的安装方式非常简单。可以参考这篇文章。
安装好Zookeeper后,我们需要完成以下几个步骤:
- 启动Zookeeper。
- 启动服务提供者。
- 启动服务消费者。
第一次启动服务消费者的过程中,你的控制台可以能会报一个找不到/rpc
节点的错误,产生这个错误的原因是我们在第一次启动的时候ZK里面并不存在/rpc
这个节点,但是如果你仔细研究源码的话,会发现当这个节点不存在的时候,我们会创建一个。所以直接忽略这个异常即可。完成以上几步之后,我们只需要在浏览器中访问http://127.0.0.1:8080/hello-rpc/hello
,如果你看到了下面的结果,那么恭喜你,整个RPC框架完美的运行成功了。
结束语
本文的主要内容是和大家一起完成了一个Demo版的RPC框架,其主要目的是让大家更深刻的理解RPC的原理以及其调用过程。当然由于文章篇幅的原因,很多代码没有直接在文中给出,您可以在Github上找到完整的实现。如果您有什么问题可以在Github上提交Issue或者发送邮件到我的邮箱(gancy.programmer@gmail.com),如果您觉得这篇文章写的还行的话,希望您能给我个Star,这是对我最好的鼓励。
PS:学习不止,码不停蹄!如果您喜欢我的文章,就关注我吧!
一文带你实现RPC框架的更多相关文章
- Hadoop系列番外篇之一文搞懂Hadoop RPC框架及细节实现
@ 目录 Hadoop RPC 框架解析 1.Hadoop RPC框架概述 1.1 RPC框架特点 1.2 Hadoop RPC框架 2.Java基础知识回顾 2.1 Java反射机制与动态代理 2. ...
- 面试都在问的微服务、服务治理、RPC、下一代微服务框架... 一文带你彻底搞懂!
文章每周持续更新,「三连」让更多人看到是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) 单体式应用程序 与微服务相对的另一个概念是传统的单体式应用程序( ...
- zookeeper(5)--基于watcher原理实现带注册中心的RPC框架
一.带版本控制的注册中心RPC框架 server端 //注册中心接口 public interface IRegisterCenter { public void register(String se ...
- 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍
概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...
- RPC 框架要实现这个功能,我们可以使用泛化调用。那什么是泛化调用呢?我们带着这个问题,先学习下如何在没有接口的情况下进行 RPC 调用。
RPC 框架要实现这个功能,我们可以使用泛化调用.那什么是泛化调用呢?我们带着这个问题,先学习下如何在没有接口的情况下进行 RPC 调用.
- 手写实现RPC框架(不带注册中心和带注册中心两种)
实现自己的RPC框架如果不需要自定义协议的话那就要基于Socket+序列化. ProcessorHandler:主要是用来处理客户端的请求. package dgb.nospring.myrpc; i ...
- 带你手写基于 Spring 的可插拔式 RPC 框架(五)注册中心
注册中心代码使用 zookeeper 实现,我们通过图片来看看我们注册中心的架构. 首先说明, zookeeper 的实现思路和代码是参考架构探险这本书上的,另外在 github 和我前面配置文件中的 ...
- 带你手写基于 Spring 的可插拔式 RPC 框架(二)整体结构
前言 上一篇文章中我们已经知道了什么是 RPC 框架和为什么要做一个 RPC 框架了,这一章我们来从宏观上分析,怎么来实现一个 RPC 框架,这个框架都有那些模块以及这些模块的作用. 总体设计 在我们 ...
- 面试都在问的「微服务」「RPC」「服务治理」「下一代微服务」一文带你彻底搞懂!
❝ 文章每周持续更新,各位的「三连」是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) ❞ 单体式应用程序 与微服务相对的另一个概念是传统的「单体式应用程 ...
随机推荐
- 百度 Echarts 地图表 js 引用路径
使用地图表格,除了需echarts,还需zrender,自行下载JS文件: 目标,做成这样的效果:http://echarts.baidu.com/doc/example/map3.html ...
- MinGW64 how-to(内含编译openssl,libjpeg,libcurl等例子)
Index of contents Setting up the MinGW 64 environment Step 1) building libiconv Step 2) building lib ...
- UILabel范例实现代码如下
#import "TWO_ViewController.h" #define SCREEN_Width [[UIScreen mainScreen] bounds].siz ...
- Laravel:php artisan key:generate三种报错解决方案,修改默认PHP版本(宝塔面板)
为了兼容N多个网站,服务器上有3个PHP版本5.3/5.6/7.2.宝塔默认为5.3,但是laravel5.7并不支持,所以在创建线上 .env 环境配置文件,初始化应用配置时候报错了. cp .en ...
- python网络编程(转)
本文代码转自廖雪峰老师的python教程 网络编程底层其实就是一个socket,代表两台机器之间的一个连接. s = socket.socket(socket.AF_INET, socket.SOCK ...
- 从零开始的Wordpress个人博客搭建
0x00前言 在博客园写了有一年的博客了,也想换换新口味,wordpress的众多的主题和个性化设置非常符合我的喜好,所以捣鼓了一天也算是把它搭好了. 直接在服务器上搭建wordpress还需要配置m ...
- Unity Shader 玻璃效果
一个玻璃效果主要分为两个部分,一部分是折射效果的计算,另一部分则是反射.下面分类进行讨论: 折射: 1.利用Grass Pass对当前屏幕的渲染图像进行采样 2.得到法线贴图对折射的影响 3.对采集的 ...
- 并发编程-concurrent指南-阻塞队列-链表阻塞队列LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表的阻塞队列. 由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选. LinkedBlocki ...
- HDU 4283:You Are the One(区间DP)
http://acm.hdu.edu.cn/showproblem.php?pid=4283 题意:有n个数字,不操作的情况下从左到右按顺序输出,但是可以先让前面的数字进栈,让后面的数字输出,然后栈里 ...
- 玲珑OJ 1082:XJT Loves Boggle(爆搜)
http://www.ifrog.cc/acm/problem/1082 题意:给出的单词要在3*3矩阵里面相邻连续(相邻包括对角),如果不行就输出0,如果可行就输出对应长度的分数. 思路:爆搜,但是 ...