第三章、ServerSocket用法简解
由于文章总结的内容太长,看起来很不方便,所以现在开始总结可能会常用的知识点。
ServerSocket是服务器端负责接受客户连接请求的类。
3.1 构造ServerSocket
backlog参数是用来设定客户连接队列的长度,即允许几个客户端连接请求缓存个数。
在以下情况下会采用操作系统限定的队列长度:
1)、backlog参数的值大于操作系统限定的队列最大长度
2)、backlog参数的值小于或等于0
3)、在ServerSocket构造方法中没有设置backlog参数
如果我们设立了backlog参数值为3,当我们用客户端进行服务器端连接请求时,没有运行ServerSocket对象的accept()方法,这种情况下,如果连接请求到了3个以上时,程序就会抛异常,因为连接队列里面已经满了,而且我们没有用accept方法从请求队列中取出连接。所以我们一般把accept方法写到while循环里,这样不出问题的话,会一直接受客户端的连接请求。
3.2 接受和关闭与客户端的连接
就如上面所诉的,ServerSocket通过accept方法来从请求队列中取出一个客户的连接请求,然后创建于客户端连接的Socket对象,并且将它返回。如果队列里面没有请求连接的话,那么accept方法就会一直等下去。
当服务器端正在给一个客户端发送数据时,该客户端断开连接了,这是服务器端就会抛出SocketExcepion异常,这是我们不愿意看到的,因为服务器端还和其他客户端进行着连接,不能因为一个客户端的出错导致整个通信网络瘫痪。所以在单线程服务器中,我们一般对该异常进行捕获。
3.3 关闭ServerSocket
ServerSocket的close方法可以使服务器释放占用的端口,并且断开与所有客户端的连接。不过当一个服务器程序运行结束时,即使没有调用该方法,默认的还是会释放服务器占用的端口。
ServerSocket的isClose方法判断ServerSocket是否关闭,只有执行了ServerSocket的close方法,isClosed才会返回true;否则即使ServerSocket还没有和特定的端口号绑定,isClosed方法也会返回false、
另外ServerSocket还提供了isBound方法判断是否已经与一个端口绑定,只要绑定了,即使它已经关闭了,isBound方法也会返回true。
3.4 获取ServerSocket信息
我们在前面的2章中得知,如果在构造函数时给其分配的端口号是0,那么会由操作系统随机分配端口号,例如ServerSocket serverSocket = new ServerSocket(0)。这时候,我们要想得知其端口号就可以用getLocalPort方法了。
不过多数服务器都会监听固定端口,方便客户端进行方法。匿名端口一般用于服务器和客户进行临时通信,通信结束,就断开连接,释放占用的相关资源。FTP就使用了匿名端口。
FTP使用了两个并行的TCP连接,一个是控制连接,一个是数据连接。控制连接用于在客户和服务器之间发送控制信息,如用户名和口令。改变远程目录的命令或上传和下载文件的命令。数据连接用于传送文件。TCP服务器在21端口上监听控制连接,如果有客户要求上传或下载文件,就另外建立了一个数据连接,通过它来传送文件。数据连接有两种建立方式。如下图所示:
1)、
所有的连接均在特定的端口
2)、
客户端和服务器进行数据连接时先创建一个监听匿名端口的ServerSocket,然通过getLocalPort获取端口号发给TCP服务器,然后由TCP服务器主动建立与客户端的连接。当然服务器端也可以使用匿名端口。
3.5 ServerSocket选项
ServerSocket有以下三个选项
1)、SO_TIMEOUT:表示等待客户连接的超时时间
2)、SO_REUSEADDR:表示允许重用服务器所绑定的地址
3)、SO_RCVBUF:表示接受数据的缓冲区的大小
详细用法见Socket那章,其实是类似的。
3.6 创建多线程的服务器
在单线程的情况下,服务器往往不能同时和多个客户同学,服务器给一个客户发送信息时,队列中的其他客户就必须排队等服务器和那个客户的通信结束。
许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接受大量的客户请求,每个客户都希望快速得到HTTP服务器的响应,如果长时间让客户等待,那估计就没人访问了。
可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:
1).能同时接受并处理多个客户连接;
1).能同时接受并处理多个客户连接;
2).对于每个客户,都会迅速给与响应。
为了实现以上的并发,我们可以使用多线程。
1).为每个客户分配一个工作线程.
2).创建一个线程池,由其中的工作线程来为客户服务。
3).利用JDK的java类库中现成的线程池,由它的工作线程来为客户服务。
3.6.1 为每个客户分配一个线程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
private int port = 8000;
private ServerSocket serverSocket;
public EchoServer() throws IOException{
serverSocket = new ServerSocket(port);
System.out.println("服务器已启动");
}
public void service() throws IOException{
while(true){
Socket socket = null;
socket = serverSocket.accept();
Thread workThread = new Thread(new Handler(socket));
workThread.start();
}
}
}
class Handler implements Runnable{
private Socket socket;
public Handler(Socket socket){
this.socket = socket;
}
private PrintWriter getWriter(Socket socket) throws IOException{
PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
return pw;
}
private BufferedReader getReader(Socket socket) throws IOException{
InputStreamReader in = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(in);
return br;
}
public String echo(String msg){
return "echo:"+msg;
}
@Override
public void run() {
System.out.println("new connection accepted"+socket.getLocalAddress()+";"+socket.getLocalPort());
try {
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while((msg = br.readLine())!=null){
System.out.println(msg);
pw.println(echo(msg));
if("bye".equals(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
把跟客户端进行数据接受发送的代码写到一个新的线程的run方法里去,当调用线程的start方法就执行了run方法里面的代码。
3.6.2 创建线程池
以上的多线程的代码多少效率上有些不高,反复的线程创建和销毁所造成的的资源开销太大。所以我们引入了线程池的概念,所谓线程池就是一个池子里事先装入了N个线程,当你要用时就拿一个,不用时就放回来,这样就不会返回的创建和销毁线程。
import java.util.LinkedList;
public class ThreadPool extends ThreadGroup{
private boolean isClosed = false; //线程池是否关闭
private LinkedList<Runnable> workQueue; //表示工作队列
private static int threadPoolID;//表示线程的ID
private int threadID; //表示工作线程的ID
public ThreadPool(int poolSize){ //指定线程池中工作线程的数目
super("ThreadPool-"+(threadPoolID++));
setDaemon(true); //守护线程
workQueue = new LinkedList<Runnable>();
for(int i = 0;i < poolSize;i++){
new WorkThread().start();
}
}
/**向工作队列中加入一个新任务,由工作线程去执行该任务*/
public synchronized void execute(Runnable task){
if(isClosed){ //如果线程池被关闭,则抛出异常
throw new IllegalStateException();
}
if(task != null){
workQueue.add(task);
notify(); //唤醒正在getTask方法中等待人物的工作线程
}
}
/**从工作队列中取出一个任务,工作线程会调用此方法
* @throws InterruptedException */
protected synchronized Runnable getTask() throws InterruptedException{
while(workQueue.size()==0){
if(isClosed){
return null;
}
wait(); //如果工作队列中没有任务,就等待任务
}
return workQueue.removeFirst();
}
/**关闭线程池*/
public synchronized void close(){
if(!isClosed){
isClosed = true;
workQueue.clear(); //清空工作队列
interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup
}
}
/**等待工作线程把所有的任务执行完*/
public void join(){
synchronized (this) {
isClosed = true;
notifyAll(); //唤醒还在getTask方法中等待任务的工作线程
}
Thread[] threads = new Thread[activeCount()];
//enumerate()方法继承自ThreadGroup类,获得线程中当前所有活着的工作线程
int count = enumerate(threads);
for(int i=0; i<count; i++){
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/** 内部类:工作线程*/
private class WorkThread extends Thread{
public WorkThread(){
//加入当前的ThreadPool线程组中
super(ThreadPool.this,"WorkThread-"+(threadID++)); //在线程组中创建线程
}
public void run(){
while(!isInterrupted()){ //判断线程是否被中断
Runnable task = null;
try {
task = getTask();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(task == null) return;
task.run();
}
}
}
}
在ThreadPool类中定义了一个workQueue用来存放线程池要执行的任务,每个任务都是Runnable的实例。ThreadPool类的客户程只要调用ThreadPool中的execute方法就能向线程池提交任务。该方法将任务加到工作队列中,并且唤醒正在等待任务的工作线程。即有了工作任务就通知线程去执行这个任务。工作线程执行完该任务后,再从工作队列中取下一个任务并执行,如果没有就wait。
线程池的join和close都可以关闭线程池,不过join在关闭前会确保工作队列中的任务都执行完毕。join的这一功能具体实现是靠enumerate方法的。
先new一个Thread数组threads,activeCount()方法返回此线程组中活动线程的估计数。然后用enumerate方法来将线程组复制到刚new出来的数组里。
运行结果:
主要的思路是在服务器程序中另外建立一个ServerSocket监听那种类似于管理员权限的客户端发送的消息,如果发送的命令为shutdown,则关闭服务器端。
然后再调用线程的join方法来等待线程数组中每一个线程的终止。
下面用一个例子来调用以上线程池
public class ThreadPoolTester {
public static void main(String[] args) {
int numTasks = 5;
int poolSize = 3;
ThreadPool threadPool = new ThreadPool(poolSize); //创建线程池
//运行任务
for(int i=0; i<numTasks; i++){
threadPool.execute(createTask(i));
}
threadPool.join();
}
private static Runnable createTask(final int i) {
return new Runnable() {
@Override
public void run() {
System.out.println("Task"+i+":start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println("Task"+i+":end");
}
};
}
}
运行结果:
Task1:start
Task2:start
Task0:start
Task2:end
Task3:start
Task1:end
Task4:start
Task0:end
Task3:end
一共5个任务,线程池中线程的个数为3,通过运行其实可以发现最多一次性连续执行的任务数是3,因为线程总数为3,一个线程在没有执行完一个任务时,是没法去执行下一个任务。
JDK自带的类库也提供了一个线程池供我们去使用,在java.util.concurrent包里面。具体用法后面有个例子。
3.6.3 使用线程池需要注意的事项
虽然线程池能大大提高服务器的并发性能,但是使用它会存在一定的风行。
1.死锁
任何多线程应用程序都存在死锁风险。所谓死锁,典型的例子就是线程A占用了资源1,它需要资源2才能继续执行下去,线程B占用了资源2,它需要资源1才能继续执行下去,两个线程都不释放自己占用的资源并且都在等着对方的资源,这样下去就产生了死锁。线程池会导致这种死锁以外的另一种死锁,假定线程池中的所有工作线程都在执行各自的任务时被阻塞,它们都在等待某一个任务A的执行结果,而任务A依然在工作队列中,由于没有空闲的线程去执行任务A,所以线程池中的工作线程都永远阻塞下去了。
2.系统资源不足
如果设计的线程池中线程的数目很多,而且使用效率不高,打个比方如果有100个线程,只需要执行10个任务,这样就严重浪费了资源了。
3.并发错误
如果没有线程池中的wait和notify方法使用不正确,导致notify没有唤醒一个wait线程,这样该工作线程就会一直空闲的wait下去。
4.线程泄漏
工作线程在执行一个任务时被阻塞,如等待用户的输入数据,但由于用户迟迟没有输入数据,导致这个线程一直处于阻塞状态,线程池所能使用的工作线程就会减少。如果很多这样的工作线程没执行完任务处于阻塞状态会导致线程池无法执行新的任务。
5.任务过载
当工作队列中有大量任务排着队,这些任务本身执行会消耗很多系统资源,如果我们在这些任务调度执行时没有一个很好的安排,例如,任务A执行过程中需要任务B的执行结果,我们先把A加了进去就不太合适了。还有任务的种类不同,有些是会经常阻塞的IO操作,有些是执行一直不会阻塞的运算操作。前者时断时续地占用CPU资源,后者对CPU资源利用率更高。所以我们要对任务进行合理的分类,根据任务的不同设置对个工作队列,对其进行不同的调度处理。
3.7 关闭服务器
服务器如何能在恰当的时刻关闭自己是我们所想要实现的。
以下代码是可以关闭自己的一个服务器例子
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
public class EchoServer2 {
private int port = 8000;
private ServerSocket serverSocket;
private ExecutorService executorService; //JDK自带的线程池
private final int POOL_SIZE = 4; //单个CPU时线程池中工作线程的数目
private int portForShutdown = 8001; //用于监听关闭服务器命令的端口
private ServerSocket serverSocketForShutdown;
private boolean isShutdown = false;
private Thread shutdownThread = new Thread(){
public void start(){
this.setDaemon(true);
super.start();
}
public void run(){
while(!isShutdown){
Socket socketForShutdown = null;
try {
socketForShutdown = serverSocketForShutdown.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socketForShutdown.getInputStream()));
String command = br.readLine();
if("shutdown".equals(command)){
long beginTime = System.currentTimeMillis();
socketForShutdown.getOutputStream().write("服务器正在关闭\r\n".getBytes());
isShutdown = true;
//请求关闭线程池
//线程池不再接受新的任务,但是会继续执行完工作队列中现有的任务
executorService.shutdown();
//等待关闭线程池,每次等待的超时时间为30秒
while(!executorService.isTerminated()){
executorService.awaitTermination(30, TimeUnit.SECONDS);
}
serverSocket.close();//关闭与EchoClient客户同学的ServerSocket
long endTime = System.currentTimeMillis();
socketForShutdown.getOutputStream().write(("服务器已经关闭,"+"关闭服务器用了"+(endTime-beginTime)+"毫秒\r\n").getBytes());
socketForShutdown.close();
}else{
socketForShutdown.getOutputStream().write("错误的命令\r\n".getBytes());
socketForShutdown.close();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
public EchoServer2() throws IOException {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(60000); //设定等待用户连接的超时时间为60秒
serverSocketForShutdown = new ServerSocket(portForShutdown);
//创建线程池
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
shutdownThread.start(); //启动负责关闭服务器的线程
System.out.println("服务器启动");
}
public void service(){
while(!isShutdown){
Socket socket = null;
try {
socket = serverSocket.accept();
socket.setSoTimeout(60000);//把等待客户发送数据的超时时间设置为60秒
executorService.execute(new Handler1(socket));
} catch (SocketTimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}catch (RejectedExecutionException e) {
if(socket!=null){
try {
socket.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
e.printStackTrace();
} catch (SocketException e) {
//如果由于在执行serverSocket的accept方法时
//ServerSocket被ShutdownThread线程关闭而导致的异常,就退出service方法
if(e.getMessage().indexOf("socket closed")!=-1)return;
} catch(IOException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new EchoServer2().service();
}
}
class Handler1 implements Runnable{
private Socket socket;
public Handler1(Socket socket){
this.socket = socket;
}
private PrintWriter getWriter(Socket socket) throws IOException{
PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
return pw;
}
private BufferedReader getReader(Socket socket) throws IOException{
InputStreamReader in = new InputStreamReader(socket.getInputStream());
BufferedReader br = new BufferedReader(in);
return br;
}
public String echo(String msg){
return "echo:"+msg;
}
@Override
public void run() {
System.out.println("new connection accepted"+socket.getLocalAddress()+";"+socket.getLocalPort());
try {
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while((msg = br.readLine())!=null){
System.out.println(msg);
pw.println(echo(msg));
if("bye".equals(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class AdminClient {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket("localhost",8001);
//发送关闭命令
OutputStream socketOut = socket.getOutputStream();
socketOut.write("shutdown\r\n".getBytes());
//获取服务器反馈
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = null;
while((msg=br.readLine())!=null){
System.out.println(msg);
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally{
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
主要的思路是在服务器程序中另外建立一个ServerSocket监听那种类似于管理员权限的客户端发送的消息,如果发送的命令为shutdown,则关闭服务器端。