项目简介
这是一个java版的有界面的聊天室项目,主要用到了GUI中的javax.swing包和网络编程中的TCP/socket编程。GUI部分的难点是图形控件的API较为复杂,可以参考API手册。网络编程部分的难点是客户端之间通过服务器进行通信,不仅服务器要使用多线程,而且每个服务线程要求能够调用其他服务线程的socket来向其他客户端发送消息,客户端和服务器的消息都通过socket来传递给对方。
一、实现功能
- 当客户端连接到服务器时,可收到服务器连接成功的提示消息,以及服务器更新的当前在线用户列表,显示在线用户的IP地址和端口号,自己的ip和端口号不显示。
- 可以用鼠标点击选择在线列表中的某一个用户,在发送消息的文本框输入内容,进行私聊;也可以用鼠标选中在线列表中的所有用户,进行群聊。
- 用户可以清空自己的聊天记录,即清屏。
- 接收消息时可以显示发送消息的ip地址和端口号。
- 客户端下线时,会给服务器发送消息,服务器向所有的客户端更新在线列表。
- 服务器使用多线程工作,由HashMap存储所有的服务线程。
二、设计图
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/2be6831076b4f2c868e2ed31b64192a7.png)
三、GUI部分介绍
- 聊天窗口:显示自己发送的和收到的信息,包括发信人的ip和端口号,用设置成不可编辑的JTextArea控件实现,用JScrollPane包装实现滚动条。
- 打字窗口:用设置成可编辑的JTextArea实现。
- 当前在线列表:显示当前在线的所有人(自己除外)的ip地址和端口号,在客户端上线或下线时,服务器向所有客户端即时更新在线列表。用JTable实现,可动态插入或删除行。
- 发送按钮:将打字窗口中的文字按照在线列表中选择的收信人发送给服务器,由服务器转发给收信人,然后将打字窗口清屏。用JButton控件实现。
- 清屏按钮:将连天窗口清屏,由JButton实现。
- 退出按钮:向服务器发送退出信息,并关闭此客户端程序。服务器接收到消息后立马更新在线列表。
- 收信人标签:提醒用户,没有收信人则不能发送消息。
四、网络编程部分
- 服务器使用多线程工作,每个客户端都享有一个服务线程。
- 每个客户端用自己的ip地址和端口号组成一个字符串作为用户标识uid,其组成格式为uid=IP:端口号,ip和端口号用冒号分隔开,方便拆分。
- 客户端和服务器之间每次通信都是传递一个字符串, 字符串可能有以下几种结构:
String | 作用 |
---|
Chat/收信人uid/聊天内容 | 客户端发往服务器,表示该客户端要向别的客户端发送消息 |
Chat/发信人uid/聊天内容 | 服务器发给客户端,表示服务器转发给收信客户端的消息 |
OnLineListUpdate/发信人uid | 服务器发给客户端,表示有客户端加入或退出,要更新所有客户端的当前在线列表 |
Exit/ | 客户端发往服务器,表示该客户端退出 |
- 收信人uid格式:ip1:port1,ip2:port2,ip3:port3…
发信人uid格式:ip:port - 服务器类静态存储容器
容器 | 内容 |
---|
String数组 | 储存当前在线所有人的uid |
HashMap<String,服务线程> | 储存所有的服务线程,可以根据uid取出对应的服务线程 |
- 服务器用while(true)循环持续监听客户端消息,根据消息类型作处理
服务器收到消息类型 | 服务器向客户端发送消息类型 |
---|
Chat/收信人uid /聊天内容 | Chat/发信人uid/聊天内容 |
Exit/ | 向所有客户端发送OnLineListUpdate/在线者uid |
- 客户端用while(true)持续监听服务器消息,根据消息类型作处处理
客户端收到消息类型 | 客户端如何处理 |
---|
Chat/发信人uid /聊天内容 | 在聊天窗口显示发信人uid以及聊天内容 |
OnLineListUpdate/在线者uid | 更新在线列表信息 |
五、运行示例
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d79a8b63e9c6cf3e62bb1b46bb16061a.png)
六、代码
1.服务器
package chat_room;
import java.io.*;
import java.util.*;
import java.net.*;
import java.text.*;
public class Server
{
public static void main(String[] args) throws Exception
{
ServerSocket ss = new ServerSocket(6666);
System.out.println("Server online... " + ss.getInetAddress().getLocalHost().getHostAddress() + ", " + 6666);
while(true)
{
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
new Thread(new ServerThread(s, ss, ip, port)).start();
}
}
}
class ServerThread implements Runnable
{
Socket s = null;
ServerSocket ss = null;
String ip = null;
int port = 0;
String uid = null;
static ArrayList<String> uid_arr = new ArrayList<String>();
static HashMap<String, ServerThread> hm = new HashMap<String, ServerThread>();
public ServerThread(Socket s, ServerSocket ss, String ip, int port)
{
this.s = s;
this.ss = ss;
this.ip = ip;
this.port = port;
uid = ip + ":" + port;
}
@Override
public void run()
{
uid_arr.add(uid);
hm.put(uid, this);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("Client connected: " + uid);
try
{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
String welcome = sdf.format(new Date()) + "\n成功连接服务器...\n服务器IP: "
+ ss.getInetAddress().getLocalHost().getHostAddress()
+ ", 端口: 6666\n客户端IP: " + ip + ", 端口: " + port + "\n";
out.write(welcome.getBytes());
updateOnlineList(out);
byte[] buf = new byte[1024];
int len = 0;
while(true)
{
len = in.read(buf);
String msg = new String(buf, 0, len);
System.out.println(msg);
String type = msg.substring(0, msg.indexOf("/"));
String content = msg.substring(msg.indexOf("/") + 1);
if(type.equals("Exit"))
{
uid_arr.remove(uid_arr.indexOf(uid));
hm.remove(uid);
updateOnlineList(out);
System.out.println("Client exited: " + uid);
break;
}
else if(type.equals("Chat"))
{
String[] receiver_arr = content.substring(0, content.indexOf("/")).split(",");
String word = content.substring(content.indexOf("/")+1);
chatOnlineList(out, uid, receiver_arr, word);
}
}
}
catch(Exception e){}
}
public void updateOnlineList(OutputStream out) throws Exception
{
for(String tmp_uid : uid_arr)
{
out = hm.get(tmp_uid).s.getOutputStream();
StringBuilder sb = new StringBuilder("OnlineListUpdate/");
for(String member : uid_arr)
{
sb.append(member);
if(uid_arr.indexOf(member) != uid_arr.size() - 1)
sb.append(",");
}
out.write(sb.toString().getBytes());
}
}
public void chatOnlineList(OutputStream out, String uid, String[] receiver_arr, String word) throws Exception
{
for(String tmp_uid : receiver_arr)
{
out = hm.get(tmp_uid).s.getOutputStream();
out.write(("Chat/" + uid + "/" + word).getBytes());
}
}
}
2.客户端
package chat_room;
import java.io.*;
import java.net.*;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.text.*;
public class Client
{
static Socket s = null;
static StringBuilder uidReceiver = null;
public static void main(String[] args)
{
ClientFrame cframe = new ClientFrame();
cframe.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
int w = Toolkit.getDefaultToolkit().getScreenSize().width;
int h = Toolkit.getDefaultToolkit().getScreenSize().height;
cframe.setLocation((w - cframe.WIDTH)/2, (h - cframe.HEIGHT)/2);
cframe.setVisible(true);
try
{
s = new Socket(InetAddress.getLocalHost(), 6666);
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
cframe.jtaChat.append(new String(buf, 0, len));
cframe.jtaChat.append("\n");
while(true)
{
in = s.getInputStream();
len = in.read(buf);
System.out.println(len);
String msg = new String(buf, 0, len);
String type = msg.substring(0, msg.indexOf("/"));
String content = msg.substring(msg.indexOf("/") + 1);
if(type.equals("OnlineListUpdate"))
{
DefaultTableModel tbm = (DefaultTableModel) cframe.jtbOnline.getModel();
tbm.setRowCount(0);
String[] onlinelist = content.split(",");
for(String member : onlinelist)
{
String[] tmp = new String[2];
if(member.equals(InetAddress.getLocalHost().getHostAddress() + ":" + s.getLocalPort()))
continue;
tmp[0] = member.substring(0, member.indexOf(":"));
tmp[1] = member.substring(member.indexOf(":") + 1);
tbm.addRow(tmp);
}
DefaultTableCellRenderer tbr = new DefaultTableCellRenderer();
tbr.setHorizontalAlignment(JLabel.CENTER);
cframe.jtbOnline.setDefaultRenderer(Object.class, tbr);
}
else if(type.equals("Chat"))
{
String sender = content.substring(0, content.indexOf("/"));
String word = content.substring(content.indexOf("/") + 1);
cframe.jtaChat.append(cframe.sdf.format(new Date()) + "\n来自 " + sender + ":\n" + word + "\n\n");
cframe.jtaChat.setCaretPosition(cframe.jtaChat.getDocument().getLength());
}
}
}
catch(Exception e)
{
cframe.jtaChat.append("服务器挂了.....\n");
e.printStackTrace();
}
}
}
class ClientFrame extends JFrame
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
final int WIDTH = 800;
final int HEIGHT = 580;
JButton btnSend = new JButton("发送");
JButton btnClear = new JButton("清屏");
JButton btnExit = new JButton("退出");
JLabel lblReceiver = new JLabel("发给:");
JTextArea jtaSay = new JTextArea();
JTextArea jtaChat = new JTextArea();
String[] colTitles = {"IP", "端口"};
String[][] rowData = null;
JTable jtbOnline = new JTable
(
new DefaultTableModel(rowData, colTitles)
{
@Override
public boolean isCellEditable(int row, int column)
{
return false;
}
}
);
JScrollPane jspChat = new JScrollPane(jtaChat);
JScrollPane jspOnline = new JScrollPane(jtbOnline);
public ClientFrame()
{
setTitle("聊天室");
setSize(WIDTH, HEIGHT);
setResizable(false);
setLayout(null);
btnSend.setBounds(20, 510, 60, 25);
btnClear.setBounds(140, 510, 60, 25);
btnExit.setBounds(260, 510, 60, 25);
lblReceiver.setBounds(20, 420, 300, 30);
this.add(btnSend);
this.add(btnClear);
this.add(btnExit);
this.add(lblReceiver);
jtaSay.setBounds(20, 460, 500, 40);
this.add(jtaSay);
jtaChat.setLineWrap(true);
jtaChat.setEditable(false);
jspChat.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
jspChat.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
jspChat.setBounds(20, 20, 500, 400);
this.add(jspChat);
jspOnline.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
jspOnline.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
jspOnline.setBounds(525, 20, 250, 400);
this.add(jspOnline);
btnSend.addActionListener
(
new ActionListener()
{
@Override
public void actionPerformed(ActionEvent event)
{
jtaChat.setCaretPosition(jtaChat.getDocument().getLength());
try
{
if(Client.uidReceiver.toString().equals("") == false)
{
jtaChat.append(sdf.format(new Date()) + "\n发往 " + Client.uidReceiver.toString() + ":\n");
jtaChat.append(jtaSay.getText() + "\n\n");
OutputStream out = Client.s.getOutputStream();
out.write(("Chat/" + Client.uidReceiver.toString() + "/" + jtaSay.getText()).getBytes());
}
}
catch(Exception e){}
finally
{
jtaSay.setText("");
}
}
}
);
btnClear.addActionListener
(
new ActionListener()
{
@Override
public void actionPerformed(ActionEvent event)
{
jtaChat.setText("");
}
}
);
btnExit.addActionListener
(
new ActionListener()
{
@Override
public void actionPerformed(ActionEvent event)
{
try
{
OutputStream out = Client.s.getOutputStream();
out.write("Exit/".getBytes());
System.exit(0);
}
catch(Exception e){}
}
}
);
jtbOnline.addMouseListener
(
new MouseListener() {
@Override
public void mouseClicked(MouseEvent event) {
DefaultTableModel tbm = (DefaultTableModel) jtbOnline.getModel();
int[] selectedIndex = jtbOnline.getSelectedRows();
Client.uidReceiver = new StringBuilder("");
for (int i = 0; i < selectedIndex.length; i++) {
Client.uidReceiver.append((String) tbm.getValueAt(selectedIndex[i], 0));
Client.uidReceiver.append(":");
Client.uidReceiver.append((String) tbm.getValueAt(selectedIndex[i], 1));
if (i != selectedIndex.length - 1)
Client.uidReceiver.append(",");
}
lblReceiver.setText("发给:" + Client.uidReceiver.toString());
}
@Override
public void mousePressed(MouseEvent event) {
}
;
@Override
public void mouseReleased(MouseEvent event) {
}
;
@Override
public void mouseEntered(MouseEvent event) {
}
;
@Override
public void mouseExited(MouseEvent event) {
}
;
}
);
}
}
源码:github源码链接:)