第三章、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,則關閉服務器端。