JavaEE 初阶第二十期:网络编程“通关记”(二)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、TCP流套接字编程

[1.1. ServerSocket](#1.1. ServerSocket)

[1.2. Socket](#1.2. Socket)

[1.3. 示例](#1.3. 示例)


一、TCP流套接字编程

1.1. ServerSocket

ServerSocket是创建TCP服务端Socket的API。

  • ServerSocket构造方法

|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到固定端口 |

  • ServerSocket方法

|-----------------|---------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口,有客户端连接后,返回一个服务端Socket对象,并基于该Socket对象建⽴与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |

1.2. Socket

Socket是客户端Socket,或服务端中接收到客户端建立连接(accept⽅法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

  • Socket构造方法

|-------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建⼀个客户端流套接字Socket,并与对应IP的主机 上,对应端口的进程建立连接 |

  • Socket方法

|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输⼊流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |

1.3. 示例

TCP是有连接的,不能一上来就读取数据,需要先处理连接。建立连接的过程,是在操作系统内核里已经完成了,只需要在代码中把操作系统里建立好的连接拿过来用就行。

SeverSocket是服务端专用的套接字 ,仅用于在服务端监听客户端的连接请求,是连接的 "管理者"。Socket是通信的端点,既可以作为客户端的套接字(主动发起连接),也可以作为服务端接受连接后生成的套接字(用于与客户端实际通信)。

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;

public class EchoServer {
    private ServerSocket serverSocket;

    public EchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 如果客户端没有建立连接就会阻塞
            Socket socket = serverSocket.accept();
            processConnection(socket);
        }
    }

    /**
    * 处理客户端连接的方法
    * @param socket 客户端套接字对象,用于与客户端进行通信
    */
    private void processConnection(Socket socket) {
        System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress().toString(), socket.getPort());
        try (InputStream inputStream = socket.getInputStream();  // 从socket获取输入流,用于读取客户端发送的数据
              OutputStream outputStream = socket.getOutputStream()) {
            Scanner in = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while (true) {
                // 读取请求并解析
                if (!in.hasNext()) {
                    // 针对客户端下线的逻辑,比如客户端结束了
                    System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
                    break;
                }
                String request = in.next();
                // 根据请求计算响应
                String response = process(request);
                // 把响应写回到客户端
                writer.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理请求字符串的方法
     * 该方法接收一个字符串参数,直接返回该参数
     * 
     * @param request 需要处理的字符串请求
     * @return 返回与输入相同的字符串
     */
    private String process(String request) {
    // 直接返回输入的请求字符串
        return request;
    }

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

我们前面也写过一个UDP服务器,两个服务器端口号一样,即使同时启动,也不会产生冲突,因为两个服务器的协议不同。

java 复制代码
import java.net.Socket;

public class EchoClient {
    private Socket socket;

    public EchoClient(String severIP, int serverIP) {
        socket = new Socket();
    }
}

这里的构造方法与UDP不同的是,因为UDP协议本身是无连接的,不记录对端的信息。而TCP是一上来需要连接的,但连接的过程在操作系统内核完成的,但还是需要告诉操作系统服务器的端口和IP是什么。

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;

/**
 * @author gao
 * @date 2025/8/21 15:28
 */

public class EchoClient {
    private Socket socket;

    public EchoClient(String severIP, int serverPort) throws IOException {
        socket = new Socket(severIP, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner in = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while (true) {
                // 从控制台读取用户的输入
                System.out.print("> ");
                String request = in.next();
                // 构造请求发送给服务器
                writer.println(request);
                // 读取服务器的响应
                if (!scannerNet.hasNext()) {
                    System.out.println("服务器断开了连接!");
                    break;
                }
                String response = scannerNet.next();
                // 响应显示到控制台上
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        EchoClient client = new EchoClient("127.0.0.1", 9090);
        client.start();
    }
}

虽然服务器和客户端都写完了,但我们一启动并输入之后,就会发现,服务器这边并没有任何响应。

这时我们要分析到底是客户端没把数据发送出去,还是服务器收到了没有正确处理。我们可以在服务器代码里面的阻塞后面加上个打印,再运行观察,还是没有。说明上面的hasNext()没有解除阻塞,大概率就是客户端没发来数据。

java 复制代码
// 发送数据的代码
writer.println(request);

第一个问题,此处的代码执行到了,但是此处的println只是写到了缓冲区,没有写到网卡,也就没有真正发送。缓冲区,英文名称"buffer",通常情况下就是一个"内存空间",计算机读取内存比读取网卡要快很多。假设要很多次写入,就要把多次的数据一次写入网卡。如果缓冲区满了,就会自动传到网卡,或者刷新缓冲区以强行切入到外设。

java 复制代码
while (true) {
    // 读取请求并解析
    if (!in.hasNext()) {
        // 针对客户端下线的逻辑,比如客户端结束了
        System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
        break;
    }
    System.out.println("服务器收到了数据");
    String request = in.next();
    // 根据请求计算响应
    String response = process(request);
    // 把响应写回到客户端
    writer.println(response);
    writer.flush();
}
java 复制代码
while (true) {
    // 从控制台读取用户的输入
    System.out.print("> ");
    String request = in.next();
    // 构造请求发送给服务器
    writer.println(request);
    writer.flush();
    // 读取服务器的响应
    if (!scannerNet.hasNext()) {
        System.out.println("服务器断开了连接!");
        break;
    }
    String response = scannerNet.next();
    // 响应显示到控制台上
    System.out.println(response);
}

第二个问题,当前程序中,存在"文件泄露"。每循环一次,就会触发一次打开文件的操作。一个服务器,不知道要处理多少个客户端,导致打开操作频繁。我们只需要在processConnection最后加上finally代码块。

java 复制代码
public void start() throws IOException {
    System.out.println("服务器启动!");
    while (true) {
        // 如果客户端没有建立连接就会阻塞
        Socket socket = serverSocket.accept();
        processConnection(socket);
    }
}
java 复制代码
try {
    socket.close();
} catch (IOException e) {
    e.printStackTrace();
}

但在UDP中的socket对象只有一个,不会频繁创建销毁,生命周期很长,只要1服务器运行,就随时能使用。

第三个问题,程序运行效果的问题。如果在启动多个客户端的情况下就会出现只有一个客户端有连接。在IntelliJ IDEA中要想启动多个实例,需要设置一下。下拉右上角的框,点击"Edit Configurations",再点击Modify options,再勾选上"Allow multiple instances"。

这样我们就可以启动两个客户端,当我们在第一个客户端里面输入任意内容会得到返回结果,但第二个客户端输入内容就没有结果。

这是因为服务器里面的start方法里面有一层循环,而start方法里面的porcessConnection方法里面还有一层循环。只要第一个客户端不退出,processConnection就不会退出。而UDP里面只有一层循环就不会出现上述问题。

我们可以利用之前的多线程知识,把porcessConnection方法放在其他线程里面。

java 复制代码
public void start() throws IOException {
    System.out.println("服务器启动!");
    while (true) {
        // 如果客户端没有建立连接就会阻塞
        Socket socket = serverSocket.accept();
        Thread t = new Thread(() ->{
            processConnection(socket);
        });
        t.start();
    }
}

其实多线程最初被发明出来的概念就是为了给每个客户端分配线程,由这个线程负责客户端的请求和响应。