參考資料:百度百科TCP協議
本文涉及Java IO流、異常的知識,可參考我的另外的部落格
1.概述
計算機網路相關知識:
OSI七層模型
一個報文可以類似於一封信,就像下圖(引自狂神說Java)非常生動。
網路程式設計的目的:資料交換、通訊
網路通訊的要素:
如何實現網路通訊?
通訊雙方地址:
- ip
- 埠號
網路協議:
HTTP, FTP, TCP, UDP 等等
1.1 IP
IP地址:InetAddress
(無構造器)
- 唯一定位一臺網路上計算機
- 127.0.0.1 :本機,localhost
- ip地址分類:ipv4(4個位元組)/ipv6(128位,8個無符號整陣列成),公網(ABCD類地址)/私網(區域網)
- 域名:記憶ip問題
主機名解析
主機名稱到IP地址解析是通過使用本地機器配置資訊和網路命名服務(如域名系統(DNS)和網路資訊服務(NIS))的組合來實現的。所使用的特定命名服務是預設配置的本地機器。對於任何主機名,返回其對應的IP地址。反向名稱解析意味著對於任何IP地址,返回與IP地址關聯的主機。
InetAddress
類提供了將主機名解析為其IP地址的方法,反之亦然。
InetAddress常用方法:
舉例:
public static void main(String[] args) throws UnknownHostException {
//查詢本機地址
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
System.out.println(inetAddress);
InetAddress i1 = InetAddress.getByName("localhost");
System.out.println(i1);
InetAddress i2 = InetAddress.getLocalHost();
System.out.println(i2);
//查詢網站ip
InetAddress i3 = InetAddress.getByName("www.baidu.com");
System.out.println(i3);
//常用方法
System.out.println(i3.getAddress()); //返回的是byte[],所以輸出了亂碼
System.out.println(i3.getCanonicalHostName());//規範的名字
System.out.println(i3.getHostAddress());//ip
System.out.println(i3.getHostName());//主機名
}
InetAddress
沒有構造器,所以需要呼叫靜態方法進行構造。上述程式碼結果為:
1.2 埠
埠表示計算機上的一個程式的程式
- 不同的程式有不同的埠號,用來區分軟體。
- 一般被規定為0~65535
- TCP埠和UDP埠,均有65536個,兩個互不衝突。單個協議下埠是不能衝突的,例:TCP佔用8080後,不能再次佔用此TCP埠了
- 埠分類:公有埠01023,HTTP:80,HTTPS:443,FTP:21,Telent:23。程式註冊的埠102449151,用來分配給使用者或者程式,Tomcat:8080,MySQL:3306,Oracle:1521。動態、私有:49152~65535,儘量不要用這裡的埠。
netstat -ano #這條命令用於檢視所有埠
netstat -ano|findstr "8080" #檢視指定的埠
tasklist|findstr "8696" #檢視指定埠的程式
以上均為Linux命令
InetSocketAddress
該類實現IP套接字地址(IP地址+埠號)它也可以是一對(主機名+埠號),在這種情況下將嘗試解析主機名。如果解決方案失敗,那麼該地址被認為是未解決的,但在某些情況下仍可以使用,例如通過代理連線。
它提供了用於繫結,連線或返回值的套接字所使用的不可變物件。
萬用字元是一個特殊的本地IP地址。 通常意味著“任何”,只能用於bind
操作。
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress inetSocketAddress1 = new InetSocketAddress("localhost",8080);
System.out.println(inetSocketAddress);
System.out.println(inetSocketAddress1);
System.out.println(inetSocketAddress.getAddress());
System.out.println(inetSocketAddress.getHostName());
System.out.println(inetSocketAddress.getPort());
以上為相關程式碼。
1.3 通訊協議
網路通訊協議可能涉及到:速率,傳輸位元速率,程式碼結構,傳輸控制等等
主要涉及的是以下兩個:
TCP:使用者傳輸協議(3次握手,確定返回資訊,以後網路相關知識具體說,不在本篇贅述)
UDP:使用者資料包協議(不確定返回資訊)
TCP和UDP對比
TCP就像打電話,需要連線,穩定
UDP就像發簡訊,不需要連線,發完即結束,不穩定
-
基於連線與無連線;
-
對系統資源的要求(TCP較多,UDP少);
-
UDP程式結構較簡單;
-
流模式與資料包模式 ;(從下面demo中即可看出)
-
TCP保證資料正確性,UDP可能丟包;
-
TCP保證資料順序,UDP不保證。
2. TCP協議
TCP三次握手的過程如下:
- 客戶端傳送SYN(SEQ=x)報文給伺服器端,進入SYN_SEND狀態。
- 伺服器端收到SYN報文,回應一個SYN (SEQ=y)ACK(ACK=x+1)報文,進入
SYN_RECV
狀態。 - 客戶端收到伺服器端的SYN報文,回應一個ACK(ACK=y+1)報文,進入
Established
狀態。
三次握手完成,TCP客戶端和伺服器端成功地建立連線,可以開始傳輸資料了。
TCP連線終止過程:
建立一個連線需要三次握手,而終止一個連線要經過四次握手,這是由TCP的半關閉(half-close)造成的。具體過程如上圖所示。
(1) 某個應用程式首先呼叫close,稱該端執行“主動關閉”(active close)。該端的TCP於是傳送一個FIN分節,表示資料傳送完畢。
(2) 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP確認。
注意:FIN的接收也作為一個檔案結束符(end-of-file)傳遞給接收端應用程式,放在已排隊等候該應用程式接收的任何其他資料之後,因為,FIN的接收意味著接收端應用程式在相應連線上再無額外資料可接收。
(3) 一段時間後,接收到這個檔案結束符的應用程式將呼叫close關閉它的套接字。這導致它的TCP也傳送一個FIN。
(4) 接收這個最終FIN的原傳送端TCP(即執行主動關閉的那一端)確認這個FIN。 [3]
既然每個方向都需要一個FIN和一個ACK,因此通常需要4個分節。
TCP協議相關資料參考自百度百科,計算機網路相關知識不再詳細描述。
2.1 TCP連線的實現
服務端:
public class TestTCPserver {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
Socket accept = serverSocket.accept();//等待client連線
InputStream is = accept.getInputStream();
//管道流,將一個輸入流通過管道轉化為一個合適的輸出流,不用管道流直接String可能會輸出亂碼
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while((len=is.read(buffer))!=-1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
baos.close();
is.close();
accept.close();
serverSocket.close();
//正式寫程式碼的過程中,一定要用try,catch,finally,為了程式碼的安全,出了事故容易判斷
}
}
用到了socket
類,其中涉及了IO流部分的知識。
客戶端:
public class TestTCPclient {
public static void main(String[] args) throws IOException {
InetAddress serverIp = InetAddress.getByName("127.0.0.1");
int port = 9999;
//建立一個socket連線,連線的是本機
Socket socket = new Socket(serverIp,port);
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
os.close();
socket.close();
}
}
注意:socket
是需要關閉的
-
socket
該類實現客戶端套接字(也稱為“套接字”)。套接字是兩臺機器之間通訊的端點。套接字的實際工作由SocketImpl
類的例項執行。 應用程式通過更改建立套接字實現的套接字工廠,可以配置自己建立適合本地防火牆的套接字。 -
ServerSocket
該類實現了伺服器套接字。 伺服器套接字等待通過網路進入的請求。 它根據該請求執行一些操作,然後可能將結果返回給請求者。
伺服器套接字的實際工作由SocketImpl
類的例項執行。 應用程式可以更改建立套接字實現的套接字工廠,以配置自己建立適合本地防火牆的套接字。
客戶端:連線伺服器socket
,傳送訊息
伺服器:建立服務的埠 ServerSocket
,等待使用者連線,接受使用者訊息
2.1 TCP實現檔案上傳
與訊息傳遞類似,只是IO操作稍微變了一下
服務端:
public class TCPserverdemo1 {
public static void main(String[] args) throws IOException {
//建立服務
ServerSocket serverSocket = new ServerSocket(9000);
//監聽客戶端的連線
Socket accept = serverSocket.accept(); //阻塞式監聽,會一定等待客戶端連線
InputStream is = accept.getInputStream();
FileOutputStream fos = new FileOutputStream(new File("copide1.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
is.close();
accept.close();
serverSocket.close();
}
}
客戶端:
public class TCPclientdemo1 {
public static void main(String[] args) throws IOException {
//建立一個socket連線
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9000);
//建立一個輸出流
OutputStream os = socket.getOutputStream(); //.getOutputStream獲得了一個SocketOutputStream例項
//讀取檔案
FileInputStream fis = new FileInputStream(new File("image.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len=fis.read(buffer))!=-1){
os.write(buffer,0,len); //涉及到BIO
}
fis.close();
os.close();
socket.close();
}
}
上面用的是位元組流,位元組緩衝流也可以使用。字元流不可以,因為可能會讀其他檔案的型別,位元組流比較穩妥。
伺服器接收完資訊後其實是可以返回訊息到客戶端的,socket
通訊:
服務端可增加:
accept.shutdownInput();
//通知客戶端已經接收完畢
OutputStream os = accept.getOutputStream();
os.write("ending".getBytes()); //傳送給客戶端
客戶端可增加:
socket.shutdownOutput();//通知伺服器傳輸完畢
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while((len2 = is.read(buffer))!=-1){
baos.write(buffer,0,len2);
}
System.out.println(baos);
客戶端讀入了服務端返回的ending
字串。
實現兩端通訊之後再決定是否進行其他操作,例如close()
等等。
3. UDP
無需連線,但是需要知道對方地址
3.1 UDP訊息傳送
主要依賴的是DatagramSocket
和DatagramPacket
DatagramSocket
此類表示用於傳送和接收資料包資料包的套接字。 資料包套接字是分組傳送服務的傳送或接收點。 在資料包套接字上傳送或接收的每個資料包都被單獨定址和路由。 從一個機器傳送到另一個機器的多個分組可以不同地路由,並且可以以任何順序到達。 在可能的情況下,新構建的DatagramSocket
啟用了SO_BROADCAST套接字選項,以允許廣播資料包的傳輸。 為了接收廣播資料包,DatagramSocket
應該繫結到萬用字元地址。 在一些實現中,當DatagramSocket
繫結到更具體的地址時,也可以接收廣播分組。DatagramPacket
該類表示資料包包。 資料包包用於實現無連線分組傳送服務。 僅基於該資料包中包含的資訊,每個訊息從一臺機器路由到另一臺機器。 從一臺機器傳送到另一臺機器的多個分組可能會有不同的路由,並且可能以任何順序到達。 包傳送不能保證。
以下實現UDP訊息傳送的一個簡單例子:
public class TestUDP1 {
//不需要連線伺服器
public static void main(String[] args) throws IOException {
//建立Socket
DatagramSocket datagramSocket = new DatagramSocket(); //為空將預設繫結一個可用埠
//建個資料包
String message = "hello,server";
InetAddress inetAddress = InetAddress.getByName("localhost");
int port = 9000;
int len = message.getBytes().length;
//資料,資料的長度起始位置,要傳送給誰
DatagramPacket datagramPacket = new DatagramPacket(message.getBytes(), 0,len, inetAddress, port);
datagramSocket.send(datagramPacket); //進行傳送
datagramSocket.close();
}
}
UDP傳送完就不需要其他操作了,為了驗證我們傳送的訊息,建立了一個接收端:
public class UDPaccept {
public static void main(String[] args) throws IOException {
//開放埠
DatagramSocket socket = new DatagramSocket(9000);
//接收資料包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); //接收並不需要對方地址和埠
socket.receive(packet); //阻塞式接收
System.out.println(packet.getAddress());
System.out.println(new String(packet.getData(),0,packet.getData().length));
socket.close();
}
}
其中收發訊息用到了兩個方法DatagramSocket.send()
和DatagramSocket.receive()
3.2 UDP聊天的實現(單向)
通過UDP協議進行一個傳送端和接收端的demo。當出現"bye"時,結束對話。
傳送端:
public class UDPsender {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(8080);//傳送埠
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
//BufferedInputStream bis = new BufferedInputStream(System.in);
int len;
while(true){
String data = reader.readLine(); //接收鍵盤輸入的資訊
DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress("localhost",6000));
socket.send(packet);
if(data.equals("bye"))
break;
}
reader.close();
socket.close();
}
}
接收端:
public class UDPreceiver {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(6000);//開啟接收埠
while(true){
byte[] container = new byte[1024]; //用來裝資料包內容
DatagramPacket packet = new DatagramPacket(container,0,container.length); //接受時候只需要一個空byte[]
socket.receive(packet);
byte[]data = packet.getData();
String datas = new String(data,0,data.length); //仍帶有byte[]的其他資訊,若轉為真正字串需trim()
System.out.println(datas);
if(datas.trim().equals("bye"))
break;
}
socket.close();
}
}
這個demo只實現了單向發訊息。後續將實現雙向傳送。
3.3 雙向聊天(多執行緒)
雙向聊天和以上內容相似,只需要每個端開啟兩個執行緒(接收執行緒和傳送執行緒),以下為程式碼演示:
public class TalkSend implements Runnable{ //傳送執行緒
DatagramSocket socket = null;
BufferedReader reader = null;
private String ToIP;
private int ToPort;
public TalkSend(String toString, int toPort) {
ToIP = toString;
ToPort = toPort;
}
@Override
public void run() {
try {
socket = new DatagramSocket();//傳送埠
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
e.printStackTrace();
}
//BufferedInputStream bis = new BufferedInputStream(System.in);
while(true){
String data = null;
try {
data = reader.readLine();
DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress(this.ToIP,this.ToPort));
socket.send(packet);
if(data.equals("bye"))
break;
} catch (IOException e) {
e.printStackTrace();
}
}
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
}
}
public class TalkReceive implements Runnable{ //接收執行緒
DatagramSocket socket = null;
private int FromPort;
private String Person;
public TalkReceive(int fromPort,String person) {
FromPort = fromPort;
Person = person;
}
@Override
public void run() {
try {
socket = new DatagramSocket(this.FromPort);//開啟接收埠
} catch (SocketException e) {
e.printStackTrace();
}
while(true){
byte[] container = new byte[1024]; //用來裝資料包內容
DatagramPacket packet = new DatagramPacket(container,0,container.length);
try {
socket.receive(packet);
} catch (IOException e) {
e.printStackTrace();
}
byte[]data = packet.getData();
String datas = new String(data,0,data.length);
System.out.println(Person+":"+datas);
if(datas.trim().equals("bye"))
break;
}
socket.close();
}
}
然後需要設定兩個端進行聊天:
new Thread(new TalkSend("localhost",8080)).start();
new Thread(new TalkReceive(6000,"老師")).start();
這裡設定為學生端,然後開啟兩個執行緒,設定傳送埠和接收埠
new Thread(new TalkSend("localhost",6000)).start();
new Thread(new TalkReceive(8080,"學生")).start();
這裡設定為老師端,然後開啟執行緒,設定埠,其實開啟了4個埠,因為學生端和老師端傳送執行緒中,DatagramSocket
預設繫結的還有兩個埠。
4. URL
統一資源定位符:定位網際網路上的某個資源
DNS域名解析: www.baidu.com ——》xxx.xxx.xxx.xxx
協議://ip地址:埠/專案名/資源
URL類的基本使用
URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=yuan&password=123");
System.out.println(url.getProtocol());//得到協議名
System.out.println(url.getHost());//主機
System.out.println(url.getPort());//埠
System.out.println(url.getPath());//檔案
System.out.println(url.getFile());//檔案全路徑
System.out.println(url.getQuery()); //得到url查詢的部分(引數)
Java萬物皆物件
下載一個URL資源
public class UrlDown {
public static void main(String[] args) throws IOException {
URL url = new URL("http://localhost:8080/gaoyuan/SecurityFile.txt");
//連線到這個資源
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); //需要轉換型別,因為返回的是URPConnection
InputStream is = urlConnection.getInputStream();
FileOutputStream fos = new FileOutputStream("SecurityFile.txt");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
is.close();
urlConnection.disconnect();//斷開連線
}
}
這是開啟Tomcat
伺服器後,把一個檔案新增到相應目錄之後下載的。
網路程式設計基礎部分結束,例如:計算機網路,BIO等知識將會在後續部落格中釋出。