如何用Netty实现一个轻量级的HTTP代理服务器

摘要:
为什么会想通过Netty构建一个HTTP代理服务器?现在迫切希望有一个HTTP代理服务器,能够路由回归环境的请求到测试环境。基于这些因素,考虑到HTTP代理服务器的主要用途是转发URL请求,可选的方案有很多种。我首先想到了使用Netty这个NIO框架,来实现一个轻量级的HTTP代理转发服务器,同时只要简单地配置过滤规则,就可以实现请求的规则路由。本文要求你熟悉Netty网络框架的工作流程,基本原理。

为什么会想通过Netty构建一个HTTP代理服务器?这也是笔者发表这篇文章的目的所在。

其主要还是源于解决在日常开发测试过程中,一直困扰测试同学很久的一个问题,现在我就来具体阐述一下这个问题。

在日常开发测试过程中,为了确保上线项目的万无一失,集成测试通常有部署,测试环境和回归环境这两套环境。开发人员根据需求编写代码模块,自测通过之后,由测试的同学更新到测试环境,进行测试。如果测试通过,确定项目具备上线条件,后续会在回归环境,进行回归测试。回归验证通过的项目,才具备上线条件。

由于模块的复杂性和多样性,我们系统要跟外系统进行一些数据的交互,这通常是通过HTTP协议方式完成的。现在由于某些条件的限制,通常只有测试环境的网络和端口是和外系统是打通的,回归环境的这块网络链路是关闭的。这样就产生了一个很尴尬的问题:如果一个模块有跟外系统进行交互,回归环境是不具备回归条件的,这样就要测试的同学,额外把模块更新到测试环境来验证,这样不仅耗时耗力。并且由于测试环境和回归环境系统数据的差异,往往可能导致项目的潜在风险没有被及时地发现。

现在迫切希望有一个HTTP代理服务器,能够路由回归环境的请求到测试环境。更进一步地,如果能根据请求报文的某些关键字来过滤,决定最终路由的地址,这个当然是最好了。

