java.io.StreamCorruptedException: invalid stream header

12 篇文章 0 订阅

韩顺平-模拟QQ离线留言项目:

项目相关背景:

这是在学习韩老师的java基础课的QQ通信系统中离线留言功能实现时产生的问题。


问题描述

问题:

在一个用户给另一个仍未上线的用户(即离线用户)发送消息时,由于对方用户离线,服务端会将message先存储起来,等待该用户上线再转发message给他。但是当我这样处理的时候,出现了如下错误。

java.io.StreamCorruptedException: invalid stream header: 7371007E
	at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:936)
	at java.io.ObjectInputStream.<init>(ObjectInputStream.java:394)
	at com.hspedu.qqclient.service.ClientConnectServerThread.run(ClientConnectServerThread.java:23)

在这里插入图片描述
而且,我发现,该离线用户一旦上线,服务端是会正常发送他离线期间收到的信息给他的,关键是接收出了问题。
服务端没有报错:
在这里插入图片描述
客户端报错,但是它是在收到一条正确的离线留言之后才产生错误的:
在这里插入图片描述

给出关键源代码如下:

服务端

//                    将离线消息都发过去
                    oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                    if(offlineDb.containsKey(user.getUserId())){
                        ArrayList<Message> messages = offlineDb.get(user.getUserId());
                        for (Message offlinems:messages
                        ) {
                            System.out.println(offlinems.getContent());
                            oos.writeObject(offlinems);
                        }
                        offlineDb.remove(user.getUserId());
                        System.out.println(user.getUserId()+"上线了,服务端已经转发完成所有离线消息");
                    }

客户端

package com.hspedu.qqclient.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;

