一、任务描述及设计要求
【设计目的】
1.熟悉开发工具 (Visual Studio、C/C++、Java等)的基本操作;
2.了解基于对话框的windows/Linux应用程序的编写过程;
3.对于Socket编程建立初步的概念。
【设计要求】
1.熟悉Socket API主要函数的使用;
2.掌握相应开发工具对Socket API的封装;
3.制作基于局域网的一对一网络即时通讯工具,实现基本数据的网络传输。
二、开发环境与工具
Eclipse IDE,jdk1.8.0_231
四、系统功能描述及软件模块划分
1、服务端多线程接收信息
主要是接收客户端发送过来的信息,通过创建多个线程并行地接收信息并显示在服务器端,可以开启服务和结束服务,可以中止对某一个客户端的服务,将从客户端接受到的信息发送到与服务器连接的其他客户端上。可以看到所有连接在此服务器的客户端。
2、客户端发送信息
连接服务器,可以断开与服务器的连接,可以看到连接到同一服务器的其他客户端,可以输出信息,可以看到自己和其他客户端输出的信息。
八、软件使用说明
本软件用于实时网络通讯,服务器端无法发言,仅有客户端可以发言,服务器端起记录、连接、踢人等功能,客户端仅能进行聊天,本软件的功能近乎于QQ群,但与QQ群并不相同,虽然有记录功能,但不会将所有的信息都同步到客户端,只会将客户端在线期间的信息同步到客户端上。对于用户端的连接,服务器不会设置限制,在同一局域网下的所有角色都可以进行连接,用户可以随意更换昵称,但不会同步到以前的信息上。
第一个client:
package Web;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class web_clientone{
public static void main(String[] args) {
Myclient one = new Myclient();
one.web_client(8848,"127.0.0.1","client_one");
}
}
@SuppressWarnings("serial")
class Myclient extends JFrame {//客户机窗体类
//该图形界面拥有四块区域,分别位于上、左、中、下(up、Left、middle、down)。
private JPanel Up = new JPanel();
private JPanel Left = new JPanel();
private JPanel Mid = new JPanel();
private JPanel Down = new JPanel();
//Up区域的子节点定义,3个标签、3个输入框、2个按钮
private JLabel lblLocalPort1 = new JLabel("Server IP: ");
private JLabel lblLocalPort2 = new JLabel("Port: ");
private JLabel lblLocalPort3 = new JLabel("My nickname: ");
protected JTextField tfLocalPort1 = new JTextField(15);
protected JTextField tfLocalPort2 = new JTextField(5);
protected JTextField tfLocalPort3 = new JTextField(5);
protected JButton butStart = new JButton("Connect server");
protected JButton butStop = new JButton("Disconnect Server");
//Left区域的子节点定义,显示框、滚动条
protected JTextArea Message = new JTextArea(28, 25);
JScrollPane scroll = new JScrollPane(Message);
//Mid区域的子节点定义,lstUsers在线用户界面
JScrollPane jsp = new JScrollPane();
@SuppressWarnings({ "rawtypes"})
JList UserList = new JList();
//Down区域的子节点定义,标签,输入框
private JLabel lblLocalPort4 = new JLabel("Message (press Enter to send): ");
protected JTextField tfLocalPort4 = new JTextField(20);
//上面是图形界面变量,下面是存放数据的变量
BufferedReader in;
PrintStream out;
public static int localPort;//默认端口
public static String localIP;//默认服务器IP地址
public static String nickname;//默认用户名
public Socket socket;
public static String msg;//存放本次发送的消息
Vector<String> clientNames = new Vector<>();
boolean JudgeLink = false;
public void web_client(int Port,String IP,String niname) {
localPort = Port;
localIP = IP;
nickname = niname;
init();
}
//初始化方法:初始化图形界面
private void init() {
//Up区域初始化:流式面板,3个标签、3个输入框,2个按钮
Up.setLayout(new FlowLayout());
Up.add(lblLocalPort1);
Up.add(tfLocalPort1);
Up.add(lblLocalPort2);
Up.add(tfLocalPort2);
Up.add(lblLocalPort3);
Up.add(tfLocalPort3);
tfLocalPort1.setText(localIP);
tfLocalPort2.setText(String.valueOf(localPort));
tfLocalPort3.setText(nickname);
Up.add(butStart);
Up.add(butStop);
butStart.addActionListener(new linkServerHandlerStart());
butStop.addActionListener(new linkServerHandlerStop());
butStop.setEnabled(false);//断开服务器按钮的初始状态应该为不可点击,只有连接服务器之后才能点击
//添加Left
Message.setEditable(false);
Left.add(scroll);
Left.setBorder(new TitledBorder("Chat -- message area"));
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
//添加Middle
Mid.setBorder(new TitledBorder("Online users"));
jsp.getViewport().setView(UserList);
UserList.setVisibleRowCount(28);
Mid.add(jsp);
//添加Down,JTextField输入框的回车事件默认存在,无需添加
Down.setLayout(new FlowLayout());
Down.add(lblLocalPort4);
Down.add(tfLocalPort4);
tfLocalPort4.addActionListener(new Myclient.SendHandler());
//图形界面的总体初始化 + 启动图形界面
this.setTitle("Client");
this.add(Up, BorderLayout.NORTH);
this.add(Left, BorderLayout.WEST);
this.add(Mid, BorderLayout.CENTER);
this.add(Down, BorderLayout.SOUTH);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.addWindowListener(new WindowHandler());
this.setPreferredSize(new Dimension(800, 600));
this.pack();
this.setVisible(true);
}
//“连接服务器”按钮的动作事件监听处理类:
private class linkServerHandlerStart implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
//当点击"连接服务器"按钮之后,该按钮被禁用(不可重复点击)。同时"断开服务器按钮"被恢复使用
localIP = tfLocalPort1.getText();
localPort = Integer.parseInt(tfLocalPort2.getText());
nickname = tfLocalPort3.getText();
linkServer();//连接服务器
if(JudgeLink) {
butStart.setEnabled(false);
butStop.setEnabled(true);
Thread acceptThread = new Thread(new Myclient.ReceiveRunnable());
acceptThread.start();
}
}
}
private class linkServerHandlerStop implements ActionListener {//“断开服务器”按钮的动作事件监听处理类
@Override
public void actionPerformed(ActionEvent e) {//当点击该按钮之后,断开服务器连接、清空图形界面所有数据
Message.append("==== You have exited ====\n");
clientNames = new Vector<>();
updateUsers();
out.println("——Customer ["+nickname+"] leaves:bye\n");
butStart.setEnabled(true);
butStop.setEnabled(false);
}
}
//连接服务器的方法
public void linkServer() {
try {
socket = new Socket(localIP, localPort);
JudgeLink = true;
} catch (Exception ex) {
Message.append("==== Failed to connect to the server ====\n");
}
}
//接收服务器消息的线程关联类
private class ReceiveRunnable implements Runnable {
public void run() {
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintStream(socket.getOutputStream());
out.println(nickname);//当用户首次连接服务器时,应该向服务器发送自己的用户名、方便服务器区分
Message.append("——["+nickname+"] successfully connected to the server......\n");
out.println("USERS");//向服务器发送请求,请求当前在线用户列表
while (true) {
if ((msg = in.readLine())!= null) {//读取服务器端的发送的数据
if (msg.matches(".*\\[.*\\].*")) {//此if语句的作用是:过滤服务器发送过来的 更新当前在线用户列表请求
clientNames.removeAllElements();
msg = msg.substring(msg.indexOf("[")+1,msg.indexOf("]"));
if(msg.contains(",")) {
String[] split = msg.split(",");
for(String ss : split) {
ss = ss.replace(" ","");
clientNames.add(ss);
}
}
else {
clientNames.add(msg);
}
updateUsers();
continue;
}
Message.append(msg + "\n");//更新"聊天——消息区" 信息
//此 if 语句作用:与服务器进行握手确认消息。
//当接收到服务器端发送的确认离开请求bye 的时候,用户真正离线
msg = msg.substring(msg.lastIndexOf(":") + 1);
if (msg.equals(nickname)) {
socket.close();
clientNames.remove(nickname);
clientout();
break;//终止线程
}
else if( msg.equals("——Server Shutdown.")|msg.equals("——You've been kicked out.")) {
socket.close();
clientNames.removeAllElements();
clientout();
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void clientout() {
updateUsers();
butStart.setEnabled(true);
butStop.setEnabled(false);
JudgeLink = false;
}
}
//"发送消息文本框"的动作事件监听处理类
private class SendHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
out.println("【" + nickname + "】:" + tfLocalPort4.getText());
tfLocalPort4.setText("");//当按下回车发送消息之后,输入框应该被清空
}
}
private class WindowHandler extends WindowAdapter {//窗口关闭的动作事件监听处理类
@Override// 当用户点击 "x" 离开窗口时,也会向服务器发送 bye 请求,目的是为了同步更新数据。
public void windowClosing(WindowEvent e) {
if(JudgeLink) {
cutServer();
}
}
}
private void cutServer() {
out.println("——Client ["+nickname+"] leaves:bye");
JudgeLink = false;
}
@SuppressWarnings("unchecked")
public void updateUsers() {//更新 "在线用户列表" 的方法
Mid.setBorder(new TitledBorder("Online users(" + clientNames.size() + ")"));
UserList.setListData(clientNames);
}
}
其余的client:
package Web;
public class web_clientone2{
public static void main(String[] args) {
Myclient one = new Myclient();
one.web_client(8848,"127.0.0.1","client_two");
}
}
server端:
package Web;
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class web_server{
public static void main(String[] args) {
Myserver one = new Myserver();
one.web_server();
}
}
@SuppressWarnings("serial")
class Myserver extends JFrame {
private JPanel frameup = new JPanel();
private JPanel framemidwest = new JPanel();
private JPanel framecenter = new JPanel();
private JLabel lblLocalPort = new JLabel("Local server listening port:");
protected JButton butStart = new JButton("Start Server");
protected JButton kickstart = new JButton("Kick out");
protected JTextField tfLocalPort = new JTextField(30);
protected JTextArea Message = new JTextArea(30, 45);
JScrollPane scroll = new JScrollPane(Message);
JScrollPane jsp = new JScrollPane();
@SuppressWarnings({ "rawtypes" })
JList UserList = new JList();
public static int localPort = 8848;// 默认端口
static int SerialNum = 0;// 用户连接数量
ServerSocket serverSocket;// 服务器端 Socket
ArrayList<AcceptRunnable.Client> clients = new ArrayList<>();// 用户连接对象数组
Vector<String> clientNames = new Vector<>();// UserList 中存放的数据
public void web_server() {
init();
}
private void init() {//初始化方法:初始化图形界面布局
frameup.setLayout(new FlowLayout());
frameup.add(lblLocalPort);
frameup.add(tfLocalPort);
frameup.add(butStart);
frameup.add(kickstart);
tfLocalPort.setText(String.valueOf(localPort));
butStart.addActionListener(new startServerHandler());
kickstart.addActionListener(new kickoutHandler());
kickstart.setEnabled(false);
framemidwest.setBorder(new TitledBorder("Listen for messages:"));
Message.setEditable(false);
framemidwest.add(scroll);
framecenter.setBorder(new TitledBorder("Online users:"));
jsp.getViewport().setView(UserList);
UserList.setVisibleRowCount(30);
framecenter.add(jsp);
this.setTitle("Server side");
this.add(frameup, BorderLayout.NORTH);
this.add(framemidwest, BorderLayout.WEST);
this.add(framecenter, BorderLayout.CENTER);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.addWindowListener(new WindowHandler());
this.setPreferredSize(new Dimension(800, 600));
this.pack();
this.setVisible(true);
}
private class startServerHandler implements ActionListener {
@Override//“启动服务器”按钮的动作事件监听处理类
public void actionPerformed(ActionEvent e) {
try {// 当点击按钮时,获取端口设置并启动新进程、监听端口
localPort = Integer.parseInt(tfLocalPort.getText());
serverSocket = new ServerSocket(localPort);
Thread acptThrd = new Thread(new AcceptRunnable());
acptThrd.start();
Message.append("**** Server (port "+localPort+") started ****\n");
butStart.setEnabled(false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
private class WindowHandler extends WindowAdapter{
public void windowClosing(WindowEvent e) {
if(UserList != null) {
for(Web.Myserver.AcceptRunnable.Client c : clients) {
c.out.println("——Server Shutdown.");
}//UserList.removeAll();
}
}
}
private class kickoutHandler implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
//Auto-generated method stub
int select = UserList.getSelectedIndex();
Object Nickname = UserList.getModel().getElementAt(select);
for(Web.Myserver.AcceptRunnable.Client c : clients) {
if(Nickname == c.nickname) {
c.out.println("——You've been kicked out.");
c.bekickout();
break;
}
}
}
}
private class AcceptRunnable implements Runnable {//接受用户连接请求的线程关联类
public void run() {//持续监听端口,当有新用户连接时 再开启新进程
while (true) {
try {
Socket socket = serverSocket.accept();//新的用户已连接,创建 Client 对象
Client client = new Client(socket);
Message.append("——Client【" + client.nickname + "】join\n");
Thread clientThread = new Thread(client);
clientThread.start();
clients.add(client);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
//服务器存放用户对象的客户类(主要编程)。每当有新的用户连接时,该类都会被调用
//该类继承自 Runnable,内部含有 run()方法
private class Client implements Runnable {
private Socket socket;//用来保存用户的连接对象
private BufferedReader in;//IO流
private PrintStream out;
private String nickname;//保存用户昵称
public Client(Socket socket) throws Exception {//Client类的构建方法。当有新用户连接时会被调用
this.socket = socket;
InputStream is = socket.getInputStream();
in = new BufferedReader(new InputStreamReader(is));
OutputStream os = socket.getOutputStream();
out = new PrintStream(os);
nickname = in.readLine();//获取用户昵称
for (Client c : clients) {//将新用户的登录消息发给所有用户
c.out.println("——Client【" + nickname + "】join");
}
}
public void bekickout() {
String usermsg = "——Client【" + this.nickname + "】have been kicked out.";
clients.remove(this);
for (Client c : clients) {
c.out.println(usermsg);
}
Message.append("——Client【" + this.nickname + "】have been kicked out.\n");
updateUsers();
}
public void run() {//客户类线程运行方法
try {
while (true) {
String usermsg = in.readLine();//读用户发来消息
if(usermsg != null) {
String secondMsg = usermsg.substring(usermsg.lastIndexOf(":") + 1);// 字符串辅助对象
if (usermsg.length() > 0) {// 如果用户发过来的消息不为空
// 如果消息是 bye,则断开与此用户的连接并告知所有用户当前信息,跳出循环终止当前进程
if (secondMsg.equals("bye")) {
clients.remove(this);
Message.append("——Client leaves:" + nickname + "\n");
for (Client c : clients) {
c.out.println(usermsg);
c.out.println("——Client leaves:" + nickname);
}
updateUsers();//更新在线用户数量UserList的界面信息
break;
}
if (usermsg.equals("USERS")) {//每当有新用户连接时,服务器就会接收到USERS请求
updateUsers();//当服务器接收到此请求时,就会要求现在所有用户更新在线用户数量的列表
continue;
}
for (Client c : clients) {//当用户发出的消息都不是以上两者时,消息才会被正常发送
c.out.println(usermsg);
}
Message.append(usermsg+'\n');
}
}
}
socket.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
@SuppressWarnings("unchecked")
public void updateUsers() {//更新在线用户数量信息,并要求所有的用户端同步更新
clientNames.removeAllElements();// clientNames是Vector<String>对象,用来存放所有用户的名字
StringBuffer allname = new StringBuffer();
for (AcceptRunnable.Client client : clients) {
clientNames.add(0, client.nickname);
allname.insert(0, "|" + client.nickname);
}
framecenter.setBorder(new TitledBorder("Online users number(" +clientNames.size() + ")"));
for (Client c : clients) {//要求所有的用户端同步更新
c.out.println(clientNames);
}
UserList.setListData(clientNames);
if(UserList.getModel().getSize()!=0) {
kickstart.setEnabled(true);
}else {
kickstart.setEnabled(false);
}
}
}
}
}
改进方向:
课设没有做心跳包等类似的检测client与server是否连接的部分,最好在后续添加上。