精华内容
下载资源
问答
  • 背景:由于公司可能需要在微信群里面使用打卡功能,因此了个技术调研。 方案:微信在更新分享接口后,原有的在onShareAppMessage中直接拿shareTicket已不复存在。根据最新文档显示,需要在App.onLaunch()跟App....
  • 刚学习完javaSE,需要一个小小的聊天软件,需要实现私聊和群聊的功能;本人觉得应当使用线程池和socket、IO流来实现,但不知道怎么?求各位大神给点意见,不胜感激~
  • 近来感觉秋招无望,学习Socket的时候,便了个基于Socket的群聊工具; 先看看最终效果吧 项目GitHub通道(详细代码请自行copy)如何利用Socket通信 socket又称为“套接字”,建立网络通信连接至少要一对端口号...

    近来感觉秋招无望,学习Socket的时候,便做了个基于Socket的群聊工具;
    先看看最终效果吧
    这里写图片描述

    这里写图片描述
    这里写图片描述

    项目GitHub通道(详细代码请自行copy)

    如何利用Socket通信

    socket又称为“套接字”,建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。 ——来自百度百科

    socket通信步骤(TCP)
    1.建立SocketServer(服务端) 和 Socket (客户端)
    2.打开两个端之间的输入输出流
    3.进行读写操作
    4.关闭socket与流

    先来看第一步
    SocketServer 与 Socket都是 java.net包里的,进行TCP通信的时候需要建立客户端与服务端。SocketServer 通过绑定端口(Port)来实现监听,而Socket则是指定服务端端口(Port)与地址(IP)。
    下面来看一个简单的通信例子

    //Server部分
    ServerSocket server = new ServerSocket(6063);//实例化(传入端口号)
    Socket s = server.accept();//调用accept接收socket
    BufferedReader in = 
    new BufferedReader(new InputStreamReader(s.getInputStream()));//获得socket的输入流(同样的,可以通过getOutput
    Stream来获取输出流)
    while((msg= in.readLine())!=null){
        System.out.println(msg);
        }
    in.close();
    s.close();
    
    
    //Client
    Socket s = new Socket("192.168.1.133",6063);//实例化Socket传入指定服务端地址和端口号
                System.out.println("客户端启动...");
                BufferedReader re = new BufferedReader(new InputStreamReader(System.in));//用输入流读取键盘的输入
                PrintWriter pw = new PrintWriter(s.getOutputStream(),true);//获取socket的输出流,第二个参数表示会自动flush
                String msg2;
                while(true){
                    msg2 = re.readLine();
                    pw.println(msg2);//输出(自动flush)
                }
    

    这样我们就简单了实现了socket通信
    当然要实现即时聊天并非这么几行就能搞定的。
    即时聊天,重点在于流的控制,需要开辟多条线程去分别做不同的事情。
    下面来本次项目的PC服务端代码

    PCServer端代码

    
    public class Server implements Runnable {
        List<Socket> sockets = new ArrayList<>();
        private static final String ServerIp = "192.168.1.133";
        private static final int ServerPort = 6066;
    
        @Override
        public void run() {
            try{
                System.out.println("服务端启动...");
                ServerSocket server = new ServerSocket(ServerPort);
                while(true){
                    Socket client = server.accept();
                    sockets.add(client);
                    receiveThread re = new receiveThread(client);
                    re.start();
                }
            }catch(Exception e){
                System.out.println("------S:Error 1-------");
                e.printStackTrace();
            }
    
        }
        //接受msg线程
        public class receiveThread extends Thread{
            Socket socket;
            private BufferedReader br;
            private PrintWriter pw;
            public String msg;
            public receiveThread(Socket s){
                socket = s;
            }
            public void run(){
                try{
                    br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));//不转码在Android端会乱码
                    msg = "sys:##:"+"欢迎"+socket.getInetAddress()+"进入聊天室,当前人数为"+sockets.size();
                    sendMsg(InetAddress.getByName("1"));
                    while((msg = br.readLine())!=null){
                        if(msg.equals("EndEndClosethesocket")){
                            close(socket.getInetAddress());
                        }else{
                            msg = socket.getInetAddress()+":##:"+msg;
                            sendMsg(socket.getInetAddress());
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
            public void sendMsg(InetAddress ip){
                try{
                    System.out.println(msg);
                    for(int i = 0;i < sockets.size();i++){
                        if(!ip.equals(sockets.get(i).getInetAddress())){
                        pw = new PrintWriter(new OutputStreamWriter(sockets.get(i).getOutputStream(),"UTF-8"),true);
                        pw.println(msg);
                        pw.flush();
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
    
        public void close(InetAddress ip){
            for(int i = 0;i < sockets.size();i++){
                if(sockets.get(i).getInetAddress()==ip){
                    sockets.remove(i);
                    msg ="sys:##:"+ip+"已经离开了聊天室";
                    try{
                        sendMsg(InetAddress.getByName("1"));
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
        }
        public static void main(String args[]){
            Thread thread = new Thread(new Server());
            thread.start();
        }
    
    }
    

    可以看到,这里用了一个ArrayList来存储SocketServer接收的Socket
    然后将接收的Socket作为参数传入自定义的线程receiveThread中,然
    后在这个线程中循环读取Client端发来的消息。然后通过sendMsg方法
    广播这条消息。
    下面看看Android客户端的主要代码

    Android端核心代码

      protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           ...//详细代码请移步GitHub
                     if(socket==null||socket.isClosed()||!socket.isConnected()){
                getIPandPort();
                creatSock();
                Log.d("aaaaaaa", "onCreate: 1111111111");
            }else {
                Log.d("aaaaaaa", "onCreate: 122222222222");
                heart();
            }
            send.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //发送button监听
                    if(sendMsg(edit.getText().toString())){
                        edit.setText("");
                    }
                }
            });
      }
       //读取ip与Port
        public void getIPandPort(){
            SharedPreferences preferences = getSharedPreferences("data", Context.MODE_PRIVATE);
            ServerIp = preferences.getString("ip","192.168.1.133");
            ServerPort = preferences.getInt("port",6066);
            title.setText(ServerIp+"\n"+ServerPort);
        }
        //心跳检测
     public void heart(){
            if (socket!=null){
                try{
                    socket.sendUrgentData(0xff);
                }catch (Exception e){
                    e.printStackTrace();
                    reconn();
                }
            }else {
                reconn();
            }
        }
        //建立连接
        public void creatSock(){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try{
    //                    if (socket!=null){
    //                       clconn();
    //                    }
                        InetAddress inetAddress = InetAddress.getByName(ServerIp);
                        socket = new Socket(inetAddress, ServerPort);
                        AcceptMsg();
                    }catch (Exception e) {
                        e.printStackTrace();
                        reconn();
                        Log.d("aaaa", "run: 连接失败");
                    }
                }
            }).start();
        }
        public void reconn(){
            sendMessenger(new Msg("无法连接服务器...请重设PORT或IP",2,"sys"));
            showReconn();
        }
         public  void sendMessenger(final Msg msg){
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    msgs.add(msg);
                    adapter.notifyDataSetChanged();
                }
            });
        }
        //发送
        private boolean sendMsg(final String msg){
            if(socket!=null&&socket.isConnected()){
                if (!msg.equals("")){
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try{
                                pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
                                pw.println(msg);
                                if (!msg.equals("EndEndClosethesocket")){
                                    sendMessenger(new Msg(msg,0,"Me"));
                                }
                                Log.d("XXXXXXX", "发送成功");
                            }catch (Exception e) {
                                heart();
                            }
    
    
                        }
                    }).start();
                    return true;
                }
            }
            return false;
        }
        //接受
        public void AcceptMsg(){
            if (socket.isConnected()&&!socket.isClosed()){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try{
                            re = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            while((socketmsg=re.readLine())!=null){
                                String[] msg = socketmsg.split(":##:");
                                Msg m;
                                if(msg[0].equals("sys")){
                                    m = new Msg(msg[1],2,msg[0]);
                                }else{
                                    Log.d("sxxxxx", "run: "+msg[0]);
                                    String ip = msg[0].substring(msg[0].length()-3,msg[0].length());
                                    m = new Msg(msg[1],1,ip);
                                }
                                sendMessenger(m);
                                Log.d("xxxxxxxxx", "接受成功"+socketmsg);
                            }
                        }catch (Exception e){
                            e.printStackTrace();
                            heart();
                            Log.d("aaaa", "run: 接受失败");
                        }
                    }
                }).start();
            }
            heart();
        }

    在安卓端实例化Socket时要注意将IP地址转化为InetAddress,再将参数传入。
    另外,Socket操作不能在主线程里直接操作,否则会报错,应该新建线程对其
    进行操作(无论是建立连接,发送消息,还是接收消息);
    本次项目开发也遇到了许多坑。
    一开始想用ViewPage+fragement来实现更好的界面效果,由于fragment的
    重载的大坑,导致我最终放弃直接用fragment实现聊天界面
    还有就是心跳包的问题,重复发送多次会导致服务端崩溃,捣鼓了一下午也没
    弄好,最后还是没用上。
    这次就到这里咯,收拾收拾好心情,面对十月的秋招吧!!!!

    ——-来自offer颗粒无收的大四狗

    展开全文
  • 昨天刚开放了群ID接口能力,说得好理解一点,就是当你把小程序分享在群聊中,被点击后开发者可获取群ID和群名称,这就意味着开发者可以更好地针对群场景提供个性化服务。 今天又宣布正式开放“附近的小程序”,心疼...
        

    昨天刚开放了群ID接口能力,说得好理解一点,就是当你把小程序分享在群聊中,被点击后开发者可获取群ID和群名称,这就意味着开发者可以更好地针对群场景提供个性化服务。

    今天又宣布正式开放“附近的小程序”,心疼天天熬夜的小程序团队。
    总的来说呢,就是有小程序的商户,可以快速将门店小程序或普通小程序展示在“附近”,用户走到某个地点,打开“发现-小程序-附近的小程序”,就可以轻松使用,不需要一个一个去搜索。

    图片描述

    1、小程序拥有全面的推广方式

    商家:快速、低门槛地在指定地点展示小程序,在无利润的前提下这是一种新型的推广方式。前几天小编接到一个客户的咨询,餐饮小程序在无利润的前提下,开发一个小程序的成本较高的话如何进行快速推广?说实话,小程序的推广方式实在是太多了,单单入口就有9种。

    总结现有的【小程序】入口有:

    1、微信【发现栏】入口

    2、长按识别二维码

    3、微信搜索页面模糊搜索

    4、公众号主体信息查看相关小程序

    5、好友分享和群分享

    6、公众号自定义菜单跳转小程序

    7、微信钱包【第三方服务】

    8、App页面跳转

    9、附近的小程序

    站在用户的角度上:方便了解到附近有什么小程序,无需搜索查找,像一些连锁餐饮企业肯德基麦当劳再也不需要专门去搜索其公众号再选择确切地址,直接选择附近门店的小程序查看优惠信息。

    2、订阅号开通小程序

    小编个人的订阅号也是可以直接开通小程序的,需要使用未注册过微信公众号平台和个人微信号的邮箱进行注册,填写个人真实资料即可拥有小程序账号。

    图片描述

    小程序的功能还有很多,微信公众号平台运营者在拥有开通小程序后,进行企业认证和申请微信支付基本功能后,即可和第三方开发公司进行上架门店小程序,可进行以下操作。

    ①门店小程序:
    从左侧“小程序”-“展示场景”进入“附近的小程序”,可将门店地点直接导入,小程序将在该地点自动展示,关联了同主体公众号的普通小程序同样可以使用这一方式。

    ②普通小程序:
    登录小程序后台,进入“附近的小程序”,开通功能后添加地点,连锁企业直接添加多个地址进行关联即可。

    除了微信小程序的这些新功能,夏日葵小编还要给大家介绍一款非常实用的小程序——群应用。教你轻松搞定在玩微信时碰到的一些小问题。这个小程序的核心功能是:群名片和社群,这也是推广你门店品牌的另一种方式。

    1.群名片

    虽说注册微信的初心是为了更方便的和家人朋友联系,但是到现在微信更多的使用在了工作方面,这也让一部分人陷入了尴尬。又想用一个可爱搞怪的头像跟朋友聊天,又希望更工作伙伴留下一个严肃认真的形象,怎么办?这个时候你就需要群应用的“群名片”功能了,搜索小程序“群应用”进行创建。

    图片描述

    它可以它会自动抓取你的微信头像和昵称,不满意的话还可以编辑修改。根据上面的要求,填写好相关信息。你的专属名片就制作完成了,可以在更多融入你的门店小程序相关信息,这无疑是推广门店品牌的另一种方式。

    如果页面上的信息还不能满足你的需求,还可以在编辑名片页面的“相册”、“当前位置信息”“更多”等加入更多信息。创建好了你的名片以后,可以发给好友也可以发到微信群,还能生成群名片小程序码发到朋友圈里,方便有工作关系的人直接了解到你的相关信息。这样,只要大家打开你的电子名片,就能立即了解你的工作信息、企业品牌,小程序名称和联系方式。

    除此之外,群名片还有一个类似于qq空间的中二玩法,就是可以看到自己的名片人气指数,包括访问次数、点赞次数和收藏次数等,说不定到时候朋友圈又会掀起一股互赞狂潮了。

    2. 群友圈

    这个功能就厉害了,整合了微信群、朋友圈和通讯录三大功能,甚至还突破了微信群 500个人的人数限制。这个功能的玩法就更多了,搜索”群+“小程序,你可以创建一个社群,为你的社群设置头像名称等,还可以开启群审核功能,调整进入群的权限。

    图片描述

    更不可思议的是,“群应用”还可以创建付费群,并提供了按年付费和永久有效两种方式。这个功能有点像付费阅读了,这也为一些内容创造者们提供了新的变现方式,如果运用到付费课程这也的场景里,会非常的方便,这可以在当中融入”你门店小程序“的主要信息,这是一种隐藏式的细微推广方式。

    加入了某个社群的用户,都可以发“群友圈”,就像我们平时发朋友圈,不过这个比发朋友圈更方便的地方就是,你不需要在刻意去屏蔽某个分组了,只有同一个社群的人才能看到。在群成员的名片权限开启的前提下,即使你是新加入的成员,也可以逐一查看同一社群成员的名片。

    拥有八亿用户的流量重地“微信”给我们的惊喜真的是越来越多了,对于微信小程序夏日葵电商小编前面有说过“西关美食外卖+”餐饮小程序,不少门店体验后纷纷表示收付款,以及点餐下单功能都很方便,作为小程序第三方开发公司,我们秉承“利他之心”为大家提供更有质量的产品,如还有其他问题咨询小编可以百度夏日葵电商官网找到我。

    【版权提示】

    夏日葵电商倡导尊重与保护知识产权。如发现本站文章存在版权问题,烦请提供版权疑问、身份证明、版权证明、联系方式等发邮件至sem@xiarikui.com,我们将及时沟通与处理。

    展开全文
  • 公司提出一个需求需要一个企业微信的一个消息推送,需要将消息发送到聊天群里详细信息如下。如何创建应用请阅读我的上篇文章:https://www.cnblogs.com/wangyajunblog/p/9996308.html# -*- coding: UTF-8 -*-...

    公司提出一个需求需要做一个企业微信的一个消息推送,需要将消息发送到聊天群里详细信息如下。

    如何创建应用请阅读我的上篇文章:https://www.cnblogs.com/wangyajunblog/p/9996308.html

    # -*- coding: UTF-8 -*-

    import requests

    import urllib.request

    import json

    def get_token(url, corpid, corpsecret):

    token_url = '%s/cgi-bin/gettoken?corpid=%s&corpsecret=%s' % (url, corpid, corpsecret)

    token = json.loads(urllib.request.urlopen(token_url).read().decode())['access_token']

    return token

    #获取成员信息

    corpid = 'ww****************' # 我的企业的id

    corpsecret = 'sGUmNQWa*************************************' #应用的Secret

    url = 'https://qyapi.weixin.qq.com'

    token=get_token(url, corpid, corpsecret)

    url="https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token="+token

    r=requests.get(url)

    department=r.json()['department'][1]['id']

    url='https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token='+token+'&department_id='+str(department)

    r=requests.get(url)

    #print(r.json())

    userid="*************************" #我的企业微信的账号

    userid_list=["********************","*******************88"] #群内的人的账号#下面是获取list

    #userlist=r.json()['userlist']

    #for user in userlist:

    # userid_list.append(user['userid'])

    #下面是建群

    '''url='https://qyapi.weixin.qq.com/cgi-bin/appchat/create?access_token='+token

    print(userid_list)

    data={"name":"测试","owner":userid,"userlist":userid_list}

    data=json.dumps(data)

    r=requests.post(url,data=data)

    chatid=r.json()["chatid"]

    print(chatid)'''

    url="https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token="+token

    data={"chatid":"*********************","msgtype":"text","text":{"content":"自定义"},"safe":0} #chatid值是群组的id

    data=json.dumps(data)

    r=requests.post(url,data=data)

    #print(r.json())

    展开全文
  • 应该是去年的时候开始接触openfire,当时在分析后发现基于xmpp协议的openfire已经具备了群聊的功能。也就没太当回事,觉得加点功能就可以成类似于QQ群的那种模式。后来仔细了解后才发现并不是这么简单: muc其实...

    openfire群聊与QQ群对比

    应该是去年的时候开始接触openfire,当时在分析后发现基于xmpp协议的openfire已经具备了群聊的功能。也就没太当回事,觉得加点功能就可以做成类似于QQ群的那种模式。后来仔细了解后才发现并不是这么简单:

    • muc其实聊天室的形式,房间创建后可以加入聊天,用户离开就退出聊天室了,并没有一个用户固化的功能,所以要单独为这部分开发
    • muc因为没有固化的成员关系,所以并没有1对1聊天的那种离线消息。而且考虑到消息量是群发的原因,所以服务器对于加入聊天室的成员只会推送一定量的消息,当然这个可以通过策略来配置为全部推送。事实上考虑到群聊天的特性,推送指定条数可能是更靠谱的。
    • 还有一些QQ特有的功能,比如邀请进群需要管理员审核之类的管理功能就更少了,这块都需要扩展实现

    改造Openfire群聊天室为群

    实际上对于openfire的muc改造来说,持久化成员是第一个重要的工作。我们期望的是这个房间里的人都是固定的成员,这些成员可以离开聊天室,但下次可以进来继续聊天。其实实现起来也挺简单的:

    基于openfire的实现

    1. 建立数据表,用于保存成员列表
      在openfire里已经有一系列的表用于保存muc相关的数据:
    • ofMucRoom-这个是房间表,保存了聊天室的信息
    • ofMucAffiliation-这个是保存房间里管理员角色人员的表(owner(10)、admin(20)、outcast(40))
    • ofMucMember-这个是房间里成员的列表(对应的是member(30))

    这里ofMucAffiliation+ofMucMember保存的数据其实是用于记录的是用户权限的,当然会发现其实这已经对应上我们需求上的群成员咯?确实是这样的。

    只不过有一个问题,就是ofMucAffiliation+ofMucMember里只能知道用户的jid,但是群的话可能每个用户会修改自己的昵称,对不对?所以还要加一个表用于保存这种用户的个性化数据。当然这个表也挺简单的就不细写了。

    1. 通过openfire的插件体系增加一个插件,在服务端实现加群、退群等功能
      毕竟xmpp协议里是没有获得群列表和房间成员的功能的,以及一些加群、退群的管理功能都没有,所以要自己开发。这里可以通过openfire的插件体系来做,这样比较独立,不影响openfire内核功能。

    这块涉及到写插件的技术,网上有很多,我就不多说了。

    1. 自己定义一套协议来完成客户端与服务端的通讯
      因为要走openfire,所以还是要定义xmpp协议,我用的是IQ。考虑到我使用的是smack做的,所以这部分就不再写了。有兴趣或者需要的网上找找IQ协议的写法就行了。

    其他方式

    其实这些功能无非就是增删改查,而且我们添加的功能完成可以独立于openfire之外,所以自己写一套也是可以的。比如用web的方式实现也是可以的。

    特别是可以设计成rest api,这样对于端来说是比较友好通用的,兼顾PC、移动端就简单多了,特别是移动端走http协议总比走长链接方便吧。

    分析openfire muc群聊历史消息的实现

    简单的介绍了群的实现,另外一个比较头痛的问题就是muc离线消息。在openfire里是有类似的支持的,这里就做一些简单的分析吧。

    历史消息策略HistoryStrategy

    因为在openfire里历史消息推送策略是这样的,我们看一下它的策略类HistoryStrategy,里面设定了一个枚举:

    /**
     * Strategy type.
     */
    public enum Type {
        defaulType, none, all, number;
    }
    

    可以看出,其实就是三种:none(不显示历史记录)、all(显示整个历史记录)、number(指定条数记录)。默认的是number。

    策略类会维护一个内存列表,用于给新加入的用户发送历史记录用:

    private ConcurrentLinkedQueue<Message> history = new ConcurrentLinkedQueue<>();

    实际上自己也可以实现一套策略来代替它,比如将消息存在redis之类。只不过Openfire并没有提供扩展,只能是修改openfire代码来实现咯。

    历史消息的保存与维护

    历史消息的保存是在openfire里的MultiUserChatServiceImpl里实现的,它会启动一个TimerTask,定时的将消息保存到历史消息表里。下面是定时任务的实现

    /**
     * Logs the conversation of the rooms that have this feature enabled.
     */
    private class LogConversationTask extends TimerTask {
        @Override
        public void run() {
            try {
                logConversation();
            }
            catch (Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }
    
    private void logConversation() {
        ConversationLogEntry entry;
        boolean success;
        for (int index = 0; index <= log_batch_size && !logQueue.isEmpty(); index++) {
            entry = logQueue.poll();
            if (entry != null) {
                success = MUCPersistenceManager.saveConversationLogEntry(entry);
                if (!success) {
                    logQueue.add(entry);
                }
            }
        }
    }

    这是具体的保存聊天历史的代码,可以看到消息是放在logQueue里的,然后定时任务从里面取一定的条数保存到数据库存中。MUCPersistenceManager就是数据库的访问类。

    在start方法里启动

    @Override
    public void start() {
        XMPPServer.getInstance().addServerListener( this );
    
        // Run through the users every 5 minutes after a 5 minutes server startup delay (default
        // values)
        userTimeoutTask = new UserTimeoutTask();
        TaskEngine.getInstance().schedule(userTimeoutTask, user_timeout, user_timeout);
        // Log the room conversations every 5 minutes after a 5 minutes server startup delay
        // (default values)
        logConversationTask = new LogConversationTask();
        TaskEngine.getInstance().schedule(logConversationTask, log_timeout, log_timeout);
        // Remove unused rooms from memory
        cleanupTask = new CleanupTask();
        TaskEngine.getInstance().schedule(cleanupTask, CLEANUP_FREQUENCY, CLEANUP_FREQUENCY);
    
        // Set us up to answer disco item requests
        XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this);
        XMPPServer.getInstance().getIQDiscoInfoHandler().setServerNodeInfoProvider(this.getServiceDomain(), this);
        XMPPServer.getInstance().getServerItemsProviders().add(this);
    
        ArrayList<String> params = new ArrayList<>();
        params.clear();
        params.add(getServiceDomain());
        Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", params));
        // Load all the persistent rooms to memory
        for (LocalMUCRoom room : MUCPersistenceManager.loadRoomsFromDB(this, this.getCleanupDate(), router)) {
            rooms.put(room.getName().toLowerCase(), room);
        }
    }

    这里是聊天室服务启动的过程,它会启动LogConversationTask用于定期将聊天记录保存到库里。而且这里最后几句会发现启动时会从库里读取数据(MUCPersistenceManager.loadRoomsFromDB),loadRoomsFromDB实现了读取Hitory数据到historyStrategy里。

    具体的数据保存在ofMucConversationLog表中。

    如何推送历史消息给客户端

    有了历史消息推送策略和数据,那么怎么样推送给客户端呢?这里有一个history协议,在LocalMUCUser处理packet的时候,如果这个packet是Presence,并且带有history节说明是客户端发来要历史记录的。

    在LocalMUCUser.process(Presence packet)里有history消息节的处理代码,因为代码太多,就截取关键的部分:

    // Get or create the room
    MUCRoom room = server.getChatRoom(group, packet.getFrom());
    // User must support MUC in order to create a room
    HistoryRequest historyRequest = null;
    String password = null;
    // Check for password & requested history if client supports MUC
    if (mucInfo != null) {
        password = mucInfo.elementTextTrim("password");
        if (mucInfo.element("history") != null) {
            historyRequest = new HistoryRequest(mucInfo);
        }
    }
    // The user joins the room
    role = room.joinRoom(recipient.getResource().trim(),
            password,
            historyRequest,
            this,
            packet.createCopy());

    这里可以看到,先获取到historyRequest节的信息,然后调用room.joinRoom方法。这里的room.joinRoom就是用户加入聊天室的关键部分。在joinRoom里会发送历史消息给这个用户:

     if (historyRequest == null) {
        Iterator<Message> history = roomHistory.getMessageHistory();
        while (history.hasNext()) {
            joinRole.send(history.next());
        }
    }
    else {
        historyRequest.sendHistory(joinRole, roomHistory);
    }

    这里会发现有两种情况,1种是historyRequest为空的情况时,服务端默认按照策略的设置向用户发送历史消息。如果不为空,则根据客户端的请求参数发送。那么这里我们看看historyRequest的实现:

    public class HistoryRequest {
    
        private static final Logger Log = LoggerFactory.getLogger(HistoryRequest.class);
        private static final XMPPDateTimeFormat xmppDateTime = new XMPPDateTimeFormat();
    
        private int maxChars = -1;
        private int maxStanzas = -1;
        private int seconds = -1;
        private Date since;
    
        public HistoryRequest(Element userFragment) {
            Element history = userFragment.element("history");
            if (history != null) {
                if (history.attribute("maxchars") != null) {
                    this.maxChars = Integer.parseInt(history.attributeValue("maxchars"));
                }
                if (history.attribute("maxstanzas") != null) {
                    this.maxStanzas = Integer.parseInt(history.attributeValue("maxstanzas"));
                }
                if (history.attribute("seconds") != null) {
                    this.seconds = Integer.parseInt(history.attributeValue("seconds"));
                }
                if (history.attribute("since") != null) {
                    try {
                        // parse since String into Date
                        this.since = xmppDateTime.parseString(history.attributeValue("since"));
                    }
                    catch(ParseException pe) {
                        Log.error("Error parsing date from history management", pe);
                        this.since = null;
                    }
                }
            }
        }
        
        /**
         * Returns the total number of characters to receive in the history.
         * 
         * @return total number of characters to receive in the history.
         */
        public int getMaxChars() {
            return maxChars;
        }
    
        /**
         * Returns the total number of messages to receive in the history.
         * 
         * @return the total number of messages to receive in the history.
         */
        public int getMaxStanzas() {
            return maxStanzas;
        }
    
        /**
         * Returns the number of seconds to use to filter the messages received during that time. 
         * In other words, only the messages received in the last "X" seconds will be included in 
         * the history.
         * 
         * @return the number of seconds to use to filter the messages received during that time.
         */
        public int getSeconds() {
            return seconds;
        }
    
        /**
         * Returns the since date to use to filter the messages received during that time. 
         * In other words, only the messages received since the datetime specified will be 
         * included in the history.
         * 
         * @return the since date to use to filter the messages received during that time.
         */
        public Date getSince() {
            return since;
        }
    
        /**
         * Returns true if the history has been configured with some values.
         * 
         * @return true if the history has been configured with some values.
         */
        private boolean isConfigured() {
            return maxChars > -1 || maxStanzas > -1 || seconds > -1 || since != null;
        }
    
        /**
         * Sends the smallest amount of traffic that meets any combination of the requested criteria.
         * 
         * @param joinRole the user that will receive the history.
         * @param roomHistory the history of the room.
         */
        public void sendHistory(LocalMUCRole joinRole, MUCRoomHistory roomHistory) {
            if (!isConfigured()) {
                Iterator<Message> history = roomHistory.getMessageHistory();
                while (history.hasNext()) {
                    joinRole.send(history.next());
                }
            }
            else {
                Message changedSubject = roomHistory.getChangedSubject();
                boolean addChangedSubject = (changedSubject != null) ? true : false;
                if (getMaxChars() == 0) {
                    // The user requested to receive no history
                    if (addChangedSubject) {
                        joinRole.send(changedSubject);
                    }
                    return;
                }
                int accumulatedChars = 0;
                int accumulatedStanzas = 0;
                Element delayInformation;
                LinkedList<Message> historyToSend = new LinkedList<>();
                ListIterator<Message> iterator = roomHistory.getReverseMessageHistory();
                while (iterator.hasPrevious()) {
                    Message message = iterator.previous();
                    // Update number of characters to send
                    String text = message.getBody() == null ? message.getSubject() : message.getBody();
                    if (text == null) {
                        // Skip this message since it has no body and no subject  
                        continue;
                    }
                    accumulatedChars += text.length();
                    if (getMaxChars() > -1 && accumulatedChars > getMaxChars()) {
                        // Stop collecting history since we have exceded a limit
                        break;
                    }
                    // Update number of messages to send
                    accumulatedStanzas ++;
                    if (getMaxStanzas() > -1 && accumulatedStanzas > getMaxStanzas()) {
                        // Stop collecting history since we have exceded a limit
                        break;
                    }
    
                    if (getSeconds() > -1 || getSince() != null) {
                        delayInformation = message.getChildElement("delay", "urn:xmpp:delay");
                        try {
                            // Get the date when the historic message was sent
                            Date delayedDate = xmppDateTime.parseString(delayInformation.attributeValue("stamp"));
                            if (getSince() != null && delayedDate != null && delayedDate.before(getSince())) {
                                // Stop collecting history since we have exceded a limit
                                break;
                            }
                            if (getSeconds() > -1) {
                                Date current = new Date();
                                long diff = (current.getTime() - delayedDate.getTime()) / 1000;
                                if (getSeconds() <= diff) {
                                    // Stop collecting history since we have exceded a limit
                                    break;
                                }
                            }
                        }
                        catch (Exception e) {
                            Log.error("Error parsing date from historic message", e);
                        }
    
                    }
    
                    // Don't add the latest subject change if it's already in the history.
                    if (addChangedSubject) {
                        if (changedSubject != null && changedSubject.equals(message)) {
                            addChangedSubject = false;
                        }
                    }
    
                    historyToSend.addFirst(message);
                }
                // Check if we should add the latest subject change.
                if (addChangedSubject) {
                    historyToSend.addFirst(changedSubject);
                }
                // Send the smallest amount of traffic to the user
                for (Object aHistoryToSend : historyToSend) {
                    joinRole.send((Message) aHistoryToSend);
                }
            }
        }
    }

    这里面主要是用于约定发送历史消息的一些参数:

    private int maxChars = -1;
    private int maxStanzas = -1;
    private int seconds = -1;
    private Date since;

    这是可以设定的几个参数,具体的对应关系如下面的表格所示

    历史管理属性

    属性 数据类型 含义
    maxchars int 限制历史中的字符总数为"X" (这里的字符数量是全部 XML 节的字符数, 不只是它们的 XML 字符数据).
    maxstanzas int 制历史中的消息总数为"X".
    seconds int 仅发送最后 "X" 秒收到的消息.
    since datetime 仅发送从指定日期时间 datetime 之后收到的消息 (这个datatime必须 MUST 符合XMPP Date and Time Profiles 13 定义的DateTime 规则,).

    还有sendHistory

    当然这里还实现了一个sendHistory方法,也就是针对客户端提交了查询要求时的历史消息发送方法。具体的实现上面的代码吧。也就是根据历史管理属性里设定的几个参数进行针对性的发送。

    但是这里有个关键点就是since属性,它表示客户端可以设定一个时间戳,服务端根据发送这个时间戳之后的增量数据给客户端。这个对于客户端而已还是很有作用的。

    实现群离线消息的方法

    那么看完了openfire的历史消息的实现,再来实现离线消息是不是就简单的多了。群聊天历史消息有几个问题:

    • 问题1:群人员庞大历史消息巨大服务端如何缓存这些历史数据?比如一个群1000人,一人一天发10条,就有10000条/天,一个月就是30万,这还只是一个聊天群的,100个群就是3000万。
    • 问题2:对于群成员而言,可能一个月未登录,那么可能就要接收这一个月的离线消息,客户端基本就崩了,网络流量也很巨大,怎么处理?

    利用HistoryStrategy限制服务端推送条数

    所以不用举太多问题,就这两个就够了,那么我觉得openfire的这种历史消息策略中使用number(条数)是很重要的。比如服务器只缓存最近1000条聊天历史,这样整体的服务器缓存量就低了。这就解决了第一个问题。

    如果群用户需要查询历史上的数据,应该是另开一个服务接口专门用于查询历史数据,这样就不用在刚上线进入群时接收一堆的离线消息。

    利用HistoryRequest来获取增量数据

    前面分析HistoryRequest时提到了它可以设置一个时间戳参数,这个是告诉服务端从这个参数之后的历史消息推送过来。

    比如,用户A昨天晚20:00下的线(最后消息时间戳是2017-06-07 20:00:00),今天早上8:00上线。在用户A离线的时间里有100条离心线消息记录。

    那么用户A上线,客户端发送HistoryRequest(since=2017-06-07 20:00:00),服务器则只发送2017-06-07 20:00:00之后的聊天记录100条。这样就实现了增量的消息,对于服务端和客户端都是友好的。

    当然,这里能发多少消息最终还是要看服务端缓存了多少消息用于发送给客户端,毕竟就像问题2中提出的那样,用户可能一个月都不上线,这期间的历史消息要是都推送那肯定崩了。所以上线时的历史消息推送这个功能仅适合推送少量的数据。这个在具体的系统设计时应该根据实际情况来设计。

    展开全文
  • 先简单说下本次的主题,由于我最近的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 所以本次分享...
  • 先简单说下本次的主题,由于我最近的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 所以本次分享...
  • 目录 写在前面 环境说明 相关代码 效果展示 写在前面   itchat基于python开发,封装了大量调取微信功能的接口,使得开发人员可以...  下面介绍基于itchat完成微信数据(好友、群聊等)的分析和展示。 环境说明 py...
  • 先简单说下本次的主题,由于我最近的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 所以本次分享的...
  • 先简单说下本次的主题,由于我最近的是物联网相关的开发工作,其中就不免会遇到和设备的交互。 最主要的工作就是要有一个系统来支持设备的接入、向设备推送消息;同时还得满足大量设备接入的需求。 所以本次分享的...
  • 喜欢看动漫的朋友们大概都能体会到一个难受的事情,就是在论坛或者群聊里面看到一张动漫截图,很想知道它的出处,但百度搜了一圈却也没有一个可靠结果,就很郁闷。今天就来带大家用Python一个简单的“以图搜番”小...
  • ​ 很久没有写东西了,今天在3D立体资讯群里群聊中有朋友聊起CDR和PS两个软件合图(3D图或变图)那个更好的问题,我隐约记得十多年前曾过比较,得出的结论是,CDR合图输出后数输出来的比较锐利,而PS比较顺滑,...
  • 具体是什么工作的?需要哪些技能?让我们一起来看看吧。 【大数据开发学习资料领取方式】:加入大数据技术学习交流群458345782,点击加入群聊,私信管理员即可免费领取 这3个职业是如何定位的? 数据科学家是个...
  • 那么我们添加客户后,改如何做,才能减少客户流失呢? 1、做好备注 在初步认识对方后,这时候就可以添加备注了,备注可以按照这样的顺序标明:城市/姓名/行业。而通过群聊添加的客户,戒备心比较高的,那么我们...
  • 最近越来越爱写博客了,以前写博客只是单纯把知识点放进去,读着索然无味,每天就像是任务一样,现在写博客,会把自己的想法,自己的日常表达出来,像是一本日记,每天记载着我学到了什么,每天结束的时候都会回忆...

空空如也

空空如也

1 2 3 4
收藏数 74
精华内容 29
关键字:

如何做群聊的聊天记录