大侠邓

大侠邓 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

大侠邓 收藏了文章 · 2019-12-09

golang版的traceroute实现

前言

以前看<<TCP/IP详解卷一>>的时候,发现可以根据IP报文中的TTL字段追踪数据包的路由详情,觉得很有意思。后来知道别人早就把它实现出来了,就是linux下的traceroute命令(windows 的tracert),学了golang后也想实现一个go版本的,但中间都给种种事情耽搁了,最近把工作辞了,刚好有点时间,就想着把它做出来,顺便当作个人项目去面试。

应用场景

在分析traceroute之前,先介绍一下它的应用场景。不知道你们有没有遇到过这样情况,就是买了个国外的服务器,用ssh连接的时候发现很慢,然后你就会忍不住ping一下看延迟多少,如果出来300的延迟你会忍不住吐槽一句:什么破服务器,延迟这么高。然后你肯定想知道原因,为什么这破服务器这么卡。

而这时候traceroute就可以派上用场了,你用traceroute测一下就知道,它会可以追踪数据包的路由详情,可以知道从你的电脑到服务器之间经过了多少跳的路由,如果是数据包经过很多跳路由最终才到服务器,自然就很卡。

下面我用 vultr.com域名测试,先ping一下

Pinging vultr.com [108.61.13.174] with 32 bytes of data:
Reply from 108.61.13.174: bytes=32 time=234ms TTL=50
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49
Reply from 108.61.13.174: bytes=32 time=247ms TTL=49
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49

Ping statistics for 108.61.13.174:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 233ms, Maximum = 247ms, Average = 236ms

200多的延迟,然后我们再用tracert(windows下的traceroute)测一下:

Tracing route to vultr.com [108.61.13.174]
over a maximum of 30 hops:

  1     1 ms     2 ms     2 ms  192.168.0.1 [192.168.0.1]
  2     2 ms     1 ms     1 ms  192.168.1.1 [192.168.1.1]
  3     4 ms     3 ms     3 ms  xxx.xx.xx.x
  4    15 ms    40 ms     4 ms  xxx.xx.xx.xx
  5     8 ms    17 ms    18 ms  xxx.xx.xx.xx
  6     9 ms     7 ms     7 ms  202.97.90.162  广州
  7    17 ms    17 ms    16 ms  202.97.38.166  昆明
  8   185 ms   192 ms   184 ms  202.97.51.94   上海
  9   164 ms   167 ms   165 ms  202.97.90.118
 10   191 ms   170 ms   183 ms  9-1-9.ear1.LosAngeles1.Level3.net [4.78.200.1]
 11     *        *        *     Request timed out.
 12   235 ms   239 ms   247 ms  214.213.15.4.in-addr.arpa [4.15.213.214]
 13     *        *        *     Request timed out.
 14     *        *        *     Request timed out.
 15   246 ms   248 ms   237 ms  174.13.61.108.in-addr.arpa [108.61.13.174]

可以看到经过了15跳的路由,如果你分别查一下这些ip对应地方,会发现它从广州绕到昆明,再绕到上海最后才去了美国,绕了中国大半圈,延迟不高才怪呢。

原理分析

下面来分析一下traceroute背后的原理,首先先介绍一个数据包在传输过程中的一个特性,就是IP报文首部的TTL字段在每经过一跳路由的时候,TTL的值都会给路由器减1。就这样每经过一跳路由就减1,当TTL的值减到0的时候,路由器将不再转发这个数据包,而是将其丢弃,然会返回一个ICMP报文到信源端。

这个特性有什么用呢?你想啊,如果我手动把数据包TTL的值设为1,发给目的地,然后IP数据报到下一条路由的时候就给丢弃了,而且还会收到下一跳路由的ICMP报文(里面有该路由器的IP)。然后我再把TTL的值设为2,数据包在第二条路由的时候又给丢弃了,又返回第二跳路由的ICMP报文,这样我又可以知道第二跳路由的IP了。就这样通过投石问路的方式,不停地给目的地发送数据报,直到数据报到达目的地,就可以把每一跳路由的IP给摸清楚了。

这里有张图,或许可以方便理解

d1900102224993c8.png

抓包分析

好了,原理分析讲完了,下面来运行tracert并抓包分析来验证一下我的观点。

首先先打开wireshark,然后运行tracert (tracert www.baidu.com),当然你会在wireshark上面看到一堆密密麻麻的数据包,所以需要过滤一下,在绿色的选框那里输入icmp即可,因为只有icmp数据包才是我们想要的,你会看到类似输出:

图片描述

我已经分别用红色和蓝色的框标记起来了,可以看到,tracert连续发送了3条TTL为1的ICMP报文 (红色框)到目的地,然后收到下一跳路由的ICMP报文(蓝色框),内容为TTL超时。

然后tracert继续发送三个TTL2的ICMP报文到目的地:

图片描述

还是收到同样的答复,TTL超时

就这样,每发送完一轮后,TTL加1,直到收到目的地的回复才停止,如图(我用蓝框标记出来了):

图片描述

看来我不是瞎猜的,上面的就是证据。
既然跟我们预料中的一样,那接下来是不是可以写代码了?别急,还差一步,就是我们刚才只分析tracert发送的过程,只是一个大致的过程。但在写代码的时候,"差不多"是不行的,你需要精确地知道报文的格式和里面的参数才可以。

比如要发送ICMP报文到目的地时,ICMP的报文中的type要改8,code要改为0,代表的是回显。如图:

图片描述

如果你熟悉ICMP报文的话,你会发现traceroute本质上就是一个ping,区别只是在于修改了一下IP首部的TTL字段而已

然后你会收到type为11,code为0的ICMP回复,代表TTL超时

图片描述

或者如果到达了目的地,会收到type为0,code为0的回复。代表Echo Reply。就跟你平时ping某台主机后所得到的回复是一样的

图片描述

关于ICMP报文格式,可以参考wiki 或者百度也行

具体实现

实现过程

traceroute本质上就是一个ping,只是修改了一下IP首部的TTL字段而已,我一开始以为是件很简单的事,但是实现过程一波三折。

我一开始先google一下,看有没有人已经实现过golang版的traceroute了,省得我到处查API。结果真的有,点这里

我满怀好奇地点了进去看了下源码,看思路是否和我是一样的,然后发现他用的syscall这个库来创建socket,不由自主地感叹了这老哥的强悍。syscall是在系统提供给的API上封装的,这么底层的东西,需要对底层有足够的了解才能驾驭。

看了一会,然后把代码复制下来跑一下,发现报了这个错:

..\traceroute.go:198:72: undefined: syscall.IPPROTO_ICMP
..\traceroute.go:211:61: undefined: syscall.SO_RCVTIMEO

就是windows不支持这个系统调用,然后我看了一下项目的README,才看注意到:Must be run as sudo on OS X
而且也有个在windows上开发的人也遇到同样的问题,作者表示无能为力,或者是懒得弄,在这个issue

然后想着既然作者用syscall实现的版本无法在windows上运行,那我干脆自己实现一个好了,然后我就去官网的标准库查API,但是看了发现标准库提供的函数不支持修改IP首部的TTL

然后我又google了一下,发现官方提供的 golang.org/x/net/ipv4的包竟然支持修改TTL,我满心欢喜地安装了这个包,但是在实现过程中发现,这个包的某些函数也是不支持windows的,如果你查看他的源码会发现,他还没有实现,只是在里面写了个TODO标签。别人也遇到同样的问题,并提交到这个issue

我本以为修改TTL只是查一下标准库函数就能搞定了,没想到不仅标准库不支持,而且官方提供的包和封装底层系统调用的syscall都不支持windows,这时候我似乎知道他们都用linux的原因了,而且这种平台的差异性已经不是我能搞定的了。应该还是有办法的,但我现在也不打算花时间纠结这个了,本着实现一个linux版本的好了的心态,打算动手开干。

但是我发现官方提供的demo里就有traceroute的实现,而且写得还很精致,既然官方例子已经实现出来了,我就没有必要再去折腾了。
我看了一下源码,思路跟的我差不多。怎么说呢,我觉得到这里,我也算是把traceroute给实现出来了把,虽然我不是去查API从零开始实现的。

代码分析

下面我摘抄一部分核心代码并分析如下:

比如ICMP报文的封装:

    wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

echo的ICMP的报文格式应该是type:0,code:0,但是他已经定义并封装好了。
还有这里的ID是进程号,用于区分不同的程序,因为这个字段在报文中是16位的,所以和0xffff做了与运算

wm.Body.(*icmp.Echo).Seq = i

这里是ICMP报文中的序列号,用于区分发送的第几个ICMP数据报

if err := p.SetTTL(i); err != nil {
    log.Fatal(err)
}

这里是设置每次发送的TTL,封装得太彻底了,一行就搞定

switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
case ipv4.ICMPTypeEchoReply:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
    return
default:
    log.Printf("unknown ICMP message: %+v\n", rm)
}

最后是根据这段代码来判断数据报是否已经到目的地的,可以看到如果收到的是TTL超时报文会继续发送,如果收到的是正常的回显,则说明已经到达目的地,函数退出。

由于这个库封装了底层的一些东西,比如不用考虑ICMP校验和字段,IP首部校验和算法的实现,所以实现起来代码量不多,包注释也就100行

完整的代码如下:

package main

import (
    "fmt"
    "golang.org/x/net/icmp"
    "golang.org/x/net/ipv4"
    "log"
    "net"
    "os"
    "time"
)

func main() {
    // Tracing an IP packet route to www.baidu.com.

    const host = "www.baidu.com"
    ips, err := net.LookupIP(host)
    if err != nil {
        log.Fatal(err)
    }
    var dst net.IPAddr
    for _, ip := range ips {
        if ip.To4() != nil {
            dst.IP = ip
            fmt.Printf("using %v for tracing an IP packet route to %s\n", dst.IP, host)
            break
        }
    }
    if dst.IP == nil {
        log.Fatal("no A record found")
    }

    c, err := net.ListenPacket("ip4:1", "0.0.0.0") // ICMP for IPv4
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
    p := ipv4.NewPacketConn(c)

    if err := p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
        log.Fatal(err)
    }
    wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

    rb := make([]byte, 1500)
    for i := 1; i <= 64; i++ { // up to 64 hops
        wm.Body.(*icmp.Echo).Seq = i
        wb, err := wm.Marshal(nil)
        if err != nil {
            log.Fatal(err)
        }
        if err := p.SetTTL(i); err != nil {
            log.Fatal(err)
        }

        // In the real world usually there are several
        // multiple traffic-engineered paths for each hop.
        // You may need to probe a few times to each hop.
        begin := time.Now()
        if _, err := p.WriteTo(wb, nil, &dst); err != nil {
            log.Fatal(err)
        }
        if err := p.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
            log.Fatal(err)
        }
        n, cm, peer, err := p.ReadFrom(rb)
        if err != nil {
            if err, ok := err.(net.Error); ok && err.Timeout() {
                fmt.Printf("%v\t*\n", i)
                continue
            }
            log.Fatal(err)
        }
        rm, err := icmp.ParseMessage(1, rb[:n])
        if err != nil {
            log.Fatal(err)
        }
        rtt := time.Since(begin)

        // In the real world you need to determine whether the
        // received message is yours using ControlMessage.Src,
        // ControlMessage.Dst, icmp.Echo.ID and icmp.Echo.Seq.
        switch rm.Type {
        case ipv4.ICMPTypeTimeExceeded:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
        case ipv4.ICMPTypeEchoReply:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
            return
        default:
            log.Printf("unknown ICMP message: %+v\n", rm)
        }
    }
}
查看原文

大侠邓 赞了文章 · 2019-12-09

golang版的traceroute实现

前言

以前看<<TCP/IP详解卷一>>的时候,发现可以根据IP报文中的TTL字段追踪数据包的路由详情,觉得很有意思。后来知道别人早就把它实现出来了,就是linux下的traceroute命令(windows 的tracert),学了golang后也想实现一个go版本的,但中间都给种种事情耽搁了,最近把工作辞了,刚好有点时间,就想着把它做出来,顺便当作个人项目去面试。

应用场景

在分析traceroute之前,先介绍一下它的应用场景。不知道你们有没有遇到过这样情况,就是买了个国外的服务器,用ssh连接的时候发现很慢,然后你就会忍不住ping一下看延迟多少,如果出来300的延迟你会忍不住吐槽一句:什么破服务器,延迟这么高。然后你肯定想知道原因,为什么这破服务器这么卡。

而这时候traceroute就可以派上用场了,你用traceroute测一下就知道,它会可以追踪数据包的路由详情,可以知道从你的电脑到服务器之间经过了多少跳的路由,如果是数据包经过很多跳路由最终才到服务器,自然就很卡。

下面我用 vultr.com域名测试,先ping一下

Pinging vultr.com [108.61.13.174] with 32 bytes of data:
Reply from 108.61.13.174: bytes=32 time=234ms TTL=50
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49
Reply from 108.61.13.174: bytes=32 time=247ms TTL=49
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49

Ping statistics for 108.61.13.174:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 233ms, Maximum = 247ms, Average = 236ms

200多的延迟,然后我们再用tracert(windows下的traceroute)测一下:

Tracing route to vultr.com [108.61.13.174]
over a maximum of 30 hops:

  1     1 ms     2 ms     2 ms  192.168.0.1 [192.168.0.1]
  2     2 ms     1 ms     1 ms  192.168.1.1 [192.168.1.1]
  3     4 ms     3 ms     3 ms  xxx.xx.xx.x
  4    15 ms    40 ms     4 ms  xxx.xx.xx.xx
  5     8 ms    17 ms    18 ms  xxx.xx.xx.xx
  6     9 ms     7 ms     7 ms  202.97.90.162  广州
  7    17 ms    17 ms    16 ms  202.97.38.166  昆明
  8   185 ms   192 ms   184 ms  202.97.51.94   上海
  9   164 ms   167 ms   165 ms  202.97.90.118
 10   191 ms   170 ms   183 ms  9-1-9.ear1.LosAngeles1.Level3.net [4.78.200.1]
 11     *        *        *     Request timed out.
 12   235 ms   239 ms   247 ms  214.213.15.4.in-addr.arpa [4.15.213.214]
 13     *        *        *     Request timed out.
 14     *        *        *     Request timed out.
 15   246 ms   248 ms   237 ms  174.13.61.108.in-addr.arpa [108.61.13.174]

可以看到经过了15跳的路由,如果你分别查一下这些ip对应地方,会发现它从广州绕到昆明,再绕到上海最后才去了美国,绕了中国大半圈,延迟不高才怪呢。

原理分析

下面来分析一下traceroute背后的原理,首先先介绍一个数据包在传输过程中的一个特性,就是IP报文首部的TTL字段在每经过一跳路由的时候,TTL的值都会给路由器减1。就这样每经过一跳路由就减1,当TTL的值减到0的时候,路由器将不再转发这个数据包,而是将其丢弃,然会返回一个ICMP报文到信源端。

这个特性有什么用呢?你想啊,如果我手动把数据包TTL的值设为1,发给目的地,然后IP数据报到下一条路由的时候就给丢弃了,而且还会收到下一跳路由的ICMP报文(里面有该路由器的IP)。然后我再把TTL的值设为2,数据包在第二条路由的时候又给丢弃了,又返回第二跳路由的ICMP报文,这样我又可以知道第二跳路由的IP了。就这样通过投石问路的方式,不停地给目的地发送数据报,直到数据报到达目的地,就可以把每一跳路由的IP给摸清楚了。

这里有张图,或许可以方便理解

d1900102224993c8.png

抓包分析

好了,原理分析讲完了,下面来运行tracert并抓包分析来验证一下我的观点。

首先先打开wireshark,然后运行tracert (tracert www.baidu.com),当然你会在wireshark上面看到一堆密密麻麻的数据包,所以需要过滤一下,在绿色的选框那里输入icmp即可,因为只有icmp数据包才是我们想要的,你会看到类似输出:

图片描述

我已经分别用红色和蓝色的框标记起来了,可以看到,tracert连续发送了3条TTL为1的ICMP报文 (红色框)到目的地,然后收到下一跳路由的ICMP报文(蓝色框),内容为TTL超时。

然后tracert继续发送三个TTL2的ICMP报文到目的地:

图片描述

还是收到同样的答复,TTL超时

就这样,每发送完一轮后,TTL加1,直到收到目的地的回复才停止,如图(我用蓝框标记出来了):

图片描述

看来我不是瞎猜的,上面的就是证据。
既然跟我们预料中的一样,那接下来是不是可以写代码了?别急,还差一步,就是我们刚才只分析tracert发送的过程,只是一个大致的过程。但在写代码的时候,"差不多"是不行的,你需要精确地知道报文的格式和里面的参数才可以。

比如要发送ICMP报文到目的地时,ICMP的报文中的type要改8,code要改为0,代表的是回显。如图:

图片描述

如果你熟悉ICMP报文的话,你会发现traceroute本质上就是一个ping,区别只是在于修改了一下IP首部的TTL字段而已

然后你会收到type为11,code为0的ICMP回复,代表TTL超时

图片描述

或者如果到达了目的地,会收到type为0,code为0的回复。代表Echo Reply。就跟你平时ping某台主机后所得到的回复是一样的

图片描述

关于ICMP报文格式,可以参考wiki 或者百度也行

具体实现

实现过程

traceroute本质上就是一个ping,只是修改了一下IP首部的TTL字段而已,我一开始以为是件很简单的事,但是实现过程一波三折。

我一开始先google一下,看有没有人已经实现过golang版的traceroute了,省得我到处查API。结果真的有,点这里

我满怀好奇地点了进去看了下源码,看思路是否和我是一样的,然后发现他用的syscall这个库来创建socket,不由自主地感叹了这老哥的强悍。syscall是在系统提供给的API上封装的,这么底层的东西,需要对底层有足够的了解才能驾驭。

看了一会,然后把代码复制下来跑一下,发现报了这个错:

..\traceroute.go:198:72: undefined: syscall.IPPROTO_ICMP
..\traceroute.go:211:61: undefined: syscall.SO_RCVTIMEO

就是windows不支持这个系统调用,然后我看了一下项目的README,才看注意到:Must be run as sudo on OS X
而且也有个在windows上开发的人也遇到同样的问题,作者表示无能为力,或者是懒得弄,在这个issue

然后想着既然作者用syscall实现的版本无法在windows上运行,那我干脆自己实现一个好了,然后我就去官网的标准库查API,但是看了发现标准库提供的函数不支持修改IP首部的TTL

然后我又google了一下,发现官方提供的 golang.org/x/net/ipv4的包竟然支持修改TTL,我满心欢喜地安装了这个包,但是在实现过程中发现,这个包的某些函数也是不支持windows的,如果你查看他的源码会发现,他还没有实现,只是在里面写了个TODO标签。别人也遇到同样的问题,并提交到这个issue

我本以为修改TTL只是查一下标准库函数就能搞定了,没想到不仅标准库不支持,而且官方提供的包和封装底层系统调用的syscall都不支持windows,这时候我似乎知道他们都用linux的原因了,而且这种平台的差异性已经不是我能搞定的了。应该还是有办法的,但我现在也不打算花时间纠结这个了,本着实现一个linux版本的好了的心态,打算动手开干。

但是我发现官方提供的demo里就有traceroute的实现,而且写得还很精致,既然官方例子已经实现出来了,我就没有必要再去折腾了。
我看了一下源码,思路跟的我差不多。怎么说呢,我觉得到这里,我也算是把traceroute给实现出来了把,虽然我不是去查API从零开始实现的。

代码分析

下面我摘抄一部分核心代码并分析如下:

比如ICMP报文的封装:

    wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

echo的ICMP的报文格式应该是type:0,code:0,但是他已经定义并封装好了。
还有这里的ID是进程号,用于区分不同的程序,因为这个字段在报文中是16位的,所以和0xffff做了与运算

wm.Body.(*icmp.Echo).Seq = i

这里是ICMP报文中的序列号,用于区分发送的第几个ICMP数据报

if err := p.SetTTL(i); err != nil {
    log.Fatal(err)
}

这里是设置每次发送的TTL,封装得太彻底了,一行就搞定

switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
case ipv4.ICMPTypeEchoReply:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
    return
default:
    log.Printf("unknown ICMP message: %+v\n", rm)
}

最后是根据这段代码来判断数据报是否已经到目的地的,可以看到如果收到的是TTL超时报文会继续发送,如果收到的是正常的回显,则说明已经到达目的地,函数退出。

由于这个库封装了底层的一些东西,比如不用考虑ICMP校验和字段,IP首部校验和算法的实现,所以实现起来代码量不多,包注释也就100行

完整的代码如下:

package main

import (
    "fmt"
    "golang.org/x/net/icmp"
    "golang.org/x/net/ipv4"
    "log"
    "net"
    "os"
    "time"
)

func main() {
    // Tracing an IP packet route to www.baidu.com.

    const host = "www.baidu.com"
    ips, err := net.LookupIP(host)
    if err != nil {
        log.Fatal(err)
    }
    var dst net.IPAddr
    for _, ip := range ips {
        if ip.To4() != nil {
            dst.IP = ip
            fmt.Printf("using %v for tracing an IP packet route to %s\n", dst.IP, host)
            break
        }
    }
    if dst.IP == nil {
        log.Fatal("no A record found")
    }

    c, err := net.ListenPacket("ip4:1", "0.0.0.0") // ICMP for IPv4
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
    p := ipv4.NewPacketConn(c)

    if err := p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
        log.Fatal(err)
    }
    wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

    rb := make([]byte, 1500)
    for i := 1; i <= 64; i++ { // up to 64 hops
        wm.Body.(*icmp.Echo).Seq = i
        wb, err := wm.Marshal(nil)
        if err != nil {
            log.Fatal(err)
        }
        if err := p.SetTTL(i); err != nil {
            log.Fatal(err)
        }

        // In the real world usually there are several
        // multiple traffic-engineered paths for each hop.
        // You may need to probe a few times to each hop.
        begin := time.Now()
        if _, err := p.WriteTo(wb, nil, &dst); err != nil {
            log.Fatal(err)
        }
        if err := p.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
            log.Fatal(err)
        }
        n, cm, peer, err := p.ReadFrom(rb)
        if err != nil {
            if err, ok := err.(net.Error); ok && err.Timeout() {
                fmt.Printf("%v\t*\n", i)
                continue
            }
            log.Fatal(err)
        }
        rm, err := icmp.ParseMessage(1, rb[:n])
        if err != nil {
            log.Fatal(err)
        }
        rtt := time.Since(begin)

        // In the real world you need to determine whether the
        // received message is yours using ControlMessage.Src,
        // ControlMessage.Dst, icmp.Echo.ID and icmp.Echo.Seq.
        switch rm.Type {
        case ipv4.ICMPTypeTimeExceeded:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
        case ipv4.ICMPTypeEchoReply:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
            return
        default:
            log.Printf("unknown ICMP message: %+v\n", rm)
        }
    }
}
查看原文

赞 4 收藏 3 评论 3

大侠邓 提出了问题 · 2019-08-14

解决golang 操作 etcd 续租

