异步非阻塞的socket通信

标签:
it |
分类: 网络编程 |
本文介绍如何使用非阻塞方式的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 null;client = -
const
int nPortListen 399;= -
try
- {
-
TcpListener listener = TcpListener( nPortListen ); -
Console.WriteLine( as ,{0}" listener.LocalEndpoint ); -
listener.Start(); -
-
{ -
[] newm_byBuff = byte[127]; -
listener.Pending() ) -
{ -
client = listener.AcceptSocket(); -
Get current date and time. -
DateTime now = DateTime.Now; -
string strDateLine = " + "G")now.ToString( + "\n\r"; -
-
Convert to byte array and send. -
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() ); -
client.Send( byteDateLine, byteDateLine.Length, ); -
} -
-
{ -
Thread.Sleep( ); -
} -
} true ); //Don't use this. - }
-
catch(
Exception ex ) - {
-
Console.WriteLine ( ex.Message ); - }
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
[] null;aryLocalAddr = -
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 ( trying ,to get local address {0} " ex.Message ); - }
-
-
//
Verify we got an IP address. Tell the user if we did -
if(
aryLocalAddr null== || 1aryLocalAddr.Length < ) - {
-
Console.WriteLine( to get local address" ); -
- }
-
Console.WriteLine(
"Listening on ,: [{0}] {1}" strHostName, 0]aryLocalAddr[ );
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( 0],aryLocalAddr[ 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 );
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( {0}, ,joined" client.RemoteEndPoint ); -
-
Get current date and time. -
DateTime now = DateTime.Now; -
string strDateLine = " + "G")now.ToString( + "\n\r"; -
-
Convert to byte array and send. -
Byte[] byteDateLine = System.Text.Encoding.ASCII.GetBytes( strDateLine.ToCharArray() ); -
client.Send( byteDateLine, byteDateLine.Length, ); -
-
listener.BeginAccept( AsyncCallback( OnConnectRequest ), listener ); - }
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 null;m_sock = -
private
void m_btnConnect_Click(object sender, System.EventArgs e) - {
-
Cursor cursor = Cursor.Current; -
Cursor.Current = Cursors.WaitCursor; -
-
{ -
Close the socket if it is still open -
m_sock null!= && m_sock.Connected ) -
{ -
m_sock.Shutdown( SocketShutdown.Both ); -
System.Threading.Thread.Sleep( ); -
m_sock.Close(); -
} -
-
Create the socket object -
m_sock = Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); -
-
Define the Server address and port -
IPEndPoint epServer = IPEndPoint( 399IPAddress.Parse( m_tbServerAddress.Text ), ); -
-
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 = -
AsyncCallback onconnect = AsyncCallback( OnConnect ); -
m_sock.BeginConnect( epServer, onconnect, m_sock ); -
} -
Exception ex ) -
{ -
MessageBox.Show( ex.Message, "ServerConnect failed!" ); -
} -
Cursor.Current = cursor; - }
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 -
-
{ -
sock.EndConnect( ar ); -
sock.Connected ) -
SetupRecieveCallback( sock ); -
-
MessageBox.Show( "Unable to ,connect to remote machine" -
Failed!" ); -
-
} -
Exception ex ) -
{ -
MessageBox.Show( ex.Message, "Unusualerror during Connect!" ); -
} - }
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 [] newm_byBuff = byte[256]; Recieved data buffer -
public
void SetupRecieveCallback( Socket sock ) - {
-
-
{ -
AsyncCallback recieveData = AsyncCallback( OnRecievedData ); -
sock.BeginReceive( m_byBuff, m_byBuff.Length, SocketFlags.None, -
recieveData, sock ); -
} -
Exception ex ) -
{ -
MessageBox.Show( ex.Message, "SetupRecieve Callback failed!" ); -
} - }
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 -
-
{ -
nBytesRec = sock.EndReceive( ar ); -
nBytesRec 0> ) -
{ -
Wrote the data to the List -
string sRecieved = Encoding.ASCII.GetString( m_byBuff, nBytesRec ); -
-
WARNING : The following line is NOT thread safe. Invoke is -
m_lbRecievedData.Items.Add( sRecieved ); -
Invoke( m_AddMessage, string [] { sRecieved } ); -
-
If the connection is still usable restablish the callback -
SetupRecieveCallback( sock ); -
} -
-
{ -
If no data was recieved then the connection is probably dead -
Console.WriteLine( {0}, ,disconnected" sock.RemoteEndPoint ); -
sock.Shutdown( SocketShutdown.Both ); -
sock.Close(); -
} -
} -
Exception ex ) -
{ -
MessageBox.Show( ex.Message, "Unusualerror druing Recieve!" ); -
} - }
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 - {
-
. . . -
class FormMain : System.Windows.Forms.Form -
{ -
event AddMessage m_AddMessage; -
Add Message Event handler for Form -
. . . -
-
FormMain() -
{ -
. . . -
Add Message Event handler for Form decoupling from input thread -
m_AddMessage = AddMessage( OnAddMessage ); -
. . . -
} -
-
void OnAddMessage( string sMessage ) -
{ -
Thread safe operation here -
m_lbRecievedData.Items.Add( sMessage ); -
} -
-
-
void OnSomeOtherThread() -
{ -
. . . -
string sSomeText = Baggins" ; -
Invoke( m_AddMessage, string [] { sSomeText } ); -
} -
. . . -
} - }
// 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这本书很不错,值得一看。