Java面试题:用Java的套接字编程实现一个多线程的回显(echo)服务器。
[
]
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class EchoServer { private static final int ECHO_SERVER_PORT = 6789; public static void main(String[] args) { try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) { System.out.println("服务器已经启动..."); while(true) { Socket client = server.accept(); new Thread(new ClientHandler(client)).start(); } } catch (IOException e) { e.printStackTrace(); } } private static class ClientHandler implements Runnable { private Socket client; public ClientHandler(Socket client) { this.client = client; } @Override public void run() { try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter pw = new PrintWriter(client.getOutputStream())) { String msg = br.readLine(); System.out.println("收到" + client.getInetAddress() + "发送的: " + msg); pw.println(msg); pw.flush(); } catch(Exception ex) { ex.printStackTrace(); } finally { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
注意:上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。
下面是一段回显客户端测试代码:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class EchoClient { public static void main(String[] args) throws Exception { Socket client = new Socket("localhost", 6789); Scanner sc = new Scanner(System.in); System.out.print("请输入内容: "); String msg = sc.nextLine(); sc.close(); PrintWriter pw = new PrintWriter(client.getOutputStream()); pw.println(msg); pw.flush(); BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); System.out.println(br.readLine()); client.close(); } }
如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class EchoServerNIO { private static final int ECHO_SERVER_PORT = 6789; private static final int ECHO_SERVER_TIMEOUT = 5000; private static final int BUFFER_SIZE = 1024; private static ServerSocketChannel serverChannel = null; private static Selector selector = null; // 多路复用选择器 private static ByteBuffer buffer = null; // 缓冲区 public static void main(String[] args) { init(); listen(); } private static void init() { try { serverChannel = ServerSocketChannel.open(); buffer = ByteBuffer.allocate(BUFFER_SIZE); serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT)); serverChannel.configureBlocking(false); selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (Exception e) { throw new RuntimeException(e); } } private static void listen() { while (true) { try { if (selector.select(ECHO_SERVER_TIMEOUT) != 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); handleKey(key); } } } catch (Exception e) { e.printStackTrace(); } } } private static void handleKey(SelectionKey key) throws IOException { SocketChannel channel = null; try { if (key.isAcceptable()) { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); channel = serverChannel.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { channel = (SocketChannel) key.channel(); buffer.clear(); if (channel.read(buffer) > 0) { buffer.flip(); CharBuffer charBuffer = CharsetHelper.decode(buffer); String msg = charBuffer.toString(); System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg); channel.write(CharsetHelper.encode(CharBuffer.wrap(msg))); } else { channel.close(); } } } catch (Exception e) { e.printStackTrace(); if (channel != null) { channel.close(); } } } }
import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; public final class CharsetHelper { private static final String UTF_8 = "UTF-8"; private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder(); private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); private CharsetHelper() { } public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{ return encoder.encode(in); } public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{ return decoder.decode(in); } }