追踪、分析Java网络编程底层系统调用

本文将会在Linux环境中让服务端与客户端通过BIO的网络模型进行通信,并且演示整个通信细节。

命令介绍

在分之前会先介绍几个命令,并说明在本次分析中的作用。

1、lsof -p pid

我们用losf命令主要是为了查看系统为某个进程分配的文件描述符的信息。

-p:列出指定进程号所打开的文件。

2、netstat -natp

显示协议统计信息和当前 TCP/IP网络连接。

-n:以数字形式显示地址和端口号。
-a:显示所有连接和监听端口。
-t:显示TCP传输协议的连线状况。
-p:显示正在使用Socket的程序识别码和程序名称。

3、strace -ff -o

常用来跟踪进程执行时的系统调用和所接收的信号。,包括参数,返回值,执行消耗的时间。

-ff:如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。
-o:后面跟filename,将strace的输出写入文件filename中。
-p:跟踪指定的进程pid。

4、tcpdump -S -nn -i eth0 port 9090

根据使用者的定义对网络上的数据包进行截获的包分析工具。

-S:用绝对而非相对数值列出TCP关联数。
-n:不把主机的网络地址转换成名字。
-i:使用指定的网络截面送出数据包。

TCP连接常见状态

演示中会看到如下状态中的部分状态信息,可先行了解。

LISTEN
侦听来自远方的TCP端口的连接请求

SYN-SENT
在发送连接请求后等待匹配的连接请求

SYN-RECEIVED
在收到和发送一个连接请求后等待对方对连接请求的确认

ESTABLISHED
代表一个打开的连接

FIN-WAIT-1
等待远程TCP连接中断请求,或先前的连接中断请求的确认

FIN-WAIT-2
从远程TCP等待连接中断请求

CLOSE-WAIT
等待从本地用户发来的连接中断请求

CLOSING
等待远程TCP对连接中断的确认

LAST-ACK
等待原来的发向远程TCP的连接中断请求的确认

TIME-WAIT
等待足够的时间以确保远程TCP接收到连接中断请求的确认

CLOSED
没有任何连接状态

代码部分

服务端代码

服务端首先通过ServerSocket(9090),创建一个socket,之后循环接收客户端连接,但为了演示效果,每次接收新的连接之前,先通过System.in.read()方法阻塞住,当按下任意键时,来到serverSocket.accept()方法,监听客户端的请求到来。

当接收到客户端请求后,会立刻开启一个新的线程,并将请求交由新的线程去处理,主线程则继续循环等待新的客户端请求到来。

新创建出来的线程则通过bufferedReader.readLine()方法,接收客户端发送的数据并在打印输出之后,循环等待数据的到来。

整个过程会看到accept、readLine两个方法都会产生阻塞(当然System.in.read()的read方法也会阻塞,但只是为了效果演示,真正处理时也不需要调用这个方法。)



