ServerSocket的生命周期
一个ServerSocket的基本生命周期:
1)使用一个ServerSocket构造函数在一个特定端口创建一个新的ServerSocket
2)ServerSocket使用accept方法监听这个端口的入站连接,accept方法会一直阻塞,直到一个客户端尝试建立连接,此时accept将返回一个连接客户端和服务器的Socket对象
3)根据服务器类型,会调用Socket的getInputSteam或getOutputStream方法,或者两个方法都调用,以获取与客户端通信的输入和输出流
4)服务器和客户端根据已协商的协议交互,直到要关闭连接
5)服务器或客户端(或二者)关闭连接
6)服务器返回到步骤2,等待下一次连接
异常
有两类异常,一类异常可能关闭服务器并记录一个错误信息,另一类异常只关闭活动连接,区分这两类异常非常重要。某个特性连接范围内的异常会关闭这个连接,但是不会影响其他异常或者关闭服务器。
单个请求范围之外的异常可能会关闭服务器。
结束处理时,一定要关闭Socket,客户端不能依赖连接的另一端关闭Socket,对于服务器尤其如此。客户端可能超时或崩溃,用户可能取消事务,网络可能在流量高峰期间瘫痪,黑客可能发动拒绝服务攻击。出于诸如此类的众多原因,你不能依赖于客户端关闭Socket,即使协议有这个要求也不能完全相信客户端一定会关闭Socket。
请求队列
操作系统把指向某个特定端口的入站连接请求存储在一个先进先出的队列中,默认地,Java将这个队列的长度设置为50,但不同的操作系统会有所不同。FreeBSD默认最大队列长度为128。在这些系统中,Java服务器socket的队列长度将是操作系统所允许的最大值(小于等于50)。队列中填入的未处理连接达到最大容量时,主机会拒绝这个端口上额外的连接,直到队列腾出新的位置出来为止。很多客户端在首次连接被拒绝后还会多次尝试建立连接。
如果默认长度不够大,一些ServerSocket的构造函数还允许改变这个队列的长度,不能不能超过操作系统支持的最大值。
package network.serversocket;
import java.net.*;
import java.io.*;
import java.util.Date;
public class DaytimeServer {
public final static int PORT = 13;
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try (Socket connection = server.accept()) {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() +"\r\n");
out.flush();
connection.close();
} catch (IOException ex) {}
}
} catch (IOException ex) {
System.err.println(ex);
}
}
}
不管怎样,我们都希望能够比新连接到来的速度更快地清空队列。
每个连接对应一个线程
解决方法是为每个连接提供它自己的一个线程,与接入站连接放入队列的那个线程分开。生成一个线程来处理每个入站的连接,这样可以防止一个慢客户端阻塞所有其他客户端,这种是“每个连接对应一个线程”的设计。
package network.serversocket;
import java.net.*;
import java.io.*;
import java.util.Date;
public class ThreadPerConnectionDemo {
public final static int PORT = 13;
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
Thread task = new DaytimeThread(connection);
task.start();
} catch (IOException ex) {}
}
} catch (IOException ex) {
System.err.println("Couldn't start server");
}
}
private static class DaytimeThread extends Thread {
private Socket connection;
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 (IOException ex) {
System.err.println(ex);
} finally {
try {
connection.close();
} catch (IOException e) {
// ignore;
}
}
}
}
}
连接池的版本:
package network.serversocket;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
public class PooledDaytimeServer {
public final static int PORT = 13;
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
Callable<Void> task = new DaytimeTask(connection);
pool.submit(task);
} catch (IOException ex) {}
}
} catch (IOException ex) {
System.err.println("Couldn't start server");
}
}
private static class DaytimeTask implements Callable<Void> {
private Socket connection;
DaytimeTask(Socket connection) {
this.connection = connection;
}
@Override
public Void call() {
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() +"\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
} finally {
try {
connection.close();
} catch (IOException e) {
// ignore;
}
}
return null;
}
}
}
优雅地关闭socket
程序员通常会在一个try-finally块中采用close-if-not-null模式了来关闭Socket,可以使用无参构造器来稍加改进,无参构造器不会抛出任何异常,也不绑定到一个端口,因此构造放在try外边,finally里头直接close。
ServerSocket server = new ServerSocket();
try {
SocketAddress address = new InetSocketAddress(port);
server.bind(address);
// ... work with the server socket
} finally {
try {
server.close();
} catch (IOException ex) {
// ignore
}
}
或者直接采用Java7的AutoCloseable模式:
try (ServerSocket server = new ServerSocket(port)) {
// ... work with the server socket
}catch (IOException ex) {
logger.log(Level.SEVERE, "Could not start server", ex);
}
如果要记录异常信息,可以catch下异常,否则可以不用catch。
要测试ServerSocket是否打开:
public static boolean isOpen(ServerSocket ss) {
return ss.isBound() && !ss.isClosed();
}
isBound方法有歧义,它是表示是否曾经绑定到某个端口,即使它目前已经关闭,isBound仍然会返回true,因此还需要判断是否是close。
构建ServerSocket
ServerSocket httpd = new ServerSocket(80, 50);
这里指定了绑定80端口,同时队列一次最多可保存50个入站连接,如果试图设置超过操作系统的最大队列长度,则会使用最大队列长度。
ServerSocket server = new ServerSocket(0);
0表示监听未指定的端口,操作系统会为你选择可用的端口,称之为匿名端口。
如果没有绑定任何端口,则getLocalPort返回-1。
ServerSocket选项
1)SO_TIMEOUT
为0的话,表示永不超时,一般服务端都设置为永不超时。客户端则要指定。
设置该值必须在调用accppt之前。
2)SO_REUSEADDR
确定是否允许一个新的Socket绑定到之前使用过的一个端口,而此时可能还有一些发送到原来Socket的数据正在网络上传输。
3)SO_RCVBUF
设置了服务器Socket接受的客户端Socket默认接收缓冲区的大小。
setReceiveBufferSize来设置,必须在accept之前设置。
4)服务类型
为TCP定义了4个通用的业务流模型:
A、低成本
B、高可靠性
C、最大吞吐量
D、最小延迟
可以使用setPerformancePreferences方法描述