65.9K
CodeProject 正在变化。 阅读更多。
Home

C# \ VB .NET 多用户通信库 (TCP)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (51投票s)

2010年10月15日

CPOL

13分钟阅读

viewsIcon

391705

downloadIcon

33614

允许多个用户在同一服务器上相互发送和接收消息。

RobotBattleNetCommServer.jpg

在我学校的机器人对战项目中使用 NetComm 库的示例,
这是服务器的屏幕截图,有 4 个客户端已连接到它。

引言

NetComm 库允许您将多个客户端连接到同一服务器。这使您可以执行以下操作:

  • 相互传输私人消息
  • 向同一服务器上的所有用户发送消息(公开消息)
  • 仅使用 ID(一个唯一的字符串)与每个客户端通信,而无需知道彼此的 IP,客户端只需要知道主机 IP 地址。

NetComm 库处理所有复杂的工作,它在与应用程序 UI 分开的线程中运行——当新消息到达时,NetComm 库将引发一个事件。

这有什么用?嗯,这使您可以轻松创建多人聊天 - 或任何其他需要处理多用户通信的应用程序。

使用此库,您可以轻松创建此类复杂的通信。

本文用 C# 编写,但我附带了一个 VB.NET 示例供 VB 用户(我最喜欢的语言)使用。该库是用 VB.NET 构建的。

背景

我为我的 学校项目 开发了这个库 - 我创建了一个服务器,用于跟踪两台由两台远程 PC 远程控制的机器人。该项目模拟了两个团队之间的机器人对战。

我将客户端(4 个客户端)连接到同一服务器。服务器处理所有游戏规则、每个机器人的生命值、子弹、起始弹药、武器。服务器的主要作用是让两个团队遵守相同的规则。服务器是进行游戏计算的地方。它基本上是游戏的“中心”,观看者可以查看服务器 UI,查看记分牌、每个团队的机器人生命值、剩余子弹数等…… NetComm 库使两个团队之间的通信变得容易得多。

概念(或,它是如何工作的?面向更高级的用户)

我用 VB.NET 构建了这个库。我使用了 Microsoft 创建的 TCPClientTCPListener (System.Net.Sockets) 作为这个库的基础。整个库都依赖于这两个类。

为了让 UI 保持响应,我在另一个线程中完成了所有通信,当新消息到达时,我需要引发一个事件到 UI 线程以通知它有关新消息。我使用了 SynchronizationContext 类 (System.Threading) 来获取 UI 线程的上下文,并将新消息发布到 UI 线程的消息队列。

为了能够使用 ID(这样客户端就可以使用字符串(如名称)传输消息,而无需知道 IP),我使用了 AsciiEncoding 类将字节转换为 string 以便进行唯一 ID。

在使用“代码”之前阅读(重要信息)

在此库中,所有客户端都连接到同一个服务器,这意味着以下几点:

  • 所有客户端应使用与服务器相同的端口。
  • 所有客户端必须知道服务器的 IP 地址。
  • 所有客户端都可以相互通信(使用 ID),ID 描述了消息指向的客户端是谁。它允许客户端相互通信。例如:ID 可以是一个名字,如“Jack”,如果一个 ID 为“Jack”的客户端想向“ElvisPresley”发送消息,他只需使用他的 ID(“ElvisPresley”)来发送消息。这些消息通过主机传输,主机负责分析它们并将它们发送给接收者。
NetCommHowItWorks.jpg

Jack 想向 ElivsPresley 发送一条消息,他使用了他的 ID (ElivsPresley)
要发送该消息,主机接收到消息,
进行分析并将其发送给 ElvisPresley。

  • 主机和客户端都可以发送和接收消息。他们可以相互发送私人消息。主机可以广播消息(将公开消息发送给所有参与的客户端)。
  • 主机 ID 始终是 ""(null),要向主机发送私人消息,我们使用 SendData 方法并带一个 "" 字符串。(如果你还不明白,最后你会明白的)。
  • NetComm 库可以发送和接收字节数组。如果您打算将此库用于聊天目的,可以使用以下函数将字节转换为字符串,并将字符串转换为字节:
string ConvertBytesToString(byte[] bytes)
{
    return ASCIIEncoding.ASCII.GetString(bytes);
}

byte[] ConvertStringToBytes(string str)
{
    return ASCIIEncoding.ASCII.GetBytes(str);
}

