go中的數據結構-通道channel

1. channel的使用

  很多文章介紹channel的時候都和併發揉在一起,這裏我想把它當做一種數據結構來單獨介紹它的實現原理。

  channel,通道。golang中用於數據傳遞的一種數據結構。是golang中一種傳遞數據的方式,也可用作事件通知。

1.1 聲明、傳值、關閉

  使用chan關鍵字聲明一個通道,在使用前必須先創建,操作符 <- 用於指定通道的方向,發送或接收。如果未指定方向,則為雙向通道。

 1 //聲明和創建
 2 var ch chan int      // 聲明一個傳遞int類型的channel
 3 ch := make(chan int) // 使用內置函數make()定義一個channel
 4 ch2 := make(chan interface{})         // 創建一個空接口類型的通道, 可以存放任意格式
 5 
 6 type Equip struct{ /* 一些字段 */ }
 7 ch2 := make(chan *Equip)             // 創建Equip指針類型的通道, 可以存放*Equip
 8 
 9 //傳值
10 ch <- value          // 將一個數據value寫入至channel,這會導致阻塞,直到有其他goroutine從這個channel中讀取數據
11 value := <-ch        // 從channel中讀取數據,如果channel之前沒有寫入數據,也會導致阻塞,直到channel中被寫入數據為止
12 
13 ch := make(chan interface{})  // 創建一個空接口通道
14 ch <- 0 // 將0放入通道中
15 ch <- "hello"  // 將hello字符串放入通道中
16 
17 //關閉
18 close(ch)            // 關閉channel

  把數據往通道中發送時,如果接收方一直都沒有接收,那麼發送操作將持續阻塞。Go 程序運行時能智能地發現一些永遠無法發送成功的語句並報錯:

fatal error: all goroutines are asleep - deadlock! 
//運行時發現所有的 goroutine(包括main)都處於等待 goroutine。

1.2 四種重要的通道使用方式

無緩衝通道

  通道默認是無緩衝的,無緩衝通道上的發送操作將會被阻塞,直到有其他goroutine從對應的通道上執行接收操作,數據傳送完成,通道繼續工作。

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 創建一個channel
    go HelloWorld()
    <-done
}
1 //輸出
2 //Hello world goroutine

  由於main不會等goroutine執行結束才返回,前文專門加了sleep輸出為了可以看到goroutine的輸出內容,那麼在這裏由於是阻塞的,所以無需sleep。

  將代碼中”done <- true”和”<-done”,去掉再執行,沒有上面的輸出內容。

管道

  通道可以用來連接goroutine,這樣一個的輸出是另一個輸入。這就叫做管道:

 

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 var echo chan string
 8 var receive chan string
 9 
10 // 定義goroutine 1 
11 func Echo() {
12     time.Sleep(1*time.Second)
13     echo <- "這是一次測試"
14 }
15 
16 // 定義goroutine 2
17 func Receive() {
18     temp := <- echo // 阻塞等待echo的通道的返回
19     receive <- temp
20 }
21 
22 
23 func main() {
24     echo = make(chan string)
25     receive = make(chan string)
26 
27     go Echo()
28     go Receive()
29 
30     getStr := <-receive   // 接收goroutine 2的返回
31 
32     fmt.Println(getStr)
33 }

  輸出字符串:”這是一次測試”。

  在這裏不一定要去關閉channel,因為底層的垃圾回收機制會根據它是否可以訪問來決定是否自動回收它。(這裏不是根據channel是否關閉來決定的)

單向通道類型
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 // 定義goroutine 1
 9 func Echo(out chan<- string) {   // 定義輸出通道類型
10     time.Sleep(1*time.Second)
11     out <- "這又是一次測試"
12     close(out)
13 }
14 
15 // 定義goroutine 2
16 func Receive(out chan<- string, in <-chan string) { // 定義輸出通道類型和輸入類型
17     temp := <-in // 阻塞等待echo的通道的返回
18     out <- temp
19     close(out)
20 }
21 
22 
23 func main() {
24     echo := make(chan string)
25     receive := make(chan string)
26 
27     go Echo(echo)
28     go Receive(receive, echo)
29 
30     getStr := <-receive   // 接收goroutine 2的返回
31 
32     fmt.Println(getStr)
33 }

  輸出:這又是一次測試。

緩衝管道

  goroutine的通道默認是是阻塞的,那麼有什麼辦法可以緩解阻塞? 答案是:加一個緩衝區。

  創建一個緩衝通道:

1 ch := make(chan string, 3) // 創建了緩衝區為3的通道
2 
3 //==
4 len(ch)   // 長度計算
5 cap(ch)   // 容量計算

  緩衝通道傳遞數據示意圖:

 

