100 行代碼搞定了 RPC 原理,大家隨便問。。

来源:https://www.cnblogs.com/javastack/archive/2022/06/20/16394139.html
-Advertisement-
Play Games

作者:孫浩 來源:https://xiaomi-info.github.io/2020/03/02/rpc-achieve/ 引言 本文主要論述的是“RPC 實現原理”,那麼首先明確一個問題什麼是 RPC 呢?RPC 是 Remote Procedure Call 的縮寫,即,遠程過程調用。RPC ...


作者:孫浩
來源:https://xiaomi-info.github.io/2020/03/02/rpc-achieve/

引言

本文主要論述的是“RPC 實現原理”,那麼首先明確一個問題什麼是 RPC 呢?RPC 是 Remote Procedure Call 的縮寫,即,遠程過程調用。RPC 是一個電腦通信協議。該協議允許運行於一臺電腦的程式調用另一臺電腦的子程式,而開發人員無需額外地為這個交互編程。
值得註意是,兩個或多個應用程式都分佈在不同的伺服器上,它們之間的調用都像是本地方法調用一樣。接下來我們便來分析一下一次 RPC 調用發生了些什麼?

一次基本的 RPC 調用會涉及到什麼?

現在業界內比較流行的一些 RPC 框架,例如 Dubbo 提供的是基於介面的遠程方法調用,即客戶端只需要知道介面的定義即可調用遠程服務。在 Java 中介面並不能直接調用實例方法,必須通過其實現類對象來完成此操作,這意味著客戶端必須為這些介面生成代理對象,對此 Java 提供了 ProxyInvocationHandler 生成動態代理的支持;生成了代理對象,那麼每個具體的發方法是怎麼調用的呢?jdk 動態代理生成的代理對象調用指定方法時實際會執行 InvocationHandler 中定義的 #invoke 方法,在該方法中完成遠程方法調用並獲取結果。

拋開客戶端,回過頭來看 RPC 是兩台電腦間的調用,實質上是兩台主機間的網路通信,涉及到網路通信又必然會有序列化、反序列化編解碼等一些必須要考慮的問題;同時實際上現在大多系統都是集群部署的,多台主機/容器對外提供相同的服務,如果集群的節點數量很大的話,那麼管理服務地址也將是一件十分繁瑣的事情,常見的做法是各個服務節點將自己的地址和提供的服務列表註冊到一個 註冊中心,由 註冊中心 來統一管理服務列表;這樣的做法解決了一些問題同時為客戶端增加了一項新的工作——那就是服務發現,通俗來說就是從註冊中心中找到遠程方法對應的服務列表並通過某種策略從中選取一個服務地址來完成網路通信。

聊了客戶端和 註冊中心,另外一個重要的角色自然是服務端,服務端最重要的任務便是提供服務介面的真正實現併在某個埠上監聽網路請求,監聽到請求後從網路請求中獲取到對應的參數(比如服務介面、方法、請求參數等),再根據這些參數通過反射的方式調用介面的真正實現獲取結果並將其寫入對應的響應流中。

綜上所述,一次基本的 RPC 調用流程大致如下:

基本實現

服務端(生產者)

  • 服務介面

在 RPC 中,生產者和消費者有一個共同的服務介面 API。如下,定義一個 HelloService 介面。

/**
 * @author 孫浩
 * @Descrption  服務介面
 ***/
public interface HelloService {
    String sayHello(String somebody);
}
  • 服務實現

生產者要提供服務介面的實現,創建 HelloServiceImpl 實現類。

/**
 * @author 孫浩
 * @Descrption 服務實現
 ***/
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String somebody) {
        return "hello " + somebody + "!";
    }
}
  • 服務註冊

本例使用 Spring 來管理 bean,採用自定義 xml 和解析器的方式來將服務實現類載入容器(當然也可以採用自定義註解的方式,此處不過多論述)並將服務介面信息註冊到註冊中心。
首先自定義xsd,