基于这些因素,考虑到HTTP代理服务器的主要用途是转发URL请求,可选的方案有很多种。比如Apache、Nginx等等。但是最终都没有被采用,主要基于以下几点考虑:

  1. Apache服务器不能根据某些指定的关键字过滤转发URL请求,只能做简单的代理转发。
  2. Nginx相比Aapche服务器单纯进行请求转发而言,通过OpenResty(http://openresty.org/cn/),可以把lua解析器内嵌到Nginx,这样可以编写lua脚本的关键字过滤规则,但是要测试同学短时间内学会配置不太现实。

有没有通过简单的几个配置,就可以达到目的的可行方案呢?

我首先想到了使用Netty这个NIO框架,来实现一个轻量级的HTTP代理转发服务器,同时只要简单地配置过滤规则,就可以实现请求的规则路由。

本文要求你熟悉Netty网络框架的工作流程,基本原理。有兴趣的朋友,可以认真研读一下《Netty in Action》这本书,对提高Netty的功力有很大帮助。

言归正传,下面是这个HTTP代理转发服务器的工作流程图:

如何用Netty实现一个轻量级的HTTP代理服务器第1张

这里我简单描述一下:

  • 首先是Netty的服务端连接器(Acceptor)线程接收到HTTP请求,然后会把这个请求放入后端Netty专门负责处理I/O操作的线程池中。这个也是Netty经典的主从Reactor多线程模型的应用。
  • I/O处理线程先对HTTP请求,调用HttpRequestDecoder解码器进行解码。
  • HttpRequestDecoder把解码的结果,通知给路由规则计算的核心模块(GatewayServerHandler),核心模块根据配置加上请求报文中的关键字,计算出要转发的URL地址。
  • 通过HTTP POST方式把请求,转发给计算出来的URL地址。
  • 获取HTTP POST的获得到的应答结果。
  • 然后通过HttpResponseEncoder编码器,把应答结果进行HTTP编码,最后透传给调用方。

流程描述很简单,现在关键是,如何设计关键字路由规则配置模块。

我是通过属性配置文件(.properties)方式来实现的,主要有两个配置文件。

  • netty-gateway.properties配置文件,主要是用来描述URL中的路径、以及其没有和请求URL路径匹配成功时,默认转发的URL地址。

配置文件的配置参考说明:

#配置说明参考:
#netty-gateway.config1.serverPath ==> URL路径关键字。
#netty-gateway.config1.defaultAddr ==> 请求报文中的关键字没有匹配成功时,默认转发的URL地址。
#config的数字后缀顺序递增即可。

netty-gateway.config1.serverPath=fcgi-bin/UIG_SFC_186
netty-gateway.config1.defaultAddr=http://10.46.158.10:8088/fcgi-bin/UIG_SFC_186

netty-gateway.config2.serverPath=fcgi-bin/BSSP_SFC
netty-gateway.config2.defaultAddr=http://10.46.158.10:8089/fcgi-bin/BSSP_SFC
  • netty-route.properties配置文件,则是主要配置URL中的路径、请求报文关键字集合、以及请求的URL路径、请求报文关键字和配置的匹配成功时,转发的URL地址。

配置文件的配置参考说明:

#配置说明参考:
#netty-gateway.config1.serverPath ==> URL路径关键字。
#netty-gateway.config1.keyWord ==> 请求报文匹配关键字。支持1~N个关键字,多个关键字用逗号分割,关键字之间是逻辑与的关系。
#netty-gateway.config1.matchAddr ==> 请求报文匹配关键字匹配成功时,转发的ULR地址。
#config的数字后缀顺序递增即可。

netty-gateway.config1.serverPath=fcgi-bin/UIG_SFC_186
netty-gateway.config1.keyWord=1,2,3netty-gateway.config1.matchAddr=http://10.46.158.20:8088/fcgi-bin/UIG_SFC_186

netty-gateway.config2.serverPath=fcgi-bin/UIG_SFC_186
netty-gateway.config2.keyWord=1,2,3,4netty-gateway.config2.matchAddr=http://10.46.158.20:8088/fcgi-bin/UIG_SFC_186

netty-gateway.config3.serverPath=fcgi-bin/BSSP_SFC
netty-gateway.config3.keyWord=HelloWorldNettyGateway
netty-gateway.config3.matchAddr=http://10.46.158.20:8089/fcgi-bin/BSSP_SFC

有了上述两个基础的配置信息之后,就可以实现基于Netty的关键字HTTP路由转发服务器了。

这里主要说明关键代码模块的设计思路:

首先是GatewayAttribute类,它主要对应netty-gateway.properties配置文件的数据结构。

packagecom.newlandframework.gateway.commons;

/*** @authortangjie<https://github.com/tang-jie>
 * @filename:GatewayAttribute.java
 * @description:GatewayAttribute功能模块
 * @blogs http://www.cnblogs.com/jietang/* @since2018/4/18
 */
public classGatewayAttribute {
    privateString serverPath;
    privateString defaultAddr;

    publicString getDefaultAddr() {
        returndefaultAddr;
    }

    public voidsetDefaultAddr(String defaultAddr) {
        this.defaultAddr =defaultAddr;
    }

    publicString getServerPath() {
        returnserverPath;
    }

    public voidsetServerPath(String serverPath) {
        this.serverPath =serverPath;
    }
}

其次是RouteAttribute类,它主要对应netty-route.properties配置文件的数据结构。

packagecom.newlandframework.gateway.commons;

/*** @authortangjie<https://github.com/tang-jie>
 * @filename:RouteAttribute.java
 * @description:RouteAttribute功能模块
 * @blogs http://www.cnblogs.com/jietang/* @since2018/4/18
 */
public classRouteAttribute {
    privateString serverPath;
    privateString keyWord;
    privateString matchAddr;

    publicString getMatchAddr() {
        returnmatchAddr;
    }

    public voidsetMatchAddr(String matchAddr) {
        this.matchAddr =matchAddr;
    }

    publicString getServerPath() {
        returnserverPath;
    }

    public voidsetServerPath(String serverPath) {
        this.serverPath =serverPath;
    }

    publicString getKeyWord() {
        returnkeyWord;
    }

    public voidsetKeyWord(String keyWord) {
        this.keyWord =keyWord;
    }
}

然后通过实现spring框架的BeanDefinitionRegistryPostProcessor接口,来实现配置文件的自动加载注入。对应代码如下:

packagecom.newlandframework.gateway.commons;

importorg.springframework.beans.BeansException;
importorg.springframework.beans.MutablePropertyValues;
importorg.springframework.beans.factory.config.ConfigurableListableBeanFactory;
importorg.springframework.beans.factory.support.BeanDefinitionRegistry;
importorg.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
importorg.springframework.beans.factory.support.GenericBeanDefinition;
importorg.springframework.core.io.ClassPathResource;
importorg.springframework.core.io.Resource;

importjava.io.IOException;
import java.util.*;

import static com.newlandframework.gateway.commons.GatewayOptions.*;

/*** @authortangjie<https://github.com/tang-jie>
 * @filename:RoutingLoader.java
 * @description:RoutingLoader功能模块
 * @blogs http://www.cnblogs.com/jietang/* @since2018/4/18
 */
public class RoutingLoader implementsBeanDefinitionRegistryPostProcessor {
    public static final List<RouteAttribute> ROUTERS = new ArrayList<RouteAttribute>();
    public static final List<GatewayAttribute> GATEWAYS = new ArrayList<GatewayAttribute>();

    private static final List<String> KEY_ROUTERS = new ArrayList<String>();
    private static final List<String> KEY_GATEWAYS = new ArrayList<String>();

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throwsBeansException {
        initGatewayRule(registry);
        initRouteRule(registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throwsBeansException {
        GATEWAYS.clear();
        ROUTERS.clear();

        for(String beanName : KEY_GATEWAYS) {
            GATEWAYS.add(beanFactory.getBean(beanName, GatewayAttribute.class));
        }

        for(String beanName : KEY_ROUTERS) {
            ROUTERS.add(beanFactory.getBean(beanName, RouteAttribute.class));
        }
    }
    
    //加载netty-gateway.properties配置文件
    private voidinitGatewayRule(BeanDefinitionRegistry registry) {
        GenericBeanDefinition beanDefinition = newGenericBeanDefinition();
        Resource resource = newClassPathResource(GATEWAY_OPTION_GATEWAY_CONFIG_FILE);
        Properties p = newProperties();
        try{
            p.load(resource.getInputStream());

            String key = null;
            String keyPrefix = null;
            String defaultAddr = null;
            String serverPath = null;

            Map<String, String> valuesMap = null;
            MutablePropertyValues mpv = null;

            for(Object obj : p.keySet()) {
                key =obj.toString();
                if(key.endsWith(GATEWAY_PROPERTIES_PREFIX_SERVER_PATH)) {
                    keyPrefix = key.substring(0, key.indexOf(GATEWAY_PROPERTIES_PREFIX_SERVER_PATH));
                    serverPath = p.getProperty(keyPrefix +GATEWAY_PROPERTIES_PREFIX_SERVER_PATH).trim();
                    defaultAddr = p.getProperty(keyPrefix +GATEWAY_PROPERTIES_PREFIX_DEFAULT_ADDR).trim();

                    valuesMap = new LinkedHashMap<String, String>();
                    valuesMap.put(GATEWAY_PROPERTIES_DEFAULT_ADDR, defaultAddr);
                    valuesMap.put(GATEWAY_PROPERTIES_SERVER_PATH, serverPath);

                    mpv = newMutablePropertyValues(valuesMap);
                    beanDefinition = newGenericBeanDefinition();
                    beanDefinition.setBeanClass(GatewayAttribute.class);
                    beanDefinition.setPropertyValues(mpv);
                    registry.registerBeanDefinition(serverPath, beanDefinition);

                    KEY_GATEWAYS.add(serverPath);
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    //加载netty-route.properties配置文件
    private voidinitRouteRule(BeanDefinitionRegistry registry) {
        GenericBeanDefinition beanDefinition = newGenericBeanDefinition();
        Resource resource = newClassPathResource(GATEWAY_OPTION_ROUTE_CONFIG_FILE);
        Properties p = newProperties();

        try{
            p.load(resource.getInputStream());

            String key = null;
            String keyPrefix = null;
            String keyWord = null;
            String matchAddr = null;
            String serverPath = null;

            Map<String, String> valuesMap = null;
            MutablePropertyValues mpv = null;

            for(Object obj : p.keySet()) {
                key =obj.toString();
                if(key.endsWith(GATEWAY_PROPERTIES_PREFIX_KEY_WORD)) {
                    keyPrefix = key.substring(0, key.indexOf(GATEWAY_PROPERTIES_PREFIX_KEY_WORD));
                    keyWord = p.getProperty(keyPrefix +GATEWAY_PROPERTIES_PREFIX_KEY_WORD).trim();
                    if (keyWord.isEmpty()) continue;
                    matchAddr = p.getProperty(keyPrefix +GATEWAY_PROPERTIES_PREFIX_MATCH_ADDR).trim();
                    serverPath = p.getProperty(keyPrefix +GATEWAY_PROPERTIES_PREFIX_SERVER_PATH).trim();

                    valuesMap = new LinkedHashMap<String, String>();
                    valuesMap.put(GATEWAY_PROPERTIES_KEY_WORD, keyWord);
                    valuesMap.put(GATEWAY_PROPERTIES_MATCH_ADDR, matchAddr);
                    valuesMap.put(GATEWAY_PROPERTIES_SERVER_PATH, serverPath);

                    mpv = newMutablePropertyValues(valuesMap);
                    beanDefinition = newGenericBeanDefinition();
                    beanDefinition.setBeanClass(RouteAttribute.class);
                    beanDefinition.setPropertyValues(mpv);
                    String beanName = serverPath + GATEWAY_OPTION_SERVER_SPLIT +keyWord;
                    registry.registerBeanDefinition(beanName, beanDefinition);

                    KEY_ROUTERS.add(beanName);
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

最后是重点的关键字过滤转发代码模块,主要完成路由转发地址的匹配计算、路由转发、以及应答转发结果给请求客户端的工作。

importcom.newlandframework.gateway.commons.GatewayAttribute;
importcom.newlandframework.gateway.commons.HttpClientUtils;
importcom.newlandframework.gateway.commons.RouteAttribute;
importcom.newlandframework.gateway.commons.RoutingLoader;
importio.netty.buffer.ByteBuf;
importio.netty.buffer.Unpooled;
importio.netty.channel.ChannelFutureListener;
importio.netty.channel.ChannelHandlerContext;
importio.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
importio.netty.util.Signal;
importio.netty.util.concurrent.Future;
importio.netty.util.concurrent.FutureListener;
importio.netty.util.concurrent.GlobalEventExecutor;
importorg.springframework.util.StringUtils;

importjava.util.concurrent.Callable;
importjava.util.concurrent.CountDownLatch;
importjava.util.concurrent.TimeUnit;

import static com.newlandframework.gateway.commons.GatewayOptions.*;
import staticio.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import staticio.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import staticio.netty.handler.codec.http.HttpResponseStatus.OK;
import staticio.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/*** @authortangjie<https://github.com/tang-jie>
 * @filename:GatewayServerHandler.java
 * @description:GatewayServerHandler功能模块
 * @blogs http://www.cnblogs.com/jietang/* @since2018/4/18
 */
public class GatewayServerHandler extends SimpleChannelInboundHandler<Object>{
    privateHttpRequest request;
    private StringBuilder buffer = newStringBuilder();
    private String url = "";
    private String uri = "";
    privateStringBuilder respone;
    private GlobalEventExecutor executor =GlobalEventExecutor.INSTANCE;
    private CountDownLatch latch = new CountDownLatch(1);

    @Override
    public voidchannelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    protected voidchannelRead0(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceofHttpRequest) {
            HttpRequest request = this.request =(HttpRequest) msg;

            //收到客户端的100-Continue协议请求,说明客户端要post数据给服务器
            if(HttpUtil.is100ContinueExpected(request)) {
                notify100Continue(ctx);
            }

            buffer.setLength(0);
            uri = request.uri().substring(1);
        }

        if (msg instanceofHttpContent) {
            HttpContent httpContent =(HttpContent) msg;
            ByteBuf content =httpContent.content();
            if(content.isReadable()) {
                buffer.append(content.toString(GATEWAY_OPTION_CHARSET));
            }

            //获取post数据完毕
            if (msg instanceofLastHttpContent) {
                LastHttpContent trace =(LastHttpContent) msg;

                System.out.println("[NETTY-GATEWAY] REQUEST : " +buffer.toString());

                //根据netty-gateway.properties、netty-route.properties匹配出最终转发的URL地址
                url =matchUrl();
                System.out.println("[NETTY-GATEWAY] URL : " +url);

                //http请求异步转发处理,不要阻塞当前的Netty Handler的I/O线程,提高服务器的吞吐量。
                Future<StringBuilder> future = executor.submit(new Callable<StringBuilder>() {
                    @Override
                    publicStringBuilder call() {
                        returnHttpClientUtils.post(url, buffer.toString(), GATEWAY_OPTION_HTTP_POST);
                    }
                });

                future.addListener(new FutureListener<StringBuilder>() {
                    @Override
                    public void operationComplete(Future<StringBuilder> future) throwsException {
                        if(future.isSuccess()) {
                            respone =((StringBuilder) future.get(GATEWAY_OPTION_HTTP_POST, TimeUnit.MILLISECONDS));
                        } else{
                            respone = newStringBuilder(((Signal) future.cause()).name());
                        }
                        latch.countDown();
                    }
                });

                try{
                    latch.await();
                    writeResponse(respone, future.isSuccess() ? trace : null, ctx);
                    ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public voidexceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    //根据netty-gateway.properties、netty-route.properties匹配出最终转发的URL地址
    privateString matchUrl() {
        for(GatewayAttribute gateway : RoutingLoader.GATEWAYS) {
            if(gateway.getServerPath().equals(uri)) {
                for(RouteAttribute route : RoutingLoader.ROUTERS) {
                    if(route.getServerPath().equals(uri)) {
                        String[] keys =StringUtils.delimitedListToStringArray(route.getKeyWord(), GATEWAY_OPTION_KEY_WORD_SPLIT);
                        boolean match = true;
                        for(String key : keys) {
                            if (key.isEmpty()) continue;
                            if (buffer.toString().indexOf(key.trim()) == -1) {
                                match = false;
                                break;
                            }
                        }
                        if(match) {
                            returnroute.getMatchAddr();
                        }
                    }
                }

                returngateway.getDefaultAddr();
            }
        }
        returnGATEWAY_OPTION_LOCALHOST;
    }

    //把路由转发的结果应答给http客户端
    private voidwriteResponse(StringBuilder respone, HttpObject current, ChannelHandlerContext ctx) {
        if (respone != null) {
            boolean keepAlive =HttpUtil.isKeepAlive(request);

            FullHttpResponse response = newDefaultFullHttpResponse(
                    HTTP_1_1, current == null ? OK : current.decoderResult().isSuccess() ?OK : BAD_REQUEST,
                    Unpooled.copiedBuffer(respone.toString(), GATEWAY_OPTION_CHARSET));

            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=GBK");

            if(keepAlive) {
                response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }

            ctx.write(response);
        }
    }

    private static voidnotify100Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = newDefaultFullHttpResponse(HTTP_1_1, CONTINUE);
        ctx.write(response);
    }
}

这样把整个工程maven打包部署运行,服务器默认启动端口8999,你可以通过netty-gateway.xml的gatewayPort属性进行配置调整。

控制台打印出如下的信息,则说明服务器启动成功。

如何用Netty实现一个轻量级的HTTP代理服务器第2张

下面继续以一个实际的案例来说明一下,如何配置使用这个HTTP服务器。

NettyGateway代理转发场景描述

  • NettyGateway部署在10.1.1.76主机,URL中的路径为:fcgi-bin/BSSP_SFC
  • 如果请求报文中出现HelloWorldNettyGateway关键字的时候,转发到http://10.46.158.20:8089/fcgi-bin/BSSP_SFC
  • 否则转发到http://10.46.158.10:8089/fcgi-bin/BSSP_SFC

NettyGateway代理转发场景配置说明

  • 配置文件netty-gateway.properties新增如下属性:
netty-gateway.config2.serverPath=fcgi-bin/BSSP_SFC
netty-gateway.config2.defaultAddr=http://10.46.158.10:8089/fcgi-bin/BSSP_SFC
  • 配置文件netty-route.properties新增如下属性:
netty-gateway.config3.serverPath=fcgi-bin/BSSP_SFC
netty-gateway.config3.keyWord=HelloWorldNettyGateway
netty-gateway.config3.matchAddr=http://10.46.158.20:8089/fcgi-bin/BSSP_SFC

NettyGateway代理转发测试

  • 发送HelloWorldNettyGateway到NettyGateway,关键字匹配成功,路由到http://10.46.158.20:8089/fcgi-bin/BSSP_SFC

如何用Netty实现一个轻量级的HTTP代理服务器第3张

  • 发送Tangjie到NettyGateway,关键字匹配不成功,路由到默认的http://10.46.158.10:8089/fcgi-bin/BSSP_SFC

如何用Netty实现一个轻量级的HTTP代理服务器第4张

到此,整个基于Netty实现的,一个轻量级HTTP代理服务器的主要设计思路已经介绍完了。整个服务器实现代码非常的少,而且通过简单地配置,就能很好的满足实际要求。相比通过“重量级”的服务器:Apache、Nginx,进行HTTP代理转发而言,提供了另外一种解决问题的思路。在部门的实际部署运行中,这个Netty写的小而精的服务器,运行良好,很好地帮助测试部门的同学,解决了一个困扰他们很久的问题。

俗话说得好,黑猫、白猫,抓到老鼠就是好猫。我把这个基于Netty的HTTP代理服务器取名“NettyGateway”,目前把代码托管在github上面:https://github.com/tang-jie/NettyGateway

有兴趣的朋友,可以关注一下。由于技术能力所限,文中难免有纰漏和不足,大家如果有疑问,欢迎在下方的博客园评论区留言,或者通过github提issue给我。

最后,感谢您耐心阅读。如果喜欢本文的话,可以点击推荐,算是给我一个小小的鼓励!谢谢大家。

附上本人曾经在博客园发表的,基于Netty框架实际应用的原创文章,有兴趣的朋友,可以关联阅读。

基于Netty构建的RPC

谈谈如何使用Netty开发实现高性能的RPC服务器

Netty实现高性能RPC服务器优化篇之消息序列化

基于Netty打造RPC服务器设计经验谈

基于Netty构建的简易消息队列

Netty构建分布式消息队列(AvatarMQ)设计指南之架构篇

Netty构建分布式消息队列实现原理浅析

免责声明:文章转载自《如何用Netty实现一个轻量级的HTTP代理服务器》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Effective Refactoring in C++ (三)vue-json-editor json编辑器下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

调用钉钉接口发送消息

1.首先登陆钉钉开发者后台 https://ding-doc.dingtalk.com/ 2.选择H5微应用,创建应用 4.创建好之后,查看所建好的应用信息 其中AgentId,AppKey,AppSecret很重要,调用时需要用到 5.直接上代码看效果 1 string appkey = "dingv0cab6brl1ax6exd"; 2...

【C#.NET Core】 跨平台执行javascript

背景 在Windows上c#调用JavaScript常用V8.NET,项目迁移到 Linux 才注意到v8不支持Linux,遂改为JavaScriptEngineSwitcher.ChakraCore(直接在nugget上搜索JavaScriptEngine) 使用引入nugget包-JavaScriptEngineSwitcher.ChakraCore...

Flask之Sqlalchemy

Sqlalchemy 开发文档:https://www.jianshu.com/p/0ad18fdd7eed 创建数据库 安装 pip instal flask-sqlalchemy 两种配置方法 # 两种配置数据库方法 第一种app.config from flask import Flask import pymysql from flask_sq...

简单对象访问协议(SOAP)初级指南[转]

这篇文章带你全面回顾对象远程进程调用(ORPC)技术的历程,以帮助你理解SOAP技术的基础,以及它克服存在技术(如CORBA和DCOM)的许多缺陷的方法。随后讲述详细的SOAP编码规则,并把焦点放在SOAP是怎样映射到存在的ORPC概念上的。   引言:   当我在1984年开始把计算作为我的职业的时候,大多数程序员并不关心网络协议。但是在九十年代网络变得...

Mongodb学习笔记五(C#操作mongodb)

mongodb c# driver(驱动)介绍 目前基于C#的mongodb驱动有两种,分别是官方驱动(下载地址)和samus驱动(下载地址)。本次我们只演示官方驱动的使用方法。官方驱动文档查看 第一步:引用驱动dll 引用驱动有两种方式:1. 根据上面的下载地址下载对应的版本,然后引用到项目中。2. 在项目的引用上右击->管理NuGet程序包(首先...

【学习】java下实现调用oracle的存储过程和函数

在oracle下创建一个test的账户,然后按一下步骤执行: 1.创建表:STOCK_PRICES View Code--创建表格CREATETABLE STOCK_PRICES( RIC VARCHAR(6) PRIMARYKEY, PRICE NUMBER(7,2), UPDATED DATE ); 2.插入测试数据: View...