2. 內部結構 

  Go語言channel是first-class的,意味着它可以被存儲到變量中,可以作為參數傳遞給函數,也可以作為函數的返回值返回。作為Go語言的核心特徵之一,雖然channel看上去很高端,但是其實channel僅僅就是一個數據結構而已,具體定義在 $GOROOT/src/runtime/chan.go里。如下:

 1 type hchan struct {
 2   qcount uint   // 隊列中的總數據
 3   dataqsiz uint   // 循環隊列的大小
 4   buf unsafe.Pointer // 指向dataqsiz元素數組
 5   elemsize uint16  // 
 6   closed uint32 
 7   elemtype *_type // 元素類型
 8   sendx uint // 發送索引
 9   recvx uint // 接收索引
10   recvq waitq // 接待員名單, 因recv而阻塞的等待隊列。
11   sendq waitq // 發送服務員列表, 因send而阻塞的等待隊列。
12   //鎖定保護hchan中的所有字段,以及幾個在此通道上阻止的sudogs中的字段。
13   //按住此鎖定時不要更改另一個G的狀態(尤其是不要準備G),因為這可能會導致死鎖堆棧縮小。
14   lock mutex 
15 }

   其中一個核心的部分是存放channel數據的環形隊列,由qcount和elemsize分別指定了隊列的容量和當前使用量。dataqsize是隊列的大小。elemalg是元素操作的一個Alg結構體,記錄下元素的操作,如copy函數,equal函數,hash函數等。

  如果是帶緩衝區的chan,則緩衝區數據實際上是緊接着Hchan結構體中分配的。不帶緩衝的 channel ,環形隊列 size 則為 0。

1 c = (Hchan*)runtime.mal(n + hint*elem->size);

  另一重要部分就是recvq和sendq兩個鏈表,一個是因讀這個通道而導致阻塞的goroutine,另一個是因為寫這個通道而阻塞的goroutine。如果一個goroutine阻塞於channel了,那麼它就被掛在recvq或sendq中。WaitQ是鏈表的定義,包含一個頭結點和一個尾結點:

1 struct    WaitQ
2 {
3     SudoG*    first;
4     SudoG*    last;
5 };

  隊列中的每個成員是一個SudoG結構體變量:

1 struct    SudoG
2 {
3     G*    g;        // g和selgen構成
4     uint32    selgen;        // 指向g的弱指針
5     SudoG*    link;
6     int64    releasetime;
7     byte*    elem;        // 數據元素
8 };

  該結構中主要的就是一個g和一個elem。elem用於存儲goroutine的數據。讀通道時,數據會從Hchan的隊列中拷貝到SudoG的elem域。寫通道時,數據則是由SudoG的elem域拷貝到Hchan的隊列中。

 

  基本的寫channel操作,在底層運行時庫中對應的是一個runtime.chansend函數。

1 c <- v
  在運行時庫中會執行:
1 void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)

  其中c就是channel,ep是取變量v的地址。這裏的傳值約定是調用者負責分配好ep的空間,僅需要簡單的取變量地址就夠了。pres參數是在select中的通道操作使用的。

  這個函數首先會區分是同步還是異步。同步是指chan是不帶緩衝區的,因此可能寫阻塞,而異步是指chan帶緩衝區,只有緩衝區滿才阻塞。在同步的情況下,由於channel本身是不帶數據緩存的,這時首先會查看Hchan結構體中的recvq鏈表時否為空,即是否有因為讀該管道而阻塞的goroutine。如果有則可以正常寫channel,否則操作會阻塞。

  recvq不為空的情況下,將一個SudoG結構體出隊列,將傳給通道的數據(函數參數ep)拷貝到SudoG結構體中的elem域,並將SudoG中的g放到就緒隊列中,狀態置為ready,然後函數返回。如果recvq為空,否則要將當前goroutine阻塞。此時將一個SudoG結構體,掛到通道的sendq鏈表中,這個SudoG中的elem域是參數eq,SudoG中的g是當前的goroutine。當前goroutine會被設置為waiting狀態並掛到等待隊列中。

  在異步的情況,如果緩衝區滿了,也是要將當前goroutine和數據一起作為SudoG結構體掛在sendq隊列中,表示因寫channel而阻塞。否則也是先看有沒有recvq鏈表是否為空,有就喚醒。跟同步不同的是在channel緩衝區不滿的情況,這裏不會阻塞寫者,而是將數據放到channel的緩衝區中,調用者返回。

  讀channel的操作也是類似的,對應的函數是runtime.chansend。一個是收一個是發,基本的過程都是差不多的。

  當協程嘗試從未關閉的 channel 中讀取數據時,內部的操作如下:

  • 當 buf 非空時,此時 recvq 必為空,buf 彈出一個元素給讀協程,讀協程獲得數據後繼續執行,此時若 sendq 非空,則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據入隊列 buf ,此時讀取操作 <- ch 未阻塞;
  • 當 buf 為空但 sendq 非空時(不帶緩衝的 channel),則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據直接傳遞給讀協程,讀協程繼續執行,此時讀取操作 <- ch 未阻塞;
  • 當 buf 為空並且 sendq 也為空時,讀協程入隊列 recvq 並轉入 blocking 狀態,當後續有其他協程往 channel 寫數據時,讀協程才會重新轉入 running 狀態,此時讀取操作 <- ch 阻塞。

  類似的,當協程嘗試往未關閉的 channel 中寫入數據時,內部的操作如下:

  • 當隊列 recvq 非空時,此時隊列 buf 必為空,從 recvq 彈出一個讀協程接收待寫數據,此讀協程此時結束阻塞並轉入 running 狀態,寫協程繼續執行,此時寫入操作 ch <- 未阻塞;
  • 當隊列 recvq 為空但 buf 未滿時,此時 sendq 必為空,寫協程的待寫數據入 buf 然後繼續執行,此時寫入操作 ch <- 未阻塞;
  • 當隊列 recvq 為空並且 buf 為滿時,此時寫協程入隊列 sendq 並轉入 blokcing 狀態,當後續有其他協程從 channel 中讀數據時,寫協程才會重新轉入 running 狀態,此時寫入操作 ch <- 阻塞。

  當關閉 non-nil channel 時,內部的操作如下:

  • 當隊列 recvq 非空時,此時 buf 必為空,recvq 中的所有協程都將收到對應類型的零值然後結束阻塞狀態;
  • 當隊列 sendq 非空時,此時 buf 必為滿,sendq 中的所有協程都會產生 panic ,在 buf 中數據仍然會保留直到被其他協程讀取。

  空通道是指將一個channel賦值為nil,或者定義后不調用make進行初始化。按照Go語言的語言規範,讀寫空通道是永遠阻塞的。其實在函數runtime.chansend和runtime.chanrecv開頭就有判斷這類情況,如果發現參數c是空的,則直接將當前的goroutine放到等待隊列,狀態設置為waiting。

  讀一個關閉的通道,永遠不會阻塞,會返回一個通道數據類型的零值。這個實現也很簡單,將零值複製到調用函數的參數ep中。寫一個關閉的通道,則會panic。關閉一個空通道,也會導致panic。

