Socket 多人聊天室的实现(App后台接收消息的处理)

Posted 宾有为

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket 多人聊天室的实现(App后台接收消息的处理)相关的知识,希望对你有一定的参考价值。

在上一篇文章中,讲解了Socket连接过程并实现连接,匿名用户发消息互怼,本篇文章将实现App后台运行程序接收消息,在状态栏显示消息的效果。

Socket 多人聊天室的实现系列文章:

封装Socket

开始的第一步,要把Socket封装起来,本篇文章的Socket代码不再是Activity类用得到,Service同样需要使用。

public class SocketUtil 
    private static String TAG = "SocketUtil";
    private static Socket socket = null;
    private static JSONObject jsonObject;
    private static String name;
    private static ExecutorService mExecutorService = null;
    private static PrintWriter printWriter;
    private static BufferedReader in;

    /**
     * 初始化用户名,随机生成一个用户名称
     */  
     public static void initUser() 
        try 
            jsonObject = new JSONObject();
            name = RandomGenerateName.randomName(true, 3);
            jsonObject.put("name", name);
         catch (JSONException e) 
            e.printStackTrace();
        
    

    /**
     * 发起连接
     */
    public static void connectSocket(MessageNotiface notiface) 
        mExecutorService = Executors.newCachedThreadPool();
        mExecutorService.execute(new Runnable() 
            @Override
            public void run() 
                //可以考虑在此处添加一个while循环,结合下面的catch语句,实现Socket对象获取失败后的超时重连,直到成功建立Socket连接
                try 
                    // 初始化socket地址、端口号
                    socket = new Socket(Config.SOCKET_HOSE, Config.SOCKET_PORT);
                    printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(   //步骤二
                            socket.getOutputStream(), "UTF-8")), true);
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                    // 发送消息到后台
                    printWriter.println(jsonObject);

                    try 
                        String receiveMsg = "";
                        while (true) 
	                        // 读取后台Socket发送过来的消息,此处会阻塞,获取不到消息会停止往下执行
                            if ((receiveMsg = in.readLine()) != null) 
                                try 
                                    JSONObject jsonObject = new JSONObject(receiveMsg);
                                    // 将消息传送到实现接口的地方
                                    notiface.onMessage(jsonObject);
                                 catch (JSONException e) 
                                    e.printStackTrace();
                                
                            
                        
                     catch (IOException e) 
                        Log.e("TAG", e.getMessage());
                    
                 catch (Exception e) 
                    try 
                        //如果Socket对象获取失败,即连接建立失败,会走到这段逻辑
                        JSONObject json = new JSONObject();
                        json.put("name", Config.SUPER_ADMINISTRATOR);
                        json.put("msg", "连接聊天室失败,请检查聊天室IP、端口是否正确,并确认服务端是否已开启");
                        notiface.onMessage(json);
                     catch (JSONException jsonException) 
                        Log.e(TAG, jsonException.getMessage());
                    
                
            
        );
    

    /**
     * 发送消息
     */
    public static void sendMessage(View view, String message) 
        if (!message.equals("")) 
            try 
                jsonObject.put("msg", message);
                mExecutorService.execute(new sendService(jsonObject.toString()));
             catch (JSONException e) 
                e.printStackTrace();
            
         else 
            Snackbar.make(view, "发送内容不能为空,请重新输入", Snackbar.LENGTH_LONG).show();
        
    

    /**
     * 断开连接(通过向服务器发送消息让服务器管理对应的socket连接)
     */
    public static void disconnectSocket() 
        try 
            jsonObject.put("msg", Config.SOCKET_DISCONNECT_MESSAGE);
            mExecutorService.execute(new sendService(jsonObject.toString()));
         catch (JSONException e) 
            e.printStackTrace();
        
    

	/**
     * 发送消息的线程
     */
    public static class sendService implements Runnable 
        private String msg;

        sendService(String msg) 
            this.msg = msg;
        

        @Override
        public void run() 
            if (printWriter != null) 
                printWriter.println(msg);
            
        
    

    public interface MessageNotiface 
        void onMessage(JSONObject json);
    

使用需要先初始化,如下:

// 连接Sokcer
SocketUtil.initUser();
SocketUtil.connectSocket(new SocketUtil.MessageNotiface() 
    @Override
    public void onMessage(JSONObject json) 
        // socket响应的消息
    
);

Service

App进入后台后的各种操作,离不开android四大组件的Service,先来简单了解一下Service

  • 简介
    Service是Android四大组件之一,其它三大组件分别是:ActivityContentProviderBroadcast ReceiverService是一个应用程序组件,表示应用程序希望在不与用户交互的情况下执行更长时间运行的操作,或者提供供其他应用程序使用的功能。
    Service启动的方式不同,会执行不同的生命周期,实现的效果各有差别,使用startService()启动Service,当这个方法执行时,服务就会启动并且可以无限期地在后台运行。而使用bindService()启动的Service,仅在组件绑定到它时运行,服务与其所有客户端解除绑定后,系统会将其销毁。
    Service生命周期如下,具体详情见:Service


