一、课程设计要求与目的
目的:
编写一个局域网Java聊天室系统,掌握Java网络通信、多线程、IO文件操作等高级应用编程技能。
要求:
以课堂所给示例为基础,编写一个小型Java聊天室系统。
完成如下功能:
- 多客户端模式下,实现客户与客户的单独通信,要求信息通过服务器中转
- 添加图形界面(选做)或其他新功能
- 实现端到端的文件传输
- 到端的通信,实现并行通信模式
二、系统设计与实现
2.1设计原理与思想
Socket套接字通信原理
Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。Socket套接字是建立连接的中间件,而连接成立的前提是要有两台不同的机器存在,一般两端分别为服务器端和客户端。
Socket通信过程:
服务端与客户端都建立一个Socket对象,然后通过Socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。
客户端过程:
创建Socket,连接服务器,将Socket与远程主机连接,发送数据,读取响应数据,直到数据交换完毕,关闭连接,结束TCP对话。
服务端过程:
服务端先初始化Socket,建立流式套接字,与本机地址及端口进行绑定,然后通知TCP,准备好接收连接,然后调用accept()进行阻塞,等待客户端连接。当客户端与服务器=端建立了连接,客户端发送数据请求,服务端接收请求并处理请求,然后把响应数据发送给客户端,客户端读取数据,直到数据交换完毕。最后关闭连接,交互结束。
系统设计思想
- 实现并行通信:初始程序,只能实现客户端发一条消息,服务端发一条消息,发送一条消息后,由于要读入对方所发消息,造成阻塞,只能你发一句,我发一句。在本次设计中,选择套接字数组,将每次产生的套接字记录下来,实现每次可发多条消息。
- 实现文件传输:与发送文字原理相同。不同的是,一端发送文件时要先选择文件,并发送文件后,在文本框输入“发送文件”,作为触发服务端发文件的开关。另一端接收方可以自己选择好储存位置。
- 设置聊天器界面:分为客户端和服务端两种。采用Swing框架,简洁明了。在服务端中,有退出服务器按钮。在客户端有发送、文本框、设置等按钮。
2.2总体设计(类与类的关系)
服务端
服务端共设计了三个类,分别是:MultiTalkServer类,ServerThread类,Server类,实现不同功能。
主类是MultiTalkServer类,创建ServerSocket类的对象,在程序运行时,先创建Server类,启动GUI界面,即服务端启动,等待客户端连接。当客户端进行请求连接时,服务端就会新建一个套接字,启动ServerThread线程,实现客户端与客户端之间互发消息。
客户端
客户端共设计了两个类,分别是:TalkClient类,SettingView类,实现不同的功能。
主类是TalkClient类,有消息收发和聊天器页面设计功能,每次运行时,会创建一个新的客户端,出现客户端页面。文本框中,输入所要发送给客户端编号+“ ”+消息内容。发文件时,可以先选择另一客户端文件存储位置,然后再发送文件,若不指定目录,就默认路径。
2.3详细设计
TalkClient类
在该类设计中,实现了GUI页面设计,包括发送、设置存储位置、选择文件等按钮。当用户启动TalkClient类时,新建一个客户端,并向服务端发送连接请求,若未连接到服务器,会出现“未连接到服务器”提示语句。若连接成功,会出现“本客户端端号”提示语句。若用户发送消息,在文本框中输入内容,点击“发送”按钮,通过addMouseListener 添加鼠标监听器send.addMouseListener(new MouseAdapter(){}),监听鼠标点击事件public void mouseClicked(MouseEvent e)。如果用户发送消息,调用相应的输出流向服务器写内容,并且向服务端发送“normal”,代表普通信息。如果用户想要发文件,首先通过“发送文件”选中文件,程序获得存储地址,然后用户在文本框中输入“发文件”,向服务端发送“File”,代表文件信息,同时将文件内容发送过去,文件发送成功后也会有相关提示语。相关程序如下:
客户端接收消息时。若消息为普通消息,即文本消息时,直接在文本框中输出消息内容。若消息为文件,则直接存储到相对应设置的路径中。
SettingView类
SettingView类为客户端界面上一个按钮的相关实现,可以设置文件的接收后的存储位置。当点击“选择文件存储位置”的按钮时,会调用ChooseDir()方法,并将文件目录赋值给客户端的目录并显示在文本框里。相关代码如下:
MultiTalkServer类
MultiTalkServer类为客户端主程序,当客户端请求连接时,为其与服务端创建一个新的线程,并且调用Server类,建立GUI界面。为了保证能多个客户端之间能相互互发消息,消息在服务器实现中转,在该类中创建socket数组。相关代码如下:
ServerThread类
ServerThread类负责收发客户端发来的消息,收发原理与客户端类似。根据客户端发来的消息,确定客户端的通信目标,获得目标的输出流,将读入的消息通过输出流发送到相应的客户端。主要代码如下:
Server类
Server类为服务器的页面,主要功能为控制服务器的开关,相关组件有
三、系统测试
服务端运行测试
启动:
点击“结束”按钮即关闭服务端
客户端运行测试
启动:
发送文件:
选择文件
设置文件存储位置
相关功能测试
客户端连接服务端
客户端未成功连接服务端时,显示“未连接到服务器”
互发消息:
可同时创建多个客户端,每两个客户端之间都可以进行单独通信,互相发送消息。客户端1给2发“你好”,2给1发“你好”,3给1发“你好”
实现并行通信,一段发送消息不受另一端影响,即没有每次只能发一条消息的限制。
1给2可以连发两条消息
发送文件:
选择所要发送的文件
在文本框中输入“发送文件”,再点击“发送”按钮
四、实验总结
在这次课程设计,实现了多客户端的并行通信,文件传输,设置存储目录,设计聊天界面等功能,使用了Java语言编程、socket网络通信编程、多线程技术等等。在设计中,刚开始没有头绪,虽然知道了通信的原理,但不知道从何下手。通过在网上查找资料,慢慢有了思路。用socket数组存储套接字,实现多个客户端同时在线进行通信,用字节流的available方法判断缓冲区是否有可读字节,实现并行通信操作。还有界面的设计,也是从网上寻找资源学习,不过界面方面还有很多可以完善的地方,比如说:字体大小,字体主题等等。在发送消息时,也只是简单实现了私聊功能,没有实现群聊功能,但是完成了并行通信,不受任何一方发送消息数量的多少。除此之外,聊天器中,发送文件的功能还不是很完善,目前只能完成保存到默认路径中,更换存储路径的功能并没有完全实现,这也是要提高的一点。在程序运行时,发现之前运行的正常,但有时发现不能同时运行多个客户端,也通过查找资料发现解决问题的方法。总的来说,这一个课程设计,让我对Java编程了解更多,成为一个小软件开发的启蒙,也希望自己能继续再接再厉,抓住细节,更好地掌握Java。
五、代码附录
MultiTalkClient.java
package multi;
import java.io.*;
import java.net.*;
public class MultiTalkClient {
public static void main(String args[]) {
try{
//向本机的4700端口发出客户请求
Socket socket=new Socket("127.0.0.1",4700);
//由系统标准输入设备构造BufferedReader对象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由Socket对象得到输出流,并构造PrintWriter对象
PrintWriter os=new PrintWriter(socket.getOutputStream());
//由Socket对象得到输入流,并构造相应的BufferedReader对象
BufferedReader is=new BufferedReader(new
InputStreamReader(socket.getInputStream()));
String readline;
readline=sin.readLine(); //从系统标准输入读入一字符串
while(!readline.equals("bye")){//若从标准输入读入的字符串为 "bye"则停止循环
//将从系统标准输入读入的字符串输出到Server
os.println(readline);
os.flush();//刷新输出流,使Server马上收到该字符串
//在系统标准输出上打印读入的字符串
System.out.println("Client:"+readline);
//从Server读入一字符串,并打印到标准输出上
System.out.println("Server:"+is.readLine());
readline=sin.readLine(); //从系统标准输入读入一字符串
} //继续循环
os.close(); //关闭Socket输出流
is.close(); //关闭Socket输入流
socket.close(); //关闭Socket
}catch(Exception e) {
System.out.println("Error"+e); //出错,则打印出错信息
}
}
}
MultiTalkServer.java
package multi;
import java.io.*;
import java.net.*;
public class MultiTalkServer{
public static int clientnum=0; //静态成员变量,记录当前客户的个数
public static Socket[] socket = new Socket[100];
public static void main(String[] args) {
new Server().init();
ServerSocket serverSocket=null;
boolean listening=true;
try{
//创建一个ServerSocket在端口4700监听客户请求
serverSocket=new ServerSocket(4700);
}catch(Exception e) {
System.out.println("Could not listen on port:4700.");
//出错,打印出错信息
System.exit(-1); //退出
}
try {
while (listening) { //循环监听
//监听到客户请求,根据得到的Socket对象和客户计数创建服务线程,并启动之
socket[++clientnum]=serverSocket.accept();
new ServerThread(socket, clientnum).start();
}
serverSocket.close(); //关闭ServerSocket
}
catch(Exception e){
System.out.println(e);
}
}
}
ServerThread.java
package multi;
import java.io.*;
import java.net.*;
public class ServerThread extends Thread{
public Socket[] sumsocket;
public Socket socket; //保存与本线程相关的Socket对象
public int clientnum; //保存本进程的客户计数
public ServerThread(Socket[] socket,int num) { //构造函数
this.socket=socket[num];
this.sumsocket=socket; //初始化socket变量
this.clientnum=num; //初始化clientnum变量
}
public void run() { //线程主体
try{
String readline=null;
DataInputStream is=new DataInputStream(socket.getInputStream());//消息,文件输入流
DataOutputStream os=new DataOutputStream(socket.getOutputStream());
os.writeUTF("本客户端编号:"+clientnum);
os.flush();
while(true) {
if(is.available()>0) {
String line=is.readUTF();//读取字符串
String[] t=line.split(" ");//以空格为分隔符分割字符
int n=Integer.parseInt(t[0]);//接收信息的客户端编号
Socket w=sumsocket[n];
DataOutputStream tos=new DataOutputStream(w.getOutputStream());//消息,文件输出流
readline=is.readUTF();
if(readline.equals("File")) {//发送文件
tos.writeUTF(t[1]);
tos.flush();
tos.writeUTF("File");
tos.flush();
// System.out.println(clientnum+" is send file to "+n+": "+t[1]);
int y=512,len=512;
byte[] data=new byte[y];//一次读取字节,一般不会太大
while(len==y) {
len=is.read(data,0,y);
tos.write(data,0,len);
tos.flush();
}
}
else if(readline.equals("normal")){
tos.writeUTF("客户端"+clientnum+":"+t[1]);
tos.flush();
tos.writeUTF("normal");
tos.flush();
}
}
}
}
catch(Exception e){
System.out.println("Error:"+e);//出错,打印出错信息
}
}
}
SettingView.java
package multi;
import javax.swing.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.io.*;
public class SettingView {
static JFrame win;
static JPanel mian;
static JButton close,choose;
static JLabel l;
static JTextField shu;
static String mu="E:\\2022-2023-1\\class\\java\\聊天室\\src\\multi";
public SettingView(String mu){
this.mu=mu;
}
static public File ChooseDir(){
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int returnVal = chooser.showOpenDialog(chooser);
if(returnVal == JFileChooser.APPROVE_OPTION) {
return chooser.getSelectedFile();
}else return null;
}
public void init() {
win=new JFrame("Setting");
l=new JLabel("文件接收目录");
l.setBounds(10,20,230,25);
choose=new JButton("自定义");
choose.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
File f=SettingView.ChooseDir();
if(f!=null) {
mu=f.getAbsolutePath();
shu.setText(f.getAbsolutePath());
}
}
});
choose.setFocusable(false);
choose.setBounds(250,20,80,25);
shu=new JTextField(mu);
shu.setBounds(10,60,320,30);
close=new JButton("确定");
close.setBounds(145,100,60,30);
close.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
win.setVisible(false);
mu=shu.getText();
}
});
mian=new JPanel();
mian.setLayout(null);
mian.add(close);
mian.add(l);
mian.add(choose);
mian.add(shu);
win.setContentPane(mian);
win.setBounds(760,500,350,200);
win.setVisible(true);
win.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
win.setVisible(false);
mu=shu.getText();
}
});
}
}
TalkClient.java
package multi;
import java.net.Socket;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.*;
import java.net.*;
import javax.swing.*;
public class TalkClient {//客户端主程序
static JFrame top;
static JButton send,setting,sf;
static JTextArea read,write;
static Font form;
static JScrollPane js;
final static String ip="127.0.0.1";
static String mu="E:\\2022-2023-1\\class\\java\\聊天室\\src\\multi";
static File fi;
final static int port=4700;
static public File ChooseFile(){
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
int returnVal = chooser.showOpenDialog(chooser);
if(returnVal == JFileChooser.APPROVE_OPTION) {
return chooser.getSelectedFile();
}else return null;
}
public static void main(String[] args) {
top = new JFrame();
top.setContentPane(new JPanel());
top.setTitle("JAVA--聊天器");
top.setSize(500, 500);
top.setLayout(null);
top.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
send=new JButton("发送");
sf=new JButton("发送文件");
read=new JTextArea();
write=new JTextArea();
send.setBounds(350,260,100,50);
sf.setBounds(0,260,100,50);
form = new Font(" ",Font.PLAIN, 20);
top.add(send);
sf.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
fi=TalkClient.ChooseFile();
}
});
top.add(sf);
write.setBounds(10,320,450,100);
write.setFont(form);
top.add(write);
read.setEditable(false);
read.setFont(form);
setting=new JButton("设置文件存储位置");
setting.setBorder(BorderFactory.createBevelBorder(0));
setting.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
new SettingView(mu).init();
}
});
setting.setBounds(150,260,100,50);
top.add(setting);
JScrollPane js = new JScrollPane(read);
js.setBounds(10,10,450,230);
top.getContentPane().add(js);
top.setVisible(true);
try {
Socket socket=new Socket(ip,port);
DataInputStream is=new DataInputStream(socket.getInputStream());
DataOutputStream os=new DataOutputStream(socket.getOutputStream());
String q=is.readUTF();
read.append(q+"\n");
char num=q.charAt(7);
send.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
String t=write.getText();
System.out.println(t);
write.setText("");
try {
char s=t.charAt(0);
char s1=t.charAt(1);
if(s<='9'&&s>='0'&&s1==' '&&s-'0'!=num){
String[] tt=t.split(" ");
if(tt[1].equals("发送文件")) {
read.append("客户端"+num+":"+"文件已发送\n");
os.writeUTF(tt[0]+" "+fi.toString());
os.flush();
os.writeUTF("File");
os.flush();
File f=fi;
System.out.print(fi);
FileInputStream fis=new FileInputStream(f);
int n=512,len=512;
byte[] data=new byte[len];
while(len==n) {
len=fis.read(data,0,n);
os.write(data,0,len);
os.flush();
}
}
else {
os.writeUTF(t);
read.append("客户端"+num+":"+tt[1]+'\n');
os.writeUTF("normal");
os.flush();
}
}
else {
read.append(" "+"\n");
}
}
catch(Exception r) {
System.out.println(r);
}
}
});
while(true) {
if(is.available()>0) {
String t=is.readUTF();
System.out.println(t);
String flag=is.readUTF();
System.out.println(flag);
if(flag.equals("File")) {
read.append("客户端"+num+":"+"已经发送到相关目录"+'\n');
int n=512,len=512;
String[] t1=t.split("\\\\");
File f=new File(mu+"\\"+t1[t1.length-1]);
if(!f.exists()) {
f.createNewFile();
}
FileOutputStream fos=new FileOutputStream(f);
byte[] data=new byte[512];
try {
while(n==len) {
len=is.read(data,0,n);
fos.write(data);
}
}
catch(Exception e) {
System.out.println(e);
}
}
else {
System.out.println(1);
read.append(t+"\n");
}
}
}
}
catch(Exception e) {
System.out.println(e);
}
read.append("未连接到服务器"+'\n');
}
}