Java 網(wǎng)絡(luò)編程

1 網(wǎng)絡(luò)通信協(xié)議

??通過計(jì)算機(jī)網(wǎng)絡(luò)可以使多臺計(jì)算機(jī)實(shí)現(xiàn)連接,位于同一個網(wǎng)絡(luò)中的計(jì)算機(jī)在進(jìn)行連接和通信時(shí)需要遵守一定的規(guī)則,這就好比在道路中行駛的汽車一定要遵守交通規(guī)則一樣。在計(jì)算機(jī)網(wǎng)絡(luò)中,這些連接和通信的規(guī)則被稱為網(wǎng)絡(luò)通信協(xié)議,它對數(shù)據(jù)的傳輸格式、傳輸速率、傳輸步驟等做了統(tǒng)一規(guī)定,通信雙方必須同時(shí)遵守才能完成數(shù)據(jù)交換。
??網(wǎng)絡(luò)通信協(xié)議有很多種,目前應(yīng)用最廣泛的是TCP/IP協(xié)議(Transmission Control Protocal/Internet Protoal傳輸控制協(xié)議/英特網(wǎng)互聯(lián)協(xié)議),它是一個包括TCP協(xié)議和IP協(xié)議,UDP(User Datagram Protocol)協(xié)議和其它一些協(xié)議的協(xié)議組,在學(xué)習(xí)具體協(xié)議之前首先了解一下TCP/IP協(xié)議組的層次結(jié)構(gòu)。
??在進(jìn)行數(shù)據(jù)傳輸時(shí),要求發(fā)送的數(shù)據(jù)與收到的數(shù)據(jù)完全一樣,這時(shí),就需要在原有的數(shù)據(jù)上添加很多信息,以保證數(shù)據(jù)在傳輸過程中數(shù)據(jù)格式完全一致。TCP/IP協(xié)議的層次結(jié)構(gòu)比較簡單,共分為四層,如圖所示。

??上圖中,TCP/IP協(xié)議中的四層分別是應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和鏈路層,每層分別負(fù)責(zé)不同的通信功能,接下來針對這四層進(jìn)行詳細(xì)地講解。
??鏈路層:鏈路層是用于定義物理傳輸通道,通常是對某些網(wǎng)絡(luò)連接設(shè)備的驅(qū)動協(xié)議,例如針對光纖、網(wǎng)線提供的驅(qū)動。
??網(wǎng)絡(luò)層:網(wǎng)絡(luò)層是整個TCP/IP協(xié)議的核心,它主要用于將傳輸?shù)臄?shù)據(jù)進(jìn)行分組,將分組數(shù)據(jù)發(fā)送到目標(biāo)計(jì)算機(jī)或者網(wǎng)絡(luò)。
??傳輸層:主要使網(wǎng)絡(luò)程序進(jìn)行通信,在進(jìn)行網(wǎng)絡(luò)通信時(shí),可以采用TCP協(xié)議,也可以采用UDP協(xié)議。
??應(yīng)用層:主要負(fù)責(zé)應(yīng)用程序的協(xié)議,例如HTTP協(xié)議、FTP協(xié)議等。

1.1 IP地址和端口號

