JAVA網絡編程入門
軟件結構
-
C/S結構
-
B/S結構
無論哪一種結構,都離不開網絡的支持。網絡編程,就是在網絡的條件下實現機器間的通信的過程
網絡通信協議
網絡通信協議:通信雙方必須同時遵守才能完成數據交換
UDP:無連接性,數據被限制在64kb,適用於丟包問題不太大的情況,效率高
TCP:面向連接,可靠無差錯,三次握手
網絡編程三要素:協議,IP地址和端口號
-
ip地址
查看本機Ip地址:控制台輸入ipconfig
查看是否可以和某一台計算機進行網絡交換:
ping ip地址
-
端口號
用協議+IP地址+端口號可以唯一的表示網絡中的進程並進行進程間的通信了
軟件在啟動時可以和操作系統要指定的端口號,或者由操作系統分配隨機的端口號
端口號由兩個字節組成,范圍為0-65535
TCP通信程序
TCP通信可以實現兩台計算機之間的數據交互,通信的兩端要嚴格區分客戶端和服務器端
客戶端和服務器端進行一個數據交流,一共需要4個IO流對象:
? Socket s1=server.accept();//獲取客戶端的流對象
java.net
套接字(Socket):包含了ip地址和端口號的網絡單位(可以借此識別設備和進程)
- 客戶端
這里使用Socket的輸入流來讀,輸出流來寫,不要混了
- 服務器端
服務器端必須明確知道是哪個客戶端請求的服務器,所以要使用accept方法
一個TCP通信的小例子:
TCPServer.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import org.omg.CORBA.portable.InputStream;
/*
*@author JiaDing
*服務器端
*/
public class TCPServer {
public static void main(String[]args)throws IOException{
//創建使用特定端口的對象
ServerSocket server=new ServerSocket(8888);
Socket socket=server.accept();
java.io.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("收到謝謝".getBytes());//要將字符串轉換為字符數組才行
socket.close();//銷毀socket對象
server.close();//銷毀server對象
; }
}
TCPClient.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/*
*@author JiaDing
*TCP通信的客戶端
*使用host(服務器主機的名稱/IP地址)和port(端口號)的構造方法
*getOutputStream 返回套接字的輸出流,使用其write方法給服務器發送數據
*getInputStream 返回套接字的輸入流,使用其read方法讀取服務器回寫數據
*close關閉套接字
*/
public class TCPClient {
public static void main (String[] args) throws IOException {
Socket socket=new Socket("127.0.0.1",8888);
OutputStream os=socket.getOutputStream();
os.write("你好服務器".getBytes());
InputStream is=socket.getInputStream();
byte[]bytes=new byte[1024];
int len=is.read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
TCP通信往往是按字節處理而不是字符處理,因為不是所有類型都是字符
127.0.0.1是什么地址?*
127.0.0.1/8整個都是環回地址,用來測試本機的TCP/IP協議棧,發往這段A類地址數據包不會出網卡,網絡設備不會對其做路由。
實例:文件上傳
Client.java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/*
*@author JiaDing
*文件上傳案例的客戶端
*/
public class Client {
public static void main(String[]args)throws IOException {
FileInputStream fis=new FileInputStream("E:\\out.txt");
Socket socket=new Socket("127.0.0.1",8888);
java.io.OutputStream os=socket.getOutputStream();
byte[] re=new byte[1024];//准備了1kb的大小
int len=0;
while((len=fis.read(re))!=-1) {//這里一定要給len=fis.read(re)加一個括號,否則兩個等號無法區分優先級
os.write(re,0,len);
}
/*
* 注意!這里有一個很重要的問題!
* 由於我們的判斷條件是len!=-1,所以我們無法將文件的結束標記上傳過去,這樣服務器會一直處於等待狀態,造成阻塞
* 而對應的,由於服務器阻塞,所以無法執行回顯,is.read()也就無法讀入數據,同樣陷入阻塞
* 解決辦法就是在客戶端利用套接字的方法人工結束輸出流
*/
socket.shutdownOutput();
InputStream is=socket.getInputStream();
while((len=is.read(re))!=-1) {//這里一定要給len=fis.read(re)加一個括號,否則兩個等號無法區分優先級
System.out.println(new String(re,0,len));
}
fis.close();
socket.close();
}
}
Server.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
*@author JiaDing
*文件上傳案例的服務器端
*/
public class Server {
public static void main(String[]args) throws IOException{
ServerSocket ss=new ServerSocket(8888);
Socket socket=ss.accept();
File file=new File("E:\\update");
if(!file.exists())
file.mkdir();
byte[] by=new byte[1024];
InputStream is=socket.getInputStream();
FileOutputStream out=new FileOutputStream("E:\\update\\t1.txt");
int len=0;
while(len!=-1) {
out.write(by, 0, len);
len=is.read(by);
}
OutputStream os=socket.getOutputStream();
os.write("Update is finished!".getBytes());
socket.close();
out.close();
ss.close();
}
}
優化:
- 名稱寫死了:自定義服務器端命名規則,防止同名文件被覆蓋
//自定義文件名規則:域名+毫秒值+隨機數
//毫秒值廣泛應用在服務器端文件名保存上
String name="jd"+System.currentTimeMillis()+new Random().nextInt(999999)+".txt";
FileOutputStream out=new FileOutputStream("E:\\update\\"+name);
- 服務器上傳后就停止:讓服務器一直處於監聽狀態:使用死循環,服務器不關閉
B\S服務器的模擬
服務器代碼-返回請求版:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
*@author JiaDing
*BS版本的TCP服務器
*/
public class Server {
public static void main(String[]args)throws IOException{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
InputStream is=socket.getInputStream();
byte[]by=new byte[1024];
int len=0;
while((len=is.read(by))!=-1) {
System.out.println(new String(by,0,len));
}
}
}
訪問時要指明文件夾名\要訪問的資源名,如果端口不是80還要在IP后面加上端口號
服務器端結果(其實就是客戶端的請求信息):
GET /src/Test1.html HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3
Sec-Fetch-Site: none
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
服務器應該回寫應該網頁(文件),我們需要讀取文件地址,地址已經包含在了請求第一行
可以使用BufferedReader中的方法readList讀取第一行
服務器代碼—返回網頁版:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
*@author JiaDing
*BS版本的TCP服務器
*/
public class Server {
public static void main(String[]args)throws IOException{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
InputStream is=socket.getInputStream();
/*byte[]by=new byte[1024];
int len=0;
while((len=is.read(by))!=-1) {
System.out.println(new String(by,0,len));
}*/
//把is網絡字節輸入流對象轉換為字符緩存輸入流
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String line=br.readLine();
String htmlpath=line.split(" ")[1].substring(1);//取第一行中間的地址,去掉最前面的、
//System.out.println(htmlpath);
FileInputStream fis=new FileInputStream(htmlpath);
OutputStream os=socket.getOutputStream();
//寫入HTTP協議響應頭,固定寫法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
//必須寫入空行,否則瀏覽器不解析
os.write("\r\n".getBytes());
int len=0;
byte[]by=new byte[1024];
while((len=fis.read(by))!=-1) {
os.write(by,0,len);
}
fis.close();
socket.close();
server.close();
}
}
-
為什么服務器要是多線程呢?
如果不采用多線程,那么服務器只能線型地處理瀏覽器的請求,瀏覽器上的資源只能一個一個打開,體驗會很差
采樣頭多線程之后的代碼:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
*@author JiaDing
*BS版本的TCP服務器
*/
public class Server {
public static void main(String[]args)throws IOException{
ServerSocket server=new ServerSocket(8080);
while(true) {
Socket socket=server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream is=socket.getInputStream();
//把is網絡字節輸入流對象轉換為字符緩存輸入流
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String line=br.readLine();
String htmlpath=line.split(" ")[1].substring(1);//取第一行中間的地址,去掉最前面的、
//System.out.println(htmlpath);
FileInputStream fis=new FileInputStream(htmlpath);
OutputStream os=socket.getOutputStream();
//寫入HTTP協議響應頭,固定寫法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
//必須寫入空行,否則瀏覽器不解析
os.write("\r\n".getBytes());
int len=0;
byte[]by=new byte[1024];
while((len=fis.read(by))!=-1) {
os.write(by,0,len);
}
fis.close();
socket.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
注意這里會報一個錯誤:java.io.FileNotFoundException: favicon.ico。這是因為瀏覽器會自動訪問網站的圖標而我們的網站沒有的原因,這個圖標必須放在根目錄,例如如果訪問的地址是“http://127.0.0.1/src/Test1.html”,那么就要放在src文件夾的外面
另外,實驗了一下,將服務器的端口改成了80好像並沒有什么問題,因為服務器監聽的是本機的80端口,而如果我們訪問外網的話,應該是訪問外網網站服務器的80端口,兩者並不沖突。部署在本機上的服務器只能監聽到訪問本機且本端口的請求。所以,網頁中如果有外鏈,那么打開這個外鏈的過程和我們的服務器是沒有絲毫關系的,我們自建的服務器的好壞不會對其造成影響