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

发布时间 - 2026-01-10 22:07:46    点击率:

引言:

前面专题中介绍了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#实现tcp通信  # C#即时通信  # c#  # tcp实现p2p通信  # C# TcpClient网络编程传输文件的示例  # C# 网络编程之UDP  # c# 网络编程之tcp  # c# 网络编程之http  # 深入学习C#网络编程之HTTP应用编程(下)  # 深入学习C#网络编程之HTTP应用编程(上)  # 浅谈C#网络编程详解篇  # 总结C#网络编程中对于Cookie的设定要点  # C# Socket网络编程实例  # C#网络编程基础之进程和线程详解  # c# socket网络编程接收发送数据示例代码  # C#开发之Socket网络编程TCP/IP层次模型、端口及报文等探讨  # C#网络编程中常用特性介绍  # 客户端  # 用户列表  # 混合型  # 端口号  # 即时通信  # 自己的  # 双击  # 都是  # 工作原理  # 就可以  # 发消息  # 腾讯  # 发送给  # 可以看出  # 本专题  # 来实现  # 过程中  # 的是  # 而不是  # 这是 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Windows10如何更改计算机工作组_Win10系统属性修改Workgroup  php嵌入式断网后怎么恢复_php检测网络重连并恢复硬件控制【操作】  如何在阿里云ECS服务器部署织梦CMS网站?  如何在沈阳梯子盘古建站优化SEO排名与功能模块?  Java Adapter 适配器模式(类适配器,对象适配器)优缺点对比  ,怎么在广州志愿者网站注册?  千问怎样用提示词获取健康建议_千问健康类提示词注意事项【指南】  如何彻底卸载建站之星软件?  Laravel如何发送邮件_Laravel Mailables构建与发送邮件的简明教程  如何在阿里云部署织梦网站?  HTML 中如何正确使用模板变量为元素的 name 属性赋值  个人网站制作流程图片大全,个人网站如何注销?  Laravel如何使用withoutEvents方法临时禁用模型事件  微信h5制作网站有哪些,免费微信H5页面制作工具?  如何在Windows服务器上快速搭建网站?  昵图网官网入口 昵图网素材平台官方入口  Laravel如何使用Passport实现OAuth2?(完整配置步骤)  Laravel怎么实现前端Toast弹窗提示_Laravel Session闪存数据Flash传递给前端【方法】  Laravel怎么自定义错误页面_Laravel修改404和500页面模板  Laravel怎么实现支付功能_Laravel集成支付宝微信支付  Laravel如何获取当前登录用户信息_Laravel Auth门面使用与Session用户读取【技巧】  如何实现建站之星域名转发设置?  laravel怎么使用数据库工厂(Factory)生成带有关联模型的数据_laravel Factory生成关联数据方法  Laravel如何配置.env文件管理环境变量_Laravel环境变量使用与安全管理  三星网站视频制作教程下载,三星w23网页如何全屏?  Java解压缩zip - 解压缩多个文件或文件夹实例  网站视频制作书签怎么做,ie浏览器怎么将网站固定在书签工具栏?  Laravel 419 page expired怎么解决_Laravel CSRF令牌过期处理  Laravel Blade模板引擎语法_Laravel Blade布局继承用法  如何快速搭建个人网站并优化SEO?  头像制作网站在线观看,除了站酷,还有哪些比较好的设计网站?  Angular 表单中正确绑定输入值以确保提交与验证正常工作  Laravel怎么上传文件_Laravel图片上传及存储配置  详解jQuery停止动画——stop()方法的使用  Laravel如何集成微信支付SDK_Laravel使用yansongda-pay实现扫码支付【实战】  如何在阿里云通过域名搭建网站?  如何快速上传建站程序避免常见错误?  Laravel如何使用软删除(Soft Deletes)功能_Eloquent软删除与数据恢复方法  如何在服务器上配置二级域名建站?  Laravel如何使用Laravel Vite编译前端_Laravel10以上版本前端静态资源管理【教程】  Laravel的路由模型绑定怎么用_Laravel Route Model Binding简化控制器逻辑  如何在建站宝盒中设置产品搜索功能?  Laravel如何配置任务调度?(Cron Job示例)  佛山网站制作系统,佛山企业变更地址网上办理步骤?  如何用IIS7快速搭建并优化网站站点?  Laravel如何实现数据导出到CSV文件_Laravel原生流式输出大数据量CSV【方案】  js代码实现下拉菜单【推荐】  高端企业智能建站程序:SEO优化与响应式模板定制开发  如何在VPS电脑上快速搭建网站?  Laravel如何使用缓存系统提升性能_Laravel缓存驱动和应用优化方案