TCPIP 服务器和客户端示例






4.98/5 (79投票s)
TCPIP 服务器示例,可监听并服务多个客户端连接
截至 2023 年 12 月 5 日已更新...
* 进行了一些更改,以更好地处理多个并发入站连接。
工作原理
当 TCPIPClient
连接到 TCPIPServer
时,TCPIPServer
会将有关已连接的其他 TCPIPClient
的信息发送回新连接的 TCPIPClient
,然后告知已连接的 TCPIPClient
新到达的 TCPIPClient
。因此,现在每个客户端都有一个列表,包含系统中所有其他客户端的信息,以及有关他们的一些详细信息,例如他们的 IP 地址、姓名、所在的计算机名称以及 TCPIPServer
分配给 TCPIPClient
的连接 ID。
当您选择一个或多个 TCPIPClient
来发送文本消息时,文本会被放入数据包中…… TCPIPServer
接收数据包,然后将数据包重新路由到特定的 TCPIPClient
。服务器知道在哪里重定向数据包,因为数据包中的“idTo
”数据字段设置为目标 TCPIPClient
……
/****************************************************************/
//prepare the start packet
xdata.Packet_Type = (UInt16)PACKETTYPES.TYPE_Message;
xdata.Data_Type = (UInt16)PACKETTYPES_SUBMESSAGE.SUBMSG_MessageStart;
xdata.Packet_Size = 16;
xdata.maskTo = 0;
// Set this so server will re-direct this message to the connected client.
// If it's '0' then it will only go to the server.
xdata.idTo = (uint)clientsHostId;
// Set this so the client who is getting your message will know who it's from.
xdata.idFrom = (uint)MyHostServerID;
注意:像这样发送数据不是最高效的方式,因为数据包必须从 TCPIPClient
计算机传输到 TCPIPServer
计算机,然后再传输到目标 TCPIPClient
……富有创意的人可能会想在每个 TCPIPClient
中创建一个服务器,这样 TCPIPServer
的唯一作用就是介绍 TCPIPClient
并向彼此提供足够的信息,以便它们能够建立点对点连接,而无需通过 TCPIPServer
发送数据。但这留待以后再讲!
文件传输
另请注意蓝色的“文件拖放”区域……将一些文件拖放到那里,看看会发生什么。:)
引言
该解决方案包含一个 TCPIPServer
项目、一个 TCPIPClient
项目以及一个 CommonClassLibs
DLL 项目,前两个项目共享该 DLL。
该解决方案使用 Microsoft Visual Studio 2015、.NET Framework 4.5 创建……服务器设置为监听端口 9999,因此它会要求打开防火墙。
TCPIPClient
……您可以在网络上运行多个这样的客户端!
TCPIPServer
带有客户端连接的 listview
和一个高级事件查看器,以便我们能够了解内部情况。
背景
作为 Windows 程序开发者,需要能够与同时使用同一应用程序的其他用户进行通信并实时发送数据。也许您开发了一个允许创建和编辑常见网络文档的应用程序……如果另一个人也在查看同一文档并进行更改,那么您将希望确保另一个人不会覆盖您此时所做的更改。因此,应该有一种方法可以在用户之间通信信息,让他们知道正在进行更改。
本文介绍了一个经典的 TCP/IP 服务器,它可以接收多个客户端连接,并处理来自客户端连接的所有数据包。数据包在服务器上进行组装和处理,或者可以转发给其他单个客户端,或同时发送给所有客户端。
如果您非常有创意,可以使用服务器来简单地通知客户端有关其他已连接客户端的信息,并相互传递足够的信息,以便它们可以直接通信(通常称为中继服务器)。这里是使用此原理的所谓“GComm”(组通信器)的图片。此应用程序是一个用于向网络上的一人或多人发送文件和 RTF 消息的工具。接收和构建数据包的核心机制是本文将要描述的。
具体的细节将在下面描述……
Using the Code
TCPIPServer 程序
让我先说明一下,在这个示例中,我使用固定的数据包大小,这是低效的,因为通常大部分数据包空间不会被使用,但正如您将看到的,这也不是世界末日……正如一位评论者指出的那样,“长度前缀”是最好的方法……在这种情况下,用户会将数据包的大小和类型放入其中,然后您只需将 TCP/IP 块组装到该长度,并将整个内容强制转换为原始状态。
让我们看一下 TCPIPServer
项目的服务器端……此应用程序的主要目的是监听并连接客户端连接。我们为项目设置了一组全局变量。
/*******************************************************/
/// <summary>
/// TCPiP server
/// </summary>
Server svr = null;
private Dictionary<int, MotherOfRawPackets> dClientRawPacketList = null;
private Queue<FullPacket> FullPacketList = null;
static AutoResetEvent autoEvent;//mutex
static AutoResetEvent autoEvent2;//mutex
private Thread DataProcessThread = null;
private Thread FullPacketDataProcessThread = null;
/*******************************************************/
- “
Server
”是 TCP 层类,它建立一个监听端口以接收入站客户端连接的 Socket,并通过事件回调向接口提供原始数据包……它还维护每个客户端的一些信息,并注意一个定义的Packet 类,其中包含一个 1024 字节的数据缓冲区。private Socket _UserSocket; private DateTime _dTimer; private int _iClientID; private string _szClientName; private string _szStationName; private UInt16 _UserListentingPort; private string _szAlternateIP; private PingStatsClass _pingStatClass; /// <summary> /// Represents a TCP/IP transmission containing the socket it is using, the clientNumber /// (used by server communication only), and a data buffer representing the message. /// </summary> private class Packet { public Socket CurrentSocket; public int ClientNumber; public byte[] DataBuffer = new byte[1024]; /// <summary> /// Construct a Packet Object /// </summary> /// <param name="sock">The socket this Packet is being used on.</param> /// <param name="client">The client number that this packet is from.</param> public Packet(Socket sock, int client) { CurrentSocket = sock; ClientNumber = client; } }
- “
dClientRawPacketList
”是一个Dictionary
,用于处理每个客户端的原始数据包。当客户端连接到服务器时,服务器会为每个客户端创建并分配一个唯一的整数值(从 1 开始)……为该客户端创建一个Dictionary
条目,其中Dictionary
的键值是唯一值。当这些客户端向服务器发送数据包时,它会在字典的MotherOfRawPackets
类中收集该客户端的数据包,该类管理一个名为RawPackets
的类队列列表。public class RawPackets { public RawPackets(int iClientId, byte[] theChunk, int sizeofchunk) { _dataChunk = new byte[sizeofchunk]; //create the space _dataChunk = theChunk; //ram it in there _iClientId = iClientId; //save who it came from _iChunkLen = sizeofchunk; //hang onto the space size } public byte[] dataChunk { get { return _dataChunk; } } public int iClientId { get { return _iClientId; } } public int iChunkLen { get { return _iChunkLen; } } private byte[] _dataChunk; private int _iClientId; private int _iChunkLen; }
- “
FullPacketList
”是一个队列类型的列表。它的目的是按到达顺序保存入站数据包。如果您有 10 个客户端连接同时向服务器发送数据,服务器的DataProcessingThread
函数会将这些数据包组装成完整的数据包,并将其存储在此列表中,以便稍后进行处理。 - 在数据包组装线程中使用 2 个 AutoEvent 互斥锁,
autoEvent
和autoEvent2
(抱歉名称通用)。这些允许这些线程函数在处理数据时高效地休眠。 - 如上所述,“
DataProcessThread
”和“FullPacketDataProcessThread
”是 2 个协同工作的线程,它们按照发送的顺序精确地组装数据包。
当 TCPIPServer
应用程序启动时,我们初始化上面定义的变量。
private void StartPacketCommunicationsServiceThread()
{
try
{
//Packet processor mutex and loop
autoEvent = new AutoResetEvent(false); //the RawPacket data mutex
autoEvent2 = new AutoResetEvent(false);//the FullPacket data mutex
DataProcessThread = new Thread(new ThreadStart(NormalizeThePackets));
FullPacketDataProcessThread = new Thread(new ThreadStart(ProcessRecievedData));
//Lists
dClientRawPacketList = new Dictionary<int, MotherOfRawPackets>();
FullPacketList = new Queue<FullPacket>();
//Create HostServer
svr = new Server();
svr.Listen(MyPort);//MySettings.HostPort);
svr.OnReceiveData += new Server.ReceiveDataCallback(OnDataReceived);
svr.OnClientConnect += new Server.ClientConnectCallback(NewClientConnected);
svr.OnClientDisconnect += new Server.ClientDisconnectCallback(ClientDisconnect);
DataProcessThread.Start();
FullPacketDataProcessThread.Start();
OnCommunications($"TCPiP Server is listening on port {MyPort}", INK.CLR_GREEN);
}
catch(Exception ex)
{
var exceptionMessage = (ex.InnerException != null) ?
ex.InnerException.Message : ex.Message;
//Debug.WriteLine($"EXCEPTION IN: StartPacketCommunicationsServiceThread -
// {exceptionMessage}");
OnCommunications($"EXCEPTION: TCPiP FAILED TO START,
exception: {exceptionMessage}", INK.CLR_RED);
}
}
请注意“NormalizeThePackets
”和“ProcessRecievedData
”(是的,拼错了)线程……当 TCP Socket 层抛出其入站数据包时,我们在 NormalizeThePackets
函数循环中获取它们。只要应用程序正在监听(while(svr.IsListening)
),函数线程就会保持活动状态,并在 autoEvent.WaitOne()
互斥锁处等待,直到 TCP 层有数据传入,然后我们调用 autoEvent.Set()
,允许应用程序进程向下执行并处理 dClientRawPacketList Dictionary
中正在收集的数据。检查每个客户端的字典条目(MotherOfRawPackets
),如果附加的客户端之一发送了数据包,则 RawPackets
队列列表将包含要处理的项目。数据包被连接起来,一旦连接了 1024 字节,我们就知道我们有 1 个完整的数据包!该数据包被入队到 FullPacket
队列列表,然后触发第二个互斥锁(autoEvent2.Set()
)以跳过另一个线程函数(ProcessRecieveData
)中的循环……请参阅下面的内容。:)
注意:使用 TCPIP,我们知道数据包可以保证以发送顺序完整地到达……
了解这一点后,我们可以假设如果我们发送数据包,那么我们知道在接收端可以组装它们……但是 TCP 层的诀窍在于,在我们获得整个数据包之前,数据包可能以不同的块到达,所以我们需要一种方法来粘合这些块以重新组装原始发送的内容……
private void NormalizeThePackets()
{
if (svr == null)
return;
while (svr.IsListening)
{
autoEvent.WaitOne();//wait at mutex until signal
/**********************************************/
lock (dClientRawPacketList)//http://www.albahari.com/threading/part2.aspx#_Locking
{
foreach (MotherOfRawPackets MRP in dClientRawPacketList.Values)
{
if (MRP.GetItemCount.Equals(0))
continue;
try
{
byte[] packetplayground = new byte[11264];//good for
//10 full packets(10240) + 1 remainder(1024)
RawPackets rp;
int actualPackets = 0;
while (true)
{
if (MRP.GetItemCount == 0)
break;
int holdLen = 0;
if (MRP.bytesRemaining > 0)
Copy(MRP.Remainder, 0, packetplayground, 0, MRP.bytesRemaining);
holdLen = MRP.bytesRemaining;
for (int i = 0; i < 10; i++)//only go through a max of
//10 times so there will be room for any remainder
{
rp = MRP.GetTopItem;//dequeue
Copy(rp.dataChunk, 0, packetplayground, holdLen, rp.iChunkLen);
holdLen += rp.iChunkLen;
if (MRP.GetItemCount.Equals(0))//make sure there is more
//in the list before continuing
break;
}
actualPackets = 0;
if (holdLen >= 1024)//make sure we have at least one packet in there
{
actualPackets = holdLen / 1024;
MRP.bytesRemaining = holdLen - (actualPackets * 1024);
for (int i = 0; i < actualPackets; i++)
{
byte[] tmpByteArr = new byte[1024];
Copy(packetplayground, i * 1024, tmpByteArr, 0, 1024);
lock (FullPacketList)
FullPacketList.Enqueue(new FullPacket
(MRP.iListClientID, tmpByteArr));
}
}
else
{
MRP.bytesRemaining = holdLen;
}
//hang onto the remainder
Copy(packetplayground, actualPackets * 1024, MRP.Remainder,
0, MRP.bytesRemaining);
if (FullPacketList.Count > 0)
autoEvent2.Set();
}//end of while(true)
}
catch (Exception ex)
{
MRP.ClearList();//pe 03-20-2013
string msg = (ex.InnerException == null) ?
ex.Message : ex.InnerException.Message;
OnCommunications
("EXCEPTION in NormalizeThePackets - " + msg, INK.CLR_RED);
}
}//end of foreach (dClientRawPacketList)
}//end of lock
/**********************************************/
if (ServerIsExiting)
break;
}//Endof of while(svr.IsListening)
Debug.WriteLine("Exiting the packet normalizer");
OnCommunications("Exiting the packet normalizer", INK.CLR_RED);
}
现在是 ProcessRecievedData
函数。
private void ProcessReceivedData()
{
if (svr == null)
return;
while (svr.IsListening)
{
autoEvent2.WaitOne();//wait at mutex until signal
try
{
while (FullPacketList.Count > 0)
{
FullPacket fp;
lock (FullPacketList)
fp = FullPacketList.Dequeue();
//Console.WriteLine(GetDateTimeFormatted +" - Full packet fromID: " +
//fp.iFromClient.ToString() + ", Type: " +
//((PACKETTYPES)fp.ThePacket[0]).ToString());
UInt16 type = (ushort)(fp.ThePacket[1] << 8 | fp.ThePacket[0]);
switch (type)//Interrogate the first 2 Bytes to see what the packet TYPE is
{
case (UInt16)PACKETTYPES.TYPE_MyCredentials:
{
PostUserCredentials(fp.iFromClient, fp.ThePacket);
SendRegisteredMessage(fp.iFromClient, fp.ThePacket);
}
break;
case (UInt16)PACKETTYPES.TYPE_CredentialsUpdate:
break;
case (UInt16)PACKETTYPES.TYPE_PingResponse:
//Debug.WriteLine(DateTime.Now.ToShortDateString() + ", " +
//DateTime.Now.ToLongTimeString() + " - Received Ping from: " +
//fp.iFromClient.ToString() + ", on " +
//DateTime.Now.ToShortDateString() + ", at: " +
//DateTime.Now.ToLongTimeString());
UpdateTheConnectionTimers(fp.iFromClient, fp.ThePacket);
break;
case (UInt16)PACKETTYPES.TYPE_Close:
ClientDisconnect(fp.iFromClient);
break;
case (UInt16)PACKETTYPES.TYPE_Message:
{
AssembleMessage(fp.iFromClient, fp.ThePacket);
}
break;
default:
PassDataThru(type, fp.iFromClient, fp.ThePacket);
break;
}
}//END while (FullPacketList.Count > 0)
}//END try
catch (Exception ex)
{
try
{
string msg = (ex.InnerException == null) ?
ex.Message : ex.InnerException.Message;
OnCommunications($"EXCEPTION in ProcessRecievedData - {msg}", INK.CLR_RED);
}
catch { }
}
if (ServerIsExiting)
break;
}//End while (svr.IsListening)
string info2 = string.Format("AppIsExiting = {0}", ServerIsExiting.ToString());
string info3 = string.Format("Past the ProcessRecievedData loop");
Debug.WriteLine(info2);
Debug.WriteLine(info3);
try
{
OnCommunications(info3, INK.CLR_RED); // "Past the ProcessRecievedData loop"
// also is logged to InfoLog.log
}
catch { }
if (!ServerIsExiting)
{
//if we got here then something went wrong, we need to shut down the service
OnCommunications("SOMETHING CRASHED", INK.CLR_RED);
}
}
好了,我们已经描述了 TCPIP 服务器应用程序如何从客户端接收数据!让我们看一下传输的数据包……服务器和客户端都在 CommonClassLib
DLL 中定义了这个数据包……我决定创建一个名为 PACKET_DATA
的通用类,其固定大小为一个计算机友好的数字 1024。您可以创建任意数量的类。只需确保它们是 1024 字节。请注意,这与上面 Service 类中描述的 Packet 类的大小相匹配。
所以!对于进入 FullPacketList
并入队的每个完整数据包,我们得到的就是这个类。
第一个变量是一个无符号 short(UInt16)
,称为 Packet_Type
。如果我们检查前 2 个字节,如上面 ProcessRecievedData
函数所示,我们就可以弄清楚该类中剩余的数据包含什么。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public class PACKET_DATA
{
/****************************************************************/
//HEADER is 18 BYTES
public UInt16 Packet_Type; //TYPE_??
public UInt16 Packet_Size;
public UInt16 Data_Type; // DATA_ type fields
public UInt16 maskTo; // SENDTO_MY_SHUBONLY and the like.
public UInt32 idTo; // Used if maskTo is SENDTO_INDIVIDUAL
public UInt32 idFrom; // Client ID value
public UInt16 nAppLevel;
/****************************************************************/
public UInt32 Data1; //miscellaneous information
public UInt32 Data2; //miscellaneous information
public UInt32 Data3; //miscellaneous information
public UInt32 Data4; //miscellaneous information
public UInt32 Data5; //miscellaneous information
public Int32 Data6; //miscellaneous information
public Int32 Data7; //miscellaneous information
public Int32 Data8; //miscellaneous information
public Int32 Data9; //miscellaneous information
public Int32 Data10; //miscellaneous information
public UInt32 Data11; //miscellaneous information
public UInt32 Data12; //miscellaneous information
public UInt32 Data13; //miscellaneous information
public UInt32 Data14; //miscellaneous information
public UInt32 Data15; //miscellaneous information
public Int32 Data16; //miscellaneous information
public Int32 Data17; //miscellaneous information
public Int32 Data18; //miscellaneous information
public Int32 Data19; //miscellaneous information
public Int32 Data20; //miscellaneous information
public UInt32 Data21; //miscellaneous information
public UInt32 Data22; //miscellaneous information
public UInt32 Data23; //miscellaneous information
public UInt32 Data24; //miscellaneous information
public UInt32 Data25; //miscellaneous information
public Int32 Data26; //miscellaneous information
public Int32 Data27; //miscellaneous information
public Int32 Data28; //miscellaneous information
public Int32 Data29; //miscellanious information
public Int32 Data30; //miscellaneous information
public Double DataDouble1;
public Double DataDouble2;
public Double DataDouble3;
public Double DataDouble4;
public Double DataDouble5;
/// <summary>
/// Long value1
/// </summary>
public Int64 DataLong1;
/// <summary>
/// Long value2
/// </summary>
public Int64 DataLong2;
/// <summary>
/// Long value3
/// </summary>
public Int64 DataLong3;
/// <summary>
/// Long value4
/// </summary>
public Int64 DataLong4;
/// <summary>
/// Unsigned Long value1
/// </summary>
public UInt64 DataULong1;
/// <summary>
/// Unsigned Long value2
/// </summary>
public UInt64 DataULong2;
/// <summary>
/// Unsigned Long value3
/// </summary>
public UInt64 DataULong3;
/// <summary>
/// Unsigned Long value4
/// </summary>
public UInt64 DataULong4;
/// <summary>
/// DateTime Tick value1
/// </summary>
public Int64 DataTimeTick1;
/// <summary>
/// DateTime Tick value2
/// </summary>
public Int64 DataTimeTick2;
/// <summary>
/// DateTime Tick value1
/// </summary>
public Int64 DataTimeTick3;
/// <summary>
/// DateTime Tick value2
/// </summary>
public Int64 DataTimeTick4;
/// <summary>
/// 300 Chars
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 300)]
public Char[] szStringDataA = new Char[300];
/// <summary>
/// 300 Chars
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 300)]
public Char[] szStringDataB = new Char[300];
/// <summary>
/// 150 Chars
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 150)]
public Char[] szStringData150 = new Char[150];
//18 + 120 + 40 + 96 + 600 + 150 = 1024
}
创建一个 enum
并定义一组数据包类型,使我们能够知道从客户端传入的数据是什么。
public enum PACKETTYPES
{
TYPE_Ping = 1,
TYPE_PingResponse = 2,
TYPE_RequestCredentials = 3,
TYPE_MyCredentials = 4,
TYPE_Registered = 5,
TYPE_HostExiting = 6,
TYPE_ClientData = 7,
TYPE_ClientDisconnecting = 8,
TYPE_CredentialsUpdate = 9,
TYPE_Close = 10,
TYPE_Message = 11,
TYPE_MessageReceived = 12,
TYPE_FileStart = 13,
TYPE_FileChunk = 14,
TYPE_FileEnd = 15,
TYPE_DoneRecievingFile = 16
}
同样,这个 PACKETTYPES enum
也是 CommonClassLib
DLL 的一部分,该 DLL 在 TCPIPServer
和 TCPIPClient
程序之间共享。
TCPIPClient 程序
TCPIPClient
程序在处理数据包方面与服务器几乎相同,但它只需要处理从服务器接收到的内容,而不是处理来自多个客户端的多个 TCP 流。
TCPIPClient
还有一个客户端版本的 TCP 层,它执行连接以连接到监听服务器。
/*******************************************************/
private Client client = null;//Client Socket class
private MotherOfRawPackets HostServerRawPackets = null;
static AutoResetEvent autoEventHostServer = null;//mutex
static AutoResetEvent autoEvent2;//mutex
private Thread DataProcessHostServerThread = null;
private Thread FullPacketDataProcessThread = null;
private Queue<FullPacket> FullHostServerPacketList = null;
/*******************************************************/
这是一个客户端示例,客户端响应来自服务器的 TYPE_Ping
消息。
private void ReplyToHostPing(byte[] message)
{
try
{
PACKET_DATA IncomingData = new PACKET_DATA();
IncomingData = (PACKET_DATA)PACKET_FUNCTIONS.ByteArrayToStructure
(message, typeof(PACKET_DATA));
/***********************************************************************************/
//calculate how long that ping took to get here
TimeSpan ts = (new DateTime(IncomingData.DataLong1)) - (new DateTime(ServerTime));
Console.WriteLine($"{GeneralFunction.GetDateTimeFormatted}:
{string.Format("Ping From Server to client: {0:0.##}ms", ts.TotalMilliseconds)}");
/***********************************************************************************/
ServerTime = IncomingData.DataLong1;// Server computer's current time!
PACKET_DATA xdata = new PACKET_DATA();
xdata.Packet_Type = (UInt16)PACKETTYPES.TYPE_PingResponse;
xdata.Data_Type = 0;
xdata.Packet_Size = 16;
xdata.maskTo = 0;
xdata.idTo = 0;
xdata.idFrom = 0;
xdata.DataLong1 = IncomingData.DataLong1;
byte[] byData = PACKET_FUNCTIONS.StructureToByteArray(xdata);
SendMessageToServer(byData);
CheckThisComputersTimeAgainstServerTime();
}
catch (Exception ex)
{
string exceptionMessage = (ex.InnerException != null) ?
ex.InnerException.Message : ex.Message;
Console.WriteLine($"EXCEPTION IN: ReplyToHostPing - {exceptionMessage}");
}
}
编译和运行解决方案中的应用程序
首先,编译 CommonClassLibs
项目。这将创建 TCPIPServer
和 TCPIPClient
所需的 DLL。它包含两侧所需的类和枚举以及一些公共函数。确保您在其他项目中引用此 DLL。
编译 TCPIPServer
和 TCPIPClient
项目,然后运行 TCPIPServer
……它很可能会在计算机防火墙中创建一个规则以允许端口 9999 通过,所以请允许它。记下计算机在网络上的 IP 地址。
(如果有多个 IP 地址,则很可能是第一个。)
一旦运行起来,就启动 TCPIPClient
应用程序……在“服务器地址”文本框中设置 TCPIPServer
的 IP 地址。如果您在同一台计算机上运行,使用 localhost 应该可以。在任意多台计算机上运行此应用程序,然后单击“连接到服务器”按钮。如果红色指示灯变为绿色,则表示连接成功……当客户端收到服务器的 TYPE_Registered
消息时,它会变为绿色。
关注点
我多年来一直使用这种方法来处理应用程序之间的通信,它非常可靠!
历史
- 2017 年 11 月 15 日,密歇根州利沃尼亚的一个雨天