多人聊天室的实现
多人聊天室
一、功能简介
每个客户端在连接到服务器端时,开始发送消息到服务端,服务端在接收到客户端的连接时,首先输出谁进入了聊天室,然后把客户端发来的消息转发给其他客户端,实现群聊的功能,最终达到实现多功能(快速、实时、多人)的多人聊天给用户带来更好的体验功能。
二、设计构想
-
设计客户端及服务器的界面
- 输入框与输出框
- 用户昵称
- 消息时间
- 添加滚动条
- 按钮的添加
- 功能键的设置
- 用户进出的显示
-
服务器端与客户端的交互
-
首先服务器端要实例化一个ServerSocket对象,并用其中的accept()方法等待客户端的连接。
这里值得注意的是:ServreSocket类中的accept()方法是一个阻塞方法,也就是说accept()方法在获取客户端的连接之前会一直进行阻塞式的等待,不会让其下面的代码执行,直到得到客户端的连接为止。 -
服务器一直等待的同时,我们就要建立客户端并向服务器申请连接。那么在客户端代码中,我们要实例化一个Socket对象,其应该具有和服务器端ServreSocket相同的端口号,以此确保对服务器提出准确的连接。在成功实例化创建Socket对象后,就相当于成功和服务器建立了连接(前提是申请的服务器对象已创建并在执行accept()方法等待)。
- 在客户端成功提出连接申请后,我们再回到服务器端accept()方法上来,accept()方法在成功得到连接申请后,返回的值是一个Socket对象,该Socket对象是通向该客户端的连接。
-
在成功通过Socket对象和ServerSocket对象在客户端与服务器间建立TCP连接后,我们就要开始进行服务器与客户端间的信息交流了,其手段则是通过IO流的实现: 通过Socket类中的getInputStream()方法和getOutputStrea()方法可以获取对应方向的输入流和输出流。
- 服务器端通过对accept()得到的Socke对象使用getOutputStrea()方法创建的OutputStream流可以向客户端写入信息,同理对该Socket对象使用getInputStream()方法获取的InputStream流可以从客户端中读取信息。反之客户端亦然。
-
优化界面
- 解决后续BUG
- 将用户界面更加简化不繁琐
三、实验结果展示
-
服务器端口
-
客户端端口
-
消息的交互
四、具体代码及步骤
-
界面设计
1.客户端
public void init(){ this.setTitle("客户端窗口"); this.add(sp,BorderLayout.CENTER); this.add(tf,BorderLayout.SOUTH); this.setBounds(300,300,300,400); //给输入框设置监听 tf.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String strSend = tf.getText(); //获取输入框中的文本 if(strSend.trim().length()==0){ //若输入框为空则不进行操作(检查) return; } //strSend发送服务器的方法 send(strSend); // 将文本内容送到发送服务器方法中 tf.setText(""); // 文本在输入完后消失在文本框 } }); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //关闭窗口及关闭程序 ta.setEditable(false); //不能在显示框中打字 tf.requestFocus(); //光标聚焦 try { s= new Socket(CONNSTR,CONNPORT); //表示连上服务器 isConn = true; } catch (UnknownHostException e1) { // TODO 自动生成的 catch 块 e1.printStackTrace(); } catch (IOException e1) { // TODO 自动生成的 catch 块 e1.printStackTrace(); } this.setVisible(true); //将构造好的数据模型显示出来 new Thread(new Receive()).start();//启动多线程
2.服务器端
public ServerChat(){ this.setTitle("服务器端"); this.add(sp,BorderLayout.CENTER); btnTool.add(startBtn); btnTool.add(stopBtn); this.add(btnTool,BorderLayout.SOUTH); this.setBounds(0,0,500,500); if(isStart){ //判断服务器是否启动 serverta.append("服务器已启动\n"); }else{ serverta.append("服务器还未启动,请点击按钮启动\n"); } //给窗口关闭键赋予监听 this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { isStart = false ; try { if (ss!=null){ ss.close(); } System.out.println("服务器停止!"); serverta.append("服务器断开"); System.exit(0); //退出程序 下面关闭按钮同理 } catch (IOException e1) { // TODO 自动生成的 catch 块 e1.printStackTrace(); } } }); //给关闭按纽加监听 stopBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { if (ss!=null){ ss.close(); //关闭服务器连接端口 isStart = false; } System.exit(0); serverta.append("服务器断开"); System.out.println("服务器停止!"); } catch (IOException e1) { // TODO 自动生成的 catch 块 e1.printStackTrace(); } } }); //给启动按钮设置一个监听 startBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("成功启动"); try { if(ss == null){ ss= new ServerSocket(PORT); //创建一个服务器端口号 } isStart = true; //启动条件变为真 serverta.append("服务器已经启动了!"+"\n"); } catch (IOException e1) { // TODO 自动生成的 catch 块 e1.printStackTrace(); } } }); this.setVisible(true); //将构造好的数据模型显示出来 startServer(); }
-
客户端与服务器端口的交互
1.在用户端建立与服务器端的通道
dos = new DataOutputStream(s.getOutputStream()); //建立一根发送管道
2.在服务器上为客户端创建端口号
try{ try{ ss = new ServerSocket(PORT); //创建端口号 isStart=true; }catch (IOException e2){ e2.printStackTrace(); } //可以接收多个客户端的连接 while(isStart){ //在这使用是否启动来作为循环判断条件 Socket s =ss.accept(); //在这等着接客户端的信息 一个客户端连一个服务器接口 ccList.add(new ClientConn(s)); //每来一个将信息加入到集合中 System.out.println("一个客户端连接服务器:"+s.getInetAddress()+"/"+s.getPort()); serverta.append("一个客户端连接服务器:"+s.getInetAddress()+"/"+s.getPort()+"\n"); }
3.客户端发送消息
public void send(String str){ //得到要发送的文本 try { dos = new DataOutputStream(s.getOutputStream()); //建立一根发送管道 dos.writeUTF(str); } catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } }
4.服务器端接收消息
public void run() { try { DataInputStream dis =new DataInputStream(s.getInputStream()); //为了能让服务器收到每个客户端多句话 while(isStart){ //在这使用是否启动来作为循环判断条件 String str =dis.readUTF(); //接收文本 System.out.println(s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n");//显示在控制台上 serverta.append(s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n"); //显示在服务器端 String strSend = s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n"; //遍历 ccList 调用send方法 在客户端接收信息是多线程的接收信息(多线程的发送消息) java.util.Iterator<ClientConn> it = ccList.iterator(); while(it.hasNext()){ ClientConn o = it.next(); o.send(strSend); } } } catch (SocketException e){ System.out.println("一个客户端下线了\n"); serverta.append(s.getLocalAddress()+"|"+s.getPort()+"客户端下线了\n"); //建立发送的管道 }catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } }
5.服务器发送消息
public void send(String str){ try { DataOutputStream dos = new DataOutputStream(this.s.getOutputStream()); dos.writeUTF(str); } catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } }
6.客户端接收消息
class Receive implements Runnable{ @Override public void run() { try { while(isConn){ DataInputStream dis = new DataInputStream(s.getInputStream()); String str = dis.readUTF(); ta.append(str); } } catch (SocketException e) { System.out.println("服务器意外终止!"); ta.append("服务器意外终止!\n"); }catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } } }
7.遍历客户端发来的消息(为了防止消息发送回自身)
java.util.Iterator<ClientConn> it = ccList.iterator(); while(it.hasNext()){ ClientConn o = it.next(); o.send(strSend); }
五、总结
- 经过查阅大料博客以及相关文献来完成多人聊天室项目,虽然我们的代码能力提高的并不是很明显,但是经过此役之后,我的java功底加深了,设计以及对项目的思考有了一定的广度和深度,还有就是在查阅相关博客的同时也就是在取之精华进行总结归纳
- 做完多人聊天项目之后,对我最大的帮助就是,让我了解到一个中间层次 Socket的概念
-
我们原一直以为,客户端发送消息之后服务端必须立刻马上进行处理,但其实还可以设置一些中间转发的过程,比如消息队列用于保存发送过程中的数据,这样做可提高系统的响应速率及系统稳定性,从而避免了一些不确定因素,比如(消息高峰时的解耦和),如果发送方和接收方步频不一致,中间转发可以达到弥补生产者消费者步频的不一致问题
- 在编写服务端代码的过程中,处处有坑,报出的错误要一一的进行处理以达到更好的优化
小组成员:
刘龙军、郭润方、惠文凯、邢润虎
链接:https://pan.baidu.com/s/1kP3PY71-Ev-e_ulc7MNomg
提取码:4ezx