---------------siwuxie095
关于 聊天服务器,详见本人博客的分类:来一杯Java,
里面的 使用ServerSocket建立聊天服务器(二)
本人博客(任选一个)链接:
https://www.baidu.com/s?ie=UTF-8&wd=siwuxie095
将 使用ServerSocket建立聊天服务器(二) 中的 ChatSocket.java
和 ChatManager.java 略作修改:
ChatSocket.java:
package com.siwuxie095.socket;
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.Socket;
/** * run()循环执行的读取的工作,即当前的服务器会不断的从客户端读取内容 * 将读取到的内容发送到集合Vector中的所有客户端(除了自身) * * output()方法中,代码段(1)等效于代码段(2) * * @author siwux * */
//创建用于Socket通信的线程:ChatSocket public class ChatSocket extends Thread {
Socket socket;
//创建构造方法,传入Socket对象 public ChatSocket(Socket socket) { this.socket=socket; }
public void output(String out) {
//(1) // try { // socket.getOutputStream().write((out+" ").getBytes("UTF-8")); // } catch (UnsupportedEncodingException e) { // e.printStackTrace(); // } catch (IOException e) { // System.out.println("断开了一个客户端连接"); // ChatManager.getChatManager().remove(this); // e.printStackTrace(); // }
//(2) try {
//对当前的Socket执行 数据输出相关功能的包装 //使用getOutputStream()获取输出流,通过输出流向外输出数据 //返回值是OutputStream类型,创建以接收返回值 OutputStream os=socket.getOutputStream();
//创建一个BufferedWriter作为数据的输出,传入匿名对象,指定编码,层层包装 BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"UTF-8"));
//让BufferedWriter输出字符串 //同时加一个换行符,表示一行已完结,这样flush时才会强制输出 //如果不加,数据只会传到当前socket的缓冲区,而发不到其他的客户端的界面上 bw.write(out+" "); //因为带缓冲,所以需要强制输出,不然无法输出 bw.flush();
}catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { System.out.println("断开了一个客户端连接"); ChatManager.getChatManager().remove(this); e.printStackTrace(); }
}
//复写run()方法 @Override public void run() {
output("你已经连接到了服务器");
try {
//对Socket的输入流进行包装 //在指定InputStreamReader时,指定编码的字符集 //有异常抛出,用 try catch 捕获 BufferedReader br=new BufferedReader( new InputStreamReader( socket.getInputStream(),"UTF-8"));
String line=null; //读取从客户端发送给服务器的数据 while ((line=br.readLine())!=null) { System.out.println(line); //通过静态方法,将自己传入,同时传入line ChatManager.getChatManager().publish(this, line); }
//关闭流 br.close();
System.out.println("断开了一个客户端连接"); ChatManager.getChatManager().remove(this);
} catch (IOException e) { System.out.println("断开了一个客户端连接"); ChatManager.getChatManager().remove(this); e.printStackTrace(); }
} } |
ChatManager.java:
package com.siwuxie095.socket;
import java.util.Vector;
/** * ChatManager 将不同的socket所新建的ChatSocket线程管理起来 * 由于一个聊天服务器只能有一个聊天的管理器:ChatManager * 所以要把这个类作单例化处理 * 单例化的第一步就是让这个类的构造方法变成 private 类型 * * @author siwux * */ public class ChatManager {
//让构造方法变成private类型,完成单例化的第一步 private ChatManager(){}
//为当前类创建一个实例 private static final ChatManager cm=new ChatManager();
//创建方法 getChatManager(),返回ChatManager类型 public static ChatManager getChatManager() { return cm; }
//至此,完成了这个类(ChatManager)的单例化 //接下来就是对ChatSocket线程进行管理
//创建一个Vector,指定泛型为ChatSocket Vector<ChatSocket> vector=new Vector<>();
//创建add()方法,为当前集合添加一个新的ChatSocket对象 //在创建ChatSocket时使用,即ServerListener.java中使用 public void add(ChatSocket cs) { //调用Vector的add()方法,传入cs即可 vector.add(cs); }
public void remove(ChatSocket cs) { vector.remove(cs); }
//创建publish()方法,其中的某个ChatSocket线程可以调用publish() //向其他的客户端(其他的ChatSocket线程)发送信息 //传入线程本身和需要发送的信息 public void publish(ChatSocket cs,String msg) {
//因为要发送给集合Vector中的其他所有线程,要使用遍历 for (int i = 0; i < vector.size(); i++) { //获取循环中的第 i 个对象 ChatSocket csx=vector.get(i);
//当前发送信息的线程就不用再接收这条信息, //判断发送消息的对象是不是当前对象 if (!cs.equals(csx)) { csx.output(msg); } } }
} |
聊天客户端:
工程名:MyJavaChatClient
包名:com.siwuxie095.main、com.siwuxie095.view
类名:StartClient.java(主类)、ChatManagerX.java、MainWindow.java
工程结构目录如下:
StartClient.java(主类):
package com.siwuxie095.main;
import java.awt.EventQueue;
import com.siwuxie095.view.MainWindow;
public class StartClient {
public static void main(String[] args) {
//将窗体MainWindow.java的主方法里的代码复制粘贴到这里(StartClient.java) //将MainWindow.java里的主方法删除,这样就可以在主程序中将窗体执行出来了 EventQueue.invokeLater(new Runnable() { public void run() { try { MainWindow frame = new MainWindow(); frame.setVisible(true); //在创建Window时传递对MainWindow的引用 ChatManagerX.getCM().setWindow(frame); } catch (Exception e) { e.printStackTrace(); } } });
}
} |
ChatManagerX.java:
package com.siwuxie095.main;
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException;
import com.siwuxie095.view.MainWindow;
//需要将这个类单例化 public class ChatManagerX {
//让构造方法变成private类型,完成单例化的第一步 private ChatManagerX(){}
//创建一个 private static final 修饰的实例 private static final ChatManagerX instance=new ChatManagerX();
//创建公有的 getChatManager()获取上面的私有实例 //这样,当前的ChatManager类就只会有唯一的一个实例 public static ChatManagerX getCM() { return instance; }
//现在界面中只能向ChatManager发送数据 //为了让接收到的数据放到界面上,最简便的方法即在本地引用MainWindow MainWindow window; //实现setWindow()方法 public void setWindow(MainWindow window) { this.window = window; //在ChatManager中操作window.appendText()在界面中显示数据 window.appendText("提示:文本框已经和ChatManager绑定了"); }
//创建一个Socket对象 Socket socket; //创建输入流 BufferedReader BufferedReader reader; //创建输出流 PrintWriter 这里不使用 BufferedWriter PrintWriter writer;
//具体实现连接服务器的操作 public void connect(String ip) {
//呼叫连接时创建一个新的线程 //对网络的处理放到线程中执行 //(先建立连接,之后循环从服务器读取数据) new Thread(){
@Override public void run() {
try { //传入ip 并设置端口为 12345 socket=new Socket(ip, 12345);
//获取输入和输出流,层层包装 writer=new PrintWriter( new OutputStreamWriter( socket.getOutputStream(),"UTF-8"));
reader=new BufferedReader( new InputStreamReader( socket.getInputStream(),"UTF-8"));
//创建完输入输出流,需要循环监听当前输入流是否有数据 String line; while ((line=reader.readLine())!=null) { //通过window.appendText()将数据输出到界面中 window.appendText("收到:"+line); }
//读取数据为空,则连接结束,关闭流 writer.close(); reader.close(); //置空,更安全 writer=null; reader=null;
} catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
}.start(); }
//点击发送按钮时的操作 public void send(String out) { //发送时,writer不为空才能发送 if (writer!=null) { //为当前的socket输出内容时,需要添加 进行换行 //这时flush才能强制把这一行输出, //即这一行完结了,flush才会把它输出 // //如果不加 数据实际上是传出去了, //但只是传到了当前socket的缓冲区域 //只有关闭了socket,socket才会把它强制输出,这是不可取的 //因为聊天要保证实时性,所以要加一个换行符 writer.write(out+" "); //flush强制刷新输出 writer.flush(); }else { //提示信息 window.appendText("提示:当前的连接已经中断..."); }
} } |
MainWindow.java:
package com.siwuxie095.view;
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent;
import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.LayoutStyle.ComponentPlacement; import javax.swing.border.EmptyBorder;
import com.siwuxie095.main.ChatManagerX;
/** * 添加 默认序列版本ID * 添加 一个TextArea:JTextArea * 选择根容器 contentPane,将布局改为 GroupLayout * * 在 JTextArea 上下方各添加一个JTextField、JButton * * @author siwux * */ public class MainWindow extends JFrame {
/** * 添加默认序列版本ID * Add default serial version ID */ private static final long serialVersionUID = 1L; private JPanel contentPane;
//将文本控件等声明到类当中,作为整个类的变量 JTextArea txt; private JTextField ip; private JTextField send;
/** * Create the frame. */ public MainWindow() { //添加属性 setAlwaysOnTop(true) //这样打开多个窗口都可以显示在其他应用的上方 setAlwaysOnTop(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 504, 353); contentPane = new JPanel(); contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); setContentPane(contentPane);
txt = new JTextArea(); txt.setText("Ready...");
ip = new JTextField(); ip.setText("127.0.0.1"); ip.setColumns(10);
JButton button = new JButton("连接到服务器"); button.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent arg0) { //使用ChatManager的connect()和按钮的监听事件绑定 //通过ip.getText()获得需要连接的服务器的IP地址 //通过IP来建立Socket连接 ChatManagerX.getCM().connect(ip.getText());
} }); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent arg0) { } });
send = new JTextField(); send.setText("你好"); send.setColumns(10);
JButton btnNewButton = new JButton("发送"); btnNewButton.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent arg0) { //使用ChatManager的send()和按钮的监听事件绑定 //传入send文本框的文本 //第一个send()到服务器 //第二个appendText()添加到中间的显示界面 ChatManagerX.getCM().send(send.getText()); appendText("我说:"+send.getText()); //当前文本发送出去后,需要将当前的文本框清空 send.setText("");
} }); btnNewButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { } });
//下面这一大段是自动生成的代码,不用管 GroupLayout gl_contentPane = new GroupLayout(contentPane); gl_contentPane.setHorizontalGroup( gl_contentPane.createParallelGroup(Alignment.LEADING) .addGroup(gl_contentPane.createSequentialGroup() .addContainerGap() .addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING) .addComponent(txt, GroupLayout.DEFAULT_SIZE, 417, Short.MAX_VALUE) .addGroup(gl_contentPane.createSequentialGroup() .addComponent(send, GroupLayout.PREFERRED_SIZE, 302, GroupLayout.PREFERRED_SIZE) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(btnNewButton, GroupLayout.DEFAULT_SIZE, 109, Short.MAX_VALUE)) .addGroup(gl_contentPane.createSequentialGroup() .addComponent(ip, GroupLayout.PREFERRED_SIZE, 321, GroupLayout.PREFERRED_SIZE) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(button, GroupLayout.PREFERRED_SIZE, 90, Short.MAX_VALUE))) .addContainerGap()) ); gl_contentPane.setVerticalGroup( gl_contentPane.createParallelGroup(Alignment.LEADING) .addGroup(gl_contentPane.createSequentialGroup() .addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE) .addComponent(ip, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(button)) .addPreferredGap(ComponentPlacement.RELATED) .addComponent(txt, GroupLayout.DEFAULT_SIZE, 201, Short.MAX_VALUE) .addPreferredGap(ComponentPlacement.RELATED) .addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE) .addComponent(send, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) .addComponent(btnNewButton))) ); contentPane.setLayout(gl_contentPane); }
//为当前类创建一个appendText()方法,传入一个String public void appendText(String in) { //添加到中间的显示界面 txt.append(" "+in); } } |
先将服务器运行:
再运行客户端:
客户端界面:
点击 连接到服务器,服务器 弹出提示:
点击 确定,客户端弹出服务器发出的消息:
再次运行另一个客户端,两个客户端之间发送消息:
此时,服务器的控制台也会打印所有经过服务器的消息:
整个流程:
两个客户端 通过 IP 和 端口 向服务器发起 Socket 通信请求,
服务器监听到请求并同意
两个客户端就是两个 Socket 对象,服务器创建两个 ChatSocket 线程,
分别处理这两个连接,且这两个线程由 ChatManager 管理
这两个 ChatSocket 线程先向两个客户端发出消息:"你已经连接
到了服务器",实际上这是服务器发送给客户端的消息
客户端之间发送消息:客户端 A 发出消息后,服务器接收到该消息,
并将其转发到客户端 B,即 服务器 相当于 中转站
如果有多个客户端,服务器依然是中转站,将一个客户端的消息转发
给其他客户端(相当于 群聊)
【made by siwuxie095】