详解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)的工作原理和软件协议的设计。

原文链接:

详解C# 网络编程系列:实现类似QQ的即时通信程序

相关文章:

时间: 2024-10-11 00:18:50

详解C# 网络编程系列:实现类似QQ的即时通信程序的相关文章

[C# 网络编程系列]专题九:实现类似QQ的即时通信程序

转自:http://www.cnblogs.com/zhili/archive/2012/09/23/2666987.html 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net类库去实现它们的.为了让大家更好的理解我们平常中常见的软件QQ的工作原理,所以在本专题中将利用前面专题介绍的知识来实现一个类似QQ的聊天程序. 一.即时通信系统 在我们的生活中经常使用即时通信的软件,我们经常接触到的有:QQ.阿里旺旺.MSN等等.这些

详解Node.js API系列C/C++ Addons(3) 程序实例

http://blog.whattoc.com/2013/09/08/nodejs_api_addon_3/ 再续前文,前文介绍了node.js 的addon用法和google v8 引擎,下面,我们进入真正的编码,下面将会通过六个例子,学习node addon 范例,了解addon编程的特性 创建一个空项目 随机数模块 向模块传递参数 回调函数处理 线程处理 对象管理 创建一个空项目 vi modulename.cpp #include <node.h> void RegisterModul

NetBios 的结构体详解(网络控制块NCB)

对之前网络基础编程用到控制块NCB进行介绍(补充): 在Win32环境下,使用VC++6.0进行NetBIOS程序开发时, 需要用到nb30.h文件和netapi32.lib静态链接库.前者定义了NetBIOS的所有函数声明和常数定义,后者定义了NetBIOS应用. Ncb的结构在nb30.h文件中定义.Ncb结构的定义:Ncb有64个字符,分为14个域(或称为字段)和一个10字节的保留域,表2.1显示了Ncb和它的域. 域名及字节偏移量说明. 各个命令详细说明: 1. 命令 NetBIOS命令

[C# 网络编程系列]专题四:自定义Web浏览器

转自:http://www.cnblogs.com/zhili/archive/2012/08/24/WebBrowser.html 前言: 前一个专题介绍了自定义的Web服务器,然而向Web服务器发出请求的正是本专题要介绍的Web浏览器,本专题通过简单自定义一个Web浏览器来简单介绍浏览器的工作原理,以及帮助一些初学者揭开浏览器这层神秘的面纱(以前总感觉这些应用感觉很深奥的,没想到自己也可以自定义一个浏览器出来),下面不啰嗦了,进入正题. 一.Web浏览器的介绍 Web浏览器是指可以显示Web

Android网络编程系列 一 TCP/IP协议族

在学习和使用Android网路编程时,我们接触的仅仅是上层协议和接口如Apache的httpclient或者Android自带的httpURlconnection等等.对于这些接口的底层实现我们也有必要进一步的了解,这就要我们了解网络通信层了,提到网络通信层不得不说起ISO-OSI的七层协议经典架构,如图所示: 上图的左边部分就是osi架构模型了, ISO/OSI模型,即开放式通信系统互联参考模型(Open System Interconnection Reference Model),是国际标

C#网络编程系列(两)它Socket同步TCPserver

声明原文 笔者:竹zz  本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处 文章系列文件夹 C#网络编程系列文章(一)之Socket实现异步TCPserver C#网络编程系列文章(二)之Socket实现同步TCPserver C#网络编程系列文章(三)之TcpListener实现异步TCPserver C#网络编程系列文章(四)之TcpListener实现同步TCPserver C#网络编程系列文章(五)

完毕port(CompletionPort)具体解释 - 手把手教你玩转网络编程系列之三

手把手叫你玩转网络编程系列之三    完毕port(Completion Port)具体解释                                                              ----- By PiggyXP(小猪) 前 言 本系列里完毕port的代码在两年前就已经写好了,可是因为许久没有写东西了,不知该怎样提笔,所以这篇文档总是在酝酿之中--酝酿了两年之后,最终决定開始动笔了,但愿还不算晚-.. 这篇文档我很具体而且图文并茂的介绍了关于网络编程模型中完毕

TCP/IP网络编程系列之三

TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值,而是为了区分程序中创建的套接字而分配给套接字的序号. 网络地址 网络地址分为IPV4和IPV6,分别你别为4个字节地址簇和6个字节地址簇.IPV4标准的4个字节的地址分为网络地址和主机地址,且分为A.B.C.D.E 等类型.一般很少用到E类型.如下图所示:net-id指网络ID,host-id指主机

TCP/IP网络编程系列之二

套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,domain是套接字使用中的协议族(Protocol Family)信息.type套接字类型里面的数据传输类型信息.protocol计算机通信中使用的协议信息. 协议族(Protocol Family) 协议族类型有: PE_INET IPV4 PE_INET6 IPV6 PF_LOCAL 本地通信的UNI