likes
comments
collection
share

go 网络编程

作者站长头像
站长
· 阅读数 31

TCP

go 网络编程中,用的最多的包是 net 使用 net.Listen 创建一个 tcp 服务,使用 net.Dial 创建一个 tcp 客户端

tcp 是流传输协议,是面向连接的,是可靠的,是有序的,是基于字节流的,是全双工的

server

先来写 server 端的代码,使用 net.Listen 创建一个 tcp 服务,服务地址为 127.0.0.1:8080

每当有一个新的连接进来,就创建一个创建一个 conn,然后可以通过这个连接进行数据的读写

我们把这数据的读写 handleConnection 放到一个 goroutine 中,这样就可以同时处理多个连接。每个连接创建一个线程是比较耗费资源的,但是每个连接创建一个 goroutine 对与资源的消耗其实还好

handlerConnection 中,数据读取使用 conn.Read,数据写入使用 conn.Write,这两个方法都是阻塞的,如果没有数据读取,就会一直阻塞在这里,直到有数据读取

由于 tcp 是基于字节流的,我们并不知道读取的数据的长度,所以 conn.Readconn.Write 返回的都是字节数

tcp 读取的数据是字节,写入的数据也是字节,就需要创建一个 byte 的切片,然后读取到的数据放入到这个切片,将写入的数据放到这个切片

func main() {
  listen, err := net.Listen("tcp", "127.0.0.1:8080")
  defer listen.Close()
  if err != nil {
    panic(err)
  }
  for {
    conn, err := listen.Accept()
    if err != nil {
      panic(err)
    }
    go handleConnection(conn)
  }
}

func handleConnection(conn net.Conn) {
  defer conn.Close()

  buf := make([]byte, 1024)
  n, err := conn.Read(buf)
  if err != nil {
    fmt.Println("read error", err.Error())
  }

  // 由于 tcp 是基于字节流的,所以读取的数据是字节,我们并不知道读取的数据的长度,所以需要使用 n 来获取读取的数据长度
  fmt.Println("read data: ", string(buf[:n]))

  _, err = conn.Write([]byte(buf[:n]))
  if err != nil {
    fmt.Println("write error", err.Error())
  }
}

client

server 端代码准备好之后,就开始写 client 端的代码,使用 net.Dial 创建一个 tcp 客户端,连接到 tcp 服务地址

然后通过 conn.Write 写入数据,通过 conn.Read 读取数据,客户端 conn.Readconn.Write 是不阻塞的

在读取或者写入数据时,需要创建一个 byte 的切片,然后将数据写入到这个切片,读取的数据也是放到这个切片

func main() {
  conn, err := net.Dial("tcp", "127.0.0.1:8080")
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  _, err = conn.Write([]byte("hello world"))
  if err != nil {
    panic(err)
  }

  buf := make([]byte, 1024)
  n, err := conn.Read(buf)
  if err != nil {
    panic(err)
  }
  fmt.Printf("read data: %v\n", string(buf[:n]))
}

启动服务

先启动 server 服务,再启动 client 服务,我就可以看到:

  • client 端输出 read data: hello world
  • server 端输出 read data: hello world

问题

我们对上面 client 端的代码进行两次写入,再次启动 client 服务,代码如下

_, err = conn.Write([]byte("hello world"))
if err != nil {
  panic(err)
}
_, err = conn.Write([]byte("hello world"))
if err != nil {
  panic(err)
}

我们会看到 server 端输出的数据是:read data: hello worldhello world

为什么会这样呢?

这是因为 tcp 是流传输,每次写和读之间没有明确的分割,所以 server 端读取的数据是两次写入的数据的拼接

这个时候就需要我们自己对数据进行一些切分,我们可以自己定义每一个数据包之间的的分割符是啥,这里用 \n 来表示

数据包之间用 \n 区分

client 端的代码中,我们在每次写入数据的后面加上 \n,这样 server 端就可以通过 \n 来切分数据包,代码如下

