Go语言网络编程:TCP、HTTP、JSON序列化、Gin、WebSocket、RPC、gRPC入门实例

Pistachiout · 收录于 2023-06-03 04:06:03 · source URL

Go语言网络编程:TCP、HTTP、Gin、WebSocket、RPC、gRPC入门实例

在本文中,我们将介绍Go语言中的网络编程的不同方式,包括TCP、HTTP、Gin框架、WebSocket、RPC、gRPC的介绍与连接实例,并对所有示例代码都给出了详细的注释,最后对每种模式进行了总结。

1. TCP网络编程

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,提供可靠的双向通信。

在Go语言中,我们可以使用net包来实现TCP编程。net 包是 Go 语言标准库中用于网络编程的包,提供了一些用于创建和操作网络连接的基本接口和函数。它支持 IP 协议簇和TCP/UDP 数据传输协议,并提供了对 DNS 查询的支持。

  • net 包中最常用的函数是 ListenDial,它们分别实现了服务端监听和客户端连接的功能。通过 Listener接口的 Accept 方法,可以接受新的连接,并返回一个表示该连接的 Conn 接口实例,
  • Conn 接口是表示网络连接的标准接口。它定义了在连接上进行数据传输的方法,包括从连接中读取数据Read()与向连接中写入数据Write(),以及关闭连接Close()等操作。

1.1 TCP服务器

以下是没有相关错误处理代码的一个简单的TCP服务器示例:

package main
import (
	"fmt"
	"io"
	"net"
)
func main() {
	// net.Listen函数创建一个TCP监听器,监听`localhost:8080`,等待连接
	listener, _ := net.Listen("tcp", "localhost:8080")
	defer listener.Close()
	for {
		// 无限循环来接受客户端连接
		conn, _ := listener.Accept()
		// 开启协程,处理连接
		go handleConnection(conn)
	}
}
func handleConnection(conn net.Conn) {
	defer conn.Close()
	for {
		// 读取数据
		buf := make([]byte, 1024)
		n, _ := conn.Read(buf)
		// 处理数据并回复客户端
		fmt.Printf("Received: %s", string(buf[:n]))
		conn.Write([]byte("Message received."))
	}
}

在这个示例中,我们首先创建了一个TCP监听器,监听localhost:8080。然后,我们使用一个无限循环来接受客户端连接。每当有新的连接时,我们将其传递给handleConnection函数处理。在这个函数中,我们读取客户端发送的数据,并将其打印到控制台,然后向客户端发送确认消息。

1.2 TCP客户端

以下是一个简单的TCP客户端示例:

package main
import (
	"fmt"
	"net"
)
func main() {
	// net.Dial函数连接 TCP 服务端
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error dialing:", err)
		return
	}
	// 延迟关闭连接
	defer conn.Close()
	// 向服务器发送数据
	conn.Write([]byte("Hello, server!"))
	// 读取从服务端返回的数据
	buf := make([]byte, 1024)
	n, _ := conn.Read(buf)
	fmt.Printf("Received: %s", string(buf[:n]))
}

在这个示例中,我们首先连接到TCP服务器,然后向服务器发送一条消息。接着,我们等待服务器的响应,并将其打印到控制台,之后会关闭连接。
若想要一直连接客户端,只需要在连接建立后用一个for循环来一直发送和接收数据即可

2. HTTP网络编程

HTTP是基于TCP的请求-响应协议,只能由客户端向服务器发送请求,并要求服务器回复响应。.

在Go语言中,我们可以使用net/http包来实现HTTP编程。 http包是Go语言中用于处理HTTP相关操作的标准库。它提供了可以发送HTTP请求和接收HTTP响应的客户端和服务器端API。

  • http.HandleFunc()函数将一个指定的函数(即HTTP请求处理函数)注册到默认的路由器上。当HTTP请求到达服务器时,默认路由器会查找与请求的URL匹配的处理函数,并调用它来处理该请求。HTTP请求处理函数的类型为func(http.ResponseWriter, *http.Request),其中第一个参数表示用于向客户端发送HTTP响应的ResponseWriter对象,而第二个参数则表示代表客户端请求的Request对象
  • http.ListenAndServe()是阻塞函数,它启动一个HTTP服务器并监听来自客户端的HTTP请求。该函数需要提供一个地址和处理HTTP请求的路由器。

综上所述,可以使用http.HandleFunc()函数将HTTP请求处理函数注册到默认路由器,并使用ListenAndServe()函数启动HTTP服务器来监听来自客户端的HTTP请求。

