飞翔飞翔
主页
  • 计算机基础

    • TCP协议
  • 数据库

    • SQL教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP协议
  • 数据库

    • SQL教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • TCP协议

    • TCP 简介与分层模型
    • TCP 连接管理
    • TCP 可靠传输机制
    • TCP 粘包与拆包
    • TCP 拥塞控制
    • TCP 头部字段详解
    • TCP 与 UDP 对比
    • TCP Socket 编程实践
    • TCP 抓包实战
    • TCP 安全与面试综合题

TCP Socket 编程实践

理论学了这么多,动手写代码才能真正理解。本篇用 Java Socket 实现一个完整的 TCP 客户端-服务端通信程序。


1. Socket 是什么?

Socket(套接字) 是操作系统提供的网络编程接口,是应用层与传输层之间的"门"。应用程序通过 Socket 发送和接收数据,不需要关心底层 TCP/IP 的实现细节。

应用层代码
    ↕  write() / read()
Socket(套接字)
    ↕
TCP / UDP(传输层)
    ↕
IP(网络层)
    ↕
网卡 → 网络

一个 TCP Socket 由五元组标识:

{协议(TCP), 本地IP, 本地端口, 远程IP, 远程端口}

2. 核心 API 速查

2.1 服务端 API

类/方法作用
ServerSocket(int port)在指定端口创建服务端 Socket,开始监听
socket.accept()阻塞等待客户端连接,返回一个 Socket 对象
socket.getInputStream()获取输入流,读取客户端数据
socket.getOutputStream()获取输出流,向客户端发送数据
socket.close()关闭连接

2.2 客户端 API

方法作用
new Socket(host, port)向服务端发起 TCP 连接(触发三次握手)
socket.getInputStream()获取输入流,读取服务端数据
socket.getOutputStream()获取输出流,向服务端发送数据
socket.close()关闭连接(触发四次挥手)

3. 最简示例:Echo 服务器

服务端收到什么就原样返回。

3.1 服务端

// 服务端:在 8080 端口监听,收到消息原样返回
public class EchoServer {

    public static void main(String[] args) throws Exception {
        // 1. 创建 ServerSocket,监听 8080 端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("服务端启动,等待连接...");

        // 2. 阻塞等待客户端连接(三次握手在这里完成)
        Socket socket = serverSocket.accept();
        System.out.println("客户端已连接:" + socket.getRemoteSocketAddress());

        // 3. 获取输入输出流
        BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

        // 4. 读取客户端消息,原样返回
        String message;
        while ((message = in.readLine()) != null) {
            System.out.println("收到: " + message);
            out.println("Echo: " + message);  // 原样返回
        }

        // 5. 关闭
        socket.close();
        serverSocket.close();
    }
}

3.2 客户端

// 客户端:连接服务端,发送消息,接收回复
public class EchoClient {

    public static void main(String[] args) throws Exception {
        // 1. 连接服务端(触发三次握手)
        Socket socket = new Socket("127.0.0.1", 8080);
        System.out.println("已连接服务端");

        // 2. 获取输入输出流
        BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

        // 3. 从控制台读取输入,发送给服务端
        BufferedReader console = new BufferedReader(
                new InputStreamReader(System.in));
        String userInput;
        while ((userInput = console.readLine()) != null) {
            out.println(userInput);              // 发送
            String response = in.readLine();      // 接收回复
            System.out.println("服务端回复: " + response);
        }

        // 4. 关闭(触发四次挥手)
        socket.close();
    }
}

3.3 运行效果

# 服务端输出
服务端启动,等待连接...
客户端已连接:/127.0.0.1:54321
收到: Hello
收到: TCP编程不难

# 客户端输出
已连接服务端
Hello                          ← 用户输入
服务端回复: Echo: Hello
TCP编程不难                    ← 用户输入
服务端回复: Echo: TCP编程不难

4. 进阶:多线程服务端

上面的 Echo 服务器只能处理一个客户端。实际应用中,服务端需要同时处理多个客户端——每来一个连接,就开一个线程处理。

