geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
166 stars 21 forks source link

动手写RPC框架 - GeeRPC第五天 支持HTTP协议 | 极客兔兔 #96

Open geektutu opened 3 years ago

geektutu commented 3 years ago

https://geektutu.com/post/geerpc-day5.html

7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第五天支持了 HTTP 协议,并且提供了一个简单的 DEBUG 页面。

furthergo commented 3 years ago

我又来了,周末把这一篇实践完了,感觉有点晕😂,这一章做的是:

  1. Server实现handler接口,只接受HTTP CONNECT的请求,并Hijack这个http的tcp连接来做Client和Server之间通信的conn,而之前是直接用Server Accept一个TCP listener,然后做通信。
  2. debugHTTP持Server变量,实现handler接口,处理函数里用持有的Server变量做一些debug相关的统计,这时候可以通过HTTP请求获取到对应的Server的一些调用状态。

所以看起来更像是让client可以通过HTTP CONNECT方法来创建RPC C/S之间的连接吗,这个对于RPC框架的使用者是不是透明的? 比如我使用这个RPC框架,调用的时候还是创建Client,然后调用CallMethod,还是这种函数的调用方式。

多谢多谢🙏

geektutu commented 3 years ago

@furthergo

HTTP 协议转化为 RPC 协议的过程是包装了的,使用者不感知,客户端的协议转换过程已经在 NewHTTPClient 里实现了。main.go 里面有使用示例的。

// 服务端
geerpc.HandleHTTP()
// 客户端
client, _ := geerpc.DialHTTP("tcp", <-addrCh)
sockstack commented 3 years ago

大佬,来一个grpc的

geektutu commented 3 years ago

@sockstack 感谢推荐,grpc 过于复杂了,不过后续可以考虑下。

TDTzzz commented 3 years ago

@geektutu @sockstack 感谢推荐,grpc 过于复杂了,不过后续可以考虑下。

grpc +1!!!大佬这几个系列太赞了👍

geektutu commented 3 years ago

@TDTzzz 感谢认可,笔芯~

ppd0705 commented 3 years ago

请教一下,不是太理解支持HTTP协议的意思。看起来是建立tcp连接后客户端发送了一个HTTP包(CONNECT方法)给服务端,之后的客户端的RPC请求应该就是普通的tcp包了?

geektutu commented 3 years ago

@ppd0705 一般来说,http 是基于 tcp的,未来可能会有不基于 tcp 实现的 http。所以客户端通过在 TCP 上包一层 HTTP 协议来支持 HTTP。使用 HTTP 有很多好处,比如监听一个 tcp 端口,可以使用不同的 PATH 支持不同的 HTTP 服务。

你可以把 HTTP 协议理解为一种协议协商的方式,客户端和服务端一侧做编码,一侧做解码就好了,除了 HTTP 协议头,后面的报文是没有变化的。

andcarefree commented 3 years ago

请问XDial测试里面,addr := "/tmp/geerpc.sock"。之后remove了这个addr。这一块是什么写法

geektutu commented 3 years ago

请问XDial测试里面,addr := "/tmp/geerpc.sock"。之后remove了这个addr。这一块是什么写法

@andcarefree net.Listen 监听 unix socket 时,会创建这个文件。如果这个文件存在,可能会监听失败,监听前删除比较安全。

andcarefree commented 3 years ago
        go func() {
            _ = os.Remove(addr)
            l, err := net.Listen("unix", addr)
            if err != nil {
                t.Fatal("failed to listen unix socket")
            }
            ch <- struct{}{}
            Accept(l)
        }()

XDail测试代码的这一段中,如果net.Listen真的报错了,t.Fatal之后不是会让主协程一直阻塞吗,就ch信道一直收不到那个空结构体

JesseStutler commented 3 years ago

想请教一下,conn, _, err := w.(http.Hijacker).Hijack()这一步是在做什么呢

wilgx0 commented 3 years ago

@JesseStutler 想请教一下,conn, _, err := w.(http.Hijacker).Hijack()这一步是在做什么呢

获取tcp 套接字,http协议也是基于tcp协议的 。注意下面的ServeConn 这个方法 他的参数是什么类型的,这就是他要从http链接中获取套接字的原因。

wilgx0 commented 3 years ago

@ppd0705 请教一下,不是太理解支持HTTP协议的意思。看起来是建立tcp连接后客户端发送了一个HTTP包(CONNECT方法)给服务端,之后的客户端的RPC请求应该就是普通的tcp包了?

谈一下我个人的看法, http协议从应用层编程的角度,我把他简单的理解成一个种数据格式, 服务器端和客服端之间的请求和响应需要按照这种数据格式来发送数据,数据格式详见:https://www.runoob.com/http/http-messages.html。 HTTP协议基于tpc但是它在应用层是感知不到TCP的存在的。 所以不存在先发了一个http包,之后的请求应该是tcp包的说法。兔老大这里只是用http 协议完成了一个握手的过程,然后在使用自己的定义的协议(详见Day1 服务端与消息编码 中的通信过程)来进行双方的通讯。

gnehcein commented 3 years ago

请问大佬能不能把http1改为http2,如果能试试3就是黑科技了

gnehcein commented 3 years ago

建议最后改成用XDial拨号,另外xdial必须http@...才能等价于dialHTTP(“tcp”,...),感觉有点奇怪。。

wbjy commented 3 years ago

@andcarefree

      go func() {
          _ = os.Remove(addr)
          l, err := net.Listen("unix", addr)
          if err != nil {
              t.Fatal("failed to listen unix socket")
          }
          ch <- struct{}{}
          Accept(l)
      }()

XDail测试代码的这一段中,如果net.Listen真的报错了,t.Fatal之后不是会让主协程一直阻塞吗,就ch信道一直收不到那个空结构体

wbjy commented 3 years ago

我试了一下好像死锁了

nanfeng1999 commented 3 years ago
const (
    connected        = "200 Connected to Gee RPC"
    defaultRPCPath   = "/_geeprc_"
    defaultDebugPath = "/debug/geerpc"
)

这里打错字了 应该是geerpc

