Java Socket多人聊天室
becatjd 人气:0一、 聊天室需求
1、一个服务端,多个客户端;
2、实现客户端和服务端的交互;
3、客户端发送信息,服务端收到信息,再转发给其他客户端;
4、上下线时显示哪个客户端上下线并且显示在线客户端数量;
二、代码分析
1. 建立连接
客户端类,创建发送端Socket对象,用自己的IP地址和端口号,与服务端建立连接。
class Client:
//用于与服务端通信的Socket private Socket socket; public Client() throws Exception { /* * 初始化Socket的同时要制定服务端的IP地址和端口号 * ip地址用于我们在网络上找到服务端的所在计算在 * 端口号用于找到服务器上的服务端应用程序。 * * 实例化Socket的过程就是连接服务端的过程若 * 服务端无响应,这里得构造方法得抛出异常。 * */ try { System.out.println("正在连接服务器......"); //localhost 127.0.0.1 socket = new Socket("LAPTOP-TCK59O6Q",8888); System.out.println("与服务端连接完毕"); } catch (Exception e) { System.out.println("初始化失败"); throw e; } }
服务端类,使用构造方法初始化服务端,创建接收端的Socket对象
class Server:
private ServerSocket server; //构造方法初始化服务端 public Server() throws IOException { //实例化serverSocket的同时,指定服务端的端口号; try { server = new ServerSocket(8888); allOut = new ArrayList<PrintWriter>(); } catch (Exception e) { System.out.println("服务端初始化失败"); throw e; } }
2. 客户端发送信息
在客户端的类中写一个start()方法,start()是客户端发送信息给服务端的方法
获取输出流对象,把键盘录入的信息发送到服务端。
class Client:
public void start() throws Exception { /* * 客户端开始工作的方法 */ try { //启动用于读取服务端发送消息的线程 ServerHandler handler = new ServerHandler(); //ServerHandler是自己写的类,实现Runnable接口,有多线程功能 Thread t = new Thread(handler); t.start(); //将数据发送到服务端 OutputStream out = socket.getOutputStream();//获取输出流对象 OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");//转化成utf-8格式 PrintWriter pw = new PrintWriter(osw,true); Scanner scan = new Scanner(System.in); while(true) { String message = scan.nextLine();//得到键盘录入的信息 pw.println(message);//把信息输出到服务端 } } catch (Exception e) { System.out.println("客户端运行失败"); throw e; } }
服务端工作的start()方法,accept()方法与客户端连接上
class Server:
//服务端工作的方法 public void start() throws IOException { /* * ServerSocket提供了一个accept的方法,该方法是一个阻塞方法, * 用于监听其打开的8888端口;当一个客户端通过该端口与服务端连接时, * accept方法就会解除阻塞,然后创建一个socket实例并返回, * socket的作用就是与刚刚连接上的客户端进行通信。 */ while(true) { System.out.println("等待客户端连接..."); Socket socket = server.accept(); System.out.println("一个客户端连接了!"); //启动一个线程来处理客户端的交互工作 ClientHandler hander = new ClientHandler(socket); Thread t = new Thread(hander); t.start(); } }
3. 开启多线程、服务端接收读取信息并广播
因为服务端与多个客户端相连,所以要用多线程,即一个客户端用一条线程。
在服务端类中创建一个内部类ClientHandler实现Runnable接口并重写run()方法创建线程
属性有客户端的Socket对象
有参构造方法中通过客户端的Socket获取到其地址host,并且把地址打印出来
这样在main()方法中,实例化服务端类的对象之后,start方法开启服务端,当有客户端连接上时,就能输出这个客户端的ip地址。
ClientHandler类要重写run()方法,使用输入流InputStream读取客户端发来的信息,再使用输出流OutputStream给所有客户端广播收到的信息、用户上下线和在线人数
class Server:
/** * ClientHandler * 该线程类是与指定的客户端进行交互工作; * @author zxm * */ class ClientHandler implements Runnable{ //当前线程客户端的Socket private Socket socket; //该客户端的地址 private String host; public ClientHandler(Socket socket) { this.socket=socket; /* * 通过socket获取远程计算机地址 * 对于服务端而言,远程计算机就是客户端 */ InetAddress address = socket.getInetAddress(); //获取ip地址 host = address.getHostAddress(); System.out.println("host"+host); } @Override public void run() { PrintWriter pw = null; try { //广播给所有客户端,当前用户上线了 sendMessage("["+host+"]上线了"); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); pw = new PrintWriter(osw,true); //将该客户的输出流存入共享集合,以便消息可以广播给该客户端 addOut(pw); //广播当前在线人数 sendMessage("当前在线人数["+allOut.size()+"]"); //处理来自客户端的数据 InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"utf-8"); BufferedReader br = new BufferedReader(isr); /* * 服务端读取客户端发送过来的每一句字符时 * 由于客户端所在的操作系统不同,这里客户端断开时结果也不同 * windows客户端开始br.readLine抛出异常 * Linux客户端断开是返回null * */ String message = null; while((message = br.readLine())!=null) { sendMessage(host+"说:"+message); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { //将该客户端的输出流从共享集合中删除 removeOut(pw); //广播给所有客户端,当前用户下线 sendMessage("["+host+"]下线了"); //广播当前在线人数 sendMessage("当前在线人数["+allOut.size()+"]"); try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
因此服务端类中要写一个sendMessage()方法,为了在接收到一个客户端的信息后,把这个信息转发给所有客户端,这就是广播的效果。
写一个集合allOut,用来存放所有客户端的输出流,客户端的数量就是集合里元素的个数
再写两个方法,一个是addOut()方法把上线客户端的输出流放进集合(在run方法中使用addOut(),获取到启用新线程的客户端的输出流,把输出流加到集合中),
另一个是removeOut()方法拿出集合(同理run()方法中使用,把socket关闭的客户端的输出流移除集合)。
所以sendMessage()方法的参数就是某个客户端发的字符串信息message,遍历allOut集合,把message在每个输出流中打印,用PrintWrite类中的print方法。
当客户端连接服务端时,sendMessage()方法打印这个服务端的地址加上上线了,同理客户端关闭socket的时候打印下线了,
同时上下线后再打印allOut集合的大小,也就是当前连接服务端的客户端数量,就是在线人数。
class Server:
//存放所有客户端的输出流的集合,用于广播 private List<PrintWriter> allOut; //将给定的输出流放入共享集合 private synchronized void addOut(PrintWriter out){ allOut.add(out); } //将给定的输出流移除共享集合 private synchronized void removeOut(PrintWriter out){ allOut.remove(out); } //将给定的消息发给多个客户端 private synchronized void sendMessage(String message) { for(PrintWriter out:allOut) { out.println(message); } }
4. 客户端读取信息
这个时候所有的客户端都收到了某个客户发的消息,但是还没读,所以客户端类中要加输入流才能读取,
创建ServerHandler类实现Runnable接口,输入流读取并输出。
class Client:
class ServerHandler implements Runnable{ /** * 该线程用于读取服务端发送过来的消息,并输出到 * 客户端的控制台上 * @author zxm * */ @Override public void run() { try { InputStream in = socket.getInputStream();//输入流 InputStreamReader isr = new InputStreamReader(in,"UTF-8");//以utf-8读 BufferedReader br = new BufferedReader(isr); String message = null; while((message=br.readLine())!=null) { System.out.println(message); } } catch (Exception e) { e.printStackTrace(); } } }
三、完整代码
1. 客户端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; /** * 聊天室服务端 * @author zxm */ public class Client { //用于与服务端通信的Socket private Socket socket; public Client() throws Exception { /* * 初始化Socket的同时要制定服务端的IP地址和端口号 * ip地址用于我们在网络上找到服务端的所在计算在 * 端口号用于找到服务器上的服务端应用程序。 * * *实例化Socket的过程就是连接服务端的过程若 * 服务端无响应,这里得构造方法得抛出异常。 * */ try { System.out.println("正在连接服务器......"); //localhost 127.0.0.1 socket = new Socket("LAPTOP-TCK59O6Q",8888); System.out.println("与服务端连接完毕"); } catch (Exception e) { System.out.println("初始化失败"); throw e; } } public void start() throws Exception { /* * 客户端开始工作的方法 */ try { //启动用于读取服务端发送消息的线程 ServerHandler handler = new ServerHandler(); //ServerHandler是自己写的类,实现Runnable接口,有多线程功能 Thread t = new Thread(handler); t.start(); //将数据发送到服务端 OutputStream out = socket.getOutputStream();//获取输出流对象 OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");//转化成utf-8格式 PrintWriter pw = new PrintWriter(osw,true); Scanner scan = new Scanner(System.in); while(true) { String message = scan.nextLine();//得到键盘录入的信息 pw.println(message);//把信息输出到服务端 } } catch (Exception e) { System.out.println("客户端运行失败"); throw e; } } public static void main(String[] args) throws Exception { try { Client client = new Client(); client.start(); } catch (Exception e) { System.out.println("客户端运行失败"); e.printStackTrace(); } } class ServerHandler implements Runnable{ /** * 该线程用于读取服务端发送过来的消息,并输出到 * 客户端的控制台上 * @author zxm * */ @Override public void run() { try { InputStream in = socket.getInputStream();//输入流 InputStreamReader isr = new InputStreamReader(in,"UTF-8");//以utf-8读 BufferedReader br = new BufferedReader(isr); String message = null; while((message=br.readLine())!=null) { System.out.println(message); } } catch (Exception e) { e.printStackTrace(); } } } }
2. 服务端
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * 聊天室服务端 * @author zxm */ public class Server { /* * 运行在服务端的socket * 该类的作用是: * 1.申请服务端口,客户端就是通过它申请的服务端口连接上服务端应用的。 * 2.监听申请的服务端口感知客户端的连接,并创建一个socket与该客户通信。 */ private ServerSocket server; //存放所有客户端的输出流的集合,用于广播 private List<PrintWriter> allOut; //将给定的输出流放入共享集合 private synchronized void addOut(PrintWriter out){ allOut.add(out); } //将给定的输出流移除共享集合 private synchronized void removeOut(PrintWriter out){ allOut.remove(out); } //将给定的消息发给多个客户端 private synchronized void sendMessage(String message) { for(PrintWriter out:allOut) { out.println(message); } } //构造方法初始化服务端 public Server() throws IOException { //实例化serverSocket的同时,指定服务端的端口号; try { server = new ServerSocket(8888); allOut = new ArrayList<PrintWriter>(); } catch (Exception e) { System.out.println("服务端初始化失败"); throw e; } } //服务端工作的方法 public void start() throws IOException { /* * ServerSocket提供了一个accept的方法,该方法是一个阻塞方法, * 用于监听其打开的8888端口;当一个客户端通过该端口与服务端连接时, * accept方法就会解除阻塞,然后创建一个socket实例并返回, * socket的作用就是与刚刚连接上的客户端进行通信。 */ while(true) { System.out.println("等待客户端连接..."); Socket socket = server.accept(); System.out.println("一个客户端连接了!"); //启动一个线程来处理客户端的交互工作 ClientHandler hander = new ClientHandler(socket); Thread t = new Thread(hander); t.start(); } } public static void main(String[] args) throws Exception { Server server = new Server(); server.start(); } /** * 该线程类是与指定的客户端进行交互工作; * @author zxm * */ class ClientHandler implements Runnable{ //当前线程客户端的Socket private Socket socket; //该客户端的地址 private String host; public ClientHandler(Socket socket) { this.socket=socket; /* * 通过socket获取远程计算机地址 * 对于服务端而言,远程计算机就是客户端 */ InetAddress address = socket.getInetAddress(); //获取ip地址 host = address.getHostAddress(); System.out.println("host"+host); } @Override public void run() { PrintWriter pw = null; try { //广播给所有客户端,当前用户上线了 sendMessage("["+host+"]上线了"); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); pw = new PrintWriter(osw,true); //将该客户的输出流存入共享集合,以便消息可以广播给该客户端 addOut(pw); //广播当前在线人数 sendMessage("当前在线人数["+allOut.size()+"]"); //处理来自客户端的数据 InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"utf-8"); BufferedReader br = new BufferedReader(isr); /* * 服务端读取客户端发送过来的每一句字符时 * 由于客户端所在的操作系统不同,这里客户端断开时结果也不同 * windows客户端开始br.readLine抛出异常 * Linux客户端断开是返回null * */ String message = null; while((message = br.readLine())!=null) { sendMessage(host+"说:"+message); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { //将该客户端的输出流从共享集合中删除 removeOut(pw); //广播给所有客户端,当前用户下线 sendMessage("["+host+"]下线了"); //广播当前在线人数 sendMessage("当前在线人数["+allOut.size()+"]"); try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
加载全部内容