刺激銷量?北京6月起收取電動車充電服務費

北京市發改委於5月7日發佈了《關於本市電動汽車充電服務收費有關問題的通知》,從2015年6月1日起,提供電動汽車充電服務的充電設施經營單位在收取電費的同時,可按充電電量額外收取充電服務費,每千瓦時收費上限標準為當日本市92號汽油每升最高零售價的15%。各經營單位可在不超過上限標準情況下,制定具體收費標準。   其充電服務費上限標準隨油價變動自行動態調整。如2015年4月29日本市92號汽油最高零售價為6.46元人民幣(下同)/升,則充電服務收費上限標準為6.46元的15%,即0.97元/度。油價上升,充電服務費相應上升油價下降,充電服務費相應下降。   北京市發改委副主任高朋指出,根據測算,此標準可以確保電動汽車動力成本低於燃油汽車。以本市銷售相對較好的北汽E150EV電動汽車為例(100公里平均耗電16度),當油價在6-10元/升區間變動時,充電服務費為每度電0.9元-1.5元。按國家發展改革委檔,對向電力公司直接報裝的充電服務設施,按大工業用電價格標準執行,按此測算,加上電價費用,電動汽車動力成本約為同款燃油汽車的50%-60%左右。   對此有觀點認為,發改委此舉會影響電動汽車銷量,降低消費者對電動車的購買慾。汽車產業分析師張志勇指出,目前,電動車推廣最大的障礙是充電問題,《通知》中規定了電動車充電服務費,可以鼓勵一些民營資本來加入電動車充電網路的建設,此舉將加快北京地區的充電網路建設。   張志勇還指出,即使充電設施經營單位額外收取充電服務費,這個成本也是遠遠低於燃油車的用車成本,這對消費者來看並不能產生多大影響。因此《通知》發布後不僅不會影響電動車的銷量,反而會增加消費者購買電動車的信心。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

一起玩轉微服務(14)——單元測試

作為一名java開發者,相信你或多或少的接觸過單元測試,對於測試來講它是一門能夠區分專業開發人員與業餘開發人員的重要學科,這篇文章將對java中最常見的一個單元測試框架junit進行一個梳理和講解。

為什麼需要單元測試

在平時的開發當中,一個項目往往包含了大量的方法,可能有成千上萬個。如何去保證這些方法產生的結果是我們想要的呢?當然了,最容易想到的一個方式,就是我們通過System.out來輸出我們的結果,看看是不是滿足我們的需求,但是項目中這些成千上萬個方法,我們總不能在每一個方法中都去輸出一遍嘛。這也太枯燥了。這時候用我們的單元測試框架junit就可以很好地解決這個問題。

junit如何解決這個問題的呢?答案在於內部提供了一個斷言機制,他能夠將我們預期的結果和實際的結果進行比對,判斷出是否滿足我們的期望。

預備工作

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

這裏的版本是4.12。當然還有最新的版本。你可以手動選擇。這裏選用的是4的版本。

案例

這裏我們要測試的功能超級簡單,就是加減乘除法的驗證。

然後我們看看如何使用junit去測試。

以上就是我們的單元測試,需要遵循一下規則:

  • •每一個測試方法上使用@Test進行修飾
  • •每一個測試方法必須使用public void 進行修飾
  • •每一個測試方法不能攜帶參數
  • •測試代碼和源代碼在兩個不同的項目路徑下
  • •測試類的包應該和被測試類保持一致
  • •測試單元中的每個方法必須可以獨立測試

以上的6條規則,是在使用單元測試的必須項,當然junit也建議我們在每一個測試方法名加上test前綴,表明這是一個測試方法。

assertEquals是一個斷言的規則,裏面有兩個參數,第一個參數表明我們預期的值,第二個參數表示實際運行的值。

我們運行一下測試類,就會運行每一個測試方法,我們也可以運行某一個,只需要在相應的測試方法上面右鍵運行即可。如果運行成功編輯器的控制台不會出現錯誤信息,如果有就會出現failure等信息。

運行流程

在上面的每一個測試方法中,代碼是相當簡單的,就一句話。現在我們分析一下這個測試的流程是什麼:

在上面的代碼中,我們使用了兩個測試方法,還有junit運行整個流程方法。我們可以運行一下,就會出現下面的運行結果:

從上面的結果我們來畫一張流程圖就知道了:

如果我們使用過SSM等其他的一些框架,經常會在before中添加打開數據庫等預處理的代碼,也會在after中添加關閉流等相關代碼。

註解

對於@Test,裏面有很多參數供我們去選擇。我們來認識一下

  • •@Test(expected=XX.class) 這個參數表示我們期望會出現什麼異常,比如說在除法中,我們1/0會出現ArithmeticException異常,那這裏@Test(expected=ArithmeticException.class)。在測試這個除法時候依然能夠通過。
  • •@Test(timeout=毫秒 ) 這個參數表示如果測試方法在指定的timeout內沒有完成,就會強制停止。
  • •@Ignore 這個註解其實基本上不用,他的意思是所修飾的測試方法會被測試運行器忽略。•@RunWith 更改測試運行器。

測試套件

如果我們的項目中如果有成千上萬個方法,那此時也要有成千上萬個測試方法嘛?如果這樣junit使用起來還不如System.out呢,現在我們認識一下測試嵌套的方法,他的作用是我們把測試類封裝起來,也就是把測試類嵌套起來,只需要運行測試套件,就能運行所有的測試類了。

下面我們使用測試套件,把這些測試類嵌套在一起。

 

 

 

參數化設置

什麼是參數化設置呢?在一開始的代碼中我們看到,測試加法的時候是1+1,不過我們如果要測試多組數據怎麼辦?總不能一個一個輸入,然後運行測試吧。這時候我們可以把我們需要測試的數據先配置好。

這時候再去測試,只需要去選擇相應的值即可,避免了我們一個一個手動輸入。

spring boot + junit

通過spring suite tools新建工程

 

 

1. Controller

@RestController
@RequestMapping
public class BookController {
    @RequestMapping("/books")
    public String book() {
        System.out.println("controller");
        return "book";
    }
}

Test1 引入Spring上下文,但不啟動tomcat

@RunWith(SpringRunner.class)
@SpringBootTest  //引入Spring上下文 -> 上下文中的 bean 可用,自動注入
public class BookControllerTest {
    
    @Autowired
    private BookController bookController;  //自動注入
    
    @Test
    public void testControllerExists() {
        Assert.assertNotNull(bookController);
    }
    
}

Test2 引入Spring上下文,且啟動Tomcat 模擬生產環境,接收Http請求

package com.cloud.skyme;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;

/** * @author zhangfeng * web單元測試 * */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Chapter0302junitApplicationTests {
	
	@LocalServerPort
    private int port;
	
	@Autowired
	private TestRestTemplate restTemplate;
    
    @Test
    public void testControllerExists() {
    	Assert.assertEquals(this.restTemplate.getForObject("http://localhost:" + port + "/books", String.class), "book");
    }

}

@RunWith(SpringRunner.class),讓測試運行於Spring測試環境,此註釋在org.springframework.test.annotation包中提供。
@SpringBootTest指定Sspring Bboot程序的測試引導入口。
TestRestTemplate是用於測試rest接口的模板類。
運行單元測試,測試上面邊構建的Wweb地址,可以看到輸出的測試結果與期望的結果相同.

運行單元測試,得到與期望相同的結果。

    
javascript    44行

