文章目錄
1. 前言
基於傳輸層協議TCP的網絡通信是可靠的、有序的、差錯控制的。TCP是面向連接的、可靠的流服務協議。TCP協議中,只有實現連接的雙發纔可以進行通信,因此廣播和多播不是基於TCP的。下面首先介紹一些Socket和ServerSocket。
服務器、客戶端建立通信的過程如下:
- 服務器網絡接口的IP地址爲10.1.1.1。服務器端應用程序在80端口運行服務,建立ServerSocket,並監聽有無連接請求。
- 客戶端必須知道服務器的IP地址或主機名、服務開放端口,客戶端向服務器的服務Socket(10.1.1.1:80)發起請求。
- 服務器一旦接收該請求,就會在80端口上建立一個Socket,該Socket的本地地址爲10.1.1.1:80,外部地址爲客戶端建立的Socket(client端IP地址,client端port)。客戶端port通常由操作系統指定。
- 服務器每接收一個客戶端的請求,就會在服務端口上建立一個Socket,這些Socket的本地地址相同,但外部地址各不相同。每個外部地址即各個客戶端的本地Socket。這樣服務器既可以監聽服務請求,又可以和每個客戶端進行獨立的通信。
- 客戶端通過本地Socket與服務器進行通信、讀寫信息。就像打電話一樣,一個號碼呼叫另一個號碼,一旦被呼叫者接聽,通信鏈路就建立起來了,雙方就可以通話了。
2. Socket類
2.1 Socket的通信過程
2.2 控制Socket連接
- 設置客戶端的地址和端口
Socket socket = new Socket(InetAddress.getByName("10.1.1.1")),8888,InetAddress.getByName("10.1.1.100"),12345);
- 綁定本地地址
try{
socket = new Socket();
socket.bind(new InetSocketAddress("10.1.1.100", 12345));
scoket.connect(new InetSocketAddress("10.1.1.1", 8888));
}catch(){
}
- 連接遠程服務器
見上一步,使用connect()方法去連接遠程服務器
- 設置Socket建立連接的超時時間
SocketAddress socketAddress = new InetSocketAddress("10.1.1.1", 8888);
//創建Socket
Socket socket = new Socket;
//連接超時時間設置爲10s
socket.connect(socketAddress, 10000);
- 獲取Socket的輸入流和輸出流
服務端和客戶端之間通信的Socket一旦連接,就會通過一對輸入輸出流進行通信。如同打電話時,一旦被叫放接聽,雙方通話的業務信道建立,就可以交流了。
Socket的getInputStream()方法和getOutputStream()方法分別實現獲得輸入流和輸出流。
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader br = new BufferedReader(new IntputStreamReader(socket.getInputStream()));
- 獲取Socket的地址端口信息
如果需要獲得本地或者遠程Socket的地址和端口信息,可以使用以下方法:
getInetAddress() //獲得遠程服務器的IP地址
getPort() //獲得遠程服務器的端口
getLocalAddress() //獲得客戶端本地的IP地址
getLocalPort() //獲得客戶端本地的端口
- 關閉Socket
當客戶端與服務器的通信結束時,應該關閉Socket,以釋放Socket佔用的包括InputStream和OutputStream在內的各種資源。
if(!s.isClosed()) s.close();
//判斷如果不是空的話則關閉
- Socket的狀態
Socket類提供了3中方法(isBound 、isClosed 和 isConnected)來判斷Socket的狀態。
isBound方法用來判斷Socket的綁定狀態,只要曾經綁定過,即使Socket已經關閉,仍然返回true。可以理解爲本地是否曾經建立過到遠程主機的Socket連接。
isClosed方法用來判斷Socket是否已經關閉。
isConnected方法用來判斷Socket的連接狀態。和isBound方法一樣,即使Socket已經關閉,仍然返回true,isConnected的狀態不清楚。可以理解爲到遠程主機的Socket是否曾經連接過。
2.3 設置Socket的選項
字段 | 含義 |
---|---|
TCP_NODELAY | 表示不要延遲和緩存 |
SO_LINGER | 表示當執行Socket的close方法時,是否立即關閉底層的socket |
SO_REUSEADDR | 表示是否允許綁定Socket,即使之前的連接處於timeout狀態 |
SO_SNDBUF | 表示輸出數據的緩衝區的大小 |
SO_RCVBUF | 表示接收數據的緩衝區的大小 |
SO_KEEPALIVE | 用來設置長時間處於空閒狀態的Socket |
SO_OOBINLINE | 表示是否支持發送一字節的TCP緊急數據 |
SO_TIMEOUT | 表示Socket的輸入流等待數據的超時時間 |
3. ServerSocket類
3.1 構造ServerSocket
- ServerSocket() throws IOException : 創建未綁定的服務器套接字
- ServerSocket(int port) throws IOException:創建綁定到特定端口的服務器套接字
- ServerSocket(int port, int backlog) throws IOException :創建綁定到特定端口的服務器套接字,並且連接請求隊列的長度由backlog來設置
- ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException:創建綁定到特定端口的服務器套接字,並且連接請求隊列的長度由backlog來設置,指定本地要綁定的IP地址bindAddr,一臺服務器可能有多個網絡接口,如果設置了bindAddr,則服務器只接收到該地址的連接請求。
3.2 ServerSocket的常用方法
- 綁定端口
ServerSocket serverSocket = new ServerSocket(8000);
ServerSocket 對象創建時,如果使用無參構造方法,通常之後會做一些選項的設置,再來進行端口的綁定,則需要使用bind方法。
- 設置連接請求隊列的長度
int queue = 100;
serverSocket.bind(s, queue);
若沒有設置,由操作系統指定
- 獲取ServerSocket的地址端口信息
- getInetAddress() : 獲得服務器的本地IP地址
- getLocalPort() : 獲得服務器本地監聽端口
- getLocalSocketAddress() : 獲得服務器的本地IP地址和監聽端口
-
關閉ServerSocket
ServerSocket 建立之後,可以顯示地關閉它。同Socket一樣,使用close方法關閉,當關閉時,如果隊列中有尚未accept的客戶端請求,則會觸發SocketException。 -
接收客戶端的連接請求
ServerSocket的accept方法監聽客戶端的連接請求並且接收請求。 -
ServerSocket的狀態
ServerSocket 類提供了兩種方法(isBound、isClosed)來判斷Socket的狀態。具體可參考Socket類的相應方法。
3.3 設置ServerSocket選項
字段 | 含義 |
---|---|
SO_TIMEOUT | 表示等待客戶端連接的超時時間 |
SO_REUSEADDR | 表示是否允許重複綁定Socket處於timeout狀態的端口 |
SO_RCVBUF | 設置接收數據的緩衝區的大小 |
4. 基於TCP的BIO通信
4.1 實現功能
服務器創建服務並監聽端口,與客戶端建立連接,並向客戶端轉發消息,客戶端輸入quit命令即可退出。
4.2 程序源代碼
Server類:
package com.company.bio;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args){
final String QUIT = "quit";
final int DEFAULT_PORT = 8888;
ServerSocket serverSocket = null;
//綁定監聽端口
try{
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("啓動服務器,監聽端口"+DEFAULT_PORT);
while (true){
// 等待客戶端連接
Socket socket = serverSocket.accept();
System.out.println("客戶端[" + socket.getPort() + "]已連接");
//創建IO流
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
String msg = null;
while ((msg = bufferedReader.readLine()) != null) {
// 讀取客戶端發送的消息,當對方關閉時返回null
System.out.println("客戶端["+socket.getPort()+"]:"+ msg);
//回覆客戶發送的消息
bufferedWriter.write("服務器:" + msg + "\n");
bufferedWriter.flush(); //保證緩衝區的數據發送出去
//查看客戶端是否退出
if(QUIT.equals(msg)){
System.out.println("客戶端["+socket.getPort()+"]已退出");
break;
}
}
}
}catch (IOException e){
e.printStackTrace();
}finally {
if (serverSocket != null){
try {
serverSocket.close();
System.out.println("關閉ServerSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Client類:
package com.company.bio;
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
final String QUIT = "quit";
final String DEFAULT_SERVER_HOST = "127.0.0.1";
final int DEFAULT_SERVER_PORT = 8888;
Socket socket = null;
BufferedWriter bufferedWriter = null;
try{
// 創建socket
socket = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
//創建IO流
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
//等待用戶輸入信息
BufferedReader consolReader = new BufferedReader(
new InputStreamReader(System.in)
);
while (true) {
String input = consolReader.readLine();
//發送消息給服務器
bufferedWriter.write(input + "\n");
bufferedWriter.flush();
//讀取服務器返回的消息
String msg = bufferedReader.readLine();
System.out.println(msg);
//查看用戶是否退出
if(QUIT.equals(input))break;
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(bufferedWriter != null){
try {
bufferedWriter.close();
System.out.println("關閉socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.3 程序效果截圖
Client:
Server: