写在前面:

  上周末抽点时间把自己写的一个简单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所属的用户):

 /**
* 客户端
* @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进行处理,这里也算一套简单的工厂组件吧,跟服务端处理接收到的数据设计是类似的,完整的类图如下:

下面给出这一块的代码,为了缩小篇幅,将所有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只是为了创建和存储聊天选项卡。设计如下:

  [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界面,分别是注册和登陆界面、聊天界面,这里给出两个丑丑的界面:

注册登录界面

聊天界面

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

 /**
* 注册、登陆
* @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());
}
}

未完,待续。

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

Socket聊天程序——客户端的更多相关文章

  1. Socket聊天程序——Common

    写在前面: 上一篇记录了Socket聊天程序的客户端设计,为了记录的完整性,这里还是将Socket聊天的最后一个模块--Common模块记录一下.Common的设计如下: 功能说明: Common模块 ...

  2. Socket聊天程序——服务端

    写在前面: 昨天在博客记录自己抽空写的一个Socket聊天程序的初始设计,那是这个程序的整体设计,为了完整性,今天把服务端的设计细化记录一下,首页贴出Socket聊天程序的服务端大体设计图,如下图: ...

  3. Socket聊天程序——初始设计

    写在前面: 可能是临近期末了,各种课程设计接踵而来,最近在csdn上看到2个一样问答(问题A,问题B),那就是编写一个基于socket的聊天程序,正好最近刚用socket做了一些事,出于兴趣,自己抽了 ...

  4. boost asio异步读写网络聊天程序客户端 实例详解

    boost官方文档中聊天程序实例讲解 数据包格式chat_message.hpp <pre name="code" class="cpp">< ...

  5. WinSocket简单聊天程序客户端

    #pragma comment(lib,"Ws2_32.lib") #include <stdio.h> #include <Winsock2.h> SOC ...

  6. Android IPC机制(五)用Socket实现跨进程聊天程序

    1.Socket简介 Socket也称作“套接字“,是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信.它分为流式套接字和数据包套接 ...

  7. 聊天程序(基于Socket、Thread)

    聊天程序简述 1.目的:主要是为了阐述Socket,以及应用多线程,本文侧重Socket相关网路编程的阐述.如果您对多线程不了解,大家可以看下我的上一篇博文浅解多线程 . 2.功能:此聊天程序功能实现 ...

  8. 聊天程序——基于Socket、Thread (二)

    聊天程序简述 1.目的:主要是为了阐述Socket,以及应用多线程,本文侧重Socket相关网路编程的阐述.如果您对多线程不了解,大家可以看下我的上一篇博文浅解多线程 . 2.功能:此聊天程序功能实现 ...

  9. 基于socket、多线程的客户端服务器端聊天程序

    服务器端: using System; using System.Windows.Forms; using System.Net.Sockets; using System.Net;//IPAddre ...

随机推荐

  1. Windows 设置Mongodb

    安装MongoDB 将解压所得的bin文件夹内文件部署于C:\mongodb\bin 建立数据库文件夹 C:\mongodb\data\db 准备以下内容的配置文件mongodb.cfg dbpath ...

  2. 在windows 与Linux间实现文件传输(C++&C实现)

    要实现windows与linux间的文件传输,可以通过socket网络编程来实现. 这次要实现的功能与<Windows下通过socket进行字符串和文件传输>中实现的功能相同,即客户端首先 ...

  3. 《C++ Primer 4th》读书笔记 第6章-语句

    原创文章,转载请注明出处: http://www.cnblogs.com/DayByDay/p/3912407.html

  4. Scroll文字滚动js

    function ScrollImgLeft(){ var speed=50, doc=document, scroll_begin = doc.getElementById("scroll ...

  5. iOS-深复制(mutableCopy)与浅复制(copy)

    浅复制:只复制指向对象的指针,而不复制引用对象本身.对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只是一个指针,对象本身资源还是只有一份(对象引用计数+1),那如果我们对A_copy执 ...

  6. 视音频编解码学习工程:JPEG分析器

    =====================================================视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习工 ...

  7. HTML 页面自动刷新

    学习就是一个不断积累的过程,每一天能够学到一点新东西说明自己就在进步!! HTML head 里面设置页面自动刷新功能 <meta http-equiv="Refresh" ...

  8. Contrast Ratio(Enhanced) (Level AAA)

    Contrast ratio between your text and background is at least 7:1 All of your users will benefit from ...

  9. Android数据存储之SQLite使用

    SQLite是D.Richard Hipp用C语言编写的开源嵌入式数据库引擎.它支持大多数的SQL92标准,并且可以在所有主要的操作系统上运行. 在Android中创建的SQLite数据库存储在:/d ...

  10. 一名前端Web架构师的成长之路(转载)

    本人也是coding很多年,虽然很失败,但也总算有点失败的心得,不过我在中国,大多数程序员都是像我一样,在一直走着弯路.如果想成为一个架构师,就必须走正确的路,否则离目标越来越远,正在辛苦工作的程序员 ...