// 多线程服务端:每个客户端连接由独立线程处理
public class MultiThreadServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("多线程服务端启动,等待连接...");

        while (true) {
            // 主线程循环:不断接受新连接
            Socket socket = serverSocket.accept();
            System.out.println("新客户端连接:" + socket.getRemoteSocketAddress());

            // 每个连接分配一个线程处理
            new Thread(() -> handleClient(socket)).start();
        }
    }

    // 处理单个客户端的通信
    private static void handleClient(Socket socket) {
        try (
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
        ) {
            String message;
            while ((message = in.readLine()) != null) {
                System.out.println(Thread.currentThread().getName()
                        + " 收到: " + message);
                out.println("Echo: " + message);
            }
        } catch (Exception e) {
            System.out.println("客户端断开:" + socket.getRemoteSocketAddress());
        } finally {
            try { socket.close(); } catch (Exception ignored) {}
        }
    }
}

5. 编程中的 TCP 细节

5.1 连接超时

客户端连接服务端时,如果服务端不可达,new Socket() 会阻塞很长时间(默认可能几十秒)。可以设置超时:

Socket socket = new Socket();
socket.connect(new InetSocketAddress("192.168.1.100", 8080), 3000);  // 3 秒超时
// 如果 3 秒内连不上,抛出 SocketTimeoutException

5.2 读写超时

读取数据时也可以设置超时,避免长时间阻塞:

socket.setSoTimeout(5000);  // 5 秒读超时
// 如果 5 秒内没收到数据,read() 抛出 SocketTimeoutException

5.3 TCP_NODELAY:关闭 Nagle 算法

默认开启 Nagle 算法(攒够一批数据再发)。对于实时性要求高的场景(游戏、即时通讯),可以关闭:

socket.setTcpNoDelay(true);  // 关闭 Nagle,每次 send 立即发出

5.4 SO_REUSEADDR:端口复用

服务端重启时,可能遇到"端口被占用"(TIME_WAIT 状态)。设置端口复用可以解决:

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);    // 允许复用处于 TIME_WAIT 的端口
serverSocket.bind(new InetSocketAddress(8080));

5.5 优雅关闭(半关闭)

TCP 支持半关闭——一方关闭输出(不再发数据),但仍然可以接收数据:

socket.shutdownOutput();   // 关闭输出流(发送 FIN),但输入流仍可读
// 对方会收到 EOF(read 返回 -1),但对方仍然可以发数据过来

// 继续读取对方数据...
String msg = in.readLine();

socket.close();  // 最终关闭

6. Socket 编程常见问题

问题原因解决方案
ConnectException: Connection refused服务端未启动或端口错误检查服务端是否启动、端口是否一致
SocketTimeoutException: connect timed out网络不通或防火墙拦截检查网络连通性、防火墙规则
EOFException / read 返回 -1对方关闭了连接正常处理连接断开逻辑
BindException: Address already in use端口被占用或 TIME_WAIT使用 SO_REUSEADDR 或等待 TIME_WAIT 过期
读取阻塞死等对方未发送数据设置 SO_TIMEOUT
乱码编码不一致统一使用 UTF-8

7. 小结

Socket 编程是 TCP 理论的实践落地。核心流程就三步:建立连接 → 读写数据 → 关闭连接。多线程是服务端处理并发连接的基础方案(生产环境通常用线程池或 NIO)。注意处理粘包问题(上一篇的内容),以及超时、端口复用等工程细节。


本篇要点

  • Socket 是操作系统提供的网络编程接口,应用程序通过 Socket API 使用 TCP/UDP 进行通信
  • 服务端核心流程:ServerSocket 监听 → accept() 阻塞等待连接 → 获取流读写数据 → 关闭
  • 多线程服务端:主线程循环 accept(),每个连接分配一个独立线程处理(生产环境用线程池或 NIO)
  • TCP_NODELAY 关闭 Nagle 算法,适合延迟敏感场景;SO_REUSEADDR 解决 TIME_WAIT 端口占用
  • 半关闭:socket.shutdownOutput() 关闭输出(发 FIN)但输入流仍可读
  • BIO 每连接一线程(阻塞);NIO 一线程管多连接(Selector 轮询);AIO 操作系统完成读写后回调
上一页
TCP 与 UDP 对比
下一页
TCP 抓包实战