原创 吴就业 178 0 2024-02-06
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/d7af16a165134b5a9312288bf2560683
作者:吴就业
链接:https://wujiuye.com/article/d7af16a165134b5a9312288bf2560683
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
抓包分析是排查网络问题的必备技能,也是排查接口问题的有效手段。带着问题学习技能往往会更容易掌握,本篇介绍如何通过抓包分析解决实际问题。
三大案例:
在获得root权限的前提下,我们可以通过tcpdump命令监听指定端口的tcp数据包,例如:
tcpdump -i any "tcp and port 9099"
命令执行输出如下:
单纯依靠这种方式我们很难从图中看出数据包是什么内容。不过tcpdump支持将数据包输出到文件,这样我们就可以通过借助其它工具来分析。
将tcpdump内容输出到文件可使用-w参数指定输出文件,例如:
tcpdump -i any -w ~/tcp_dump.pcap "tcp and port 9099"
执行此命令dump数据包将输出到~/
目录下的tcp_dump.pcap文件。
然后将tcpdump的tcp数据包文件下载到本地。
使用Wireshark工具难度较高,需要先掌握tcp协议。推荐给初学者的一个TCP数据包分析学习工具:https://www.chattcp.com/
安装Wireshark工具,使用Wireshark打开tcpdump下来的数据包。Wireshark的使用教程有很多,可以网上搜索学习。
有时候应用会请求一些第三方的接口,只提供https域名。又或者抓线上数据包的时候,用的也是https协议。要分析这些数据包就需要拿到解密数据所需要的密钥。
这需要我们能拿到每次https建连完成TLS握手后生成的tls session key(Master Secret),该session key是TLS握手协商生成的对称加密密钥。
这里介绍三种场景下的session key获取方式。
使用Chrome浏览器访问接口或者使用curl命令访问接口的场景下。
Linux、Mac系统,在~/.profile文件下配置SSLKEYLOGFILE环境变量。
export SSLKEYLOGFILE=${自定义文件路径}
在应用里面调用第三方接口,需要抓取数据包分析的场景下,可以在代码里面配置输出session key日记到文件,再将文件从服务器上下载到本地。
以go为例:
func initHttpClient(){
config := &tls.Config{
KeyLogWriter: os.Stdout, // 这里是输出到控制台,改为输出到文件即可
}
transport := &http.Transport{
TLSClientConfig: config,
}
httpCli = &http.Client{
Transport: transport,
}
}
日记内容如下:
CLIENT_RANDOM 566410c77ddd57eb1527b8dd6992ac23a44c3bf171bd3e9d8f708102c62a8fd6 cec00adeb348715f2e5f28cbccd809d31e3c50bac9a048e1fcf3414fab20f074fc3418ebef21c589f72268a7f7fff9e7
CLIENT_RANDOM 1c319799192126f896019e91baa72f764e2235bdc0357d854ee631d3c3601a64 634a2135e8e50eb44adc214088e2845c21a58f2654abac0f8e44289ec5257bf75754422252fabc9bc605f9d2a5cf2d33
Java JDK的SSLSessionImpl类记录了TLS/SSL协商过程中的Master Secret,可通过反射或者字节码插桩方式获取。例如开源的插桩实现工具extract-tls-secrets:https://github.com/neykov/extract-tls-secrets。(此方案对Netty无效)
依次打开配置页面:Perferences->Protocols->TLS,选择session key日记文件。
在未配置之前
配置之后
内部测试环境,客户端反馈发送IDL请求经常连接不上,然后过一会又可以,然后调通一次之后,kill掉app进程重启又不行了。
在部署网关的机器上抓包如下:
发现与客户端建立连接成功之后,立马就被服务端主动断开了。
原因是以前为了测试限流功能,配置了按连接数限流,超过1个连接之后,后面的连接就会被断开。
因为客户端是直接kill进程的,没主动关闭连接,而过一会后,前面建立的连接超时关闭了,所以后面再发起请求就又可以了。
基于websocket实现的自定义请求响应协议。
测试反馈,并发请求会偶现服务端接收请求延迟十几秒的现象。
客户端日记打印消息发送时间与服务端打印的接收时间相差十几秒,并且客户端出现等待响应超时情况。
网络延迟不可能有十几秒,因此我们需要确定服务端接收到数据包的时间,是否与日记打印的时间相同,进一步排查问题。
通过tcpdump抓包,使用Wireshark分析dump文件,找到客户端发送的数据包如下。
而服务端打印的接收请求的日记如下。
对比抓包分析的服务端接收到数据包的时间,确实相差了十几秒,说明不是网络延迟问题,而是服务端的代码问题。
我们通过进一步分析服务端打印的日记,发现出现这种情况的时间点,都有打印kafka消息发送超时的错误信息。
进一步通过jstack查看,发现配置的200个核心线程的业务线程池只创建了4个线程,再分析日记,发现这个连接发送的请求都是由同一个线程处理。
综上我们可以得出结论。由于前面的请求阻塞在kafka消息发送,导致后续的请求都在排队,最终就是我们所看到的现象:服务端日记打印的接收时间比实际数据包到达时间晚了十几秒。
最后通过分析Netty源代码发现,我们配置的DefaultEventLoopGroup线程池,一个连接Netty只会通过next()绑定一个线程(EventLoop),该连接的所有消息都由这个线程处理。
消息推送系统(中间件)的架构:
请求响应的架构:客户端app -> 推送网关 -> 连接管理服务
推送消息的架构:客户端app <- 推送网关 <- 连接管理服务 <- 业务服务
在开发测试过程中,我们发现,推送网关使用单一连接并发请求连接管理服务,在连接管理服务侧会偶现解码失败,导致连接管理服务主动与推送网关断开连接。现象截图如下:
推送网关与连接管理服务之间的通信协议是WebSocket。推送网关使用go语言开发,使用gobwas实现WebSocket Clinet。连接管理服务使用Java语言开发,使用Netty实现WebSocket Server。
通过抓包分析,我们发现,WebSocket连接关闭数据包附近的几个数据包,有出现乱码包的情况。
但这个数据包并不是我们发的,所以我们怀疑是gobwas这个库发的。为了排除是库的问题,我们把gobwas换成gorilla。而换gorilla之后,问题不再出现,所以确定是gobwas的问题。
了解网络协议、学会利用tcpdump抓包,学会利用Wireshark分析数据包,将能帮助我们解决一些仅从客户端日记分析或仅从服务端日记分析无法解决的疑难杂症。
线上容器化部署我们很难通过tcpdump在服务端一侧抓包,需要在pod所在的node节点上抓包,并且真实环境网络流量大,并不适合抓包。不过通过Charles工具我们可以从客户端侧抓包,并支持https协议抓包,也能解决大部分接口问题。
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
当我们抓包并写代码解码的过程中发现,我们解码每个TCP数据包,无论是rpc请求还是rpc响应,都是要先跳过前四个字节,才是rpc协议的消息id,这样解码才正确,为什么呢?
自研实现文件上传下载的中间件在测试阶段发现断点续传有问题。具体表现是:使用wget下载mp4文件可以正常播放,用google浏览器打开链接,google浏览器无法正常播放mp4视频。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。