13:31:03.722 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate] 13:31:03.739 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)] 13:31:03.801 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.cloud.skyme.Chapter0302junitApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] 13:31:03.830 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.cloud.skyme.Chapter0302junitApplicationTests], using SpringBootContextLoader 13:31:03.837 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTests-context.xml] does not exist 13:31:03.838 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTestsContext.groovy] does not exist 13:31:03.838 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
13:31:03.839 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: Chapter0302junitApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 13:31:03.918 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.cloud.skyme.Chapter0302junitApplicationTests] 13:31:04.070 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [C:\java\workspace\microservice\chapter0302junit\target\classes\com\cloud\skyme\Chapter0302junitApplication.class] 13:31:04.073 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.cloud.skyme.Chapter0302junitApplication for test class com.cloud.skyme.Chapter0302junitApplicationTests 13:31:04.225 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.cloud.skyme.Chapter0302junitApplicationTests]: using defaults. 13:31:04.226 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener] 13:31:04.243 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource] 13:31:04.244 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute] 13:31:04.244 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@7133da86, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@3232a28a, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@73e22a3d, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@47faa49c, org.springframework.test.context.support.DirtiesContextTestExecutionListener@28f2a10f, org.springframework.test.context.event.EventPublishingTestExecutionListener@f736069, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@6da21078, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@7fee8714, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@4229bb3f, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@56cdfb3b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@2b91004a, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@20ccf40b] 13:31:04.250 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]], class annotated with @DirtiesContext [false] with mode [null]. 13:31:04.267 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener - Performing dependency injection for test context [[DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = com.cloud.skyme.Chapter0302junitApplicationTests@31fa1761, testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]]].
13:31:04.306 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE) 2020-06-28 13:31:04.940 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Starting Chapter0302junitApplicationTests on WIN-55FHBQI56BD with PID 8376 (started by Administrator in C:\java\workspace\microservice\chapter0302junit) 2020-06-28 13:31:04.942 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : No active profile set, falling back to default profiles: default 2020-06-28 13:31:09.134 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 0 (http) 2020-06-28 13:31:09.160 INFO 8376 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-06-28 13:31:09.161 INFO 8376 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36] 2020-06-28 13:31:09.372 INFO 8376 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-06-28 13:31:09.372 INFO 8376 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4316 ms 2020-06-28 13:31:10.029 INFO 8376 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-06-28 13:31:10.655 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 59724 (http) with context path '' 2020-06-28 13:31:10.673 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Started Chapter0302junitApplicationTests in 6.362 seconds (JVM running for 8.218) 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-06-28 13:31:11.461 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 37 ms controller 2020-06-28 13:31:13.497 INFO 8376 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'

 

 這樣,一個web應用從構建到單元測試就都已經完成了,可見,構建一個Spring Web MVC的應用就是如此簡單。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

TCP協議粘包問題詳解

TCP協議粘包問題詳解

前言

  在本章節中,我們將探討TCP協議基於流式傳輸的最大一個問題,即粘包問題。本章主要介紹TCP粘包的原理與其三種解決粘包的方案。並且還會介紹為什麼UDP協議不會產生粘包。

 

基於TCP協議的socket實現遠程命令輸入

  我們準備做一個可以在Client端遠程執行Server端shell命令並拿到其執行結果的程序,而涉及到網絡通信就必然會出現socket模塊,關於如何抉擇傳輸層協議的選擇?我們選擇使用TCP協議,因為它是可靠傳輸協議且數據量支持比UDP協議要大。好了廢話不多說直接上代碼了。

 

  Server端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0",6666))  # 放在遠程填入0.0.0.0,放在本地填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn,client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,)

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個
            conn.send(cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

from socket import *

client = socket(AF_INET,SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx",6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))
    cmd_res = client.recv(1024)  # 本次接收1024字節數據
    print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼

client.close()

 

  測試結果:

 

粘包問題及其原理

  上面的測試一切看起來都非常完美,但是是有一個BUG的。當我們如果讀取一條非常長的命令實際上是會出問題的,比如:

  這種現象被稱之為粘包,那麼為何會產生這樣的現象呢?

 

  這是由於recv()沒有一次性讀取完整個內核緩衝區的內容導致的。其實歸根結底還是怪TCP是字節流方式傳輸數據。

 

  我們來解析一下這種現象產生的原因:

 

  由於我們的recv()只是按照固定的1024去讀取數據,那麼一旦整體內核緩衝區中所存儲的整體數據大於1024,就會產生粘包現象。所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

 

  這裏我還畫了一幅圖,可以方便讀者理解:

 

  那麼我們可以通過不斷的增大recv()中的讀取範圍來解決這個問題嗎?就像對應上圖中的,一次性把快遞櫃包裹全取完,答案是不可以!你再大你也不可能大過內核緩衝區,這個東西都是有一個一定的閾值。一旦超出了這個閾值就會引發異常或者乾脆無效。那麼有什麼好的辦法呢?哈,下面會教給你一些解決辦法的。不過在此之前我們要先看一個TCP協議特有的Nagle算法。

 

Nagle算法與粘包

 

  基於TCP協議的socket通信有一個特點,即:一方的send()與另一方的recv()可以沒有任何關係,即:一方send()三次,另一方recv()一次就可以將數據全部取出來。

 

  TCP協議的發送方有一個特徵。他會進行組包,如果一次發送的數據量很小,比如第一次發送10個字節,第二次發生2個字節,第三次發生3個字節。他可能會將這15個字節湊到一塊發送出去,這是採用了Nagle算法來進行的,這麼做有一個弊端就是接收方想要將這個大的數據包按照發送方的發送次數精確無誤的接收拆分成10 2 3必須要有發送方提供的拆包機制才行。

 

  如下圖組所示

 

  發送方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024
back_log = 5

server = socket(AF_INET,SOCK_STREAM)
server.bind(ip_port)
server.listen(back_log)

conn,addr = server.accept()
conn.send("hello,".encode("utf-8"))  # 第一次發送是6Bytes的數據
conn.send("world,".encode("utf-8"))     # 第二次也是6Bytes的數據
conn.send("yunyaGG!!".encode("utf-8"))  # 第三次是9Bytes的數據

 

  接收方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024

client = socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

data_1 = client.recv(buffer_size)  # 我們讀取數據時統一用設定的 buffer_size 來讀取
print("這是第一次的數據包:",data_1.decode("utf-8"))
data_2 = client.recv(buffer_size)
print("這是第二次的數據包:",data_2.decode("utf-8"))
data_3 = client.recv(buffer_size)
print("這是第三次的數據包:",data_3.decode("utf-8"))

 

  接收結果:

# ==== 執行結果 ====
"""
這是第一次的數據包: hello,
這是第二次的數據包: world,yunyaGG!!
這是第三次的數據包: 
"""

 

  和預想的有點不太一樣哈,居然把第二次和第三次組成了一個大的數據包發送過來了。這就是Nagle算法,這樣的組包策略很容易就會產生粘包。我不知道你是以什麼樣的方式發過來的,所以我recv()就只能按照自己設定的方式去接收。

 

  現在思考一下粘包的思路,我們的發送方需要將切分解包的規則告訴給接收方。

  我們嘗試改一下每一次的buffer_size接收大小:

 

  接收方:

from socket import *
ip_port = ("127.0.0.1",12306)
buffer_size = 1024

client = socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

data_1 = client.recv(6)  # 我們手動的按照對方發送時的規則來進行拆包
print("這是第一次的數據包:",data_1.decode("utf-8"))
data_2 = client.recv(6)
print("這是第二次的數據包:",data_2.decode("utf-8"))
data_3 = client.recv(9)
print("這是第三次的數據包:",data_3.decode("utf-8"))

 

  接收結果:

# ==== 執行結果 ====
"""
這是第一次的數據包: hello,
這是第二次的數據包: world,
這是第三次的數據包: yunyaGG!!
"""

 

  粘包被我們手動的計算字節數來精確的分割數據接受量的大小給解決了,但是這樣做是不現實的..我們不可能知道對方發送的數據到底是怎麼樣的,更不用說手動計算。所以有沒有更好的解決方案呢?

 

解決方案1:預先發送消息長度

  好了,其實上面關於解決粘包的思路已經出來了。我們需要做的就是讓接收方知道本次發送內容的大小,接收方才能夠精確的將所有數據全部提取出來不產生遺漏。其實實現方式很簡單,可以嘗試以下思路:

 

  1.發送方發送一個此次數據固定的長度

  2.接收方接收到該數據長度並且回應

  3.發送方收到回應並且發送真正的數據

  4.接收方不斷的用默認的buffer_size值接收新的數據並存儲起來直到超出整個數據的長度,代表此處數據全部接收完畢

 

  Server端:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0", 6666))  # 放在遠程填入0.0.0.0 放在本地測試填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn, client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE, )

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個
            msg_length = len(cmd_res)  # 本次數據的長度
            conn.send(str(msg_length).encode("utf-8"))  # 先將要發的整體內容長度發送過去
            if conn.recv(1024) == b"ready":  # 如果接收方回應了ready則開始發送真正的數據體
                conn.send(cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx", 6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))

    msg_length = int(client.recv(1024).decode("utf-8"))  # 接收到此次發送內容的整體長度
    recv_length = 0  # 代表已接收的內容長度
    cmd_res = b""

    client.send(b"ready")  # 發送給Server端,代表自己已經接收到此次內容長度,可以發送真正的數據啦

    while recv_length < msg_length:
        cmd_res += client.recv(1024)  # 本次接收1024字節數據,可能是一小節數據
        recv_length += len(cmd_res)  # 添加上本次讀取的長度,當全部讀取完后應該 recv_length == msg_length

    else:
        print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼

client.close()

 

  結果如下:

 

解決方案2:json+struct方案

  其實上面的解決方案還是有一些弊端,因為Server端是發送了2次send(),第1次發送數據整體長度,第2次發送數據內容主體,這樣其實是不太好的(Server端可能同時處理多個鏈接,所以send()次數越少越好),而且如果Server端傳的是一個文件的話那麼局限性就太強了。因為我們只能將整體的消息長度發送過去而諸如文件名,文件大小之內的信息就發送不過去。

  所以我們需要一個更加完美的解決方案,即Server端發送一次send()就將本次的數據整體長度發送過去(還可以包括文件姓名,文件大小等信息。)

 

  struct模塊使用介紹

 

  struct模塊可以將其某一種數據格式序列化為固定長度的Bytes類型,其中最重要的兩個方法就是pack()unpack()

 

  pack(fmt,*args): 根據格式將其轉換為Bytes類型

  unpack(fmt,string):根據格式將Bytes類型數據反解為其原本的形式

 

格式 C語言類型 Python類型 字節數大小
x 填充字節 沒有值  
c char 字節長度為1 1
b signed char 整數 1
B unsigned char 整數 1
? _Bool bool 1
h short 整數 2
H unsigned short 整數 2
i int 整數 4
I unsigned int 整數 4
l long 整數 4
L unsigned long 整數 4
q long long 整數 8
Q unsigned long long 整數 8
n ssize_t 整數  
N size_t 整數  
f float 浮點數 4
d double 浮點數 8
s char[] 字節  
p char[] 字節  
P void * 整數  

 

  使用演示:

>>> import struct
>>> b1 = struct.pack("i",12)  # 嘗試將 int類型的12進行序列化,得到一個4字節的對象
>>> b1
b'\x0c\x00\x00\x00'
>>> struct.unpack("i",b1)  # 嘗試將12的序列化對象字節進行反解,得出元組,第1位就是需要的數據。
(12,)
>>>

 

  好了,了解到這裏我們就可以開始進行改寫了。

  Server端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Server ====

import json
import struct
import subprocess
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("0.0.0.0", 6666))  # 放在遠程填入0.0.0.0 放在本地測試填入127.0.0.1
server.listen(5)

while 1:  # 鏈接循環
    conn, client_addr = server.accept()
    while 1:  # 通信循環
        try:  # 防止Windows平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
            cmd = conn.recv(1024)
            if not cmd:  # 防止類Unix平台下Client端異常關閉導致雙向鏈接崩塌Server端異常的情況發生
                break
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE, )

            stdout_res = res.stdout.read()  # 正確結果
            stderr_res = res.stderr.read()  # 錯誤結果
            # subprocess模塊拿到的是bytes類型,所以直接發送即可

            cmd_res = stdout_res if stdout_res else stderr_res  # 因為兩個結果只有一個有信息,所以我們只拿到有結果的那個

            # 解決粘包:構建字典,包含數據主體長度,這個就相當於其頭部信息
            head_msg = {
                "msg_length": len(cmd_res), # 包含數據主體部分的長度
                # 如果是文件,還可以添加file_name,file_size等屬性。
            }

            # 序列化成json格式,並且統計其頭部的長度
            head_data = json.dumps(head_msg).encode("utf-8")
            head_length = struct.pack("i", len(head_data))  # 得到4字節的頭部信息,裡面包含頭部的長度

            # 發送頭部長度信息,頭部數據,與真實數據部分
            conn.send(head_length + head_data + cmd_res)

        except Exception:
            break

    conn.close()  # 由於client端鏈接異常,故關閉鏈接循環

 

  Client端代碼如下:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# ==== 基於TCP協議的socket實現遠程命令輸入之Client ====

import json
import struct
from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("xxx.xxx.xxx.xxx", 6666))  # 填入Server端公網IP

