Socket Programming in C#--Getting Started

时间:2024-10-14 17:33:02

Getting Started

You can argue that one can overcome these shortcomings by multithreading meaning that one can spawn a new thread and let that thread do the polling which then notifies the main thread of the data. This concept could work well, but even if you create a new thread it would require your main thread to share the CPU time with this new thread. Windows operating system (Windows NT /2000 /XP) provide what is called Completion Port IO model for doing overlapped ( asynchronous) IO.

你可能会提出克服上述不足之处可以用多线程的方式,这意味着一个线程可以衍生出一个新的线程、让这个新线程做轮询工作通知主线程中的数据。这种理念可以很好地工作,但即使你创建一个新的线程,它需要你的主线程和这个新线程共享CPU时间。Windows操作系统(Windows NT /2000 /XP)提供了一个被称为IO异步操作模型来处理重叠(异步)IO操作。

The details of IO Completion port are beyond the scope of the current discussion, but to make it simple you can think of IO Completion Ports as the most efficient mechanism for doing asynchronous IO in Windows that is provided by the Operating system. Completion Port model can be applied to any kind of IO including the file read /write and serial communication.

IOCP模型的细节已经超出了本文讨论的范围,但是可以让你对I/O完成端口的理解更简单些,它是最有效率的处理IO异步操作的模型在windows系统中,这个模型是由操作系统提供的。I/O完成端口模型能被应用于各种类型的IO操作包括对文件的读写和串行通讯。

The .NET asynchronous socket programming helper class's Socket provides the similar model.

.Net异步socket编程辅助类提供了相似的模型。

BeginReceive

.NET framework's Socket class provides BeginReceive method to receive data asynchronously i.e., in an non-blocking manner The BeginReceive method has following signature:

public IAsyncResult BeginReceive( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state );

.Net 框架的Socket类提供了异步接收数据的方法BeginReceive(),在非阻塞的方式下BeginReceive()方法具有以下特征:

public IAsyncResult BeginReceive( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state );

The way BeginReceive function works is that you pass the function a buffer , a callback function (delegate) which will be called whenever data arrives.

BeginReceive()函数是通过这种方式工作的:你传递给此函数一个数据流缓冲区,当数据到达时将会调用一个回调函数(委托)。

The last parameter, object, to the BeginReceive can be any class derived from object ( even null ) . When the callback function is called it means that the BeginReceive function completed which means that the data has arrived. The callback function needs to have the following signature:

void AsyncCallback( IAsyncResult ar);

方法中的最后一个形参是Object类型的, 对于BeginReceive()可能是派生的任何类对象(即使为空)。当BeginReceive()中的回调函数被调用,意味着当接收的数据到达时BeginReceive()的功能完成了。回调函数需要具有以下签名:

void AsyncCallback( IAsyncResult ar);

As you can see the callback returns void and is passed in one parameter , IAsyncResult interface , which contains the status of the asynchronous receive operation.

正如你所看到得,AsyncCallback回调函数不返回值,只被传递了一个参数:IAsyncResult接口,它包含了异步接收操作的各种状态。

The IAsyncResult interface has several properties. The first parameter - AsyncState - is an object which is same as the last parameter that you passed to BeginReceive(). The second property is AsyncWaitHandle which we will discuss in a moment. The third property indicates whether the receive was really asynchronous or it finished synchronously. The important thing to follow here is that it not necessary for an asynchronous function to always finish asynchronously - it can complete immediately if the data is already present. Next parameter is IsComplete which indicates whether the operation has completed or not.

IAsyncResult具有一些属性。第一个参数-AsyncState-是一个object类型和你传输到BeginReceive()方法的最后一个参数一样。第二个属性是AsyncWaitHandle,这个我们一会讨论。第三个属性CompletedSynchronously表示接收是否真的异步完成的还是同步完成的。这里重要的事要遵循的是对于一个异步功能函数来说它总是以异步的方式完成-它可以立即完成如果数据已经存在。第四个参数是IsComplete是指示异步操作是否完成。

If you look at the signature of the BeginReceive again you will note that the function also returns IAsyncResult. This is interesting. Just now I said that I will talk about the second peoperty of the IAsyncResult in a moment. Now is that moment. The second parameter is called AsyncWaitHandle.

如果你看下BeginReceive()的签名你会注意到这个函数也返回IAsyncResult接口。这是有趣的。刚辞我说到我一会儿将要讨论IAsyncResult接口的第二个属性。就是现在了,第二个属性被叫做AsyncWaitHandle。

The AsyncWaitHandle is of type WaitHandle, a class defined in the System.Threading namespace. WaitHandle class encapsulates a Handle (which is a pointer to int or handle ) and provides a way to wait for that handle to become signaled. The class has several static methods like WaitOne ( which is similar to WaitForSingleObject ) WaitAll ( similar to WaitForMultipleObjects with waitAll true ) , WaitAny etc. Also there are overloads of these functions available with timeouts.

AsyncWaitHandle是WaitHandle类型的,这个类被定义在System.Threading命名空间。WaitHandle类封装了一个句柄(这是一个指向int的指针),提供了一种方式去等待该句柄被发消息。这个类包含了一些静态方法例如:WaitOne(和WaitForSingleObject相似)、WaitAll(和WaitForMultipleObjects相似,直到所有的等待都为真)、WaitAny等等。同时也重载了一些适合超时操作的函数。

