C# .NET Framework 中的网络编程
主要学习UDP 相关的
要使用 UDP 发送数据报,必须知道承载所需服务的网络设备的网络地址以及该服务用来通信的 UDP 端口号。
特殊网络地址用于支持基于 IP 的网络上的 UDP 广播消息。将 IP 地址的所有数位均设置为同一个(即 255.255.255.255),可构成有限的广播地址。 将 UDP 数据报发送到此地址会将消息传递到本地网络段上的任何主机。 由于路由器不会转发发送到此地址的消息,因此只有网络段上的主机会接收到广播消息。通过设置主机标识符的所有数位,可以将广播定向到网络的特定部分。 例如,若要将广播发送到以 192.168.1 开头的 IP 地址标识的网络上的所有主机,请使用地址 192.168.1.255。
UdpClient 侦听端口 11,000 上的 UDP 数据报。 客户端将接收消息字符串并将消息写入控制台

using System; using System.Net; using System.Net.Sockets; using System.Text; public class UDPListener { private const int listenPort = 11000; private static void StartListener() { UdpClient listener = new UdpClient(listenPort); IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, listenPort); try { while (true) { Console.WriteLine("Waiting for broadcast"); byte[] bytes = listener.Receive(ref groupEP); Console.WriteLine($"Received broadcast from {groupEP} :"); Console.WriteLine($" {Encoding.ASCII.GetString(bytes, 0, bytes.Length)}"); } } catch (SocketException e) { Console.WriteLine(e); } finally { listener.Close(); } } public static void Main() { StartListener(); } }
广播发送

using System; using System.Net; using System.Net.Sockets; using System.Text; class Program { static void Main(string[] args) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPAddress broadcast = IPAddress.Parse("192.168.1.255"); byte[] sendbuf = Encoding.ASCII.GetBytes(args[0]); IPEndPoint ep = new IPEndPoint(broadcast, 11000); s.SendTo(sendbuf, ep); Console.WriteLine("Message sent to the broadcast address"); } }
System.Net.Sockets 命名空间包含 Windows 套接字接口的托管实现。 System.Net 命名空间中的所有其他网络访问类均建立在套接字的此实现之上。
大多数情况下,Socket 类方法只是将数据封送到其本机 Win32 对等体中,并负责任何必要的安全检查。
Socket 类支持同步和异步两种基本模式。 在同步模式下,对执行网络操作(例如 Send 和 Receive)的函数的调用等待操作完成,再将控制权返回给调用程序。 在异步模式下,这些调用立即返回。
创建套接字
AddressFamily 枚举指定 Socket 类用其解析网络地址的标准地址系列(例如,AddressFamily.InterNetwork 成员指定 IP 版本 4 地址系列) 。
SocketType 枚举指定套接字的类型(例如,SocketType.Stream 成员指定用流控制来发送和接收数据的标准套接字)。
ProtocolType 枚举指定通信时套接字使用的网络协议(例如:ProtocolType.Tcp 表示套接字使用 TCP;ProtocolType.Udp 表示套接字使用 UDP) 。
套接字创建完成后,可启动与远程终结点的连接或接收来自远程设备的连接。

Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
使用同步客户端套接字
若要发送数据,请将字节数组传递到 Socket 类的数据发送方法之一(Send 和 SendTo)
Send 方法从缓冲区移除字节,并用网络接口将这些字节排队以便发送到网络设备。 网络接口可能不会立即发送数据,但它最终将发送,只要使用 Shutdown 方法正常关闭连接。

byte[] msg = System.Text.Encoding.ASCII.GetBytes("This is a test"); int bytesSent = s.Send(msg);
若要从网络设备接收数据,请将缓冲区传递到 Socket 类的数据接收方法之一(Receive 和 ReceiveFrom)。 同步套接字将挂起应用程序,直到从网络收到字节或者套接字关闭。 下面的示例接收来自网络的数据,然后将其显示在控制台上。 该示例假定来自网络的数据是用 ASCII 编码的文本。 Receive 方法返回从网络接收的字节数。

