Java网络编程

1.网络编程概述

Java是 Internet 上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序。
Java提供的网络类库,可以很容易的实现网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制。并且 Java 实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。

1.1计算机网络

计算机网络就是把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、 共享硬件、软件、数据信息等资源。 (因特网的发展过程可以在这里给出来)
网络编程的目的:直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。
网络编程中要解决的两个主要的问题:

  • 如何准确地定位网络上一台或多台主机;定位主机上的特定的应用;
  • 找到主机后如何可靠高效地进行数据传输。

2.网络通信要素概述

如何实现网络中的主机互相通信 ?

  1. 知道通信双方地址:IP 和端口号
  2. 需要依靠一定的规则(即:网络通信协议)

计算机网络分层结构:
国际标准化组织(ISO)于1984年提出开放系统互联(OSI)参考模型。
image.png
使用ISO/OSI参考模型解释通信过程:
BU5JW_ZC5XYRD2}@AY@HFIL_edit_613863283964942.png
image.png
但是,理论成功,市场失败。

2.1通信要素1:IP和端口号

2.1.1 IP地址

IP地址的作用: 唯一的标识 Internet 上的计算机(通信实体)
IP地址分类

  • 方式一:分为IPV4 和 IPV6

IPV4: 4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已 经用尽。以点分十进制表示,如192.168.0.1 。
IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984 。

  • 方式二:公网地址(万维网使用)和私有地址(局域网使用)。192.168. 开头的就是私有址址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。

在java中使用InetAddress类代表IP,具体来说就是:一个具体的InetAddress对象代表一个具体的IP地址。api中的解释如下:
image.png
如何实例化InetAddress? 常用的是如下两个方法:
image.png
image.png
可见,这两个是InetAddress类的两个静态方法。其中,getLocalHost()获取的是本机的ip地址。本地回路地址: 127.0.0.1, 对应着: localhost。
下面给出测试:

public static void main(String[] args) {
    try {
        //传入IPV4格式的地址
        InetAddress inet1 = InetAddress.getByName("192.168.10.14");  
        System.out.println(inet1);

        //传入的是域名
        InetAddress inet2 = InetAddress.getByName("www.mi.com");  
        System.out.println(inet2);

        //获取本地ip(法一)
        InetAddress inet3 = InetAddress.getByName("127.0.0.1");
        System.out.println(inet3);

        //获取本机地址(法二)
        InetAddress inet4 = InetAddress.getLocalHost();
        System.out.println(inet4);
        
    } catch (UnknownHostException e) {
        e.printStackTrace();
    }
    
}

测试结果如下:
image.png
可以看到,使用getLocalHost()得到的ip地址是当前局域网内分配的ip地址,所以其实还是本机,如下图所示:
image.png
在获得InetAddress实例后,还可以调用对象的方法获得当前对象的域名和ip地址,两个常用方法: getHostName() / getHostAddress(),下面以刚刚得到的inet2对象给出测试:

public static void main(String[] args) {
    try {
        InetAddress inet1 = InetAddress.getByName("192.168.10.14");  //传入的是IPV4格式的地址

        System.out.println(inet1);

        InetAddress inet2 = InetAddress.getByName("www.mi.com");  //传入的是域名
        System.out.println(inet2);

        //获取本地ip
        InetAddress inet3 = InetAddress.getByName("127.0.0.1");
        System.out.println(inet3);

        //获取本机地址(法二)
        InetAddress inet4 = InetAddress.getLocalHost();
        System.out.println(inet4);

        //getHostName()   : 获取域名
        System.out.println(inet2.getHostName());
        //getHostAddress()   : 获取ip地址
        System.out.println(inet2.getHostAddress());
    } catch (UnknownHostException e) {
        e.printStackTrace();
    }

}

测试结果如下:
image.png
可以看到,使用getHostName()获取的是域名;使用getHostAddress()获取的是ip地址。

2.1.2 端口号

 端口号 标识正在计算机上运行的进程(程序),长度为16bit,能表示65536 (216 )个不同的端口号。端口号只具有本地意义,即端口号只标识本计算机应用层的各进程,在因特网中不同计算机的相同端口号是没有联系的。