<xsd:element name="service">
    <xsd:complexType>
        <xsd:complexContent>
            <xsd:extension base="beans:identifiedType">
                <xsd:attribute name="interface" type="xsd:string" use="required"/>
                <xsd:attribute name="timeout" type="xsd:int" use="required"/>
                <xsd:attribute name="serverPort" type="xsd:int" use="required"/>
                <xsd:attribute name="ref" type="xsd:string" use="required"/>
                <xsd:attribute name="weight" type="xsd:int" use="optional"/>
                <xsd:attribute name="workerThreads" type="xsd:int" use="optional"/>
                <xsd:attribute name="appKey" type="xsd:string" use="required"/>
                <xsd:attribute name="groupName" type="xsd:string" use="optional"/>
            </xsd:extension>
        </xsd:complexContent>
    </xsd:complexType>
</xsd:element>

分別指定 schema 和 xmd,schema 和對應 handler 的映射:

schema
http\://www.storm.com/schema/storm-service.xsd=META-INF/storm-service.xsd
http\://www.storm.com/schema/storm-reference.xsd=META-INF/storm-reference.xsd
handler
http\://www.storm.com/schema/storm-service=com.hsunfkqm.storm.framework.spring.StormServiceNamespaceHandler
http\://www.storm.com/schema/storm-reference=com.hsunfkqm.storm.framework.spring.StormRemoteReferenceNamespaceHandler

將編寫好的文件放入 classpath 下的 META-INF 目錄下:

在 Spring 配置文件中配置服務類:

<!-- 發佈遠程服務 -->
 <bean id="helloService" class="com.hsunfkqm.storm.framework.test.HelloServiceImpl"/>
 <storm:service id="helloServiceRegister"
                     interface="com.hsunfkqm.storm.framework.test.HelloService"
                     ref="helloService"
                     groupName="default"
                     weight="2"
                     appKey="ares"
                     workerThreads="100"
                     serverPort="8081"
                     timeout="600"/>

編寫對應的 Handler 和 Parser:StormServiceNamespaceHandler

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
 * @author 孫浩
 * @Descrption 服務發佈自定義標簽
 ***/
public class StormServiceNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("service", new ProviderFactoryBeanDefinitionParser());
    }
}

ProviderFactoryBeanDefinitionParser

protected Class getBeanClass(Element element) {
        return ProviderFactoryBean.class;
    }

    protected void doParse(Element element, BeanDefinitionBuilder bean) {

        try {
            String serviceItf = element.getAttribute("interface");
            String serverPort = element.getAttribute("serverPort");
            String ref = element.getAttribute("ref");
            // ....
            bean.addPropertyValue("serverPort", Integer.parseInt(serverPort));
            bean.addPropertyValue("serviceItf", Class.forName(serviceItf));
            bean.addPropertyReference("serviceObject", ref);
            //...
            if (NumberUtils.isNumber(weight)) {
                bean.addPropertyValue("weight", Integer.parseInt(weight));
            }
            //...
       } catch (Exception e) {
            // ...
      }
    }

ProviderFactoryBean

/**
 * @author 孫浩
 * @Descrption 服務發佈
 ***/
public class ProviderFactoryBean implements FactoryBean, InitializingBean {

    //服務介面
    private Class<?> serviceItf;
    //服務實現
    private Object serviceObject;
    //服務埠
    private String serverPort;
    //服務超時時間
    private long timeout;
    //服務代理對象,暫時沒有用到
    private Object serviceProxyObject;
    //服務提供者唯一標識
    private String appKey;
    //服務分組組名
    private String groupName = "default";
    //服務提供者權重,預設為 1 , 範圍為 [1-100]
    private int weight = 1;
    //服務端線程數,預設 10 個線程
    private int workerThreads = 10;

    @Override
    public Object getObject() throws Exception {
        return serviceProxyObject;
    }

    @Override
    public Class<?> getObjectType() {
        return serviceItf;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //啟動 Netty 服務端
        NettyServer.singleton().start(Integer.parseInt(serverPort));
        //註冊到 zk, 元數據註冊中心
        List<ProviderService> providerServiceList = buildProviderServiceInfos();
        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
        registerCenter4Provider.registerProvider(providerServiceList);
    }
}