3. channel的高級用法

3.1 條件變量(condition variable)

  類型於 POSIX 接口中線程通知其他線程某個事件發生的條件變量,channel 的特性也可以用來當成協程之間同步的條件變量。因為 channel 只是用來通知,所以 channel 中具體的數據類型和值並不重要,這種場景一般用 strct {} 作為 channel 的類型。

一對一通知

  類似 pthread_cond_signal() 的功能,用來在一個協程中通知另個某一個協程事件發生:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     ch := make(chan struct{})
10     nums := make([]int, 100)
11 
12     go func() {
13         time.Sleep(time.Second)
14         for i := 0; i < len(nums); i++ {
15             nums[i] = i
16         }
17         // send a finish signal
18         ch <- struct{}{}
19     }()
20 
21     // wait for finish signal
22     <-ch
23     fmt.Println(nums)
24 }
廣播通知

  類似 pthread_cond_broadcast() 的功能。利用從已關閉的 channel 讀取數據時總是非阻塞的特性,可以實現在一個協程中向其他多個協程廣播某個事件發生的通知:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func main() {
 9     N := 10
10     exit := make(chan struct{})
11     done := make(chan struct{}, N)
12 
13     // start N worker goroutines
14     for i := 0; i < N; i++ {
15         go func(n int) {
16             for {
17                 select {
18                 // wait for exit signal
19                 case <-exit:
20                     fmt.Printf("worker goroutine #%d exit\n", n)
21                     done <- struct{}{}
22                     return
23                 case <-time.After(time.Second):
24                     fmt.Printf("worker goroutine #%d is working...\n", n)
25                 }
26             }
27         }(i)
28     }
29 
30     time.Sleep(3 * time.Second)
31     // broadcast exit signal
32     close(exit)
33     // wait for all worker goroutines exit
34     for i := 0; i < N; i++ {
35         <-done
36     }
37     fmt.Println("main goroutine exit")
38 }

3.2 信號量

  channel 的讀/寫相當於信號量的 P / V 操作,下面的示例程序中 channel 相當於信號量:

 1 package main
 2 
 3 import (
 4     "log"
 5     "math/rand"
 6     "time"
 7 )
 8 
 9 type Seat int
10 type Bar chan Seat
11 
12 func (bar Bar) ServeConsumer(customerId int) {
13     log.Print("-> consumer#", customerId, " enters the bar")
14     seat := <-bar // need a seat to drink
15     log.Print("consumer#", customerId, " drinks at seat#", seat)
16     time.Sleep(time.Second * time.Duration(2+rand.Intn(6)))
17     log.Print("<- consumer#", customerId, " frees seat#", seat)
18     bar <- seat // free the seat and leave the bar
19 }
20 
21 func main() {
22     rand.Seed(time.Now().UnixNano())
23 
24     bar24x7 := make(Bar, 10) // the bar has 10 seats
25     // Place seats in an bar.
26     for seatId := 0; seatId < cap(bar24x7); seatId++ {
27         bar24x7 <- Seat(seatId) // none of the sends will block
28     }
29 
30     // a new consumer try to enter the bar for each second
31     for customerId := 0; ; customerId++ {
32         time.Sleep(time.Second)
33         go bar24x7.ServeConsumer(customerId)
34     }
35 }