端口的作用:
端口能够让应用层的各种应用进程将其数据通过端口向下交付给传输层,以及让传输层知道应当将其报文段中的数据向上通过端口交付给应用层相应的进程。(复用和分用功能)
所以端口是传输层的服务访问点(TSAP),它在传输层的作用类似于IP地址在网络层的作用或MAC地址在数据链路层的作用,只不过IP地址和MAC地址标识的是主机,而端口标识的是主机中的应用进程。
端口分类:
根据端口号范围可将端口分为两类:

  • 服务端使用的端口号。它又分为两类,最重要的一类是熟知端口号,数值为:0 ~ 1023,互联网地址指派机构(IANA)把这些端口号指派给了TCP/IP最重要的一些应用程序,让所有的用户都知道。另一类称为登记端口号,数值为:1024 ~ 49151。它是供没有熟知端口号的应用程序使用的,使用这些端口号必须在IANA登记,以防止重复。下面给出一些常用的熟知端口号:
    | 应用程序 | FTP | TELNET | SMTP | DNS | TFTP | HTTP | SNMP |
    | — | — | — | — | — | — | — | — |
    | 熟知端口号 | 21 | 23 | 25 | 53 | 69 | 80 | 161 |

  • 客户端使用的端口号。数值为:49152 ~ 65535。由于这类端口号仅在客户进程运行时才动态地选择,因此又称为短暂端口号(也称临时端口)。通信结束后,刚用过的客户端口号就不复存在,从而这个端口号就可供其他客户进程以后使用。

套接字:在网络中通过IP地址来标识和区别不同的主机,通过端口号来标识和区分一台主机中的不同应用进程,端口号拼接到IP地址即构成套接字Socket。在网络中采用发送方和接收方的套接字来识别端点。套接字,实际上就是一个通信端点,即:
Socket = (IP地址 : 端口号)
它唯一地标识网络中的一台主机和其上的一个应用(进程)。

2.2 通信要素2:网络通信协议

计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代 码结构、传输控制步骤、出错控制等制定标准。
TCP/IP协议簇
传输层协议中有两个非常重要的协议:

  • 传输控制协议TCP(Transmission Control Protocol)
  • 用户数据报协议UDP(User Datagram Protocol)

TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即:物理链路层、IP层、传输层和应用层。

3.TCP网络编程

3.1TCP协议

3.1.1TCP协议的特点

1.TCP是面向连接(虚连接)的传输层协议。
2.每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的。
3.TCP提供可靠交付的服务,无差错,不丢失,不重复,按序到达。可靠有序,不丢不重
4.TCP提供全双工通信。
发送缓存:准备发送的数据&已发送但尚未收到确认的数据
接收缓存:按序到达但尚未被接受应用程序读取的数据&不按序到达的数据
5.TCP面向字节流, TCP把应用程序交下来的数据看成仅是一连串的无结构的字节流。
(流:流入到进程或从进程留出得到字节序列)

3.1.2 TCP连接管理