//================RegisterCenter#registerProvider======================
@Override
public void registerProvider(final List<ProviderService> serviceMetaData) {
    if (CollectionUtils.isEmpty(serviceMetaData)) {
        return;
    }

    //連接 zk, 註冊服務
    synchronized (RegisterCenter.class) {
        for (ProviderService provider : serviceMetaData) {
            String serviceItfKey = provider.getServiceItf().getName();

            List<ProviderService> providers = providerServiceMap.get(serviceItfKey);
            if (providers == null) {
                providers = Lists.newArrayList();
            }
            providers.add(provider);
            providerServiceMap.put(serviceItfKey, providers);
        }

        if (zkClient == null) {
            zkClient = new ZkClient(ZK_SERVICE, ZK_SESSION_TIME_OUT, ZK_CONNECTION_TIME_OUT, new SerializableSerializer());
        }

        //創建 ZK 命名空間/當前部署應用 APP 命名空間/
        String APP_KEY = serviceMetaData.get(0).getAppKey();
        String ZK_PATH = ROOT_PATH + "/" + APP_KEY;
        boolean exist = zkClient.exists(ZK_PATH);
        if (!exist) {
            zkClient.createPersistent(ZK_PATH, true);
        }

        for (Map.Entry<String, List<ProviderService>> entry : providerServiceMap.entrySet()) {
            //服務分組
            String groupName = entry.getValue().get(0).getGroupName();
            //創建服務提供者
            String serviceNode = entry.getKey();
            String servicePath = ZK_PATH + "/" + groupName + "/" + serviceNode + "/" + PROVIDER_TYPE;
            exist = zkClient.exists(servicePath);
            if (!exist) {
                zkClient.createPersistent(servicePath, true);
            }

            //創建當前伺服器節點
            int serverPort = entry.getValue().get(0).getServerPort();//服務埠
            int weight = entry.getValue().get(0).getWeight();//服務權重
            int workerThreads = entry.getValue().get(0).getWorkerThreads();//服務工作線程
            String localIp = IPHelper.localIp();
            String currentServiceIpNode = servicePath + "/" + localIp + "|" + serverPort + "|" + weight + "|" + workerThreads + "|" + groupName;
            exist = zkClient.exists(currentServiceIpNode);
            if (!exist) {
                //註意,這裡創建的是臨時節點
                zkClient.createEphemeral(currentServiceIpNode);
            }
            //監聽註冊服務的變化,同時更新數據到本地緩存
            zkClient.subscribeChildChanges(servicePath, new IZkChildListener() {
                @Override
                public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
                    if (currentChilds == null) {
                        currentChilds = Lists.newArrayList();
                    }
                    //存活的服務 IP 列表
                    List<String> activityServiceIpList = Lists.newArrayList(Lists.transform(currentChilds, new Function<String, String>() {
                        @Override
                        public String apply(String input) {
                            return StringUtils.split(input, "|")[0];
                        }
                    }));
                    refreshActivityService(activityServiceIpList);
                }
            });

        }
    }
}

至此服務實現類已被載入 Spring 容器中,且服務介面信息也註冊到了註冊中心。

  • 網路通信

作為生產者對外提供 RPC 服務,必須有一個網路程式來來監聽請求和做出響應。在 Java 領域 Netty 是一款高性能的 NIO 通信框架,很多的框架的通信都是採用 Netty 來實現的,本例中也採用它當做通信伺服器。

構建並啟動 Netty 服務監聽指定埠:

