异步Socket通信
本文介绍如何使用非阻塞方式的Socket通信,并且创建了一个聊天程序的例子来帮助说明。
介绍
本文介绍如何在多个应用程序之间创建和使用TCP/IP Socket来进行通信。这些应用程序可以运行在同一台机器,也可以在局域网内,甚至也可以是跨越Internet的*。这种方法的好处是不需要你自己来使用线程,而是通过调用Socket的非阻塞模式来实现。在例子中:服务器创建病侦听客户端的连接,一旦有客户连接,服务器就将其加入到一个活动客户的列表中,某个客户端发送的消息也有服务器发送到各个连接的客户端,就好像聊天室中的那样。或许Remoting (远程调用)是做这种工作更好的办法,但是我们这里还是来学习学习如何使用Socket来实现。
*注意:跨越Internet的通讯要求服务器有独立的IP地址并且不在代理或是防火墙之后。
事件时序
服务器必须要先侦听,客户端才能够连接。下面的图例说明了在一个异步Socket会话中的事件时序。
运行示例
实例代码分为两部分:ChatServer 和ChatClient. 我们首先来创建ChatServer ,然后使用下面的Telnet命令来测试它。
telnet {server machine IP address or machine name} 399
telnet 10.328.32.76 399
这时,服务器上应该出现一条消息来表明这个客户连接的地址和端口。在任一个telnet窗口中键入的字符都会回显到所有与服务器连接的telnet的窗口中。试试从多台机器上并发连接服务器。不要使用localhost或者127.0.0.1来作为服务器程序唯一的侦听地址。
然后运行ChatClient实例作相同的试验和多个客户端和多个telnet并存的测试。
为什么要使用.NET的Socket?
.NET在很多地方都用到了sockets,比如:WebServices和Remoting。但是在那些应用中底层的Socket支持已经做好了,不需要直接使用。但是,和其他非.NET系统的Socket打交道或简单通信的场合中Socket的使用还是很有必要的。它可以用来和诸如DOS,Windows和UNIX系统进行通信。底层的Socket应用也可以让你减少了诸如组测,权限,域(domains),用户ID,密码等这些麻烦的安全方面的顾虑。
ChatServer / Listener
服务器侦听端口,当有连接请求时,接受该连接并返回一条欢迎信息。在例子中客户连接被加到一个活动客户列表m_aryClients中去。这个列表会根据客户加入和离开作相应的增删。在某些情况下可能会丢失连接,所以在实际的系统中还应该有轮询侦测客户端是否在线的部分。当服务器端的listener收到客户端发来的信息后,它会把消息广播到所有连接的客户端。
下面讨论两种侦听的方法,一个是用轮询(polling),另外一个在使用事件来侦测连接的请求。
方法1 – 使用轮询的 TcpListener
System.Net.Sockets中的TcpListener 类为我们提供了一个侦听和处理客户连接的简单手段。下面的代码侦听连接,接受连接,并且向客户连接发回一个带有时间戳的欢迎信息。如果有另外一个连接请求到来,原来的连接将会丢失。注意,欢迎信息是采用ASCII编码,而不是UNICODE。
private Socket client = null;
const int nPortListen = 399;
try
{
TcpListener listener = new TcpListener( nPortListen );
Console.WriteLine( "Listening as {0}", listener.LocalEndpoint );
listener.Start();
do
{
byte [] m_byBuff = new byte[127];
if( listener.Pending() )
{
client = listener.AcceptSocket();
// Get current date and time.
DateTime now = DateTime.Now;
string strDateLine = "Welcome " + now.ToString("G") + "\n\r";
// Convert to byte array and send.
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );
client.Send( byteDateLine, byteDateLine.Length, 0 );
}
else
{
Thread.Sleep( 100 );
}
} while( true ); // Don't use this.
}
catch( Exception ex )
{
Console.WriteLine ( ex.Message );
}
方法2 – 使用带事件的Socket
一个更为优雅的方法是创建一个事件来捕捉连接请求。ChatServer实例就采用了这种方法。首先服务器的名字和地址用下面的代码取得。
IPAddress [] aryLocalAddr = null;
string strHostName = "";
try
{
// NOTE: DNS lookups are nice and all but quite time consuming.
strHostName = Dns.GetHostName();
IPHostEntry ipEntry = Dns.GetHostByName( strHostName );
aryLocalAddr = ipEntry.AddressList;
}
catch( Exception ex )
{
Console.WriteLine ("Error trying to get local address {0} ", ex.Message );
}
// Verify we got an IP address. Tell the user if we did
if( aryLocalAddr == null || aryLocalAddr.Length < 1 )
{
Console.WriteLine( "Unable to get local address" );
return;
}
Console.WriteLine( "Listening on : [{0}] {1}", strHostName, aryLocalAddr[0] );
得到地址之后,我们要把listener这个Socket绑定到这个地址。我们这里使用的侦听端口是399。此外,从位于"C:\WinNT\System32\drivers\etc\Services"的服务文件中读取端口号应该是一个很好的练习。下面的代码绑定Listener并且开始侦听。一个事件handler把所有的连接请求都指向了OnConnectRequest。这样程序就可以不需要等待或者轮询来处理客户连接了。
const int nPortListen = 399;
// Create the listener socket in this machines IP address
Socket listener = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
listener.Bind( new IPEndPoint( aryLocalAddr[0], 399 ) );
//listener.Bind( new IPEndPoint( IPAddress.Loopback, 399 ) ); // For use with localhost 127.0.0.1
listener.Listen( 10 );
// Setup a callback to be notified of connection requests
listener.BeginAccept( new AsyncCallback( app.OnConnectRequest ), listener );
当客户连接请求到达时,就会激发下面的处理事件。下面的代码首先创建了client (Socket),然后发回欢迎信息,接着重新建立了接受事件处理(accept event handler)。
Socket client;
public void OnConnectRequest( IAsyncResult ar )
{
Socket listener = (Socket)ar.AsyncState;
client = listener.EndAccept( ar );
Console.WriteLine( "Client {0}, joined", client.RemoteEndPoint );
// Get current date and time.
DateTime now = DateTime.Now;
string strDateLine = "Welcome " + now.ToString("G") + "\n\r";
// Convert to byte array and send.
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );
client.Send( byteDateLine, byteDateLine.Length, 0 );
listener.BeginAccept( new AsyncCallback( OnConnectRequest ), listener );
}
这段代码可以扩展,维护客户Socket的列表,监控数据接收和连接断开。对于连接断开的侦测放在AsyncCallback 事件处理中。ChatClient部分将在下面细述该机制。
ChatClient
ChatClient是一个Windows Form应用程序,用来连接服务器,收发消息。
连接
当点击界面上的连接按钮使执行下面的程序使客户连接到服务器。
private Socket m_sock = null;
private void m_btnConnect_Click(object sender, System.EventArgs e)
{
Cursor cursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
// Close the socket if it is still open
if( m_sock != null && m_sock.Connected )
{
m_sock.Shutdown( SocketShutdown.Both );
System.Threading.Thread.Sleep( 10 );
m_sock.Close();
}
// Create the socket object
m_sock = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
// Define the Server address and port
IPEndPoint epServer = new IPEndPoint( IPAddress.Parse( m_tbServerAddress.Text ), 399 );
// Connect to the server blocking method and setup callback for recieved data
// m_sock.Connect( epServer );
// SetupRecieveCallback( m_sock );
// Connect to server non-Blocking method
m_sock.Blocking = false;
AsyncCallback onconnect = new AsyncCallback( OnConnect );
m_sock.BeginConnect( epServer, onconnect, m_sock );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Server Connect failed!" );
}
Cursor.Current = cursor;
}
如果连接已经存在就销毁它。创建一个Socket和指定的端点相连。 被注释掉部分的代码采用简单的阻塞式连接方法。BeginConnect 则用来做一个非阻塞的连接请求。注意,即使是一个非阻塞的用户连接请求,连接也回被阻塞知道机器名称被解析为IP地址。所以,要尽量使用IP地址而不是机器名来避免这种情况。一旦连接请求处理完毕就会调用下面的方法,它显示连接错误或者在成功连接的情况下建立起接收数据的回调。
public void OnConnect( IAsyncResult ar )
{
// Socket was the passed in object
Socket sock = (Socket)ar.AsyncState;
// Check if we were sucessfull
try
{
// sock.EndConnect( ar );
if( sock.Connected )
SetupRecieveCallback( sock );
else
MessageBox.Show( this, "Unable to connect to remote machine",
"Connect Failed!" );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Unusual error during Connect!" );
}
}
接收数据
为了异步接收数据,有必要建立一个AsyncCallback 来处理被诸如接到数据和连接断开所激发的事件。用下面的方法。
private byte [] m_byBuff = new byte[256]; // Recieved data buffer
public void SetupRecieveCallback( Socket sock )
{
try
{
AsyncCallback recieveData = new AsyncCallback( OnRecievedData );
sock.BeginReceive( m_byBuff, 0, m_byBuff.Length, SocketFlags.None,
recieveData, sock );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Setup Recieve Callback failed!" );
}
}
SetupRecieveCallback 方法启动了BeginReceive ,并利用代理指针把回调指向OnReceveData 方法。同时它也把一个用来接收数据的缓冲传递过去。
public void OnRecievedData( IAsyncResult ar )
{
// Socket was the passed in object
Socket sock = (Socket)ar.AsyncState;
// Check if we got any data
try
{
int nBytesRec = sock.EndReceive( ar );
if( nBytesRec > 0 )
{
// Wrote the data to the List
string sRecieved = Encoding.ASCII.GetString( m_byBuff, 0, nBytesRec );
// WARNING : The following line is NOT thread safe. Invoke is
// m_lbRecievedData.Items.Add( sRecieved );
Invoke( m_AddMessage, new string [] { sRecieved } );
// If the connection is still usable restablish the callback
SetupRecieveCallback( sock );
}
else
{
// If no data was recieved then the connection is probably dead
Console.WriteLine( "Client {0}, disconnected", sock.RemoteEndPoint );
sock.Shutdown( SocketShutdown.Both );
sock.Close();
}
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Unusual error druing Recieve!" );
}
}
当上面的事件被激发时,接收到的数据被默认为是ASCII编码的。新数据也会被激发的事件显示出来。尽管可以调用Add() 在列表中显示新数据,但这并不是一个好主意,因为收到的数据很有可能要被送到其他线程中去处理。注意,需要在接收之后重建接收回调,来确保可以继续接收数据。因为有可能数据很多,超过最初的buffer容量。
创建 AddMessage 委托可以降低Socket线程和用户界面线程的耦合程度,如下所示:
// Declare the delegate prototype to send data back to the form
delegate void AddMessage( string sNewMessage );
namespace ChatClient
{
. . .
public class FormMain : System.Windows.Forms.Form
{
private event AddMessage m_AddMessage;
// Add Message Event handler for Form
. . .
public FormMain()
{
. . .
// Add Message Event handler for Form decoupling from input thread
m_AddMessage = new AddMessage( OnAddMessage );
. . .
}
public void OnAddMessage( string sMessage )
{
// Thread safe operation here
m_lbRecievedData.Items.Add( sMessage );
}
public void OnSomeOtherThread()
{
. . .
string sSomeText = "Bilbo Baggins";
Invoke( m_AddMessage, new string [] { sSomeText } );
}
. . .
}
}
使用UNICODE
当时用比特流来发送接收数据时,数据就需要被适当的编码。C# 采用多字节字符编码,尽管这里使用Encoding.ASCII ,但如果需要也可以使用Encoding.UNICODE
不要相信发出什么就能收到什么
当接收数据事件被激发,接收的数据被放置到接收缓冲中去。在我们的开发中,分组发送往往对应一个分组接收事件。但是在真正的系统中并非如此。数据并不是都是规规矩矩的在报文中,而有可能被拆分到若干个分组中。不要指望总能收到完整的报文,也不要指望建立自己的符号标记报文的开始和结束就万事大吉了。
结论
尽管使用Socket并不难,但是要用的很好还是需要大量的实践练习。当然在合适的场合你也应该试试使用WebServices或Remoting。此外,Wrox出版社的Professional ADO.NET Programming这本书很不错,值得一看。
英文原文
Asynchronous socket communication
Introduction
This article shows how to create both ends of a TCP/IP socket connection between two or more applications. These application may be run, on the same machine, machines connected by a local area network or even machines communicating across the internet*. A key feature of this method is it does not your threads, instead using sockets in a non-blocking mode. In this example a server is created that listens for clients to connect. Once a client connects it is added to a list of active clients. If a client sends in a message it is broadcast to all connected clients in much the same way as a chat group would operate. Remoting would be a better way to do this, but we are here to learn about Sockets.
*Note, to communicate across the internet will require the server to have an IP address on the internet not hidden behind a proxy server as many ISPs do.
Sequence of events
Before a client can connect, the server must be listening. The following diagram shows the sequence of events that make up an asynchronous socket session.
Running the sample
The sample code is divided into two application ChatServer that the clients connect to and ChatClient that connects to the client. Build the ChatServer first and test it using Telnet with the following command.
telnet {server machine IP address or machine name} 399
telnet 10.328.32.76 399
A message should appear on ChatServer indicating the address and the port number the client can connected on. Anything typed into the telnet window should echo back to all telnet windows connected to the server. Try making several concurrent connections from various machines. Do not use localhost or the 127.0.0.1 address as the server application is only listening on the address shown in the server start-up message.
Next run the ChatClient sample any try the same test using multiple instances of ChatClient and Telnet across various machines.
Why use sockets with .NET?
.NET uses sockets in may instances such as WebServices and Remoting, but in these instances the low level socket stuff is done for you and there is no need to use sockets directly. However, when interfacing to other non .NET systems sockets are a necessary and simple communication method. They can be used to communicate with DOS, Windows and UNIX systems. The low level sockets also allows you to avoid having to worry about registration, privileges, domains, user id's, password and other troublesome security issues.
ChatServer/Listener
The server listens for clients to connect when a connection is requested the server will accept the connection and return a welcome message. In the sample the connections are added to an array of active clients m_aryClients. As client connect and disconnect, this list will grow and shrink. It is not always possible to detect the loss of a connection, so in a production system there should be some form of polling to detect if the connection is still alive. When data is received on a listener it is broadcast to all connected clients.
Two methods of listening are discussed below one using polling the other events to detect connection requests.
Method 1 - Using polled TcpListener
Using the TcpListener class from System.Net.Sockets provides a simple method to listen for client connections and process them. The following code listens for a connection, accepts it and sends a welcome message with a time stamp. If another connection is requested the old one is lost. Note, the welcome message is returned in ASCII not UNICODE.
private Socket client = null;
const int nPortListen = 399;
try
{
TcpListener listener = new TcpListener( nPortListen );
Console.WriteLine( "Listening as {0}", listener.LocalEndpoint );
listener.Start();
do
{
byte [] m_byBuff = new byte[127];
if( listener.Pending() )
{
client = listener.AcceptSocket();
// Get current date and time.
DateTime now = DateTime.Now;
string strDateLine = "Welcome " + now.ToString("G") + "\n\r";
// Convert to byte array and send.
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );
client.Send( byteDateLine, byteDateLine.Length, 0 );
}
else
{
Thread.Sleep( 100 );
}
} while( true ); // Don't use this.
}
catch( Exception ex )
{
Console.WriteLine ( ex.Message );
}
Method 2 - Using Socket with event
A more elegant method is to setup an event to catch connection attempts. The ChatServer sample uses this method. Firstly the name and address of the server is identified with the following code.
IPAddress [] aryLocalAddr = null;
string strHostName = "";
try
{
// NOTE: DNS lookups are nice and all but quite time consuming.
strHostName = Dns.GetHostName();
IPHostEntry ipEntry = Dns.GetHostByName( strHostName );
aryLocalAddr = ipEntry.AddressList;
}
catch( Exception ex )
{
Console.WriteLine ("Error trying to get local address {0} ", ex.Message );
}
// Verify we got an IP address. Tell the user if we did
if( aryLocalAddr == null || aryLocalAddr.Length < 1 )
{
Console.WriteLine( "Unable to get local address" );
return;
}
Console.WriteLine( "Listening on : [{0}] {1}", strHostName, aryLocalAddr[0] );
With the address identified we need to bind the listener to this address. Here we are listening on port 399. It is good practice to read the port number from from the Services file located in "C:\WinNT\System32\drivers\etc\Services". The following code binds the listener and begins to listen. An event handler is added pointing all connection requests to OnConnectRequest. The application can now go about its business without having to wait or poll for clients to connect.
const int nPortListen = 399;
// Create the listener socket in this machines IP address
Socket listener = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
listener.Bind( new IPEndPoint( aryLocalAddr[0], 399 ) );
//listener.Bind( new IPEndPoint( IPAddress.Loopback, 399 ) ); // For use with localhost 127.0.0.1
listener.Listen( 10 );
// Setup a callback to be notified of connection requests
listener.BeginAccept( new AsyncCallback( app.OnConnectRequest ), listener );
When a client requests a connection, the connection request event handler is fired as follows. The following code creates a client sends a welcome message and and re-establishes the accept event handler.
Socket client;
public void OnConnectRequest( IAsyncResult ar )
{
Socket listener = (Socket)ar.AsyncState;
client = listener.EndAccept( ar );
Console.WriteLine( "Client {0}, joined", client.RemoteEndPoint );
// Get current date and time.
DateTime now = DateTime.Now;
string strDateLine = "Welcome " + now.ToString("G") + "\n\r";
// Convert to byte array and send.
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );
client.Send( byteDateLine, byteDateLine.Length, 0 );
listener.BeginAccept( new AsyncCallback( OnConnectRequest ), listener );
}
This is expanded on in the sample code to allow the client socket to be kept in a list and monitored for received data and disconnections. Disconnections are detected on the client socket in the AsyncCallback event handler. The ChatClient details this mechanism below.
ChatClient
The ChatClient is a windows form application that connects to the server and displays messages that it receives and allow messages to be sent.
Connecting
The client connects connect to the server when the Connect button is pressed with the following code;
private Socket m_sock = null;
private void m_btnConnect_Click(object sender, System.EventArgs e)
{
Cursor cursor = Cursor.Current;
Cursor.Current = Cursors.WaitCursor;
try
{
// Close the socket if it is still open
if( m_sock != null && m_sock.Connected )
{
m_sock.Shutdown( SocketShutdown.Both );
System.Threading.Thread.Sleep( 10 );
m_sock.Close();
}
// Create the socket object
m_sock = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
// Define the Server address and port
IPEndPoint epServer = new IPEndPoint( IPAddress.Parse( m_tbServerAddress.Text ), 399 );
// Connect to the server blocking method and setup callback for recieved data
// m_sock.Connect( epServer );
// SetupRecieveCallback( m_sock );
// Connect to server non-Blocking method
m_sock.Blocking = false;
AsyncCallback onconnect = new AsyncCallback( OnConnect );
m_sock.BeginConnect( epServer, onconnect, m_sock );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Server Connect failed!" );
}
Cursor.Current = cursor;
}
If the connection already exists it is destroyed. A Socket is then created and an end point established. The commented out code allows for the simpler blocking connection attempt. BeginConnect is used to commence a non blocking connection attempt. Note, even if a non-blocking connection is attempted, the connection will block until the machine name is resolved into an IP address, for this reason it is better to use the IP address than the machine name if possible to avoid blocking. The following method is called once the connection attempt is complete, it displays connection error or sets up the receive data callback if connected OK.
public void OnConnect( IAsyncResult ar )
{
// Socket was the passed in object
Socket sock = (Socket)ar.AsyncState;
// Check if we were sucessfull
try
{
// sock.EndConnect( ar );
if( sock.Connected )
SetupRecieveCallback( sock );
else
MessageBox.Show( this, "Unable to connect to remote machine",
"Connect Failed!" );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Unusual error during Connect!" );
}
}
Receiving data
To receive data asynchronously, it is necessary to setup an AsyncCallback to handle events triggered by the Socket such as new data and loss of connection. This is done using the following method;
private byte [] m_byBuff = new byte[256]; // Recieved data buffer
public void SetupRecieveCallback( Socket sock )
{
try
{
AsyncCallback recieveData = new AsyncCallback( OnRecievedData );
sock.BeginReceive( m_byBuff, 0, m_byBuff.Length, SocketFlags.None,
recieveData, sock );
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Setup Recieve Callback failed!" );
}
}
The SetupRecieveCallback method starts a BeginReceive using a delegate pointing to the OnReceveData method that follows. It also passes a buffer for the receive data to be inserted into.
public void OnRecievedData( IAsyncResult ar )
{
// Socket was the passed in object
Socket sock = (Socket)ar.AsyncState;
// Check if we got any data
try
{
int nBytesRec = sock.EndReceive( ar );
if( nBytesRec > 0 )
{
// Wrote the data to the List
string sRecieved = Encoding.ASCII.GetString( m_byBuff, 0, nBytesRec );
// WARNING : The following line is NOT thread safe. Invoke is
// m_lbRecievedData.Items.Add( sRecieved );
Invoke( m_AddMessage, new string [] { sRecieved } );
// If the connection is still usable restablish the callback
SetupRecieveCallback( sock );
}
else
{
// If no data was recieved then the connection is probably dead
Console.WriteLine( "Client {0}, disconnected", sock.RemoteEndPoint );
sock.Shutdown( SocketShutdown.Both );
sock.Close();
}
}
catch( Exception ex )
{
MessageBox.Show( this, ex.Message, "Unusual error druing Recieve!" );
}
}
When the above event is fired the receive data is assumed to be ASCII. The new data is sent to the display by invoking a delegate. Although it is possible to call Add() on the list to display the new data, it is a very bad idea because the received data will most likely be running in another thread. Note the receive callback must also be established again to continue to receive more events. Even if more data was received than can be placed in the input buffer, restabilising the receive callback will cause it to trigger until all data has been read.
The AddMessage delegate is created to decouple socket thread from user interface thread as follows;
// Declare the delegate prototype to send data back to the form
delegate void AddMessage( string sNewMessage );
namespace ChatClient
{
. . .
public class FormMain : System.Windows.Forms.Form
{
private event AddMessage m_AddMessage;
// Add Message Event handler for Form
. . .
public FormMain()
{
. . .
// Add Message Event handler for Form decoupling from input thread
m_AddMessage = new AddMessage( OnAddMessage );
. . .
}
public void OnAddMessage( string sMessage )
{
// Thread safe operation here
m_lbRecievedData.Items.Add( sMessage );
}
public void OnSomeOtherThread()
{
. . .
string sSomeText = "Bilbo Baggins";
Invoke( m_AddMessage, new string [] { sSomeText } );
}
. . .
}
}
Working in UNICODE
When data is received or is to be sent the data is held in an array of 8 bit bytes. This data must be encoded to a format suitable for .NET when received and a format suitable for the receiving application when sent. C# uses a multibyte character encoding internally so data must be converted to that when it is received and before it is sent out using the Encoding.ASCII or Encoding.UNICODE static methods as necessary.
Don't believe a packet sent is a packet received
When the receive data event is triggered the received data is held in the input buffer. During development a packet sent often corresponds to a single trigging of the receive event and one complete set of data in the receive buffer. This is definitely not the case in a production system. Data is not packet-ised and actually consists of a stream of individual bytes that may be broken up into may packets. Do not relay on receiving complete packets and develop your own tags to indicate start and end of packet.
Conclusion
Although fairly simple to use Sockets do require a fair amount of code to get working well. Where possible you should try to use WebServices or Remoting in there place. Professional ADO.NET Programming by Wrox is a good book on other stuff, check it out.
About John McTainsh
I starting writing code when Apple IIe was all the rage (1982?). Graduated Mechanical Engineering in New Zealand, so I could build Robots. Took up Commerical diving in Asia for a few years to make some quick money (Spent it all). I have been writing C/C++/Assembly and building underwater robots since MS QuickC/Quick Assembler in 1989. I currently live in Australia and work mosly with MS VisualC.