socket簡單案例實現
socket簡單案例實現
終於還是吃了自己的狗糧......
關於 客戶端-服務端 網路模型
常規情況下,網路應用都會存在客戶端和伺服器端,比如平時外賣應用一樣,我們在外賣應用上的操作,都對應著客戶端應用想伺服器發起請求,並收到響應的過程。伺服器為客戶端提供業務資料支援,客戶端則為使用者提供互動介面。
在網路程式設計中,具體到客戶端 - 伺服器模型時,我們經常會考慮是使用TCP還是UDP,其實它們二者的區別也很簡單:在TCP中連線是誰發起的,在UDP中報文是誰傳送的。在TCP中,建立連線是一個非常重要的環節。區別出客戶端和伺服器,本質上是因為二者程式設計模型是的不同的。
伺服器端需要在一開始就監聽在一個確定的埠上,等待客戶端傳送請求,一旦有客戶端建立連線,伺服器端則會消耗一定的計算機資源為它服務。
客戶端相對簡單,它向伺服器的監聽埠發起請求,連線建立之後,通過連線通路和伺服器端進行通訊。
還有一點要強調的是,無論是客戶端還是伺服器端,它們執行的基本單位都是程序(Process),而不是機器。一個客戶端,可以同時建立多個到不同伺服器的連線;而伺服器更是可能在一臺機器上部署執行多個服務。
什麼是 socket
socket是一種作業系統提供的程序間通訊機制。這裡並不侷限於本地,可以是本地程序間的通訊,也可以是遠端程序間的通訊。在作業系統中,通常會為應用程式提供一組應用程式介面(API),稱為套接字介面(socket API)。應用程式可以通過套接字介面來使用套接字(socket),已進行資料交換。
這裡要注意一下,我們常說的TCP和UDP只是傳輸層協議,是一種約定。TCP三次握手則是基於TCP協議建立網路通路,該通路的具體建立與實現還是socket完成。socket是我們用來建立連線、傳輸資料的唯一途徑。
如何使用 socket 建立連線
通過前面的客戶端 - 伺服器模型,我們知道至少需要一對套接字才能進行網路連線的建立,它們分別是服務端套接字和客戶端套接字,這裡我們先從服務端說起。
服務端準備連線過程
- 建立套接字(我們這裡會使用
TCP
的實現) - 繫結監聽地址:即為繫結需要監聽的
IP地址
以及埠號
,這裡也可以使用本機IP
,但是考慮到部署環境IP
可能會發生變化,所以這裡需要進行IP地址
IP
)。如果不顯式的指定埠號,就意味著把埠的選擇權交給作業系統核心來處理,作業系統核心會根據一定的演算法選擇一個空閒的埠,完成套接字的繫結。 - 開啟套接字監聽模式:bind函式只是實現套接字與地址的關聯,如同登記了電話號碼,如果要讓別人打通帶年華,還需要我們把電話裝置接入電話線,讓伺服器真正處於可接聽的狀態,這個過程需要依賴
listen
函式。這裡可以這麼理解,socket
存在主動
和被動
模式,比如伺服器就是處於被動
模式下,它需要等待客戶端套接字的主動
連線。而listen
函式便是可以將套接字設定為被動
模式,即告訴核心:“我這個套接字是用來等待使用者請求的”。 - 建立連線(
accept
阻塞):在客戶端連線請求到達時,服務端應答成功,便完成連線建立。
package com.zhoujian.socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 執行緒池工具類
* @author zhoujian
*/
public class ExecutorServicePool {
/**
* 初始化執行緒池
*/
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
}
package com.zhoujian.socket.server;
import com.zhoujian.socket.ExecutorServicePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Socket服務端示例
* @author zhoujian
*/
public class SocketServer {
/**
* 監聽埠
*/
private static int PORT = 8088;
private ServerSocket serverSocket;
private static Logger logger = LoggerFactory.getLogger(SocketServer.class);
public static void main(String[] args) {
try {
new SocketServer().startUp(PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
private void startUp(int port) throws IOException {
/**
* 在初始化的過程中先後完成了監聽地址繫結和 listen 函式呼叫
*/
serverSocket = new ServerSocket(port);
logger.info("Socket Server is online, listening at port {}", PORT);
while (true){
/**
* 此處阻塞,等待客戶端連線,在三次握手成功完成後,釋放阻塞
*/
Socket socket = serverSocket.accept();
logger.info("socket port is {} connect successful", socket.getPort());
ExecutorServicePool.executorService.execute(new AnswerThread(socket));
}
}
/**
* 應答執行緒
*/
static class AnswerThread implements Runnable {
private Socket socket;
public AnswerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
String content = null;
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
/**
* 這裡的判斷條件就是資料傳送完畢的標識
*/
while ((content = bufferedReader.readLine()) != null){
logger.info("form client: {}", content);
socket.getOutputStream().write(content.getBytes());
socket.getOutputStream().write("\n".getBytes());
socket.getOutputStream().flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客戶端發起連線過程
- 建立套接字
- 使用
connect
發起連線:呼叫connect
函式向服務端發起連線請求,這裡傳入的是服務端套接字的地址,connect
函式可以看做是將套接字轉換為主動
模式。這裡值得注意的是,客戶端在呼叫connect
函式前不是非得呼叫bind
函式,因為如果需要(TCP|UDP|本地 套接字)的話,作業系統核心會確定源IP地址,並按照一定的演算法選擇一個臨時埠作為源埠(這裡是客戶端,服務端應當先完成地址繫結,因為需要一個穩定的地址進行標記,客戶端大可不必,還可以減小埠衝突的可能)。在這裡我們使用的是TCP套接字
,在呼叫connect
函式時將激發TCP的三次握手
,貼圖如下(圖片來自於極客時間
),這裡務必注意阻塞狀態的改變情況:
注意:Read方法也是阻塞的,當呼叫Read方法是,它就會一直阻塞在那裡,直到另一方告訴它資料已經發送完畢(一般情況下,都會使用長度進行控制,這裡是採用`\n`來作為資料完結髮送的標識)
package com.zhoujian.socket.client;
import com.zhoujian.socket.ExecutorServicePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.Socket;
/**
* Socket客戶端示例
* @author zhoujian
*/
public class SocketClient {
/**
* 服務端套接字IP地址
*/
private static String HOST = "127.0.0.1";
/**
* 服務端套接字監聽埠
*/
private static int PORT = 8088;
private static Logger logger = LoggerFactory.getLogger(SocketClient.class);
/**
* 於主執行緒中初始化客戶端套接字,並完成與服務端套接字的連線
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
Socket client = new Socket(HOST, PORT);
ExecutorServicePool.executorService.execute(new ReceiveThread(client));
BufferedReader reader = new BufferedReader(new
InputStreamReader(System.in, "UTF-8"));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
while (true){
String msg = reader.readLine();
writer.write(msg);
writer.write("\n");
writer.flush();
}
}
/**
* 用於接收服務端套接字的應答
*/
static class ReceiveThread implements Runnable{
private Socket socket;
public ReceiveThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
String receive = null;
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
while ((receive = bufferedReader.readLine()) != null){
logger.info("from server: {}", receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
執行截圖
服務端套接字啟動
客戶端A套接字啟動
客戶端B套接字啟動
客戶端A套接字與服務端套接字通訊
客戶端B套接字與服務端套接字通訊
拓展
待更新......
引用
說明
本節內容涉及的完整程式碼地址:socket-example
本文內容大部分源自極客時間以及網路部落格圖文內容節選,如有冒犯,還請告知我進行處理
郵箱:[email protected]