早期 Go 语言不使用 go module 进行包管理,而是使用 go path 进行包管理,这种管理方式十分老旧,两者最显著的区别就是:Go Path 创建之后没有 go.mod 文件被创建出来,而 go module 模式会创建出一个 go.mod 文件用于管理包信息
现在就是:尽量使用 Go Modules 模式
另外,我们在引入包的时候,可以先进行 import 再通过编译器来下载内容,这样能让我们更简便的处理包关系
命名首字母大写 被视为 Public,小写被视为 Private
包名和目录名应为小写,不带有下划线与驼峰
文件名应为全小写,其中可能带有的多个单词以下划线分隔
对于接口的命名,我们应该在末尾加上 ‘er’ 来标注
常量使用全大写命名,中间使用下划线隔开
Go 语言中的包分为三种:Go 语言自带的标准库中的包、第三方包、自己写的包
rpc 是指 remote procedure call 也就是远程节点调用,其实就是一个节点调用另一个节点
这之中最关键的三个问题是:Call的id映射、序列化与反序列化、网络传输
Call id 映射问题解决的是:系统A的程序想要远程调用系统B的程序时,B中有许多个程序,到底调用哪个程序的问题,也就是说,B系统中的每个程序都具有一个唯一 id,只要其他系统在发起远程调用时携带自己要调用程序的 Call id,系统B就能成功识别他想要运行的程序
我们的调用逻辑是:
将传递的参数使用 json 协议进行传输(类似的协议还有 xml、protobuf、msgpack)另外现在网络调用有两个端:客户端用于发送数据,服务器端用于接收数据
另外一个问题就是:JSON 不是一个高性能的编码协议,我们在追求极致性能的时候可能不会优先考虑 json
另外:json 的优势在于其通用性,可扩展性,几乎所有的系统都支持 json,但其另外的问题就是其过于灵活,不能将其作为程序的对象存储来代替struct
而我们的网络协议是看不懂 struct 的,其只能识别二进制的流,故而我们必须将我们转换的数据转换成二进制的流才可以进行传输
在一次 RPC 过程中,服务器端和客户端分别要做的事情:
客户端:
1. 建立连接:tcp \ http
2. 将我们要发送的数据序列化为 json 字符串 - 序列化
3. 发送,实际上发送的是二进制流
4. 等待服务器结果
5. 服务器返回结果,客户端将结果反序列化为可识别数据
服务器端:
1. 监听网络端口(80)
2. 读取客户端发来的二进制数据,并将其转换成Employee对象,反序列化
3. 处理数据,生成一个带有更完成信息的对象,例如:R,其中封装了404、201等信息
4. 将处理的数据结果转换成 json 二进制, 序列化 发送给客户端
我们的序列化技术不一定非要使用 json、我们还可以选择:xml、protobuf、msgpack 等
我们只要解决了 序列化问题,其实就解决了数据互通问题,其实也就屏蔽了我们相互调用时的语言不同的问题(java、python、go)
网络问题:
我们使用 http 与 tcp 最大的区别就是:
http是一次性的,建立连接之后,一旦收到数据的返回,tcp 连接就断开,而 tcp 连接是可以复用的,解决了 tcp 连接要重新建立的问题
另外,我们也可以使用 http2.0 来解决这个问题,http2.0 支持长连接,可以解决连接的建立问题
一个简单的例子:(服务器端)
func main() {
http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
// 这里面写逻辑
_ = r.ParseForm() // 解析参数,可能会报错
fmt.Println("path: ", r.URL.Path)
a, _ := strconv.Atoi(r.Form["a"][0])
b, _ := strconv.Atoi(r.Form["a"][0])
// 进行返回
w.Header().Set("Content-Type", "application/json")
// 构建返回体
jData, _ := json.Marshal(map[string]int{
"data": a + b,
})
// 真正写入
w.Write(jData)
})
// 设置监听的端口
_ = http.ListenAndServe(":8000", nil)
}
上面这个例子就典型的解决了:
Call ID问题:使用URL路径指明要调用的方法
数据传输协议:Http 的参数传递协议
但这里的问题是:使用的是 http1.0的协议,性能低,手写http,数据需要自己解析,效率低
客户端举例:
我们也可以不写客户端,直接使用浏览器来发送请求解决问题,
我们访问:127.0.0.1:8000/add?a=1&b=4
RPC 技术原理:
客户端发送请求,由客户端存根处理,客户端存根将请求整理成协议对应的格式进行发送,将其发送到服务器端,由服务器端接收这之后发送给服务器端存根进行解码,再返回给服务器处理
创建目录结构
Project
server
server.go
client
client.go
server.go:
type HelloService struct {
}
// 给这个类绑定这个方法
func (s *HelloService) Hello(request string, reply *string) error {
// 通过修改 reply 值来进行返回
*reply = "hello, " + request
return nil
}
/*
*
1. 实例化一个server
2. 注册逻辑
3. 开启服务
*/
func main() {
// 第一步:实例化 server
listener, _ := net.Listen("tcp", ":1234")
// 第二步:将我们的 struct 注册进 RPC
// 我们如果把形参列表中的参数定义为 interface{} 就代表这个参数我们可以任意传入
_ = rpc.RegisterName("HelloService", &HelloService{})
// 第三部:启动服务,绑定rpc
conn, _ := listener.Accept()
rpc.ServeConn(conn)
}
client.go:
func main() {
// Dial() 意思是拨号,也就是尝试进行连接,同时进行Gob进行编码
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
panic("链接失败")
}
// 注意这种方式会直接开辟一片空间,并且给这片空间的 string 赋予初值 ''
//var reply string
// 如果以下面这种方式就复杂一点
var replyy *string = new(string)
// 发送请求,请求对应的方法,其后面的参数是传入的参数,根据我们方法的编写,我们最后一个参数用来接收数据
err = client.Call("HelloService.Hello", "Chen", replyy)
if err != nil {
panic("方法调用出错")
}
fmt.Println(*replyy)
}
hello, Chen
注意在上面的代码中,实例化 server、启动服务都是由 net 包完成的,但单独 net 包是不能完成一整个流程的,这是因为还有 call id 的匹配以及序列化机制是由 rpc 来完成的
另外的是:GRPC在此时还不够简洁,使用效率不够高,这体现在包括客户端在调用时不能直接调用方法,而是需要明确方法名等
同时,上面这种最基本的rpc使用的编码解码协议是 Gob,这只能在 go 语言中进行通信,其不支持跨语言
建立目录结构:
json_rpc_test
server
server.go
client
client.go
server.go
type HelloService struct {
}
func (s *HelloService) Hello(request string, reply *string) error {
// 通过修改 reply 值来进行返回
*reply = "hello, " + request
return nil
}
func main() {
listener, _ := net.Listen("tcp", ":1234")
_ = rpc.RegisterName("HelloService", &HelloService{})
// 允许服务器处理多次请求:
for {
conn, _ := listener.Accept()
// 使用自定义协议进行修改
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) // 这里不加 go 协程会出现多个请求的并发问题
}
}
client.go
func main() {
// 使用基础的拨号,不进行编码,编码在后面进行
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
panic("链接失败")
}
// 进行 json 编码
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var replyy *string = new(string)
err = client.Call("HelloService.Hello", "Chen", replyy)
if err != nil {
panic("方法调用出错")
}
fmt.Println(*replyy)
}
测试 RPC 的跨语言特性:
使用 python 的socket 编程发送一个json (不使用http,因为我们服务器没有使用http,无法进行解析)
import json
import socket
request = {
"id":0,
"params":["Zhang"],
"method":"HelloService.Hello"
}
client = socket.create_connection(("localhost", 1234))
client.sendall(json.dumps(request).encode())
# 获取服务器返回的数据
rsp = client.recv(1024)
rsp = json.loads(rsp.decode())
print(rsp["result"])
hello, Zhang
使用 java:
public static void main(String[] args) {
try {
// Connect to the server
Socket socket = new Socket("localhost", 1234);
// Create a JSON request
String jsonRequest = "{\"id\": 0, \"method\": \"HelloService.Hello\", \"params\": [\"Yang\"]}";
// Send the JSON request to the server
OutputStream outputStream = socket.getOutputStream();
outputStream.write(jsonRequest.getBytes());
outputStream.flush();
// Receive the response from the server
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = reader.readLine();
System.out.println("Response from server: " + response);
// Close the socket
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Response from server: {"id":0,"result":"hello, Yang","error":null}
其实在这一步,已经有成熟框架可以使用,这里为了学习,我们使用rpc搭建一个自己的框架
构建目录:
http_rpc_test
server
server.go
client
client.go
server.go
type HelloService struct {
}
func (s *HelloService) Hello(request string, reply *string) error {
// 通过修改 reply 值来进行返回
*reply = "hello, " + request
return nil
}
func main() {
_ = rpc.RegisterName("HelloService", &HelloService{})
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
ReadCloser: r.Body,
Writer: w,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":1234", nil)
}
在这里就可以使用其他语言发送 http 请求,获取结果了
以基础的 HelloWorld 级别代码为例,构建目录:
new_helloworld
server
server.go
client
client.go
handler
handler.go
server_proxy
server_proxy.go
client_proxy
client_proxy.go
我们通过定义一个公共文件,来实现:
handler.go
const HelloServiceName = "handler/HelloService"
type HelloService struct{}
// 给这个类绑定这个方法
func (s *HelloService) Hello(request string, reply *string) error {
// 通过修改 reply 值来进行返回
*reply = "hello, " + request
return nil
}
server.go
func main() {
// 第一步:实例化 server
listener, _ := net.Listen("tcp", ":1234")
// 第二步:将我们的 struct 注册进 RPC
// 我们如果把形参列表中的参数定义为 interface{} 就代表这个参数我们可以任意传入
_ = rpc.RegisterName(handler.HelloServiceName, &handler.HelloService{})
// 第三部:启动服务,绑定rpc
for {
conn, _ := listener.Accept()
go rpc.ServeConn(conn)
}
client.go
func main() {
// Dial() 意思是拨号,也就是尝试进行连接
client := client_proxy.NewHelloServiceClient("tcp", "127.0.0.1:1234")
// 注意这种方式会直接开辟一片空间,并且给这片空间的 string 赋予初值 ''
//var reply string
// 如果以下面这种方式就复杂一点
var replyy *string = new(string)
// 发送请求,请求对应的方法,其后面的参数是传入的参数,根据我们方法的编写,我们最后一个参数用来接收数据
_ = client.Hello("Chen", replyy)
fmt.Println(*replyy)
}
server_proxy.go
type HellosSrvicer interface {
Hello(request string, reply *string) error
}
func RegisterHelloService(srv HellosSrvicer) error {
return rpc.RegisterName(handler.HelloServiceName, srv)
}
client_proxy.go
type HelloServiceStub struct {
*rpc.Client
}
// 初始化,在Go中使用 Newxxx进行初始化
func NewHelloServiceClient(protcol, address string) HelloServiceStub {
conn, err := rpc.Dial(protcol, address)
if err != nil {
panic("connect error")
}
return HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
err := c.Call(handler.HelloServiceName+".Hello", request, reply)
return err
}