1.TCP连接传输三个阶段:
image.png
TCP连接的建立采用客户服务器方式,主动发起连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫服务器。
2.TCP的连接建立
假设运行在一合主机(客户)上的一个进程想与另一合主机(服务器)上的一个进程建立一条连接,客户应用进程首先通知客户TCP,他想建立一个与服务器上某个进程之同的连接,客户中的TCP会用以下步骤与服务器中的TCP建立一条TCP连接:
![}~PR7%5T{P2O3M0W@0Q7]NA_edit_614339358599083.png](https://img-blog.csdnimg.cn/img_convert/a957208a0a0b720d2284ad8b92d917d8.png)
①客户端发送连接请求报文段,无应用层数据。
SYN=1,seq=x (随机)
②服务器端为该TCP连接分配缓存和变量,并向客户端返回确认报文段,允许连接,无应用层数据。
SYN=1,ACK=1,seq=y(随机), ack=x+1
③客户端为该TCP连接分配缓存和变量,并向服务器端返回确认的确认,可以携带数据。
SYN=0,ACK=1,seq=x+1, ack=y+1
3.TCP的连接释放
①客户端发送连接释放报文段,停止发送数据(结束位FIN置为1),主动关闭TCP连接。
FIN=1,seq=u
②服务器端回送一个确认报文段,客户端到服务器这个方向的连接就释放了----->半关闭状态。(客户端不会再发送数据,但服务器还会发送数据给客户端)
ACK=1, seq=v, ack=u+1
③服务器端发完数据,就发出连接释放报文段,主动关闭TCP连接。(只要是表示关闭连接,结束位FIN都置为1)
FIN=1,ACK=1,seq=w,ack=u+1
④客户端回送一个确认报文段,再等到时间等待计时器设置的2MSL(最长报文段寿命)后,连接彻底关闭。
ACK=1,seq=u+1,ack=w+1
为什么要等待2MSL?
如果客户端发送的确认报文段丢失了,就会导致服务器收不到来自客户端的确认。它就会重传一个连接释放报文段给客户端,客户端会在2MSL的时间内收到这个连接释放报文段,进而再次回送一个确认报文段给服务端,同样地,客户端还会再次开启计时器。

3.2基于Socket的TCP编程

利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。 通信的两端都要有Socket,是两台机器间通信的端点,所以网络通信其实就是Socket间的通信。
Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
(在IO流章节中我们提到,输入和输出是相对的概念,我们站位在内存的角度来说:读取磁盘等外部数据到内存的过程是输入;将内存中的数据输出到磁盘等存储设备的过程叫"输出"。但是在网络编程中,信息的交互是在两个终端之间发生的,所以当我们站位在客户端上时,发送请求给服务器的过程就叫"输出";接收服务器的响应就叫"输入"。同理,站位在服务端时,接收来自客户端的请求就叫"输入",响应客户端的请求就叫"输出"。搞懂了这个逻辑,后面的程序设计过程中什么时候该用输入流,什么时候该用输出流就很会很清楚了。)

3.2.1 Socket简介

1.Socket的分类

  • 流套接字(stream socket): 使用TCP提供可依赖的字节流服务
  • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务

2.Socket类的常用构造器

public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号。

3.Socket类的常用方法

public InputStream getInputStream()返回此套接字的输入流。 可以用于接收网络消息
public OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
public InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
public InetAddress getLocalAddress()获取套接字绑定的本地地址。 即本端的IP地址
public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
public int getLocalPort()返回此套接字绑定到的本地端口。 如果尚未绑定套接字,则返回 -1。即本端的 端口号。
public void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接 或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
public void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将 返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
public void shutdownOutput()禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发 送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流, 则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。

3.2.2 基于Socket的TCP编程过程

**客户端Socket的工作过程 **

  1. 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端 响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
  2. 打开连接到 Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输。
  3. 按照一定的协议对 Socket 进行读/写操作:通过输入流读取服务器放入线路的信息 (但不能读取自己放入线路的信息),通过输出流将信息写入线程。
  4. 关闭 Socket:断开客户端到服务器的连接,释放线路

服务器程序的工作过程

  1. 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口 上。用于监听客户端的请求。
    调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信 套接字对象。
  2. 调用 该Socket类对象的 getOutputStream() 和 getInputStream ():获取输出 流和输入流,开始网络数据的发送和接收。
  3. 关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字。

基于Socket的TCP编程的通信模型如下图所示: image.png
下面给出三道例题,体会TCP网络编程的实现过程。

例题1. 客户端发送内容给服务端,服务端将内容打印到控制台上。

实现过程如下:
客户端

//客户端
@Test
public void client(){

    Socket socket = null;
    OutputStream os = null;
    try {
        //1.创建Socket对象,指明服务器的ip和端口号
        InetAddress inet = InetAddress.getByName("192.168.75.1");
        socket = new Socket(inet, 8899);

        //2.获取一个输出流,用于输出数据
        os = socket.getOutputStream();

        //3.写出数据的操作(这里的write函数需要传入一个字节数组作为参数,所以把下面的字符串转换为字节数组)
        os.write("你好,我是古伊娜!".getBytes());  //写在了os这个输出流对象中
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭资源
        if(os != null){
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket != null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端:

//服务端
@Test
public void server() {

    ServerSocket ss = null;
    Socket socket = null;
    InputStream is = null;
    ByteArrayOutputStream baos = null;

    try {
        //1.创建服务器端的ServerSocket,指明自己的端口号
        ss = new ServerSocket(8899);

        //2.调用accept()表示接收来自于客户端的socket
        socket = ss.accept();

        //3.获取输入流
        is = socket.getInputStream();

        //读取输入流中的数据
        //不建议这么写,可能会出现乱码(现在是用字节流去处理客户端写出的字符串,中文的一个字符在读取时可能被拆开成两部分)
//            byte[] buffer = new byte[20];
//            int len;
//            while((len = is.read(buffer)) != -1){
//                String str = new String(buffer,0,len);
//                System.out.print(str);
//            }

        //4.读取输入流中的数据
        baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[5];
        int len;
        while((len = is.read(buffer)) != -1){
            baos.write(buffer,0,len);    //写在了哪里? ——> baos对象中的一个字节数组里(这个字节数组是自动扩充的)
        }

        System.out.println(baos.toString());

        System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");

    } catch (IOException e) {
        e.printStackTrace();
    } finally {

        //5.资源关流
        if(baos != null){
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(is != null){
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket != null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(ss != null){
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

}

服务端创建的是ServerSocket,ServerSocket可以接收来自于客户端的socket。(理解为监听客户端的连接)
ByteArrayOutputStream类的对象中自动扩充的数组:
image.png
ByteArrayOutputStream类的toString()函数:
image.png
可见,这个toString方法的作用就是把写入baos中的数据生成字符串并返回。
测试结果如下,先启动服务器端:
image.png
可以看到,此时的服务端已经启动起来了,正在等待客户端的连接。
接着启动客户端:
image.png
客户端也启动起来了,查看服务端的控制台输出:
image.png

例题2. 客户端发送文件给服务端,服务端将文件保存在本地。 比如客户端发送一张图片给服务端,服务端在收到后保存到本地。

设计过程:与例题1不同的是,这次是服务端接收客户端发来的文件,并保存到"服务端"。同样的操作,首先在客户端创建Socket对象,指明要发送到的主机和端口号;然后获取该套接字的输出流对象,用于发送文件;接着使用客户端套接字的输出流写入"文件数据"到这个输出流对象当中。(然而由于是非文本文件,所以需要先读取才能写出。)写出后关闭资源。
在服务端,首先创建一个服务器套接字,指明一下自己的端口号是多少;接着监听客户端的连接请求,即获取到客户端创建的Socket对象;拿到了Socket对象就相当于拿到了Socket对象中的输入流,接下来只要把socket输出流中的数据读入即可,由于是要保存到本地,所以读入数据的同时需要指明一个位置完成同步的写出。
实现过程如下:

**
 * 实现TCP的网络编程
 * 例题2:客户端发送文件给服务端,服务端将文件保存在本地。
 *
 *
 */
public class TCPTest2 {

    //客户端
    @Test
    public void client() {
        Socket socket = null;
        OutputStream os = null;
        FileInputStream fis = null;
        try {
            //1.
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);

            //2.客户端要输出,创建输出流
            os = socket.getOutputStream();
            //3.要输出的是本地的jpg文件,所以首先要读入该文件,因此创建输入流
            fis = new FileInputStream(new File("aiping.jpg"));
            //4.读入并写出
            byte[] buffer = new byte[1024];
            int len;
            while((len = fis.read(buffer)) != -1){
                os.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.关闭流
            if(fis != null){

                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(os != null){

                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(socket != null){

                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Test
    public void server() {
        ServerSocket ss = null;
        Socket socket = null;
        InputStream is = null;
        FileOutputStream fos = null;

        try {
            //服务端指明自己的端口号
            ss = new ServerSocket(9999);

            //监听
            socket = ss.accept();

            //服务端要读入客户端发送的jpg文件
            is = socket.getInputStream();

            fos = new FileOutputStream(new File("aiping1.jpg"));

            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) != -1){
                fos.write(buffer,0,len);        //写出到服务端的"本地"
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关流
            if(fos != null){

                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(is != null){

                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(socket != null){

                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(ss != null){
                try {
                    ss.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

    }

}

测试结果如下:
启动服务端:
image.png
启动客户端:
image.png
接下来去当前的module下(即服务端的本地)查看有没有生成一张名为aiping1.jpg的新图片。
image.png

例题3. 从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。

思路:相比于例题2,只是增加了服务端向客户端的一个反馈,所以服务端在成功接收到文件保存在本地后,需要创建一个输出流并写出反馈内容;客户端需要拿到自己socket的输入流来读入服务端写出的内容,并使用输出流将读取到的内容写出到客户端的控制台。
客户端:

//客户端
@Test
public void client() {
    Socket socket = null;
    OutputStream os = null;
    FileInputStream fis = null;
    InputStream is = null;
    ByteArrayOutputStream baos = null;
    try {
        //1.
        socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);

        //2.客户端要输出,创建输出流
        os = socket.getOutputStream();
        //3.要输出的是本地的jpg文件,所以首先要读入该文件,因此创建输入流
        fis = new FileInputStream(new File("aiping.jpg"));
        //4.读入并写出
        byte[] buffer = new byte[1024];
        int len;
        while((len = fis.read(buffer)) != -1){
            os.write(buffer,0,len);
        }

        //5.接受来自于服务器的数据,并显示到控制台上。
        //先获取socket的输入流
        is = socket.getInputStream();
        baos = new ByteArrayOutputStream();
        byte[] bufferr = new byte[20];
        int len1;
        while ((len1 = is.read(bufferr)) != -1){
            baos.write(bufferr, 0, len1);
        }
        System.out.println(baos.toString());

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //6.关闭流
        if(baos != null){

            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(is != null){

            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fis != null){

            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(os != null){

            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket != null){

            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

服务端:

@Test
public void server() {
    ServerSocket ss = null;
    Socket socket = null;
    InputStream is = null;
    FileOutputStream fos = null;
    OutputStream os = null;
    try {
        //服务端自己的端口号是多少
        ss = new ServerSocket(9999);

        //监听
        socket = ss.accept();

        //服务端要读入客户端发送的jpg文件
        is = socket.getInputStream();

        fos = new FileOutputStream(new File("aiping2.jpg"));

        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }

        //6.服务器端给予客户端反馈
        os = socket.getOutputStream();
        os.write("发送成功".getBytes());

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关流
        if(fos != null){

            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(is != null){

            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if(socket != null){

            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(ss != null){
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }

}

测试结果如下:
启动服务端:
image.png
启动客户端:
image.png
可以看到,此时客户端和服务端都处在一个转圈的状态,当前module下也没有生成aiping2.jpg这个文件,即服务端没有完成保存文件到本地; 且客户端也没有收到服务端“发送成功”的反馈。当我们关闭程序的时候,aiping2.jpg文件出现了,同时客户端出现了套接字异常。如下图所示:
image.png
为什么会这样呢?
这是因为服务端接收图片的read方法是一个阻塞式的方法,这个方法的特点是如果没有收到明确的结束信号,它就不会终止。而如果是读取一个现有的文件的话,读到文件末尾会返回-1,就符合while循环中的判断,但是现在是在接收客户端发来的数据,客户端没有给它结束的信号它就不会停止。所以这里read方法一直处在循环中没有出来,一直在读取数据。
所以只需在客户端传完数据后给出一个结束的指示即可,即在while循环后加上如下的代码:

//关闭数据的输出(给一个表示数据输出结束的指示)
socket.shutdownOutput();

所以完整的客户端设计如下:

//客户端
@Test
public void client() {
    Socket socket = null;
    OutputStream os = null;
    FileInputStream fis = null;
    InputStream is = null;
    ByteArrayOutputStream baos = null;
    try {
        //1.
        socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);

        //2.客户端要输出,创建输出流
        os = socket.getOutputStream();
        //3.要输出的是本地的jpg文件,所以首先要读入该文件,因此创建输入流
        fis = new FileInputStream(new File("aiping.jpg"));
        //4.读入并写出
        byte[] buffer = new byte[1024];
        int len;
        while((len = fis.read(buffer)) != -1){
            os.write(buffer,0,len);
        }

        //关闭数据的输出(给一个表示数据输出结束的指示)
        socket.shutdownOutput();

        //5.接受来自于服务器的数据,并显示到控制台上。
        //先获取socket的输入流
        is = socket.getInputStream();
        baos = new ByteArrayOutputStream();
        byte[] bufferr = new byte[20];
        int len1;
        while ((len1 = is.read(bufferr)) != -1){
            baos.write(bufferr, 0, len1);
        }
        System.out.println(baos.toString());

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //6.关闭流
        if(baos != null){

            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(is != null){

            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fis != null){

            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(os != null){

            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket != null){

            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

接下来再做一下测试:
启动服务端:
image.png
启动客户端:
image.png
可见,客户端已经收到了服务端的反馈,同时保存下来了名为aiping2.jpg的文件。
image.png

4.UDP网络编程

4.1UDP协议

UDP仅在IP数据报服务之上增加了两个基本的服务:复用和分用以及差错检测。
UDP的特点
1.UDP无需建立连接,可以减小开销和发送数据之前的时延。
2.UDP使用最大努力交付,即不保证可靠交付。
3.UDP是面向报文的,适合一次性传输少量数据的网络。
image.png
如上图所示:发送方UDP对应用层交下来的报文,在添加首部后就向下交付给IP层,一次发送一个报文,既不合并,也不拆分,而是保留这些报文的边界;接收方UDP对IP层交上来的UDP数据报,在去除首部后就原封不动地交付给上层应用进程,一次交付一个完整的报文。因此报文不可分割,是UDP数据报处理的最小单元。
(因此,应用程序必须选择合适大小的报文,若报文太长,UDP把它交给IP层后,可能导致分片;若报文太短,UDP把它交给IP层后,会使IP数据报的首部的相对长度太大,两者都会降低IP层的效率。)
4.UDP无拥塞控制,适合很多实时应用。对于某些实时应用,要求以稳定的速度发送,能容忍一些数据的丢失,但不允许有较大的时延,UDP正好满足这些应用的需求。

4.2UDP网络通信过程

基于UDP网络协议的通信实现需要用到两个类: DatagramSocket 和 DatagramPacket。
具体来说, UDP数据报通过数据报套接字 DatagramSocket 发送和接收 ,但系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达; 而DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP 地址和端口号以及接收端的IP地址和端口号。
注: UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和 接收方的连接。如同发快递包裹一样。

  1. DatagramSocket 类的常用方法
public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被 绑定到通配符地址,IP 地址由内核来选择。
public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将 要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法 在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的 长度长,该信息将被截短。
public void close()关闭此数据报套接字。
  1. DatagramPacket类的常用方法
public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长 度为 length 的数据包。 length 参数必须小于等于 buf.length。(所以这个构造器是在接收端接收数据的时候用的)
public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数 据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。 (所以这个构造器是在发送端发送数据的时候用的)
public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该 机器或者是从该机器接收到的。
public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或 者是从该主机接收到的。
public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区 中的偏移量 offset 处开始,持续 length 长度。
public int getLength()返回将要发送或接收到的数据的长度。

下面给出一个UDP网络编程的例子:

package com.atguigu.java1;

import org.junit.Test;

import java.io.IOException;
import java.net.*;

/**
 * UDP协议的网络编程
 *
 */
public class UDPTest {

    //发送端
    @Test
    public void sender() {
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket();

            String str = "UDP方式";
            byte[] data = str.getBytes();
            InetAddress inet = InetAddress.getLocalHost();   //指明了是本机(要发送的地址)

            //数据报的信息都封装在DatagramPacket的对象当中(比如发送给哪个主机的哪个应用程序、发送的内容及长度)
            //所选择的构造器封装了:要发送的数据(字节数组方式存储的)、从数组的开头起、总长度为数组的长度、要发送的ip地址和端口号
            DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 9090);

            socket.send(packet);   //从socket这个套接字发送数据报
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(socket != null){

                socket.close();
            }
        }

    }


    //接收端
    @Test
    public void receiver(){

        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket(9090);   //创建接收端的socket时指明一个自己的端口号

            byte[] buffer = new byte[100];


            DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length); //接收到的数据就放在了packet对象里

            socket.receive(packet);

            //将接收到的数据输出到控制台上
            System.out.println(new String(packet.getData(), 0, packet.getLength()));

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(socket != null){

                socket.close();
            }
        }


    }

}

测试结果如下:
为了能够让接收端成功接收数据,还是要先启动接收端,再启动发送端发送数据:
image.png
启动发送端:
image.png
此时我们查看接收端的控制台输出:
image.png
可见,接收端成功接收到数据,通信过程结束。
与TCP的区别:UDP只管发,不管对方有没有收到,所以先启动发送方是不会报错的,只是接收端收不到而已;而TCP方式中如果先启动客户端,则客户端会尝试去握手(与服务端建立连接),然而由于服务端没有启动,所以会报建立连接失败。

5.URL编程

5.1URL类

1.什么是URL?
URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。 它是一种具体的URI(Uniform Resource Identifier),即URI可以用来标识一个资源,而且还指明了如何定位这个资源。
通过URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
2.URL 的基本结构
URL由五部分组成:
<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
比如:
http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
#片段名:锚点, 比如看小说时,可以直接定位到章节。
参数列表格式: 参数名=参数值&参数名=参数值… ,上述例子中的参数要实现是登录的功能。
3.URL类的构造器
为了表示URL,java.net 中实现了URL类。我们可以通过下面的构造器来初始化一个URL 对象:

构造器说明举例
public URL (String spec)通过一个表示URL地址的字符串可以构造一个URL对象。URL url = new URL (“http://www. atguigu.com/”)
public URL(URL context, String spec)通过基 URL 和相对 URL 构造一个 URL 对象。URL downloadUrl = new URL(url, “download.html")
public URL(String protocol, String host, String file)
new URL(“http”, “www.atguigu.com”, “download. html")
public URL(String protocol, String host, int port, String file)
URL gamelan = new URL(“http”, “www.atguigu.com”, 80, “download.html")

4.URL类常用方法
一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:

public String getProtocol()获取该URL的协议名
public String getHost()获取该URL的主机名
public String getPort()获取该URL的端口号
public String getPath()获取该URL的文件路径
public String getFile()获取该URL的文件名
public String getQuery()获取该URL的查询名

下面我们创建一个URL的对象,参数为字符串方式表示的URL ,并使用上述的方法来获取它的属性。

/**
 * URL网络编程
 * 1.URL:统一资源定位符,对应着互联网的某一资源地址
 * 2.格式:
 *  http://localhost:8080/examples/beauty.jpg?username=Tom
 *  协议   主机名    端口号  资源地址           参数列表
 *
 */

public class URLTest {

    public static void main(String[] args) throws MalformedURLException {
        URL url =  new URL("http://localhost:8080/examples/2.jpg?username=Tom");

        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());  // url的文件名

        System.out.println(url.getQuery()); // url的查询名

    }

}

测试结果如下:
image.png
接下来我们做这样一个操作:把上述URL定位的资源下载到本地。
这个2.jpg文件是我们部署在tomcat服务器上的,所在位置如下:
image.png
所以就相当于编写程序充当是客户端从tomcat上下载文件。
具体实现如下:

public class URLTest1 {

    public static void main(String[] args) {
        HttpURLConnection urlConnection = null;
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            URL url = new URL("http://localhost:8080/examples/2.jpg?username=Tom");

            //获取到与服务器的连接
            urlConnection = (HttpURLConnection) url.openConnection();
            
            //拿到了连接这个对象后就可以去访问服务器了(真正的获取到了连接)
            urlConnection.connect();

            //获取输入流
            is = urlConnection.getInputStream();

            fos = new FileOutputStream("day10\\2.jpg");

            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) != -1){
                fos.write(buffer,0,len);
            }

            System.out.println("下载完成");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            if(is != null){

                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fos != null){

                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(urlConnection != null){

                urlConnection.disconnect();
            }
        }

    }
}

上述代码段中用到了URLConnection类,现在对该类做出补充说明:
这是个针对HTTP协议的类, 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时就需要使用 URLConnection 。
URLConnection表示的是到URL所引用的远程对象的连接。当与一个URL建立连接时, 首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection对象。如果连接过程失败,将产生IOException.
所以在上述代码段中,创建了URL对象后,就与服务器端建立连接(这个时候要保证服务器是开启的,打开cmd命令行程序后键入:catalina run命令,即可启动tomcat服务器)。如下所示:
image.png
获取到与服务器的连接后,就可以用输入流读取要下载的文件,然后用输出流将读取到的文件写出到本地(读取与写出的过程是IO流中很常用的处理手段了)。这样就把2.jpg文件下载到了本地指定的位置。
测试结果如下:
image.png
然后去查看一下创建的输出流指定的位置处有没有生成2.jpg这个图片。
image.png
至此,从URL下载资源的操作完成。

5.2 URI、URL和URN的区别

URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。而URL是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。 而URN,uniform resource name,统一资源命名,是通过名字来标识资源, 比如mailto:java-net@java.sun.com。也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL 和URN都是一种URI。
在Java的URI中,一个URI实例可以代表绝对的,也可以是相对的,只要它符 合URI的语法规则。而URL类则 不仅符合语义,还包含了定位该资源的信息, 因此它不能是相对的。
image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值