GO web编程实战:https://www.topgoer.cn/docs/golangweb/golangweb-1ck1kt56ksq5u
HTTP标准库介绍:https://draveness.me/golang/docs/part4-advanced/ch09-stdlib/golang-net-http/
以下是一个简单的HTTP服务器示例:

package main
import (
	"fmt"
	"net/http" // 导入http包
)
func main() {
	// 定义路由函数, 第一个参数注册了URL 路径“/”,第二个参数则是处理函数 
	//w http.ResponseWriter 是向客户端发送HTTP响应的`ResponseWriter`响应对象
	//r *http.Request 代表客户端请求的`Request`请求对象,包含了关于客户端请求的元数据信息,如请求方法、URL、请求头等
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 
		fmt.Fprintf(w, "Hello, world!") 
	})
	// 开启HTTP服务器,监听本地8080端口
	err := http.ListenAndServe(":8080", nil) 
	if err != nil {
		fmt.Println("Error starting server:", err) 
	}
}

3. JSON序列化

在这里插入图片描述
在网络编程中,我们通常需要将数据序列化为一种可传输的格式,并将其发送到远程服务器。而JSON是一种轻量级的数据交换格式,它具有易于阅读和编写、跨语言支持、轻量级等优点,因此在网络编程中被广泛使用。通过将结构体JSON序列化,我们可以将结构体转换为JSON格式的字符串,并将其发送到远程服务器。这使得我们可以在不同的系统之间传输和共享数据,而不需要考虑不同系统之间的差异。
在Go语言,我们可以使用标准库中的encoding/json包来进行JSON序列化。通过将结构体JSON序列化,我们可以将结构体换为JSON格式的字符串,并将其发送到远程服务器。然后,服务器可以将JSON字符串反序列化为结构体,并对其进行处理。
JSON介绍:https://0c526fc1-7d0a-4456-a05c-0b0261436922.topgoer.cn/docs/gopl-zh/gopl-zh-1d2a0cqm1nbe1

package main

import (
    "encoding/json"
    "fmt"
)
type Student struct {
    Name    string `json:"name"`
        int    `json:"age"`
    IsMale  bool   `json:"is_male"`
}
type Class struct {
    Id       string    `json:"id"`
    Students []Student `json:"students"`
}
func main() {
    s := Student{"张三", 18, true}
    c := Class{
        Id:       "1(2)班",
        Students: []Student{s, s, s},
    }
    bytes, _ := json.Marshal(c) //json序列化
    str := string(bytes)
    fmt.Println(str)
    var c2 Class
    json.Unmarshal(bytes, &c2)//json反序列化
}

4. Gin框架网络编程

Gin是一个用Go语言编写的Web框架,它提供了许多实用的功能,如路由、中间件、模板渲染等。要使用Gin框架,首先需要安装它并在代码中导入。

go get -u github.com/gin-gonic/gin

Gin中文文档:https://www.kancloud.cn/shuangdeyu/gin_book/949413

以下是一个简单的Gin Web应用示例:

