├── .gitignore ├── README.md ├── demo.png ├── index.html ├── pom.xml ├── running.png └── src └── main └── java └── me └── jittagornp └── example ├── AppStarter.java ├── util └── ByteBufferUtils.java └── websocket ├── BinaryWebSocketHandler.java ├── CloseStatus.java ├── FrameData.java ├── FrameDataByteBufferConverter.java ├── FrameDataByteBufferConverterImpl.java ├── MultipleWebSocketHandler.java ├── Opcode.java ├── TextWebSocketHandler.java ├── WebSocket.java ├── WebSocketHandler.java ├── WebSocketImpl.java └── WebSocketServer.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | .DS_Store 4 | */.DS_Store 5 | /.idea/ 6 | /*.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Native WebSocket Server Example 2 | 3 | > ตัวอย่างการเขียน WebSocket Server ด้วย Native Java (โดยไม่ใช้ Dependencies ใด ๆ) implement ตามแนวทางของ RFC6455 (The WebSocket Protocol) https://tools.ietf.org/html/rfc6455 4 | 5 | # บทความอ้างอิง 6 | 7 | - [WebSocket คืออะไร ทำงานยังไง (อธิบายแบบละเอียด)](https://www.jittagornp.me/blog/what-is-websocket/) 8 | 9 | # วิธีการใช้งาน 10 | 11 | ให้ run ไฟล์ `AppStarter.java` เพื่อทดสอบดู 12 | 13 | ![](./demo.png) 14 | 15 | ![](./running.png?v=1) 16 | 17 | จากนั้นให้เขียน WebSocket Client เชื่อมต่อมาที่ `ws://localhost` เพื่อลองทดสอบดู 18 | 19 | ```html 20 | 43 | ``` 44 | 45 | สามารถดูตัวอย่างการเขียน WebSocket Client ได้จากไฟล์ [index.html](./index.html) 46 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jittagornp/java-native-websocket-server-example/8bc7ba4c6b817d45732eb3f8574b054c623946ae/demo.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket 5 | 22 | 23 | 24 |

WebSocket Demo

25 | 26 |

Browser :

27 |
Status :
28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 | 37 | 84 | 85 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | me.jittagornp.example 8 | java-native-websocket-server-example 9 | 1.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 11 14 | 11 15 | 16 | 17 | -------------------------------------------------------------------------------- /running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jittagornp/java-native-websocket-server-example/8bc7ba4c6b817d45732eb3f8574b054c623946ae/running.png -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/AppStarter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example; 5 | 6 | import me.jittagornp.example.websocket.*; 7 | 8 | import java.io.IOException; 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | /** 12 | * @author jitta 13 | */ 14 | public class AppStarter { 15 | 16 | public static void main(final String[] args) throws IOException, NoSuchAlgorithmException { 17 | 18 | final WebSocketHandler handler = new TextWebSocketHandler() { 19 | @Override 20 | public void onConnect(final WebSocket webSocket) { 21 | System.out.println("Client connected => " + webSocket); 22 | } 23 | 24 | @Override 25 | public void onMessage(final WebSocket webSocket, final String message) { 26 | System.out.println("Client message => " + message); 27 | webSocket.send("Server reply : " + message); 28 | } 29 | 30 | @Override 31 | public void onError(final WebSocket webSocket, final Throwable e) { 32 | System.out.println("Client error => " + webSocket); 33 | System.out.println(e); 34 | } 35 | 36 | @Override 37 | public void onDisconnect(final WebSocket webSocket, final CloseStatus status) { 38 | System.out.println("Client disconnected => " + webSocket); 39 | System.out.println("Close status => " + status); 40 | } 41 | }; 42 | 43 | WebSocketServer.port(80) 44 | .addWebSocketHandler(handler) 45 | .start(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/util/ByteBufferUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.util; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.ReadableByteChannel; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | /** 15 | * @author jitta 16 | */ 17 | public class ByteBufferUtils { 18 | 19 | private ByteBufferUtils() { 20 | 21 | } 22 | 23 | public static ByteBuffer create(final String text) { 24 | final byte[] byteArray = text.getBytes(StandardCharsets.UTF_8); 25 | final ByteBuffer byteBuffer = ByteBuffer.allocate(byteArray.length); 26 | byteBuffer.put(byteArray); 27 | return byteBuffer; 28 | } 29 | 30 | public static ByteBuffer read(final ReadableByteChannel channel, final int bufferSize) throws IOException { 31 | final ByteBuffer buffer = ByteBuffer.allocate(bufferSize); 32 | final List buffers = new ArrayList<>(); 33 | while (true) { 34 | //Read data / Write data from channel to byteBuffer 35 | int status = channel.read(buffer.clear()); 36 | if (status <= 0) { 37 | break; 38 | } 39 | 40 | final boolean hasData = buffer.flip().remaining() > 0; 41 | if (!hasData) { 42 | break; 43 | } 44 | 45 | buffers.add(copy(buffer)); 46 | } 47 | 48 | return concat(buffers); 49 | } 50 | 51 | public static ByteBuffer copy(final ByteBuffer origin) { 52 | final int realSize = origin.remaining(); 53 | final ByteBuffer copy = ByteBuffer.allocate(realSize); 54 | copy.put(origin.array(), origin.position(), origin.limit()); 55 | return copy; 56 | } 57 | 58 | public static ByteBuffer concat(final Collection byteBuffers) { 59 | 60 | if (byteBuffers.isEmpty()) { 61 | return ByteBuffer.allocate(0); 62 | } 63 | 64 | if (byteBuffers.size() == 1) { 65 | return byteBuffers.stream().findFirst().get(); 66 | } 67 | 68 | final int totalSize = byteBuffers.stream() 69 | .map(ByteBuffer::flip) 70 | .mapToInt(ByteBuffer::remaining) 71 | .sum(); 72 | 73 | if (totalSize == 0) { 74 | return ByteBuffer.allocate(0); 75 | } 76 | 77 | final ByteBuffer concatenated = ByteBuffer.allocate(totalSize); 78 | byteBuffers.forEach(buffer -> concatenated.put(buffer.duplicate())); 79 | return concatenated; 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/BinaryWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * @author jitta 10 | */ 11 | public interface BinaryWebSocketHandler extends WebSocketHandler { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/CloseStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | /** 7 | * https://tools.ietf.org/html/rfc6455#section-7.4.1 8 | * 9 | * @author jitta 10 | */ 11 | public enum CloseStatus { 12 | 13 | NORMAL(1000, "Normal close"), 14 | GOING_AWAY(1001, "Going away"), 15 | PROTOCOL_ERROR(1002, "Protocol error"), 16 | REFUSE(1003, "Refuse"), 17 | NO_STATUS_CODE(1005, "No status code"), 18 | ABNORMAL_CLOSE(1006, "Abnormal close"), 19 | NON_UTF8(1007, "Non UTF-8"), 20 | POLICY_VALIDATION(1008, "Policy validation"), 21 | TOO_BIG(1009, "Too big"), 22 | EXTENSION(1010, "Extension"), 23 | UNEXPECTED_CONDITION(1011, "Unexpected condition"), 24 | SERVICE_RESTART(1012, "Service restart"), 25 | TRY_AGAIN_LATER(1013, "Try again later"), 26 | BAD_GATEWAY(1014, "Bad gateway"), 27 | TLS_ERROR(1015, "TLS error"); 28 | 29 | private final int code; 30 | private final String reason; 31 | 32 | private CloseStatus(final int code, final String reason) { 33 | this.code = code; 34 | this.reason = reason; 35 | } 36 | 37 | public int getCode() { 38 | return code; 39 | } 40 | 41 | public String getReason() { 42 | return reason; 43 | } 44 | 45 | public static CloseStatus fromCode(final int code) { 46 | final CloseStatus[] closeStatuses = values(); 47 | for (CloseStatus closeStatus : closeStatuses) { 48 | if (closeStatus.getCode() == code) { 49 | return closeStatus; 50 | } 51 | } 52 | throw new UnsupportedOperationException("Unknown status code"); 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "CloseStatus{" + 58 | "code=" + code + 59 | ", reason='" + reason + '\'' + 60 | '}'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/FrameData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * https://tools.ietf.org/html/rfc6455#section-5.2 10 | *

11 | * 0 1 2 3 12 | * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 13 | * +-+-+-+-+-------+-+-------------+-------------------------------+ 14 | * |F|R|R|R| opcode|M| Payload len | Extended payload length | 15 | * |I|S|S|S| (4) |A| (7) | (16/64) | 16 | * |N|V|V|V| |S| | (if payload len==126/127) | 17 | * | |1|2|3| |K| | | 18 | * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 19 | * | Extended payload length continued, if payload len == 127 | 20 | * + - - - - - - - - - - - - - - - +-------------------------------+ 21 | * | |Masking-key, if MASK set to 1 | 22 | * +-------------------------------+-------------------------------+ 23 | * | Masking-key (continued) | Payload Data | 24 | * +-------------------------------- - - - - - - - - - - - - - - - + 25 | * : Payload Data continued ... : 26 | * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 27 | * | Payload Data continued ... | 28 | * +---------------------------------------------------------------+ 29 | * 30 | * @author jitta 31 | */ 32 | public class FrameData { 33 | 34 | private final boolean isFin; 35 | 36 | private final boolean isRSV1; 37 | 38 | private final boolean isRSV2; 39 | 40 | private final boolean isRSV3; 41 | 42 | private final Opcode opcode; 43 | 44 | private final boolean isMask; 45 | 46 | private final ByteBuffer payloadData; 47 | 48 | public FrameData( 49 | final boolean isFin, 50 | final boolean isRSV1, 51 | final boolean isRSV2, 52 | final boolean isRSV3, 53 | final Opcode opcode, 54 | final boolean isMask, 55 | final ByteBuffer payloadData 56 | ) { 57 | this.isFin = isFin; 58 | this.isRSV1 = isRSV1; 59 | this.isRSV2 = isRSV2; 60 | this.isRSV3 = isRSV3; 61 | this.opcode = opcode; 62 | this.isMask = isMask; 63 | this.payloadData = payloadData; 64 | } 65 | 66 | public boolean isFin() { 67 | return isFin; 68 | } 69 | 70 | public boolean isRSV1() { 71 | return isRSV1; 72 | } 73 | 74 | public boolean isRSV2() { 75 | return isRSV2; 76 | } 77 | 78 | public boolean isRSV3() { 79 | return isRSV3; 80 | } 81 | 82 | public Opcode getOpcode() { 83 | return opcode; 84 | } 85 | 86 | public boolean isMask() { 87 | return isMask; 88 | } 89 | 90 | public ByteBuffer getPayloadData() { 91 | return payloadData; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "FrameData{" + 97 | "isFin=" + isFin + 98 | ", isRSV1=" + isRSV1 + 99 | ", isRSV2=" + isRSV2 + 100 | ", isRSV3=" + isRSV3 + 101 | ", opcode=" + opcode + 102 | ", isMask=" + isMask + 103 | ", payloadData=" + payloadData + 104 | '}'; 105 | } 106 | 107 | public static Builder builder() { 108 | return new Builder(); 109 | } 110 | 111 | public static class Builder { 112 | 113 | private boolean isFin; 114 | 115 | private boolean isRSV1; 116 | 117 | private boolean isRSV2; 118 | 119 | private boolean isRSV3; 120 | 121 | private Opcode opcode; 122 | 123 | private boolean isMask; 124 | 125 | private ByteBuffer payloadData; 126 | 127 | public Builder fin(final boolean fin) { 128 | isFin = fin; 129 | return this; 130 | } 131 | 132 | public Builder rsv1(final boolean rsv1) { 133 | isRSV1 = rsv1; 134 | return this; 135 | } 136 | 137 | public Builder rsv2(final boolean rsv2) { 138 | isRSV2 = rsv2; 139 | return this; 140 | } 141 | 142 | public Builder rsv3(final boolean rsv3) { 143 | isRSV3 = rsv3; 144 | return this; 145 | } 146 | 147 | public Builder opcode(final Opcode opcode) { 148 | this.opcode = opcode; 149 | return this; 150 | } 151 | 152 | public Builder mask(final boolean mask) { 153 | isMask = mask; 154 | return this; 155 | } 156 | 157 | public Builder payloadData(final ByteBuffer payloadData) { 158 | this.payloadData = payloadData; 159 | return this; 160 | } 161 | 162 | public FrameData build() { 163 | return new FrameData( 164 | isFin, 165 | isRSV1, 166 | isRSV2, 167 | isRSV3, 168 | opcode, 169 | isMask, 170 | payloadData 171 | ); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/FrameDataByteBufferConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.nio.ByteBuffer; 7 | /** 8 | * @author jitta 9 | */ 10 | interface FrameDataByteBufferConverter { 11 | 12 | FrameData convertToFrameData(final ByteBuffer byteBuffer); 13 | 14 | ByteBuffer convertToByteBuffer(final FrameData frameData); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/FrameDataByteBufferConverterImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.math.BigInteger; 7 | import java.nio.ByteBuffer; 8 | import java.util.Random; 9 | 10 | /** 11 | * @author jitta 12 | */ 13 | class FrameDataByteBufferConverterImpl implements FrameDataByteBufferConverter { 14 | 15 | //1000 0000 16 | private static final byte FIN_BITS = (byte) 0b10000000; 17 | 18 | //0100 0000 19 | private static final byte RSV1_BITS = (byte) 0b01000000; 20 | 21 | //0010 0000 22 | private static final byte RSV2_BITS = (byte) 0b00100000; 23 | 24 | //0001 0000 25 | private static final byte RSV3_BITS = (byte) 0b00010000; 26 | 27 | //0000 1111 28 | private static final byte OPCODE_BITS = (byte) 0b00001111; 29 | 30 | //1000 0000 31 | private static final byte MASK_BITS = (byte) 0b10000000; 32 | 33 | //0111 1111 34 | private static final byte PAYLOAD_LENGTH_BITS = (byte) 0b01111111; 35 | 36 | private final Random random = new Random(); 37 | 38 | @Override 39 | public FrameData convertToFrameData(final ByteBuffer byteBuffer) { 40 | 41 | final byte firstByte = byteBuffer.get(); 42 | 43 | final boolean isFin = (((byte) (firstByte & FIN_BITS)) >> 7 & 1) == 1; 44 | final boolean isRSV1 = (((byte) (firstByte & RSV1_BITS)) >> 6 & 1) == 1; 45 | final boolean isRSV2 = (((byte) (firstByte & RSV2_BITS)) >> 5 & 1) == 1; 46 | final boolean isRSV3 = (((byte) (firstByte & RSV3_BITS)) >> 4 & 1) == 1; 47 | 48 | final byte opcodeByteValue = (byte) (firstByte & OPCODE_BITS); 49 | final Opcode opcode = Opcode.fromByteValue(opcodeByteValue); 50 | 51 | //========================================== 52 | final byte secondByte = byteBuffer.get(); 53 | 54 | final boolean isMask = (((byte) (secondByte & MASK_BITS)) >> 7 & 1) == 1; 55 | final byte payloadLength = (byte) (secondByte & PAYLOAD_LENGTH_BITS); 56 | 57 | //========================================== 58 | int payloadDataBufferSize = getPayloadDataBufferSize(payloadLength, byteBuffer); 59 | final ByteBuffer payloadData = ByteBuffer.allocate(payloadDataBufferSize); 60 | 61 | //========================================== 62 | if (isMask) { 63 | final ByteBuffer maskingKey = ByteBuffer.allocate(4); 64 | maskingKey.put(byteBuffer.get()); 65 | maskingKey.put(byteBuffer.get()); 66 | maskingKey.put(byteBuffer.get()); 67 | maskingKey.put(byteBuffer.get()); 68 | 69 | //XOR 70 | for (int i = 0; byteBuffer.hasRemaining(); i++) { 71 | final byte encoded = (byte) (byteBuffer.get() ^ maskingKey.get(i % 4)); 72 | payloadData.put(encoded); 73 | } 74 | } else { 75 | payloadData.put(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); 76 | } 77 | 78 | return FrameData.builder() 79 | .fin(isFin) 80 | .rsv1(isRSV1) 81 | .rsv2(isRSV2) 82 | .rsv3(isRSV3) 83 | .opcode(opcode) 84 | .mask(isMask) 85 | .payloadData(payloadData) 86 | .build(); 87 | } 88 | 89 | private int getPayloadDataBufferSize(final byte payloadLength, final ByteBuffer byteBuffer) { 90 | 91 | if (payloadLength >= 0 && payloadLength <= 125) { 92 | return payloadLength; 93 | } 94 | 95 | if (payloadLength == 126) { 96 | final ByteBuffer extended = ByteBuffer.allocate(2); 97 | extended.put(byteBuffer.get()); 98 | extended.put(byteBuffer.get()); 99 | return new BigInteger(extended.array()).intValue(); 100 | } 101 | 102 | if (payloadLength == 127) { 103 | final ByteBuffer extended = ByteBuffer.allocate(8); 104 | extended.put(byteBuffer.get()); 105 | extended.put(byteBuffer.get()); 106 | extended.put(byteBuffer.get()); 107 | extended.put(byteBuffer.get()); 108 | extended.put(byteBuffer.get()); 109 | extended.put(byteBuffer.get()); 110 | extended.put(byteBuffer.get()); 111 | extended.put(byteBuffer.get()); 112 | return new BigInteger(extended.array()).intValue(); 113 | } 114 | 115 | throw new UnsupportedOperationException("Invalid payload length"); 116 | } 117 | 118 | @Override 119 | public ByteBuffer convertToByteBuffer(final FrameData frameData) { 120 | 121 | final ByteBuffer payloadData = frameData.getPayloadData().flip(); 122 | 123 | //========================================== 124 | byte firstByte = (byte) 0b00000000; 125 | 126 | //FIN: 1 bit 127 | if (frameData.isFin()) { 128 | firstByte |= FIN_BITS; 129 | } 130 | 131 | //RSV1, RSV2, RSV3: 1 bit each 132 | if (frameData.isRSV1()) { 133 | firstByte |= RSV1_BITS; 134 | } 135 | 136 | if (frameData.isRSV2()) { 137 | firstByte |= RSV2_BITS; 138 | } 139 | 140 | if (frameData.isRSV3()) { 141 | firstByte |= RSV3_BITS; 142 | } 143 | 144 | //Opcode: 4 bits 145 | firstByte |= frameData.getOpcode().getByteValue(); 146 | 147 | //========================================== 148 | //Mask: 1 bit (1000 0000 or 0000 0000) 149 | final byte maskBits = frameData.isMask() ? MASK_BITS : (byte) 0b00000000; 150 | 151 | //Payload length: 7 bits, 7+16 bits, or 7+64 bits 152 | final ByteBuffer payloadLength = buildPayloadLengthByteBuffer(payloadData.remaining(), maskBits).flip(); 153 | 154 | //========================================== 155 | //Allocate frame buffer 156 | final int maskingKeySize = frameData.isMask() ? 4 : 0; 157 | final int frameBufferSize = 1 + payloadLength.remaining() + maskingKeySize + payloadData.remaining(); 158 | final ByteBuffer frameBuffer = ByteBuffer.allocate(frameBufferSize); 159 | 160 | frameBuffer.put(firstByte); 161 | frameBuffer.put(payloadLength.array()); 162 | 163 | //========================================== 164 | //Masking-key: 0 or 4 bytes 165 | if (frameData.isMask()) { 166 | final ByteBuffer maskingKey = randomMaskingKey(); 167 | frameBuffer.put(maskingKey.array()); 168 | 169 | //XOR 170 | for (int i = 0; payloadData.hasRemaining(); i++) { 171 | final byte encoded = (byte) (payloadData.get() ^ maskingKey.get(i % 4)); 172 | frameBuffer.put(encoded); 173 | } 174 | } else { 175 | frameBuffer.put(payloadData.array(), payloadData.position(), payloadData.limit()); 176 | } 177 | 178 | return frameBuffer; 179 | } 180 | 181 | private ByteBuffer randomMaskingKey() { 182 | final ByteBuffer maskingKey = ByteBuffer.allocate(4); 183 | maskingKey.putInt(random.nextInt()); 184 | return maskingKey; 185 | } 186 | 187 | private ByteBuffer buildPayloadLengthByteBuffer(final int length, final byte maskBits) { 188 | 189 | if (length >= 0 && length <= 125) { 190 | //1 byte = 1 bit + 7 bits 191 | return buildPayloadLengthAndExtended( 192 | length, 193 | maskBits, 194 | 1, 195 | (byte) length 196 | ); 197 | } 198 | 199 | if (length <= 65535) { 200 | //3 bytes = 1 bit + 7+16 bits 201 | return buildPayloadLengthAndExtended( 202 | length, 203 | maskBits, 204 | 3, 205 | (byte) 126 206 | ); 207 | } 208 | 209 | //9 bytes = 1 bit + 7+64 bits 210 | return buildPayloadLengthAndExtended( 211 | length, 212 | maskBits, 213 | 9, 214 | (byte) 127 215 | ); 216 | } 217 | 218 | private ByteBuffer buildPayloadLengthAndExtended( 219 | final int extendedLength, 220 | final byte maskBits, 221 | final int byteCapacity, 222 | final byte payloadLength 223 | ) { 224 | final ByteBuffer buffer = ByteBuffer.allocate(byteCapacity); 225 | final byte firstByte = (byte) (payloadLength | maskBits); 226 | buffer.put(firstByte); 227 | if (byteCapacity > 1) { 228 | final byte[] extended = convertToByteArray(extendedLength, byteCapacity - 1); 229 | buffer.put(extended); 230 | } 231 | return buffer; 232 | } 233 | 234 | private byte[] convertToByteArray(final int value, final int byteCapacity) { 235 | final ByteBuffer buffer = ByteBuffer.allocate(byteCapacity); 236 | buffer.putInt(value); 237 | return buffer.array(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/MultipleWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.math.BigInteger; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author jitta 13 | */ 14 | class MultipleWebSocketHandler implements WebSocketHandler { 15 | 16 | private List handlers; 17 | 18 | public List getHandlers() { 19 | if (handlers == null) { 20 | handlers = new LinkedList<>(); 21 | } 22 | return handlers; 23 | } 24 | 25 | public void setHandlers(final List handlers) { 26 | this.handlers = handlers; 27 | } 28 | 29 | @Override 30 | public void onConnect(final WebSocket webSocket) { 31 | handlers.stream() 32 | .forEach(handler -> { 33 | try { 34 | handler.onConnect(webSocket); 35 | } catch (final Throwable e) { 36 | handleError(handler, webSocket, e); 37 | } 38 | }); 39 | } 40 | 41 | @Override 42 | public void onMessage(final WebSocket webSocket, final FrameData frameData) { 43 | handlers.stream() 44 | .forEach(handler -> handleMessage(handler, webSocket, frameData)); 45 | } 46 | 47 | @Override 48 | public void onError(final WebSocket webSocket, final Throwable e) { 49 | handlers.stream() 50 | .forEach(handler -> handleError(handler, webSocket, e)); 51 | } 52 | 53 | @Override 54 | public void onDisconnect(final WebSocket webSocket, final CloseStatus status) { 55 | handlers.stream() 56 | .forEach(handler -> handleConnectionCloseFrame(handler, webSocket, null)); 57 | } 58 | 59 | private void handleError(final WebSocketHandler handler, final WebSocket webSocket, final Throwable e) { 60 | try { 61 | handler.onError(webSocket, e); 62 | } catch (final Throwable ex) { 63 | ex.printStackTrace(); 64 | } 65 | } 66 | 67 | private void handleMessage(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 68 | 69 | /** 70 | * |Opcode | Meaning | Reference | 71 | * -+--------+-------------------------------------+-----------| 72 | * | 0 | Continuation Frame | RFC 6455 | 73 | * -+--------+-------------------------------------+-----------| 74 | * | 1 | Text Frame | RFC 6455 | 75 | * -+--------+-------------------------------------+-----------| 76 | * | 2 | Binary Frame | RFC 6455 | 77 | * -+--------+-------------------------------------+-----------| 78 | * | 8 | Connection Close Frame | RFC 6455 | 79 | * -+--------+-------------------------------------+-----------| 80 | * | 9 | Ping Frame | RFC 6455 | 81 | * -+--------+-------------------------------------+-----------| 82 | * | 10 | Pong Frame | RFC 6455 | 83 | * -+--------+-------------------------------------+-----------| 84 | */ 85 | 86 | final Opcode opcode = frameData.getOpcode(); 87 | System.out.println("opcode : " + opcode); 88 | 89 | if (opcode == Opcode.CONTINUATION_FRAME) { 90 | handleContinuationFrame(handler, webSocket, frameData); 91 | } else if (opcode == Opcode.TEXT_FRAME) { 92 | handleTextFrame(handler, webSocket, frameData); 93 | } else if (opcode == Opcode.BINARY_FRAME) { 94 | handleBinaryFrame(handler, webSocket, frameData); 95 | } else if (opcode == Opcode.CONNECTION_CLOSE) { 96 | handleConnectionCloseFrame(handler, webSocket, convertToCloseStatus(frameData.getPayloadData().array())); 97 | } else if (opcode == Opcode.PING) { 98 | handlePingFrame(handler, webSocket, frameData); 99 | } else if (opcode == Opcode.PONG) { 100 | handlePongFrame(handler, webSocket, frameData); 101 | } else { 102 | throw new UnsupportedOperationException("Unknown opcode " + opcode); 103 | } 104 | } 105 | 106 | private void handleContinuationFrame(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 107 | try { 108 | if (handler instanceof TextWebSocketHandler) { 109 | final String text = new String(frameData.getPayloadData().array(), StandardCharsets.UTF_8); 110 | handler.onMessage(webSocket, text); 111 | } else if (handler instanceof BinaryWebSocketHandler) { 112 | handler.onMessage(webSocket, frameData.getPayloadData()); 113 | } else { 114 | handler.onMessage(webSocket, frameData); 115 | } 116 | } catch (final Throwable e) { 117 | handleError(handler, webSocket, e); 118 | } 119 | } 120 | 121 | private void handleTextFrame(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 122 | try { 123 | if (handler instanceof TextWebSocketHandler) { 124 | final String text = new String(frameData.getPayloadData().array(), StandardCharsets.UTF_8); 125 | handler.onMessage(webSocket, text); 126 | } else { 127 | handler.onMessage(webSocket, frameData); 128 | } 129 | } catch (final Throwable e) { 130 | handleError(handler, webSocket, e); 131 | } 132 | } 133 | 134 | private void handleBinaryFrame(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 135 | try { 136 | if (handler instanceof BinaryWebSocketHandler) { 137 | handler.onMessage(webSocket, frameData.getPayloadData()); 138 | } else { 139 | handler.onMessage(webSocket, frameData); 140 | } 141 | } catch (final Throwable e) { 142 | handleError(handler, webSocket, e); 143 | } 144 | } 145 | 146 | private void handleConnectionCloseFrame(final WebSocketHandler handler, final WebSocket webSocket, final CloseStatus status) { 147 | try { 148 | handler.onDisconnect(webSocket, status); 149 | } catch (final Throwable e) { 150 | handleError(handler, webSocket, e); 151 | } 152 | } 153 | 154 | private CloseStatus convertToCloseStatus(final byte[] byteArray) { 155 | if (byteArray.length == 0) { 156 | return CloseStatus.NORMAL; 157 | } 158 | final int code = new BigInteger(byteArray).intValue(); 159 | return CloseStatus.fromCode(code); 160 | } 161 | 162 | private void handlePingFrame(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 163 | //TODO 164 | } 165 | 166 | private void handlePongFrame(final WebSocketHandler handler, final WebSocket webSocket, final FrameData frameData) { 167 | //TODO 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/Opcode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | /** 7 | * Opcode: 4 bits 8 | *

9 | * Defines the interpretation of the "Payload data". If an unknown 10 | * opcode is received, the receiving endpoint MUST _Fail the 11 | * WebSocket Connection_. The following values are defined. 12 | *

13 | * * %x0 denotes a continuation frame 14 | *

15 | * * %x1 denotes a text frame 16 | *

17 | * * %x2 denotes a binary frame 18 | *

19 | * * %x3-7 are reserved for further non-control frames 20 | *

21 | * * %x8 denotes a connection close 22 | *

23 | * * %x9 denotes a ping 24 | *

25 | * * %xA denotes a pong 26 | *

27 | * * %xB-F are reserved for further control frames 28 | * 29 | * @author jitta 30 | */ 31 | public enum Opcode { 32 | 33 | CONTINUATION_FRAME((byte) 0b00000000), 34 | TEXT_FRAME((byte) 0b00000001), 35 | BINARY_FRAME((byte) 0b00000010), 36 | CONNECTION_CLOSE((byte) 0b00001000), 37 | PING((byte) 0b00001001), 38 | PONG((byte) 0b00001010); 39 | 40 | private final byte byteValue; 41 | 42 | private Opcode(final byte byteValue) { 43 | this.byteValue = byteValue; 44 | } 45 | 46 | public byte getByteValue() { 47 | return byteValue; 48 | } 49 | 50 | public static Opcode fromByteValue(final byte byteValue) { 51 | final Opcode[] opcodes = values(); 52 | for (Opcode opcode : opcodes) { 53 | if (opcode.getByteValue() == byteValue) { 54 | return opcode; 55 | } 56 | } 57 | throw new UnsupportedOperationException("Unknown opcode of " + byteValue); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/TextWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | /** 7 | * @author jitta 8 | */ 9 | public interface TextWebSocketHandler extends WebSocketHandler { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/WebSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * @author jitta 10 | */ 11 | public interface WebSocket { 12 | 13 | String getSessionId(); 14 | 15 | void send(final String message); 16 | 17 | void send(final ByteBuffer message); 18 | 19 | void send(final FrameData message); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/WebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | /** 7 | * @author jitta 8 | */ 9 | public interface WebSocketHandler { 10 | 11 | void onConnect(final WebSocket webSocket); 12 | 13 | void onMessage(final WebSocket webSocket, final T message); 14 | 15 | void onError(final WebSocket webSocket, final Throwable e); 16 | 17 | void onDisconnect(final WebSocket webSocket, final CloseStatus status); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/WebSocketImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import me.jittagornp.example.util.ByteBufferUtils; 7 | import java.nio.ByteBuffer; 8 | import java.util.*; 9 | 10 | /** 11 | * @author jitta 12 | */ 13 | class WebSocketImpl implements WebSocket { 14 | 15 | private String sessionId; 16 | 17 | private boolean handshake; 18 | 19 | private final Queue messageQueue; 20 | 21 | public WebSocketImpl() { 22 | this.messageQueue = new LinkedList<>(); 23 | this.sessionId = UUID.randomUUID().toString(); 24 | } 25 | 26 | @Override 27 | public String getSessionId() { 28 | return sessionId; 29 | } 30 | 31 | public boolean isHandshake() { 32 | return handshake; 33 | } 34 | 35 | public void setHandshake(final boolean handshake) { 36 | this.handshake = handshake; 37 | } 38 | 39 | public Queue getMessageQueue() { 40 | return messageQueue; 41 | } 42 | 43 | @Override 44 | public void send(final String message) { 45 | send( 46 | FrameData.builder() 47 | .fin(true) 48 | .rsv1(false) 49 | .rsv2(false) 50 | .rsv3(false) 51 | .opcode(Opcode.TEXT_FRAME) 52 | .mask(false) 53 | .payloadData(ByteBufferUtils.create(message)) 54 | .build() 55 | ); 56 | } 57 | 58 | @Override 59 | public void send(final ByteBuffer message) { 60 | send( 61 | FrameData.builder() 62 | .fin(true) 63 | .rsv1(false) 64 | .rsv2(false) 65 | .rsv3(false) 66 | .opcode(Opcode.BINARY_FRAME) 67 | .mask(false) 68 | .payloadData(message) 69 | .build() 70 | ); 71 | } 72 | 73 | @Override 74 | public void send(final FrameData message) { 75 | messageQueue.add(message); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return "WebSocket{" + 81 | "sessionId='" + sessionId + '\'' + 82 | '}'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/me/jittagornp/example/websocket/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-Current jittagornp.me 3 | */ 4 | package me.jittagornp.example.websocket; 5 | 6 | import me.jittagornp.example.util.ByteBufferUtils; 7 | import java.io.IOException; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.InetSocketAddress; 10 | import java.nio.ByteBuffer; 11 | import java.nio.channels.*; 12 | import java.nio.charset.StandardCharsets; 13 | import java.security.MessageDigest; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.util.*; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | /** 20 | * Implement follow RFC6455 (The WebSocket Protocol) 21 | * https://tools.ietf.org/html/rfc6455 22 | * 23 | * @author jitta 24 | */ 25 | public class WebSocketServer { 26 | 27 | private static final int READ_BUFFER_SIZE = 100; //100 bytes 28 | 29 | private static final String RFC6455_CONSTANT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 30 | 31 | private final int port; 32 | 33 | private final FrameDataByteBufferConverter converter; 34 | 35 | private ServerSocketChannel serverSocketChannel; 36 | 37 | private MultipleWebSocketHandler handler; 38 | 39 | private WebSocketServer(final int port) { 40 | this.port = port; 41 | this.handler = new MultipleWebSocketHandler(); 42 | this.converter = new FrameDataByteBufferConverterImpl(); 43 | } 44 | 45 | public static WebSocketServer port(final int port) { 46 | return new WebSocketServer(port); 47 | } 48 | 49 | public WebSocketServer addWebSocketHandler(final WebSocketHandler handler) { 50 | this.handler.getHandlers().add(handler); 51 | return this; 52 | } 53 | 54 | public WebSocketServer setHandlers(final List handlers) { 55 | this.handler.setHandlers(handlers); 56 | return this; 57 | } 58 | 59 | public void start() throws IOException, NoSuchAlgorithmException { 60 | 61 | System.out.println("WebSocketServer started on port " + port); 62 | 63 | //1. Define server channel 64 | serverSocketChannel = ServerSocketChannel.open(); 65 | serverSocketChannel.bind(new InetSocketAddress(port)); 66 | 67 | //2. Define selector for monitor channels 68 | final Selector selector = Selector.open(); 69 | serverSocketChannel.configureBlocking(false); 70 | serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 71 | 72 | //3. Event loop for monitor channels 73 | while (true) { 74 | final int readyChannels = selector.selectNow(); 75 | if (readyChannels > 0) { 76 | 77 | final Set selectedKeys = selector.selectedKeys(); 78 | final Iterator keyIterator = selectedKeys.iterator(); 79 | 80 | while (keyIterator.hasNext()) { 81 | 82 | final SelectionKey key = keyIterator.next(); 83 | 84 | if (key.isAcceptable()) { 85 | 86 | handleAcceptable(selector); 87 | 88 | } else if (key.isReadable()) { 89 | 90 | handleReadable((SocketChannel) key.channel(), (WebSocketImpl) key.attachment()); 91 | 92 | } else if (key.isWritable()) { 93 | 94 | handleWritable((SocketChannel) key.channel(), (WebSocketImpl) key.attachment()); 95 | } 96 | 97 | keyIterator.remove(); 98 | } 99 | } 100 | } 101 | } 102 | 103 | private void handleAcceptable(final Selector selector) throws IOException { 104 | final SocketChannel channel = serverSocketChannel.accept(); 105 | final WebSocketImpl webSocket = new WebSocketImpl(); 106 | channel.configureBlocking(false); 107 | channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, webSocket); 108 | } 109 | 110 | private void handleReadable(final SocketChannel channel, final WebSocketImpl webSocket) throws IOException, NoSuchAlgorithmException { 111 | final ByteBuffer buffer = readByteBuffer(channel, webSocket); 112 | final boolean hasData = (buffer != null) && (buffer.remaining() > 0); 113 | if (hasData) { 114 | if (webSocket.isHandshake()) { 115 | processFrameData(channel, webSocket, buffer); 116 | } else { 117 | final String secWebSocketKey = getSecWebSocketKey(buffer); 118 | handShake(channel, webSocket, secWebSocketKey); 119 | } 120 | } 121 | } 122 | 123 | private void handleWritable(final SocketChannel channel, final WebSocketImpl webSocket) { 124 | final Queue queue = webSocket.getMessageQueue(); 125 | while (!queue.isEmpty()) { 126 | try { 127 | //Take element from queue 128 | final FrameData frameData = queue.poll(); 129 | final ByteBuffer frameBuffer = converter.convertToByteBuffer(frameData).flip(); 130 | channel.write(frameBuffer); 131 | } catch (final IOException e) { 132 | handler.onError(webSocket, e); 133 | } 134 | } 135 | } 136 | 137 | private void handShake(final SocketChannel channel, final WebSocketImpl webSocket, final String secWebSocketKey) throws IOException, NoSuchAlgorithmException { 138 | if (secWebSocketKey != null) { 139 | final String response = buildHandshakeResponse(secWebSocketKey); 140 | final ByteBuffer byteBuffer = ByteBufferUtils.create(response).flip(); 141 | 142 | channel.write(byteBuffer); 143 | webSocket.setHandshake(true); 144 | 145 | System.out.println("==============================="); 146 | System.out.println("WebSocket Handshake"); 147 | System.out.println("Request Sec-WebSocket-Key : " + secWebSocketKey); 148 | System.out.println("-------------------------------"); 149 | System.out.println("Http Response : "); 150 | System.out.println(response); 151 | 152 | handler.onConnect(webSocket); 153 | } 154 | } 155 | 156 | private String getSecWebSocketKey(final ByteBuffer byteBuffer) { 157 | final String text = new String(byteBuffer.array(), StandardCharsets.UTF_8); 158 | final boolean isHttpGET = text.startsWith("GET /"); 159 | if (!isHttpGET) { 160 | return null; 161 | } 162 | 163 | System.out.println("==============================="); 164 | System.out.println("Http Request"); 165 | System.out.println(text); 166 | 167 | final Pattern pattern = Pattern.compile("Sec-WebSocket-Key: (.*?)\\r\\n"); 168 | final Matcher matcher = pattern.matcher(text); 169 | matcher.find(); 170 | return matcher.group(1); 171 | } 172 | 173 | private String buildAcceptKey(final String secWebSocketKey) throws NoSuchAlgorithmException { 174 | final String concatKey = secWebSocketKey + RFC6455_CONSTANT; 175 | final byte[] sha1Bytes = MessageDigest.getInstance("SHA-1").digest(concatKey.getBytes(StandardCharsets.UTF_8)); 176 | return Base64.getEncoder().encodeToString(sha1Bytes); 177 | } 178 | 179 | private String buildHandshakeResponse(final String secWebSocketKey) throws NoSuchAlgorithmException, UnsupportedEncodingException { 180 | final String secWebSocketAccept = buildAcceptKey(secWebSocketKey); 181 | return new StringBuilder() 182 | .append("HTTP/1.1 101 Switching Protocols\r\n") 183 | .append("Connection: Upgrade\r\n") 184 | .append("Upgrade: websocket\r\n") 185 | .append("Sec-WebSocket-Accept: ").append(secWebSocketAccept).append("\r\n\r\n") 186 | .toString(); 187 | } 188 | 189 | private void processFrameData(final SocketChannel channel, final WebSocketImpl webSocket, final ByteBuffer byteBuffer) { 190 | try { 191 | final FrameData frameData = converter.convertToFrameData(byteBuffer); 192 | if (frameData.getOpcode() == Opcode.CONNECTION_CLOSE) channel.close(); 193 | handler.onMessage(webSocket, frameData); 194 | } catch (final Throwable e) { 195 | handler.onError(webSocket, e); 196 | } 197 | } 198 | 199 | private ByteBuffer readByteBuffer(final SocketChannel channel, final WebSocketImpl webSocket) { 200 | ByteBuffer buffer = null; 201 | try { 202 | buffer = ByteBufferUtils.read(channel, READ_BUFFER_SIZE).flip(); 203 | } catch (final IOException e) { 204 | handler.onError(webSocket, e); 205 | } 206 | return buffer; 207 | } 208 | 209 | public void stop() throws IOException { 210 | serverSocketChannel.close(); 211 | handler.getHandlers().clear(); 212 | } 213 | } 214 | --------------------------------------------------------------------------------