前言
上篇文章我們描述了傳輸層協議TCP、UDP,但它們畢竟只是協議,看不見摸不著,那我們怎們通過TCP、和UDP進行實際傳輸呢?不用著急,等看完這篇文章你一定會明白的。
1 Socket概述
Socket中文意思為插座
的意思,專業術語稱之為套接字
,它把TCP/IP封裝成了調用接口供開發者調用,也就是說開發者可以通過調用Socket相關API來實現網絡通訊。在Java中也存在Socket相關API,主要分為兩個,分別是基于UDP傳輸協議的Socket和基于TCP傳輸協議的Socket,本篇文章會對基于這兩種傳輸協議的Socket進行詳細描述。
2 UDP Socket
2.1 基本使用
通過上節的內容我們知道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。
2.2 聊天實例
把上面的例子進行一些小改動就可以實現聊天功能
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”,接收內容和發送內容完全一致,一個簡單的聊天功能就實現了,希望不熟悉的朋友也可以敲一遍代碼練一遍。
3 TCP Socket
3.1 基本使用
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獲取的流進行通訊,這也讓服務端得以區分每個客戶端。
3.2 文件傳輸實例
流程:客戶端上傳一個文件到服務端,服務端收到文件后進行保存,保存成功后給客戶端一個響應。
客戶端代碼
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''
效果
客戶端控制臺
success
這樣我們就實現了一哥客戶端上傳文件的功能,還是比較簡單的,希望大家也能夠跟著代碼敲一遍。
3.3 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的理解更加深刻。本篇文章對Socket的描述到此為止,下篇文章《Android網絡編程(三)HTTP、HTTPS》