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 }
View Code
技术分享
 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 }
View Code
技术分享
 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 }
View Code
技术分享
 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 }
View Code 
技术分享
 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 }
View Code

 对于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聊天程序——客户端的主要内容,如果未能解决你的问题,请参考以下文章

C++语言实现网络聊天程序的设计与实现(基于TCP/IP协议的SOCKET编程)超详细(代码+解析)

Socket聊天程序——客户端

一次与两个用户随机聊天(Socket.io)

Socket聊天程序——Common

一个hello/hi的简单的网络聊天程序

Python学习:Socket编程——多客户端通信聊天室