while 1:
    cmd = input("請輸入命令>>>:").strip()
    if not cmd:
        continue
    if cmd == "quit":
        break
    client.send(cmd.encode("utf-8"))  # 發送終端命令

    # 解決粘包
    head_length = struct.unpack("i", client.recv(4))[0]  # 接收到頭部的長度信息
    head_data = json.loads(client.recv(head_length))  # 接收到真實的頭部信息

    msg_length = head_data["msg_length"]  # 獲取到數據主體的長度信息
    recv_length = 0  # 代表已接收的內容長度
    cmd_res = b""

    # 開始獲取真正的數據主體信息
    while recv_length < msg_length:
        cmd_res += client.recv(1024)  # 本次接收1024字節數據,可能是一小節數據
        recv_length += len(cmd_res)  # 添加上本次讀取的長度,當全部讀取完后應該 recv_length == msg_length

    else:
        print(cmd_res.decode("utf-8"))  # 如果Server端是Windows則用gbk解碼,類Unix用utf-8解碼


client.close()

 

  思想如下:

    1.Server端構建自身的數據頭部分,其中包含數據體整體長度,如果傳輸的是文件的話還可以包含文件名,文件大小等信息

    2.將數據頭部分json序列化后再轉換為Bytes類型

    3.使用struct.pack()模塊獲取數據頭的長度,得到一個長度為4的Bytes類型

    4.Server端將 數據頭長度 + 數據頭部分 + 數據體部分 全部發送給Client端

    5. Client端recv()接收值改為4,拿到數據頭長度Bytes類型

    6. Client端使用struct.unpack(數據頭長度Bytes類型)模塊反解出數據頭真實的長度

    7. Client端使用recv()接收值為數據頭真實的長度拿到真正的數據頭

    8. 通過json反序列化出真正的數據頭,在到其中取出數據體的長度

    9. 開始while循環不斷的讀取真實的數據體數據

 

 

解決方案3:iter()與偏函數(失敗案例)

 

  上面那麼做看似完美但還是美中不足。因為內存緩衝區本來就是只能取一次值,和迭代器很像,只能迭代一次便不能繼續迭代了。基於這一點我們來做一個終極優化:

  還記得iter()方法嗎?iter()方法除開創建迭代器外實際上還有一個參數:

 

def iter(source, sentinel=None):  # known special case of iter
    """
    iter(iterable) -> iterator
    iter(callable, sentinel) -> iterator

    Get an iterator from an object.  In the first form, the argument must
    supply its own iterator, or be a sequence.
    In the second form, the callable is called until it returns the sentinel.
    """
    pass

 

  我們來試試這個參數做什麼用的。

li = [1, 2, 3, 4]

def my_iter():
    return li.pop()

res = iter(my_iter, 2)  # 代表這個迭代器沒__next__一下就會執行my_iter函數,並且該函數返回值如果是2則終止迭代
print(res.__next__())  # 4
print(res.__next__())  # 3
print(res.__next__())  # StopIteration

 

  第二個參數看來可以設置迭代的終點。

 

  那麼偏函數是什麼呢?偏函數可以設定一個固定的參數給第一個位置的值

  效果如下:

from functools import partial  # 導入偏函數

def add(x, y):
    return x + y

func = partial(add, 1)  # 設置辨寒暑綁定的第一個參數的值
print(func(1))  # 2
print(func(5))  # 6

 

  現在我們仔細回想,當緩衝區的消息接收完畢後為空的狀態是會變成 b""的形式。那麼這個時候我們可以使用iter()方法設置為不斷的取出緩存中的值直到出現b"",而偏函數可以對recv()函數進行設置讓它始終取一個值,最後通過join來拼接出取出的所有值即可。

  可以使用 "".join(iter(partial(tcp_clien.recv,back_log)),b"")

 

  我們嘗試用函數來查看一下效果:

from functools import partial  # 導入偏函數

li = [b"","1","2","3","4","5"]  # 模擬內核緩衝區

def test(buffer_size):
    if buffer_size:  # 模擬recv的數據大小
        return li.pop()
    print("buffer_size必須為一個int類型的值")

res = "".join(iter(partial(test,1024),b""))
print(res)  # 54321

# join()方法會不斷的調用iter()下的__next__,每調用一次就執行一次偏函數。知道出現b""停止

 

  最後我們發現,這樣的做法是會產生recv()阻塞的,總體來說還是不能夠成功。因為join()方法會不斷的執行,即使內核緩衝區的數據被recv()讀完了也不會終止迭代而是繼續阻塞下次的recv(),故這種方式宣告失敗。(還是iter()的第二個參數導致的,或許讀取完后內核緩衝區中的數據並不是b""

 

  測試的Server端代碼如下:

from socket import *
import subprocess
import struct
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr=tcp_server.accept()
    print('新的Client鏈接',addr)
    while True:
        #
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:break
            print('收到Client的命令',cmd)

            #執行命令,得到命令的運行結果cmd_res
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stderr=subprocess.PIPE,
                                 stdout=subprocess.PIPE,
                                 stdin=subprocess.PIPE)
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()

            #
            if not cmd_res:
                cmd_res='執行成功'.encode('gbk')

            length=len(cmd_res)

            data_length=struct.pack('i',length)
            conn.send(data_length)
            conn.send(cmd_res)
        except Exception as e:
            print(e)
            break

 

  測試的Client代碼如下:

from socket import *
import struct
from functools import partial   #偏函數
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    if cmd == 'quit':break

    tcp_client.send(cmd.encode('utf-8'))


    #解決粘包
    length_data=tcp_client.recv(4)
    length=struct.unpack('i',length_data)[0]
   
  #第一種方法
    recv_size=0
    recv_msg=b''
    while recv_size < length:
        #為何recv里是buffer_size,不是length,因為length如果為24G,系統內存沒有那麼大
        #所以每次buffer_size,當recv_size < length時,循環接收,直到recv_size =length,退出循環
        recv_msg += tcp_client.recv(buffer_size)
        recv_size=len(recv_msg) #1024

    #第二種方法 失敗版本,會引發recv()的阻塞,而不會終止迭代。因為join()方法會不斷的調用其iter()方法產生的迭代器,也就是調用其__next__方法,所以第二次沒消息的recv()會阻塞住。
    #recv_msg=''.join(iter(partial(tcp_client.recv, buffer_size), b''))
    print('命令的執行結果是 ',recv_msg.decode('gbk'))
tcp_client.close()

 

UDP協議為何不會產生粘包

 

  UDP協議是面向消息的協議,每一次的sendto()recvfrom()必須一一對應,否則就會收不到消息。

 

  UDP是面向消息的協議,每個UDP段都是一條消息,每sendto()一次就是發送一次消息,而不管接收方有沒有收到消息發送方只管自己的發送任務,這也是UDP被稱為不可靠傳輸協議的由來。接收端的套接字緩衝區採用了鏈式的結構來記錄每一個到達的UDP包,在每一個UDP包中都有了消息頭,包括端口,消息源等等..於是UDP就能夠去區分出一個明確的消息定義,即面向消息的通信是有消息邊界的,所以UDP的傳輸叫做數據報的形式。

 

  並且每一次recvform()buffer_size最大值如果不夠獲取完全部的內核緩衝區里的數據的話,那麼只會收夠指定的最大字節數量(即buffer_size的設定值),剩餘的就不要了。所以UDP不會存在粘包,多麼乾脆利落…

 

  我們還是用一個快遞員的那個圖來進行演示:

  還有一點需要注意一下。使用UDP協議進行通信的時候不管首先啟動哪一方都不會報錯,因為它只管發,不管有沒有人接收。

  所以,這也是我稱UDP協議比較隨便的原因。

 

  那麼隨便有沒有什麼好處呢?有的,速度快。不用建立雙向鏈接通道,但是其代價就是數據可靠性與安全性的問題,效率和安全從來都是相對的,這個也只能在從中做取捨。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Jmeter(十三) – 從入門到精通 – JMeter定時器 – 上篇(詳解教程)

1.簡介

  用戶實際操作時,並非是連續點擊,而是存在很多停頓的情況,例如:用戶需要時間閱讀文字內容、填表、或者查找正確的鏈接等。為了模擬用戶實際情況,在性能測試中我們需要考慮思考時間。若不認真考慮思考時間很可能會導致測試結果的失真。例如,估計的可支撐用戶數偏小。在性能測試中,訪問請求之間的停頓時間被稱之為思考時間,那麼如何模擬這種停頓呢?我們可以藉助JMeter的定時器實現。

  JMeter中的定時器一般被我們用來設置延遲與同步。定時器的執行優先級高於Sampler(取樣器),在同一作用域(例如控制器下)下有多個定時器存在時,每一個定時器都會執行,如果想讓某一定時器僅對某一Sampler有效,則可以把定時器加在此Sampler節點下。

2.預覽定時器

首先我們來看一下JMeter的定時器,路徑:線程組(用戶)->添加->定時器(Timer);我們可以清楚地看到JMeter5中共有9個定時器,如下圖所示:

如果上圖您看得不是很清楚的話,宏哥總結了一個思維導圖,關於JMeter5的邏輯控制器類型,如下圖所示: 

 通過以上的了解,我們對定時器有了一個大致的了解和認識。下面宏哥就給小夥伴或則童鞋們分享講解一些通常在工作中會用到的定時器。 

4.常用定時器詳解

這一小節,宏哥就由上而下地詳細地講解一下常用的定時器。

4.1Constant Timer

固定定時器,看名稱大家也知道是一個固定定時器,多用來模擬思考時間,顧名思義是:請求之間的間隔時間為固定值。

作用:通過ThreadDelay設定每個線程請求之前的等待時間(單位為毫秒)。注意:固定定時是有作用域的,放到線程組下其作用域是所有請求都會延遲固定器設置的時間,如果放到請求內,作用域是單個請求延遲時間(常用)。

1、我們先來看看這個Constant Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 固定定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Thread Delay(in milliseconds):線程等待時間,單位毫秒。

用法(場景),更真實的模擬用戶場景,需要設置等待時間,或是等待上一個請求的時間,才執行,給sampler之間的思考時間;

4.1.1實例

場景應用:性能測試中,根據用戶操作預估時間,或者需要等待一段時間來加載數據。
PS:在實際模擬用戶請求的過程中,會失去靈活性,不推薦大量使用

1、新建測試計劃,線程組下添加2個取樣器 訪問博客園首頁、訪問度娘,如下圖所示:

2、然後再添加固定定時器,設置延遲時間3000ms,即3s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(取樣器訪問博客園首頁和訪問度娘間隔3s),如下圖所示:

4.2Uniform Random Timer

統一(均勻)隨機定時器,也是讓線程暫停一個隨機時間,只不過力求隨機時間能夠更均勻,都會出現。均勻隨機定時器,顧名思義,它產生的延遲時間是個隨機值,而各隨機值出現的概率均等。總的延遲時間等於一個隨機延遲時間加上一個固定延遲時間,用戶可以設置隨機延遲時間和固定延遲時間。

作用:它產生的延遲時間是個隨機值,而各隨機值出現的概率均等。總的延遲時間等於一個隨機延遲時間加上一個固定延遲時間,用戶可以設置隨機延遲時間和固定延遲時間。每個線程的延遲時間是符合標準正態分佈的隨機時間停頓,那麼使用這個定時器,總延遲 = 高斯分佈值(平均0.0和標準偏差1.0)* 指定的偏差值+固定延遲偏移(Math.abs((this.random.nextGaussian() * 偏差值) + 固定延遲偏移))

總延遲時間 = 指定範圍內的隨機時間(在範圍內各隨機值等概率)+ 固定延遲時間

1、我們先來看看這Uniform Random Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 統一隨機定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Random Delay Maximum:最大隨機延遲時間;

Constant Delay Offset: 固定延遲時間。

4.2.1實例

1、新建測試計劃,線程組下添加2個取樣器 訪問博客園首頁、訪問度娘,如下圖所示:

2、然後再添加統一隨機定時器,設置延遲時間3s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(取樣器訪問博客園首頁和訪問度娘間隔4s = 1000ms + 3000ms),如下圖所示:

4.3Precise Throughput Timer

準確的吞吐量定時器,顧名思義,這個就是控制吞吐量的。和Constant Throughput Timer類似,但是能更精準的控制請求。區別就是Constant Throughput Timer根據時間來做定時器(到了多少秒就發請求);Precise Throughput Timer是根據吞吐量在做計時器(到了多少量就發請求)。也就是能做到控制請求的速度和個數。

1、我們先來看看這個Precise Throughput Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 準確的吞吐量定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Thread Delay:忽略子控制器,即子控制器失效,由交替控制器接管。

Target Throught:目標吞吐量

Throught Period:表示在多長時間內發送Target Throught指定的請求數(以秒為單位)

Test Druation:指定測試運行時間(以秒為單位)

Number of threads in the bath:用來設置集合點,等到指定個數的請求后併發執行

4.3.1實例

1、新建測試計劃,線程組(設置線程組,保證有足夠的時間)下添加2個取樣器 訪問博客園首頁(已禁用)、訪問度娘,如下圖所示:

2、然後再添加準確的吞吐量定時器,設置10個吞吐量,設置10s啟動完10個請求,設置運行時間20s,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(大約用了20秒啟動了21個線程),如下圖所示:

4、設置集合點在Precise Throughput Timer中設置集合點為10,其它參數不變,如下圖所示:

5、在Thread Group中設置線程數為10,如下圖所示: 

6、配置好以後,點擊“保存”,運行JMeter,查看錶格結果(可以看到,每10個線程為1組,同時啟動。),如下圖所示: 

4.4Constant Throughput Timer

固定吞吐量定時器,這個定時器引入了變量暫停,通過計算使總吞吐量(以每分鐘去楊樹計)盡可能接近給定的数字。當然,如果服務器不能處理它,或者如果其他定時器或耗時的測試原件阻止它,那麼吞吐量將更低。
雖然計時器被稱為常數吞吐量定時器,但吞吐量值並不一定是常數。它可以根據變量或函數調用定義,並且可以在測試期間改變該值。通過以下多種方式都可以改變:
使用計數器變量
使用一個 __jexl3, __groovy 函數來提供一個變化的值
使用遠程BeeShell服務器更改Jmeter屬性
請注意,在測試期間,不應該頻繁地更改吞吐量值——新值生效需要一段時間。

常數吞吐量定時器作用:控制吞吐量(線上壓測時候,避免一下就上百上千的吞吐量影響線上性能,加上這個之後較安全,可以一點一點往上加); 按指定的吞吐量執行,以每分鐘為單位。計算吞吐量依據是最後一次線程的執行時延。

作用域:此定時器放在請求的下級,只對它的上級請求起作用

1、我們先來看看這個Constant Throughput Timer長得是啥樣子,路徑:線程組 > 添加 > 定時器 > 常數吞吐量定時器,如下圖所示: 

2、關鍵參數說明如下:

Name:名稱,可以隨意設置,甚至為空;

Comments:註釋,可隨意設置,可以為空;

Target throughput(in samples per minute):目標吞吐量。注意這裡是每分鐘發送的請求數,可以選擇作用的線程:當前線程、當前線程組、所有線程組等,具體含義如下:

this thread only: 設置每個線程的吞吐量。總的吞吐量=線程數*該值。

all active threads in current thread group:吞吐量被分攤到當前線程組所有的活動線程上。每個線程將根據上次運行時間延遲。

all active threads:吞吐量被分配到所有線程組的所有活動線程的總吞吐量。每個線程將根據上次運行時間延遲。在這種情況下,每個線程組需要一個具有相同設置的固定吞吐量定時器。(不常用)

all active threads in current thread group (shared):同上,但是每個線程是根據組中的線程的上一次運行時間來延遲。相當於線程組組內排隊。(不常用)

all active threads (shared):同上,但每個線程是根據線程的上次運行時間來延遲。相當於讓所有線程組整體排隊。(不常用)

 4.4.1實例

1、新建測試計劃,線程組下添加1個取樣器 訪問博客園首頁(已禁用)、訪問度娘,如下圖所示:

2、然後再添加常數吞吐量定時器,設置目標吞吐量為300,如下圖所示:

3、配置好以後,點擊“保存”,運行JMeter,查看jp@gc – Transactions per Second(常數吞吐量定時器設置300/分鐘,也就是5/秒,故tps最大5,這裏的tps大約都是5,說明已經超過5,可以往上增加了),如下圖所示:

5. 定時器的作用域

1. 定時器是在每個sampler(採樣器)之前執行的,而不是之後(無論定時器位置在sampler之前還是下面);
2. 當執行一個sampler之前時,所有當前作用域內的定時器都會被執行;
3. 如果希望定時器僅應用於其中一個sampler,則把定時器作為子節點加入;
4. 如果希望在sampler執行完之後再等待,則可以使用Test Action;

6.小結

6.1安裝插件管理

1、安裝前查看選項,沒有看到插件管理,如下圖所示:

2、想安裝一個jmeter的插件,到官網(http://jmeter-plugins.org)上去下載插件安裝包,但是頁面一直都是搜索狀態,如下圖所示:

3、然後宏哥找了一個下載一個jmeter的插件管理工具 地址: http://jmeter-plugins.org/get/

4、將下載的文件拷貝的你的JMeter根目錄下的 lib/ext 目錄,如下圖所示:

5、 重啟jmeter,在選項中可以看到插件管理工具已經安裝成功,如下圖所示:

 6、勾選要下載的插件,點擊Apply changes and restart JMeter按鈕就完成了

Installed Plugins:用於查看已安裝的插件,並可通過 取消勾選 – 應用操作 來卸載插件

Available Plugins:用於查看和安裝可用的插件,通過 勾選-應用操作(右下側有按鈕Apply changes and restart JMeter) 來安裝插件

Upgrades:用於升級插件

   好了,今天關於定時器的上篇就講解到這裏,這一篇主要介紹了 Constant TimerUniform Random TimerPrecise Throughput Timer Constant Throughput Timer。感謝你耐心的閱讀和學習。

 

您的肯定就是我進步的動力。如果你感覺還不錯,就請鼓勵一下吧!記得隨手點波  推薦  不要忘記哦!!!

別忘了點 推薦 留下您來過的痕迹

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

Spring Cloud Alibaba系列(五)sentinel實現服務限流降級

一、sentinel是什麼

sentinel的官方名稱叫分佈式系統的流量防衛兵。Sentinel 以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。在Spring Cloud項目中最開始我們使用的是Hystrix,目前已停止更新了。現在Spring Cloud官方推薦的是rensilience4j。當然還有我們今天學習的sentinel。

Sentinel 具有以下特徵:

  • 豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
  • 完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機器秒級數據,甚至 500 台以下規模的集群的匯總運 行情況。
  • 廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
  • 完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快速地定製邏輯。例如定製規則管理、適配動態數據源等。

二、sentinel實現限流

2.1 安裝sentinel控制台

  • 下載地址:https://github.com/alibaba/Sentinel/releases

這裏我們直接下載jar包即可,下載后通過命令行啟動:

java -jar sentinel-dashboard-1.7.2.jar
  • 默認端口:8080
  • 默認用戶名:sentinel
  • 默認密碼:sentinel

啟動成功后,我們瀏覽器訪問http://localhost:8080,出現如下界面。

2.2 微服務繼承sentinel

  • 引入sentinel依賴
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 添加sentinel的相關配置
server:
  port: 7003
spring:
  application:
    name: sentinel-provider
  cloud:
	nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
  • 提供個接口用來測試限流
@SpringBootApplication
public class SentinelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SentinelApplication.class, args);
    }
}

@RestController
class TestController{
    @GetMapping("/test")
    public String test(){
        return "hello! sentinel!";
    }
}

我們請求幾次這個接口后,打開sentinel控制台,就可以實時監控到這個sentinel-provider服務接口調用情況了。

2.3 配置限流規則

我們這裏做一個簡單的規則配置:

  • 閥值類型:QPS

  • 單機閥值:2

意思就是:該接口每秒最多允許進入兩個請求。

點擊新增后,在流控規則里發現了一條規則:

現在,我們繼續請求3次這個接口。第三次響應的內容如下:

Blocked by Sentinel (flow limiting)

我們打開控制台發現拒絕了一條請求。

三、Sentinel規則介紹

不管是限流還是降級,它都是按照某種規則進行的,下面具體介紹一下sentinel支持的幾種規則。

3.1 流控規則

流量控制,其原理是監控應用流量的QPS(每秒查詢率) 或併發線程數等指標,當達到指定的閾值時

對流量進行控制,以避免被瞬時的流量高峰衝垮,從而保障應用的高可用性。

資源名:唯一名稱,默認是請求路徑,可自定義

針對來源:指定對哪個微服務進行限流,默認指default,意思是不區分來源,全部限制

閾值類型/單機閾值

  • QPS(每秒請求數量): 當調用該接口的QPS達到閾值的時候,進行限流

  • 線程數:當調用該接口的線程數達到閾值的時候,進行限流

3.2 降級規則

降級規則就是當滿足什麼條件時,對服務降級——即將請求轉發到另外接口上,這個接口與業務無關,只是為了保證系統的完整性。

  • RT(平均響應時間) :當資源的平均響應時間超過閾值(以 ms 為單位)之後,資源進入准降級狀態。如果接下來 1s 內持續進入 5 個請求,它們的 RT都持續超過這個閾值,那麼在接下的時間窗口(以 s 為單位)之內,就會對這個方法進行服務降級。

    注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。

  • 異常比例:當資源的每秒異常總數占通過量的比值超過閾值之後,資源進入降級狀態,即在接下的時間窗口(以 s 為單位)之內,對這個方法的調用都會自動地返回。異常比率的閾值範圍是 [0.0,1.0]。

  • 異常數 :當資源近 1 分鐘的異常數目超過閾值之後會進行服務降級。注意由於統計時間窗口是分鐘級別的,若時間窗口小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。