3.3 互斥量

  互斥量相當於二元信號里,所以 cap 為 1 的 channel 可以當成互斥量使用:

 1 package main
 2 
 3 import "fmt"
 4 
 5 func main() {
 6     mutex := make(chan struct{}, 1) // the capacity must be one
 7 
 8     counter := 0
 9     increase := func() {
10         mutex <- struct{}{} // lock
11         counter++
12         <-mutex // unlock
13     }
14 
15     increase1000 := func(done chan<- struct{}) {
16         for i := 0; i < 1000; i++ {
17             increase()
18         }
19         done <- struct{}{}
20     }
21 
22     done := make(chan struct{})
23     go increase1000(done)
24     go increase1000(done)
25     <-done; <-done
26     fmt.Println(counter) // 2000
27 }

4. 關閉 channel

  關閉不再需要使用的 channel 並不是必須的。跟其他資源比如打開的文件、socket 連接不一樣,這類資源使用完后不關閉後會造成句柄泄露,channel 使用完后不關閉也沒有關係,channel 沒有被任何協程用到后最終會被 GC 回收。關閉 channel 一般是用來通知其他協程某個任務已經完成了。golang 也沒有直接提供判斷 channel 是否已經關閉的接口,雖然可以用其他不太優雅的方式自己實現一個:

1 func isClosed(ch chan int) bool {
2     select {
3     case <-ch:
4         return true
5     default:
6     }
7     return false
8 }

  不過實現一個這樣的接口也沒什麼必要。因為就算通過 isClosed() 得到當前 channel 當前還未關閉,如果試圖往 channel 里寫數據,仍然可能會發生 panic ,因為在調用 isClosed() 后,其他協程可能已經把 channel 關閉了。
關閉 channel 時應該注意以下準則:

  • 不要在讀取端關閉 channel ,因為寫入端無法知道 channel 是否已經關閉,往已關閉的 channel 寫數據會 panic ;
  • 有多個寫入端時,不要再寫入端關閉 channle ,因為其他寫入端無法知道 channel 是否已經關閉,關閉已經關閉的 channel 會發生 panic ;
  • 如果只有一個寫入端,可以在這個寫入端放心關閉 channel 。

  關閉 channel 粗暴一點的做法是隨意關閉,如果產生了 panic 就用 recover 避免進程掛掉。稍好一點的方案是使用標準庫的 sync 包來做關閉 channel 時的協程同步,不過使用起來也稍微複雜些。下面介紹一種優雅些的做法。

4.1 一寫多讀

  這種場景下這個唯一的寫入端可以關閉 channel 用來通知讀取端所有數據都已經寫入完成了。讀取端只需要用 for range 把 channel 中數據遍歷完就可以了,當 channel 關閉時,for range 仍然會將 channel 緩衝中的數據全部遍歷完然後再退出循環:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6 )
 7 
 8 func main() {
 9     wg := &sync.WaitGroup{}
10     ch := make(chan int, 100)
11 
12     send := func() {
13         for i := 0; i < 100; i++ {
14             ch <- i
15         }
16         // signal sending finish
17         close(ch)
18     }
19 
20     recv := func(id int) {
21         defer wg.Done()
22         for i := range ch {
23             fmt.Printf("receiver #%d get %d\n", id, i)
24         }
25         fmt.Printf("receiver #%d exit\n", id)
26     }
27 
28     wg.Add(3)
29     go recv(0)
30     go recv(1)
31     go recv(2)
32     send()
33 
34     wg.Wait()
35 }

4.2 多寫一讀

  這種場景下雖然可以用 sync.Once 來解決多個寫入端重複關閉 channel 的問題,但更優雅的辦法設置一個額外的 channel ,由讀取端通過關閉來通知寫入端任務完成不要再繼續再寫入數據了:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6 )
 7 
 8 func main() {
 9     wg := &sync.WaitGroup{}
10     ch := make(chan int, 100)
11     done := make(chan struct{})
12 
13     send := func(id int) {
14         defer wg.Done()
15         for i := 0; ; i++ {
16             select {
17             case <-done:
18                 // get exit signal
19                 fmt.Printf("sender #%d exit\n", id)
20                 return
21             case ch <- id*1000 + i:
22             }
23         }
24     }
25 
26     recv := func() {
27         count := 0
28         for i := range ch {
29             fmt.Printf("receiver get %d\n", i)
30             count++
31             if count >= 1000 {
32                 // signal recving finish
33                 close(done)
34                 return
35             }
36         }
37     }
38 
39     wg.Add(3)
40     go send(0)
41     go send(1)
42     go send(2)
43     recv()
44 
45     wg.Wait()
46 }

