前一章从客户端的角度讨论了Socket,客户端就是向监听连接的服务器打开一个Socket的程序,不过如果不能与服务器对话,客户端并没有什么用处。对于接受连接的服务器,Java提供了一个ServerSocket类表示服务器Socket,基本来说服务器Socket的任务就是坐在电话旁等电话。从技术上讲,服务器Socket在服务器上运行,监听入站TCP连接,每个服务器Socket监听服务器机器上的一个特定端口。
1、使用ServerSocket
在Java中,服务器Socket程序的基本生命周期如下:
- 使用一个ServerSocket()构造函数在一个特定的端口创建一个新的ServerSocket。
- ServerSocket使用其accept()方法监听这个端口的入站连接,accept()会一直阻塞,直到一个客户端尝试建立连接,此时accept()将返回一个连接客户端和服务器的Socket对象。
- 根据服务器的类型,会调用Socket的getInputStream()方法或getOutputStream()方法,获得与客户端通信的输入和输出流。
- 服务器和客户端根据已协商的协议交互, 直到要关闭连接。
- 服务器或客户端关闭连接。
- 服务器返回步骤2,等待下一次连接。
下面是一个daytime服务器的例子:
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class TestSocket {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(13)) {
while (true) {
try (Socket connection = serverSocket.accept()) {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
connection.close();
} catch (Exception e) {
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里的服务器发送最多几十个字节,然后关闭连接,非常快。但可能会有一个很慢或者崩溃的客户端使用服务器挂起几秒的时间,直到它注意到socket已经中断,如果发送数据花费大量时间,你肯定不希望每个连接都必须等待。这里的解决方案是为每个连接提供它自己的一个线程,与接收入站连接放入队列的那个线程分开,防止一个慢客户端阻塞所有其他客户端。这是一种“每个连接对应一个线程”的设计。
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class TestSocket {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(13)) {
while (true) {
try {
Socket connection = serverSocket.accept();
Thread task = new DaytimeThread(connection);
task.start();
} catch (IOException e) {
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static class DaytimeThread extends Thread {
private Socket connection;
public DaytimeThread(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
connection.close();
} catch (IOException e) {
}
}
}
}
}
不过,这个服务器上有可能发生一种拒绝服务攻击(denial-of-service),由于上述示例为每个连接生成一个新线程,大量几乎同时入站连接可能导致它生成极大数量的线程,最终Java虚拟机会耗尽内存而崩溃。一种更好的办法是使用一个固定的线程池来限制可能的资源使用,假设最多50个线程,它可能会拒绝连接,但起码不会崩溃。
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestSocket {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket serverSocket = new ServerSocket(13)) {
while (true) {
try {
Socket connection = serverSocket.accept();
Callable<Void> task = new DaytimeTask(connection);
pool.submit(task);
} catch (IOException e) {
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static class DaytimeTask implements Callable<Void> {
private Socket connection;
public DaytimeTask(Socket connection) {
this.connection = connection;
}
@Override
public Void call() throws Exception {
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
connection.close();
} catch (IOException e) {
}
}
return null;
}
}
}
在目前为止的所有例子中,服务器只写入客户端socket,而没有从客户端读取。不过大多数协议要求服务器同时读写客户端,下面将会给出相关例子。如果使用完一个服务器Socket,就应当将它关闭,这会释放端口,使其他希望使用这个端口的程序可以使用。
2、日志
服务器要在无人看管的情况下运行很长时间,通常需要在很久之后对服务器中发生的情况进行调试,这很重要。建议使用Logger logger = LoggerFactory.getLogger(“sampleName”)来打印日志,具体用法不在这里细述。
3、构造服务器Socket
服务器Socket的构造函数可以指定端口、保存入站连接请求所用的队列的长度,以及要绑定的本地网络接口。例如,在端口80创建服务器Socket可以写为ServerSocket server=new ServerSocket (80),队列一次最多可以保存50个入站连接,则写为ServerSocket server=new ServerSocket (80,50),如果试图将队列长度设置为大于操作系统的最大队列长度,则会使用最大队列长度。
如果一个主机有多个网络接口或IP地址,服务器Socket会在所有接口和IP地址的指定端口上监听。不过,还可以增加第3个参数,要求只绑定一个特定的本地IP地址。如:
InetAddress local=InetAddress.getByName("192.168.210.122");
ServerSocket server=new ServerSocket (80,50,local);
所有在这3个构造函数中,可以为端口号传入0,这样系统就会为你选择可用的端口,像这样由系统选择的端口有时称为匿名端口(anonymous port),这对于多socket协议(如FTP)很有用。
无参数构造函数会创建一个ServerSocket对象,但未将它具体绑定到某个端口,所以初始时它不能接受任何连接,之后可以用bind()来进行绑定。这个特性的主要用途是,允许程序在绑定端口之前设置服务器socket选项,有些选项在服务器socket绑定后必须固定。
4、获得服务器socket的有关信息
ServerSocket类提供了 两个获取方法,可以指出这个服务器Socket占用的本地地址和端口,如果你在一个匿名端口或一个未指定的网络接口上打开一个服务器socket,这会很有用。
5、Socket选项
Socket选项指定了ServerSocket类所依赖的原生Socket如何发送和接收数据,对于服务器Socket,Java支持一线3个选项:
- SO_TIMEOUT:accept()在抛出java.io.InterruptedIOException异常前等待入站连接的时间,一般使用它的默认值0,表示永远不超时。
- SO_REUSEADDR:跟客户端Socket的选项类似,表示是否允许一个新的Socket绑定到之前使用过的一个端口,而此时可能还有一些发送到原Socket的数据正在网络上传输。
- SO_RCVBUF:设置服务器Socket接受的客户端Socket默认接收缓冲区大小。
不同的Internet服务类型有不同的性能需求,例如,体育运动的直播视频需要相对较高的带宽,另一方面,电影可能仍需要高带宽,但是可以接受较大的延迟,电子邮件可以通过低带宽的连接传递,甚至延迟几个小时也不会造成大的危害。为TCP定义了4个通用业务流类型:
- 低成本
- 高可靠性
- 最大吞吐量
- 最小延迟
对于服务器所接受的Socket,setPerformancePreferences()方法描述了为其连接时间、延迟和带宽给定的相对优先级。
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
例如通过将connectionTime设置为2,latency设置为1,bandwidth设置为3,表示最大带宽是最重要的特性,最小延迟最不重要,连接时间居中。ss.setPerformancePreferences(2,1,3)。至于给定的VM究竟如何实现,则取决于具体的Socket实现,底层Socket实现不要求考虑这些需求。它们只是为TCP栈提供了所需策略的一个提示。
6、HTTP服务器
HTTP是个大协议,提供完备功能的HTTP服务器必须响应文件请求,将URL转换为本地系统的文件名,响应POST和GET请求,处理不存在的文件的请求,解释MIME类型等。大多数HTTP服务器都是受带宽和延迟限制,而不是CPU速度的限制,因此Java的其他优点,如半编译/半解释特性、动态类加载、垃圾回收和内存保护得到了发挥的机会,特别是有些网站通过servlet或其他机制大量使用了动态内容,会在Java的Web服务器上运行的更快,这是Java可与C竞争的一个领域。利用Java的网络类库,我们可以编写简单的定制服务器。下面给出一个例子:
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JHTTP {
private static final Logger logger = LoggerFactory.getLogger(JHTTP.class.getCanonicalName());
private static final int NUM_THREADS=50;
private static final String INDEX_FILE="index.html";
private final File rootDirectory;
private final int port;
/**
* @param rootDirectory
* @param port
* @throws IOException
*/
public JHTTP(File rootDirectory, int port) throws IOException {
if(!rootDirectory.isDirectory()){
throw new IOException("根目录不是以一个目录的形式存在");
}
this.rootDirectory = rootDirectory;
this.port = port;
}
public void start() throws IOException {
ExecutorService pool=Executors.newFixedThreadPool(NUM_THREADS);
try(ServerSocket server=new ServerSocket(port)){
logger.info("端口:"+server.getLocalPort());
logger.info("根目录:"+rootDirectory);
while(true){
try {
Socket request=server.accept();
Runnable runnable=new RequestProcessor(rootDirectory, INDEX_FILE, request);
pool.submit(runnable);
} catch (Exception e) {
logger.error("接收连接异常",e);
}
}
}
}
public static void main(String[] args) {
File docroot;//得到文档根
try {
docroot=new File(args[0]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("使用java JHTTP docroot port");
return;
}
int port;//设置要监听的端口
try {
port=Integer.parseInt(args[1]);
if(port<0||port>65535){
port=8080;
}
} catch (RuntimeException e) {
port=8080;
}
try {
JHTTP webserver=new JHTTP(docroot,port);
webserver.start();
} catch (Exception e) {
logger.error("服务器启动异常",e);
}
}
}
处理HTTP请求的runnable类:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RequestProcessor implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(RequestProcessor.class.getCanonicalName());
private File rootDirectory;
private String indexFileName = "index.html";
private Socket connection;
public RequestProcessor(File rootDirectory, String indexFileName, Socket connection) {
if (rootDirectory.isFile()) {
throw new IllegalArgumentException("根目录必须是目录,不是文件");
}
try {
rootDirectory = rootDirectory.getCanonicalFile();
} catch (Exception e) {
}
this.rootDirectory = rootDirectory;
if (indexFileName != null) {
this.indexFileName = indexFileName;
}
this.connection = connection;
}
@Override
public void run() {
// 安全检查
String root = rootDirectory.getPath();
try {
OutputStream raw = new BufferedOutputStream(connection.getOutputStream());
Writer out = new OutputStreamWriter(raw);
Reader in = new InputStreamReader(new BufferedInputStream(connection.getInputStream()));
StringBuilder requestLine = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\r' || c == '\n') {
break;
}
requestLine.append((char) c);
}
String get = requestLine.toString();
logger.info(connection.getRemoteSocketAddress() + " " + get);
String[] tokens = get.split("\\s+");
String method = tokens[0];
String version = "";
if (method.equals("GET")) {
String fileName = tokens[1];
if (fileName.endsWith("/")) {
fileName += indexFileName;
}
String contentType = URLConnection.getFileNameMap().getContentTypeFor(fileName);
if (tokens.length > 2) {
version = tokens[2];
}
File theFile = new File(rootDirectory, fileName.substring(1, fileName.length()));
// 不要让客户端超出文档根之外
if (theFile.canRead() && theFile.getCanonicalPath().startsWith(root)) {
byte[] theData = Files.readAllBytes(theFile.toPath());
if (version.startsWith("HTTP/")) {
// 发送一个MIME首部
sendHeader(out, "HTTP/1.0 200 OK", contentType, theData.length);
}
raw.write(theData);
raw.flush();
} else {
// 无法找到文件
String body = new StringBuilder("<HTML>\r\n").append("<HEAD><TITLE>File Not Found</TITLE>\r\n")
.append("</HEAD>\r\n").append("<BODY>")
.append("<H1>HTTP Error 404:File Not Found</H1>\r\n").append("</BODY></HTML>\r\n")
.toString();
if (version.startsWith("HTTP/")) {
// 发送一个MIME首部
sendHeader(out, "HTTP/1.0 404 File Not Found", "text/html;charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
} else {
// 不是get方法
String body = new StringBuilder("<HTML>\r\n").append("<HEAD><TITLE>Not Implemented</TITLE>\r\n")
.append("</HEAD>\r\n").append("<BODY>").append("<H1>HTTP Error 501:Not Implemented</H1>\r\n")
.append("</BODY></HTML>\r\n").toString();
if (version.startsWith("HTTP/")) {
// 发送一个MIME首部
sendHeader(out, "HTTP/1.0 501 Not Implemented", "text/html;charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
} catch (Exception e) {
logger.error("Error talking to " + connection.getRemoteSocketAddress(), e);
} finally {
try {
connection.close();
} catch (IOException ex) {
}
}
}
private void sendHeader(Writer out, String responseCode, String contentType, int length) throws IOException {
out.write(responseCode + "\r\n");
Date now = new Date();
out.write("Date:" + now + "\r\n");
out.write("Server:JHTTP 2.0\r\n");
out.write("Content-length:" + length + "\r\n");
out.write("Content-type:" + contentType + "\r\n\r\n");
out.flush();
}
}
这个服务器还可以添加以下的一些特性:
- 服务器管理界面
- 支持Java Servlet API
- 支持其他请求方法,如POST、PUT等
- 支持多个文档根目录
如果真的希望使用JHTTP来运行一个高流量的网站,还要做很多事情来优化,比如实现智能缓存,使用非阻塞I/O和通道来代替线程和流。