Socket聊天程序——客户端

写在前面:

  上周末抽点时间把自己写的一个简单Socket聊天程序的初始设计服务端细化设计记录了一下,周二终于等来毕业前考的软考证书,然后接下来就是在加班的日子度过了,今天正好周五,打算把客户端的详细设计和Common模块记录一下,因为这个周末开始就要去忙其他东西了。

设计:

  客户端设计主要分成两个部分,分别是socket通讯模块设计和UI相关设计。

客户端socket通讯设计:

这里的设计其实跟服务端的设计差不多,不同的是服务端是接收心跳包,而客户端是发送心跳包,由于客户端只与一个服务端进行通讯(客户端之间的通讯也是由服务端进行分发的),所以这里只使用了一个大小为2的线程池去处理这两件事(newFixedThreadPool(2)),对应的处理类分别是ReceiveListener、KeepAliveDog,其中ReceiveListener在初始化的时候传入一个Callback作为客户端收到服务端的消息的回调,Callback的默认实现是DefaultCallback,DefaultCallback根据不同的事件通过HF分发给不同Handler去处理,而ClientHolder则是存储当前客户端信息,设计如下:Socket聊天程序——客户端

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所属的用户):

 /**
* 客户端
* @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),所以这里的代码也会调整):

 /**
* 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的具体实现代码如下:

 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去处理。具体代码如下:

 public interface Callback {
public void doWork(Socket server, Object data);
 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进行处理,这里也算一套简单的工厂组件吧,跟服务端处理接收到的数据设计是类似的,完整的类图如下:

Socket聊天程序——客户端

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

 public interface Handler {
public Object handle(Object obj);
 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;
}
}
 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;
} }
 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;
} }
 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;
}
}
 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;
} }
 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是类似的。

 /**
* @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只是为了创建和存储聊天选项卡。设计如下:

Socket聊天程序——客户端

  [Router.java、View.java]

所有UI继承JFrame并实现View接口,Handler实现类通过Router获取(存在则直接返回,不存在则创建并存储)指定的UI,View中提供了UI的创建create()、获取container()、获取UI中的组件getComponent(),显示display(),回收trash(),具体实现如下:

 /**
* 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;
}
 /**
* 所有界面的规范接口
* @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界面,分别是注册和登陆界面、聊天界面,这里给出两个丑丑的界面:

Socket聊天程序——客户端

注册登录界面
Socket聊天程序——客户端

聊天界面

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

 /**
* 注册、登陆
* @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);
}
}
 /**
* 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处理。具体实现如下:

 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);
}
}
 public class ChatRoomAction {

     public void send(String to, String content) {
if (StringUtil.isEmpty(content)) {
return;
}
ChatMessage message = new ChatMessage()
.setContent(content);
message.setFrom(ClientHolder.getClient().getFrom()).setTo(to);
SendHelper.send(ClientHolder.getClient().getSocket(), message);
} public void upload(String to, File file) {
if (! ConstantValue.TO_ALL.equals(to)) {
String ext = "";
if (file.getName().indexOf(".") != -1) {
try {
ext = file.getName().substring(file.getName().indexOf(".") + 1);
} catch (Exception e) {// pos exception ??
}
}
FileMessage message = new FileMessage()
.setName(file.getName())
.setExt(ext)
.setSize(file.length());
message.setFrom(ClientHolder.getClient().getFrom()).setTo(to);
SendHelper.send(ClientHolder.getClient().getSocket(), message);
SendHelper.upload(ClientHolder.getClient().getSocket(), file);
}
}
}

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

 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;
}
 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;
}

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

 /**
*
* @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());
}
}

未完,待续。

(本文所有的代码已经挂在****的仓库中 :地址 https://code.****.net/yaoIin/niloay-chat ,完整代码参考这里,有时间将更新)

上一篇:Coloring Trees CodeForces - 711C


下一篇:HL AsySocket 服务开发框架 - 业务逻辑层