2020年全國鄉村春晚聯動“百縣萬村”_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

2019年12月26日,2020年全國鄉村春晚集中展示活動暨麗水鄉村春晚建設成果新聞發布會在北京召開。活動由文化和旅遊部公共服務司、全國公共文化發展中心、浙江省文化和旅遊廳和浙江省麗水市政府主辦。2020年全國鄉村春晚集中展示活動啟動儀式將於2020年1月8日在浙江麗水舉辦。屆時,以麗水為主會場,安徽省、四川省、廣東省、河南省等8省區同步啟動鄉村春晚活動。

此次活動將通過線下鄉村春晚年俗展示、線上互聯網科技互動形式,拉開2020年全國鄉村春晚“百縣萬村”區域聯動大幕。此外,全國鄉村“鬧”春晚展演展示活動也將在全國篩選最具區域特色的節目,到麗水主會場以斗藝和吆喝的形式,集中展示全國鄉村藝術普及成果。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

在麗水,活動主要以鄉村春晚品牌建設為主導,同步舉辦“麗水味道”年味集市、“麗水山耕”年貨集市、“僑鄉中國年”等十大特色活動,形成文、農、旅大融合,通過網絡直播、主會場與分會場聯動、線上線下互動,全面展現社會發展成就。

據介紹,鄉村春晚起源於1981年由麗水市慶元縣舉水鄉月山村农民自編、自導、自演的“月山春晚”,比央視春晚還早兩年,至今已有30多年的歷史。經過幾十年的發展,麗水鄉村春晚點面結合、遍地開花,成為當地重要的公共文化品牌。(記者 杜潔芳)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

讀懂時代,從讀懂語言開始_網頁設計公司

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

時節如流,歲月不居。年終歲末,不同機構相繼發布年度漢字、漢語盤點,國家語言資源監測與研究中心發布了中國媒體十大流行語:我和我的祖國、金色十年、學習強國、中美經貿磋商、最美奮鬥者、硬核、垃圾分類、先行示範區、基層減負年、我太難了。《咬文嚼字》編輯部、《語言文字周報》等機構公布的年度流行語與之有重合也有不同。

年度流行語盤點,既是語言文字研究領域的一件盛事,也是整個文化界的一樁“雅事”,而且被大眾看成是觀察中國社會、洞悉世道人心的一扇窗口。因此,年復一年,如期而至,廣受關注。今年的年度流行語形態各異,既有“我和我的祖國”“最美奮鬥者”“硬核”等與時代大局大勢同頻共振的“鐘鼓之音”,也有諸如“我太難了”“好嗨呦”之類,反映社會風尚、大眾心態、百姓心聲的“網言網語”。“大珠小珠落玉盤”,盡顯漢語言文字的豐富性。

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

過去的2019年,我們為共和國70年的輝煌成就喝彩,愛國主義情感讓我們熱淚盈眶,“我和我的祖國”在大江南北唱響。我們感動於千千萬萬奮鬥者“擼起袖子加油干”的蓬勃朝氣,也為基層幹部切實減輕了負擔鬆了一口氣;我們為偉大祖國的欣欣向榮而驕傲,也每每因工作的壓力、生活的不易感到焦慮……不論社會如何變化,總有一些與每個人息息相關的國家大事、社會大潮引領着我們的注意力,總有一些標誌性的人和事讓我們產生情感的交集;總有一些共同的情感把我們的心凝聚在一起。也因此,在人口基數如此龐大的中國,每年總有一些被普遍認同的年度流行語。

過去的2019年,我們的社交方式、信息獲取方式乃至生活方式,都在發生更深刻改變。人們在為奮鬥者喝彩的同時,也對所謂的“996工作制”產生質疑;既熱衷於以“X千萬條,Y第一條”造句,也在類似“我不要你覺得,我要我覺得”的表達中展現着自我意識的增強、個體個性的鮮明;盤他、雨女無瓜、檸檬精、斷舍離等等流行語,讓很多人感到摸不着頭腦,一些人卻高度認同、非常默契,這提示我們,分眾化的社交趨勢更加明顯,不同社交圈的人往往有着全然不同的文化生態和話語體系。如果把這種巨大的差異性放在70年歲月變遷的歷史長河中去觀察,我們就會看到,這種千差萬別、百花齊放、形態各異,展現的正是生活選擇的自由、社會文化的多元、人的個性的舒展。“和而不同,各美其美”是社會繁榮進步的體現。

語言是時代的風向標,讀懂時代,當從讀懂語言開始。十多年來,每至歲末,一串串閃現在語言文字大潮中的流行語被“打撈”出來。這種“打撈”,實際上是對國家大勢、世界風雲、民生實事、社會熱點的“打撈”,是對時代變化、社會變遷的“打撈”。一個個時代流行語鋪展在面前,嘴角輕揚,看似波瀾不驚地咂摸間,往往蘊含着人們對自身生活、社會變革與人類發展的深長思考,往往寄予着擁抱更美好生活、更美好時代的蓬勃進取心。“只爭朝夕,不負韶華”,新年已至,讓我們整裝出發。(作者:李思輝,系華中科技大學新聞評論研究中心特聘研究員)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

恢復古城風貌 傳承傳統文化_租車

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

本報石家莊1月8日電  (記者張志鋒)“登得上城樓,望得見古塔,記得住鄉愁。”新年伊始,河北正定東城門、府文廟、府城隍廟和正定博物館正式開放。歷時三年,正定縣24項古城風貌恢復提升工程全部完成。據了解,2019年正定縣旅遊接待預計突破1471萬人次,同比增長逾16.7%。

正定是國家歷史文化名城,有1600多年的建城史,縣城有“九樓四塔八大寺,二十四座金牌坊”,全縣有全國重點文物保護單位9處、河北省文物保護單位6處,各類館藏文物7672件。2017年1月,當地啟動24項古城風貌恢復提升工程。

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

提升工程主要包括:修復加固南城門系統和南部城牆5800米;在北城門建成遺址公園,修建東城門主城及瓮城,恢復護城河;原址復建“鎮府巨觀”陽和樓,展示正定傳統文化等。

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

曹工說mini-dubbo(2)–分析eureka client源碼,想辦法把我們的服務提供者註冊到eureka server(上)_網頁設計

台北網頁設計公司這麼多該如何選擇?

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

前言

eureka是spring cloud Netflix技術體系中的重要組件,主要完成服務註冊和發現的功能;那現在有個問題,我們自己寫的rpc服務,如果為了保證足夠的開放性和功能完善性,那肯定要支持各種註冊中心。目前我們只支持redis註冊中心,即服務提供者,在啟動的時候,將自身的ip+端口信息寫入到redis,那,我們是否註冊到 eureka中呢?

這個想法可行嗎?可行。eureka client 和eureka server間,無非是網絡通信,既然是網絡通信,那就有網絡協議,那我們的應用,只要遵照eureka server的協議來,就可以接入。

另外,eureka server沒有採用spring mvc來實現,而是採用了jersey框架,這個框架啥意思呢,可以理解為對Restful的實現。我從網上找了一段(https://www.jianshu.com/p/88f97b90963c):

SpringMVC在開發REST應用時,是不支持JSR311/JSR339標準的。如果想要按照標準行事,最常用的實現了這兩個標準的框架就是Jersey和CxF了。但是,因為Jersey是最早的實現,也是JSR311參考的主要對象,所以,可以說Jersey就是事實上的標準(類似Hibernate是JPA的事實上的標準),也是現在使用最為廣泛的REST開發框架之一。

因為eureka server採用了jersey,所以eureka client最終也是使用了配套的jersey client來和服務端通信。

所以,eureka client,裏面其實依賴了一堆jersey的包:

注意,上面的jersey-client、jersey-core等包,其group id都是這樣的:

    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.19.1</version>
      <scope>runtime</scope>
    </dependency>

但是,不知道為啥,eureka client中,最終並沒有完全使用jersey-client,而是使用了

    <dependency>
      <groupId>com.sun.jersey.contribs</groupId>
      <artifactId>jersey-apache-client4</artifactId>
      <version>1.19.1</version>
      <scope>runtime</scope>
    </dependency>

這個包,內部引入了:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.1.1</version>
        </dependency>

這個包,你可以簡單理解為,jersey-client變成了一個接口,jersey-apache-client4是它的一個實現,實現里,用了httpClient去實現。

httpClient,沒有幾個java同學不知道吧?這麼做是可行的,因為你最終通信,還是http,不管你服務端框架,是jersey、還是spring mvc、甚至以前的struts,這都不重要。

所以,大家在下面的源碼中看到jersey的時候,腦海里可以有這麼一張圖。從上層到底層的接口,分別是:

CloudEurekaClient
       ...
DiscoveryClient  
	...
EurekaClient
	...
JerseyClient
	...
HttpClient	

在此之前,我們還是先分析下eureka client 註冊到eureka server的源碼。

源碼環境

minidubbo代碼和相關博文在:
曹工說mini-dubbo(1)–為了實踐動態代理,我寫了個簡單的rpc框架
https://gitee.com/ckl111/mini-dubbo

代碼很簡單,不過還是給個代碼鏈接吧:

https://gitee.com/ckl111/all-simple-demo-in-work-1/tree/master/eureka-client

主要就是在pom.xml中,引入:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

然後啟動類:

@SpringBootApplication
public class DemoApplication {

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

}

源碼分析

spring.factory支持自動配置

因為前面的pom,引入了如下jar包:

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-netflix-eureka-client</artifactId>
		</dependency>

該jar包的META-INF\spring.factories中,有如下幾行:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration

我們看到,key是org.springframework.boot.autoconfigure.EnableAutoConfiguration,value是逗號分割的列表,這裏面都是需要被自動裝配的配置類,其中,我們看第三行的:

org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration

這個類,是自動裝配的配置類,我們可以簡單一覽:

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })
public class EurekaClientAutoConfiguration {

裏面一堆@ConditionalOn***,主要是看該配置類是否生效。

我們不管,這裏條件是滿足的,所以,看具體java文件里有什麼要裝配的內容,裏面內容較多,我們關注我們需要關注的:

		@Bean(destroyMethod = "shutdown")
		@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
		@Lazy
		public EurekaClient eurekaClient(ApplicationInfoManager manager,
				EurekaClientConfig config, EurekaInstanceConfig instance,
				@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
			ApplicationInfoManager appManager;
			if (AopUtils.isAopProxy(manager)) {
				appManager = ProxyUtils.getTargetObject(manager);
			}
			else {
				appManager = manager;
			}
            // 1
			CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
					config, this.optionalArgs, this.context);
			cloudEurekaClient.registerHealthCheck(healthCheckHandler);
			return cloudEurekaClient;
		}

這裡會自動裝配一個EurekaClient類型的bean,(從返回值可以看出來),而具體的類型呢,從上面的1處,可以看出,具體類型是CloudEurekaClient。

所以,我們開始看1處,這個CloudEurekaClient是怎麼new出來的。

CloudEurekaClient的創建

先看看其繼承結構:

我們這個CloudEurekaClient,位於spring-cloud-netflix-eureka-client-2.1.5.RELEASE包。

而其父類DiscoveryClient和接口EurekaClient,位於eureka-client-1.9.13

大致能分析出,CloudEurekaClient的底層實現是eureka,其本身,是一個膠水,集成 spring 和 Netflix。

CloudEurekaClient的構造函數

	public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
			EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args,
			ApplicationEventPublisher publisher) {
        // 1
		super(applicationInfoManager, config, args);
        // 2
		this.applicationInfoManager = applicationInfoManager;
		this.publisher = publisher;
		this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class,
				"eurekaTransport");
		ReflectionUtils.makeAccessible(this.eurekaTransportField);
	}

