第三章、ServerSocket用法简解
由于文章总结的内容太长,看起来很不方便,所以现在开始总结可能会常用的知识点。
ServerSocket是服务器端负责接受客户连接请求的类。
3.1 构造ServerSocket
backlog参数是用来设定客户连接队列的长度,即允许几个客户端连接请求缓存个数。
在以下情况下会采用操作系统限定的队列长度:
1)、backlog参数的值大于操作系统限定的队列最大长度
2)、backlog参数的值小于或等于0
3)、在ServerSocket构造方法中没有设置backlog参数
如果我们设立了backlog参数值为3,当我们用客户端进行服务器端连接请求时,没有运行ServerSocket对象的accept()方法,这种情况下,如果连接请求到了3个以上时,程序就会抛异常,因为连接队列里面已经满了,而且我们没有用accept方法从请求队列中取出连接。所以我们一般把accept方法写到while循环里,这样不出问题的话,会一直接受客户端的连接请求。
3.2 接受和关闭与客户端的连接
3.3 关闭ServerSocket
3.4 获取ServerSocket信息
3.5 ServerSocket选项
3.6 创建多线程的服务器
1).能同时接受并处理多个客户连接;
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 创建线程池
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方法来等待线程数组中每一个线程的终止。
下面用一个例子来调用以上线程池
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 使用线程池需要注意的事项
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,则关闭服务器端。