1、網絡模型:OSI參考模型和TCP/IP參考模型
通常用戶操作的是應用層,而編程人員需要做的是傳輸層和網際層,用戶在應用層操作的數據,經過逐層封包,最後到物理層發送到另一個模型中,再進行逐層解包,圖示爲:
![package1](https://img-blog.csdn.net/20150812000227184)
2、網絡通信三要素:IP地址,端口號,傳輸協議
1.IP地址
1. 它是網絡中的設備標識
2. 不易記憶,可用主機名錶示,兩者存在映射關係
3. 本機迴環地址:127.0.0.1,主機名爲:localhost。
IP地址:java中對應的是InetAddress類,存在於java.net包中。
InetAddress類:
(一)無構造函數,可通過getLocalHost()方法返回本地主機獲取
InetAddress對象,此方法是靜態的。
InetAddress i = InetAddress.getLocalHost();
(二)方法:
1. static InetAddress getByName(String host):獲取指定主機的IP和主機名。(最好用ip地址去獲取,主機名需要解析)
2. static InetAddress[] getAllByName(String host):在給定主機名的情況下,根據系統上配置的名稱服務返回IP地址所組成的數組。返回對象不唯一時,用此方法。
3. String getHostAddress():返回IP地址字符串文本形式,以IP地址爲主。
4. String getHostName():返回IP地址主機名。
(三)如何獲取任意一臺主機的IP地址對象:
1. 功能:返回InetAddress對象
2. 對於任意主機,需要指定傳入主機名的參數
注意:如果IP地址和對應的主機名,這種映射關係沒有在網絡上,就不會解析成功,返回的還是指定的IP。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpDemo {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
//獲取我的本機信息
InetAddress ip = InetAddress.getByName("LQX");
//本機名和IP的打印
System.out.println("IP:"+ip.getHostAddress()+"\tname="+ip.getHostName());
//這個是我在hosts文件中定義的一個對應關係
InetAddress ip2 = InetAddress.getByName("www.lqx.com");
System.out.println("IP:"+ip2.getHostAddress()+"\tname:"+ip2.getHostName());
//查詢百度網站的IP,百度應該不止有一個主機
InetAddress[] baidu=InetAddress.getAllByName("www.baidu.com");
for (InetAddress b :baidu)
{
String baddress=b.getHostAddress();
String bname=b.getHostName();
System.out.println("baiduIP="+baddress+"\tbaiduname="+bname);
}
}
}
打印結果:
2.端口號
1. 定義:
隨着計算機網絡技術的發展,原來物理上的接口(如鍵盤、鼠標、網卡、顯示卡等輸入/輸出接口)已不能滿足網絡通信的要求,TCP/IP協議作爲網絡通信的標準協議就解決了這個通信難題。TCP/IP協議集成到操作系統的內核中,這就相當於在操作系統中引入了一種新的輸入/輸出接口技術,因爲在TCP/IP協議中引入了一種稱之爲"Socket(套接字)"應用程序接口。有了這樣一種接口技術,一臺計算機就可以通過軟件的方式與任何一臺具有Socket接口的計算機進行通信。端口在計算機編程上也就是"Socket接口"。
a、用於標識進程的邏輯地址,不用進程的標識。
b、有效端口:0 ~65535,系統使用或保留的端口是:0~ 1024。
3.傳輸協議
即通信規則,包含TCP和UDP協議
一. UDP(User Datagram Protocol)用戶數據報協議
1. 一種無連接的傳輸層協議,位於第四層——傳輸層,處於IP協議的上一層
2. 當報文發送之後,是無法得知其是否安全完整到達的
3. 資源消耗小,處理速度快的優點
所以通常音頻、視頻和普通數據在傳送時使用UDP較多,因爲它們即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。比如我們聊天用的ICQ和QQ就是使用的UDP協議。
二. TCP(Transmission Control Protocol 傳輸控制協議)
1. 一種面向連接的、可靠的、基於字節流的傳輸層通信協議,位於第四層——傳輸層,和UDP協議位於同一層
2. 通過三次握手完成連接,是可靠的協議
3. 必須建立連接,效率稍慢
TCP三次握手的過程如下:
(1)
客戶端發送報文給服務器進入SEND狀態。
(2)
服務器端收到報文,迴應一個ACK報文,進入RECV狀態。
(3)
客戶端收到服務器端的SYN報文,迴應一個ACK報文,進入Established
建立狀態。
3、JAVA的UDP和TCP編程
1.Socket套接字
1. 用於描述IP地址和端口,是一個通信鏈的句柄,可以用來實現不同虛擬機或不同計算機之間的通信
2.它被稱之爲插座,相當於港口一樣,是網絡服務提供的一種機制。
3.通信兩端都要有Socket,才能建立服務。
4.網絡通信其實就是Socket間的通信,數據在兩個Socket間通過IO傳輸。
2.UDP傳輸
在Java API中,實現UDP方式的編程,包含客戶端網絡編程和服務器端網絡編程,主要由兩個類實現,分別是:
1. DatagramSocket:UDP客戶端編程涉及的步驟也是4個部分:建立連接、發送數據、接收數據和關閉連接。
DatagramSocket ds = new DatagramSocket();
這樣就建立了一個鏈接,但是沒有指定端口,系統會使用未被佔用的端口,一般常用來做發送端。
DatagramSocket receive = new DatagramSocket(10001);
這種定義常用來做接收端,他會一直監聽10001端口。
2. DatagramPacket:這個類中封裝了許多傳輸的信息,對象中包含發送到的地址、發送到的端口號以及發送的內容等。
DatagramPacket dp = new DatagramPacket(byte[] buf, int length, InetAddress address, int port) ;
IO編程在UDP方式的網絡編程中變得不是必須的內容。
接着,介紹一下UDP客戶端編程中發送數據的實現。在UDP方式的網絡編程中,IO技術不是必須的,在發送數據時,需要將需要發送的數據內容首先轉換爲byte數組,然後將數據內容、服務器IP和服務器端口號一起構造成一個DatagramPacket類型的對象,這樣數據的準備就完成了,發送時調用網絡連接對象中的send方法發送該對象即可。
/***
* UDP發送端:
* 需求:通過udp傳輸方式,將一段文字數據發送出去
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class Send {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.獲取目標地址(本機地址)
InetAddress ip = InetAddress.getLocalHost();
//2.發送端的聲明
DatagramSocket send = new DatagramSocket();
//3.要發送的字符串
String s = "Hello my name is Luan";
//轉byte
byte []b = s.getBytes();
//4.傳送字節數組b,目標地址爲本機,端口10000
DatagramPacket packet = new DatagramPacket(b, b.length, ip, 10000);
//5.使用DatagramSocket類中的Send方法發送數據包
send.send(packet);
//6.資源關閉
send.close();
}
}
/***
* UDP接收端
* 需求:定義一個應用程序,用於接收udp協議傳輸的數據並處理。
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Receive {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.接收端需要提供端口,實時監聽
DatagramSocket recv = new DatagramSocket(10000);
//2.定義數據包,用於存儲數據
byte b[] = new byte[1024];
//定義收到的包是放在字節數組b裏的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法獲取數據包
recv.receive(packet);//阻塞式方法:沒有數據就不繼續往下執行,至到得到數據。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
//4.關閉資源
recv.close();
}
}
信息傳過來了,ip地址是沒有問題的,我上的校園網分發的IP就是他。端口號是發送端的端口號,沒有問題。
/***
* UDP的發送端
* 需求:用鍵盤錄入的方式,來發送數據
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class InputSend {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.獲取目標地址(本機地址)
InetAddress ip = InetAddress.getLocalHost();
//2.發送端的聲明
DatagramSocket send = new DatagramSocket();
DatagramPacket packet = null;
//3.輸入的字符讀取到內存
BufferedReader in
= new BufferedReader(new InputStreamReader(System.in));
String s =null;
while((s=in.readLine())!=null)
{
//如果發的是“88”,則資源關閉
if(s.equals("88"))
{
break;
}
//轉byte
byte []b = s.getBytes();
//傳送字節數組b,目標地址爲本機,端口10000
packet = new DatagramPacket(b, b.length, ip, 10000);
//使用DatagramSocket類中的Send方法發送數據包
send.send(packet);
}
send.close();
}
}
/***
* 接收端
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class InputReceive {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.接收端需要提供端口,實時監聽
DatagramSocket recv = new DatagramSocket(10000);
//2.定義數據包,用於存儲數據
while(true)
{
byte b[] = new byte[1024];
//定義收到的包是放在字節數組b裏的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法獲取數據包
recv.receive(packet);//阻塞式方法 :沒有數據就不繼續往下執行,至到得到數據。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
}
}
}
發送端:
接收端:
/***
*需求:UDP的客戶端的聊天程序
*需要用到線程的使用:發送端和接收端分別做成兩個線程
*共同組成了這個客戶端的進程
*/
import java.io.*;
import java.net.*;
public class UDPTalk {
public static void main(String[] args) {
// TODO Auto-generated method stub
Send s1 = new Send();
receive r1 = new receive();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
}
}
//接收端
class Send implements Runnable {
private DatagramSocket send = null;
private DatagramPacket packet = null;
@Override
public void run() {
// TODO Auto-generated method stub
try {
InetAddress ip = InetAddress.getLocalHost();
send = new DatagramSocket();
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String s =null;
while((s=in.readLine())!=null)
{
//如果發的是“88”,則資源關閉
if(s.equals("88"))
{
break;
}
//轉byte
byte []b = s.getBytes();
//傳送字節數組b,目標地址爲本機,端口10000
packet = new DatagramPacket(b, b.length, ip, 10000);
//使用DatagramSocket類中的Send方法發送數據包
send.send(packet);
}
send.close();
} catch (Exception e) {
// TODO Auto-generated catch block
throw new RuntimeException("發送數據失敗");
//System.out.println("目的主機無效!");
}
}
}
//接收端
class receive implements Runnable
{
private DatagramSocket recv = null;
private DatagramPacket packet = null;
@Override
public void run() {
// TODO Auto-generated method stub
//1.接收端需要提供端口,實時監聽
try {
recv = new DatagramSocket(10000);
while(true)
{
byte b[] = new byte[1024];
//定義收到的包是放在字節數組b裏的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法獲取數據包
recv.receive(packet);//阻塞式方法 :沒有數據就不繼續往下執行,至到得到數據。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
}
} catch (Exception e) {
// TODO Auto-generated catch block
throw new RuntimeException("接收端接收數據失敗");
}
}
}
只能自己和自己聊了,如果想和別人聊,那麼要把目標IP改爲廣播地址。
比如我這個就應該是111.114.116.255,那麼大家都往這個IP發送內容的話就可以一起聊天了。
2.TCP傳輸
TCP傳輸,需要明確的區分客戶端和服務端。
客戶端使用的類是:Socket,他的構造方法會指定目標地址和端口;
Socket client1 = new Socket(InetAddress address, int port);兩個基本方法:
public InputStream getInputStream():返回此套接字的輸入流,Socket對象調用public OutputStream getOutputStream():返回套接字的輸出流,Socket對象調用
服務器端使用的類:ServerSocket,他的構造方法只需制定需要監聽的端口號即可。
ServerSocket server = new ServerSocket(int port);
基本方法:
public Socket accept():返回值是一個Socket對象,也就是說在服務器端開闢了一個空間,這個空間專門爲對應的客戶端服務。可以理解爲,只要客戶端對服務器有請求,那麼在服務器就會獲取這個客戶端的Socket對象,並使用這個服務器端的客戶端Socket對象與客戶端交流。
/***
* 客戶端代碼
* 需求:客戶端給服務端發送數據,服務端收到後,給客戶端反饋信息。
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//目標IP獲取
InetAddress ip = InetAddress.getLocalHost();
//1.建立客戶端Socket,指定目的主機和端口
Socket client1 = new Socket(ip, 10000);
//2.獲取Socket流中輸出流,發送數據
OutputStream ou = client1.getOutputStream();
String s = "這是我的第一個TCP編程!";
byte b[] = s.getBytes();
ou.write(b);
//對服務器返回的數據進行接收
InputStream in =client1.getInputStream();
byte b2[] = new byte[1024];
int len2 = in.read(b2);
String s2 = new String(b2, 0, len2);
System.out.println(s2);
client1.close();
}
}
/***
* 服務器端
*需求: 響應客戶端的請求
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.服務端使用的是ServerSocket類
ServerSocket server = new ServerSocket(10000);
//2.accept方法會獲得一個Socket對象,並偵聽此對象
Socket client = server.accept();
//3、獲取對應客戶端對象的讀取流讀取發過來的數據,並打印
InputStream in = client.getInputStream();
byte b[] = new byte[1024];
int len = in.read(b);
String s = new String(b, 0, len);
System.out.println(s);
//對客戶端的回覆
OutputStream ou = client.getOutputStream();
ou.write("我是服務器,收到了".getBytes());
server.close();
}
}
服務器端顯示收到了:
客戶端收到了服務器端的響應:
/***
* 練習
* 需求:建立一個文本轉換服務器
* 客戶端給服務端發送文本,服務端會將文本轉成大寫再返回給客戶端。
* 而且客戶端可以不斷的進行文本轉換。當客戶端輸入over時,轉換結束。
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class UpCaseClient1 {
public static void main(String[] args) throws IOException, IOException {
// TODO Auto-generated method stub
//創建socket服務
Socket client = new Socket("127.0.0.1", 10000);
//定義目的,將數據寫入到Socket輸出流。發給服務端
PrintWriter pw = new PrintWriter(client.getOutputStream(), true);
//定義讀取鍵盤數據的流對象
BufferedReader br1 =
new BufferedReader(new InputStreamReader(System.in));
//定義一個Socket讀取流,讀取服務端返回的大寫信息。
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
String ret = null;
while((s=br1.readLine())!=null)
{
//判斷,是“over”,結束
if(s.equals("over"))
{
break;
}else{
pw.println(s);//發送
ret = br2.readLine();//讀取返回的信息
System.out.println(ret);
}
}
client.close();
}
}
/**
* 服務器端
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpCaseServer1 {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//創建服務端的ServerSocket服務,並指定監聽端口
ServerSocket server = new ServerSocket(10000);
//獲取客戶端連接
Socket client1 = server.accept();
//讀取Socket流中的數據
BufferedReader br1 =
new BufferedReader(new InputStreamReader(client1.getInputStream()));
//將大寫數據寫入到Socket輸出流,併發送給客戶端。
PrintWriter pw = new PrintWriter(client1.getOutputStream(), true);
String s = null;
while((s = br1.readLine())!=null)
{
if(s.equals("over"))
{
break;
}else{
//將讀到數據轉換爲大寫後返回
s = s.toUpperCase();
pw.println(s);
System.out.println(s);
}
}
server.close();
}
}
這裏遇到的問題主要是:由於Writer類實現了Flushable接口,調用 flush 方法將所有已緩衝輸出寫入底層流。
所以,出現的兩邊都在等待的問題,第一點就是因爲,數據沒有實時刷新造成的,那麼需要調用flush方法手動刷進去。
錯誤案例,客戶端和服務器端都沒有反應
/**客戶端
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class UpCaseClient1 {
public static void main(String[] args) throws IOException, IOException {
//創建socket服務
Socket client = new Socket("127.0.0.1", 10000);
//定義目的,將數據寫入到Socket輸出流。發給服務端
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
//定義讀取鍵盤數據的流對象
BufferedReader br1 =
new BufferedReader(new InputStreamReader(System.in));
//定義一個Socket讀取流,讀取服務端返回的大寫信息。
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
String ret = null;
while((s=br1.readLine())!=null)
{
//判斷,是“over”,結束
if(s.equals("over"))
{
break;
}else{
bw.write(s+"\r\n");
//bw.newLine();//換行
//bw.flush();//此處是重點,如果此處不打開,那麼數據將會不再刷新出去,那麼客戶端和服務器端都在等待。
ret = br2.readLine();//讀取返回的信息
System.out.println(ret);
}
}
client.close();
}
}
/**
* 服務器端
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpCaseServer1 {
public static void main(String[] args) throws IOException {
//創建服務端的ServerSocket服務,並指定監聽端口
ServerSocket server = new ServerSocket(10000);
//獲取客戶端連接
Socket client1 = server.accept();
//讀取Socket流中的數據
BufferedReader br1 =
new BufferedReader(new InputStreamReader(client1.getInputStream()));
//將大寫數據寫入到Socket輸出流,併發送給客戶端。
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client1.getOutputStream()));
String s = null;
while((s = br1.readLine())!=null)
{
if(s.equals("over"))
{
break;
}else{
//將讀到數據轉換爲大寫後返回
s = s.toUpperCase();
//pw.println(s);
bw.write(s+"\r\n");
//bw.newLine();//換行
//bw.flush();//此處重點同上
System.out.println(s);
}
}
server.close();
}
}
問題:
上面的代碼運行不了,歸根結底就是因爲,Write繼承了接口 Flushable,那麼,每次服務器讀取一行,如果讀不到換行標誌,則等待;客戶端讀取鍵盤輸入,讀取後不使用flush方法刷出去,那麼積壓住,兩邊都等待,造成兩邊都是等待的問題
解決辦法:
1.手動添加換行標誌。
2.使用PrintWriter pw = new PrintWriter(client.getOutputStream(), true);treu的意思爲自動刷新,且提供prinlln()方法,免去了手動添加換行和刷新的麻煩。
/***
* 需求:向服務器上傳一個文件,服務返回一條信息
* 1、客戶端:
* 源:硬盤上的文件;目的:網絡設備,即網絡輸出流。
* 若操作的是文本數據,可選字符流,並加入高效緩衝區。若是媒體文件,用字節流。
* 2、服務端:
* 源:socket讀取流;目的:socket輸出流。
*
* 3、出現的問題:
* 現象:
* a、文件已經上傳成功了,但是沒有得到服務端的反饋信息。
* b、即使得到反饋信息,但得到的是null,而不是“上傳成功”的信息
*
* 原因:
* a、因爲客戶端將數據發送完畢後,由於雙發並沒有指定結束標誌,那麼服務端仍然在等待讀取數據,雙方都在等待。
* b、上個問題解決後,收到的不是指定信息而是null,是因爲服務端寫入數據後,需要刷新,才能將信息反饋給客服端。
*
* 解決a:
* 方法一:定義結束標記,先將結束標記發送給服務端,讓服務端接收到結束標記,然後再發送上傳的數據。但是這樣定義可能會發生定義的標記和文件中的數據重複,而導致提前結束。
* 方法二:定義時間戳,由於時間是唯一的,在發送數據前,先獲取時間,發送完後在結尾處寫上相同的時間戳,在服務端,接收數據前先接收一個時間戳,然後在循環中判斷時間戳以結束標記。
* 方法三:通過socket方法中的shutdownOutput(),關閉輸入流資源,從而結束傳輸流,以給定結束標記。通常用這個方法。
* 解決b:
* 方法一:若使用 BufferedWriter bwout=new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); 則需要調用flush()方法刷新
* 方法二:直接使用PrintWriter pw2 = new PrintWriter(client.getOutputStream(),true);自動刷新。
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/***
* 練習
* 需求:向服務器上傳一個文件,服務器返回一條信息
* @author LQX
*
*/
public class UpLoadClient {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
Socket client = new Socket("127.0.0.1", 10000);
//文件的讀取
BufferedReader br1 =
new BufferedReader(new FileReader("f:\\DemoClass.java"));
//定義目的,將數據寫入到Socket數據流中,併發送給服務端
//此處我設置自動刷新,方便操作,也可以使用BufferedReader,那麼後面就必須手動刷新
PrintWriter pw = new PrintWriter(client.getOutputStream(), true);
//讀取Socket讀取流中的數據
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
while((s = br1.readLine())!=null)
{
pw.println(s);
}
/**
* 通過socket方法中的shutdownOutput(),關閉輸入流資源,
* 從而結束傳輸流,以給定結束標記。通常用這個方法。
* 不加這個,那麼,服務器端沒有結束標誌,仍然等待客戶端發送數據。
*/
client.shutdownOutput();//關閉客戶端的輸出流。相當於給流中加入一個結束標記-1.
System.out.println(br2.readLine());
client.close();
}
}
/***
* 服務器端
* 主要使用了PrintWriter(OutputStream out, boolean autoFlush) 構造方法
* 直接指定了自動刷新,比較方便
*
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpLoadServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(10000);
//獲取客戶端鏈接
Socket client = server.accept();
//讀取Socket讀取流中的數據
BufferedReader br =
new BufferedReader(new InputStreamReader(client.getInputStream()));
//
PrintWriter pw1 = new PrintWriter(new FileWriter("f:\\Copy.txt"),true);
//將返回信息寫入Socket流的寫入流中
PrintWriter pw2 = new PrintWriter(client.getOutputStream(),true);
String s = null;
while((s=br.readLine())!=null)
{
System.out.println(s);
pw1.println(s);
}
pw2.println("接收成功!");
server.close();
}
}