websocket support, issue with connection closing
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
package io.github.lumijiez.core.ws;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class WebSocketConnection {
|
||||||
|
private final Socket socket;
|
||||||
|
private final String id;
|
||||||
|
private final String path;
|
||||||
|
private boolean isOpen;
|
||||||
|
|
||||||
|
public WebSocketConnection(Socket socket, String path) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.id = UUID.randomUUID().toString();
|
||||||
|
this.path = path;
|
||||||
|
this.isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(String message) throws IOException {
|
||||||
|
if (!isOpen) throw new IOException("Connection is closed");
|
||||||
|
|
||||||
|
byte[] payload = message.getBytes();
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
frame.setFin(true);
|
||||||
|
frame.setOpcode(0x1);
|
||||||
|
frame.setPayload(payload);
|
||||||
|
|
||||||
|
frame.write(socket.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPong() throws IOException {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
frame.setFin(true);
|
||||||
|
frame.setOpcode(0xA);
|
||||||
|
frame.setPayload(new byte[0]);
|
||||||
|
|
||||||
|
frame.write(socket.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
frame.setFin(true);
|
||||||
|
frame.setOpcode(0x8);
|
||||||
|
frame.setPayload(new byte[0]);
|
||||||
|
|
||||||
|
frame.write(socket.getOutputStream());
|
||||||
|
isOpen = false;
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/main/java/io/github/lumijiez/core/ws/WebSocketFrame.java
Normal file
91
src/main/java/io/github/lumijiez/core/ws/WebSocketFrame.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package io.github.lumijiez.core.ws;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class WebSocketFrame {
|
||||||
|
private boolean fin;
|
||||||
|
private byte opcode;
|
||||||
|
private byte[] payload;
|
||||||
|
private boolean masked;
|
||||||
|
|
||||||
|
public static WebSocketFrame read(InputStream in) throws IOException {
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
|
||||||
|
int firstByte = in.read();
|
||||||
|
if (firstByte == -1) return null;
|
||||||
|
|
||||||
|
frame.fin = (firstByte & 0x80) != 0;
|
||||||
|
frame.opcode = (byte)(firstByte & 0x0F);
|
||||||
|
|
||||||
|
int secondByte = in.read();
|
||||||
|
if (secondByte == -1) return null;
|
||||||
|
|
||||||
|
frame.masked = (secondByte & 0x80) != 0;
|
||||||
|
int payloadLength = secondByte & 0x7F;
|
||||||
|
|
||||||
|
if (payloadLength == 126) {
|
||||||
|
payloadLength = (in.read() << 8) | in.read();
|
||||||
|
} else if (payloadLength == 127) {
|
||||||
|
throw new IOException("Payload length too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] maskingKey = new byte[4];
|
||||||
|
if (frame.masked) {
|
||||||
|
int bytesRead = in.read(maskingKey);
|
||||||
|
if (bytesRead != 4) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.payload = new byte[payloadLength];
|
||||||
|
int bytesRead = in.read(frame.payload);
|
||||||
|
if (bytesRead != payloadLength) return null;
|
||||||
|
|
||||||
|
if (frame.masked) {
|
||||||
|
for (int i = 0; i < frame.payload.length; i++) {
|
||||||
|
frame.payload[i] ^= maskingKey[i % 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(OutputStream out) throws IOException {
|
||||||
|
int firstByte = (fin ? 0x80 : 0x00) | (opcode & 0x0F);
|
||||||
|
out.write(firstByte);
|
||||||
|
|
||||||
|
if (payload.length < 126) {
|
||||||
|
out.write(payload.length);
|
||||||
|
} else if (payload.length < 65536) {
|
||||||
|
out.write(126);
|
||||||
|
out.write(payload.length >> 8);
|
||||||
|
out.write(payload.length & 0xFF);
|
||||||
|
} else {
|
||||||
|
throw new IOException("Payload too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write payload
|
||||||
|
out.write(payload);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFin(boolean fin) {
|
||||||
|
this.fin = fin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpcode(int opcode) {
|
||||||
|
this.opcode = (byte) opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayload(byte[] payload) {
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getOpcode() {
|
||||||
|
return opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package io.github.lumijiez.core.ws;
|
||||||
|
|
||||||
|
public interface WebSocketHandler {
|
||||||
|
void onConnect(WebSocketConnection connection);
|
||||||
|
void onMessage(WebSocketConnection connection, String message);
|
||||||
|
void onDisconnect(WebSocketConnection connection);
|
||||||
|
}
|
||||||
177
src/main/java/io/github/lumijiez/core/ws/WebSocketServer.java
Normal file
177
src/main/java/io/github/lumijiez/core/ws/WebSocketServer.java
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package io.github.lumijiez.core.ws;
|
||||||
|
|
||||||
|
import io.github.lumijiez.logging.Logger;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class WebSocketServer {
|
||||||
|
private boolean running;
|
||||||
|
private final int port;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
private final ExecutorService threadPool;
|
||||||
|
private final ConcurrentHashMap<String, WebSocketConnection> connections;
|
||||||
|
private final Map<String, WebSocketHandler> handlers;
|
||||||
|
|
||||||
|
public WebSocketServer(int port) {
|
||||||
|
this.port = port;
|
||||||
|
this.running = false;
|
||||||
|
this.threadPool = Executors.newCachedThreadPool();
|
||||||
|
this.connections = new ConcurrentHashMap<>();
|
||||||
|
this.handlers = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addHandler(String path, WebSocketHandler handler) {
|
||||||
|
handlers.put(path, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcast(String path, String message) {
|
||||||
|
connections.values().stream()
|
||||||
|
.filter(conn -> conn.getPath().equals(path))
|
||||||
|
.forEach(conn -> {
|
||||||
|
try {
|
||||||
|
conn.send(message);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("WS", "Error broadcasting message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
try {
|
||||||
|
serverSocket = new ServerSocket(port);
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
Logger.info("WS", "WebSocket server started on port " + port);
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
threadPool.submit(() -> handleClient(clientSocket));
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
Logger.error("WS", "Error accepting WebSocket connection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("WS", "Error starting WebSocket server: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClient(Socket clientSocket) {
|
||||||
|
try {
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||||
|
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
|
||||||
|
|
||||||
|
String line = in.readLine();
|
||||||
|
if (line == null) return;
|
||||||
|
|
||||||
|
String[] requestLine = line.split(" ");
|
||||||
|
if (requestLine.length != 3) return;
|
||||||
|
|
||||||
|
String path = requestLine[1];
|
||||||
|
WebSocketHandler handler = handlers.get(path);
|
||||||
|
|
||||||
|
if (handler == null) {
|
||||||
|
clientSocket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
while (!(line = in.readLine()).isEmpty()) {
|
||||||
|
String[] parts = line.split(": ", 2);
|
||||||
|
if (parts.length == 2) {
|
||||||
|
headers.put(parts[0].toLowerCase(), parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = headers.get("sec-websocket-key");
|
||||||
|
if (key == null) {
|
||||||
|
clientSocket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String acceptKey = generateAcceptKey(key);
|
||||||
|
out.write("HTTP/1.1 101 Switching Protocols\r\n");
|
||||||
|
out.write("Upgrade: websocket\r\n");
|
||||||
|
out.write("Connection: Upgrade\r\n");
|
||||||
|
out.write("Sec-WebSocket-Accept: " + acceptKey + "\r\n");
|
||||||
|
out.write("\r\n");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
WebSocketConnection connection = new WebSocketConnection(clientSocket, path);
|
||||||
|
String connId = connection.getId();
|
||||||
|
connections.put(connId, connection);
|
||||||
|
|
||||||
|
handler.onConnect(connection);
|
||||||
|
|
||||||
|
while (running && connection.isOpen()) {
|
||||||
|
WebSocketFrame frame = WebSocketFrame.read(clientSocket.getInputStream());
|
||||||
|
if (frame == null) break;
|
||||||
|
|
||||||
|
switch (frame.getOpcode()) {
|
||||||
|
case 0x1:
|
||||||
|
handler.onMessage(connection, new String(frame.getPayload()));
|
||||||
|
break;
|
||||||
|
case 0x8:
|
||||||
|
connection.close();
|
||||||
|
break;
|
||||||
|
case 0x9:
|
||||||
|
connection.sendPong();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.onDisconnect(connection);
|
||||||
|
connections.remove(connId);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("WS", "Error handling WebSocket client: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateAcceptKey(String key) {
|
||||||
|
String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
return Base64.getEncoder().encodeToString(
|
||||||
|
md.digest((key + GUID).getBytes())
|
||||||
|
);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
connections.values().forEach(conn -> {
|
||||||
|
try {
|
||||||
|
conn.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("WS", "Error closing connection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connections.clear();
|
||||||
|
if (serverSocket != null) {
|
||||||
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
Logger.info("WS", "WebSocket server stopped");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("WS", "Error stopping WebSocket server: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threadPool.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package io.github.lumijiez.example;
|
|||||||
import io.github.lumijiez.core.config.ServerConfig;
|
import io.github.lumijiez.core.config.ServerConfig;
|
||||||
import io.github.lumijiez.core.http.HttpServer;
|
import io.github.lumijiez.core.http.HttpServer;
|
||||||
import io.github.lumijiez.core.http.HttpStatus;
|
import io.github.lumijiez.core.http.HttpStatus;
|
||||||
|
import io.github.lumijiez.core.ws.WebSocketConnection;
|
||||||
|
import io.github.lumijiez.core.ws.WebSocketHandler;
|
||||||
|
import io.github.lumijiez.core.ws.WebSocketServer;
|
||||||
import io.github.lumijiez.example.daos.ProductDao;
|
import io.github.lumijiez.example.daos.ProductDao;
|
||||||
import io.github.lumijiez.example.models.Product;
|
import io.github.lumijiez.example.models.Product;
|
||||||
import io.github.lumijiez.logging.Logger;
|
import io.github.lumijiez.logging.Logger;
|
||||||
@@ -34,6 +37,27 @@ public class Main {
|
|||||||
res.sendResponse(HttpStatus.OK, product.toString());
|
res.sendResponse(HttpStatus.OK, product.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
server.start();
|
WebSocketServer wsServer = new WebSocketServer(8081);
|
||||||
|
|
||||||
|
wsServer.addHandler("/chat", new WebSocketHandler() {
|
||||||
|
@Override
|
||||||
|
public void onConnect(WebSocketConnection connection) {
|
||||||
|
Logger.info("WS", "Client connected to chat: " + connection.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocketConnection connection, String message) {
|
||||||
|
Logger.info("WS", "Received message: " + message);
|
||||||
|
wsServer.broadcast("/chat", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnect(WebSocketConnection connection) {
|
||||||
|
Logger.info("WS", "Client disconnected from chat: " + connection.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(server::start).start();
|
||||||
|
new Thread(wsServer::start).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,7 @@ public class ProductDao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void deleteProduct(int id) {
|
public void deleteProduct(int id) {
|
||||||
Transaction transaction = null;
|
Transaction transaction = null;
|
||||||
try (Session session = new Configuration().configure().buildSessionFactory().openSession()) {
|
try (Session session = new Configuration().configure().buildSessionFactory().openSession()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user