详解基于java的Socket聊天程序——客户端(附demo)
来源:互联网 发布:js 获取鼠标位置 编辑:程序博客网 时间:2024/06/05 11:57
写在前面:
上周末抽点时间把自己写的一个简单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);
}
}
对于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());
}
}
}
demo下载地址:demo
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
- 详解基于java的Socket聊天程序——客户端(附demo)
- 详解基于java的Socket聊天程序——服务端(附demo)
- Java 基于多客户端的ServerSocket—聊天服务器端,客户端Socket “操作文件”系统的程序拼接
- Java基于Tcp的socket聊天程序
- 简单的Socket聊天程序(客户端)
- 基于socket、多线程的客户端服务器端聊天程序
- 【Java学习】基于Socket的多用户聊天Demo
- 基于Java Socket客户端、服务端聊天雏形
- 基于Java socket和多线程的简易聊天小程序
- Java 基于socket thread 简单聊天程序
- java聊天程序 基于socket 包含MySQL
- JAVA基于NIO客户端对客户端简单聊天DEMO(服务器转发消息)
- 基于socket的简单聊天程序(一)
- 基于socket的简单聊天程序(二)
- 基于socket的简单聊天程序(三)
- 基于Socket简单的聊天程序
- Linux Linux函数 Linux聊天程序 基于socket的TCP(有连接的)聊天程序
- Linux Linux函数 Linux聊天程序 基于socket的TCP(有连接的)聊天程序
- 字符串处理之反转单词
- crm与移动app(andriod和ios)图片的查看与保存
- Activity四种启动模式详解
- MySql数据库语法对比
- js取整数四舍五入
- 详解基于java的Socket聊天程序——客户端(附demo)
- jpa多条件查询重写Specification的toPredicate方法
- 16.下拉菜单的样式
- 1021. Deepest Root 解析
- Java中弹出对话框中的几种方式
- 1025. PAT Ranking (25)
- Android保证service不被杀掉-增强版: 进程保活(根据用户需求慎用)
- mysql 重设root 密码
- Codevs 1242 布局 2005年USACO(差分约束)