** 异步 ** ** Socket ** ** 通信
By John McTainsh
From: ** _ http://www.codeproject.com/csharp/socketsincs.asp _ **
Translate by: Hillfree **
本文介绍如何使用非阻塞方式的 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 } );
</SPA