基于socket的并发编程

大概要求是编写一个支持多客户端并发访问的key-value存储服务器,支持 put get delete exit 服务
还要用JMeter测试服务器并发性能(包括平均响应时间和TPS)

在线程池模型、事件驱动(proactor)模型、虚拟线程模型中,我选择了高并发下性能较好,代码实现较简单的虚拟线程模型

关键代码

// Server.java
package com.lzx;
import java.io.*;
import java.net.*;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server {
    private static final int PORT = 12345;
    private static final KeyValueStore store = new KeyValueStore();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            serverSocket.setReuseAddress(true);
            System.out.println("Server started on port " + PORT);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                executor.execute(() -> handleClient(clientSocket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleClient(Socket clientSocket) {
    //     System.out.println("New connection from: " + 
    // clientSocket.getInetAddress().getHostAddress() + ":" +
    // clientSocket.getPort()); // 调试
        try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
    
            // 跳过Telnet协商阶段
            // while (in.ready()) in.read();
    
            String inputLine;
            while (!clientSocket.isClosed() && (inputLine = in.readLine()) != null) {
                // 清洗输入:去除退格符、非打印字符,合并空格
                // System.out.println("Raw input: [" + inputLine + "]");// 调试
                inputLine = inputLine
                    .replaceAll("[\b\u007F]", "")       // 过滤退格键(Backspace)和删除键(DEL)
                    .replaceAll("[^\\x20-\\x7E]", "")    // 过滤非ASCII可打印字符
                    .trim()
                    .replaceAll("\\s+", " ");
    
                // 分割命令并校验
                String[] parts = inputLine.split(" ", 3);
                if (parts.length == 0 || parts[0].isEmpty()) {
                    out.println("ERROR: Empty command");
                    continue;
                }
    
                String command = parts[0].toUpperCase().replaceAll("[^A-Z]", "");
                // System.out.println("DEBUG - Parsed command: " + command + ", parts: " + Arrays.toString(parts));
    
                String response;
                switch (command) {
                    case "PUT":
                        if (parts.length < 3) {
                            response = "ERROR: Usage: PUT key value";
                        } else {
                            response = store.put(parts[1], parts[2]);
                        }
                        break;
                    case "GET":
                        if (parts.length < 2) {
                            response = "ERROR: Usage: GET key";
                        } else {
                            response = store.get(parts[1]);
                        }
                        break;
                    case "DELETE":
                        if (parts.length < 2) {
                            response = "ERROR: Usage: DELETE key";
                        } else {
                            response = store.delete(parts[1]);
                        }
                        break;
                    case "EXIT":
                        clientSocket.close();
                        return;
                    default:
                        response = "ERROR: Unknown command";
                }
                out.println(response);
                out.flush(); // 确保立即发送响应
                // 新增连接保持检查
                if ("EXIT".equals(command)) {
                    clientSocket.close();
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("Client error: " + e.getMessage());
        } finally {
            try { clientSocket.close(); } catch (IOException ignored) {}
        }
    }   
}

遇到一些问题:

  • 用cmd的telnet客户端输入的第一行无法显示在cmd窗口中,不过会被服务器接收到并响应(所以无伤大雅
  • 以为是telnet协商的问题所以跳过了tel协商while (in.ready()) in.read(); (然而并不是),不过这一行导致后来用JMeter的TCP取样器发现能连接到服务器,但是服务器接收不到数据,所以注释掉这一行代码就可以了
  • TCP取样器需要设置EOL,否则读取不到结束字节会一直等待响应直到超时,我又没选择改JMeter配置文件所以就怎么简单怎么来,就发送一行数据,接收一行响应
  • 还有一个很重要的问题,TCP取样器的结果树(调试用的)感觉占用很多资源,严重拉低TPS是其次,主要是线程或者循环数量一多就经常导致线程崩溃(连不上服务器)或者失败报错(超时),建议还是参数设置1线程循环1次,调试好,稳定后,把结果树删除掉再测试,就比较正常了

JMeter中线程组和TCP相关配置如下:

(1)TCP取样器相关参数
服务器名/IP:localhost
端口:12345
超时连接(ms):1000 响应(ms):1000
行尾EOL字节值:10(对应\n,此处选择EOL字节因为如果不是设置响应读取结束则会一直等待直至超时,同时也因为此处设置了换行符为结束符导致只能接收一行响应,所以每次发送命令均为一行put命令)

(2)线程组参数
线程数:分别设置了50,100,200进行对比测试
Rame-up时间(秒):1
循环次数:永远 
设置调度器-持续时间(秒):60

(3)还添加了断言,确保响应正常

image
image
image
image

posted @ 2025-03-19 13:28  liuzhaoxu  阅读(25)  评论(0)    收藏  举报