最近老师叫我们做一个基于Socket的多人聊天室,网上很多教程都只讲了如何通过Socket来建立连接以及通过控制台一遍打印证明连接已经完成但还没有具体实现多人聊天。这次我整理了一下自己的实现代码,希望能帮助到和我一样学习时感到困惑的兄弟姐妹。
这是基于TCP协议实现的Socket多人聊天室,分别用到ServerSocket(服务器)和Socket(客户端),说到TCP当然也离不开建立我们常常听到的三次握手、四次挥手,这里附上几位大佬的总结。
https://blog.csdn.net/jia281460530/article/details/41901069
https://blog.csdn.net/u013782203/article/details/51767763
聊天室有一个简单的图形界面,每一次客户端请求并完成连接时,服务器端会显示当前在线人数,同时在侧边栏添加了一个更新在线客户端信息的功能界面,显示在线的客户端(具体是显示每个客户端的名字,当然有一个局限就是客户端的名字不能相同)。如果想要在多台计算机之间实现通信,可以以管理员身份运行cmd命令行输入<mark>ipconfig</mark>查看相应的ip地址。
服务器主要功能:
服务器端的任务是不断地接收来自于多个客户端的连接请求,并且为每一个请求连接的客户端分配一个线程管理,每一个线程只管理与它相连接的客户端Socket。这里我用了Client内部类来封装每一个已经连接的客户端,这样子的好处是Socket的信息及输入输出流都能够一起保存;并将它们添加到List中,用于记录在线的客户端信息。同时,服务器起到的一个最重要的作用是转发,将A客户端发出的消息转发给B、C、D等多个在线客户端,这个时候保存了各个Client类的List就派上了关键用场,服务器每次收到一个客户端的消息就在List中查找每个在线客户端并向他们转发这个客户端所发出的消息(当然,跳过它自己本身)。总的来说服务器有以下几个功能:
- 循环监听并响应各个客户端的连接并为他们分配线程 **
- 接受各个客户端的消息并处理(代码中主要是在消息前面加上这个客户端的名字 如:Client_Name: + aLine(消息))
- 通过已保存好各个客户端信息的List来转发通过处理后的消息 **
- 如果有某个客户端A下线,则告诉其他在线客户端这个客户端A下线(代码中通过客户端发送Good Bye来说明离线)
- 更新【在线客户端信息界面】并将更新通知发往每一个在线客户端
客户端功能:
- 发送消息
- 接收消息并处理显示
- 更新在线客户端
其中,接收到的消息为:【Client_Name:+消息 】的时候显示在主文本框中;接收到的消息为:【update:+内容】的时候代表这是一个更新在线客户端的通知,显示在侧边的在线Client文本框中,内容中就是在线客户端的全部信息,并且每个客户端是以一个空格分隔的(这样子处理是因为我用BufferedReader读取消息时每次只读一行,(因为本人对输入输出流也并非太了解。。。))这个地方感觉做的不太好,另外,如果大家有什么建议或想法欢迎大家能跟我说说讨论讨论。
用到的技术:
- 图形界面GUI及响应事件
- 基于TCP的ServerSocket、Socket套接字
- 输入输出流
- Thread类
- 其中,服务器Thread主要是为每一个【封装了Socket的Client类】分配一个线程,客户端Thread主要是为了能实现不断地发送消息和同时不断地接受消息的功能。
以下是具体实现的代码
服务器:
package Server;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.border.LineBorder;
public class Server extends JFrame {
// server 对象
private ServerSocket server;
// Client's socket对象
private Socket socket;
// 存放客户端Client
List<Client> list;
// 窗口标签
private JLabel label;
// 面板用于容纳文本区和按钮
private JPanel labelPanel, clientPanel;
// 显示区
private JTextArea centerTextArea, clientTextArea;
public Server(int port) throws IOException {
server = new ServerSocket(port);
//控制台输出服务器端口号和ip地址
System.out.println(server.getLocalPort());
System.out.println(InetAddress.getLocalHost().getHostAddress());
//初始化存放Client的链表
list = new ArrayList<Client>();
setTitle("服务器");
//图形界面GUI
create();
//添加客户端
addClient();
}
private void create() {
setSize(500, 500);
// 窗口label,显示服务器名字
label = new JLabel("10086 服务器");
label.setFont(new Font("宋体", 5, 15));
labelPanel = new JPanel();
labelPanel.setSize(150, 20);
labelPanel.add(label, BorderLayout.CENTER);
// 窗口center部分,显示接收到的Client的消息
centerTextArea = new JTextArea(" ---聊天室已连接--- " + "\r\n");
centerTextArea.setFont(new Font("微软雅黑", 5, 13));
centerTextArea.setEditable(false); // 不可编辑
centerTextArea.setBackground(Color.LIGHT_GRAY);
// 窗口client部分,显示当前在线的Client
clientTextArea = new JTextArea("--在线Client--" + "\r\n");
clientTextArea.setEditable(false); // 不可编辑
//装载clientTextArea的容器
clientPanel = new JPanel(new FlowLayout());
clientPanel.setBorder(new LineBorder(null));
clientPanel.add(clientTextArea);
//JFrame框架用于添加各种容器
add(clientPanel, BorderLayout.EAST);
add(labelPanel, BorderLayout.NORTH);
add(new JScrollPane(centerTextArea), BorderLayout.CENTER);
setVisible(true);
setResizable(false); // 窗口大小不可调整
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
// 添加客户端
private void addClient() throws IOException {
//循环监听客户端的连接请求并逐个分配线程
while (true) {
// 接受客户端连接请求并保存
socket = server.accept();
//图形界面输出当前客户端ip地址以及在线人数
String ip = socket.getInetAddress().getHostAddress();
centerTextArea.append(ip + "连接成功,当前客户端数为:" + (list.size() + 1) + "\r\n");
Client client = new Client(socket);
// 添加用户到列表
list.add(client);
// 为用户创建并启动一个接受线程
new Thread(client).start();
//【在线客户端】更新通知
client.update();
}
}
//客户端处理类(【接受线程】用于不断接受消息 同时不影响自己发送消息)
class Client implements Runnable {
String name;
Socket socket;
// 输出流
private PrintWriter out;
// 输入流
private BufferedReader in;
public Client(Socket socket) throws IOException {
this.socket = socket;
// 获得相应Client's Socket 输入流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 获得相应Client's Socket 输出流
out = new PrintWriter(socket.getOutputStream());
//获取当前Client的名字并更新clientTextArea
name = in.readLine();
clientTextArea.append(name+"\r\n");
}
//向对应流发送消息
public void send(String str) {
out.println(str);
out.flush();
}
//给【每一个客户端】发送更新clientTextArea的消息
public void update() {
StringBuffer sBuffer = new StringBuffer("update:");
clientTextArea.setText("--在线Client--\r\n");
for(int i = 0;i<list.size();i++) {
clientTextArea.append(list.get(i).name+"\r\n");
sBuffer.append(list.get(i).name+" ");
}
for(int i = 0;i<list.size();i++) {
list.get(i).out.println(sBuffer);
list.get(i).out.flush();
}
}
@Override
public void run() {
try {
String aLine;
boolean flag = true;
while (flag && (aLine = in.readLine()) != null) {
//处理并保存当前客户端发来的消息(消息前加上当前客户端名字)
String str = this.name + "说:" + aLine;
//内容显示
centerTextArea.append(str + "\r\n");
// 给每一个保存在List中的客户端(除了当前客户端)转发当前客户端发来的消息
for (int i = 0; i < list.size(); i++) {
Client client = list.get(i);
if (client != this) {
client.send(str);
/* * 如果当前客户端发来的消息是“Good Bye”表示其要下线 * 服务端则转告给【其他的客户端】该客户端下线的消息并进行对 * 当前客户端Socket关闭的处理 */
if (aLine.equals("Good Bye")) {
client.send(this.name + "已离线");
flag = false;
}
}
}
}
} catch (Exception e1) {
} finally {
try {
//当前客户端Socket关闭处理
//1.链表中移除该Client
if (list.contains(this))
list.remove(this);
/* * 2.更新服务器界面中的【在线客户端窗口】以及 * 为其他的客户端发送【更新在线客户端窗口】的消息 */
update();
System.out.println(this.name+"已离开");
//3.输出输入流关闭处理
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
} catch (Exception e) {
System.out.println(this.name + "出现异常强行关闭");
}
}
}
}
public static void main(String[] args) throws IOException {
Server Test = new Server(10086);
}
}
客户端:
package Client;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.LineBorder;
public class Client extends JFrame implements Runnable {
// Client_Name
private String name;
// Client's socket对象
private Socket socket;
// 输出流
private PrintWriter out;
// 输入流
private BufferedReader in;
// 用于关闭线程
Thread receivethread;
// 窗口标签
private JLabel label;
// 面板用于容纳文本区和按钮
private JPanel buttomPanel, inputPanel, labelPanel, centenPanel;
// 显示区以及输入区
private JTextArea centerTextArea, inputTextArea, clientTextArea;
// 发送与清除按钮
private JButton send, clear;
//Client
public Client(String name, Socket socket) throws IOException {
this.name = name;
this.socket = socket;
//控制台输出端口号以及主机地址
System.out.println(socket.getLocalPort());
System.out.println(InetAddress.getLocalHost().getHostAddress());
//得到输入输出流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream());
//发送名字给服务器
out.println(name);
out.flush();
setTitle(name);
//Client端GUI
create();
//创建并启动自己的接收线程
receivethread = new Thread(this);
receivethread.start();
//监听程序启动
setActionLister();
}
//GUI
private void create() {
setSize(500, 500);
// 窗口label
label = new JLabel("10086 聊天室");
label.setFont(new Font("宋体", 5, 15));
labelPanel = new JPanel();
labelPanel.setSize(150, 20);
labelPanel.add(label, BorderLayout.CENTER);
// 窗口center部分
centerTextArea = new JTextArea(" ---聊天室已连接--- " + "\r\n");
centerTextArea.setFont(new Font("微软雅黑", 5, 13));
centerTextArea.setEditable(false); // 不可编辑
centerTextArea.setBackground(Color.LIGHT_GRAY);
// 窗口client部分
clientTextArea = new JTextArea("--在线Client--" + "\r\n");
clientTextArea.setEditable(false); // 不可编辑
clientTextArea.setBorder(new LineBorder(null));
// centerPanel 充当center和client的容器
centenPanel = new JPanel(new BorderLayout());
centenPanel.add(new JScrollPane(centerTextArea), BorderLayout.CENTER);
centenPanel.add(clientTextArea, BorderLayout.EAST);
// 窗口button按钮
send = new JButton("发送");
clear = new JButton("清空");
buttomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5));
buttomPanel.add(clear);
buttomPanel.add(send);
// 窗口input部分
inputPanel = new JPanel(new BorderLayout());
inputTextArea = new JTextArea();
inputTextArea = new JTextArea(7, 20);
inputPanel.add(new JScrollPane(inputTextArea), BorderLayout.CENTER);
inputPanel.add(buttomPanel, BorderLayout.SOUTH);
add(labelPanel, BorderLayout.NORTH);
add(centenPanel, BorderLayout.CENTER);
add(inputPanel, BorderLayout.SOUTH);
setVisible(true);
setResizable(false); // 窗口大小不可调整
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//inpuTextArea以及按钮send&clear的监听程序
private void setActionLister() {
//关闭窗口时通知服务器本客户端已关闭
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
try {
if(out!=null) {
out.println("Good Bye");
out.flush();
}
}
catch (Exception e2) {
System.out.println("客户端已关闭");
}
}
});
//点击send按钮将inputTextArea的消息全部发送
send.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String aLine = inputTextArea.getText();
inputTextArea.setText(""); //输入框清空
centerTextArea.append("你说: " + aLine + "\r\n");
try {
out.println(aLine);
out.flush();
if (aLine.equals("Good Bye")) {
//接受线程中断
receivethread.interrupt();
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
}
} catch (Exception e1) {
System.out.println("已断开");
}
}
});
//键盘监听事件
inputTextArea.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
//CTRL+ENTER相当于点击send按钮
if (e.getKeyCode() == KeyEvent.VK_ENTER && e.isControlDown()) {
send.doClick();
}
}
});
//清除inputTextArea的内容
clear.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
inputTextArea.setText("");
}
});
}
@Override
public void run() {
try {
String aLine = "";
while ((aLine = in.readLine()) != null) {
//centerTextAre显示接受的消息
if (!aLine.split(":")[0].equals("update"))
centerTextArea.append(aLine + "\r\n");
//在綫client更新
else {
String[] strings = (aLine.split(":")[1]).split(" ");
clientTextArea.setText("--在线Client--\r\n");
for (String s : strings)
clientTextArea.append(s + "\r\n");
}
}
} catch (Exception e) {
centerTextArea.append("当前连接已断开\r\n");
System.out.println("当前连接已断开");
}
}
public static void main(String[] args) throws IOException {
new Client("Client5", new Socket("localhost", 10086));
// new clientGUI("Client2", new Socket("localhost", 10086));
}
}