4.2 多寫多讀

  這種場景稍微複雜,和上面的例子一樣,也需要設置一個額外 channel 用來通知多個寫入端和讀取端。另外需要起一個額外的協程來通過關閉這個 channel 來廣播通知:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6     "time"
 7 )
 8 
 9 func main() {
10     wg := &sync.WaitGroup{}
11     ch := make(chan int, 100)
12     done := make(chan struct{})
13 
14     send := func(id int) {
15         defer wg.Done()
16         for i := 0; ; i++ {
17             select {
18             case <-done:
19                 // get exit signal
20                 fmt.Printf("sender #%d exit\n", id)
21                 return
22             case ch <- id*1000 + i:
23             }
24         }
25     }
26 
27     recv := func(id int) {
28         defer wg.Done()
29         for {
30             select {
31             case <-done:
32                 // get exit signal
33                 fmt.Printf("receiver #%d exit\n", id)
34                 return
35             case i := <-ch:
36                 fmt.Printf("receiver #%d get %d\n", id, i)
37                 time.Sleep(time.Millisecond)
38             }
39         }
40     }
41 
42     wg.Add(6)
43     go send(0)
44     go send(1)
45     go send(2)
46     go recv(0)
47     go recv(1)
48     go recv(2)
49 
50     time.Sleep(time.Second)
51     // signal finish
52     close(done)
53     // wait all sender and receiver exit
54     wg.Wait()
55 }

  channle 作為 golang 最重要的特性,用起來還是比較方便的。傳統的 C 里要實現類似的功能的話,一般需要用到 socket 或者 FIFO 來實現,另外還要考慮數據包的完整性與併發衝突的問題,channel 則屏蔽了這些底層細節,使用者只需要考慮讀寫就可以了。 channel 是引用類型,了解一下 channel 底層的機制對更好的使用 channel 還是很用必要的。雖然操作原語簡單,但涉及到阻塞的問題,使用不當可能會造成死鎖或者無限制的協程創建最終導致進程掛掉。

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

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

電子廢棄物回收會議在菲登場 台美菲合作

摘錄自2018年9月24日中央社報導

台灣、美國與菲律賓環境暨天然資源部合作舉辦的「第8屆國際電子廢棄物回收管理夥伴會議」(The 8th International E-waste Management Network Workshop)24日在菲律賓奎松市(Quezon City)Seda Vertis大酒店開幕,為期五天,來自全球5大洲的11個夥伴國家逾50位專家學者參與,交流各國電子廢棄物管理經驗。行政院環境保護署以資源回收基金補貼業者,以此建立回收處理體系的成功經驗,在這次會議中成為各國諮詢的焦點。

環保署表示,透過多年宣揚,現在已有馬來西亞參考台灣資源回收制度,推動家用電子廢棄物回收試辦計畫,隨後也宣布將參考台灣家用電子廢棄物的管理模式及制度,成立基金管理會,是台灣環保政策輸出國外的成功案例。

這場會議,目的在提升全球電子廢棄物回收處理環境無害化的管理能力,由台灣行政院環境保護署與美國環境保護署贊助。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

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

Spring框架學習筆記(7)——Spring Boot 實現上傳和下載

最近忙着都沒時間寫博客了,做了個項目,實現了下載功能,沒用到上傳,寫這篇文章也是順便參考學習了如何實現上傳,上傳和下載做一篇筆記吧

下載

主要有下面的兩種方式:

  • 通過ResponseEntity 實現
  • 通過寫HttpServletResponse的OutputStream實現

我只測試了ResponseEntity<InputStreamResource>這種方法可行,另外一種方法請各位搜索資料。

我們在controller層中,讓某個方法返回ResponseEntity,之後,用戶打開這個url,就會直接開始下載文件

這裏,封裝了一個方法export,負責把File對象轉為ResponseEntity

public ResponseEntity<FileSystemResource> export(File file) {
    if (file == null) {
        return null;
    }
    HttpHeaders headers = new HttpHeaders();
    headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
    headers.add("Content-Disposition", "attachment; filename=" + System.currentTimeMillis() + ".xls");//以時間命名文件,防止出現文件存在的情況,根據實際情況修改,我這裡是返回一個xls文件
    headers.add("Pragma", "no-cache");
    headers.add("Expires", "0");
    headers.add("Last-Modified", new Date().toString());
    headers.add("ETag", String.valueOf(System.currentTimeMillis()));

    return ResponseEntity
            .ok()
            .headers(headers)
            .contentLength(file.length())
            .contentType(MediaType.parseMediaType("application/octet-stream"))
            .body(new FileSystemResource(file));
}

Controller

@RequestMapping("download")
public ResponseEntity<FileSystemResource> downloadFile() {
    return export(new FIle());//這裏返回調用export的結果
}

上傳

1.配置

spring boot使用上傳功能,得先進行配置,spring boot配置方式有兩種,一種是資源文件properties配置,另外一種方式則是yml配置

properties配置:

## MULTIPART (MultipartProperties)
# 開啟 multipart 上傳功能
spring.servlet.multipart.enabled=true
# 文件寫入磁盤的閾值
spring.servlet.multipart.file-size-threshold=2KB
# 最大文件大小
spring.servlet.multipart.max-file-size=200MB
# 最大請求大小
spring.servlet.multipart.max-request-size=215MB

yml配置:

spring:
    servlet:
        multipart:
          enabled: true # 開啟 multipart 上傳功能
          max-file-size: 200MB # 最大文件大小
          max-request-size: 215MB # 最大文件請求大小
          file-size-threshold: 2KB # 文件寫入磁盤的閾值

2.編寫url請求

controller

@PostMapping("/upload")
@ResponseBody
public String upload(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return "上傳失敗,請選擇文件";
    }

    String fileName = file.getOriginalFilename();
    String filePath = "/Users/itinypocket/workspace/temp/";//文件上傳到服務器的路徑,根據實際情況修改
    File dest = new File(filePath + fileName);
    try {
        file.transferTo(dest);
        LOGGER.info("上傳成功");
        return "上傳成功";
    } catch (IOException e) {
        LOGGER.error(e.toString(), e);
    }
    return "上傳失敗!";
}

3.Web頁面上傳文件

注意,input標籤的name與url的請求參數名相同,上傳只能使用post請求
單個文件上傳:

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
</form>

多個文件上傳:

input標籤加上multiple屬性,即可一次選擇多個文件

<form method="post"  action="/upload" enctype="multipart/form-data">
    <input type="file" multiple name="file"><br>
    <input type="submit" value="提交">
</form>

4.Android端上傳文件

使用okhttp上傳文件

RequestBody filebody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
RequestBody body = new MultipartBody.Builder()
        .addFormDataPart("file", file.getName(), filebody)
        .build();
Request request = new Request.Builder()
        .url("http://192.168.1.106:8080/webapp/fileUploadPage")
        .post(body)
        .build();

Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.e(TAG, "請求失敗:" + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Log.e(TAG, "請求成功!");
    }
});

參考鏈接:

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

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

世界最大單轉子潮汐能設備,裝置量突破 2MW 壽命達 25 年

摘錄自2018年10月3日科技新報報導

近日,潮汐能公司 SIMEC Atlantis Energy(SAE) 打造出世界最大、裝置量高達 2MW 的單轉子潮汐能設備 AR2000,該系統使用壽命長達 25 年,每六年進行一次檢查即可。該設備也採用濕式配對(wet-mate)系統,可快速且安全地裝設在重力結構或獨立基座上,一天最多可部署 8 座潮汐能渦輪機,開發商也可將多個渦輪機串起連接,進一步降低海底作業的成本與生態影響。

SAE 的 AR1500 潮汐能設備先前也在英國大放異彩,其中蘇格蘭全球最大的潮汐能發電計畫「MeyGen」便是出自 SAE 之手,目前 AR1500 已於 2017 年第三季末開始運行,未來 AR2000 也會在 MeyGen 發電廠服役。計畫總監 David Taaffe 更預計 2018 年該潮汐發電廠將可生產約 1.9GWh 電力。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

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

前端小白在asp.net core mvc中使用ECharts

對於在瀏覽器中繪製圖形圖表,目前有較多的js類庫可以使用,如:ChartJS,Flot,canvasjs等,但是今天介紹的主角為國產圖表庫,並在apache孵化,就是大名鼎鼎的echarts。

前方高能【官方介紹】

特性 – Apache ECharts (incubating)

ECharts,一個使用 JavaScript 實現的開源可視化庫,可以流暢的運行在 PC 和移動設備上,兼容當前絕大部分瀏覽器(IE8/9/10/11,Chrome,Firefox,Safari等),底層依賴矢量圖形庫 ZRender,提供直觀,交互豐富,可高度個性化定製的數據可視化圖表。

豐富的可視化類型

ECharts 提供了常規的折線圖、柱狀圖、散點圖、餅圖、K線圖,用於統計的盒形圖,用於地理數據可視化的地圖、熱力圖、線圖,用於關係數據可視化的關係圖、treemap、旭日圖,多維數據可視化的平行坐標,還有用於 BI 的漏斗圖,儀錶盤,並且支持圖與圖之間的混搭。

除了已經內置的包含了豐富功能的圖表,ECharts 還提供了自定義系列,只需要傳入一個renderItem函數,就可以從數據映射到任何你想要的圖形,更棒的是這些都還能和已有的交互組件結合使用而不需要操心其它事情。

你可以在下載界面下載包含所有圖表的構建文件,如果只是需要其中一兩個圖表,又嫌包含所有圖表的構建文件太大,也可以在在線構建中選擇需要的圖表類型后自定義構建。

多種數據格式無需轉換直接使用

ECharts 內置的 dataset 屬性(4.0+)支持直接傳入包括二維表,key-value 等多種格式的數據源,通過簡單的設置 encode 屬性就可以完成從數據到圖形的映射,這種方式更符合可視化的直覺,省去了大部分場景下數據轉換的步驟,而且多個組件能夠共享一份數據而不用克隆。

為了配合大數據量的展現,ECharts 還支持輸入 TypedArray 格式的數據,TypedArray 在大數據量的存儲中可以佔用更少的內存,對 GC 友好等特性也可以大幅度提升可視化應用的性能。

實踐

1、 使用VS2019創建一個asp.net core web項目,選擇ASP.NET Core Web 應用程序,並使用Web應用程序(模型視圖控制器)模板

2、使用libman添加echarts庫

