使用异步 UDP 套接字的聊天应用程序






4.86/5 (50投票s)
2006年12月29日
4分钟阅读

453573

27898
本文介绍如何使用 UDP 套接字开发聊天应用程序。
引言
在上一篇文章中,我讨论了使用 TCP 套接字构建聊天应用程序。在本文中,我将展示如何使用 UDP 套接字来实现。
UDP 和 TCP 之间的区别
TCP 是面向连接的,并提供错误控制和流量控制。UDP 不提供这些服务,而是依赖应用程序层来实现。UDP 允许发送带有或不带有校验和的数据包;不维护连接,因此每个数据包都是独立发送的。如果数据包丢失或数据包乱序到达,则应用程序应自行检测并纠正这种情况。此外,UDP 不提供 TCP 的安全功能,例如三次握手。
那么 UDP 有什么 TCP 没有的功能呢?首先,UDP 支持多播——将单个数据包发送到多台机器。这很有用,因为它节省了带宽,每个数据包只传输一次,整个网络都能接收到。在 TCP 的开销(延迟)很昂贵的地方也使用了 UDP。
UDP 的一些应用包括 VoIP、音频和视频流、DNS、TFTP、SNMP、在线游戏等。
异步 UDP 套接字
异步 UDP 套接字在标准套接字函数后面附加了 Begin 和 End,例如 BeginSendTo
、BeginReceiveFrom
、EndSendTo
和 EndReceiveFrom
。我们来看其中一个。
IAsyncResult BeginReceiveFrom(byte[] buffer, int offset, int size,
SocketFlags sockflag, ref EndPoint ep, AsyncCallback callback, object state)
BeginReceiveFrom()
方法从无连接套接字上的任何远程主机接收数据。请注意,BeginReceiveFrom()
方法与 BeginReceive()
方法类似,只是它指定了一个 EndPoint
对象的引用。EndPoint
对象定义了发送数据的远程主机 IP 地址和端口号。
当函数完成后,会调用 AsyncCallback
函数。就像事件可以触发委托一样,.NET 也提供了一种方法让方法触发委托。.NET 的 AsyncCallback
类允许方法启动异步函数,并在异步函数完成后提供一个要调用的委托方法。
状态对象用于在 BeginAccept
和相应的 AsyncCallback
函数之间传递信息。
一个示例 BeginReceiveFrom()
方法如下所示:
sock.BeginReceive(data, 0, data.Length, SocketFlags.None,
ref iep, new AsyncCallback(ReceiveData), sock);
相应的 EndReceiveFrom()
方法放在适当的 AsyncCallback
方法中。
void ReceiveData(IasyncResult iar)
{
Socket remote = (Socket)iar.AsyncState;
int recv = remote.EndReceiveFrom(iar);
string stringData = Encoding.ASCII.GetString(data, 0, recv);
Console.WriteLine(stringData);
}
EndReceiveFrom()
方法返回从套接字读取的字节数,并将接收到的数据放入 BeginReceiveFrom()
方法中定义的数据缓冲区。要访问这些数据,这两种方法都应该可以访问数据缓冲区。
入门
使用 TCP 套接字和使用 UDP 套接字的应用程序的架构非常相似。两个应用程序都使用相同的数据结构在服务器和客户端之间进行通信。
对于客户端和服务器之间的消息交换,它们都使用以下简单的命令:
//The commands for interaction between
//the server and the client
enum Command
{
//Log into the server
Login,
//Logout of the server
Logout,
//Send a text message to all the chat clients
Message,
//Get a list of users in the chat room from the server
List
}
下面显示了用于在客户端和服务器之间交换的数据结构。套接字以字节数组的形式传输和接收数据,重载的构造函数和 ToByte
成员函数执行此转换。
//The data structure by which the server
//and the client interact with each other
class Data
{
//Default constructor
public Data()
{
this.cmdCommand = Command.Null;
this.strMessage = null;
this.strName = null;
}
//Converts the bytes into an object of type Data
public Data(byte[] data)
{
//The first four bytes are for the Command
this.cmdCommand = (Command)BitConverter.ToInt32(data, 0);
//The next four store the length of the name
int nameLen = BitConverter.ToInt32(data, 4);
//The next four store the length of the message
int msgLen = BitConverter.ToInt32(data, 8);
//Makes sure that strName has been
//passed in the array of bytes
if (nameLen > 0)
this.strName =
Encoding.UTF8.GetString(data, 12, nameLen);
else
this.strName = null;
//This checks for a null message field
if (msgLen > 0)
this.strMessage =
Encoding.UTF8.GetString(data, 12 + nameLen, msgLen);
else
this.strMessage = null;
}
//Converts the Data structure into an array of bytes
public byte[] ToByte()
{
List<byte> result = new List<byte>();
//First four are for the Command
result.AddRange(BitConverter.GetBytes((int)cmdCommand));
//Add the length of the name
if (strName != null)
result.AddRange(BitConverter.GetBytes(strName.Length));
else
result.AddRange(BitConverter.GetBytes(0));
//Length of the message
if (strMessage != null)
result.AddRange(
BitConverter.GetBytes(strMessage.Length));
else
result.AddRange(BitConverter.GetBytes(0));
//Add the name
if (strName != null)
result.AddRange(Encoding.UTF8.GetBytes(strName));
//And, lastly we add the message
//text to our array of bytes
if (strMessage != null)
result.AddRange(Encoding.UTF8.GetBytes(strMessage));
return result.ToArray();
}
//Name by which the client logs into the room
public string strName;
//Message text
public string strMessage;
//Command type (login, logout, send message, etc)
public Command cmdCommand;
}
UDP 服务器
以下是服务器应用程序使用的一些数据成员:
//The ClientInfo structure holds the
//required information about every
//client connected to the server
struct ClientInfo
{
//Socket of the client
public EndPoint endpoint;
//Name by which the user logged into the chat room
public string strName;
}
//The collection of all clients logged into
//the room (an array of type ClientInfo)
ArrayList clientList;
//The main socket on which the server listens to the clients
Socket serverSocket;
byte[] byteData = new byte[1024];
这里需要注意的一点是,在 UDP 中,客户端和服务器应用程序之间没有这种区别。与 TCP 不同,UDP 服务器不监听传入的客户端;它们只查找来自其他客户端的数据。一旦收到数据,我们会查看它是否是登录、注销等消息。
private void Form1_Load(object sender, EventArgs e)
{
try
{
//We are using UDP sockets
serverSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
//Assign the any IP of the machine and listen on port number 1000
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 1000);
//Bind this address to the server
serverSocket.Bind(ipeServer);
IPEndPoint ipeSender = new IPEndPoint(IPAddress.Any, 0);
//The epSender identifies the incoming clients
EndPoint epSender = (EndPoint) ipeSender;
//Start receiving data
serverSocket.BeginReceiveFrom (byteData, 0, byteData.Length,
SocketFlags.None, ref epSender,
new AsyncCallback(OnReceive), epSender);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "SGSServerUDP",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
使用 IPAddress.Any
,我们指定服务器应接受来自任何接口的客户端请求。要使用任何特定接口,可以使用 IPAddress.Parse (“192.168.1.1”)
代替 IPAddress.Any
。然后 Bind
函数将 serverSocket
绑定到此 IP 地址。epSender
标识数据来自的客户端。
使用 BeginReceiveFrom
,我们开始接收将由客户端发送的数据。请注意,我们将 epSender
作为 BeginReceiveFrom
的最后一个参数传递,AsyncCallback OnReceive
通过 IAsyncResult
的 AsyncState
属性获取此对象,然后它处理客户端请求(登录、注销、向用户发送消息)。有关 OnReceive
的实现,请参阅附带的代码。
TCP 客户端
客户端使用的一些数据成员是:
public Socket clientSocket; //The main client socket
public string strName; //Name by which the user logs into the room
public EndPoint epServer; //The EndPoint of the server
byte []byteData = new byte[1024];
客户端首先连接到服务器。
try
{
//Using UDP sockets
clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
//IP address of the server machine
IPAddress ipAddress = IPAddress.Parse(txtServerIP.Text);
//Server is listening on port 1000
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 1000);
epServer = (EndPoint)ipEndPoint;
Data msgToSend = new Data ();
msgToSend.cmdCommand = Command.Login;
msgToSend.strMessage = null;
msgToSend.strName = strName;
byte[] byteData = msgToSend.ToByte();
//Login to the server
clientSocket.BeginSendTo(byteData, 0, byteData.Length,
SocketFlags.None, epServer,
new AsyncCallback(OnSend), null);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "SGSclient",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
客户端连接到服务器后会发送登录消息。然后,我们发送一个 List 消息来获取聊天室中客户端的名称。
//Broadcast the message typed by the user to everyone
private void btnSend_Click(object sender, EventArgs e)
{
try
{
//Fill the info for the message to be send
Data msgToSend = new Data();
msgToSend.strName = strName;
msgToSend.strMessage = txtMessage.Text;
msgToSend.cmdCommand = Command.Message;
byte[] byteData = msgToSend.ToByte();
//Send it to the server
clientSocket.BeginSendTo (byteData, 0, byteData.Length,
SocketFlags.None, epServer,
new AsyncCallback(OnSend), null);
txtMessage.Text = null;
}
catch (Exception)
{
MessageBox.Show("Unable to send message to the server.",
"SGSclientUDP: " + strName, MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
用户键入的消息作为 Command 消息发送到服务器,然后服务器将其发送给聊天室中的所有其他用户。
收到消息后,客户端会根据情况进行处理(取决于它是登录、注销、命令还是列表消息)。这部分代码相当直接,请参阅附带的项目。
结论
正如您可能注意到的,UDP 聊天应用程序与 TCP 聊天应用程序非常相似;事实上,对我来说,将 TCP 聊天应用程序转换为 UDP 只用了不到一天的时间。希望您觉得这些文章有用。谢谢。