_, err = conn.Write([]byte("hello world\n"))
if err != nil {
  panic(err)
}
_, err = conn.Write([]byte("hello world\n"))
if err != nil {
  panic(err)
}

server 端读取数据 conn.Read 一次读取就能把所有的数据都能读出来,用 strings.Split 来切分数据包,然后遍历数据包,再写入数据

func handleConnection(conn net.Conn) {
  defer conn.Close()

  buf := make([]byte, 1024)
  n, err := conn.Read(buf)
  if err != nil {
    fmt.Println("read error", err.Error())
  }

  packs := strings.Split(string(buf[:n]), "\n")

  for _, s := range packs {
    n, err := conn.Write([]byte(s + "\n"))
    fmt.Println(string(s), n)
    if err != nil {
      fmt.Println("write error", err.Error())
    }
  }
}

然后启动服务运行时会发现,client 传输两次数据,server 端也会返回两次数据,但是 client 接收到的数据,有时候是一条,有时候又是两条

这是因为 client 也是只读取了一次,当在读取的时候,第二次的数据还没有过来,所以就只能读到一条数据

解决这个问题就是在读取数据包时按行读取,使用 bufio.NewReader 创建一个 reader,然后使用 reader.ReadLine()

server 端代码如下:

func handleConnection(conn net.Conn) {
  defer conn.Close()
  defer func() {
    e := recover()
    if e != nil {
      fmt.Printf("recovery panic: %v\n", e)
    }
  }()

  // conn 是一个 io.Reader,所以可以使用 bufio.NewReader 创建一个 reader
  reader := bufio.NewReader(conn)
  for {
    line, _, err := reader.ReadLine()
    fmt.Printf("read data: %v\n", string(line))
    if err != nil {
      if err == io.EOF {
        break
      }
      panic(err)
    }
    _, err = conn.Write([]byte(string(line) + "\n"))
    if err != nil {
      panic(err)
    }
  }
}

client 端代码如下:

func main() {
  conn, err := net.Dial("tcp", "127.0.0.1:8080")
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  _, err = conn.Write([]byte("hello world\n"))
  if err != nil {
    panic(err)
  }

  _, err = conn.Write([]byte("hello world\n"))
  if err != nil {
    panic(err)
  }

  // conn 是一个 io.Reader,所以可以使用 bufio.NewReader 创建一个 reader
  reader := bufio.NewReader(conn)
  for {
    line, _, err := reader.ReadLine()
    if err != nil {
      if err == io.EOF {
        break
      }
      panic(err)
    }
    fmt.Printf("response data: %v\n", string(line))
  }
}

使用数据长度进行包拆分

tcp 是面向流传输的,我们不知道两次请求之间的间隙是啥,就需要加一些标识,比如:\n

我们还可以自己定义一些数据包,比如在每个数据包前写入一个 int64 的数字

把一个数字加到 byte 中,代码如下:

byteSlice := make([]byte, 8)
binary.BigEndian.PutUint64(byteSlice, uint64(len([]byte(input))))
conn.Write(byteSlice)

然后在写入要传输的数据,这里就不要换行符了,代码如下:

conn.Write([]byte("hello world"))

server 端读取数据时,先读取 int64 的数字,然后再读取数据,代码如下:

// 先读取数据长度
reqDataLengthSlice := make([]byte, 8)
_, err := conn.Read(reqDataLengthSlice)

// 读实际的内容
dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)
dataSlice := make([]byte, dataLength)
_, err = conn.Read(dataSlice)

client 端完整代码:

