likes
comments
collection
share

透过 Java 看看 Socket 的那些事

作者站长头像
站长
· 阅读数 21

众所周知,在互联网的世界中,当一方想要与另一方进行通信时,大部分情况下,通信双方需要先建立一个连接,也就是我们常说的 TCP 连接。但是 TCP 只是 IETF 发布的一个在传输层的协议,属于概念性的东西,那实现这个协议的事情是谁做的呢?

在日常开发和使用中,我们绝大多数的程序员都只会去关注应用层的东西,很少会去思考,在我的机器上,我的程序是如何和另一台机器上的程序建立连接并进行数据传输的?

因为这些事情,操作系统,也就是内核,已经帮我们封装好了,那么我们今天就一起来看下,内核都帮我们做了什么?

理论

TCP / IP 四层模型

首先,让我们先来回顾下著名的 TCP/IP 四层模型:

透过 Java 看看 Socket 的那些事

从图中可以看出,位于最上边的应用层,其包含了众多的协议,而这些协议,也是我们日常开发和使用中接触最多的。比如,当我们准备搭建一个微服务时,我们会考虑是否让它支持 http 协议或者 https 协议;当我们开发一个发送邮件的功能时,又要考虑到 SMTP 协议,等等。那我们今天的主角:socket 它属于哪一层呢?其实 socket 并不是某一个具体的协议,它是操作系统为我们应用程序提供的接口,当我们需要和内核进行交互时,只需要调用其提供的 API,而不需要考虑具体是如何实现的。

Socket 四元组

知道了 socket 是什么后,让我们来看看它具体长什么样子?官方对其定义,在 RFC 9293 中提到:

透过 Java 看看 Socket 的那些事

翻译过来就是说,一个 socket 需要包含一个地址和一个端口号。那也就意味着,如果两台机器想要进行通讯,那么在客户端要有一个 socket,在服务端也要有一个 socket,双方需要知道彼此的 ip 和端口号后,才能建立连接。所以,一个 socket 连接需要包含自己的 ip, 端口号和对方的 ip, 端口号。而这四个元素,也就是我们常说的 socket 四元组。

透过 Java 看看 Socket 的那些事


实践

在我们对 socket 有了一个基本的了解后,我们用一段代码,来看一下 socket 是如何帮助我们进行进程间的通信的?

public class ServerSocketDemo {

    public static void main(String[] args) throws IOException {

        try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("listening 8888");

            System.out.println("waiting input...");
            System.in.read();
            System.out.println("received input");

            System.out.println("waiting client...");
            Socket socket = serverSocket.accept();
            int port = socket.getPort();
            System.out.println("remote port is: " + port);

            BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream());

            byte[] buffer = new byte[1024];
            int len;
            while ((len = bufferedInputStream.read(buffer)) != -1) {
                System.out.println(new String(buffer, 0, len));
            }
        }
    }

}
  • 在第五行,我们首先开启一个服务端 socket,然后在第九行,利用等待输入阻塞住程序。此时,我们查看进程信息,可以看到我们的系统上已经有了一个进程在监听8888端口,进程号1844

    透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

  • 然后我们打开一个新的会话窗口,利用 nc 命令模拟客户端建立连接,然后再次查看进程信息,我们发现此时系统上多了两个进程:

    透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

    1. 绿色方框选中的进程:表示站在客户端,也就是 nc 的角度,我和监听8888端口的进程成功的建立了连接。
    2. 红色方框选中的进程:表示站在服务端的角度,因为我的 java 进程被阻塞住了,我没有办法接收这个连接,但是操作系统,也就是内核帮助我们建立了这个 tcp 连接。
  • 此时,我们让 java 进程继续保持阻塞,然后向8888端口发送 hello 字符串。发送后再次查看进程信息,我们会发现那个没有进程号的进程的 Recv-Q 列,多了6个字节。而这就是内核在建立连接后帮助 java 进程接收了数据(一个 hello 字符串和一个回车换行符)并把数据放到了内核缓冲区。

    透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

  • 这时候,我们在 java 进程里输入123,让程序继续执行。此时,我们会看到 java 程序会将内核中的信息,也就是队列中的6个字节,读取出来并打印到控制台。而且,那个没有进程号的进程也已经被 java 进程接受了。

    透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

  • 到这里,我们清楚的看到了,在 TCP/IP 四层模型中,下三层的事情,是内核做的,而我们只实现了应用层的事情,而应用程序和内核之间的交互,就是 socket 做的事情,它帮助了应用程序读取了内核中缓冲的数据。


了解了 socket 的作用后,我们再看一下 socket 具体是如何和内核交互的? strace 命令,可以帮助我们查看进程在和系统交互时都调用了哪些系统指令集,让我们更新下代码,去除不必要的打印和等待输入的代码,然后通过 strace 再次启动 ServerSocketDemo

public class ServerSocketDemo {

    public static void main(String[] args) throws IOException {

        try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("listening 8888");

            System.out.println("waiting client...");
            Socket socket = serverSocket.accept();
            int port = socket.getPort();
            System.out.println("remote port is: " + port);

            BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream());

            byte[] buffer = new byte[1024];
            int len;
            while ((len = bufferedInputStream.read(buffer)) != -1) {
                System.out.println(new String(buffer, 0, len));
            }
        }
    }

}

透过 Java 看看 Socket 的那些事

启动后,我们会发现,在目录下多出了很多以 socket 开头的文件,其实这些文件就代表每一个线程,包括主线程,jvm 的线程、守护线程等等,查看 socket.2320 这个文件,并搜索 8888 关键字。

透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

查看日志,我们会看到:

  1. 操作系统首先会创建一个 socket,并用数字4进行标识。
  2. 调用指令 bind 进行 ip 和端口号绑定。
  3. 调用指令 listen 进行端口监听。
  4. 调用指令 aceept 并且阻塞在这里。说明此时,正在等待一个 socket 连接。

接下来,我们再次用 nc 命令建立连接,并观察 socket.2320 这个文件的变化:

透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事

我们可以看到,连接建立后,accept 方法接收了 nc 的连接并且等待对方的输入。

紧接着,我们输入123,我们可以看到 socket 接收到了包含换行符的信息,并且向控制台进行了打印。

透过 Java 看看 Socket 的那些事 透过 Java 看看 Socket 的那些事


总结

最开始我们从理论层面,了解了 socket 是什么,有什么作用,然后,用代码演示了,在 api 层面它是如何帮助应用程序和内核进行交互的,最后,我们一起看了下 socket 的具体实现过程。希望通过阅读本文能帮助你更好的理解 socket 这个深藏功与名的接口

(感谢阅读,如有不正确之处,欢迎指教)