基于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)还添加了断言,确保响应正常