JavaEE(6)TCP_IP

目录


TCP/IP 不是单一协议,是一整套协议簇。IP负责寻址路由(网络层),TCP负责可靠端到端传输(传输层)。

前置

  1. IP(网络层):解决「我这个包要发给哪台机器,怎么走路由」;
  2. TCP(传输层):解决「数据可靠、有序传输,流量控制,连接管理」;
  3. 应用层Java代码,我们只操作Socket,操作系统内核完成TCP/IP头部封装、分用。
  4. TCP核心特性:有连接、可靠传输、面向字节流、全双工。

注意:Java代码我们写在应用层;TCP、IP头部处理全部由操作系统内核完成,我们代码看不到TCP报文,但是代码行为会映射TCP协议行为。


案例1:TCP回显服务器&客户端(Echo)

功能:客户端输入字符串发给服务端,服务端原封不动返回。

流程:客户端new Socket()触发TCP三次握手建立连接 ;双方通过输入输出流收发字节;调用close()触发四次挥手断开连接。

TCP服务端代码 TcpServer.java

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * TCP BIO 回显服务端
 */
public class TcpServer {
    private ServerSocket serverSocket;

    public TcpServer(int port) throws IOException {
        // 绑定本机端口,开始监听
        serverSocket = new ServerSocket(port);
        System.out.println("TCP服务端启动,监听端口:" + port);
    }

    public void start() throws IOException {
        // BIO单线程,一次只能处理1个客户端
        while (true) {
            // accept() 阻塞!等待客户端连接,三次握手完成之后,才返回Socket对象
            Socket clientSocket = serverSocket.accept();
            System.out.printf("客户端接入:%s:%d%n",
                    clientSocket.getInetAddress(),
                    clientSocket.getPort());
            // 处理该客户端连接
            handleClient(clientSocket);
        }
    }