imtzer commented 2 years ago
switch protocol {
    case "http":
        return DialHTTP("tcp", addr, opts...)

明明是http请求为什么在dialhttp用的还是tcp

lwb0214 commented 2 years ago

go1.12 报错了,读不到返回的请求- -

yqchilde commented 2 years ago

@goder-tao

switch protocol {
  case "http":
      return DialHTTP("tcp", addr, opts...)

明明是http请求为什么在dialhttp用的还是tcp

http目前是基于tcp的

src/net/dial.go: The network must be "tcp", "tcp4", "tcp6", "unix" or "unixpacket".

wangxso commented 2 years ago
defaultRPCPath   = "/_geeprc_"

这里不该叫/geerpc

whitebluepants commented 2 years ago

这里踩了个坑。ServeHTTP最后写入"HTTP/1.0 "应该有个空格,一开始省略了这个导致一直rpc server: options error: EOF 另外这里要往响应里写入数据,用io的WriteString是不是不太好,为什么不用ResponseWriter的Write方法呢

imtzer commented 2 years ago

@wilgx0

@ppd0705 请教一下,不是太理解支持HTTP协议的意思。看起来是建立tcp连接后客户端发送了一个HTTP包(CONNECT方法)给服务端,之后的客户端的RPC请求应该就是普通的tcp包了?

谈一下我个人的看法, http协议从应用层编程的角度,我把他简单的理解成一个种数据格式, 服务器端和客服端之间的请求和响应需要按照这种数据格式来发送数据,数据格式详见:https://www.runoob.com/http/http-messages.html。 HTTP协议基于tpc但是它在应用层是感知不到TCP的存在的。 所以不存在先发了一个http包,之后的请求应该是tcp包的说法。兔老大这里只是用http 协议完成了一个握手的过程,然后在使用自己的定义的协议(详见Day1 服务端与消息编码 中的通信过程)来进行双方的通讯。

刚刚开始码的时候一样有疑惑,先说我自己的结论,添加http支持是为了支持不同路径提供不同的服务,至于你说的先建立一个tcp请求再发送http connect方法似乎是connect方法一个常见的操作流程,我在这篇文章里面找到了相似的地方https://www.jianshu.com/p/54357cdd4736,connect方法被服务器接收到之后,劫持了conn,之后就只是tcp传输了,和楼上说的connect只是完成了一个握手的过程

fanandli commented 2 years ago

兔大,这里的功能是不是就是用http建立连接,然后客户端和服务端的数据还是用的我们实现的格式传输。那“完整”的支持http是不是建立连接也是用http,客户端传数据也是http协议格式,中间需要实现一个http格式转为我们格式的功能呢?

vvzhihao commented 1 year ago
我理解了这一段支持HTTP协议的意义了,要对HTTP,RPC和TCP有了解:1.HTTP 对应于应用层,TCP 协议对应于传输层,HTTP 协议是在 TCP 协议之上建立的,HTTP 在发起请求时通过 ,TCP是传输层协议,定义的是数据传输和连接方式的规范,HTTP是应用层协议,定义的是传输数据的内容的规范
  1. RPC是一种API,HTTP是一种无状态的网络协议。RPC可以基于HTTP协议实现,也可以直接在TCP协议上实现 3.也就是说其实传输层用TCP协议是不变的,只不过在应用层多了一层HTTP协议用于定义传输数据的内容和规范
ShiMaRing commented 1 year ago

@goder-tao

switch protocol {
  case "http":
      return DialHTTP("tcp", addr, opts...)

明明是http请求为什么在dialhttp用的还是tcp

http应用层协议,下面用的还是tcp

onlyshawn commented 1 year ago

求问为啥客户端通过conn发起CONNECT方法的请求服务端收不到呀

SharkLJ commented 1 year ago

最后测试的时候,为什么把call函数中的内容直接放在main函数里会造成死锁?

2507110556 commented 1 year ago

那请问一下,实现支持http,除了实现debug页面,还可以干啥呀,有点想不到

China-zhang-hui commented 1 year ago

server.go中的ServeHTTP在项目启动时,一开始就隐式调用,而debug.go中的ServeHTTP,在发送get请求时才调用。望有大佬解答

6adore commented 1 year ago

死锁报错的可以检查看下"HTTP/1.0 "最后的空格有没有漏掉
_, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")

charfole commented 11 months ago

最后测试的时候,为什么把call函数中的内容直接放在main函数里会造成死锁?

@SharkLJ 因为call是客户端调用某个方法的过程,在调用之前必须先知道服务端的地址,也就是代码geerpc.DialHTTP("tcp", <-addr)里的addr,而这个addr是一个无缓冲的通道,必须等到startServer(addr)执行完成,即服务器启动了才有值(服务器的地址)。所以需要通过协程的方式执行call,等待服务器启动再接收addr来进行调用。

charfole commented 11 months ago

请问博主和各位同学,ServeHTTP中为什么不采用Accept函数里的类似实现,采用for循环和go server.ServeConn(conn)的方式来同时响应多个连接呢?

Larry-shuo commented 8 months ago

@wangxso

defaultRPCPath   = "/_geeprc_"

这里不该叫/geerpc

为什么呀?另外这个前后两个下划线的命名方式有什么含义

SCUTking commented 7 months ago

func (server Server) ServeHTTP(w http.ResponseWriter, req http.Request) { if req.Method != "CONNECT" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusMethodNotAllowed) , = io.WriteString(w, "405 must CONNECT\n") return } conn, , err := w.(http.Hijacker).Hijack() if err != nil { log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) return } , _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") server.ServeConn(conn) }

这个好像也是用旧的RPC协议解析报文呀,只是简单判断了一下是否是CONNECT方法,如果是HTTPS的报文如何解析呢?

ztisdashen commented 3 months ago

为什么我把里面的CONNECT方法换成GET方法好像还是可以正常使用?

ztisdashen commented 3 months ago

@charfole 请问博主和各位同学,ServeHTTP中为什么不采用Accept函数里的类似实现,采用for循环和go server.ServeConn(conn)的方式来同时响应多个连接呢?

http.Serve里面源代码也是一个for+l.Accept()的循环