在解決方案資源管理器中選擇項目名稱->wwwroot->lib,在lib文件夾上右擊,選擇:添加->客戶端庫,彈出窗口輸入echarts,自動帶出最新的版本,由於使用了cdnjs,上面最新版為4.3.0,在其他的如:jsdelivr、unpkg上面最新版為4.5.0。不過使用jsdelivr、unpkg現在4.5.0版本失敗,所以選擇了cdnjs上面的4.3.0版本。

插曲:在彈出的客戶端庫輸入內容時,如果時中文輸入法,已輸入部分文字,切換為英文輸入法時,會導致vs2019重啟,所以在客戶端庫窗口輸入內容時一定要前切換到英文輸入法

3、添加一個action,名為Echarts,並添加相應的視圖

在視圖中添加如下代碼,idmychart的div用於放置echarts圖表

<div class="row">
<div class="col-md-12">
<div id="myechart" style="width: 100%; height: 500px;">

</div>
</div>
</div>

添加相關js的引用

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/echarts/echarts.js"></script>

4、使用Echarts

準備的json數據存放到了上面:
在引用的的js庫的下方編寫js代碼使用echarts显示圖表。
首先定義了一個結構,用於存放數據。

function Series(type, xdata, seriesData) {
        this.typeName = type;//相當於title
        this.xdata = xdata;//X軸數據
        this.seriesData = seriesData;//Y軸數據
    }

聲明一個charts實例。
> echartInstance.clear
清空當前實例,會移除實例中所有的組件和圖表。清空后調用 getOption 方法返回一個{}空對象。在多次查詢時需要用到清除上次添加的內容

var myEChart = echarts.init(document.getElementById('myechart'));
myEChart.clear();//用於清除已存在的內容

定義option

    var options = {
        title: {
            text: '數據曲線:',
            x: 'center'
        },
        
        legend: {
            data: [],
            x: 'left'
        },
        
        xAxis: {
            type: 'category'//必須
        },
        yAxis: {
            type: 'value',
            max: 90,
            min: -10
        },
        series: []
    }

設置myEChart的option

myEChart.setOption(options);

獲取json數據,經過處理后並將數據显示到myEChart

    $.get('/data.json').done(function (data) {

        //var aa = JSON.parse(data);
        var x = [];
        $.each(data.List,
            function (n, value) {
                var xd = [], sd = [];
                $.each(value.Data,
                    function (m, mValue) {
                        xd.push(mValue.SaveTime);
                        sd.push(mValue.WorkStationData);
                    });
                var s = new Series(value.TypeTitleName, xd, sd);
                x.push(s);
            });

        var xStart = data.Start;
        var xEnd = data.End;
        options.xAxis.data = x[0].xdata;
        options.xAxis.start = xStart;
        options.xAxis.end = xEnd;

        $.each(x,
            function (i, value) {
                options.title.text += value.typeName + " ";
                options.legend.data.push(value.typeName);
                options.series.push({
                    name: value.typeName,
                    type: 'line',
                    smooth: true,
                    data: value.seriesData
                });
            });
        myEChart.setOption(options);

    });
說明1

此段代碼是將json數據處理后,將x,y軸的數據分開存到到數組x中

        var x = [];
        $.each(data.List,
            function (n, value) {
                var xd = [], sd = [];
                $.each(value.Data,
                    function (m, mValue) {
                        xd.push(mValue.SaveTime);
                        sd.push(mValue.WorkStationData);
                    });
                var s = new Series(value.TypeTitleName, xd, sd);
                x.push(s);
            });
說明2

對option的xAxis軸的內容進行處理

var xStart = data.Start;
var xEnd = data.End;
options.xAxis.data = x[0].xdata;
options.xAxis.start = xStart;
options.xAxis.end = xEnd;
說明3

對option的titlelegendseries軸的內容進行處理

        $.each(x,
            function (i, value) {
                options.title.text += value.typeName + " ";
                options.legend.data.push(value.typeName);
                options.series.push({
                    name: value.typeName,
                    type: 'line',
                    smooth: true,
                    data: value.seriesData
                });
            });
###### 說明4
設置`myEChart`的option

myEChart.setOption(options);


### 5、運行程序
點擊“CTRL+F5”運行程序,切換到相應的action,可以看到圖表显示如下內容,所有的數據都正確的显示了。
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121084936137-183805478.png)


到目前為止,貼出上面完整的代碼,到目前為止,你已經可以使用echarts處理圖表問題了:

### 6、功能擴展

###### 6.1 圖表左右空白較多
為了解決圖表中左右空白較多的問題,可以在options中添加如下內容:
    grid: {
        show: true,
        borderColor: '#19507c',
        x: 50,
        x2: 50,
        y: 40
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121084959477-981010092.png)

###### 6.2 鼠標滑過圖表显示當前時間的各項數據內容
上面的截圖,我們使用數據劃過圖表時,不能現在當前時間上各項數據,在options中添加如下代碼可以實現鼠標滑過显示各項數據的功能。
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'cross',
            animation: false,
            label: {
                backgroundColor: '#505765'
            }
        }
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085018478-908000477.png)