我們看1處,調用了父類的構造函數;2處下面的幾行,主要是對本類中的幾個field進行賦值,這幾個字段,我們不關心,所以,直接看父類的構造函數吧。

DiscoveryClient的構造函數

    public DiscoveryClient(ApplicationInfoManager applicationInfoManager, final EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args) {
        this(applicationInfoManager, config, args, ResolverUtils::randomize);
    }

    public DiscoveryClient(ApplicationInfoManager applicationInfoManager, final EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, EndpointRandomizer randomizer) {
        // 1
        this(applicationInfoManager, config, args, null, randomizer);
    }

上面兩個,都是重載。1處調用的,我們接下來會重點分析。

步驟1:一堆field賦值

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    	// 0	
        if (args != null) {
            this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
            this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
            this.eventListeners.addAll(args.getEventListeners());
            this.preRegistrationHandler = args.preRegistrationHandler;
        } else {
            this.healthCheckCallbackProvider = null;
            this.healthCheckHandlerProvider = null;
            this.preRegistrationHandler = null;
        }
        
        this.applicationInfoManager = applicationInfoManager;
        InstanceInfo myInfo = applicationInfoManager.getInfo();
		// 1
        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        instanceInfo = myInfo;
        if (myInfo != null) {
            appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
        } else {
            logger.warn("Setting instanceInfo to a passed in null value");
        }
		...

這一堆都是根據入數,來對類中的field進行賦值。比如

0處,主要是一些健康檢查的東西;1處,config類型為 com.netflix.discovery.EurekaClientConfig,這裏主要是eureka client的一些配置,比如我們在yml中配置了eureka.client.*之類的,就會到這裏。

步驟2:判斷是否要獲取eureka server中的服務提供者信息

	// 1
	if (config.shouldFetchRegistry()) {
            this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }

1處,可以看出來,是根據config中的shouldFetchRegistry進行判斷,是否要去獲取eureka server。

然後進行了一些監控指標的初始化。

步驟3:判斷是否要註冊到eureka server

    	// 1    
		if (config.shouldRegisterWithEureka()) {
            this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }

同上。

步驟4:如果既不註冊,也不獲取,則處理基本結束

		// 1
		if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            logger.info("Client configured to neither register nor query for data.");
            scheduler = null;
            heartbeatExecutor = null;
            cacheRefreshExecutor = null;
            eurekaTransport = null;
            instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());

            DiscoveryManager.getInstance().setDiscoveryClient(this);
            DiscoveryManager.getInstance().setEurekaClientConfig(config);
			// 2
            return;  // no need to setup up an network tasks and we are done
        }
  • 1處,既不註冊,也不從eureka server獲取
  • 2處,直接結束

步驟5:定義三個線程池

            //1 default size of 2 - 1 each for heartbeat and cacheRefresh
            scheduler = Executors.newScheduledThreadPool(2,
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-%d")
                            .setDaemon(true)
                            .build());
			// 2
            heartbeatExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff
			// 3 
			cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff
  • 1處,定義一個用於服務提供者信息的緩存刷新的定時線程池
  • 2處,定義一個心跳線程池
  • 3處,這個看起來也是用於緩存刷新的

步驟6:創建eurekaTransport對象

com.netflix.discovery.DiscoveryClient#scheduleServerEndpointTask
// 1
eurekaTransport = new EurekaTransport();
// 2
scheduleServerEndpointTask(eurekaTransport, args);
  • 1處,eurekaTransport是一個field,該類主要封裝了幾個後續通信要使用的底層client。

    
        private static final class EurekaTransport {
            private ClosableResolver bootstrapResolver;
            private TransportClientFactory transportClientFactory;
    		// 1.1
            private EurekaHttpClient registrationClient;
            private EurekaHttpClientFactory registrationClientFactory;
    		// 1.2
            private EurekaHttpClient queryClient;
            private EurekaHttpClientFactory queryClientFactory;
    

    1.1處,這個應該是註冊用的,也是我們需要的;

    1.2處,應該是查詢信息用的。

  • 調用了當前類的方法scheduleServerEndpointTask,且把eurekaTransport傳入了

步驟7:schedule周期任務

創建抽象工廠

因我們只是new了eurekaTransport,沒有對其field進行任何賦值,所以,這個scheduleServerEndpointTask總,有個地方對其field進行賦值。

com.netflix.discovery.DiscoveryClient#scheduleServerEndpointTask
// 1
TransportClientFactories transportClientFactories =new Jersey1TransportClientFactories();

// 2
eurekaTransport.transportClientFactory = transportClientFactories.newTransportClientFactory(clientConfig, additionalFilters, applicationInfoManager.getInfo(), sslContext, hostnameVerifier)

  • 1處,就是new了一個抽象工廠,抽象工廠,我個人理解是工廠的工廠,其產出的東西,不是直接的最終對象,而是另一種工廠。

    TransportClientFactories 是一個接口,主要包含了如下方法:

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

    擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

    	public TransportClientFactory newTransportClientFactory(
            	final EurekaClientConfig clientConfig,
            	final Collection<F> additionalFilters,
                final InstanceInfo myInstanceInfo,
                final Optional<SSLContext> sslContext,
                final Optional<HostnameVerifier> hostnameVerifier);
    

    主要5個參數,排除掉最後的倒數2個,可選參數,剩3個。分別是:eurekaClient的配置bean,額外的filter集合,當前實例信息。

具體工廠的職責

  • 2處,就是利用1處創建的抽象工廠,來生成我們需要的工廠。

    這裏,我們可以先看看,最終我們需要的工廠,是什麼樣的。

    /**
     * A low level client factory interface. Not advised to be used by top level consumers.
     *
     * @author David Liu
     */
    public interface TransportClientFactory {
    
        EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
    
        void shutdown();
    
    }
    

    newClient這個方法,聽名字,就是一個創建客戶端的,創建客戶端,需要什麼參數呢?總得知道要連接到哪個eureka server服務器吧,服務器地址是啥吧?沒錯,參數EurekaEndpoint serviceUrl可以給我們提供需要的這些:

    package com.netflix.discovery.shared.resolver;
    
    public interface EurekaEndpoint extends Comparable<Object> {
    	// 1
        String getServiceUrl();
    	// 2
        String getNetworkAddress();
    	// 3
        int getPort();
    
        boolean isSecure();
    
        String getRelativeUri();
    
    }
    
    
    • 1處,獲取url
    • 2處,獲取網絡地址
    • 3處,獲取端口

    基本對於我們一個客戶端來說,需要的參數就這些。

    說完了newClient的參數,再來看看響應:

    
    /**
     * Low level Eureka HTTP client API.
     *
     * @author Tomasz Bak
     */
    public interface EurekaHttpClient {
    
        EurekaHttpResponse<Void> register(InstanceInfo info);
    
        EurekaHttpResponse<Void> cancel(String appName, String id);
    
        EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus);
    
        EurekaHttpResponse<Void> statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info);
    
        EurekaHttpResponse<Void> deleteStatusOverride(String appName, String id, InstanceInfo info);
    
        EurekaHttpResponse<Applications> getApplications(String... regions);
    
        EurekaHttpResponse<Applications> getDelta(String... regions);
    
        EurekaHttpResponse<Applications> getVip(String vipAddress, String... regions);
    
        EurekaHttpResponse<InstanceInfo> getInstance(String appName, String id);
    
        EurekaHttpResponse<InstanceInfo> getInstance(String id);
    
        void shutdown();
    }
    

    看到了嗎,各種註冊、取消、發送心跳、狀態更新啥的,這幾本涵蓋了eureka client的所有操作了,沒錯,我們就是需要這麼個東西。

創建具體工廠

看完了我們需要的工廠的功能,我們馬上來看看這麼厲害的工廠怎麼創建出來?

com.netflix.discovery.shared.transport.jersey.Jersey1TransportClientFactories#newTransportClientFactory(...)
        
	@Override
    public TransportClientFactory newTransportClientFactory(
        	EurekaClientConfig clientConfig,
            Collection<ClientFilter> additionalFilters,
        	InstanceInfo myInstanceInfo,
        	Optional<SSLContext> sslContext,
        	Optional<HostnameVerifier> hostnameVerifier) {
    	// 2.1
        final TransportClientFactory jerseyFactory = JerseyEurekaHttpClientFactory.create(
                clientConfig,
                additionalFilters,
                myInstanceInfo,
                new EurekaClientIdentity(myInstanceInfo.getIPAddr()),
                sslContext,
                hostnameVerifier
        );
        // 2.2
        final TransportClientFactory metricsFactory = MetricsCollectingEurekaHttpClient.createFactory(jerseyFactory);
		// 2.3
        return new TransportClientFactory() {
            @Override
            public EurekaHttpClient newClient(EurekaEndpoint serviceUrl) {
                return metricsFactory.newClient(serviceUrl);
            }

            @Override
            public void shutdown() {
                metricsFactory.shutdown();
                jerseyFactory.shutdown();
            }
        };
    }
  • 2.1處,調用JerseyEurekaHttpClientFactory的create 靜態方法,生成了一個工廠
  • 2.2處,對生成的工廠,進行了包裝,看名稱,應該是包裝了統計相關信息。
  • 2.3處,對2.2處生成的工廠,用匿名內部類進行了包裝,調用匿名內部類的newClient時,直接代理給了metricsFactory;而shutdown方法,則主要是關閉 metricsFactory 和 jerseyFactory 工廠。

所以,我們現在要看看,2.1處,是怎麼創建工廠的。

com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory#create
    
    public static JerseyEurekaHttpClientFactory create(
    	EurekaClientConfig clientConfig,
        Collection<ClientFilter> additionalFilters,
    	InstanceInfo myInstanceInfo,                                                       		 AbstractEurekaIdentity clientIdentity) {
        // 1
    	boolean useExperimental = "true".equals(clientConfig.getExperimental("JerseyEurekaHttpClientFactory.useNewBuilder"));
		// 2
        JerseyEurekaHttpClientFactoryBuilder clientBuilder = (useExperimental ? experimentalBuilder() : newBuilder())
                .withAdditionalFilters(additionalFilters)
                .withMyInstanceInfo(myInstanceInfo)
                .withUserAgent("Java-EurekaClient")
                .withClientConfig(clientConfig)
                .withClientIdentity(clientIdentity);
    	// 3
        clientBuilder.withClientName("DiscoveryClient-HTTPClient");
		// 4
        return clientBuilder.build();
    }
  • 1處,砍斷是否要使用實驗性的builder
  • 2處,創建對應的builder,並把我們的參數,通過with*方法,設置進去
  • 3處,設置客戶端名稱
  • 4處,生成客戶端工廠
com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory.JerseyEurekaHttpClientFactoryBuilder#build

    
        @Override
        public JerseyEurekaHttpClientFactory build() {
    		// 1
            Map<String, String> additionalHeaders = new HashMap<>();
    		// 2
            if (allowRedirect) {
                additionalHeaders.put(HTTP_X_DISCOVERY_ALLOW_REDIRECT, "true");
            }
            if (EurekaAccept.compact == eurekaAccept) {
                additionalHeaders.put(EurekaAccept.HTTP_X_EUREKA_ACCEPT, eurekaAccept.name());
            }
			
            // 3
            return buildLegacy(additionalHeaders, systemSSL);
        }

這裏就是弄了個hashmap,設置了幾個header進去,然後3處,調用buildLegacy。

