參考資料:百度百科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等知識將會在后續博客中發布。