package main
import (
	"github.com/gin-gonic/gin"
)
func main() {
	// 创建一个默认 Gin 路由器
	router := gin.Default()
	// 定义处理器函数,匹配 HTTP GET 请求,当请求的 URL 路径为"/"时,执行指定的处理函数func输出Hello, world!。
	router.GET("/", func(c *gin.Context) {
		c.String(200, "Hello, world!")
	})
	// 启动 HTTP 服务器并监听来自8080端口的请求
	router.Run(":8080")

在这个示例中,我们首先创建了一个Gin路由器,并为其添加了一个处理函数,该函数将向客户端发送“Hello, world!”消息。然后,我们使用router.Run函数启动Web应用,监听8080端口。

5. WebSocket网络编程

  • TCP/IP代表传输控制协议/网际协议,指的是系列协议族,需要IP协议来连接网络,TCP是-种允许我们安全传输数据的机制,
  • HTTP基于TCP协议,是从Web服务器传输超文本到本地浏览器的传送协议。
  • Socket是TCP/IP网络的API, 其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。对用户来说,一组简单的接口就是全部,让Socket去组织数据, 以符合指定的协议。
  • WebScoket是基于Socket的全双工通信应用层协议,使用单个TCP连接来进行全双工通信,定义了特定的握手过程,以确保客户端和服务端可以互相识别,并开始进行数据传输,通常用于在Web应用程序和服务端之间进行实时数据传输。

WebSocket基于HTTP协议,但是它是一种完全不同的协议。在客户端和服务端首次进行连接时,建立连接时需要进行一次HTTP握手,WebSocket会通过HTTP请求发送一个Upgrade头,以升级连接协议。如果服务器同意进行升级,客户端和服务端之间就可以建立WebSocket连接,开始进行实时数据传输。因此,我们可以认为WebSocket是基于HTTP的,在HTTP协议上进行了改进和扩展,实现了基于浏览器的远程socket,使浏览器和服务器可以进行全双工通信。

与HTTP不同,WebSocket是一种持久化的连接,因此可以用于实时数据传输,比如即时聊天、实时位置追踪等,它使得浏览器和服务器之间可以进行实时通信。WebSocket协议通过HTTP协议的升级实现,客户端和服务器之间的通信是基于消息的,可以发送文本、二进制数据等。

它解决了Web实时化的问题,相比传统HTTP有如下好处:

  • 一个Web客户端只建立一个TCP连接
  • Websocket服务端可以推送(push)数据到web客户端.
  • 有更加轻量级的头,减少数据传送量

WebSocket介绍:https://www.topgoer.cn/docs/golangweb/golangweb-1ck1l3rhnfn0l

在Go语言中,我们可以使用github.com/gorilla/websocket包来实现WebSocket编程。以下是一个简单的WebSocket服务器示例:

package main

import (
	"fmt"
	"net/http"
	"github.com/gorilla/websocket"
)

func main() {
	// 定义WebSocket请求处理函数
	upgrader := websocket.Upgrader{}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// 通过Upgrader升级HTTP连接为WebSocket连接
		conn, _ := upgrader.Upgrade(w, r, nil)
		// 向客户端发送消息
		conn.WriteMessage(websocket.TextMessage, []byte("Hello, client!"))
		// 接收客户端消息并打印
		_, msg, _ := conn.ReadMessage()
		fmt.Printf("Received message: %s\n", string(msg))
	})

	// 启动Web服务器并监听端口
	err := http.ListenAndServe(":8080", nil)
	fmt.Println(err)
}

该示例代码中,通过使用github.com/gorilla/websocket包提供的Upgrader结构体将HTTP连接升级为WebSocket连接。然后向客户端发送一条文本消息,并接收客户端返回的消息,其余操作与http基本一致。

6. RPC网络编程

Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。

但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。RPC就是想实现函数调用模式的网络化,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。

在 Go 语言中,使用 net/rpc 包可以实现 RPC(Remote Procedure Call)远程过程调用。RPC 的核心思想是让应用程序像调用本地函数一样调用远程函数,并且可以在不同的机器上运行。下面介绍一些经常用到的函数和概念:

  1. rpc.Register():将指定的对象中所包含的方法注册到 RPC-default-server 中。调用该函数需要传入实现了这些方法的对象,将方法注册到注册中心之后,客户端就可以通过服务名和函数名来调用这些方法了。
    这些方法必须满足一定的条件才能被注册成功,如下:func (t *T) MethodName(argType T1, replyType *T2) error
    • 函数必须是导出的(首字母大写)
    • 必须有两个导出类型的参数,第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
    • 函数还要有一个返回值error
  2. 句柄(handle):句柄是一个代理客户端对象,通过它可以向服务器发送函数调用请求,并接收响应结果。通过 net/rpc 包的 Dial() 方法可以创建一个连接到指定网络地址的 RPC.Client 对象,该对象即为代理客户端对象。
  3. Call():该方法用于向 RPC 服务器发起函数调用请求。其中需要注意三个参数:函数名,在服务器上的参数列表,在服务器上返回结果的指针。RPC 客户端通过调用该函数来向服务器发起函数调用请求,并将参数、函数名等信息传递给 RPC 服务器。服务器执行完相应的操作后,将计算结果返回客户端并存储在指定的指针中。

Go RPC开发指南文档:https://www.topgoer.cn/docs/rpc/rpc-1c5ck8luqjf3l

6.1 服务端

以下是使用Go语言建立RPC连接的示例代码:

package main

import (
	"fmt"
	"net"
	"net/rpc"
)
// 定义数据结构Args,表示两个整数相乘的乘数和被乘数
type Args struct {
	A, B int
}
// 定义数据结构Arith,表示支持对Args类型进行RPC调用的服务端
type Arith struct{}

// 为Arith服务端定义方法Multiply,用于实现两个数相乘的逻辑
func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}
func main() {
	// 创建一个新的Arith对象
	arith := new(Arith)
	// 通过rpc.Register将arith注册到RPC服务器中
	rpc.Register(arith)
	// 监听TCP网络地址,等待客户端连接
	l, _:= net.Listen("tcp", ":1234")
	// 程序结束前关闭监听
	defer l.Close()
	for {
		// 接收客户端连接请求,并将连接作为参数调用RPC服务器
		conn, _ := l.Accept()
		go rpc.ServeConn(conn)
	}
}

