Wireshark抓包与常见问题解决

简介

Wireshark是一个网络抓包分析软件,当线上出现各种连接相关的问题,如连接不复用,大量CLOSE_WAIT时,可以方便的使用Wireshark抓包软件进行抓包分析

安装

Wirewark在windows系统上默认使用的是WinPcap来抓包的,只能看到经过网卡的流量,看不到访问localhost的流量,可先安装Npcap,安装Wirewark时再选择不安装WInPcap即可抓localhost的包

基本使用

window下,直接只用wireshark客户端进行抓包

Linux下,使用tcpdump产生pcap文件,再通过wireshark导入分析
tcpdump -s0 host 192.168.162.103 and port 9999 -w my.pcap

典型场景分析

服务端代码:

public class SocketServer {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress((InetAddress)null, 9999));
        Socket socket = server.accept();
 //       socket.setKeepAlive(true); 默认情况下不进行心跳检测
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        Runtime.getRuntime().addShutdownHook(new Thread(()-> {
            try {
                socket.close();
                server.close();
            } catch (IOException e) {
                System.out.println("close exception" + e);
            }
        }));
        while(true) {
            byte[] bytes = new byte[1024];
            int size = in.read(bytes);
            System.out.println("server read " + new String(Arrays.copyOf(bytes, size)));
            out.write("hello client".getBytes());
            out.flush();
        }
    } 
}

客户端代码:

public class SocketClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("192.168.162.105", 9999);
        OutputStream out = socket.getOutputStream();
        out.write("hello server".getBytes());
        out.flush();
        
        InputStream in = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int size = in.read(bytes);
        System.out.println("server read " + new String(Arrays.copyOf(bytes, size)));
//        Thread.sleep(22 * 1000);  休眠一段时间使得服务端进行心跳检测
        out.close();
        in.close();
        socket.close();
    } 
}

1 观察三次握手、四次挥手


其中四次挥手只有3个tcp分组原因:四次挥手的时候,两个方向的断开是独立的,每个方向发送一个FIN,对方回复一个ACK,但同时,TCP规定ACK可以捎带在其他数据包当中,所以你看到的主动断开连接一方本应收到的ACK,是被对方的FIN包捎带过来的,就变成了三个包。并不是所有的情况下都是这样,典型的一种情况是,主动断开的一方发送FIN之后,被动一方仍然有数据要继续发送,就会先ACK这个FIN,然后继续发送数据(在此过程中主动断开一方仍然会继续ACK这些数据),直到数据发送完毕之后再发送FIN并接收对方的ACK。

2 观察tcp心跳检测机制(放开注释)

tcp心跳服务端参数说明

2.1 模拟客户端一段时间不传输数据


服务器net.ipv4.tcp_keepalive_intvl = 10,以上代码客户端sleep了22s,因此服务端进行了2次心跳检测

2.2 模拟MySQL Client突然掉线,抓取Server端

  • 最后一次正常请求后10s,服务端开始发送心跳包
  • 心跳包间隔3秒,发送3次
  • 3次后,服务端关闭连接

3 线上CLOSE_WAIT问题

在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态,由此分析可知通常是被动关闭方代码问题。
线上应用程序引入连接池后,访问clickhouse出现CLOSE_WAIT

  • 可以看到客户端一直没有回复FIN,研究clickhouse客户端源码实现发现clickhouse底层基于HttpClient实现JDBC接口,由于Http服务端并不会永久保持连接,当服务端超过Keep-Alive时间后会主动关闭连接,而客户端使用连接池后,不会释放关闭连接,导致客户端CLOSE_WAIT
  • 因此通过增加服务端和客户端的http层Keep-Alive时间,可以缓解这个问题,但是并不能根本解决
  • 由于HttpClient本身可以支持多个连接,所以对一个Connection进行管理,即可支持连接池,后续舍弃了Druid连接池,自己进行了客户端JDBC封装

此问题中客户端没有关闭连接,发送FIN导致客户端处于CLOSE_WAIT状态,理论上服务端没有收到FIN,应该处于FIN_WAIT_2的状态,但实际观察发现服务端已经完全关闭了,查看TCP配置发现,FIN_WAIT_2可通过tcp_fin_timeout配置FIN_WAIT_2超时时间,一旦超时会直接进入CLOSED状态,而不经过TIME_WAIT

4 服务端TIME_WAIT

主动关闭TIME_WAIT,被动关闭CLOSE_WAIT

TIME_WAIT时间配置内核没有透出,如果要改需重新编译内核
查看内核源码发现,默认TIME_WAIT时间为60s

https://yq.aliyun.com/ziliao/256040