??要想使網(wǎng)絡(luò)中的計(jì)算機(jī)能夠進(jìn)行通信,必須為每臺計(jì)算機(jī)指定一個標(biāo)識號,通過這個標(biāo)識號來指定接受數(shù)據(jù)的計(jì)算機(jī)或者發(fā)送數(shù)據(jù)的計(jì)算機(jī)。
??在TCP/IP協(xié)議中,這個標(biāo)識號就是IP地址,它可以唯一標(biāo)識一臺計(jì)算機(jī),目前,IP地址廣泛使用的版本是IPv4,它是由4個字節(jié)大小的二進(jìn)制數(shù)來表示,如:00001010000000000000000000000001。由于二進(jìn)制形式表示的IP地址非常不便記憶和處理,因此通常會將IP地址寫成十進(jìn)制的形式,每個字節(jié)用一個十進(jìn)制數(shù)字(0-255)表示,數(shù)字間用符號“.”分開,如 “192.168.1.100”。
??隨著計(jì)算機(jī)網(wǎng)絡(luò)規(guī)模的不斷擴(kuò)大,對IP地址的需求也越來越多,IPV4這種用4個字節(jié)表示的IP地址面臨枯竭,因此IPv6 便應(yīng)運(yùn)而生了,IPv6使用16個字節(jié)表示IP地址,它所擁有的地址容量約是IPv4的8×1028倍,達(dá)到2128個(算上全零的),這樣就解決了網(wǎng)絡(luò)地址資源數(shù)量不夠的問題。
??通過IP地址可以連接到指定計(jì)算機(jī),但如果想訪問目標(biāo)計(jì)算機(jī)中的某個應(yīng)用程序,還需要指定端口號。在計(jì)算機(jī)中,不同的應(yīng)用程序是通過端口號區(qū)分的。端口號是用兩個字節(jié)(16位的二進(jìn)制數(shù))表示的,它的取值范圍是065535,其中,01023之間的端口號用于一些知名的網(wǎng)絡(luò)服務(wù)和應(yīng)用,用戶的普通應(yīng)用程序需要使用1024以上的端口號,從而避免端口號被另外一個應(yīng)用或服務(wù)所占用。
??接下來通過一個圖例來描述IP地址和端口號的作用,如下圖所示。

??從上圖中可以清楚地看到,位于網(wǎng)絡(luò)中一臺計(jì)算機(jī)可以通過IP地址去訪問另一臺計(jì)算機(jī),并通過端口號訪問目標(biāo)計(jì)算機(jī)中的某個應(yīng)用程序。

1.2 InetAddress

??了解了IP地址的作用,我們看學(xué)習(xí)下JDK中提供了一個InetAdderss類,該類用于封裝一個IP地址,并提供了一系列與IP地址相關(guān)的方法,下表中列出了InetAddress類的一些常用方法。

返回 方法描述
static InetAddress getByName(String host)在給定主機(jī)名的情況下確認(rèn)主機(jī)的IP地址
static InetAddress getLocalHost(String host)返回本地主機(jī)
String getHostName()獲取次IP地址的主機(jī)名
String getHostAddress()返回IP地址字符串

??上圖中,列舉了InetAddress的四個常用方法。其中,前兩個方法用于獲得該類的實(shí)例對象,第一個方法用于獲得表示指定主機(jī)的InetAddress對象,第二個方法用于獲得表示本地的InetAddress對象。通過InetAddress對象便可獲取指定主機(jī)名,IP地址等,接下來通過一個案例來演示InetAddress的常用方法,如下所示。
TestDemo.java

package com.qtw.api;
import java.net.InetAddress;

public class TestDemo {
    public static void main(String[] args) throws Exception{
        InetAddress local = InetAddress.getLocalHost();
        InetAddress remote = InetAddress.getByName("www.baidu.com");
        System.out.println("本機(jī)的IP地址:" + local.getHostAddress());
        System.out.println("baidu的IP地址:" + remote.getHostAddress());
        System.out.println("baidu的主機(jī)名為:" + remote.getHostName());
    }
}

2 UDP與TCP協(xié)議

??在介紹TCP/IP結(jié)構(gòu)時(shí),提到傳輸層的兩個重要的高級協(xié)議,分別是UDP和TCP,其中UDP是User Datagram Protocol的簡稱,稱為用戶數(shù)據(jù)報(bào)協(xié)議,TCP是Transmission Control Protocol的簡稱,稱為傳輸控制協(xié)議。

2.1 UDP協(xié)議

