Komunikator w C#

Komunikator w C#

Autor: Marcin Hałaczkiewicz

Opublikowano: 6/27/2006, 12:00 AM

Liczba odsłon: 208612

Dzisiaj pokażemy jak napisać prosty komunikatorek internetowy (coś jak Gadu-Gadu). Będzie on działał zgodnie z zasadą klient - serwer. Do komunikacji pomiędzy jednym a drugim użyjemy protokółu TCP/IP. W programie tym wykorzystamy nowość w .NET 2.0 - kontrolkę BackgroundWorker. Właśnie ona (a dokładniej dwie) będzie odpowiedzialna za nawiązanie połączenia z drugim komputerem oraz za odbieranie komunikatów. Będziemy się nią posługiwać, ponieważ ważne jest, aby komunikacja odbywała się w tle.

Serwer

Standardowo zaczniemy od utworzenia nowego projektu. Nazwijmy go Serwer. Zmieńmy również nazwę pliku Form1.cs na Serwer.cs. Następnie klikamy w naszą formę i zerkamy do okna Properties. Ustawiamy pole Name na frmSerwer, AutoSizeMode zmieniamy na GrowAndShrink - dzięki temu nie będzie można zmienić rozmiarów formy. Jeszcze tylko zmieniamy wartość MaximizeBox na false, żeby nie można było zmaksymalizować okna. W polu Text wpiszmy jeszcze Serwer.
Następnie tworzymy 3 kontrolki RichTextBox i nazywamy je kolejno txtOdbieranie, txtWysyłanie i txtLog. Dodatkowo dla txtOdbieranie i txtLog ustawiamy właściwość ReadOnly na true - nie będziemy tam nic pisać, a jedynie odczytywać. Tworzymy również 2 etykiety. Jedna ma pokazywać Nr portu, a druga Log. Potrzebujemy jeszcze dwóch przycisków i kontrolkę TextBox. Buttony nazywamy cmdSluchaj i cmdWyslij. Tekst w nich to odpowiednio Czekaj na połączenie i Wyślij. Dla cmdWyslij ustawiamy jeszcze Enabled na false, aby po starcie programu przycisk był nieaktywny. Textboxa nazywamy txtPort i wpisujemy do środka (właściwość Text) 8000 - z takiego portu będziemy domyślnie korzystali. Teraz wszystko odpowiednio układamy. Powinno to wyglądać mniej więcej tak (górne pole tekstowe to Odbieranie):

Teraz dodajmy do naszego projektu nowy plik: komunikaty.cs. Stworzymy w nim klasy KomunikatySerwera oraz KomunikatyKlienta, które będą zawierały stałe tekstowe, za pomocą których serwer i klient będą się porozumiewać. Oto treść pliku:

namespace komunikaty
{
    public class KomunikatySerwera
    {
        public const string OK = "###__OK";
        public const string Cancel = "###__Cancel";
        public const string Rozlacz = "###__Rozlacz";
    }

    public class KomunikatyKlienta
    {
        public const string Zadaj = "###__Zadaj";
        public const string Rozlacz = "###__Rozlacz";
    }
}

Zanim przejdziemy do dalszej pracy przejdźmy do widoku kodu naszej formy i dodajmy kilka using... , dzięki którym będziemy mogli korzystać z protokołu sieciowego. U góry dopiszmy:

using System.Net;
using System.Net.Sockets;
using System.IO;
using komunikaty;

Do klasy frmSerwer dodajmy następującą funkcję:

public void wyswietl(RichTextBox o, string tekst)
{
    o.Focus();
    o.AppendText(tekst);
    o.ScrollToCaret();
    txtWysylane.Focus();
}

Będzie ona dodawać tekst tekst do okna tekstowego o oraz zadba o autoscrool okna. Dodajmy jeszcze pola:

private TcpListener listener = null;
private TcpClient klient = null;
private bool czypolaczono = false;
private BinaryReader r = null;
private BinaryWriter w = null;