3.3 熱點規則

熱點規則允許將規則具體到參數上。

我們用個例子來看看效果。

  • 編寫接口
@GetMapping("/myTest")
@SentinelResource("test3")
public String test123(String name,String age){
    return  name + "----"+ age;
}
  • 添加規則
  • 運行效果

結果显示,第一個參數被限流了,而第二個參數正常。

3.4 系統規則

系統保護規則是從應用級別的入口流量進行控制,從單台機器的總體 Load、RT、入口 QPS 、CPU使用率和線程數五個維度監控應用數據,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。

系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量 (進入應用的流量) 生效。

  • Load(僅對 Linux/Unix-like 機器生效):當系統 load1 超過閾值,且系統當前的併發線程數超過系統容量時才會觸發系統保護。系統容量由系統的 maxQps * minRt 計算得出。設定參考值一般是 CPU cores * 2.5。

  • RT:當單台機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。

  • 線程數:當單台機器上所有入口流量的併發線程數達到閾值即觸發系統保護。

  • 入口 QPS:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。

  • CPU使用率:當單台機器上所有入口流量的 CPU使用率達到閾值即觸發系統保護。

3.5 授權規則

很多時候,我們需要根據調用來源來判斷該次請求是否允許放行,這時候可以使用 Sentinel 的來源問控制的功能。來源訪問控制根據資源的請求來源(origin)限制資源是否通過:

  • 若配置白名單,則只有請求來源位於白名單內時才可通過;

  • 若配置黑名單,則請求來源位於黑名單時不通過,其餘的請求通過。

流控應用:sentinel提供了RequestOriginParser來處理接口來源。

我們運行abc來源的請求訪問/test接口。

@Component
class requestOrigin implements RequestOriginParser{

    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
        String server = httpServletRequest.getParameter("server");
        return server;
    }
}

我們請求http://localhost:7003/test?server=abc 和 http://localhost:7003/test?server=ab來分別看看效果。

@SentinelResource的使用

@SentinelResource 用於定義資源,並提供可選的異常處理和 fallback 配置項。

主要參數有以下幾個

屬性 作用
value 資源名稱
entryType entry類型,標記流量的方向,取值IN/OUT,默認是OUT
blockHandler 處理BlockException的函數名稱,函數要求:1. 必須是 public;2.返回類型 參數與原方法一致;3. 默認需和原方法在同一個類中。若希望使用其他類的函數,可配置blockHandlerClass ,並指定blockHandlerClass裏面的方法。
blockHandlerClass 存放blockHandler的類,對應的處理函數必須static修飾。
fallback 1. 返回類型與原方法一致;2. 參數類型需要和原方法相匹配;3. 默認需和原方法在同一個類中。若希望使用其他類的函數,可配置fallbackClass
fallbackClass 存放fallback的類。對應的處理函數必須static修飾。
defaultFallback 若同時配置了 fallback 和 defaultFallback,以fallback為準。
exceptionsToIgnore 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。
exceptionsToTrace 需要trace的異常

@sentinelResource可結合blockHandler用於限流處理,結合fallback用於降級處理。具體規則可通過sentinel控制台配置,具體我就不演示了,在下一章內容中,我會分別演示限流和降級的應用。

public class MySentinelResource {

    @SentinelResource(value="message",blockHandler="blockHandler",fallback="fallback")
    public String message(String str){
        if(StringUtils.isBlank(str)){
            throw new RuntimeException();
        }
        return str;
    }
    /**
     * 限流處理
     * @param str
     * @param ex
     * @return
     */
    public String blockHandler(String str, BlockedException ex){
        return str + "--"+ ex;
    }
    /**
     * 降級處理
     * @param str
     * @return
     */
    public String fallback(String str){
        return null;
    }
}

代碼示例

gitee:https://gitee.com/zhixie/spring-cloud-alibaba-learning/tree/master/sentinel-server

github:https://github.com/binzh303/spring-cloud-alibaba-learning/tree/master/sentinel-server

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

易到用車APP上線數千輛新能源車 最低5元人民幣即可搭乘

日前,易到用車啟動了「E-Car計畫」,將陸續在北京、上海、廣州、深圳上線數千輛新能源車型,打造新能源出行平台;與此同時易到用車還宣佈將與普天新能源、龐大集團、騰勢等品牌展開深度合作,共同打造新能源出行聯盟,共建新能源城市。   屆時,易到用車App將上線開通「新能源」車型選項。此次加入易到「E-Car計畫」的車型包括混合動力的雙擎普銳斯、沃爾沃S60L、北汽EV系列以及特斯拉,用戶可通過易到用車App,在「新能源」車型中按需選擇,價格最低5元人民幣(下同)起步,0元/分鐘,0.99元/公里。   易到方面介紹,作為聯盟計畫的一部分,易到用車將與普天合作,在未來共同建設1000個充電樁設施,也同時尋找更多合作夥伴,提供場地共同建樁,而且充電樁建好後,將面向社會全面開放使用。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

加速電動車佈局!首爾3年內增設10萬個充電站

為了加速電動車(electric vehicles)普及率,南韓政府宣佈將主導建設名為「EV-Line」的行動充電站,在首都首爾地區擴增10萬個充電設施;此計畫預計在2018年完工,由當地業者Power Cube和KT南韓電信承包建設業務。充電站將包括停車場、公園、住宅區等各式建築物內。   Power Cube董事表示,首爾當地有8成民眾都住在公寓,而非獨棟房屋,因此家用車都停在公用停車廠。每一個「EV-Line」充電站每小時約可充3.3 KW電量,要充飽整台車約需耗費6至8小時。有些充電站則可提供每小時8KW電量,雖可以縮短一半以上的充電時間,但充電站內的安全考慮成為最大考驗。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

併發07–線程池及Executor框架

一、JAVA中的線程池

線程池的實現原理及流程如下圖所示:

 

 

   如上圖所示,當一個線程提交到線程池時(execute()或submit()),先判斷核心線程數(corePoolSize)是否已滿,如果未滿,則直接創建線程執行任務;如果已滿,則判斷隊列(BlockingQueue)是否已滿,如果未滿,則將線程添加到隊列中;如果已滿,則判斷線程池(maximumPoolSize)是否已滿,如果未滿,則創建線程池執行任務;如果線程池已滿,則交給飽和策略(RejectedExecutionHandler.rejectExcution())來處理。

  可以看下線程池ThreadPoolExecutor的全參構造函數源碼:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

對其入參釋義如下:

參數 描述 作用
coolPoolSize 線程核心線程數 當一個任務提交到線程池時,線程池會創建一個線程來執行任務,即使其他的核心線程足夠執行新任務,也會創建線程,直到需要執行的任務數大於核心線程數后才不再創建;如果線程池先調用了preStartAllCoreThread()方法,則會先啟動所有核心線程。
maximumPoolSize 線程池最大線程數 如果隊列滿了,並且已創建的線程數小於該值,則會創建新的線程執行任務。這裏需要說明一點,如果使用的隊列時無界隊列,那麼該值無用。
keepAliveTime 存活時間 當線程池中線程超過超時時間沒有新的任務進入,則停止該線程;只會停止多於核心線程數的那幾個線程。
unit 線程存活的時間單位 可以有天、小時、分鐘、秒、毫秒、微妙、納秒
workQueue 任務隊列

用於保存等待執行任務的阻塞隊列。可以選擇如下幾個隊列:數組結構的有界隊列ArrayBlockingQueue、鏈表結果的有界隊列LinkedBlockingQueue、不存儲元素的阻塞隊列SynchronousQueue、一個具有優先級的無界阻塞隊列PriortyBlockingQueue

threadFactory 創建線程的工廠

可以通過工廠給每個線程創建更有意義的名字。使用Guava提供的ThreadFactoryBuilder可以快速的給線程池裡的線程創建有意義的名字,代碼如下

new ThreadFactoryBuilder().setNameFormat(“aaaaaaaa”).build();

handler 包和策略

當隊列和線程都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略來處理新提交的任務。

AbortPolicy(默認),表示無法處理新任務時拋出異常。

CallerRunsPolicy:只有調用者所在線程來運行

DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務

DiscardPolicy:不處理,直接丟棄

  上面說到,向線程池提交任務有兩種方法,分別是execute()和submit(),兩者的區別主要是execute()提交的是不需要有返回值的任務,而submit提交的是需要有返回值的任務,並且submit()會返回一個Furure對象,並且可以使用future.get()方法獲取返回值,並且get方法會阻塞,直到有返回值。

  線程池的關閉有shutdown()和shutdownNow兩個方法,他們的原理是遍歷線程池中的工作線程,然後逐個調用interrupt方法來中斷線程,所以無法中斷的線程可能永遠無法終止;但是二者也有區別,shutdownNow是將線程池的狀態設置為STOP,然後嘗試停止所有正在執行或者暫停的線程,並返回等待執行任務列表;而shutdown只是將線程池的狀態設置成SHUTDOWN,然後中斷所有沒有正在執行的任務。當調用這兩個方法中的任何一個后,isShutdown方法就會返回true,當所有任務都已經關閉后,調用isTerminaed方法會返回true。

  使用線程池時,需要從任務的性質(IO密集型還是CPU密集型或是混合型)、任務的優先級、任務的執行時常、任務的依賴性(是否依賴其他系統資源,如數據庫連接等)來綜合判斷,比如說,CPU密集型,就可以就可以配置N+1個線程個數,其中N為CPU核數,如果是IO密集型,則可以配置2*N個線程數;如果是混合型的任務,可以將其拆分成IO密集型和CPU密集型,但是如果兩個任務的執行時間相差較大,則沒有必要進行拆分;優先級不同的任務可以使用優先級隊列PriortyBlockingQueue來處理;依賴數據出等其它資源的線程池,比如說依賴數據庫,那麼就可以加大線程數量,因為在等待sql執行的時候,線程是處於空閑狀態;另外,最好使用有界隊列,因為無界隊列,因為有界隊列可以增加系統的穩定性和預警能力。

  對於線程的監控,還有以下幾個方法可以使用:

方法 描述
taskCount() 線程池需要執行的任務數量
completedTskCount 線程池運行過程中已經執行完畢的任務數量
IarestPoolSize 線程池中曾經創建過的最大線程數
getPoolSize 線程池的線程數量
getActiveCount 獲取活動的線程數

二、Exector框架

   在java中,是用線程來異步執行任務,java線程的創建與銷毀需要一定的開銷。如果我們為每一個任務創建一個線程的話,這些線程就會消耗大量的計算資源,會使處於高負荷的應用崩潰。

  在HotSpot虛擬機中,JAVA線程被一對一的映射為本地操作系統線程。JAVA線程啟動時會創建一個本地操作系統線程,當該JAVA線程終止時,這個操作系統線程也會被收回,操作系統會調用多有線程並將他們分配給可用的CPU。

 

   Executor框架的兩級調度模型如上圖所示,應用程序通過Executor控制上層的調度,而下層的調用由操作系統內核控制,將線程映射到硬件處理器上,下層的調用不受應用程序的控制。

   關於Executor的組成部分如下所示:

元素 描述
任務 包括被執行任務需要實現的接口Runnable和Callable接口
任務的執行 包括任務執行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口。Executor接口有兩個關鍵的實現類實現了ExecutorService接口:ThreadPoolExecutor和ScheduledThreadPoolExecutor
異步計算的結果 包括接口Future和實現Future接口的FurureTask類

  Executor框架使用示意圖如下:

 

 

   如上圖所示,主線程首先創建實現Runnable或Callable接口的任務對象,然後把任務對象提交給ExecutorService執行,如果使用的是submit提交,執行完畢后將返回一個實現Future接口的對象,最後,主線程可以執行FutureTask.get()方法來獲取返回值;主線程也可以調用FutureTask.cancel()方法來取消此任務的執行。

  Executor框架的成員如下:

成員 描述 子類 描述
ThreadPoolExecutor

通常使用工廠類Executors來創建,Executors可以創建三種類型的ThreadPoolExecutor

固定線程數的FixedThreadPool

適用於為了滿足資源管理的需求,而需要限制當前線程數量的應用場景,它適用於負載比較重的應用。
單一線程的SingleThreadPool 適用於需要保證順序的執行各個任務,並且在任意時間點都不會有多個線程活動的場景。
根據需要創建線程的CacheThreadPool 這是一個無界的線程池,適用於執行很多短期異步任務的小程序,或者是負載比較輕的服務器。
ScheduledThreadPoolExecutor 通常使用工廠類Executors創建,Executors可以創建兩種類型的ScheduledThreadPoolExecutor 包含若干線程的ScheduledThreadPoolExecutor 適用於需要多個後台線程執行周期任務,同時為了滿足資源管理的需求而需要限制後台線程數量的應用場景。
只包含一個線程的SingleThreadScheduledExecutor                         適用於需要單個後台線程執行周期任務,同時需要保證順序的執行各個任務的場景。                                               
ForkJoinsPool

 newWorkStealingPool適合使用在很耗時的操作,但是newWorkStealingPool不是ThreadPoolExecutor的擴展,它是新的線程池類ForkJoinPool的擴展,但是都是在統一的一個Executors類中實現,由於能夠合理的使用CPU進行對任務操作(并行操作),所以適合使用在很耗時的任務中 

   
Future Future接口和實現了該接口的FutureTask類來表示異步計算的結果    
Runnable和Callable接口

Runnable和Callable接口的實現類,都可以被ThreadPoolExecutor、ScheduledThreadPool、ForkJoinThred執行;除了可以自己實現Callable接口外,我們還可以使用工廠類Executors來把一個Runnable包裝成一個Callable

   

 

ThreadPoolExecutor詳解

1、ThreadPoolExecutor

  (1)FixedThreadPool

  構造函數如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

  構造函數中,核心線程數和最大線程數一致,keepAliveTime為0,隊列使用的是無界阻塞隊列LinkedBlockingQueue(最大值是Integer.MAX_VALUE);

  核心線程數和最大線程數保持一致,表明:如果隊列滿了之後,不會再創建新的線程;

  keepAliveTime為0,表明:如果運行線程數大於核心線程數時,如果線程執行完畢,空閑線程立刻被終止;

  使用無界阻塞隊列,表明:當運行線程到達核心線程數時,不會再創建線程,只會將任務加入阻塞隊列;因此最大線程數參數無效;因此keepAliveTime參數無效;且不會拒絕任務(既不會執行包和策略)

  (2)SingleThreadExecutor

  構造函數如下:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  構造函數中,核心線程數和最大線程數均為1,keepAliveTime為0,隊列使用的是無界阻塞隊列LinkedBlockingQueue(最大值是Integer.MAX_VALUE)

  除了固定了核心線程數和最大線程數為1外,其餘的參數均與FixedThreadPool一致,那麼就是只有一個線程會反覆循環從阻塞隊列中獲取任務執行

  (3)CacheThreadPool

  構造函數如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

  構造函數中,核心線程數為0,最大線程數為Integer.MAX_VALUE,意味着無界,keepAliveTime為60秒,阻塞隊列使用沒有存儲空間的SynchronousQueue

  核心線程數為0,最大線程數為無界,表明:只要隊列滿了,就會創建新的線程放入線程池

  使用沒有存儲空間的SynchronousQueue表明:線程提交的速度高於線程被消費的速度,那麼線程會被不斷的創建,最終會因為線程創建過多而耗盡CPU和內存資源

2、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor的運行機制如下:

   (1)當調用ScheduledTreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時會向ScheduledThreadPoolExecutor的DelayQueue添加一個實現了RunnableScheduledFuture接口的ScheduledFutureTask

  (2)線程池中的線程從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

  ScheduledFutureTask主要包含以下三個成員變量