public void start(final int port) {
    synchronized (NettyServer.class) {
        if (bossGroup != null || workerGroup != null) {
            return;
        }

        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        //註冊解碼器 NettyDecoderHandler
                        ch.pipeline().addLast(new NettyDecoderHandler(StormRequest.class, serializeType));
                        //註冊編碼器 NettyEncoderHandler
                        ch.pipeline().addLast(new NettyEncoderHandler(serializeType));
                        //註冊服務端業務邏輯處理器 NettyServerInvokeHandler
                        ch.pipeline().addLast(new NettyServerInvokeHandler());
                    }
                });
        try {
            channel = serverBootstrap.bind(port).sync().channel();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

上面的代碼中向 Netty 服務的 pipeline 中添加了編解碼和業務處理器,當接收到請求時,經過編解碼後,真正處理業務的是業務處理器,即NettyServerInvokeHandler, 該處理器繼承自SimpleChannelInboundHandler, 當數據讀取完成將觸發一個事件,並調用NettyServerInvokeHandler#channelRead0方法來處理請求。

@Override
protected void channelRead0(ChannelHandlerContext ctx, StormRequest request) throws Exception {
    if (ctx.channel().isWritable()) {
        //從服務調用對象里獲取服務提供者信息
        ProviderService metaDataModel = request.getProviderService();
        long consumeTimeOut = request.getInvokeTimeout();
        final String methodName = request.getInvokedMethodName();

        //根據方法名稱定位到具體某一個服務提供者
        String serviceKey = metaDataModel.getServiceItf().getName();
        //獲取限流工具類
        int workerThread = metaDataModel.getWorkerThreads();
        Semaphore semaphore = serviceKeySemaphoreMap.get(serviceKey);
        if (semaphore == null) {
            synchronized (serviceKeySemaphoreMap) {
                semaphore = serviceKeySemaphoreMap.get(serviceKey);
                if (semaphore == null) {
                    semaphore = new Semaphore(workerThread);
                    serviceKeySemaphoreMap.put(serviceKey, semaphore);
                }
            }
        }

        //獲取註冊中心服務
        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
        List<ProviderService> localProviderCaches = registerCenter4Provider.getProviderServiceMap().get(serviceKey);

        Object result = null;
        boolean acquire = false;

        try {
            ProviderService localProviderCache = Collections2.filter(localProviderCaches, new Predicate<ProviderService>() {
                @Override
                public boolean apply(ProviderService input) {
                    return StringUtils.equals(input.getServiceMethod().getName(), methodName);
                }
            }).iterator().next();
            Object serviceObject = localProviderCache.getServiceObject();

            //利用反射發起服務調用
            Method method = localProviderCache.getServiceMethod();
            //利用 semaphore 實現限流
            acquire = semaphore.tryAcquire(consumeTimeOut, TimeUnit.MILLISECONDS);
            if (acquire) {
                result = method.invoke(serviceObject, request.getArgs());
                //System.out.println("---------------"+result);
            }
        } catch (Exception e) {
            System.out.println(JSON.toJSONString(localProviderCaches) + "  " + methodName+" "+e.getMessage());
            result = e;
        } finally {
            if (acquire) {
                semaphore.release();
            }
        }
        //根據服務調用結果組裝調用返回對象
        StormResponse response = new StormResponse();
        response.setInvokeTimeout(consumeTimeOut);
        response.setUniqueKey(request.getUniqueKey());
        response.setResult(result);
        //將服務調用返回對象回寫到消費端
        ctx.writeAndFlush(response);
    } else {
        logger.error("------------channel closed!---------------");
    }
}

此處還有部分細節如自定義的編解碼器等,篇幅所限不在此詳述,繼承 MessageToByteEncoderByteToMessageDecoder 覆寫對應的 encodedecode 方法即可自定義編解碼器,使用到的序列化工具如 Hessian/Proto 等可參考對應的官方文檔。

  • 請求和響應包裝
    為便於封裝請求和響應,定義兩個 bean 來表示請求和響應。

請求:

/**
 * @author 孫浩
 * @Descrption
 ***/
public class StormRequest implements Serializable {

    private static final long serialVersionUID = -5196465012408804755L;
    //UUID, 唯一標識一次返回值
    private String uniqueKey;
    //服務提供者信息
    private ProviderService providerService;
    //調用的方法名稱
    private String invokedMethodName;
    //傳遞參數
    private Object[] args;
    //消費端應用名
    private String appName;
    //消費請求超時時長
    private long invokeTimeout;
    // getter/setter
}

響應:

/**
 * @author 孫浩
 * @Descrption
 ***/
public class StormResponse implements Serializable {
    private static final long serialVersionUID = 5785265307118147202L;
    //UUID, 唯一標識一次返回值
    private String uniqueKey;
    //客戶端指定的服務超時時間
    private long invokeTimeout;
    //介面調用返回的結果對象
    private Object result;
    //getter/setter
}

客戶端(消費者)

客戶端(消費者)在 RPC 調用中主要是生成服務介面的代理對象,並從註冊中心獲取對應的服務列表發起網路請求。
客戶端和服務端一樣採用 Spring 來管理 bean 解析 xml 配置等不再贅述,重點看下以下幾點:

  • 通過 jdk 動態代理來生成引入服務介面的代理對象
public Object getProxy() {
    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{targetInterface}, this);
}
  • 從註冊中心獲取服務列表並依據某種策略選取其中一個服務節點
