说是简单聊天系统,压根不能算是一个系统,顶多算个雏形。本文重点不在聊天系统设计和实现上,而是通过实现类似效果,展示下NIO 和Socket两种编程方式的差异性。说是Socket与NIO的编程方式,不太严谨,因为NIO的底层也是通过Socket实现的,但又想不出非常好的题目,就这样吧。
主要内容
Socket方式实现简易聊天效果
NIO方式实现简易聊天效果
两种方式的性能对比
前言
预期效果,是客户端之间进行“广播”式聊天,类似于QQ群聊天。希望以后有机会,以此简易版为基础,不断演进,演练下在线聊天系统。
1.Socket方式实现简易聊天效果
1.1服务端 Server.java
package com.example.socket.server; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; public class Server { private static int port =9999; // 可接受请求队列的最大长度 private static int backlog=100; // 绑定到本机的IP地址 private static final String bindAddr = "127.0.0.1"; //socket字典列表 private static List<Socket> nodes= new ArrayList<Socket>(); public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(port, backlog,InetAddress.getByName(bindAddr)); for(;;){ //发生阻塞,等待客户端连接 Socket sc = ss.accept(); nodes.add(sc); InetAddress addr = sc.getLocalAddress(); System.out.println("create new session from "+addr.getHostName()+":"+sc.getPort()+"\n"); //针对一个Socket 客户端 启动两个线程,分别是接收信息,发送信息 new Thread(new ServerMessageReceiver(sc,nodes)).start(); new ServerMessageSender(sc).start(); } } catch (IOException e) { e.printStackTrace(); } } }
1.2 消息接收端 ServerMessageReceiver.java
额外负责信息广播
package com.example.socket.server; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * * 接收消息 * */ public class ServerMessageReceiver implements Runnable{ private Socket socket; //socket字典列表 private List<Socket> nodes= new ArrayList<Socket>(); public ServerMessageReceiver(Socket sc,List<Socket> nodes){ this.socket=sc; this.nodes=nodes; } /** * 信息广播到其他节点 */ @Override public void run() { try { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); //接收到的消息 String content; while (true) { if(socket.isClosed()){ System.out.println("Socket已关闭,无法获取消息"); reader.close(); socket.close(); break; } content=reader.readLine(); if(content!=null && content.equals("bye")){ System.out.println("对方请求关闭连接,无法继续进行聊天"); reader.close(); socket.close(); break; } String message =socket.getPort()+":"+content; //广播信息 for(Socket n:this.nodes){ if(n !=this.socket){ BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(n.getOutputStream(),"UTF-8")); writer.write(message); writer.newLine(); writer.flush(); } } } } catch (IOException e) { e.printStackTrace(); } } }
1.3消息发送服务端 ServerMessageSender.java
主要作用:发送欢迎信息
package com.example.socket.server; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.Socket; public class ServerMessageSender extends Thread{ private Socket socket; public ServerMessageSender(Socket socket) { this.socket = socket; } /** * 只发送一个欢迎信息 */ @Override public void run() { try { BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")); // BufferedReader inputReader=new BufferedReader(new InputStreamReader(System.in)); try { String msg="server :welcome "+socket.getPort(); writer.write(msg); writer.newLine(); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } }
1.4 客户端 Client.java
package com.example.socket.client; import java.net.InetAddress; import java.net.Socket; public class Client { // 监听端口号 private static final int port = 9999; // 绑定到本机的IP地址 private static final String bindAddr = "127.0.0.1"; public static void main(String[] args) { try { System.out.println("正在连接Socket服务器"); Socket socket=new Socket(InetAddress.getByName(bindAddr),port); System.out.println("已连接\n=================================="); new ClientMessageSender(socket).start(); new ClientMessageReceiver(socket).start(); } catch (Exception e) { e.printStackTrace(); } } }
1.4 消息接收客户端 ClientMessageReceiver.java
仅仅是输出
package com.example.socket.client; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.Socket; public class ClientMessageReceiver extends Thread { private Socket socket; public ClientMessageReceiver(Socket socket) { this.socket=socket; } @Override public void run() { try { // 获取socket的输 出\入流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); //接收到的消息 String content; while (true) { if(socket.isClosed()){ System.out.println("Socket已关闭,无法获取消息"); reader.close(); socket.close(); break; } content=reader.readLine(); if(content.equals("bye")){ System.out.println("对方请求关闭连接,无法继续进行聊天"); reader.close(); socket.close(); break; } System.out.println(content+"\n"); } reader.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
1.5 消息发送客户端 ClientMessageSender.java
通过输入流输入,将信息传入Socket
package com.example.socket.client; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; public class ClientMessageSender extends Thread { private Socket socket; public ClientMessageSender(Socket socket) { this.socket = socket; } @Override public void run() { try { BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")); BufferedReader inputReader=new BufferedReader(new InputStreamReader(System.in)); try { String msg; for(;;){ msg=inputReader.readLine(); if(msg.toLowerCase().equals("exit")){ System.exit(0); } if(socket.isClosed()){ System.out.println("Socket已关闭,无法发送消息"); writer.close(); socket.close(); break; } writer.write(msg); writer.newLine(); writer.flush(); System.out.println(); } } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } }
1.6 效果
2.NIO方式实现简易聊天效果
2.1服务端 NServer.java
package com.example.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; /** * 服务器端 */ public class NServer { private Selector selector; private Charset charset = Charset.forName("UTF-8"); public void init() throws Exception { selector = Selector.open(); ServerSocketChannel server = ServerSocketChannel.open(); InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 3000); server.socket().bind(isa); server.configureBlocking(false); server.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0) { for (SelectionKey key : selector.selectedKeys()) { selector.selectedKeys().remove(key); if (key.isAcceptable()) { SocketChannel sc = server.accept(); System.out.println("create new session from "+sc.getRemoteAddress()+"\n"); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); key.interestOps(SelectionKey.OP_ACCEPT); sc.write(charset.encode("welcome"+sc.getRemoteAddress())); } if (key.isReadable()) { SocketChannel sc = (SocketChannel)key.channel(); ByteBuffer buff = ByteBuffer.allocate(1024); String content = ""; try { while (sc.read(buff) > 0) { buff.flip(); content += charset.decode(buff); buff.clear(); } key.interestOps(SelectionKey.OP_READ); } catch (IOException e) { key.cancel(); if (key.channel() != null) key.channel().close(); } if (content.length() > 0) { for (SelectionKey sk : selector.keys()) { Channel targetchannel = sk.channel(); if (targetchannel instanceof SocketChannel && targetchannel!=sc) { SocketChannel dest = (SocketChannel)targetchannel; dest.write(charset.encode(content)); } } } } } } } public static void main(String[] args) throws Exception { new NServer().init(); } }
2.2 客户端 NClient.java
package com.example.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Scanner; /** * 客户端 */ public class NClient { private Selector selector; private Charset charset = Charset.forName("UTF-8"); private SocketChannel sc = null; public void init() throws IOException { selector = Selector.open(); InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 3000); sc = SocketChannel.open(isa); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); new ClientThread().start(); @SuppressWarnings("resource") Scanner scan = new Scanner(System.in); while (scan.hasNextLine()) { sc.write(charset.encode(scan.nextLine())); } } private class ClientThread extends Thread { public void run() { try { while (selector.select() > 0) { for (SelectionKey sk : selector.selectedKeys()) { selector.selectedKeys().remove(sk); if (sk.isReadable()) { SocketChannel sc = (SocketChannel)sk.channel(); ByteBuffer buff = ByteBuffer.allocate(1024); String content = ""; while (sc.read(buff) > 0) { sc.read(buff); buff.flip(); content += charset.decode(buff); buff.clear(); } System.out.println("chat info: " + content); sk.interestOps(SelectionKey.OP_READ); } } } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { new NClient().init(); } }
代码来自
https://github.com/xeostream/chat
2.3 效果
3. 对比
从API操作上来看,NIO偏复杂,面向的是异步编程方式,重点围绕Selector,SelectKey操作。
性能对比,主要简单模拟下Echo情景:客户端连接成功,服务端返回一条信息。
3.1Socket性能测试入口
可以关闭ServerMessageReceiver线程
package com.example.socket.client; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class BenchmarkClient { // 监听端口号 private static final int port = 9999; // 绑定到本机的IP地址 private static final String bindAddr = "127.0.0.1"; /** * @param <T> * @param args */ public static <T> void main(String[] args) { try { long s=System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { final Socket socket = new Socket( InetAddress.getByName(bindAddr), port); Future<String> future = Executors.newFixedThreadPool(4).submit( new Callable<String>() { @Override public String call() throws Exception { BufferedReader reader = new BufferedReader( new InputStreamReader(socket .getInputStream(), "UTF-8")); String content = reader.readLine(); return Thread.currentThread().getName()+"--->"+content; } }); System.out.println(i+":"+future.get()); socket.close(); } long e=System.currentTimeMillis(); System.out.println(e-s); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
3.2 NIO性能测试入口
package com.example.nio; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Scanner; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * 客户端 * @author arthur */ public class BenchMarkNClient { private Selector selector; private Charset charset = Charset.forName("UTF-8"); private SocketChannel sc = null; public void init() throws IOException { long s = System.currentTimeMillis(); selector = Selector.open(); InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 3000); for (int i = 0; i < 10000; i++) { sc = SocketChannel.open(isa); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); Future<String> future = Executors.newFixedThreadPool(4).submit(new ClientTask()); try { System.out.println(i+":"+future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } long e= System.currentTimeMillis(); System.out.println(e-s); } private class ClientTask implements Callable<String> { public String call() { try { while (selector.select() > 0) { for (SelectionKey sk : selector.selectedKeys()) { selector.selectedKeys().remove(sk); if (sk.isReadable()) { SocketChannel sc = (SocketChannel)sk.channel(); ByteBuffer buff = ByteBuffer.allocate(1024); String content = ""; while (sc.read(buff) > 0) { sc.read(buff); buff.flip(); content += charset.decode(buff); buff.clear(); } sk.interestOps(SelectionKey.OP_READ); return content; } } } } catch (IOException e) { e.printStackTrace(); } return null; } } public static void main(String[] args) throws IOException { new BenchMarkNClient().init(); } }
3.3 性能对比
次数 | NIO | SOCKET(ms) |
1000 | 525 | 637 |
2000 | 1411 | 1215 |
5000 | 6731 | 2976 |
次数较少时,NIO性能较好。但随着次数增加,性能下降非常厉害。(存疑)