Socket聊天程序——客户端
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket聊天程序——客户端相关的知识,希望对你有一定的参考价值。
写在前面:
上周末抽点时间把自己写的一个简单Socket聊天程序的初始设计和服务端细化设计记录了一下,周二终于等来毕业前考的软考证书,然后接下来就是在加班的日子度过了,今天正好周五,打算把客户端的详细设计和Common模块记录一下,因为这个周末开始就要去忙其他东西了。
设计:
客户端设计主要分成两个部分,分别是socket通讯模块设计和UI相关设计。
客户端socket通讯设计:
这里的设计其实跟服务端的设计差不多,不同的是服务端是接收心跳包,而客户端是发送心跳包,由于客户端只与一个服务端进行通讯(客户端之间的通讯也是由服务端进行分发的),所以这里只使用了一个大小为2的线程池去处理这两件事(newFixedThreadPool(2)),对应的处理类分别是ReceiveListener、KeepAliveDog,其中ReceiveListener在初始化的时候传入一个Callback作为客户端收到服务端的消息的回调,Callback的默认实现是DefaultCallback,DefaultCallback根据不同的事件通过HF分发给不同Handler去处理,而ClientHolder则是存储当前客户端信息,设计如下:
Socket通讯模块具体实现:
[Client.java]
Client是客户端连接服务端的入口,创建Client需要指定一个Callback作为客户端接收服务端消息时的回调,然后由Client的start()方法启动对服务端的监听(ReceiveListener),当ReceiveListener接收到服务端发来的数据时,调用回调(Callback)的doWork()方法去处理;同时Client中还需要发送心跳包来通知服务端自己还在连接着服务端,发心跳包由Client中keepAlive()启动,由KeepAliveDog实现;这两个步骤由一个固定大小为2为线程池newFixedThreadPool(2)去执行,可能这里使用一个newFixedThreadPool(1)和newScheduledThreadPool(1)去处理更合理,因为心跳包是定时发的,服务端就是这样实现的(这个后续调整),Client的具体代码如下(这里暴露了另外两个方法用于获取socket和当前socket所属的用户):
1 /** 2 * 客户端 3 * @author yaolin 4 * 5 */ 6 public class Client { 7 8 private final Socket socket; 9 private String from; 10 private final ExecutorService pool; 11 private final Callback callback; 12 13 public Client(Callback callback) throws IOException { 14 this.socket = new Socket(ConstantValue.SERVER_IP, ConstantValue.SERVER_PORT); 15 this.pool = Executors.newFixedThreadPool(2); 16 this.callback = callback; 17 } 18 19 public void start() { 20 pool.execute(new ReceiveListener(socket, callback)); 21 } 22 23 public void keepAlive(String from) { 24 this.from = from; 25 pool.execute(new KeepAliveDog(socket, from)); 26 } 27 28 public Socket getSocket() { 29 return socket; 30 } 31 32 public String getFrom() { 33 return from; 34 } 35 }
[KeepAliveDog.java]
客户端在与服务端建立连接之后(该程序中是指登陆成功之后,因为登陆成功之后客户端的socket才会被服务端的SocketHolder管理),需要每个一段时间就给服务端发送心跳包告诉服务端自己还在跟服务端保持联系,不然服务端会在一段时间之后将没有交互的socket丢弃(详见服务端那篇博客),KeepAliveDog的代码实现如下(后期可能会调整为newScheduledThreadPool(1),所以这里的代码也会调整):
1 /** 2 * KeepAliveDog : tell Server this client is running; 3 * 4 * @author yaolin 5 */ 6 public class KeepAliveDog implements Runnable { 7 8 private final Socket socket; 9 private final String from; 10 11 public KeepAliveDog(Socket socket, String from) { 12 this.socket = socket; 13 this.from = from; 14 } 15 16 @Override 17 public void run() { 18 while (socket != null && !socket.isClosed()) { 19 try { 20 21 PrintWriter out = new PrintWriter(socket.getOutputStream()); 22 AliveMessage message = new AliveMessage(); 23 message.setFrom(from); 24 out.println(JSON.toJSON(message)); 25 out.flush(); 26 27 Thread.sleep(ConstantValue.KEEP_ALIVE_PERIOD * 1000); 28 29 } catch (Exception e) { 30 LoggerUtil.error("Client send message failed !" + e.getMessage(), e); 31 } 32 } 33 } 34 }
[ReceiveListener.java]
Client的start()方法启动对服务端的监听由ReceiveListener实现,ReceiveListener接收到服务端的消息之后会回调Callback的doWork()方法,让回调去处理具体的业务逻辑,所以ReceiveListener只负责监听服务端的消息,具体的处理由Callback负责,这里需要提一下的是当消息类型是文件类型的时候会睡眠配置执行的间隔时间,这样Callback中的doWork才能对读取来至服务端的文件流,而不是直接进入下一次循环,这里的设计跟服务端是类似的。ReceiveListener的具体实现代码如下:
1 public class ReceiveListener implements Runnable { 2 3 private final Socket socket; 4 private final Callback callback; 5 6 public ReceiveListener(Socket socket, Callback callback) { 7 this.socket = socket; 8 this.callback = callback; 9 } 10 11 @Override 12 public void run() { 13 if (socket != null) { 14 while (!socket.isClosed()) { 15 try { 16 InputStream is = socket.getInputStream(); 17 String line = null; 18 StringBuffer sb = null; 19 20 if (is.available() > 0) { 21 22 BufferedReader bufr = new BufferedReader(new InputStreamReader(is)); 23 sb = new StringBuffer(); 24 while (is.available() > 0 && (line = bufr.readLine()) != null) { 25 sb.append(line); 26 } 27 LoggerUtil.trach("RECEIVE [" + sb.toString() + "] AT " + new Date()); 28 29 callback.doWork(socket, sb.toString()); 30 BaseMessage message = JSON.parseObject(sb.toString(), BaseMessage.class); 31 if (message.getType() == MessageType.FILE) { 32 // PAUSE TO RECEIVE FILE 33 LoggerUtil.trach("CLIENT:PAUSE TO RECEIVE FILE"); 34 Thread.sleep(ConstantValue.MESSAGE_PERIOD); 35 } 36 } else { 37 Thread.sleep(ConstantValue.MESSAGE_PERIOD); 38 } 39 } catch (Exception e) { 40 LoggerUtil.error("Client send message failed !" + e.getMessage(), e); 41 } 42 } 43 } 44 } 45 46 }
[Callback.java、DefaultCallback.java]
从上面可以看出Client对消息的处理是Callback回调,其Callback只是一个接口,所有Callback实现该接口根据自己的需要对消息进行相应地处理,这里Callback默认的实现是DefaultCallback,DefaultCallback只对三种消息进行处理,分别是聊天消息、文件消息、返回消息。对于聊天消息,DefaultCallback将通过UI中的Router路由获取到相应的界面(详见下面的UI设计),然后将消息展现在对应的聊天框中;对于文件消息,DefaultCallback则是将文件写入到配置中指定的路径中(这里没有通过用户的允许就接收文件,这种设计不是很友好,目前先这样);对于返回消息,DefaultCallback会根据返回消息中的KEY叫给不同的Handler去处理。具体代码如下:
1 public interface Callback { 2 public void doWork(Socket server, Object data); 3 }
1 public class DefaultCallback implements Callback { 2 3 @Override 4 public void doWork(Socket server, Object data) { 5 if (data != null) { 6 BaseMessage message = JSON.parseObject(data.toString(), BaseMessage.class); 7 switch (message.getType()) { 8 case MessageType.CHAT: 9 handleChatMessage(data); 10 break; 11 case MessageType.FILE: 12 handleFileMessage(server, data); 13 break; 14 case MessageType.RETURN: 15 handleReturnMessage(data); 16 break; 17 } 18 } 19 } 20 21 private void handleChatMessage(Object data) { 22 ChatMessage m = JSON.parseObject(data.toString(), ChatMessage.class); 23 String tabKey = m.getFrom();// FROM 24 JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.CHATTABBED); 25 if (comp instanceof JTabbedPane) { 26 JTabbedPane tab = (JTabbedPane) comp; 27 int index = tab.indexOfTab(tabKey); 28 if (index == -1) { 29 tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane()); 30 } 31 JTextArea textArea = ResultHolder.get(tabKey).getTextArea(); 32 textArea.setText(new StringBuffer() 33 .append(textArea.getText()).append(System.lineSeparator()).append(System.lineSeparator()) 34 .append(" [").append(m.getOwner()).append("] : ").append(System.lineSeparator()) 35 .append(m.getContent()) 36 .toString()); 37 // SCROLL TO BOTTOM 38 textArea.setCaretPosition(textArea.getText().length()); 39 } 40 } 41 42 private void handleFileMessage(Socket server, Object data) { 43 FileMessage message = JSON.parseObject(data.toString(), FileMessage.class); 44 if (message.getSize() > 0) { 45 OutputStream os = null; 46 try { 47 if (server != null) { 48 InputStream is = server.getInputStream(); 49 File dir = new File(ConstantValue.CLIENT_RECEIVE_DIR); 50 if (!dir.exists()) { 51 dir.mkdirs(); 52 } 53 os = new FileOutputStream( 54 new File(PathUtil.combination(ConstantValue.CLIENT_RECEIVE_DIR, new Date().getTime() + message.getName()))); 55 int total = 0; 56 while (!server.isClosed()) { 57 if (is.available() > 0) { 58 byte[] buff = new byte[ConstantValue.BUFF_SIZE]; 59 int len = -1; 60 while (is.available() > 0 && (len = is.read(buff)) != -1) { 61 os.write(buff, 0, len); 62 total += len; 63 LoggerUtil.debug("RECEIVE BUFF [" + len + "]"); 64 } 65 os.flush(); 66 if (total >= message.getSize()) { 67 LoggerUtil.info("RECEIVE BUFF [OK]"); 68 break; 69 } 70 } 71 } 72 } 73 } catch (Exception e) { 74 LoggerUtil.error("Receive file failed ! " + e.getMessage(), e); 75 } finally { 76 if (os != null) { 77 try { 78 os.close(); 79 } catch (Exception ignore) { 80 } 81 os = null; 82 } 83 } 84 } 85 } 86 87 private void handleReturnMessage(Object data) { 88 ReturnMessage m = JSON.parseObject(data.toString(), ReturnMessage.class); 89 if (StringUtil.isNotEmpty(m.getKey())) { 90 switch (m.getKey()) { 91 case Key.NOTIFY: // Notify client to update usr list 92 HF.getHandler(Key.NOTIFY).handle(data); 93 break; 94 case Key.LOGIN: 95 HF.getHandler(Key.LOGIN).handle(data); 96 break; 97 case Key.REGISTER: 98 HF.getHandler(Key.REGISTER).handle(data); 99 break; 100 case Key.LISTUSER: 101 HF.getHandler(Key.LISTUSER).handle(data); 102 break; 103 case Key.TIP: 104 HF.getHandler(Key.TIP).handle(data); 105 break; 106 } 107 } 108 } 109 }
[Handler.java、HF.java、ListUserHdl.java...]
Handler组件负责对服务端返回消息类型的消息进行处理,DefaultCallback根据不同的KEY将消息分发给不同的Handler进行处理,这里也算一套简单的工厂组件吧,跟服务端处理接收到的数据设计是类似的,完整的类图如下:
下面给出这一块的代码,为了缩小篇幅,将所有Handler实现的代码收起来。
1 public interface Handler { 2 public Object handle(Object obj); 3 }
1 public class HF { 2 3 public static Handler getHandler(String key) { 4 switch (key) { 5 case Key.NOTIFY: 6 return new NotifyHdl(); 7 case Key.LOGIN: 8 return new LoginHdl(); 9 case Key.REGISTER: 10 return new RegisterHdl(); 11 case Key.LISTUSER: 12 return new ListUserHdl(); 13 case Key.TIP: 14 return new TipHdl(); 15 } 16 return null; 17 } 18 }
1 public class ListUserHdl implements Handler { 2 3 @Override 4 public Object handle(Object obj) { 5 if (obj != null) { 6 try { 7 ReturnMessage rm = JSON.parseObject(obj.toString(), ReturnMessage.class); 8 if (rm.isSuccess() && rm.getContent() != null) { 9 ClientListUserDTO dto = JSON.parseObject(rm.getContent().toString(), ClientListUserDTO.class); 10 JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.LISTUSRLIST); 11 if (comp instanceof JList) { 12 @SuppressWarnings("unchecked") // 13 JList<String> listUsrList = (JList<String>) comp; 14 List<String> listUser = new LinkedList<String>(); 15 listUser.addAll(dto.getListUser()); 16 Collections.sort(listUser); 17 listUser.add(0, ConstantValue.TO_ALL); 18 listUsrList.setListData(listUser.toArray(new String[]{})); 19 } 20 } 21 } catch (Exception e) { 22 LoggerUtil.error("Handle listUsr failed! " + e.getMessage(), e); 23 } 24 } 25 return null; 26 } 27 28 }
1 public class LoginHdl implements Handler { 2 3 @Override 4 public Object handle(Object obj) { 5 if (obj != null) { 6 try { 7 ReturnMessage rm = JSON.parseObject(obj.toString(),ReturnMessage.class); 8 if (rm.isSuccess()) { 9 Router.getView(RegisterAndLoginView.class).trash(); 10 Router.getView(ChatRoomView.class).create().display(); 11 ClientHolder.getClient().keepAlive(rm.getTo()); // KEEP... 12 } else { 13 Container container = Router.getView(RegisterAndLoginView.class).container(); 14 if (container != null) { 15 // show error 16 JOptionPane.showMessageDialog(container, rm.getMessage()); 17 } 18 } 19 } catch (Exception e) { 20 LoggerUtil.error("Handle login failed! " + e.getMessage(), e); 21 } 22 } 23 return null; 24 } 25 26 }
1 public class NotifyHdl implements Handler { 2 3 @Override 4 public Object handle(Object obj) { 5 if (obj != null) { 6 try { 7 ReturnMessage rm = JSON.parseObject(obj.toString(), ReturnMessage.class); 8 if (rm.isSuccess() && rm.getContent() != null) { 9 ClientNotifyDTO dto = JSON.parseObject(rm.getContent().toString(), ClientNotifyDTO.class); 10 JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.LISTUSRLIST); 11 if (comp instanceof JList) { 12 @SuppressWarnings("unchecked") // 13 JList<String> listUsrList = (JList<String>) comp; 14 List<String> listUser = modelToList(listUsrList.getModel()); 15 if (dto.isFlag()) { 16 if (!listUser.contains(dto.getUsername())) { 17 listUser.add(dto.getUsername()); 18 listUser.remove(ConstantValue.TO_ALL); 19 Collections.sort(listUser); 20 listUser.add(0, ConstantValue.TO_ALL); 21 } 22 } else { 23 listUser.remove(dto.getUsername()); 24 } 25 listUsrList.setListData(listUser.toArray(new String[]{})); 26 } 27 } 28 } catch (Exception e) { 29 LoggerUtil.error("Handle nofity failed! " + e.getMessage(), e); 30 } 31 } 32 return null; 33 } 34 35 private List<String> modelToList(ListModel<String> listModel) { 36 List<String> list = new LinkedList<String>(); 37 if (listModel != null) { 38 for (int i = 0; i < listModel.getSize(); i++) { 39 list.add(listModel.getElementAt(i)); 40 } 41 } 42 return list; 43 } 44 }
1 public class RegisterHdl implements Handler { 2 3 @Override 4 public Object handle(Object obj) { 5 if (obj != null) { 6 try { 7 ReturnMessage rm = JSON.parseObject(obj.toString(),ReturnMessage.class); 8 Container container = Router.getView(RegisterAndLoginView.class).container(); 9 if (container != null) { 10 if (rm.isSuccess()) { 11 JOptionPane.showMessageDialog(container, rm.getContent()); 12 } else { 13 JOptionPane.showMessageDialog(container, rm.getMessage()); 14 } 15 } 16 } catch (Exception e) { 17 LoggerUtil.error("Handle register failed! " + e.getMessage(), e); 18 } 19 } 20 return null; 21 } 22 23 }
1 public class TipHdl implements Handler { 2 3 @Override 4 public Object handle(Object obj) { 5 if (obj != null) { 6 try { 7 ReturnMessage m = JSON.parseObject(obj.toString(), ReturnMessage.class); 8 if (m.isSuccess() && m.getContent() != null) { 9 String tabKey = m.getFrom(); 10 String tip = m.getContent().toString(); 11 JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.CHATTABBED); 12 if (comp instanceof JTabbedPane) { 13 JTabbedPane tab = (JTabbedPane) comp; 14 int index = tab.indexOfTab(tabKey); 15 if (index == -1) { 16 tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane()); 17 } 18 JTextArea textArea = ResultHolder.get(tabKey).getTextArea(); 19 textArea.setText(new StringBuffer() 20 .append(textArea.getText()).append(System.lineSeparator()).append(System.lineSeparator()) 21 .append(" [").append(m.getOwner()).append("] : ").append(System.lineSeparator()) 22 .append(tip) 23 .toString()); 24 // SCROLL TO BOTTOM 25 textArea.setCaretPosition(textArea.getText().length()); 26 } 27 } 28 } catch (Exception e) { 29 LoggerUtil.error("Handle tip failed! " + e.getMessage(), e); 30 } 31 } 32 return null; 33 } 34 35 }
对于Socket通讯模块还有一个类,那就是ClientHolder,这个类用于存储当前Client,跟服务端的SocketHolder是类似的。
1 /** 2 * @author yaolin 3 */ 4 public class ClientHolder { 5 6 public static Client client; 7 8 public static Client getClient() { 9 return client; 10 } 11 12 public static void setClient(Client client) { 13 ClientHolder.client = client; 14 } 15 }
UI模块具体实现:
上面记录了socket通讯模块的设计,接下来记录一下UI的设计模块,我不打算自己写UI,毕竟自己写出来的太丑了,所以后期可能会叫同学或朋友帮忙敲一下,所以我将UI的事件处理都交由Action去处理,将UI设计和事件响应简单分离,所有UI继承JFrame并实现View接口,上面的Handler实现类通过Router获取(存在则直接返回,不存在则创建并存储)指定的UI,View中提供了UI的创建create()、获取container()、获取UI中的组件getComponent(),显示display(),回收trash();ResultWrapper和ResultHolder只是为了创建和存储聊天选项卡。设计如下:
[Router.java、View.java]
所有UI继承JFrame并实现View接口,Handler实现类通过Router获取(存在则直接返回,不存在则创建并存储)指定的UI,View中提供了UI的创建create()、获取container()、获取UI中的组件getComponent(),显示display(),回收trash(),具体实现如下:
1 /** 2 * View 路由 3 * @author yaolin 4 */ 5 public class Router { 6 7 private static Map<String, View> listRoute = new HashMap<String,View>(); 8 9 public static View getView(Class<?> clazz) { 10 View v = listRoute.get(clazz.getName()); 11 if (v == null) { 12 try { 13 v = (View) Class.forName(clazz.getName()).newInstance(); 14 listRoute.put(clazz.getName(), v); 15 } catch (Exception e) { 16 LoggerUtil.error("Create view failed! " + e.getMessage(), e); 17 } 18 } 19 return v; 20 } 21 }
1 /** 2 * 所有界面的规范接口 3 * @author yaolin 4 * 5 */ 6 public interface View { 7 8 /** 9 * 10 */ 11 public View create(); 12 13 /** 14 * 15 */ 16 public Container container(); 17 18 /** 19 * @param key 20 */ 21 public JComponent getComponent(String key); 22 23 /** 24 * 25 */ 26 public void display(); 27 28 /** 29 * 30 */ 31 public void trash(); 32 33 }
[RegisterAndLoginView.java、ChatRoomView.java]
由于不想自己写UI,我这里只是简单的写了两个UI界面,分别是注册和登陆界面、聊天界面,这里给出两个丑丑的界面:
注册登录界面
聊天界面
下面给出这两个这界面的具体代码:
1 /** 2 * 注册、登陆 3 * @author yaolin 4 */ 5 public class RegisterAndLoginView extends JFrame implements View { 6 7 private static final long serialVersionUID = 6322088074312546736L; 8 private final RegisterAndLoginAction action = new RegisterAndLoginAction(); 9 10 private static boolean CREATE = false; 11 12 @Override 13 public View create() { 14 if (! CREATE) { 15 init(); 16 CREATE = true; 17 } 18 return this; 19 } 20 21 public Container container() { 22 create(); 23 return getContentPane(); 24 } 25 26 @Override 27 public JComponent getComponent(String key) { 28 return null; 29 } 30 31 @Override 32 public void display() { 33 setVisible(true); 34 } 35 36 @Override 37 public void trash() { 38 dispose(); 39 } 40 41 42 private void init() { 43 // Attribute 44 setSize(500, 300); 45 setResizable(false); 46 setLocationRelativeTo(null); 47 48 // Container 49 JPanel panel = new JPanel(); 50 panel.setLayout(null); 51 52 // Component 53 // username 54 JLabel lbUsername = new JLabel(I18N.TEXT_USERNAME); 55 lbUsername.setBounds(100, 80, 200, 30); 56 final JTextField tfUsername = new JTextField(); 57 tfUsername.setBounds(150, 80, 230, 30); 58 panel.add(lbUsername); 59 panel.add(tfUsername); 60 // passsword 61 JLabel lbPassword = new JLabel(I18N.TEXT_PASSWORD); 62 lbPassword.setBounds(100, 120, 200, 30); 63 final JPasswordField pfPassword = new JPasswordField(); 64 pfPassword.setBounds(150, 120, 230, 30); 65 panel.add(lbPassword); 66 panel.add(pfPassword); 67 // btnRegister 68 JButton btnRegister = new JButton(I18N.BTN_REGISTER); 69 btnRegister.setBounds(100, 175, 80, 30); 70 // btnLogin 71 final JButton btnLogin = new JButton(I18N.BTN_LOGIN); 72 btnLogin.setBounds(200, 175, 80, 30); 73 // btnCancel 74 JButton btnExit = new JButton(I18N.BTN_EXIT); 75 btnExit.setBounds(300, 175, 80, 30); 76 panel.add(btnRegister); 77 panel.add(btnLogin); 78 panel.add(btnExit); 79 80 // Event 81 pfPassword.addKeyListener(new KeyAdapter() { 82 public void keyPressed(final KeyEvent e) { 83 if (e.getKeyCode() == KeyEvent.VK_ENTER) 84 btnLogin.doClick(); 85 } 86 });// end of addKeyListener 87 88 btnRegister.addActionListener(new ActionListener() { 89 public void actionPerformed(final ActionEvent e) { 90 if (StringUtil.isEmpty(tfUsername.getText()) 91 || StringUtil.isEmpty(new String(pfPassword.getPassword()))) { 92 JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_REGISTER_EMPTY_DATA); 93 return ; 94 } 95 action.handleRegister(tfUsername.getText(), new String(pfPassword.getPassword())); 96 } 97 });// end of addActionListener 98 99 btnLogin.addActionListener(new ActionListener() { 100 public void actionPerformed(final ActionEvent e) { 101 if (StringUtil.isEmpty(tfUsername.getText()) 102 || StringUtil.isEmpty(new String(pfPassword.getPassword()))) { 103 JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_LOGIN_EMPTY_DATA); 104 return ; 105 } 106 action.handleLogin(tfUsername.getText(), new String(pfPassword.getPassword())); 107 } 108 });// end of addActionListener 109 110 btnExit.addActionListener(new ActionListener() { 111 public void actionPerformed(final ActionEvent e) { 112 System.exit(0); 113 } 114 });// end of addActionListener 115 116 getContentPane().add(panel); 117 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 118 } 119 }
1 /** 2 * Client 聊天窗口 3 * 4 * @author yaolin 5 */ 6 public class ChatRoomView extends JFrame implements View { 7 8 private static final long serialVersionUID = -4515831172899054818L; 9 10 public static final String LISTUSRLIST = "LISTUSRLIST"; 11 public static final String CHATTABBED = "CHATTABBED"; 12 13 private static boolean CREATE = false; 14 private ChatRoomAction action = new ChatRoomAction(); 15 16 private JList<String> listUsrList = null; 17 private JTabbedPane chatTabbed = null; 18 19 @Override 20 public View create() { 21 if (!CREATE) { 22 init(); 23 CREATE = true; 24 } 25 return this; 26 } 27 28 public Container container() { 29 create(); 30 return getContentPane(); 31 } 32 33 @Override 34 public JComponent getComponent(String key) { 35 create(); 36 switch (key) { 37 case LISTUSRLIST: 38 return listUsrList; 39 case CHATTABBED: 40 return chatTabbed; 41 } 42 return null; 43 } 44 45 @Override 46 public void display() { 47 setVisible(true); 48 } 49 50 @Override 51 public void trash() { 52 dispose(); 53 } 54 55 public void init() { 56 setTitle(I18N.TEXT_APP_NAME); 57 setSize(800, 600); 58 setResizable(false); 59 setLocationRelativeTo(null); 60 61 setLayout(new BorderLayout()); 62 add(createChatPanel(), BorderLayout.CENTER); 63 add(createUsrListView(), BorderLayout.EAST); 64 65 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 66 } 67 68 private JComponent createChatPanel() { 69 // FILE SELECTOR 70 final JFileChooser fileChooser = new JFileChooser(); 71 72 JPanel panel = new JPanel(new BorderLayout()); 73 // CENTER 74 chatTabbed = new JTabbedPane(); 75 chatTabbed.addTab(ConstantValue.TO_ALL, ResultHolder.get(ConstantValue.TO_ALL).getScrollPane()); 76 panel.add(chatTabbed, BorderLayout.CENTER); 77 78 // SOUTH 79 JPanel south = new JPanel(new BorderLayout()); 80 // SOUTH - FILE 81 JPanel middle = new JPanel(new BorderLayout()); 82 middle.add(new JLabel(), BorderLayout.CENTER); // JUST FOR PADDING 83 JButton btnUpload = new JButton(I18N.BTN_SEND_FILE); 84 middle.add(btnUpload, BorderLayout.EAST); 85 south.add(middle, BorderLayout.NORTH); 86 // SOUTH - TEXTAREA 87 final JTextArea taSend = new JTextArea(); 88 taSend.setCaretColor(Color.BLUE); 89 taSend.setMargin(new Insets(10, 10, 10, 10)); 90 taSend.setRows(10); 91 south.add(taSend, BorderLayout.CENTER); 92 // SOUTH - BTN 93 JPanel bottom = new JPanel(new BorderLayout()); 94 bottom.add(new JLabel(), BorderLayout.CENTER); // JUST FOR PADDING 95 JButton btnSend = new JButton(I18N.BTN_SEND); 96 bottom.add(btnSend, BorderLayout.EAST); 97 98 south.add(bottom, BorderLayout.SOUTH); 99 100 btnUpload.addActionListener(new ActionListener() { 101 public void actionPerformed(final ActionEvent e) { 102 if (! ConstantValue.TO_ALL.equals(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()))) { 103 int returnVal = fileChooser.showOpenDialog(ChatRoomView.this); 104 if (returnVal == JFileChooser.APPROVE_OPTION) { 105 File file = fileChooser.getSelectedFile(); 106 action.upload(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()), file); 107 } 108 } else { 109 JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_FILE_TO_ALL_ERROR); 110 } 111 } 112 }); 113 114 btnSend.addActionListener(new ActionListener() { 115 public void actionPerformed(final ActionEvent e) { 116 if (StringUtil.isNotEmpty(taSend.getText())) { 117 action.send(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()), taSend.getText()); 118 taSend.setText(null); 119 } 120 } 121 }); 122 123 panel.add(south, BorderLayout.SOUTH); 124 return panel; 125 } 126 127 private JComponent createUsrListView() { 128 listUsrList = new JList<String>(); 129 listUsrList.setBorder(new LineBorder(Color.BLUE)); 130 listUsrList.setListData(new String[] { ConstantValue.TO_ALL }); 131 listUsrList.setFixedCellWidth(200); 132 listUsrList.setFixedCellHeight(30); 133 listUsrList.addListSelectionListener(new ListSelectionListener() { 134 @Override 135 public void valueChanged(ListSelectionEvent e) { // chat to 136 if (chatTabbed.indexOfTab(listUsrList.getSelectedValue()) == -1 137 && listUsrList.getSelectedValue() != null 138 && !listUsrList.getSelectedValue().equals(ClientHolder.getClient().getFrom())) { 139 chatTabbed.addTab(listUsrList.getSelectedValue(), 140 ResultHolder.get(listUsrList.getSelectedValue()).getScrollPane()); 141 chatTabbed.setSelectedIndex(chatTabbed.indexOfTab(listUsrList.getSelectedValue())); 142 } 143 } 144 }); 145 return listUsrList; 146 } 147 }
[RegisterAndLoginAction.java、ChatRoomAction.java]
这里UI的事件处理都交由Action去处理,将UI设计和事件响应简单分离,RegisterAndLoginView的事件由RegisterAndLoginAction处理,ChatRoomView的事件由ChatRoomAction处理。具体实现如下:
1 public class RegisterAndLoginAction { 2 3 public void handleRegister(String username, String password) { 4 if (StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) { 5 return; 6 } 7 RegisterMessage message = new RegisterMessage() 8 .setUsername(username) 9 .setPassword(password); 10 message.setFrom(username); 11 SendHelper.send(ClientHolder.getClient().getSocket(), message); 12 } 13 14 15 public void handleLogin(String username, String password) { 16 if (StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) { 17 return; 18 } 19 LoginMessage message = new LoginMessage() 20 .setUsername(username) 21 .setPassword(password); 22 message.setFrom(username); 23 SendHelper.send(ClientHolder.getClient().getSocket(), message); 24 } 25 }
对于UI设计还有两个类,分别是ResultHolder和ResultWrapper,ResultWrapper和ResultHolder只是为了创建和存储聊天选项卡,具体实现如下:
1 public class ResultWrapper { 2 3 private JScrollPane scrollPane; 4 private JTextArea textArea; 5 6 public ResultWrapper(JScrollPane scrollPane, JTextArea textArea) { 7 this.scrollPane = scrollPane; 8 this.textArea = textArea; 9 } 10 public JScrollPane getScrollPane() { 11 return scrollPane; 12 } 13 public void setScrollPane(JScrollPane scrollPane) { 14 this.scrollPane = scrollPane; 15 } 16 public JTextArea getTextArea() { 17 return textArea; 18 } 19 public void setTextArea(JTextArea textArea) { 20 this.textArea = textArea; 21 } 22 }
1 public class ResultHolder { 2 3 private static Map<String, ResultWrapper> listResultWrapper = new HashMap<String,ResultWrapper>(); 4 5 public static void put(String key, ResultWrapper wrapper) { 6 listResultWrapper.put(key, wrapper); 7 } 8 9 public static ResultWrapper get(String key) { 10 ResultWrapper wrapper = listResultWrapper.get(key); 11 if (wrapper == null) { 12 wrapper = create(); 13 put(key, wrapper); 14 } 15 return wrapper; 16 } 17 18 19 private static ResultWrapper create() { 20 JTextArea resultTextArea = new JTextArea(); 21 resultTextArea.setEditable(false); 22 resultTextArea.setBorder(new LineBorder(Color.BLUE)); 23 JScrollPane scrollPane = new JScrollPane(resultTextArea); 24 scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 25 scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); 26 ResultWrapper wrapper = new ResultWrapper(scrollPane, resultTextArea); 27 return wrapper; 28 } 29 }
最后的最后给出,客户端运行的入口:
1 /** 2 * 3 * @author yaolin 4 * 5 */ 6 public class NiloayChat { 7 8 public static void main(String[] args) { 9 View v = Router.getView(RegisterAndLoginView.class).create(); 10 try { 11 v.display(); 12 Client client = new Client(new DefaultCallback()); 13 client.start(); 14 ClientHolder.setClient(client); 15 } catch (IOException e) { 16 JOptionPane.showMessageDialog(v.container(), e.getMessage()); 17 } 18 } 19 }
未完,待续。
(本文所有的代码已经挂在csdn的仓库中 :地址 https://code.csdn.net/yaoIin/yl-chat ,完整代码参考这里,有时间将更新)
以上是关于Socket聊天程序——客户端的主要内容,如果未能解决你的问题,请参考以下文章