Coming back to our discussion of IAsyncResult interface, the handle in AsyncWaitHandle (WaitHandle) is signalled when the receive operation completes. So if we wait on that handle infinitely we will be able to know when the receive completed. This means if we pass that WaitHandle to a different thread, the different thread can wait on that handle and can notify us of the fact that the data has arrived and so that we can read the data. So you must be wondering if we use this mechanism why would we use callback function. We won't. Thats right. If we choose to use this mechanism of the WaitHandle then the callback function parameter to the BeginReceive can be null as shown here:

回到我们讨论的IAsyncResult接口,当接收操作完成时AsyncWaitHandle (WaitHandle)的句柄被通知。所以,如果我们无限地等待句柄我们能知道什么时候接收完成。这意味,如果我们给WaitHandle传递不同的线程,不同的线程能够等待响应的句柄并能够通知我们数据已经传输过来的事实,所以我们能够读取数据了。所以你一定要问:如果我们使用上述机制的话,我们为什么还要使用回调函数。我们可以不用,这是对的。如果我们使用WaitHandle的这种异步处理机制的话,BeginReceive()函数中的形参-回调函数可以为空,如下所示:

//m_asynResult is declared of type IAsyncResult and assumming that m_socClient has made a connection.
m_asynResult = m_socClient.BeginReceive(m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,null,null);
if ( m_asynResult.AsyncWaitHandle.WaitOne () )
{
    int iRx = 0 ;
    iRx = m_socClient.EndReceive (m_asynResult);
    char[] chars = new char[iRx + 1];
    System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
    int charLen = d.GetChars(m_DataBuffer, 0, iRx, chars, 0);
    System.String szData = new System.String(chars);
    txtDataRx.Text = txtDataRx.Text + szData;
}

Even though this mechanism will work fine using multiple threads, we will for now stick to our callback mechanism where the system notifies us of the completion of asynchronous operation which is Receive in this case .

尽管使用多线程的机制的工作模式会很好, 我们将坚持我们的回调机制,在接收数据流的情况下,通过操作系统通知我们异步操作的完成。

Lets say we made the call to BeginReceive and after some time the data arrived and our callback function got called.Now question is where's the data? The data is now available in the buffer that you passed as the first parameter while making call to BeginReceive() method . In the following example the data will be available in m_DataBuffer :

BeginReceive(m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,pfnCallBack,null);

比方说,我们已将调用了BeginReceive()方法,经过一段时间后数据达到了,我们的回调函数被调用了。现在的问题是数据在哪里?现在缓冲区的数据是可用的,当调用BeginRecieve()方法时候你传递了第一个方法中的参数。在下面的例子中,数据在BeginReceive()方法的形参变量m_DataBuffer是可用的。

BeginReceive(m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,pfnCallBack,null);

But before you access the buffer you need to call EndReceive() function on the socket. The EndReceive will return the number of bytes received . Its not legal to access the buffer before calling EndReceive. To put it all together look at the following simple code:

在访问数据缓冲区之前,你需要调用套接字中提供的API函数EndReceive()。EndReceive()函数返回接收到的字节流。在调用EndReceive()函数之前,就去访问数据流是不合法的。下面将通过代码将上面讲到的知识点做下实践:

byte[] m_DataBuffer = new byte [10];
IAsyncResult m_asynResult;
public AsyncCallback pfnCallBack ;
public Socket m_socClient;
// create the socket...
public void OnConnect()
{
    m_socClient = new Socket (AddressFamily.InterNetwork,SocketType.Stream ,ProtocolType.Tcp );
    // get the remote IP address...
    IPAddress ip = IPAddress.Parse ("10.10.120.122");
    int iPortNo = 8221;
    //create the end point
    IPEndPoint ipEnd = new IPEndPoint (ip.Address,iPortNo);
    //connect to the remote host...
    m_socClient.Connect ( ipEnd );
    //watch for data ( asynchronously )...
    WaitForData();
}
public void WaitForData()
{
    if ( pfnCallBack == null )
    pfnCallBack = new AsyncCallback (OnDataReceived);
    // now start to listen for any data...
    m_asynResult =
    m_socClient.BeginReceive (m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,pfnCallBack,null);
}
public void OnDataReceived(IAsyncResult asyn)
{
    //end receive...
    int iRx = 0 ;
    iRx = m_socClient.EndReceive (asyn);
    char[] chars = new char[iRx + 1];
    System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
    int charLen = d.GetChars(m_DataBuffer, 0, iRx, chars, 0);
    System.String szData = new System.String(chars);
    WaitForData();
}

The OnConnect function makes a connection to the server and then makes a call to WaitForData. WaitForData creates the callback function and makes a call to BeginReceive passing a global buffer and the callback function. When data arrives the OnDataReceive is called and the m_socClient's EndReceive is called which returns the number of bytes received and then the data is copied over to a string and a new call is made to WaitForData which will call BeginReceive again and so on. This works fine if you have one socket in you application.

OnConnect函数实现连接到服务器,然后调用WaitForData函数。WaitForData函数创建了一个回调函数pfnCallBack、调用BeginReceive()方法的参数中传递一个全局变量的字节缓冲区并调用回调函数pfnCallBack中的方法OnDataReceived。当接受的数据到达后,OnDataReceived函数被调用、相应的客户端的EndReceive函数被调用,OnDataReceived函数实现:接收客户端发送的字节流、并把接收的数据复制到一个字符串上,接着再次调用WaitForData函数并会重新调用BeginRceive()方法等等一系列操作。如果在你的应用程序中只有一个套接字的话,这会工作的很好。