成員變量 描述
long time 表示這個任務要被執行的時間
long sequenceNumber 表示該任務被添加到ScheduledThreadPoolExecutor中的序號
long period 表示任務執行的間隔周期

  DelayQueue封裝了一個PriorityQueue,當添加任務時,這個PriorityQueue會對隊列中的ScheduledFutureTask進行排序,time最小的在最前面(最先被執行),如果time一致,就比較sequenceNumber,sequenceNumber小的排在前面。

  當線程執行任務時,先從DelayQueue隊列中獲取已經到期的任務(time大於當前時間),然後執行該任務,執行完畢后,根據任務的執行周期,修改任務下次的執行時間time,並重新將任務添加到DelayQueue

 

 

  FutureTask詳解

  Future接口和實現該接口的FutureTask類,代表異步計算的結果。

  FutureTask的使用方法是將其交給Executor執行,也可以通過ExecutorService.submit()方法返回一個FutureTask,然後執行FutureTask.get()方法或FutureTask.cancel()方法,除此之外,還可以但是使用FutureTask。

  FutureTask有三種狀態:未啟動(FutureTask.run()沒有被執行之前的狀態)、已啟動(FutureTask.run()方法執行過程中)、已完成(FutureTask.run()方法執行完成或被取消),這三種狀態的流轉如下圖所示:

 

  FutureTask的實現是基於AQS(AbstractQueuedSynchrouizer)來實現的,之前已經說過,每一個基於AQS實現的同步器都會至少包含一個acquire操作和至少一個release操作。AQS被作為模板方法模式的基礎類提供給FutureTask的內部子類Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。

  FutureTask涉及示意圖如下圖所示:

 

  如上圖所示,FutureTask.get()方法會調用AQS的acquireSharedInterruptibly(int)方法,該方法首先會回調在子類Sync中的tryAcquireShared()方法來判斷acquire操作是否成功(state狀態狀態是否為執行完成RAN或取消狀態CANCELED&runner不為null),如果成功則get()方法立刻返回,如果失敗則到線程等待隊列中去等待其他線程執行release操作;當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel())喚醒當前線程后,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列並喚醒它的後繼線程。

  Run方法執行過程如下:

  執行在構造函數中指定的任務(Callable.call()),然後以原子方式來更新狀態(調用AQS.compareAndSetState(int expect, int update),設置state的狀態為RAN),如果這個原子操作成功,就設置代表計算結果的變量result的值為Callable.call()的返回值,然後調用AQS.release(int)。

  AQS.rease首先會調用子類Sync中實現的tryReleaseShared方法來執行release操作(設置運行任務的線程為null,然後返回false),然後喚醒等待隊列中的第一個線程。

  最後調用Future.done()方法。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

Spring Redis開啟事務支持錯誤用法導致服務不可用

 

 

1.事故背景

在APP訪問服務器接口時需要從redis中獲取token進行校驗,服務器上線后發現一開始可以正常訪問,但只要短時間內請求量增長服務則無法響應

2.排查流程

(1)使用top指令查看CPU資源佔用還遠遠達不到瓶頸,排查因為CPU資源不足導致服務不可用的可能

(2)查看tomcat線程池配置,默認最大線程數為200,理論上可以支持目前服務器的訪問量

(3)使用jmap指令保存堆棧信息,jmap -dump:format=b,file=dump.log pid,pid為進程號

(4)使用Java visualVM打開保存的堆棧日誌dump.log,發現絕大部分的線程都阻塞在從redis連接池中獲取連接的代碼,如下圖所示

 3.原理分析

 根據堆棧日誌所显示得知線程訪問redis前需要從連接池隊列中推出一個連接,當連接池沒有連接時,則會阻塞等待,阻塞等待的時間可以自行設置MAA_WAIT參數,默認是-1表示不限時等待,目前項目使用默認配置,所以所有的線程都會一直阻塞在獲取連接的步驟,如果設置了最大等待時間,當超過最大等待時間會報出Could not get a resource from the pool的異常

(1)在spring配置文件中的stringRedisTemplate對象配置參數中打開了事務支持,而redis的事務支持是用MUTI和EXEC指令來支持,以下事務實例截圖來自菜鳥教程 https://www.runoob.com/redis/redis-transactions.html

 

(2)如果要保證在事務能正常執行,那麼在一個方法中多次操作redis必須是同一條連接,這樣才能保證事務能正常執行,所以在stringRedisTemplate會將連接綁定在當前線程,當第二次訪問redis時直接從當前線程中獲取連接,綁定連接源碼如下:

 

 

 (3)按照流程,先綁定連接,最後在finally代碼塊中釋放連接,看起來並沒有問題,但跳進去releaseConnection方法的代碼發現連接需要在事務提交后才能釋放,也就是說service方法上必須使用@Transation註解修飾,但因為業務方法上少寫了@Transation註解導致連接將一直綁定第一次獲取他的線程上,當線程池的線程被獲取完之後,其他線程就會就如阻塞等待狀態,導致服務不可用

 

 (4)如果加上@Transation註解,那麼方法執行完之後將會執行TransactionSynchronizationUtils.invokeAfterCompletion這個方法,mysql事務也是在這個方法執行commit操作,如下圖所示方法的第一個參數是List<TransactionSynchronization> synchronizations,代表可以有多個事務,redis,mysql等,都會此進行事務提交操作,這裏使用多態,根據對象的具體類型執行不同的方法,redis則執行redis的事務提交操作,mysql則執行mysql的事務提交操作

(5)以下為redis事務提交的代碼,也跟我們上面提到的一樣,發送exec指令提交事務

 

 4.如何修改代碼

(1)確認實際需求是否需要事務支持,如果需要則在對應方法上加上@Transaction註解

(2)如果不需要事務支持則將enableTransactionSupport設置為false

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

接口調用超時的實現原理

  平常開發過程中,如果涉及到RPC調用,對於服務調用方和服務提供方,都是可以設置接口超時時間的。以調用方為例,調用方需要調用遠程的一個接口,為了保證服務的質量,一般會設置調用接口的超時時間,比如將調用接口的超時時間設置為1秒,當調用遠程接口后,經過1秒還未拿到結果,那麼就認為是超時了,調用方就不會繼續等待服務提供方返回結果,而是直接拋出一個SocketTimeOutException。

  其實不僅僅是RPC接口調用需要設置超時時間,數據庫、緩存這些一樣的,一般都會設置超時時間,不能讓程序無休止的等待下去。

  那麼問題來了!!應用或者說框架,是如何設置超時時間的呢?我們設置超時時間為1秒,那麼程序是怎麼保證1秒后就停止調用了呢?這也就是本文要介紹的,接口調用設置超時時間的原理。

  本文將以設置調用數據庫超時為例,介紹設置超時時間是怎麼實現的,明白這個原理后,其他比如RPC調用、緩存調用的超時原理也就明白。

 

CallUtils-帶有超時的執行工具類

  下面是一個CallUtils,包含一個線程池和execute方法,execute方法中默認超時時間為1秒。

package cn.ganlixin.util;
import java.util.concurrent.*;

/**
 * 帶有超時的任務執行器
 */
public class CallUtils {

    /**
     * 用於執行任務的線程池
     */
    private static ExecutorService executorService = Executors.newFixedThreadPool(5);

    /**
     * 執行任務,並且超時時間為1秒
     *
     * @param callable 需要執行的任務
     * @param <T>      任務執行完畢后返回值類型
     * @return 任務結果
     */
    public static <T> T execute(Callable<T> callable) {

        // 提交任務
        Future<T> future = executorService.submit(callable);

        try {
            return future.get(1, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            e.printStackTrace();
        }

        return null;
    }
}

  

UserService-將調用數據庫

  UserService使用CallUtils來執行數據庫操作,因為CallUtils設置有調用超時:

package cn.ganlixin.service;

import cn.ganlixin.dao.UserDAO;
import cn.ganlixin.model.User;
import cn.ganlixin.util.CallUtils;

import java.util.List;

public class UserService {

    private UserDAO userDAO = new UserDAO();

    public List<User> getAllUser() {
        return CallUtils.execute(() -> userDAO.getAllUserFromDB());
    }
}

  

UserDAO-耗時的數據庫操作

  userDAO中,作為測試,只提供了一個getAllUserFromDB方法,休眠2秒來模擬數據庫操作,因為UserService使用CallUtils來調用該方法,CallUtils的超時時間為1秒,所以該數據庫操作必定會超時:

package cn.ganlixin.dao;
import cn.ganlixin.model.User;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class UserDAO {

    /**
     * 從DB獲取全量user列表
     */
    public List<User> getAllUserFromDB() {
        try {
            // 模擬數據庫操作,耗時2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return null;
    }
}

  

進行測試

  創建測試類,測試UserService的getAllUser方法,因為getAllUser方法中使用CallUtils來調用數據庫操作,CallUtils的超時時間為1秒,而數據庫操作需要2秒,所以getAllUser方法必然會超時。

package cn.ganlixin.test;
import cn.ganlixin.service.UserService;
import org.junit.Test;

public class TestUserService {

    public UserService userService = new UserService();

    @Test
    public void testGetAllUser() {
        userService.getAllUser();
    }
}

  執行測試,輸出如下:

  

 

 

總結

  本文演示了接口超時調用的原理,實現接口調用超時,無非是通過將任務提交到線程池后,使用future.get,設置超時時間即可。

  上面的代碼很多細節都不太規範,比如涉及到數據庫的超時,應該是數據庫連接池的超時配置,而我在演示時是直接使用CallUtils來替代了,但是明白這個原理就OK。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務