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

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

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

[Router.java、View.java]
所有UI繼承JFrame并實現(xiàn)View接口,Handler實現(xiàn)類通過Router獲?。ù嬖趧t直接返回,不存在則創(chuàng)建并存儲)指定的UI,View中提供了UI的創(chuàng)建create()、獲取container()、獲取UI中的組件getComponent(),顯示display(),回收trash(),具體實現(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]
由于不想自己寫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設(shè)計和事件響應(yīng)簡單分離,RegisterAndLoginView的事件由RegisterAndLoginAction處理,ChatRoomView的事件由ChatRoomAction處理。具體實現(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);
}
}
對于UI設(shè)計還有兩個類,分別是ResultHolder和ResultWrapper,ResultWrapper和ResultHolder只是為了創(chuàng)建和存儲聊天選項卡,具體實現(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;
}
}
最后的最后給出,客戶端運行的入口:
/**
*
* @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
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- java中UDP簡單聊天程序?qū)嵗a
- java網(wǎng)絡(luò)編程學(xué)習(xí)java聊天程序代碼分享
- java基于TCP協(xié)議實現(xiàn)聊天程序
- java基于C/S模式實現(xiàn)聊天程序(客戶端)
- 詳解基于java的Socket聊天程序——服務(wù)端(附demo)
- java實現(xiàn)基于Tcp的socket聊天程序
- 詳解基于java的Socket聊天程序——初始設(shè)計(附demo)
- java實現(xiàn)簡單TCP聊天程序
- 基于Java的Socket多客戶端Client-Server聊天程序的實現(xiàn)
- 用Java實現(xiàn)聊天程序
相關(guān)文章
java一個接口多個實現(xiàn)類的調(diào)用方式
這篇文章主要給大家介紹了關(guān)于java一個接口多個實現(xiàn)類的調(diào)用方式的相關(guān)資料,經(jīng)測試確認(rèn),當(dāng)一個接口有多個實現(xiàn)時,調(diào)用時只會執(zhí)行一個,有時候需要多個實現(xiàn)調(diào)用,需要的朋友可以參考下2023-09-09
Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能詳解
這篇文章主要介紹了Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能,較為詳細(xì)的講述了橋接模式的概念、原理并結(jié)合實例形式分析了Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能相關(guān)操作步驟與注意事項,需要的朋友可以參考下2018-05-05
解決SpringBoot整合MybatisPlus分模塊管理遇到的bug
這篇文章主要介紹了解決SpringBoot整合MybatisPlus分模塊管理遇到的bug,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
Java異常(Exception)處理以及常見異??偨Y(jié)
在《Java編程思想》中這樣定義異常,阻止當(dāng)前方法或作用域繼續(xù)執(zhí)行的問題,雖然java中有異常處理機(jī)制,但是要明確一點,決不應(yīng)該用"正常"的態(tài)度來看待異常,這篇文章主要給大家介紹了關(guān)于Java異常(Exception)處理以及常見異常的相關(guān)資料,需要的朋友可以參考下2021-10-10
SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存
本文主要介紹了SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01

