什麼是Socket
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
如下圖所示
我們可以選擇基於TCP或UDP協議進行網絡通信
1. 基於TCP協議進行網絡通信
通過TCP發送數據時,首先要創建連接。建立TCP連接後,TCP保證數據到達另一端,或者它會告訴我們發生了錯誤。
1.1 客戶端
爲了實現通過TCP協議連接到服務器,需要創建一個java.net.Socket並將其連接到服務器。
如下爲連接到ip爲127.0.0.1,端口號爲8000的服務器。ip地址也可以爲域名。
Socket socket = new Socket("127.0.0.1", 8000);
socket.getOutputStream().write("hello world".getBytes());
InputStream inputStream = socket.getInputStream();
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
- 注意:在讀取文件時,不能通過返回-1來判斷是否讀完這個文件。因爲只有在服務器關閉的情況下連接才返回-1。
socket.close();
完整代碼如下:
@Slf4j
public class TCPClient {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 8000);
for (int i= 0;i<100;i++) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
log.info("client發送消息");
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
countDownLatch.await();
}
}
1.2 服務端
爲了實現通過TCP協議來監聽客戶端傳入Java服務器的連接,需要使用到java.net.ServerSocket,當客戶端通過客戶端套接字連接到服務器的ServerSocket時,服務器上會爲該連接分配一個Socket。客戶端和服務器通過Socket-to-Socket方式通信。
如下,創建一個ServerSocket監聽端口8000。
ServerSocket serverSocket = new ServerSocket(8000);
while(true){
Socket socket = serverSocket.accept();
//todo:連接後的操作
}
- 注意:只有當前服務線程調用accept()方法時,客戶端的連接才能被接收,除此之外的任何時間都沒有客戶端可以連接到服務器。所以在接收連接後,一般都將Socket放到工作線程執行後續邏輯。
當客戶端的請求完成,並且不會從該客戶端收到進一步的請求,則需要將Socket關閉。
socket.close();
如果有需要關閉服務:
serverSocket.close();
完整代碼如下:
@Slf4j
public class TCPServer {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
ServerSocket serverSocket = new ServerSocket(8000);
// (1) 接收新連接線程
new Thread(() -> {
while (true) {
try {
// (1) 阻塞方法獲取新的連接
Socket socket = serverSocket.accept();
log.info("service接收消息");
// (2) 每一個新的連接都創建一個線程,負責讀取數據
new Thread(() -> {
log.info("創建線程");
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// (3) 按字節流方式讀取數據
while ((len = inputStream.read(data)) != -1) {
log.info(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
countDownLatch.await();
}
}
2. 基於UDP協議進行網絡通信
使用UDP協議,客戶端和服務器之間沒有連接。客戶端可以向服務器發送數據,並且服務器可以(或可以不)接收該數據。客戶端永遠不會知道數據是否在另一端收到。服務器發送數據到客戶端也是如此。
UDP和TCP端口不一樣。計算機可以具有不同的進程,例如在UDP和TCP中同時偵聽端口8000。
2.1 客戶端
DatagramSocket datagramSocket = new DatagramSocket();
參數data爲此次通信需要發送的數據,inetAddress爲此次通信的節點地址的ip信息,最後一個參數爲端口號。
InetAddress inetAddress = InetAddress.getLocalHost();
byte[] data = "123123123".getBytes();
DatagramPacket datagramPacket = new DatagramPacket(data,data.length,inetAddress,8000);
datagramSocket.send(datagramPacket);
完整代碼如下:
public class UDPClient {
private static DatagramPacket datagramPacket;
private static InetAddress inetAddress;
private static DatagramSocket datagramSocket;
public static void main(String[] args) throws IOException, InterruptedException {
inetAddress = InetAddress.getLocalHost();
datagramSocket = new DatagramSocket();
while (true){
byte[] data = "123123123".getBytes();
datagramPacket = new DatagramPacket(data,data.length,inetAddress,8000);
datagramSocket.send(datagramPacket);
Thread.sleep(2000);
}
}
}
2.2 服務端
監聽8000端口
DatagramSocket datagramSocket = new DatagramSocket(8000);
創建一個緩衝區,接收客戶端發送的數據
byte[] data = new byte[9];
datagramPacket = new DatagramPacket(data,data.length);
接收的數據爲byte數組
byte[] receive = datagramPacket.getData();
System.out.println(new String(receive));
完整代碼如下:
public class UDPServer {
private static DatagramSocket datagramSocket;
private static DatagramPacket datagramPacket;
public static void main(String[] args) throws IOException, InterruptedException {
//UDP和TCP端口不一樣。計算機可以具有不同的進程,例如在UDP和TCP中同時偵聽端口80。
datagramSocket = new DatagramSocket(8000);
while (true){
byte[] data = new byte[9];
datagramPacket = new DatagramPacket(data,data.length);
datagramSocket.receive(datagramPacket);
byte[] receive = datagramPacket.getData();
System.out.println(new String(receive));
}
}
}
後記
Java IO編程,每建立一個連接,就需要提供一個單獨的線程從流中讀取數據。儘管可以把事件放到線程池中處理,但當併發量很大的時候,系統開銷仍然是支撐不住的。
參考鏈接:
Jenkov.com
圖片來源: