精通Java Socket编程:深入剖析系统调用与TCP状态管理

概述

在现代网络编程的广阔天地中,Java Socket API 为我们提供了强大的工具,以实现客户端与服务器之间的高效通信。本文将带您深入Linux系统的内核,揭示Java Socket编程背后的系统调用机制,并详细解读TCP连接状态的每一个关键阶段。通过实际的代码示例和系统调用的深入分析,我们将一探究竟,理解在Linux环境下,Java Socket是如何完成其魔法般的通信任务的。

命令介绍

在深入Java Socket的内部工作之前,让我们先来熟悉几个Linux系统调用和网络命令,这些工具将帮助我们监视和分析网络活动。

  1. lsof -p pid :这个强大的命令使我们能够窥视进程的文件描述符,揭示系统资源的分配情况。
  2. netstat -natp :提供了一个全面的视图,展示当前TCP/IP网络连接的状态,以及相关的协议统计信息。
    • -n:以数字形式显示地址和端口号。
    • -a:显示所有连接和监听端口。
    • -t :显示TCP传输协议的连线状况。
    • -p :显示使用Socket的程序识别码和程序名称。
  3. strace -ff -o :作为系统调用的侦探,它能够跟踪进程的每一个系统调用和信号,为我们提供详尽的执行细节。
    • -ff:输出所有进程的跟踪结果到相应的文件中。
    • -o filename :将输出写入指定文件。
    • -p pid :跟踪指定进程。
  4. tcpdump -S -nn -i eth0 port 9090 :这个网络包分析工具允许我们根据定义捕获网络上的数据包,为我们提供了深入网络层面的洞察力。
    • -S :列出TCP关联数的绝对数值。
    • -n:不解析主机的网络地址。
    • -i interface :指定网络截面。
    • port port :指定端口。

TCP连接常见状态

TCP连接的状态管理是确保数据可靠传输的核心。以下是TCP连接在其生命周期中可能经历的状态,每个状态都代表了连接的一个特定阶段:

  • LISTEN:服务器在此状态下等待接受来自远程TCP端口的连接请求。
  • SYN-SENT:服务器已发送连接请求,等待远程TCP的确认。
  • SYN-RECEIVED:服务器已接收并确认了连接请求,等待最终的确认。
  • ESTABLISHED:连接已成功建立,数据传输可以开始。
  • FIN-WAIT-1 & FIN-WAIT-2:服务器准备关闭连接,正在发送结束信号。
  • CLOSE-WAIT:服务器已接收到关闭请求,等待应用程序关闭。
  • CLOSING:双方均已发出关闭请求,等待最终确认。
  • LAST-ACK:服务器已发送关闭确认,等待远程TCP的最终确认。
  • TIME-WAIT:服务器已关闭连接,等待足够的时间以确保对方收到最终确认。
  • CLOSED:连接已完全关闭,所有资源已释放。

Java Socket编程:实战演练

接下来,我们将通过一系列精心设计的Java代码示例,展示如何在Linux环境下使用Java Socket API实现服务器和客户端之间的通信。

服务端的构建与监听

我们的服务器端程序首先创建一个ServerSocket实例,绑定到指定的端口,并开始监听客户端的连接请求。通过循环和多线程的策略,服务器能够同时处理多个客户端的连接。

客户端的连接与通信

客户端程序则简单直接,它建立到服务器的连接,并发送消息。通过这个简单的行为,我们展示了客户端如何与服务器进行交互。

服务端代码

java 复制代码
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();
            }
        }
    }
}

客户端代码

java 复制代码
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捕获数据包,然后观察TCP三次握手的过程。同时,利用strace工具,我们追踪了服务端程序的系统调用,从而揭示了Java Socket API背后的Linux系统调用细节。

开启tcpdump抓取数据包

1. 启动服务端

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

  1. 服务端创建ServerSocket时,会通过调用socket函数完成,并得到一个FD=5。
  2. 通过bind函数,将5绑定到9090端口上。
  3. 通过listen函数,开启监听。
  4. 通过write打印输出。
  5. 最终阻塞在read函数中,等待任意键输入,阻塞在这行代码System.in.read()。

2. 状态为LISTEN

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

3. 启动客户端

4. 完成三次握手

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

5. 服务端状态为ESTABLISHED

服务端建立了连接,因为没有accept,所以也没分配PID,但是状态已经为ESTABLISHED了。

6. 客户端状态也为ESTABLISHED

再去查询客户端,客户端也建立连接,并分配了PID,状态为ESTABLISHED。

7. 服务端accept

服务端键入任意键,执行serverSocket.accept()代码。

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()),等待任意键输入。
  5. 此时再来看服务端打开的文件信息时,多了一条FD=6的TCP信息。

8. 完成PID分配

之前未分配PID,现在也已经完成了分配。

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

9. 客户端发送数据

现在让客户端发送一点数据

服务端可以正常接收
5667线程接收到输出后,继续等待新的数据到来。

10. 启动多个客户端

现在让我们再启动一个客户端,服务端正常接收连接。

服务端依然通过clone函数,创建一个新的线程, 并且pid -> 5674。

服务端进程中又多了一条FD=7的TCP信息。

正常建立了连接。

新的线程同样等待数据到来。

11. 客户端下线

最后我们让一个客户端下线,观察netstat,此时下线的客户端与服务端的连接状态为TIME_WAIT。
分配的FD=6也没了。

总结

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

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

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

感谢:点赞、收藏、评论

相关推荐
秋饼8 分钟前
Claude Haiku 5.5 + Spring AI 2.0 企业级低成本高并发 AI Agent 实战:子智能体分工、成本熔断与百万上下文落地
java·ai·技术分享·后端开发
xuxigifxfh22 分钟前
牛客:BISHI8 大整数哈希
java·算法·哈希算法
怕浪猫24 分钟前
给AI配一台电脑,开源版Muse来了,OpenMuse赶紧去二次开发吧
javascript·面试·github
涉密IT资质笔记25 分钟前
涉密资质投入产出分析模型:时间成本、四块成本构成与 ROI 决策框架
java·开发语言·算法
木井巳28 分钟前
【BFS 解决拓扑排序】火星词典
java·算法·leetcode·广度优先·宽度优先·图搜索算法
霸道流氓气质28 分钟前
Java调用StableDiffusion/ComfyUI图像生成:DAG工作流编排、WebSocket实时进度与Flux模型集成的生产级实战
java·websocket·stable diffusion
北风toto38 分钟前
KMP 算法详解:从暴力匹配到 O(n+m) 的字符串搜索
java·开发语言·算法
william_yangshun39 分钟前
WordCraft 观察:纯 Rust 重写文字处理器,把文档变成可编程对象
开发语言·后端·rust·文档自动化
weixin1997010801639 分钟前
《京东JOS API:落地方案、接口边界与业务踩坑 —— 联盟免费 vs 商家按量的双轨制》(附python源码)
java·网络·python
衡石科技42 分钟前
从语义准备到模型验收:HENGSHI SENSE Data Agent 的 RAG 与部署工程实践
java·大数据·人工智能·数据分析·rag