哈喽观众老爷们,经过不懈的努力,在6月6日的早上,聊天室3.0版本终于诞生了!!!(下载地址在文章末尾)
看过我前两期的聊天室的看客们可能知道,前两期的内容涉及的东西仅只有TCP,并且智能在cmd命令下运行,但是这一次的项目已经是比较完善的聊天室了,实现了客户端:群聊,私聊,GUI界面(Swing编程),以及文件传输(UDP),可更新的在线用户列表和文件列表!服务端:可将指定用户移除聊天室,将所有用户移除聊天室,对聊天室中的文件进行增删.
通过前两期的简单了解,我们知道了TCP的基本工作原理,并以此设计出了大纲:
System.in->Client->ServerThread>-ClientThread->System.out 的顺序传递
使用对不同的内容进行不同的标识,识别,处理,输出的方法,实现了群聊与私聊功能,同样,今天我们打算以相同的模式进行消息处理,但是传递的对象有所改变(因为增加了Swing界面):
JFrame.in->Client->ServerThread->ClientThread->JFrame.out 的顺序传递
1.私聊以及群聊基本不变,如果对标识符处理和识别这块不知道怎么实现的同学萌可以去看看我上期的代码,由于这次的代码量太大,我会在文章的末尾放出完整代码的下载地址! 所以看到这块我将默认大家已经对消息处理(即对标识符的处理)有了一定的基础来讲述.
2.GUI界面的实现,GUI界面实现这块比较简单我们将创建一个setInterface类作为客户端的主类,这个类继承了JFrame,所以可以作为一个自带容器的类,这个类中包括了消息输入的文本框JTextField,消息输出的JTextArea,显示在线用户列表的Jlist(后文中会详细描述).以及菜单栏JMenuBar实现上传以及下载功能.在服务端中,我们直接创建一个JFrame,向中间添加两个储存文件列表和用户列表JList,即可实现将用户移除聊天室,对聊天室的文件进行增删!实现出来的效果如下图所示:
3.文件上传与下载的实现: 关于文件的上传与下载以及我们第四点要提到的list这两个模块,是我在这次项目中遇到的比较困难的两个点, 在我刚看到文件的上传和下载的需求时,我的第一个想法是:将一个文件不断的从ServerThread线程中读取,然后向ClientThread线程里发送,同样是将读取到的字节数组转化为String类型,然后在头尾加上标识符,最后在ClientThread线程中接收,向新建的文件里输入,可惜事与愿违,这个方法失败了,第一个错误的地方是文件在每次收到ServerThread线程的消息后,都将关闭一次,导致文件接收出错,在发生这个错误后的第一时间,我采取的补救是将打开新建文件的方式设置为追加,这就可以避免文件内容错误,令人遗憾的是,还是发生了错误,可是这次的错误确是致命的----我在ClientThread线程中使用的接受方法是BufferedReader中的readLine(),即读取一行,这使得读取的内容有限,甚至于丢失用于识别用的标识符!这个错误围绕了我一天,最终在6月6日早上,我完成了对他的改造!
由于在那个时候,整个程序都使用的是单一的TCP传输,导致我的思想开始固化,我开始不断的查找资料,怎么让能实现TCP传输文件,遗憾的是,我找到的文章大都千篇一律:不使用标识符,只能进行单一的传输文件. 但是很明显,在我写的程序 中,使用标识符来确定消息的类型是整个程序的框架,想要改动就会触及所有的代码,显然不能实现,突然有点想法在我脑中乍现!为什么在主体是TCP的程序中就只能使用TCP呢?我需要用到UDP! 于是乎我写出了UpLoadFileFrame类与DownLoadFileFrame类,使用的还是标识符代表操作类型,但是这次的标识符只用于开启两端的UDP端口!这下,一切的一切变得简单了起来,我们只需要在上传与下载时开启UDP进行文件的传送以及下载,在传输结束后关闭就可以了.上传与下载同理,上传是客户端传输文件至服务端,下载是服务端传输文件至客户端,都使用的UDP.
4.在线的JList列表的实现,同样,在实现用户列表时,对于JList和DefaultlistModel的使用困扰了我一天.说说我的想法,最开始我的想法是,在Server客户端中使用一个静态的DefaultListModel用于存储在线用户名,每当一个Socket对应的IO流消失(即用户关闭客户端),则移除对应的model中的元素(model可视为一个Vector容器),同时JList会自动刷新,即可完成在线用户的实现,但是在使用的过程中,发生了一件出乎意料的事情,在使用model的过程中,Client客户端无法访问到Server客户端中的静态model的内容,为什么???它可是静态的啊,当我们对每个Client客户端的model进行测试model中的用户数量时,会惊讶的发现,每个不同的客户端中的访问到的model是独立的!!似乎是每个客户端中都有一块独立的内存空间用于储存model. 实际上这也是符合程序设计的,我们确实不可能在两台主机运行这个聊天室的时候,这台主机能访问到另一台主机的内存! 于是乎,我想到了新的方法,在每次我们需要更新数据的时候,向服务端发送请求(同样使用标识符代表请求类型),服务端接受到请求时,将model中的用户名称连接为一个字符串发往每个客户端,客户端进行拆分然后存入客户端访问到的model中,如此以来,我们就巧妙的解决了上述问题,同样,实现文件列表的查看也是如此.
由于篇幅限制,这里只放出最关键的四个类的代码:(想要完整代码的可以去最后的下载连接中下载):
Server类:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import static com.sun.java.accessibility.util.AWTEventMonitor.addMouseListener;
public class Server implements Protocal{
public static DefaultListModel listModel = new DefaultListModel<>();
public static JList userList = new JList<>(listModel);
public static DefaultListModel fileListModel = new DefaultListModel<>();
public static JList fileList = new JList<>(fileListModel);
private final int SERVER_PORT = 40000;
public static UserMap clients = new UserMap<>();
public void init(){
File file = new File("fileListData.txt");
try {
FileInputStream fi = new FileInputStream(file);
byte[] bytes = new byte[1024*50];
fi.read(bytes);
String temp = new String(bytes);
//System.out.println(temp);
String[] filename = temp.split(SPLIT_SIGN);
for(String stemp : filename) {
//System.out.println(stemp);
fileListModel.addElement(stemp);
}
}
catch (Exception e) {
e.printStackTrace();
}
try {
ServerSocket ss = new ServerSocket(SERVER_PORT);
while(true) {
Socket s = ss.accept();//获取连接服务器的对接的Socket
new ServerThread(s).start();//创建对应的Thread线程响应Socket.
}
}
catch(Exception e) {//接受当端口发生错误时的异常,并提示用户
System.out.println("Faild!/nPlease check the port!");
}
}
public static void main(String[] args) throws Exception{
JFrame jf = new JFrame("服务端");
userList.addMouseListener(new ChangeMusicListener());
jf.add(new JScrollPane(Server.userList));
//Client.listModel.addElement("123");
//jf.pack();
int width = Toolkit.getDefaultToolkit().getScreenSize().width;
int height = Toolkit.getDefaultToolkit().getScreenSize().height;
jf.setLocation((width - 700)/2, (height - 700)/2);
JPanel jp = new JPanel();
//jp.setPreferredSize(new Dimension(20,20));
JTextField jt = new JTextField(15);
jp.add(jt);
JButton moveall = new JButton("关闭聊天室");
moveall.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String choice;
choice = JOptionPane.showInputDialog("是否断开所有用户连接?\n确认请输入YES");
if(choice.equals("YES")) {
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println("您被管理员移出群聊!再会!");
Server.listModel.removeElement(Server.clients.getKeyByValue(clientPS));
PrintStream ps = Server.clients.map.remove(Server.clients.getKeyByValue(clientPS));
}
}
}
});
JButton send = new JButton("发送");
send.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String msg = jt.getText();
jt.setText("");
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println("@全体成员"+msg);
}
}
});
jp.add(send);
jf.add(moveall,BorderLayout.NORTH);
jf.add(jp,"South");
jf.setSize(250,400);
//jf.pack();
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
SwingUtilities.updateComponentTreeUI(jf.getContentPane());
jf.setVisible(true);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Server server = new Server();//启用服务端
server.init();//运行服务端主程序
}
}
class ChangeMusicListener extends MouseAdapter
{
public void mouseClicked(MouseEvent e)
{
if (e.getClickCount() >= 2)
{
String choice;
choice = JOptionPane.showInputDialog("是否断开该用户连接?\n确认请输入YES");
if(choice.equals("YES")) {
String userName = (String)Server.userList.getSelectedValue();
PrintStream ps = Server.clients.map.remove(userName);
Server.listModel.removeElement(userName);
ps.println("您被管理员移出群聊!再会!");
}
}
}
}
Client类:
import javax.swing.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client implements Protocal{
private DefaultListModel defaultListModel;
private DefaultListModel fileListModel;
private static final int SERVER_PORT = 40000;
private Socket socket;
private PrintStream ps;
private BufferedReader brServer;
private BufferedReader keyIn;
public setInterFace s;
String result;
public static String userName = null;
public Client(setInterFace s,DefaultListModel defaultListModel,DefaultListModel fileListModel) {
this.s = s;
this.fileListModel = fileListModel;
this.defaultListModel = defaultListModel;
}
public setInterFace getsetInterface() {
return this.s;
}
public void init() {
try {
//keyIn = new BufferedReader(new InputStreamReader(System.in));//创建一个由键盘输入的包装流
socket = new Socket("127.0.0.1",SERVER_PORT);//获取一个Socket连接服务端
ps = new PrintStream(socket.getOutputStream());//通过Socket创建一个输出流
s.setPs(ps);
brServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));//通过Socket获取一个输入流读取服务端数据
String tip = "";//创建一个用于提示的String
while(true) {
userName = JOptionPane.showInputDialog(tip + "请输入用户名:");//通过新提示框获取Name;
if(userName.equals("")){
tip = "用户名不能为空 !\n";
continue;
}
else {
ps.println(USER_ROUND + userName +USER_ROUND);
}
result = brServer.readLine();//从服务端获取反馈
if(result.equals(NAME_REP)) {
tip = "用户名重复!\n";//重置tip提示 并continue在窗口输出要求重新输入用户名Name
continue;
}
if(result.equals(LOGIN_SUCCESS)) {
break;
}
}
}
catch (UnknownHostException e) {
System.out.println("connot find the host!");
closeRs();
System.exit(1);
}
catch (IOException a) {
System.out.println("internet error!");
closeRs();
System.exit(1);
}
catch (Exception e) {
e.printStackTrace();
}
if(result.equals(LOGIN_SUCCESS)) {
//ChatRoom chatRoom = s.getChatRoom();
new ClientThread(brServer,s,defaultListModel,fileListModel).start();//启用新线程;将br输入流添加给新线程;在线程中进行输入操作
ps.println(UPDATE +""+ UPDATE);
s.setVisible(true);
System.out.println(Server.clients.map.size());
}
}
private void closeRs() {
try {
if(keyIn != null) {
ps.close();
}
if(brServer != null) {
ps.close();
}
if(ps != null) {
ps.close();
}
if(socket != null) {
keyIn.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
DefaultListModel listModel = new DefaultListModel<>();
DefaultListModel filelistModel = new DefaultListModel<>();
JList userList = new JList<>(listModel);
JList fileList = new JList<>(filelistModel);
//System.out.println(filelistModel.size());
setInterFace s = new setInterFace(userName,userList,fileList,filelistModel);
Client client = new Client(s,listModel,filelistModel);
client.init();
s.setName(userName);
//client.readAndSend();
}
}
ServerThread类:
import javax.swing.*;
import java.io.*;
import java.net.*;
import java.nio.channels.ServerSocketChannel;
public class ServerThread extends Thread implements Protocal{
private Socket s = null;
private BufferedReader br = null;
private PrintStream ps = null;
private setInterFace interFace;
public ServerThread(Socket s) throws Exception{
this.s = s;
}
public void run(){
String line = null;
try {
br = new BufferedReader(new InputStreamReader(s.getInputStream()));//以Socket创建对饮的输入流,获取客户端输入的信息
ps = new PrintStream(s.getOutputStream());//创建输出流向客户端输出
while((line = br.readLine()) != null) {
if(line.startsWith(USER_ROUND) && line.endsWith(USER_ROUND)) {//判断客户端消息是否为创建用户类型
String userName = getRealMsg(line);//获取用户名
System.out.println(userName);
if(Server.clients.map.containsKey(userName)) {//对用户名进行判断
System.out.println("Repetitive!");//提示创建失败
ps.println(NAME_REP);//向客户端反馈
}
else {
System.out.println("SUCCESS!");//提示创建成功
ps.println(LOGIN_SUCCESS);//向客户端反馈
Server.clients.put(userName,ps);//向map创建对应用户名的输出流
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(userName + " 上线! ");
}
Server.listModel.addElement(userName);
System.out.println(Server.clients.map.size());
}
}
else if (line.startsWith(PRIVATE_ROUND) && line.endsWith(PRIVATE_ROUND)) {//向私聊客户端发送私聊信息
String msgIn = getRealMsg(line);
String user = msgIn.split(SPLIT_SIGN)[0];//将用户名和消息分离为两部分 [0]为用户名 [1]为消息
String msgTo = msgIn.split(SPLIT_SIGN)[1];
Server.clients.map.get(user).println(Server.clients.getKeyByValue(ps) + " 对你说 : "+msgTo);//发送消息
ps.println("我对: "+user + " 说: "+msgTo);
}
else if(line.startsWith(UPDATE) && line.endsWith(UPDATE)) {
Integer sumName = Server.clients.map.size();
String userNameList = UPDATE + sumName.toString();
for(String name : Server.clients.map.keySet()) {
userNameList += LIST_SIGN + name;
}
userNameList += UPDATE;
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(userNameList);
}
Integer sumFile = Server.fileListModel.size();
System.out.println(Server.fileListModel.size()+"!");
String fileListName = FILE_SIGN + sumFile.toString();
for(int i = 0;i < sumFile;i++) {
fileListName += LIST_SIGN + Server.fileListModel.getElementAt(i);
}
fileListName += FILE_SIGN;
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(fileListName);
}
}
else if(line.startsWith(FILE_ROUND) && line.endsWith(FILE_ROUND)) {
String fileName = getRealMsg(line);
Server.fileListModel.insertElementAt("Server"+fileName,0);
OutputStream os = null;
DatagramSocket ds = null;
try {
os = new FileOutputStream(new File("Server"+fileName));
ds= new DatagramSocket(9001);
byte[] bytes = new byte[1024*50];
DatagramPacket dp = new DatagramPacket(bytes,0,bytes.length);
ds.receive(dp);
byte[] data = dp.getData();
System.out.println(new String(data));
os.write(data,0,data.length);
System.out.println("上传成功!");
}
catch (Exception eq) {
eq.printStackTrace();
}
finally {
try {
ds.close();
os.close();
}
catch (Exception e1) {
e1.printStackTrace();
}
}
String name = "";
for(int i = 0;i < Server.fileListModel.size();i++) {
name += Server.fileListModel.getElementAt(i) + SPLIT_SIGN;
}
try {
File file = new File("fileListData.txt");
FileOutputStream fs = new FileOutputStream(file);
fs.write(name.getBytes(),0,name.getBytes().length);
System.out.println("保存成功!");
}
catch (Exception e) {
e.printStackTrace();
}
}
else if(line.startsWith(DOWN_SIGN) && line.endsWith(DOWN_SIGN)) {
String fileName = getRealMsg(line);
DatagramSocket ds = null;
InputStream is = null;
try {
ds = new DatagramSocket(9000);
is = new FileInputStream(new File(fileName));
byte[] bytes = new byte[is.available()];
is.read(bytes);
DatagramPacket dp = new DatagramPacket(bytes,0,bytes.length);
dp.setPort(9001);
dp.setAddress(InetAddress.getByName("127.0.0.1"));
ds.send(dp);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
try {
ds.close();
is.close();
}
catch (Exception e1){
e1.printStackTrace();
}
}
}
else {//向全体客户端发送消息
String msg = getRealMsg(line);
if(Server.clients.getKeyByValue(ps).equals("")) {
continue;
}
else {
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(Server.clients.getKeyByValue(ps) + " 说: "+ msg);
}
}
}
}
}
catch(IOException e) {//当捕获IOException时,将此输出流移除图,并关闭对应输出流输入流与Socket.
String name = Server.clients.getKeyByValue(ps);
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(name + " has Offline! ");
}
Server.listModel.removeElement(Server.clients.getKeyByValue(ps));
Server.clients.removeValue(ps);
Integer sum = Server.clients.map.size();
String userNameList = UPDATE + sum.toString();
for(String name1 : Server.clients.map.keySet()) {
userNameList += LIST_SIGN + name1;
}
userNameList += UPDATE;
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(userNameList);
}
System.out.println(Server.clients.map.size());
try {
if(br != null) {
br.close();
}
if(ps != null) {
ps.close();
}
if(s != null) {
s.close();
}
}
catch(IOException a){
a.printStackTrace();
}
}
}
public String getRealMsg(String line) {//对消息处理,取出左右两边的修饰符号.
return line.substring(PROTOCAL_LEN,line.length()-PROTOCAL_LEN);
}
}
ClientThread类:
import javax.swing.*;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Socket;
import java.time.Clock;
import java.util.Date;
public class ClientThread extends Thread implements Protocal{//客户端线程,启动后用于接受来自服务端的输入,对消息进行输出.
BufferedReader br ;
private JTextArea ja;
// private JTextArea ja2;
// private JTextField jf2;
private JTextField jf;
// private ChatRoom chatRoom = null;
private setInterFace s = null;
DefaultListModel defaultListModel = null;
DefaultListModel fileListModel = null;
public ClientThread(BufferedReader br,setInterFace s,DefaultListModel defaultListModel,DefaultListModel fileListModel) {
this.br = br;
this.s = s;
this.fileListModel = fileListModel;
this.defaultListModel = defaultListModel;
this.ja = s.getJa();
this.jf = s.getJf();
}
public void run() {
try {
String line = null;
while((line = br.readLine())!= null) {//输出来自服务端经过处理后的输入
if(line.startsWith(UPDATE) && line.endsWith(UPDATE)) {
String userNameList = line.substring(PROTOCAL_LEN,line.length()-PROTOCAL_LEN);
String number = userNameList.split(LIST_SIGN)[0];
Integer sum = Integer.parseInt(number);
//ja.append(sum.toString());
defaultListModel.removeAllElements();
for(int i = 0;i < sum;i++) {
defaultListModel.addElement(userNameList.split(LIST_SIGN)[i+1]);
}
}
// else if(line.startsWith(PRIVATE_ROUND) && line.endsWith(PRIVATE_ROUND)){
// line = line.substring(PROTOCAL_LEN,line.length()-PROTOCAL_LEN);
// Date data = new Date(System.currentTimeMillis());
// ja2.append(data.toString());
// ja2.append('\n'+line+'\n'+'\n');
// }
else if(line.startsWith(FILE_SIGN) && line.endsWith(FILE_SIGN)) {
String fileNameList = line.substring(PROTOCAL_LEN,line.length()-PROTOCAL_LEN);
String number = fileNameList.split(LIST_SIGN)[0];
Integer sum = Integer.parseInt(number);
fileListModel.removeAllElements();
for(int i = 0;i < sum;i++) {
fileListModel.addElement(fileNameList.split(LIST_SIGN)[i+1]);
}
}
// else if() {
//
// }
else {
Date data = new Date(System.currentTimeMillis());
ja.append(data.toString());
ja.append('\n'+line+'\n'+'\n');
//Server.listModel.addElement("123");
//System.out.println(Server.listModel.size());
//s.setVisible(true);
}
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
try {
if(br!=null) {
br.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
最后的最后,放上代码的下载连接:https://download.csdn.net/download/qq_43188744/11229890
还请各位看官老爷们多多支持!!!!码字不易!