加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

异步非阻塞的socket通信

(2011-03-02 16:34:21)
标签:

it

分类: 网络编程

本文介绍如何使用非阻塞方式的Socket通信,并且创建了一个聊天程序的例子来帮助说明。

 

介绍
本文介绍如何在多个应用程序之间创建和使用TCP/IP Socket来进行通信。这些应用程序可以运行在同一台机器,也可以在局域网内,甚至也可以是跨越Internet的*。这种方法的好处是不需要你自己来使用线程,而是通过调用Socket的非阻塞模式来实现。在例子中:服务器创建病侦听客户端的连接,一旦有客户连接,服务器就将其加入到一个活动客户的列表中,某个客户端发送的消息也有服务器发送到各个连接的客户端,就好像聊天室中的那样。或许Remoting (远程调用)是做这种工作更好的办法,但是我们这里还是来学习学习如何使用Socket来实现。

*注意:跨越Internet的通讯要求服务器有独立的IP地址并且不在代理或是放火墙之后。

事件时序
服务器必须要先侦听,客户端才能够连接。下面的图例说明了在一个异步Socket会话中的事件时序。

 

运行示例
实例代码分为两部分:ChatServer 和ChatClient. 我们首先来创建ChatServer ,然后使用下面的Telnet命令来测试它。

 

C代码 复制代码
  1. telnet {server machine IP address or machine name} 399   
  2. telnet 10.328.32.76 399  
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。

 

Java代码 复制代码
  1. private Socket client null;   
  2. const int nPortListen 399;   
  3. try  
  4. {   
  5.     TcpListener listener new TcpListener( nPortListen );   
  6.     Console.WriteLine( "Listening as {0}"listener.LocalEndpoint );   
  7.     listener.Start();   
  8.     do  
  9.     {   
  10.         byte [] m_byBuff new byte[127];   
  11.         iflistener.Pending() )   
  12.         {   
  13.             client listener.AcceptSocket();   
  14.             // Get current date and time.   
  15.             DateTime now DateTime.Now;   
  16.             string strDateLine "Welcome " now.ToString("G""\n\r";   
  17.     
  18.             // Convert to byte array and send.   
  19.             Byte[] byteDateLine System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );   
  20.             client.Send( byteDateLine, byteDateLine.Length, 0 );   
  21.         }   
  22.         else  
  23.         {   
  24.             Thread.Sleep( 100 );   
  25.         }   
  26.     whiletrue );    // Don't use this.    
  27. }   
  28. catchException ex )   
  29. {   
  30.     Console.WriteLine ex.Message );   
  31.  
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实例就采用了这种方法。首先服务器的名字和地址用下面的代码取得。

 

 

 

 

 

Java代码 复制代码
  1. IPAddress [] aryLocalAddr null;   
  2. string strHostName "";   
  3. try  
  4. {   
  5.     // NOTE: DNS lookups are nice and all but quite time consuming.   
  6.     strHostName Dns.GetHostName();   
  7.     IPHostEntry ipEntry Dns.GetHostByName( strHostName );   
  8.     aryLocalAddr ipEntry.AddressList;   
  9. }   
  10. catchException ex )   
  11. {   
  12.     Console.WriteLine ("Error trying to get local address {0} "ex.Message );   
  13. }   
  14.     
  15. // Verify we got an IP address. Tell the user if we did   
  16. ifaryLocalAddr == null || aryLocalAddr.Length 1 )   
  17. {   
  18.     Console.WriteLine( "Unable to get local address" );   
  19.     return;   
  20. }   
  21. Console.WriteLine( "Listening on [{0}] {1}"strHostName, aryLocalAddr[0);  
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。这样程序就可以不需要等待或者轮询来处理客户连接了。

 

 

 

 

Java代码 复制代码
  1. const int nPortListen 399;   
  2. // Create the listener socket in this machines IP address   
  3. Socket listener new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );   
  4. listener.Bind( new IPEndPoint( aryLocalAddr[0], 399 );   
  5. //listener.Bind( new IPEndPoint( IPAddress.Loopback, 399 );    // For use with localhost 127.0.0.1   
  6. listener.Listen( 10 );   
  7.     
  8. // Setup callback to be notified of connection requests   
  9. listener.BeginAccept( new AsyncCallback( app.OnConnectRequest ), listener );  
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)。