??UDP是無連接通信協(xié)議,即在數(shù)據(jù)傳輸時(shí),數(shù)據(jù)的發(fā)送端和接收端不建立邏輯連接。簡單來說,當(dāng)一臺計(jì)算機(jī)向另外一臺計(jì)算機(jī)發(fā)送數(shù)據(jù)時(shí),發(fā)送端不會確認(rèn)接收端是否存在,就會發(fā)出數(shù)據(jù),同樣接收端在收到數(shù)據(jù)時(shí),也不會向發(fā)送端反饋是否收到數(shù)據(jù)。
??由于使用UDP協(xié)議消耗資源小,通信效率高,所以通常都會用于音頻、視頻和普通數(shù)據(jù)的傳輸例如視頻會議都使用UDP協(xié)議,因?yàn)檫@種情況即使偶爾丟失一兩個數(shù)據(jù)包,也不會對接收結(jié)果產(chǎn)生太大影響。
??但是在使用UDP協(xié)議傳送數(shù)據(jù)時(shí),由于UDP的面向無連接性,不能保證數(shù)據(jù)的完整性,因此在傳輸重要數(shù)據(jù)時(shí)不建議使用UDP協(xié)議。UDP的交換過程如下圖所示。

1.1 TCP協(xié)議

??TCP協(xié)議是面向連接的通信協(xié)議,即在傳輸數(shù)據(jù)前先在發(fā)送端和接收端建立邏輯連接,然后再傳輸數(shù)據(jù),它提供了兩臺計(jì)算機(jī)之間可靠無差錯的數(shù)據(jù)傳輸。在TCP連接中必須要明確客戶端與服務(wù)器端,由客戶端向服務(wù)端發(fā)出連接請求,每次連接的創(chuàng)建都需要經(jīng)過“三次握手”。第一次握手,客戶端向服務(wù)器端發(fā)出連接請求,等待服務(wù)器確認(rèn),第二次握手,服務(wù)器端向客戶端回送一個響應(yīng),通知客戶端收到了連接請求,第三次握手,客戶端再次向服務(wù)器端發(fā)送確認(rèn)信息,確認(rèn)連接。整個交互過程如下圖所示。

??由于TCP協(xié)議的面向連接特性,它可以保證傳輸數(shù)據(jù)的安全性,所以是一個被廣泛采用的協(xié)議,例如在下載文件時(shí),如果數(shù)據(jù)接收不完整,將會導(dǎo)致文件數(shù)據(jù)丟失而不能被打開,因此,下載文件時(shí)必須采用TCP協(xié)議。

3 UDP通信

3.1 DatagramPacket

??前面介紹了UDP是一種面向無連接的協(xié)議,因此,在通信時(shí)發(fā)送端和接收端不用建立連接。UDP通信的過程就像是貨運(yùn)公司在兩個碼頭間發(fā)送貨物一樣。在碼頭發(fā)送和接收貨物時(shí)都需要使用集裝箱來裝載貨物,UDP通信也是一樣,發(fā)送和接收的數(shù)據(jù)也需要使用“集裝箱”進(jìn)行打包,為此JDK中提供了一個DatagramPacket類,該類的實(shí)例對象就相當(dāng)于一個集裝箱,用于封裝UDP通信中發(fā)送或者接收的數(shù)據(jù)。
??想要創(chuàng)建一個DatagramPacket對象,首先需要了解一下它的構(gòu)造方法。在創(chuàng)建發(fā)送端和接收端的DatagramPacket對象時(shí),使用的構(gòu)造方法有所不同,接收端的構(gòu)造方法只需要接收一個字節(jié)數(shù)組來存放接收到的數(shù)據(jù),而發(fā)送端的構(gòu)造方法不但要接收存放了發(fā)送數(shù)據(jù)的字節(jié)數(shù)組,還需要指定發(fā)送端IP地址和端口號。
??接下來根據(jù)API文檔的內(nèi)容,對DatagramPacket的構(gòu)造方法進(jìn)行逐一詳細(xì)地講解。

??使用該構(gòu)造方法在創(chuàng)建DatagramPacket對象時(shí),指定了封裝數(shù)據(jù)的字節(jié)數(shù)組和數(shù)據(jù)的大小,沒有指定IP地址和端口號。很明顯,這樣的對象只能用于接收端,不能用于發(fā)送端。因?yàn)榘l(fā)送端一定要明確指出數(shù)據(jù)的目的地(ip地址和端口號),而接收端不需要明確知道數(shù)據(jù)的來源,只需要接收到數(shù)據(jù)即可。

