享受代码,享受人生

SOA is an integration solution. SOA is message oriented first.
The Key character of SOA is loosely coupled. SOA is enriched
by creating composite apps.
posts - 207, comments - 2345, trackbacks - 162, articles - 44
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

异步Socket

Posted on 2005-04-29 15:44 idior 阅读(11587) 评论(15)  编辑 收藏 网摘

在网络通讯的编程中我们经常使用到Socket, 这种情况下我们往往需要长期的监听某个端口, 以获得相应的Socket, 然后再利用它进行相关操作. 但是这样的话, 主线程就会被阻塞.无法对其他时间做出相应. 其实在.Net的Socket类中提供了对异步操作的支持. 下面将介绍其基本原理, 以及利用它做的一个P2P的实现.


背景知识:

你需要了解有关Socket的基本知识, 以及Delegate的异步调用操作.


在这个例子中, 我们实现了一个利用非阻塞(non-blocking)的Socket进行局域网通讯的P2P应用. 每个客户拥有一个Grid(类似于一个二维数组), 当它启动Grid设置服务的时候,一旦别的客户与它相连就可以查询并修改某个网格中的数值.(比如查询 grid[1][2]的值).

运行步骤:

1.       启动服务 在某个客户端输入 start 400 (400是端口号, 你可以任意指定)

2.       连接其他Peer  在另一个客户端中输入 connect 202.119.9.12 400 (202.119.9.12 400是某个开启服务的客户端的IP地址)

3.       输入 get 1 1  表示你想获得grid[1][1]这个网格中的数值. 默认情况下得到0

4.       输入 set 1 1 5 表示你想设置grid[1][1]这个网格中的数值为5 .

5.       再次输入 get 1 1 查询到结果为已修改的5

6.      输入shutdown 关闭与刚才与当前的Peer的连接. 你可以再次连接别的Peer

运行示意图.

 


在通常的应用中Server往往需要长期处于监听状态, 以等待Client的连接. 下面是一个典型的应用.

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"+ "nr";
// 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 );
}

}
 whiletrue ); // Don't use this.
}

catch( Exception ex )
{
Console.WriteLine ( ex.Message );
}

看到那个do {} while( true )了吗?

只要if( listener.Pending() )的条件不被满足,这个过程中,主线程就处于被阻塞的状态, 当然很不利于与用户的交互(还以为死机了呢).

于是就希望有一种非阻塞的机制来实现网络间的通讯. 如果你熟悉java的话, 你可能用过java1.4中的nio (new io). 其中的select机制就是用于解决此问题的. 其实在.net中也有类似于它的一个机制, 而且通过事件触发的异步操作, 使得它更方便被使用, 也更容易被理解.

首先来看看服务器是如何监听客户端的连接的.

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( OnConnectRequest ), listener ); 


注意最后一行代码, BeginAccept 为以后client真正接入的时候设置好了回调函数, 也就是说一旦server发现有client连接它, server端的 OnConnectRequest方法就将被调用.

那么OnConnectRequest方法中又将做一些什么事呢?

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"+ "nr";
// 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, 向client发回了连接成功的信息.

随后又跳回了BeginAccept的状态, 继续监听, 也就是允许有多用户连接.

再来看看连接的那方.

         /// <summary>
        
/// Connect to the server, setup a callback to connect
        
/// </summary>
        
/// <param name="serverAdd">server ip address</param>
        
/// <param name="port">port</param>

        public void Connect(string serverAdd, int port)
        
{
            
try
            
{
                
// Create the socket object
                clientSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                
// Define the Server address and port
                IPEndPoint epServer = new IPEndPoint(IPAddress.Parse(serverAdd), port);
                
// Connect to server non-Blocking method
                clientSock.Blocking = false;
                
                
// Setup a callback to be notified of connection success 
                clientSock.BeginConnect(epServer, new AsyncCallback(OnConnect), clientSock);
            }

            
catch (Exception ex)
            
{
                Console.WriteLine(
"Server Connect failed!");
                Console.WriteLine(ex.Message);
            }
 

     }


BeginConnect为连接成功设置了回调方法OnConnect, 一旦与服务器连接成功就会执行该方法. 来看看OnConnect具体做了什么

        /// <summary>
        
/// Callback used when a server accept a connection. 
        
/// setup to receive message
        
/// </summary>
        
/// <param name="ar"></param>

        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)
                
{
                    AsyncCallback recieveData 
= new AsyncCallback(OnRecievedData);
                    sock.BeginReceive(msgBuff, 
0, msgBuff.Length, SocketFlags.None, recieveData, sock);
                }
               
 
else
                    Console.WriteLine(
"Unable to connect to remote machine""Connect Failed!");
            }

            
catch (Exception ex)
            
{
                Console.WriteLine(ex.Message, 
"Unusual error during Connect!");
            }


        }



它在检测确实连接成功后, 又使用BeginReceive注册了接受数据的回调函数.
       

       

 /// <summary>
        
/// Callback used when receive data., both for server or client
        
/// Note: If not data was recieved the connection has probably died.
        
/// </summary>
        
/// <param name="ar"></param>

        public void OnRecievedData(IAsyncResult ar)
        
{
            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(msgBuff, 0, nBytesRec);
                    ParseMessage(sock ,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("disconnect from server {0}", sock.RemoteEndPoint);
                    sock.Shutdown(SocketShutdown.Both);
                    sock.Close();
                }

            }

            