在后台接收消息,就要求Service在销毁App后还在继续执行,用startService()去启动Service才能实现我们的需求。

Notification

Service继承了Context类,使得我们可以直接调用getSystemService(NOTIFICATION_SERVICE)方法得到一个NotificationManager,通过在ServiceonStartCommand(Intent intent, int flags, int startId)生命周期配置上一系列参数,当App运行在后台时,通知栏就可以展示最新接收到的消息。

public class SockerService extends Service 
    /**
     * 通知的标识符,唯一
     */
    private final int NotifyId = 0;

    public SockerService() 
    

    @Override
    public void onCreate() 
        super.onCreate();
        Log.d("SockerService", "Service创建成功");
    

    /**
     * startService启动Service触发的生命周期
     *
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) 
        super.onStartCommand(intent, flags, startId);
        Log.d("SockerService", "Service启动成功");
        SocketUtil.initUser();
        SocketUtil.connectSocket(new SocketUtil.MessageNotiface() 
            @Override
            public void onMessage(JSONObject json) 
                // socket接收到的消息
                NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                String notificationId = UUID.randomUUID().toString();
                String notificationName = UUID.randomUUID().toString();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 
                    // 高版本的模拟器或手机还需要开启渠道才能显示通知
                    NotificationChannel notificationChannel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
                    notificationManager.createNotificationChannel(notificationChannel);
                
                NotificationCompat.Builder builder = new NotificationCompat.Builder(SockerService.this, notificationId);
                // 实例化一个意图,当点击通知时会跳转执行这个意图
                Intent intent1 = new Intent(SockerService.this, MainActivity.class);
                // 将意图进行封装
                PendingIntent pendingIntent = PendingIntent.getActivity(SockerService.this, 0, intent1, PendingIntent.FLAG_CANCEL_CURRENT);
                //设置Notification的点击之后执行的意图
                builder.setContentIntent(pendingIntent);
                builder.setSmallIcon(R.mipmap.ic_launcher);
                try 
                    String name = json.getString("name");
                    String msg = json.getString("msg");
                    builder.setContentTitle(name + "发来了一条消息");
                    builder.setContentText(msg);
                    notificationManager.notify(NotifyId, builder.build());
//                    startForeground(28, builder.build());
                 catch (JSONException e) 
                    e.printStackTrace();
                
            
        );
        // 返回值为如果服务被异常杀掉,系统就会重启该服务,并传入Intent的原值
        return START_STICKY_COMPATIBILITY;
    

    /**
     * bindService启动Service触发的生命周期
     *
     * @param intent
     * @return
     */
    @Override
    public IBinder onBind(Intent intent) 
        // MyIBinder myIBinder = new MyIBinder();
        return null;
    

    /**
     * bindService取消绑定Service触发的生命周期
     *
     * @param conn
     */
    @Override
    public void unbindService(ServiceConnection conn) 
        super.unbindService(conn);
    

    @Override
    public void onDestroy() 
        super.onDestroy();
        Log.d("SockerService", "Service断开连接并尝试重新创建");
        Intent intent = new Intent(this, SockerService.class);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) 
            startForegroundService(intent);
         else 
            startService(intent);
        
        // Service销毁时断开连接
        SocketUtil.disconnectSocket();
    


but,事情到此尚未结束。博主在测试过程中发现,有几次App进入后台一段时间后,就再也接手不到新消息了,后台打印一下日志,发现Service未经过博主大大的允许,竟然断开了连接!!!


想不明白问题所在,干脆翻一下开发文档,别说,真让我找着了。

为了多出点内存让系统运行更加的流畅,系统会在手机内存不足,杀死部分服务。也因此,使用Service在后台运行,最好使用startForeground(int, Notification)API 将服务置于前台状态才不会被系统随意杀死(有一种保活方式是在ServiceonDestroy()方法中启动Service,即Serivce销毁自启),这不就类似各大音乐平台app进入后台运行的处理方式嘛?

后面,我也的确加上了startForeground(int, Notification)API的调用,但效果已与使用NotificationManager.notify(int id, Notification notification)有所差别。

小结

  • 运行程序在手机,IP地址输入无误,连接Socket超时也许是电脑防火墙未关闭,导致手机无法访问电脑IP地址。
  • 点击下载源码

参考文档:
关于Android Service真正的完全详解,你需要知道的一切

以上是关于Socket 多人聊天室的实现(App后台接收消息的处理)的主要内容,如果未能解决你的问题,请参考以下文章

java是如何实现聊天功能的?

游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)

游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)

多线程+socket实现多人聊天室

基于Netty的简单多人聊天程序(服务端)

使用node.js实现多人聊天室(socket.ioB/S)