回归java12-java进阶-网络编程

网络编程

java.net,包含了网络编程所需要的最基础一些类和接口。

这些类和接口面向两个不同的层次:基于Socket的低层次网络编程和基于URL的高层次网络编程。

高低层次就是通信协议的高低层次,Socket采用TCP、UDP等协议,这些协议属于低层次的通信协议;URL采用HTTP和HTTPS这些属于高层次的通信协议。

低层次网络编程,因为它面向底层,比较复杂,但是“低层次网络编程”并不等于它功能不强大。恰恰相反,正因为层次低,Socket编程与基于URL的高层次网络编程比较,能够提供更强大的功能和更灵活的控制,但是要更复杂一些。

网络基础

网络结构

网络结构是网络的构建方式,目前流行的有客户端服务器结构网络和对等结构网络。

  1. 客户端服务器结构网络
    客户端服务器(Client Server,缩写C/S)结构网络,是一种主从结构网络。
    服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。
    服务器是被动的,客户端是主动的。

    生活中很多网络服务都采用这种结构:Web服务、文件传输服务和邮件服务等。

  2. 对等结构网络
    对等结构网络也叫点对点网络(Peer to Peer,缩写P2P),每个节点之间是对等的。
    每个节点既是服务器又是客户端。
    对等结构网络分布范围比较小。

    通常在一间办公室或一个家庭内,适合于移动设备间的网络通讯,网络链路层是由蓝牙和WiFi实现。

TCP/IP协议

网络通信会用到协议,其中TCP/IP协议是非常重要的。TCP/IP协议是由IP和TCP两个协议构成的。

IP(Internet Protocol)协议是一种低级的路由协议,它将数据拆分成许多小的数据包,并通过网络将它们发送到某一特定地址,但无法保证所有包都抵达目的地,也不能保证包的顺序。

由于IP协议传输数据的不安全性,网络通信时还需要TCP协议。传输控制协议(Transmission Control Protocol,TCP)是一种高层次的协议,面向连接的可靠数据传输协议,如果有些数据包没有收到,则会重发,并对数据包内容准确性检查,且保证数据包顺序,所以该协议保证数据包能够安全地按照发送时顺序送达目的地。

IP地址

为实现网络中不同计算机之间的通信,每台计算机都必须有一个与众不同的标识,这就是IP地址, TCP/IP使用IP地址来标识源地址和目的地址。
最初所有的IP地址都是32位数字,由4个8位的二进制数组成,每8位之间用圆点隔开,如192.168.1.1,这种类型的地址通过IPv4指定。
而现在有一种新的地址模式称为IPv6,IPv6使用128位数字表示一个地址,分为8个16位块。尽管IPv6比IPv4有很多优势,但是由于习惯的问题,很多设备还是采用IPv4。
Java语言同时采用IPv4和IPv6。

在IPv4地址模式中IP地址分为A、B、C、D和E等5类:
A类地址用于大型网络,地址范围:1.0.0.1~126.155.255.254。
B类地址用于中型网络,地址范围:128.0.0.1~191.255.255.254。
C类地址用于小规模网络,192.0.0.1~223.255.255.254。
D类地址用于多目的地信息的传输和作为备用。
E类地址保留仅作实验和开发用。

有时还会用到一个特殊的IP地址127.0.0.1,127.0.0.1称为回送地址,指本机。主要用于网络软件测试以及本地机进程间通信,使用回送地址发送数据,不进行任何网络传输,只在本机进程间通信。

端口

一个IP地址标识一台计算机,每一台计算机又有很多网络通信程序在运行,提供网络服务或进行通信,这就需要不同的端口进行通信。
如果把IP地址比作电话号码,那么端口就是分机号码,进行网络通信时不仅要指定IP地址,还要指定端口号。

TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。
小于1024的端口号保留给预定义的服务,如HTTP是80,FTP是21,Telnet是23,Email是25等,除非要和那些服务进行通信,否则不应该使用小于1024的端口。

TCP Socket低层次网络编程

TCP/IP协议的传输层有两种传输协议:TCP(传输控制协议)和 UDP(用户数据报协议)。

TCP是面向连接的可靠数据传输协议。TCP好比电话,电话接通后双方才能通话,在挂断电话之前,电话一直占线。TCP连接一旦建立起来,一直占用,直到关闭连接。TCP为了保证数据的正确性,会重发一切没有收到的数据,还会对数据内容进行验证,并保证数据传输的正确顺序。因此TCP协议对系统资源的要求较多。
基于TCP Socket编程很有代表性。

TCP Socket通信简介

Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换。
这个双向链路的一端称为一个Socket。
Socket通常用来实现客户端和服务器端的连接。
Socket是TCP/IP协议的一个十分流行的编程接口,一个Socket由一个IP地址和一个端口号唯一确定,一旦建立连接,Socket还会包含本机和远程主机的IP地址和端口号,Socket是成对出现的。
在这里插入图片描述

TCP Socket通信过程

使用Socket进行C/S结构编程,通信过程:
在这里插入图片描述
服务器端监听某个端口是否有连接请求,服务器端程序处于阻塞状态,直到客户端向服务器端发出连接请求,服务器端接收客户端请求,服务器会响应请求,处理请求,然后将结果应答给客户端,这样就会建立连接。一旦连接建立起来,通过Socket可以获得输入输出流对象。借助于输入输出流对象就可以实现服务器与客户端的通信,最后不要忘记关闭Socket和释放一些资源(包括关闭输入输出流)。

Socket类

java.net包为TCP Socket编程提供了两个核心类:Socket和ServerSocket,分别用来表示双向连接的客户端和服务器端。

构造方法:
Socket(InetAddress address, int port) 创建Socket对象,并指定远程主机IP地址和端口号。
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) 创建Socket对象,并指定远程主机IP地址和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。
Socket(String host, int port) 创建Socket对象,并指定远程主机名和端口号,IP地址为null,null表示回送地址,即127.0.0.1。
Socket(String host, int port, InetAddress localAddr, int localPort) 创建Socket对象,并指定远程主机和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。host为主机名,IP地址为 null,null表示回送地址,即127.0.0.1。

常用方法:
InputStream getInputStream() 通过此Socket返回输入流对象。
OutputStream getOutputStream() 通过此Socket返回输出流对象。
int getPort() 返回Socket连接到的远程端口。
int getLocalPort() 返回Socket绑定到的本地端口。
InetAddress getInetAddress() 返回Socket连接的地址。
InetAddress getLocalAddress() 返回Socket绑定的本地地址。
boolean isClosed() 返回Socket是否处于关闭状态。
boolean isConnected() 返回Socket是否处于连接状态。
void close() 关闭Socket。

Socket与流类似,所占用的资源不能通过JVM的垃圾收集器回收,需要程序员释放:
一种方法是可以在finally代码块调用close()方法关闭Socket,释放流所占用的资源。
另一种方法通过自动资源管理技术释放资源,Socket和ServerSocket都实现了AutoCloseable接口。

ServerSocket类

构造方法:
ServerSocket(int port, int maxQueue) 创建绑定到特定端口的服务器Socket。maxQueue设置连接请求最大队列长度,如果队列满,则拒绝该连接。默认值是50。
ServerSocket(int port) 创建绑定到特定端口的服务器Socket。最大队列长度是50。

常用方法:
InputStream getInputStream() 通过此Socket返回输入流对象。
OutputStream getOutputStream() 通过此Socket返回输出流对象。
boolean isClosed() 返回Socket是否处于关闭状态。
Socket accept() 侦听并接收到Socket的连接。此方法在建立连接之前一直阻塞。
void close() 关闭Socket。

ServerSocket类本身不能直接获得I/O流对象,而是通过accept()方法返回Socket对象,通过Socket对象取得I/O流对象,进行网络通信。ServerSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭ServerSocket。

案例:文件上传工具

上传过程是一个单向Socket通信过程,客户端通过文件输入流读取文件,然后从Socket获得输出流写入数据,写入数据完成上传成功,客户端任务完成。服务器端从Socket获得输入流,然后写入文件输出流,写入数据完成上传成功,服务器端任务完成。
在这里插入图片描述

// 服务器端
public class UploadServer {
    
    public static void main(String[] args) {
        
        System.out.println("服务器端运行...");
        
        try ( // 创建一个ServerSocket监听8080端口的客户端请求
            ServerSocket server = new ServerSocket(8080);
            // 使用accept()阻塞当前线程,等待客户端请求
            Socket socket = server.accept();
            // 由Socket获得输入流,并创建缓冲输入流
            BufferedInputStream in = 
            new BufferedInputStream(socket.getInputStream());
            // 由文件输出流创建缓冲输出流
            FileOutputStream out = 
            new FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg")) {
            
            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次从Socket读取数据
            int len = in.read(buffer);
            while (len != -1) {
                // 写入数据到文件
                out.write(buffer, 0, len);
                // 再次从Socket读取数据
                len = in.read(buffer);
            }
            System.out.println("接收完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第8行创建ServerSocket对象监听本机的8080端口,这是当前线程还没有阻塞,调用第10行的server.accept()才会阻塞当前线程,等待客户端请求。

由于当前线程是主线程,所以server.accept()会阻塞主线程,阻塞主线程是不明智的,如果是在一个图形界面的应用程序,阻塞主线程会导致无法进行任何的界面操作,就是常见的“卡”现 象,所以最好是把server.accept()语句放到子线程中。

// 客户端
public class UploadClient {
    
    public static void main(String[] args) {
        
        System.out.println("客户端运行...");
        
        try ( // 向本机的8080端口发出请求
            Socket socket = new Socket("127.0.0.1", 8080);
            // 由Socket获得输出流,并创建缓冲输出流
            BufferedOutputStream out = 
            new BufferedOutputStream(socket.getOutputStream());
            // 创建文件输入流
            FileInputStream fin = new FileInputStream("./TestDir/coco2dxcplus.jpg");
            // 由文件输入流创建缓冲输入流
            BufferedInputStream in = new BufferedInputStream(fin)) {
            
            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次读取文件
            int len = in.read(buffer);
            while (len != -1) {
                // 数据写入Socket
                out.write(buffer, 0, len);
                // 再次读取文件
                len = in.read(buffer);
            }
            System.out.println("上传成功!");
        } catch (ConnectException e) {
            System.out.println("服务器未启动!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码第9行创建Socket,指定远程主机的IP地址和端口号。代码第12行是从socket对象获得输出流。代码第29行是捕获ConnectException异常,这个异常引起的原因是在代码第9行向服务器发出请求时,服务器拒绝了客户端请求,这有两种可能性:一是服务器没有启动,服务器的8080端口没有打开;二是服务器请求队列已满(默认是50个)。

案例测试时,先运行服务器,再运行客户端。测试Socket程序最好打开两个命令行窗口,通过java指令分别运行服务器程序和客户端程序。
(需要注意当前运行的路径是Eclipse工程根目录,需要指定类路径,命令的-cp .;./bin就是指定类路径,包括两个当前路径。指定bin目录是因为编译之后的字节码文件放在此目录中。
如果想在Eclipse中查看多个控制台信息,在控制台上面的工具栏中,单击“选择控制台”按钮实现切换。)

案例:聊天工具

上例只是单向传输的Socket,Socket可以双向数据传输,比较有代表性的案例就是聊天工具。

下图所示基于TCP Socket聊天工具案例,标准输入是键盘,标准输出是显示器的控制台。首先客户端通过键盘输入字符串,通过标准输入流读取字符串,然后通过Socket获得输出流,将字符串写入输出流。接着服务器通过Socket获得输入流,从输入流中读取来自客户端发送过来的字符串,然后通过标准输入流输出到显示器的控制台。服务器向客户端字符串过程类似。
在这里插入图片描述

// 服务器端
public class ChatServer {
    
    public static void main(String[] args) {
        
        System.out.println("服务器运行...");
        
        Thread t = new Thread(() -> {
            
            try ( // 创建一个ServerSocket监听端口8080客户请求
                ServerSocket server = new ServerSocket(8080);
                // 使用accept()阻塞等待客户端请求
                Socket socket = server.accept();
                DataInputStream in = new DataInputStream(socket.getInputStream());
                DataOutputStream out = 
                new DataOutputStream(socket.getOutputStream());
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                
                while (true) {
                    /* 接收数据 */
                    String str = in.readUTF();
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:【%s】\n", str);
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 发送
                    out.writeUTF(keyboardInputString);
                    out.flush();
                }
            } catch (Exception e) {
            }
            System.out.println("服务器停止...");
        });
        
        t.start();
    }
}

第8行是创建一个子线程,将网络通信放到子线程中处理是一种很好的做法,因为网络通信往往有线程阻塞过程,放到子线程中处理就不会阻塞主线程了。
代码第13行是从socket中获得数据输入流,代码第15行是从socket中获得数据输出流,数据流主要面向基本数据类型,本例中使用它们主要用来输入输出UTF编码的字符串,代码第20行readUTF()是数据输入流读取字符串。代码第31行writeUTF()是数据输出流写入字符串。代码第17行中的System.in是标准输入流,然后使用标准输入流创建缓冲输入流。

// 客户端
public class ChatClient {
    
    public static void main(String[] args) {
        
        System.out.println("客户端运行...");
        
        Thread t = new Thread(() -> {
            
            try ( // 向127.0.0.1主机8080端口发出连接请求
                Socket socket = new Socket("127.0.0.1", 8080);
                DataInputStream in = new DataInputStream(socket.getInputStream());
                DataOutputStream out = 
                new DataOutputStream(socket.getOutputStream());
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                
                while (true) {
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 发送
                    out.writeUTF(keyboardInputString);
                    out.flush();
                    /* 接收数据 */
                    String str = in.readUTF();
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:【%s】\n", str);
                }
            } catch (ConnectException e) {
                System.out.println("服务器未启动!");
            } catch (Exception e) {
            }
            System.out.println("客户端停止!");
        });
        
        t.start();
    }
}

UDP Socket低层次网络编程

UDP(用户数据报协议)就像日常生活中的邮件投递,是不能保证可靠地寄到目的地。
UDP是无连接的,对系统资源的要求较少,UDP可能丢包,不保证数据顺序。
对于网络游戏和在线视频等要求传输快、实时性高、质量可稍差一点的数据传输,UDP还是非常不错的。
UDP Socket网络编程比TCP Socket编程简单多,UDP是无连接协议,不需要像TCP一样监听端口,建立连接,然后才能进行通信。

DatagramSocket类

用于在程序之间建立传送数据报的通信连接。

构造方法:
DatagramSocket() 创建数据报DatagramSocket对象,并将其绑定到本地主机上任何可用的端口。
DatagramSocket(int port) 创建数据报DatagramSocket对象,并将其绑定到本地主机上的指定端口。
DatagramSocket(int port, InetAddress laddr) 创建数据报DatagramSocket对象,并将其绑定到指定的本地地址。

常用方法:
void send(DatagramPacket p) 发送数据报包。
void receive(DatagramPacket p) 接收数据报包。
int getPort() 返回DatagramSocket连接到的远程端口。
int getLocalPort() 返回DatagramSocket绑定到的本地端口。
InetAddress getInetAddress() 返回DatagramSocket连接的地址。
InetAddress getLocalAddress() 返回DatagramSocket绑定的本地地址。
boolean isClosed() 返回DatagramSocket是否处于关闭状态。
boolean isConnected() 返回DatagramSocket是否处于连接状态。
void close() 关闭DatagramSocket。

DatagramSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭DatagramSocket。

DatagramPacket类

用来表示数据报包,是数据传输的载体。
DatagramPacket实现无连接数据包投递服务,每次投递数据包仅根据该包中信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达,不保证包都能到达目的地。

构造方法:
DatagramPacket(byte[] buf, int length) 构造数据报包,buf包数据,length是接收包数据的长度。DatagramPacket(byte[] buf, int length, InetAddress address, int port) 构造数据报包,包发送到指定主机上的指定端口号。
DatagramPacket(byte[] buf, int offset, int length) 构造数据报包,offset是buf字节数组的偏移量。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 构造数据报包,包发送到指定主机上的指定端口号。

常用方法
InetAddress getAddress() 返回发往或接收该数据报包相关的主机的IP地址。
byte[] getData() 返回数据报包中的数据。
int getLength() 返回发送或接收到的数据(byte[])的长度。
int getOffset() 返回发送或接收到的数据(byte[])的偏移量。
int getPort() 返回发往或接收该数据报包相关的主机的端口号。

案例:文件上传工具

// 服务器端
public class UploadServer {
    
    public static void main(String args[]) {
        
        System.out.println("服务器端运行...");
        
        // 创建一个子线程
        Thread t = new Thread(() -> {
            
            try ( // 创建DatagramSocket对象,指定端口8080
                DatagramSocket socket = new DatagramSocket(8080);
                FileOutputStream fout = 
                new FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg");
                BufferedOutputStream out = new BufferedOutputStream(fout)) {
                // 准备一个缓冲区
                byte[] buffer = new byte[1024];
                //循环接收数据报包
                while (true) {
                    // 创建数据报包对象,用来接收数据
                    DatagramPacket packet = 
                        new DatagramPacket(buffer, buffer.length);
                    // 接收数据报包
                    socket.receive(packet);
                    // 接收数据长度
                    int len = packet.getLength();
                    if (len == 3) {
                        // 获得结束标志
                        String flag = new String(buffer, 0, 3);
                        // 判断结束标志,如果是bye结束接收
                        if (flag.equals("bye")) {
                            break;
                        }
                    }
                    // 写入数据到文件输出流
                    out.write(buffer, 0, len);
                }
                System.out.println("接收完成!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

第9行是创建一个子线程,由于客户端上传的数据分为很多数据包,因此需要一个循环接收数据包。
调用后receive()方法会导致线程阻塞,因此需要将接收数据的处理放到一个子线程中。

第12行是创建DatagramSocket对象,并指定端口8080,作为服务器一般应该明确指定绑定的端口。

与TCP Socket不同UDP Socket无法知道哪些数据包已经是最后一个了,因此需要发送方发出一个特殊的数据包,包中包含了一些特殊标志。代码第27~32行是取出并判断这个标志。

// 客户端
public class UploadClient {
    
    public static void main(String[] args) {
        
        System.out.println("客户端运行...");
        
        try (
            // 创建DatagramSocket对象,由系统分配可以使用的端口
            DatagramSocket socket = new DatagramSocket();
            FileInputStream fin = new FileInputStream("./TestDir/coco2dxcplus.jpg");
            BufferedInputStream in = new BufferedInputStream(fin)) {
            // 创建远程主机IP地址对象
            InetAddress address = InetAddress.getByName("localhost");
            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次从文件流中读取数据
            int len = in.read(buffer);
            while (len != -1) {
                // 创建数据报包对象
                DatagramPacket packet = 
                    new DatagramPacket(buffer, len, address, 8080);
                // 发送数据报包
                socket.send(packet);
                // 再次从文件流中读取数据
                len = in.read(buffer);
            }
            // 创建数据报对象
            DatagramPacket packet = 
                new DatagramPacket("bye".getBytes(), 3, address, 8080);
            // 发送结束标志
            socket.send(packet);
            System.out.println("上传完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上传文件客户端,发送数据不会堵塞线程,因此没有使用子线程。
第10行是创建DatagramSocket对象,由系统分配可以使用的端口,客户端DatagramSocket对象经常自己不指定 了,而是由系统分配。
第32行是发送结束标志,这个结束标志是字符串bye,服务器端接收到这个字符串则结束接收数据包。

案例:聊天工具

// 服务器端
public class ChatServer {
    
    public static void main(String args[]) {
        
        System.out.println("服务器运行...");
        
        // 创建一个子线程
        Thread t = new Thread(() -> {
            try ( // 创建DatagramSocket对象,指定端口8080
                DatagramSocket socket = new DatagramSocket(8080);
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                while (true) {
                    /* 接收数据报 */
                    // 准备一个缓冲区
                    byte[] buffer = new byte[128];
                    DatagramPacket packet = 
                        new DatagramPacket(buffer, buffer.length);
                    socket.receive(packet);
                    // 接收数据长度
                    int len = packet.getLength();
                    String str = new String(buffer, 0, len);
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:【%s】\n", str);
                    /* 发送数据 */
                    // 从客户端传来的数据包中得到客户端地址
                    InetAddress address = packet.getAddress();
                    // 从客户端传来的数据包中得到客户端端口号
                    int port = packet.getPort();
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 读取键盘输入的字节数组
                    byte[] b = keyboardInputString.getBytes();
                    // 创建DatagramPacket对象,用于向客户端发送数据
                    packet = new DatagramPacket(b, b.length, address, port);
                    // 向客户端发送数据
                    socket.send(packet);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

第9行是创建一个子线程,因为socket.receive(packet)方法会阻塞主线程。
服务器给客户端发数据包,也需要知道它的IP地址和端口号,第28行根据接收的数据包获得客户端的地址,第30行根据接收的数据包获得客户端的端口号。

public class ChatClient {
    
    public static void main(String[] args) {
        
        System.out.println("客户端运行...");
        
        // 创建一个子线程
        Thread t = new Thread(() -> {
            try ( // 创建DatagramSocket对象,由系统分配可以使用的端口
                DatagramSocket socket = new DatagramSocket();
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                while (true) {
                    /* 发送数据 */
                    // 准备一个缓冲区
                    byte[] buffer = new byte[128];
                    // 服务器IP地址
                    InetAddress address = InetAddress.getByName("localhost");
                    // 服务器端口号
                    int port = 8080;
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 退出循环,结束线程
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 读取键盘输入的字节数组
                    byte[] b = keyboardInputString.getBytes();
                    // 创建DatagramPacket对象
                    DatagramPacket packet = 
                        new DatagramPacket(b, b.length, address, port);
                    // 发送
                    socket.send(packet);
                    /* 接收数据报 */
                    packet = new DatagramPacket(buffer, buffer.length);
                    socket.receive(packet);
                    // 接收数据长度
                    int len = packet.getLength();
                    String str = new String(buffer, 0, len);
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:【%s】\n", str);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

数据交换格式

数据交换格式主要分为纯文本格式、XML格式、JSON格式,其中纯文本格式是一种简单的、无格式的数据交换方式。

纯文本格式留言条:
“云龙同学”,“你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》。可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!”,“关东升”,“2012年12月08日”

留言条中的4部分数据按照顺序存放,各个部分之间用逗号分隔。数据量小的时候,可以采用这种格式。
XML格式和JSON格式可以带有描述信息,它们叫做“自描述的”结构化文档。

XML格式留言条:

<?xml version="1.0" encoding="UTF-8"?>
<note>
    <to>云龙同学</to>
    <conent>你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!</conent>
    <from>关东升</from>
    <date>2012年12月08日</date>
</note>

尖括号中的内容(<to>…</to>等)就是描述数据的标识,在XML中称为“标签”。

JSON格式留言条:

{to:"云龙同学",conent:"你好!\n今天上午,我到你家来想向你借一本《小学生常用成语词典》。可是不巧,你不在。我准备晚上6时再来借书。请你在家里等我,谢谢!",from:"关东升",data:"2012年12月08日"}

数据放置在大括号{}之中,每个数据项目之前都有一个描述名字,描述名字和数据项目之间用冒号(:)分开。

JSON文档结构

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。所谓轻量级,是与XML文档结构相比而言的,描述项目的字符少,所以描述相同数据所需的字符个数要少,那么传输速度就会提高,而流量却会减少。

构成JSON文档的两种结构为对象和数组。对象是“名称-值”对集合,它类似于Java中Map类型,而数组是一连串元素的集合。
对象是一个无序的“名称/值”对集合,一个对象以{(左括号)开始,}(右括号)结束。每个“名称”后跟一个:(冒号),“名称-值”对之间使用,(逗号)分隔。

JSON对象:

{
    "name":"a.htm",
    "size":345,
    "saved":true
}

数组是值的有序集合,以[(左中括号)开始,](右中括号)结束,值之间使用,(逗号)分隔。

JSON数组:

["text","html","css"]

在数组中,值可以是双引号括起来的字符串、数值、true、false、null、对象或者数组,而且这些结构可以嵌套。

目前Java官方没有提供JSON编码和解码所需要的类库,需要使用第三方JSON库。
推荐 JSON-java库,JSON-java库提供源代码,不依赖于其他第三方库,API在线文档

JSON数据编码和解码

在传输和存储之前需要把JSON对象转换成为字符串才能传输和存储,这个过程称之为“编码”过程。
接收方需要将接收到的字符串转换成为JSON对象,这个过程称之 为“解码”过程。

编码

若想获得如下JSON字符串:

{"name": "tony", "age": 30, "a": [1, 3]}
try {
    JSONObject jsonObject = new JSONObject(); // 对象
    // 把JSON数据项添加到JSON对象jsonObject中
    jsonObject.put("name", "tony");
    jsonObject.put("age", 30);
    
    JSONArray jsonArray = new JSONArray(); // 数组
    // 向JSON数组中添加1和3两个元素
    jsonArray.put(1).put(3);
    // 将JSON数组jsonArray作为JSON对象jsonObject的数据项添加到JSON对象。 
    jsonObject.put("a", jsonArray);
    // 将JSON对象转换为字符串,真正完成了JSON编码过程
    System.out.println(jsonObject.toString())
} catch (JSONException e) {
    e.printStackTrace();
}
解码

解码过程是编码反向操作,如果有如下JSON字符串:

{"name":"tony", "age":30, "a":[1, 3]}

把这个JSON字符串解码成JSON对象或数组:

String jsonString = "{\"name\":\"tony\", \"age\":30, \"a\":[1, 3]}";
try {
    JSONObject jsonObject = new JSONObject(jsonString);
    // 从JSON对象中按照名称取出JSON中对应的数据
    String name = jsonObject.getString("name");
    System.out.println("name : " + name);
    int age = jsonObject.getInt("age");
    System.out.println("age : " + age);
    
    // 取出一个JSON数组对象
    JSONArray jsonArray = jsonObject.getJSONArray("a");
    int n1 = jsonArray.getInt(0);
    System.out.println("数组a第一个元素 : " + n1);
    int n2 = jsonArray.getInt(1);
    System.out.println("数组a第二个元素 : " + n2); } catch (JSONException e) {
    e.printStackTrace();
}

第1行声明一个JSON字符串,网络通信过程中JSON字符串是从服务器返回的。
第3行通过JSON字符串创建JSON对象,这个过程事实上就是JSON字符串解析过程,如果能够成功地创建JSON对象,说明解析成功,如果发生异常则说明解析失败。

案例:聊天工具

客户端与服务器之间采用JSON数据交换格式,JSON格式内部结构是自定义的。

{"message":"Hello","userid":"javaee","username":"关东升"}
// 服务器端

import org.json.JSONObject;

public class ChatServer {
    
    public static void main(String[] args) {
        
        System.out.println("服务器运行...");
        Thread t = new Thread(() -> {
            try ( // 创建一个ServerSocket监听端口8080客户请求
                ServerSocket server = new ServerSocket(8080);
                // 使用accept()阻塞等待客户端请求
                Socket socket = server.accept();
                DataInputStream in = new DataInputStream(socket.getInputStream());
                DataOutputStream out = 
                new DataOutputStream(socket.getOutputStream());
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                
                while (true) {
                    /* 接收数据 */
                    String str = in.readUTF();
                    // JSON解码
                    JSONObject jsonObject = new JSONObject(str);
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:%s\n", jsonObject);
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 编码
                    jsonObject = new JSONObject();
                    jsonObject.put("message", keyboardInputString);
                    jsonObject.put("userid", "acid");
                    jsonObject.put("username", "赵1");
                    // 发送
                    out.writeUTF(jsonObject.toString());
                    out.flush();
                }
            } catch (Exception e) {
            }
            System.out.println("服务器停止...");
        });
        t.start();
    }
}
// 客户端

import org.json.JSONObject;

public class ChatClient {
    
    public static void main(String[] args) {
        
        System.out.println("客户端运行...");
        
        Thread t = new Thread(() -> {
            try ( // 向127.0.0.1主机8080端口发出连接请求
                Socket socket = new Socket("127.0.0.1", 8080);
                DataInputStream in = new DataInputStream(socket.getInputStream());
                DataOutputStream out = 
                new DataOutputStream(socket.getOutputStream());
                BufferedReader keyboardIn = 
                new BufferedReader(new InputStreamReader(System.in))) {
                
                while (true) {
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("message", keyboardInputString);
                    jsonObject.put("userid", "javaee");
                    jsonObject.put("username", "关东升");
                    // 发送
                    out.writeUTF(jsonObject.toString());
                    out.flush();
                    /* 接收数据 */
                    String str = in.readUTF();
                    jsonObject = new JSONObject(str);
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:%s \n", str);
                }
            } catch (ConnectException e) {
                System.out.println("服务器未启动!");
            } catch (Exception e) {
            }
            System.out.println("客户端停止!");
        });
        t.start();
    }
}

访问互联网资源

Java的java.net包中还提供了高层次网络编程类——URL,通过URL类访问互联网资源。
使用URL进行网络编程,不需要对协议本身有太多的了解,相对而言是比较简单的。

URL概念

互联网资源是通过URL(Uniform Resource Locator,“一致资源定位器”)指定的。

URL组成格式:协议名://资源名
协议名:指明获取资源所使用的传输协议,如http、ftp、gopher和file等,
资源名:资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。
例:
http://www.sina.com/
http://home.sohu.com/home/welcome.html
http://www.51work6.com:8800/Gamelan/network.html#BOTTOM

HTTP/HTTPS协议

访问互联网大多都基于HTTP/HTTPS协议。

HTTP协议

HTTP(Hypertext Transfer Protocol,超文本传输协议),是一个属于应用层的面向对象的协议,其简捷、快速的方式适用于分布式超文本信息的传输。
HTTP协议支持C/S网络结构,是无连接协议,即每一次请求时建立连接,服务器处理完客户端的请求后,应答给客户端然后断开连接,不会一直占用网络资源。

HTTP/1.1协议共定义了8种请求方法:OPTIONS、HEAD、GET、POST、PUT、DELETE、 TRACE和CONNECT。在HTTP访问中,一般使用GET和HEAD方法。
GET方法:是向指定的资源发出请求,发送的信息“显式”地跟在URL后面。GET方法应该只用在读取数据,例如静态图片等。是不安全的。
POST方法:是向指定资源提交数据,请求服务器进行处理,例如提交表单或者上传文件等。数据被包含在请求体中。是安全的。

HTTPS协议

HTTPS(Hypertext Transfer Protocol Secure,超文本传输安全协议),是超文本传输协议和SSL 的组合,用以提供加密通信及对网络服务器身份的鉴定。

HTTPS是HTTP的升级版,HTTPS与HTTP的区别:
HTTPS使用https://代替http://,
HTTPS使用端口443,而HTTP使用端口80来与TCP/IP进行通信。
SSL使用40位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。
HTTPS和SSL支持使用X.509数字认证,如果需要的话,用户可以确认发送者是谁。

使用URL类

Java 的java.net.URL类用于请求互联网上的资源,采用HTTP/HTTPS协议,请求方法是GET方法,一般是请求静态的、少量的服务器端数据。

构造方法:
URL(String spec) 根据字符串表示形式创建URL对象。
URL(String protocol, String host, String file) 根据指定的协议名、主机名和文件名称创建URL对 象。
URL(String protocol, String host, int port, String file) 根据指定的协议名、主机名、端口号和文件名称创建URL对象。

常用方法:
InputStream openStream() 打开到此URL的连接,并返回一个输入流。
URLConnection openConnection() 打开到此URL的新连接,返回一个URLConnection对象。

public class HelloWorld {
    
    public static void main(String[] args) {
        // Web网址
        String url = "http://www.sina.com.cn/";
        
        URL reqURL;
        try {
            reqURL = new URL(url);
        } catch (MalformedURLException e1) {
            return;
        }
        
        try ( // 打开网络通信输入流
            InputStream is = reqURL.openStream();
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr)) {
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                sb.append('\n');
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用HttpURLConnection发送GET请求

由于URL类只能发送HTTP/HTTPS的GET方法请求,如果要想发送其他的情况或者对网络请求有更深入的控制时,可以使用HttpURLConnection类型。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HelloWorld {
    
    // Web服务网址
    static String urlString = "http://www.51work6.com/service/mynotes/WebService.php?" + "email=<换成你在51work6.com注册时填写的邮箱>&type=JSON&action=query";
    
    public static void main(String[] args) {
        BufferedReader br = null;
        HttpURLConnection conn = null;
        try {
            URL reqURL = new URL(urlString);
            conn = (HttpURLConnection) reqURL.openConnection();
            // 设置请求方法为GET方法
            conn.setRequestMethod("GET");
            
            // 打开网络通信输入流
            InputStream is = conn.getInputStream();
            // 通过is创建InputStreamReader对象
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            // 通过isr创建BufferedReader对象
            br = new BufferedReader(isr);
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                // 断开连接,可以释放资源
                conn.disconnect();
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

发送GET请求时发送给服务器的参数是放在URL的“?”之后,参数采用键值对形式,例如: 第11行的URL中type=JSON是一个参数,type是参数名,JSON是参数名,服务器端会根据参数名获得参数值。多个参数之间用“&”分隔,例如type=JSON&action=query就是两个参数。

代码第18行是用reqURL.openConnection()方法打开一个连接,返回URLConnection对象,由于本次连接是HTTP连接,所以返回的是HttpURLConnection对象。
URLConnection是抽象类,HttpURLConnection是URLConnection的子类。

从服务器端返回的数据是JSON字符串,格式化后内容:

{
    "ResultCode": 0,
    "Record": [
        {
            "ID": 5238,
            "CDate": "2017-05-18",
            "Content": "欢迎来到智捷课堂。"
        },
        {
            "ID": 5239,
            "CDate": "2018-10-18",
            "Content": "Welcome to zhijieketang."
        }
    ]
}

《Java从小白到大牛》——关东升:
上述示例中URL所指向的Web服务是由作者所在的智捷课堂提供的,读者要想使用这个 Web服务需要在www.51work6.com进行注册,注册时需要提供自己有效的邮箱,这个邮箱用来激活用户。在网络请求时需要提交email参数,这个参数是注册时填写的邮箱。

使用HttpURLConnection发送POST请求

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HelloWorld {
    
    // Web服务网址
    static String urlString = 
        "http://www.51work6.com/service/mynotes/WebService.php";
    
    public static void main(String[] args) {
        
        BufferedReader br = null;
        HttpURLConnection conn = null;
        try {
            URL reqURL = new URL(urlString);
            // 通过reqURL.openConnection()建立HTTP连接
            conn = (HttpURLConnection) reqURL.openConnection();
            conn.setRequestMethod("POST");
            // 设置请求过程中可以传递参数给服务器
            conn.setDoOutput(true);
            
            // 设置请求参数格式化字符串
            String param = String.format("email=%s&type=%s&action=%s", "<换成你在51work6.com注册时填写的邮箱>", "JSON", "query");
            // 设置参数
            // conn.getOutputStream()打开输出流
            // new DataOutputStream(conn.getOutputStream())创建基于数据输出流
            DataOutputStream dStream = new DataOutputStream(conn.getOutputStream());
            // 向输出流中写入数据
            dStream.writeBytes(param);
            // 关闭流,并将数据写入到服务器端
            dStream.close();
            
            // 打开网络通信输入流
            InputStream is = conn.getInputStream();
            // 通过is创建InputStreamReader对象
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            // 通过isr创建BufferedReader对象
            br = new BufferedReader(isr);
            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

第12行URL后面不带参数,因为要发送的是POST请求,POST请求参数是放在请求体中。
第32~36行是将请求参数发送给服务器。

实例:Downloader

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class Downloader {
    
    // Web服务网址
    private static String urlString = "https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/" + "static/superman/img/logo/bd_logo1_31bdc765.png";
    
    public static void main(String[] args) {
        download();
    }
    
    // 下载方法
    private static void download() {
        
        HttpURLConnection conn = null;
        
        try {
            // 创建URL对象
            URL reqURL = new URL(urlString);
            // 打开连接
            conn = (HttpURLConnection) reqURL.openConnection();
            
            try (// 从连接对象获得输入流
                InputStream is = conn.getInputStream();
                BufferedInputStream bin = new BufferedInputStream(is);
                // 创建文件输出流
                OutputStream os = new FileOutputStream("./download.png");
                BufferedOutputStream bout = new BufferedOutputStream(os);) {
                byte[] buffer = new byte[1024];
                int bytesRead = bin.read(buffer);
                while (bytesRead != -1) {
                    bout.write(buffer, 0, bytesRead);
                    bytesRead = bin.read(buffer);
                }
            } catch (IOException e) {
            }
            System.out.println("下载完成。");
        } catch (IOException e) {
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }
}

需要关闭什么流的,放到finally,而不是在try()里面用自动资源管理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值