Będą one kolejno reprezentowały nasłuchiwanie połączenia, klienta (drugi komputer), czypolaczono chyba nie trzeba tłumaczyć ;). r i w posłużą do zapisywania i czytania danych ze strumienia sieciowego.

Teraz stwórzmy kontrolkę BackgroundWorker i nazwijmy ją Polaczenie. Klikamy w nią 2 razy (będzie widoczna u dołu ekranu), aby dodać obsługę zdarzenia DoWork. Będzie się ono uaktywniać, gdy uruchomimy kontrolkę za pomocą polecenia RunWorkerAsync(). Na początek wpiszmy tam:

wyswietl(txtLog, "Czekam na połaczenie\n" );
listener = new TcpListener(int.Parse(txtPort.Text));
listener.Start();
while ( !listener.Pending() )
{
    if (this.polaczenie.CancellationPending)
    {
        if (klient != null) klient.Close();
        listener.Stop();
        czypolaczono = false;
        cmdSluchaj.Text = "Czekaj na połączenie";
        return;
    }
}

Pętla przestanie się wykonywać, gdy klient zażąda połączenia lub użytkownik przerwie operację. Teraz dodajmy następujący kod:

klient = listener.AcceptTcpClient();
wyswietl(txtLog, "Zażądano połączenia\n" );
w = new BinaryWriter(stream);
r = new BinaryReader(stream);
if (r.ReadString() == KomunikatyKlienta.Zadaj)
{
    w.Write(KomunikatySerwera.OK);
    wyswietl(txtLog, "Połączono\n" );
    czypolaczono = true;
    cmdWyslij.Enabled = true;
    Odbieranie.RunWorkerAsync();
}
else
{
    wyswietl(txtLog, "Klient odrzucony\nRozlaczono\n" );
    if (klient != null) klient.Close();
    listener.Stop();
    czypolaczono = false;
}

Tutaj akceptujemy klienta i odczytujemy dane przez niego wysyłane. Jeśli jest to komunikat klienta Zadaj - akceptujemy połączenie i uruchamiamy kontrolkę Odbieranie (zaraz ją stworzymy), która również należy do klasy BackgroundWorker. Jeśli klient nadesłał inny komunikat - odrzucamy go i zamykamy połączenie.

Przejdźmy do kontrolki Odbieranie. Tworzymy ją i nadajemy odpowiednią nazwę, a następnie klikamy na niej 2 razy. We wnętrzu funkcji, która nam się ukaże wpisujemy:

string tekst;
while ((tekst = r.ReadString()) != KomunikatyKlienta.Rozlacz)
{
    wyswietl(txtOtrzymywane, "===== Rozmówca =====\n" + tekst + '\n' );
}
wyswietl(txtLog, "Rozlaczono\n" );
czypolaczono = false;
klient.Close();
listener.Stop();
cmdSluchaj.Text = "Czekaj na połączenie";

Odbieranie będzie zajmować się odbieraniem komunikatów od klienta oraz wyświetlaniem ich w odpowiednim oknie. Zakończy swoją pracę, gdy otrzyma komunikat Rozłącz. Zmieńmy jeszcze właściwość WorkerSupportsCancellation na true, aby można było przerwać pracę kontrolki. To samo zróbmy dla Polaczenie. Stwórzmy teraz obsługę zdarzenia naciśnięcia przycisku cmdWyslij klikając go dwa razy. Wewnątrz wpiszmy:

string tekst = txtWysylane.Text;
if (tekst == "") { txtWysylane.Focus(); return; }
if (tekst[tekst.Length - 1] == '\n')
    tekst = tekst.TrimEnd('\n');
w.Write(tekst);
wyswietl(txtOtrzymywane, "===== Ja =====\n" + tekst + '\n');
txtWysylane.Text = "";