??使用該構(gòu)造方法在創(chuàng)建DatagramPacket對象時(shí),不僅指定了封裝數(shù)據(jù)的字節(jié)數(shù)組和數(shù)據(jù)的大小,還指定了數(shù)據(jù)包的目標(biāo)IP地址(addr)和端口號(port)。該對象通常用于發(fā)送端,因?yàn)樵诎l(fā)送數(shù)據(jù)時(shí)必須指定接收端的IP地址和端口號,就好像發(fā)送貨物的集裝箱上面必須標(biāo)明接收人的地址一樣。
??上面我們講解了DatagramPacket的構(gòu)造方法,接下來對DatagramPacket類中的常用方法進(jìn)行詳細(xì)地講解,如下表所示。

3.2 DatagramSocket

??DatagramPacket數(shù)據(jù)包的作用就如同是“集裝箱”,可以將發(fā)送端或者接收端的數(shù)據(jù)封裝起來。然而運(yùn)輸貨物只有“集裝箱”是不夠的,還需要有碼頭。在程序中需要實(shí)現(xiàn)通信只有DatagramPacket數(shù)據(jù)包也同樣不行,為此JDK中提供的一個DatagramSocket類。DatagramSocket類的作用就類似于碼頭,使用這個類的實(shí)例對象就可以發(fā)送和接收DatagramPacket數(shù)據(jù)包,發(fā)送數(shù)據(jù)的過程如下圖所示。

??在創(chuàng)建發(fā)送端和接收端的DatagramSocket對象時(shí),使用的構(gòu)造方法也有所不同,下面對DatagramSocket類中常用的構(gòu)造方法進(jìn)行講解。

??該構(gòu)造方法用于創(chuàng)建發(fā)送端的DatagramSocket對象,在創(chuàng)建DatagramSocket對象時(shí),并沒有指定端口號,此時(shí),系統(tǒng)會分配一個沒有被其它網(wǎng)絡(luò)程序所使用的端口號。

??該構(gòu)造方法既可用于創(chuàng)建接收端的DatagramSocket對象,又可以創(chuàng)建發(fā)送端的DatagramSocket對象,在創(chuàng)建接收端的DatagramSocket對象時(shí),必須要指定一個端口號,這樣就可以監(jiān)聽指定的端口。
??上面我們講解了DatagramSocket的構(gòu)造方法,接下來對DatagramSocket類中的常用方法進(jìn)行詳細(xì)地講解。

3.3 UDP網(wǎng)絡(luò)程序

??講解了DatagramPacket和DatagramSocket的作用,接下來通過一個案例來學(xué)習(xí)一下它們在程序中的具體用法。
??下圖為UDP發(fā)送端與接收端交互圖解

要實(shí)現(xiàn)UDP通信需要創(chuàng)建一個發(fā)送端程序和一個接收端程序,很明顯,在通信時(shí)只有接收端程序先運(yùn)行,才能避免因發(fā)送端發(fā)送的數(shù)據(jù)無法接收,而造成數(shù)據(jù)丟失。因此,首先需要來完成接收端程序的編寫。
UDP完成數(shù)據(jù)的發(fā)送

package com.qtw.api;
/*
 * 發(fā)送端
 * 1,創(chuàng)建DatagramSocket對象
 * 2,創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
 * 3,發(fā)送數(shù)據(jù)
 * 4,釋放流資源
 */

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPSend {
    public static void main(String[] args) throws IOException {
        //1,創(chuàng)建DatagramSocket對象
        DatagramSocket sendSocket = new DatagramSocket();
        //2,創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
        //public DatagramPacket(byte[] buf, int length, InetAddress address,  int port)
        //構(gòu)造數(shù)據(jù)報(bào)包,用來將長度為 length 的包發(fā)送到指定主機(jī)上的指定端口號。
        byte[] buffer = "hello,UDP".getBytes();
        DatagramPacket dp = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("192.168.134.1"), 12306);
        //3,發(fā)送數(shù)據(jù)
        //public void send(DatagramPacket p) 從此套接字發(fā)送數(shù)據(jù)報(bào)包
        sendSocket.send(dp);
        //4,釋放流資源
        sendSocket.close();
    }
}