com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory.JerseyEurekaHttpClientFactoryBuilder#buildLegacy
    
        private JerseyEurekaHttpClientFactory buildLegacy(Map<String, String> additionalHeaders, boolean systemSSL) {
    		// 1
            EurekaJerseyClientBuilder clientBuilder = new EurekaJerseyClientBuilder()
                    .withClientName(clientName)
                    .withUserAgent("Java-EurekaClient")
                    .withConnectionTimeout(connectionTimeout)
                    .withReadTimeout(readTimeout)
                    .withMaxConnectionsPerHost(maxConnectionsPerHost)
                    .withMaxTotalConnections(maxTotalConnections)
                    .withConnectionIdleTimeout((int) connectionIdleTimeout)
                    .withEncoderWrapper(encoderWrapper)
                    .withDecoderWrapper(decoderWrapper);
			...
            
			// 2
            EurekaJerseyClient jerseyClient = clientBuilder.build();
    		// 3
            ApacheHttpClient4 discoveryApacheClient = jerseyClient.getClient();
            addFilters(discoveryApacheClient);
			// 4
            return new JerseyEurekaHttpClientFactory(jerseyClient, additionalHeaders);
        }
  • 1處,通過我們傳入的一些參數,以及該類自身的一些field,比如connectionTimeout、readTimeout、maxTotalConnections、maxConnectionsPerHost這些,構造一個builder。

    這些參數,已經看出來,是網絡通信所需要的東西了

  • 2處,通過1處的builder,調用build,拿到了EurekaJerseyClient類型的對象,可以說,這裏其實是已經把客戶端構造好了。也就是說,在構造這個工廠的過程中,其實已經在生成對應的產品了

  • 3處,對2處拿到的客戶端,做一些處理

  • 4處,將2處拿到的客戶端,封裝到了工廠的一些field中,後續調用工廠生產產品的時候,直接從field中取就行了。

        public JerseyEurekaHttpClientFactory(EurekaJerseyClient jerseyClient, Map<String, String> additionalHeaders) {
            this(jerseyClient, null, -1, additionalHeaders);
        }
    	private JerseyEurekaHttpClientFactory(EurekaJerseyClient jerseyClient,
                                              ApacheHttpClient4 apacheClient,
                                              long connectionIdleTimeout,
                                              Map<String, String> additionalHeaders) {
            this.jerseyClient = jerseyClient;
            this.apacheClient = jerseyClient != null ? jerseyClient.getClient() : apacheClient;
            this.additionalHeaders = additionalHeaders;
        }
    

所以,我們的重點,要放在2處的build身上。

	com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl.EurekaJerseyClientBuilder#build
	public EurekaJerseyClient build() {
            MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
            try {
                // 1
                return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
            } catch (Throwable e) {
                throw new RuntimeException("Cannot create Jersey client ", e);
            }
        }

接下來看1處:

public EurekaJerseyClientImpl(int connectionTimeout, int readTimeout, final int connectionIdleTimeout,ClientConfig clientConfig) {
        try {
            jerseyClientConfig = clientConfig;
            // 1
            apacheHttpClient = ApacheHttpClient4.create(jerseyClientConfig);
            // 2
            HttpParams params = apacheHttpClient.getClientHandler().getHttpClient().getParams();

            HttpConnectionParams.setConnectionTimeout(params, connectionTimeout);
            HttpConnectionParams.setSoTimeout(params, readTimeout);
			
        } catch (Throwable e) {
            throw new RuntimeException("Cannot create Jersey client", e);
        }
    }
  • 1處,創建com.sun.jersey.client.apache4.ApacheHttpClient4類型的對象

    該類型,就位於:

        <dependency>
          <groupId>com.sun.jersey.contribs</groupId>
          <artifactId>jersey-apache-client4</artifactId>
          <version>1.19.1</version>
          <scope>runtime</scope>
        </dependency>
    
    
        public static ApacheHttpClient4 create(final ClientConfig cc) {
            return new ApacheHttpClient4(createDefaultClientHandler(cc), cc);
        }
    

    這裏的createDefaultClientHandler(cc),裏面會去創建HttpClient。

    private static ApacheHttpClient4Handler createDefaultClientHandler(final ClientConfig cc) {
    		...
    
    		// 1
            final DefaultHttpClient client = new DefaultHttpClient(
                    (ClientConnectionManager)connectionManager,
                    (HttpParams)httpParams
            );
    
            ...
    		
            return new ApacheHttpClient4Handler(client, cookieStore, preemptiveBasicAuth);
        }
    

    這裏面細節省略了部分,主要就是1處,創建了HttpClient,這個就是平時我們用來發http請求的那個。

  • 2處,設置一些參數,這裏的HttpParams,從哪兒取出來的?apacheHttpClient.getClientHandler().getHttpClient()。這裏取到的,已經是HttpClient了。

    到此為止,我們可以看看httpParams中有哪些header:

在具體工廠基礎上,對註冊用的工廠進行封裝

        com.netflix.discovery.DiscoveryClient#scheduleServerEndpointTask
        // 1    
		if (clientConfig.shouldRegisterWithEureka()) {
            EurekaHttpClientFactory newRegistrationClientFactory = null;
            EurekaHttpClient newRegistrationClient = null;
            // 2
            newRegistrationClientFactory = EurekaHttpClients.registrationClientFactory(
                eurekaTransport.bootstrapResolver,
                eurekaTransport.transportClientFactory,
                transportConfig
            );
            // 3
            newRegistrationClient = newRegistrationClientFactory.newClient();
            // 4
            eurekaTransport.registrationClientFactory = newRegistrationClientFactory;
            eurekaTransport.registrationClient = newRegistrationClient;
        }

我們前面的n步,已經把通信用的客戶端,及對應的工廠,都已經創建出來了,為啥這裏又要創建什麼工廠。

簡單來說,前面的工廠,造出來的客戶端,通信是沒問題了;但是,你通信失敗了,要重試嗎,重試的話,換哪一台呢?你每次通信是成功,還是失敗,還是超時,需要統計嗎?一個生產級的框架,是要有這些功能的。

所以,這裏主要是進行一些上層的封裝。

ok,繼續分析上面的代碼。

  • 1處,判斷是否要註冊到eureka
  • 2處,生成一個工廠,該工廠負責生產:註冊用的客戶端
  • 3處,使用2處拿到的工廠,創建註冊用的客戶端
  • 4處,把3處拿到的客戶端,存儲到eurekaTransport的field中。

繼續深入2處。

    com.netflix.discovery.shared.transport.EurekaHttpClients#canonicalClientFactory
	static EurekaHttpClientFactory canonicalClientFactory(
        final String name,
        final EurekaTransportConfig transportConfig,
        final ClusterResolver<EurekaEndpoint> clusterResolver,
        final TransportClientFactory transportClientFactory) {
		// 1
        return new EurekaHttpClientFactory() {
            // 2
            @Override
            public EurekaHttpClient newClient() {
                // 3
                return new SessionedEurekaHttpClient(
                        name,
                        RetryableEurekaHttpClient.createFactory(...),
                        transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
                );
            }

            @Override
            public void shutdown() {
                wrapClosable(clusterResolver).shutdown();
            }
        };
    }
  • 1處,返回了一個工廠對象
  • 2處,工廠里重寫了newClient
  • 3處,返回了一個包裝過的EurekaClient。

可以看下這裏返回的SessionedEurekaHttpClient類。

這裏就是裝飾器模式,對enreka進行了層層封裝,和 java 的 io 流那樣理解就對了。

在具體工廠基礎上,對查詢用的工廠進行封裝

		// 1
		if (clientConfig.shouldFetchRegistry()) {
            EurekaHttpClientFactory newQueryClientFactory = null;
            EurekaHttpClient newQueryClient = null;
            // 2
            newQueryClientFactory = EurekaHttpClients.queryClientFactory(
                eurekaTransport.bootstrapResolver,
                eurekaTransport.transportClientFactory,
                clientConfig,
                transportConfig,
                applicationInfoManager.getInfo(),
                applicationsSource,
                endpointRandomizer
            );
            // 3
            newQueryClient = newQueryClientFactory.newClient();
            eurekaTransport.queryClientFactory = newQueryClientFactory;
            eurekaTransport.queryClient = newQueryClient;
        }

這裏的代碼,和上面基本相似。只不過,這裡是給查詢用的,所謂查詢,就是去eureka server獲取信息,比如服務提供者列表啥的。

  • 1處,判斷是否要去eureka server獲取
  • 2處,創建查詢用的工廠
  • 3處,利用2處拿到的工廠,創建查詢客戶端

步驟8:去eureka server獲取服務提供者信息

我們終於把步驟7講完了,實在有點長。

com.netflix.discovery.DiscoveryClient#DiscoveryClient(...)
    
// 1    
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    // 2
    fetchRegistryFromBackup();
}

這裏1處,就是判斷要不要去獲取,如果要的話,就調用fetchRegistry(false)

2處,如果1處沒取到,則要從backup地方去取。這塊可以自己定製backup策略。

註冊到eureka server

        if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            // 1
            if (!register() ) {
                throw new IllegalStateException("Registration error at startup. Invalid server response.");
            }
        }

這裡會判斷是否要註冊,是否要在初始化的時候註冊,如果要的話,進入1處,進行註冊。

初始化周期執行的任務

        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
        initScheduledTasks();

看這裏註釋,初始化的任務包括:集群解析、心跳、實例信息註冊、周期從eureka server獲取信息等。

周期任務:獲取服務提供者信息

if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

默認30s一次。

周期任務:定時發心跳,向eureka server進行renew

            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

這個也是30s。

心跳包,基本就是個put請求,裏面攜帶了2個參數。

@Override
    public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
        String urlPath = "apps/" + appName + '/' + id;
        ClientResponse response = null;
        try {
            WebResource webResource = jerseyClient.resource(serviceUrl)
                    .path(urlPath)
                    .queryParam("status", info.getStatus().toString())
                    .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());

周期任務:InstanceInfoReplicator

這個任務,默認也是30s執行一次。

            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

這個任務,其實現了runnable,註釋如下:


/**
 * A task for updating and replicating the local instanceinfo to the remote server. Properties of this task are:
 * - 1 configured with a single update thread to guarantee sequential update to the remote server
 * - 2 update tasks can be scheduled on-demand via onDemandUpdate()
 * - 3 task processing is rate limited by burstSize
 * - 4 a new update task is always scheduled automatically after an earlier update task.  However if an on-demand task is started, the scheduled automatic update task is discarded (and a new one will be scheduled after the new
 *   on-demand update).
 *
 *   @author dliu
 */
class InstanceInfoReplicator implements Runnable 
  • 1處,配置了一個單線程,保證向遠程eureka server,順序更新
  • 2處,通過本類的onDemandUpdate,可以強行插入一個任務,而無需通過定時執行
  • 3處,限流相關
  • 4處,執行完一個周期任務后,馬上會給自己安排下一個周期任務

其run方法:

    public void run() {
        try {
            // 1
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                // 2
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        }finally {
            // 3
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }
  • 1處,刷新實例信息
  • 2處,如果有需要的話,向eureka server進行註冊
  • 3處,調度下一次任務

初始化結束

基本,這個CloudEurekaClient構造就結束了,後續就依靠其開啟的一堆定時任務去進行工作。

總結

eureka client的初始化就講了這麼多,註冊還沒講,留帶下一講吧。

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

網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

風俗畫里的年_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

年夜飯 戴奔洪/繪

春聯、年畫,張貼了四季,那色彩還未曾褪去,又到了一個年的面前,就要開始除舊換新了。似乎,硃砂紅、黑墨,還有絢麗的色彩,流淌着的都是濃濃的新春氣息。

好些年了,我一直對春聯與年畫入迷,常常循着這樣的路徑去感知中華大地上傳統的民間年俗。曾經,我在宋代稱為“紙畫”的物件里,找到了年畫的初名。而迎新納福的春聯呢,比年畫還早,可以追溯到漢代。江西老鄉王安石《元日》中“千門萬戶曈曈日,總把新桃換舊符”的“桃符”,便是春聯的前身了。

不承想,比我更加痴迷的是婺源風俗畫家戴奔洪。

我熟知的老戴,早年是以畫人物、花鳥出道的。他在南方打拚多年,堅持以畫畫為生,不料十幾年前卻回到了家鄉婺源,開始沉浸在家鄉人的生產生活,以及“四時八節”的風俗畫創作中。無一例外,老戴作品的背景是鄉村、田園,是河流、廊橋、徽派民居,細節呢,是鄉村人物举手投足的動作,臉部舒展的表情,甚至是一個會心的眼神。在老戴的心目中,父老鄉親常年的勞作,抑或生活場景的某一個瞬間,都可以用手中的畫筆來呈現——對於他們的生息勞作,節令風俗,一筆一畫都是自己熱愛家鄉情感的詩意表達。《農耕圖》《割稻圖》《扇谷》《舂麻粿》《做清明粿》,以及《洗三朝》《裁縫店》《納鞋底》《上脊桁》《新娘出門》《露天電影》等,堪稱他的代表作。在畫境之中,我彷彿還能依稀聽到牛哞、雞鳴、犬吠,孩童的歡娛,父母的叮囑,嗩吶的吹奏,藝匠的唱彩,還有父老鄉親其樂融融的談笑風生。不熟悉老戴的人,很難將他粗獷的外表與細膩的筆法聯繫在一起。在很多人眼裡,那畫中的犁、耙、耖、牛軛、禾鐮、禾戽、風車,以及火桶、木盆、笸籮、筲箕,都已逐漸淡出了我們的日常生活。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

在老戴看來,宋代李嵩的《貨郎圖》,以及張擇端的《清明上河圖》,都是值得他努力去研習的風俗畫經典之作,而詩人陸遊“莫笑農家臘酒渾,豐年留客足雞豚,山重水復疑無路,柳暗花明又一村”的經典詩句,也應是作為一名從事風俗畫創作的畫家去常畫常新的題材。於是,老戴的作品畫境大多聚焦一個村莊,或一個家庭的生產生活,以及民俗場景,寫實而不落俗套。比如,他筆下刻畫的《拜壽圖》,畫境是表現在掛着中堂、對聯的堂前,晚輩跪地作揖,向長輩祝壽、敬茶的情景,不免讓人心感溫暖。

無疑老戴筆下的,都是我在婺源鄉村周遭的生產生活場景與民間相沿成習的風俗。無論繁複還是極簡,都是他畫意的表達。老戴畫的雖然不是年畫,卻一次次在風俗畫中去表現年的主題。

往往民間“四時八節”,既是民俗的,亦是味蕾的。老戴一個起承轉合,把創作的筆觸伸向了風俗中的年。《掃塵》《殺年豬》《年夜飯》《守歲》《拜年》《元宵迎龍燈》,每一幅都散發著濃郁的年味。貼春聯、掛年畫,或許是我喜歡老戴畫境中庭院與堂前色彩明艷的樣子,引發的卻是更多人的關注與好奇。譬如《年夜飯》,是表現婺源人家一家老少團團圓圓坐在堂前的八仙桌上吃年夜飯的場景,那桌上擺着的,分明是一盤一碟的雞鴨魚肉,以及風味小菜,還在氤氳着美味的鮮香。一家人吃了團圓飯之後,應是圍爐而坐的《守歲》了吧。《拜年》的順序呢,不僅僅是長幼有序、躬身作揖、問候新春吉祥如意,還有“初一親,初二鄰,初三初四拜丈人”的規矩。在婺源鄉村,民間還十分注重二十四節氣中的起首——“立春”,歷來有“新春大似年”的習俗。

顯然,老戴風俗畫中的年,每一幅都以工筆畫的底子去呈現年俗,用筆工整細緻,敷色濃淡相宜,尤其人物達到了形體與神態的完美統一。他所追求的,是線條與色彩的流暢飄逸,以及畫意的傳神。一幅幅作品,意趣盎然,展現的都是婺源民間年俗的獨特魅力。

似乎,老戴作品中表達的年俗,是生活畫意與藝術詩意的融合,大俗之中蘊含的雅緻,還有對民間文化中年味的傳承。又似乎,一些場景是我們在年中遺失或正在遺失的,老戴卻正在畫案前一筆一畫地打撈而起。

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

吃糖瓜、祭灶神……小年如何過出儀式感?_網頁設計公司

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

“二十三日過小年,差不多就是過新年的‘綵排’。”著名作家老舍在《北京的春節》中,曾記錄過一系列有關春節的民俗,其中就提到了小年,字里行間透出了濃濃的年味兒。

不過,小年的日期並不一致,有的是在臘月二十三,有的是在臘月二十四……但一般而言,小年都被當做是春節的序曲,等那首“過年謠”唱起來,人們就開始按部就班做準備,直到迎來熱熱鬧鬧的中國年。

小年日期不相同

小年是一個傳統節日,也被稱為謝灶、祭灶節、灶王節等。由於各地風俗,“小年”有着不同的概念,日期也不盡相同。

在古代,過小年有“官三民四船五”的傳統,也就是說,官家的小年是臘月二十三,百姓家的是臘月二十四,而水上人家則是臘月二十五。還有的地方把“臘月廿四”和“除夕前一夜”都稱為小年。

關於小年,還有一首“過年謠”,忙碌中自帶喜慶:“二十三,捏糖瓜;二十四,掃屋子;二十五,凍豆腐;二十六,燉豬肉;二十七,殺肥雞;二十八,把面發;二十九,蒸饅頭;三十上炕包餃子。”

不同地區的過年謠,內容會略有差別。但總體來說,都是表達了一種辭舊迎新、迎祥納福的美好心愿。

過小年為啥要“祭灶”?

小年被視為過年的開端,也自然會受到格外重視,祭灶便是期間一項重要的民俗活動。

灶神是中國古代神話傳說中的司飲食之神。也有傳說稱,灶王爺原本是一介平民,叫做張生。他成家后終日花天酒地,終於淪落到以乞討為生的地步。有一天,他恰好乞討到前妻家,羞愧難當,一頭鑽到灶鍋底下燒死了。

玉帝知道后,認為張生還有羞恥心,又是在鍋底死去,就把他封為灶王,每年臘月二十三、二十四上天“彙報”一家人的善惡,大年三十再回去。於是,漢族民間就有了“小年”祭灶的習慣,祈求來年平安順遂。

宋代的范成大在《祭灶詩》中說:“古傳臘月二十四,灶君朝天欲言事……杓長杓短勿復雲,乞取利市歸來分。”這首詩很形象地說明了古代有關祭灶的一些風俗習慣。

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

灶王爺畫像一般貼在灶台附近,祭灶時要把關東糖用火融化,塗在灶王爺的嘴上,這樣他就不能在玉帝面前說壞話。還要把舊畫像揭下來,再和用稻草紮成的“馬”一起燒掉,象徵著送灶神上天,也叫“辭灶”。之後,新年後再把新畫像貼上,迎回灶神。

除了糖瓜,這時還會吃些啥?

提到小年的飲食,糖瓜自然是標誌性食品之一。除了用來祭灶外,過去也是人們口中的美食。

糖瓜一般用黃米和麥芽熬制而成。這種糖有兩種形狀,長條的叫“關東糖”,扁圓的叫“糖瓜”,吃起來滿口生香,別有風味。

舊日,农民們一般到了隆冬季節沒什麼農活時才能休息,那時從臘月二十三到正月十五甚至是整個正月都是“年”,過年自然要吃餃子,時間久了,吃餃子也成了小年的習俗。

晉東南地區,流行小年吃炒玉米的習俗,民諺有“二十三,不吃炒,大年初—一鍋倒”的說法。人們喜歡將炒玉米用麥芽糖粘結起來,冰凍成大塊,咬上一口,又酥又脆。

剪窗花、掃房子……過年準備工作有哪些?

過了小年,離着春節只剩下不到十天時間了。剪窗花、貼對聯、貼年畫……過年的準備工作會更加熱鬧。

例如,各家各戶都會認真打掃房子、清除灰塵,窗明几淨迎接新年。據說,小年也叫掃塵日,源於堯舜時期的“掃年”習俗,原本是古代人民驅除病疫的一種儀式。

在所有準備過年的活動中,剪貼窗花是最流行的。窗花的內容可以是喜鵲登梅、燕穿桃柳、蓮(連)年有魚(餘)、和合二仙等,也有各種戲劇故事。

過去,臘月二十三后,家家戶戶要蒸饅頭,特別要製做一個“大棗山”來供奉灶君,饅頭也可以有許多造型,往往是大家一展靈巧手藝的好機會。

俗話說,“有錢沒錢,剃頭過年”。在此期間,大人、小孩都要洗澡、理髮,一身清爽迎接新年,人們認為,這樣也有“辭舊迎新”的意味。

其實,小年並不只是一個簡單的日期,重要的是它作為春節的前奏,此後的一系列民俗活動都強調了“參與感”,也是在這個過程中,年味越來越濃了。(記者 上官雲)

本站聲明:網站內容來http://www.societynews.cn/html/wh/fq/,如有侵權,請聯繫我們,我們將及時處理

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

7-8萬市區行車最實用的合資車型 性價比真不低!_潭子電動車

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

pOLO的外觀圓潤飽滿,看起來比較中規中矩,符合大眾的中庸式的設計風格,適合成熟的消費者。內飾也依然很大眾風,簡單實用,缺乏新意,容易審美疲勞。pOLO的動力系統種類繁多,發動機為1。4L 90馬力 L4、1。6L 110馬力 L4和1。

雖然國人都是很喜歡大的車子,但是也不能忽略一個客觀的事實,那就是城市越來越擁堵了,在這環境下開一個大的車子確實很不方便。但是這時候如果有一台靈活的小車,那麼在市區開起來就輕鬆多了。

長安福特-嘉年華

福特嘉年華兩廂車的車身尺寸為3970*1722*1470mm,三廂的為4320*1722*1470mm,軸距都為2495mm,定位小型車。嘉年華的前臉採取了福特家族式設計特徵-馬丁臉的設計,再配合炯炯有神的前大燈組,使得嘉年華看起來動感十足。

嘉年華的動力系統為1.5L 110馬力+5擋手動/6擋雙離合,1.0T 125馬力+6擋雙離合。我在路上基本沒有見過三廂的嘉年華,不過我們也真的不推薦三廂版的車型,看起來真的是好難看。反之兩廂版的看起來則協調很多。

嘉年華的這款1.0T的發動機為三缸發動機,曾經榮獲2015年1.0L以下組的年度國際最佳發動機,擁有缸內直噴、渦輪增壓、可變氣門正時三大技術,燃油經濟性很好,同時平順性和普通的4缸發動機一樣,一般人很難覺察出來這是一款3缸發動機。但是,這麼先進的一款發動機,配合6擋雙離合算是有點失敗了,如果不是特別喜歡,

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

盡量不推薦購買雙離合版本的車型。

上汽大眾-pOLO

大眾pOLO的車身尺寸為3970*1682*1462mm、3975*1682*1487mm、3987*1705*1486mm,軸距2470mm。車身尺寸幾乎和嘉年華完全一樣。pOLO的外觀圓潤飽滿,看起來比較中規中矩,符合大眾的中庸式的設計風格,適合成熟的消費者。

內飾也依然很大眾風,簡單實用,缺乏新意,容易審美疲勞。pOLO的動力系統種類繁多,發動機為1.4L 90馬力 L4、1.6L 110馬力 L4和1.4T 150馬力 L4,匹配5擋手動、6擋自動、6擋手自一體和7擋雙離合四款變速箱。雖然大眾曾經被雙離合折磨的夠嗆,但是就現在來看,大眾目前的雙離合和同級別比起來,還是比較好用的。但是然並卵,買pOLO的幾乎都是自吸版本的,1.4T車型幾乎無人問津,畢竟老百姓家用車,還是自吸最省心、省錢。

廣汽豐田-YARiS L 致炫

致炫的車身尺寸為4115*1700*1485mm、4160*1700*1485mm,軸距為2550mm。雖然都是小型兩廂車,但是致炫的長度要大於嘉年華和pOLO,較大的軸距加上日系車的偷空間技術,所以致炫的後排空間是最大的,甚至可以和緊湊型轎車媲美。

致炫的動力系統為1.3L 99馬力/1.5L 107馬力+5擋手動/4擋自動/CVT。其中4AT車型為停產在售狀態,因為前段時間新款致炫上市,用CVT取代了老舊的4AT,同時新款的致炫標配了CVT變速箱,無論在燃油的經濟性和平順性都會有提高。新款致炫空間大,配置不低,皮實耐操,省油省心,也是一款不錯的家庭代步車。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

Microsoft 與 Parallels 合作,要將 Windows 10 帶到 M1 版 Mac 上_網頁設計

台北網頁設計公司這麼多該如何選擇?

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

Apple 在上個月推出了以 ARM 為基礎的 M1 晶片,現在應該全球的開發者都在日以繼夜地努力將自家的應用程導入這個新平台上,對於雙系統用戶來說,還是最關心 Windows 10 到底什麼時候可以支援 M1 晶片。日前 Microsoft 與 Parallels 開展合作,盡量讓大家不用久等就能享用。

Microsoft 與 Parallels 合作,要將 Windows 10 帶到 M1 版 Mac 上

對雙系統用戶來說,Parallels 絕對不陌生,該公司已經開始開發自己的軟體,要將 Windows 10 帶到 Apple Silicon 的世界,在不久前該公司才剛發布了第一個測試預覽版本,讓用戶能夠盡早試用該產品。事實證明,Parallels 並不是唯一一個在這方面使力的人,雖然不清楚兩家公司的合作方式為何,但 Microsoft 旗下 OneDrive 副總裁 Omar Shahine 在 Twitter 上面讚揚了此一合作。

Dang this is amazing! Windows 10 ARM running on MacBook Air M1. Great performance and battery life! Thanks @ParallelsMac and @Microsoft for releasing the ARM build of Windows! pic.twitter.com/BR8vZhJV7V

— Omar Shahine (@OmarShahine) December 23, 2020

有趣的是,在不久前,Apple 的高層主管曾表示將 Windows 引進 Apple Silicon 是僅有 Microsoft 自己能夠做決定,在彼時,Apple 軟體工程高級副總裁 Craig Federighi 曾表示 Apple 擁有 Microsoft 能做到這點的核心技術,可以直接運行 ARM 版本的 Windows,但又能反過來支援 X86 的應用程式,但這必須由 Microsoft 做出決定,將該技術授權給用戶在 M1 版 Mac 設備上運行。

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

於此同時,Apple Silicon 世界正在持續壯大,不久前也有傳聞 Microsoft 也正在為新晶片的軟體最佳化投入大量資金,Office、Teams 與其他 Microsoft 應用程式正陸陸續續開放支援 M1 晶片,以求一切都能在新的 Apple 設備上盡可能地平穩運作。

◎資料來源:SoftPedia

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

Cypress系列(3)- Cypress 的初次體驗_台北網頁設計

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

如果想從頭學起Cypress,可以看下面的系列文章哦

https://www.cnblogs.com/poloyy/category/1768839.html

 

前言

這裏的栗子項目是 Cypress 提供的,在 github 上,所以要 clone 到本地的話需要裝 Git 哦!

 

下載被測應用

進入要安裝該應用的目錄,cmd 敲

git clone git@github.com:cypress-io/cypress-example-recipes.git

進入項目目錄下,安裝項目所需依賴包,敲

npm install

安裝成功后,項目的文件結構如下圖;所有被測應用栗子都在 examples 文件夾中

 

啟動被測應用

啟動測試應用時,可以進入不同子項目文件夾來啟動不同的應用;

假如,我們要測試表單類型的登錄,可以打開以下被測應用

cd examples\logging-in__html-web-forms>

 

啟動本地server

npm start

 

啟動成功后,cmd窗口將显示服務器的地址和端口

 

打開瀏覽器訪問:http://localhost:7077/,即可看到登錄頁面

 

快速測試登錄頁面

首先,設計測試用例步驟

  1. 訪問http://localhost:7077
  2. 輸入用戶名、密碼,點擊登錄
  3. 如果用戶名和密碼正確,則登錄成功,否則登錄失敗

 

接下來,我們來看看實現測試用例的步驟

 

創建測試

在此目錄下 cypress安裝路徑\node_modules\.bin\cypress\integration ,創建一個 js 文件,比如:testLogin.js

integration文件夾

  • Cypress 安裝完畢后自動生成的文件夾
  • 也是 Cypress 默認存放測試用例的根目錄,任何創建在此目錄下的文件都將被當作測試用例

 

編寫測試用例

首先,要在網頁上定位到用戶名、密碼輸入框,此案例中使用標籤+屬性名來定位;最終測試代碼如下

咱們在後面再講解代碼的意思哦

 

運行測試

進入 Cypress 安裝文件夾,cmd執行命令

yarn cypress:open

 

單擊 testLogin.js,Cypress 會啟動 Test  Runner 運行測試,運行成功后,將看到運行結果頁面;運行時長是真的快….

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

 

調試測試用例

前言

  • 測試用例運行時,難免會發生各種情況導致運行失敗;快速定位發生錯誤的位置,了解錯誤信息,一直是自動化測試的痛點
  • 而 Cypress 提供了多種 debug 能力,可以在測試運行錯誤時直達錯誤位置,並支持回放錯誤發生時的上下文信息,可直接看到測試失敗的原因 

 

Cypress Debug 能力介紹

每個命令均有快照且支持回放

像下圖,左側就是測試步驟,右側是測試頁面

  • 鼠標 hover 測試步驟,在右側可以看到執行該命令時的頁面效果
  • 鼠標點擊測試步驟,可以鎖定該步驟,然後查看上下文信息

 

支持查看測試運行時發生的特殊頁面事件

包括:

  • 網絡 XHR 請求
  • URL 哈希更改
  • 頁面加載
  • 表單提交

例如,上面測試用例中,點擊【submit】后產生的就是提交表單的請求,看下圖

可以看到一個 submit 操作,分成了三步走

  1. form sub:提交表單
  2. page load:頁面加載
  3. new url:訪問新的頁面

 

Console 輸出每個命令的詳細信息

瀏覽器F12即可見到熟悉的開發者工具頁面了

以上圖為栗子,一個 submitting form 表單提交的請求,在 Console 中打印了詳細的信息,可以快速了解在運行時的詳細狀態信息

 

暫停測試並逐步運行、恢復執行

在調試測試代碼時,Cypress 提供了兩個命令來暫停測試運行

  1. cy.pause()
  2. cy.debug()

 

cy.pause() 的栗子

在表單提交之前暫停測試,我們來看看運行結果

左上角有兩個按鈕,從左往右分別是

  • Resume:繼續執行測試用例並運行到結束
  • Next:get:測試會變成逐步運行,點一下執行下一個命令

 

cy.debug() 的栗子

運行測試看看下圖結果

測試運行在找到表單的時候,暫停運行並等待用戶操作

頂部的Paused in debugger,右邊兩個按鈕分別是
  • Resume Script Execution(F8):繼續執行測試用例並運行到結束
  • Step Over next function call(F10):跳轉到下一個調用debug()函數的地方 

 

當找到隱藏或多個元素時,可視化結果

更改 username 輸入框的定位器,使他匹配到不止一個元素

運行測試看看下面結果

因為定位表達式匹配到不止一個元素,所以執行 type() 方法時以失敗告終

 

總結

這一節咱們以測試一個登錄界面為需求,寫了一個簡單的測試用例來做栗子,後面將詳細講解 Cypress 的各部分內容哦

 

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

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

一個工業級、跨平台、輕量級的 tcp 網絡服務框架:gevent_網頁設計公司

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

作為公司的公共產品,經常有這樣的需求:就是新建一個本地服務,產品線作為客戶端通過 tcp 接入本地服務,來獲取想要的業務能力。

與印象中動輒處理成千上萬連接的 tcp 網絡服務不同,這個本地服務是跑在客戶機器上的,Win32 上作為開機自啟動的 windows 服務運行;

Linux 上作為 daemon 在後台運行。總的說來就是用於接收幾個產品進程的連接,因此輕量化是其最重要的要求,在這個基礎上要能兼顧跨平台就可以了。

其實主要就是 windows,再兼顧一點兒 linux。

 

考察了幾個現有的開源網絡框架,從 ACE 、boost::asio 到 libevent,都有不盡於人意的地方:

a) ACE:太重,只是想要一個網絡框架,結果它扒拉扒拉一堆全提供了,不用還不行;

