一.引言
```java
/*标志服务端是否开启*/
private boolean isStart;
/**
* 服务端使用ServerSocket,port作为监听的端口
* 客户端使用Socket
*/
private ServerSocket serverSocket;
/**
* 客户端集群
*/
private List<Client> clients;
1.2 构造函数
public QQServer(String host,int port){
try {
serverSocket=new ServerSocket();
SocketAddress socketAddress=new InetSocketAddress(host,port);
serverSocket.bind(socketAddress,port);
isStart=true;
clients=new ArrayList<>();
System.out.println("服务器启动成功!"+serverSocket.getLocalSocketAddress());
}catch (Exception e){
System.out.println("服务器启动异常: "+e.getMessage());
System.exit(0);
}
}
主要是进行变量初始化以及绑定服务器ip以及端口
1.3 实现多线程
由于服务端要一直处理客户端连接,消息的接受与发送,于是我们需要将其设计为多线程。实现多线程的方式主要有四种:继承Thread类,实现Runnable接口,实现Callable接口,匿名内部类。这里,选择第一种。
public class QQServer extends Thread
覆写 start方法,这里是处理客户端的连接,Client是服务端内部的客户端存储类,目的是为了将远程客户端抽象为内部Socket对象存储,存储后便启动该对象线程来接受消息,显然,client也应当是多线程的。详细之后会介绍。
@Override
public synchronized void start() {
try {
System.out.println("等待连接中...");
while (isStart){
/*循环等待*/
Socket socket=serverSocket.accept();
System.out.println("连接成功! Address: "+socket.getRemoteSocketAddress());
Client client=new Client(socket);
clients.add(client);
System.out.println("当前在线人数: "+(clients.isEmpty()?0:clients.size()));
/*这里只能用start,用run会阻塞*/
new Thread(client).start();
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.4 Client实现 (服务端内部存储对象)
class Client implements Runnable
1.4.1 成员变量
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
private boolean isConnect;
private Socket client;
主要是输入输出流以及socket实例
1.4.2 发送消息
利用DataOutputStream的writeUTF方法发送
private void sendMessage(String message){
try {
dataOutputStream.writeUTF(message);
} catch (IOException e) {
e.printStackTrace();
}
}
1.4.3 覆写run函数
这里主要是用于接受该client中socket实例的消息,利用DataInputStream的readUTF函数,并将该消息发送给客户端队列中的所有client,实现消息群发。
@Override
public void run() {
try {
while(isConnect){
String message=dataInputStream.readUTF();
//为每个客户端发送消息
for (Client client : clients) {
client.sendMessage(message);
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (dataOutputStream != null) {
dataOutputStream.close();
}
if (serverSocket != null) {
serverSocket.close();
serverSocket = null;
}
isConnect=false;
clients.remove(client);
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private JTextArea outArea; //输出区域
private JTextArea inputArea; //输入区域
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
private Socket socket;
private boolean isConnect;
private Thread receive;//消息监听线程
private int name; //客户端名字,目前以端口做名字
2.2 界面设计
public class QQClient extends JFrame
客户端是供用户使用,所以需要实现一个简单的界面。于是,我采用JFrame进行界面设计。JFrame功能也很强大,组件也相当齐全,而且使用简单,但缺点就是没有合适的可视化界面,无法立即呈现效果。如果想有图形化界面,可以使用JavaFX。
public void createFrame(){
/*窗口面板设置*/
this.setLocation(250,100);
this.setTitle("中间件虚拟群");
this.setPreferredSize(new Dimension(550,600));
/*输出区域*/
outArea.setBackground(Color.LIGHT_GRAY);
outArea.setEditable(false);
outArea.setPreferredSize(new Dimension(550,400));
/*输入区域*/
inputArea.setBackground(Color.WHITE);
inputArea.setPreferredSize(new Dimension(550,50));
inputArea.setLineWrap(true);
/*名字*/
JTextArea nameArea=new JTextArea();
nameArea.setText("name: "+name);
nameArea.setEditable(false);
nameArea.setBackground(Color.gray);
/*发送按钮*/
JButton send=new JButton();
send.setText("发送");
add(outArea,BorderLayout.NORTH);
add(inputArea,BorderLayout.CENTER);
add(nameArea,BorderLayout.SOUTH);
add(send,BorderLayout.EAST);
//窗口关闭监听
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
disConnect();
}
});
//发送按钮响应
send.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String text = inputArea.getText().trim();
// 清空输入区域信息
inputArea.setText("");
// 回车后发送数据到服务器
sendMessage(text);
}
});
pack();
setVisible(true);
}
注意,最后要加上pack,以及设置窗口可视化。
2.3 发送消息
这里同服务端设计,比较简单
private void sendMessage(String text) {
try{
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dataOutputStream.writeUTF(formatter.format(new Date())+" " +name+": \n"+" "+text);
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
2.4 消息监听线程
当服务端消息到达时,该线程会立即接收并显示在界面中
private class ReceiverThread implements Runnable{
@Override
public void run() {
try {
System.out.println(name+" 的消息线程启动,开始监听...");
while (isConnect){
String message=dataInputStream.readUTF();
outArea.setText(outArea.getText() + "\n" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5 断开连接
当客户端断开连接后,需要释放相应资源以及让其消息线程让出cpu占有权,这里使用join指令。
private void disConnect() {
try {
isConnect=false;
//消息线程释放cpu
receive.join();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
if (dataOutputStream != null) {
dataOutputStream.close();
}
if (socket != null) {
socket.close();
socket = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class QQChat {
public static void main(String[] args) {
QQServer qqServer=new QQServer("127.0.0.1",10000);
qqServer.start();
}
}
再启动多个客户端
public class QQClientChat {
public static void main(String[] args) {
QQClient qqClient=new QQClient("127.0.0.1",10000);
qqClient.createFrame();
}
}
总结
1.SocketAPI调用过程
2. 多线程编程
详情可以参考:https://m.runoob.com/java/java-multithreading.html
3. JFrame
详情可以参考:https://docs.oracle.com/javase/8/docs/api/javax/swing/JFrame.html
4. Github
https://github.com/XMU-YG/Lab1_MiniQQ
持续更新,拓展功能。