:Class TCP ⍝ Class for Asynchronous TCP/IP Communication ⍝ List of methods: ⍝ Connect = Connect to a remote host ⍝ SendReceive = Send the bytes and return the bytes received ⍝ Close = Close the connection ⍝ ConnectSendReceiveClose = Connect, Send, Receive and Close as one operation. ⍝ Version 1.0 January 2016 ⍝ .Net assemblies: :Using :Using System,mscorlib.dll :Using System.IO,mscorlib.dll :Using System.Net,System.dll :Using System.Net.Sockets,System.dll :Using System.Threading,mscorlib.dll (⎕IO ⎕ML ⎕WX)←1 3 3 ⍝ System variables ⍝ YOU CAN CHANGE THE VALUE OF THE FOLLOWING 3 FIELDS AFTER ⍝ INTANTIATING THE CLASS BUT BEFORE CALLING THE 'Connect' METHOD. ⍝ Size of the Input and Output Buffers in Bytes. Should be ⍝ as Big as the Largest Number of Bytes to Send or Receive. :Field Public _BufferSize←1000 ⍝ Time in milliseconds to Wait for a TcpClient that is not Active (Receiving Nothing). ⍝ If exceeded Windows Stop Waiting and returns the Control to the Calling Application. :Field Public _PortTimeOut←100 ⍝ Number of retries if no answer from remote host or errors. Used only by 'SendRceive'. :Field Public _Retries←3 ⍝ Time in milliseconds to wait after writing the bytes and before reading the bytes received. ⍝ When used for serial half duplex communication over IP you will need a small delay ⍝ between sending and receiving to let the serial port switch from receiving to sending mode. ⍝ Rule of thumb for the delay in millisec: 1E6÷(Baud Rate of serial device) ⍝ Otherwise leave this delay to zero if not used for serial communication. :Field Public _SwitchingDelay←0 ⍝ Used internally :Field Public tcpObj ⍝ TcpClient object :Field Public _LocalEndPoint ⍝ address and port on local machine :Field Public _RemoteEndPoint ⍝ address and port on remote machine :Field Public _IP ⍝ IP Address :Field Public _Port ⍝ Port Number :Field Public Type←'TCP' ⍝ Identifier :Field _buffer ⍝ Empty Byte[] array same size as _BufferSize :Field _stream ⍝ NetworkStream used to send and receive data ∇ Init0 :Access Public :Implements Constructor tcpObj←⎕NULL ∇ ∇ r←Connect(ip port);lingerOption;task :Access Public ⍝ Connect Asynchronously to a remote host ⍝ ip = IP Address where to send the bytes in characters ('192.168.1.1') or numbers (192 168 1 1) ⍝ port = Port Number to send the bytes (0 - 65535). ⍝ ⍝ r[1] = 1 for Success, 0 for Failure ⍝ r[2] = Literal Error if Failure ⍝ Check if port and ip already connected :If ⎕NULL≢tcpObj :AndIf tcpObj.Client.Connected r←0 'TCP Error Connecting: Already connected' ⋄ →0 :EndIf :Trap 0 ⍝ Prepare the IP adress: :If 0=↑1↑0⍴ip ⍝ Numbers ? ip←⎕NEW IPAddress(⊂,ip) _IP←ip.ToString ⍝ Saved as a field for reuse. :Else _IP←ip ⍝ Saved as a field for reuse. :End ⍝ Prepare the TcpClient; tcpObj←⎕NEW TcpClient ⍝ Initializes a new instance of the TcpClient class. tcpObj.ReceiveTimeout←_PortTimeOut ⍝ Amount of time a TcpClient will wait to receive data (msec) tcpObj.SendTimeout←_PortTimeOut ⍝ Amount of time a TcpClient will wait for a send operation to complete successfully (msec) tcpObj.ReceiveBufferSize←_BufferSize ⍝ Size of the receive buffer. tcpObj.SendBufferSize←_BufferSize ⍝ Size of the send buffer. tcpObj.NoDelay←1 ⍝ Sends data immediately when calling Write lingerOption←⎕NEW LingerOption(1 0) ⍝ When Closing: Discards any pending data and Winsock resets the connection. tcpObj.LingerState←lingerOption ⍝ Connect; task←tcpObj.ConnectAsync(ip port) ⍝ Connect to a remote host. :If 0=⎕TSYNC{task.Wait ⍵}&(_PortTimeOut) ⍝ Wait no longer than _PortTimeOut to make the connection. ⍝ Failure to Connect. ⍝ Could be here if the ethernet cable is disconnected from Remote Host. ⍝ The time-out on Connect is about 20 seconds and cannot be changed. ⍝ If _PortTimeOut is smaller than 20 seconds it is not possible to get the EXCEPTION. {}Close r←0 'TCP Error Connecting: Remote host did not answer.' :Else ⍝ Success _buffer←Array.CreateInstance(Byte _BufferSize) ⍝ empty Byte[] array same size as _BufferSize _stream←tcpObj.GetStream ⍝ Returns the NetworkStream used to send and receive data _LocalEndPoint←tcpObj.Client.LocalEndPoint.ToString ⍝ For information only. Not used by the class. _RemoteEndPoint←tcpObj.Client.RemoteEndPoint.ToString ⍝ For information only. Not used by the class. _Port←port ⍝ Saved as a field for reuse. r←1 :EndIf :Else ⍝ Close the socket in case of error. {}Close ⍝ Show the error. :If 90=⎕EN ⍝ .Net Error ⍝ Could be here if the ethernet cable is disconnected from the computer. r←0('TCP Error Connecting: ',⎕EXCEPTION.GetBaseException.Message) :Else ⍝ APL Error r←0(1⊃⎕DM),': ',{(' '=1↑⍵)↓((1↓a,0)∨a←' '≠⍵)/⍵}(2⊃⎕DM) :EndIf :EndTrap ∇ ∇ r←{retries}SendReceive bytes;count;data;retries;task :Access Public ⍝ Write Bytes Asynchronously at the Port of an IP address and return the bytes received. ⍝ bytes = Bytes (as Numbers 0 to 255) to be Written to the Communication Port ⍝ retries = Optional number of times to retry the communication if no data is received ⍝ ⍝ r[1] = 1 for Success, 0 for Failure ⍝ r[2] = Response if Success, Literal Error if Failure ⍝ Parse 'retries' :If 0=⎕NC'retries' retries←_Retries :EndIf ⍝ Loop until some data is received ⍝ or the number of retries is exceeded. :While retries>0 ⍝ Check if port and ip are already connected :If ⎕NULL≡tcpObj :OrIf ~tcpObj.Client.Connected :OrIf ~_stream.CanWrite :OrIf ~_stream.CanRead ⍝ Socket is not connected. Reconnect. {}Close :If 1↑r←Connect(_IP _Port) ⍝ Success Reconnecting. Do nothing. :Else r←0 'TCP Error Sending: Not Connected and Failed to Reconnect (',(2⊃r),')' ⋄ →RETRIES :End :EndIf :Trap 0 ⍝ Empty the receiving buffer buffer before sending if not empty. ⍝ There is no method to empty the buffer other than Read the data and discard it. :If _stream.DataAvailable task←_stream.ReadAsync(_buffer 0 _BufferSize) :If ⎕TSYNC{task.Wait ⍵}&(_PortTimeOut) ⍝ If there is bytes here it is probably because the receiving end is ⍝ behaving badly and sending data when not supposed. Uncomment the ⍝ next line to debug this bad behavior. ⍝ ⎕←task.Result ⍝ =number of bytes erased task.Dispose :EndIf :End :EndTrap :Trap 0 ⍝ Send the data: ⍝ According to the litterature, sending the data will detect if the connection is lost. ⍝ Surprinsingly when sending the data with the ethernet cable unplugged it will not make an error. ⍝ It is normal when receiving the data that there is no detection if the connection is lost. :If _stream.CanWrite :AndIf tcpObj.Client.Connected ⍝ Write the bytes to the port task←_stream.WriteAsync(bytes 0(⍬⍴⍴,bytes)) ⍝ Wait asynchronously for the task to complete :If 0=⎕TSYNC{task.Wait ⍵}&(_PortTimeOut) ⍝ Failure to send all the data. r←0 'TCP Error Sending: Task did not complete sending the bytes' ⋄ →RETRIES :Else ⍝ Success task.Dispose :EndIf :Else r←0 'TCP Error Sending: Socket no longer available to send data.' ⋄ →RETRIES :EndIf ⍝ Receive the data: :If _stream.CanRead :AndIf tcpObj.Client.Connected ⍝ Delay before starting the ReadAsync operation (used for serial communication over IP) ⎕TSYNC ⎕DL&_SwitchingDelay÷1000 task←_stream.ReadAsync(_buffer 0 _BufferSize) ⍝ _buffer is filled with the bytes received ⍝ Waiting asynchronously for the task to complete :If ⎕TSYNC{task.Wait ⍵}&(_PortTimeOut) ⍝ taskResult=0 when no data is received count←task.Result ⍝ count = number of byte(s) received data←count↑⌷_buffer task.Dispose r←1 data :Else ⍝ When the task does not complete because there is no data received ⍝ the socket will not be able to receive other data for a couple of seconds. ⍝ It is necessary to close the socket and remake the connection. ⍝ If there is no data received it could be that there was no data send from the receiving end ⍝ or there is a 'Half-Open Connections' see http://blog.stephencleary.com/2009/05/detection-of-half-open-dropped.html ⍝ An aggressive way would be to Close and Re-Connect the socket and see if the connection can be made again. ⍝ This would suppose that the receiving end can accept more than one TCP connection on the same port and will ⍝ drop eventually the connection that are not active. {}Close :If 1↑r←Connect(_IP _Port) r←0 'TCP: No Data Received. Socket Was Successfully Reset' ⋄ →RETRIES :Else r←0 'TCP: No Data Received. Socket Fail to Reset (',(2⊃r),')' ⋄ →RETRIES :End :EndIf :Else r←0 'TCP Receiving Error: Socket no longer available to receive data and failed to reset.' ⋄ →RETRIES :EndIf :Else ⍝ Close the socket in case of error. {}Close ⍝ Show the error. :If 90=⎕EN ⍝ .Net Error r←0('TCP Error Sending/Receiving: ',⎕EXCEPTION.GetBaseException.Message) :Else ⍝ APL Error r←0(1⊃⎕DM),': ',{(' '=1↑⍵)↓((1↓a,0)∨a←' '≠⍵)/⍵}(2⊃⎕DM) :EndIf :EndTrap RETRIES:retries-←1 :Until 1=1↑r ∇ ∇ r←Close :Access Public ⍝ Close the Connection ⍝ ⍝ r[1] = 1 for Success, 0 for Failure ⍝ r[2] = Literal Error if Failure :If ⎕NULL≡tcpObj ⋄ r←0 'TCP Error Closing: Already Closed' ⋄ →0 ⋄ :EndIf :Trap 0 (tcpObj.GetStream).Close ⍬ :EndTrap :Trap 0 tcpObj.Close :EndTrap tcpObj←⎕NULL r←1 ∇ ∇ r←{retries}ConnectSendReceiveClose(bytes ip port) :Access Public ⍝ Connect, Send, Receive and Close as one operation. ⍝ ⍝ bytes = numbers from 0 to 255 ⍝ ip = IP Address where to send the bytes in characters ('192.168.1.1') or numbers (192 168 1 1) ⍝ port = Port Number to send the bytes (0 - 65535). ⍝ retries = Optional Number of times to retry the communication if no data is received ⍝ Close the socket if not already closed. :If ⎕NULL≢tcpObj {}Close :End :If 1↑r←Connect(ip port) ⍝ Success connecting. Send the bytes. :If 0≠⎕NC'retries' r←retries SendReceive bytes :Else r←SendReceive bytes :EndIf {}Close :EndIf ∇ :EndClass