千家信息网

简易版聊天系统实现 Socket VS NIO两种实现方式

发表于:2025-01-29 作者:千家信息网编辑
千家信息网最后更新 2025年01月29日,说是简单聊天系统,压根不能算是一个系统,顶多算个雏形。本文重点不在聊天系统设计和实现上,而是通过实现类似效果,展示下NIO 和Socket两种编程方式的差异性。说是Socket与NIO的编程方式,不太
千家信息网最后更新 2025年01月29日简易版聊天系统实现 Socket VS NIO两种实现方式

说是简单聊天系统,压根不能算是一个系统,顶多算个雏形。本文重点不在聊天系统设计和实现上,而是通过实现类似效果,展示下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 nodes= new ArrayList();    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 nodes= new ArrayList();    public ServerMessageReceiver(Socket sc,List 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      * @param args     */    public static  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 future = Executors.newFixedThreadPool(4).submit(                        new Callable() {                            @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 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 {        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
14111215
2000(休眠时间为100毫秒)
205928206313
5000
6731
2976

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

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

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

0