golang 如何通过指定lease id 和 etcd的 key进行续约

没有之前的grant生成etcd lease id 的步骤。

比如我在命令行设置了租约,我想在我的程序继续续租这个租约

关注 2 回答 1

大侠邓 回答了问题 · 2019-06-26

grafana import 出现forbidden?

解决了,是因为网络问题,阿里云服务器针对某些post不能过去?

关注 1 回答 1

大侠邓 关注了问题 · 2019-06-26

grafana import 出现forbidden?

clipboard.png

如上图,直接import模板报错forbidden,使用的是admin用户,docker新安装的grafana

关注 1 回答 1

大侠邓 提出了问题 · 2019-06-26

grafana import 出现forbidden?

clipboard.png

如上图,直接import模板报错forbidden,使用的是admin用户,docker新安装的grafana

关注 1 回答 1

大侠邓 赞了文章 · 2019-05-22

Golang gRPC实践 连载七 HTTP协议转换

gRPC HTTP协议转换

正当有这个需求的时候,就看到了这个实现姿势。源自coreos的一篇博客,转载到了grpc官方博客gRPC with REST and Open APIs

etcd3改用grpc后为了兼容原来的api,同时要提供http/json方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步。

他们实现了一个协议转换的网关,对应github上的项目grpc-gateway,这个网关负责接收客户端请求,然后决定直接转发给grpc服务还是转给http服务,当然,http服务也需要请求grpc服务获取响应,然后转为json响应给客户端。结构如图:

图片描述

下面我们就直接实战吧。基于hello-tls项目扩展,客户端改动不大,服务端和proto改动较大。

安装grpc-gateway

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

项目结构:

$GOPATH/src/grpc-go-practice/

example/
|—— hello-http-2/
    |—— client/
        |—— main.go   // 客户端
    |—— server/
        |—— main.go   // 服务端
|—— keys/                 // 证书目录
    |—— server.key
    |—— server.pem
|—— proto/
    |—— google       // googleApi http-proto定义
        |—— api
            |—— annotations.proto
            |—— annotations.pb.go
            |—— http.proto
            |—— http.pb.go
    |—— hello_http.proto   // proto描述文件
    |—— hello_http.pb.go   // proto编译后文件
    |—— hello_http_pb.gw.go // gateway编译后文件

这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供支持。

示例代码

proto/hello_http.proto

syntax = "proto3";  // 指定proto版本

package proto;     // 指定包名

import "google/api/annotations.proto";

// 定义Hello服务
service HelloHttp {
    // 定义SayHello方法
    rpc SayHello(HelloHttpRequest) returns (HelloHttpReply) {
        // http option
        option (google.api.http) = {
            post: "/example/echo"
            body: "*"
        };
    }
}

// HelloRequest 请求结构
message HelloHttpRequest {
    string name = 1;
}

// HelloReply 响应结构
message HelloHttpReply {
    string message = 1;
}

这里在原来的SayHello方法定义中增加了http option, POST方式,路由为"/example/echo"。

编译proto

cd $GOPATH/src/grpc-go-practice/example/hello-http-2/proto

# 编译google.api
protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto

# 编译hello_http.proto
protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=git.vodjk.com/go-grpc/example/proto/google/api:. ./*.proto

# 编译hello_http.proto gateway
protoc --grpc-gateway_out=logtostderr=true:. ./hello_http.proto

注意这里需要编译google/api中的两个proto文件,同时在编译hello_http.proto时指定引入包名,最后使用grpc-gateway编译生成hello_http_pb.gw.go文件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理上面定义的路由"example/echo"接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。

server/main.go

package main

import (
    "crypto/tls"
    "fmt"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "strings"

    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"

    pb "git.vodjk.com/go-grpc/example/proto"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
)

// 定义helloHttpService并实现约定的接口
type helloHttpService struct{}

// HelloHttpService ...
var HelloHttpService = helloHttpService{}

func (h helloHttpService) SayHello(ctx context.Context, in *pb.HelloHttpRequest) (*pb.HelloHttpReply, error) {
    resp := new(pb.HelloHttpReply)
    resp.Message = "Hello " + in.Name + "."

    return resp, nil
}

// grpcHandlerFunc 检查请求协议并返回http handler
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // TODO(tamird): point to merged gRPC code rather than a PR.
        // This is a partial recreation of gRPC's internal checks https://github.com/grpc/grpc-go/pull/514/files#diff-95e9a25b738459a2d3030e1e6fa2a718R61
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    })
}

func main() {
    endpoint := "127.0.0.1:50052"

    // 实例化标准grpc server
    creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }
    conn, _ := net.Listen("tcp", endpoint)
    grpcServer := grpc.NewServer(grpc.Creds(creds))
    pb.RegisterHelloHttpServer(grpcServer, HelloHttpService)

    // http-grpc gateway
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    gwmux := runtime.NewServeMux()
    err = pb.RegisterHelloHttpHandlerFromEndpoint(ctx, gwmux, endpoint, dopts)
    if err != nil {
        fmt.Printf("serve: %v\n", err)
        return
    }

    mux := http.NewServeMux()
    mux.Handle("/", gwmux)

    if err != nil {
        panic(err)
    }

    // 开启HTTP服务
    cert, _ := ioutil.ReadFile("../../keys/server.pem")
    key, _ := ioutil.ReadFile("../../keys/server.key")
    var demoKeyPair *tls.Certificate
    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        panic(err)
    }
    demoKeyPair = &pair

    srv := &http.Server{
        Addr:    endpoint,
        Handler: grpcHandlerFunc(grpcServer, mux),
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{*demoKeyPair},
        },
    }

    fmt.Printf("grpc and https on port: %d\n", 50052)

    err = srv.Serve(tls.NewListener(conn, srv.TLSConfig))

    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }

    return
}

好吧,这么大一坨。核心就是开启了一个http server,收到请求后检查请求是grpc还是http,然后决定是由grpc服务直接处理还是交给gateway做转发处理。其中grpcHandlerFunc函数负责处理决定用哪个handler处理请求,这个方法是直接Copy过来用的,原文的注释说他们也是从别处Copy的。感谢贡献者。

基本流程:

  • 实例化标准grpc server
  • 将grpc server注册给gateway
  • 开启http服务,handler指定给grpcHandlerFunc方法

注意:必须开启HTTPS

运行结果

开启服务:

# hello-http-2/server
go run main.go

> grpc and https on port: 50052    

调用grpc客户端:

# hello-http-2/client
go run main.go

> Hello gRPC.

请求https:

curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}'

> {"message":"Hello gRPC-HTTP is working!."}

为什么是hello-http-2,因为1是个不完整的实现姿势,可以不用https,但是需要分别开启grpc服务和http服务,这里不做说明了。

参考

本系列示例代码

查看原文

赞 15 收藏 26 评论 10

大侠邓 赞了回答 · 2019-04-28

golang如何计算两个日期之间的日期差?

这就需要使用 time 包了。

time 包有个函数 Parse 可以将时间字符串解析成 Time 对象,而 Time 对象有个 Sub 方法可以计算与某个时间的差,返回值是 Duration 对象,而 Duration 有一个 Hours 方法,除以 24 就是天数了

关注 4 回答 3

大侠邓 提出了问题 · 2019-04-28

golang如何计算两个日期之间的日期差?

golang如何计算两个日期之间的日期差?
日期格式:“2017-09-01” ,“2018-03-11”

如何计算这两个日期之间差了多少天啊。求教

关注 4 回答 3

大侠邓 提出了问题 · 2019-04-28

golang如何计算两个日期之间的日期差?

golang如何计算两个日期之间的日期差?
日期格式:“2017-09-01” ,“2018-03-11”

如何计算这两个日期之间差了多少天啊。求教

关注 4 回答 3

认证与成就

  • 获得 0 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-01
个人主页被 87 人浏览