UDP完成數(shù)據(jù)的接收

package com.qtw.api;
/*
 * UDP接收端
 *
 * 1,創(chuàng)建DatagramSocket對象
 * 2,創(chuàng)建DatagramPacket對象
 * 3,接收數(shù)據(jù)存儲到DatagramPacket對象中
 * 4,獲取DatagramPacket對象的內(nèi)容
 * 5,釋放流資源
 */

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPReceive {
    public static void main(String[] args) throws IOException {
        //1,創(chuàng)建DatagramSocket對象,并指定端口號
        DatagramSocket receiveSocket = new DatagramSocket(12306);
        //2,創(chuàng)建DatagramPacket對象, 創(chuàng)建一個空的倉庫
        byte[] buffer = new byte[1024];
        DatagramPacket dp = new DatagramPacket(buffer, 1024);
        //3,接收數(shù)據(jù)存儲到DatagramPacket對象中
        receiveSocket.receive(dp);
        //4,獲取DatagramPacket對象的內(nèi)容
        //誰發(fā)來的數(shù)據(jù)  getAddress()
        InetAddress ipAddress = dp.getAddress();
        String ip = ipAddress.getHostAddress();//獲取到了IP地址
        //發(fā)來了什么數(shù)據(jù)  getData()
        byte[] data = dp.getData();
        //發(fā)來了多少數(shù)據(jù) getLenth()
        int length = dp.getLength();
        //顯示收到的數(shù)據(jù)
        String dataStr = new String(data,0,length);
        System.out.println("IP地址:"+ip+ "數(shù)據(jù)是"+ dataStr);
        //5,釋放流資源
        receiveSocket.close();
    }
}

4 TCP通信

??TCP通信同UDP通信一樣,都能實(shí)現(xiàn)兩臺計(jì)算機(jī)之間的通信,通信的兩端都需要創(chuàng)建socket對象。
??區(qū)別在于,UDP中只有發(fā)送端和接收端,不區(qū)分客戶端與服務(wù)器端,計(jì)算機(jī)之間可以任意地發(fā)送數(shù)據(jù)。
??而TCP通信是嚴(yán)格區(qū)分客戶端與服務(wù)器端的,在通信時(shí),必須先由客戶端去連接服務(wù)器端才能實(shí)現(xiàn)通信,服務(wù)器端不可以主動連接客戶端,并且服務(wù)器端程序需要事先啟動,等待客戶端的連接。
??在JDK中提供了兩個類用于實(shí)現(xiàn)TCP程序,一個是ServerSocket類,用于表示服務(wù)器端,一個是Socket類,用于表示客戶端。
??通信時(shí),首先創(chuàng)建代表服務(wù)器端的ServerSocket對象,該對象相當(dāng)于開啟一個服務(wù),并等待客戶端的連接,然后創(chuàng)建代表客戶端的Socket對象向服務(wù)器端發(fā)出連接請求,服務(wù)器端響應(yīng)請求,兩者建立連接開始通信。

4.1 ServerSocket

??通過前面的學(xué)習(xí)知道,在開發(fā)TCP程序時(shí),首先需要創(chuàng)建服務(wù)器端程序。JDK的java.net包中提供了一個ServerSocket類,該類的實(shí)例對象可以實(shí)現(xiàn)一個服務(wù)器段的程序。通過查閱API文檔可知,ServerSocket類提供了多種構(gòu)造方法,接下來就對ServerSocket的構(gòu)造方法進(jìn)行逐一地講解。
構(gòu)造方法:

ServerSocket(int port)  //創(chuàng)建綁定到特定端口的套接字對象