该示例代码中创建了一个名称为Arith的RPC服务,其中包含一个名为Multiply的函数。然后将该服务注册到默认的RPC服务器中,并通过TCP在端口号为1234的地址上启动RPC服务器,等待客户端连接。当有客户端连接时,将会创建一个新的goroutine来处理该客户端的请求。

6.2客户端:

package main

import (
	"fmt"
	"net/rpc"
)
type Args struct {
	A, B int
}
func main() {
	// 连接RPC服务器
	client, _ := rpc.Dial("tcp", "localhost:1234")
	args := Args{7, 8}
	var reply int
	// 通过client调用远程的"Arith.Multiply"函数,并传递参数args,将结果存储于reply中
	client.Call("Arith.Multiply", args, &reply)
	fmt.Printf("Multiply: %d * %d = %d", args.A, args.B, reply)
	client.Close()
}

该代码使用 Args 结构体来传递参数,并且只定义了一个变量 reply 来存储相乘的结果。在客户端调用远程服务端的 Arith.Multiply 函数时,将 args 和 &reply 作为参数传递给 Call 方法,这样远程函数就可以计算它们的积并将结果存储到 reply 变量中。最后输出计算的结果,并关闭连接。

7. gRPC网络编程

gRPC和传统的RPC都是实现远程过程调用的协议。它们的主要区别在于采用不同的传输协议和数据格式。

  • gRPC使用HTTP/2作为底层协议,支持多路复用和流控制等功能,并使用Protocol Buffers(一种语言中立、平台中立、可扩展且高效的序列化机制)作为默认的数据编解码协议。相比之下,传统的RPC协议通常使用TCP或UDP进行通信,而数据格式则更加灵活多样。
  • gRPC 支持多种编程语言,且 gRPC客户端和服务端可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言写客户端调用,即服务端与客户端可使用不同语言环境。
  • gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是微服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers,当然也可以使用其他数据格式如JSON。可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型。

gRPC文档:https://www.topgoer.cn/docs/grpc/grpc-1d2ud3fh1d74h

7.1 gRPC 服务端和客户端示例:

服务端代码:

package main
import (
	"context"
	"fmt"
	"net"
	"google.golang.org/grpc"
	//引用 hello-grpc 项目中定义的 Protocol Buffers(protobuf)协议。
	pb "github.com/user/hello-grpc/hello"
)
// 定义服务
type HelloServer struct{} 
// 实现SayHello函数
func (s *HelloServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{Message: "Hello " + in.Name}, nil
}
func main() {
	// 创建grpc监听器
	lis, _ := net.Listen("tcp", ":50051")
	// 创建grpc服务器		
	grpcServer := grpc.NewServer()
	// 注册HelloServer服务
	pb.RegisterHelloServer(grpcServer, &HelloServer{})
	// 启动服务
	if err := grpcServer.Serve(lis); err != nil {
		fmt.Printf("failed to serve: %s\n", err)
	}
}

客户端代码:

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	pb "github.com/user/hello-grpc/hello"
)

func main() {
	// 连接GRPC服务
	conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
	defer conn.Close()
	// 创建GRPC客户端	
	client := pb.NewHelloClient(conn)
	// 输入参数
	req := &pb.HelloRequest{Name: "gRPC"}
	// 调用远程函数,获取响应结果	
	resp, _ := client.SayHello(context.Background(), req)
	fmt.Printf(resp.Message)
}

上述代码中服务端注册了 HelloServer 结构体下的 SayHello() 方法。当客户端通过会话连接调用该方法时,服务端将依据其输入值返回一个包含欢迎信息的响应。客户端向服务端发起请求和调用服务,通过创建grpc和protobuf协议的客户端/服务器端代码。同时标准化定义protobuf文件或proto文件,并生成相关代码。通过这样的方式我们就能够像调用本地函数一样来调用在不同语言和平台之间运行的远程函数,极大方便了分布式系统的开发与部署。

7.2 protobuf协议介绍

上述服务端代码的pb "github.com/user/hello-grpc/hello" 是引用 hello-grpc 项目中定义的 Protocol Buffers(protobuf)协议。

在 RPC 通信过程中,客户端和服务器之间需要传输数据。 protobuf 则是一种序列化/反序列化协议,能够将结构化数据编码为字节流进行传输,并能够将接收到的字节流转换回原始数据类型。使用 protobuf 协议,可以在不同语言和平台之间轻松传输结构化数据。

