ServerSocket
用法詳解
在B/S通信模式中,服務端需要創建監聽特定端口的ServerSocket
,ServerSocket
負責接收客戶的連接請求。
構造ServerSocket
serverSocket
的構造函數有四種
ServerSocket() throws IOException
ServerSocket(int port) throws IOException
ServerSocket(int port, int backlog) throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
其中參數port用來綁定端口號(即服務器監聽的端口),
參數backlog顯示設置連接請求隊列的長度,當服務器運行時候,會監聽多個客戶的連接請求,當服務器端收到多個連接請求,操作系統會把這些連接請求存儲到一個先進先出的隊列中。許多操作系統限定了隊列的最大長度,一般爲50。
參數bindAddr
,當主機有多個IP
地址(多網卡)的時候,就需要顯示指定那個IP
地址。
上面有一個默認構造方法,它的作用是,允許服務器在綁定到特定的端口之前,先設置ServerSocket的一些選項,因爲一旦服務器與特定的端口綁定,有些選項就不能再改變了。
例如,可以先設置ServerSocket的SO_REUSEADDR的選項爲true,然後再把它與8888端口綁定
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8888));
serverSocket
選項
SO_TIMEOUT
:等待客戶連接的超時時間SO_REUSEADDR
:表示是否允許重用服務器所綁定的地址,booleanSO_RCVBUF
:表示接受數據的緩衝區的大小
SO_TIMEOUT表示serverSocket
的accept()方法等待客戶端連接的超時時間,以毫秒爲單位。如果SO_TIMEOUT的值爲0,表示永遠不會超時,這是SO_TIMEOUT的默認值。accept方法會一直阻塞直到有客戶端連接,方法才返回,或者超出了超時時間,那麼accept方法會拋出SocketTimeoutException
.
SO_REUSEADDR
=false,當serverSocket
關閉時,如果網絡上還有發送到這個ServerSocket
的數據,這個ServerSocket
不會立刻釋放本地端口,而是會等待一段時間,確保接收到了網絡上發送過來的延遲數據,然後再釋放端口。許多服務器程序都使用固定的端口,當服務器關閉時,它的端口可能還會被佔用一段時間.如果立刻釋放端口,釋放的端口可能會立刻連上新的應用程序,這樣存活在網絡中的TCP報文會與新的TCP連接報文衝突,造成數據衝突。需要耐心等待網絡中老的TCP連接的活躍報文全部消失,2MSL
時間可以滿足要求,這裏涉及到TCP的四次揮手。
單線程Socket實例
創建ServerSocket
服務器
public class Server {
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8888);//監聽8888端口
while(true) {
System.out.println("socket已連接,等待客戶端請求...");
Socket socket = serverSocket.accept();
new WebAction(socket).service();//獲取請求內容並響應
}
}
}
WebAction
類用來獲取請求的內容並響應給瀏覽器
public class WebAction {
private Socket socket;
private BufferedReader bufferedReader=null;
private BufferedWriter bufferedWriter=null;
public WebAction(Socket socket) {
this.socket=socket;
}
public void service() {
try {
readRequest(socket);
writeHtml(socket);
close();
} catch (IOException e) {
}
}
public void readRequest(Socket socket) throws IOException {
StringBuffer sb=null;
String temp=null;
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//讀取請求的內容
sb=new StringBuffer();
System.out.println("---------------------");
while((temp=bufferedReader.readLine())!=null) {//注意這裏,如果客戶端不關閉,服務器就會一直等待
sb.append(temp);
System.out.println(temp);
}
}
public void writeHtml(Socket socket) throws IOException {
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
StringBuffer sb=new StringBuffer();
sb.append("http/1.1 200 ok").append("\n\n");
sb.append("success");
bufferedWriter.write(sb.toString());
bufferedWriter.flush();
bufferedWriter.close();
}
public void close() {
try {
bufferedReader.close();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
啓動服務器,並在瀏覽器中輸入 http://localhost:8888/app?name=zhangsan ,發現請求的內容服務器已經獲取到了
但是瀏覽器一直在等待,這是怎麼回事呢
原來如果客戶端打開一個輸入流,如果不做約定,也不關閉它,那麼服務端永遠不知道客戶端是否發送完消息,那麼服務端就會一直等待下去,直到讀取超時。所以怎麼告訴服務端已經發送完消息就很重要。
socket判斷髮送完成的方式
-
方法一:Socket關閉,當socket關閉,服務端會接收到響應的關閉信號,那麼服務端就知道流已經關閉了,這個時候讀操作完成。但是這種方法太暴力,而且socket關閉後,客戶端將不能接收服務器發送的消息,也不能再次發送消息了。如果客戶端想再次發送消息,需要重新創建Socket連接。
-
方法二:客戶端發送完成後,調用
socket.shutdownOutput()
而不是
outputStream.close() //如果關閉了輸出流,那麼相應的socket也會關閉,和直接調用socket.close是一樣的。
這種方法優點是發送完成之後可以接收服務端返回的數據,缺點是也不能再次發送了。
- 方法三:客戶端與服務器約定符號
例如,客戶端在發送消息的最後加上一個“end”標誌,當服務端讀取到該標誌,就知道客戶端已經發送完成了。但這種方法容易引起誤操作。
- 方法四:客戶端指定長度
可以採用計算機界普遍採用的,先通過前面幾個字節來說明後面跟隨的消息的長度。例如0xxxxxxx
表示第一個字節表示內容的長度,內容最大是128個字節,即128B
話說回來,通過瀏覽器訪問socket服務器,服務器如何知道發送結束了呢?因爲瀏覽器採用的是http協議,HTTP請求包括了一下內容:一個請求行、若干個請求頭、實體內容,以GET請求爲例,請求格式如下圖所示
從上圖可知,get請求的最後會有一個空行,我們可以以此作爲get請求發送結束的標誌。
只要修改一行代碼即可
public void readRequest(Socket socket) throws IOException {
StringBuffer sb=null;
String temp=null;
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
sb=new StringBuffer();
System.out.println("---------------------");
while((temp=bufferedReader.readLine())!=null && !"".equals(temp)) {//判斷get請求是否結束
sb.append(temp);
System.out.println(temp);
}
}
但是上面的例子是單線程的,如果同時有多個請求,那隻能排隊等待,這顯然不太合適,下面我們就改造成多線程的Socket服務器,每當有新的請求,就分配一個線程給該請求。
多線程服務器
爲了讓服務器能夠同時爲多個客戶提供服務,提高服務器的併發能力,可以創建一個線程池,每次從線程池中取出工作線程爲客戶服務。
public class Server {
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8888);
ExecutorService executorService = Executors.newCachedThreadPool();
while(true) {
System.out.println("socket已連接,等待客戶端請求...");
final Socket socket = serverSocket.accept();
// 每個請求分配一個線程
executorService.execute(new Runnable() {
public void run() {
new WebAction(socket).service();//獲取請求內容並響應
}
});
}
}
}