├── .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 |  14 | 15 |  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 |
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
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