b) boost::asio:太複雜,牽扯到 boost 庫,並且引入了一堆 c++ 模板,需要高版本 c++ 編譯器支持;

c) libevent:這個看着不錯,當時確實用這個做底層封裝了一版,結果發版后發現一個比較致命的問題,導致在防火牆設置比較嚴格的機器上初始化失敗,這個後面我會詳細提到。

 

其它的就更不用說了,之前也粗略看過陳碩的 muddo,總的感覺吧,它是基於其它開源框架不足地方改進的一個庫,有相當可取的地方,但是這個改進的方向也主要是解決更大併發、更多連接,不是我的痛點,所以沒有繼續深入研究。

 

好了,與其在不同開源框架之間糾結,不如自己動手寫一個。

反正我的場景比較固定,不用像它們那樣面面俱,我給自己羅列了一些這個框架需要支持基本的功能:

1)同步寫、異步讀;

2)可同時監聽多路事件,基於 1)這裏只針對異步 READ 事件(包含連接進入、連接斷開),寫數據是同步的,因而不需要處理異步 WRITE 事件;

3)要有設置一次性和周期性定時器的能力 (業務決定的);

4)不需要處理信號 (windows 上也沒信號這一說,linux 自己搞搞 sigaction 就好啦);

……

 

雖然這個框架未來只會運行在用戶的單機上,但是我不希望它一出生就帶有性能缺陷,所以性能平平的 select 沒能進入我的法眼,我決定給它裝上最強大的心臟:

Windows 平台: iocp

Linux 平台:epoll

 

ok,從需求到底層技術路線,貌似都講清楚了,依照 libevent 我給它取名為 gevent,下面我們從代碼級別看下這個框架是怎麼簡化 tcp 服務搭建這類工作的。

首先看一下這個 tcp 服務框架的 sample:

svc_handler.h

 1 #include "EventBase.h"
 2 #include "EventHandler.h"
 3 
 4 class GMyEventBase : public GEventBase
 5 {
 6 public:
 7     GEventHandler* create_handler (); 
 8 }; 
 9 
10 
11 class svc_handler : public GJsonEventHandler
12 {
13 public:
14     virtual ~svc_handler () {}
15     virtual void on_read_msg (Json::Value const& val); 
16 };

epoll_svc.cpp

 1 #include <stdio.h>
 2 #include "svc_handler.h"
 3 #include <signal.h>
 4 
 5 GMyEventBase g_base; 
 6 GEventHandler* GMyEventBase::create_handler () 
 7 {
 8     return new svc_handler; 
 9 }
