在之前的文章《網路程式設計雜談之TCP協議》中,我們闡述了TCP協議的基本概念,TCP作為一種可靠的、面向連線的資料傳輸協議,確保了資料在傳送和接收之間的可靠性、順序性和完整性,特點可以概括如下:
1、面向連線:在進行資料傳輸之前,TCP需要客戶端和伺服器之間建立一個連線,這個連線包括一系列的握手和協商步驟,以確保通訊雙方都準備好進行資料傳輸。
2、可靠性:TCP是一種可靠的協議,它使用各種機制來確保資料的可靠傳輸,包括資料分段的確認和重傳機制,以及流量控制等多種手段。
3、順序性:TCP保證資料段的到達順序與傳送順序相同,即使資料在傳輸過程中被拆分成多個資料包,接收方也會將它們按照正確的順序重新組裝,比如說連結的一方發了ABC,那麼接收的一方收到的也一定是ABC。
4、流量控制:TCP使用滑動視窗協議來實現流量控制,確保了傳送方不會以超過接收方處理能力的速度傳送資料,從而避免了資料丟失和網路擁塞。
5、擁塞控制:TCP還具有擁塞控制機制,它可以檢測到網路中的擁塞並採取相應的措施來減輕擁塞,從而實現降低傳送速率和重新傳送丟失的資料包。
6、面向位元組流:TCP是面向位元組流的協議,它不會保留訊息邊界。這意味著接收方需要自行解析和分割接收到的位元組流,以還原原始訊息。
7、可靠的錯誤檢測和糾正:TCP具有強大的錯誤檢測和糾正機制,它可以檢測並糾正在資料傳輸過程中出現的錯誤,以確保資料的完整性。
8、全雙工通訊:TCP支援全雙工通訊,所謂全雙工是指建立連線後,通訊雙方可以同時傳送和接收資料,而不需要等待對方的響應。
9、Socket(套接字):TCP使用埠號來標識不同的應用程式或服務,通訊的兩端透過IP地址和埠號來建立連線,而套接字(Socket)就是對其中任意一端的抽象,分為伺服器端套接字(Server Socket)或客戶端套接字(Client Socket),分別用於伺服器和客戶端的通訊。
總的來說,TCP協議作用於傳輸層且適用於大多數需要可靠資料傳輸的應用程式,如檔案傳輸、上位機通訊等,並可以做為其他應用層協議的實現基礎,如HTTP、MQTT等。
在程式碼實現層面,Socket是指一種程式設計介面(API),不同開發語言基本上都圍繞Socket提供了一組用於建立、連線、傳送和接收資料的API。下面我們以Java為例,透過java.net包下提供的Socket操作API與 java.io包下提供的IO操作API, 實現一個基本的TCP服務端與客戶端的監聽、連結並進行訊息收發的示例。
服務端
在TCP服務端的實現中我們需要先定義一個ServerSocket物件,並實現對指定IP與埠號的繫結與監聽,這裡需要注意的是如果一直沒有客戶端連結,serverSocket.accept會一直處在阻塞狀態,一旦有客戶端連結事件發生才會向下執行,而服務需要滿足與多個客戶端進行連結,這也是為什麼我們需要一個while (true)去一直輪詢執行,因為我們其實是不知道客戶端什麼時候會連結上來的,下面的讀寫操作也是同一個道理, 所以為了不影響主執行緒accept新的客戶端,我們把完成連結的Socket開啟一個獨立的執行緒或者拋給執行緒池來處理後續IO讀寫操作,這是一種典型的BIO(Blocking I/O)即阻塞IO的處理模式。
public class BioServer {
public static void main(String[] args) throws IOException{
ExecutorService executor = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy());
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",9091));//繫結IP地址與埠,定義一個服務端socket,開啟監聽
while (true) {
Socket socket = serverSocket.accept();//這裡如果沒有客戶端連結,會一直阻塞等待,一旦有客戶端連結就會向下執行
executor.execute(new BioServerHandler(socket)); //我們把連結Socket拋給執行緒池
}
}
}
讀寫操作是透過Socket獲取InputStream與OutputStream來完成的,這裡的Input與Output是站在你程式的視角來區分的,所以Input是收,Output是發,同理inputStream.read作為IO讀操作也是阻塞的,程式只有接受到資料時才會向下執行,由於我們不確定Socket連結的讀寫操作何時發生,也只能依靠 while (true)輪詢執行。
public class BioServerHandler implements Runnable{
private final Socket socket;
public BioServerHandler(Socket socket) {
this.socket=socket;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (true) {
byte[] rbytes = new byte[1024];
InputStream inputStream = socket.getInputStream(); //透過IO輸入流接受訊息
int rlength=inputStream.read(rbytes, 0, 1024); //讀操作阻塞,一旦接受到資料向下執行並返回接收到的資料長度
byte[] bytes = new byte[rlength];
System.arraycopy(rbytes, 0, bytes, 0, rlength);
String message = new String(bytes);
System.out.printf("Client: %s%n", message);
PrintStream writer = new PrintStream(socket.getOutputStream()); //透過IO輸出流傳送訊息
writer.println("Hello BIO Client");
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客戶端
public class BioClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 9091));
while (true) {
if (!socket.isConnected()) {
System.out.print("connecting...");
continue;
}
PrintStream writer = new PrintStream(socket.getOutputStream());
writer.write("Hello BIO Server".getBytes(StandardCharsets.UTF_8));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message = reader.readLine();
System.out.printf("Server: %s%n", message);
}
}
}
透過上面程式碼大家能夠對Java下TCP網路程式設計的開發、Socket的操作、BIO(阻塞IO)模型有了基本的瞭解,當然一個完整的TCP服務或客戶端開發需要考慮的問題還有很多,如IO與執行緒模型、協議的制定、連結的管理、應用層報文粘包、半包等等,後續我們會在此基礎上進行進一步的擴充套件與完善。