寫在前面:
上周末抽點時間把自己寫的一個簡單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 { private final Socket socket; private String from; private final ExecutorService pool; private final Callback callback; public Client(Callback callback) throws IOException { this .socket = new Socket(ConstantValue.SERVER_IP, ConstantValue.SERVER_PORT); this .pool = Executors.newFixedThreadPool( 2 ); this .callback = callback; } public void start() { pool.execute( new ReceiveListener(socket, callback)); } public void keepAlive(String from) { this .from = from; pool.execute( new KeepAliveDog(socket, from)); } public Socket getSocket() { return socket; } public String getFrom() { return from; } } |
[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 implements Runnable { private final Socket socket; private final String from; public KeepAliveDog(Socket socket, String from) { this .socket = socket; this .from = from; } @Override public void 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 implements Runnable { private final Socket socket; private final Callback callback; public ReceiveListener(Socket socket, Callback callback) { this .socket = socket; this .callback = callback; } @Override public void 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( new InputStreamReader(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 { public void 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 implements Callback { @Override public void doWork(Socket server, Object data) { if (data != null ) { BaseMessage message = JSON.parseObject(data.toString(), BaseMessage. class ); switch (message.getType()) { case MessageType.CHAT: handleChatMessage(data); break ; case MessageType.FILE: handleFileMessage(server, data); break ; case MessageType.RETURN: handleReturnMessage(data); break ; } } } private void 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 instanceof JTabbedPane) { JTabbedPane tab = (JTabbedPane) comp; int index = tab.indexOfTab(tabKey); if (index == - 1 ) { tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane()); } JTextArea textArea = ResultHolder.get(tabKey).getTextArea(); textArea.setText( new StringBuffer() .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()); } } private void 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( new File(PathUtil.combination(ConstantValue.CLIENT_RECEIVE_DIR, new Date().getTime() + message.getName()))); int total = 0 ; while (!server.isClosed()) { if (is.available() > 0 ) { byte [] buff = new byte [ConstantValue.BUFF_SIZE]; int len = - 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 ; } } } } private void handleReturnMessage(Object data) { ReturnMessage m = JSON.parseObject(data.toString(), ReturnMessage. class ); if (StringUtil.isNotEmpty(m.getKey())) { switch (m.getKey()) { case Key.NOTIFY: // Notify client to update usr list HF.getHandler(Key.NOTIFY).handle(data); break ; case Key.LOGIN: HF.getHandler(Key.LOGIN).handle(data); break ; case Key.REGISTER: HF.getHandler(Key.REGISTER).handle(data); break ; case Key.LISTUSER: HF.getHandler(Key.LISTUSER).handle(data); break ; case Key.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 { public Object handle(Object obj); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class HF { public static Handler getHandler(String key) { switch (key) { case Key.NOTIFY: return new NotifyHdl(); case Key.LOGIN: return new LoginHdl(); case Key.REGISTER: return new RegisterHdl(); case Key.LISTUSER: return new ListUserHdl(); case Key.TIP: return new TipHdl(); } return null ; } } |
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 implements Handler { @Override public Object 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 instanceof JList) { @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( new String[]{})); } } } catch (Exception e) { LoggerUtil.error( "Handle listUsr failed! " + e.getMessage(), e); } } return null ; } } |
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 implements Handler { @Override public Object 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); } } return null ; } } |
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 implements Handler { @Override public Object 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 instanceof JList) { @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( new String[]{})); } } } catch (Exception e) { LoggerUtil.error( "Handle nofity failed! " + e.getMessage(), e); } } return null ; } private List<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)); } } return list; } } |
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 implements Handler { @Override public Object 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); } } return null ; } } |
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 implements Handler { @Override public Object 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 instanceof JTabbedPane) { JTabbedPane tab = (JTabbedPane) comp; int index = tab.indexOfTab(tabKey); if (index == - 1 ) { tab.addTab(tabKey, ResultHolder.get(tabKey).getScrollPane()); } JTextArea textArea = ResultHolder.get(tabKey).getTextArea(); textArea.setText( new StringBuffer() .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); } } return null ; } } |
對于Socket通訊模塊還有一個類,那就是ClientHolder,這個類用于存儲當前Client,跟服務端的SocketHolder是類似的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/** * @author yaolin */ public class ClientHolder { public static Client client; public static Client getClient() { return client; } public static 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 { private static Map<String, View> listRoute = new HashMap<String,View>(); public static 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); } } return v; } } |
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 { /** * */ public View create(); /** * */ public Container container(); /** * @param key */ public JComponent getComponent(String key); /** * */ public void display(); /** * */ public void 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 extends JFrame implements View { private static final long serialVersionUID = 6322088074312546736L; private final RegisterAndLoginAction action = new RegisterAndLoginAction(); private static boolean CREATE = false ; @Override public View create() { if (! CREATE) { init(); CREATE = true ; } return this ; } public Container container() { create(); return getContentPane(); } @Override public JComponent getComponent(String key) { return null ; } @Override public void display() { setVisible( true ); } @Override public void trash() { dispose(); } private void 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 ); final JTextField tfUsername = new JTextField(); 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 ); final JPasswordField pfPassword = new JPasswordField(); 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 final JButton btnLogin = new JButton(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( new KeyAdapter() { public void keyPressed( final KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) btnLogin.doClick(); } }); // end of addKeyListener btnRegister.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent e) { if (StringUtil.isEmpty(tfUsername.getText()) || StringUtil.isEmpty( new String(pfPassword.getPassword()))) { JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_REGISTER_EMPTY_DATA); return ; } action.handleRegister(tfUsername.getText(), new String(pfPassword.getPassword())); } }); // end of addActionListener btnLogin.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent e) { if (StringUtil.isEmpty(tfUsername.getText()) || StringUtil.isEmpty( new String(pfPassword.getPassword()))) { JOptionPane.showMessageDialog(getContentPane(), I18N.INFO_LOGIN_EMPTY_DATA); return ; } action.handleLogin(tfUsername.getText(), new String(pfPassword.getPassword())); } }); // end of addActionListener btnExit.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent 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 extends JFrame implements View { private static final long serialVersionUID = -4515831172899054818L; public static final String LISTUSRLIST = "LISTUSRLIST" ; public static final String CHATTABBED = "CHATTABBED" ; private static boolean CREATE = false ; private ChatRoomAction action = new ChatRoomAction(); private JList<String> listUsrList = null ; private JTabbedPane chatTabbed = null ; @Override public View create() { if (!CREATE) { init(); CREATE = true ; } return this ; } public Container container() { create(); return getContentPane(); } @Override public JComponent getComponent(String key) { create(); switch (key) { case LISTUSRLIST: return listUsrList; case CHATTABBED: return chatTabbed; } return null ; } @Override public void display() { setVisible( true ); } @Override public void trash() { dispose(); } public void init() { setTitle(I18N.TEXT_APP_NAME); setSize( 800 , 600 ); setResizable( false ); setLocationRelativeTo( null ); setLayout( new BorderLayout()); add(createChatPanel(), BorderLayout.CENTER); add(createUsrListView(), BorderLayout.EAST); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } private JComponent createChatPanel() { // FILE SELECTOR final JFileChooser fileChooser = new JFileChooser(); JPanel panel = new JPanel( new BorderLayout()); // 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( new BorderLayout()); // SOUTH - FILE JPanel middle = new JPanel( new BorderLayout()); middle.add( new JLabel(), 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 final JTextArea taSend = new JTextArea(); taSend.setCaretColor(Color.BLUE); taSend.setMargin( new Insets( 10 , 10 , 10 , 10 )); taSend.setRows( 10 ); south.add(taSend, BorderLayout.CENTER); // SOUTH - BTN JPanel bottom = new JPanel( new BorderLayout()); bottom.add( new JLabel(), BorderLayout.CENTER); // JUST FOR PADDING JButton btnSend = new JButton(I18N.BTN_SEND); bottom.add(btnSend, BorderLayout.EAST); south.add(bottom, BorderLayout.SOUTH); btnUpload.addActionListener( new ActionListener() { public void actionPerformed( final ActionEvent e) { if (! ConstantValue.TO_ALL.equals(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()))) { int returnVal = 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( new ActionListener() { public void actionPerformed( final ActionEvent e) { if (StringUtil.isNotEmpty(taSend.getText())) { action.send(chatTabbed.getTitleAt(chatTabbed.getSelectedIndex()), taSend.getText()); taSend.setText( null ); } } }); panel.add(south, BorderLayout.SOUTH); return panel; } private JComponent createUsrListView() { listUsrList = new JList<String>(); listUsrList.setBorder( new LineBorder(Color.BLUE)); listUsrList.setListData( new String[] { ConstantValue.TO_ALL }); listUsrList.setFixedCellWidth( 200 ); listUsrList.setFixedCellHeight( 30 ); listUsrList.addListSelectionListener( new ListSelectionListener() { @Override public void 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())); } } }); return listUsrList; } } |
[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 { public void 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); } public void 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 { private JScrollPane scrollPane; private JTextArea textArea; public ResultWrapper(JScrollPane scrollPane, JTextArea textArea) { this .scrollPane = scrollPane; this .textArea = textArea; } public JScrollPane getScrollPane() { return scrollPane; } public void setScrollPane(JScrollPane scrollPane) { this .scrollPane = scrollPane; } public JTextArea getTextArea() { return textArea; } public void 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 { private static Map<String, ResultWrapper> listResultWrapper = new HashMap<String,ResultWrapper>(); public static void put(String key, ResultWrapper wrapper) { listResultWrapper.put(key, wrapper); } public static ResultWrapper get(String key) { ResultWrapper wrapper = listResultWrapper.get(key); if (wrapper == null ) { wrapper = create(); put(key, wrapper); } return wrapper; } private static ResultWrapper create() { JTextArea resultTextArea = new JTextArea(); resultTextArea.setEditable( false ); resultTextArea.setBorder( new LineBorder(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); return wrapper; } } |
最后的最后給出,客戶端運行的入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/** * * @author yaolin * */ public class NiloayChat { public static void main(String[] args) { View v = Router.getView(RegisterAndLoginView. class ).create(); try { v.display(); Client client = new Client( new DefaultCallback()); client.start(); ClientHolder.setClient(client); } catch (IOException e) { JOptionPane.showMessageDialog(v.container(), e.getMessage()); } } } |
demo下載地址:demo
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/niloay/p/socket-chatclient.html