10 
11 void sig_int (int signo)
12 {
13     printf ("%d caught\n", signo); 
14     g_base.exit (1); 
15     printf ("exit ok\n"); 
16 }
17 
18 int main (int argc, char *argv[])
19 {
20     if (argc < 2)
21     {
22         printf ("usage: epoll_svc port\n"); 
23         return -1; 
24     }
25 
26     unsigned short port = atoi (argv[1]);
27 
28 #ifndef WIN32
29     struct sigaction act; 
30     act.sa_handler = sig_int; 
31     sigemptyset(&act.sa_mask);   
32     act.sa_flags = SA_RESTART; 
33     if (sigaction (SIGINT, &act, NULL) < 0)
34     {
35         printf ("install SIGINT failed, errno %d\n", errno); 
36         return -1; 
37     }
38     else
39         printf ("install SIGINT ok\n"); 
40 #endif
41 
42     // to test small message block
43     if (g_base.init (/*8, 10*/) < 0)
44         return -1; 
45 
46     printf ("init ok\n"); 
47     do
48     {
49         if (!g_base.listen (port))
50         {
51             g_base.exit (0); 
52             printf ("exit ok\n"); 
53             break; 
54         }
55 
56         printf ("listen ok\n"); 
57         g_base.run (); 
58         printf ("run  over\n"); 
59     } while (0); 
60 
61     g_base.fini (); 
62     printf ("fini ok\n"); 
63 
64     g_base.cleanup (); 
65     printf ("cleanup ok\n"); 
66     return 0; 
67 }

 

這個服務的核心是 GMyEventBase 類,它使用了框架中的 GEventBase 類,從後者派生而來,

只改寫了一個 create_handler 接口來提供我們的事件處理對象 svc_handler,它是從框架中的 GEventHandler 派生而來,

svc_handler 只改寫了一個 on_read_msg 來處理 Json 格式的消息輸入。

 

程序的運行就是分別調用 GMyEventBase(實際上是GEventBase)  的 init / listen / run / fini / cleaup 方法。

而與業務相關的代碼,都在 svc_handler 中處理:

svc_handler.cpp

 1 #include "svc_handler.h"
 2 
 3 void svc_handler::on_read_msg (Json::Value const& val)
 4 {
 5     int key = val["key"].asInt (); 
 6     std::string data = val["data"].asString (); 
 7     printf ("got %d:%s\n", key, data.c_str ()); 
 8 
 9     Json::Value root; 
10     Json::FastWriter writer; 
11     root["key"] = key + 1; 
12     root["data"] = data; 
13 
14     int ret = 0;
15     std::string resp = writer.write(root); 
16     resp = resp.substr (0, resp.length () - 1); // trim tailing \n
17     if ((ret = send (resp)) <= 0)
18         printf ("send response failed, errno %d\n", errno); 
19     else 
20         printf ("response %d\n", ret); 
21 }

 

它期待 Json 格式的數據,並且有兩個字段 key(int) 與 data (string),接收數據后將 key 增 1 后返回給客戶端。

再來看下客戶端 sample:

clt_handler.h

 1 #include "EventBaseAR.h"
 2 #include "EventHandler.h"
 3 
 4 class GMyEventBase : public GEventBaseWithAutoReconnect
 5 {
 6 public:
 7     GEventHandler* create_handler (); 
 8 }; 
 9 
10 
11 class clt_handler : public GJsonEventHandler
12 {
13 public:
14     virtual ~clt_handler () {}
15 #ifdef TEST_TIMER
16     virtual bool on_timeout (GEV_PER_TIMER_DATA *gptd); 
17 #endif
18     virtual void on_read_msg (Json::Value const& val); 
19 };

 

epoll_clt.cpp

  1 #include <stdio.h>
  2 #include "clt_handler.h"
  3 #include <signal.h>
  4 
  5 //#define TEST_READ
  6 //#define TEST_CONN
  7 //#define TEST_TIMER
  8 
  9 GMyEventBase g_base; 
 10 GEventHandler* GMyEventBase::create_handler () 
 11 {
 12     return new clt_handler; 
 13 }
 14 
 15 
 16 int sig_caught = 0; 
 17 void sig_int (int signo)
 18 {
 19     sig_caught = 1; 
 20     printf ("%d caught\n", signo); 
 21     g_base.exit (0); 
 22     printf ("exit ok\n"); 
 23 }
 24 
 25 void do_read (GEventHandler *eh, int total)
 26 {
 27     char buf[1024] = { 0 }; 
 28     int ret = 0, n = 0, key = 0, err = 0;
 29     char *ptr = nullptr; 
 30     while ((total == 0 ||  n++ < total) && fgets (buf, sizeof(buf), stdin) != NULL)
 31     {
 32         // skip \n
 33         buf[strlen(buf) - 1] = 0; 
 34         //n = sscanf (buf, "%d", &key); 
 35         key = strtol (buf, &ptr, 10); 
 36         if (ptr == nullptr)
 37         {
 38             printf ("format: int string\n"); 
 39             continue; 
 40         }
 41 
 42         Json::Value root; 
 43         Json::FastWriter writer; 
 44         root["key"] = key; 
 45         // skip space internal
 46         root["data"] = *ptr == ' ' ? ptr + 1 : ptr;  
 47 
 48         std::string req = writer.write (root); 
 49         req = req.substr (0, req.length () - 1); // trim tailing \n
 50         if ((ret = eh->send (req)) <= 0)
 51         {
 52             err = 1; 
 53             printf ("send %d failed, errno %d\n", req.length (), errno); 
 54             break; 
 55         }
 56         else 
 57             printf ("send %d\n", ret); 
 58     }
 59 
 60     if (total == 0)
 61         printf ("reach end\n"); 
 62 
 63     if (!err)
 64     {
 65         eh->disconnect (); 
 66         printf ("call disconnect to notify server\n"); 
 67     }
 68 
 69     // wait receiving thread 
 70     //sleep (3); 
 71     // if use press Ctrl+D, need to notify peer our break
 72 }
 73 
 74 #ifdef TEST_TIMER
 75 void test_timer (unsigned short port, int period_msec, int times)
 76 {
 77     int n = 0; 
 78     GEventHandler *eh = nullptr; 
 79 
 80     do
 81     {
 82         eh = g_base.connect (port); 
 83         if (eh == nullptr)
 84             break;
 85 
 86         printf ("connect ok\n"); 
 87         void* t = g_base.timeout (1000, period_msec, eh, NULL); 
 88         if (t == NULL)
 89         {
 90             printf ("timeout failed\n"); 
 91             break; 
 92         }
 93         else 
 94             printf ("set timer %p ok\n", t); 
 95 
 96         // to wait timer
 97         do
 98         {
 99             sleep (400); 
100             printf ("wake up from sleep\n"); 
101         } while (!sig_caught && n++ < times);
102 
103         g_base.cancel_timer (t); 
104     } while (0); 
105 }
106 #endif
107 
108 #ifdef TEST_CONN
109 void test_conn (unsigned short port, int per_read, int times)
110 {
111 #  ifdef WIN32
112     srand (GetCurrentProcessId()); 
113 #  else
114     srand (getpid ()); 
115 #  endif
116     int n = 0, elapse = 0; 
117     clt_handler *eh = nullptr; 
118 
119     do
120     {
121         eh = (clt_handler *)g_base.connect (port); 
122         if (eh == nullptr)
123             break;
124 
125         printf ("connect ok\n"); 
126 
127         do_read (eh, per_read); 
128 #  ifdef WIN32
129         elapse = rand() % 1000; 
130         Sleep(elapse); 
131         printf ("running  %d ms\n", elapse); 
132 #  else
133         elapse = rand () % 1000000; 
134         usleep (elapse); 
135         printf ("running  %.3f ms\n", elapse/1000.0); 
136 #  endif
137 
138     } while (!sig_caught && n++ < times);
139 }
140 #endif
141 
142 #ifdef TEST_READ
143 void test_read (unsigned short port, int total)
144 {
145     int n = 0; 
146     GEventHandler *eh = nullptr; 
147 
148     do
149     {
150         eh = g_base.connect (port); 
151         if (eh == nullptr)
152             break;
153 
154         printf ("connect ok\n"); 
155         do_read (eh, total); 
156     } while (0); 
157 }
158 #endif
159 
160 int main (int argc, char *argv[])
161 {
162     if (argc < 2)
163     {
164         printf ("usage: epoll_clt port\n"); 
165         return -1; 
166     }
167 
168     unsigned short port = atoi (argv[1]); 
169 
170 #ifndef WIN32
171     struct sigaction act; 
172     act.sa_handler = sig_int; 
173     sigemptyset(&act.sa_mask);   
174     // to ensure read be breaked by SIGINT
175     act.sa_flags = 0; //SA_RESTART;  
176     if (sigaction (SIGINT, &act, NULL) < 0)
177     {
178         printf ("install SIGINT failed, errno %d\n", errno); 
179         return -1; 
180     }
181 #endif
182 
183     if (g_base.init (2) < 0)
184         return -1; 
185 
186     printf ("init ok\n"); 
187 
188 #if defined(TEST_READ)
189     test_read (port, 0); // 0 means infinite loop until user break
190 #elif defined(TEST_CONN)
191     test_conn (port, 10, 100); 
192 #elif defined (TEST_TIMER)
193     test_timer (port, 10, 1000); 
194 #else
195 #  error please define TEST_XXX macro to do something!
196 #endif
197 
198     if (!sig_caught)
199     {
200         // Ctrl + D ?
201         g_base.exit (0); 
202         printf ("exit ok\n"); 
203     }
204     else 
205         printf ("has caught Ctrl+C\n"); 
206 
207     g_base.fini (); 
208     printf ("fini ok\n"); 
209 
210     g_base.cleanup (); 
211     printf ("cleanup ok\n"); 
212     return 0; 
213 }

 

客戶端同樣使用了 GEventBase 的派生類 GMyEventBase 來作為事件循環的核心,所不同的是(注意並非之前例子里的那個類,雖然同名),它提供了 clt_handler 來處理自己的業務代碼。

另外為了提供連接中斷後自動向服務重連的功能,這裏 GMyEventBase 派生自 GEventBase 類的子類 GEventBaseWithAutoReconnect (位於 EventBaseAR.h/cpp 中)。

程序的運行是分別調用 GEventBase 的 init / connect / fini / cleaup 方法以及 GEventHandler 的 send / disconnect 來測試讀寫與連接。

定義宏 TEST_READ 用來測試讀寫;定義宏 TEST_CONN 可以測試連接的通斷及讀寫;定義宏 TEST_TIMER 來測試周期性定時器及讀寫。它們是互斥的。

clt_handler 主要用來異步接收服務端的回送數據並打印:

clt_handler.cpp

 1 #include "clt_handler.h"
 2 
 3 #ifdef TEST_TIMER
 4 extern void do_read (clt_handler *, int); 
 5 bool clt_handler::on_timeout (GEV_PER_TIMER_DATA *gptd)
 6 {
 7     printf ("time out ! id %p, due %d, period %d\n", gptd, gptd->due_msec, gptd->period_msec); 
 8     do_read ((clt_handler *)gptd->user_arg, 1); 
 9     return true; 
10 }
11 #endif
12 
13 void clt_handler::on_read_msg (Json::Value const& val)
14 {
15     int key = val["key"].asInt (); 
16     std::string data = val["data"].asString (); 
17     printf ("got %d:%s\n", key, data.c_str ()); 
18 }

 

這個測試程序可以通過在控制台手工輸入數據來驅動,也可以通過測試數據文件來驅動,下面的 awk 腳本用來製造符合格式的測試數據:

epoll_gen.awk

 1 #! /bin/awk -f
 2 BEGIN {
 3         WORDNUM = 1000
 4         for (i = 1; i <= WORDNUM; i++) {
 5                 printf("%d %s\n", randint(WORDNUM), randword(20))
 6         }
 7 }
 8 
 9 # randint(n): return a random integer number which is >= 1 and <= n