这两个函数都使用 AsciiEncoding 类,该类使用 ASCII 表将 char 转换为其对应的数值。(每个字节代表 string 中的一个字符)。

NetCommChatExampleWithNames.jpg

附带了 VB.NET 示例 - 也附带了 C# 示例

Using the Code

要开始,请创建您的项目并执行以下操作:

  1. 打开您的项目。
  2. 右键单击“引用”并选择“添加引用”。
  3. 选择“浏览”选项卡,然后选择已解压的“NetComm.dll”。
  4. 接受所有对话框。

创建主机

HostScreenshot.jpg

要开始我们的第一步,我们需要创建一个监听任何传入客户端的主机。主机本身可以发送和接收消息。

在您的 Form 类的全局声明中,添加以下声明:

NetComm.Host Server; //Creates the host variable object

Form_Load 事件处理方法中,添加以下代码:

Server = new NetComm.Host(2020); 	//Initialize the Server object, 
				//connection will use the 2020 port number
Server.StartConnection(); 		//Starts listening for incoming clients

2020 是端口号,您可以使用任何您想要的端口号,但请确保客户端使用相同的端口号。

第二行告诉 Server 对象开始监听任何传入的客户端。

Host 类包含以下事件:

  • onConnection(string id)
    每次有用户连接时,此事件都会引发以通知应用程序。id 参数包含已连接客户端的 ID。
  • lostConnection(string id)
    每次有用户断开连接时,此事件都会引发以通知应用程序。id 参数包含已断开连接的客户端的 ID。
  • errEncounter(Execption ex)
    每次发生错误时,此事件都会引发以通知应用程序。ex 参数包含异常对象。
  • ConnectionLost()
    当服务器连接关闭(停止监听)时,将引发此事件。
  • DataReceived(string ID, byte[] Data)
    每次收到消息时,此事件都会引发以通知应用程序。ID 参数包含发送消息的客户端的 ID。Data 参数包含该客户端发送的字节数组。
  • DataTransferred(string Sender, string Recipient, byte[] Data)
    每次消息在客户端之间传输时(使用 Client.SendData 方法),此事件都会引发以通知应用程序。Sender 包含发送消息的客户端的 ID。Recipient 包含接收消息的客户端的 ID。Data 参数包含在客户端之间传输的数据。

我创建了一个 textbox,并将其 name 属性更改为“Log”,此 textbox 将记录服务器上发生的所有事件。

让我们在 Form_Load 事件处理方法中(在之前的行下方)添加以下几行:

//Adding event handling methods, to handle the server messages
Server.onConnection += new NetComm.Host.onConnectionEventHandler(Server_onConnection);
Server.lostConnection += new NetComm.Host.lostConnectionEventHandler
			(Server_lostConnection);
Server.DataReceived += new NetComm.Host.DataReceivedEventHandler(Server_DataReceived);

我们需要创建一些方法来处理这些事件,对于 onConnection 事件,我添加了以下方法:

void Server_onConnection(string id)
{
    Log.AppendText(id + " connected!" + 
	Environment.NewLine); //Updates the log textbox when new user joined
} 

每次有用户加入房间时,您的应用程序都会进入此方法,并在用户进入服务器时更新日志。

日志会持续更新,以通知我们新用户加入房间。如果您注意到 onConnection 方法具有id 参数,则此参数包含加入服务器的用户的 ID(名称/唯一字符串)。

现在让我们添加 Server_lostConnection 方法:

void Server_lostConnection(string id)
{
    Log.AppendText(id + " disconnected" + 
	Environment.NewLine); //Updates the log textbox when user leaves the room
}

每次用户离开房间时,您的应用程序都会进入此方法,并在用户离开服务器时更新日志。

让我们创建 DataReceived 方法来处理新消息:

void Server_DataReceived(string ID, byte[] Data)
{
    Log.AppendText(ID + ": " + ConvertBytesToString(Data) + 
	Environment.NewLine); 	//Updates the log when a new message arrived, 
				//converting the Data bytes to a string
}

这有点棘手。每次收到新消息时,您的应用程序都会进入此方法。如果 Jack(Jack 是 ID)发送了一条消息“Hello!”,Log 文本框将被更新为以下文本:“Jack: Hello!”。

但是,为什么 DataReceived 方法的 Data 变量是字节类型?为什么它不是 string?嗯,如前所述,NetComm 类可以在客户端之间传输字节,因此我们使用ConvertBytesToString 函数。此函数写在本文章的顶部。

如前所述,客户端也可以发送和接收消息。为了发送消息,我们使用以下方法:

Server.SendData("Jack", Data); 	//Jack is the ID of the client 
				//we want to send the data to

如前所述,“Jack”是要发送数据的客户端的 ID。Data 变量是我们想要传输的字节数组。如果您想传输 string,您应该使用 AsciiEncoding 类将 bytes 转换为 string。或者继续阅读,我们稍后将构建一个将 string 转换为字节数组的函数。

要向所有参与的客户端发送消息,我们使用 Brodcast 方法:

Server.Brodcast(Data); //Sends the Data bytes to all participating clients

同样,Data 变量是我们想要发送给每个客户端的字节集。

要踢出特定客户端,我们使用 DisconnectUser 方法:

Server.DisconnectUser("Jack"); //Kicks Jack out of the server

要获取所有连接用户的列表,我们使用 User 属性:

List<string> usersList = Server.Users; //Gets a list of all the connected clients

每次关闭应用程序时,您都应该关闭 Server 对象连接。在 Form_Closing 事件处理方法中,添加以下代码:

Server.CloseConnection(); //Closes all of the opened connections and stops listening

Host 类(或我们创建的 Server 对象)还有其他属性和方法我们没有讨论,但您可以随时探索 Host 类并自行查找。

创建客户端

ClientScreenshot.jpg

第二步是创建客户端。如果您还没有阅读本文的“创建主机”部分,您可能需要回头看看 - 我们将在客户端方面使用一些我们之前收集到的信息。

将以下变量添加到您的客户端应用程序全局声明中:

NetComm.Client client; //The client object used for the communication

客户端变量保存所有通信处理、事件和方法。

Form_Load 事件处理方法中添加以下行:

client = new NetComm.Client(); //Initialize the client object

Client 类包含以下事件:

  • Connected()
    当客户端成功连接到主机时,将引发此事件以通知应用程序。
  • Disconnected()
    当客户端与主机断开连接时,将引发此事件以通知应用程序。
  • errEncounter(Execption ex)
    每次发生错误时,此事件都会引发以通知应用程序。ex 参数包含异常对象。
  • DataReceived(byte[] Data, string ID)
    每次收到消息时,此事件都会引发以通知应用程序。ID 参数包含发送消息的客户端的 ID。Data 参数包含该客户端发送的字节数组。

我们需要处理客户端对象的事件(ConnectedDisconnectedDataReceived 等)。这使我们能够响应它们并根据它们更新我们的应用程序,将以下代码放在 Form_Load 事件处理方法中:

//Adding event handling methods for the client
client.Connected += new NetComm.Client.ConnectedEventHandler(client_Connected);
client.Disconnected += new NetComm.Client.DisconnectedEventHandler(client_Disconnected);
client.DataReceived += new NetComm.Client.DataReceivedEventHandler(client_DataReceived);

现在我们应该添加将响应这些事件的方法。

让我们创建 Connected 方法:

void client_Connected()
{
    Log.AppendText("Connected successfully!" + 
	Environment.NewLine); //Updates the log with the current connection state
} 

每次客户端成功连接时,您的应用程序都会进入此方法,然后日志 textbox 将更新其文本,显示“Connected successfully!”。

现在我们将添加 Disconnected 方法:

void client_Disconnected()
{
    Log.AppendText("Disconnected from host!" + 
	Environment.NewLine); //Updates the log with the current connection state
}

每次客户端与主机断开连接时,您的应用程序都会进入此方法,然后日志 textbox 将更新其文本,显示“Disconnected from host!”。

让我们添加 DataReceived 方法:

void client_DataReceived(byte[] Data, string ID)
{
    Log.AppendText(ID + ": " + ConvertBytesToString(Data)  + 
	Environment.NewLine); //Updates the log with the current connection state
}

每次客户端接收到新数据时,您的应用程序都会进入此方法,然后日志 textbox 将更新其文本,显示发送数据的客户端 ID 和数据的 string ID 参数包含发送数据的客户端的 ID(唯一的 string)。Data 参数是一个字节数组,包含该客户端发送的数据。

ConvertBytesToString 函数在本文章的顶部有介绍。