public class ClientConnectServerThread extends Thread{
//    该线程需要持有socket对象
    private  Socket socket;
//构造器接收socket
    public ClientConnectServerThread(Socket socket) {
        this.socket = socket;
    }
    private ObjectInputStream ois;
    @Override
    public void run() {
//        因为Thread需要后台和服务器通信,所以我们一直循环
        while(true){
            System.out.println("客户端线程,等待读取从服务器端发送过来的消息");
            try {
                ois= new ObjectInputStream(socket.getInputStream());
//                如果服务器没有发送Message对象,线程就会阻塞在这里
                Message message=(Message)(ois.readObject());
//如果读取到的是服务端返回的在线用户列表
                if(message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)){
                    //取出在线列表信息并显示
                    String[] onlineUsers=message.getContent().split(" ");
                    System.out.println("============当前在线用户列表=============");
                    for (int i = 0; i < onlineUsers.length; i++) {
                        System.out.println(onlineUsers[i]);
                    }
                }else if(message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){
                    //收到私聊信息
                    System.out.println("我"+message.getGetter()+"收到了来自"+message.getSender()+"的私聊信息:"+message.getContent());
                }else if(message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)){
                    //收到群发信息
                    System.out.println("我收到了来自"+message.getSender()+"的群发信息:"+message.getContent());
                }else if(message.getMesType().equals(MessageType.MESSAGE_NEWS)){
                    System.out.println("我收到了来自服务端推送的新闻:"+message.getContent());
                }
                else{
                    System.out.println("是其他类型的message,暂时不处理");
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public Socket getSocket() {
        return socket;
    }

    public void setSocket(Socket socket) {
        this.socket = socket;
    }
}

该文件全部源代码:

服务端

package com.hspedu.qqserver.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqcommon.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;

public class QQServer {
    private ServerSocket ss = null;
    //    创建一个集合,存放多个用户,如果是这些用户登录,就认为合法
    private static HashMap<String, User> validUsers = new HashMap<>();
    private static HashMap<String, ArrayList<Message>> offlineDb =new HashMap<>();
    static {//在静态代码块内初始化validUsers
        validUsers.put("100", new User("100", "123456"));
        validUsers.put("200", new User("200", "123456"));
        validUsers.put("300", new User("300", "123456"));
        validUsers.put("李东海", new User("李东海", "李赫宰"));
        validUsers.put("李赫宰", new User("李赫宰", "李东海"));
    }

    public static HashMap<String, ArrayList<Message>> getOfflineDb() {
        return offlineDb;
    }

    //    验证用户是否有效的方法
    private boolean checkUser(String userId, String password) {
        User user = validUsers.get(userId);
        if (user != null && user.getPassword().equals(password)) {
            return true;
        }
        return false;
    }


    public QQServer() {
        System.out.println("服务端在9999端口监听...");
        try {
            ss = new ServerSocket(9999);

            SendNewsToAllService sendNewsToAllService = new SendNewsToAllService();
            sendNewsToAllService.start();


            //当和某个客户端建立连接之后,会继续监听,因为应该用while循环
            while (true) {
                Socket socket = ss.accept();
                //得到socket关联的对象输入流
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //得到socket关联的对象输出流
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                User user = (User) (ois.readObject());
//            验证
                Message message = new Message();
                if (checkUser(user.getUserId(), user.getPassword())) {
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
//                回复客户端一个Message对象
                    oos.writeObject(message);
//创建一个线程,和客户端保持通信,该线程需要持有socket对象
                    ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, user.getUserId());
//               启动线程
                    serverConnectClientThread.start();
//           把该线程对象放到集合中管理
                    ManageClientThreads.addClientThread(user.getUserId(), serverConnectClientThread);

//                    将离线消息都发过去
                    oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                    if(offlineDb.containsKey(user.getUserId())){
                        ArrayList<Message> messages = offlineDb.get(user.getUserId());
                        for (Message offlinems:messages
                        ) {
                            System.out.println(offlinems.getContent());
                            oos.writeObject(offlinems);
                        }
                        offlineDb.remove(user.getUserId());
                        System.out.println(user.getUserId()+"上线了,服务端已经转发完成所有离线消息");
                    }


                } else {
                    System.out.println("登录失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    socket.close();
                }
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

// 客户端全部代码都在上面放了

原因分析:

分析过程:

刚开始,我觉得很奇怪,按道理来说要不就都收不到,要不就都收到,怎么会只收到一条正确离线信息呢?
后来我看到了这两位大佬的文章,我才知道是ObjectOutputStream和ObjectInputStream Header的问题。
在此也给出两位大佬的文章链接,方便查阅。
链接1必看,解释的很到位!!!
链接2作为补充。

分析原因:

原因的关键就是在于,ObjectOutputStream每次new的时候都会调用writeStreamHeader()方法写入4个字节的StreamHeader。源码如下:

public ObjectOutputStream(OutputStream out) throws IOException {  
        verifySubclass();  
        bout = new BlockDataOutputStream(out);  
        handles = new HandleTable(10, (float) 3.00);  
        subs = new ReplaceTable(10, (float) 3.00);  
        enableOverride = false;  
        writeStreamHeader();  
        bout.setBlockDataMode(true);  
        if (extendedDebugInfo) {  
            debugInfoStack = new DebugTraceInfoStack();  
        } else {  
            debugInfoStack = null;  
        }  
    }  

同样的,ObjectInputStream每次new的时候都会调用readStreamHeader()方法读入4个字节的StreamHeader,标记这是对象处理流。源码如下:

public ObjectInputStream(InputStream in) throws IOException {  
        verifySubclass();  
        bin = new BlockDataInputStream(in);  
        handles = new HandleTable(10);  
        vlist = new ValidationList();  
        enableOverride = false;  
        readStreamHeader();  
        bin.setBlockDataMode(true);  
    }  

理解了原理之后,再看看我自己写的代码:每次读入我都是先在client端先new一个新的ObjectInputStream

while(true){
            System.out.println("客户端线程,等待读取从服务器端发送过来的消息");
            try {
                ois= new ObjectInputStream(socket.getInputStream());
//                如果服务器没有发送Message对象,线程就会阻塞在这里
                Message message=(Message)(ois.readObject());

而我在server端的写入多次,只new过一个ObjectOutputStream:

//                    将离线消息都发过去
                    oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                    if(offlineDb.containsKey(user.getUserId())){
                        ArrayList<Message> messages = offlineDb.get(user.getUserId());
                        for (Message offlinems:messages
                        ) {
                            //oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                            System.out.println(offlinems.getContent());
                            oos.writeObject(offlinems);
                        }
                        offlineDb.remove(user.getUserId());
                        System.out.println(user.getUserId()+"上线了,服务端已经转发完成所有离线消息");
                    }

这样导致客户端读入对象的时候,每次先读Header,第一个离线留言是有头部的,因为第一次new ObjectSteam会给它加入头部,可是后面的object写入的时候都没有头部,而我客户端想要读入第二个对象的头部的时候,你并没有提供,我就以为内容就是头部,导致出错。
在这里插入图片描述


解决方案(两个):

  1. 直接在服务端new多个ObjectOutputStream,每次传对象就new一个。
//                    将离线消息都发过去
                    //oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                    if(offlineDb.containsKey(user.getUserId())){
                        ArrayList<Message> messages = offlineDb.get(user.getUserId());
                        for (Message offlinems:messages
                        ) {
                         oos=new ObjectOutputStream(ManageClientThreads.getClientThread(user.getUserId()).getSocket().getOutputStream());
                            System.out.println(offlinems.getContent());
                            oos.writeObject(offlinems);
                        }
                        offlineDb.remove(user.getUserId());
                        System.out.println(user.getUserId()+"上线了,服务端已经转发完成所有离线消息");
                    }
  1. 服务端直接把整个Arraylist放到一个new Message()对象的content中,客户端收到整个ArrayList再打开一个个Message执行相应工作。
  • 35
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值