绝大部分知识与实例来自O’REILLY的《Java网络编程》(Java Network Programming,Fourth Edition,by Elliotte Rusty Harold(O’REILLY))。
ServerSocket简介
ServerSocket类包含了使用Java编写服务器所需的全部内容,其中包括创建新ServerSocket对象的构造函数、在指定端口监听连接的方法、配置各个服务器Socket选项的方法,以及一些常见的方法(如toString)。
在Java中,一个服务器的生命周期如下:
- 使用ServerSocket的构造函数在一个特定端口创建一个新的ServerSocket;
- ServerSocket使用其accept()方法监听这个端口的入站连接。accept()方法会一直阻塞,直到一个客户端尝试建立连接,此时方法会返回一个连接客户端和服务器的Socket对象;
- 根据服务器的类型调用Socket的getInputStream()和getOutputStream()获取输入/输出流;
- 服务器和客户端根据协议交互,直到关闭连接;
- 服务器或客户端关闭连接;
- 服务器回到步骤2,等待下一次连接。
下面看一个简单的服务器,用于获取时间:
实例1:Daytime服务器
public static void createDaytimeServer(){
try(ServerSocket server = new ServerSocket(13)){
while(true){
try(Socket connection = server.accept()){
Writer out = new OutputStreamWriter(
connection.getOutputStream(),
"UTF-8");
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
}catch (IOException e) {
}
}
} catch (IOException e) {
System.err.println("服务器意外退出");
}
}
启动服务器:
createDaytimeServer();
请求服务器提供服务:
try(Socket socket = new Socket("127.0.0.1", 13)){
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "UTF-8"));
System.out.println(in.readLine());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
输出:
Wed Sep 13 09:34:09 CST 2017
注意点有以下几个:
- 客户端Socket与服务器Socket使用完毕后都需要调用close()关闭连接,Java 7及之后的版本可以利用try-with-resources实现。
- 启动服务器的代码和请求服务器提供服务的代码要分开执行,否则可能得不到输出。
- 服务器的代码中有两类异常,一类是某个客户端连接出错时抛出的,一类是服务器出错时抛出的,必须分别处理。
- 在同一台机器上测试的话,可以为Socket传入本地回送地址127.0.0.1。
多线程服务器
前面的服务器一次只能处理一个请求,如果有一个很慢的客户端先请求到了服务,会使得后面的客户端阻塞很长时间。一个比较好的方法是为每个连接提供一个线程。
实例2:使用线程池的多线程Daytime服务器
public static void createPooledDaytimeServer(){
ExecutorService pool = Executors.newFixedThreadPool(50);
try(ServerSocket server = new ServerSocket(13)){
while(true){
try {
Socket connection = server.accept();
pool.execute(new DaytimeTask(connection));
} catch (IOException e) {
}
}
} catch (IOException e) {
System.err.println("服务器意外退出");
}
}
private static class DaytimeTask implements Runnable{
private Socket connection;
DaytimeTask(Socket connection){
this.connection = connection;
}
@Override
public void run(){
try(Writer out = new OutputStreamWriter(
connection.getOutputStream(),
"UTF-8")){
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用方法和输出结果和实例1基本相同。
注意点有以下几个:
- 多线程的情况下,客户端Socket的关闭应当由处理这个连接的线程完成,因为服务器不知道应当什么时候关闭。
日志(暂缺)
ServerSocket的其他构造器
public ServerSocket() throws IOException
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog)
public ServerSocket(int port, int backlog, InetAddress bindAddr)
port指要绑定的端口;如果传入0,系统会自动选择一个可用的端口;
backlog指等待连接队列的最大长度;
bindAddr指一个特定的本地IP地址。默认情况下,如果一个主机有多个网络接口或IP地址,ServerSocket会在每个接口和IP地址上监听。如果设置了bindAddr,那么就只会监听这个地址上的入站连接。
无参构造器在创建对象时不会绑定端口,可以在之后使用bind()方法进行绑定。
性能优先级
利用setPerformancePreferences(int connectionTime,int latency,int bandwidth)可以设置服务器各项性能指标的优先级。例如:setPerformancePreferences(2,1,3)表示最大带宽是最重要的性能,最小延迟最不重要,连接时间居中。
HTTP服务器
构建一个HTTP服务器的关键在于实现HTTP协议,即根据HTTP协议读取客户端的请求报文,并构造出对应的响应报文。下面是一个简单的单文件服务器,它无论接收到什么请求都返回200 OK,并传输一个指定文件。
实例3:单文件HTTP服务器
public class SingleFileHTTPServer {
private final byte[] content;
private final byte[] header;
private final int port = 80;
private final String encoding = "UTF-8";
public SingleFileHTTPServer(String data,String mimeType) {
this(data.getBytes(), mimeType);
}
public SingleFileHTTPServer(byte[] data, String mimeType){
this.content = data;
//响应报文首部
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(10);
try(ServerSocket server = new ServerSocket(port)){
while(true){
try{
Socket connection = server.accept();
pool.execute(new HTTPHandler(connection));
}catch (IOException e) {
e.printStackTrace();
}catch (RuntimeException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private class HTTPHandler implements Runnable{
private final Socket connection;
HTTPHandler(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try{
OutputStream out =
new BufferedOutputStream(connection.getOutputStream());
InputStream in =
new BufferedInputStream(connection.getInputStream());
StringBuilder request = new StringBuilder(80);
while(true){
int c = in.read();
if(c == '\r' || c == '\n' || c == -1){
break;
}
request.append((char)c);
}
System.out.println(request.toString());
if(request.toString().indexOf("HTTP/") != -1){
out.write(header);
}
out.write(content);
out.flush();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//客户端测试用,会保存服务器发来的文件
public static void test(){
try {
URL url = new URL("http://127.0.0.1");
URLConnection connection = url.openConnection();
InputStream in = new BufferedInputStream(connection.getInputStream());
int total = Integer.parseInt(connection.getHeaderField("Content-length"));
File file = new File("C:\\Users\\qwer\\Desktop"
+ File.separator + "123.pdf");
try(FileOutputStream out = new FileOutputStream(file)){
byte[] bytes = new byte[64 * 1024];
int size;
int len = 0;
while(len < total){
size = in.read(bytes);
out.write(bytes, 0, size);
len += size;
System.out.println(len);
}
out.flush();
System.out.println("finished");
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
String filepath = "C:\\Users\\qwer\\Desktop\\osu\\漫画数据库.pdf";
Path path = Paths.get(filepath);
byte[] data = Files.readAllBytes(path);
//用于获取文件资源的MIME类型
String contentType = URLConnection.getFileNameMap().getContentTypeFor(filepath);
SingleFileHTTPServer server = new SingleFileHTTPServer(data, contentType);
server.start();
} catch (IOException e) {
e.printStackTrace();
}
// test();
}
}