//服務介面名稱
String serviceKey = targetInterface.getName();
//獲取某個介面的服務提供者列表
IRegisterCenter4Invoker registerCenter4Consumer = RegisterCenter.singleton();
List<ProviderService> providerServices = registerCenter4Consumer.getServiceMetaDataMap4Consume().get(serviceKey);
//根據軟負載策略,從服務提供者列表選取本次調用的服務提供者
ClusterStrategy clusterStrategyService = ClusterEngine.queryClusterStrategy(clusterStrategy);
ProviderService providerService = clusterStrategyService.select(providerServices);
  • 通過 Netty 建立連接,發起網路請求
/**
 * @author 孫浩
 * @Descrption Netty 消費端 bean 代理工廠
 ***/
public class RevokerProxyBeanFactory implements InvocationHandler {
    private ExecutorService fixedThreadPool = null;
    //服務介面
    private Class<?> targetInterface;
    //超時時間
    private int consumeTimeout;
    //調用者線程數
    private static int threadWorkerNumber = 10;
    //負載均衡策略
    private String clusterStrategy;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        ...

        //複製一份服務提供者信息
        ProviderService newProvider = providerService.copy();
        //設置本次調用服務的方法以及介面
        newProvider.setServiceMethod(method);
        newProvider.setServiceItf(targetInterface);

        //聲明調用 AresRequest 對象,AresRequest 表示發起一次調用所包含的信息
        final StormRequest request = new StormRequest();
        //設置本次調用的唯一標識
        request.setUniqueKey(UUID.randomUUID().toString() + "-" + Thread.currentThread().getId());
        //設置本次調用的服務提供者信息
        request.setProviderService(newProvider);
        //設置本次調用的方法名稱
        request.setInvokedMethodName(method.getName());
        //設置本次調用的方法參數信息
        request.setArgs(args);

        try {
            //構建用來發起調用的線程池
            if (fixedThreadPool == null) {
                synchronized (RevokerProxyBeanFactory.class) {
                    if (null == fixedThreadPool) {
                        fixedThreadPool = Executors.newFixedThreadPool(threadWorkerNumber);
                    }
                }
            }
            //根據服務提供者的 ip,port, 構建 InetSocketAddress 對象,標識服務提供者地址
            String serverIp = request.getProviderService().getServerIp();
            int serverPort = request.getProviderService().getServerPort();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIp, serverPort);
            //提交本次調用信息到線程池 fixedThreadPool, 發起調用
            Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
            //獲取調用的返回結果
            StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
            if (response != null) {
                return response.getResult();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return null;
    }
    //  ...
}

Netty 的響應是非同步的,為了在方法調用返回前獲取到響應結果,需要將非同步的結果同步化。

  • Netty 非同步返回的結果存入阻塞隊列
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, StormResponse response) throws Exception {
    //將 Netty 非同步返回的結果存入阻塞隊列,以便調用端同步獲取
    RevokerResponseHolder.putResultValue(response);
}
  • 請求發出後同步獲取結果
//提交本次調用信息到線程池 fixedThreadPool, 發起調用
Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
//獲取調用的返回結果
StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (response != null) {
    return response.getResult();
}

//===================================================
//從返回結果容器中獲取返回結果,同時設置等待超時時間為 invokeTimeout
long invokeTimeout = request.getInvokeTimeout();
StormResponse response = RevokerResponseHolder.getValue(request.getUniqueKey(), invokeTimeout);

測試

Server