??使用該構(gòu)造方法在創(chuàng)建ServerSocket對象時(shí),就可以將其綁定到一個指定的端口號上(參數(shù)port就是端口號)。
??接下來學(xué)習(xí)一下ServerSocket的常用方法,如表所示。

方法摘要
Socket accept()偵聽并接受套接字的連接
InetAddress getInetAddress()返回此服務(wù)器套接字的本地地址

??ServerSocket對象負(fù)責(zé)監(jiān)聽某臺計(jì)算機(jī)的某個端口號,在創(chuàng)建ServerSocket對象后,需要繼續(xù)調(diào)用該對象的accept()方法,接收來自客戶端的請求。當(dāng)執(zhí)行了accept()方法之后,服務(wù)器端程序會發(fā)生阻塞,直到客戶端發(fā)出連接請求,accept()方法才會返回一個Scoket對象用于和客戶端實(shí)現(xiàn)通信,程序才能繼續(xù)向下執(zhí)行。

4.2 Socket

??講解了ServerSocket對象可以實(shí)現(xiàn)服務(wù)端程序,但只實(shí)現(xiàn)服務(wù)器端程序還不能完成通信,此時(shí)還需要一個客戶端程序與之交互,為此JDK提供了一個Socket類,用于實(shí)現(xiàn)TCP客戶端程序。
??通過查閱API文檔可知Socket類同樣提供了多種構(gòu)造方法,接下來就對Socket的常用構(gòu)造方法進(jìn)行詳細(xì)講解。

構(gòu)造方法
Socket(String host,int port) 創(chuàng)建一個流套接字并將其連接到指定的主機(jī)上的指定端口
Socket(InetAddress address,int port) 創(chuàng)建一個流套接字并將其連接到指定的主機(jī)上的指定端口

在以上Socket的構(gòu)造方法中,最常用的是第一個構(gòu)造方法。
接下來學(xué)習(xí)一下Socket的常用方法,如表所示。

方法聲明 功能描述
int getPort() 該方法返回一個int類型對象,該對象是Socket對象與服務(wù)器端連接的端口號
InetAddress getLocalAddress() 該方法用于獲取Socket對象綁定的本地IP地址,并將IP地址封裝成InetAddress類型的對象返回
void close() 該方法用于關(guān)閉Socket連接,結(jié)束本次通信。在關(guān)閉socket之前,應(yīng)將與socket相關(guān)的所有的輸入/輸出流全部關(guān)閉,這是因?yàn)橐粋€良好的程序應(yīng)該在執(zhí)行完畢時(shí)釋放所有的資源
InputStream getInputStream() 該方法返回一個InputStream類型的輸入流對象,如果該對象是由服務(wù)器端的Socket返回,就用于讀取客戶端發(fā)送的數(shù)據(jù),反之,用于讀取服務(wù)器端發(fā)送的數(shù)據(jù)
OutputStream getOutputStream() 該方法返回一個OutputStream類型的輸出流對象,如果該對象是由服務(wù)器端的Socket返回,就用于向客戶端發(fā)送數(shù)據(jù),反之,用于向服務(wù)器端發(fā)送數(shù)據(jù)

??在Socket類的常用方法中,getInputStream()和getOutStream()方法分別用于獲取輸入流和輸出流。當(dāng)客戶端和服務(wù)端建立連接后,數(shù)據(jù)是以IO流的形式進(jìn)行交互的,從而實(shí)現(xiàn)通信。
??接下來通過一張圖來描述服務(wù)器端和客戶端的數(shù)據(jù)傳輸,如下圖所示。

4.3 簡單的TCP網(wǎng)絡(luò)程序

??要實(shí)現(xiàn)TCP通信需要創(chuàng)建一個服務(wù)器端程序和一個客戶端程序,為了保證數(shù)據(jù)傳輸?shù)陌踩?,首先需要?shí)現(xiàn)服務(wù)器端程序。
服務(wù)端TCPServer.java

package com.qtw.api;