byte[] bytes = new byte[1024]; int bytesRec = s.Receive(bytes); Console.WriteLine("Echoed text = {0}", System.Text.Encoding.ASCII.GetString(bytes, 0, bytesRec));
不再需要套接字时需将其释放,方法是调用 Shutdown 方法,再调用 Close 方法。 下面的示例释放套接字。SocketShutdown 枚举定义常数以指示应该关闭套接字用于发送,还是用于接收,或者用于两者。

s.Shutdown(SocketShutdown.Both);
s.Close();
使用异步客户端套接字
Socket 类遵循异步方法的 .NET Framework 命名模式;例如,同步 Receive 方法对应于异步 BeginReceive 和 EndReceive 方法。
异步操作要求使用回调方法返回操作结果。
如何使用某个方法开始连接到网络设备并使用回调方法完成此连接?
在下面的示例中,为了将异步套接字连接到网络设备,Connect
方法会初始化 Socket,然后调用 Socket.Connect 方法(传递表示网络设备的远程终结点)、连接回调方法和状态对象(即客户端 Socket,用于在异步调用之间传递状态信息) 。 该示例实现 Connect
方法,将指定的 Socket 连接到指定的终结点。 它假定一个名为 connectDone
的全局 ManualResetEvent。

public static void Connect(EndPoint remoteEP, Socket client) { client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client ); connectDone.WaitOne(); }
连接回调方法 ConnectCallback
实现 AsyncCallback 委托。 它在远程设备可用时连接到远程设备,然后通过设置 ManualResetEvent connectDone
向应用程序线程发出连接完成的信号。 下面的代码实现 ConnectCallback
方法。

private static void ConnectCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket client = (Socket) ar.AsyncState; // Complete the connection. client.EndConnect(ar); Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString()); // Signal that the connection has been made. connectDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } }
如何使用某个方法开始发送数据并使用回调方法完成此次发送?
示例方法 Send
以 ASCII 格式对指定的字符串数据进行编码,并将其异步发送到指定套接字所表示的网络设备。 以下示例实现 Send
方法。