func main() {
  conn, err := net.Dial("tcp", "127.0.0.1:8080")
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  scanner := bufio.NewScanner(os.Stdin)
  for scanner.Scan() {
    input := scanner.Text()
    if input == "EOF" {
      break
    }

    byteSlice := make([]byte, 8)
    binary.BigEndian.PutUint64(byteSlice, uint64(len([]byte(input))))
    _, err := conn.Write(byteSlice)
    if err != nil {
      panic(err)
    }

    _, err = conn.Write([]byte(input))
    if err != nil {
      panic(err)
    }

    // 读一个标志
    reqDataLengthSlice := make([]byte, 8)
    _, err = conn.Read(reqDataLengthSlice)
    if err != nil {
      if err == io.EOF {
        break
      }
      panic(err)
    }

    // 读实际的内容
    dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)

    dataSlice := make([]byte, dataLength)
    _, err = conn.Read(dataSlice)
    if err != nil {
      panic(err)
    }

    fmt.Printf("response data: %v\n", string(dataSlice))
  }
}

server 端完整代码:

func main() {
  listen, err := net.Listen("tcp", "127.0.0.1:8080")
  defer listen.Close()
  if err != nil {
    panic(err)
  }
  for {
    conn, err := listen.Accept()
    if err != nil {
      panic(err)
    }
    go handleConnection(conn)
  }
}
func handleConnection(conn net.Conn) {
  defer conn.Close()
  defer func() {
    e := recover()
    if e != nil {
      fmt.Printf("recovery panic: %v\n", e)
    }
  }()

  for {
    // 读一个标志
    reqDataLengthSlice := make([]byte, 8)
    _, err := conn.Read(reqDataLengthSlice)
    if err != nil {
      if err == io.EOF {
        break
      }
      panic(err)
    }

    // 读实际的内容
    dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)
    dataSlice := make([]byte, dataLength)
    _, err = conn.Read(dataSlice)
    if err != nil {
      panic(err)
    }

    fmt.Printf("read data: %v\n", string(dataSlice))

    // 写一个标志
    byteSlice := make([]byte, 8)
    binary.BigEndian.PutUint64(byteSlice, uint64(len(byteSlice)))
    _, err = conn.Write(byteSlice)
    if err != nil {
      panic(err)
    }

    _, err = conn.Write(dataSlice)
    if err != nil {
      panic(err)
    }
  }
}

UDP

建立 upd 连接,可以使用 net.ListenUDP,创建一个 udp 服务

udp 是无连接的,所以不需要 Accept,直接读取数据就可以了

读取数据使用 listen.ReadFromUDP,它会返回三个值,分别是:

  • n:数据字节数
  • addr:数据来源的地址
  • err:错误信息

写入数据使用 listen.WriteToUDP,如果传输的数据太长,我们可以使用 goroutine 去处理,这样就可以同时处理多个请求

server 完整代码如下:

func main() {
  listen, err := net.ListenUDP("udp", &net.UDPAddr{
    IP:   net.ParseIP("127.0.0.1"),
    Port: 8080,
  })
  defer listen.Close()
  if err != nil {
    panic(err)
  }
  for {
    data := make([]byte, 1024)
    n, addr, err := listen.ReadFromUDP(data)

    fmt.Printf("read data: %v", string(data[:n]))

    go func() {
      _, err = listen.WriteToUDP([]byte(string(data[:n])), addr)
      if err != nil {
        panic(err)
      }
    }()
  }
}

客户端连接服务,可以使用 net.DialUDP,它会返回一个连接 conn,但这个连接只是一个逻辑上的连接,帮助我们更方便的进行数据的读写

ReadFromUDPRead 区别是,ReadFromUDP 会返回一个 AddrRead 不会返回 Addr

client 完整代码如下:

func main() {
  conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
    IP:   net.ParseIP("127.0.0.1"),
    Port: 8080,
  })
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  scanner := bufio.NewScanner(os.Stdin)
  for scanner.Scan() {
    input := scanner.Text()
    if input == "EOF" {
      break
    }

    n, err := conn.Write([]byte(input))
    if err != nil {
      panic(err)
    }

    resp := make([]byte, 1024)
    n, err = conn.Read(resp)
    if err != nil {
      panic(err)
    }

    fmt.Printf("response data: %v\n", string(resp[:n]))
  }
}

http

http 服务是比较简单的,使用 http.ListenAndServe 创建一个 http 服务,然后使用 http.HandleFunc 注册一个路由

server 端代码:

func main() {
  http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
    writer.Write([]byte("pong"))
  })
  http.ListenAndServe(":8080", nil)
}

客户端使用 http.Get 或者 http.Post 请求服务端

client 端代码:

func main() {
  resp, err := http.Get("http://127.0.0.1:8080/ping")
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()

  if resp.StatusCode != 200 {
    fmt.Println("get failed, status code: " + strconv.Itoa(resp.StatusCode))
    return
  }

  respData, err := io.ReadAll(resp.Body)
  if err != nil {
    fmt.Println("read data failed, err: %v\n", err)
  }
  fmt.Println(string(respData))
}

路由匹配

go 自带的 http 服务,是不区分 Method 也就是说不区分 Get 还是 Post

路由: /ping
请求: /ping/xxx
不会匹配到 /ping 路由
路由: /ping/
请求: /ping/xxxx
会匹配到 /ping/ 路由
路由: /ping
路由: /ping/
路由: /ping/xxx
请求: /ping/xxx
会匹配到 /ping/xxx 路由

请求分发

http.HandleFunc 可以注册多个路由,然后根据请求的 Method 来分发请求,代码如下

func main() {
  http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) {
    switch request.Method {
    case http.MethodGet:
      getUser(writer, request)
    case http.MethodPost:
      createUser(writer, request)
    }
  })
  http.ListenAndServe(":8080", nil)
}

func getUser(writer http.ResponseWriter, request *http.Request) {
  resp, _ := json.Marshal(map[string]interface{}{
    "code": 200,
    "data": map[string]interface{}{
      "name": "uccs",
      "age":  18,
    },
  })
  writer.Write(resp)
}
func createUser(writer http.ResponseWriter, request *http.Request) {
  resp, _ := json.Marshal(map[string]interface{}{
    "code": 200,
  })
  writer.Write(resp)
}

设置 Response Header

writer 是一个 http.ResponseWriter,可以设置 Header,代码如下:

func getUser(writer http.ResponseWriter, request *http.Request) {
  writer.Header().Set("Content-Type", "application/json; charset=utf-8")

  writer.Write(resp)
}

设置 http 状态码:

writer.WriteHeader(http.StatusOK)

要注意的是,Header 必须在 Write 之前设置,否则设置的 Header 不会生效

获取 query 参数

获取 query 参数,可以通过 request.URL.Query().Get 方法,代码如下:

func getUser(writer http.ResponseWriter, request *http.Request) {
  userId := request.URL.Query().Get("user_id")
}

获取 form 参数

获取 form 参数,首先需要使用 request.ParseForm 方法,解析参数,然后使用 request.PostForm.Get 方法获取参数,代码如下:

  • 也可以使用 request.Form.Get 方法获取参数
func createUser(writer http.ResponseWriter, request *http.Request) {
  err := request.ParseForm()
  if err != nil {
    writer.WriteHeader(http.StatusBadRequest)
    return
  }
  name := request.PostForm.Get("name")
  resp, _ := json.Marshal(map[string]interface{}{
    "code": 200,
    "data": map[string]interface{}{
      "name": name,
    },
  })
  writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  writer.Write(resp)
}

获取 body 参数

获取请求中的 body 参数,可以使用 io.ReadAll 方法,读取 request.Body,然后在使用 json.Unmarshal 的方法将请求体给解析出来,代码如下:

func createUser(writer http.ResponseWriter, request *http.Request) {
  body, err := io.ReadAll(request.Body)
  if err != nil {
    writer.WriteHeader(http.StatusBadRequest)
    return
  }

  user := struct {
    Name string `json:"name"`
    Age  int64  `json:"age"`
  }{}

  if err := json.Unmarshal(body, &user); err != nil {
    writer.WriteHeader(http.StatusBadRequest)
    return
  }

  resp, _ := json.Marshal(map[string]interface{}{
    "code": 200,
    "data": user,
  })
  writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  writer.Write(resp)
}
转载自:https://juejin.cn/post/7372235558222037033
评论
请登录