精华内容
下载资源
问答
  • 下面为即时通讯工具的有
    千次阅读
    2021-02-19 17:34:47

    即时通信工具

     

    1. 什么是即时通信

    即时通信是基于网络的一种新兴应用,它最基本的特征就是信息的即时传递和用户的交互性,并可将音、视频通信、文件传输及网络聊天等业务集成为一体,为人们开辟了一种新型的沟通途径。简单地讲,即时通信是一种使人们能在网络上方便快捷识别在线用户并与他们实时交换信息的技术,并逐渐成为继电子邮件之后最受欢迎的在线通讯和交流方式。与传统通信方式相比,即时通信具备快捷、廉价、隐秘性高的特点,在网络中可以跨年龄、身份、行业、地域的限制,达到人与人、人与信息之间的零距离交流。

    1. 即时通信的原理

    即时通信是一种基于网络 的通信技术, 涉及到IP/TCP/UDP/Sockets、P2P、C/S、多媒体音视频编解码/传送、Web Service等多种技术手段。无论即时通信系统的功能如何复杂,它们大都基于相同的技术原理,主要包括客户/服务器(C/S)通信模式和对等通信(P2P)模式。

    C/S结构以数据库服务为核心将连接在网络中的多个计算机形成一个有机的整体,客户机(Client)和服务器(Server)分别完成不同的功能。但在客户/服务器结构中,多个客户机并行操作,存在更新丢失和多用户控制问题。因此,在设计时要充分考虑信息处理的复杂程度来选择合适的结构。实际应用中,可以采用三层C/S结构,三层C/S结构与中间件模型非常相似,由基于工作站的客户层、基于服务器的中间层和基于主机的数据层组成。在三层结构中,客户不产生数据库查询命令,它访问服务器上的中间层,由中间层产生数据库查

    更多相关内容
  • 在以前,即时通讯软件被认为是一种专供个人使用的通信工具,随着社会的发展,各大商家企业开始慢慢接受这种通信工具并利用其协调公司内部沟通,从而满足公司的业务需求。那么即时通讯软件优势哪些?你细数它的七...

    在以前,即时通讯软件被认为是一种专供个人使用的通信工具,随着社会的发展,各大商家企业开始慢慢接受这种通信工具并利用其协调公司内部沟通,从而满足公司的业务需求。那么即时通讯软件优势有哪些?为你细数它的七大优势!

    即时消息软件不仅用于简化通信和快速响应,而且还用于文件共享和信息更新,它可帮助公司中的员工进行沟通、满足需求并实现目标。在即时通讯的帮助下,员工无需离开办公室就可以轻松地相互交流。

     

    下面为大家介绍即时通讯软件的七大优势:

    1、增强移动性和可访问性

    即时消息系统配备了移动功能,这样每个员工都可以与其他人进行交流,无论他们是否在办公室。即时通讯工具可以通过手机、电脑登陆,无论你身处办公室、室外或家里都可以使用。例如,公司出现突发问题,您可以通过即时消息向同事寻求帮助。

    2、提供实时通讯

    与使用电子邮件时您不会收到对方已阅读您的消息的通知不同,即时通讯软件允许用户之间进行实时通信。当您的联系人在线时,您可以比电子邮件更快地发送和接收消息,这对于需要快速响应的业务至关重要。

    3、省钱

    您可以使用即时通讯软件与任何客户或海外员工进行交流,而无需支付国际资费。您还可以在其他国家/地区雇用员工,并在国际上开展业务。

    4、保持记录

    即时通讯软件记录对话就像文本和电子邮件一样。当您需要时,随时都能查找到,每条对话记录都会保存下来,除了对话记录之外,图片还有文件同样会被保存下来,用户相当于多了一个小云盘。

    5、操作简单

    即时通讯软件可以随时访问,每个人都可以随时联系,就算用户之间不在同一地区办公也可以随时联系,即时通讯软件还支持视频会议功能,很好的解决异地开会难的问题,再也不必把公司的每个人都放在一个会议室里,使用即时通之后,一切都在你面前。即时通讯开发

    6、消息传递误差较小

    即时通讯软件传递消息的误差远远低于电话,与一次性接通多个电话相比,一次性处理多个窗口聊天明显更加简单,一次管理多个呼叫记忆可能会出现误差,但聊天窗口不会出现在这个问题,因此,即时通讯软件优势比电话通讯更加明显。让聊天窗口保持打开状态还可以提醒您忙完之后回复其他成员的消息。

    7、提高生产力

    即时消息有助于通过沟通提高员工的工作效率。它让员工之间更容易联系,无论成员身处哪里都可以随时办公,相当于有了一个移动的办公室。当成员需要任何帮助时,他们不需要打电话、发送电子邮件或留下语音消息,因为成员之间可以直接联系,他们不必一直等待回应。

    企业发展中面对的问题有很多,但是在成员沟通这个问题上,即时通讯软件给出了一个很好的解决方案,即时通是一个能够降低成本并激励员工的通信系统。因此,越来越多企业开始启用即时通,今天为大家介绍了即时通讯软件七大优势,需要可以帮助大家。

    展开全文
  • 即时通信聊天工具的原理与设计

    千次阅读 2017-12-08 16:16:58
    该软件采用P2P方式,各个客户端之间直接发消息进行会话聊天,服务器在其中只扮演协调者的角色(混合型P2P)。...每当新用户加入或在线用户退出时,服务器都会及时发消息通知系统中的所有其他用户,以便它们实时地

    转载:http://www.cnblogs.com/phquan/archive/2012/03/26/2417460.html

    该软件采用P2P方式,各个客户端之间直接发消息进行会话聊天,服务器在其中只扮演协调者的角色(混合型P2P)。

    1.会话流程设计

       当一个新用户通过自己的客户端登陆系统后,从服务器获取当前在线的用户信息列表,列表信息包括了系统中每个用户的地址。用户就可以开始独立工作,自主地向其他用户发送消息,而不经过服务器。每当有新用户加入或在线用户退出时,服务器都会及时发消息通知系统中的所有其他用户,以便它们实时地更新用户信息列表。
    
      按照上述思路,设计系统会话流程如下:
    
      (1)用户通过客户端进入系统,向服务器发出消息,请求登陆。
    
      (2)服务器收到请求后,向客户端返回应答消息,表示同意接受该用户加入,并顺带将自己服务线程所在的监听端口号告诉用户。
    
      (3)客户端按照服务器应答中给出的端口号与服务器建立稳定的连接。
    
      (4)服务器通过该连接将当前在线用户的列表信息传给新加入的客户端。
    
      (5)客户端获得了在线用户列表,就可以独立自主地与在线的其他用户通信了。
    
      (6)当用户退出系统时要及时地通知服务器。
    

    2.用户管理

    系统中,无论是服务器还是客户端都保存一份在线用户列表,客户端的用户表在一开始登陆时从服务器索取获得。在程序运行的过程中,服务器负责实时地将系统内用户的变动情况及时地通知在线的每个成员用户。
    
    新用户登录时,服务器将用户表传给他,同时向系统内每个成员广播“login”消息,各成员收到后更新自己的用户表。
    
    同样,在有用户退出系统时,服务器也会及时地将这一消息传给各个用户,当然这也就要求每个用户在自己想要退出之前,必须要先告诉服务器。
    

    3.协议设计

    3.1 客户端与服务器会话

    (1)登陆过程。
    
      客户端用匿名UDP向服务器发送消息:
    
      login,username,localIPEndPoint
    
      消息内容包括3个字段,各字段之间用“,”分隔:“login”表示请求登陆;“username”为用户名;“localIPEndPoint”是客户端本地地址。
    
      服务器收到后以匿名UDP返回如下消息:
    
      Accept,port
    
      其中,“Accept”表示服务器接受了请求;“port”是服务所在端口,服务线程在这个端口上监听可能的客户连接,该连接使用同步的TCP。
    
      连上服务器,获取用户列表:
    
      客户端从上一会话的“port”字段的值服务所在端口,于是向端口发起TCP连接,向服务器索取在线的用户列表,服务器接受连接后将用户列别传输给客户端。
    
      用户列表格式如下:
    
      username1,IPEndPoint1;username2,IPEndPoint2;.....;end
    
      username1,username2.....为用户名,IPEndPoint1,IPEndPoint2....为它们对应的端点。每个用户的信息都有个“用户名+端点”组成,用户信息之间以“;”隔开,整个用户列表以“end”结尾。
    

    3.2 服务器协调管理用户

    (1)新用户加入通知。
    
      由于系统中已存在的每个用户都有一份当前用户表,因此当有新成员加入时,服务器无需重复给系统中的每个成员再传送用户表,只要将新加入成员的信息告诉系统内的其他用户,再由他们各自更新自己的用户表就行了。
    
      服务器向系统内用户广播发送如下消息:
    
      端点字段写为“remoteIPEndPoint”,表示是远程某个用户终端登陆了,本地客户线程据此更新用户列表。其实,在这个过程中,服务器只是将受到的“login”消息简单地转发而已。
    
     (2)用户退出。
    
      与新成员加入时一样,服务器将用户退出的消息直接进行广播转发:
    
      logout,username,remoteIPEndPoint
    
      其中,“remoteIPEndPoint”为退出系统的远程用户终端的端点地址。
    

    3.3 用户终端之间聊天

      用户聊天时,他们各自的客户端之间是以P2P方式工作的,彼此地位对等,独立,不与服务器发生直接联系。
    
      聊天时发送的信息格式为:
    
      talk,longTime,selfUserName,message
    
      “talk”表明这是聊天内容;“longTime”是长时间格式的当前系统时间;“selfUserName”为自己的用户名;“message”是聊天的内容。
    

    4.系统实现

    4.1 服务线程

    系统运行后,先有服务器启动服务线程,只需单击“启动”按钮即可。
    
    “启动”按钮的事件过程:
    
    //点击开始事件处理函数
            private void buttonStart_Click(object sender, EventArgs e)
            {
                //创建接收套接字
                serverIp = IPAddress.Parse(textBoxServerIp.Text);
                serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
                receiveUdpClient = new UdpClient(serverIPEndPoint);
    
                //启动接收线程
                Thread threadReceive = new Thread(ReceiveMessage);
                threadReceive.Start();
                buttonStart.Enabled = false;
                buttonStop.Enabled = true;
    
                //随机指定监听端口 N( P+1 ≤ N < 65536 )
                Random random = new Random();
                tcport = random.Next(port + 1, 65536);
    
                //创建监听套接字
                myTcpListener = new TcpListener(serverIp, tcport);
                myTcpListener.Start();
    
                //启动监听线程
                Thread threadListen = new Thread(ListenClientConnect);
                threadListen.Start();
                AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
            }

    可以看到,服务器先后启动了两个线程:一个是接收线程threadReceive,它在一个实名UDP端口上,时刻准备着接收客户端发来的会话消息;另一个是监听线程threadListen,它在某个随机指定的端口上监听。

      服务器接收线程关联的ReceiveMessage()方法:
    
    //接收数据
            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])
                        {
                            //收到注册关键字"login"
                            case "login":
                                User user = new User(splitString[1], clientIPEndPoint);
                                userList.Add(user);
                                AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
                                string sendString = "Accept," + tcport.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;
    
                            //收到关键字"logout"
                            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));
            }

    接收线程执行该方法,进入while()循环,对每个收到的消息进行解析,根据消息头是“login”或“logout”转入相应的处理。

     监听线程对应ListenClientConnect()方法:
    
    //接受客户端连接
            private void ListenClientConnect()
            {
                TcpClient newClient = null;
                while (true)
                {
                    try
                    {
                        //获得用于传递数据的TCP套接口
                        newClient = myTcpListener.AcceptTcpClient();
                        AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
                    }
                    catch
                    {
                        AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
                        break;
                    }
    
                    //启动发送用户列表线程
                    Thread threadSend = new Thread(SendData);
                    threadSend.Start(newClient);
                }
            }

    当客户端请求到达后,与之建立TCP连接,然后创建一个新的线程threadSend,他通过执行SendData()方法传送用户列表。

      在服务器运行过程中,可随时通过点击“停止”按钮关闭服务线程。
    
      ”停止“按钮的事件过程:
    
    //当点击关闭按钮的事件处理程序
            private void buttonStop_Click(object sender, EventArgs e)
            {
                myTcpListener.Stop();
                receiveUdpClient.Close();
                buttonStart.Enabled = true;
                buttonStop.Enabled = false;
            }

    这里myTcpListener是TCP监听套接字,而receiveUdpClient是UDP套接字。当执行Stop()方法关闭监听套接字时,myTcpListener.AcceptTcpClient()会产生异常。
    这里写图片描述

    4.2 登陆/注销

    (1) 用户对象

     为了便于服务器对全体用户的管理,在服务器工程中添加自定义User类。代码如下:
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    //添加的命名空间引用
    using System.Net;
    
    namespace Server
    {
        //用户信息类      蒲泓全(18/3/2012)
        class User
        {
            private string userName;           //用户名
            private IPEndPoint userIPEndPoint;  //用户地址 
            public User(string name, IPEndPoint ipEndPoint)
            {
                userName = name;
                userIPEndPoint = ipEndPoint;
            }
            public string GetName()
            {
                return userName;
            }
            public IPEndPoint GetIPEndPoint()
            {
                return userIPEndPoint;
            }
        }
    }

    User类具有用户名和端点地址两个属性,这也正是用户列表中需要填写的信息项。
    (2) 用户登录功能

      当服务器的两个服务线程运行起来之后,各用户就可以通过客户端程序登录到系统了。用户在客户端上单击“登录”按钮后,客户端就向服务器发出“login”请求。
    
     “登录”按钮的事件过程为:
    
    private void buttonLogin_Click(object sender, EventArgs e)
            {
                //创建接收套接字
                IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
                clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
                receiveUdpClient = new UdpClient(clientIPEndPoint);
    
                //启动接收线程
                Thread threadReceive = new Thread(ReceiveMessage);
                threadReceive.Start();
                AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));
    
                //匿名发送
                sendUdpClient = new UdpClient(0);
    
                //启动发送线程
                Thread threadSend = new Thread(SendMessage);
                threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
                AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
                buttonLogin.Enabled = false;
                buttonLogout.Enabled = true;
                this.Text = textBoxUserName.Text;  //使当前窗体名字变为当前用户名
            }

    可以看到客户端在登录时也启动了两个线程,其中一个threadReceive是用实名UDP创建的接收线程,又称为客户线程,这是因为,它代表客户端程序处理与服务器的会话消息。另一个线程threadSend则是临时创建的,并以匿名UDP向服务器发出“login”消息。
    登陆请求发出之后,客户线程就循环执行ReceiveMessage()方法,以随时接受和处理服务器的应答消息。
    客户线程关联的ReceiveMessage()方法:

    //接收数据
            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(',');   //除去','
                        switch (splitString[0])
                        {
                            //若接收连接
                            case "Accept":
                                try
                                {
                                    AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
                                    myTcpClient = new TcpClient();
                                    myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
                                    if (myTcpClient != null)
                                    {
                                        AddItemToListBox("连接成功!");
                                        networkStream = myTcpClient.GetStream();
                                        br = new BinaryReader(networkStream);
                                    }
                                }
                                catch
                                {
                                    AddItemToListBox("连接失败!");
                                }
                                Thread threadGetList = new Thread(GetUserList);
                                threadGetList.Start(); //请求获得用户列表信息线程启动
                                break;
    
                                //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
                            case "login":
                                AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
                                string userItemInfo = splitString[1] + "," + splitString[2];
                                AddItemToListView(userItemInfo);
                                break;
    
                            //若收到注册关键字"logout",代表有用户退出,并更新用户列表
                            case "logout":
                                AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
                                RmvItemfromListView(splitString[1]);
                                break;
    
                            //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
                            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;
                    }
                }
                AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
            }

    如下图所示:为第一个用户登陆系统时,从状态监控屏幕上看到的客户端与服务器程序的会话过程。
    这里写图片描述
    (3) 用户注销

    当用用户需要下线退出时,单击客户端界面上的“注销”按钮。
    
    “注销”按钮的过程代码:
    
    //当点击退出按钮的事件处理函数
            private void buttonLogout_Click(object sender, EventArgs e)
            {
                //匿名发送
                sendUdpClient = new UdpClient(0);
    
                //启动发送线程
                Thread threadSend = new Thread(SendMessage);
                threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
                AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
                receiveUdpClient.Close();
                listViewOnline.Items.Clear();
                buttonLogin.Enabled = true;
                buttonLogout.Enabled = false;
                this.Text = "Client";       //恢复到原来的名字
            }
         注销操作很简单,凡要注销的用户只需向服务器发出“logou”消息,告知服务器就可以了,最好还要关闭客户端自身的UDP套接字。
    
     服务器在收到“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));
        服务程序在自己维护的User对象列表中删除这个用户,并且将这个消息广播给所有的用户。
    

    这里写图片描述

    图3 注销时双方的会话

    (3) 更新用户列表

     系统内的在线用户收到服务器发来的消息后,实时地更新自己的用户列表。当服务器发来“login”消息时,说明有新成员加入,客户端执行下面的代码:
    
      AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
      string userItemInfo = splitString[1] + "," + splitString[2];
      AddItemToListView(userItemInfo);
             break;

    若收到的是“logout”,则执行下面的代码:

    AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
    RmvItemfromListView(splitString[1]);
             break;

    为了是程序简单,客户端并没有使用特定的数据结构存储用户列表,而是直接将列表用ListView空间显示在界面上,并用委托机制定义了两个回调函数AddItemToListView()和RmvItemfromListView(),向空间中添加/删除用户信息。

    4.3 及时聊天

    带有聊天谈话内容的消息以“talk”为首部,采用点对点(P2P)方式发给对方。“talk”消息的发送,接收和显示都由专门的聊天子窗口负责,当客户端主程序收到“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;

    系统中的每个用户都对应一个聊天子窗体对象,上段代码的作用就是将一个“talk”消息定位到它的接受者的子窗体对象,再由该对象调用自身的ShwoTalkInfo()方法显示聊天内容。

    要打开对应某个用户的子窗口,只需双击在线用户列表中的该用户项即可,代码如下:
    
    //当点击两次发起回话的事件处理函数
            private void listViewOnline_DoubleClick(object sender, EventArgs e)
            {            
                string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
                if (peerName == textBoxUserName.Text)
                {
                    return;
                }
                string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
                string[] splitString = ipendp.Split(':');   //除去':'
                IPAddress peerIp = IPAddress.Parse(splitString[0]);
                IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
                ChatForm dlgChatForm = new ChatForm();
                dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
                dlgChatForm.Text = peerName;
                chatFormList.Add(dlgChatForm);
                dlgChatForm.Show();            
            }

    其中,chatFormList是客户端程序定义的数据结构,用于保存每一个在线用户的子窗口列表,它与服务器端的userList结构是相对应的。每当用户双击了列表中的某个用户项时,程序就用该项的信息创建一个新的子窗体对象并添加到chatFormList表中。子窗体的初始化使用其自身的SetUserInfo()方法。

    哈哈哈哈,现在整个通信聊天软件就完成了,我们看看下面运行的效果。
    

    5.运行效果
    同时运行一个服务器(Server)程序和三个客户端(Client)程序,启动服务线程,在三个客户端分别以用户名“泓全”,“爱田”,“爱盼”登陆服务器。如下图所示:
    这里写图片描述
    图4 登陆服务器

      此时,每个在线用户两两之间就都可以即时聊天了。不过,在聊天之前,聊天双方要先打开对方的子窗口,操作方法很简单,只要在客户端界面上双击“在线”列表中相应的用户项即可。如下图所示:
    

    这里写图片描述
    图5 在线交谈

    6.源代码
    6.1 服务器端

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    //添加的命名空间引用
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.IO;
    
    namespace Server
    {
        public partial class MainForm : Form
        {
            private List<User> userList = new List<User>(); //保存登录的所有用户
            int  port;                          //服务端口
            int tcport;                         //监听端口
            private UdpClient sendUdpClient;     //匿名发送套接口
            private UdpClient receiveUdpClient;  //实名接收套接口
            private IPEndPoint serverIPEndPoint; //服务器地址
            private TcpListener myTcpListener;   //服务器监听套接口
            private IPAddress serverIp;          //服务器IP
            private NetworkStream networkStream;  //网络流
            private BinaryWriter bw;             //避免出现网络边界问题的写入流
            string userListString;               //用户列表串
            public MainForm()
            {
                InitializeComponent();
    
                //服务器IP
                IPAddress[] ServerIP = Dns.GetHostAddresses("");
                IPAddress address = IPAddress.Any;
                for (int i = 0; i < ServerIP.Length; i++ )
                {
                    if (ServerIP[i].AddressFamily == AddressFamily.InterNetwork)
                    {
                        address = ServerIP[i];
                        break;
                    }
                }
                textBoxServerIp.Text = address.ToString();
    
                //随机选择服务端口 Port( Port > 1024 )
                port = new Random().Next(1024, 65535);
                textBoxServerPort.Text = port.ToString();
                buttonStop.Enabled = false;
            }
    
            //点击开始事件处理函数
            private void buttonStart_Click(object sender, EventArgs e)
            {
                //创建接收套接字
                serverIp = IPAddress.Parse(textBoxServerIp.Text);
                serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
                receiveUdpClient = new UdpClient(serverIPEndPoint);
    
                //启动接收线程
                Thread threadReceive = new Thread(ReceiveMessage);
                threadReceive.Start();
                buttonStart.Enabled = false;
                buttonStop.Enabled = true;
    
                //随机指定监听端口 N( P+1 ≤ N < 65536 )
                Random random = new Random();
                tcport = random.Next(port + 1, 65536);
    
                //创建监听套接字
                myTcpListener = new TcpListener(serverIp, tcport);
                myTcpListener.Start();
    
                //启动监听线程
                Thread threadListen = new Thread(ListenClientConnect);
                threadListen.Start();
                AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
            }
    
    
            //接受客户端连接
            private void ListenClientConnect()
            {
                TcpClient newClient = null;
                while (true)
                {
                    try
                    {
                        //获得用于传递数据的TCP套接口
                        newClient = myTcpListener.AcceptTcpClient();
                        AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
                    }
                    catch
                    {
                        AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
                        break;
                    }
    
                    //启动发送用户列表线程
                    Thread threadSend = new Thread(SendData);
                    threadSend.Start(newClient);
                }
            }
    
            //向客户端发送在线用户列表信息
            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();
                bw = new BinaryWriter(networkStream);
                bw.Write(userListString);
                bw.Flush();      //不保留现在写入的数据
                AddItemToListBox(string.Format("向{0}传送:[{1}]", newUserClient.Client.RemoteEndPoint, userListString));
                bw.Close();
                newUserClient.Close();
            }
    
            //接收数据
            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])
                        {
                            //收到注册关键字"login"
                            case "login":
                                User user = new User(splitString[1], clientIPEndPoint);
                                userList.Add(user);
                                AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
                                string sendString = "Accept," + tcport.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;
    
                            //收到关键字"logout"
                            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 buttonStop_Click(object sender, EventArgs e)
            {
                myTcpListener.Stop();
                receiveUdpClient.Close();
                buttonStart.Enabled = true;
                buttonStop.Enabled = false;
            }
    
            //用委托机制解决显示问题
            private delegate void AddItemToListBoxDelegate(string str);
            private void AddItemToListBox(string str)
            {
                if (listBoxStatus.InvokeRequired)
                {
                    AddItemToListBoxDelegate d = AddItemToListBox;
                    listBoxStatus.Invoke(d, str);
                }
                else
                {
                    listBoxStatus.Items.Add(str);
                    listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
                    listBoxStatus.ClearSelected();
                }
            }
        }
    }

    6.2 客户端

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    //添加的命名空间引用
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.IO;
    
    namespace Client
    {
        public partial class MainForm : Form
        {
            int  port;                           //端口号                 
            private UdpClient sendUdpClient;       //匿名发送套接口
            private UdpClient receiveUdpClient;    //实名接收套接口
            private IPEndPoint clientIPEndPoint;   //客户端地址
            private TcpClient myTcpClient;        //TCP套接字
            private NetworkStream networkStream;  //网络流
            private BinaryReader br;             //避免网络边界问题的读数据流
            string userListString;               //用户名字串
            private List<ChatForm> chatFormList = new List<ChatForm>();   //用户窗体列表
            public MainForm()
            {
                InitializeComponent();
    
                //本地IP和端口号的初始化
                IPAddress[] LocalIP = Dns.GetHostAddresses("");
                IPAddress address = IPAddress.Any;
                for (int i = 0; i < LocalIP.Length; i++)
                {
                    if (LocalIP[i].AddressFamily == AddressFamily.InterNetwork)
                    {
                        address = LocalIP[i];
                        break;
                    }
                }
                textBoxServerIp.Text = address.ToString();
                textBoxLocalIp.Text = address.ToString();
    
                //获得随机端口号
                port = new Random().Next(1024, 65535);
                textBoxLocalPort.Text = port.ToString();
    
                //随机生成用户名
                Random r = new Random((int)DateTime.Now.Ticks);   //类似于与C++中的种子
                textBoxUserName.Text = "user" + r.Next(100, 999);
                buttonLogout.Enabled = false;
            }
    
            private void buttonLogin_Click(object sender, EventArgs e)
            {
                //创建接收套接字
                IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
                clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
                receiveUdpClient = new UdpClient(clientIPEndPoint);
    
                //启动接收线程
                Thread threadReceive = new Thread(ReceiveMessage);
                threadReceive.Start();
                AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));
    
                //匿名发送
                sendUdpClient = new UdpClient(0);
    
                //启动发送线程
                Thread threadSend = new Thread(SendMessage);
                threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
                AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
                buttonLogin.Enabled = false;
                buttonLogout.Enabled = true;
                this.Text = textBoxUserName.Text;  //使当前窗体名字变为当前用户名
            }
    
            //发送数据
            private void SendMessage(object obj)
            {
                string message = (string)obj;
                byte[] sendbytes = Encoding.Unicode.GetBytes(message);
    
                //服务器端的IP和端口号
                IPAddress remoteIp = IPAddress.Parse(textBoxServerIp.Text);
                IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(textBoxServerPort.Text));
                sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);  //匿名发送
                sendUdpClient.Close();
            }
    
            //接收数据
            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(',');   //除去','
                        switch (splitString[0])
                        {
                            //若接收连接
                            case "Accept":
                                try
                                {
                                    AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
                                    myTcpClient = new TcpClient();
                                    myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
                                    if (myTcpClient != null)
                                    {
                                        AddItemToListBox("连接成功!");
                                        networkStream = myTcpClient.GetStream();
                                        br = new BinaryReader(networkStream);
                                    }
                                }
                                catch
                                {
                                    AddItemToListBox("连接失败!");
                                }
                                Thread threadGetList = new Thread(GetUserList);
                                threadGetList.Start(); //请求获得用户列表信息线程启动
                                break;
    
                                //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
                            case "login":
                                AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
                                string userItemInfo = splitString[1] + "," + splitString[2];
                                AddItemToListView(userItemInfo);
                                break;
    
                            //若收到注册关键字"logout",代表有用户退出,并更新用户列表
                            case "logout":
                                AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
                                RmvItemfromListView(splitString[1]);
                                break;
    
                            //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
                            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;
                    }
                }
                AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
            }
    
            //获得用户列表
            private void GetUserList()
            {
                while (true)
                {
                    userListString = null;
                    try
                    {
                        userListString = br.ReadString();
                        if (userListString.EndsWith("end"))
                        {
                            AddItemToListBox(string.Format("收到:[{0}]", userListString));
                            string[] splitString = userListString.Split(';');
                            for (int i = 0; i < splitString.Length - 1; i++)
                            {
                                AddItemToListView(splitString[i]);
                            }
                            br.Close();
                            myTcpClient.Close();
                            break;
                        }
                    }
                    catch
                    {
                        break;
                    }
                }
            }
    
            //当点击退出按钮的事件处理函数
            private void buttonLogout_Click(object sender, EventArgs e)
            {
                //匿名发送
                sendUdpClient = new UdpClient(0);
    
                //启动发送线程
                Thread threadSend = new Thread(SendMessage);
                threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
                AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
                receiveUdpClient.Close();
                listViewOnline.Items.Clear();
                buttonLogin.Enabled = true;
                buttonLogout.Enabled = false;
                this.Text = "Client";       //恢复到原来的名字
            }
    
            //当点击两次发起回话的事件处理函数
            private void listViewOnline_DoubleClick(object sender, EventArgs e)
            {            
                string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
                if (peerName == textBoxUserName.Text)
                {
                    return;
                }
                string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
                string[] splitString = ipendp.Split(':');   //除去':'
                IPAddress peerIp = IPAddress.Parse(splitString[0]);
                IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
                ChatForm dlgChatForm = new ChatForm();
                dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
                dlgChatForm.Text = peerName;
                chatFormList.Add(dlgChatForm);
                dlgChatForm.Show();            
            }
    
            //利用委托机制显示信息
            private delegate void AddItemToListBoxDelegate(string str);
            private void AddItemToListBox(string str)
            {
                if (listBoxStatus.InvokeRequired)
                {
                    AddItemToListBoxDelegate d = AddItemToListBox;
                    listBoxStatus.Invoke(d, str);
                }
                else
                {
                    listBoxStatus.Items.Add(str);
                    listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
                    listBoxStatus.ClearSelected();
                }
            }
    
            private delegate void AddItemToListViewDelegate(string str);
            private void AddItemToListView(string str)
            {
                if (listViewOnline.InvokeRequired)
                {
                    AddItemToListViewDelegate d = AddItemToListView;
                    listViewOnline.Invoke(d, str);
                }
                else
                {
                    string[] splitString = str.Split(',');
                    ListViewItem item = new ListViewItem();
                    item.SubItems.Add(splitString[0]);
                    item.SubItems.Add(splitString[1]);
                    listViewOnline.Items.Add(item);
                }
            }
    
            private delegate void RmvItemfromListViewDelegate(string str);
            private void RmvItemfromListView(string str)
            {
                if (listViewOnline.InvokeRequired)
                {
                    RmvItemfromListViewDelegate d = RmvItemfromListView;
                    listViewOnline.Invoke(d, str);
                }
                else
                {
                    for (int i = 0; i < listViewOnline.Items.Count; i++)
                    {
                        if (listViewOnline.Items[i].SubItems[1].Text == str)
                        {
                            listViewOnline.Items[i].Remove();
                        }
                    }
                }
            }
        }
    }

    6.3 聊天子窗口的代码

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    //添加的命名空间引用
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    
    namespace Client
    {
        public partial class ChatForm : Form
        {
            private string selfUserName;          //自己的用户名
            private string peerUserName;          //对方的用户名
            private IPEndPoint peerUserIPEndPoint; //对方的地址
            private UdpClient sendUdpClient;      //匿名发送套接口
            public ChatForm()
            {
                InitializeComponent();
            }
    
            //类似于构造函数
            public void SetUserInfo(string selfName,string peerName,IPEndPoint peerIPEndPoint)
            {            
                selfUserName = selfName;
                peerUserName = peerName;
                peerUserIPEndPoint = peerIPEndPoint;
            }
    
            //点击发送按钮的事件处理程序
            private void buttonSend_Click(object sender, EventArgs e)
            {
                //匿名发送
                sendUdpClient = new UdpClient(0);
    
                //启动发送线程
                Thread threadSend = new Thread(SendMessage);
                threadSend.Start(string.Format("talk,{0},{1},{2}", DateTime.Now.ToLongTimeString(), selfUserName, textBoxSend.Text));            
                richTextBoxTalkInfo.AppendText(selfUserName + "" + DateTime.Now.ToLongTimeString() + Environment.NewLine + textBoxSend.Text);
                richTextBoxTalkInfo.AppendText(Environment.NewLine);
                richTextBoxTalkInfo.ScrollToCaret();            
                textBoxSend.Text = "";
                textBoxSend.Focus();
            }
    
            //数据发送函数
            private void SendMessage(object obj)
            {
                string message = (string)obj;
                byte[] sendbytes = Encoding.Unicode.GetBytes(message);
                sendUdpClient.Send(sendbytes, sendbytes.Length, peerUserIPEndPoint);
                sendUdpClient.Close();
            }
    
            //显示通话内容
            public void ShowTalkInfo(string peerName, string time, string content)
            {
                richTextBoxTalkInfo.AppendText(peerName + "" + time + Environment.NewLine + content);
                richTextBoxTalkInfo.AppendText(Environment.NewLine);
                richTextBoxTalkInfo.ScrollToCaret();
            }
    
            //当点击关闭时的事件处理程序
            private void buttonClose_Click(object sender, EventArgs e)
            {
                this.Close();
            }
        }
    }
    展开全文
  • (尾部下载完整版PHP即时通讯源码)借助 你可以实现以下需求:本次主要涉及到 IM 即时通讯,所以特地截了很多演示图。 下面来看看具体的架构设计。整体主要由以下模块组成: 服务端;用于接收 连接、消息透传、消息...

    前言

    FEIIM一款面向开发者的 IM(即时通讯)系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。尾部下载完整版PHP即时通讯源码)

    借助 FEIIM 你可以实现以下需求:

    • IM 即时通讯系统。
    • 适用于 APP 的消息推送中间件。
    • IOT 海量连接场景中的消息透传中间件。

    演示

    本次主要涉及到 IM 即时通讯,所以特地截了很多演示图。

     

     

     

    架构设计

    下面来看看具体的架构设计。

    • FEIIM 中的各个组件均采用 SpringBoot 构建。
    • 采用 Netty + Google Protocol Buffer 构建底层通信。
    • Redis 存放各个客户端的路由信息、账号信息、在线状态等。
    • Zookeeper 用于 IM-server 服务的注册与发现。

    整体主要由以下模块组成:

    cim-server

    IM 服务端;用于接收 client 连接、消息透传、消息推送等功能。

    支持集群部署。

    cim-forward-route

    消息路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运营工具(获取在线用户数等)。

    cim-client

    IM 客户端;给用户使用的消息终端,一个命令即可启动并向其他人发起通讯(群聊、私聊);同时内置了一些常用命令方便使用。

    流程图

    整体的流程也比较简单,流程图如下:

    • 客户端向 route 发起登录。
    • 登录成功从 Zookeeper 中选择可用 IM-server 返回给客户端,并保存登录、路由信息到 Redis
    • 客户端向 IM-server 发起长连接,成功后保持心跳。
    • 客户端下线时通过 route 清除状态信息。

    所以当我们自己部署时需要以下步骤:

    • 搭建基础中间件 Redis、Zookeeper
    • 部署 cim-server,这是真正的 IM 服务器,为了满足性能需求所以支持水平扩展,只需要注册到同一个 Zookeeper 即可。
    • 部署 cim-forward-route,这是路由服务器,所有的消息都需要经过它。由于它是无状态的,所以也可以利用 Nginx 代理提高可用性。
    • cim-client 真正面向用户的客户端;启动之后会自动连接 IM 服务器便可以在控制台收发消息了。

    更多使用介绍可以参考快速启动

    详细设计

    接下来重点看看具体的实现,比如群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。

    IM 服务端

    先来看看服务端;主要是实现客户端上下线、消息下发等功能。

    首先是服务启动:

    由于是在 SpringBoot 中搭建的,所以在应用启动时需要启动 Netty 服务。

    从 pipline 中可以看出使用了 Protobuf 的编解码(具体报文在客户端中分析)。

    注册发现

    需要满足 IM 服务端的水平扩展需求,所以 cim-server 是需要将自身数据发布到注册中心的。

    这里参考之前分享的《搞定服务注册与发现》有具体介绍。

    所以在应用启动成功后需要将自身数据注册到 Zookeeper 中。

    最主要的目的就是将当前应用的 ip + cim-server-port+ http-port 注册上去。

    上图是我在演示环境中注册的两个 cim-server 实例(由于在一台服务器,所以只是端口不同)。

    这样在客户端(监听这个 Zookeeper 节点)就能实时的知道目前可用的服务信息。

    登录

    当客户端请求 cim-forward-route 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接,如之前的流程所示:

    这时客户端会发送一个特殊报文,表明当前是登录信息。

    服务端收到后就需要将该客户端的 userID 和当前 Channel 通道关系保存起来。

    同时也缓存了用户的信息,也就是 userID 和 用户名。

    离线

    当客户端断线后也需要将刚才缓存的信息清除掉。

    同时也需要调用 route 接口清除相关信息(具体接口看下文)。

    IM 路由

    从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 HTTP 服务承接了客户端和服务端。

    目前主要是以下几个接口。

    注册接口

    由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。

    这里就设计的比较简单,直接利用 Redis 来存储用户信息;用户信息也只有 ID 和 userName 而已。

    只是为了方便查询在 Redis 中的 KV 又反过来存储了一份 VK,这样 ID 和 userName 都必须唯一。

    登录接口

    这里的登录和 cim-server 中的登录不一样,具有业务性质,

    • 登录成功之后需要判断是否是重复登录(一个用户只能运行一个客户端)。
    • 登录成功后需要从 Zookeeper 中获取服务列表(cim-server)并根据某种算法选择一台服务返回给客户端。
    • 登录成功之后还需要保存路由信息,也就是当前用户分配的服务实例保存到 Redis 中。

    为了实现只能一个用户登录,使用了 Redis 中的 set 来保存登录信息;利用 userID 作为 key ,重复的登录就会写入失败。

    类似于 Java 中的 HashSet,只能去重保存。

    获取一台可用的路由实例也比较简单:

    • 先从 Zookeeper 获取所有的服务实例做一个内部缓存。
    • 轮询选择一台服务器(目前只有这一种算法,后续会新增)。

    当然要获取 Zookeeper 中的服务实例前自然是需要监听 cim-server 之前注册上去的那个节点。

    具体代码如下:


     

    也是在应用启动之后监听 Zookeeper 中的路由节点,一旦发生变化就会更新内部缓存。

    这里使用的是 Guava 的 cache,它基于 ConcurrentHashMap,所以可以保证清除、新增缓存的原子性。

    群聊接口

    这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其余所有客户端都能收到!

    流程肯定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 SessionSocketHolder 中遍历所有 Channel(通道)然后下发消息即可。

    服务端是单机倒也可以,但现在是集群设计。所以所有的客户端会根据之前的轮询算法分配到不同的 cim-server 实例中。

    因此就需要路由层来发挥作用了。

    路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。

    路由关系在 Redis 中的存放如下:

    由于 Redis 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 cim-route:* 数据,会导致 Redis 不能处理其他请求。

    所以这里改为使用 scan 命令来遍历所有的 cim-route:*


    接着会挨个调用每个客户端所在的服务端的 HTTP 接口用于推送消息。

    在 cim-server 中的实现如下:

    cim-server 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。

    在线用户接口

    这是一个辅助接口,可以查询出当前在线用户信息。

    实现也很简单,也就是查询之前保存 ”用户登录状态的那个去重 set “即可。

    私聊接口

    之所以说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。

    一般我们使用私聊的前提肯定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。

    类似于这样:

    在我们这个场景中,私聊的前提就是需要获得在线用户的 userID

    所以私聊接口在收到消息后需要查询到接收者所在的 cim-server 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 HTTP 接口下发信息。

    只是群聊是遍历所有的在线用户,私聊只发送一个的区别。

    下线接口

    一旦客户端下线,我们就需要将之前存放在 Redis 中的一些信息删除掉(路由信息、登录状态)。

    IM 客户端

    客户端中的一些逻辑其实在上文已经谈到一些了。

    登录

    第一步也就是登录,需要在启动时调用 route 的登录接口,获得 cim-server 信息再创建连接。

    登录过程中 route 接口会判断是否为重复登录,重复登录则会直接退出程序。

    接下来是利用 route 接口返回的 cim-server 实例信息(ip+port)创建连接。

    最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 Channel 的关系。

    自定义协议

    上文提到的一些登录报文、真正的消息报文这些其实都是在我们自定义协议中可以区别出来的。

    由于是使用 Google Protocol Buffer 编解码,所以先看看原始格式。

    其实这个协议中目前一共就三个字段:

    • requestId 可以理解为 userId
    • reqMsg 就是真正的消息。
    • type 也就是上文提到的消息类别。

    目前主要是三种类型,分别对应不同的业务:

    心跳

    为了保持客户端和服务端的连接,每隔一段时间没有发送消息都需要自动的发送心跳。

    目前的策略是每隔一分钟就是发送一个心跳包到服务端:

    这样服务端每隔一分钟没有收到业务消息时就会收到 ping 的心跳包:

    内置命令

    客户端也内置了一些基本命令来方便使用。

    命令描述
    :q退出客户端
    :olu获取所有在线用户信息
    :all获取所有命令
    :更多命令正在开发中。。

    比如输入 :q 就会退出客户端,同时会关闭一些系统资源。

    当输入 :olu(onlineUser 的简写)就会去调用 route 的获取所有在线用户接口。

    群聊

    群聊的使用非常简单,只需要在控制台输入消息回车即可。

    这时会去调用 route 的群聊接口。

    私聊

    私聊也是同理,但前提是需要触发关键字;使用 userId;;消息内容 这样的格式才会给某个用户发送消息,所以一般都需要先使用 :olu 命令获取所以在线用户才方便使用。

    消息回调

    为了满足一些定制需求,比如消息需要保存之类的。

    所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。

    因此先创建了一个 caller 的 bean,这个 bean 中包含了一个 CustomMsgHandleListener 接口,需要自行处理只需要实现此接口即可。

    自定义界面

    由于我自己不怎么会写界面,但保不准有其他大牛会写。所以客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及之后的业务)都是以接口形式提供。

    也方便后面做页面集成,只需要调这些接口就行了;具体实现不用怎么关心。

    源码打包

    源码以本文演示图片为准。

    下载地址:https://yfi.lanzouj.com/iMJVn06qfq8h

    展开全文
  • Android 即时通讯

    万次阅读 2017-07-25 15:07:47
    那么下面我就来给大家介绍一下我是怎么实现即时通讯的。 首先我们要明白这篇文章是为了解决哪些问题,提出问题如下: 1. 什么是即时通讯? 2. 怎样实现即时通讯? 3. 即时通讯有什么作用?什么是即时通讯?在...
  • 即时通讯基础

    千次阅读 2017-03-16 16:49:36
    即时通讯系列阅读 即时通讯基础 即时通讯:XMPP基础 即时通讯:XMPP项目实践-微聊 1. 即时通讯简介即时通讯(Instant Messaging)是目前Internet 上最为流行的通讯方式,各种各样的即时通讯软件也层出不穷;服务提供...
  • (gaim-Pidgin,eva,kopete)-QQ-Yahoo-MSN-AIM-Gadu-GroupWise-ICQ-IRC gaim,eva,kopete是一个即时消息工具平台,象一个容器一样,把msn、icq、Yahoo通等聊天工具以插件形式的装载起来,用户根据自己的需要来选择应用...
  • 新生代产品即时通讯软件可以解决传统通讯方式的痛点,它可以实现即时消息对话,还可以进行文件传输、音视频通话等,企业提供更高效的通讯平台。一般的即时通讯软件都是通过互联网进行消息传输,但特定行业不想通过...
  • 局域网即时通讯软件怎么部署

    千次阅读 2022-03-21 14:37:31
    新生代产品即时通讯软件可以解决传统通讯方式的痛点,它可以实现即时消息对话,还可以进行文件传输、音视频通话等,企业提供更高效的通讯平台。一般的即时通讯软件都是通过互联网进行消息传输,但特定行业不想通过...
  • 即时通讯项目(一)

    千次阅读 2022-02-18 23:31:42
    即时通讯项目,基于开源项目TeamTalk二次开发。整个系统的架构设计如下: TeamTalk目前的开源版本较多的bug,功能也不够完善,始于TeamTalk但不止于TeamTalk,将会持续迭代改进该项目,最终开发成可以商业使
  • 写在最前面现在很多软件都要求加入即时通信的功能,当然很多都用了三方(环信、融信。。。)。最近,项目也此需求,我们选择的是环信。环信也提供了UI框架,但是说实在的一般的应用用不了那么多功能,可能就简单的...
  • 在我编写的文章中,基本都是以实践代码验证结果核心来讲述文章内容。可能人不知道 Netty 是什么,这里简单介绍下: Netty 是一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速...
  • 即时通信是一种以“同步通信”方式主进行消息交换的通信服务。即时通信是指能够即时发送和接收互联网消息等的业务,它允许两人或多人使用网路即时...即时通讯(Instant Messaging,简称IM)是指能够即时发送和接收互...
  • 一、是什么? Openfire 采用Java开发...如果你想轻易地构建高效率的即时通信服务器,那就选择它吧! 二、做什么? 我们要了解Openfire,首先要了解XMPP协议,因为Openfire是用Java语言编写的,基于XMPP协议、开源...
  • 即时通信系统IM

    千次阅读 2019-06-21 10:59:52
    即时通讯(Instant Messaging)是目前Internet上最为流行的通讯方式,各种各样的即时通讯软件也层出不穷;服务提供商也提供了越来越丰富的通讯服务功能。 不容置疑,Internet已经成为真正的信息高速公路。从实际工程...
  • 即时通信工具

    2011-10-29 12:12:42
    腾讯的即时通信领域霸主地位在中国无人能敌,借助IM,企鹅大部分业务很容易就做了起来,游戏,邮箱,空间等等,IM的社交特性将用户紧紧地圈在了一起。君不见360和腾讯发生冲突的时候,腾讯轻而易举地反击了,因为大...
  • 融云即时通讯

    千次阅读 2017-01-02 14:49:58
    即时通讯是目前最为流行的通讯方式,大多数app都使用到即时通讯,Android开发中,比较火的即时通讯有:融云、环信、Jpush。融云可以实现IM、推送、直播、客服等功能,下面就来讲解一下融云即时通讯的使用以及聊天...
  • IM即时通讯框架设计(1)

    千次阅读 2019-08-27 15:59:07
    今天和大家先简单分享一下即时通讯技术哪些应用场景以及这个技术对于我们来说哪方面的帮助。 1.1即时通讯技术的使用场景 即时通讯技术应用非常广泛,涉及教育、电商、金融、泛娱乐、生活服务、医疗健康、...
  • Android即时通讯实现原理

    万次阅读 2016-07-19 20:58:38
    即时通讯实现原理即时通讯(Instant Messenger,简称IM)软件多是基于TCP/IP和UDP进行通讯的,TCP/IP和UDP都是建立在更低层的IP协议上的两种通讯传输协议。前者是以数据流的形式,将传输数据经分割、打包后,通过两...
  • 即时通讯(Instant Messenger,简称IM)软件多是基于TCP/IP和UDP进行通讯的,TCP/IP和UDP都是建立在更低层的IP协议上的两种通讯传输协议。前者是以数据流的形式,将传输数据经分割、打包后,通过两台机器之间建立起...
  • IM即时通讯实现原理

    千次阅读 2019-04-24 13:37:31
    IM即时通讯实现原理 即时通讯(Instant Messenger,简称IM)软件多是基于TCP/IP和UDP进行通讯的,TCP/IP和UDP都是建立在更低层的IP协议上的两种通讯传输协议。前 者是以数据流的形式,将传输数据经分割、打包后,...
  • 随着科技发展,无论是工作还是生活即时通讯已成为了大家传输信息必不可少的工具。既然要承担传输消息的重任,传输的可靠性自然是**即时通讯**系统中是最为基础、也是最核心的部分之一,同时也是衡量通讯系统的质量的...
  • RabbitMQ实现即时通讯

    千次阅读 2020-10-12 10:15:33
    时候我们的项目中会用到即时通讯功能,比如电商系统中的客服聊天功能,还有在支付过程中,当用户支付成功后,第三方支付服务会回调我们的回调接口,此时我们需要通知前端支付成功。最近发现RabbitMQ可以很方便的...
  • Qt制作局域网即时通讯软件

    千次阅读 多人点赞 2019-12-21 21:35:07
    Qt制作局域网即时通讯软件 利用Qt制作的局域网即时通信软件,可实现文本信息、表情包、图片、文档等的传输功能。界面风格模仿的Tim,所以本软件取名Timi,tim的mini版本。 登录界面:使用之前做的登录界面,...
  • Open-IM是由前微信技术专家打造的开源的即时通讯组件。Open-IM包括IM服务端和客户端SDK,实现了高性能、轻量级、易扩展等重要特性。开发者通过集成Open-IM组件,并私有化部署服务端,可以将即时通讯、实时网络能力...
  • c.m.mall.tiny.config.MqttInboundConfig : handleMessage : 来自网页上的消息 总结 消息中间件应用越来越广泛,不仅可以实现可靠的异步通信,还可以实现即时通讯,掌握一个消息中间件还是很必要的。如果没有特殊...
  • 即时通讯软件即所谓的聊天工具,其主要用途是用于文字信息的传递与文件传输。使用eclipse作为即时通讯软件的开发工具,使用Socket建立通讯渠道,多线程实现多台计算机同时进行信息的传递,swing技术等进行实际开发...
  • 作为外贸人,不要一味的跟客人谈生意,当然目的是这个。私下的问候,关心,沟通也是不可或缺的。很多老外不喜欢来回发邮件,那样速度慢,还得来回查看,因此很多在线的聊天工具就备受欢迎。
  • 随着网络的普及,即时通信(Instant messaging,简称:IM)工具已经成为 人们进行信息交互的一种主要方式,目前的IM工具大都支持多人会话,也就是 通常所说的群组,同一群组的各用户可以共同进行信息交流,极大的...
  • IM 即时通讯实现原理

    千次阅读 2018-01-31 23:25:05
    即时通讯(Instant Messenger,简称IM)软件多是基于TCP/IP和UDP进行通讯的,TCP/IP和UDP都是建立在更低层的IP协议上的两种通讯传输协议。前 者是以数据流的形式,将传输数据经分割、打包后,通过两台机器之间建立起...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 27,522
精华内容 11,008
热门标签
关键字:

下面为即时通讯工具的有