詳解基于java的Socket聊天程序——客戶端(附demo)
寫(xiě)在前面:
上周末抽點(diǎn)時(shí)間把自己寫(xiě)的一個(gè)簡(jiǎn)單Socket聊天程序的初始設(shè)計(jì)和服務(wù)端細(xì)化設(shè)計(jì)記錄了一下,周二終于等來(lái)畢業(yè)前考的軟考證書(shū),然后接下來(lái)就是在加班的日子度過(guò)了,今天正好周五,打算把客戶端的詳細(xì)設(shè)計(jì)和Common模塊記錄一下,因?yàn)檫@個(gè)周末開(kāi)始就要去忙其他東西了。
設(shè)計(jì):
客戶端設(shè)計(jì)主要分成兩個(gè)部分,分別是socket通訊模塊設(shè)計(jì)和UI相關(guān)設(shè)計(jì)。
客戶端socket通訊設(shè)計(jì):
這里的設(shè)計(jì)其實(shí)跟服務(wù)端的設(shè)計(jì)差不多,不同的是服務(wù)端是接收心跳包,而客戶端是發(fā)送心跳包,由于客戶端只與一個(gè)服務(wù)端進(jìn)行通訊(客戶端之間的通訊也是由服務(wù)端進(jìn)行分發(fā)的),所以這里只使用了一個(gè)大小為2的線程池去處理這兩件事(newFixedThreadPool(2)),對(duì)應(yīng)的處理類(lèi)分別是ReceiveListener、KeepAliveDog,其中ReceiveListener在初始化的時(shí)候傳入一個(gè)Callback作為客戶端收到服務(wù)端的消息的回調(diào),Callback的默認(rèn)實(shí)現(xiàn)是DefaultCallback,DefaultCallback根據(jù)不同的事件通過(guò)HF分發(fā)給不同Handler去處理,而ClientHolder則是存儲(chǔ)當(dāng)前客戶端信息,設(shè)計(jì)如下:

Socket通訊模塊具體實(shí)現(xiàn):
[Client.java]
Client是客戶端連接服務(wù)端的入口,創(chuàng)建Client需要指定一個(gè)Callback作為客戶端接收服務(wù)端消息時(shí)的回調(diào),然后由Client的start()方法啟動(dòng)對(duì)服務(wù)端的監(jiān)聽(tīng)(ReceiveListener),當(dāng)ReceiveListener接收到服務(wù)端發(fā)來(lái)的數(shù)據(jù)時(shí),調(diào)用回調(diào)(Callback)的doWork()方法去處理;同時(shí)Client中還需要發(fā)送心跳包來(lái)通知服務(wù)端自己還在連接著服務(wù)端,發(fā)心跳包由Client中keepAlive()啟動(dòng),由KeepAliveDog實(shí)現(xiàn);這兩個(gè)步驟由一個(gè)固定大小為2為線程池newFixedThreadPool(2)去執(zhí)行,可能這里使用一個(gè)newFixedThreadPool(1)和newScheduledThreadPool(1)去處理更合理,因?yàn)樾奶嵌〞r(shí)發(fā)的,服務(wù)端就是這樣實(shí)現(xiàn)的(這個(gè)后續(xù)調(diào)整),Client的具體代碼如下(這里暴露了另外兩個(gè)方法用于獲取socket和當(dāng)前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]
客戶端在與服務(wù)端建立連接之后(該程序中是指登陸成功之后,因?yàn)榈顷懗晒χ罂蛻舳说膕ocket才會(huì)被服務(wù)端的SocketHolder管理),需要每個(gè)一段時(shí)間就給服務(wù)端發(fā)送心跳包告訴服務(wù)端自己還在跟服務(wù)端保持聯(lián)系,不然服務(wù)端會(huì)在一段時(shí)間之后將沒(méi)有交互的socket丟棄(詳見(jiàn)服務(wù)端那篇博客),KeepAliveDog的代碼實(shí)現(xiàn)如下(后期可能會(huì)調(diào)整為newScheduledThreadPool(1),所以這里的代碼也會(huì)調(diào)整):
/**
* 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()方法啟動(dòng)對(duì)服務(wù)端的監(jiān)聽(tīng)由ReceiveListener實(shí)現(xiàn),ReceiveListener接收到服務(wù)端的消息之后會(huì)回調(diào)Callback的doWork()方法,讓回調(diào)去處理具體的業(yè)務(wù)邏輯,所以ReceiveListener只負(fù)責(zé)監(jiān)聽(tīng)服務(wù)端的消息,具體的處理由Callback負(fù)責(zé),這里需要提一下的是當(dāng)消息類(lèi)型是文件類(lèi)型的時(shí)候會(huì)睡眠配置執(zhí)行的間隔時(shí)間,這樣Callback中的doWork才能對(duì)讀取來(lái)至服務(wù)端的文件流,而不是直接進(jìn)入下一次循環(huán),這里的設(shè)計(jì)跟服務(wù)端是類(lèi)似的。ReceiveListener的具體實(shí)現(xiàn)代碼如下:
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對(duì)消息的處理是Callback回調(diào),其Callback只是一個(gè)接口,所有Callback實(shí)現(xiàn)該接口根據(jù)自己的需要對(duì)消息進(jìn)行相應(yīng)地處理,這里Callback默認(rèn)的實(shí)現(xiàn)是DefaultCallback,DefaultCallback只對(duì)三種消息進(jìn)行處理,分別是聊天消息、文件消息、返回消息。對(duì)于聊天消息,DefaultCallback將通過(guò)UI中的Router路由獲取到相應(yīng)的界面(詳見(jiàn)下面的UI設(shè)計(jì)),然后將消息展現(xiàn)在對(duì)應(yīng)的聊天框中;對(duì)于文件消息,DefaultCallback則是將文件寫(xiě)入到配置中指定的路徑中(這里沒(méi)有通過(guò)用戶的允許就接收文件,這種設(shè)計(jì)不是很友好,目前先這樣);對(duì)于返回消息,DefaultCallback會(huì)根據(jù)返回消息中的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組件負(fù)責(zé)對(duì)服務(wù)端返回消息類(lèi)型的消息進(jìn)行處理,DefaultCallback根據(jù)不同的KEY將消息分發(fā)給不同的Handler進(jìn)行處理,這里也算一套簡(jiǎn)單的工廠組件吧,跟服務(wù)端處理接收到的數(shù)據(jù)設(shè)計(jì)是類(lèi)似的,完整的類(lèi)圖如下:

下面給出這一塊的代碼,為了縮小篇幅,將所有Handler實(shí)現(xiàn)的代碼收起來(lái)。
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;
}
}
對(duì)于Socket通訊模塊還有一個(gè)類(lèi),那就是ClientHolder,這個(gè)類(lèi)用于存儲(chǔ)當(dāng)前Client,跟服務(wù)端的SocketHolder是類(lèi)似的。
/**
* @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模塊具體實(shí)現(xiàn):
上面記錄了socket通訊模塊的設(shè)計(jì),接下來(lái)記錄一下UI的設(shè)計(jì)模塊,我不打算自己寫(xiě)UI,畢竟自己寫(xiě)出來(lái)的太丑了,所以后期可能會(huì)叫同學(xué)或朋友幫忙敲一下,所以我將UI的事件處理都交由Action去處理,將UI設(shè)計(jì)和事件響應(yīng)簡(jiǎn)單分離,所有UI繼承JFrame并實(shí)現(xiàn)View接口,上面的Handler實(shí)現(xiàn)類(lèi)通過(guò)Router獲?。ù嬖趧t直接返回,不存在則創(chuàng)建并存儲(chǔ))指定的UI,View中提供了UI的創(chuàng)建create()、獲取container()、獲取UI中的組件getComponent(),顯示display(),回收trash();ResultWrapper和ResultHolder只是為了創(chuàng)建和存儲(chǔ)聊天選項(xiàng)卡。設(shè)計(jì)如下:

[Router.java、View.java]
所有UI繼承JFrame并實(shí)現(xiàn)View接口,Handler實(shí)現(xiàn)類(lèi)通過(guò)Router獲取(存在則直接返回,不存在則創(chuàng)建并存儲(chǔ))指定的UI,View中提供了UI的創(chuàng)建create()、獲取container()、獲取UI中的組件getComponent(),顯示display(),回收trash(),具體實(shí)現(xiàn)如下:
/**
* 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;
}
}
/**
* 所有界面的規(guī)范接口
* @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]
由于不想自己寫(xiě)UI,我這里只是簡(jiǎn)單的寫(xiě)了兩個(gè)UI界面,分別是注冊(cè)和登陸界面、聊天界面,這里給出兩個(gè)丑丑的界面:

注冊(cè)登錄界面

聊天界面
下面給出這兩個(gè)這界面的具體代碼:
/**
* 注冊(cè)、登陸
* @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設(shè)計(jì)和事件響應(yīng)簡(jiǎn)單分離,RegisterAndLoginView的事件由RegisterAndLoginAction處理,ChatRoomView的事件由ChatRoomAction處理。具體實(shí)現(xiàn)如下:
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);
}
}
對(duì)于UI設(shè)計(jì)還有兩個(gè)類(lèi),分別是ResultHolder和ResultWrapper,ResultWrapper和ResultHolder只是為了創(chuàng)建和存儲(chǔ)聊天選項(xiàng)卡,具體實(shí)現(xiàn)如下:
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;
}
}
最后的最后給出,客戶端運(yùn)行的入口:
/**
*
* @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
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- java中UDP簡(jiǎn)單聊天程序?qū)嵗a
- java網(wǎng)絡(luò)編程學(xué)習(xí)java聊天程序代碼分享
- java基于TCP協(xié)議實(shí)現(xiàn)聊天程序
- java基于C/S模式實(shí)現(xiàn)聊天程序(客戶端)
- 詳解基于java的Socket聊天程序——服務(wù)端(附demo)
- java實(shí)現(xiàn)基于Tcp的socket聊天程序
- 詳解基于java的Socket聊天程序——初始設(shè)計(jì)(附demo)
- java實(shí)現(xiàn)簡(jiǎn)單TCP聊天程序
- 基于Java的Socket多客戶端Client-Server聊天程序的實(shí)現(xiàn)
- 用Java實(shí)現(xiàn)聊天程序
相關(guān)文章
java一個(gè)接口多個(gè)實(shí)現(xiàn)類(lèi)的調(diào)用方式
這篇文章主要給大家介紹了關(guān)于java一個(gè)接口多個(gè)實(shí)現(xiàn)類(lèi)的調(diào)用方式的相關(guān)資料,經(jīng)測(cè)試確認(rèn),當(dāng)一個(gè)接口有多個(gè)實(shí)現(xiàn)時(shí),調(diào)用時(shí)只會(huì)執(zhí)行一個(gè),有時(shí)候需要多個(gè)實(shí)現(xiàn)調(diào)用,需要的朋友可以參考下2023-09-09
詳解Java編程中線程同步以及定時(shí)啟動(dòng)線程的方法
這篇文章主要介紹了詳解Java編程中線程同步以及定時(shí)啟動(dòng)線程的方法, 講到了wait()與notify()方法以及阻塞隊(duì)列等知識(shí),需要的朋友可以參考下2016-01-01
Java使用橋接模式實(shí)現(xiàn)開(kāi)關(guān)和電燈照明功能詳解
這篇文章主要介紹了Java使用橋接模式實(shí)現(xiàn)開(kāi)關(guān)和電燈照明功能,較為詳細(xì)的講述了橋接模式的概念、原理并結(jié)合實(shí)例形式分析了Java使用橋接模式實(shí)現(xiàn)開(kāi)關(guān)和電燈照明功能相關(guān)操作步驟與注意事項(xiàng),需要的朋友可以參考下2018-05-05
解決SpringBoot整合MybatisPlus分模塊管理遇到的bug
這篇文章主要介紹了解決SpringBoot整合MybatisPlus分模塊管理遇到的bug,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java異常(Exception)處理以及常見(jiàn)異??偨Y(jié)
在《Java編程思想》中這樣定義異常,阻止當(dāng)前方法或作用域繼續(xù)執(zhí)行的問(wèn)題,雖然java中有異常處理機(jī)制,但是要明確一點(diǎn),決不應(yīng)該用"正常"的態(tài)度來(lái)看待異常,這篇文章主要給大家介紹了關(guān)于Java異常(Exception)處理以及常見(jiàn)異常的相關(guān)資料,需要的朋友可以參考下2021-10-10
SpringBoot+MyBatis+Redis實(shí)現(xiàn)分布式緩存
本文主要介紹了SpringBoot+MyBatis+Redis實(shí)現(xiàn)分布式緩存,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01
maven項(xiàng)目打包如何去掉不需要的module
文章總結(jié):在my-project工程依賴(lài)my-core和my-common,且my-project在總工程AAA中時(shí),建議通過(guò)以下兩種方案優(yōu)化打包流程:1.?使用pom區(qū)分,重新編寫(xiě)pom文件,并指定需要重新編譯的工程到modules中,然后在編譯時(shí)指定pom文件;2024-12-12
SpringBoot 如何實(shí)時(shí)刷新靜態(tài)文件
這篇文章主要介紹了SpringBoot如何實(shí)時(shí)刷新靜態(tài)文件,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12