Po naciśnięciu tego klawisza wpisany przez nas tekst zostanie przesłany przez sieć. Ponadto zostanie z niego usunięty ostatni znak '\n', aby nie było pustych miejsc gdy będziemy używać klawisza enter zamiast przycisku. Takiej możliwości jeszcze nie ma, ale zaraz ją dodamy. W tym celu tworzymy obsługę zdarzenia KeyPress dla pola tekstowego txtWysylane. Wewnątrz funkcji wpisujemy:

if ( cmdWyslij.Enabled && e.KeyChar == (char)13 ) cmdWyslij_Click(sender, e);

Jeśli przycisk do wysyłania jest aktywny i został wciśnięty enter, to zostanie wywołana funkcja naciśnięcia klawisza cmdWyslij. Następnie klikamy 2 razy w cmdSluchaj i wewnątrz funkcji obsługi zdarzenia wpisujemy:

if (cmdSluchaj.Text == "Czekaj na połączenie")
{
    Polaczenie.RunWorkerAsync();
    cmdSluchaj.Text = "Rozłącz";
}
else
{
    if (czypolaczono)
    {
        w.Write(KomunikatySerwera.Rozlacz);
        listener.Stop();
        if (klient != null) klient.Close();
        czypolaczono = false;
    }
    wyswietl(txtLog, "Rozlaczono\n");
    cmdSluchaj.Text = "Czekaj na połączenie";
    cmdWyslij.Enabled = false;
    Polaczenie.CancelAsync();
    Odbieranie.CancelAsync();
}

Jeśli na przycisku jest napisane "Czekaj na połączenie", to funkcja uruchomi kontrolkę Polaczenie, która rozpocznie nasłuch. Jeśli zaś napis na buttonie to "Rozłącz" - połączenie zostanie przerwane (o ile jakieś było). Anulowana zostanie również praca kontrolek Polaczenie i Odbieranie. Ponadto na przycisku zmieni się napis tak, abyśmy po naciśnięciu "Czekaj na połączenie" mieli możliwość przerwania operacji i rozłączenia się (napis "Rozłącz"), na podstawie tego właśnie napisu przycisk odróżnia, którą z operacji ma wykonać.

Na końcu dodajemy jeszcze zdarzenie FormClosed dla naszej formy i wpisujemy kod:

if (czypolaczono)
{
    w.Write(KomunikatySerwera.Rozlacz);
    listener.Stop();
    if (klient != null) klient.Close();
    czypolaczono = false;
}
Polaczenie.CancelAsync();
Odbieranie.CancelAsync();

Jego zadaniem jest wysłanie klientowi komunikatu, że się rozłączamy, a następnie zamknięcie połączenia. Kod ten będzie wywoływany przy zamykaniu programu (formy). No i serwer mamy gotowy zajmijmy się teraz klientem.

@STRONA@

Klient

Zaczniemy oczywiście od stworzenia nowego projektu, nadajemy mu nazwę Klient. Dodajemy nowy plik komunikaty.cs, którego zawartość ma być taka sama jak w serwerze. Zmieniamy nazwę pliku Form1.cs na Klient.cs. Formę nazywamy frmKlient, każemy jej wyświetlać (pole Text) Klient. Pozostałe właściwości ustawiamy tak samo jak w serwerze (również rozmiar). Następnie zaznaczamy wszystkie kontrolki wewnątrz formy w serwerze, kopiujemy je i wklejamy do klienta. Teraz musimy wprowadzić parę zmian. Nazwę przycisku cmdSluchaj zmieniamy na cmdPolacz i wyświetlamy w środku Połącz. Tworzymy dodatkową etykietę - Adres IP oraz pole TextBox - txtIP. Teraz trochę przestawiamy elementy, aby okno programu wyglądało mniej więcej tak:

Do naszej formy dodajemy te same pola co w serwerze, ale bez listener, czyli klient, czypolaczono, r, w oraz funkcję wyswietl. Na górze pliku dodajemy również te same using... co w serwerze. Kopiujemy jeszcze z serwera kontrolki Polaczenie i Odbieranie. Klikamy 2 razy na Polaczenie i wewnątrz obsługi zdarzenia DoWork wpisujemy:

klient = new TcpClient();
wyswietl(txtLog, "Próbuje się połączyć\n");
klient.Connect(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text));
wyswietl(txtLog, "Połączenie nawiązane\nŻądam zezwolenia\n");
NetworkStream stream = klient.GetStream();
w = new BinaryWriter(stream);
r = new BinaryReader(stream);
w.Write(KomunikatyKlienta.Zadaj);
if (r.ReadString() == KomunikatySerwera.OK)
{
    wyswietl(txtLog,"Połączono\n");
    czypolaczono = true;
    cmdWyslij.Enabled = true;
    Odbieranie.RunWorkerAsync();
}
else
{
    wyswietl(txtLog, "Brak odpowiedzi\nRozlaczono\n");
    czypolaczono = false;
    if (klient != null) klient.Close();
    cmdWyslij.Enabled = false;
    cmdPolacz.Text = "Połącz";
}

Funkcja ta próbuje się połączyć z serwerem o określonym IP i na określonym porcie. Jeśli to się uda, wysyła żądanie połączenia i jeśli otrzyma odpowiedź, potwierdza nawiązanie łączności i uruchamia Odbieranie. Zajmijmy się teraz odbieraniem. Klikamy 2 razy na odpowiednią kontrolkę i wpisujemy:

string tekst;
while ((tekst = r.ReadString()) != KomunikatySerwera.Rozlacz)
{
    wyswietl(txtOtrzymywane, "===== Rozmówca =====\n" + tekst + '\n');
}
cmdWyslij.Enabled = false;
wyswietl(txtLog, "Rozlaczono\n");
cmdPolacz.Text = "Połącz";
czypolaczono = false;
if (klient != null) klient.Close();

Funkcja ta zajmuje się dokładnie tym samym co jej odpowiednik w serwerze. Dodajemy teraz obsługę kliknięcia w cmdWyslij oraz zdarzenia KeyPress dla kontrolki txtWysylane. Będą one dokładnie takie same jak w serwerze, więc zawartość funkcji możemy skopiować. Pozostał nam jeszcze przycisk cmpPolacz. Standardowo podwójne kliknięcie, a następnie kod:

if (cmdPolacz.Text == "Połącz")
{
    Polaczenie.RunWorkerAsync();
    cmdPolacz.Text = "Rozłącz";
}
else
{
    if (czypolaczono)
    {
        w.Write(KomunikatyKlienta.Rozlacz);
        klient.Close();
        czypolaczono = false;
    }
    cmdPolacz.Text = "Połącz";
    cmdWyslij.Enabled = false;
    wyswietl(txtLog, "Rozlaczono\n");
}

Jeśli na przycisku będzie napisane Połącz - zostanie uruchomione Polaczenie. W przeciwnym wypadku (napis Rozłącz) - połączenie zostanie zerwane. Zostało już ostatnie zdarzenie - FormClosed dla naszej formy frmKlient. Tworzymy je i wpisujemy:

if (czypolaczono)
{
    w.Write(KomunikatyKlienta.Rozlacz);
    klient.Close();
    czypolaczono = false;
}
Polaczenie.CancelAsync();
Odbieranie.CancelAsync();

Kod ten dopilnowuje, aby po zamknięciu formy połączenie zostało przerwane. I oto mamy gotowy program. Możemy się oddać jego używaniu. Należy pamiętać, że po stronie serwera musi być odblokowany odpowiedni port w firewallu Windowsa (lub innym, jeśli takiego używamy).

Stworzony tutaj program wraz z plikami źródłowymi możemy pobrać stąd.

Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

Wydarzenia