├── .gitignore ├── LICENSE ├── README.md ├── SampleProject └── Assets │ ├── Samples │ ├── Connect │ │ ├── ConnectSample.unity │ │ └── ConnectionSampleScript.cs │ ├── RTT_by_Ping │ │ ├── PingSpeed.cs │ │ └── PingSpeed.unity │ ├── Reconnect │ │ ├── ReconnectSample.unity │ │ └── ReconnectionSampleScript.cs │ └── Timeout │ │ ├── TimeoutSample.unity │ │ └── TimeoutSampleScript.cs │ └── WebuSocket │ ├── Lib │ ├── BouncyCastle.dll │ └── derivedfrom.txt │ ├── WebSocketByteGenerator.cs │ ├── WebSocketEncryption.cs │ └── WebuSocket.cs ├── WebuSocket ├── WebSocketByteGenerator.cs ├── WebSocketEncryption.cs ├── WebuSocket.cs └── lib │ ├── BouncyCastle.dll │ └── derivedfrom.txt └── WebuSocketTests ├── Editor ├── TestRunner.cs └── Tests │ ├── Test0.cs │ ├── Test0.cs.meta │ ├── Test1.cs │ ├── Test1.cs.meta │ ├── Test2.cs │ ├── Test2.cs.meta │ ├── Test3.cs │ ├── Test3.cs.meta │ ├── Test4.cs │ └── Test4.cs.meta └── MiyamasuTestRunner └── Editor ├── MiyamasuTestIgniter.cs └── MiyamasuTestRunner.cs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode/launch.json 3 | 4 | Assembly-CSharp-Editor.csproj 5 | 6 | Assembly-CSharp.csproj 7 | 8 | Assets/VSCode/* 9 | 10 | Library/* 11 | 12 | obj/* 13 | 14 | Temp/* 15 | 16 | WebuSocket.sln 17 | 18 | Assembly-CSharp-firstpass.csproj 19 | 20 | Assets/Plugins/UniRx/* 21 | 22 | Assets/Plugins/UniRx.meta 23 | 24 | 25 | ProjectSettings/ProjectVersion.txt 26 | 27 | *.meta 28 | 29 | SampleProject/.vscode/* 30 | 31 | SampleProject/Assets/VSCode/* 32 | 33 | SampleProject/Library/* 34 | 35 | SampleProject/obj/* 36 | 37 | SampleProject/ProjectSettings/* 38 | 39 | SampleProject/SampleProject.sln 40 | SampleProject/Temp/* 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 sassembla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebuSocket 2 | 3 | WebSocket Client implementation for C#. 4 | ver 0.7.6 5 | 6 | ## motivation 7 | 8 | * async default. 9 | * lightweight. 10 | * thread free. 11 | * task free. 12 | * runnable on C# 3.5 or later(includes Unity). 13 | 14 | ## usage 15 | 16 | ```C# 17 | var webuSocket = new WebuSocket( 18 | "ws://somewhere:someport", 19 | 1024 * 100,// default buffer size. 20 | () => { 21 | // connected. 22 | }, 23 | (Queue> datas) => { 24 | // data received. 25 | 26 | while (0 < datas.Count) { 27 | var data = datas.Dequeue(); 28 | var bytes = new byte[data.Count]; 29 | Buffer.BlockCopy(data.Array, data.Offset, bytes, 0, data.Count); 30 | 31 | // use "bytes". 32 | } 33 | }, 34 | () => { 35 | // pinged. 36 | }, 37 | closeReason => { 38 | // closed. 39 | }, 40 | (errorReason, e) => { 41 | // error happedned. 42 | }, 43 | customHeaderKeyValues // Dictionary which can send with connecting signal. 44 | ); 45 | ``` 46 | 47 | sample unity project is here 48 | [SampleProject](https://github.com/sassembla/WebuSocket/tree/master/SampleProject) 49 | 50 | ## implemented 51 | * basic WebSocket API 52 | * connect by ipv4 & 6 53 | * connect by domain 54 | * tls 55 | * reconnection 56 | * disconnect detection by ping-pong timeout 57 | * basic auth 58 | 59 | ## not yet implemented 60 | * http redirection(necessary?) 61 | * tls 1.3 62 | * else... 63 | 64 | **contribution welcome!** 65 | 66 | ## license 67 | see below. 68 | [LICENSE](./LICENSE) 69 | 70 | -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Connect/ConnectSample.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/SampleProject/Assets/Samples/Connect/ConnectSample.unity -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Connect/ConnectionSampleScript.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | using UnityEngine; 6 | 7 | using WebuSocketCore; 8 | 9 | 10 | /** 11 | webuSocket connection sample. 12 | */ 13 | public class ConnectionSampleScript : MonoBehaviour { 14 | 15 | WebuSocket webSocket; 16 | 17 | void Start () { 18 | 19 | webSocket = new WebuSocket( 20 | // url. 21 | "wss://echo.websocket.org:443/", 22 | 23 | // buffer size. 24 | 1024, 25 | 26 | // handler for connection established to server. 27 | () => { 28 | Debug.Log("connected to websocket echo-server. send hello to echo-server"); 29 | webSocket.SendString("hello!"); 30 | webSocket.SendString("wooooo!"); 31 | webSocket.SendString("looks!"); 32 | webSocket.SendString("fine!"); 33 | }, 34 | 35 | // handler for receiving data from server. 36 | datas => { 37 | /* 38 | this handler is called from system thread. not Unity's main thread. 39 | 40 | and, datas is ArraySegment x N. 41 | 42 | SHOULD COPY byte data from datas HERE. 43 | 44 | do not copy ArraySegment itself. 45 | these data array will be destroyed soon after leaving this block. 46 | */ 47 | while (0 < datas.Count) { 48 | ArraySegment data = datas.Dequeue(); 49 | 50 | byte[] bytes = new byte[data.Count]; 51 | Buffer.BlockCopy(data.Array, data.Offset, bytes, 0, data.Count); 52 | 53 | Debug.Log("message:" + Encoding.UTF8.GetString(bytes)); 54 | } 55 | }, 56 | () => { 57 | Debug.Log("received server ping. automatically ponged."); 58 | }, 59 | closeReason => { 60 | Debug.Log("closed, closeReason:" + closeReason); 61 | }, 62 | (errorEnum, exception) => { 63 | Debug.LogError("error, errorEnum:" + errorEnum + " exception:" + exception); 64 | }, 65 | new Dictionary{ 66 | // set WebSocket connection header parameters here! 67 | } 68 | ); 69 | } 70 | 71 | void OnApplicationQuit () { 72 | webSocket.Disconnect(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/RTT_by_Ping/PingSpeed.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using WebuSocketCore; 4 | using System.Collections.Generic; 5 | 6 | public class PingSpeed : MonoBehaviour { 7 | private WebuSocket webSocket; 8 | 9 | // Use this for initialization 10 | void Start () { 11 | webSocket = new WebuSocket( 12 | "wss://echo.websocket.org:443/", 13 | 1024, 14 | () => { 15 | webSocket.Ping( 16 | pingRtt => { 17 | Debug.Log("websocket ping rtt:" + pingRtt); 18 | Debug.Log("websocket last ping rtt:" + webSocket.RttMilliseconds); 19 | } 20 | ); 21 | }, 22 | datas => {}, 23 | () => {}, 24 | closeReason => { 25 | Debug.Log("closed, closeReason:" + closeReason); 26 | } 27 | ); 28 | } 29 | 30 | void OnApplicationQuit() { 31 | webSocket.Disconnect(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/RTT_by_Ping/PingSpeed.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/SampleProject/Assets/Samples/RTT_by_Ping/PingSpeed.unity -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Reconnect/ReconnectSample.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/SampleProject/Assets/Samples/Reconnect/ReconnectSample.unity -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Reconnect/ReconnectionSampleScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | using UnityEngine; 4 | 5 | using WebuSocketCore; 6 | 7 | 8 | /** 9 | sample of websocket-disconnect then reconnect. 10 | repeat connect -> disconnect -> reconnect -> disconnect... 11 | */ 12 | public class ReconnectionSampleScript : MonoBehaviour { 13 | 14 | WebuSocket webSocket; 15 | 16 | bool opened = false; 17 | 18 | void Start () { 19 | webSocket = new WebuSocket( 20 | "wss://echo.websocket.org:443/", 21 | 1024, 22 | () => { 23 | Debug.Log("connection succeeded."); 24 | opened = true; 25 | }, 26 | datas => {}, 27 | () => {}, 28 | closeReason => { 29 | Debug.Log("closed, closeReason:" + closeReason); 30 | switch (closeReason) { 31 | case WebuSocketCloseEnum.CLOSED_BY_TIMEOUT: { 32 | Debug.Log("start reconnect."); 33 | StartCoroutine(Reconnection(webSocket)); 34 | break; 35 | } 36 | } 37 | } 38 | ); 39 | } 40 | 41 | private IEnumerator Reconnection (WebuSocket ws) { 42 | yield return new WaitForSeconds(1); 43 | webSocket = WebuSocket.Reconnect(ws); 44 | } 45 | 46 | int frame = 0; 47 | void Update () { 48 | // disconnect after 2sec. 49 | if (opened) { 50 | if (frame == 120) { 51 | opened = false; 52 | frame = 0; 53 | // set timeout for sample. 54 | webSocket.Disconnect(WebuSocketCloseEnum.CLOSED_BY_TIMEOUT); 55 | } 56 | frame++; 57 | } 58 | } 59 | 60 | void OnApplicationQuit() { 61 | webSocket.Disconnect(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Timeout/TimeoutSample.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/SampleProject/Assets/Samples/Timeout/TimeoutSample.unity -------------------------------------------------------------------------------- /SampleProject/Assets/Samples/Timeout/TimeoutSampleScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | using UnityEngine; 4 | 5 | using WebuSocketCore; 6 | 7 | 8 | /** 9 | sample of timeout. 10 | */ 11 | public class TimeoutSampleScript : MonoBehaviour { 12 | 13 | WebuSocket webSocket; 14 | bool opened = false; 15 | 16 | void Start () { 17 | webSocket = new WebuSocket( 18 | "wss://echo.websocket.org:443/", 19 | 1024, 20 | () => { 21 | Debug.Log("connect succeeded."); 22 | opened = true; 23 | }, 24 | datas => {}, 25 | () => {}, 26 | closeReason => { 27 | Debug.Log("closed, closeReason:" + closeReason); 28 | }, 29 | (error, ex) => { 30 | Debug.LogError("error:" + error + " ex:" + ex); 31 | } 32 | ); 33 | } 34 | 35 | void Update () { 36 | /* 37 | IsConnected(newTimeoutSec) method can detect timeout of websocket connection. 38 | You can call this method at your appropriate intervals for connectivity checking. 39 | 40 | default timeout sec is 10. 41 | */ 42 | if (opened && !webSocket.IsConnected(5)) { 43 | Debug.LogError("timeout detected."); 44 | opened = false; 45 | } 46 | } 47 | 48 | void OnApplicationQuit() { 49 | webSocket.Disconnect(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SampleProject/Assets/WebuSocket/Lib/BouncyCastle.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/SampleProject/Assets/WebuSocket/Lib/BouncyCastle.dll -------------------------------------------------------------------------------- /SampleProject/Assets/WebuSocket/Lib/derivedfrom.txt: -------------------------------------------------------------------------------- 1 | https://github.com/bcgit/bc-csharp -------------------------------------------------------------------------------- /SampleProject/Assets/WebuSocket/WebSocketByteGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace WebuSocketCore 8 | { 9 | public static class WebSocketByteGenerator 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 | public const byte OP_CONTINUATION = 0x0; 30 | public const byte OP_TEXT = 0x1;// 0001 31 | public const byte OP_BINARY = 0x2;// 0010 32 | public const byte OP_CLOSE = 0x8;// 1000 33 | public const byte OP_PING = 0x9;// 1001 34 | public const byte OP_PONG = 0xA;// 1010 35 | 36 | public const byte OPFilter = 0xF;// 1111 37 | 38 | public static byte[] Ping(byte[] data = null) 39 | { 40 | if (data == null) data = new byte[0]; 41 | return WSDataFrame(1, 0, 0, 0, OP_PING, 1, data); 42 | } 43 | 44 | public static byte[] Pong(byte[] data = null) 45 | { 46 | if (data == null) data = new byte[0]; 47 | return WSDataFrame(1, 0, 0, 0, OP_PONG, 1, data); 48 | } 49 | 50 | public static byte[] SendTextData(byte[] data) 51 | { 52 | return WSDataFrame(1, 0, 0, 0, OP_TEXT, 1, data); 53 | } 54 | public static byte[] SendBinaryData(byte[] data) 55 | { 56 | return WSDataFrame(1, 0, 0, 0, OP_BINARY, 1, data); 57 | } 58 | 59 | public static byte[] CloseData() 60 | { 61 | return WSDataFrame(1, 0, 0, 0, OP_CLOSE, 1, new byte[0]); 62 | } 63 | 64 | private static byte[] WSDataFrame( 65 | byte fin, 66 | byte rsv1, 67 | byte rsv2, 68 | byte rsv3, 69 | byte opCode, 70 | byte mask, 71 | byte[] data) 72 | { 73 | uint length = (uint)(data.Length); 74 | 75 | byte dataLength7bit = 0; 76 | UInt16 dataLength16bit = 0; 77 | UInt64 dataLength64bit = 0; 78 | 79 | if (length < 126) 80 | { 81 | dataLength7bit = (byte)length; 82 | } 83 | else if (65535 < length) 84 | { 85 | dataLength7bit = 127; 86 | dataLength64bit = length; 87 | } 88 | else 89 | {// 126 ~ 65535 90 | dataLength7bit = 126; 91 | dataLength16bit = (UInt16)length; 92 | } 93 | 94 | /* 95 | ready data stream structure for send. 96 | */ 97 | using (var dataStream = new MemoryStream()) 98 | { 99 | dataStream.WriteByte((byte)((fin << 7) | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) | opCode)); 100 | dataStream.WriteByte((byte)((mask << 7) | dataLength7bit)); 101 | 102 | // 126 ~ 65535. 103 | if (0 < dataLength16bit) 104 | { 105 | var intBytes = new byte[2]; 106 | intBytes[0] = (byte)(dataLength16bit >> 8); 107 | intBytes[1] = (byte)dataLength16bit; 108 | 109 | // dataLength16 to 2bytes. 110 | dataStream.Write(intBytes, 0, intBytes.Length); 111 | } 112 | 113 | // 65536 ~. 114 | if (0 < dataLength64bit) 115 | { 116 | var intBytes = new byte[8]; 117 | intBytes[0] = (byte)(dataLength64bit >> (8 * 7)); 118 | intBytes[1] = (byte)(dataLength64bit >> (8 * 6)); 119 | intBytes[2] = (byte)(dataLength64bit >> (8 * 5)); 120 | intBytes[3] = (byte)(dataLength64bit >> (8 * 4)); 121 | intBytes[4] = (byte)(dataLength64bit >> (8 * 3)); 122 | intBytes[5] = (byte)(dataLength64bit >> (8 * 2)); 123 | intBytes[6] = (byte)(dataLength64bit >> 8); 124 | intBytes[7] = (byte)dataLength64bit; 125 | 126 | // dataLength64 to 8bytes. 127 | dataStream.Write(intBytes, 0, intBytes.Length); 128 | } 129 | 130 | // client should mask control frame. 131 | var maskKey = NewMaskKey(); 132 | dataStream.Write(maskKey, 0, maskKey.Length); 133 | 134 | // mask data. 135 | var maskedData = Masked(data, maskKey); 136 | dataStream.Write(maskedData, 0, maskedData.Length); 137 | 138 | return dataStream.ToArray(); 139 | } 140 | } 141 | 142 | private static byte[] Masked(byte[] data, byte[] maskKey) 143 | { 144 | for (var i = 0; i < data.Length; i++) data[i] ^= maskKey[i % 4]; 145 | return data; 146 | } 147 | 148 | /** 149 | get message detail from data. 150 | no copy emitted. only read data then return there indexies of messages. 151 | */ 152 | public static List GetIndexies(byte[] data) 153 | { 154 | var opCodeAndPayloadIndexies = new List(); 155 | 156 | uint cursor = 0; 157 | while (cursor < data.Length) 158 | { 159 | 160 | // first byte = fin(1), rsv1(1), rsv2(1), rsv3(1), opCode(4) 161 | var opCode = (byte)(data[cursor++] & OPFilter); 162 | 163 | // second byte = mask(1), length(7) 164 | /* 165 | mask of data from server is definitely zero(0). 166 | ignore reading mask bit. 167 | */ 168 | uint length = (uint)data[cursor++]; 169 | switch (length) 170 | { 171 | case 126: 172 | { 173 | // next 2 byte is length data. 174 | length = (uint)( 175 | (data[cursor++] << 8) + 176 | (data[cursor++]) 177 | ); 178 | break; 179 | } 180 | case 127: 181 | { 182 | // next 8 byte is length data. 183 | length = (uint)( 184 | (data[cursor++] << (8 * 7)) + 185 | (data[cursor++] << (8 * 6)) + 186 | (data[cursor++] << (8 * 5)) + 187 | (data[cursor++] << (8 * 4)) + 188 | (data[cursor++] << (8 * 3)) + 189 | (data[cursor++] << (8 * 2)) + 190 | (data[cursor++] << 8) + 191 | (data[cursor++]) 192 | ); 193 | break; 194 | } 195 | default: 196 | { 197 | break; 198 | } 199 | } 200 | 201 | 202 | /* 203 | shortage of payload length. 204 | the whole payload datas of this message is not yet read from socket. 205 | 206 | break indexing then store the rest = header of fragment data and half of payload. 207 | */ 208 | if ((data.Length - cursor) < length) break; 209 | 210 | if (length != 0) 211 | { 212 | var payload = new byte[length]; 213 | Array.Copy(data, cursor, payload, 0, payload.Length); 214 | } 215 | 216 | opCodeAndPayloadIndexies.Add(new OpCodeAndPayloadIndex(opCode, cursor, length)); 217 | 218 | cursor = cursor + length; 219 | } 220 | 221 | return opCodeAndPayloadIndexies; 222 | } 223 | 224 | public struct OpCodeAndPayloadIndex 225 | { 226 | public readonly byte opCode; 227 | public readonly uint start; 228 | public readonly uint length; 229 | public OpCodeAndPayloadIndex(byte opCode, uint start, uint length) 230 | { 231 | this.opCode = opCode; 232 | this.start = start; 233 | this.length = length; 234 | } 235 | } 236 | 237 | private static RNGCryptoServiceProvider randomGen = new RNGCryptoServiceProvider(); 238 | 239 | public static string GenerateExpectedAcceptedKey(string baseStr) 240 | { 241 | var concat = (baseStr.TrimEnd() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 242 | var sha1d = new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(concat)); 243 | return Convert.ToBase64String(sha1d); 244 | } 245 | 246 | public static string GeneratePrivateBase64Key() 247 | { 248 | var src = new byte[16]; 249 | randomGen.GetBytes(src); 250 | return Convert.ToBase64String(src); 251 | } 252 | 253 | public static byte[] NewMaskKey() 254 | { 255 | var maskingKeyBytes = new byte[4]; 256 | randomGen.GetBytes(maskingKeyBytes); 257 | return maskingKeyBytes; 258 | } 259 | 260 | } 261 | } -------------------------------------------------------------------------------- /SampleProject/Assets/WebuSocket/WebSocketEncryption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WebuSocketEncryption.Org.BouncyCastle.Crypto.Tls; 3 | using WebuSocketEncryption.Org.BouncyCastle.Security; 4 | using WebuSocketEncryption.Org.BouncyCastle.Utilities; 5 | 6 | namespace WebuSocketCore.Encryption 7 | { 8 | public class WebuSocketTlsClientProtocol : TlsClientProtocol 9 | { 10 | /** 11 | non-blocking mode constructor of tlsClientProtocol. 12 | */ 13 | public WebuSocketTlsClientProtocol() : base(new SecureRandom()) { } 14 | 15 | private byte[] emptyByteBuffer = new byte[0]; 16 | 17 | /** 18 | write byte datas to tls input without creating new partial buffer from exists buffer. 19 | this method does not require buffer copy for input. 20 | */ 21 | public void OfferInputBytes(byte[] bytes, int length) 22 | { 23 | /* 24 | changed for no-new buffer. 25 | */ 26 | mInputBuffers.Write(bytes, 0, length); 27 | base.OfferInput(emptyByteBuffer); 28 | } 29 | } 30 | 31 | public class WebuSocketTlsClient : DefaultTlsClient 32 | { 33 | 34 | internal TlsSession mSession; 35 | private readonly Action handshakeDone; 36 | private readonly Action handleError; 37 | 38 | public WebuSocketTlsClient(Action handshakeDone, Action handleError) 39 | { 40 | this.handshakeDone = handshakeDone; 41 | this.handleError = handleError; 42 | this.mSession = null; 43 | } 44 | 45 | public override TlsSession GetSessionToResume() 46 | { 47 | return this.mSession; 48 | } 49 | 50 | public override void NotifyAlertRaised(byte alertLevel, byte alertDescription, string message, Exception cause) 51 | { 52 | if (message != null) 53 | { 54 | handleError(null, message); 55 | } 56 | if (cause != null) 57 | { 58 | handleError(cause, string.Empty); 59 | } 60 | } 61 | 62 | public override void NotifyAlertReceived(byte alertLevel, byte alertDescription) 63 | { 64 | // Log("TLS client received alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription)); 65 | } 66 | 67 | public override void NotifyServerVersion(ProtocolVersion serverVersion) 68 | { 69 | base.NotifyServerVersion(serverVersion); 70 | } 71 | 72 | public override TlsAuthentication GetAuthentication() 73 | { 74 | return new WebuSocketTlsAuthentication(mContext); 75 | } 76 | 77 | 78 | private class WebuSocketTlsAuthentication : TlsAuthentication 79 | { 80 | #pragma warning disable 414 81 | private readonly TlsContext mContext; 82 | #pragma warning restore 414 83 | 84 | internal WebuSocketTlsAuthentication(TlsContext context) 85 | { 86 | this.mContext = context; 87 | } 88 | 89 | public void NotifyServerCertificate(Certificate serverCertificate) 90 | { 91 | // X509CertificateStructure[] chain = serverCertificate.GetCertificateList(); 92 | // Console.WriteLine("TLS client received server certificate chain of length " + chain.Length); 93 | // for (int i = 0; i != chain.Length; i++) { 94 | // X509CertificateStructure entry = chain[i]; 95 | // // TODO Create fingerprint based on certificate signature algorithm digest 96 | // Console.WriteLine(" fingerprint:SHA-256 " + TlsTestUtilities.Fingerprint(entry) + " (" + entry.Subject + ")"); 97 | // } 98 | // なんもしてない。certが正しいかどうか、チェックしないといけないはず。 99 | } 100 | 101 | public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) 102 | { 103 | byte[] certificateTypes = certificateRequest.CertificateTypes; 104 | if (certificateTypes == null || !Arrays.Contains(certificateTypes, ClientCertificateType.rsa_sign)) 105 | { 106 | return null; 107 | } 108 | // return TlsTestUtilities.LoadSignerCredentials(mContext, certificateRequest.SupportedSignatureAlgorithms, SignatureAlgorithm.rsa, "x509-client.pem", "x509-client-key.pem"); 109 | return null; 110 | } 111 | } 112 | 113 | public override void NotifyHandshakeComplete() 114 | { 115 | base.NotifyHandshakeComplete(); 116 | 117 | TlsSession newSession = mContext.ResumableSession; 118 | if (newSession != null) 119 | { 120 | // byte[] newSessionID = newSession.SessionID; 121 | // string hex = Hex.ToHexString(newSessionID); 122 | 123 | // if (this.mSession != null && Arrays.AreEqual(this.mSession.SessionID, newSessionID)) { 124 | // Debug.LogError("Resumed session: " + hex); 125 | // } else { 126 | // Debug.LogError("Established session: " + hex); 127 | // } 128 | 129 | this.mSession = newSession; 130 | } 131 | 132 | handshakeDone(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /WebuSocket/WebSocketByteGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace WebuSocketCore 8 | { 9 | public static class WebSocketByteGenerator 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 | public const byte OP_CONTINUATION = 0x0; 30 | public const byte OP_TEXT = 0x1;// 0001 31 | public const byte OP_BINARY = 0x2;// 0010 32 | public const byte OP_CLOSE = 0x8;// 1000 33 | public const byte OP_PING = 0x9;// 1001 34 | public const byte OP_PONG = 0xA;// 1010 35 | 36 | public const byte OPFilter = 0xF;// 1111 37 | 38 | public static byte[] Ping(byte[] data = null) 39 | { 40 | if (data == null) data = new byte[0]; 41 | return WSDataFrame(1, 0, 0, 0, OP_PING, 1, data); 42 | } 43 | 44 | public static byte[] Pong(byte[] data = null) 45 | { 46 | if (data == null) data = new byte[0]; 47 | return WSDataFrame(1, 0, 0, 0, OP_PONG, 1, data); 48 | } 49 | 50 | public static byte[] SendTextData(byte[] data) 51 | { 52 | return WSDataFrame(1, 0, 0, 0, OP_TEXT, 1, data); 53 | } 54 | public static byte[] SendBinaryData(byte[] data) 55 | { 56 | return WSDataFrame(1, 0, 0, 0, OP_BINARY, 1, data); 57 | } 58 | 59 | public static byte[] CloseData() 60 | { 61 | return WSDataFrame(1, 0, 0, 0, OP_CLOSE, 1, new byte[0]); 62 | } 63 | 64 | private static byte[] WSDataFrame( 65 | byte fin, 66 | byte rsv1, 67 | byte rsv2, 68 | byte rsv3, 69 | byte opCode, 70 | byte mask, 71 | byte[] data) 72 | { 73 | uint length = (uint)(data.Length); 74 | 75 | byte dataLength7bit = 0; 76 | UInt16 dataLength16bit = 0; 77 | UInt64 dataLength64bit = 0; 78 | 79 | if (length < 126) 80 | { 81 | dataLength7bit = (byte)length; 82 | } 83 | else if (65535 < length) 84 | { 85 | dataLength7bit = 127; 86 | dataLength64bit = length; 87 | } 88 | else 89 | {// 126 ~ 65535 90 | dataLength7bit = 126; 91 | dataLength16bit = (UInt16)length; 92 | } 93 | 94 | /* 95 | ready data stream structure for send. 96 | */ 97 | using (var dataStream = new MemoryStream()) 98 | { 99 | dataStream.WriteByte((byte)((fin << 7) | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) | opCode)); 100 | dataStream.WriteByte((byte)((mask << 7) | dataLength7bit)); 101 | 102 | // 126 ~ 65535. 103 | if (0 < dataLength16bit) 104 | { 105 | var intBytes = new byte[2]; 106 | intBytes[0] = (byte)(dataLength16bit >> 8); 107 | intBytes[1] = (byte)dataLength16bit; 108 | 109 | // dataLength16 to 2bytes. 110 | dataStream.Write(intBytes, 0, intBytes.Length); 111 | } 112 | 113 | // 65536 ~. 114 | if (0 < dataLength64bit) 115 | { 116 | var intBytes = new byte[8]; 117 | intBytes[0] = (byte)(dataLength64bit >> (8 * 7)); 118 | intBytes[1] = (byte)(dataLength64bit >> (8 * 6)); 119 | intBytes[2] = (byte)(dataLength64bit >> (8 * 5)); 120 | intBytes[3] = (byte)(dataLength64bit >> (8 * 4)); 121 | intBytes[4] = (byte)(dataLength64bit >> (8 * 3)); 122 | intBytes[5] = (byte)(dataLength64bit >> (8 * 2)); 123 | intBytes[6] = (byte)(dataLength64bit >> 8); 124 | intBytes[7] = (byte)dataLength64bit; 125 | 126 | // dataLength64 to 8bytes. 127 | dataStream.Write(intBytes, 0, intBytes.Length); 128 | } 129 | 130 | // client should mask control frame. 131 | var maskKey = NewMaskKey(); 132 | dataStream.Write(maskKey, 0, maskKey.Length); 133 | 134 | // mask data. 135 | var maskedData = Masked(data, maskKey); 136 | dataStream.Write(maskedData, 0, maskedData.Length); 137 | 138 | return dataStream.ToArray(); 139 | } 140 | } 141 | 142 | private static byte[] Masked(byte[] data, byte[] maskKey) 143 | { 144 | for (var i = 0; i < data.Length; i++) data[i] ^= maskKey[i % 4]; 145 | return data; 146 | } 147 | 148 | /** 149 | get message detail from data. 150 | no copy emitted. only read data then return there indexies of messages. 151 | */ 152 | public static List GetIndexies(byte[] data) 153 | { 154 | var opCodeAndPayloadIndexies = new List(); 155 | 156 | uint cursor = 0; 157 | while (cursor < data.Length) 158 | { 159 | 160 | // first byte = fin(1), rsv1(1), rsv2(1), rsv3(1), opCode(4) 161 | var opCode = (byte)(data[cursor++] & OPFilter); 162 | 163 | // second byte = mask(1), length(7) 164 | /* 165 | mask of data from server is definitely zero(0). 166 | ignore reading mask bit. 167 | */ 168 | uint length = (uint)data[cursor++]; 169 | switch (length) 170 | { 171 | case 126: 172 | { 173 | // next 2 byte is length data. 174 | length = (uint)( 175 | (data[cursor++] << 8) + 176 | (data[cursor++]) 177 | ); 178 | break; 179 | } 180 | case 127: 181 | { 182 | // next 8 byte is length data. 183 | length = (uint)( 184 | (data[cursor++] << (8 * 7)) + 185 | (data[cursor++] << (8 * 6)) + 186 | (data[cursor++] << (8 * 5)) + 187 | (data[cursor++] << (8 * 4)) + 188 | (data[cursor++] << (8 * 3)) + 189 | (data[cursor++] << (8 * 2)) + 190 | (data[cursor++] << 8) + 191 | (data[cursor++]) 192 | ); 193 | break; 194 | } 195 | default: 196 | { 197 | break; 198 | } 199 | } 200 | 201 | 202 | /* 203 | shortage of payload length. 204 | the whole payload datas of this message is not yet read from socket. 205 | 206 | break indexing then store the rest = header of fragment data and half of payload. 207 | */ 208 | if ((data.Length - cursor) < length) break; 209 | 210 | if (length != 0) 211 | { 212 | var payload = new byte[length]; 213 | Array.Copy(data, cursor, payload, 0, payload.Length); 214 | } 215 | 216 | opCodeAndPayloadIndexies.Add(new OpCodeAndPayloadIndex(opCode, cursor, length)); 217 | 218 | cursor = cursor + length; 219 | } 220 | 221 | return opCodeAndPayloadIndexies; 222 | } 223 | 224 | public struct OpCodeAndPayloadIndex 225 | { 226 | public readonly byte opCode; 227 | public readonly uint start; 228 | public readonly uint length; 229 | public OpCodeAndPayloadIndex(byte opCode, uint start, uint length) 230 | { 231 | this.opCode = opCode; 232 | this.start = start; 233 | this.length = length; 234 | } 235 | } 236 | 237 | private static RNGCryptoServiceProvider randomGen = new RNGCryptoServiceProvider(); 238 | 239 | public static string GenerateExpectedAcceptedKey(string baseStr) 240 | { 241 | var concat = (baseStr.TrimEnd() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 242 | var sha1d = new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(concat)); 243 | return Convert.ToBase64String(sha1d); 244 | } 245 | 246 | public static string GeneratePrivateBase64Key() 247 | { 248 | var src = new byte[16]; 249 | randomGen.GetBytes(src); 250 | return Convert.ToBase64String(src); 251 | } 252 | 253 | public static byte[] NewMaskKey() 254 | { 255 | var maskingKeyBytes = new byte[4]; 256 | randomGen.GetBytes(maskingKeyBytes); 257 | return maskingKeyBytes; 258 | } 259 | 260 | } 261 | } -------------------------------------------------------------------------------- /WebuSocket/WebSocketEncryption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WebuSocketEncryption.Org.BouncyCastle.Crypto.Tls; 3 | using WebuSocketEncryption.Org.BouncyCastle.Security; 4 | using WebuSocketEncryption.Org.BouncyCastle.Utilities; 5 | 6 | namespace WebuSocketCore.Encryption 7 | { 8 | public class WebuSocketTlsClientProtocol : TlsClientProtocol 9 | { 10 | /** 11 | non-blocking mode constructor of tlsClientProtocol. 12 | */ 13 | public WebuSocketTlsClientProtocol() : base(new SecureRandom()) { } 14 | 15 | private byte[] emptyByteBuffer = new byte[0]; 16 | 17 | /** 18 | write byte datas to tls input without creating new partial buffer from exists buffer. 19 | this method does not require buffer copy for input. 20 | */ 21 | public void OfferInputBytes(byte[] bytes, int length) 22 | { 23 | /* 24 | changed for no-new buffer. 25 | */ 26 | mInputBuffers.Write(bytes, 0, length); 27 | base.OfferInput(emptyByteBuffer); 28 | } 29 | } 30 | 31 | public class WebuSocketTlsClient : DefaultTlsClient 32 | { 33 | 34 | internal TlsSession mSession; 35 | private readonly Action handshakeDone; 36 | private readonly Action handleError; 37 | 38 | public WebuSocketTlsClient(Action handshakeDone, Action handleError) 39 | { 40 | this.handshakeDone = handshakeDone; 41 | this.handleError = handleError; 42 | this.mSession = null; 43 | } 44 | 45 | public override TlsSession GetSessionToResume() 46 | { 47 | return this.mSession; 48 | } 49 | 50 | public override void NotifyAlertRaised(byte alertLevel, byte alertDescription, string message, Exception cause) 51 | { 52 | if (message != null) 53 | { 54 | handleError(null, message); 55 | } 56 | if (cause != null) 57 | { 58 | handleError(cause, string.Empty); 59 | } 60 | } 61 | 62 | public override void NotifyAlertReceived(byte alertLevel, byte alertDescription) 63 | { 64 | // Log("TLS client received alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription)); 65 | } 66 | 67 | public override void NotifyServerVersion(ProtocolVersion serverVersion) 68 | { 69 | base.NotifyServerVersion(serverVersion); 70 | } 71 | 72 | public override TlsAuthentication GetAuthentication() 73 | { 74 | return new WebuSocketTlsAuthentication(mContext); 75 | } 76 | 77 | 78 | private class WebuSocketTlsAuthentication : TlsAuthentication 79 | { 80 | #pragma warning disable 414 81 | private readonly TlsContext mContext; 82 | #pragma warning restore 414 83 | 84 | internal WebuSocketTlsAuthentication(TlsContext context) 85 | { 86 | this.mContext = context; 87 | } 88 | 89 | public void NotifyServerCertificate(Certificate serverCertificate) 90 | { 91 | // X509CertificateStructure[] chain = serverCertificate.GetCertificateList(); 92 | // Console.WriteLine("TLS client received server certificate chain of length " + chain.Length); 93 | // for (int i = 0; i != chain.Length; i++) { 94 | // X509CertificateStructure entry = chain[i]; 95 | // // TODO Create fingerprint based on certificate signature algorithm digest 96 | // Console.WriteLine(" fingerprint:SHA-256 " + TlsTestUtilities.Fingerprint(entry) + " (" + entry.Subject + ")"); 97 | // } 98 | // なんもしてない。certが正しいかどうか、チェックしないといけないはず。 99 | } 100 | 101 | public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) 102 | { 103 | byte[] certificateTypes = certificateRequest.CertificateTypes; 104 | if (certificateTypes == null || !Arrays.Contains(certificateTypes, ClientCertificateType.rsa_sign)) 105 | { 106 | return null; 107 | } 108 | // return TlsTestUtilities.LoadSignerCredentials(mContext, certificateRequest.SupportedSignatureAlgorithms, SignatureAlgorithm.rsa, "x509-client.pem", "x509-client-key.pem"); 109 | return null; 110 | } 111 | } 112 | 113 | public override void NotifyHandshakeComplete() 114 | { 115 | base.NotifyHandshakeComplete(); 116 | 117 | TlsSession newSession = mContext.ResumableSession; 118 | if (newSession != null) 119 | { 120 | // byte[] newSessionID = newSession.SessionID; 121 | // string hex = Hex.ToHexString(newSessionID); 122 | 123 | // if (this.mSession != null && Arrays.AreEqual(this.mSession.SessionID, newSessionID)) { 124 | // Debug.LogError("Resumed session: " + hex); 125 | // } else { 126 | // Debug.LogError("Established session: " + hex); 127 | // } 128 | 129 | this.mSession = newSession; 130 | } 131 | 132 | handshakeDone(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /WebuSocket/WebuSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | 7 | namespace WebuSocketCore 8 | { 9 | 10 | public enum SocketState 11 | { 12 | EMPTY, 13 | CONNECTING, 14 | TLS_HANDSHAKING, 15 | TLS_HANDSHAKE_DONE, 16 | WS_HANDSHAKING, 17 | OPENED, 18 | CLOSING, 19 | CLOSED 20 | } 21 | 22 | public enum WebuSocketCloseEnum 23 | { 24 | CLOSED_FORCIBLY, 25 | CLOSED_GRACEFULLY, 26 | CLOSED_BY_SERVER, 27 | CLOSED_BY_TIMEOUT, 28 | } 29 | 30 | public enum WebuSocketErrorEnum 31 | { 32 | UNKNOWN_ERROR, 33 | DOMAIN_UNRESOLVED, 34 | CONNECTION_FAILED, 35 | TLS_HANDSHAKE_FAILED, 36 | TLS_ERROR, 37 | WS_HANDSHAKE_FAILED, 38 | WS_HANDSHAKE_KEY_UNMATCHED, 39 | SEND_FAILED, 40 | PING_FAILED, 41 | PONG_FAILED, 42 | RECEIVE_FAILED, 43 | CONNECTING, 44 | ALREADY_DISCONNECTED, 45 | } 46 | 47 | public class WebuSocket 48 | { 49 | public const int DEFAULT_TIMEOUT_SEC = 10; 50 | 51 | /** 52 | create new WebuSocket instance from source WebuSocket instance. 53 | */ 54 | public static WebuSocket Reconnect(WebuSocket sourceSocket) 55 | { 56 | return new WebuSocket( 57 | sourceSocket.url, 58 | sourceSocket.baseReceiveBufferSize, 59 | sourceSocket.OnConnected, 60 | sourceSocket.OnMessage, 61 | sourceSocket.OnPinged, 62 | sourceSocket.OnClosed, 63 | sourceSocket.OnError, 64 | sourceSocket.additionalHeaderParams 65 | ); 66 | } 67 | 68 | private EndPoint endPoint; 69 | 70 | private const string CRLF = "\r\n"; 71 | private const string WEBSOCKET_VERSION = "13"; 72 | 73 | 74 | private SocketToken socketToken; 75 | 76 | public readonly string webSocketConnectionId; 77 | 78 | public class SocketToken 79 | { 80 | public SocketState socketState; 81 | public WebuSocketCloseEnum closeReason; 82 | public readonly Socket socket; 83 | 84 | public byte[] receiveBuffer; 85 | 86 | public readonly SocketAsyncEventArgs connectArgs; 87 | public readonly SocketAsyncEventArgs sendArgs; 88 | public readonly SocketAsyncEventArgs receiveArgs; 89 | 90 | public SocketToken(Socket socket, int bufferLen, SocketAsyncEventArgs connectArgs, SocketAsyncEventArgs sendArgs, SocketAsyncEventArgs receiveArgs) 91 | { 92 | this.socket = socket; 93 | 94 | this.receiveBuffer = new byte[bufferLen]; 95 | 96 | this.connectArgs = connectArgs; 97 | this.sendArgs = sendArgs; 98 | this.receiveArgs = receiveArgs; 99 | 100 | this.connectArgs.UserToken = this; 101 | this.sendArgs.UserToken = this; 102 | this.receiveArgs.UserToken = this; 103 | 104 | this.receiveArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length); 105 | } 106 | 107 | /* 108 | default constructor. 109 | */ 110 | public SocketToken() 111 | { 112 | this.socketState = SocketState.EMPTY; 113 | } 114 | } 115 | 116 | /* 117 | WebuSocket basement parameters. 118 | */ 119 | public readonly string url; 120 | public readonly int baseReceiveBufferSize; 121 | 122 | public readonly Action OnConnected; 123 | public readonly Action OnPinged; 124 | public readonly Action>> OnMessage; 125 | public readonly Action OnClosed; 126 | public readonly Action OnError; 127 | 128 | public readonly Dictionary additionalHeaderParams; 129 | 130 | 131 | /* 132 | temporary parameters. 133 | */ 134 | private readonly string base64Key; 135 | 136 | private readonly bool isWss; 137 | private readonly Encryption.WebuSocketTlsClientProtocol tlsClientProtocol; 138 | 139 | private readonly byte[] websocketHandshakeRequestBytes; 140 | 141 | public WebuSocket( 142 | string url, 143 | int baseReceiveBufferSize, 144 | Action OnConnected = null, 145 | Action>> OnMessage = null, 146 | Action OnPinged = null, 147 | Action OnClosed = null, 148 | Action OnError = null, 149 | Dictionary additionalHeaderParams = null 150 | ) 151 | { 152 | this.webSocketConnectionId = Guid.NewGuid().ToString(); 153 | 154 | this.url = url; 155 | this.baseReceiveBufferSize = baseReceiveBufferSize; 156 | 157 | this.OnConnected = OnConnected; 158 | this.OnMessage = OnMessage; 159 | this.OnPinged = OnPinged; 160 | this.OnClosed = OnClosed; 161 | this.OnError = OnError; 162 | 163 | this.additionalHeaderParams = additionalHeaderParams; 164 | 165 | this.base64Key = WebSocketByteGenerator.GeneratePrivateBase64Key(); 166 | 167 | var uri = new Uri(url); 168 | 169 | var host = uri.Host; 170 | var scheme = uri.Scheme; 171 | var port = uri.Port; 172 | var path = uri.LocalPath; 173 | var userInfo = uri.UserInfo; 174 | 175 | /* 176 | set default port. 177 | */ 178 | if (port == -1) 179 | { 180 | if (scheme == "wss") 181 | { 182 | port = 443; 183 | } 184 | else 185 | { 186 | port = 80; 187 | } 188 | } 189 | 190 | // check if dns or ip. 191 | var isDns = (uri.HostNameType == UriHostNameType.Dns); 192 | 193 | // ready tls machine for wss. 194 | if (scheme == "wss") 195 | { 196 | this.isWss = true; 197 | this.tlsClientProtocol = new Encryption.WebuSocketTlsClientProtocol(); 198 | tlsClientProtocol.Connect(new Encryption.WebuSocketTlsClient(TLSHandshakeDone, TLSHandleError)); 199 | } 200 | 201 | var requestHeaderParams = new Dictionary { 202 | {"Host", isDns ? uri.DnsSafeHost : uri.Authority}, 203 | {"Upgrade", "websocket"}, 204 | {"Connection", "Upgrade"}, 205 | {"Sec-WebSocket-Key", base64Key}, 206 | {"Sec-WebSocket-Version", WEBSOCKET_VERSION} 207 | }; 208 | 209 | /* 210 | basic auth. 211 | */ 212 | if (!string.IsNullOrEmpty(userInfo)) 213 | { 214 | requestHeaderParams["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo)); 215 | } 216 | 217 | if (additionalHeaderParams != null) 218 | { 219 | foreach (var key in additionalHeaderParams.Keys) requestHeaderParams[key] = additionalHeaderParams[key]; 220 | } 221 | 222 | /* 223 | construct http request bytes data. 224 | */ 225 | var requestData = new StringBuilder(); 226 | { 227 | requestData.AppendFormat("GET {0} HTTP/1.1{1}", path, CRLF); 228 | foreach (var key in requestHeaderParams.Keys) requestData.AppendFormat("{0}: {1}{2}", key, requestHeaderParams[key], CRLF); 229 | requestData.Append(CRLF); 230 | } 231 | 232 | this.websocketHandshakeRequestBytes = Encoding.UTF8.GetBytes(requestData.ToString()); 233 | 234 | if (isDns) 235 | { 236 | // initialize socketToken with empty state. 237 | this.socketToken = new SocketToken(); 238 | 239 | Dns.BeginGetHostEntry( 240 | host, 241 | new AsyncCallback( 242 | result => 243 | { 244 | IPHostEntry addresses = null; 245 | 246 | try 247 | { 248 | addresses = Dns.EndGetHostEntry(result); 249 | } 250 | catch (Exception e) 251 | { 252 | if (OnError != null) 253 | { 254 | OnError(WebuSocketErrorEnum.DOMAIN_UNRESOLVED, e); 255 | } 256 | Disconnect(); 257 | return; 258 | } 259 | 260 | if (addresses.AddressList.Length == 0) 261 | { 262 | if (OnError != null) 263 | { 264 | var domainUnresolvedException = new Exception("failed to resolve domain."); 265 | OnError(WebuSocketErrorEnum.DOMAIN_UNRESOLVED, domainUnresolvedException); 266 | } 267 | Disconnect(); 268 | return; 269 | } 270 | 271 | // choose valid ip. 272 | foreach (IPAddress ipaddress in addresses.AddressList) 273 | { 274 | if (ipaddress.AddressFamily == AddressFamily.InterNetwork || ipaddress.AddressFamily == AddressFamily.InterNetworkV6) 275 | { 276 | this.endPoint = new IPEndPoint(ipaddress, port); 277 | StartConnectAsync(); 278 | return; 279 | } 280 | } 281 | 282 | if (OnError != null) 283 | { 284 | var unresolvedAddressFamilyException = new Exception("failed to get valid address family from list."); 285 | OnError(WebuSocketErrorEnum.DOMAIN_UNRESOLVED, unresolvedAddressFamilyException); 286 | } 287 | Disconnect(); 288 | } 289 | ), 290 | this 291 | ); 292 | return; 293 | } 294 | 295 | // raw ip. 296 | this.endPoint = new IPEndPoint(IPAddress.Parse(host), port); 297 | StartConnectAsync(); 298 | } 299 | 300 | private void TLSHandshakeDone() 301 | { 302 | switch (socketToken.socketState) 303 | { 304 | case SocketState.TLS_HANDSHAKING: 305 | { 306 | socketToken.socketState = SocketState.TLS_HANDSHAKE_DONE; 307 | break; 308 | } 309 | default: 310 | { 311 | if (OnError != null) 312 | { 313 | var error = new Exception("tls handshake failed in unexpected state."); 314 | OnError(WebuSocketErrorEnum.TLS_HANDSHAKE_FAILED, error); 315 | } 316 | Disconnect(); 317 | break; 318 | } 319 | } 320 | } 321 | 322 | private void TLSHandleError(Exception e, string errorMessage) 323 | { 324 | if (OnError != null) 325 | { 326 | if (e == null) e = new Exception("tls error:" + errorMessage); 327 | OnError(WebuSocketErrorEnum.TLS_ERROR, e); 328 | } 329 | Disconnect(); 330 | } 331 | 332 | private void StartConnectAsync() 333 | { 334 | var clientSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 335 | clientSocket.NoDelay = true; 336 | clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); 337 | 338 | var connectArgs = new SocketAsyncEventArgs(); 339 | connectArgs.AcceptSocket = clientSocket; 340 | connectArgs.RemoteEndPoint = endPoint; 341 | connectArgs.Completed += new EventHandler(OnConnect); 342 | 343 | var sendArgs = new SocketAsyncEventArgs(); 344 | sendArgs.AcceptSocket = clientSocket; 345 | sendArgs.RemoteEndPoint = endPoint; 346 | sendArgs.Completed += new EventHandler(OnSend); 347 | 348 | var receiveArgs = new SocketAsyncEventArgs(); 349 | receiveArgs.AcceptSocket = clientSocket; 350 | receiveArgs.RemoteEndPoint = endPoint; 351 | receiveArgs.Completed += new EventHandler(OnReceived); 352 | 353 | socketToken = new SocketToken(clientSocket, baseReceiveBufferSize, connectArgs, sendArgs, receiveArgs); 354 | socketToken.socketState = SocketState.CONNECTING; 355 | 356 | // start connect. 357 | if (!clientSocket.ConnectAsync(socketToken.connectArgs)) OnConnect(clientSocket, connectArgs); 358 | } 359 | private byte[] webSocketHandshakeResult; 360 | private void OnConnect(object unused, SocketAsyncEventArgs args) 361 | { 362 | var token = (SocketToken)args.UserToken; 363 | switch (token.socketState) 364 | { 365 | case SocketState.CONNECTING: 366 | { 367 | if (args.SocketError != SocketError.Success) 368 | { 369 | token.socketState = SocketState.CLOSED; 370 | 371 | if (OnError != null) 372 | { 373 | var error = new Exception("connect error:" + args.SocketError.ToString()); 374 | OnError(WebuSocketErrorEnum.CONNECTION_FAILED, error); 375 | } 376 | return; 377 | } 378 | 379 | if (isWss) 380 | { 381 | SendTLSHandshake(token); 382 | return; 383 | } 384 | 385 | SendWSHandshake(token); 386 | return; 387 | } 388 | default: 389 | { 390 | // unexpected error, should fall this connection. 391 | if (OnError != null) 392 | { 393 | var error = new Exception("unexpcted connection state error."); 394 | OnError(WebuSocketErrorEnum.CONNECTION_FAILED, error); 395 | } 396 | Disconnect(); 397 | return; 398 | } 399 | } 400 | } 401 | 402 | private void SendTLSHandshake(SocketToken token) 403 | { 404 | token.socketState = SocketState.TLS_HANDSHAKING; 405 | 406 | // ready receive. 407 | ReadyReceivingNewData(token); 408 | 409 | // first, send clientHello to server. 410 | // get ClientHello byte data from tlsClientProtocol instance and send it to server. 411 | var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()]; 412 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 413 | 414 | token.sendArgs.SetBuffer(buffer, 0, buffer.Length); 415 | if (!token.socket.SendAsync(token.sendArgs)) OnSend(token.socket, token.sendArgs); 416 | } 417 | 418 | private void SendWSHandshake(SocketToken token) 419 | { 420 | token.socketState = SocketState.WS_HANDSHAKING; 421 | 422 | ReadyReceivingNewData(token); 423 | 424 | if (isWss) 425 | { 426 | tlsClientProtocol.OfferOutput(websocketHandshakeRequestBytes, 0, websocketHandshakeRequestBytes.Length); 427 | 428 | var count = tlsClientProtocol.GetAvailableOutputBytes(); 429 | var buffer = new byte[count]; 430 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 431 | 432 | token.sendArgs.SetBuffer(buffer, 0, buffer.Length); 433 | } 434 | else 435 | { 436 | token.sendArgs.SetBuffer(websocketHandshakeRequestBytes, 0, websocketHandshakeRequestBytes.Length); 437 | } 438 | 439 | if (!token.socket.SendAsync(token.sendArgs)) OnSend(token.socket, token.sendArgs); 440 | } 441 | 442 | private void OnDisconnected(object unused, SocketAsyncEventArgs args) 443 | { 444 | var token = (SocketToken)args.UserToken; 445 | switch (token.socketState) 446 | { 447 | case SocketState.CLOSED: 448 | { 449 | // do nothing. 450 | break; 451 | } 452 | default: 453 | { 454 | lock (lockObj) 455 | { 456 | token.socketState = SocketState.CLOSED; 457 | 458 | try 459 | { 460 | token.socket.Close(); 461 | } 462 | catch 463 | { 464 | // do nothing. 465 | } 466 | 467 | if (OnClosed != null) 468 | { 469 | if (token.closeReason != WebuSocketCloseEnum.CLOSED_GRACEFULLY) OnClosed(token.closeReason); 470 | else OnClosed(WebuSocketCloseEnum.CLOSED_GRACEFULLY); 471 | } 472 | } 473 | break; 474 | } 475 | } 476 | } 477 | 478 | private void OnSend(object unused, SocketAsyncEventArgs args) 479 | { 480 | var socketError = args.SocketError; 481 | switch (socketError) 482 | { 483 | case SocketError.Success: 484 | { 485 | // do nothing. 486 | break; 487 | } 488 | default: 489 | { 490 | if (OnError != null) 491 | { 492 | var error = new Exception("send error:" + socketError.ToString()); 493 | OnError(WebuSocketErrorEnum.SEND_FAILED, error); 494 | } 495 | Disconnect(); 496 | break; 497 | } 498 | } 499 | } 500 | 501 | private object lockObj = new object(); 502 | 503 | 504 | /* 505 | buffers. 506 | */ 507 | private byte[] wsBuffer; 508 | private int wsBufIndex; 509 | private int wsBufLength; 510 | 511 | 512 | private void OnReceived(object unused, SocketAsyncEventArgs args) 513 | { 514 | var token = (SocketToken)args.UserToken; 515 | 516 | if (args.SocketError != SocketError.Success) 517 | { 518 | lock (lockObj) 519 | { 520 | switch (token.socketState) 521 | { 522 | case SocketState.CLOSING: 523 | case SocketState.CLOSED: 524 | { 525 | // already closing, ignore. 526 | return; 527 | } 528 | default: 529 | { 530 | // show error, then close or continue receiving. 531 | if (OnError != null) 532 | { 533 | var error = new Exception("receive error:" + args.SocketError.ToString() + " size:" + args.BytesTransferred); 534 | OnError(WebuSocketErrorEnum.RECEIVE_FAILED, error); 535 | } 536 | Disconnect(); 537 | return; 538 | } 539 | } 540 | } 541 | } 542 | 543 | if (args.BytesTransferred == 0) 544 | { 545 | if (OnError != null) 546 | { 547 | var error = new Exception("failed to receive. args.BytesTransferred = 0." + " args.SocketError:" + args.SocketError); 548 | OnError(WebuSocketErrorEnum.RECEIVE_FAILED, error); 549 | } 550 | Disconnect(); 551 | return; 552 | } 553 | 554 | switch (token.socketState) 555 | { 556 | case SocketState.TLS_HANDSHAKING: 557 | { 558 | // set received data to tlsClientProtocol by "OfferInput" method. 559 | // tls handshake phase will progress. 560 | tlsClientProtocol.OfferInputBytes(args.Buffer, args.BytesTransferred); 561 | 562 | // state is changed to TLS_HANDSHAKE_DONE if tls handshake is done inside tlsClientProtocol. 563 | if (token.socketState == SocketState.TLS_HANDSHAKE_DONE) 564 | { 565 | SendWSHandshake(token); 566 | return; 567 | } 568 | 569 | /* 570 | continue handshaking. 571 | */ 572 | 573 | var outputBufferSize = tlsClientProtocol.GetAvailableOutputBytes(); 574 | 575 | if (outputBufferSize == 0) 576 | { 577 | // ready receive next data. 578 | ReadyReceivingNewData(token); 579 | return; 580 | } 581 | 582 | // next tls handshake data is ready inside tlsClientProtocol. 583 | var buffer = new byte[outputBufferSize]; 584 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 585 | 586 | // ready receive next data. 587 | ReadyReceivingNewData(token); 588 | 589 | // send. 590 | token.sendArgs.SetBuffer(buffer, 0, buffer.Length); 591 | if (!token.socket.SendAsync(token.sendArgs)) OnSend(token.socket, token.sendArgs); 592 | return; 593 | } 594 | case SocketState.WS_HANDSHAKING: 595 | { 596 | var receivedData = new byte[args.BytesTransferred]; 597 | Buffer.BlockCopy(args.Buffer, 0, receivedData, 0, receivedData.Length); 598 | 599 | if (isWss) 600 | { 601 | tlsClientProtocol.OfferInput(receivedData); 602 | if (0 < tlsClientProtocol.GetAvailableInputBytes()) 603 | { 604 | var index = 0; 605 | var length = tlsClientProtocol.GetAvailableInputBytes(); 606 | if (webSocketHandshakeResult == null) 607 | { 608 | webSocketHandshakeResult = new byte[length]; 609 | } 610 | else 611 | { 612 | index = webSocketHandshakeResult.Length; 613 | // already hold some bytes, and should expand for holding more decrypted data. 614 | Array.Resize(ref webSocketHandshakeResult, webSocketHandshakeResult.Length + length); 615 | } 616 | 617 | tlsClientProtocol.ReadInput(webSocketHandshakeResult, index, length); 618 | } 619 | 620 | // failed to get tls decrypted data from current receiving data. 621 | // continue receiving next data at the end of this case. 622 | } 623 | else 624 | { 625 | var index = 0; 626 | var length = args.BytesTransferred; 627 | if (webSocketHandshakeResult == null) 628 | { 629 | webSocketHandshakeResult = new byte[args.BytesTransferred]; 630 | } 631 | else 632 | { 633 | index = webSocketHandshakeResult.Length; 634 | // already hold some bytes, and should expand for holding more decrypted data. 635 | Array.Resize(ref webSocketHandshakeResult, webSocketHandshakeResult.Length + length); 636 | } 637 | Buffer.BlockCopy(args.Buffer, 0, webSocketHandshakeResult, index, length); 638 | } 639 | 640 | if (0 < webSocketHandshakeResult.Length) 641 | { 642 | var lineEndCursor = ReadUpgradeLine(webSocketHandshakeResult, 0, webSocketHandshakeResult.Length); 643 | if (lineEndCursor != -1) 644 | { 645 | try 646 | { 647 | var protocolData = new SwitchingProtocolData(Encoding.UTF8.GetString(webSocketHandshakeResult, 0, lineEndCursor)); 648 | var expectedKey = WebSocketByteGenerator.GenerateExpectedAcceptedKey(base64Key); 649 | if (protocolData.securityAccept != expectedKey) 650 | { 651 | if (OnError != null) 652 | { 653 | var error = new Exception("WebSocket Key Unmatched."); 654 | OnError(WebuSocketErrorEnum.WS_HANDSHAKE_KEY_UNMATCHED, error); 655 | } 656 | Disconnect(); 657 | return; 658 | } 659 | } 660 | catch (Exception e) 661 | { 662 | if (OnError != null) 663 | { 664 | OnError(WebuSocketErrorEnum.WS_HANDSHAKE_FAILED, e); 665 | } 666 | Disconnect(); 667 | return; 668 | } 669 | 670 | token.socketState = SocketState.OPENED; 671 | if (OnConnected != null) OnConnected(); 672 | 673 | 674 | var afterHandshakeDataIndex = lineEndCursor + 1;// after last crlf. 675 | 676 | 677 | /* 678 | ready buffer data. 679 | */ 680 | wsBuffer = new byte[baseReceiveBufferSize]; 681 | wsBufIndex = 0; 682 | 683 | 684 | /* 685 | if end cursor of handshake is not equal to holded data length, received data is already contained. 686 | */ 687 | if (webSocketHandshakeResult.Length == afterHandshakeDataIndex) 688 | { 689 | // no extra data exists. 690 | 691 | // ready for receiving websocket data. 692 | ReadyReceivingNewData(token); 693 | } 694 | else 695 | { 696 | wsBufLength = webSocketHandshakeResult.Length - afterHandshakeDataIndex; 697 | 698 | if (wsBuffer.Length < wsBufLength) 699 | { 700 | Array.Resize(ref wsBuffer, wsBufLength); 701 | } 702 | 703 | Buffer.BlockCopy(webSocketHandshakeResult, afterHandshakeDataIndex, wsBuffer, 0, wsBufLength); 704 | 705 | ReadBuffer(token); 706 | } 707 | return; 708 | } 709 | } 710 | 711 | // continue receiveing websocket handshake data. 712 | ReadyReceivingNewData(token); 713 | return; 714 | } 715 | case SocketState.OPENED: 716 | { 717 | if (isWss) 718 | { 719 | // write input to tls buffer. 720 | tlsClientProtocol.OfferInputBytes(args.Buffer, args.BytesTransferred); 721 | 722 | if (0 < tlsClientProtocol.GetAvailableInputBytes()) 723 | { 724 | var additionalLen = tlsClientProtocol.GetAvailableInputBytes(); 725 | 726 | if (wsBuffer.Length < wsBufIndex + additionalLen) 727 | { 728 | Array.Resize(ref wsBuffer, wsBufIndex + additionalLen); 729 | // resizeイベント発生をどう出すかな〜〜 730 | } 731 | 732 | // transfer bytes from tls buffer to wsBuffer. 733 | tlsClientProtocol.ReadInput(wsBuffer, wsBufIndex, additionalLen); 734 | 735 | wsBufLength = wsBufLength + additionalLen; 736 | } 737 | else 738 | { 739 | // received incomlete tls bytes, continue. 740 | ReadyReceivingNewData(token); 741 | return; 742 | } 743 | } 744 | else 745 | { 746 | var additionalLen = args.BytesTransferred; 747 | 748 | if (wsBuffer.Length < wsBufIndex + additionalLen) 749 | { 750 | Array.Resize(ref wsBuffer, wsBufIndex + additionalLen); 751 | // resizeイベント発生をどう出すかな〜〜 752 | } 753 | 754 | Buffer.BlockCopy(args.Buffer, 0, wsBuffer, wsBufIndex, additionalLen); 755 | wsBufLength = wsBufLength + additionalLen; 756 | } 757 | 758 | ReadBuffer(token); 759 | return; 760 | } 761 | default: 762 | { 763 | var error = new Exception("fatal error, could not detect error, receive condition is strange, token.socketState:" + token.socketState); 764 | if (OnError != null) OnError(WebuSocketErrorEnum.RECEIVE_FAILED, error); 765 | Disconnect(); 766 | return; 767 | } 768 | } 769 | } 770 | 771 | /** 772 | consume buffered data. 773 | */ 774 | private void ReadBuffer(SocketToken token) 775 | { 776 | var result = ScanBuffer(wsBuffer, wsBufLength); 777 | 778 | // read completed datas. 779 | if (0 < result.segments.Count) 780 | { 781 | OnMessage(result.segments); 782 | } 783 | 784 | // if the last result index is matched to whole length, receive finished. 785 | if (result.lastDataTail == wsBufLength) 786 | { 787 | wsBufIndex = 0; 788 | wsBufLength = 0; 789 | ReadyReceivingNewData(token); 790 | return; 791 | } 792 | 793 | // unreadable data still exists in wsBuffer. 794 | var unreadDataLength = wsBufLength - result.lastDataTail; 795 | 796 | if (result.lastDataTail == 0) 797 | { 798 | // no data is read as WS data. 799 | // this means the all data in wsBuffer is not enough to read as WS data yet. 800 | // need more data to add the last of wsBuffer. 801 | 802 | // set wsBufferIndex and wsBufLength to the end of current buffer. 803 | wsBufIndex = unreadDataLength; 804 | wsBufLength = unreadDataLength; 805 | } 806 | else 807 | { 808 | // not all of wsBuffer data is read as WS data. 809 | // data which is located before alreadyReadDataTail is already read. 810 | 811 | // move rest "unreaded" data to head of wsBuffer. 812 | Array.Copy(wsBuffer, result.lastDataTail, wsBuffer, 0, unreadDataLength); 813 | 814 | // then set wsBufIndex to 815 | wsBufIndex = unreadDataLength; 816 | wsBufLength = unreadDataLength; 817 | } 818 | 819 | // should read rest. 820 | ReadyReceivingNewData(token); 821 | } 822 | 823 | 824 | private void ReadyReceivingNewData(SocketToken token) 825 | { 826 | token.receiveArgs.SetBuffer(token.receiveBuffer, 0, token.receiveBuffer.Length); 827 | if (!token.socket.ReceiveAsync(token.receiveArgs)) OnReceived(token.socket, token.receiveArgs); 828 | } 829 | 830 | public void Disconnect(WebuSocketCloseEnum closeReason = WebuSocketCloseEnum.CLOSED_FORCIBLY) 831 | { 832 | lock (lockObj) 833 | { 834 | switch (socketToken.socketState) 835 | { 836 | case SocketState.CLOSING: 837 | case SocketState.CLOSED: 838 | { 839 | // do nothing 840 | break; 841 | } 842 | default: 843 | { 844 | socketToken.socketState = SocketState.CLOSING; 845 | if (closeReason != WebuSocketCloseEnum.CLOSED_FORCIBLY) socketToken.closeReason = closeReason; 846 | StartCloseAsync(); 847 | break; 848 | } 849 | } 850 | } 851 | } 852 | 853 | private void StartCloseAsync() 854 | { 855 | var closeEventArgs = new SocketAsyncEventArgs(); 856 | closeEventArgs.UserToken = socketToken; 857 | closeEventArgs.AcceptSocket = socketToken.socket; 858 | 859 | var closeData = WebSocketByteGenerator.CloseData(); 860 | 861 | if (isWss) 862 | { 863 | tlsClientProtocol.OfferOutput(closeData, 0, closeData.Length); 864 | 865 | var count = tlsClientProtocol.GetAvailableOutputBytes(); 866 | var buffer = new byte[count]; 867 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 868 | 869 | closeEventArgs.SetBuffer(buffer, 0, buffer.Length); 870 | } 871 | else 872 | { 873 | closeEventArgs.SetBuffer(closeData, 0, closeData.Length); 874 | } 875 | 876 | closeEventArgs.Completed += new EventHandler(OnDisconnected); 877 | 878 | if (!socketToken.socket.SendAsync(closeEventArgs)) OnDisconnected(socketToken.socket, closeEventArgs); 879 | } 880 | 881 | 882 | public static byte ByteCR = Convert.ToByte('\r'); 883 | public static byte ByteLF = Convert.ToByte('\n'); 884 | public static int ReadUpgradeLine(byte[] bytes, int cursor, long length) 885 | { 886 | while (cursor < length) 887 | { 888 | if (4 < cursor && 889 | bytes[cursor - 3] == ByteCR && 890 | bytes[cursor - 2] == ByteLF && 891 | bytes[cursor - 1] == ByteCR && 892 | bytes[cursor] == ByteLF 893 | ) return cursor;// end point of linefeed. 894 | 895 | cursor++; 896 | } 897 | 898 | return -1; 899 | } 900 | 901 | 902 | private class SwitchingProtocolData 903 | { 904 | // HTTP/1.1 101 Switching Protocols 905 | // Server: nginx/1.7.10 906 | // Date: Sun, 22 May 2016 18:31:47 GMT 907 | // Connection: upgrade 908 | // Upgrade: websocket 909 | // Sec-WebSocket-Accept: C3HoL/ER1LOnEj8yVINdXluouHw= 910 | 911 | public string protocolDesc; 912 | public string httpResponseCode; 913 | public string httpMessage; 914 | public string serverInfo; 915 | public string date; 916 | public string connectionType; 917 | public string upgradeMethod; 918 | public string securityAccept; 919 | 920 | public SwitchingProtocolData(string source) 921 | { 922 | var acceptedResponseHeaderKeyValues = source.Split('\n'); 923 | foreach (var line in acceptedResponseHeaderKeyValues) 924 | { 925 | if (line.StartsWith("HTTP")) 926 | { 927 | var httpResponseHeaderSplitted = line.Split(' '); 928 | this.protocolDesc = httpResponseHeaderSplitted[0]; 929 | this.httpResponseCode = httpResponseHeaderSplitted[1]; 930 | this.httpMessage = httpResponseHeaderSplitted[2] + httpResponseHeaderSplitted[3]; 931 | continue; 932 | } 933 | 934 | if (!line.Contains(": ")) continue; 935 | 936 | var keyAndValue = line.Replace(": ", ":").Split(':'); 937 | 938 | switch (keyAndValue[0].ToLower()) 939 | { 940 | case "server": 941 | { 942 | this.serverInfo = keyAndValue[1]; 943 | break; 944 | } 945 | case "date": 946 | { 947 | this.date = keyAndValue[1]; 948 | break; 949 | } 950 | case "connection": 951 | { 952 | this.connectionType = keyAndValue[1]; 953 | break; 954 | } 955 | case "upgrade": 956 | { 957 | this.upgradeMethod = keyAndValue[1]; 958 | break; 959 | } 960 | case "sec-websocket-accept": 961 | { 962 | this.securityAccept = keyAndValue[1].TrimEnd(); 963 | break; 964 | } 965 | default: 966 | { 967 | throw new Exception("invalid key value found. line:" + line); 968 | } 969 | } 970 | } 971 | } 972 | } 973 | 974 | private Queue> receivedDataSegments = new Queue>(); 975 | private byte[] continuationBuffer; 976 | private int continuationBufferIndex; 977 | private WebuSocketResults ScanBuffer(byte[] buffer, long bufferLength) 978 | { 979 | receivedDataSegments.Clear(); 980 | 981 | int cursor = 0; 982 | int lastDataEnd = 0; 983 | while (cursor < bufferLength) 984 | { 985 | 986 | // first byte = fin(1), rsv1(1), rsv2(1), rsv3(1), opCode(4) 987 | var opCode = (byte)(buffer[cursor++] & WebSocketByteGenerator.OPFilter); 988 | 989 | // second byte = mask(1), length(7) 990 | if (bufferLength < cursor) break; 991 | 992 | /* 993 | mask of data from server is definitely zero(0). 994 | ignore reading mask bit. 995 | */ 996 | int length = buffer[cursor++]; 997 | switch (length) 998 | { 999 | case 126: 1000 | { 1001 | // next 2 byte is length data. 1002 | if (bufferLength < cursor + 2) break; 1003 | 1004 | length = ( 1005 | (buffer[cursor++] << 8) + 1006 | (buffer[cursor++]) 1007 | ); 1008 | break; 1009 | } 1010 | case 127: 1011 | { 1012 | // next 8 byte is length data. 1013 | if (bufferLength < cursor + 8) break; 1014 | 1015 | length = ( 1016 | (buffer[cursor++] << (8 * 7)) + 1017 | (buffer[cursor++] << (8 * 6)) + 1018 | (buffer[cursor++] << (8 * 5)) + 1019 | (buffer[cursor++] << (8 * 4)) + 1020 | (buffer[cursor++] << (8 * 3)) + 1021 | (buffer[cursor++] << (8 * 2)) + 1022 | (buffer[cursor++] << 8) + 1023 | (buffer[cursor++]) 1024 | ); 1025 | break; 1026 | } 1027 | default: 1028 | { 1029 | // other. 1030 | break; 1031 | } 1032 | } 1033 | 1034 | // read payload data. 1035 | if (bufferLength < cursor + length) break; 1036 | 1037 | // payload is fully contained! 1038 | switch (opCode) 1039 | { 1040 | case WebSocketByteGenerator.OP_CONTINUATION: 1041 | { 1042 | if (continuationBuffer == null) continuationBuffer = new byte[baseReceiveBufferSize]; 1043 | if (continuationBuffer.Length <= continuationBufferIndex + length) Array.Resize(ref continuationBuffer, continuationBufferIndex + length); 1044 | 1045 | // pool data to continuation buffer. 1046 | Buffer.BlockCopy(buffer, cursor, continuationBuffer, continuationBufferIndex, length); 1047 | continuationBufferIndex += length; 1048 | break; 1049 | } 1050 | case WebSocketByteGenerator.OP_TEXT: 1051 | case WebSocketByteGenerator.OP_BINARY: 1052 | { 1053 | if (continuationBufferIndex == 0) receivedDataSegments.Enqueue(new ArraySegment(buffer, cursor, length)); 1054 | else 1055 | { 1056 | if (continuationBuffer.Length <= continuationBufferIndex + length) Array.Resize(ref continuationBuffer, continuationBufferIndex + length); 1057 | Buffer.BlockCopy(buffer, cursor, continuationBuffer, continuationBufferIndex, length); 1058 | continuationBufferIndex += length; 1059 | 1060 | receivedDataSegments.Enqueue(new ArraySegment(continuationBuffer, 0, continuationBufferIndex)); 1061 | 1062 | // reset continuationBuffer index. 1063 | continuationBufferIndex = 0; 1064 | } 1065 | break; 1066 | } 1067 | case WebSocketByteGenerator.OP_CLOSE: 1068 | { 1069 | CloseReceived(); 1070 | break; 1071 | } 1072 | case WebSocketByteGenerator.OP_PING: 1073 | { 1074 | /* 1075 | if server sent ping data with application data, open it. 1076 | */ 1077 | if (0 < length) 1078 | { 1079 | var data = new byte[length]; 1080 | Buffer.BlockCopy(buffer, cursor, data, 0, length); 1081 | PingReceived(data); 1082 | } 1083 | else 1084 | { 1085 | PingReceived(); 1086 | } 1087 | break; 1088 | } 1089 | case WebSocketByteGenerator.OP_PONG: 1090 | { 1091 | /* 1092 | if server sent pong with application data, open it. 1093 | */ 1094 | if (0 < length) 1095 | { 1096 | var data = new byte[length]; 1097 | Buffer.BlockCopy(buffer, cursor, data, 0, length); 1098 | PongReceived(data); 1099 | } 1100 | else 1101 | { 1102 | PongReceived(); 1103 | } 1104 | break; 1105 | } 1106 | default: 1107 | { 1108 | break; 1109 | } 1110 | } 1111 | 1112 | cursor = cursor + length; 1113 | 1114 | // set end of data. 1115 | lastDataEnd = cursor; 1116 | } 1117 | 1118 | // finally return payload data indexies. 1119 | return new WebuSocketResults(receivedDataSegments, lastDataEnd); 1120 | } 1121 | 1122 | private struct WebuSocketResults 1123 | { 1124 | public Queue> segments; 1125 | public int lastDataTail; 1126 | 1127 | public WebuSocketResults(Queue> segments, int lastDataTail) 1128 | { 1129 | this.segments = segments; 1130 | this.lastDataTail = lastDataTail; 1131 | } 1132 | } 1133 | private byte[] sendingPingData; 1134 | private byte[] ignoringPingData = new byte[0]; 1135 | private Action _OnPonged; 1136 | public void Ping(Action onPonged, byte[] data = null) 1137 | { 1138 | if (timeoutCheckCoroutine != null) 1139 | { 1140 | // すでに動いていたら、現在動いているping sendedの返答が帰ってくるのを待っている。 1141 | // これをignoreする必要がある。 1142 | // timeoutCheckCoroutineの初期化とpingがセットになっているので、現状sendingPingDataは必ずnullではない。 1143 | // IsConnectedを実行するのが先か、Pingを実行するのが先か、という感じになる。 1144 | ignoringPingData = sendingPingData; 1145 | } 1146 | 1147 | // force renew timeout-check-coroutine. 1148 | timeoutCheckCoroutine = GenerateTimeoutCheckCoroutine(data, onPonged); 1149 | 1150 | // update coroutine. first time = send ping data to server. 1151 | UpdateTimeoutCoroutine(); 1152 | } 1153 | 1154 | private void _Ping(Action _onPonged, byte[] data = null) 1155 | { 1156 | if (socketToken.socketState != SocketState.OPENED) 1157 | { 1158 | WebuSocketErrorEnum ev = WebuSocketErrorEnum.UNKNOWN_ERROR; 1159 | Exception error = null; 1160 | switch (socketToken.socketState) 1161 | { 1162 | case SocketState.TLS_HANDSHAKING: 1163 | case SocketState.WS_HANDSHAKING: 1164 | { 1165 | ev = WebuSocketErrorEnum.CONNECTING; 1166 | error = new Exception("send error:" + "not yet connected."); 1167 | break; 1168 | } 1169 | case SocketState.CLOSING: 1170 | case SocketState.CLOSED: 1171 | { 1172 | ev = WebuSocketErrorEnum.ALREADY_DISCONNECTED; 1173 | error = new Exception("send error:" + "connection was already closed. please create new connection by new WebuSocket()."); 1174 | break; 1175 | } 1176 | default: 1177 | { 1178 | ev = WebuSocketErrorEnum.CONNECTING; 1179 | error = new Exception("send error:" + "not yet connected."); 1180 | break; 1181 | } 1182 | } 1183 | if (OnError != null) OnError(ev, error); 1184 | return; 1185 | } 1186 | 1187 | /* 1188 | update onPong handler. this handler will be fired at most once. 1189 | */ 1190 | this._OnPonged = _onPonged; 1191 | var pingBytes = WebSocketByteGenerator.Ping(data); 1192 | 1193 | if (isWss) 1194 | { 1195 | tlsClientProtocol.OfferOutput(pingBytes, 0, pingBytes.Length); 1196 | 1197 | var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()]; 1198 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 1199 | 1200 | try 1201 | { 1202 | socketToken.socket.BeginSend( 1203 | buffer, 1204 | 0, 1205 | buffer.Length, 1206 | SocketFlags.None, 1207 | result => 1208 | { 1209 | var s = (Socket)result.AsyncState; 1210 | var len = s.EndSend(result); 1211 | if (0 < len) 1212 | { 1213 | // do nothing. 1214 | } 1215 | else 1216 | { 1217 | // send failed. 1218 | if (OnError != null) 1219 | { 1220 | var error = new Exception("send error:" + "ping failed by unknown reason."); 1221 | OnError(WebuSocketErrorEnum.PING_FAILED, error); 1222 | } 1223 | } 1224 | }, 1225 | socketToken.socket 1226 | ); 1227 | } 1228 | catch (Exception e) 1229 | { 1230 | if (OnError != null) 1231 | { 1232 | OnError(WebuSocketErrorEnum.PING_FAILED, e); 1233 | } 1234 | Disconnect(); 1235 | } 1236 | } 1237 | else 1238 | { 1239 | try 1240 | { 1241 | socketToken.socket.BeginSend( 1242 | pingBytes, 1243 | 0, 1244 | pingBytes.Length, 1245 | SocketFlags.None, 1246 | result => 1247 | { 1248 | var s = (Socket)result.AsyncState; 1249 | var len = s.EndSend(result); 1250 | if (0 < len) 1251 | { 1252 | // do nothing. 1253 | } 1254 | else 1255 | { 1256 | // send failed. 1257 | if (OnError != null) 1258 | { 1259 | var error = new Exception("send error:" + "ping failed by unknown reason."); 1260 | OnError(WebuSocketErrorEnum.PING_FAILED, error); 1261 | } 1262 | } 1263 | }, 1264 | socketToken.socket 1265 | ); 1266 | } 1267 | catch (Exception e) 1268 | { 1269 | if (OnError != null) 1270 | { 1271 | OnError(WebuSocketErrorEnum.PING_FAILED, e); 1272 | } 1273 | Disconnect(); 1274 | } 1275 | } 1276 | } 1277 | 1278 | public void Send(byte[] data) 1279 | { 1280 | if (socketToken.socketState != SocketState.OPENED) 1281 | { 1282 | WebuSocketErrorEnum ev = WebuSocketErrorEnum.UNKNOWN_ERROR; 1283 | Exception error = null; 1284 | switch (socketToken.socketState) 1285 | { 1286 | case SocketState.TLS_HANDSHAKING: 1287 | case SocketState.WS_HANDSHAKING: 1288 | { 1289 | ev = WebuSocketErrorEnum.CONNECTING; 1290 | error = new Exception("send error:" + "not yet connected."); 1291 | break; 1292 | } 1293 | case SocketState.CLOSING: 1294 | case SocketState.CLOSED: 1295 | { 1296 | ev = WebuSocketErrorEnum.ALREADY_DISCONNECTED; 1297 | error = new Exception("send error:" + "connection was already closed. please create new connection by new WebuSocket()."); 1298 | break; 1299 | } 1300 | default: 1301 | { 1302 | ev = WebuSocketErrorEnum.CONNECTING; 1303 | error = new Exception("send error:" + "not yet connected."); 1304 | break; 1305 | } 1306 | } 1307 | if (OnError != null) OnError(ev, error); 1308 | return; 1309 | } 1310 | 1311 | var payloadBytes = WebSocketByteGenerator.SendBinaryData(data); 1312 | 1313 | if (isWss) 1314 | { 1315 | tlsClientProtocol.OfferOutput(payloadBytes, 0, payloadBytes.Length); 1316 | 1317 | var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()]; 1318 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 1319 | 1320 | try 1321 | { 1322 | socketToken.socket.BeginSend( 1323 | buffer, 1324 | 0, 1325 | buffer.Length, 1326 | SocketFlags.None, 1327 | result => 1328 | { 1329 | var s = (Socket)result.AsyncState; 1330 | var len = s.EndSend(result); 1331 | if (0 < len) 1332 | { 1333 | // do nothing. 1334 | } 1335 | else 1336 | { 1337 | // send failed. 1338 | if (OnError != null) 1339 | { 1340 | var error = new Exception("send error:" + "send failed by unknown reason."); 1341 | OnError(WebuSocketErrorEnum.SEND_FAILED, error); 1342 | } 1343 | } 1344 | }, 1345 | socketToken.socket 1346 | ); 1347 | } 1348 | catch (Exception e) 1349 | { 1350 | if (OnError != null) 1351 | { 1352 | OnError(WebuSocketErrorEnum.SEND_FAILED, e); 1353 | } 1354 | Disconnect(); 1355 | } 1356 | } 1357 | else 1358 | { 1359 | try 1360 | { 1361 | socketToken.socket.BeginSend( 1362 | payloadBytes, 1363 | 0, 1364 | payloadBytes.Length, 1365 | SocketFlags.None, 1366 | result => 1367 | { 1368 | var s = (Socket)result.AsyncState; 1369 | var len = s.EndSend(result); 1370 | if (0 < len) 1371 | { 1372 | // do nothing. 1373 | } 1374 | else 1375 | { 1376 | // send failed. 1377 | if (OnError != null) 1378 | { 1379 | var error = new Exception("send error:" + "send failed by unknown reason."); 1380 | OnError(WebuSocketErrorEnum.SEND_FAILED, error); 1381 | } 1382 | } 1383 | }, 1384 | socketToken.socket 1385 | ); 1386 | } 1387 | catch (Exception e) 1388 | { 1389 | if (OnError != null) 1390 | { 1391 | OnError(WebuSocketErrorEnum.SEND_FAILED, e); 1392 | } 1393 | Disconnect(); 1394 | } 1395 | } 1396 | } 1397 | 1398 | public void SendString(string data) 1399 | { 1400 | if (socketToken.socketState != SocketState.OPENED) 1401 | { 1402 | WebuSocketErrorEnum ev = WebuSocketErrorEnum.UNKNOWN_ERROR; 1403 | Exception error = null; 1404 | switch (socketToken.socketState) 1405 | { 1406 | case SocketState.TLS_HANDSHAKING: 1407 | case SocketState.WS_HANDSHAKING: 1408 | { 1409 | ev = WebuSocketErrorEnum.CONNECTING; 1410 | error = new Exception("send error:" + "not yet connected."); 1411 | break; 1412 | } 1413 | case SocketState.CLOSING: 1414 | case SocketState.CLOSED: 1415 | { 1416 | ev = WebuSocketErrorEnum.ALREADY_DISCONNECTED; 1417 | error = new Exception("send error:" + "connection was already closed. please create new connection by new WebuSocket()."); 1418 | break; 1419 | } 1420 | default: 1421 | { 1422 | ev = WebuSocketErrorEnum.CONNECTING; 1423 | error = new Exception("send error:" + "not yet connected."); 1424 | break; 1425 | } 1426 | } 1427 | if (OnError != null) OnError(ev, error); 1428 | return; 1429 | } 1430 | 1431 | var byteData = Encoding.UTF8.GetBytes(data); 1432 | var payloadBytes = WebSocketByteGenerator.SendTextData(byteData); 1433 | 1434 | if (isWss) 1435 | { 1436 | tlsClientProtocol.OfferOutput(payloadBytes, 0, payloadBytes.Length); 1437 | 1438 | var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()]; 1439 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 1440 | 1441 | try 1442 | { 1443 | socketToken.socket.BeginSend( 1444 | buffer, 1445 | 0, 1446 | buffer.Length, 1447 | SocketFlags.None, 1448 | result => 1449 | { 1450 | var s = (Socket)result.AsyncState; 1451 | var len = s.EndSend(result); 1452 | if (0 < len) 1453 | { 1454 | // do nothing. 1455 | } 1456 | else 1457 | { 1458 | // send failed. 1459 | if (OnError != null) 1460 | { 1461 | var error = new Exception("send error:" + "send failed by unknown reason."); 1462 | OnError(WebuSocketErrorEnum.SEND_FAILED, error); 1463 | } 1464 | } 1465 | }, 1466 | socketToken.socket 1467 | ); 1468 | } 1469 | catch (Exception e) 1470 | { 1471 | if (OnError != null) 1472 | { 1473 | OnError(WebuSocketErrorEnum.SEND_FAILED, e); 1474 | } 1475 | Disconnect(); 1476 | } 1477 | } 1478 | else 1479 | { 1480 | try 1481 | { 1482 | socketToken.socket.BeginSend( 1483 | payloadBytes, 1484 | 0, 1485 | payloadBytes.Length, 1486 | SocketFlags.None, 1487 | result => 1488 | { 1489 | var s = (Socket)result.AsyncState; 1490 | var len = s.EndSend(result); 1491 | if (0 < len) 1492 | { 1493 | // do nothing. 1494 | } 1495 | else 1496 | { 1497 | // send failed. 1498 | if (OnError != null) 1499 | { 1500 | var error = new Exception("send error:" + "send failed by unknown reason."); 1501 | OnError(WebuSocketErrorEnum.SEND_FAILED, error); 1502 | } 1503 | } 1504 | }, 1505 | socketToken.socket 1506 | ); 1507 | } 1508 | catch (Exception e) 1509 | { 1510 | if (OnError != null) 1511 | { 1512 | OnError(WebuSocketErrorEnum.SEND_FAILED, e); 1513 | } 1514 | Disconnect(); 1515 | } 1516 | } 1517 | } 1518 | 1519 | private bool IsStateOpened() 1520 | { 1521 | if (socketToken.socketState != SocketState.OPENED) return false; 1522 | return true; 1523 | } 1524 | 1525 | /** 1526 | CAUTION: this feature expects that Ping with application data will be returned by server as Pong with same application data. 1527 | by https://tools.ietf.org/html/rfc6455#section-5.5.2 1528 | 1529 | this method is for checking websocket connectivity with timeout sec setting. 1530 | it is good that call this method very constantly. because this check method is very lightweight. 1531 | 1532 | if you set a timeout of 2 second and call this method every 3 seconds, you can detect the timeout in 3 seconds. 1533 | also if you set a timeout of X second and call this method every Y seconds, you can detect the timeout in Y seconds. 1534 | 1535 | this method calls Ping internally but interval of Ping is always larger than timeout sec. 1536 | 1537 | return true if waiting ping in timeout sec. 1538 | return true if ping is returned in timeout sec. 1539 | return true and send new ping if timeout sec passed. 1540 | else, return false. 1541 | */ 1542 | private byte pingCount; 1543 | public bool IsConnected(int newTimeoutSec = DEFAULT_TIMEOUT_SEC) 1544 | { 1545 | if (newTimeoutSec <= 0) return IsStateOpened(); 1546 | 1547 | // state check. 1548 | if (!IsStateOpened()) return false; 1549 | 1550 | // set new timeout. 1551 | this.timeoutSec = newTimeoutSec; 1552 | 1553 | /* 1554 | create coroutine for holding timeout checker if this method called faster than Ping. 1555 | */ 1556 | if (timeoutCheckCoroutine == null) 1557 | { 1558 | var data = new byte[] { pingCount }; 1559 | timeoutCheckCoroutine = GenerateTimeoutCheckCoroutine(data, _ => { }); 1560 | } 1561 | 1562 | return UpdateTimeoutCoroutine(); 1563 | } 1564 | 1565 | private bool UpdateTimeoutCoroutine() 1566 | { 1567 | // update coroutine. 1568 | timeoutCheckCoroutine.MoveNext(); 1569 | var isInTimelimit = timeoutCheckCoroutine.Current; 1570 | 1571 | if (!isInTimelimit) 1572 | { 1573 | Disconnect(WebuSocketCloseEnum.CLOSED_BY_TIMEOUT); 1574 | } 1575 | 1576 | return isInTimelimit; 1577 | } 1578 | private int timeoutSec; 1579 | private int rttMilliSec; 1580 | 1581 | public int RttMilliseconds 1582 | { 1583 | get 1584 | { 1585 | return rttMilliSec; 1586 | } 1587 | } 1588 | 1589 | private IEnumerator timeoutCheckCoroutine; 1590 | 1591 | /** 1592 | このメソッドは2つのルートで使われている。 1593 | ・IsConnected()で接続性チェックをするため 1594 | ・PingでPingデータを送るため 1595 | 1596 | ユーザーがPingデータを送付しようとした際、このCoroutineは常に新規作成され、Pingデータを直ちに送付し、その後サーバからPongが帰ってくる。 1597 | そのままCoroutine自体は生き続ける。 1598 | 1599 | ユーザーがIsConnectedを実行した際、もしPingを打っていない状態だったら、Pingを送る。 1600 | 以降、このメソッドを実行するたびに、次のことが起きる。 1601 | 実行時、Ping送信から指定時間内だったら、trueを返す。 1602 | 実行時、指定時間外でPongが帰ってきていたら、trueを返しつつ、次のPingを送付する。 1603 | 実行時、指定時間外でまだPongが帰ってきていなかったら、falseを返し、Disconnectを行う。 1604 | 1605 | もしユーザーがIsConnected -> Pingの順で呼んだ場合、IsConnectedで送付したPingのPongは無視される。 1606 | (送信済みのidentifyされたbyteが帰ってきても、無視する) 1607 | Ping実行の際、Coroutineは新規作成されるので、以降のIsConnectedで利用される。 1608 | 1609 | もしユーザーがPing -> IsConnectedの順で呼んだ場合、PingでCoroutineは新規作成され、Ping中になるため、 1610 | IsConnectedはtimeoutまでのあいだ常にtrueを返す。 1611 | */ 1612 | private IEnumerator GenerateTimeoutCheckCoroutine(byte[] firstPingData, Action firstOnPong) 1613 | { 1614 | var waitingPong = true; 1615 | var currentDate = DateTime.UtcNow; 1616 | var limitTick = (TimeSpan.FromTicks(currentDate.Ticks) + TimeSpan.FromSeconds(timeoutSec)).Ticks; 1617 | sendingPingData = firstPingData; 1618 | _Ping( 1619 | () => 1620 | { 1621 | this.rttMilliSec = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - currentDate.Ticks).Milliseconds; 1622 | waitingPong = false; 1623 | if (firstOnPong != null) firstOnPong(rttMilliSec);// call only first pong. 1624 | }, 1625 | firstPingData 1626 | ); 1627 | 1628 | // return true at first time. 1629 | yield return true; 1630 | 1631 | // second and later. called by IsConnected method. 1632 | while (true) 1633 | { 1634 | /* 1635 | if state is not OPENED, this connection is already disconnected. 1636 | */ 1637 | var isConnectedSync = IsStateOpened(); 1638 | if (!isConnectedSync) 1639 | { 1640 | yield return false; 1641 | break; 1642 | } 1643 | 1644 | /* 1645 | continue if current sec does not reached to timelimit. 1646 | */ 1647 | if (DateTime.UtcNow.Ticks < limitTick) 1648 | { 1649 | yield return true; 1650 | continue; 1651 | } 1652 | 1653 | /* 1654 | if pong is not returned yet, that is timeout. 1655 | (this method call is at least exceed the timeout.) 1656 | this connection is already disconnected. 1657 | */ 1658 | if (waitingPong) 1659 | { 1660 | yield return false; 1661 | break; 1662 | } 1663 | 1664 | /* 1665 | send next ping and timeout after timelimit. 1666 | */ 1667 | waitingPong = true; 1668 | currentDate = DateTime.UtcNow; 1669 | limitTick = (TimeSpan.FromTicks(currentDate.Ticks) + TimeSpan.FromSeconds(timeoutSec)).Ticks; 1670 | 1671 | /* 1672 | set new ping data. 1673 | data is 1 ~ byte.MaxValue. 1674 | */ 1675 | var data = new byte[] { ++pingCount }; 1676 | if (byte.MaxValue == pingCount) 1677 | { 1678 | pingCount = 0; 1679 | } 1680 | 1681 | sendingPingData = data; 1682 | _Ping( 1683 | () => 1684 | { 1685 | this.rttMilliSec = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - currentDate.Ticks).Milliseconds; 1686 | waitingPong = false; 1687 | }, 1688 | data 1689 | ); 1690 | 1691 | yield return true; 1692 | continue; 1693 | } 1694 | } 1695 | 1696 | private void CloseReceived() 1697 | { 1698 | lock (lockObj) 1699 | { 1700 | switch (socketToken.socketState) 1701 | { 1702 | case SocketState.OPENED: 1703 | { 1704 | socketToken.socketState = SocketState.CLOSED; 1705 | if (OnClosed != null) OnClosed(WebuSocketCloseEnum.CLOSED_BY_SERVER); 1706 | Disconnect(); 1707 | break; 1708 | } 1709 | default: 1710 | { 1711 | 1712 | break; 1713 | } 1714 | } 1715 | } 1716 | } 1717 | 1718 | private void PingReceived(byte[] data = null) 1719 | { 1720 | var pongBytes = WebSocketByteGenerator.Pong(data); 1721 | 1722 | if (isWss) 1723 | { 1724 | tlsClientProtocol.OfferOutput(pongBytes, 0, pongBytes.Length); 1725 | 1726 | var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()]; 1727 | tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length); 1728 | 1729 | try 1730 | { 1731 | socketToken.socket.BeginSend( 1732 | buffer, 1733 | 0, 1734 | buffer.Length, 1735 | SocketFlags.None, 1736 | result => 1737 | { 1738 | var s = (Socket)result.AsyncState; 1739 | var len = s.EndSend(result); 1740 | if (0 < len) 1741 | { 1742 | // do nothing. 1743 | } 1744 | else 1745 | { 1746 | // send failed. 1747 | if (OnError != null) 1748 | { 1749 | var error = new Exception("send error:" + "pong failed by unknown reason."); 1750 | OnError(WebuSocketErrorEnum.PONG_FAILED, error); 1751 | } 1752 | } 1753 | }, 1754 | socketToken.socket 1755 | ); 1756 | } 1757 | catch (Exception e) 1758 | { 1759 | if (OnError != null) 1760 | { 1761 | OnError(WebuSocketErrorEnum.PONG_FAILED, e); 1762 | } 1763 | Disconnect(); 1764 | } 1765 | } 1766 | else 1767 | { 1768 | try 1769 | { 1770 | socketToken.socket.BeginSend( 1771 | pongBytes, 1772 | 0, 1773 | pongBytes.Length, 1774 | SocketFlags.None, 1775 | result => 1776 | { 1777 | var s = (Socket)result.AsyncState; 1778 | var len = s.EndSend(result); 1779 | if (0 < len) 1780 | { 1781 | // do nothing. 1782 | } 1783 | else 1784 | { 1785 | // send failed. 1786 | if (OnError != null) 1787 | { 1788 | var error = new Exception("send error:" + "pong failed by unknown reason."); 1789 | OnError(WebuSocketErrorEnum.PONG_FAILED, error); 1790 | } 1791 | } 1792 | }, 1793 | socketToken.socket 1794 | ); 1795 | } 1796 | catch (Exception e) 1797 | { 1798 | if (OnError != null) 1799 | { 1800 | OnError(WebuSocketErrorEnum.PONG_FAILED, e); 1801 | } 1802 | Disconnect(); 1803 | } 1804 | } 1805 | 1806 | if (OnPinged != null) OnPinged(); 1807 | } 1808 | 1809 | private void PongReceived(byte[] data = null) 1810 | { 1811 | if (data != null && ignoringPingData.Length == data.Length) 1812 | { 1813 | // check if data is perfectly same. 1814 | for (var i = 0; i < ignoringPingData.Length; i++) 1815 | { 1816 | /* 1817 | data not matched. call pong once. 1818 | */ 1819 | if (ignoringPingData[i] != data[i]) 1820 | { 1821 | if (_OnPonged != null) 1822 | { 1823 | _OnPonged(); 1824 | _OnPonged = null; 1825 | } 1826 | return; 1827 | } 1828 | } 1829 | } 1830 | 1831 | /* 1832 | call pong once. 1833 | */ 1834 | if (_OnPonged != null) 1835 | { 1836 | _OnPonged(); 1837 | _OnPonged = null; 1838 | } 1839 | } 1840 | } 1841 | } 1842 | -------------------------------------------------------------------------------- /WebuSocket/lib/BouncyCastle.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassembla/WebuSocket/d032babe835dc530479f0af11e4a30734c57a23e/WebuSocket/lib/BouncyCastle.dll -------------------------------------------------------------------------------- /WebuSocket/lib/derivedfrom.txt: -------------------------------------------------------------------------------- 1 | https://github.com/bcgit/bc-csharp -------------------------------------------------------------------------------- /WebuSocketTests/Editor/TestRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Threading; 7 | using UnityEditor; 8 | using UnityEngine; 9 | 10 | using WebuSocketCore; 11 | 12 | public class TestRunner { 13 | 14 | 15 | 16 | [MenuItem("WebuSocketTest/RunTests")] public static void RunTests () { 17 | /* 18 | note that this tests requires echo WebSocket server. 19 | And Server code is not contained this repo yet. 20 | */ 21 | var testRunner = new TestRunner(); 22 | } 23 | 24 | List tests; 25 | public TestRunner () { 26 | totalFrame = 0; 27 | 28 | Start(); 29 | 30 | // ver 0.5.0 31 | // using new frequently. 32 | // 3586 33 | // 4162 34 | // 3632 35 | // 3861 36 | // 3677 37 | 38 | // total avg 3783.6 39 | 40 | // using Array.Resize. Winner!! 41 | // 3511 42 | // 3411 43 | // 3465 44 | // 3468 45 | // 3451 46 | 47 | // total avg 3461.2 FASTEST. ver 0.5.1 48 | 49 | // take2 50 | // 3452 51 | // 3610 52 | // 3397 53 | // 3400 54 | // 3455 55 | 56 | // total 3462.8 57 | 58 | 59 | // using pre-allocated buffer. 65535 * 10. 60 | // 3389 61 | // 3445 62 | // 3577 63 | // 3492 64 | // 3539 65 | 66 | // total avg 3488.4 67 | 68 | // take2 69 | // 3419 70 | // 3395 71 | // 3569 72 | // 3526 73 | // 3528 74 | 75 | // total avg 3487.4 76 | 77 | // ver 0.5.1 78 | // 3416 79 | // 3574 80 | // 3447 81 | // 3504 82 | // 3502 83 | 84 | // total avg 3488.6 85 | 86 | } 87 | 88 | private int totalFrame; 89 | 90 | private void Start () { 91 | WebuSocket webuSocket = null; 92 | RunThrough(webuSocket, tests, Teardown); 93 | } 94 | 95 | private void Next () { 96 | tests.RemoveAt(0); 97 | 98 | if (!tests.Any()) { 99 | Debug.LogError("all tests finished. totalFrame:" + totalFrame); 100 | return; 101 | } 102 | 103 | Start(); 104 | } 105 | 106 | private IEnumerator Setup (WebuSocket webuSocket, ITestCase test) { 107 | Debug.LogWarning("test:" + test.ToString() + " started,"); 108 | 109 | var optionalParams = test.OnOptionalSettings(); 110 | var throttle = optionalParams.throttle; 111 | var hearderValues = optionalParams.headerValues; 112 | 113 | webuSocket = new WebuSocket( 114 | "ws://127.0.0.1:2501", 115 | 102400, 116 | () => { 117 | test.OnConnect(webuSocket); 118 | }, 119 | datas => { 120 | var dataBytes = new Queue();// とりあえず空の。 121 | test.OnReceived(webuSocket, dataBytes); 122 | }, 123 | () => { 124 | // onPinged. 125 | }, 126 | closeReason => { 127 | Debug.LogWarning("closeReason:" + closeReason); 128 | }, 129 | (errorReason, exception) => { 130 | Debug.LogError("errorReason:" + errorReason); 131 | if (exception != null) { 132 | if (exception.GetType() == typeof(SocketException)) Debug.LogError("SocketErrorCode:" + ((SocketException)exception).SocketErrorCode); 133 | } 134 | } 135 | ); 136 | 137 | var frame = 0; 138 | while (!webuSocket.IsConnected()) { 139 | frame++; 140 | yield return 0; 141 | } 142 | 143 | // while (webuSocket.State() != WebuSocket.WSConnectionState.Closed) { 144 | // frame++; 145 | // yield return 0; 146 | // } 147 | 148 | // closed. 149 | Debug.Log("test:" + test.ToString() + " passed. spent frame:" + frame); 150 | yield return frame; 151 | } 152 | 153 | private void Teardown (WebuSocket webuSocket) { 154 | if (webuSocket != null) { 155 | Debug.LogError("failed to close."); 156 | webuSocket.Disconnect(true); 157 | } 158 | webuSocket = null; 159 | } 160 | 161 | private Thread RunThrough (WebuSocket webuSocket, List tests, Action Teardown) { 162 | var framePerSecond = 60; 163 | var mainThreadInterval = 1000f / framePerSecond; 164 | 165 | var test = tests[0]; 166 | 167 | var timeout = test.OnOptionalSettings().timeout; 168 | 169 | Action loopMethod = () => { 170 | try { 171 | var enumeration = Setup(webuSocket, test); 172 | 173 | double nextFrame = (double)System.Environment.TickCount; 174 | 175 | var before = 0.0; 176 | var tickCount = (double)System.Environment.TickCount; 177 | 178 | var testFrameCount = 0; 179 | 180 | while (true) { 181 | tickCount = System.Environment.TickCount * 1.0; 182 | if (nextFrame - tickCount > 1) { 183 | Thread.Sleep((int)(nextFrame - tickCount)/2); 184 | /* 185 | waitを半分くらいにすると特定フレームで安定する。よくない。 186 | */ 187 | continue; 188 | } 189 | 190 | if (tickCount >= nextFrame + mainThreadInterval) { 191 | nextFrame += mainThreadInterval; 192 | continue; 193 | } 194 | 195 | // run action for update. 196 | var continuation = enumeration.MoveNext(); 197 | if (!continuation) break; 198 | 199 | nextFrame += mainThreadInterval; 200 | before = tickCount; 201 | 202 | testFrameCount++; 203 | 204 | if (timeout < testFrameCount) { 205 | Debug.LogError("timeout:" + test.ToString()); 206 | break; 207 | } 208 | } 209 | totalFrame += enumeration.Current; 210 | Teardown(webuSocket); 211 | } catch (Exception e) { 212 | Debug.LogError("test loopId:" + test.ToString() + " error:" + e); 213 | } 214 | 215 | Next(); 216 | }; 217 | 218 | var thread = new Thread(new ThreadStart(loopMethod)); 219 | thread.Start(); 220 | return thread; 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test0.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Miyamasu; 4 | using UnityEngine; 5 | using WebuSocketCore; 6 | 7 | public class Receiver { 8 | public int connectedCount = 0; 9 | public List byteDatas = new List(); 10 | public int pingedCount = 0; 11 | public string closedReason; 12 | public string errorReason; 13 | public Exception errorException; 14 | 15 | public void Connected () { 16 | connectedCount++; 17 | } 18 | public void Received (Queue> incomingDatas) { 19 | while (0 < incomingDatas.Count) { 20 | var incomingArraySegment = incomingDatas.Dequeue(); 21 | 22 | var byteData = new byte[incomingArraySegment.Count]; 23 | Buffer.BlockCopy(incomingArraySegment.Array, incomingArraySegment.Offset, byteData, 0, incomingArraySegment.Count); 24 | byteDatas.Add(byteData); 25 | } 26 | } 27 | public void Pinged () { 28 | pingedCount++; 29 | } 30 | public void Closed (string reason) { 31 | closedReason = reason; 32 | } 33 | public void Error (string error, Exception ex) { 34 | errorReason = error; 35 | errorException = ex; 36 | } 37 | } 38 | 39 | 40 | public class Test0 : MiyamasuTestRunner { 41 | WebuSocket webuSocket; 42 | 43 | private Receiver receiverInstance; 44 | 45 | [MSetup] public void Setup () { 46 | receiverInstance = new Receiver(); 47 | 48 | var connected = false; 49 | webuSocket = new WebuSocket( 50 | "ws://127.0.0.1:8081", 51 | 1024 * 100, 52 | () => { 53 | connected = true; 54 | receiverInstance.Connected(); 55 | }, 56 | arraySegments => { 57 | receiverInstance.Received(arraySegments); 58 | }, 59 | () => { 60 | receiverInstance.Pinged(); 61 | }, 62 | closedReason => { 63 | receiverInstance.Closed(closedReason.ToString()); 64 | }, 65 | (errorReason, ex) => { 66 | receiverInstance.Error(errorReason.ToString(), ex); 67 | }, 68 | new Dictionary() 69 | ); 70 | WaitUntil(() => connected, 3); 71 | Assert(webuSocket.IsConnected(), "not connected."); 72 | } 73 | 74 | 75 | [MTeardown] public void Teardown () { 76 | webuSocket.Disconnect(true); 77 | } 78 | 79 | [MTest] public void SendBytesAndReceive () { 80 | webuSocket.Send(new byte[]{1,2,3,4}); 81 | 82 | WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 83 | 84 | Assert(receiverInstance.byteDatas[0].Length == 4, "not match."); 85 | 86 | Assert(receiverInstance.byteDatas[0][0] == 1, "not match."); 87 | Assert(receiverInstance.byteDatas[0][1] == 2, "not match."); 88 | Assert(receiverInstance.byteDatas[0][2] == 3, "not match."); 89 | Assert(receiverInstance.byteDatas[0][3] == 4, "not match."); 90 | } 91 | 92 | [MTest] public void SizeMatch126 () { 93 | var data = new byte[126]; 94 | for (var i = 0; i < data.Length; i++) data[i] = 1; 95 | webuSocket.Send(data); 96 | 97 | WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 98 | 99 | Assert(receiverInstance.byteDatas[0].Length == 126, "not match."); 100 | for (var i = 0; i < receiverInstance.byteDatas[0].Length; i++) Assert(receiverInstance.byteDatas[0][i] == 1, "not match. actual:" + receiverInstance.byteDatas[0][i] + " at:" + i); 101 | } 102 | 103 | [MTest] public void SizeMatch127 () { 104 | var data = new byte[127]; 105 | for (var i = 0; i < data.Length; i++) data[i] = 1; 106 | webuSocket.Send(data); 107 | 108 | WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 109 | 110 | Assert(receiverInstance.byteDatas[0].Length == 127, "not match."); 111 | for (var i = 0; i < receiverInstance.byteDatas[0].Length; i++) Assert(receiverInstance.byteDatas[0][i] == 1, "not match. actual:" + receiverInstance.byteDatas[0][i] + " at:" + i); 112 | } 113 | 114 | [MTest] public void SizeMatch3000 () { 115 | var data = new byte[3000]; 116 | for (var i = 0; i < data.Length; i++) data[i] = 1; 117 | webuSocket.Send(data); 118 | 119 | WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 120 | 121 | Assert(receiverInstance.byteDatas[0].Length == 3000, "not match."); 122 | for (var i = 0; i < receiverInstance.byteDatas[0].Length; i++) Assert(receiverInstance.byteDatas[0][i] == 1, "not match. actual:" + receiverInstance.byteDatas[0][i] + " at:" + i); 123 | } 124 | 125 | [MTest] public void SizeMatch65534 () { 126 | var data = new byte[65534]; 127 | for (var i = 0; i < data.Length; i++) data[i] = 1; 128 | webuSocket.Send(data); 129 | 130 | WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 131 | 132 | Assert(receiverInstance.byteDatas[0].Length == 65534, "not match."); 133 | for (var i = 0; i < receiverInstance.byteDatas[0].Length; i++) Assert(receiverInstance.byteDatas[0][i] == 1, "not match. actual:" + receiverInstance.byteDatas[0][i] + " at:" + i); 134 | } 135 | 136 | // [MTest] public void SizeMatch65535 () { 137 | // var data = new byte[65535]; 138 | // for (var i = 0; i < data.Length; i++) data[i] = 1; 139 | // webuSocket.Send(data); 140 | 141 | // WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 142 | 143 | // Assert(receiverInstance.byteDatas[0].Length == 65535, "not match."); 144 | // for (var i = 0; i < receiverInstance.byteDatas[0].Length; i++) Assert(receiverInstance.byteDatas[0][i] == 1, "not match. actual:" + receiverInstance.byteDatas[0][i] + " at:" + i); 145 | // } 146 | 147 | // [MTest] public void SizeMatch65536 () { 148 | // var data = new byte[65536]; 149 | // for (var i = 0; i < data.Length; i++) data[i] = 1; 150 | // webuSocket.Send(data); 151 | 152 | // WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 153 | 154 | // Assert(receiverInstance.byteDatas[0].Length == 65536, "not match."); 155 | // } 156 | 157 | // [MTest] public void SizeMatch141400 () { 158 | // var data = new byte[141400]; 159 | // for (var i = 0; i < data.Length; i++) data[i] = 1; 160 | // webuSocket.Send(data); 161 | 162 | // WaitUntil(() => 0 < receiverInstance.byteDatas.Count, 3); 163 | 164 | // Assert(receiverInstance.byteDatas[0].Length == 141400, "not match."); 165 | // } 166 | 167 | } 168 | 169 | 170 | 171 | public interface ITestCase { 172 | OptionalSettings OnOptionalSettings (); 173 | void OnConnect (WebuSocket webuSocket); 174 | void OnReceived (WebuSocket webuSocket, Queue datas); 175 | } 176 | 177 | public struct OptionalSettings { 178 | public int throttle; 179 | public Dictionary headerValues; 180 | public int timeout; 181 | 182 | public OptionalSettings (int throttle=0, Dictionary headerValues=null, int timeout=60 * 5) { 183 | this.throttle = throttle; 184 | this.headerValues = headerValues; 185 | this.timeout = timeout; 186 | } 187 | } 188 | 189 | public static class DefaultSetting { 190 | public static OptionalSettings Default () { 191 | return new OptionalSettings(0, null, 60*5); 192 | } 193 | } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test0.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a14eccb65c734d9d90bbe0fc7b285e8 3 | timeCreated: 1457488580 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using WebuSocketCore; 5 | 6 | /* 7 | tests for 1 frame per sec. 8 | */ 9 | 10 | public class TestFrame { 11 | public const int throttleFrame = 1; 12 | } 13 | 14 | public class Test_1_0_OrderAndDataShouldMatchWithThrottle : ITestCase { 15 | public OptionalSettings OnOptionalSettings () { 16 | return new OptionalSettings(TestFrame.throttleFrame); 17 | } 18 | 19 | public void OnConnect(WebuSocket webuSocket) { 20 | webuSocket.Send(new byte[]{1,2,3}); 21 | } 22 | 23 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 24 | if (datas.Count == 1) { 25 | var data = datas.Dequeue(); 26 | if (data[0] == 1 && data[1] == 2 && data[2] == 3) { 27 | webuSocket.Disconnect(); 28 | } else { 29 | Debug.LogError("not match."); 30 | } 31 | } 32 | } 33 | } 34 | 35 | public class Test_1_1_SizeMatching_126WithThrottle : ITestCase { 36 | public OptionalSettings OnOptionalSettings () { 37 | return new OptionalSettings(TestFrame.throttleFrame); 38 | } 39 | 40 | public void OnConnect(WebuSocket webuSocket) { 41 | var data126 = new byte[126]; 42 | for (var i = 0; i < data126.Length; i++) data126[i] = 1; 43 | webuSocket.Send(data126); 44 | } 45 | 46 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 47 | if (datas.Count == 1) { 48 | var data = datas.Dequeue(); 49 | if (data.Length != 126) Debug.LogError("not match."); 50 | webuSocket.Disconnect(); 51 | return; 52 | } 53 | Debug.LogError("not match 2."); 54 | } 55 | } 56 | 57 | public class Test_1_2_SizeMatching_127WithThrottle : ITestCase { 58 | public OptionalSettings OnOptionalSettings () { 59 | return new OptionalSettings(TestFrame.throttleFrame); 60 | } 61 | 62 | public void OnConnect(WebuSocket webuSocket) { 63 | var data = new byte[127]; 64 | for (var i = 0; i < data.Length; i++) data[i] = 1; 65 | webuSocket.Send(data); 66 | } 67 | 68 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 69 | if (datas.Count == 1) { 70 | var data = datas.Dequeue(); 71 | if (data.Length != 127) Debug.LogError("not match."); 72 | webuSocket.Disconnect(); 73 | return; 74 | } 75 | Debug.LogError("not match 2."); 76 | } 77 | } 78 | 79 | 80 | public class Test_1_3_SizeMatching_65534WithThrottle : ITestCase { 81 | public OptionalSettings OnOptionalSettings () { 82 | return new OptionalSettings(TestFrame.throttleFrame); 83 | } 84 | 85 | public void OnConnect(WebuSocket webuSocket) { 86 | var data = new byte[65534]; 87 | for (var i = 0; i < data.Length; i++) data[i] = 1; 88 | webuSocket.Send(data); 89 | } 90 | 91 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 92 | if (datas.Count == 1) { 93 | var data = datas.Dequeue(); 94 | if (data.Length != 65534) Debug.LogError("not match."); 95 | webuSocket.Disconnect(); 96 | return; 97 | } 98 | Debug.LogError("not match 2."); 99 | } 100 | } 101 | 102 | 103 | public class Test_1_4_SizeMatching_65535WithThrottle : ITestCase { 104 | public OptionalSettings OnOptionalSettings () { 105 | return new OptionalSettings(TestFrame.throttleFrame); 106 | } 107 | 108 | public void OnConnect(WebuSocket webuSocket) { 109 | var data = new byte[65535]; 110 | for (var i = 0; i < data.Length; i++) data[i] = 1; 111 | webuSocket.Send(data); 112 | } 113 | 114 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 115 | if (datas.Count == 1) { 116 | var data = datas.Dequeue(); 117 | if (data.Length != 65535) Debug.LogError("not match."); 118 | webuSocket.Disconnect(); 119 | return; 120 | } 121 | Debug.LogError("not match 2."); 122 | } 123 | } 124 | 125 | 126 | public class Test_1_5_SizeMatching_14140WithThrottle : ITestCase { 127 | public OptionalSettings OnOptionalSettings () { 128 | return new OptionalSettings(TestFrame.throttleFrame); 129 | } 130 | 131 | public void OnConnect(WebuSocket webuSocket) { 132 | var data = new byte[14140]; 133 | for (var i = 0; i < data.Length; i++) data[i] = (byte)i; 134 | webuSocket.Send(data); 135 | } 136 | 137 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 138 | if (datas.Count == 1) { 139 | var data = datas.Dequeue(); 140 | if (data.Length != 14140) Debug.LogError("not match."); 141 | webuSocket.Disconnect(); 142 | return; 143 | } 144 | Debug.LogError("not match 2."); 145 | } 146 | } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test1.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fd274c255e8d54f4087a51f77eaae326 3 | timeCreated: 1457488580 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using WebuSocketCore; 5 | 6 | /* 7 | tests for 1 frame per sec. 8 | */ 9 | 10 | public class TestFrame2 { 11 | public const int throttleFrame = 1; 12 | } 13 | 14 | public class Test_2_0_TwoPingReceivedOnSameFrame : ITestCase { 15 | public OptionalSettings OnOptionalSettings () { 16 | return new OptionalSettings(TestFrame2.throttleFrame); 17 | } 18 | 19 | private int count = 0; 20 | 21 | public void OnConnect(WebuSocket webuSocket) { 22 | Action act = () => { 23 | count++; 24 | if (count == 2) { 25 | webuSocket.Disconnect(); 26 | } 27 | }; 28 | 29 | webuSocket.Ping(act); 30 | webuSocket.Ping(act); 31 | } 32 | 33 | public void OnReceived(WebuSocket webuSocket, Queue datas) {} 34 | } 35 | 36 | 37 | public class Test_2_1_TwoSmall125MessageReceivedOnSameFrame : ITestCase { 38 | public OptionalSettings OnOptionalSettings () { 39 | return new OptionalSettings(TestFrame2.throttleFrame); 40 | } 41 | 42 | public void OnConnect(WebuSocket webuSocket) { 43 | var message = new byte[125]; 44 | for (var i = 0; i < message.Length; i++) message[i] = (byte)i; 45 | webuSocket.Send(message); 46 | webuSocket.Send(message); 47 | } 48 | 49 | private int count; 50 | 51 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 52 | while (0 < datas.Count) { 53 | var data0 = datas.Dequeue(); 54 | 55 | if (data0.Length != 125) Debug.LogError("faild to match Test_2_1_TwoSmall125MessageReceivedOnSameFrame. data0.Length:" + data0.Length); 56 | 57 | count++; 58 | if (count == 2) webuSocket.Disconnect(); 59 | } 60 | } 61 | } 62 | 63 | public class Test_2_2_Two125_126MessageReceivedOnSameFrame : ITestCase { 64 | public OptionalSettings OnOptionalSettings () { 65 | return new OptionalSettings(TestFrame2.throttleFrame); 66 | } 67 | 68 | public void OnConnect(WebuSocket webuSocket) { 69 | var message0 = new byte[126]; 70 | for (var i = 0; i < message0.Length; i++) message0[i] = (byte)i; 71 | 72 | webuSocket.Send(message0); 73 | webuSocket.Send(message0); 74 | } 75 | 76 | private int count; 77 | 78 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 79 | while (0 < datas.Count) { 80 | var data0 = datas.Dequeue(); 81 | 82 | if (data0.Length != 126) Debug.LogError("faild to match Test_2_2_Two125_126MessageReceivedOnSameFrame. data0.Length:" + data0.Length); 83 | 84 | count++; 85 | if (count == 2) webuSocket.Disconnect(); 86 | } 87 | } 88 | } 89 | 90 | public class Test_2_3_Two127_127MessageReceivedOnSameFrame : ITestCase { 91 | public OptionalSettings OnOptionalSettings () { 92 | return new OptionalSettings(TestFrame2.throttleFrame); 93 | } 94 | 95 | public void OnConnect(WebuSocket webuSocket) { 96 | var message0 = new byte[127]; 97 | for (var i = 0; i < message0.Length; i++) message0[i] = (byte)i; 98 | 99 | webuSocket.Send(message0); 100 | 101 | var message1 = new byte[127]; 102 | for (var i = 0; i < message1.Length; i++) message1[i] = (byte)i; 103 | webuSocket.Send(message1); 104 | } 105 | 106 | private int count; 107 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 108 | while (0 < datas.Count) { 109 | var data0 = datas.Dequeue(); 110 | 111 | if (data0.Length != 127) Debug.LogError("faild to match Test_2_3_Two127_127MessageReceivedOnSameFrame. data0.Length:" + data0.Length); 112 | 113 | count++; 114 | if (count == 2) webuSocket.Disconnect(); 115 | } 116 | } 117 | } 118 | 119 | public class Test_2_4_Two128_128MessageReceivedOnSameFrame : ITestCase { 120 | public OptionalSettings OnOptionalSettings () { 121 | return new OptionalSettings(TestFrame2.throttleFrame); 122 | } 123 | 124 | public void OnConnect(WebuSocket webuSocket) { 125 | var message0 = new byte[128]; 126 | for (var i = 0; i < message0.Length; i++) message0[i] = (byte)i; 127 | 128 | webuSocket.Send(message0); 129 | 130 | var message1 = new byte[128]; 131 | for (var i = 0; i < message1.Length; i++) message1[i] = (byte)i; 132 | webuSocket.Send(message1); 133 | } 134 | 135 | private int count; 136 | 137 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 138 | while (0 < datas.Count) { 139 | var data0 = datas.Dequeue(); 140 | 141 | if (data0.Length != 128) Debug.LogError("faild to match Test_2_4_Two128_128MessageReceivedOnSameFrame. data0.Length:" + data0.Length); 142 | 143 | count++; 144 | if (count == 2) webuSocket.Disconnect(); 145 | } 146 | } 147 | } 148 | 149 | public class Test_2_5_LargeSizeMessageReceived : ITestCase { 150 | public OptionalSettings OnOptionalSettings () { 151 | return DefaultSetting.Default(); 152 | } 153 | 154 | /* 155 | このパラメータの組み合わせだと、3回に一回くらい再現できる。 156 | throttle unlimited + 43000byte x 32 message at once -> refrelction server refrects -> fragmentation appears. 157 | */ 158 | 159 | private int size = 43000;// 単純にluajitの耐えられる一回の実行の重さの限界がありそう。このあたりが限界っぽい。10k x 100 / frame 160 | private int amount = 32; 161 | 162 | public void OnConnect(WebuSocket webuSocket) { 163 | for (var i = 0; i < amount; i++) { 164 | var message0 = new byte[size]; 165 | for (var j = 0; j < message0.Length; j++) message0[j] = (byte)j; 166 | webuSocket.Send(message0); 167 | } 168 | } 169 | private int count = 0; 170 | private int total = 0; 171 | 172 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 173 | while (0 < datas.Count) { 174 | var data0 = datas.Dequeue(); 175 | 176 | if (data0.Length != size) Debug.LogError("faild to match Test_2_5_FiveMiddleSizeMessageReceivedOnSameFrame. data0.Length:" + data0.Length); 177 | 178 | total += data0.Length; 179 | // Debug.LogError("total:" + total); 180 | count++; 181 | 182 | if (count == amount) { 183 | webuSocket.Disconnect(); 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test2.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a4e13d5bf03c42eeaed3ef79af2061c 3 | timeCreated: 1457498545 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using UnityEngine; 5 | using WebuSocketCore; 6 | 7 | /* 8 | tests for closing. 9 | */ 10 | 11 | public class Test_3_0_DisconnectFromServer : ITestCase { 12 | public OptionalSettings OnOptionalSettings () { 13 | return DefaultSetting.Default(); 14 | } 15 | public void OnConnect(WebuSocket webuSocket) { 16 | var closeRequest = "closeRequest"; 17 | webuSocket.Send(Encoding.UTF8.GetBytes(closeRequest.ToCharArray())); 18 | } 19 | 20 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 21 | webuSocket.Disconnect();// サーバから切断するような仕掛けは存在しない、、 22 | } 23 | } 24 | 25 | 26 | public class Test_3_1_DisconnectFromClient : ITestCase { 27 | public OptionalSettings OnOptionalSettings () { 28 | return DefaultSetting.Default(); 29 | } 30 | 31 | public void OnConnect(WebuSocket webuSocket) { 32 | webuSocket.Disconnect(); 33 | } 34 | 35 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 36 | } 37 | } 38 | 39 | public class Test_3_2_DisconnectWithClose : ITestCase { 40 | public OptionalSettings OnOptionalSettings () { 41 | return DefaultSetting.Default(); 42 | } 43 | 44 | public void OnConnect(WebuSocket webuSocket) { 45 | webuSocket.Disconnect(); 46 | } 47 | 48 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 49 | 50 | } 51 | } 52 | 53 | public class Test_3_3_DisconnectWithCloseSync : ITestCase { 54 | public OptionalSettings OnOptionalSettings () { 55 | return DefaultSetting.Default(); 56 | } 57 | 58 | public void OnConnect(WebuSocket webuSocket) { 59 | webuSocket.Disconnect(true); 60 | } 61 | 62 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 63 | 64 | } 65 | } 66 | 67 | public class Test_3_4_DisconnectWithoutClose : ITestCase { 68 | public OptionalSettings OnOptionalSettings () { 69 | return DefaultSetting.Default(); 70 | } 71 | 72 | public void OnConnect(WebuSocket webuSocket) { 73 | // 突然の死を演じたい、、 74 | } 75 | 76 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 77 | 78 | } 79 | } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test3.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 790a98fabedac458d887c3289c9b332b 3 | timeCreated: 1457706236 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test4.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using UnityEngine; 5 | using WebuSocketCore; 6 | 7 | /* 8 | tests for check correctness of payload. 9 | */ 10 | 11 | 12 | public class Test_4_0_ReceiveManyTimes : ITestCase { 13 | public OptionalSettings OnOptionalSettings () { 14 | return DefaultSetting.Default(); 15 | } 16 | public void OnConnect(WebuSocket webuSocket) { 17 | var manyDataRequest = "10000DataRequest"; 18 | webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 19 | } 20 | 21 | int count; 22 | 23 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 24 | foreach (var data in datas) { 25 | var message = Encoding.UTF8.GetString(data); 26 | if (message == "10000DataRequest") count++; 27 | else Debug.LogError("message:" + message); 28 | } 29 | 30 | if (count == 10000) { 31 | webuSocket.Disconnect(); 32 | } 33 | } 34 | } 35 | 36 | public class Test_4_1_ReceiveManyManyTimes : ITestCase { 37 | public OptionalSettings OnOptionalSettings () { 38 | return new OptionalSettings(0, null, 60 * 100); 39 | } 40 | public void OnConnect(WebuSocket webuSocket) { 41 | var manyDataRequest = "100000DataRequest"; 42 | webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 43 | } 44 | 45 | int count; 46 | 47 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 48 | foreach (var data in datas) { 49 | var message = Encoding.UTF8.GetString(data); 50 | if (message == "100000DataRequest") count++; 51 | else Debug.LogError("message:" + message); 52 | } 53 | 54 | if (count == 100000) { 55 | webuSocket.Disconnect(); 56 | } 57 | } 58 | } 59 | 60 | public class Test_4_2_SendManyTimes : ITestCase { 61 | public OptionalSettings OnOptionalSettings () { 62 | return DefaultSetting.Default(); 63 | } 64 | public void OnConnect(WebuSocket webuSocket) { 65 | var manyDataRequest = "1000DataSendAndReturn"; 66 | for (var i = 0; i < 1000; i++) webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 67 | } 68 | 69 | int count; 70 | 71 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 72 | foreach (var data in datas) { 73 | var message = Encoding.UTF8.GetString(data); 74 | if (message == "1000DataSendAndReturn") count++; 75 | else Debug.LogError("message:" + message); 76 | } 77 | 78 | if (count == 1000) { 79 | webuSocket.Disconnect(); 80 | } 81 | } 82 | } 83 | 84 | public class Test_4_3_SendManyManyTimes : ITestCase { 85 | public OptionalSettings OnOptionalSettings () { 86 | return new OptionalSettings(0, null, 60 * 180); 87 | } 88 | public void OnConnect(WebuSocket webuSocket) { 89 | var manyDataRequest = "10000DataSendAndReturn"; 90 | for (var i = 0; i < 10000; i++) webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 91 | } 92 | 93 | int count; 94 | 95 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 96 | foreach (var data in datas) { 97 | var message = Encoding.UTF8.GetString(data); 98 | if (message == "10000DataSendAndReturn") count++; 99 | else Debug.LogError("message:" + message); 100 | } 101 | 102 | if (count == 10000) { 103 | webuSocket.Disconnect(); 104 | } 105 | } 106 | } 107 | 108 | 109 | 110 | public class Test_4_4_SendAndReceiveManyTimes : ITestCase { 111 | public OptionalSettings OnOptionalSettings () { 112 | return new OptionalSettings(0, null, 60 * 180); 113 | } 114 | public void OnConnect(WebuSocket webuSocket) { 115 | var manyDataRequest = "10000DataRequest"; 116 | webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 117 | 118 | var manyDataRequest2 = "1000DataSend"; 119 | for (var i = 0; i < 1000; i++) webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest2.ToCharArray())); 120 | } 121 | 122 | int count; 123 | 124 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 125 | foreach (var data in datas) { 126 | var message = Encoding.UTF8.GetString(data); 127 | if (message == "10000DataRequest") count++; 128 | else Debug.LogError("message:" + message); 129 | } 130 | 131 | if (count == 10000) { 132 | webuSocket.Disconnect(); 133 | } 134 | } 135 | } 136 | 137 | public class Test_4_5_SendAndReceiveManyManyTimes : ITestCase { 138 | public OptionalSettings OnOptionalSettings () { 139 | return new OptionalSettings(0, null, 60 * 180); 140 | } 141 | public void OnConnect(WebuSocket webuSocket) { 142 | var manyDataRequest = "100000DataRequest"; 143 | webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 144 | 145 | var manyDataRequest2 = "10000DataSend"; 146 | for (var i = 0; i < 10000; i++) webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest2.ToCharArray())); 147 | } 148 | 149 | int count; 150 | 151 | public void OnReceived(WebuSocket webuSocket, Queue datas) { 152 | foreach (var data in datas) { 153 | var message = Encoding.UTF8.GetString(data); 154 | if (message == "100000DataRequest") count++; 155 | else Debug.LogError("message:" + message); 156 | } 157 | 158 | if (count == 100000) { 159 | webuSocket.Disconnect(); 160 | } 161 | } 162 | } 163 | 164 | // public class Test_4_6_SendAndReceiveAsyncManyTimes : ITestCase { 165 | // public OptionalSettings OnOptionalSettings () { 166 | // return new OptionalSettings(0, null, 60 * 180); 167 | // } 168 | 169 | // public void OnConnect(WebuSocket webuSocket) { 170 | // var manyDataRequest = "10000DataRequestAsync"; 171 | // webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest.ToCharArray())); 172 | 173 | // var i = 0; 174 | 175 | // var manyDataRequest2 = "10000DataSend"; 176 | // ServerInitializer.SetupUpdaterThread( 177 | // "10000DataSendThread", 178 | // () => { 179 | // webuSocket.Send(Encoding.UTF8.GetBytes(manyDataRequest2.ToCharArray())); 180 | // i++; 181 | // if (i == 10000) return false; 182 | // return true; 183 | // } 184 | // ); 185 | // } 186 | 187 | // int count; 188 | 189 | // public void OnReceived(WebuSocket webuSocket, Queue datas) { 190 | // foreach (var data in datas) { 191 | // var message = Encoding.UTF8.GetString(data); 192 | // if (message == "10000DataRequestAsync") count++; 193 | // else Debug.LogError("message:" + message); 194 | // } 195 | 196 | // if (count == 10000) { 197 | // webuSocket.Disconnect(); 198 | // } 199 | // } 200 | // } -------------------------------------------------------------------------------- /WebuSocketTests/Editor/Tests/Test4.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 95053991ef33740ea886c602a4ccd932 3 | timeCreated: 1457707000 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /WebuSocketTests/MiyamasuTestRunner/Editor/MiyamasuTestIgniter.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace Miyamasu { 5 | /* 6 | Run tests when Initialize on load. 7 | this thread is Unity Editor's MainThread, is ≒ Unity Player's MainThread. 8 | */ 9 | [InitializeOnLoad] public class MiyamasuTestIgniter { 10 | static MiyamasuTestIgniter () { 11 | 12 | #if CLOUDBUILD 13 | { 14 | // do nothing. 15 | /* 16 | そのうちUnityTestからMiyamasu起動したい。起動さえできれば勝てる気がする。 17 | */ 18 | } 19 | #else 20 | { 21 | Debug.LogWarning("miyamasu start running."); 22 | var testRunner = new MiyamasuTestRunner(); 23 | testRunner.RunTestsOnEditorMainThread(); 24 | } 25 | #endif 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /WebuSocketTests/MiyamasuTestRunner/Editor/MiyamasuTestRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading; 7 | using Diag = System.Diagnostics; 8 | using UnityEditor; 9 | 10 | /** 11 | MiyamasuTestRunner 12 | this testRunner is only test runner which contains "WaitUntil" method. 13 | "WaitUntil" method can wait Async code execution until specified sec passed. 14 | 15 | This can be replacable when Unity adopts C# 4.0 by using "async" and "await" keyword. 16 | */ 17 | namespace Miyamasu { 18 | public class MiyamasuTestRunner { 19 | public MiyamasuTestRunner () {} 20 | 21 | private class TypeAndMedhods { 22 | public Type type; 23 | 24 | public bool hasTests = false; 25 | 26 | public MethodInfo[] asyncMethodInfos; 27 | public MethodInfo setupMethodInfo = null; 28 | public MethodInfo teardownMethodInfo = null; 29 | 30 | public TypeAndMedhods (Type t) { 31 | var testMethods = t.GetMethods() 32 | .Where(methods => 0 < methods.GetCustomAttributes(typeof(MTestAttribute), false).Length) 33 | .ToArray(); 34 | 35 | if (!testMethods.Any()) return; 36 | this.hasTests = true; 37 | 38 | /* 39 | hold type. 40 | */ 41 | this.type = t; 42 | 43 | /* 44 | collect tests. 45 | */ 46 | this.asyncMethodInfos = testMethods; 47 | 48 | /* 49 | collect setup and teardown. 50 | */ 51 | this.setupMethodInfo = t.GetMethods().Where(methods => 0 < methods.GetCustomAttributes(typeof(MSetupAttribute), false).Length).FirstOrDefault(); 52 | this.teardownMethodInfo = t.GetMethods().Where(methods => 0 < methods.GetCustomAttributes(typeof(MTeardownAttribute), false).Length).FirstOrDefault(); 53 | } 54 | } 55 | 56 | public void RunTestsOnEditorMainThread () { 57 | var typeAndMethodInfos = Assembly.GetExecutingAssembly().GetTypes() 58 | .Select(t => new TypeAndMedhods(t)) 59 | .Where(tAndMInfo => tAndMInfo.hasTests) 60 | .ToArray(); 61 | 62 | 63 | if (!typeAndMethodInfos.Any()) { 64 | TestLogger.Log("no tests found. please set \"[MTest]\" attribute to method.", true); 65 | return; 66 | } 67 | 68 | var passed = 0; 69 | var failed = 0; 70 | 71 | TestLogger.Log("tests started.", true); 72 | 73 | // generate waiting thread for waiting asynchronous(=running on MainThread or other thread) ops on Not-MainThread. 74 | Thread thread = null; 75 | thread = new Thread( 76 | () => { 77 | foreach (var typeAndMethodInfo in typeAndMethodInfos) { 78 | 79 | var instance = Activator.CreateInstance(typeAndMethodInfo.type); 80 | 81 | 82 | foreach (var methodInfo in typeAndMethodInfo.asyncMethodInfos) { 83 | if (typeAndMethodInfo.setupMethodInfo != null) typeAndMethodInfo.setupMethodInfo.Invoke(instance, null); 84 | 85 | var methodName = methodInfo.Name; 86 | 87 | try { 88 | methodInfo.Invoke(instance, null); 89 | passed++; 90 | } catch (Exception e) { 91 | failed++; 92 | TestLogger.Log("test:" + methodName + " FAILED by exception:" + e, true); 93 | } 94 | if (typeAndMethodInfo.teardownMethodInfo != null) typeAndMethodInfo.teardownMethodInfo.Invoke(instance, null); 95 | } 96 | } 97 | 98 | TestLogger.Log("tests end. passed:" + passed + " failed:" + failed, true); 99 | thread.Abort(); 100 | } 101 | ); 102 | try { 103 | thread.Start(); 104 | } catch (Exception e) { 105 | TestLogger.Log("Miyamasu TestRunner error:" + e); 106 | } 107 | } 108 | 109 | /** 110 | can wait Async code execution until specified sec passed. 111 | */ 112 | public void WaitUntil (Func isCompleted, int timeoutSec=1) { 113 | var methodName = new Diag.StackFrame(1).GetMethod().Name; 114 | var timeout = false; 115 | 116 | var resetEvent = new ManualResetEvent(false); 117 | var waitingThread = new Thread( 118 | () => { 119 | resetEvent.Reset(); 120 | var startTime = DateTime.Now; 121 | 122 | while (!isCompleted()) { 123 | var current = DateTime.Now; 124 | var distanceSeconds = (current - startTime).Seconds; 125 | 126 | if (0 < timeoutSec && timeoutSec < distanceSeconds) { 127 | timeout = true; 128 | break; 129 | } 130 | 131 | System.Threading.Thread.Sleep(10); 132 | } 133 | 134 | resetEvent.Set(); 135 | } 136 | ); 137 | 138 | waitingThread.Start(); 139 | 140 | resetEvent.WaitOne(); 141 | 142 | if (timeout) { 143 | TestLogger.Log("timeout:" + methodName, true); 144 | throw new Exception("timeout:" + methodName); 145 | } 146 | } 147 | 148 | public void RunOnMainThread (Action invokee) { 149 | UnityEditor.EditorApplication.CallbackFunction runner = null; 150 | runner = () => { 151 | // run only once. 152 | EditorApplication.update -= runner; 153 | if (invokee != null) invokee(); 154 | }; 155 | 156 | EditorApplication.update += runner; 157 | } 158 | 159 | public void Assert (bool condition, string message) { 160 | var methodName = new Diag.StackFrame(1).GetMethod().Name; 161 | if (!condition) { 162 | var situation = "test:" + methodName + " ASSERT FAILED:" + message; 163 | TestLogger.Log(situation); 164 | throw new Exception(situation); 165 | } 166 | } 167 | 168 | public void Assert (object expected, object actual, string message) { 169 | var methodName = new Diag.StackFrame(1).GetMethod().Name; 170 | if (expected.ToString() != actual.ToString()) { 171 | var situation = "test:" + methodName + " ASSERT FAILED:" + message + " expected:" + expected + " actual:" + actual; 172 | TestLogger.Log(situation); 173 | throw new Exception(situation); 174 | } 175 | } 176 | 177 | public const string MIYAMASU_TESTLOG_FILE_NAME = "miyamasu_test.log"; 178 | 179 | public static class TestLogger { 180 | private static object lockObject = new object(); 181 | 182 | private static string pathOfLogFile; 183 | private static StringBuilder _logs = new StringBuilder(); 184 | 185 | public static void Log (string message, bool export=false) { 186 | lock (lockObject) { 187 | if (!export) { 188 | _logs.AppendLine(message); 189 | return; 190 | } 191 | 192 | pathOfLogFile = MIYAMASU_TESTLOG_FILE_NAME; 193 | 194 | // file write 195 | using (var fs = new FileStream( 196 | pathOfLogFile, 197 | FileMode.Append, 198 | FileAccess.Write, 199 | FileShare.ReadWrite) 200 | ) { 201 | using (var sr = new StreamWriter(fs)) { 202 | if (0 < _logs.Length) { 203 | sr.WriteLine(_logs.ToString()); 204 | _logs = new StringBuilder(); 205 | } 206 | sr.WriteLine("log:" + message); 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | /** 215 | attributes for TestRunner. 216 | */ 217 | [AttributeUsage(AttributeTargets.Method)] public class MSetupAttribute : Attribute { 218 | public MSetupAttribute() {} 219 | } 220 | 221 | [AttributeUsage(AttributeTargets.Method)] public class MTestAttribute : Attribute { 222 | public MTestAttribute() {} 223 | } 224 | 225 | [AttributeUsage(AttributeTargets.Method)] public class MTeardownAttribute : Attribute { 226 | public MTeardownAttribute() {} 227 | } 228 | } --------------------------------------------------------------------------------