在 gRPC 中,将定义服务所使用的 protobuf 文件,然后通过预处理工具将其转换为不同编程语言所对应的源代码,然后再使用生成的代码来实现服务端和客户端程序。

hello-grpc项目中定义的 Protocol Buffers(protobuf)协议是用于定义 SayHello 服务接口的。

hello.proto文件定义如下:

syntax = "proto3";  // 设置使用的协议版本

package hello;      // 设置包名

service Hello {      // 定义服务,类比接口,入参HelloRequest ,出餐HelloResponse ,
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {     // 定义请求消息,类比struct,使用驼峰命名规则
    string name = 1;
}
message HelloResponse {    // 定义响应消息
    string message = 1;
}

上述代码中,syntax指定使用的 Protocol Buffers 协议版本为 3。package 指定了协议所属的包名,使用 “.” 连接不同的包名,可以生成多个组成层级关系的包。service 定义了具体的服务,在本例中为 Hello。其中的 rpc 关键字定义了每个服务 api 的名称及其输入输出参数类型。message 分别定义了请求消息和响应消息,在本例中分别为 HelloRequestHelloResponse

而在 Go 语言中,可以使用 protoc 工具将 protobuf 文件转换为 Go 代码。通过执行以下命令可以将 hello.proto 转换为 Go 代码:

protoc -I . hello.proto --go_out=plugins=grpc:.

生成的 Go 代码类似于以下内容:

// HelloMessage 是消息定义
type HelloMessage struct {
	Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

// HelloResponse 是响应消息定义
type HelloResponse struct {
	Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}

// 定义 Hello 服务接口
type HelloServer interface {
	// SayHello 是一个简单的RPC方法
	SayHello(context.Context, *HelloMessage) (*HelloResponse, error)
}

上述代码展示了通过 protobuf 协议定义的服务接口中所定义的请求和响应消息结构体,以及在 HelloServer 接口中声明的函数。这些 Go 结构体和函数的实现将会由 protoc 命令生成的代码实现。在实际使用时,我们只需要导入对应的包,并在自己的程序中实现服务接口即可。

8.总结

  1. TCP:TCP 是一种可靠的传输控制协议,在 Go 中可以通过 net 包轻松实现 TCP 通信。可以使用 net.Listen() 创建服务端,使用 net.Dial() 创建客户端,并调用 conn.Read()conn.Write() 进行数据读写。

  2. HTTP:HTTP 是应用最广泛的网络协议之一,Go 标准库提供了强大而稳定的 HTTP 相关库函数,通过 http.NewServeMux() 可以创建路由器,通过 http.HandleFunc() 注册请求处理函数,通过 http.ListenAndServe() 启动服务器。

  3. JSON序列化:在Go语言中,我们可以使用标库中的encoding/json包来进行JSON序列化和反序列化。序列化是将数据结构转为一种可传输格式,JSON格式的字符串。在Go语言中,我们可以使用json.Marshal()函数将结体序列化为格式的字符串。反序列化将序列化后的数据结构转换回原始的数据结构。在Go语言中,我们可以使用json.Unmarshal()函数将JSON格式的字符串反序列化为结构体。

  4. Gin: Gin 是一个高性能、易扩展的 Web 框架,具有快速的处理速度,强大的中间件支持,使用简单。在使用 Gin 框架时,首先需要通过 gin.Default() 创建路由器,然后通过 router.GET()router.POST() 加入若干处理函数。可以使用 router.Run() 启动服务器。

  5. WebSocket:WebSocket 是一种全双工通信协议,在 Go 中使用 github.com/gorilla/websocket 包可以方便地实现 WebSocket 通信。在服务端中,通过 Upgrader.Upgrade() 将 HTTP 连接从长连接升级为 WebSocket 连接;在客户端中,通过 websocket.Dial() 创建 WebSocket 连接,通过 conn.ReadMessage()conn.WriteMessage() 进行消息读写。

  6. RPC:RPC 是远程过程调用的缩写,在 Go 中自带了一个简单但是功能完善的 RPC 实现,即 net/rpc 包。可以通过 rpc.Register() 注册服务对象,通过 rpc.Dial() 连接到服务端,然后通过 client.Call() 调用指定方法。

  7. gRPC:gRPC 是基于 Protocol Buffers 的高性能、开源的 RPC 框架,可以支持多种编程语言。在 Go 中可以通过 google.golang.org/grpc 包来实现 gRPC 服务端和客户端。可以先定义 .proto 文件,然后通过 protoc 工具生成代码和数据结构,并使用生成的代码和数据结构实现服务端和客户端程序。