简单的聊天室制作
简单的聊天室制作
一个简单的聊天室,主要是就两个部分,一部分就是我们进行的时候那个聊天窗口,另外一个就是背后的服务器,我们要写一个聊天窗口,也要写一个相对应的服务器。
做一个项目过程中,写一个代码很简单,但是把逻辑分析清楚,将制作的过程中所有的逻辑关系分析清楚是项目的最重要的环节。
下面的一步一步,将这个聊天室的制作过程一步一步制作出来。
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
第八步:
第九步:
第十步:
第十一步:
这就是简单的聊天室的制作的过程。这样我们来看一下实现后的代码:
客户端的的代码:
1 import java.awt.BorderLayout; 2 import java.awt.Frame; 3 import java.awt.TextArea; 4 import java.awt.TextField; 5 import java.awt.event.ActionEvent; 6 import java.awt.event.ActionListener; 7 import java.awt.event.WindowAdapter; 8 import java.awt.event.WindowEvent; 9 import java.io.DataInputStream; 10 import java.io.DataOutputStream; 11 import java.io.EOFException; 12 import java.io.IOException; 13 import java.net.Socket; 14 import java.net.SocketException; 24 //该类继承了Frame,所以本质就是一个Fame,具有图新界面的一切特征 25 public class ChatClient extends Frame { 26 //声明一个文本框,用用于客户的消息输入 27 TextField tfTxt = new TextField(); 28 //显示客户的输入,包括别的客户端通过服务器转发过来的消息 29 TextArea taContent = new TextArea(); 30 //声明一个空的socket链接,用于和服务器进行信息交互,下面合适的时候,会进行赋值(变量声明的三个步骤) 31 Socket soc = null; 32 //声明一个boolean,判断和服务器端是否链接上了,特指socket 33 private boolean bConnected = false; 34 //接受从服务器端发送过来的信息流 35 DataInputStream dis = null; 36 //向服务器端发送客户输入消息 37 DataOutputStream dos = null; 38 //声明一个线程变量,专门用于接受服务器端发过来的消息.死循环,阻塞接受 39 Thread tRecv = new Thread(new RecvThread()); 40 41 //main程序的入口,用于启动客户端,怎么启动的?静态和成员的区别 42 public static void main(String[] args) { 43 new ChatClient().launFrame(); 44 } 45 46 //客户端的启动方法,启动了客户端界面,链接服务器,启动接受线程 47 public void launFrame() { 48 //设置界面的位置宽高 49 setBounds(1000, 300, 300, 300); 50 //设置界面的布局. 51 this.add(tfTxt, BorderLayout.SOUTH); 52 this.add(taContent, BorderLayout.NORTH); 53 //把Frame里面的控件打包然后按照默认设计好的方式显示 54 this.pack(); 55 // 窗口关闭按钮,事件监听,用的是匿名内部类,关闭窗口的同时关闭了和服务器端链接,然后退出系统,线程不用管.都关机了 56 this.addWindowListener(new WindowAdapter() { 57 public void windowClosing(WindowEvent e) { 58 disConnect(); 59 System.exit(0); 60 } 61 }); 62 63 // 添加文本框监听事件,是一个成员内部类,目的是为了获得客户输入的消息,默认触发回车触发 64 tfTxt.addActionListener(new TxtListener()); 65 //让整个界面可见 66 setVisible(true); 67 68 // 链接服务器方法调用了,怎么就叫链接了.soc,dis,dos都链接上了才叫链接 69 connect(); 70 //全部链接上以后,才启动消息接收线程 71 tRecv.start(); 72 } 73 74 // 添加客户端链接服务器的方法 75 public void connect() { 76 try { 77 //给声明好的Socket指定链接的IP和端口,是个成员变量?? 78 soc = new Socket("127.0.0.1", 9999); 79 //dos通过Socket向服务器端发送消息,用的什么方法? 80 dos = new DataOutputStream(soc.getOutputStream()); 81 //dis通过Socket接收服务器端发过来的消息,什么方法? 82 dis = new DataInputStream(soc.getInputStream()); 83 //System.out.println(dis); 84 //上面都都准备好,才叫连接上,所以,把链接成员变量设置为真 85 bConnected = true; 86 } catch (IOException e) { 87 e.printStackTrace(); 88 } 89 } 90 91 //断开服务器连接,把打开的那三个链接全部关闭掉 92 public void disConnect() { 93 try { 94 dos.close(); 95 dis.close(); 96 soc.close(); 97 } catch (IOException e) { 98 e.printStackTrace(); 99 } 100 101 /*try{ 102 bConnected = false; 103 tRecv.join(); //合并线程 104 }catch(InterruptedException e){ 105 e.printStackTrace(); 106 }finally{ 107 try{ 108 dos.close(); 109 dis.close(); 110 soc.close(); 111 }catch(IOException e){ 112 e.printStackTrace(); 113 } 114 }*/ 115 } 116 117 118 // 创建一个内部类,实现控件本身的事件监听,不同的控件有自己默认的事件,有三种/四种来设置监听:匿名内部类,成员内部类,外部类 119 private class TxtListener implements ActionListener { 120 @Override 121 public void actionPerformed(ActionEvent e) {//获得触发事件的那个最初控件对象 122 //经过e获得了事件源对象控件,但是我们没用他,因为我们把事件源对象设置成了成员变量,我们内部类可以直接访问成员变量 123 String s = tfTxt.getText().trim(); 124 // System.out.println(s); 125 // 把客户往输入文本框的消息,取到然后输出文本显示框,这里也是为了避免和服务器端发送回来的消息叠加,所以注释掉 126 //taContent.setText(s); 127 //同时把输入框中客户输入的值清空.要不然会叠加 128 tfTxt.setText(""); 129 130 // 把数据发送到服务器 131 try { 132 //DataOutputStream,会把数据的数据类型也保存进去,发送出去,什么样的顺序写,就什么样的顺序. 133 //也需要按照对应的数据类型来读取 134 dos.writeUTF(s); 135 //只要是输出流,都需要flush,要不然会把数据缓存下来,而输出不出去 136 dos.flush(); 137 } catch (IOException e1) { 138 e1.printStackTrace(); 139 } 140 } 141 } 142 143 //阻塞是读取服务器端发送过来的消息 144 private class RecvThread implements Runnable { 145 //当线程类启动该线程的时候,调用start(),然后jvm默认会调用run()方法开启新线程 146 public void run() { 147 try { 148 //如果链接全准备好了,我们就不间断的等待读取服务器端发送过来的消息. 149 while (bConnected) { 150 //通过dis的readUTF()读取 151 String str = dis.readUTF(); 152 //然后把获取到的消息,和输出到文本显示框中原本的消息一起,加上换行符 153 taContent.setText(taContent.getText() + str + '\n'); 154 } 155 } catch (SocketException e) { 156 System.out.println("客户端已经关闭"); 157 }catch (EOFException e) { 158 System.out.println("读取完毕"); 159 }catch (IOException e) { 160 System.out.println("IO异常"); 161 } 162 } 163 164 } 165 }
服务器端代码:
1 import java.io.DataInputStream; 2 import java.io.DataOutputStream; 3 import java.io.EOFException; 4 import java.io.IOException; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 import java.util.ArrayList; 8 import java.util.Iterator; 9 import java.util.List; 10 11 import javax.xml.stream.events.StartDocument; 12 13 /** 14 * 思路: 1 开启服务器,接受客户端链接,接受客户端数据 start():方法,负责服务器的开启 client():只负责管道的链接 15 * run():负责数据的读取 16 * 17 * @author Admin 18 * 19 */ 20 21 public class ChatServer { 22 // 判断服务器是否启动,启动以后就不间断的接受客户端链接,就是while(true)接受链接,接受链接,不是消息 23 boolean started = false; 24 //声明一个ServerSocket变量,用于转载启动服务器对象 25 ServerSocket ss = null; 26 //声明一个Client集合,用于装在所有连接到服务器的客户端 27 List<Client> clients = new ArrayList<>(); 28 29 //main()方法,调用方法启动服务器,start(),自定义的 30 public static void main(String[] args) { 31 new ChatServer().start(); 32 } 33 34 // 负责启动服务器方法声明 35 public void start() { 36 try { 37 //实例化一个服务器对象,然后赋值给上面的服务器变量 38 ss = new ServerSocket(9999); 39 //启动以后赋值给判断变量 40 started = true; 41 //如果 启动了,就不间断声明socket接收客户端的链接 42 while (started) { 43 // 声明socket 44 Socket soc = ss.accept(); 45 //每个Socket对应一个Client对象,然后把Socket传递到对象的内部,Socket和客户端就一一对应了 46 Client c = new Client(soc); 47 //然后根据客户端开启这个客户端对应的消息收发线程.这样线程,客户端,Socket就一一对应了. 48 new Thread(c).start(); 49 //然后把客户端的引用保存到集合中,这样,客户单就有了顺序,相当于有了一个唯一的识别符号 50 clients.add(c); 51 // 上一行如果没有链接上会报错,到达这里就证明链接上了 52 53 } 54 } catch (IOException e) { 55 // e.printStackTrace(); 56 System.out.println("客户端关闭,服务器端也会关闭"); 57 } finally { 58 try { 59 if (ss != null) 60 ss.close(); 61 } catch (Exception e2) { 62 e2.printStackTrace(); 63 } 64 } 65 } 66 67 class Client implements Runnable { 68 //每个客户端对象,都肯定具备以下属性 69 //对应的Socket 70 private Socket soc; 71 //对应的输入属性 72 private DataInputStream dis = null; 73 //对应的输出 74 private DataOutputStream dos = null; 75 //对应的链接属性,是否链接好了 76 private boolean bConnected = false; 77 78 // 构造方法,只是成功的创建这个类,构造方法用于创建这个类 79 //同时需要输入对应的Socket,还同时实例化输入和输出的对应的流及其方法,和Socket绑定 80 public Client(Socket s) { 81 this.soc = s; 82 try { 83 dis = new DataInputStream(s.getInputStream());//s是绑定 84 dos = new DataOutputStream(s.getOutputStream()); 85 } catch (IOException e) { 86 e.printStackTrace(); 87 } 88 bConnected = true; 89 } 90 91 @Override 92 public void run() { 93 try { 94 //如果链接成功的话,这个线程就不间断的读取客户端的消息 95 while (bConnected) { 96 //读取客户端发过来的消息 97 String str = dis.readUTF(); 98 System.out.println(str); 99 //同时遍历集合中的每一个对象,然后挨个发送收到消息 100 for (int i = 0; i < clients.size(); ++i) { 101 //i是Client,Socket,对应的下标 102 Client c = clients.get(i); 103 //发送方法 104 c.send(str); 105 } 106 } 107 } catch (EOFException e) { 108 //如果客户端断开,就需要移除这个客户端.输入移除和输出移除都一样.因为都是不间断的监听 109 clients.remove(this); 110 System.out.println("客户端已经断开连接,所以从集合中移除该客户端"); 111 } catch (IOException e) { 112 e.printStackTrace(); 113 } finally { 114 try { 115 if (dis != null) { 116 dis.close(); 117 } 118 if (dos != null) { 119 dos.close(); 120 } 121 if (soc != null) { 122 soc.close(); 123 } 124 } catch (IOException e2) { 125 e2.printStackTrace(); 126 } 127 } 128 } 129 public void send(String str) { 130 try { 131 //真正向客户端发送消息的方法 132 dos.writeUTF(str); 133 } catch (IOException e) { 134 e.printStackTrace(); 135 } 136 } 137 } 138 139 }
代码完成了,让我们来看看我们的聊天室做好以后的效果:
在我敲击回车后,观看一下结果:
我们这个小的聊天室就完成了!