10 function randint(n) {
11         return int(n *rand()) + 1
12 }
13 
14 # randlet(): return a random letter, which maybe upper, lower or number. 
15 function randlet() {
16         return substr("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", randint(62), 1)
17 }
18 
19 # randword(LEN): return a rand word with a length of LEN
20 function randword(LEN) {
21         randw=""
22         for( j = 1; j <= LEN; j++) {
23                 randw=randw randlet()
24         }
25         return randw
26 }

 

生成的測試文件格式如下:

238 s0jKlYkEjwE4q3nNJugF
568 0cgNaSgDpP3VS45x3Wum
996 kRF6SgmIReFmrNBcCecj
398 QHQqCrB5fC61hao1BV2x
945 XZ6KLtA4jZTEnhcAugAM
619 WE95NU7FnsYar4wz279j
549 oVCTmD516yvmtuJB2NG3
840 NDAaL5vpzp8DQX0rLRiV
378 jONIm64AN6UVc7uTLIIR
251 EqSBOhc40pKXhCbCu8Ey

 

整個工程編譯的話就是一個 CMakeLists 文件,可以通過 cmake 生成對應的 Makefile 或 VS solution 來編譯代碼:

CMakeLists.txt

 1 cmake_minimum_required(VERSION 3.0)
 2 project(epoll_svc)
 3 include_directories(../core ../include)
 4 set(CMAKE_CXX_FLAGS "-std=c++11 -pthread -g -Wall ${CMAKE_CXX_FLAGS}")
 5 link_directories(${PROJECT_SOURCE_DIR}/../lib)
 6 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/../bin)
 7 
 8 add_executable (epoll_svc epoll_svc.cpp svc_handler.cpp ../core/EventBase.cpp ../core/EventHandler.cpp ../core/log.cpp)
 9 IF (WIN32)
10 target_link_libraries(epoll_svc jsoncpp ws2_32)
11 ELSE ()
12 target_link_libraries(epoll_svc jsoncpp rt)
13 ENDIF ()
14 
15 add_executable (epoll_clt epoll_clt.cpp clt_handler.cpp ../core/EventBase.cpp ../core/EventBaseAR.cpp ../core/EventHandler.cpp ../core/log.cpp)
16 target_compile_definitions(epoll_clt PUBLIC -D TEST_READ)
17 IF (WIN32)
18 target_link_libraries(epoll_clt jsoncpp ws2_32)
19 ELSE ()
20 target_link_libraries(epoll_clt jsoncpp rt)
21 ENDIF ()
22 
23 add_executable (epoll_local epoll_local.cpp)
24 IF (WIN32)
25 target_link_libraries(epoll_local jsoncpp ws2_32)
26 ELSE ()
27 target_link_libraries(epoll_local jsoncpp rt)
28 ENDIF ()

 

 這個項目包含三個編譯目標,分別是 epoll_svc 、epoll_clt 與 epoll_local,其中前兩個可以跨平台編譯,后一個只能在 Linux 平台編譯,用來驗證 epoll 的一些特性。

編譯完成后,首先運行服務端:

>./epoll_svc 1025 

 然後運行客戶端:

>./epoll_clt 1025 < demo

測試多個客戶端同時連接,可以使用下面的腳本:

epoll_start.sh

1 #! /bin/bash
2 # /bin/sh -> /bin/dash, do not recognize our for loop
3 
4 for((i=0;i<10;i=i+1))
5 do
6     ./epoll_clt 1025 < demo &
7     echo "start $i"
8 done

 

可以同時啟動 10 個客戶端。

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

通過 Ctrl+C 退出服務端;通過 Ctrl+C 或 Ctrl+D 退出單個客戶端;

通過下面的腳本來停止多個客戶端與服務端:

epoll_stop.sh

1 #! /bin/sh
2 pkill -INT epoll_clt
3 sleep 1
4 pkill -INT epoll_svc

 

 

框架的用法介紹完之後,再簡單遊覽一下這個庫的各層級對外接口。

EventBase.h

  1 #pragma once
  2 
  3 
  4 #include "EventHandler.h" 
  5 #include <string>
  6 #include <map>
  7 #include <mutex> 
  8 #include <condition_variable>
  9 #include "thread_group.hpp"
 10 
 11 #define GEV_MAX_BUF_SIZE 65536
 12 
 13 class GEventBase : public IEventBase
 14 {
 15 public:
 16     GEventBase();
 17     ~GEventBase();
 18 
 19 #ifdef WIN32
 20     virtual HANDLE iocp () const; 
 21 #else
 22     virtual int epfd () const; 
 23 #endif
 24     virtual bool post_timer(GEV_PER_TIMER_DATA *gptd); 
 25     virtual GEventHandler* create_handler() = 0; 
 26 
 27     // thr_num : 
 28     //  =0 - no default thread pool, user provide thread and call run
 29     //  <0 - use max(|thr_num|, processer_num)
 30     //  >0 - use thr_num
 31     bool init(int thr_num = -8, int blksize = GEV_MAX_BUF_SIZE
 32 #ifndef WIN32
 33               , int timer_sig = SIGUSR1
 34 #endif
 35               ); 
 36 
 37     bool listen(unsigned short port, unsigned short backup = 10);
 38     GEventHandler* connect(unsigned short port, GEventHandler* exist_handler = NULL); 
 39     // PARAM
 40     // due_msec: first timeout milliseconds
 41     // period_msec: later periodically milliseconds
 42     // arg: user provied argument
 43     // exist_handler: reuse the timer handler
 44     //
 45     // RETURN
 46     //   NULL: failed
 47     void* timeout(int due_msec, int period_msec, void *arg, GEventHandler *exist_handler);
 48     bool cancel_timer(void* tid); 
 49     void fini();  
 50     void run(); 
 51     void exit(int extra_notify = 0); 
 52     void cleanup(); 
 53 
 54 protected:
 55 #ifdef WIN32
 56     bool do_accept(GEV_PER_IO_DATA *gpid); 
 57     bool do_recv(GEV_PER_HANDLE_DATA *gphd, GEV_PER_IO_DATA *gpid); 
 58     void do_error(GEV_PER_HANDLE_DATA *gphd); 
 59 
 60     int init_socket();
 61     bool issue_accept(); 
 62     bool issue_read(GEV_PER_HANDLE_DATA *gphd);
 63     bool post_completion(DWORD bytes, ULONG_PTR key, LPOVERLAPPED ol); 
 64 
 65 #else
 66     bool do_accept(int fd); 
 67     bool do_recv(conn_key_t key); 
 68     void do_error(conn_key_t key); 
 69 
 70     bool init_pipe(); 
 71     void close_pipe(); 
 72     bool post_notify (char ch, void* ptr = nullptr); 
 73     void promote_leader (std::unique_lock<std::mutex> &guard); 
 74 
 75     GEventHandler* find_by_key (conn_key_t key, bool erase); 
 76     GEventHandler* find_by_fd (int fd, conn_key_t &key, bool erase); 
 77 
 78 #  ifdef HAS_SIGTHR
 79     void sig_proc (); 
 80 #  endif
 81 #endif
 82 
 83     bool do_timeout(GEV_PER_TIMER_DATA *gptd); 
 84 
 85     virtual bool on_accept(GEV_PER_HANDLE_DATA *gphd);
 86     virtual bool on_read(GEventHandler *h, GEV_PER_IO_DATA *gpid); 
 87     virtual void on_error(GEventHandler *h);
 88     virtual bool on_timeout (GEV_PER_TIMER_DATA *gptd); 
 89     
 90 
 91 protected:
 92     volatile bool m_running = false;
 93     int m_thrnum = 0; 
 94     int m_blksize = GEV_MAX_BUF_SIZE; 
 95     std::thread_group m_grp; 
 96     SOCKET m_listener = INVALID_SOCKET;
 97 
 98     std::mutex m_mutex;  // protect m_map
 99     std::mutex m_tlock; // protect m_tmap
100     // timer_t may conflict when new timer created after old timer closed
101     //std::map <timer_t, GEventHandler *> m_tmap; 
102     std::map <GEV_PER_TIMER_DATA*, GEventHandler *> m_tmap; 
103 
104 #ifdef WIN32
105     LPFN_ACCEPTEX m_acceptex = nullptr; 
106     LPFN_GETACCEPTEXSOCKADDRS m_getacceptexsockaddrs = nullptr; 
107     HANDLE m_iocp = NULL; 
108     HANDLE m_timerque = NULL; 
109 
110     std::map<GEV_PER_HANDLE_DATA*, GEventHandler*> m_map; 
111 #else
112     int m_ep = -1; 
113     int m_pp[2]; 
114     int m_tsig = 0; // signal number for timer
115 
116     std::mutex m_lock;   // protect epoll
117     pthread_t m_leader = -1; 
118     std::map<conn_key_t, GEventHandler*> m_map; 
119 #  ifdef HAS_SIGTHR
120     // special thread only cares about signal
121     std::thread *m_sigthr = nullptr; 
122 #  endif
123 #endif
124 };

 

  • init,它在底層啟動 thr_num 個線程來跑 run 方法;每次 IO 的塊緩衝區大小由 blksize 指定;它內部還創建了對應的 iocp 或 epoll 對象,便於之后加入 socket 句柄進行處理。
  • exit,它通知線程池中的所有線程退出等待,windows 上是通過 PostQueuedCompletionStatus,Linux 上是通過在自建的一個 pipe 上寫數據以觸發 epoll 退出(這個 pipe 在 init 中創建並加入 epoll);
  • fini,它在所有工作線程退出后,關閉之前創建的對象,清理事件循環用到的資源;
  • cleanup,它清理之前建立的 fd-handler 映射,清理遺留的處理器並釋放資源;
  • run,它是線程池運行函數,windows 上是通過 GetQueuedCompletionStatus 在 iocp 上等待;在 linux 上是通過 epoll_wait 在 epoll 上等待事件。當有事件產生后,根據事件類型,分別調用 do_accept / on_accept、do_recv / on_read、do_error / on_error 回調來分派事件;
  • listen,創建偵聽 socket 並加入到 iocp 或 epoll 中;
  • connect,連接到遠程服務並將成功連接的 socket 加入到 iocp 或  epoll 中;
  • timeout,設置定時器事件,windows 上是通過 CreateTimerQueueTimer 實現定時器超時;linux 則是通過 timer_create 實現的,都是系統現成的東西,只不過在系統定時器到期后,給對應的 iocp 或 epoll 對象發送了一個通知而已,在 linux 上這個通知機制是上面提到過的 pipe 來實現的,因而有一定延遲,不能指定精度太小的定時器;
  • cancel_timer,取消之前設置的定時器。

 

然後看下 GEventHandler 提供的回調接口,應用可以從它派生並完成業務相關代碼:

