使用 C# 在 .NET 中进行套接字编程入门






4.66/5 (107投票s)
2005 年 6 月 11 日
6分钟阅读

885937
在本文中,我们将学习使用 C# 在 .NET Framework 中进行套接字编程的基础知识。其次,我们将创建一个由服务器和客户端组成的小型应用程序,它们将使用 TCP 和 UDP 协议进行通信。
引言
在本文中,我们将学习使用 C# 在 .NET Framework 中进行套接字编程的基础知识。其次,我们将创建一个由服务器和客户端组成的小型应用程序,它们将使用 TCP 和 UDP 协议进行通信。
先决条件
- 必须熟悉 .NET Framework。
- 应该对 C# 有深入的了解。
- 套接字编程的基础知识。
1.1 网络基础知识
进程间通信(IPC),即两个或多个物理连接的机器交换数据的能力,在企业软件开发中起着非常重要的作用。TCP/IP 是为此类通信采用的最常见标准。在 TCP/IP 下,每台机器都由一个唯一的 4 字节整数标识,称为其 IP 地址(通常格式为 192.168.0.101)。为了方便记忆,此 IP 地址通常绑定到一个用户友好的主机名。下面的程序(showip.cs)使用 System.Net.Dns
类显示传递在第一个命令行参数中的机器的 IP 地址。如果没有命令行参数,它将显示本地机器的名称和 IP 地址。
using System;
using System.Net;
class ShowIP{
public static void Main(string[] args){
string name = (args.Length < 1) ? Dns.GetHostName() : args[0];
try{
IPAddress[] addrs = Dns.Resolve(name).AddressList;
foreach(IPAddress addr in addrs)
Console.WriteLine("{0}/{1}",name,addr);
}catch(Exception e){
Console.WriteLine(e.Message);
}
}
}
Dns.GetHostName()
返回本地机器的名称
,而 Dns.Resolve()
返回给定名称
的机器的 IPHostEntry
,其 AddressList
属性返回该机器的 IPAdresses
。如果找不到指定的宿主,Resolve
方法将引发异常。
虽然 IPAddress
允许标识网络中的机器,但每台机器可能托管多个使用网络进行数据交换的应用程序。在 TCP/IP 下,每个面向网络的应用程序都绑定到一个唯一的 2 字节整数,称为其端口号,该端口号标识在其上执行该应用程序的机器。数据传输以称为 _IP 数据包_ 或 _数据报_ 的字节束的形式进行。每个数据报的大小为 64 KB,其中包含要传输的数据、数据的大小、发送方和预期接收方的 IP 地址以及端口号。一旦一台机器将数据报放到网络上,所有其他机器都会接收到它,但只有 IP 地址与数据报中的接收方 IP 地址匹配的机器才会接受它。之后,该机器会将数据报传输到运行在其上并且绑定到数据报中接收方端口号的应用程序。
TCP/IP 协议族实际上提供了两种不同的数据交换协议。_传输控制协议_(TCP)是一种可靠的面向连接的协议,而 _用户数据报协议_(UDP)是一种不太可靠(但速度快)的无连接协议。
1.2 使用 TCP/IP 进行客户端-服务器编程
在 TCP 下,_服务器进程_和_客户端进程_之间有明显的区别。服务器进程在众所周知的端口(客户端知道该端口)上启动并监听传入的连接请求。客户端进程在任何端口上启动并发出连接请求。
创建 TCP/IP 服务器的基本步骤如下:
- 使用给定的本地端口创建一个
System.Net.Sockets.TcpListener
并启动它TcpListener listener = new TcpListener(local_port); listener.Start();
- 等待传入的连接请求,并在请求出现时从监听器接受一个
System.Net.Sockets.Socket
对象Socket soc = listener.AcceptSocket(); // blocks
- 从上面的
Socket
创建一个System.Net.Sockets.NetworkStream
Stream s = new NetworkStream(soc);
- 使用预定义的协议(_数据交换的既定规则_)与客户端通信
- 关闭
Stream
s.Close();
- 关闭
Socket
s.Close();
- 转到 **步骤 2**。
请注意,当通过步骤 2 接受一个请求时,在代码到达步骤 7 之前,不会接受其他任何请求。(请求将放入队列或 _积压_ 中。)为了同时接受和处理多个客户端,必须在多个线程中执行步骤 2 – 7。下面的程序(emptcpserver.cs)是一个多线程 TCP/IP 服务器,它接受来自其客户端的员工姓名,并将员工的工作发回。客户端通过发送一个空的员工姓名来终止会话。员工数据是从应用程序的配置文件中检索的(应用程序目录中的 XML 文件,其名称为应用程序名称加上 .config 扩展名)。
using System;
using System.Threading;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Configuration;
class EmployeeTCPServer{
static TcpListener listener;
const int LIMIT = 5; //5 concurrent clients
public static void Main(){
listener = new TcpListener(2055);
listener.Start();
#if LOG
Console.WriteLine("Server mounted,
listening to port 2055");
#endif
for(int i = 0;i < LIMIT;i++){
Thread t = new Thread(new ThreadStart(Service));
t.Start();
}
}
public static void Service(){
while(true){
Socket soc = listener.AcceptSocket();
//soc.SetSocketOption(SocketOptionLevel.Socket,
// SocketOptionName.ReceiveTimeout,10000);
#if LOG
Console.WriteLine("Connected: {0}",
soc.RemoteEndPoint);
#endif
try{
Stream s = new NetworkStream(soc);
StreamReader sr = new StreamReader(s);
StreamWriter sw = new StreamWriter(s);
sw.AutoFlush = true; // enable automatic flushing
sw.WriteLine("{0} Employees available",
ConfigurationSettings.AppSettings.Count);
while(true){
string name = sr.ReadLine();
if(name == "" || name == null) break;
string job =
ConfigurationSettings.AppSettings[name];
if(job == null) job = "No such employee";
sw.WriteLine(job);
}
s.Close();
}catch(Exception e){
#if LOG
Console.WriteLine(e.Message);
#endif
}
#if LOG
Console.WriteLine("Disconnected: {0}",
soc.RemoteEndPoint);
#endif
soc.Close();
}
}
}
这是上述应用程序的配置文件(emptcpserver.exe.config)的内容:
<configuration>
<appSettings>
<add key = "john" value="manager"/>
<add key = "jane" value="steno"/>
<add key = "jim" value="clerk"/>
<add key = "jack" value="salesman"/>
</appSettings>
</configuration>
#if LOG
和 #endif
之间的代码仅在编译期间定义了 LOG
符号时(条件编译)才会被编译器添加。您可以通过定义 LOG
符号(信息将在屏幕上记录)来编译上述程序
- csc /D:LOG emptcpserver.cs
或者,不带 LOG
符号(静默模式)
- csc emptcpserver.cs
使用命令 start emptcpserver
启动服务器。
要测试服务器,您可以使用:telnet localhost 2055
。
或者,我们可以创建一个客户端程序。创建 TCP/IP 客户端的基本步骤如下:
- 使用服务器的主机名和端口创建一个
System.Net.Sockets.TcpClient
TcpClient client = new TcpClient(host, port);
- 从上面的
TCPClient
获取流。Stream s = client.GetStream()
- 使用预定义的协议与服务器通信。
- 关闭
Stream
s.Close();
- 关闭连接
client.Close();
下面的程序(emptcpclient.cs)与 EmployeeTCPServer
通信。
using System;
using System.IO;
using System.Net.Sockets;
class EmployeeTCPClient{
public static void Main(string[] args){
TcpClient client = new TcpClient(args[0],2055);
try{
Stream s = client.GetStream();
StreamReader sr = new StreamReader(s);
StreamWriter sw = new StreamWriter(s);
sw.AutoFlush = true;
Console.WriteLine(sr.ReadLine());
while(true){
Console.Write("Name: ");
string name = Console.ReadLine();
sw.WriteLine(name);
if(name == "") break;
Console.WriteLine(sr.ReadLine());
}
s.Close();
}finally{
// code in finally block is guranteed
// to execute irrespective of
// whether any exception occurs or does
// not occur in the try block
client.Close();
}
}
}
1.3 使用 UDP 进行多播
与 TCP 不同,UDP 是无连接的,即可以使用单个套接字将数据发送到多个接收者。基本的 UDP 操作如下:
- 使用本地端口或远程主机和远程端口创建一个
System.Net.Sockets.UdpClient
UdpClient client = new UdpClient(local_ port);
或
UdpClient client = new UdpClient(remote_host, remote_port);
- 使用上面的
UdpClient
接收数据System.Net.IPEndPoint ep = null; byte[] data = client.Receive(ref ep);
byte
数组data
将包含接收到的数据,而ep
将包含发送方的地址。 - 使用上面的
UdpClient
发送数据。如果远程主机名和端口号已通过构造函数传递给
UdpClient
,则使用以下方法发送byte
数组data
:client.Send(data, data.Length);
否则,使用接收方的
IPEndPoint ep
发送byte
数组data
。client.Send(data, data.Length, ep);
下面的程序(empudpserver.cs)接收远程客户端的员工姓名,并使用 UDP 将该员工的工作发回。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Configuration;
class EmployeeUDPServer{
public static void Main(){
UdpClient udpc = new UdpClient(2055);
Console.WriteLine("Server started, servicing on port 2055");
IPEndPoint ep = null;
while(true){
byte[] rdata = udpc.Receive(ref ep);
string name = Encoding.ASCII.GetString(rdata);
string job = ConfigurationSettings.AppSettings[name];
if(job == null) job = "No such employee";
byte[] sdata = Encoding.ASCII.GetBytes(job);
udpc.Send(sdata,sdata.Length,ep);
}
}
}
这是上述应用程序的配置文件(empudpserver.exe.config)的内容:
<configuration>
<appSettings>
<add key = "john" value="manager"/>
<add key = "jane" value="steno"/>
<add key = "jim" value="clerk"/>
<add key = "jack" value="salesman"/>
</appSettings>
</configuration>
下一个程序(empudpclient.cs)是上述服务器程序的 UDP 客户端。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class EmployeeUDPClient{
public static void Main(string[] args){
UdpClient udpc = new UdpClient(args[0],2055);
IPEndPoint ep = null;
while(true){
Console.Write("Name: ");
string name = Console.ReadLine();
if(name == "") break;
byte[] sdata = Encoding.ASCII.GetBytes(name);
udpc.Send(sdata,sdata.Length);
byte[] rdata = udpc.Receive(ref ep);
string job = Encoding.ASCII.GetString(rdata);
Console.WriteLine(job);
}
}
}
UDP 还支持 _多播_,即使用单个数据报发送到多个接收者。为此,发送方将数据包发送到 224.0.0.1 – 239.255.255.255 范围内的 IP 地址(_D 类_ 地址组)。多个接收者可以 _加入该地址的组_ 并接收数据包。下面的程序(stockpricemulticaster.cs)每 5 秒发送一个数据报,其中包含一个虚构公司的股票价格(随机计算的值)到地址 230.0.0.1。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class StockPriceMulticaster{
static string[] symbols = {"ABCD","EFGH", "IJKL", "MNOP"};
public static void Main(){
UdpClient publisher = new UdpClient("230.0.0.1",8899);
Console.WriteLine("Publishing stock prices to 230.0.0.1:8899");
Random gen = new Random();
while(true){
int i = gen.Next(0,symbols.Length);
double price = 400*gen.NextDouble()+100;
string msg = String.Format("{0} {1:#.00}",symbols,price);
byte[] sdata = Encoding.ASCII.GetBytes(msg);
publisher.Send(sdata,sdata.Length);
System.Threading.Thread.Sleep(5000);
}
}
}
编译并启动 stockpricemulticaster
。
下一个程序(stockpricereceiver.cs)加入地址 230.0.0.1 的组,接收 10 个股票价格,然后离开该组。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class StockPriceReceiver{
public static void Main(){
UdpClient subscriber = new UdpClient(8899);
IPAddress addr = IPAddress.Parse("230.0.0.1");
subscriber.JoinMulticastGroup(addr);
IPEndPoint ep = null;
for(int i=0; i<10;i++){
byte[] pdata = subscriber.Receive(ref ep);
string price = Encoding.ASCII.GetString(pdata);
Console.WriteLine(price);
}
subscriber.DropMulticastGroup(addr);
}
}
编译并运行 stockpricereceiver
。