網絡編程最主要的工作就是在發送端把信息通過規定好的協議進行組裝包,在接收端按照規定好的協議把包進行解析,從而提取出對應的信息,達到通信的目的。 —— 百度百科
Table of Contents
先來一張Java網絡編程基礎知識體系架構:
網絡通信協議
現在來簡單回憶一下,計算機網絡的有關基礎原理:
網絡通信協議有很多種,目前應用最廣泛的是TCP/IP協議(Transmission Control Protocal/Internet Protoal傳輸控制協議/英特網互聯協議),它是一個包括TCP協議和IP協議,UDP(User Datagram Protocol)協議和其它一些協議的協議組,在學習具體協議之前首先了解一下TCP/IP協議組的層次結構。
在進行數據傳輸時,要求發送的數據與收到的數據完全一樣,這時,就需要在原有的數據上添加很多信息,以保證數據在傳輸過程中數據格式完全一致。TCP/IP協議的層次結構比較簡單,共分爲四層,如圖所示。
TCP/IP協議中的四層分別是應用層、傳輸層、網絡層和鏈路層,每層分別負責不同的通信功能,接下來針對這四層進行詳細地講解。
- 鏈路層:鏈路層是用於定義物理傳輸通道,通常是對某些網絡連接設備的驅動協議,例如針對光纖、網線提供的驅動。
- 網絡層:網絡層是整個TCP/IP協議的核心,它主要用於將傳輸的數據進行分組,將分組數據發送到目標計算機或者網絡。
- 傳輸層:主要使網絡程序進行通信,在進行網絡通信時,可以採用TCP協議,也可以採用UDP協議。
- 應用層:主要負責應用程序的協議,例如HTTP協議、FTP協議等。
要想使網絡中的計算機能夠進行通信,必須爲每臺計算機指定一個標識號,通過這個標識號來指定接受數據的計算機或者發送數據的計算機。在TCP/IP協議中,這個標識號就是IP地址,它可以唯一標識一臺計算機,目前,IP地址廣泛使用的版本是IPv4,它是由4個字節大小的二進制數來表示。
由於二進制形式表示的IP地址非常不便記憶和處理,因此通常會將IP地址寫成十進制的形式,每個字節用一個十進制數字(0-255)表示,數字間用符號“.”分開,如 “192.168.1.100”。
通過IP地址可以連接到指定計算機,但如果想訪問目標計算機中的某個應用程序,還需要指定端口號。在計算機中,不同的應用程序是通過端口號區分的。端口號是用兩個字節(16位的二進制數)表示的,它的取值範圍是0~65535,其中,0~1023之間的端口號用於一些知名的網絡服務和應用,用戶的普通應用程序需要使用1024以上的端口號,從而避免端口號被另外一個應用或服務所佔用。
網絡通信中的IP和端口:
IP管理類(InetAddress類)
功能:管理IP地址與主機名(域名)之間的轉換,以及IP地址本身格式的轉換(把32二進制轉換爲4個十進制數)。
靜態方法:
- static InetAddress[] getAllByName(String host) 根據主機或者域名的名稱,根據系統上配置的名稱服務返回其IP地址數組。
- static InetAddress getByAddress(byte[] addr) 給定原始IP地址返回 InetAddress對象。
- static InetAddress getByAddress(String host, byte[] addr) 根據提供的主機名和IP地址創建InetAddress。
- static InetAddress getByName(String host) 根據主機名、域名稱確定主機的IP地址。
IP管理類的方法使用
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Main {
public static void main(String[] args) throws UnknownHostException {
ipAddress();
}
public static void ipAddress() throws UnknownHostException {
//getLocalHost 獲取本機的IP地址對象
InetAddress address = InetAddress.getLocalHost();
System.out.println("IP地址:"+address.getHostAddress());
System.out.println("主機名:"+address.getHostName());
//獲取別人機器的IP地址對象。
//可以根據一個IP地址的字符串形式或者是一個主機名生成一個IP地址對象。
InetAddress address1 = InetAddress.getByName("KYLE");
System.out.println("IP地址:"+address1.getHostAddress());
System.out.println("主機名:"+address1.getHostName());
InetAddress[] arr = InetAddress.getAllByName("www.baidu.com");//域名
for(InetAddress s:arr) { //輸出該域名所表示的所有IP地址
System.out.println(s);
}
}
public static void fun() throws UnknownHostException {
//根據主機名或域名獲取iP地址
InetAddress ip01 = InetAddress.getByName("KYLE"); //根據主機名獲取IP地址
InetAddress ip02 = InetAddress.getByName("hackyle.net"); //根據域名獲取主機的IP地址
System.out.println(ip01); //KYLE/192.168.1.50
System.out.println(ip02); //hackyle.net/116.62.148.72
//將IP地址轉換爲字節數組
byte[] byteIP = ip02.getAddress();
//根據IP地址的字節形式和返回其IP
InetAddress ip03 = InetAddress.getByAddress(byteIP);
System.out.println(ip03); //輸出:116.62.148.72
}
}
基於UDP協議編程
理解UDP:
- UDP(User Datagram Protocol)是無連接通信協議,即在數據傳輸時,數據的發送端和接收端不建立邏輯連接。
- 簡單來說,當一臺計算機向另外一臺計算機發送數據時,發送端不會確認接收端是否存在,就會發出數據,同樣接收端在收到數據時,也不會向發送端反饋是否收到數據。
- 由於使用UDP協議消耗資源小,通信效率高,所以通常都會用於音頻、視頻和普通數據的傳輸例如視頻會議都使用UDP協議,因爲這種情況即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。
- 但是在使用UDP協議傳送數據時,由於UDP的面向無連接性,不能保證數據的完整性,因此在傳輸重要數據時不建議使用UDP協議。
特性:
- 將數據封裝爲數據包,面向無連接。
- 每個數據包大小限制在64K中
- 因爲無連接,所以不可靠
- 因爲不需要建立連接,所以速度快
- UDP通訊是不分服務端與客戶端的,只分發送端與接收端。
UDP協議下的Socket相關類:
- java.net.DatagramPacket類:將數據包裝起來
- java.net.DatagramSocket類:將數據包發送和接收
DatagramSocket類
構造方法:
DatagramSocket(int port) :數據報套接字並將其綁定到本地主機上的指定端口。
- 該構造方法既可用於創建接收端的DatagramSocket對象,又可以創建發送端的DatagramSocket對象;
- 在創建接收端的DatagramSocket對象時,必須要指定一個端口號,這樣就可以監聽指定的端口。
DatagramSocket() :據報套接字並將其綁定到本地主機上任何可用的端口。
- 該構造方法用於創建發送端的DatagramSocket對象;
- 在創建DatagramSocket對象時,並沒有指定端口號,此時,系統會分配一個沒有被其它網絡程序所使用的端口號。
方法:
- void receive(DatagramPacket p):從此套接字接收數據報包;
- void send(DatagramPacket p):從此套接字發送數據報包
DatagramPacket類
構造方法:
DatagramPacket(byte[] buf, int length) :DatagramPacket,用來接收長度爲 length 的數據包。
- 使用該構造方法在創建DatagramPacket對象時,指定了封裝數據的字節數組和數據的大小,沒有指定IP地址和端口號。
- 很明顯,這樣的對象只能用於接收端,不能用於發送端。
- 因爲發送端一定要明確指出數據的目的地(ip地址和端口號),而接收端不需要明確知道數據的來源,只需要接收到數據即可。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):構造數據報包,用來將長度爲 length 偏移量爲 offset 的包發送到指定主機上的指定端口號。
- 使用該構造方法在創建DatagramPacket對象時,不僅指定了封裝數據的字節數組和數據的大小,還指定了數據包的目標IP地址(addr)和端口號(port)。
- 該對象通常用於發送端。
- 因爲在發送數據時必須指定接收端的IP地址和端口號,就好像發送貨物的集裝箱上面必須標明接收人的地址一樣。
實例-UDP發送端
發送端的使用步驟:
1. 建立UDP的服務:啓動插座(DatagramSocket)
2. 準備數據,把數據封裝到數據包中發送。 發送端的數據包要帶上ip地址與端口號:建立包(DatagramPacket)
3. 調用UDP的服務,發送數據:從插座中發送;
4. 關閉資源:關閉插座;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.net.DatagramPacket;
import java.io.IOException;
//發送端
public class Sender {
private static String ip = "116.62.148.72";
private static int port = 9190;
public static void main(String[] args) {
InetAddress iaddress = null;
DatagramSocket ds = null;
System.out.println("Start sending...");
try {
iaddress = InetAddress.getByName(ip);
ds = new DatagramSocket();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
}
String data = "嗨,接收端。";
try {
DatagramPacket dp = new DatagramPacket(data.getBytes(),data.getBytes().length,iaddress,port);
ds.send(dp);
} catch(IOException e) {
e.printStackTrace();
} finally {
ds.close();
System.out.println("Send Finished...");
}
}
}
實例-UDP接收端
接收端的使用步驟
- 建立udp的服務:啓用插座(DatagramSocket);
- 準備空 的數據 包接收數據:(byte[];DatagramPacket);
- 調用udp的服務接收數據:插座的對象receive存放於DatagramPacket對象的包中;
- 關閉資源:關閉插座;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.io.IOException;
public class Receiver {
public static void main(String[] args) {
DatagramSocket ds = null;
DatagramPacket dp = null;
System.out.println("Start receiving....");
byte[] buf = new byte[1024];
try {
ds = new DatagramSocket(9190);
dp = new DatagramPacket(buf,buf.length);
ds.receive(dp);
System.out.println(new String(buf,0,dp.getLength()));
} catch (IOException e) {
e.printStackTrace();
} finally {
ds.close();
System.out.println("Receive Finished....");
}
}
}
基於TCP協議編程
理解TCP:
- TCP協議是面向連接的通信協議,即在傳輸數據前先在發送端和接收端建立邏輯連接,然後再傳輸數據,它提供了兩臺計算機之間可靠無差錯的數據傳輸。
- 在TCP連接中必須要明確客戶端與服務器端,由客戶端向服務端發出連接請求,每次連接的創建都需要經過“三次握手”。
- 第一次握手,客戶端向服務器端發出連接請求,等待服務器確認;
- 第二次握手,服務器端向客戶端回送一個響應,通知客戶端收到了連接請求;
- 第三次握手,客戶端再次向服務器端發送確認信息,確認連接。
- 由於TCP協議的面向連接特性,它可以保證傳輸數據的安全性,所以是一個被廣泛採用的協議,例如在下載文件時,如果數據接收不完整,將會導致文件數據丟失而不能被打開,因此,下載文件時必須採用TCP協議。
理解TCP3次握手建立連接:
- 發送方:你聽得到嗎(SYN)?
- 接收方:我聽得到(ACK),你能聽得到我嗎(SYN)?
- 發送方:我聽得到(ACK)。
- 此時雙方都知道彼此具備收發數據的能力!
TCP通訊協議特點:
- 是基於IO流進行數據 的傳輸,面向連接(雙方都確認存活才建立連接)。
- 進行數據傳輸的時候是沒有大小限制的。
- 是面向連接,通過三次握手的機制保證數據的完整性。是可靠協議。
- 是面向連接的,所以速度慢。
- 是區分客戶端與服務端的。
Socket類
功能:工作在客戶端。專用於客戶端去連接服務端。
構造方法:
- Socket(InetAddress address, int port):根據參數去連接在指定地址和端口上運行的服務器程序,參數address用於接收一個InetAddress類型的對象,該對象用於封裝一個IP地址。
- Socket(String host, int port):根據參數去連接在指定地址和端口上運行的服務器程序,其中參數host接收的是一個字符串類型的IP地址。
方法:
- int getPort():該方法返回一個int類型對象,該對象是Socket對象與服務器端連接的端口號
- InetAddress getLocalAddress():該方法用於獲取Socket對象綁定的本地IP地址,並將IP地址封裝成InetAddress類型的對象返回
- void close():該方法用於關閉Socket連接,結束本次通信。在關閉socket之前,應將與socket相關的所有的輸入/輸出流全部關閉,這是因爲一個良好的程序應該在執行完畢時釋放所有的資源
- InputStream getInputStream():該方法返回一個InputStream類型的輸入流對象,如果該對象是由服務器端的Socket返回,就用於讀取客戶端發送的數據,反之,用於讀取服務器端發送的數據
- OutputStream getOutputStream():該方法返回一個OutputStream類型的輸出流對象,如果該對象是由服務器端的Socket返回,就用於向客戶端發送數據,反之,用於向服務器端發送數據
ServerSocket類
功能:工作在服務器。專門用於與客戶端之間建立連接,然後互相通信。
核心方法:
- 創建一個監聽端口的服務器套接字:ServerSocket(int port);
- 等待連接,直道有客戶端連接爲止,連城成功後返回一個Socket對象:accept();
- 關閉連接:void close();
實例-TCP服務端
ServerSocket的使用 步驟
1. 建立tcp服務端 的服務:啓用插座(ServerSocket)
2. 接受客戶端的連接產生一個Socket:建立接受從插座來的數據接口Socket;
3. 獲取對應的流對象讀取或者寫出數據:以字節流方式讀取數據;
4. 關閉資源:關閉插座
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.io.InputStream;
import java.io.OutputStream;
public class TcpServer {
public static void main(String[] args) {
tcp_server();
}
public static void tcp_server() {
ServerSocket ss = null;
Socket so = null;
InputStream inputStream = null; //從客戶端接收
OutputStream outputStream = null; //從服務器發送
//創建連接
try {
System.out.println("服務器啓動成功!");
ss = new ServerSocket(9595); //建立Tcp的服務端,並且監聽一個端口
so = ss.accept();
} catch(IOException e) {
e.printStackTrace();
}
while(true) {
System.out.println("作爲服務器,你要發送(1),還是接收(2),或者是關閉(3)?");
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
if(x==1) {
sendToClient(so,outputStream);
} else if(x==2) {
recevice(so,inputStream);
} else {
try {
ss.close();
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void recevice(Socket so, InputStream inputStream) {
/*** 接收客戶端 ***/
byte[] buf = new byte[1024];
int length = 0;
try {
inputStream = so.getInputStream();
length = inputStream.read(buf);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("服務端接收到的數據:" + new String(buf,0,length));
}
public static void sendToClient(Socket so, OutputStream outputStream) {
/*** 發送:向客戶端 ***/
String data = "客戶端你好!我是服務器";
try {
outputStream = so.getOutputStream();
outputStream.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("數據從服務器發送到客戶端成功!");
}
}
實例-TCP客戶端
TCP的客戶端使用步驟:
1. 建立TCP的客戶端服務:啓用插座;
2. 獲取到對應的流對象:建立輸出通道,即輸出到服務端;
3. 寫出或讀取數據:輸出;
4. 關閉資源:關閉插座;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpClient {
public static void main(String[] args) {
tcp_client();
}
public static void tcp_client() {
Socket so = null;
String ip = "127.0.0.1";
int port = 9595;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
so = new Socket(ip,port);
} catch (IOException e) {
e.printStackTrace();
}
while(true) {
System.out.println("作爲客戶端,你要發送(1),還是接收(2),或者是關閉(3)?");
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
if(x==1) {
sendToServer(so,outputStream);
} else if (x==2){
recevice(so,inputStream);
} else {
try {
so.close();
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void recevice(Socket so, InputStream inputStream) {
/** 從服務器接收 **/
byte[] buf = new byte[1024];
int length = 0;
try {
inputStream = so.getInputStream();
length = inputStream.read(buf);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客戶端接收到的數據:" + new String(buf,0,length));
}
public static void sendToServer(Socket so, OutputStream outputStream) {
/** 向服務器發送 **/
String data = "服務器你好!我是客戶端";
try {
outputStream = so.getOutputStream();
outputStream.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("數據從客戶端發送到服務器成功!");
}
}
實現結果:
TCP-文件上傳
實現展示:
客戶端
- Socket套接字連接服務器
- 通過Socket獲取字節輸出流,寫圖片
- 使用自己的流對象,讀取圖片數據源:FileInputStream
- 讀取圖片,使用字節輸出流,將圖片寫到服務器
- 採用字節數組進行緩衝
- 通過Socket套接字獲取字節輸入流
- 讀取服務器發回來的上傳成功
- 關閉資源
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TcpPictureUploadClient {
public static void main(String[] args) throws IOException{
Socket socket = new Socket("127.0.0.1", 8000);
//獲取字節輸出流,圖片寫到服務器
OutputStream out = socket.getOutputStream();
//創建字節輸入流,讀取本機上的數據源圖片
FileInputStream fis = new FileInputStream("c:\\users\\kyle\\desktop\\a.jpg");
//開始讀寫字節數組
int len = 0 ;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes))!=-1){
out.write(bytes, 0, len);
}
//給服務器寫終止序列
socket.shutdownOutput();
//獲取字節輸入流,讀取服務器的上傳成功
InputStream in = socket.getInputStream();
len = in.read(bytes);
System.out.println(new String(bytes,0,len));
fis.close();
socket.close();
}
}
服務器
- ServerSocket套接字對象,監聽端口8000
- 方法accept()獲取客戶端的連接對象
- 客戶端連接對象獲取字節輸入流,讀取客戶端發送圖片
- 創建File對象,綁定上傳文件夾,判斷文件夾存在, 不存,在創建文件夾
- 創建字節輸出流,數據目的File對象所在文件夾
- 字節流讀取圖片,字節流將圖片寫入到目的文件夾中
- 將上傳成功會寫客戶端
- 關閉資源
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
public class TcpPictureUploadServer {
public static void main(String[] args) throws IOException{
ServerSocket server = new ServerSocket(8000);
Socket socket = server.accept();
//通過客戶端連接對象,獲取字節輸入流,讀取客戶端圖片
InputStream in = socket.getInputStream();
//將目的文件夾封裝到File對象
File upload = new File("c:\\users\\kyle\\desktop\\up");
if(!upload.exists()) {
upload.mkdirs();
}
//防止文件同名被覆蓋,從新定義文件名字
// 規則: 域名+毫秒值+6位隨機數
String filename = "itcast" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg";
// 創建字節輸出流,將圖片寫入到目的文件夾中
FileOutputStream fos = new FileOutputStream(upload + File.separator + filename);
//讀寫字節數組
byte[] bytes = new byte[1024];
int len = 0;
while ((len = in.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
// 通過客戶端連接對象獲取字節輸出流
// 上傳成功寫回客戶端
socket.getOutputStream().write("上傳成功".getBytes());
fos.close();
socket.close();
server.close();
}
}