Java代码 复制代码
  1. Socket client;   
  2. public void OnConnectRequest( IAsyncResult ar )   
  3. {   
  4.     Socket listener (Socket)ar.AsyncState;   
  5.     client listener.EndAccept( ar );   
  6.     Console.WriteLine( "Client {0}, joined"client.RemoteEndPoint );   
  7.     
  8.     // Get current date and time.   
  9.     DateTime now DateTime.Now;   
  10.     string strDateLine "Welcome " now.ToString("G""\n\r";   
  11.     
  12.     // Convert to byte array and send.   
  13.     Byte[] byteDateLine System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() );   
  14.     client.Send( byteDateLine, byteDateLine.Length, 0 );   
  15.     
  16.     listener.BeginAccept( new AsyncCallback( OnConnectRequest ), listener );   
  17.  
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应用程序,用来连接服务器,收发消息。

 

连接
当点击界面上的连接按钮使执行下面的程序使客户连接到服务器。

 

Java代码 复制代码
  1. private Socket m_sock null;   
  2. private void m_btnConnect_Click(object sender, System.EventArgs e)   
  3. {   
  4.     Cursor cursor Cursor.Current;   
  5.     Cursor.Current Cursors.WaitCursor;   
  6.     try  
  7.     {   
  8.         // Close the socket if it is still open   
  9.         ifm_sock != null && m_sock.Connected )   
  10.         {   
  11.             m_sock.Shutdown( SocketShutdown.Both );   
  12.             System.Threading.Thread.Sleep( 10 );   
  13.             m_sock.Close();   
  14.         }   
  15.     
  16.         // Create the socket object   
  17.         m_sock new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );       
  18.     
  19.         // Define the Server address and port   
  20.         IPEndPoint epServer new IPEndPoint(  IPAddress.Parse( m_tbServerAddress.Text ), 399 );   
  21.     
  22.         // Connect to the server blocking method and setup callback for recieved data   
  23.         // m_sock.Connect( epServer );   
  24.         // SetupRecieveCallback( m_sock );   
  25.            
  26.         // Connect to server non-Blocking method   
  27.         m_sock.Blocking false;   
  28.         AsyncCallback onconnect new AsyncCallback( OnConnect );   
  29.         m_sock.BeginConnect( epServer, onconnect, m_sock );   
  30.     }   
  31.     catchException ex )   
  32.     {   
  33.         MessageBox.Show( thisex.Message, "Server Connect failed!" );   
  34.     }   
  35.     Cursor.Current cursor;   
  36.  
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地址而不是机器名来避免这种情况。一旦连接请求处理完毕就会调用下面的方法,它显示连接错误或者在成功连接的情况下建立起接收数据的回调。

Java代码 复制代码
  1. public void OnConnect( IAsyncResult ar )   
  2. {   
  3.     // Socket was the passed in object   
  4.     Socket sock (Socket)ar.AsyncState;   
  5.     
  6.     // Check if we were sucessfull   
  7.     try  
  8.     {   
  9.         //    sock.EndConnect( ar );   
  10.         ifsock.Connected )   
  11.             SetupRecieveCallback( sock );   
  12.         else  
  13.             MessageBox.Show( this"Unable to connect to remote machine"   
  14.                              "Connect Failed!" );   
  15.     
  16.     }   
  17.     catchException ex )   
  18.     {   
  19.         MessageBox.Show( thisex.Message, "Unusual error during Connect!" );   
  20.           
  21.  
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 来处理被诸如接到数据和连接断开所激发的事件。用下面的方法。

 

Java代码 复制代码
  1. private byte []    m_byBuff new byte[256];    // Recieved data buffer   
  2. public void SetupRecieveCallback( Socket sock )   
  3. {   
  4.     try  
  5.     {   
  6.         AsyncCallback recieveData new AsyncCallback( OnRecievedData );   
  7.         sock.BeginReceive( m_byBuff, 0m_byBuff.Length, SocketFlags.None,    
  8.             recieveData, sock );   
  9.     }   
  10.     catchException ex )   
  11.     {   
  12.         MessageBox.Show( thisex.Message, "Setup Recieve Callback failed!" );   
  13.     }   
  14.  
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 方法。同时它也把一个用来接收数据的缓冲传递过去。

 

Java代码 复制代码
  1. public void OnRecievedData( IAsyncResult ar )   
  2. {   
  3.     // Socket was the passed in object   
  4.     Socket sock (Socket)ar.AsyncState;   
  5.     
  6.     // Check if we got any data   
  7.     try  
  8.     {   
  9.         int nBytesRec sock.EndReceive( ar );   
  10.         ifnBytesRec 0 )   
  11.         {   
  12.             // Wrote the data to the List   
  13.             string sRecieved Encoding.ASCII.GetString( m_byBuff, 0nBytesRec );   
  14.     
  15.             // WARNING The following line is NOT thread safe. Invoke is   
  16.             // m_lbRecievedData.Items.Add( sRecieved );   
  17.             Invoke( m_AddMessage, new string [] sRecieved );   
  18.     
  19.             // If the connection is still usable restablish the callback   
  20.             SetupRecieveCallback( sock );   
  21.         }   
  22.         else  
  23.         {   
  24.             // If no data was recieved then the connection is probably dead   
  25.             Console.WriteLine( "Client {0}, disconnected"sock.RemoteEndPoint );   
  26.             sock.Shutdown( SocketShutdown.Both );   
  27.             sock.Close();   
  28.         }   
  29.     }   
  30.     catchException ex )   
  31.     {   
  32.         MessageBox.Show( thisex.Message, "Unusual error druing Recieve!" );   
  33.     }   
  34.  
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线程和用户界面线程的耦合程度,如下所示:

 

Java代码 复制代码
  1. // Declare the delegate prototype to send data back to the form   
  2. delegate void AddMessage( string sNewMessage );   
  3.     
  4. namespace ChatClient   
  5. {   
  6.     .   
  7.     public class FormMain System.Windows.Forms.Form   
  8.     {   
  9.         private event AddMessage m_AddMessage;               
  10.         // Add Message Event handler for Form   
  11.         .   
  12.            
  13.         public FormMain()   
  14.         {   
  15.                
  16.             // Add Message Event handler for Form decoupling from input thread   
  17.             m_AddMessage new AddMessage( OnAddMessage );   
  18.             .   
  19.         }   
  20.            
  21.         public void OnAddMessage( string sMessage )   
  22.         {   
  23.             // Thread safe operation here   
  24.             m_lbRecievedData.Items.Add( sMessage );   
  25.         }   
  26.            
  27.     
  28.         public void OnSomeOtherThread()   
  29.         {   
  30.             .   
  31.             string sSomeText "Bilbo Baggins";   
  32.             Invoke( m_AddMessage, new string [] sSomeText );   
  33.         }   
  34.         .   
  35.           
  36.  
// 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这本书很不错,值得一看。

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有