说是简单聊天系统,压根不能算是一个系统,顶多算个雏形。本文重点不在聊天系统设计和实现上,而是通过实现类似效果,展示下NIO 和Socket两种编程方式的差异性。说是Socket与NIO的编程方式,不太严谨,因为NIO的底层也是通过Socket实现的,但又想不出非常好的题目,就这样吧。

主要内容

Socket方式实现简易聊天效果

NIO方式实现简易聊天效果

两种方式的性能对比


前言

预期效果,是客户端之间进行“广播”式聊天,类似于QQ群聊天。希望以后有机会,以此简易版为基础,不断演进,演练下在线聊天系统。

1.Socket方式实现简易聊天效果

1.1服务端 Server.java

packagecom.example.socket.server;importjava.io.IOException;importjava.net.InetAddress;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.ArrayList;importjava.util.List;publicclassServer{privatestaticintport=9999;//可接受请求队列的最大长度privatestaticintbacklog=100;//绑定到本机的IP地址privatestaticfinalStringbindAddr="127.0.0.1";//socket字典列表privatestaticList<Socket>nodes=newArrayList<Socket>();publicstaticvoidmain(String[]args){try{ServerSocketss=newServerSocket(port,backlog,InetAddress.getByName(bindAddr));for(;;){//发生阻塞,等待客户端连接Socketsc=ss.accept();nodes.add(sc);InetAddressaddr=sc.getLocalAddress();System.out.println("createnewsessionfrom"+addr.getHostName()+":"+sc.getPort()+"\n");//针对一个Socket客户端启动两个线程,分别是接收信息,发送信息newThread(newServerMessageReceiver(sc,nodes)).start();newServerMessageSender(sc).start();}}catch(IOExceptione){e.printStackTrace();}}}

1.2 消息接收端 ServerMessageReceiver.java

额外负责信息广播

packagecom.example.socket.server;importjava.io.BufferedReader;importjava.io.BufferedWriter;importjava.io.IOException;importjava.io.InputStreamReader;importjava.io.OutputStreamWriter;importjava.net.Socket;importjava.util.ArrayList;importjava.util.List;/****接收消息**/publicclassServerMessageReceiverimplementsRunnable{privateSocketsocket;//socket字典列表privateList<Socket>nodes=newArrayList<Socket>();publicServerMessageReceiver(Socketsc,List<Socket>nodes){this.socket=sc;this.nodes=nodes;}/***信息广播到其他节点*/@Overridepublicvoidrun(){try{BufferedReaderreader=newBufferedReader(newInputStreamReader(socket.getInputStream(),"UTF-8"));//接收到的消息Stringcontent;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;}Stringmessage=socket.getPort()+":"+content;//广播信息for(Socketn:this.nodes){if(n!=this.socket){BufferedWriterwriter=newBufferedWriter(newOutputStreamWriter(n.getOutputStream(),"UTF-8"));writer.write(message);writer.newLine();writer.flush();}}}}catch(IOExceptione){e.printStackTrace();}}}

1.3消息发送服务端 ServerMessageSender.java

主要作用:发送欢迎信息

packagecom.example.socket.server;importjava.io.BufferedWriter;importjava.io.IOException;importjava.io.OutputStreamWriter;importjava.net.Socket;publicclassServerMessageSenderextendsThread{privateSocketsocket;publicServerMessageSender(Socketsocket){this.socket=socket;}/***只发送一个欢迎信息*/@Overridepublicvoidrun(){try{BufferedWriterwriter=newBufferedWriter(newOutputStreamWriter(socket.getOutputStream(),"UTF-8"));//BufferedReaderinputReader=newBufferedReader(newInputStreamReader(System.in));try{Stringmsg="server:welcome"+socket.getPort();writer.write(msg);writer.newLine();writer.flush();}catch(IOExceptione){e.printStackTrace();}}catch(Exceptione){e.printStackTrace();}}}

1.4 客户端 Client.java

packagecom.example.socket.client;importjava.net.InetAddress;importjava.net.Socket;publicclassClient{//监听端口号privatestaticfinalintport=9999;//绑定到本机的IP地址privatestaticfinalStringbindAddr="127.0.0.1";publicstaticvoidmain(String[]args){try{System.out.println("正在连接Socket服务器");Socketsocket=newSocket(InetAddress.getByName(bindAddr),port);System.out.println("已连接\n==================================");newClientMessageSender(socket).start();newClientMessageReceiver(socket).start();}catch(Exceptione){e.printStackTrace();}}}

1.4 消息接收客户端 ClientMessageReceiver.java

仅仅是输出

packagecom.example.socket.client;importjava.io.BufferedReader;importjava.io.InputStreamReader;importjava.net.Socket;publicclassClientMessageReceiverextendsThread{privateSocketsocket;publicClientMessageReceiver(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){try{//获取socket的输出\入流BufferedReaderreader=newBufferedReader(newInputStreamReader(socket.getInputStream(),"UTF-8"));//接收到的消息Stringcontent;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(Exceptione){e.printStackTrace();}}}

1.5 消息发送客户端 ClientMessageSender.java

通过输入流输入,将信息传入Socket

packagecom.example.socket.client;importjava.io.BufferedReader;importjava.io.BufferedWriter;importjava.io.IOException;importjava.io.InputStreamReader;importjava.io.OutputStreamWriter;importjava.net.Socket;publicclassClientMessageSenderextendsThread{privateSocketsocket;publicClientMessageSender(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){try{BufferedWriterwriter=newBufferedWriter(newOutputStreamWriter(socket.getOutputStream(),"UTF-8"));BufferedReaderinputReader=newBufferedReader(newInputStreamReader(System.in));try{Stringmsg;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(IOExceptione){e.printStackTrace();}}catch(Exceptione){e.printStackTrace();}}}

1.6 效果

2.NIO方式实现简易聊天效果

2.1服务端 NServer.java

packagecom.example.nio;importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.Channel;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.ServerSocketChannel;importjava.nio.channels.SocketChannel;importjava.nio.charset.Charset;/***服务器端*/publicclassNServer{privateSelectorselector;privateCharsetcharset=Charset.forName("UTF-8");publicvoidinit()throwsException{selector=Selector.open();ServerSocketChannelserver=ServerSocketChannel.open();InetSocketAddressisa=newInetSocketAddress("127.0.0.1",3000);server.socket().bind(isa);server.configureBlocking(false);server.register(selector,SelectionKey.OP_ACCEPT);while(selector.select()>0){for(SelectionKeykey:selector.selectedKeys()){selector.selectedKeys().remove(key);if(key.isAcceptable()){SocketChannelsc=server.accept();System.out.println("createnewsessionfrom"+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()){SocketChannelsc=(SocketChannel)key.channel();ByteBufferbuff=ByteBuffer.allocate(1024);Stringcontent="";try{while(sc.read(buff)>0){buff.flip();content+=charset.decode(buff);buff.clear();}key.interestOps(SelectionKey.OP_READ);}catch(IOExceptione){key.cancel();if(key.channel()!=null)key.channel().close();}if(content.length()>0){for(SelectionKeysk:selector.keys()){Channeltargetchannel=sk.channel();if(targetchannelinstanceofSocketChannel&&targetchannel!=sc){SocketChanneldest=(SocketChannel)targetchannel;dest.write(charset.encode(content));}}}}}}}publicstaticvoidmain(String[]args)throwsException{newNServer().init();}}

2.2 客户端 NClient.java

packagecom.example.nio;importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.SocketChannel;importjava.nio.charset.Charset;importjava.util.Scanner;/***客户端*/publicclassNClient{privateSelectorselector;privateCharsetcharset=Charset.forName("UTF-8");privateSocketChannelsc=null;publicvoidinit()throwsIOException{selector=Selector.open();InetSocketAddressisa=newInetSocketAddress("127.0.0.1",3000);sc=SocketChannel.open(isa);sc.configureBlocking(false);sc.register(selector,SelectionKey.OP_READ);newClientThread().start();@SuppressWarnings("resource")Scannerscan=newScanner(System.in);while(scan.hasNextLine()){sc.write(charset.encode(scan.nextLine()));}}privateclassClientThreadextendsThread{publicvoidrun(){try{while(selector.select()>0){for(SelectionKeysk:selector.selectedKeys()){selector.selectedKeys().remove(sk);if(sk.isReadable()){SocketChannelsc=(SocketChannel)sk.channel();ByteBufferbuff=ByteBuffer.allocate(1024);Stringcontent="";while(sc.read(buff)>0){sc.read(buff);buff.flip();content+=charset.decode(buff);buff.clear();}System.out.println("chatinfo:"+content);sk.interestOps(SelectionKey.OP_READ);}}}}catch(IOExceptione){e.printStackTrace();}}}publicstaticvoidmain(String[]args)throwsIOException{newNClient().init();}}

代码来自

https://github.com/xeostream/chat
2.3 效果


3. 对比

从API操作上来看,NIO偏复杂,面向的是异步编程方式,重点围绕Selector,SelectKey操作。

性能对比,主要简单模拟下Echo情景:客户端连接成功,服务端返回一条信息。

3.1Socket性能测试入口

可以关闭ServerMessageReceiver线程

packagecom.example.socket.client;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.InetAddress;importjava.net.Socket;importjava.net.UnknownHostException;importjava.util.concurrent.Callable;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;publicclassBenchmarkClient{//监听端口号privatestaticfinalintport=9999;//绑定到本机的IP地址privatestaticfinalStringbindAddr="127.0.0.1";/***@param<T>*@paramargs*/publicstatic<T>voidmain(String[]args){try{longs=System.currentTimeMillis();for(inti=0;i<1000;i++){finalSocketsocket=newSocket(InetAddress.getByName(bindAddr),port);Future<String>future=Executors.newFixedThreadPool(4).submit(newCallable<String>(){@OverridepublicStringcall()throwsException{BufferedReaderreader=newBufferedReader(newInputStreamReader(socket.getInputStream(),"UTF-8"));Stringcontent=reader.readLine();returnThread.currentThread().getName()+"--->"+content;}});System.out.println(i+":"+future.get());socket.close();}longe=System.currentTimeMillis();System.out.println(e-s);}catch(UnknownHostExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();}}}

3.2 NIO性能测试入口

packagecom.example.nio;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.SocketChannel;importjava.nio.charset.Charset;importjava.util.Scanner;importjava.util.concurrent.Callable;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;/***客户端*@authorarthur*/publicclassBenchMarkNClient{privateSelectorselector;privateCharsetcharset=Charset.forName("UTF-8");privateSocketChannelsc=null;publicvoidinit()throwsIOException{longs=System.currentTimeMillis();selector=Selector.open();InetSocketAddressisa=newInetSocketAddress("127.0.0.1",3000);for(inti=0;i<10000;i++){sc=SocketChannel.open(isa);sc.configureBlocking(false);sc.register(selector,SelectionKey.OP_READ);Future<String>future=Executors.newFixedThreadPool(4).submit(newClientTask());try{System.out.println(i+":"+future.get());}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();}}longe=System.currentTimeMillis();System.out.println(e-s);}privateclassClientTaskimplementsCallable<String>{publicStringcall(){try{while(selector.select()>0){for(SelectionKeysk:selector.selectedKeys()){selector.selectedKeys().remove(sk);if(sk.isReadable()){SocketChannelsc=(SocketChannel)sk.channel();ByteBufferbuff=ByteBuffer.allocate(1024);Stringcontent="";while(sc.read(buff)>0){sc.read(buff);buff.flip();content+=charset.decode(buff);buff.clear();}sk.interestOps(SelectionKey.OP_READ);returncontent;}}}}catch(IOExceptione){e.printStackTrace();}returnnull;}}publicstaticvoidmain(String[]args)throwsIOException{newBenchMarkNClient().init();}}

3.3 性能对比

次数
NIO
SOCKET(ms)
1000
525
6372000
14111215
2000(休眠时间为100毫秒)
2059282063135000
6731
2976

次数较少时,NIO性能较好。但随着次数增加,性能下降非常厉害。(存疑)

当通讯时间变长时,发现NIO性能又相对提高了。

可见一个技术的好坏,是和业务场景分不开的。