Skip to content

Commit a5eea07

Browse files
committed
Full non-fragmented text WebSocket support. Closing connection. Refactoring.
1 parent 402b411 commit a5eea07

File tree

9 files changed

+1404
-91
lines changed

9 files changed

+1404
-91
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ sysinfo.txt
3535
# Builds
3636
*.apk
3737
*.unitypackage
38+
39+
# macOS
40+
.DS_Store
41+
*.DS_Store

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
{
1+
{
22
"files.exclude":
33
{
44
"**/.DS_Store":true,
55
"**/.git":true,
6-
"**/.gitignore":true,
6+
"**/.gitignore":false,
77
"**/.gitmodules":true,
88
"**/*.booproj":true,
99
"**/*.pidb":true,

Assets/SomeDemoScript.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
using System.Collections.Generic;
33
using UnityEngine;
44

5+
using WebSocketServer;
6+
57
public class SomeDemoScript : MonoBehaviour
68
{
7-
public void onMessageReceived (string message) {
8-
Debug.Log("Received new message: " + message);
9+
public void onMessageReceived (WebSocketMessage message) {
10+
Debug.Log("Received new message: " + message.data);
911
}
1012
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using UnityEngine;
2+
3+
using System;
4+
using System.Net;
5+
using System.Net.Sockets;
6+
using System.Collections.Concurrent;
7+
// For parsing the client websocket requests
8+
using System.Text;
9+
using System.Text.RegularExpressions;
10+
// For creating a thread
11+
using System.Threading;
12+
13+
14+
namespace WebSocketServer {
15+
16+
public struct WebSocketMessage {
17+
public WebSocketMessage(WebSocketConnection connection, string data) {
18+
this.id = Guid.NewGuid().ToString();
19+
this.connection = connection;
20+
this.data = data;
21+
}
22+
23+
public string id { get; }
24+
public WebSocketConnection connection { get; }
25+
public string data { get; }
26+
}
27+
28+
public class WebSocketConnection {
29+
30+
private string id;
31+
private TcpClient client;
32+
private NetworkStream stream;
33+
private WebSocketServer server;
34+
private Thread connectionHandler;
35+
36+
public WebSocketConnection(TcpClient client, WebSocketServer server) {
37+
this.id = Guid.NewGuid().ToString();
38+
this.client = client;
39+
this.stream = client.GetStream();
40+
this.server = server;
41+
}
42+
43+
public bool Establish() {
44+
// Wait for enough bytes to be available
45+
while (!stream.DataAvailable);
46+
while(client.Available < 3);
47+
// Translate bytes of request to a RequestHeader object
48+
Byte[] bytes = new Byte[client.Available];
49+
stream.Read(bytes, 0, bytes.Length);
50+
RequestHeader request = new RequestHeader(Encoding.UTF8.GetString(bytes));
51+
52+
// Check if the request complies with WebSocket protocol.
53+
if (WebSocketProtocol.CheckConnectionHandshake(request)) {
54+
// If so, initiate the connection by sending a reply according to protocol.
55+
Byte[] response = WebSocketProtocol.CreateHandshakeReply(request);
56+
stream.Write(response, 0, response.Length);
57+
58+
Debug.Log("WebSocket client connected.");
59+
60+
// Start message handling
61+
connectionHandler = new Thread(new ThreadStart(HandleConnection));
62+
connectionHandler.IsBackground = true;
63+
connectionHandler.Start();
64+
65+
// Call the server callback.
66+
server.OnOpen(this);
67+
return true;
68+
} else {
69+
return false;
70+
}
71+
}
72+
73+
private void HandleConnection () {
74+
while (true) {
75+
WebSocketDataFrame dataframe = ReadDataFrame();
76+
77+
if (dataframe.fin) {
78+
if ((WebSocketOpCode)dataframe.opcode == WebSocketOpCode.Text) {
79+
// Let the server know of the message.
80+
string data = WebSocketProtocol.DecodeText(dataframe);
81+
WebSocketMessage message = new WebSocketMessage(this, data);
82+
server.messages.Enqueue(message);
83+
} else if ((WebSocketOpCode)dataframe.opcode == WebSocketOpCode.Close) {
84+
// Handle closing the connection
85+
Debug.Log("Client closed the connection.");
86+
stream.Close();
87+
client.Close();
88+
server.OnClose(this);
89+
break;
90+
}
91+
} else {
92+
Debug.Log("Framentation encoutered.");
93+
}
94+
}
95+
}
96+
97+
98+
private WebSocketDataFrame ReadDataFrame() {
99+
const int DataframeHead = 2; // Length of dataframe head
100+
const int ShortPayloadLength = 2; // Length of a short payload length field
101+
const int LongPayloadLength = 8; // Length of a long payload length field
102+
const int Mask = 4; // Length of the payload mask
103+
104+
// Wait for a dataframe head to be available, then read the data.
105+
while (!stream.DataAvailable && client.Available < DataframeHead);
106+
Byte[] bytes = new Byte[DataframeHead];
107+
stream.Read(bytes, 0, DataframeHead);
108+
109+
// Decode the message head, including FIN, OpCode, and initial byte of the payload length.
110+
WebSocketDataFrame dataframe = WebSocketProtocol.CreateDataFrame();
111+
WebSocketProtocol.ParseDataFrameHead(bytes, ref dataframe);
112+
113+
// Depending on the dataframe length, read & decode the next bytes for payload length
114+
if (dataframe.length == 126) {
115+
while (client.Available < ShortPayloadLength); // Wait until data is available
116+
Array.Resize(ref bytes, bytes.Length + ShortPayloadLength);
117+
stream.Read(bytes, bytes.Length - ShortPayloadLength, ShortPayloadLength); // Read the next two bytes for length
118+
} else if (dataframe.length == 127) {
119+
while (client.Available < LongPayloadLength); // Wait until data is available
120+
Array.Resize(ref bytes, bytes.Length + LongPayloadLength);
121+
stream.Read(bytes, bytes.Length - LongPayloadLength, LongPayloadLength); // Read the next two bytes for length
122+
}
123+
WebSocketProtocol.ParseDataFrameLength(bytes, ref dataframe); // Parse the length
124+
125+
if (dataframe.mask) {
126+
while (client.Available < Mask); // Wait until data is available
127+
Array.Resize(ref bytes, bytes.Length + Mask);
128+
stream.Read(bytes, bytes.Length - Mask, Mask); // Read the next four bytes for mask
129+
}
130+
131+
while (client.Available < dataframe.length); // Wait until data is available
132+
Array.Resize(ref bytes, bytes.Length + dataframe.length);
133+
stream.Read(bytes, bytes.Length - dataframe.length, dataframe.length); // Read the payload
134+
dataframe.data = bytes;
135+
136+
return dataframe;
137+
}
138+
}
139+
140+
}

Assets/WebSocketServer/WebSocketConnection.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/WebSocketServer/WebSocketProtocol.cs

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@
99

1010
namespace WebSocketServer {
1111

12+
struct WebSocketDataFrame {
13+
public WebSocketDataFrame(bool fin, bool mask, int opcode, int length, int offset, Byte[] data) {
14+
this.fin = fin;
15+
this.mask = mask;
16+
this.opcode = opcode;
17+
this.length = length;
18+
this.offset = offset;
19+
this.data = data;
20+
}
21+
22+
public bool fin { get; set; }
23+
public bool mask { get; set; }
24+
public int opcode { get; set; }
25+
public int length { get; set; }
26+
public int offset { get; set; }
27+
public byte[] data { get; set; }
28+
}
29+
1230
class RequestHeader {
1331

1432
static Regex head = new Regex("^(GET|POST|PUT|DELETE|OPTIONS) (.+) HTTP/([0-9.]+)", RegexOptions.Compiled);
@@ -101,43 +119,70 @@ public static Byte[] CreateHandshakeReply(RequestHeader request) {
101119
return response;
102120
}
103121

104-
public static string DecodeMessage(byte[] bytes) {
122+
public static WebSocketDataFrame CreateDataFrame() {
123+
return new WebSocketDataFrame(false, false, 0, 0, 0, null);
124+
}
125+
126+
public static void ParseDataFrameHead(byte[] bytes, ref WebSocketDataFrame dataframe) {
105127
bool fin = (bytes[0] & 0b10000000) != 0,
106128
mask = (bytes[1] & 0b10000000) != 0; // must be true, "All messages from the client to the server have this bit set"
107129

108-
int opcode = bytes[0] & 0b00001111, // expecting 1 - text message
109-
msglen = bytes[1] - 128, // & 0111 1111
130+
int opcode = bytes[0] & 0b00001111;
131+
int msglen = bytes[1] & 0b01111111,
110132
offset = 2;
133+
134+
dataframe.fin = fin;
135+
dataframe.mask = mask;
136+
dataframe.opcode = opcode;
137+
dataframe.length = msglen;
138+
dataframe.offset = offset;
139+
dataframe.data = bytes;
140+
}
111141

112-
if (msglen == 126) {
142+
public static void ParseDataFrameLength(byte[] bytes, ref WebSocketDataFrame dataframe) {
143+
if (dataframe.length == 126) {
113144
// was ToUInt16(bytes, offset) but the result is incorrect
114-
msglen = BitConverter.ToUInt16(new byte[] { bytes[3], bytes[2] }, 0);
115-
offset = 4;
116-
} else if (msglen == 127) {
117-
Debug.Log("TODO: msglen == 127, needs qword to store msglen");
118-
// i don't really know the byte order, please edit this
119-
// msglen = BitConverter.ToUInt64(new byte[] { bytes[5], bytes[4], bytes[3], bytes[2], bytes[9], bytes[8], bytes[7], bytes[6] }, 0);
120-
// offset = 10;
145+
dataframe.length = BitConverter.ToUInt16(new byte[] { bytes[3], bytes[2] }, 0);
146+
dataframe.offset = 4;
147+
} else if (dataframe.length == 127) {
148+
dataframe.length = (int) BitConverter.ToUInt64(new byte[] {
149+
bytes[9], bytes[8], bytes[7], bytes[6],
150+
bytes[5], bytes[4], bytes[3], bytes[2]
151+
}, 0);
152+
dataframe.offset = 10;
121153
}
154+
}
122155

123-
if (msglen == 0)
124-
Debug.Log("msglen == 0");
125-
else if (mask) {
126-
byte[] decoded = new byte[msglen];
127-
byte[] masks = new byte[4] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] };
128-
offset += 4;
129-
130-
for (int i = 0; i < msglen; ++i)
131-
decoded[i] = (byte)(bytes[offset + i] ^ masks[i % 4]);
156+
public static string DecodeText(WebSocketDataFrame dataframe) {
157+
if (dataframe.length > 0 && dataframe.mask) {
158+
byte[] decoded = new byte[dataframe.length];
159+
byte[] masks = new byte[4] {
160+
dataframe.data[dataframe.offset],
161+
dataframe.data[dataframe.offset + 1],
162+
dataframe.data[dataframe.offset + 2],
163+
dataframe.data[dataframe.offset + 3]
164+
};
165+
int payloadOffset = dataframe.offset + 4;
166+
167+
for (int i = 0; i < dataframe.length; ++i) {
168+
decoded[i] = (byte)(dataframe.data[payloadOffset + i] ^ masks[i % 4]);
169+
}
132170

133171
string text = Encoding.UTF8.GetString(decoded);
134172
return text;
135-
} else {
136-
Debug.Log("mask bit not set");
137173
}
138174
return "";
139175
}
140176

141177
}
142178

179+
enum WebSocketOpCode {
180+
Continuation = 0x0,
181+
Text = 0x1,
182+
Binary = 0x2,
183+
Close = 0x8,
184+
Ping = 0x9,
185+
Pong = 0xA
186+
}
187+
143188
}

0 commit comments

Comments
 (0)