/* TCP 服務(wù)器端
*實(shí)現(xiàn)TCP服務(wù)器程序
* 表示服務(wù)器程序的類 java.net.ServerSocket
* 構(gòu)造方法
*       ServerSocket(int port) 傳遞端口號
* 注意:必須要獲得客戶端的套接字對象Socket,方法accept()
*
*/

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8888);
        //調(diào)用服務(wù)器套接字對象的方法accept()獲取客戶端的套接字對象
        Socket socket = server.accept();
        System.out.println(socket);
        //通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取客戶端發(fā)送的數(shù)據(jù)
        InputStream in = socket.getInputStream();
        byte[] data = new byte[1024];
        int len = in.read(data);
        System.out.println(new String(data,0,len));

        //服務(wù)器向客戶端回?cái)?shù)據(jù),字節(jié)輸出流,通過客戶端套接字對象獲取字節(jié)輸出流
        OutputStream out = socket.getOutputStream();
        out.write("服務(wù)器收到數(shù)據(jù)".getBytes());

        socket.close();
        server.close();
    }
}

完成了服務(wù)器端程序的編寫,接下來編寫客戶端程序。
客戶端TCPClient.java

package com.qtw.api;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/*
 * 實(shí)現(xiàn)TCP客戶端,連接到服務(wù)器,和服務(wù)器實(shí)現(xiàn)數(shù)據(jù)交換
 * 實(shí)現(xiàn)TCP客戶端程序的類,java.net.Scoket
 *
 * 構(gòu)造方法
 *      Socket(String host,int port) 傳遞服務(wù)器IP和端口號
 *      注意:構(gòu)造方法只要運(yùn)行就會和服務(wù)器進(jìn)行連接,連接失敗,拋出異常
 * OutputStream getOutputStream() 返回套接字的輸出流
 *      作用:將數(shù)據(jù)輸出,輸出到服務(wù)器
 * InputStream getInputStream() 返回套接字的輸入流
 *      作用:從服務(wù)器讀取數(shù)據(jù)
 * 客戶端與服務(wù)器數(shù)據(jù)交換,必須使用套接字對象Socket中獲取的IO流,自己new不行
 *
 */
public class TCPClient {
    public static void main(String[] args) throws IOException {
        //創(chuàng)建Scoket對象連接服務(wù)器
        Socket socket = new Socket("192.168.3.3",8888);
        //通過客戶端的套接字對象Socket方法,獲取字節(jié)輸出流,將數(shù)據(jù)寫向服務(wù)器
        OutputStream out = socket.getOutputStream();
        out.write("請求連接服務(wù)器".getBytes());

        //讀取服務(wù)器發(fā)回的數(shù)據(jù),使用socket套接字對象中的字節(jié)輸入流
        InputStream in = socket.getInputStream();
        byte[] data = new byte[1024];
        int len = in.read(data);
        System.out.println(new String(data,0,len));

        socket.close();
    }
}

4.4 文件上傳案例

??目前大多數(shù)服務(wù)器都會提供文件上傳的功能,由于文件上傳需要數(shù)據(jù)的安全性和完整性,很明顯需要使用TCP協(xié)議來實(shí)現(xiàn)。接下來通過一個案例來實(shí)現(xiàn)圖片上傳的功能
? 首先編寫服務(wù)器端程序,用來接收圖片

package com.qtw.api;

/*
*TCP圖片上傳服務(wù)器
*   1. ServerSocket套接字對象,監(jiān)聽8000端口
*   2. 方法accept()獲取客戶端的連接對象
*   3. 客戶端連接對象獲取字節(jié)輸入流,獲取客戶端發(fā)送圖片
*   4. 創(chuàng)建File對象,綁定上傳文件夾,判斷文件夾是否存在
*   5. 創(chuàng)建字節(jié)輸出流,數(shù)據(jù)目的File對象所在文件夾
*   6. 字節(jié)流讀取圖片,字節(jié)流將圖片寫入到目的文件夾中
*   7. 將上傳成功回寫客戶端
*   8. 關(guān)閉資源
*/

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8000);
        //調(diào)用服務(wù)器套接字對象的方法accept()獲取客戶端的套接字對象
        Socket socket = server.accept();
        //通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取圖片
        InputStream in = socket.getInputStream();
        //將目的文件夾封裝到File對象
        File upload = new File("d:\\upload");
        if(!upload.exists()){
            upload.mkdirs();
        }

        //防止文件同名覆蓋,重新定義文件名字
        String filename = System.currentTimeMillis()+".jpg";
        //創(chuàng)建字節(jié)輸出流,將圖片寫入目的文件夾中
        FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
        //讀字節(jié)數(shù)組
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = in.read(bytes)) != -1){
            fos.write(bytes,0,len);
        }

        //服務(wù)器向客戶端回?cái)?shù)據(jù),上傳成功
        OutputStream out = socket.getOutputStream();
        out.write("上傳成功".getBytes());

        socket.close();
        server.close();
    }
}

? 編寫客戶端,完成上傳圖片

package com.qtw.api;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/*
 * 實(shí)現(xiàn)TCP圖片上傳客戶端
 * 實(shí)現(xiàn)步驟
 *      1. Socket 套接字連接服務(wù)器
 *      2. 通過Socket獲取字節(jié)輸出流,寫圖片
 *      3. 使用自己的流對象,讀取圖片數(shù)據(jù)流
 *          FileInputStream
 *      4. 讀取圖片,使用字節(jié)輸出流,將圖片寫入服務(wù)器,采用字節(jié)數(shù)據(jù)進(jìn)行緩存
 *      5. 通過Socket套接字獲取字節(jié)輸入流,讀取服務(wù)器發(fā)回來的上傳成功
 *      6. 關(guān)閉資源
 */
public class TCPClient {
    public static void main(String[] args) throws IOException {
        //創(chuàng)建Scoket對象連接服務(wù)器
        Socket socket = new Socket("192.168.3.3",8000);
        //通過客戶端的套接字對象Socket方法,獲取字節(jié)輸出流,將數(shù)據(jù)寫向服務(wù)器
        OutputStream out = socket.getOutputStream();
        //讀取本機(jī)圖片資源
        FileInputStream fis = new FileInputStream("e:\\test\\timg.jpg");
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1){
            out.write(bytes,0,len);
        }
        //給服務(wù)器寫終止序列
        socket.shutdownOutput();
        //讀取服務(wù)器發(fā)回的數(shù)據(jù),上傳成功
        InputStream in = socket.getInputStream();
        byte[] data = new byte[1024];
        len = in.read(data);
        System.out.println(new String(data,0,len));

        fis.close();
        socket.close();
    }
}

4.5 文件上傳案例多線程版本

實(shí)現(xiàn)服務(wù)器端可以同時(shí)接收多個客戶端上傳的文件。
我們要修改服務(wù)器端代碼
新建線程類Upload.java

package com.qtw.api;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Upload implements Runnable{
    private Socket socket;
    public Upload(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try{
            //通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取圖片
            InputStream in = socket.getInputStream();
            //將目的文件夾封裝到File對象
            File upload = new File("d:\\upload");
            if(!upload.exists()){
                upload.mkdirs();
            }

            //防止文件同名覆蓋,重新定義文件名字
            String filename = System.currentTimeMillis()+".jpg";
            //創(chuàng)建字節(jié)輸出流,將圖片寫入目的文件夾中
            FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
            //讀字節(jié)數(shù)組
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = in.read(bytes)) != -1){
                fos.write(bytes,0,len);
            }

            //服務(wù)器向客戶端回?cái)?shù)據(jù),上傳成功
            OutputStream out = socket.getOutputStream();
            out.write("上傳成功".getBytes());

            socket.close();
        }catch (Exception ex){}
    }
}

服務(wù)器端TCPThreadServer.java

package com.qtw.api;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPThreadServer {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8000);
        //獲取到一個客戶端,必須開啟新線程
        while(true){
            Socket socket = server.accept();
            new Thread(new Upload(socket)).start();
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容