我们需要获取主机的 IP 地址和用户想要的 ID,为此我们创建一个简单的表单来收集信息,但仅出于本文的目的,我们将使用 ID“Jack”和 IP 地址“localhost”,这意味着我们正在使用的计算机的 IP 地址(我们将在同一台计算机上运行示例)。

因为这是一个示例,我们将把通信设置代码放在 Form_Load 事件处理方法中,但在您的项目中,您应该创建一个连接表单来收集这些信息。

设置连接(连接主机)

//Connecting to the host
client.Connect("localhost", 2020, "Jack"); //Connecting to the host 
			//(on the same machine) with port 2020 and ID "Jack"

如果连接成功建立,您的应用程序应该会进入我们之前构建的 Connected 方法。

发送消息是通信的“核心”。这就是为什么我们需要首先进行通信。假设我们想将一个 string 发送给另一个客户端,为此,我们应该在我们的项目中添加一个 textbox,我将其命名为“ChatMessage”。然后创建一个按钮,我将其命名为“SendButton”。当用户在“ChatMessage”文本框中写完消息后,他将单击我们之前创建的“SendButton”。因此,我们应该在 SendButton_Click 事件处理方法中编写此代码:

private void SendButton_Click(object sender, EventArgs e)
{
    client.SendData(ConvertStringToBytes(ChatMessage.Text), "Jack");
}

每次单击 SendButton 时,我们将向“Jack”发送一条私人消息,消息内容为 textbox 中显示的文本。请记住,我们需要将 string 转换为字节数组,因此我们应该使用以下函数:

byte[] ConvertStringToBytes(string str)
{
    return ASCIIEncoding.ASCII.GetBytes(str);
}

阅读本文顶部的解释,了解有关此函数的更多信息。

要向主机发送私人消息,我们只需使用 SendData 方法:

//Sending private message to the host
client.SendData(ConvertStringToBytes(ChatMessage.Text)); 

最后,要结束通信,我们在 Form_Closing 事件处理方法中使用以下方法:

if (client.isConnected) client.Disconnect(); //Disconnects if the 
			//client is connected, closing the communication thread

加快通信速度

为了加快通信速度,您可以使用 NoDelaySendBufferSize ReceiveBufferSize 属性。这里有一段简短的代码应该可以加快通信速度:

//Speeding up the connection
Server.SendBufferSize = 400;
Server.ReceiveBufferSize = 50;
Server.NoDelay = True;

将此代码放在您的 Form_Load 事件处理方法中。主机和客户端都应使用这些代码行才能使连接更快。

使客户端广播消息

我直到现在才注意到 Client 类中没有任何“Broadcast”方法。这是一种使客户端能够广播消息的方法,我还没有测试过。

正如您所读到的,主机是唯一能够广播消息的。为了让客户端广播消息,我们可以将消息发送到主机(私人消息),然后告诉主机广播该消息给所有人。一种方法是在主机 DataReceived 事件处理方法中添加以下代码:

void Server_DataReceived(string ID, byte[] Data)
{
    foreach (string clientID in Server.Users)
    {
        if (ID != clientID) Server.SendData(clientID, Data);
    }
}

换句话说(不是代码“语言”),我们模拟了主机 Broadcast 方法,但这次我们忽略了发送消息的客户端,因此他不会收到他发送的消息。

客户端使用 SendData 方法将消息发送给主机:

client.SendData(Data); 	//This message will be pointed to the host, 
			//then the host will broadcast it

ID 问题 - “D”字符

当使用“David”或“Dan”之类的名字时,NetComm 库出于某种原因工作不正常。目前,唯一的解决方案是使用不以“D”字符开头的其他 ID。

关注点

这个库是为我的学校项目(机器人对战)而开发的,但我决定与大家分享。
编写它很有趣,也很有挑战性(第一次了解 TCP 协议)。我编写这个库是为了在 5 台计算机之间创建通信,其中一台是服务器,负责管理游戏规则、生命值等……另外 2 台计算机在另一个房间控制 2 台机器人,所有消息都通过 NetComm 库传输。随意将此库用于您想要的任何目的!

历史

  • 2010.10.15 - 添加了 ID 问题信息,添加了图片
  • 2010.10.16 - 修正了错误信息,添加了演示图片,删除了额外信息,为每个类添加了引用链接
© . All rights reserved.