EventHandler.h

  1 #pragma once
  2 #include "platform.h"
  3 
  4 #ifdef WIN32
  5 // must ensure <winsock2.h> precedes <widnows.h> included, to prevent winsock2.h conflict with winsock.h
  6 #  include <WinSock2.h>
  7 #  include <Windows.h>
  8 #  include <mswsock.h>  // for LPFN_ACCEPTEX & LPFN_GETACCEPTEXSOCKADDRS later in EventBase.h
  9 #else
 10 #  include <unistd.h> // for close
 11 #  include <sys/socket.h>
 12 #  include <sys/epoll.h>
 13 #  include <sys/time.h>
 14 #  include <netinet/in.h> // for struct sockaddr_in
 15 #  include <arpa/inet.h> // for inet_addr/inet_ntoa
 16 #  include <string.h> // for memset/memcpy
 17 #  include <signal.h>
 18 #endif
 19 
 20 #include <mutex>
 21 #include "jsoncpp/json.h"
 22 
 23 
 24 class GEventHandler; 
 25 struct GEV_PER_TIMER_DATA; 
 26 class IEventBase
 27 {
 28 public:
 29 #ifdef WIN32
 30     virtual HANDLE iocp () const = 0; 
 31 #else
 32     virtual int epfd () const = 0; 
 33 #endif
 34 
 35     virtual void* timeout(int due_msec, int period_msec, void *arg, GEventHandler *exist_handler) = 0; 
 36     virtual bool cancel_timer(void* tid) = 0; 
 37     virtual bool post_timer(GEV_PER_TIMER_DATA *gptd) = 0; 
 38 };
 39 
 40 
 41 #ifdef WIN32
 42 enum GEV_IOCP_OP
 43 {
 44     OP_TIMEOUT = 1, 
 45     OP_ACCEPT,
 46     OP_RECV,
 47 };
 48 #else 
 49 // the purpose of this key is to distinguish different connections with same fd !
 50 // (when connection break and re-established soon, fd may not change but port will change)
 51 struct conn_key_t
 52 {
 53     int fd; 
 54     unsigned short lport; 
 55     unsigned short rport; 
 56 
 57     conn_key_t (int f, unsigned short l, unsigned short r); 
 58     bool operator< (struct conn_key_t const& rhs) const; 
 59 }; 
 60 #endif
 61 
 62 
 63 struct GEV_PER_HANDLE_DATA
 64 {
 65     SOCKET so;
 66     SOCKADDR_IN laddr;
 67     SOCKADDR_IN raddr;
 68 
 69 #ifndef WIN32
 70     conn_key_t key () const; 
 71 #endif
 72 
 73     GEV_PER_HANDLE_DATA(SOCKET s, SOCKADDR_IN *l, SOCKADDR_IN *r); 
 74     virtual ~GEV_PER_HANDLE_DATA(); 
 75 };
 76 
 77 struct GEV_PER_IO_DATA
 78 {
 79     SOCKET so;
 80 #ifdef WIN32
 81     GEV_IOCP_OP op;
 82     OVERLAPPED ol;
 83     WSABUF wsa;         // wsa.len is buffer length
 84     DWORD bytes;        // after compeleted, bytes trasnfered
 85 #else
 86     char *buf; 
 87     int len; 
 88 #endif
 89 
 90     GEV_PER_IO_DATA(
 91 #ifdef WIN32
 92             GEV_IOCP_OP o, 
 93 #endif
 94             SOCKET s, int l); 
 95     virtual ~GEV_PER_IO_DATA(); 
 96 };
 97 
 98 struct GEV_PER_TIMER_DATA
 99 #ifdef WIN32
100        : public GEV_PER_IO_DATA
101 #endif
102 {
103     IEventBase *base; 
104     int due_msec; 
105     int period_msec; 
106     void *user_arg;
107     bool cancelled;
108 #ifdef WIN32
109     HANDLE timerque; 
110     HANDLE timer; 
111 #else
112     timer_t timer; 
113 #endif
114 
115     GEV_PER_TIMER_DATA(IEventBase *base, int due, int period, void *arg
116 #ifdef WIN32
117             , HANDLE tq);
118 #else
119             , timer_t tid); 
120 #endif
121 
122     virtual ~GEV_PER_TIMER_DATA(); 
123     void cancel (); 
124 };
125 
126 class GEventHandler
127 {
128 public:
129     GEventHandler();
130     virtual ~GEventHandler();
131 
132     GEV_PER_HANDLE_DATA* gphd(); 
133     GEV_PER_TIMER_DATA* gptd(); 
134     bool connected();
135     void disconnect(); 
136     void clear(); 
137     SOCKET fd(); 
138 
139     int send(char const* buf, int len);
140     int send(std::string const& str);
141     
142     virtual bool reuse();
143     virtual bool auto_reconnect();
144     virtual void arg(void *param) = 0;
145     virtual void reset(GEV_PER_HANDLE_DATA *gphd, GEV_PER_TIMER_DATA *gptd, IEventBase *base);
146     virtual bool on_read(GEV_PER_IO_DATA *gpid) = 0;
147     virtual void on_error(GEV_PER_HANDLE_DATA *gphd); 
148     // note when on_timeout called, handler's base may cleared by cancel_timer, use gptd->base instead if it is not null.
149     virtual bool on_timeout(GEV_PER_TIMER_DATA *gptd) = 0; 
150     virtual void cleanup(bool terminal);
151     void close(bool terminal);
152 
153 protected:
154     GEV_PER_HANDLE_DATA *m_gphd = nullptr; 
155     GEV_PER_TIMER_DATA *m_gptd = nullptr; 
156     IEventBase *m_base = nullptr;
157     // us so instead of m_gphd, 
158     // as the later one may destroyed during using..
159     SOCKET m_so;
160 };
161 
162 // a common handler to process json protocol.
163 class GJsonEventHandler : public GEventHandler
164 {
165 public:
166     //virtual void on_read();
167     virtual void arg(void *param);
168     virtual void reset(GEV_PER_HANDLE_DATA *gphd, GEV_PER_TIMER_DATA *gptd, IEventBase *base);
169     virtual bool on_read(GEV_PER_IO_DATA *gpid);
170     virtual void on_read_msg(Json::Value const& root) = 0;
171     virtual bool on_timeout(GEV_PER_TIMER_DATA *gptd);
172     virtual void cleanup(bool terminal);
173 
174 protected:
175     // protect m_stub to prevent multi-entry
176 #ifdef HAS_ET
177     std::mutex m_mutex; 
178 #endif
179 
180     std::string m_stub;
181 };

 

這裏主要有兩個類,GEventHandler 處理通用的基於流的數據;GJsonEventHandler 處理基於 json 格式的數據。

前者需要重寫 on_read 方法來處理塊數據;後者需要重寫 on_read_msg 方法來處理 json 數據。

目前 json 的解析是通過 jsoncpp 庫完成的,這個庫本身是跨平台的(本 git 庫僅提供 64 位 Linux 靜態鏈接庫及 VS2013 的 32 位 Release 版本 Windows 靜態庫)。

svc_handler 與 clt_handler  均從 GJsonEventHandler 派生。

如果有新的流格式需要處理 ,只需要從 GEventHandler 類派生新的處理類即可。

 

除了讀取連接上的數據,還有其它一些重要的回調接口,列明如下:

  • on_read,連接上有數據到達;
  • on_error,連接斷開;
  • on_tmeout,定時器事件;
  • ……

如果有新的事件需要處理 ,也可以在這裏擴展。

最後看下 GEventBaseWithAutoReconnect 提供的與自動重連相關的接口:

EventBaseAR.h

 1 #pragma once
 2 
 3 
 4 #include "EventBase.h"
 5 #include <thread>
 6 
 7 #define GEV_RECONNECT_TIMEOUT 2 // seconds
 8 #define GEV_MAX_RECONNECT_TIMEOUT 256 // seconds
 9 
10 class GEventBaseWithAutoReconnect : public GEventBase
11 {
12 public:
13     GEventBaseWithAutoReconnect(int reconn_min = GEV_RECONNECT_TIMEOUT, int reconn_max = GEV_MAX_RECONNECT_TIMEOUT);
14     ~GEventBaseWithAutoReconnect();
15 
16     bool do_connect(unsigned short port, void *arg);
17     GEventHandler* connector(); 
18 
19 protected:
20     virtual void on_error(GEventHandler *h);
21     virtual bool on_timeout(GEV_PER_TIMER_DATA *gptd);
22 
23     virtual void on_connect_break(); 
24     virtual bool on_connected(GEventHandler *app);
25 
26 protected:
27     void do_reconnect(void *arg);
28 
29 protected:
30     unsigned short m_port; 
31     GEventHandler* m_app;
32     GEventHandler* m_htimer; 
33     void* m_timer;
34     int m_reconn_min; 
35     int m_reconn_max; 
36     int m_reconn_curr;
37 };

 

其實比較簡單,只比 GEventBase 類多了一個  do_connect 方法,來擴展 connect 不能自動重連的問題。

底層的話,是通過定時器來實現指數後退重連算法的。

 

最後,如果你還是感到雲里霧裡的,可以參考一下下面的類結構圖:

 

 

黑色標註的是框架提供的類,紅色是服務端派生的類,藍色是客戶端派生的類,其實 GMyEventBase 的唯一作用就是將 svc_handler 與 clt_handler 分別引入各自的框架中,

所以用戶的關注點主要還是在派生自己的 GEventHandler 類,並在其中的回調接口中處理數據就可以了。

 

 

後記

這個框架已經應用到我司的公共產品中,併為數個 tcp 服務提供底層支撐,經過百萬級別用戶機器驗證,運行穩定性還是可以的,所以當得起“工業級”這三個字。

 

前面在說到開源庫的選型時還留了一個口子沒有交待,這裏一併說下。

其實最早的重構版本是使用 libevent 來實現的,但是發現它在 windows 上使用的是低效的 select,

而且為了增加、刪除句柄,它又使用了一種 self-pipe-trick 的技巧,簡單說來的就是下面的代碼序列:

listen (listen_fd, 1); 
……
connect (connect_fd, &addr, size); 
……
accept_fd = accept (listen_fd, &addr, &size); 

 

在缺乏 pipe 調用的 win32 環境製造了一個 socket 自連接,從而進行一些通知。

這一步是必要的,如果不能成功連接就會導致整個 libevent 初始化失敗,從而運行不起來。

不巧的是,在一些 windows 機器上(約佔用戶總量 10%),由於防火牆設置嚴格,上述 listen 與 connect 調用可以成功,

但是 accept 會失敗返回,從而導致整個服務退出 (防火牆會嚴格禁止不在白名單上偵聽的端口的連接)。

對於已知端口,可以通過在防火牆上設置白名單來避免,但是對於這種隨機 listen 的端口,真的是太難了,基本無解。

 

回頭考察了一下 asio,windows 上使用的是 iocp,自然沒有這個自連接;

ACE 有多種實現可供選擇,如果使用  ACE_Select_Reactor / ACE_TP_Reactor 是會有這個自連接,

但是你可以選擇其它實現,如基於 WaitForMultipleEvents 的 ACE_WFMO_Reactor(最大隻支持 62 個句柄,放棄),

或基於 iocp 的 ACE_Proactor (前攝式,與反應式在編程上稍有不同,更接近於 asio)就沒有這個自連接。

 

再說的深一點,其實公司最早的網絡庫使用的就是基於 boost 的 asio,大量的使用了 c++ 模板,

有時候產生了一些崩潰,但是根據 dump 完全無法定位崩潰點(各種冗長的模板展開名稱),

導致了一些頑固的已知 bug 一直找不到崩潰點而無法解決(雖然量不大),所以才有了要去重新選型網絡庫以及後來這一系列的東西。

 

本來一開始我是想用 ACE 的,因為我讀過這個庫的源碼,對裏面所有的東西都非常熟悉,

但是看看 ACE 小 5 MB 的 dll 尺寸,還是放棄了(產品本身安裝包也就這麼大吧),

對於一個公司底層的公共組件,被各種產品攜帶,需要嚴格控制“體重”

(後來聽說 ACE 按功能拆分了代碼模塊,你只需要選自己依賴的部分即可,不過我還沒有試過)。

 

使用這個庫代替之前的 boost::asio 后,我還有一個意外收穫,就是編譯出來的 dll 尺寸明顯小了很多,700 K -> 500 K 的樣子,看來所謂模板膨脹是真有其事……

 

最後奉上 gevent 的 github 鏈接,歡迎有相同需求的小夥伴前來“復刻” :

https://github.com/goodpaperman/gevent

 

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

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!