手动搭建I/O网络通信框架2:BIO编程模型实现群聊
转载自https://www.cnblogs.com/lbhym/p/12681787.html
手动搭建I/O网络通信框架2:BIO编程模型实现群聊
在第一章中运用Socket和ServerSocket简单的实现了网络通信。这一章,利用BIO编程模型进行升级改造,实现群聊聊天室。
所谓BIO,就是Block IO,阻塞式的IO。这个阻塞主要发生在:ServerSocket接收请求时(accept()方法)、InputStream、OutputStream(输入输出流的读和写)都是阻塞的。这个可以在下面代码的调试中发现,比如在客户端接收服务器消息的输入流处打上断点,除非服务器发来消息,不然断点是一直停在这个地方的。也就是说这个线程在这时间是被阻塞的。

如图:当一个客户端请求进来时,接收器会为这个客户端分配一个工作线程,这个工作线程专职处理客户端的操作。在上一章中,服务器接收到客户端请求后就跑去专门服务这个客户端了,所以当其他请求进来时,是处理不到的。
看到这个图,很容易就会想到线程池,BIO是一个相对简单的模型,实现它的关键之处也在于线程池。
在上代码之前,先大概说清楚每个类的作用,以免弄混淆。更详细的说明,都写在注释当中。
服务器端:
ChatServer:这个类的作用就像图中的Acceptor。它有两个比较关键的全局变量,一个就是存储在线用户信息的Map,一个就是线程池。这个类会监听端口,接收客户端的请求,然后为客户端分配工作线程。还会提供一些常用的工具方法给每个工作线程调用,比如:发送消息、添加在线用户等。我之前简单用过Netty和WebSocket,这个类看上去就已经和这些框架有点相似了。学习IO编程模型也是为了接下来深入学习Netty做准备。
ChatHandler:这个类就是工作线程的类。在这个项目中,它的工作很简单:把接收到的消息转发给其他客户端,当然还有一些小功能,比如添加\移除在线用户。
客户端:
相较于服务器,客户端的改动较小,主要是把等待用户输入信息这个功能分到其他线程做,不然这个功能会一直阻塞主线程,导致无法接收其他客户端的消息。
ChatClient:客户端启动类,也就是主线程,会通过Socket和服务器连接。也提供了两个工具方法:发送消息和接收消息。
UserInputHandler:专门负责等待用户输入信息的线程,一旦有信息键入,就马上发送给服务器。
首先创建两个包区分一下客户端和服务器,client和server
服务器端ChatServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public class ChatServer { private int DEFAULT_PORT = 8888;
private Map<Integer, Writer> map=new ConcurrentHashMap<>();
private ExecutorService executorService= Executors.newFixedThreadPool(10); public void addClient(Socket socket) throws IOException { if (socket != null) { BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); map.put(socket.getPort(), writer); System.out.println("Client["+socket.getPort()+"]:Online"); } }
public void removeClient(Socket socket) throws Exception { if (socket != null) { if (map.containsKey(socket.getPort())) { map.get(socket.getPort()).close(); map.remove(socket.getPort()); } System.out.println("Client[" + socket.getPort() + "]Offline"); } }
public void sendMessage(Socket socket, String msg) throws IOException { for (Integer port : map.keySet()) { if (port != socket.getPort()) { Writer writer = map.get(port); writer.write(msg); writer.flush(); } } }
public void start() { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { System.out.println("Server Start,The Port is:"+DEFAULT_PORT); while (true){ Socket socket=serverSocket.accept(); executorService.execute(new ChatHandler(this,socket)); } } catch (IOException e) { e.printStackTrace(); } }
public static void main(String[] args) { ChatServer server=new ChatServer(); server.start(); } }
|
服务器端ChatHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class ChatHandler implements Runnable { private ChatServer server; private Socket socket;
public ChatHandler(ChatServer server, Socket socket) { this.server = server; this.socket = socket; }
@Override public void run() { try { server.addClient(socket); BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); String msg = null; while ((msg = reader.readLine()) != null) { String sendmsg = "Client[" + socket.getPort() + "]:" + msg; System.out.println(sendmsg); server.sendMessage(socket, sendmsg + "\n"); if (msg.equals("quit")) { break; } } } catch (IOException e) { e.printStackTrace(); } finally { try { server.removeClient(socket); } catch (Exception e) { e.printStackTrace(); } } } }
|
客户端ChatClient:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| public class ChatClient { private BufferedReader reader; private BufferedWriter writer; private Socket socket; public void sendToServer(String msg) throws IOException { if (!socket.isOutputShutdown()) { writer.write(msg + "\n"); writer.flush(); } } public String receive() throws IOException { String msg = null; if (!socket.isInputShutdown()) { msg = reader.readLine(); } return msg; }
public void start() { try { socket = new Socket("127.0.0.1", 8888); reader=new BufferedReader( new InputStreamReader(socket.getInputStream()) ); writer=new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); new Thread(new UserInputHandler(this)).start();
String msg=null; while ((msg=receive())!=null){ System.out.println(msg); } } catch (IOException e) { e.printStackTrace(); }finally { try { if(writer!=null){ writer.close(); } } catch (IOException e) { e.printStackTrace(); } } }
public static void main(String[] args) { new ChatClient().start(); } }
|
客户端UserInputHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class UserInputHandler implements Runnable { private ChatClient client;
public UserInputHandler(ChatClient client) { this.client = client; }
@Override public void run() { try { BufferedReader reader = new BufferedReader( new InputStreamReader(System.in) ); while (true) { String input = reader.readLine(); client.sendToServer(input); if (input.equals("quit")) break; } } catch (IOException e) { e.printStackTrace(); } } }
|
运行测试:
通过打开终端,通过javac编译。如果大家是在IDEA上编码的话可能会报编码错误,在javac后面加上-encoding utf-8再接java文件就好了。
编译后运行,通过java运行时,又遇到了一个坑。会报找不到主类的错误,原来是因为加上两个包,要在class文件名前面加上包名。比如当前在src目录,下面有client和server两个包,要这么运行:java client.XXXX。可我之前明明在client文件夹下运行的java,也是不行,不知道为什么。
接着测试:
1.首先在一个终端里运行ChatServer,打开服务器

2.在第二个终端里打开ChatClient,暂且叫A,此时服务器的终端显示:

3.类似的,在第三个终端里打开ChatClient,暂且叫B,此时服务器显示:

4.A中输入hi,除了服务器会打印hi外,B中也会显示,图片中的端口号和前面的不一样,是因为中间出了点小问题,前三张截图和后面的不是同时运行的。实际中同一个客户端会显示一样的端口号:

5.当客户端输入quit时就会断开连接,最后,服务器的显示为:
