package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; /** * 聊天室服务端 */ public class Server { /** * java.net.ServerSocket * ServerSocket是运行在服务端的,它的主要工作: * 1:打开服务端口(客户端就是通过这个端口与服务端建立链接) * 2:监听该服务端口,一旦一个客户端链接,则会返回一个Socket实例,并通过这个 * Socket实例与链接的客户端进行交互 * <p> * 如果我们将Socket比喻为"电话",那么ServerSocket相当于是"总机"。 */ private ServerSocket server; //该数组用于存放所有客户端的输出流,用于广播消息给所有客户端 // private PrintWriter[] allOut = {}; private Collection<PrintWriter> allOut = new ArrayList<>(); public Server() { try { /* 实例化ServerSocket的同时需要指定打开的服务端口,客户端就是通过该 端口建立链接的。 如果该端口已经被操作系统其他程序占用了,那么这里会抛出异常: java.net.BindException:address already in use 绑定异常 地址 已经 被占用了 */ System.out.println("正在启动服务端..."); server = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } public void start() { try { /* ServerSocket提供的方法: Socket accept() 该方法是一个阻塞方法,调用后开始等待,直到一个客户端与服务端建立链接 为止,此时该方法会立即返回一个Socket,通过这个Socket与该客户端交互 */ while (true) { System.out.println("等待客户端链接..."); Socket socket = server.accept(); System.out.println("一个客户端链接了!"); //启动一个线程来处理与该客户端的交互 Runnable handler = new ClientHandler(socket); Thread t = new Thread(handler); t.start(); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } /** * 该线程任务负责与特定客户端交互 */ private class ClientHandler implements Runnable{ private Socket socket; private String host;//客户端的地址信息 public ClientHandler(Socket socket){ this.socket = socket; //通过socket获取远端计算机的地址信息 host = socket.getInetAddress().getHostAddress(); } public void run(){ PrintWriter pw = null; try { /* 通过Socket的方法: InputStream getInputStream() 获取一个字节输入流,可以读取来自远端计算机发送过来的字节数据 */ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr); //通过socket获取输出流,用于给客户端发送消息 OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8); BufferedWriter bw = new BufferedWriter(osw); pw = new PrintWriter(bw,true); //将该客户端的输出流存入共享数组allOut中 //不行,因为this是ClientHandler。不同线程是不同的ClientHandler任务 // synchronized (this) { /* 通常情况下,同步监视器对象选取的就是多个线程并发操作的临界资源。这里临界资源 就是数组allOut。但是这里之所以不可以是因为同步块中有对该数组扩容的操作,这 会导致allOut对象发生了变化,其他需要同步执行该代码块看到的就不再是之前的allOut 导致同步失效 */ // synchronized (allOut) { synchronized (Server.this) { //1对allOut扩容 // allOut = Arrays.copyOf(allOut, allOut.length + 1); //2将pw存入共享数组最后一个位置 // allOut[allOut.length - 1] = pw; allOut.add(pw); } //广播通知所有客户端该用户上线了 // sendMessage(host + "上线了,当前在线人数:" + allOut.length); sendMessage(host + "上线了,当前在线人数:" + allOut.size()); String line; /* 服务端在读取客户端消息这里,如果客户端没有调用socket.close()与服务端 正常断开连接(例如客户端直接被杀掉了进程等操作),那么服务端这里会抛出 一个异常:SocketException: Connection reset 这是由于客户端非正常操作导致的,服务端无法通过逻辑避免该异常的产生。 */ // long time = System.currentTimeMillis(); while ((line = br.readLine()) != null) { // long cur = System.currentTimeMillis();//客户端发过来这句话的时间 // if(cur-time<=500){ // sendMessage("你发的消息太快了!"); // continue; // } System.out.println(host+"说:" + line); //将消息回复给所有客户端 sendMessage(host + "说:" + line); // time = cur; } }catch(IOException e){ e.printStackTrace(); }finally{ //将当前客户端的输出流(pw)从allOut数组中删除 synchronized (Server.this) { // for (int i = 0; i < allOut.length; i++) { // if (allOut[i] == pw) { // allOut[i] = allOut[allOut.length - 1]; // allOut = Arrays.copyOf(allOut, allOut.length - 1); // break; // } // } allOut.remove(pw); } // sendMessage(host + "下线了,当前在线人数:" + allOut.length); sendMessage(host + "下线了,当前在线人数:" + allOut.size()); try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 广播消息给所有客户端 * send:发送 * message:消息 */ private void sendMessage(String message){ synchronized (Server.this) { // for (int i = 0; i < allOut.length; i++) { // allOut[i].println(message); // } for (PrintWriter pw : allOut) { pw.println(message); } } } } }
BIO(聊天室服务端代码)
最新推荐文章于 2023-09-04 16:46:36 发布