网络编程
java.net,包含了网络编程所需要的最基础一些类和接口。
这些类和接口面向两个不同的层次:基于Socket的低层次网络编程和基于URL的高层次网络编程。
高低层次就是通信协议的高低层次,Socket采用TCP、UDP等协议,这些协议属于低层次的通信协议;URL采用HTTP和HTTPS这些属于高层次的通信协议。
低层次网络编程,因为它面向底层,比较复杂,但是“低层次网络编程”并不等于它功能不强大。恰恰相反,正因为层次低,Socket编程与基于URL的高层次网络编程比较,能够提供更强大的功能和更灵活的控制,但是要更复杂一些。
网络基础
网络结构
网络结构是网络的构建方式,目前流行的有客户端服务器结构网络和对等结构网络。
-
客户端服务器结构网络
客户端服务器(Client Server,缩写C/S)结构网络,是一种主从结构网络。
服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。
服务器是被动的,客户端是主动的。生活中很多网络服务都采用这种结构:Web服务、文件传输服务和邮件服务等。
-
对等结构网络
对等结构网络也叫点对点网络(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()里面用自动资源管理。