一、项目简述 这是一个即时通信软件的简单实现,通过自定义协议实现登录、退出等控制命令,即时通信软件需要有服务器端与客户端。
二、自定义协议 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(); } }五、运行效果 界面做的比较粗糙,效果如下:
