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

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

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.38/5 (13投票s)

2004 年 11 月 3 日

3分钟阅读

viewsIcon

189953

downloadIcon

12519

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

Sample Image - shot1.jpg

您可以在此处找到更新。

引言

这是一个简单的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文件中。

我知道有很多不完善的地方,但这仅仅是为了举例。

我希望您喜欢这篇文章。

© . All rights reserved.