    private void handleClient(Socket clientSocket) {
        // try‑with‑resource:自动关闭流
        try (InputStream is = clientSocket.getInputStream();
             OutputStream os = clientSocket.getOutputStream();
             Scanner scanner = new Scanner(is);
             PrintWriter printWriter = new PrintWriter(os)) {

            // 循环读取客户端发来的数据
            while (scanner.hasNext()) {
                // 读取客户端一行字符串
                String req = scanner.next();
                System.out.println("收到客户端消息:" + req);
                // 业务逻辑:回显,原样返回
                String resp = req;
                // println 会追加换行符\n,作为应用层报文边界
                printWriter.println(resp);
                // ⚠️ 必须flush!缓冲区数据不会立刻发送,flush才会把数据交给TCP内核缓冲区
                printWriter.flush();
            }
            // scanner.hasNext() 返回false:代表客户端调用close发送FIN报文,连接关闭
            System.out.println("客户端正常断开连接");

        } catch (IOException e) {
            System.err.println("客户端异常断开:" + e.getMessage());
        } finally {
            try {
                clientSocket.close(); // 关闭socket,发出FIN报文,开启四次挥手
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TcpServer server = new TcpServer(9000);
        server.start();
    }
}

TCP客户端代码 TcpClient.java

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * TCP BIO 客户端
 */
public class TcpClient {
    private Socket socket;

    public TcpClient(String ip, int port) throws IOException {
        // new Socket 这一步:发起TCP三次握手!和服务端建立连接,阻塞直到连接建立成功
        socket = new Socket(ip, port);
        System.out.println("客户端连接服务端成功");
    }

    public void start() {
        try (InputStream is = socket.getInputStream();
             OutputStream os = socket.getOutputStream();
             Scanner consoleScanner = new Scanner(System.in);
             Scanner netScanner = new Scanner(is);
             PrintWriter printWriter = new PrintWriter(os)) {

            while (true) {
                System.out.print("请输入发送给服务端的内容:");
                String input = consoleScanner.next();
                // 发送给服务端
                printWriter.println(input);
                printWriter.flush();

                // 读取服务端回显响应
                String resp = netScanner.next();
                System.out.println("收到服务端返回:" + resp);

                if ("exit".equals(input)) {
                    System.out.println("客户端准备关闭连接");
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("通信异常:" + e.getMessage());
        } finally {
            try {
                socket.close(); // 客户端发送FIN,开始四次挥手
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        // 127.0.0.1 本机回环IP,不走物理网卡,用于本地测试
        TcpClient client = new TcpClient("127.0.0.1", 9000);
        client.start();
    }
}

运行步骤

  1. 先运行TcpServer,控制台输出:TCP服务端启动,监听端口:9000
  2. 再运行TcpClient,建立TCP连接(三次握手完成)
  3. 客户端控制台输入文字,服务端回显;输入exit关闭连接(触发四次挥手)

代码对应TCP/IP底层行为

  1. new Socket("127.0.0.1",9000):客户端内核发出SYN报文,三次握手开始;握手全部完成,这个构造方法才返回。
  2. serverSocket.accept()阻塞,直到三次握手完成,返回Socket对象。
  3. printWriter.println + flush:数据从JVM缓冲区拷贝到操作系统TCP发送缓冲区;内核TCP协议栈加上TCP头部,IP协议栈加上IP头部,交给链路层封装以太网帧。
  4. socket.close():内核发送FIN报文,开启四次挥手断开TCP连接。
  5. scanner.hasNext()返回false:代表收到对端FIN报文,对方已经关闭输出。

案例2:多客户端支持(多线程版本TCP服务端)

上面单线程版本服务端同一时刻只能服务一个客户端;TCP本身支持多路连接,BIO下需要每个客户端分配一个线程。

java 复制代码
public void start() throws IOException {
    System.out.println("多线程TCP服务端启动,监听端口9000");
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 每来一个客户端就开一个新线程处理
        new Thread(() -> {
            handleClient(clientSocket);
        }).start();
    }
}

原理:操作系统内核可以维护成千上万TCP连接;BIO问题:每个连接占用一个线程,高并发下线程太多,开销巨大;生产环境会使用NIO。


结合代码理解TCP核心机制

1. 三次握手(建立连接)

代码触发点:Socket client = new Socket(ip,port)

  1. 客户端内核发送SYN报文(同步报文段)
  2. 服务端内核回复SYN+ACK
  3. 客户端回复ACK;三次握手完成。
  • 握手作用:确认双向收发能力,协商MSS最大报文段大小,初始化双方序列号。

Java应用层代码看不到SYN/SYN+ACK报文,全部操作系统内核完成。

2. 四次挥手(断开连接)

代码触发点:调用socket.close()

TCP是全双工,读、写通道可以分别关闭。

  1. 主动关闭方调用close → 内核发出FIN报文,表示我不再发送数据。
  2. 对端回复ACK确认FIN。
  3. 对端应用调用close → 内核发出自己的FIN。
  4. 本端回复ACK,连接释放。

TIME_WAIT状态

主动关闭连接一方,收到对方FIN、发出最后一个ACK之后,进入TIME_WAIT状态,保持2MSL时间。

目的:

  1. 保证最后一个ACK报文能够到达对方,如果ACK丢失,可以重发ACK;
  2. 网络中延迟滞留的旧TCP报文全部消失,防止旧连接报文干扰新连接。

在Windows cmd执行netstat -ano可以观察TIME_WAIT状态连接。

3. TCP面向字节流 & 粘包问题

TCP没有报文边界,内核缓冲区是字节流。上面示例代码我们用println()加换行符\n作为报文边界。

假设:

客户端连续发送两段消息:hello 和 world

内核TCP缓冲区中字节是连续的:hello\nworld\n

如果服务端一次性读全部字节,分不清哪一段是一个请求,这就是粘包。

解决粘包三种方案(应用层处理,TCP内核不管)

  1. 分隔符 :例如换行符\n,上面代码示例;适合文本协议;
  2. 定长包:每次固定长度读取;
  3. 包头携带数据长度:最常用;先4字节表示后续数据长度,再发送数据。

UDP不会粘包;UDP保留数据包边界。

4. IP地址在代码中的体现

  • 127.0.0.1:本地回环IP,数据包直接本机内核流转,不经过网卡硬件。
  • socket.getInetAddress() 获取对端IP地址(IP协议,网络层)。
  • 五元组:源IP、源端口、目的IP、目的端口、协议(TCP=6),标识一条TCP连接。

5. 端口占用异常

启动服务端如果抛出

复制代码
java.net.BindException: Address already in use: bind

代表该端口被别的进程占用。

Windows排查命令:

cmd 复制代码
netstat -ano | findstr 9000

得到PID,任务管理器结束对应进程。

6. 抓包验证TCP/IP(Wireshark)

  1. 打开Wireshark,选择Loopback回环接口;
  2. 运行上面Java程序;
  3. 过滤器输入tcp.port == 9000

可以肉眼观察:SYN、SYN‑ACK、ACK握手报文;数据报文;FIN挥手报文,看TCP头部、IP头部。


补充:UDP对比代码,凸显TCP面向连接特性

UDP没有三次握手,DatagramSocket.send()直接发数据包,不需要建立连接。

java 复制代码
// UDP简单发送片段
DatagramSocket socket = new DatagramSocket();
byte[] data = "hello udp".getBytes();
InetSocketAddress address = new InetSocketAddress("127.0.0.1",9001);
DatagramPacket packet = new DatagramPacket(data,data.length,address);
socket.send(packet);

UDP代码执行send就直接发包,没有任何握手过程;丢包不会重传。

关键总结(代码 ↔ TCP/IP对应)

Java代码行为 底层TCP/IP发生的事情
new Socket(ip,port) 内核发起TCP三次握手
ServerSocket.accept() 阻塞,等待三次握手完成返回连接
printWriter.flush() 数据拷贝到内核TCP发送缓冲区,封装TCP头+IP头
socket.getInputStream().read 从内核TCP接收缓冲区读取字节
socket.close() 内核发送FIN报文,四次挥手释放连接

小结:Java网络编程只操作应用层Socket API;TCP、IP头部封装、确认应答、超时重传、滑动窗口全部由操作系统内核完成,应用层代码感知不到这些细节。

相关推荐
承渊政道1 小时前
Linux网络学习【数据链路层、ARP工作过程与ARP欺骗详解】
linux·网络·学习·数据链路层·arp
Novproxy_Z10 小时前
商用代理避坑:如何识别商家拿机房 IP 冒充住宅 IP
java·网络·tcp/ip
垆边人似月.11 小时前
RDMA 文件传输踩坑(五):READY 发送成功后,为什么出现 Work Request Flushed Error?
服务器·网络·tcp/ip
HideF11 小时前
nmcli
linux·服务器·网络
qetfw12 小时前
UOS DHCP Relay:跨网段分配与 IP、DNS、网关验证
linux·服务器·网络·tcp/ip·uos
尹人入圣12 小时前
目前热门的IP驱动产业新场景新工具
网络·python·网络协议·tcp/ip
中屹指纹浏览器svip13 小时前
只用指纹浏览器,不搭配IP可以吗?
网络
jimmyleeee13 小时前
Agent安全之八:OWASP ASI08 级联失效------当每一个 Agent 都“正确”,系统却走向灾难
网络·安全·web安全
缓慢更新13 小时前
AI 辅助生成单测:从接口定义到用例的流水线
java·服务器·前端·网络·数据库·人工智能