程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 詳解C# 網絡編程系列:完成相似QQ的即時通訊順序

詳解C# 網絡編程系列:完成相似QQ的即時通訊順序

編輯:C#入門知識

詳解C# 網絡編程系列:完成相似QQ的即時通訊順序。本站提示廣大學習愛好者:(詳解C# 網絡編程系列:完成相似QQ的即時通訊順序)文章只能為提供參考,不一定能成為您想要的結果。以下是詳解C# 網絡編程系列:完成相似QQ的即時通訊順序正文


引言:

後面專題中引見了UDP、TCP和P2P編程,並且經過一些小的示例來讓大家更好的了解它們的任務原理以及怎樣.Net類庫去完成它們的。為了讓大家更好的了解我們往常中罕見的軟件QQ的任務原理,所以在本專題中將應用後面專題引見的知識來完成一個相似QQ的聊天順序。

 一、即時通訊零碎

在我們的生活中常常運用即時通訊的軟件,我們常常接觸到的有:QQ、阿裡旺旺、MSN等等。這些都是屬於即時通訊(Instant Messenger,IM)軟件,IM是指一切可以即時發送和接納互聯網音訊的軟件。

在後面專題P2P編程中引見過P2P零碎分兩品種型——單純型P2P和混合型P2P(QQ就是屬於混合型的使用),混合型P2P零碎中的服務器(也叫索引服務器)起到協調的作用。在文件共享類使用中,假如采用混合型P2P技術的話,索引服務器就保管著文件信息,這樣就能夠會形成版權的問題,但是在即時通訊類的軟件中, 由於客戶端傳遞的都是復雜的聊地理本而不是網絡媒體資源,這樣就不存在版權問題了,在這種狀況下,就可以采用混合型P2P技術來完成我們的即時通訊軟件。後面曾經講了,騰訊的QQ就是屬於混合型P2P的軟件。

因而本專題要完成一個相似QQ的聊天順序,其中用到的P2P技術是屬於混合型P2P,而不是前一專題中的采用的單純型P2P技術,同時本順序的完成也會用到TCP、UDP編程技術。

二、順序完成的詳細設計

本順序采用P2P方式,各個客戶端之間直接發音訊停止聊天,服務器在其中只是起到協調的作用,上面先理清下順序的流程:

2.1 順序流程設計

當一個新用戶經過客戶端登陸零碎後,從服務器獲取當在線的用戶信息列表,列表信息包括零碎中每個用戶的地址,然後用戶就可以獨自向其他發音訊。假如有用戶參加或許在線用戶加入時,服務器就會及時發音訊告訴零碎中的一切其他客戶端,到達它們即時地更新用戶信息列表。

依據下面大致的描繪,我們可以把零碎的流程分為上面幾步來更好的了解(大家可以參考QQ順序將會更好的了解本順序的流程):

1.用戶經過客戶端進入零碎,向服務器收回音訊,懇求登陸

2.服務器收到懇求後,向客戶端前往回應音訊,表示贊同承受該用戶參加,並把自己(指的是服務器)所在監聽的端口發送給客戶端

3.客戶端依據服務器發送過去的端口號和服務器樹立銜接

4.服務器經過該銜接 把在線用戶的列表信息發送給新參加的客戶端。

5.客戶端取得了在線用戶列表後就可以自己選擇在線用戶聊天。(順序中另外設計一個相似QQ的聊天窗口來停止聊天)

6.當用戶加入零碎時也要及時告訴服務器,服務器再把這個音訊轉發給每個在線的用戶,使客戶端及時更新本地的用戶信息列表。 

2.2 通訊協議設計

所謂協議就是商定,即服務器和客戶端之間會話信息的內容格式停止商定,使單方都可以辨認,到達更好的通訊。

上面就詳細引見下協議的設計:

1. 客戶端和服務器之間的對話

(1)登陸進程

① 客戶端用匿名UDP的方式向服務器收回上面的信息:

login, username, localIPEndPoint

 音訊內容包括三個字段,每個字段用 “,”聯系,login表示的是懇求登陸;username表示用戶名;localIPEndPint表示客戶端本地地址。

② 服務器收到後以匿名UDP前往上面的回應:

Accept, port