catch (Exception ex)
            
{
                Console.WriteLine(ex.Message, 
"Unusual error druing Recieve!");
            }

        }

它在检测确实连接成功后又使用注册了接受数据的回调函数

我们可以发现在整个过程中就是通过事件的不断触发, 然后在预先设置好的回调函数中做相应的处理工作,比如发送接受数据.下面这幅图将让你对这个事件触发的过程有一个形象的认识.

 

配合附带的源代码, 相信可以让你对此过程有更加深入的了解.

至于本文有关P2P的示例, 其实还很不完善. 只是为每个Peer同时提供了充当服务器和客户端的功能. 当然在这个基础上你可以很方便的做出你想要的效果.


源代码下载

参考资料

Feedback

#1楼   回复  引用    

2005-04-29 15:52 by ocean[未注册用户]
我通常的做法是在一个线程中运行你的那个监听的代码,这样自然就不会阻塞主进程了。

#2楼   回复  引用    

2005-04-29 17:17 by idior
一个线程中运行你的那个监听的代码.
这个方法显然没有事件触发来的方便. 不然java也就不会搞nio

#3楼   回复  引用    

2005-04-29 18:20 by 小陆
可以参考一下tomcat的实现, 看看里面的connector接口, connector里面有个叫做Lifecycle什么的, 通过一个observer模式实现了事件的触发, 听到一个消息就通过这个事件扔到上面去了. 还是比较简单的

#4楼   回复  引用    

2005-04-29 19:22 by idior
@小陆
难道你不觉的现在已经很简单了吗?
用delegate(event)实现observer模式可是比java要方便很多啊.

#5楼   回复  引用  查看    

2005-04-29 21:56 by 小陆      
re:
我明白你的意思, 其实tomcat的Connector类应该处于你这里的Socket类一样的抽象层次, 封装了监听的细节.
并不是所有的语言都可以delegate和event的, 了解一些别的办法有时候也是很有用的, 是不是:)
tomcat没有直接使用jdk提供的Socket, 而是封装了一个Connector, 目的就是想隔离具体的连接方式和各种服务处理模块. tomcat为Connector实现了两个类, 分别处理不同的连接, 其中一个监听8080端口上的http请求, 另一个在8009上面监听其他web服务器的jsp处理请求.

#6楼[楼主]   回复  引用  查看    

2005-04-29 22:31 by idior      
@小陆
那么对于接受消息的事件 tomcat是怎么做的呢?
是不是还有一个类?我对tomcat不是很了解,不过java下的nio和这个很像。

#7楼   回复  引用    

2005-04-30 12:14 by 小陆
tomcat为每一个客户请求建立一个线程, 在线程中分配一个监听socket, 如果你有代码的话, 可以看看PoolTcpEndpoint的run方法, 那个while循环里面就是每次收到客户请求的过程, 首先建立一个工作线程, 然后在acceptSocket的地方等待一个客户请求, 等到请求以后将socket分配给工作线程, 工作线程调用一个notifyAll通知他所有的listener, 然后进入下一次循环. 工作线程是在空闲的时间创建的. 更具体的过程就没有看明白了.

#8楼   回复  引用    

2005-05-09 15:28 by pp
可以做一个Wrapper,包装一个Socket,在里面实现了异步接受/发送数据的方法,并定义了自己的数据接收/发送完毕事件

这样用起来就简单多了

#9楼   回复  引用    

2005-05-17 13:58 by notXX
C#的aio和java的nio不太相似。select的功能差不多,但是C#通过异步调用来实现异步io,更适合写客户端的代码;这种方式如果处理大批的socket应该没有java的效率好。当然这样写更简单。

#10楼   回复  引用    

2005-07-24 11:34 by 飞刀
如果你在C#中用微软的(非Mono)的ThreadPool,实际上已经和Java中处理差不多了。

我是从我的blog,你给的链接过来看的。
如果你把每个连接请求抛给ThreadPool来处理,主线程是不会阻塞,而且由于ThreadPool是一个由操作系统完成的池程池,因为反编译也看不到源代码(extern的),他也应当是把这些东东交由操作系统自己来调用。
所以与nio的思想差不多。

Sokcet本身的异步调用,我想应当内部一样有线程,不过我还没有反编译源代码看过,有时间再看吧。

#11楼   回复  引用    

2006-02-17 16:19 by Sunheart[未注册用户]
源码下不了,老大能给我发一份吗?
我很需要
谢谢了
sunheartlee@sina.com

#12楼   回复  引用    

2006-11-02 09:27 by xujinbiao[未注册用户]
我来 说说 JAVA的NIO 是没有实现操作系统底层的传输的 要用JNI 去扩展 ,实现起来很麻烦。
不知道 C# 的NIO 实现了 操作系统的底层传输 没有 ?????

#13楼   回复  引用  查看    

2007-10-29 12:57 by 金宝      
一样的开线程无意义

#14楼   回复  引用  查看    

2008-12-08 16:53 by 我是农民      
我想问下,socket能否用到网页中,就想网页中的聊天室,从而实现实时聊天

#15楼   回复  引用    

2008-12-22 17:00 by libaosky[未注册用户]
ParseMessage(sock ,sRecieved);
这个ParseMessage是干什么用的?在下愚钝,这儿看不懂



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 147648




相关文章:

相关链接: