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;
}
}
常见错误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();
}
}
}
这样,在处理某一客户端请求,如果出现了运行时异常,就会让整个服务器程序挂掉。