傳輸層協議TCP、UDP,但它們畢竟只是協議,看不見摸不着,那我們怎們通過TCP、和UDP進行實際傳輸呢?不用着急,等看完這篇文章你一定會明白的
Socket概述
Socket中文意思爲插座的意思,專業術語稱之爲套接字,它把TCP/IP封裝成了調用接口供開發者調用,也就是說開發者可以通過調用Socket相關API來實現網絡通訊。在Java中也存在Socket相關API,主要分爲兩個,分別是基於UDP傳輸協議的Socket和基於TCP傳輸協議的Socket,本篇文章會對基於這兩種傳輸協議的Socket進行詳細描述。
UDP Socket
通過上節的內容我們知道UDP是無連接的,只要提供對方的IP地址和端口號就能進行數據的傳輸,其中IP負責定位主機端口負責定位應用。知道了目標IP和目標端口號通過Java中的UDP Socket就能進行IO傳輸,我們來看一下具體的代碼體現
/**
* 發送方UDP
*/
public class UDPSocketSend {
public static void main(String[] args) throws IOException {
System.out.println("Sender Start...");
//1.創建socket服務
DatagramSocket ds = new DatagramSocket();
//2.封裝數據
String str = "Did you recite words today";
byte[] bytes = str.getBytes();
//地址
InetAddress address =InetAddress.getByName("192.168.31.137");
//參數:數據、長度、地址、端口
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);
//3.發送數據包
ds.send(dp);
//4.關閉socket服務
ds.close();
}
/**
* 接收方UDP
*/
public class UDPSocketReceive{
public static void main(String[] args) throws IOException {
System.out.println("Receiver Start...");
//1.創建udp的socket服務,並聲明端口號
DatagramSocket ds = new DatagramSocket(6666);
//2.創建接收數據的數據包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
//3.將數據接收到數據包中,爲阻塞式方法
ds.receive(dp);
//4.解析數據
InetAddress address = dp.getAddress();//發送方IP
int port = dp.getPort();//發送方端口
String content = new String(dp.getData(),0,dp.getLength());
System.out.println("address:"+address+"---port:"+port+"---content:"+content);
//關閉服務
ds.close();
}
}
分別啓動發送方和接收方,我們來看一下打印結果
發送方:
Sender Start...
接收方:
Receiver Start...
address:/192.168.31.137---port:65037---content:Did you recite words today
成功接收到消息,並打印出發送方IP和端口,下面我來個大家擼一遍步驟
發送方:
首先創建udp的socket服務
將需要發送的數據放在數據包DatagramSocket中,DatagramSocket會根據UDP協議對數據包、IP、端口號進行封裝
通過udp的socket服務將數據包發送
最後將udp服務關閉
接收方:
創建udp的socket服務,並且明確自己的端口號
創建DatagramSocket用來解析數據接收到的數據包
將數據接收到數據包DatagramSocket中
通過DatagramSocket解析數據
關閉服務
整個UDP發送數據的流程就是這樣
注意點:
爲UDP是無連接的不可靠傳輸,所以接收方需要在發送方發送數據之前就啓動,否則會接收不到數據,也就是說必須先運行UDPSocketReceive再運行UDPSocketSend。
聊天實例
把上面的例子進行一些小改動就可以實現聊天功能
public class UDPSocket1 {
public static void main(String[] args) {
try {
new Thread(new Runnable() {
@Override
public void run() {
try {
receive();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
send();
} catch (IOException e) {
e.printStackTrace();
}
}
//接收消息方法
private static void receive() throws IOException {
System.out.println("UDOSocket1 Receiver Start...");
//1.創建udp的socket服務,並聲明端口號
DatagramSocket ds = new DatagramSocket(6666);
//無限循環,一直處於接收狀態
while (true) {
//2.創建接收數據的數據包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//3.將數據接收到數據包中
ds.receive(dp);
//4.解析數據
String content = new String(dp.getData(), 0, dp.getLength());
System.out.println("UDPSocket1 Receive:" + content);
}
}
private static void send() throws IOException {
//1.創建socket服務
DatagramSocket ds = new DatagramSocket();
//將鍵盤輸入的信息轉換成輸入流再放入到緩衝區
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while ((line=br.readLine())!=null){
//2.封裝數據
byte[] bytes = line.getBytes();
//地址
InetAddress address =InetAddress.getByName("192.168.31.137");
//參數:數據、長度、地址、端口
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,7777);
//3.發送數據包
ds.send(dp);
}
//4.關閉socket服務
ds.close();
}
}
public class UDPSocket2 {
public static void main(String[] args){
try {
new Thread(new Runnable() {
@Override
public void run() {
try {
receive();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
send();
} catch (IOException e) {
e.printStackTrace();
}
}
//接收消息方法
private static void receive() throws IOException {
System.out.println("UDOSocket2 Receiver Start...");
//1.創建udp的socket服務,並聲明端口號
DatagramSocket ds = new DatagramSocket(7777);
//無限循環,一直處於接收狀態
while (true) {
//2.創建接收數據的數據包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//3.將數據接收到數據包中
ds.receive(dp);
//4.解析數據
String content = new String(dp.getData(), 0, dp.getLength());
System.out.println("UDPSocket2 Receive:" + content);
}
//關閉服務
// ds.close();
}
private static void send() throws IOException {
//1.創建socket服務
DatagramSocket ds = new DatagramSocket();
//將鍵盤輸入的信息轉換成輸入流再放入到緩衝區
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while ((line=br.readLine())!=null){
//2.封裝數據
byte[] bytes = line.getBytes();
//地址
InetAddress address =InetAddress.getByName("192.168.31.137");
//參數:數據、長度、地址、端口
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);
//3.發送數據包
ds.send(dp);
}
//4.關閉socket服務
ds.close();
}
}
在接收方方法寫一個無限循環讓其處於持續接收狀態,發送發通過對鍵盤錄入加回車進行消息的發送,並且兩個端點都具備發送和接收功能。需要注意的是receive()會執行一個無限循環,所以receive()和send()必須位於不同線程,否則會導致無法發送消息從而也接收不到消息。
來看打印結果:
UDPSocket1
UDPSocket Receiver Start...
UDPSocket Receive:hello udp1
heelo udp2
UDPSocket2
UDPSocket2 Receiver Start...
hello udp1
UDPSocket2 Receive:hello udp2
我在UDPSocket1 的控制檯中輸入了:“hello udp2”、在UDPSocket2 的控制檯中輸入了:“hello udp1”,接收內容和發送內容完全一致,一個簡單的聊天功能就實現了,希望不熟悉的朋友也可以敲一遍代碼練一遍。
TCP Socket
TCP基於client-server是面向連接的可靠傳輸,上篇文章我們也講解了TCP安全傳輸機制,可謂是相當複雜,如果需要個人對TCP協議進行封裝顯然大多數開發者是很難實現的,所以Java也爲開發者提供了基於TCP的Socket,不同於UDP,TCP Socket分爲Socket和ServerSocket對應着client和server,下面我來用代碼實現一個簡單的TCP通訊功能:
客戶端:
//客戶端
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.創建TCP客戶端Socket服務
Socket client = new Socket();
//2.與服務端進行連接
InetSocketAddress address = new InetSocketAddress("192.168.31.137",10000);
client.connect(address);
//3.連接成功後獲取客戶端Socket輸出流
OutputStream outputStream = client.getOutputStream();
//4.通過輸出流往服務端寫入數據
outputStream.write("hello server".getBytes());
//5.關閉流
client.close();
}
}
首先創建一個Socket和InetSocketAddress ,然後通過Socket的connect()方法進行連接,連接成功後可以獲取到輸出流,通過該輸出流就可以向服務端傳輸數據。
服務端:
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.創建服務端Socket並明確端口號
ServerSocket serverSocket = new ServerSocket(10000);
//2.獲取到客戶端的Socket
Socket socket = serverSocket.accept();
//3.通過客戶端的Socket獲取到輸入流
InputStream inputStream = socket.getInputStream();
//4.通過輸入流獲取到客戶端傳遞的數據
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = bufferedReader.readLine())!=null){
System.out.println(line);
}
//5.關閉流
socket.close();
serverSocket.close();
}
首先創建一個服務端Socket並明確端口號,通過accept()方法獲取到鏈接過來的客戶端Socket,從客戶端Socket中獲取輸入流,最後由輸入流讀取客戶端傳輸來的數據。
我們來看看服務端的打印結果:
hello server
成功接收到客戶端發來的數據
注意點:
一個服務端是可以同時和多個客戶端進行通信的,那麼它是如何區分不同客戶端呢?從上面代碼我們可以看到,服務端首先通過accept()獲取到客戶端Socket,然後通過客戶端的Socket獲取的流進行通訊,這也讓服務端得以區分每個客戶端。
文件傳輸實例
流程:客戶端上傳一個文件到服務端,服務端收到文件後進行保存,保存成功後給客戶端一個響應。
客戶端代碼
public class TCPUploadClient {
public static void main(String[] args) throws IOException {
//1.創建TCP客戶端Socket服務
Socket client = new Socket();
//2.與服務端進行連接
InetSocketAddress address = new InetSocketAddress("192.168.31.137",10001);
client.connect(address);
//3.讀取客戶端文件
FileInputStream fis = new FileInputStream("E://girl.jpg");
//4.獲取輸出流
OutputStream outputStream = client.getOutputStream();
//5.將文件寫入到服務端
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes))!=-1){
outputStream.write(bytes,0,len);
}
//6.通知服務器數據寫入完畢
client.shutdownOutput();
//7.讀取服務端響應的數據
InputStream inputStream = client.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line = br.readLine();
System.out.println(line);
//8.關閉流
inputStream.close();
fis.close();
client.close();
}
}
創建Socket並連接,讀取本地文件輸入流,將文件寫入到服務端,寫入成功後讀取服務端返回的數據。
服務端代碼
public class TCPUploadServer {
public static void main(String[] args) throws IOException {
//1.創建客戶端Socket
ServerSocket serverSocket = new ServerSocket(10001);
//2.獲取到客戶端Socket
Socket socket = serverSocket.accept();
//3.通過客戶端Socket獲取到輸入流
InputStream is = socket.getInputStream();
//4.將流以文件的形式寫入到磁盤
File dir = new File("F://tcp");
//如果文件夾不存在就創建文件夾
if(!dir.exists()){
dir.mkdirs();
}
File file = new File(dir,"girl.jpg");
FileOutputStream fos = new FileOutputStream(file);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes))!=-1){
fos.write(bytes,0,len);
}
//5.通知客戶端文件保存完畢
OutputStream os = socket.getOutputStream();
os.write("success".getBytes());
//6.關閉流
fos.close();
os.close();
socket.close();
}
}
創建Socket並獲取到客戶端Socket和輸入流,在F盤的tcp目錄下創建一個文件,將從客戶端讀取到的數據輸出到文件中,保存成功後給客戶端返回’‘success’’
這樣我們就實現了一哥客戶端上傳文件的功能,還是比較簡單的,希望大家也能夠跟着代碼敲一遍。
TCP Socket多線程應用
細心的同學可能已經發現,上面我們的實例中一個服務端只能接收一個客戶端的一次請求,這顯然是不符合實際的,那麼如何使一個服務端同時服務多個客戶端呢?接着擼代碼
//客戶端1
public class TCPClient1 {
public static void main(String[] args) throws IOException {
System.out.println("TCPClient1 Start...");
//1.創建TCP客戶端Socket服務
Socket client = new Socket();
//2.與服務端進行連接
InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
client.connect(address);
//3.連接成功後獲取客戶端Socket輸出流
OutputStream outputStream = client.getOutputStream();
//4.通過輸出流往服務端寫入數據
outputStream.write("Hello my name is Client1".getBytes());
//5.告訴服務端發送完畢
client.shutdownOutput();
//6.讀取服務端返回數據
InputStream is = client.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//7.關閉流
client.close();
}
}
//客戶端2
public class TCPClient2 {
public static void main(String[] args) throws IOException {
System.out.println("TCPClient2 Start...");
//1.創建TCP客戶端Socket服務
Socket client = new Socket();
//2.與服務端進行連接
InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
client.connect(address);
//3.連接成功後獲取客戶端Socket輸出流
OutputStream outputStream = client.getOutputStream();
//4.通過輸出流往服務端寫入數據
outputStream.write("Hello my name is Client2".getBytes());
//5.告訴服務端發送完畢
client.shutdownOutput();
//6.讀取服務端返回數據
InputStream is = client.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//7.關閉流
client.close();
}
}
//服務端
public class TCPServer {
public static void main(String[] args) throws IOException {
receive();
}
private static void receive() throws IOException {
System.out.println("Server Start...");
//創建服務端Socket並明確端口號
ServerSocket serverSocket = new ServerSocket(10004);
while (true){
//獲取到客戶端的Socket
Socket socket = serverSocket.accept();
//通過客戶端的Socket獲取到輸入流
InputStream is = socket.getInputStream();
//通過輸入流獲取到客戶端傳遞的數據
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//將客戶端發來的數據原封不動返回
OutputStream os = socket.getOutputStream();
os.write(new String(bytes,0,len).getBytes());
//關閉連接
socket.close();
}
}
}
客戶端1控制檯
TCPClient1 Start...
Hello my name is Client1
客戶端2控制檯
TCPClient2 Start...
Hello my name is Client2
這樣就可以實現一個服務端服務多個客戶端
細心的同學可能又發現了,上面的寫法是存在問題的,由於服務端始終都在主線程中處理請求,所以客戶端的請求需要被服務端排隊處理,舉個例子:Client1對服務端進行了一次請求,服務端在響應Client1之前是不會接受其他請求的,顯然這是不符合邏輯的,真正的服務器是要具備併發處理的。而多線程恰好能解決這個問題,我們來看修改之後的服務端代碼
public class TCPServer {
public static void main(String[] args) throws IOException {
receive();
}
private static void receive() throws IOException {
System.out.println("Server Start...");
//創建服務端Socket並明確端口號
ServerSocket serverSocket = new ServerSocket(10004);
while (true){
//獲取到客戶端的Socket
final Socket socket = serverSocket.accept();
//通過線程執行客戶端請求
new Thread(new Runnable() {
@Override
public void run() {
try {
//通過客戶端的Socket獲取到輸入流
InputStream is = socket.getInputStream();
//通過輸入流獲取到客戶端傳遞的數據
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//將客戶端發來的數據原封不動返回
OutputStream os = socket.getOutputStream();
os.write(new String(bytes,0,len).getBytes());
//關閉連接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
運行效果適合加線程之前是一樣的,但這種方式效率更高。
本篇文章敘述了基於UDP和TCP的Socket,UDP是無連接的,所以UDP Socket在發送數據的時候只需要目標IP和端口即可發送。TCP是面向連接的並且是基於client-server模式,在傳輸數據前需要進行連接,可以通過多線程技術實現併發處理客戶端請求。本篇文章內容還是比較簡單的,希望大家能把文章中代碼自己敲一遍,掌握Socket的同時也能讓你自己UDP和TCP的理解更加深刻。