From dbd084cb2cb365b0b53fde3498ec6a92e803347a Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 4 Mar 2014 21:09:12 -0800 Subject: [PATCH] Initial WebSocket projects, handshake, framing. --- .gitignore | 21 ++ WebSockets.sln | 28 ++ .../ClientWebSocket.cs | 209 +++++++++++++ src/Microsoft.Net.WebSockets/Constants.cs | 26 ++ src/Microsoft.Net.WebSockets/FrameHeader.cs | 235 +++++++++++++++ .../Microsoft.Net.WebSockets.csproj | 56 ++++ .../Properties/AssemblyInfo.cs | 36 +++ .../WebSocketClient.cs | 55 ++++ .../Microsoft.Net.WebSockets.Test.csproj | 71 +++++ .../Properties/AssemblyInfo.cs | 36 +++ .../WebSocketClientTests.cs | 284 ++++++++++++++++++ .../packages.config | 5 + 12 files changed, 1062 insertions(+) create mode 100644 .gitignore create mode 100644 WebSockets.sln create mode 100644 src/Microsoft.Net.WebSockets/ClientWebSocket.cs create mode 100644 src/Microsoft.Net.WebSockets/Constants.cs create mode 100644 src/Microsoft.Net.WebSockets/FrameHeader.cs create mode 100644 src/Microsoft.Net.WebSockets/Microsoft.Net.WebSockets.csproj create mode 100644 src/Microsoft.Net.WebSockets/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.Net.WebSockets/WebSocketClient.cs create mode 100644 test/Microsoft.Net.WebSockets.Test/Microsoft.Net.WebSockets.Test.csproj create mode 100644 test/Microsoft.Net.WebSockets.Test/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.Net.WebSockets.Test/WebSocketClientTests.cs create mode 100644 test/Microsoft.Net.WebSockets.Test/packages.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..cb3474e72b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +bin +obj +*.suo +*.user +_ReSharper.* +*.DS_Store +*.userprefs +*.pidb +*.vspx +*.psess +TestResults/* +TestResult.xml +nugetkey +packages +target +artifacts +StyleCop.Cache +node_modules +*.snk +.nuget/NuGet.exe +docs/build diff --git a/WebSockets.sln b/WebSockets.sln new file mode 100644 index 0000000000..98f957f7e5 --- /dev/null +++ b/WebSockets.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30203.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.WebSockets", "src\Microsoft.Net.WebSockets\Microsoft.Net.WebSockets.csproj", "{6C1D09CA-F799-44AE-8EC8-9D19C76080C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.WebSockets.Test", "test\Microsoft.Net.WebSockets.Test\Microsoft.Net.WebSockets.Test.csproj", "{EF1FE910-6E0C-4DE8-8CC1-6118B726A59E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C1D09CA-F799-44AE-8EC8-9D19C76080C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C1D09CA-F799-44AE-8EC8-9D19C76080C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C1D09CA-F799-44AE-8EC8-9D19C76080C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C1D09CA-F799-44AE-8EC8-9D19C76080C1}.Release|Any CPU.Build.0 = Release|Any CPU + {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Microsoft.Net.WebSockets/ClientWebSocket.cs b/src/Microsoft.Net.WebSockets/ClientWebSocket.cs new file mode 100644 index 0000000000..3334508bbd --- /dev/null +++ b/src/Microsoft.Net.WebSockets/ClientWebSocket.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Net.WebSockets +{ + public class ClientWebSocket : WebSocket + { + private readonly Stream _stream; + private readonly string _subProtocl; + private WebSocketState _state; + + private byte[] _receiveBuffer; + private int _receiveOffset; + private int _receiveCount; + + private FrameHeader _frameInProgress; + private long _frameBytesRemaining = 0; + + public ClientWebSocket(Stream stream, string subProtocol, int receiveBufferSize) + { + _stream = stream; + _subProtocl = subProtocol; + _state = WebSocketState.Open; + _receiveBuffer = new byte[receiveBufferSize]; + } + + public override WebSocketCloseStatus? CloseStatus + { + get { throw new NotImplementedException(); } + } + + public override string CloseStatusDescription + { + get { throw new NotImplementedException(); } + } + + public override WebSocketState State + { + get { return _state; } + } + + public override string SubProtocol + { + get { return _subProtocl; } + } + + public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + // TODO: Validate arguments + // TODO: Check state + // TODO: Check concurrent writes + // TODO: Check ping/pong state + // TODO: Masking + FrameHeader frameHeader = new FrameHeader(endOfMessage, GetOpCode(messageType), true, 0, buffer.Count); + ArraySegment segment = frameHeader.Buffer; + await _stream.WriteAsync(segment.Array, segment.Offset, segment.Count, cancellationToken); + await _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken); + } + + private int GetOpCode(WebSocketMessageType messageType) + { + switch (messageType) + { + case WebSocketMessageType.Text: return Constants.OpCodes.TextFrame; + case WebSocketMessageType.Binary: return Constants.OpCodes.BinaryFrame; + case WebSocketMessageType.Close: return Constants.OpCodes.CloseFrame; + default: throw new NotImplementedException(messageType.ToString()); + } + } + + public async override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + // TODO: Validate arguments + // TODO: Check state + // TODO: Check concurrent reads + // TODO: Check ping/pong state + + // No active frame + if (_frameInProgress == null) + { + await EnsureDataAvailableOrReadAsync(2, cancellationToken); + int frameHeaderSize = FrameHeader.CalculateFrameHeaderSize(_receiveBuffer[_receiveOffset + 1]); + await EnsureDataAvailableOrReadAsync(frameHeaderSize, cancellationToken); + _frameInProgress = new FrameHeader(new ArraySegment(_receiveBuffer, _receiveOffset, frameHeaderSize)); + _receiveOffset += frameHeaderSize; + _receiveCount -= frameHeaderSize; + _frameBytesRemaining = _frameInProgress.DataLength; + } + + WebSocketReceiveResult result; + // TODO: Close frame + // TODO: Ping or Pong frames + + // Make sure there's at least some data in the buffer + if (_frameBytesRemaining > 0) + { + await EnsureDataAvailableOrReadAsync(1, cancellationToken); + } + + // Copy buffered data to the users buffer + int bytesToRead = (int)Math.Min((long)buffer.Count, _frameBytesRemaining); + if (_receiveCount > 0) + { + int bytesToCopy = Math.Min(bytesToRead, _receiveCount); + Array.Copy(_receiveBuffer, _receiveOffset, buffer.Array, buffer.Offset, bytesToCopy); + if (bytesToCopy == _frameBytesRemaining) + { + result = new WebSocketReceiveResult(bytesToCopy, GetMessageType(_frameInProgress.OpCode), _frameInProgress.Fin); + _frameInProgress = null; + } + else + { + result = new WebSocketReceiveResult(bytesToCopy, GetMessageType(_frameInProgress.OpCode), false); + } + _frameBytesRemaining -= bytesToCopy; + _receiveCount -= bytesToCopy; + _receiveOffset += bytesToCopy; + } + else + { + // End of an empty frame? + result = new WebSocketReceiveResult(0, GetMessageType(_frameInProgress.OpCode), true); + } + + return result; + } + + private async Task EnsureDataAvailableOrReadAsync(int bytes, CancellationToken cancellationToken) + { + // Insufficient data + while (_receiveCount < bytes && bytes <= _receiveBuffer.Length) + { + // Some data in the buffer, shift down to make room + if (_receiveCount > 0 && _receiveOffset > 0) + { + Array.Copy(_receiveBuffer, _receiveOffset, _receiveBuffer, 0, _receiveCount); + } + _receiveOffset = 0; + // Add to the end + int read = await _stream.ReadAsync(_receiveBuffer, _receiveCount, _receiveBuffer.Length - (_receiveCount), cancellationToken); + if (read == 0) + { + throw new IOException("Unexpected end of stream"); + } + _receiveCount += read; + } + } + + private WebSocketMessageType GetMessageType(int opCode) + { + switch (opCode) + { + case Constants.OpCodes.TextFrame: return WebSocketMessageType.Text; + case Constants.OpCodes.BinaryFrame: return WebSocketMessageType.Binary; + case Constants.OpCodes.CloseFrame: return WebSocketMessageType.Close; + default: throw new NotImplementedException(opCode.ToString()); + } + } + + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // TODO: Validate arguments + // TODO: Check state + // TODO: Check concurrent writes + // TODO: Check ping/pong state + _state = WebSocketState.CloseSent; + + // TODO: Masking + byte[] buffer = Encoding.UTF8.GetBytes(statusDescription); + FrameHeader frameHeader = new FrameHeader(true, Constants.OpCodes.CloseFrame, true, 0, buffer.Length); + ArraySegment segment = frameHeader.Buffer; + await _stream.WriteAsync(segment.Array, segment.Offset, segment.Count, cancellationToken); + await _stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + public override void Abort() + { + if (_state >= WebSocketState.Closed) // or Aborted + { + return; + } + + _state = WebSocketState.Aborted; + _stream.Dispose(); + } + + public override void Dispose() + { + if (_state >= WebSocketState.Closed) // or Aborted + { + return; + } + + _state = WebSocketState.Closed; + _stream.Dispose(); + } + } +} diff --git a/src/Microsoft.Net.WebSockets/Constants.cs b/src/Microsoft.Net.WebSockets/Constants.cs new file mode 100644 index 0000000000..c7ebabaab7 --- /dev/null +++ b/src/Microsoft.Net.WebSockets/Constants.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Net.WebSockets +{ + public static class Constants + { + public static class Headers + { + public const string WebSocketVersion = "Sec-WebSocket-Version"; + public const string SupportedVersion = "13"; + } + + public static class OpCodes + { + public const int TextFrame = 0x1; + public const int BinaryFrame = 0x2; + public const int CloseFrame = 0x8; + public const int PingFrame = 0x8; + public const int PongFrame = 0xA; + } + } +} diff --git a/src/Microsoft.Net.WebSockets/FrameHeader.cs b/src/Microsoft.Net.WebSockets/FrameHeader.cs new file mode 100644 index 0000000000..54d53fc1c7 --- /dev/null +++ b/src/Microsoft.Net.WebSockets/FrameHeader.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Net.WebSockets +{ + public class FrameHeader + { + private byte[] _header; + + public FrameHeader(ArraySegment header) + { + _header = new byte[header.Count]; + Array.Copy(header.Array, header.Offset, _header, 0, _header.Length); + } + + public FrameHeader(bool final, int opCode, bool masked, int maskKey, long dataLength) + { + int headerLength = 2; + if (masked) + { + headerLength += 4; + } + + if (dataLength <= 125) + { + } + else if (125 < dataLength && dataLength <= 0xFFFF) + { + headerLength += 2; + } + else + { + headerLength += 8; + } + _header = new byte[headerLength]; + + Fin = final; + OpCode = opCode; + Masked = masked; + if (masked) + { + MaskKey = maskKey; + } + DataLength = dataLength; + } + + public bool Fin + { + get + { + return (_header[0] & 0x80) == 0x80; + } + private set + { + if (value) + { + _header[0] |= 0x80; + } + else + { + _header[0] &= 0x7F; + } + } + } + + public int OpCode + { + get + { + return (_header[0] & 0xF); + } + private set + { + // TODO: Clear out a prior value? + _header[0] |= (byte)(value & 0xF); + } + } + + public bool Masked + { + get + { + return (_header[1] & 0x80) == 0x80; + } + private set + { + if (value) + { + _header[1] |= 0x80; + } + else + { + _header[1] &= 0x7F; + } + } + } + + public int MaskKey + { + get + { + if (!Masked) + { + return 0; + } + int offset = ExtendedLengthFieldSize + 2; + return (_header[offset] << 24) + (_header[offset + 1] << 16) + + (_header[offset + 2] << 8) + _header[offset + 4]; + } + private set + { + int offset = ExtendedLengthFieldSize + 2; + _header[offset] = (byte)(value >> 24); + _header[offset + 1] = (byte)(value >> 16); + _header[offset + 2] = (byte)(value >> 8); + _header[offset + 3] = (byte)value; + } + } + + public int PayloadField + { + get + { + return (_header[1] & 0x7F); + } + private set + { + // TODO: Clear out a prior value? + _header[1] |= (byte)(value & 0x7F); + } + } + + public int ExtendedLengthFieldSize + { + get + { + int payloadField = PayloadField; + if (payloadField <= 125) + { + return 0; + } + if (payloadField == 126) + { + return 2; + } + return 8; + } + } + + public long DataLength + { + get + { + int extendedFieldSize = ExtendedLengthFieldSize; + if (extendedFieldSize == 0) + { + return PayloadField; + } + if (extendedFieldSize == 2) + { + return (_header[2] << 8) + _header[3]; + } + return (_header[2] << 56) + (_header[3] << 48) + + (_header[4] << 40) + (_header[5] << 32) + + (_header[6] << 24) + (_header[7] << 16) + + (_header[8] << 8) + _header[9]; + } + private set + { + if (value <= 125) + { + PayloadField = (int)value; + } + else if (125 < value && value <= 0xFFFF) + { + PayloadField = 0x7E; + + _header[2] = (byte)(value >> 8); + _header[3] = (byte)value; + } + else + { + PayloadField = 0x7F; + + _header[2] = (byte)(value >> 56); + _header[3] = (byte)(value >> 48); + _header[4] = (byte)(value >> 40); + _header[5] = (byte)(value >> 32); + _header[6] = (byte)(value >> 24); + _header[7] = (byte)(value >> 16); + _header[8] = (byte)(value >> 8); + _header[9] = (byte)value; + } + } + } + + public ArraySegment Buffer + { + get + { + return new ArraySegment(_header); + } + } + + // Given the second bytes of a frame, calculate how long the whole frame header should be. + // Range 2-12 bytes + public static int CalculateFrameHeaderSize(byte b2) + { + int headerLength = 2; + if ((b2 & 0x80) == 0x80) // Masked + { + headerLength += 4; + } + + int payloadField = (b2 & 0x7F); + if (payloadField <= 125) + { + // headerLength += 0 + } + else if (payloadField == 126) + { + headerLength += 2; + } + else + { + headerLength += 8; + } + return headerLength; + } + } +} diff --git a/src/Microsoft.Net.WebSockets/Microsoft.Net.WebSockets.csproj b/src/Microsoft.Net.WebSockets/Microsoft.Net.WebSockets.csproj new file mode 100644 index 0000000000..c0283932c2 --- /dev/null +++ b/src/Microsoft.Net.WebSockets/Microsoft.Net.WebSockets.csproj @@ -0,0 +1,56 @@ + + + + + Debug + AnyCPU + {6C1D09CA-F799-44AE-8EC8-9D19C76080C1} + Library + Properties + Microsoft.Net.WebSockets + Microsoft.Net.WebSockets + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Net.WebSockets/Properties/AssemblyInfo.cs b/src/Microsoft.Net.WebSockets/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b8a0860d30 --- /dev/null +++ b/src/Microsoft.Net.WebSockets/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.Net.WebSockets")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.Net.WebSockets")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9a9e41ae-1494-4d87-a66f-a4019ff68ce5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Microsoft.Net.WebSockets/WebSocketClient.cs b/src/Microsoft.Net.WebSockets/WebSocketClient.cs new file mode 100644 index 0000000000..3cac40fe79 --- /dev/null +++ b/src/Microsoft.Net.WebSockets/WebSocketClient.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Net.WebSockets.Client +{ + public class WebSocketClient + { + static WebSocketClient() + { + try + { + // Only call once + WebSocket.RegisterPrefixes(); + } + catch (Exception) + { + // Already registered + } + } + + public WebSocketClient() + { + ReceiveBufferSize = 1024; + } + + public int ReceiveBufferSize + { + get; + set; + } + + public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); + + request.Headers[Constants.Headers.WebSocketVersion] = Constants.Headers.SupportedVersion; + // TODO: Sub-protocols + + WebResponse response = await request.GetResponseAsync(); + // TODO: Validate handshake + + Stream stream = response.GetResponseStream(); + // Console.WriteLine(stream.CanWrite + " " + stream.CanRead); + + return new ClientWebSocket(stream, null, ReceiveBufferSize); + } + } +} diff --git a/test/Microsoft.Net.WebSockets.Test/Microsoft.Net.WebSockets.Test.csproj b/test/Microsoft.Net.WebSockets.Test/Microsoft.Net.WebSockets.Test.csproj new file mode 100644 index 0000000000..23b2f9f0f3 --- /dev/null +++ b/test/Microsoft.Net.WebSockets.Test/Microsoft.Net.WebSockets.Test.csproj @@ -0,0 +1,71 @@ + + + + + Debug + AnyCPU + {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E} + Library + Properties + Microsoft.Net.WebSockets.Test + Microsoft.Net.WebSockets.Test + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + ..\..\packages\xunit.1.9.2\lib\net20\xunit.dll + + + ..\..\packages\xunit.extensions.1.9.2\lib\net20\xunit.extensions.dll + + + + + + + + + {6c1d09ca-f799-44ae-8ec8-9d19c76080c1} + Microsoft.Net.WebSockets + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Net.WebSockets.Test/Properties/AssemblyInfo.cs b/test/Microsoft.Net.WebSockets.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f30741c6b2 --- /dev/null +++ b/test/Microsoft.Net.WebSockets.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.Net.WebSockets.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.Net.WebSockets.Test")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("237d6e8f-6e5e-4c3f-96b4-b19cf3bf4d80")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/Microsoft.Net.WebSockets.Test/WebSocketClientTests.cs b/test/Microsoft.Net.WebSockets.Test/WebSocketClientTests.cs new file mode 100644 index 0000000000..7ac37e8bc3 --- /dev/null +++ b/test/Microsoft.Net.WebSockets.Test/WebSocketClientTests.cs @@ -0,0 +1,284 @@ +using Microsoft.Net.WebSockets.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Net.WebSockets.Test +{ + public class WebSocketClientTests + { + private static string ClientAddress = "ws://localhost:8080/"; + private static string ServerAddress = "http://localhost:8080/"; + + [Fact] + public async Task Connect_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + clientSocket.Dispose(); + } + } + + [Fact] + public async Task SendShortData_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes("Hello World"); + await clientSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + + byte[] serverBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await serverWebSocketContext.WebSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, serverBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task SendMediumData_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes(new string('a', 130)); + await clientSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Text, true, CancellationToken.None); + + byte[] serverBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await serverWebSocketContext.WebSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(orriginalData, serverBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task SendLongData_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null, 0xFFFF, TimeSpan.FromMinutes(100)); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); + await clientSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Text, true, CancellationToken.None); + + byte[] serverBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await serverWebSocketContext.WebSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + int intermediateCount = result.Count; + Assert.False(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + result = await serverWebSocketContext.WebSocket.ReceiveAsync(new ArraySegment(serverBuffer, intermediateCount, orriginalData.Length - intermediateCount), CancellationToken.None); + intermediateCount += result.Count; + Assert.False(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + result = await serverWebSocketContext.WebSocket.ReceiveAsync(new ArraySegment(serverBuffer, intermediateCount, orriginalData.Length - intermediateCount), CancellationToken.None); + intermediateCount += result.Count; + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, intermediateCount); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + Assert.Equal(orriginalData, serverBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task ReceiveShortData_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes("Hello World"); + await serverWebSocketContext.WebSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + + byte[] clientBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await clientSocket.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task ReceiveMediumData_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes(new string('a', 130)); + await serverWebSocketContext.WebSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + + byte[] clientBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await clientSocket.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task ReceiveLongDataInSmallBuffer_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient(); + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); + await serverWebSocketContext.WebSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + + byte[] clientBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result; + int receivedCount = 0; + do + { + result = await clientSocket.ReceiveAsync(new ArraySegment(clientBuffer, receivedCount, clientBuffer.Length - receivedCount), CancellationToken.None); + receivedCount += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + } + while (!result.EndOfMessage); + + Assert.Equal(orriginalData.Length, receivedCount); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + + clientSocket.Dispose(); + } + } + + [Fact] + public async Task ReceiveLongDataInLargeBuffer_Success() + { + using (HttpListener listener = new HttpListener()) + { + listener.Prefixes.Add(ServerAddress); + listener.Start(); + Task serverAccept = listener.GetContextAsync(); + + WebSocketClient client = new WebSocketClient() { ReceiveBufferSize = 0xFFFFFF }; + Task clientConnect = client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None); + + HttpListenerContext serverContext = await serverAccept; + Assert.True(serverContext.Request.IsWebSocketRequest); + HttpListenerWebSocketContext serverWebSocketContext = await serverContext.AcceptWebSocketAsync(null); + + WebSocket clientSocket = await clientConnect; + + byte[] orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); + await serverWebSocketContext.WebSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + + byte[] clientBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result = await clientSocket.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + + clientSocket.Dispose(); + } + } + } +} diff --git a/test/Microsoft.Net.WebSockets.Test/packages.config b/test/Microsoft.Net.WebSockets.Test/packages.config new file mode 100644 index 0000000000..67a23e70da --- /dev/null +++ b/test/Microsoft.Net.WebSockets.Test/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file