Hertz 学习笔记(12)

308 阅读3分钟

今天学流的 I/O

hertz server 的流读/写示例

流式读

/*
 * Copyright 2022 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"context"
	"io/ioutil"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"), server.WithStreamBody(true))

	h.POST("/bodyStream", handler)

	h.Spin()
}

func handler(ctx context.Context, c *app.RequestContext) {
	// Acquire body streaming
	bodyStream := c.RequestBodyStream()
	// Read half of body bytes
	p := make([]byte, c.Request.Header.ContentLength()/2)
	r, err := bodyStream.Read(p)
	if err != nil {
		panic(err)
	}
	left, _ := ioutil.ReadAll(bodyStream)
	c.String(consts.StatusOK, "bytes streaming_read: %d\nbytes left: %d\n", r, len(left))
}

这个代码要运行需要配合客户端的代码(此处略过客户端代码):

go run streaming/streaming_read/main.go

这个代码仔细看可以发现每次读 request 的一半,然后值得借鉴的是这里把 POST 请求的逻辑写到外面了。

流式写

/*
 * Copyright 2022 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"bytes"
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"), server.WithStreamBody(true))

	h.GET("/streamWrite", handler1)

	// Demo: synchronized reading and writing
	h.GET("/syncWrite", handler2)

	h.Spin()
}

func handler1(ctx context.Context, c *app.RequestContext) {
	bs := []byte("hello, hertz!")
	wb := bytes.NewBuffer(bs)
	c.SetBodyStream(wb, len(bs))
}

func handler2(ctx context.Context, c *app.RequestContext) {
	rw := newChunkReader()
	// Content-Length may be negative:
	// -1 means Transfer-Encoding: chunked.
	// -2 means Transfer-Encoding: identity.
	c.SetBodyStream(rw, -1)

	go func() {
		for i := 1; i < 1000; i++ {
			// For each streaming_write, the upload_file prints
			rw.Write([]byte(fmt.Sprintf("===%d===\n", i)))
			fmt.Println(i)
			time.Sleep(100 * time.Millisecond)
		}
		rw.Close()
	}()

	go func() {
		<-c.Finished()
		fmt.Println("request process end")
	}()
}

type ChunkReader struct {
	rw  bytes.Buffer
	w2r chan struct{}
	r2w chan struct{}
}

func newChunkReader() *ChunkReader {
	var rw bytes.Buffer
	w2r := make(chan struct{})
	r2w := make(chan struct{})
	cr := &ChunkReader{rw, w2r, r2w}
	return cr
}

var closeOnce = new(sync.Once)

func (cr *ChunkReader) Read(p []byte) (n int, err error) {
	for {
		_, ok := <-cr.w2r
		if !ok {
			closeOnce.Do(func() {
				close(cr.r2w)
			})
			n, err = cr.rw.Read(p)
			return
		}

		n, err = cr.rw.Read(p)

		cr.r2w <- struct{}{}

		if n == 0 {
			continue
		}
		return
	}
}

func (cr *ChunkReader) Write(p []byte) (n int, err error) {
	n, err = cr.rw.Write(p)
	cr.w2r <- struct{}{}
	<-cr.r2w
	return
}

func (cr *ChunkReader) Close() {
	close(cr.w2r)
}

要运行还是分服务端和客户端,第一个终端跑服务端代码:

go run streaming/streaming_write/main.go

第二个终端跑客户端代码:

go run client/streaming_read/main.go

这段代码里有两个 GET 请求,第一个是服务端纯写客户端纯读,第二个是同时读写。纯写这个没啥好讲的,看同时读写的这个例子。首先定义一个结构体 ChunkReader,里面 rw 代表一块 buffer,然后两个 channel w2rr2w 控制当前是读还是写。

读的逻辑:一个死循环里,首先等 w2r 的信号。如果没等到这个信号,就关一次 r2w,直接调用 buffer 的读方法然后返回,这里就跳出死循环了,说明此时应该是没有写的东西了。如果等到了这个信号,调用 buffer 的读方法,然后发 r2w 的信号,只要 n 不是 0 就继续死循环。

写的逻辑:直接调用 buffer 的写方法,发 w2r 的信号,然后再等 r2w 的信号。写的逻辑里没有死循环是因为在之前的 GET 请求里就处在一个循环内了。

这个地方的读写逻辑不对称这样容易分辨,整个思路顺下来感觉这里的同时读写实际上读的是写的内容,这样发的时候能看到自己发的东西对不对,便于实时监测。感觉只要是高速的应用总会有类似的结构,单片机和 FPGA 里面有双向 I/O 口,网络设备(七层架构里面的物理层)里面绝对会用到,发送数据的同时也需要自己看到自己发出去的数据格式对不对。找一张网上的截图,一看就通了:

image.png

也可能是我理解错了,这里给自己挖个坑,如果错了评论区帮帮忙纠正一下,我改成对的,以防误导之后和我一样的小白。