传统服务器是阻塞的,即ServerSocket的accept方法,在新到达一个客户端连接前,不会返回(当然,有异常发生时例外)。
为应付多个客户端同时连接,往往会为每个客户端创建一个线程,开销较大,所以一般会用线程池解决。
但总感觉这种方法不够优雅,也不够好。还好有NIO。
其实接触NIO已经有一段时间了,但是总是服务器NIO这块没弄明白,最近终于把它弄的差不多了。写了一个ECHO的例子,希望对以后面临同样问题的人一点提示。
(ECHO就是客户端发给服务器什么信息,服务器就把什么信息返回给客户端)
首先是服务器端代码:
- package nio.server;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.net.ServerSocket;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- import java.util.Set;
- public class MainServer {
- public static final int DEFAULT_PORT=22333;
- private int port;
- private boolean stopped=false;
- private byte[] signature="ECHO:".getBytes();
- private ServerSocketChannel serverChannel;
- private Selector selector;
- public MainServer(int port){
- this.port=port;
- }
- public MainServer(){
- this.port=DEFAULT_PORT;
- }
- public void startUp(){
- ByteBuffer buffer=ByteBuffer.allocate(1024);
- try {
- //打开一个ServerSocketChannel
- serverChannel=ServerSocketChannel.open();
- //获取ServerSocketChannel的对等ServerSocket,将其绑定到指定端口
- ServerSocket serverSocket=serverChannel.socket();
- serverSocket.bind(new InetSocketAddress(port));
- //将ServerSocketChannel设置为非阻塞状态
- serverChannel.configureBlocking(false);
- //打开一个选择器
- selector=Selector.open();
- //将ServerSocketChannel注册到选择器
- serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- while(false == stopped){
- //阻塞
- selector.select();
- //获取所有的被激活的选择键,并遍历
- Set<SelectionKey> keys=selector.selectedKeys();
- Iterator<SelectionKey> iterator=keys.iterator();
- while(iterator.hasNext()){
- SelectionKey key=iterator.next();
- //移除正在遍历的选择键,防止以后再次调用它
- iterator.remove();
- //如果被激活的选择键的channel是ServerSocketChannel,则说明有新的Socket连接
- if(key.isAcceptable()){
- ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();
- //获取新的Socket对应的SocketChannel
- SocketChannel socketChannel=serverSocketChannel.accept();
- //将SocketChannel设置为非阻塞
- socketChannel.configureBlocking(false);
- //将SocketChannel注册到选择器上,为读状态
- socketChannel.register(selector, SelectionKey.OP_READ);
- //如果被激活的键的channel是SocketChannel,且其为读状态
- }else if(key.isReadable()){
- SocketChannel socketChannel=(SocketChannel)key.channel();
- buffer.clear();
- //读取信息
- int length=socketChannel.read(buffer);
- byte[] attachment=new byte[length];
- buffer.flip();
- buffer.get(attachment,0,length);
- //将读取的信息作为附件添加给这个键
- key.attach(attachment);
- //将这个键的状态转换为写状态
- key.interestOps(SelectionKey.OP_WRITE);
- //如果被激活的键的channel是SocketChannel,且其为写状态
- }else if(key.isWritable()){
- SocketChannel socketChannel=(SocketChannel)key.channel();
- //获取这个键所对应的附件
- byte[] attachment=(byte[])key.attachment();
- buffer.clear();
- //添加标记
- buffer.put(signature,0,signature.length);
- buffer.put(attachment,0,attachment.length);
- //注意,仅在window上测试可行,其他操作系统不一定以/r/n作为行结束符
- buffer.put((byte)'/r');
- buffer.put((byte)'/n');
- buffer.flip();
- socketChannel.write(buffer);
- //将这个键的状态转换为读状态
- key.interestOps(SelectionKey.OP_READ);
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public void shutDown(){
- this.stopped=true;
- try {
- serverChannel.close();
- selector.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- selector=null;
- }
- public static void main(String[] args){
- MainServer server=new MainServer();
- server.startUp();
- }
- }
客户端代码,没太多注释,感觉没必要
- package nio.server;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStream;
- import java.net.InetSocketAddress;
- import java.net.Socket;
- import java.net.SocketAddress;
- public class MainClient {
- public final static String DEFAULT_IP="127.0.0.1";
- public final static int DEFAULT_PORT=22333;
- private String ip;
- private int port;
- private boolean stopped=false;
- private Socket socket;
- private BufferedReader input;
- public MainClient(String ip,int port){
- this.ip=ip;
- this.port=port;
- }
- public MainClient(){
- this.ip=DEFAULT_IP;
- this.port=DEFAULT_PORT;
- }
- public void startUp(){
- SocketAddress address=new InetSocketAddress(ip,port);
- socket=new Socket();
- BufferedReader input=new BufferedReader(new InputStreamReader(System.in));
- OutputStream output=null;
- BufferedReader reader=null;
- try {
- socket.connect(address);
- output=socket.getOutputStream();
- reader=new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf8"));
- while(false == stopped){
- String inputString=input.readLine();
- output.write(inputString.getBytes("utf8"));
- output.flush();
- String echoMessage=reader.readLine();
- System.out.println(echoMessage);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- public void shutDown(){
- this.stopped=true;
- try {
- socket.close();
- input.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- socket=null;
- input=null;
- }
- public static void main(String[] args) {
- MainClient client=new MainClient();
- client.startUp();
- }
- }
OK,说简单也不简单,说难也不难。主要弄懂Selector Channel Buffer三个大概念就好,希望对需要的人能有所帮助!