其中Accept表示服務器承受懇求,port表示服務器所在的端口號,服務器監聽著這個端口的客戶端銜接

③ 銜接服務器,獲取用戶列表

客戶端從上一步取得了端口號,然後向該端口發起TCP銜接,向服務器討取在線用戶列表,服務器承受銜接後將用戶列表傳輸到客戶端。用戶列表信息格式如下:

 username1,IPEndPoint1;username2,IPEndPoint2;...;end

username1、username2表示用戶名,IPEndPoint1,IPEndPoint2表示對應的端點,每個用戶信息都是由"用戶名+端點"組成,用戶信息以“;”隔開,整個用戶列表以“end”開頭。

(2)登記進程

用戶加入時,向服務器發送如下音訊:

logout,username,localIPEndPoint

這條音訊看字面意思大家都知道就是通知服務器 username+localIPEndPoint這個用戶要加入了。

2. 服務器管理用戶

(1)新用戶參加告訴

  由於零碎中在線的每個用戶都有一份以後在線用戶表,因而當有新用戶登錄時,服務器不需求反復地給零碎中的每個用戶再發送一切用戶信息,只需求將新參加用戶的信息告訴其他用戶,其他用戶再更新自己的用戶列表。

服務器向零碎中每個用戶播送如下信息:login,username,remoteIPEndPoint

在這個進程中服務器只是擔任將收到的"login"信息轉收回去。

(2)用戶加入

  與新用戶參加一樣,服務器將用戶加入的音訊停止播送轉發:logout,username,remoteIPEndPoint

3. 客戶端之間聊天

用戶停止聊地利,各自的客戶端之間是以P2P方式停止任務的,不與服務器有直接聯絡,這也是P2P技術的特點。

聊天發送的音訊格式如下:talk, longtime, selfUserName, message

其中,talk標明這是聊天內容的音訊;longtime是長時間格式的以後零碎時間;selfUserName為發送發的用戶名;message表示音訊的內容。

協議設計引見完後,上面就進入本順序的詳細完成的引見的。

注:協議是本順序的中心,也是一切軟件的中心,每個軟件產品的協議都是不一樣的,QQ有自己的一套協議,MSN又有另一套協議,所以運用的QQ的用戶無法和用MSN的冤家停止聊天。 

三、順序的完成

 服務器端中心代碼:

