详解基于java的Socket聊天程序——客户端(附demo)

来源:互联网 发布:js 获取鼠标位置 编辑:程序博客网 时间:2024/06/05 11:57
这篇文章主要介绍了详解基于java的Socket聊天程序——客户端(附demo),客户端设计主要分成两个部分,分别是socket通讯模块设计和UI相关设计。有兴趣的可以了解一下。

写在前面:

上周末抽点时间把自己写的一个简单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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * 客户端
 * @author yaolin
 *
 */
public class Client {
   
  privatefinal Socket socket;
  privateString from;
  privatefinal ExecutorService pool;
  privatefinal Callback callback;
 
  publicClient(Callback callback) throwsIOException {
    this.socket =new Socket(ConstantValue.SERVER_IP, ConstantValue.SERVER_PORT);
    this.pool = Executors.newFixedThreadPool(2);
    this.callback = callback;
  }
   
  publicvoid start() {
    pool.execute(newReceiveListener(socket, callback));
  }
   
  publicvoid keepAlive(String from) {
    this.from = from;
    pool.execute(newKeepAliveDog(socket, from));
  }
   
  publicSocket getSocket() {
    returnsocket;
  }
   
  publicString getFrom() {
    returnfrom;
  }
}

[KeepAliveDog.java]

客户端在与服务端建立连接之后(该程序中是指登陆成功之后,因为登陆成功之后客户端的socket才会被服务端的SocketHolder管理),需要每个一段时间就给服务端发送心跳包告诉服务端自己还在跟服务端保持联系,不然服务端会在一段时间之后将没有交互的socket丢弃(详见服务端那篇博客),KeepAliveDog的代码实现如下(后期可能会调整为newScheduledThreadPool(1),所以这里的代码也会调整):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * KeepAliveDog : tell Server this client is running;
 *
 * @author yaolin
 */
public class KeepAliveDog implementsRunnable {
 
  privatefinal Socket socket;
  privatefinal String from;
   
  publicKeepAliveDog(Socket socket, String from) {
    this.socket = socket;
    this.from = from;
  }
 
  @Override
  publicvoid run() {
    while(socket != null&& !socket.isClosed()) {
      try{
         
        PrintWriter out =new PrintWriter(socket.getOutputStream());
        AliveMessage message =new AliveMessage();
        message.setFrom(from);
        out.println(JSON.toJSON(message));
        out.flush();
         
        Thread.sleep(ConstantValue.KEEP_ALIVE_PERIOD *1000);
         
      }catch (Exception e) {
        LoggerUtil.error("Client send message failed !"+ e.getMessage(), e);
      }
    }
  }
}

[ReceiveListener.java]

Client的start()方法启动对服务端的监听由ReceiveListener实现,ReceiveListener接收到服务端的消息之后会回调Callback的doWork()方法,让回调去处理具体的业务逻辑,所以ReceiveListener只负责监听服务端的消息,具体的处理由Callback负责,这里需要提一下的是当消息类型是文件类型的时候会睡眠配置执行的间隔时间,这样Callback中的doWork才能对读取来至服务端的文件流,而不是直接进入下一次循环,这里的设计跟服务端是类似的。ReceiveListener的具体实现代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ReceiveListener implementsRunnable {
 
  privatefinal Socket socket;
  privatefinal Callback callback;
 
  publicReceiveListener(Socket socket, Callback callback) {
    this.socket = socket;
    this.callback = callback;
  }
 
  @Override
  publicvoid run() {
    if(socket != null) {
      while(!socket.isClosed()) {
        try{
          InputStream is = socket.getInputStream();
          String line =null;
          StringBuffer sb =null;
 
          if(is.available() > 0) {
 
            BufferedReader bufr =new BufferedReader(newInputStreamReader(is));
            sb =new StringBuffer();
            while(is.available() > 0&& (line = bufr.readLine()) != null) {
              sb.append(line);
            }
            LoggerUtil.trach("RECEIVE ["+ sb.toString() + "] AT "+ new Date());
             
            callback.doWork(socket, sb.toString());
            BaseMessage message = JSON.parseObject(sb.toString(), BaseMessage.class);
            if(message.getType() == MessageType.FILE) {
              // PAUSE TO RECEIVE FILE
              LoggerUtil.trach("CLIENT:PAUSE TO RECEIVE FILE");
              Thread.sleep(ConstantValue.MESSAGE_PERIOD);
            }
          }else {
            Thread.sleep(ConstantValue.MESSAGE_PERIOD);
          }
        }catch (Exception e) {
          LoggerUtil.error("Client send message failed !"+ e.getMessage(), e);
        }
      }
    }
  }
 
}

[Callback.java、DefaultCallback.java]

从上面可以看出Client对消息的处理是Callback回调,其Callback只是一个接口,所有Callback实现该接口根据自己的需要对消息进行相应地处理,这里Callback默认的实现是DefaultCallback,DefaultCallback只对三种消息进行处理,分别是聊天消息、文件消息、返回消息。对于聊天消息,DefaultCallback将通过UI中的Router路由获取到相应的界面(详见下面的UI设计),然后将消息展现在对应的聊天框中;对于文件消息,DefaultCallback则是将文件写入到配置中指定的路径中(这里没有通过用户的允许就接收文件,这种设计不是很友好,目前先这样);对于返回消息,DefaultCallback会根据返回消息中的KEY叫给不同的Handler去处理。具体代码如下:

?
1
2
3
public interface Callback {
  publicvoid doWork(Socket server, Object data);
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
public class DefaultCallback implementsCallback {
 
  @Override
  publicvoid doWork(Socket server, Object data) {
    if(data != null) {
      BaseMessage message = JSON.parseObject(data.toString(), BaseMessage.class);
      switch(message.getType()) {
      caseMessageType.CHAT:
        handleChatMessage(data);
        break;
      caseMessageType.FILE:
        handleFileMessage(server, data);
        break;
      caseMessageType.RETURN:
        handleReturnMessage(data);
        break;
      }
    }
  }
 
  privatevoid handleChatMessage(Object data) {
    ChatMessage m = JSON.parseObject(data.toString(), ChatMessage.class);
    String tabKey = m.getFrom();// FROM
    JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.CHATTABBED);
    if(comp instanceofJTabbedPane) {
      JTabbedPane tab = (JTabbedPane) comp;
      intindex = tab.indexOfTab(tabKey);
      if(index == -1) {
        tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane());
      }
      JTextArea textArea = ResultHolder.get(tabKey).getTextArea();
      textArea.setText(newStringBuffer()
          .append(textArea.getText()).append(System.lineSeparator()).append(System.lineSeparator())
          .append(" [").append(m.getOwner()).append("] : ").append(System.lineSeparator())
          .append(m.getContent())
          .toString());
      // SCROLL TO BOTTOM
      textArea.setCaretPosition(textArea.getText().length());
    }
  }
 
  privatevoid handleFileMessage(Socket server, Object data) {
    FileMessage message = JSON.parseObject(data.toString(), FileMessage.class);
    if(message.getSize() > 0) {
      OutputStream os =null;
      try{
        if(server != null) {
          InputStream is = server.getInputStream();
          File dir =new File(ConstantValue.CLIENT_RECEIVE_DIR);
          if(!dir.exists()) {
            dir.mkdirs();
          }
          os =new FileOutputStream(
              newFile(PathUtil.combination(ConstantValue.CLIENT_RECEIVE_DIR,new Date().getTime() + message.getName())));
          inttotal = 0;
          while(!server.isClosed()) {
            if(is.available() > 0) {
              byte[] buff =new byte[ConstantValue.BUFF_SIZE];
              intlen = -1;
              while(is.available() > 0&& (len = is.read(buff)) != -1) {
                os.write(buff,0, len);
                total += len;
                LoggerUtil.debug("RECEIVE BUFF ["+ len + "]");
              }
              os.flush();
              if(total >= message.getSize()) {
                LoggerUtil.info("RECEIVE BUFF [OK]");
                break;
              }
            }
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Receive file failed ! "+ e.getMessage(), e);
      }finally {
        if(os != null) {
          try{
            os.close();
          }catch (Exception ignore) {
          }
          os =null;
        }
      }
    }
  }
 
  privatevoid handleReturnMessage(Object data) {
    ReturnMessage m = JSON.parseObject(data.toString(), ReturnMessage.class);
    if(StringUtil.isNotEmpty(m.getKey())) {
      switch(m.getKey()) {
      caseKey.NOTIFY: // Notify client to update usr list
        HF.getHandler(Key.NOTIFY).handle(data);
        break;
      caseKey.LOGIN:
        HF.getHandler(Key.LOGIN).handle(data);
        break;
      caseKey.REGISTER:
        HF.getHandler(Key.REGISTER).handle(data);
        break;
      caseKey.LISTUSER:
        HF.getHandler(Key.LISTUSER).handle(data);
        break;
      caseKey.TIP:
        HF.getHandler(Key.TIP).handle(data);
        break;
      }
    }
  }
}

[Handler.java、HF.java、ListUserHdl.java...]

Handler组件负责对服务端返回消息类型的消息进行处理,DefaultCallback根据不同的KEY将消息分发给不同的Handler进行处理,这里也算一套简单的工厂组件吧,跟服务端处理接收到的数据设计是类似的,完整的类图如下:

下面给出这一块的代码,为了缩小篇幅,将所有Handler实现的代码收起来。 

?
1
2
3
public interface Handler {
   publicObject handle(Object obj);
 }
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HF {
 
  publicstatic Handler getHandler(String key) {
    switch(key) {
    caseKey.NOTIFY:
      returnnew NotifyHdl();
    caseKey.LOGIN:
      returnnew LoginHdl();
    caseKey.REGISTER:
      returnnew RegisterHdl();
    caseKey.LISTUSER:
      returnnew ListUserHdl();
    caseKey.TIP:
      returnnew TipHdl();
    }
    returnnull;
  }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ListUserHdl implementsHandler {
 
  @Override
  publicObject handle(Object obj) {
    if(obj != null) {
      try{
        ReturnMessage rm = JSON.parseObject(obj.toString(), ReturnMessage.class);
        if(rm.isSuccess() && rm.getContent() != null) {
          ClientListUserDTO dto = JSON.parseObject(rm.getContent().toString(), ClientListUserDTO.class);
          JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.LISTUSRLIST);
          if(comp instanceofJList) {
            @SuppressWarnings("unchecked")//
            JList<String> listUsrList = (JList<String>) comp;
            List<String> listUser =new LinkedList<String>();
            listUser.addAll(dto.getListUser());
            Collections.sort(listUser);
            listUser.add(0, ConstantValue.TO_ALL);
            listUsrList.setListData(listUser.toArray(newString[]{}));
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Handle listUsr failed! "+ e.getMessage(), e);
      }
    }
    returnnull;
  }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LoginHdl implementsHandler {
 
  @Override
  publicObject handle(Object obj) {
    if(obj != null) {
      try{
        ReturnMessage rm = JSON.parseObject(obj.toString(),ReturnMessage.class);
        if(rm.isSuccess()) {
          Router.getView(RegisterAndLoginView.class).trash();
          Router.getView(ChatRoomView.class).create().display();
          ClientHolder.getClient().keepAlive(rm.getTo());// KEEP...
        }else {
          Container container = Router.getView(RegisterAndLoginView.class).container();
          if(container != null) {
            // show error
            JOptionPane.showMessageDialog(container, rm.getMessage());
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Handle login failed! "+ e.getMessage(), e);
      }
    }
    returnnull;
  }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class NotifyHdl implementsHandler {
 
  @Override
  publicObject handle(Object obj) {
    if(obj != null) {
      try{
        ReturnMessage rm = JSON.parseObject(obj.toString(), ReturnMessage.class);
        if(rm.isSuccess() && rm.getContent() != null) {
          ClientNotifyDTO dto = JSON.parseObject(rm.getContent().toString(), ClientNotifyDTO.class);
          JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.LISTUSRLIST);
          if(comp instanceofJList) {
            @SuppressWarnings("unchecked")//
            JList<String> listUsrList = (JList<String>) comp;
            List<String> listUser = modelToList(listUsrList.getModel());
            if(dto.isFlag()) {
              if(!listUser.contains(dto.getUsername())) {
                listUser.add(dto.getUsername());
                listUser.remove(ConstantValue.TO_ALL);
                Collections.sort(listUser);
                listUser.add(0, ConstantValue.TO_ALL);
              }
            }else {
              listUser.remove(dto.getUsername());
            }
            listUsrList.setListData(listUser.toArray(newString[]{}));
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Handle nofity failed! "+ e.getMessage(), e);
      }
    }
    returnnull;
  }
 
  privateList<String> modelToList(ListModel<String> listModel) {
    List<String> list =new LinkedList<String>();
    if(listModel != null) {
      for(int i = 0; i < listModel.getSize(); i++) {
        list.add(listModel.getElementAt(i));
      }
    }
    returnlist;
  }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RegisterHdl implementsHandler {
 
  @Override
  publicObject handle(Object obj) {
    if(obj != null) {
      try{
        ReturnMessage rm = JSON.parseObject(obj.toString(),ReturnMessage.class);
        Container container = Router.getView(RegisterAndLoginView.class).container();
        if(container != null) {
          if(rm.isSuccess()) {
            JOptionPane.showMessageDialog(container, rm.getContent());
          }else {
            JOptionPane.showMessageDialog(container, rm.getMessage());
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Handle register failed! "+ e.getMessage(), e);
      }
    }
    returnnull;
  }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TipHdl implementsHandler {
 
  @Override
  publicObject handle(Object obj) {
    if(obj != null) {
      try{
        ReturnMessage m = JSON.parseObject(obj.toString(), ReturnMessage.class);
        if(m.isSuccess() && m.getContent() != null) {
          String tabKey = m.getFrom();
          String tip = m.getContent().toString();
          JComponent comp = Router.getView(ChatRoomView.class).getComponent(ChatRoomView.CHATTABBED);
          if(comp instanceofJTabbedPane) {
            JTabbedPane tab = (JTabbedPane) comp;
            intindex = tab.indexOfTab(tabKey);
            if(index == -1) {
              tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane());
            }
            JTextArea textArea = ResultHolder.get(tabKey).getTextArea();
            textArea.setText(newStringBuffer()
                .append(textArea.getText()).append(System.lineSeparator()).append(System.lineSeparator())
                .append(" [").append(m.getOwner()).append("] : ").append(System.lineSeparator())
                .append(tip)
                .toString());
            // SCROLL TO BOTTOM
            textArea.setCaretPosition(textArea.getText().length());
          }
        }
      }catch (Exception e) {
        LoggerUtil.error("Handle tip failed! "+ e.getMessage(), e);
      }
    }
    returnnull;
  }
 
}

 对于Socket通讯模块还有一个类,那就是ClientHolder,这个类用于存储当前Client,跟服务端的SocketHolder是类似的。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @author yaolin
 */
public class ClientHolder {
 
  publicstatic Client client;
 
  publicstatic Client getClient() {
    returnclient;
  }
 
  publicstatic void setClient(Client client) {
    ClientHolder.client = client;
  }
}

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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * View 路由
 * @author yaolin
 */
public class Router {
 
  privatestatic Map<String, View> listRoute =new HashMap<String,View>();
   
  publicstatic View getView(Class<?> clazz) {
    View v = listRoute.get(clazz.getName());
    if(v == null) {
      try{
        v = (View) Class.forName(clazz.getName()).newInstance();
        listRoute.put(clazz.getName(), v);
      }catch (Exception e) {
        LoggerUtil.error("Create view failed! "+ e.getMessage(), e);
      }
    }
    returnv;
  }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * 所有界面的规范接口
 * @author yaolin
 *
 */
public interface View {
   
  /**
   *
   */
  publicView create();
 
  /**
   *
   */
  publicContainer container();
   
  /**
   * @param key
   */
  publicJComponent getComponent(String key);
   
  /**
   *
   */
  publicvoid display();
   
  /**
   *
   */
  publicvoid trash();
   
}

[RegisterAndLoginView.java、ChatRoomView.java]

由于不想自己写UI,我这里只是简单的写了两个UI界面,分别是注册和登陆界面、聊天界面,这里给出两个丑丑的界面:

注册登录界面

聊天界面

下面给出这两个这界面的具体代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
 * 注册、登陆
 * @author yaolin
 */
public class RegisterAndLoginView extendsJFrame implementsView {
 
  privatestatic final long serialVersionUID = 6322088074312546736L;
  privatefinal RegisterAndLoginAction action =new RegisterAndLoginAction();
   
  privatestatic booleanCREATE = false;
   
  @Override
  publicView create() {
    if(! CREATE) {
      init();
      CREATE =true;
    }
    returnthis;
  }
   
  publicContainer container() {
    create();
    returngetContentPane();
  }
   
  @Override
  publicJComponent getComponent(String key) {
    returnnull;
  }
   
  @Override
  publicvoid display() {
    setVisible(true);
  }
   
  @Override
  publicvoid trash() {
    dispose();
  }
   
   
  privatevoid init() {
    // Attribute
    setSize(500,300);
    setResizable(false);
    setLocationRelativeTo(null);
     
    // Container
    JPanel panel =new JPanel();
    panel.setLayout(null);
     
    // Component
    // username
    JLabel lbUsername =new JLabel(I18N.TEXT_USERNAME);
    lbUsername.setBounds(100,80, 200,30);
    finalJTextField tfUsername = newJTextField();
    tfUsername.setBounds(150,80, 230,30);
    panel.add(lbUsername);
    panel.add(tfUsername);
    // passsword
    JLabel lbPassword =new JLabel(I18N.TEXT_PASSWORD);
    lbPassword.setBounds(100,120, 200,30);
    finalJPasswordField pfPassword = newJPasswordField();
    pfPassword.setBounds(150,120, 230,30);
    panel.add(lbPassword);
    panel.add(pfPassword);
    // btnRegister
    JButton btnRegister =new JButton(I18N.BTN_REGISTER);
    btnRegister.setBounds(100,175, 80,30);
    // btnLogin
    finalJButton btnLogin = newJButton(I18N.BTN_LOGIN);
    btnLogin.setBounds(200,175, 80,30);
    // btnCancel
    JButton btnExit =new JButton(I18N.BTN_EXIT);
    btnExit.setBounds(300,175, 80,30);
    panel.add(btnRegister);
    panel.add(btnLogin);
    panel.add(btnExit);
     
    // Event
    pfPassword.addKeyListener(newKeyAdapter() {
      publicvoid keyPressed(finalKeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_ENTER)
          btnLogin.doClick();
      }
    });// end of addKeyListener
     
    btnRegister.addActionListener(newActionListener() {
      publicvoid actionPerformed(finalActionEvent e) {
        if(StringUtil.isEmpty(tfUsername.getText())
            || StringUtil.isEmpty(newString(pfPassword.getPassword()))) {
          JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_REGISTER_EMPTY_DATA);
          return;
        }
        action.handleRegister(tfUsername.getText(),new String(pfPassword.getPassword()));
      }
    });// end of addActionListener
     
    btnLogin.addActionListener(newActionListener() {
      publicvoid actionPerformed(finalActionEvent e) {
        if(StringUtil.isEmpty(tfUsername.getText())
            || StringUtil.isEmpty(newString(pfPassword.getPassword()))) {
          JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_LOGIN_EMPTY_DATA);
          return;
        }
        action.handleLogin(tfUsername.getText(),new String(pfPassword.getPassword()));
      }
    });// end of addActionListener
     
    btnExit.addActionListener(newActionListener() {
      publicvoid actionPerformed(finalActionEvent e) {
        System.exit(0);
      }
    });// end of addActionListener
 
    getContentPane().add(panel);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
 * Client 聊天窗口
 *
 * @author yaolin
 */
public class ChatRoomView extendsJFrame implementsView {
 
  privatestatic final long serialVersionUID = -4515831172899054818L;
 
  publicstatic final String LISTUSRLIST = "LISTUSRLIST";
  publicstatic final String CHATTABBED = "CHATTABBED";
 
  privatestatic booleanCREATE = false;
  privateChatRoomAction action = newChatRoomAction();
 
  privateJList<String> listUsrList = null;
  privateJTabbedPane chatTabbed = null;
 
  @Override
  publicView create() {
    if(!CREATE) {
      init();
      CREATE =true;
    }
    returnthis;
  }
 
  publicContainer container() {
    create();
    returngetContentPane();
  }
 
  @Override
  publicJComponent getComponent(String key) {
    create();
    switch(key) {
    caseLISTUSRLIST:
      returnlistUsrList;
    caseCHATTABBED:
      returnchatTabbed;
    }
    returnnull;
  }
 
  @Override
  publicvoid display() {
    setVisible(true);
  }
 
  @Override
  publicvoid trash() {
    dispose();
  }
 
  publicvoid init() {
    setTitle(I18N.TEXT_APP_NAME);
    setSize(800,600);
    setResizable(false);
    setLocationRelativeTo(null);
 
    setLayout(newBorderLayout());
    add(createChatPanel(), BorderLayout.CENTER);
    add(createUsrListView(), BorderLayout.EAST);
 
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }
 
  privateJComponent createChatPanel() {
    // FILE SELECTOR
    finalJFileChooser fileChooser = newJFileChooser();
 
    JPanel panel =new JPanel(newBorderLayout());
    // CENTER
    chatTabbed =new JTabbedPane();
    chatTabbed.addTab(ConstantValue.TO_ALL, ResultHolder.get(ConstantValue.TO_ALL).getScrollPane());
    panel.add(chatTabbed, BorderLayout.CENTER);
 
    // SOUTH
    JPanel south =new JPanel(newBorderLayout());
    // SOUTH - FILE
    JPanel middle =new JPanel(newBorderLayout());
    middle.add(newJLabel(), BorderLayout.CENTER); // JUST FOR PADDING
    JButton btnUpload =new JButton(I18N.BTN_SEND_FILE);
    middle.add(btnUpload, BorderLayout.EAST);
    south.add(middle, BorderLayout.NORTH);
    // SOUTH - TEXTAREA
    finalJTextArea taSend = newJTextArea();
    taSend.setCaretColor(Color.BLUE);
    taSend.setMargin(newInsets(10,10, 10,10));
    taSend.setRows(10);
    south.add(taSend, BorderLayout.CENTER);
    // SOUTH - BTN
    JPanel bottom =new JPanel(newBorderLayout());
    bottom.add(newJLabel(), BorderLayout.CENTER); // JUST FOR PADDING
    JButton btnSend =new JButton(I18N.BTN_SEND);
    bottom.add(btnSend, BorderLayout.EAST);
 
    south.add(bottom, BorderLayout.SOUTH);
 
    btnUpload.addActionListener(newActionListener() {
      publicvoid actionPerformed(finalActionEvent e) {
        if(! ConstantValue.TO_ALL.equals(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()))) {
          intreturnVal = fileChooser.showOpenDialog(ChatRoomView.this);
          if(returnVal == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            action.upload(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()), file);
          }
        }else {
          JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_FILE_TO_ALL_ERROR);
        }
      }
    });
 
    btnSend.addActionListener(newActionListener() {
      publicvoid actionPerformed(finalActionEvent e) {
        if(StringUtil.isNotEmpty(taSend.getText())) {
          action.send(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()), taSend.getText());
          taSend.setText(null);
        }
      }
    });
 
    panel.add(south, BorderLayout.SOUTH);
    returnpanel;
  }
 
  privateJComponent createUsrListView() {
    listUsrList =new JList<String>();
    listUsrList.setBorder(newLineBorder(Color.BLUE));
    listUsrList.setListData(newString[] { ConstantValue.TO_ALL });
    listUsrList.setFixedCellWidth(200);
    listUsrList.setFixedCellHeight(30);
    listUsrList.addListSelectionListener(newListSelectionListener() {
      @Override
      publicvoid valueChanged(ListSelectionEvent e) {// chat to
        if(chatTabbed.indexOfTab(listUsrList.getSelectedValue()) == -1
            && listUsrList.getSelectedValue() !=null
            && !listUsrList.getSelectedValue().equals(ClientHolder.getClient().getFrom())) {
          chatTabbed.addTab(listUsrList.getSelectedValue(),
              ResultHolder.get(listUsrList.getSelectedValue()).getScrollPane());
          chatTabbed.setSelectedIndex(chatTabbed.indexOfTab(listUsrList.getSelectedValue()));
        }
      }
    });
    returnlistUsrList;
  }
}

[RegisterAndLoginAction.java、ChatRoomAction.java]

这里UI的事件处理都交由Action去处理,将UI设计和事件响应简单分离,RegisterAndLoginView的事件由RegisterAndLoginAction处理,ChatRoomView的事件由ChatRoomAction处理。具体实现如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RegisterAndLoginAction {
 
  publicvoid handleRegister(String username, String password) {
    if(StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) {
      return;
    }
    RegisterMessage message =new RegisterMessage()
        .setUsername(username)
        .setPassword(password);
    message.setFrom(username);
    SendHelper.send(ClientHolder.getClient().getSocket(), message);
  }
   
   
  publicvoid handleLogin(String username, String password) {
    if(StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) {
      return;
    }
    LoginMessage message =new LoginMessage()
        .setUsername(username)
        .setPassword(password);
    message.setFrom(username);
    SendHelper.send(ClientHolder.getClient().getSocket(), message);
  }
}

对于UI设计还有两个类,分别是ResultHolder和ResultWrapper,ResultWrapper和ResultHolder只是为了创建和存储聊天选项卡,具体实现如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ResultWrapper {
   
  privateJScrollPane scrollPane;
  privateJTextArea textArea;
   
  publicResultWrapper(JScrollPane scrollPane, JTextArea textArea) {
    this.scrollPane = scrollPane;
    this.textArea = textArea;
  }
  publicJScrollPane getScrollPane() {
    returnscrollPane;
  }
  publicvoid setScrollPane(JScrollPane scrollPane) {
    this.scrollPane = scrollPane;
  }
  publicJTextArea getTextArea() {
    returntextArea;
  }
  publicvoid setTextArea(JTextArea textArea) {
    this.textArea = textArea;
  }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ResultHolder {
 
  privatestatic Map<String, ResultWrapper> listResultWrapper =new HashMap<String,ResultWrapper>();
   
  publicstatic void put(String key, ResultWrapper wrapper) {
    listResultWrapper.put(key, wrapper);
  }
   
  publicstatic ResultWrapper get(String key) {
    ResultWrapper wrapper = listResultWrapper.get(key);
    if(wrapper == null) {
      wrapper = create();
      put(key, wrapper);
    }
    returnwrapper;
  }
   
   
  privatestatic ResultWrapper create() {
    JTextArea resultTextArea =new JTextArea();
    resultTextArea.setEditable(false);
    resultTextArea.setBorder(newLineBorder(Color.BLUE));
    JScrollPane scrollPane =new JScrollPane(resultTextArea);
    scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
    ResultWrapper wrapper =new ResultWrapper(scrollPane, resultTextArea);
    returnwrapper;
  }
}

最后的最后给出,客户端运行的入口:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 *
 * @author yaolin
 *
 */
public class NiloayChat {
 
  publicstatic void main(String[] args) {
    View v = Router.getView(RegisterAndLoginView.class).create();
    try{
      v.display();
      Client client =new Client(newDefaultCallback());
      client.start();
      ClientHolder.setClient(client);
    }catch (IOException e) {
      JOptionPane.showMessageDialog(v.container(), e.getMessage());
    }
  }
}

demo下载地址:demo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

0 0
原创粉丝点击