Java客户端Socket常用配置(进程级别配置)

  • socket.setKeepAlive(true);
    是否开启tcp心跳检测机制,默认不开启,开启后会根据OS tcp_keepalive_time、tcp_keepalive_intvl、tcp_keepalive_probes进行心跳检测
  • socket.setReuseAddress(true);
    允许复用处于TIME_WAIT的socket
  • socket.setTcpNoDelay(true);在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1个字节,而数据包的头却有几十个字节(IP头+TCP头)时,系统会在发送之前先将较小的包合并到较大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;在默认情况下,Nagle算法是开启的。
    这种算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet等),使用这种方式传输数据会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle算法时就使用它,不需要时就关闭它。而使用setTcpToDelay正好可以满足这个需求。当使用setTcpNoDelay(true)将Nagle算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出
  • socket.setSoLinger(true, 0);
  • socket.setSoTimeout(soTimeout);
    配置inputstream一个阻塞read的超时时间

Linux常用TCP参数(OS级别配置)

  • net.ipv4.tcp_keepalive_time
    当keepalive起用的时候,TCP发送keepalive消息的频度,单位为秒,缺省是7200秒(即2小时)
  • net.ipv4.tcp_keepalive_intvl
    keepalive探测包的发送间隔
  • net.ipv4.tcp_keepalive_probes
    如果对方不予应答,探测包的发送次数
  • net.ipv4.tcp_timestamps
    为1表示开启TCP时间戳,用来计算往返时间RTT(Round-Trip Time)和防止序列号回绕
  • net.ipv4.tcp_tw_reuse
    为1表示允许将TIME-WAIT的句柄重新用于新的TCP连接
  • net.ipv4.tcp_tw_recycle
    为1表示开启TCP连接中TIME-WAIT的快速回收,NAT环境可能导致DROP掉SYN包(回复RST),不要轻易与net.ipv4.tcp_timestamps一起开启
  • net.ipv4.tcp_fin_timeout
    FIN_WAIT_2状态的超时时长
  • net.ipv4.tcp_syncookies
    为1时SYN Cookies,当SYN等待队列溢出时启用cookies来处理,可防范少量SYN攻击
  • net.ipv4.tcp_max_tw_buckets
    保持TIME_WAIT套接字的最大个数,超过这个数字TIME_WAIT套接字将立刻被清除并打印警告信息
  • net.ipv4.ip_local_port_range
    设定tcp客户端发起连接随机端口范围,默认32768,61000,这个配置限制了此机器访问外部机器的连接数目
  • net.ipv4.tcp_max_syn_backlog
    端口最大backlog内核限制,防止占用过大内核内存
  • net.ipv4.tcp_syn_retries
    对一个新建连接,内核要发送多少个SYN连接请求才决定放弃,不应该大于255
  • net.ipv4.tcp_retries1
    放弃回应一个TCP连接请求前﹐需要进行多少次重试,RFC规定最低的数值是3,这也是默认值
  • net.ipv4.tcp_retries2
    在丢弃激活(已建立通讯状况)的TCP连接之前﹐需要进行多少次重试,默认值为15
  • net.ipv4.tcp_synack_retries
    TCP三次握手的SYN/ACK阶段重试次数,缺省5
  • net.ipv4.tcp_max_orphans
    不属于任何进程(已经从进程上下文中删除)的sockets最大个数,超过这个值会被立即RESET,并同时显示警告信息
  • net.ipv4.tcp_orphan_retries
    孤儿sockets废弃前重试的次数,缺省值是7
  • net.ipv4.tcp_mem
    内核分配给TCP连接的内存,单位是page:
    第一个数字表示TCP使用的page少于此值时,内核不进行任何处理(干预),
    第二个数字表示TCP使用的page超过此值时,内核进入“memory pressure”压力模式,
    第三个数字表示TCP使用的page超过些值时,报“Out of socket memory”错误,TCP 连接将被拒绝
  • net.ipv4.tcp_rmem
    为每个TCP连接分配的读缓冲区内存大小,单位是byte
  • net.ipv4.tcp_wmem
    为每个TCP连接分配的写缓冲区内存大小,单位是byte:
    第一个数字表示,为TCP连接分配的最小内存,
    第二个数字表示,为TCP连接分配的缺省内存,
    第三个数字表示,为TCP连接分配的最大内存(net.core.wmem_max可覆盖该值)

参考文档:
https://www.cnblogs.com/wangjq19920210/p/8440824.htm
https://www.zhihu.com/question/55890292
https://yq.aliyun.com/articles/581106
http://elf8848.iteye.com/blog/1739598
https://segmentfault.com/a/1190000012345710
TIME_WAIT很好的文章:
https://jin-yang.github.io/post/network-tcpip-timewait.html
tcp_timestamps抓包分析文章:
http://www.bubuko.com/infodetail-1650846.html

posted @ 2018-10-11 17:31  nlskyfree  阅读(8231)  评论(0编辑  收藏  举报