// 啟動服務器
    // 依據博客中協議的設計局部
    // 客戶端先向服務器發送登錄懇求,然後經過服務器前往的端口號
    // 再與服務器樹立銜接
    // 所以啟動服務按鈕事情中有兩個套接字:一個是接納客戶端信息套接字和
    // 監聽客戶端銜接套接字
    private void btnStart_Click(object sender, EventArgs e)
    {
      // 創立接納套接字
      serverIp = IPAddress.Parse(txbServerIP.Text);
      serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(txbServerport.Text));
      receiveUdpClient = new UdpClient(serverIPEndPoint);
      // 啟動接納線程
      Thread receiveThread = new Thread(ReceiveMessage);
      receiveThread.Start();
      btnStart.Enabled = false;
      btnStop.Enabled = true;

      // 隨機指定監聽端口
      Random random = new Random();
      tcpPort = random.Next(port + 1, 65536);

      // 創立監聽套接字
      tcpListener = new TcpListener(serverIp, tcpPort);
      tcpListener.Start();

      // 啟動監聽線程
      Thread listenThread = new Thread(ListenClientConnect);
      listenThread.Start();
      AddItemToListBox(string.Format("服務器線程{0}啟動,監聽端口{1}",serverIPEndPoint,tcpPort));
    }

    // 接納客戶端發來的信息
    private void ReceiveMessage()
    {
      IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
      while (true)
      {
        try
        {
          // 封閉receiveUdpClient時上面一行代碼會發生異常
          byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
          string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);

          // 顯示音訊內容
          AddItemToListBox(string.Format("{0}:{1}",remoteIPEndPoint,message));

          // 處置音訊數據
          // 依據協議的設計局部,從客戶端發送來的音訊是具有一定格式的
          // 服務器接納音訊後要抵消息做處置
          string[] splitstring = message.Split(',');
          // 解析用戶端地址
          string[] splitsubstring = splitstring[2].Split(':');
          IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitsubstring[0]), int.Parse(splitsubstring[1]));
          switch (splitstring[0])
          {
            // 假如是登錄信息,向客戶端發送應對音訊和播送有新用戶登錄音訊
            case "login":
              User user = new User(splitstring[1], clientIPEndPoint);
              // 往在線的用戶列表添加新成員
              userList.Add(user);
              AddItemToListBox(string.Format("用戶{0}({1})參加", user.GetName(), user.GetIPEndPoint()));
              string sendString = "Accept," + tcpPort.ToString();
              // 向客戶端發送應對音訊
              SendtoClient(user, sendString);
              AddItemToListBox(string.Format("向{0}({1})收回:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
              for (int i = 0; i < userList.Count; i++)
              {
                if (userList[i].GetName() != user.GetName())
                {
                  // 給在線的其他用戶發送播送音訊
                  // 告訴有新用戶參加
                  SendtoClient(userList[i], message);
                }
              }

              AddItemToListBox(string.Format("播送:[{0}]", message));
              break;
            case "logout":
              for (int i = 0; i < userList.Count; i++)
              {
                if (userList[i].GetName() == splitstring[1])
                {
                  AddItemToListBox(string.Format("用戶{0}({1})加入",userList[i].GetName(),userList[i].GetIPEndPoint()));
                  userList.RemoveAt(i); // 移除用戶
                }
              }
              for (int i = 0; i < userList.Count; i++)
              {
                // 播送登記音訊
                SendtoClient(userList[i], message);
              }
              AddItemToListBox(string.Format("播送:[{0}]", message));
              break;
          }
        }
        catch
        {
          // 發送異常加入循環
          break;
        }
      }
      AddItemToListBox(string.Format("服務線程{0}終止", serverIPEndPoint));
    }

    // 向客戶端發送音訊
    private void SendtoClient(User user, string message)
    {
      // 匿名方式發送
      sendUdpClient = new UdpClient(0);
      byte[] sendBytes = Encoding.Unicode.GetBytes(message);
      IPEndPoint remoteIPEndPoint =user.GetIPEndPoint();
      sendUdpClient.Send(sendBytes,sendBytes.Length,remoteIPEndPoint);
      sendUdpClient.Close();
    }
    
    // 承受客戶端的銜接
    private void ListenClientConnect()
    {
      TcpClient newClient = null;
      while (true)
      {
        try
        {
          newClient = tcpListener.AcceptTcpClient();
          AddItemToListBox(string.Format("承受客戶端{0}的TCP懇求",newClient.Client.RemoteEndPoint));
        }
        catch
        {
          AddItemToListBox(string.Format("監聽線程({0}:{1})", serverIp, tcpPort));
          break;
        }

        Thread sendThread = new Thread(SendData);
        sendThread.Start(newClient);
      }
    }

    // 向客戶端發送在線用戶列表信息
    // 服務器經過TCP銜接把在線用戶列表信息發送給客戶端
    private void SendData(object userClient)
    {
      TcpClient newUserClient = (TcpClient)userClient;
      userListstring = null;
      for (int i = 0; i < userList.Count; i++)
      {
        userListstring += userList[i].GetName() + ","
          + userList[i].GetIPEndPoint().ToString() + ";";
      }

      userListstring += "end";
      networkStream = newUserClient.GetStream();
      binaryWriter = new BinaryWriter(networkStream);
      binaryWriter.Write(userListstring);
      binaryWriter.Flush();
      AddItemToListBox(string.Format("向{0}發送[{1}]", newUserClient.Client.RemoteEndPoint, userListstring));
      binaryWriter.Close();
      newUserClient.Close();
    }

客戶端中心代碼:

// 登錄服務器
    private void btnlogin_Click(object sender, EventArgs e)
    {
      // 創立承受套接字
      IPAddress clientIP = IPAddress.Parse(txtLocalIP.Text);
      clientIPEndPoint = new IPEndPoint(clientIP, int.Parse(txtlocalport.Text));
      receiveUdpClient = new UdpClient(clientIPEndPoint);
      // 啟動接納線程
      Thread receiveThread = new Thread(ReceiveMessage);
      receiveThread.Start();

      // 匿名發送
      sendUdpClient = new UdpClient(0);
      // 啟動發送線程
      Thread sendThread = new Thread(SendMessage);
      sendThread.Start(string.Format("login,{0},{1}", txtusername.Text, clientIPEndPoint));

      btnlogin.Enabled = false;
      btnLogout.Enabled = true;
      this.Text = txtusername.Text;
    }

    // 客戶端承受服務器回應音訊 
    private void ReceiveMessage()
    {
      IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any,0);
      while (true)
      {
        try
        {
          // 封閉receiveUdpClient時會發生異常
          byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
          string message = Encoding.Unicode.GetString(receiveBytes,0,receiveBytes.Length);

          // 處置音訊
          string[] splitstring = message.Split(',');

          switch (splitstring[0])
          {
            case "Accept":
              try
              {
                tcpClient = new TcpClient();
                tcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitstring[1]));
                if (tcpClient != null)
                {
                  // 表示銜接成功
                  networkStream = tcpClient.GetStream();
                  binaryReader = new BinaryReader(networkStream);
                }
              }
              catch
              {
                MessageBox.Show("銜接失敗", "異常");
              }

              Thread getUserListThread = new Thread(GetUserList);
              getUserListThread.Start();
              break;
            case "login":
              string userItem = splitstring[1] + "," + splitstring[2];
              AddItemToListView(userItem);
              break;
            case "logout":
              RemoveItemFromListView(splitstring[1]);
              break;
            case "talk":
              for (int i = 0; i < chatFormList.Count; i++)
              {
                if (chatFormList[i].Text == splitstring[2])
                {
                  chatFormList[i].ShowTalkInfo(splitstring[2], splitstring[1], splitstring[3]);
                }
              }

              break;
          }
        }
        catch
        {
          break;
        }
      }
    }

    // 從服務器獲取在線用戶列表
    private void GetUserList()
    {
      while (true)
      {
        userListstring = null;
        try
        {
          userListstring = binaryReader.ReadString();
          if (userListstring.EndsWith("end"))
          {
            string[] splitstring = userListstring.Split(';');
            for (int i = 0; i < splitstring.Length - 1; i++)
            {
              AddItemToListView(splitstring[i]);
            }

            binaryReader.Close();
            tcpClient.Close();
            break;
          }
        }
        catch
        {
          break;
        }
      }
    }
  // 發送登錄懇求
    private void SendMessage(object obj)
    {
      string message = (string)obj;
      byte[] sendbytes = Encoding.Unicode.GetBytes(message);
      IPAddress remoteIp = IPAddress.Parse(txtserverIP.Text);
      IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(txtServerport.Text));
      sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
      sendUdpClient.Close();
    }

順序的運轉後果:

首先先運轉服務器窗口,在服務器窗口點擊“啟動”按鈕來啟動服務器,然後客戶端首先指定服務器的端口號,修正用戶名(這裡也可以不修正,運用默許的也可以),然後點擊“登錄”按鈕來登陸服務器(也就是通知服務器本地的客戶端地址),然後從服務器端取得在線用戶列表,界面演示如下:

然後用戶可以雙擊在線用戶停止聊天(此順序支持與多人停止聊天),上面是功用的演示圖片:

單方停止聊地利,這裡沒有完成像QQ一樣,有人發信息來在對應的客戶端就有音訊提示的功用的, 所以單方停止聊天的進程中,每個客戶端都需求在在線用戶列表中點擊聊天的對象來激活聊天對話框(意思就是從圖片中可以看出“天涯”客戶端想和劍癡聊天的話,就在“在線用戶”列表雙擊劍癡來激活聊天窗口,同時“劍癡”客戶端也必需雙擊“天涯”來激活聊天窗口,這樣單方就看到對方發來的信息了,(不激活窗口,也是發送了信息,只是沒有一個窗口來停止顯示)),而且從圖片中也可以看出——此順序支持與多人聊天,即天涯同時與“劍癡”和"大地"同時聊天。

本順序的源代碼鏈接:demo

四、總結

 本專題引見了如何去完成一個相似QQ的聊天順序,一方面讓大家可以穩固後面專題的內容,另一方面讓大家更好的了解即時通訊軟件(騰訊QQ)的任務原理和軟件協議的設計。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支持。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved