写在前面:
上周末抽点时间把自己写的一个简单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聊天程序——客户端的更多相关文章
-
Socket聊天程序——Common
写在前面: 上一篇记录了Socket聊天程序的客户端设计,为了记录的完整性,这里还是将Socket聊天的最后一个模块--Common模块记录一下.Common的设计如下: 功能说明: Common模块 ...
-
Socket聊天程序——服务端
写在前面: 昨天在博客记录自己抽空写的一个Socket聊天程序的初始设计,那是这个程序的整体设计,为了完整性,今天把服务端的设计细化记录一下,首页贴出Socket聊天程序的服务端大体设计图,如下图: ...
-
Socket聊天程序——初始设计
写在前面: 可能是临近期末了,各种课程设计接踵而来,最近在csdn上看到2个一样问答(问题A,问题B),那就是编写一个基于socket的聊天程序,正好最近刚用socket做了一些事,出于兴趣,自己抽了 ...
-
boost asio异步读写网络聊天程序客户端 实例详解
boost官方文档中聊天程序实例讲解 数据包格式chat_message.hpp <pre name="code" class="cpp">< ...
-
WinSocket简单聊天程序客户端
#pragma comment(lib,"Ws2_32.lib") #include <stdio.h> #include <Winsock2.h> SOC ...
-
Android IPC机制(五)用Socket实现跨进程聊天程序
1.Socket简介 Socket也称作“套接字“,是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信.它分为流式套接字和数据包套接 ...
-
聊天程序(基于Socket、Thread)
聊天程序简述 1.目的:主要是为了阐述Socket,以及应用多线程,本文侧重Socket相关网路编程的阐述.如果您对多线程不了解,大家可以看下我的上一篇博文浅解多线程 . 2.功能:此聊天程序功能实现 ...
-
聊天程序——基于Socket、Thread (二)
聊天程序简述 1.目的:主要是为了阐述Socket,以及应用多线程,本文侧重Socket相关网路编程的阐述.如果您对多线程不了解,大家可以看下我的上一篇博文浅解多线程 . 2.功能:此聊天程序功能实现 ...
-
基于socket、多线程的客户端服务器端聊天程序
服务器端: using System; using System.Windows.Forms; using System.Net.Sockets; using System.Net;//IPAddre ...
随机推荐
-
hibernate笔记--组件映射方法
假设我们需要保存学生student的信息,student中有一个address属性,我们知道像这种信息其值可能会有多个,就像一个人会有两个以上的手机号,这种情况在hibernate中应该这样配置: 新 ...
-
冲刺阶段 day11
项目进展:系部,专业,班级管理这三项没有学生,教师管理部分输入项目多,所以窗体搭建比较简单,代码也简单一些,进行的还算顺利,有了之前的编写经验,很多代码可以直接copy过来,越进行编写越顺利了,已经进 ...
-
BZOJ 2438: [中山市选2011]杀人游戏
Description 给你一个有向图,求至少询问多少次能够得到全部点的信息. Sol Tarjan + 强连通分量缩点 + 判断. 先缩点,如果我们知道了强连通分量里的任意一个点,我们就可以知道这些 ...
-
Android-BaiduMapSDK示例的key验证失败问题
首先,Android Studio获取SHA1会出现问题. 链接:Android Studio 获取 sha1 方法如下: 根据百度的教程,使用该SHA1并不能成功验证key 在Android SDK ...
-
子查询解嵌套in改写为exists
SELECT * FROM (SELECT pubformdat0_.id id332_, pubformdat0_.domain_id domain2_332_, pubformdat0_.proc ...
-
读undo问题
SQL> drop table test1 purge; Table dropped. SQL> create table test1 as select * from dba_objec ...
-
linux基础--chkconfig 详解
chkconfig命令主要用来更新(启动或停止)和查询系统服务的运行级信息.谨记chkconfig不是立即自动禁止或激活一个服务,它只是简单的改变了符号连接. 使用语法:chkconfig [--ad ...
-
autoware
在 Autoware目录下执行 git checkout 将版本切换到1.10.0 因为最新版驱动有问题然后执行 sudo apt-get update sudo apt-get install -y ...
-
(大数)Computer Transformation hdu1041
Computer Transformation Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/ ...
-
hive 动态分区实现 (hive-1.1.0)
笔者使用的hive版本是hive-1.1.0 hive-1.1.0动态分区的默认实现是只有map没有reduce,通过执行计划就可以看出来.(执行计划如下) insert overwrite tabl ...