一个简单的TCP/IP聊天客户端/服务器






1.38/5 (13投票s)
2004 年 11 月 3 日
3分钟阅读

189953

12519
这篇文章有助于理解简单的线程使用、TCP/IP网络以及XML的基本用法。

您可以在此处找到更新。
引言
这是一个简单的TCP/IP聊天示例。该解决方案由三个不同的项目组成。主要项目是“Listener”,您可以在其中找到该程序的核心。在Rete.cs文件中,包含使该程序工作的核心方法。
请注意,源代码注释(//)是用意大利语编写的。
最重要的类是rete.cs中的Poller。在Main.cs中创建了一个Poller对象,该对象用于连接、轮询和写入套接字。
在Main.cs中启动了一个线程,用于刷新GUI;该线程执行
public void StartGUIRefreshThread()
{
    ThreadStart GuiRefreshDelegate=new ThreadStart(this.GUIRefreshThread);
    GUIRefresh= new Thread(GuiRefreshDelegate);
    GUIRefresh.Name="GUIRefresh";
    GUIRefresh.Start();
}
这是一个使用线程的简单示例。启动一个新线程意味着在“同一时间”运行代码的不同部分。当一个线程正在运行循环时,“主程序”(它是另一个线程)会执行其他指令。
ThreadStart对象包含一个指向线程启动时要运行的方法的引用。如果该函数不包含循环,线程将在一段时间后停止。如果引用的方法包含无限循环,线程将一直运行直到应用程序停止。
GUIRefresh.Start();执行“this.GUIRefreshThread()”方法,如“new ThreadStart(this.GUIRefreshThread);”中所指定的。
我的“StartGUIRefreshThread”方法启动了一个包含无限循环的线程
public void GUIRefreshThread()
{
    int localDelay, totalDelay=1200, newDataPresentIconDelay=300;
    while (true)
    {
        ...
        Thread.Sleep(localDelay);
    }
}
这会检查Poller的状态,并向用户显示相应的输出。
Poller对象处理TCP连接,使用“原始”Socket对象,如果当前应用程序作为客户端运行,则使用“InizializzaTX()”方法进行初始化;如果应用程序作为服务器(监听器)运行,则使用“InizializzaRX”进行初始化。
服务器情况
public bool InizializzaRX()
{
    if (this.IsListen)
        this.FermaListening();
    this.ListenSocket= new Socket(AddressFamily.InterNetwork, 
                       SocketType.Stream, ProtocolType.Tcp);
    try
    {
        this.ListenSocket.Bind(LIPE);
        this.ListenSocket.Listen(10);
        this.IsListen=true;
        return true;
    }
    
    ...
此代码创建一个新套接字(this.ListenSocket),该套接字绑定到监听IP地址和端口(LIPE)。
现在套接字已正确配置,但它不是“活动”的。我们必须使用<Socket>.Accept()方法启动它以开始监听,该方法将套接字置于“等待状态”以接收客户端连接。
<Socket>.Blocking属性决定执行是否会停止等待客户端连接(true=停止)。我使用的是blocking=true。请注意,如果主线程使用blocking=true,您的应用程序将挂起直到接受连接。
private void PollingRX()
{
    string s="Inizio";
    while (s!=MSGTag.ConnectionStop+"\n")
    {
        if (this.UsedSocket==null&&this.ListenSocket!=null)
        {
            this.DebugRefresh("PollingRX: fermo in accept() su"+ this.LIPE.Port);
            this.IsListen=true;
            this.UsedSocket=this.ListenSocket.Accept();
            this.DebugRefresh("PollingRX: Accept() eseguita");
            this.IsConnectedAsRX=true;
            //Comunica il nome al client, come in InizializzaTX() 
            //il client comunica il suo nome al server
            this.PushString(MSGTag.UserName/*+"Server "*/+this.LocalName);
            this.RTM.RemoteIP=this.UsedScocket.RemoteEndPoint.ToString();
            this.RTM.LocalIP=this.LIPE.ToString();
            //Non uso UsedScocket.LocalEndPoint xchè non è definito l'IP locale
            this.ControlClear();
        }
        s=this.CheckAndRetrive();
        int n=0;
        if (s!=null)
            n=s.Length;
        this.DebugRefresh("PollingRX: eseguito ("+n+" bytes)");
        System.Threading.Thread.Sleep(500);
    }
    this.DebugRefresh("PollingRX: ricevuto EXIT");
    this.KillRX();
}
当客户端建立连接时,Accept()会返回一个与客户端连接的新套接字,用于传输数据,使用我的CheckAndRetrive()(从套接字读取)和PushString()(写入套接字)方法。有关读取相关代码,请参见下文。
此循环将继续,直到从客户端接收到“停止字符串”。
客户端情况
public void InizializzaTX()
{
    if ((!this.IsConnected)) 
    {
        try
        {
            this.UsedScocket = new Socket(AddressFamily.InterNetwork, 
                               SocketType.Stream, ProtocolType.Tcp);
            this.UsedScocket.Connect(this.RIPE);
            this.IsConnectedAsTX=true;
            this.PushString(MSGTag.UserName+/*"Client "+*/this.LocalName);
            this.ControlClear();
            //E'= a RIPE
            this.RTM.RemoteIP=this.UsedScocket.RemoteEndPoint.ToString();
            this.RTM.LocalIP=this.UsedScocket.LocalEndPoint.ToString();
        }
        ...
这是RX的对等操作:创建一个新套接字,并建立与远程IP和端口(RIPE)的连接。
private void PollingTX()
{
    string s="inizio";
    while (s!=MSGTag.ConnectionStop+"\n")
    {
        if (this.UsedScocket!=null)
        {
            s=this.CheckAndRetrive();
        }
        int n=0;
        if (s!=null)
            n=s.Length;
        this.DebugRefresh("PollingTX: eseguito("+n+" bytes)");
        System.Threading.Thread.Sleep(500);
    }
    this.DebugRefresh("PollingTX: ricevuto EXIT");
    this.KillTX();
}
与RX非常相似……一旦套接字连接成功,通信就是“对称”的。
读写数据
这是我如何从套接字获取字符串(CheckAndRetrive()使用此函数从套接字获取数据)
public static string GetStringFromSocket(Socket s)
{
    Byte[] recBytes=new Byte[10001];
    int i=0; 
    string st=null;
    if (s.Available>0)
    {
        i=s.Receive(recBytes,0,s.Available,SocketFlags.None);
        char[] ac=Encoding.UTF8.GetChars(recBytes,0,i);
        foreach (char c in ac)
        {
            st+=c.ToString();
        }
    }
    return st;
}
套接字必须是一个已连接的套接字;您必须使用字节数组来存放接收到的数据。然后,您必须使用一种编码机制对其进行编码,该机制与解码数据使用的机制相同。我使用UTF8编码。
这会从套接字读取最多10001字节的数据块。要读取更多字节,需要一个循环,类似于
while (s.Available>0){...}
这是如何将字符串放入套接字
public static int PutStringInSocket(Socket s, string msg)
{
    Byte[] sendBytes=Encoding.UTF8.GetBytes(Framing.IncapsulaMSG(msg));
    if (s.Connected)
        return s.Send(sendBytes);
    else return 0;
}
Framing.IncapsulaMSG()仅仅是一个方法,它将标签写入消息中,使其易于解释,并返回一个字符串。
轮询线程
在Poller内部执行其他线程,负责通过TCP连接接收/检查和发送数据。PollingRX()和PollingTX()中的循环在两个不同的线程中运行。
public bool StartPollingTX()
{
    try
    {
        if (!this.IsRunningPollingTX)
        {
            ThreadStart TXPollingDelegate = new ThreadStart(this.PollingTX);
            TXPolling= new Thread(TXPollingDelegate);
            TXPolling.Name="TXPolling";
            TXPolling.Start();
            this.IsRunningPollingTX=true;
            this.DebugRefresh("PollingTX: (Start) eseguito");
        }
        return true;
    }
    catch
    {
        return false;
    }
}
这是一个显示“传输”线程如何启动的示例。(它运行上面描述的“this.PollingTX”。设置选项卡存储在单独的DLL中,配置实用程序也是如此。设置存储在XML文件中。
我知道有很多不完善的地方,但这仅仅是为了举例。
我希望您喜欢这篇文章。
