Java提供了ServerSocket类代表服务端的socket,服务端的socket主要工作是在某个端口上等待tcp连接。当远程的客户端尝试连接服务端,服务端就会被“唤醒”,然后协调处一条与客户端的连接并返回一个代表客户端和服务器之间的套接字的Socket对象。
ServerSocket的使用
在Java中,服务器程序的基本生命周期是这样的:
还记得上一篇文章中用到的daytime服务器吗?下面我们实现自己的daytime服务器:
public class MyDaytiemServer {public final static int PORT = 13;
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("Listening on port 13");
while (true) {
try (Socket connection = server.accept()) {
System.out.println("A request come in...");
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
} catch (IOException e) {}
}
}catch
多线程服务器
我们上面的服务器是单线程的,如果有多个请求连接进来,操作系统会把这些请求存储到一个先进先出队列中,这个队列的默认大小为50。一旦这个队列满了,服务器就会拒绝额外的连接请求直到队列不是满的状态。
因此,合理的做法是把每个连接放到一个独立的线程中去处理:
public class MyDaytiemServerpublic final static int PORT = 13;
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("Listening on port 13");
while (true) {
try{
Socket connection = server.accept();
Thread t = new DaytimeThread(connection);
t.start();
} catch (IOException e) {}
}
}catch (IOException ex) {
System.err.println(ex);
}
}
private static class DaytimeThread extends Thread{
private Socket connection;
public DaytimeThread(Socket connection){
this.connection = connection;
}
@Override
public void run() {
try {
System.out.println("A request come in...");
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
}catch (IOException ex){
System.out.println(ex);
}finally {
try{
connection.close();
}catch
好了,现在是多线程了,可以处理并发请求了。但是,服务器能创建的线程数量是有限的,如果同时涌入了大量的请求,也会造成服务器崩溃。因此,考虑使用固定大小的线程池:
public class MyDaytiemServerpublic final static int PORT = 13;
public static void main(String[] args) throws IOException {
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("Listening on port 13");
while (true) {
try{
Socket connection = server.accept();
Runnable task = new DaytimeThread(connection);
pool.submit(task);
} catch (IOException e) {}
}
}catch (IOException ex) {
System.err.println(ex);
}
}
private static class DaytimeThread implements Runnable{
private Socket connection;
public DaytimeThread(Socket connection){
this.connection = connection;
}
@Override
public void run() {
try {
System.out.println("A request come in...");
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
}catch (IOException ex){
System.out.println(ex);
}finally {
try{
connection.close();
}catch
ServerSocket的构造函数
public ServerSocket(int port) throws BindException, IOExceptionpublic ServerSocket(int port, int queueLength) throws BindException, IOException
public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws IOException
public ServerSocket() throws
这些构造函数指定了端口,存储请求的队列大小,本地绑定的地址(对应不同的网卡)
如果主机有多个网卡或IP地址,在默认情况下,服务端socket会在所有的网卡和地址上监听连接请求。如果你想只绑定某个网卡或IP,那么你就可以使用第三个构造函数来指定。
获取ServerSocket的相关信息
public InetAddress getInetAddress()public int getLocalPort()
如果本机有多个IP地址,getInetAddress会返回其中一个,你无法预测返回的是哪个。
try {ServerSocket server = new ServerSocket(0);
System.out.println("This server runs on port "
+ server.getLocalPort());
} catch
通过getLocalPort()方法可以得知是在哪个端口上监听。
自己动手实现HTTP服务器
文件服务器
import java.io.*;import java.net.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.logging.*;
public class SingleFileHTTPServer {
private static final Logger logger = Logger.getLogger("SingleFileHTTPServer");
private final byte [] content;
private final byte [] header;
private final int port;
private final String encoding;
public SingleFileHTTPServer(String data,String encoding,String mimeType,int port) throws UnsupportedEncodingException {
this(data.getBytes(encoding),encoding,mimeType,port);
}
public SingleFileHTTPServer(byte [] data,String encoding,String mimeType,int port) {
this.content = data;
this.port = port;
this.encoding = encoding;
// 返回的头部
String header = "HTTP/1.0 200 OK\r\n"
+ "Server: OneFile 2.0\r\n"
+ "Content-length: " + this.content.length + "\r\n"
+ "Content-type: " + mimeType + "; charset=" + encoding + "\r\n\r\n";
this.header = header.getBytes(Charset.forName("US-ASCII"));
}
public void start() {
ExecutorService pool = Executors.newFixedThreadPool(100);
try (ServerSocket server = new ServerSocket(this.port)){
logger.info("Accepting connections on port " + server.getLocalPort());
logger.info("Data to be sent:");
logger.info(new String(this.content, encoding));
while (true) {
try {
Socket connection = server.accept();
pool.submit(new HTTPHandler(connection));
}catch (IOException ex) {
logger.log(Level.WARNING, "Exception accepting connection", ex);
}catch (RuntimeException ex) {
logger.log(Level.SEVERE, "Unexpected error", ex);
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Could not start server", ex);
}
}
private class HTTPHandler implements Callable<Void> {
private final Socket connection;
HTTPHandler(Socket connection) {
this.connection = connection;
}
@Override
public Void call() throws Exception {
try {
OutputStream out = new BufferedOutputStream(connection.getOutputStream());
InputStream in = new BufferedInputStream(connection.getInputStream());
// read the first line only; that's all we need
StringBuilder request = new StringBuilder(80);
while (true) {
int c = in.read();
if(c == '\r' || c == '\n' || c == -1) break;;
request.append((char)c);
}
logger.log(Level.INFO,"Request is: " + request);
// IF this is HTTP/1.0 or later send a MIME header
if(request.toString().indexOf("HTTP/") != -1) {
out.write(header);
}
out.write(content);
out.flush();
}catch (IOException ex){
logger.log(Level.WARNING, "Error writing to client", ex);
}finally {
connection.close();
}
return null;
}
}
public static void main(String[] args) {
int port = 80;
String encoding = "UTF-8";
String pathStr = "D:\\workspace\\js\\AllBranchs\\5\\ha-web\\coverage\\index.html";
try {
Path path = Paths.get(pathStr);
byte[] data = Files.readAllBytes(path);
String contentType = URLConnection.getFileNameMap().getContentTypeFor(pathStr);
SingleFileHTTPServer server = new SingleFileHTTPServer(data, encoding,
contentType, port);
server.start();
} catch
只要用户访问localhost,就给他返回一个固定的文件。
重定向服务器
import java.io.*;import java.net.*;
import java.util.*;
import java.util.logging.*;
/**
* Create by yinjingwei on 2018/6/22/022.
*/
public class Redirector
private static final Logger logger = Logger.getLogger("Redirector");
private final int port;
private final String newSite;
public Redirector(String newSite, int port) {
this.port = port;
this.newSite = newSite;
}
public void start() {
try (ServerSocket server = new ServerSocket(port)) {
logger.info("Redirecting connections on port "
+ server.getLocalPort() + " to " + newSite);
while (true) {
try {
Socket s = server.accept();
Thread t = new RedirectThread(s);
t.start();
} catch (IOException ex) {
logger.warning("Exception accepting connection");
} catch (RuntimeException ex) {
logger.log(Level.SEVERE, "Unexpected error", ex);
}
}
} catch (BindException ex) {
logger.log(Level.SEVERE, "Could not start server.", ex);
} catch (IOException ex) {
logger.log(Level.SEVERE, "Error opening server socket", ex);
}
}
private class RedirectThread extends Thread
private final Socket connection;
RedirectThread(Socket s) {
this.connection = s;
}
public void run() {
try {
Writer out = new BufferedWriter(
new OutputStreamWriter(
connection.getOutputStream(), "US-ASCII"
)
);
Reader in = new InputStreamReader(
new BufferedInputStream(
connection.getInputStream())
);
// read the first line only; that's all we need
StringBuilder request = new StringBuilder(80);
while (true) {
int c = in.read();
if (c == '\r' || c == '\n' || c == -1) break;
request.append((char) c);
}
String get = request.toString();
String[] pieces = get.split("\\w*");
String theFile = pieces[1];
// If this is HTTP/1.0 or later send a MIME header
if (get.indexOf("HTTP") != -1) {
out.write("HTTP/1.0 302 FOUND\r\n"); //302重定向
Date now = new Date();
out.write("Date: " + now + "\r\n");
out.write("Server: Redirector 1.1\r\n");
out.write("Location: " + newSite + theFile + "\r\n");
out.write("Content-type: text/html\r\n\r\n");
out.flush();
}
// Not all browsers support redirection so we need to
// produce HTML that says where the document has moved to.
out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
out.write("<BODY><H1>Document moved</H1>\r\n");
out.write("The document " + theFile
+ " has moved to\r\n<A HREF=\"" + newSite + theFile + "\">"
+ newSite + theFile
+ "</A>.\r\n Please update your bookmarks<P>");
out.write("</BODY></HTML>\r\n");
out.flush();
logger.log(Level.INFO,
"Redirected " + connection.getRemoteSocketAddress());
} catch(IOException ex) {
logger.log(Level.WARNING,
"Error talking to " + connection.getRemoteSocketAddress(), ex);
} finally {
try {
connection.close();
} catch (IOException ex) {}
}
}
}
public static void main(String[] args) {
int thePort = 80;
String theSite = "http://www.baidu.com";
Redirector redirector = new
访问localhost会重定向到百度。
特定用途的HTTP服务器就举例到这里了,接下来是压轴大戏,一个功能基本完整的HTTP服务器,能访问整个网站目录,包括html,js,css等。
五脏俱全的HTTP服务器
麻雀虽小,五脏俱全。
import java.io.*;import java.net.*;
import java.nio.file.Files;
import java.util.Date;
import java.util.concurrent.*;
import java.util.logging.*;
/**
* Create by yinjingwei on 2018/6/22/022.
*/
public class JHTTP
private static final Logger logger = Logger.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;
public JHTTP(File rootDirectory, int port) throws IOException {
if (!rootDirectory.isDirectory()) {
throw new IOException(rootDirectory + " dose not exist as a dirctory");
}
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("Accepting connections on port " + server.getLocalPort());
logger.info("Document Root: " + rootDirectory);
while (true) {
try{
Socket request = server.accept();
Runnable r = new RequestProcessor(rootDirectory,INDEX_FILE,request);
pool.submit(r);
}catch (IOException ex){
logger.log(Level.WARNING,"Error accepting connection",ex);
}
}
}
}
public static void main(String[] args) {
File docroot;
try {
docroot = new File(args[0]);
}catch (ArrayIndexOutOfBoundsException ex){
System.out.println("Usage: java JHTTP docroot port");
return;
}
int port;
try{
port = Integer.parseInt(args[1]);
if(port <0 || port > 65535) port = 8888;
}catch (RuntimeException ex){
port = 8888;
}
try {
JHTTP webserver = new JHTTP(docroot, port);
webserver.start();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Server could not start", ex);
}
}
}
class RequestProcessor implements Runnable{
private final static Logger logger = Logger.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(
"rootDirectory must be a directory, not a file");
}
try {
rootDirectory = rootDirectory.getCanonicalFile();
}catch (IOException ex){}
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()),
"UTF-8");
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 (fileName.endsWith(".css")) {
contentType = "text/css"; // 修复css无法解析的问题
}
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/")) {
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/")) {
sendHeader(out, "HTTP/1.0 404 File Not Found",
"text/html; charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
} else { // method dose not equal "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/")) { // send a MIME header
sendHeader(out, "HTTP/1.0 501 Not Implemented",
"text/html; charset=utf-8", body.length());
}
out.write(body);
out.flush();
}
}catch (IOException ex) {
logger.log(Level.WARNING,"Error talking to " + connection.getRemoteSocketAddress(), ex);
}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();
}
}
这个服务器暂时只能处理get请求,但是用作静态网页的服务器已经足够了。
由于css文件getContentTypeFor会解析为text/plain。导致css样式无法解析,可通过下面的代码解析:
contentType = "text/css"; // 修复css无法解析的问题