import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerSocketBIO {
    
    
    public static void main(String[] args) {
    
    
        ServerSocket serverSocket = null;
        try {
    
    
            serverSocket = new ServerSocket(9090);
            System.out.println("--server start--");
            while (true) {
    
    
                //在accept之前,先阻塞住直到按下任意键,方便分析调用流程
                System.in.read();
                Socket client = serverSocket.accept();
                System.out.println("---accept client ---");
                new Thread(() -> {
    
    
                    InputStream inputStream;
                    try {
    
    
                        inputStream = client.getInputStream();
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                        while (true) {
    
    
                            System.out.println("*wait client send data...*");
                            String readLine = bufferedReader.readLine();
                            if (readLine.equals("quit")) {
    
    
                                System.out.println("---client down...---");
                                bufferedReader.close();
                                inputStream.close();
                                client.close();
                                break;
                            } else {
    
    
                                System.out.println("recv client data:" + readLine);
                            }
                        }
                    } catch (IOException e) {
    
    
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                serverSocket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}



客户端代码

客户端代码就非常简单,请求连接服务端后,将数据通过socket发送出去即可。

import java.io.*;
import java.net.Socket;

public class SocketCli {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Socket socket = new Socket("10.0.0.101", 9090);
        System.out.println("client start...");
        OutputStream outputStream = socket.getOutputStream();
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
    
    
            String readLine = reader.readLine();
            if (readLine.equals("quit")) {
    
    
                bufferedWriter.write(readLine);
                bufferedWriter.newLine();
                bufferedWriter.flush();
                System.exit(-1);
            } else {
    
    
                bufferedWriter.write(readLine);
                bufferedWriter.newLine();
                bufferedWriter.flush();
            }
        }
    }
}

追踪分析

开启tcpdump抓取数据包
在这里插入图片描述

启动服务端
在这里插入图片描述

查看strace输出的out文件,分析如下:

1、服务端创建ServerSocket时,会通过调用socket函数完成,并得到一个FD=5。

2、通过bind函数,将5绑定到9090端口上。

3、通过listen函数,开启监听。

4、通过write打印输出。

5、最终阻塞在read函数中,等待任意键输入(阻塞在这行代码System.in.read())。
在这里插入图片描述

查看为服务端分配的文件描述符,FD5对应一个TCP连接,并且状态为LISTEN。
在这里插入图片描述


启动一个客户端。
在这里插入图片描述

注意此时服务端并没有通过accept接收客户端的请求,但是在TCP层面双方已经完成了三次握手(意味着已经可以进行数据传输了)。
在这里插入图片描述

服务端建立了连接,因为没有accept,所以也没分配PID,但是状态已经为ESTABLISHED了。
在这里插入图片描述

再去查询客户端,客户端也建立连接,并分配了PID,状态为ESTABLISHED。
在这里插入图片描述

服务端键入任意键,表示接收了客户端请求。(意味着调用了serverSocket.accept())
在这里插入图片描述

1、通过阻塞式函数poll,等待FD=5的文件描述符就绪,如果没有客户端请求到来,则会一直阻塞在这个方法上(也就是serverSocket.accept(),这也是一个阻塞点)。

2、调用accept函数,创建一个与10.0.0.101建立连接的socket,并返回一个引用这个socket新的FD=6。
在这里插入图片描述

3、accept之后,代码中是直接创建了一个新的线程处理,所以当java中调用new Thread时,实际上在linux中通过clone这个函数完成的,并返回了新的线程pid,5667。

4、之后继续阻塞在read函数(也就是回到了代码中的System.in.read()),等待任意键输入。
在这里插入图片描述

此时再来看服务端打开的文件信息时,多了一条FD=6的TCP信息。
在这里插入图片描述

之前未分配PID,现在也已经完成了分配。
在这里插入图片描述

再来查看strace跟踪到的5667这个新创建出来的线程,阻塞在了recvfrom函数中,也是bufferedReader.readLine()这行代码,等待socket中的消息到来。
在这里插入图片描述


现在让客户端发送一点数据
在这里插入图片描述
服务端可以正常接收
在这里插入图片描述

5667线程接收到输出后,继续等待新的数据到来。
在这里插入图片描述


现在让我们再启动一个客户端,服务端正常接收连接。
在这里插入图片描述

服务端依然通过clone函数,创建一个新的线程, 并且pid为5674。
在这里插入图片描述

服务端进程中又多了一条FD=7的TCP信息。
在这里插入图片描述

正常建立了连接。
在这里插入图片描述

新的线程同样等待数据到来。
在这里插入图片描述


最后我们让一个客户端下线,观察netstat,此时下线的客户端与服务端的连接状态为TIME_WAIT。
在这里插入图片描述

分配的FD=6也没了。
在这里插入图片描述


最后再证实一下accept方法会阻塞。

重启服务端,直接按下任意键,此时并没有任何客户端的连接请求。
在这里插入图片描述

可以看到请求阻塞在了poll函数中,对于的就是serverSocket.accept()代码。
在这里插入图片描述


总结

当服务端创建一个socket并绑定到9090端口上时,系统会为服务端进程分配一个FD并专门用来监听客户端的请求,当有客户端连接时,即使服务端没有accept,也会完成三次握手并建立连接,只不过对于服务端来说此时建立的连接并没有分配到某个具体的PID上,一旦服务端调用accept接收客户端的连接后,就会创建一个新的FD,专门用来处理服务端与客户端数据的交互。

通过演示我们也能看到在传统BIO模式下,服务端的accept和read都会导致线程阻塞,所以我们让主线程专门用来监听客户端的请求,把监听到的请求全部交给一个新的线程去处理,这样实现了一个服务端能同时接收多个客户端的需求,但是你始终不能无限的创建线程,它始终会有瓶颈,所以之后也就出现了NIO,多路复用等IO模型,在这些模型下可以完成一个线程同时处理多个客户端的请求。

至此也就完成了最简单的BIO模式下的系统调用分析,大家可以参考文章,自己进行实验。

感谢

最后感谢各位老铁:点赞、收藏、评论

猜你喜欢

转载自blog.csdn.net/CSDN_WYL2016/article/details/113309599