###### 6.3 添加相關功能按鈕
使用如下設置,添加了3個按鈕,分別為:區域縮放,區域縮放還原和還原。
區域縮放:可以在圖標上按住鼠標左鍵,用鼠標滑定一段區域,显示滑定區域的數據,相當於區域放大
區域縮放還原:還原到初始狀態
還原:就是還原功能
    toolbox: {
        feature: {
            dataZoom: {
                yAxisIndex: 'none'
            },
            restore: {},
            saveasImage: {}
        }
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085036835-316142579.png)


###### 6.4 區域縮放
dataZoom 組件 用於區域縮放,從而能自由關注細節的數據信息,或者概覽數據整體,或者去除離群點的影響。
    dataZoom: [
        {
            show: true,
            realtime: true,
            start: 80,
            end: 100
        },
        {
            type: 'inside',
            realtime: true,
            start: 65,
            end: 100
        }
    ],
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085050551-185052507.png)


添加完4項擴展后,完整代碼如下:

“`
給個關注:

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

Nissan夥同東風汽車,為中國開發低價純電動車

日商日產汽車(Nissan)將與中國東風汽車集團展開合作,共同為中國市場推出較現有車型便宜30%左右的低價款純電動車;新車最快會在今年夏天上市。

日產的LEAF(聆風)是目前全球銷量最佳電動車車款之一。為搶攻中國市場,日產已在2014年改良LEAF車款,推出「起辰e30」純電動車。而為進一步提高在中國市場的能見度、並降低成本,日產宣布與東風汽車合作,推出比e30便宜20~30%的新款純電動車,售價約在人民幣20萬左右。

《日經》中文網指出,日產的新款低價純電動車將增加核心零組件(包括車用電池)的在地採購量,以降低零件進口之價格、關稅、運輸費成本。採用在地供應鏈後,新款車輛的價格將與中國在地廠商的純電動車價格水準相當;部分地區扣除政府購車補貼後,實際售價更可低至每輛人民幣10~15萬元,相當於同等級的汽油車。藉著推出新款低價電動車,日產目標將市占率從2015年的2%在幾年內提高到5~10%。

中國中央政府對純電動車、PHEV等新能源車輛,最高提供人民幣5.5萬元的購車補貼;若再加上地方政府補貼金,最高可獲得人民幣11萬元補貼。在強大的政策支援下,中國新能源車的掛牌量可望從2015年的33萬輛,大幅成長至2020年的500萬輛。

(照片來源:維基共享資源)

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

特斯拉九月正式來台,首家據點落腳新光三越

全球電動車龍頭廠商特斯拉(Tesla)終於將與台灣駕駛相見歡!特斯拉已確定位於台灣的首家據點落腳台北市信義區新光三越A11館,將於今年九月正式開幕。

本土電動機車品牌Gogoro在信義區設置旗艦體驗店,並成功打響名號。全球電動車指標品牌特斯拉也選擇在信義區設立據點,將在新光三越A11一樓松壽路門面櫃位展開服務,預計將提供獨立出口、招牌與車款展示等服務。這是台灣首度有汽車品牌進駐百貨公司的黃金櫃位,櫃位空間約60坪,正在整修中。

與此同時,市場持續瘋傳特斯拉將在中國上海打造生產基地,並已與上海金僑集團簽署備忘錄,將合資建廠;但特斯拉創辦人Elon Musk表示,目前還未走到簽署備忘錄或任何文件的階段。

(照片來源:Tesla Taiwan 臉書專頁)

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

電動巴士開上阿里山!政府續推電池在地生產

知名台灣旅遊景點阿里山森林遊樂區交通運輸再突破!在地遊覽車業者加成交通引入電動巴士作為遊園車使用,可克服高海拔、高濕度、低溫等環境問題穩定運轉。

繼太魯閣、日月潭等觀光名勝之後,阿里山也於去年開始陸續採用電動巴士作為交通運輸方案,排氣與噪音汙染都能減輕。加成交通董事長鄭榮華表示,自從公司開始接觸政府標案後,陸續引入電動巴士車隊;巴士在前一晚充飽電後,白天可以行使一整天,一年多來未發生故障的問題。

經濟部:推動生產在地化

阿里山電動巴士車隊有三分之二零件於台灣在地生產,包括動力系統馬達與控制器;且成功行駛經驗證實台產電動巴士的品質穩定,未來10年內希望能在全台投入一萬輛電動巴士。

經濟部工業局智慧電動車辦公室主任王正健指出,目前電動巴士在地化生產比例已達67%,希望明年能達到70%,關鍵是電池技術的進步。電動車用電池在車輛加速時,需有瞬間大量放電的功能,與台灣所擅長的筆電、手機電池等產品性質有別。

目前,台灣已有本地業者開始生產電池模組,但電池芯多向日本、南韓、中國大陸進口,希望能推動在地研發電池芯,並進一步落實電動巴士的在地化生產。電動車搭載的電池亦可整合智慧電網,作為儲能設備來使用。

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務