背景与目标

最近在项目中重温了 Socket 编程,鉴于长期未使用,许多细节已较为模糊,因此通过一个小例子进行回顾。

实现目标

实现一个简单的通信模型:客户端向服务器端发送消息,服务器端读取信息后回复客户端,如此循环往复。

服务端代码实现

服务端监听指定端口,等待客户端连接,接收消息后通过控制台输入回复内容。

package com.dai.socket;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * 服务端实现
 * @author 代长亚
 * @version V1.0
 */
public class Server {
    private ServerSocket serverSocket = null;
    private Socket socket = null;

    public Server() {
        try {
            // 启动服务器监听
            serverSocket = new ServerSocket(8888);
            System.out.println("服务器端已经启动.....");
            
            while (true) {
                // 等待客户端连接
                socket = serverSocket.accept(); 
                
                // 获取输入流
                DataInputStream dis = new DataInputStream(socket.getInputStream()); 
                // 获取输出流
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                
                System.out.println("客户端发来信息:" + dis.readUTF());
                System.out.print("请求回复信息:");
                
                Scanner sc = new Scanner(System.in);
                dos.writeUTF(sc.nextLine());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Server();
    }
}

客户端代码实现

客户端主动连接服务器,发送控制台输入的消息,并接收服务端的回复。

package com.dai.socket;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
 * 客户端实现
 * @author 代长亚
 * @version V1.0
 */
public class Client {
    private Socket socket = null;

    public Client() {
        try {
            System.out.println("客户端已经启动.....");
            
            while (true) {
                // 连接服务器
                socket = new Socket("127.0.0.1", 8888); 
                
                // 获取输入流
                DataInputStream dis = new DataInputStream(socket.getInputStream()); 
                // 获取输出流
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                
                System.out.print("请输入要发送的话:");
                Scanner sc = new Scanner(System.in);
                dos.writeUTF(sc.nextLine());
                
                System.out.println("服务器端:" + dis.readUTF());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Client();
    }
}

概念回顾

Java Socket 简介

所谓 Socket(套接字),通常用于描述 IP 地址和端口,是一个通信链的句柄。应用程序通常通过“套接字”向网络发出请求或者应答网络请求。

以 J2SDK-1.3 为例,SocketServerSocket 类库位于 java.net 包中:

  • ServerSocket:用于服务器端。
  • Socket:建立网络连接时使用。

在连接成功时,应用程序两端都会产生一个 Socket 实例,操作这个实例即可完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是 Socket 还是 ServerSocket,它们的工作都是通过 SocketImpl 类及其子类完成的。

核心 API

java.net.Socket 继承于 java.lang.Object,有八个构造器。以下介绍使用最频繁的三个方法(其它方法可参考 JDK 文档):

  1. accept 方法:用于产生“阻塞”,直到接受到一个连接,并且返回一个客户端的 Socket 对象实例。“阻塞”是一个术语,它使程序运行暂时“停留”在这个地方,直到一个会话产生,然后程序继续;通常“阻塞”是由循环产生的。
  2. getInputStream 方法:获得网络连接输入,同时返回一个 InputStream 对象实例。
  3. getOutputStream 方法:连接的另一端将得到输入,同时返回一个 OutputStream 对象实例。
注意getInputStreamgetOutputStream 方法均会产生一个 IOException,它必须被捕获,因为它们返回的流对象,通常都会被另一个流对象使用。

SocketImpl 与端口说明

抽象类 SocketImpl 是实际实现套接字的所有类的通用超类,创建客户端和服务器套接字都可以使用它。

关于端口分配,需注意以下几点:

  • 端口的分配必须是唯一的,因为端口是为了唯一标识每台计算机唯一服务的。
  • 端口号范围是 0~65535
  • 1024 个端口已经被 TCP/IP 作为保留端口,因此用户分配的端口只能是 1024 之后的。

说明

  1. 版本时效:本文代码与概念基于较早期的 JDK 版本(如 JDK 1.3/1.6 风格)回顾整理,核心原理在现代 Java 版本中依然适用。
  2. 资源管理:示例代码为了保持逻辑简洁,未在循环中显式关闭 Socket 及流对象。在实际生产环境中,务必使用 try-with-resources 或在 finally 块中关闭资源,以防止内存泄漏。
  3. 并发处理:当前服务端模型为单线程循环处理,同一时间只能服务一个客户端。如需支持多客户端并发,通常需为每个 accept 后的 Socket 开启独立线程或使用线程池。