Java NIO编写Socket服务器的一个例子

Java中编写Socket服务器,通常有一下几种模式:

1. 一个链接一个线程;优点:程序编写简单; 缺点:如果链接非常多,分配的线程会非常多,机器可能资源耗尽而崩溃。

2.把每一个新链接,交接给一个拥有固定数量线程的连接池;优点:程序编写相对简单,可以处理大量的链接。确定:线程的开销非常大,链接很多的情况,排队现象会比较严重。

3. 使用Java中NIO,用异步IO方式处理。这种模式,可以用一个线程,处理大量的链接。


下面使用java中NIO,编写一个Socket服务器程序。要使用java中NIO,必须掌握下面几个概念: ByteBuffer, Channel, Selector和SelectionKey。

这里就不介绍这些基本概念了,网上资料很多。


下面程序接收客户端输入一行文本(以“\r\n”)结束,并向客户端回显输入的文本。如果输入的文本是 “get file:xxxx”模式,则把“xxxx” 解析为服务器class path下的一个文件,如果找到该文件,则回显该文件的内容,如果找不到文件,回显文件不能找到的消息。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URL;
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.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
	private static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;

	private static final int BUFFER_SIZE = 1024;

	private int port = 8081;

	public NIOServer(int port) {
		this.port = port;
	}

	public NIOServer() {
	}

	public void start() {
		ServerSocketChannel ssc = null;
		try {
			ssc = ServerSocketChannel.open();
			ssc.configureBlocking(false);
			ssc.bind(new InetSocketAddress(this.port));
			Selector sel = Selector.open();
			ssc.register(sel, SelectionKey.OP_ACCEPT);
			while (true) {
				Set keySet = null;
				try {
					sel.select();
					keySet = sel.selectedKeys();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}

				for (Iterator it = keySet.iterator(); it.hasNext();) {
					SelectionKey sKey = it.next();
					it.remove();
					try {
						if (sKey.isAcceptable()) {
							ServerSocketChannel serChannel = (ServerSocketChannel) sKey.channel();
							SocketChannel clientChannel = serChannel.accept();
							clientChannel.configureBlocking(false);

							SelectionKey k2 = clientChannel.register(sel, SelectionKey.OP_READ);
							k2.attach(ByteBuffer.allocate(BUFFER_SIZE));

						} else if (sKey.isWritable()) {
							SocketChannel clientChannel = (SocketChannel) sKey.channel();
							ByteBuffer[] bfs = (ByteBuffer[]) sKey.attachment();
							if (bfs[bfs.length - 1].hasRemaining()) {
								clientChannel.write(bfs);
							} else {
								clientChannel.close();

							}

						} else if (sKey.isReadable()) {
							SocketChannel clientChannel = (SocketChannel) sKey.channel();
							ByteBuffer bf = (ByteBuffer) sKey.attachment();
							String msg = "";
							boolean clientEnd = false;
							if (bf.hasRemaining()) {
								int len = clientChannel.read(bf);
								if (len != -1 && bf.position() > 1) {
									char lastChar = (char) bf.get(bf.position() - 1);
									char last2Char = (char) bf.get(bf.position() - 2);
									if (String.valueOf(new char[] { last2Char, lastChar }).equals("\r\n")) {
										System.out.println("client inupt end.");
										clientEnd = true;
									}
								}

								if (len == -1) {
									System.out.println("client closed.");
									clientEnd = true;
								}

							} else {
								System.out.println("buff is full.");
								msg = "You can only enter " + BUFFER_SIZE + " chars\r\n";
								clientEnd = true;
							}

							if (clientEnd) {
								ByteBuffer[] att = processInput(bf, msg);
								clientChannel.register(sel, SelectionKey.OP_WRITE, att);
							}
						}
					} catch (Exception e) {
						sKey.cancel();
						e.printStackTrace();
					}
				}

			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (ssc != null) {
				try {
					ssc.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

	}

	private ByteBuffer[] processInput(ByteBuffer bf, String msg) throws Exception {
		bf.flip();
		ByteBuffer promptMsg = ByteBuffer.wrap((msg + "You just input:\r\n").getBytes(DEFAULT_CHARSET));

		String inputMsg = new String(bf.array(), bf.position(), bf.limit(), DEFAULT_CHARSET).trim();

		ByteBuffer[] att = new ByteBuffer[] { promptMsg, bf };

		if (inputMsg.indexOf("get file:") >= 0) {
			String fileName = inputMsg.substring("get file:".length()).trim();
			System.out.println("fileName=" + fileName);
			URL fileURL = this.getClass().getClassLoader().getResource(fileName);
			if (fileURL != null) {
				Path path = Paths.get(fileURL.toURI());
				System.out.println(path);

				ByteBuffer fileData = ByteBuffer.wrap(Files.readAllBytes(path));
				ByteBuffer info = ByteBuffer
						.wrap(("The content of file " + fileName + ":\r\n").getBytes(DEFAULT_CHARSET));
				att = new ByteBuffer[] { promptMsg, bf, info, fileData };

			} else {
				String errMsg = "fileName: " + fileName + " not found in the classpath.";
				System.out.println(errMsg);

				att = new ByteBuffer[] { promptMsg, bf, ByteBuffer.wrap(errMsg.getBytes(DEFAULT_CHARSET)) };

			}

		}
		return att;
	}

	public static void main(String[] args) {
		new NIOServer().start();
	}

}


运行上面的程序。


然后在 window的命令行,用telnet 测试:

telnet localhost 8081

然后 输入: 

get file:test.txt

注意:在我的class path 下有 test.txt 这个文件(放到eclipse src/test.txt). 

然后看到下面的输出:

You just input:
get file:test.txt
The content of file test.txt:
Hello World 1!
Hello World 2!
Hello World 3!
Hello World 4!

Connection to host lost.


常见错误1:

在写缓冲区的时候这样:

					     if (sKey.isWritable()){	
							   while((buffer.hasRemaining())) {
								clientChannel.write(bfs);
					          }
						  }


读缓冲区的时候这样:


				   if (sKey.isReadable()) {
							while(buffer.hasRemaining()) {
								int len = clientChannel.read(bf);
								if (len != -1 ) break;
						
							}
					}

上面的的操作,只有一个channel数据处理完后,才会处理其他channel。这样就会阻塞其他channel。


常见错误2:


把所有代码都放到,一个try-catch中,如下代码:

        try {
			ssc = ServerSocketChannel.open();
			ssc.configureBlocking(false);
			ssc.bind(new InetSocketAddress(this.port));
			Selector sel = Selector.open();
			ssc.register(sel, SelectionKey.OP_ACCEPT);
			while (true) {
				Set keySet = null;
				sel.select();
			        keySet = sel.selectedKeys();
				for (Iterator it = keySet.iterator(); it.hasNext();) {
					SelectionKey sKey = it.next();
					it.remove();
					
					if (sKey.isAcceptable()) {
						....

					} else if (sKey.isWritable()) {
						
						....

					} else if (sKey.isReadable()) {
						....
	
					}
					
				}

			}

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (ssc != null) {
				try {
					ssc.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}


这样,在处理某一客户端请求,如果出现了运行时异常,就会让整个服务器程序挂掉。






你可能感兴趣的:(Java)