/**
 * @author 孫浩
 * @Descrption
 ***/
public class MainServer {
    public static void main(String[] args) throws Exception {
        //發佈服務
        final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-server.xml");
        System.out.println(" 服務發佈完成");
    }
}

Client

public class Client {

    private static final Logger logger = LoggerFactory.getLogger(Client.class);

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

        final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-client.xml");
        final HelloService helloService = (HelloService) context.getBean("helloService");
        String result = helloService.sayHello("World");
        System.out.println(result);
        for (;;) {

        }
    }
}

結果

生產者:

消費者:

註冊中心

img

總結

本文簡單介紹了 RPC 的整個流程,並實現了一個簡單的 RPC 調用。希望閱讀完本文之後,能加深你對 RPC 的一些認識。

  • 生產者端流程:
    • 載入服務介面,並緩存
    • 服務註冊,將服務介面以及服務主機信息寫入註冊中心(本例使用的是 zookeeper)
    • 啟動網路伺服器並監聽
    • 反射,本地調用
  • 消費者端流程:
    • 代理服務介面生成代理對象
    • 服務發現(連接 zookeeper,拿到服務地址列表,通過客戶端負載策略獲取合適的服務地址)
    • 遠程方法調用(本例通過 Netty,發送消息,並獲取響應結果)

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 基於Vue.js 2.x系列 + Element UI 的後臺系統許可權控制 前言:關於vue許可權路由的那些事兒…… 項目背景:現有一個後臺管理系統,共存在兩種類型的人員 ①超級管理員(稱作admin),②普通用戶(稱作editor) 每種 ...
  • 1、新建文件夾在終端打開執行 npm init -y 生成package.json如下,註意如果要發佈到npm,name不能有下劃線,大寫字母等 { "name": "vuecomponentdi", "version": "1.0.0", "description": "", "main": "i ...
  • 介紹 TypeScript 的核心原則之一是對值所具有的結構進行類型檢查。我們使用介面(Interfaces)來定義對象的類型。介面是對象的狀態(屬性)和行為(方法)的抽象(描述) 介面初探 聲明介面需要使用關鍵字interface,接下來我們定義一個介面,需求如下 需求: 創建人的對象, 需要對人 ...
  • HTML基本結構 HTML初識 HTML(Hyper Text Markup Language):超文本標記語言 所謂超文本,有2層含義: 它可以加入圖片、聲音、動畫、多媒體等內容(超越文本限制 ) 它可以從一個文件跳轉到另一個文件,與世界各地主機的文件連接(超級鏈接文本)。 「HTML骨架格式」 ...
  • 本章是系列文章的第七章,終於來到了鼎鼎大名的SSA,SSA是編譯器領域最偉大的發明之一,也是影響最廣的發明。 本文中的所有內容來自學習DCC888的學習筆記或者自己理解的整理,如需轉載請註明出處。周榮華@燧原科技 7.1 控制流圖回顧 對下麵的c代碼保存成7.1.cc: 1 int max(int ...
  • 1、HashSet特點 存放的元素是無序的(不保證添加元素的順序) 元素唯一(不可以重覆) 可以存null,但是只能存放1個 雖然set集合不保證添加元素的順序,但是集合中存放的元素順序其實是固定的,根據元素的hash值確定的順序 2、HashSet原理分析 HashSet底層,是藉助HashMap ...
  • 1. 演示數據 本文的所有演示數據,均是基於下方的四張表。下麵這四張表大家應該不陌生,這就是網傳50道經典MySQL面試題中使用到的幾張原表。關於下方各表之間的關聯關係,我就不給大家說明瞭,仔細觀察欄位名,應該就可以發現。 2. pandasql的使用 1)簡介 pandas中的DataFrame是 ...
  • 前言 Python是一種通用的高級編程語言。用它可以做許多事,比如開發桌面 GUI 應用程式、網站和 Web 應用程式等。 作為一種高級編程語言,Python 還可以讓你通過處理常見的編程任務來專註應用程式的核心功能。並且,編程語言的簡單語法規 則進一步簡化了代碼庫的可讀性和應用程式的可維護性。 與 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...