private static void Send(Socket client, String data) { // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. client.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(SendCallback), client); }
发送回调方法 SendCallback
实现 AsyncCallback 委托。 它在网络设备准备好接收时发送数据。 下面的示例演示 SendCallback
方法的实现。 它假定一个名为 sendDone
的全局 ManualResetEvent。

private static void SendCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket client = (Socket) ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = client.EndSend(ar); Console.WriteLine("Sent {0} bytes to server.", bytesSent); // Signal that all bytes have been sent. sendDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } }
如何使用某个方法开始接收数据并使用回调方法结束接收数据?
从客户端套接字读取数据需要一个在异步调用之间传递值的状态对象。 下面的类是一个用于从客户端套接字接收数据的示例状态对象。 它包含以下各项的字段:客户端套接字、已接收数据的缓冲区,和用于保留传入数据字符串的 StringBuilder。 将这些字段放入该状态对象中,使这些字段的值在多个调用之间得以保留,以便从客户端套接字读取数据。

public class StateObject { // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 256; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public StringBuilder sb = new StringBuilder(); }
Receive
方法示例设置状态对象,然后调用 BeginReceive 方法从客户端套接字异步读取数据。 以下示例实现 Receive
方法。

private static void Receive(Socket client) { try { // Create the state object. StateObject state = new StateObject(); state.workSocket = client; // Begin receiving the data from the remote device. client.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } catch (Exception e) { Console.WriteLine(e.ToString()); } }
接收回调方法 ReceiveCallback 实现 AsyncCallback 委托。 它接收来自网络设备的数据并生成消息字符串。 它将来自网络的一个或多个数据字节读入数据缓冲区,然后再次调用 BeginReceive 方法,直到客户端完成数据发送为止。 从客户端读取所有数据后,ReceiveCallback 通过设置 ManualResetEvent sendDone 向应用程序线程发出数据完成的信号。
下面的示例代码实现 ReceiveCallback 方法。 它假定一个名为 response 的全局字符串(该字符串保留接收的字符串)和一个名为 receiveDone 的全局 ManualResetEvent。 服务器必须正常关闭客户端套接字才能结束网络会话。

private static void ReceiveCallback( IAsyncResult ar ) { try { // Retrieve the state object and the client socket // from the asynchronous state object. StateObject state = (StateObject) ar.AsyncState; Socket client = state.workSocket; // Read data from the remote device. int bytesRead = client.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.ASCII.GetString(state.buffer,0,bytesRead)); // Get the rest of the data. client.BeginReceive(state.buffer,0,StateObject.BufferSize,0, new AsyncCallback(ReceiveCallback), state); } else { // All the data has arrived; put it in response. if (state.sb.Length > 1) { response = state.sb.ToString(); } // Signal that all bytes have been received. receiveDone.Set(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } }
使用套接字侦听
侦听器或服务器套接字打开网络上的端口,然后等待客户端连接到该端口。
创建 TCP/IP 网络的远程服务。
略
同步服务器套接字
同步服务器套接字会挂起应用程序的执行,直到在套接字上收到连接请求。 同步服务器套接字不适用于在其操作中大量使用网络的应用程序,但它们可能适用于简单的网络应用程序。
使用 Bind 和 Listen 方法将 Socket 设置为侦听终结点之后,便可以开始使用 Accept 方法接受传入的连接请求。 调用 Accept 方法时,应用程序将处于挂起状态,直到收到连接请求。
在收到连接请求时,Accept 会返回一个与连接客户端关联的新 Socket 实例。
下面的示例从客户端读取数据,并在控制台上显示该数据,然后将数据回显到客户端。 套接字不指定任何消息协议,因此字符串“<EOF>”标记消息数据的结尾。它假定名为 listener
的 Socket 已初始化并绑定到终结点。

Console.WriteLine("Waiting for a connection..."); Socket handler = listener.Accept(); String data = null; while (true) { bytes = new byte[1024]; int bytesRec = handler.Receive(bytes); data += Encoding.ASCII.GetString(bytes,0,bytesRec); if (data.IndexOf("<EOF>") > -1) { break; } } Console.WriteLine( "Text received : {0}", data); byte[] msg = Encoding.ASCII.GetBytes(data); handler.Send(msg); handler.Shutdown(SocketShutdown.Both); handler.Close();
异步套接字使用系统线程池中的多个线程处理网络连接。 一个线程负责发起数据的发送或接收;其他线程完成与网络设备的连接以及发送或接收数据。
使用异步服务器套接字
异步服务器套接字使用 .NET Framework 异步编程模型处理网络服务请求。 Socket 类遵循标准 .NET Framework 异步命名模式;例如,同步 Accept 方法对应于异步 BeginAccept 和 EndAccept 方法。
异步服务器套接字需要一个开始接受网络连接请求的方法、一个处理连接请求并开始接收网络数据的回调方法,以及一个结束接收数据的回调方法。 本部分将进一步讨论所有这些方法。
要开始接受来自网络的连接请求,StartListening
方法会初始化 Socket ,然后使用 BeginAccept 方法开始接受新的连接。 当套接字上接收到新的连接请求时,将调用接受回调方法。 它负责获取将要处理连接的 Socket 实例,并将该 Socket 提交给将处理请求的线程。 接受回调方法实现 AsyncCallback 委托;它返回 void,并取一个 IAsyncResult 类型的参数。 下面的示例是接受回调方法的 shell。

void AcceptCallback(IAsyncResult ar) { // Add the callback code here. }
BeginAccept 方法取两个参数:一个指向接受回调方法的 AsyncCallback 委托和一个用于将状态信息传递给回调方法的对象。 在下面的示例中,侦听 Socket 通过 state 参数传递给回调方法。 此示例会创建一个 AsyncCallback 委托并开始接受来自网络的连接。

listener.BeginAccept(new AsyncCallback(SocketListener.AcceptCallback), listener);
异步套接字使用系统线程池中的线程处理传入的连接。 一个线程负责接受连接,另一个线程则用于处理每个传入的连接,还有一个线程负责接收来自连接的数据。 这些线程可以是同一个线程,具体取决于线程池分配了哪一个线程。 在下面的示例中,System.Threading.ManualResetEvent 类将挂起主线程的执行并在执行可以继续时发出信号。
下面的示例演示在本地计算机上创建异步 TCP/IP 套接字并开始接受连接的异步方法。 它假定存在一个名为 allDone 的全局 ManualResetEvent,该方法是名为 SocketListener 的类的成员,并且假定定义了一个名为 AcceptCallback 的回调方法。