Java Socket实现简单的即时通信

一、项目简述
这是一个即时通信软件的简单实现,通过自定义协议实现登录、退出等控制命令,即时通信软件需要有服务器端与客户端。

二、自定义协议
1.Protocol协议实体类,封装了消息类型以及发送消息、解析消息的方法,Protocol.java代码如下:

package myutil;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
 * 协议工具类
 * 封装了消息的类型以及发和收的方法
 * @author Administrator
 *
 */
public class Protocol {
    // text
    public static final int TYPE_TEXT = 1;

    // 登录
    public static final int TYPE_LOAD = 2;

    // 退出
    public static final int TYPE_LOGOUT = 3;

    //登录成功
    public static final int TYPE_LOADSUCCESS = 4;

    //退出成功
    public static final int TYPE_LOGOUTSUCCESS = 5;

    /**
     * 向输出流中发送消息
     * @param type 消息类型
     * @param bytes 消息内容
     * @param dos 输出流
     */
    public static void send(int type, byte[] bytes, DataOutputStream dos){
        int totalLen = 1 + 4 + bytes.length;
        try {
            //依次读取消息的三个部分
            dos.writeByte(type);
            dos.writeInt(totalLen);
            dos.write(bytes);
            dos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从输入流中解析消息
     * @param dis 输入流
     * @return 解析之后的结果
     */
    public static Result getResult(DataInputStream dis) {
        byte type;
        try {
            //依次取出消息的三个部分
            type = dis.readByte();
            int totalLen = dis.readInt();
            byte[] bytes = new byte[totalLen - 4 - 1];
            dis.readFully(bytes);
            //返回解析结果
            return new Result(type & 0xFF, totalLen, bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.Result是结果类,封装了一次协议解析数据的结果对象,Result.java代码如下:

package myutil;

/**
 * 封装一个消息,亦是一次解析的结果
 */
public class Result {
        //消息类型
        private int type;

        //消息总长度
        private int totalLen;

        //消息内容
        private byte[] data;

        //以消息的三个部分构造一个消息实体
        public Result(int type, int totalLen, byte[] data) {
            super();
            this.type = type;
            this.totalLen = totalLen;
            this.data = data;
        }

        //以下是setter、getter方法
        public int getType() {
            return type;
        }
        public void setType(int type) {
            this.type = type;
        }
        public int getTotalLen() {
            return totalLen;
        }
        public void setTotalLen(int totalLen) {
            this.totalLen = totalLen;
        }
        public byte[] getData() {
            return data;
        }
        public void setData(byte[] data) {
            this.data = data;
        }
}

三、服务器端
1.Server类是服务器类,负责接收客户端连接请求并将连接上的客户端套接字交付给服务器端线程类ServerThread,Server.java代码如下:

package Server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
 * 服务器类
 * 负责接受客户端的连接
 * 将客户端的连接交付给服务器端线程处理
 */
public class Server {
    //维护客户端的配置信息
    public static List<Map<String,Object>> clients=new ArrayList<>();

    //主方法
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(30000);
            while (true) {
                Socket socket = serverSocket.accept();
                new Thread(new ServerThread(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.ServerThread服务器端线程类,负责处理单个的客户端套接字,向其发送消息以及接收其发送的消息,ServerThread.java代码如下:

package Server;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import myutil.Protocol;
import myutil.Result;

/**
 * 服务器端线程
 * 负责与客户端通信
 * @author Administrator
 *
 */
public class ServerThread implements Runnable{
    //套接字
    public Socket socket;

    //输入、输出流
    public DataInputStream dis=null;
    public DataOutputStream dos=null;

    //用户昵称
    public String userName=null;

    //用户配置信息的Map
    public Map<String, Object> thisMap=null;

    //标志线程是否生存
    public boolean isLive=true;

    /**
     * 构造服务器端线程实体
     * 初始化输入、输出流
     * @param socket 客户端套接字
     */
    public ServerThread(Socket socket){
        this.socket=socket;
        try {
            dis=new DataInputStream(socket.getInputStream());
            dos=new DataOutputStream(socket.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 线程体
     */
    public void run() {
        while(isLive){
            //解析消息
            Result result = null;
            result = Protocol.getResult(dis);
            if(result!=null)
            //按类型处理
            handleType(result.getType(),result.getData());
        }
    }

    /**
     * 根据消息类型执行相应操作
     * @param type 类型
     * @param data 消息内容
     */
    public void handleType(int type, byte[] data) {
        switch (type) {
        case 1:
            //遍历集合,获取输出流
            //向所有用户转发消息
            for(int i=0;i<Server.clients.size();i++){
                System.out.println("message:"+new String(data));
                DataOutputStream  dos2=(DataOutputStream) Server.clients.get(i).get("dos");
                String msg=new String(data);
                Protocol.send(Protocol.TYPE_TEXT,(userName+"说:"+msg).getBytes(),dos2);
            }
            break;
        case 2:
            //设置配置信息并添加至服务器端的集合中
            userName=new String(data);
            Map<String,Object> map=new HashMap<>();
            map.put("dos",dos);
            map.put("user",userName);
            Server.clients.add(map);

            //通知所有用户有人登陆聊天室
            thisMap=map;
            for(int i=0;i<Server.clients.size();i++){
                DataOutputStream  dos2=(DataOutputStream) Server.clients.get(i).get("dos");
                Protocol.send(Protocol.TYPE_LOADSUCCESS, ("   系统:"+userName+"进入聊天室").getBytes(), dos2);
            }
            break;
        case 3:
            //告知所有用户有人要退出聊天室
            for(int i=0;i<Server.clients.size();i++){
                DataOutputStream  dos2=(DataOutputStream) Server.clients.get(i).get("dos");
                Protocol.send(Protocol.TYPE_LOGOUTSUCCESS, ("   系统:"+userName+"退出聊天室").getBytes(), dos2);
            }
            //删除集合中保存的该客户端信息
            Server.clients.remove(thisMap);
            isLive=false;
            break;
        default:
            break;
        }
    }
}

四、客户端
1.Client是客户端,负责发送连接请求,Client.java代码如下:

package client;

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

import myutil.Protocol;

/**
 * 封装客户端与服务器通信的细节
 */
public class Client {

    //套接字
    Socket socket;

    //输出流
    DataOutputStream dos = null;

    /**
     * 连接服务器并初始化输出流
     * 开启客户端线程负责消息的接收
     * @param address 服务器IP地址
     * @param port 服务器端口号
     */
    public void conn(String address, int port) {
        try {
            socket = new Socket(address, port);
            dos = new DataOutputStream(socket.getOutputStream());
            new ClientThread(socket).start();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 登录
     * @param user 用户昵称
     */
    public void load(String user) {
        Protocol.send(Protocol.TYPE_LOAD,user.getBytes(), dos);
    }

    /**
     * 发送消息
     * @param msg 消息内容
     */
    public void sendMsg(String msg) {
        Protocol.send(Protocol.TYPE_TEXT, msg.getBytes(), dos);
    }

    /**
     * 退出
     */
    public void logout(){
        Protocol.send(Protocol.TYPE_LOGOUT, "logout".getBytes(), dos);
    }

    /**
     * 关闭客户端,释放掉资源
     */
    public void close() {
        // 向服务器发送退出命令
        Protocol.send(Protocol.TYPE_LOGOUT, new String("logout").getBytes(), dos);
        // 关闭资源
        try {
            if (dos != null)
                dos.close();
            if (socket != null)
                socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.ClientThread客户端线程类,负责接收服务器端发送的消息,ClientThread.java代码如下:

package client;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

import myutil.Protocol;
import myutil.Result;

/**
 * 客户端消息线程
 * 用以接收服务器消息
 * @author Administrator
 *
 */
public class ClientThread extends Thread {
    private Socket socket;//套接字
    private DataInputStream dis;//输入流

    //初始化套接字与输入流
    public ClientThread(Socket socket) {
        this.socket=socket;
        try {
            dis=new DataInputStream(socket.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(true){
            //解析消息
            Result result =  Protocol.getResult(dis);
            if(result!=null)
            //根据消息类型处理
            handleType(result.getType(),result.getData());
        }
    }

    /**
     * 根据消息的类型对消息处理
     * @param type 消息类型
     * @param data 消息内容
     */
    private void handleType(int type, byte[] data) {
        SimpleDateFormat df=new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
        String time=df.format(new Date());
        switch (type) {
        case 1:
            //文本
            String[] args=new String(data).split("说:");

            View.area.append("  "+args[0]+"("+time+")\n  "+args[1]+"\n");
            break;
        case 4:
            View.area.append("  "+new String(data)+"\n");
            break;
        case 5:
            View.area.append("  "+new String(data)+"\n");
        default:
            break;
        }
        View.area.select(View.area.getText().length(), View.area.getText().length());
    }
}

3.View视图类,负责与用户交互获取发送的消息以及显示服务器端反馈的消息,View.java代码如下:

package client;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;

import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

/**
 * 聊天视图
 * 
 * @author Administrator
 *
 */
public class View {

    // 窗口属性值
    private final int WIDTH = 600;
    private final int HEIGHT = 500;

    // 聊天记录文本域
    public static JTextArea area;

    // 客户端实体对象
    Client client=new Client();

    /**
     * 创建一个视图
     */
    public void create() {
        // 连接服务器
        client.conn("127.0.0.1", 30000);

        //窗口
        JFrame frame = new JFrame("聊天程序");

        // 登录面板
        JPanel loadPanel = new JPanel();
        loadPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
        frame.add(loadPanel, BorderLayout.NORTH);

        // 标签以及输入框
        final JLabel userLabel = new JLabel("   用户未登录");
        final JTextField userTextField = new JTextField(20);
        //添加
        loadPanel.add(userLabel);
        loadPanel.add(userTextField);

        //设置回车登录事件
        userTextField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (e.getKeyCode() == 10) {
                    String user = userTextField.getText();
                    if (user != null && !user.equals("")) {
                        client.load(user);
                        userLabel.setText("   user:" + user);
                        userTextField.setText("");
                        userTextField.setVisible(false);
                    }
                }
            }
        });

        // 聊天记录面板
        JPanel topPanel = new JPanel();
        loadPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
        // 聊天记录文本域
        area = new JTextArea(14, 51);
        area.setEditable(false);
        // 滚动条
        JScrollPane jsp = new JScrollPane(area, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        //添加
        frame.add(topPanel);
        topPanel.add(jsp);

        // 底部输入面板
        JPanel bottomPanel = new JPanel();
        frame.add(bottomPanel, BorderLayout.SOUTH);
        bottomPanel.setPreferredSize(new Dimension(WIDTH, 165));
        // 文本域
        final JTextArea ta = new JTextArea();
        ta.setBorder(BorderFactory.createLineBorder(Color.darkGray));
        ta.setFont(new Font("宋体", Font.PLAIN, 15));
        ta.setPreferredSize(new Dimension(WIDTH - 35, 100));
        ta.setText("//输入聊天内容");
        ta.select(0, 0);
        ta.setLineWrap(true);
        //设置回车发送消息
        ta.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                    if (!ta.getText().equals("") && userLabel.getText().indexOf("user:") != -1) {
                        client.sendMsg(ta.getText());
                        ta.setText("");
                    } else {
                        System.out.println("用户未登录或内容为空");
                    }
                    e.consume();
                }
            }
        });

        //输入聊天输入框随鼠标的动态效果
        ta.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                if (ta.getText().equals("") || ta.getText().equals("//输入聊天内容"))
                    ta.setText("");
            }

            @Override
            public void mouseExited(MouseEvent e) {
                if (ta.getText().equals(""))
                    ta.setText("//输入聊天内容");
            }
        });

        // 按钮面板
        JPanel buttonPanel = new JPanel();
        buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20));
        buttonPanel.setPreferredSize(new Dimension(WIDTH, 50));
        buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));

        // 按钮
        JButton sendButton = new JButton("发送");
        buttonPanel.add(sendButton);
        sendButton.setFocusPainted(false);
        //添加按钮点击发送事件
        sendButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (ta.getText() != null && ta.getText().length() != 0 && userLabel.getText().indexOf("user:") != -1) {
                    client.sendMsg(ta.getText());
                    ta.setText("");
                } else {
                    System.out.println("用户未登录或内容为空");
                }
            }
        });

        // 底部面板添加控件
        bottomPanel.add(ta);
        bottomPanel.add(buttonPanel);
        //添加窗口关闭自动退出系统事件
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                client.logout();
            }
        });

        // 窗口设置
        frame.setSize(WIDTH, HEIGHT);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    /**
     * 主函数,程序的入口
     * 执行视图的实例化
     * @param args
     */
    public static void main(String[] args) {
        View view=new View();
        view.create();
    }
}

五、运行效果
界面做的比较粗糙,效果如下:
这里写图片描述

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值