JAVA编程讲义之网络编程
Java语言的优点之一就是对网络通信的支持,通过一系列的网络支持类,Java程序能够方便的访问互联网上的资源,还可以向远程资源发送GET、POST请求。Java虚拟机已经实现了底层复杂的网络协议,使用Java进行网络编程时,只需调用Java标准库提供的接口,就可以简单高效地编写网络程序。本章将针对Java网络编程展开讲解,内容涉及网络编程基础、UDP通信技术、TCP通信技术、代理服务器网络、HTTPClient模块等。在当今万物皆互联的时代,只有充分理解并掌握Java网络编程,才能设计出适应互联网时代的优秀程序。
16.1 网络编程基础
时至今日,互联网技术已经深入到人们生活中的角角落落,万物互联的时代已经到来,支持万物互联的基础就是网络,所有本节首先介绍网络编程的基础知识。
16.1.1 网络通信协议
21世纪又被称为信息化时代,计算机网络遍布于人们生活的各个角落。计算机网络的种类很多,通常是按照规模大小和延伸范围来分类的,最常见的划分为:局域网(LAN)、城域网(MAN)和广域网(WAN),我们平时最熟悉的Internet可以说是目前世界上最大的广域网。
计算机网络给我们的生活带来了很大的便利,但是要在计算机网络中实现通信必须有一些约定,就像我们常说的无规矩不成方圆,没有规则约束的网络会像没有交通规则的马路一样,出现瘫痪,互联网中的这些约定被称为网络通信协议。他负责对传输速率、传输代码、传输格式、传输控制步骤、出错控制等做出统一规定。通信协议通常由三部分组成:一是语义部分,用于决定双方对话讲的是什么;二是语法部分,用于决定双方对话的格式;三是变换规则,用于决定通信双方的应答关系。
最早的互联网络通信协议是开放系统互联,又称为OSI(Open System Interconnection),是由国际标准化组织ISO于1987年发起的,力求将网络简化,并以模块化的方式来设计网络,把计算机网络分成7层,分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
目前运用最为广泛的通信协议是IP(Internet Protocol)协议,又称为互联网协议,它能提供完善的网路连接功能,与IP协议放在一起的还有TCP(Transmission Control Protocol)协议,即传输控制协议,它规定一种可靠的数据信息传递服务。TCP与IP是在同一时期作为协议来设计的,功能互补,所以常统称为TCP/IP协议,它是事实上的国际标准。TCP/IP协议模型将网络分为4层,分别为物理+数据链路层、网络层、传输层和应用层,每层分别负责不同的通信功能,它与OSI的7层模型对应关系和各层对应协议如图16.1所示。
在图16.1中,列举了TCP/IP参考模型的分层以及分层所对应的协议,这里简单介绍各层如下:
• 物理+数据链路层:物理层是负责数据传输的硬件,但是因为人们在物理层面上所使用的传输媒介不同,导致网络的安全性、延迟等有所不同,所以在这方面目前还没有一个既定的标准。数据链路层是用于定义物理传输通道,属于接口层,可以把它看作NIC(也就是我们经常说的网卡)的驱动程序。
• 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,并将分组数据发送到目标计算机或者网络。 网络层的功能通常由操作系统提供,尤其是路由器,它必须实现通过互联网层转发分组数据包的功能。此外,链接互联网的所有主机跟路由器必须实现IP的功能。其他链接互联网的网络设备就没必要一定实现IP或TCP的功能。
• 传输层: 传输层最主要的功能就是能够让应用程序之间实现通信。在计算机内部,通常同一时间运行着多个程序。为此必须分清是哪些程序与哪些程序在进行通信。识别这些应用程序的是端口号,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
• 应用层: 应用层作为和用户交互的最高层,他的任务是为互联网中的各种网络应用提供服务。应用层的具体内容就是规定应用进程在通信时所遵循的协议,TCP/IP 应用的架构绝大多数属于客户端/服务端模型。提供服务的程序叫服务端,接受服务的程序叫客户端。在这种通信模式中,提供服务的程序会预先被部署在主机上,等待接收任何时刻客户可能发送的请求。客户端可以随时发送请求给服务端, 有时服务端可能会出现异常、超出负载等情况,这时客户端可以在等待片刻后重发一次请求。
16.1.2 IP地址和端口号
在Internet网络上存在着数以亿记的设备(不只是电脑,还包括我们的手机、iPad等联网设备),就像我们的身份证号一样,我们需要为每台计算机指定一个标识号,通过这个标识号来指定接收或发送数据的设备,在TCP/IP协议中,这个标识号就是IP地址,它能唯一地标识 Internet上的所有设备。
IP地址是数字型的,目前我们使用最为广泛的是IPV4,它由四个字节也就是一个32位整数表示,但这样不方便用户去记忆识别,例如01111011001110001001100111001110,相信大家看见这种ip都会觉得头皮发麻,所以我们通常把它分成4个8位的二进制数,每8位之间用圆点隔开,每8位整数可以转换成一个0~255的十进制整数,例如:192.168.0.1。
虽然IPV4已经包含了足够多的ip地址,但是随着互联网的迅速发展,IPV4终究还是会面临资源枯竭的问题,所以又一版本IPV6随之而出,IPV6使用十六个字节表示IP地址,所拥有的地址容量是IPV4的2的96次方倍,号称可以为地球上的每一粒沙创建一个ip,彻底解决了ip地址不够使用的问题,虽然目前IPV6还没完全普及,不过阿里巴巴集团曾定下在2025年全部清退ipv4的目标。
通过IP地址可以唯一标识网络上的一个通信设备,但一个通信设备可以由多个通信程序同时提供网络服务,就像我们要找到一个客服人员,除了要知道他是哪个公司的以外,还需要知道他的工号,我们要想找到具体的某个网络程序,也需要为他们加上唯一标识来区分不同的应用程序,不同应用程序处理不同端口上的数据。我们将这个唯一标识称为端口号,比如计算机同时运行QQ和MSN,如果不为他们添加端口号,也就没办法为他们制定规则,势必发生冲突紊乱。
端口号是一个16位的二进制整数,取值范围0~65535,其中0~1023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,避免端口号冲突。通常将它们分为如下3类:
• 公认端口(Well Known Ports):从0到1023,它们紧密绑定(Binding) 一些特定的服务。
• 注册端口(Registered Ports):从1024到49151,它们松散地绑定一些服务。应用程序通常应该使用这个范围内的端口,例如Tomcat端口8080,MySQL端口3306。
• 动态和/或私有端口 (Dynamic and/or Private Ports):从49152到65535,这些端口是应用程序使用的动态端口,应用程序一般不会主动使用这些端口。
接下来,这里用一张图来描述IP地址和端口号作用,如图16.2所示。
图16.2 IP地址和端口号
图16.2中,IP为192.168.100.1的计算机和IP为192.168.100.2的计算机的微信应用相互通信,先要根据IP地址找到网络位置,然后根据端口号找到具体的应用程序。
16.1.3 InetAddress工具类
在上一节中讲解了IP和通信地址的相关知识,在JDK中提供了InetAddress类来代表IP地址,InetAddress有2个子类,对应IP的两个版本,分别为Inet4Address类和Inet6Address类,它们分别代表IPv4和IPv6的地址。InetAddress类提供了5个静态方法来获取InetAddress实例,如表16.1所示:
表16.1 InetAddress静态方法
方法声明 |
功能描述 |
static InetAddress[] getAllByName(String host) |
通过主机名返回其所有IP 地址所组成的数组。 |
static InetAddress getByAddress(byte[] addr) |
通过原始IP地址返回 InetAddress 对象。 |
static InetAddress getByAddress(String host, byte[] addr) |
通过主机名和IP 地址创建 InetAddress。 |
static InetAddress getByName(String host) |
通过主机名,确定主机的 IP 地址。 |
static InetAddress getLocalHost() |
返回本机的IP地址。 |
另外,InetAddress类还有一些常用方法,如表16.2所示。
表16.2 InetAddress常用方法
方法声明 |
功能描述 |
String getCanonicalHostName() |
通过IP地址返回全限定域名。 |
String getHostAddress() |
返回InetAddress实例对应的IP地址字符串。(点分十进制) |
String getHostName() |
返回此IP地址的主机名。 |
boolean isReachable(int timeout) |
判定指定时间内是否可以访问目标地址。 |
接下来,通过案例来演示这些方法的使用,如例16-1所示。
例16-1 Demo1601.java
1 package com.aaa.p1601;
2 import java.net.InetAddress;
3
4 public class Demo1601 {
5 public static void main(String[] args) throws Exception {
6 // 先获取到本机的地址并打印
7 InetAddress localHost = InetAddress.getLocalHost();
8 System.out.println("本机地址为:" + localHost);
9 // 如果只需要获取到本机的ip号,则需要用到getHostAdress()方法
10 System.out.println("获取本机ip为:"+localHost.getHostAddress());
11 // 获取主机名为"www.aaajiaoyu.cn"的InetAddress地址,并打印
12 InetAddress address = InetAddress.getByName("www.aaajiaoyu.cn");
13 System.out.println(address);
14 // 打印"aaajiaoyu"的ip地址
15 System.out.println("aaajiaoyu的ip为:"+address.getHostAddress());
16 // 打印"aaa.jiaoyu"的主机名;
17 System.out.println("aaajiaoyu的主机名为:"+address.getHostName());
18 // 判断能否在3秒内进行访问
19 System.out.println("3秒内可以访问:"+address.isReachable(3000));
20 }
21 }
程序运行结果如下:
本机地址为:PC-202006302049/192.168.145.199
获取本机ip为:192.168.145.199
www.aaajiaoyu.cn/122.116.169.212
aaajiaoyu的ip为:122.116.169.212
aaajiaoyu的主机名为:www.aaajiaoyu.cn
3秒内可以访问:true
例16-1中,先调用getLocalHost()方法获取本地IP地址对应的InetAddress实例并打印本机IP地址,然后根据主机名“www.aaajiaoyu.cn”获得InetAddress实例,再打印出InetAddress实例对应的IP地址和主机名,最后打印3秒内是否可达这个实例,这是InetAddress类的基本使用。
16.1.4 URLDecoder类和URLEncoder类
在学习本节知识之前,首先我们要认识一个叫做application/x-www-form-urlencoded MiME的字符串。很多读者看见一串这么长的字符串可能会觉得有点懵,其实这个字符串我们经常见到,只是不知道它什么名字而已,比如在我们使用浏览器进行关键词搜索时,系统会自动将我们所使用的中文转换成特殊字符串,例如图16.3所示,我们在浏览器中搜索‘AAA甲骨文’。
图16.3 关键字包含中文
在图 16.3中,我们可以看到,当搜索关键字包含中文时,导航栏地址会显示乱码,其实这不是乱码,只是系统在识别到非西欧字符串时,会自动将它换为特殊字符串,当我们在编程过程中遇到这种需要进行普通字符和特殊字符之间的转换时,我们就要用到本节的知识点:URLDecoder类和URLEncoder类。
当我们将普通字符串转换成特殊字符串时,需要用到URLDecoder类中的encode(String s, String enc)方法,当将特殊字符转换为普通字符时,就需要用到URLEncoder类中的decode(String s, String enc)方法。接下来我们用一个小案例来演示一下它们的使用方法。
例16-2 Demo1602.java
1 package com.aaa.p1602;
22 import java.net.URLDecoder;
23 import java.net.URLEncoder;
24
25 public class Demo1602 {
26 public static void main(String[] args)throws Exception {
27 // 将普通字符串转换为 application/x-www-form-urlencoded字符串
28 String url = URLEncoder.encode("有梦想我们一起实现","utf-8");
29 System.out.println("转换后的特殊字符串:"+url);
30 // 将 application/x-www-form-urlencoded字符串转换为普通字符串
31 String world = URLDecoder.decode("%E6%9C%89%E6%A2%A6%E6%83%B3%E6%88%91%E4
32 %BB%AC%E4%B8%80%E8%B5%B7%E5%AE%9E%E7%8E%B0","utf-8");
33 System.out.println("转换后的普通字符串:" + world);
34 }
35 }
程序运行结果如下:
转换后的特殊字符串:
%E6%9C%89%E6%A2%A6%E6%83%B3%E6%88%91%E4%BB%AC%E4%B8%80%E8%B5%B7%E5%AE%9E%E7%8E%B0
转换后的普通字符串:有梦想我们一起实现
知识点拨:Java只有包含中文字符的字符串需要转换,转换方法是每个中文字符占两个字节,每个字节可以转换成两个十六进制的数字,所以每个中文字符将转换成“%XX%XX”的形式,采用不同的字符集时,每个中文字符对应的字节数并不完全相同,所以使用URLEncoder和URLDecoder进行转换时也需要指定字符集。
16.1.5 URL、URLConenction和URLPermission
URL统一资源定位系统(uniform resource locator)是因特网 的万维网服务程序上用来指定信息位置的表示方法,在通常情况下,URL可以由协议名、主机、端口和资源组成。它最初是由蒂姆 · 伯纳斯 · 李 发明用来作为万维网 的地址。现在它已经被万维网联盟 编制为互联网标准RFC 1738。
URL类提供了多个构造器用于创建URL对象,还提供了很多方法用来访问对应的资源,如表16.3所示。
表16.3 URL常用方法
方法声明 |
功能描述 |
String getFile() |
获取该URL的资源名 |
String getHost() |
获取该URL的主机名 |
String getPath() |
获取该URL的路径部分 |
Int getPort() |
获取该URL的端口号 |
String getProtocol() |
获取该URL的协议名称 |
URLConnection openConnection() |
返回一个URLConnection对象,代表了与URL所引用的远程对象的连接 |
InputStream openStream() |
打开与此URL的连接,并返回一个用于读取该URL资源的InputStream |
URLConnection是一个抽象类,表示指向URL和应用程序的通信连接。程序可以通过URLConnection实例向该URL发送请求、读取URL引用的资源。
通常创建一个URL的连接,并发送请求,读取URL引用的资源需要几个步骤:
• 构造一个URL对象。
• 通过调用URL对象的openConnection()方法来创建URLConnection对象。
• 设置URLConnection的参数和普通请求属性。
• 读取首部字段。
• 远程资源变为可用。
URLPermission类是Java8新增的一个工具类,用来管理HttpURLConnection的权限问题,如果在HttpURLConnection安装了安全管理器,通过该对象打开连接时就需要首先获得权限。如表16.4和表16.5所示,分别给出了URLPermission类的构造方法与常用方法。
16.4 URLPermission构造方法
构造方法说明 |
功能描述 |
URLPermission (String url) |
从url字符串创建一个新的URLPermission,它允许给定的请求方法和用户可设置的请求标头。 |
URLPermission (String url, String actions) |
通过调用两个参数构造函数,使用给定的url字符串和不受限制的方法以及请求标头创建URLPermission,如下所示:URLPermission(url,“*:*”)。 |
表16.5 URLPermission常用方法
方法说明 |
功能描述 |
如果this.getActions().equals(p.getActions())并且p的url等于这个url,则返回true。 |
|
String getActions () |
返回规范化方法列表和请求标头列表,格式如下: "method-names : header-names" |
int hashCode () |
返回根据actions 字符串和url字符串的哈希码计算出的哈希码。 |
boolean implies (Permission p) |
检查此URLPermission是否匹配给定的权限。 |
16.2 UDP通信
UDP协议是英文User Datagram Protocol的缩写,即用户数据报文协议。UDP协议是一种不可靠的网络协议,它在通信实例双方各自创建一个Socket,但是这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报文的对象。
16.2.1 UDP概念
UDP在通信时没有创建虚拟链路,所有是一种面向无连接的协议。UDP通信的过程就像是邮政公司在两个网点之间进行邮件配送一样,在网点发送和接收邮件时都需要使用快递车来装载货物。在使用UDP通信过程中,发送和接收的数据也需要使用“快递车”进行统一装车运输,所以在JDK中提供了一个DatagramPacket类,这个类的实例对象就相当于一个快递车,用于封装UDP通信中发送或者接收的数据。然而运输邮件只有“快递车”是不够的,还需要有邮政网点用来承载。为此,JDK提供了与之对应的DatagramSocket类,这个类的作用就相当于各个网点,使用这个类的实例对象就可以发送和接收DatagramPacket数据报,发送和接收数据的过程如图16.4所示。
16.2.2 DatagramPacket
我们在前面讲解UDP在发送数据的时候,需要先使用java.net包中的DatagramPacket类将数据封装成数据包,DatagramPacket类就表示存放数据的数据包,它的构造方法如表16.6所示。
表16.6 DatagramPacket类构造方法
构造方法声明 |
功能描述 |
public DatagramPacket(byte[] buf, int length) |
构造 DatagramPacket用于接收长度为 length数据包 |
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) |
构造一个数据报包,用于将长度为length的数据包发送到指定主机上的指定端口号 |
public DatagramPacket(byte[] buf, int offset, int length) |
构造 DatagramPacket用于接收长度为 length,偏移量为offset数据包 |
public DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) |
构造一个数据报包,用于将长度为 length且偏移量为 offset的数据包发送到指定主机上的指定端口号 |
public DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个数据报包,用于将长度为 length的数据包发送到指定主机上的指定端口号 |
public DatagramPacket(byte[] buf, int length, SocketAddress address) |
构造数据报包,用于将长度为 length的数据包发送到指定主机上的指定端口号 |
DatagramPacket还有一些常用方法,如表16.7所示。
表16.7 DatagramPacket常用方法
方法声明 |
功能描述 |
InetAddress getAddress() |
返回发送此数据报或从中接收数据报的计算机的IP地址 |
byte[] getData() |
返回数据缓冲区 |
int getLength() |
返回将要发送或接收到的数据的长度 |
int getPort() |
返回发送此数据报或从中接收数据报的远程主机上的端口号 |
SocketAddress getSocketAddress() |
获取要将此包发送到的或发出此数据报的远程主机的 SocketAddress(通常为 IP 地址 + 端口号) |
16.2.3 DatagramSocket
DatagramSocket类是与DatagramPacket类关系密切的一个类,它位于java.net包中,是数据报文通信的Socket,包含了源IP地址和目的IP地址以及源端口号和目的端口号,用于创建发送端和接收端对象,但是在创建发送端和接收对象时,需要使用不同的构造方法,DatagramSocket类的构造方法如表16.8所示。
表16.8 DatagramSocket类构造方法
构造方法声明 |
功能描述 |
public DatagramSocket() |
构造数据报套接字并将其绑定到本地主机上任何可用的端口(1-65535) |
protected DatagramSocket(DatagramSocketImpl impl) |
使用指定的DatagramSocketImpl创建未绑定的数据报套接字 |
public DatagramSocket(int port) |
构造一个数据报套接字并将其绑定到本地主机上的指定端口 |
public DatagramSocket(int port, InetAddress laddr) |
构造数据报套接字,将其绑定到指定的本地地址 |
public DatagramSocket(SocketAddress bindaddr) |
构造数据报套接字,将其绑定到指定的本地套接字地址 |
DatagramSocket类还有一些常用方法,如表16.9所示。
表16.9 DatagramSocket常用方法
方法声明 |
功能描述 |
int getPort() |
返回此数据报文套接字连接的端口 |
boolean isConnected() |
返回此数据报文套接字的连接状态 |
void receive(DatagramPacket p) |
从此数据报文套接字接收数据报包 |
void send(DatagramPacket p) |
从此数据报文套接字发送数据报包 |
void close() |
关闭此数据报文套接字 |
16.2.4 UDP网络程序
前面讲解了DatagramPacket类和DatagramSocket类,接下来通过一个实战案例来学习它们的使用,这里需要创建一个发送端程序,一个接收端程序,在运行程序时,必须首先运行接收端程序才能接受来自发送端程序发送的信息,接下来开始编写接收端程序,如例16-3所示。
例16-3 Demo1603.java
36 package com.aaa.p1603;
37 import java.net.DatagramPacket;
38 import java.net.DatagramSocket;
39
40 public class Demo1603 {
41 public static void main(String[] args) throws Exception {
42 // 创建DatagramSocket对象,并且为他创建端口号 8888
43 DatagramSocket socket = new DatagramSocket(8888);
44 // 创建一个长度1024字节的数组,用来接收发送端发送的数据
45 byte[] array = new byte[1024];
46 // 创建一个DatagramPacket对象,用来存放数据
47 DatagramPacket packet = new DatagramPacket(array, array.length);
48 // 验证DatagramSocket是否做好接收数据的准备
49 System.out.println("等待接收数据");
50 // DatagramSocket对象等待接收发送端的数据,如果等不到数据,会发生阻塞,一直等下去
51 socket.receive(packet);
52 // 定义一个字符串,用来获取DatagramPacket接收到的源数据
53 String s = new String(packet.getData(), 0, packet.getLength());
54 // 打印发送端的ip地址,端口号,以及发送过来的数据
55 System.out.println("ip地址为:"+packet.getAddress().getHostAddress() +
56 "里的端口号:" + packet.getPort() + "告诉您:"+s);
57 socket.close();
58 }
59 }
程序运行结果如下:
等待接收数据
例16-3中,先创建了DatagramSocket对象,并指定它的端口号为8888,监听8888端口,然后创建大小为1024字节的数组用来接收数组,并创建DatagramPacket对象,用于接收数据,最后调用receive(DatagramPacket p)方法等待接收数据,接收到数据以后,数据会填充到DatagramPacket中,并输出,但是我们从运行结果可以看到,程序在接收数据的时候发生了阻塞,不再往下运行,这是因为我们只创建了接收端,并没有数据传输过来,端口会一直等待数据,直到数据传送过来为止。
接下来,我们创建一个发送端,用来发送数据,如例16-4所示。
例16-4 Demo1604.java
1 package com.aaa.p1604;
2 import java.net.*;
3
4 public class Demo1604 {
5 public static void main(String[] args) throws Exception {
6 // 创建DatagramSocket对象,作为发送端端口,并指明其端口号为8090
7 DatagramSocket socket = new DatagramSocket(8090);
8 // 定义我们要发送的数据
9 String str = "好好学习天天向上";
10 /**
11 * 创建DatagramPacket对象,用来向接收端发送数据
12 * str.getBytes():因为我们使用的是字节流传输,所以要讲字符串转换为字节
13 * 指定接收端ip为本机,端口号为8888
14 **/
15 DatagramPacket packet = new DatagramPacket(str.getBytes(),0,
16 str.getBytes().length,InetAddress.getByName("localhost"),8888);
17 System.out.println("发送消息");
18 // 通过send()方法将数据添加到DatagramSocket中,发送出去
19 socket.send(packet);
20 // 关闭DatagramSocket,释放资源
21 socket.close();
22 }
23 }
程序运行结果如下:
等待接收数据
在例16-4中,先创建了DatagramSocket对象,并为它指定端口号为8090,使用这个端口向接收端发送数据,然后将要发送的字符串转换为字节数组作为要发送的数据,因为我们这里是模拟,因此使用本机ip作为接收端的ip号,并且指定接收端端口号为8888,注意,我们在这里指定的端口号必须与我们创建的接收端监听的端口号一致,最后调用send(DatagramPacket p)方法发送数据。
在创建好发送端以后,先运行我们创建好的接收端程序,让其进入阻塞状态,再运行发送端程序,这时接收端程序就会结束阻塞状态,程序的运行结果如下所示。
等待接收数据
ip地址为:127.0.0.1里的端口号:8090告诉您:好好学习天天向上
例16-4中运行结果打印出了接收端接收到的数据信息,接收到字符串“好好学习天天向上”,它来自IP为127.0.0.1,也就是我们本机的ip号,端口号为8090的发送端,这里的8090就是在我们在例16-4中第6行代码指定的端口号。
另外,我们在创建自定义端口时,可能会出现异常,比如在运行例16-3时,程序运行会报错,提示“Address already in use: Cannot bind”,中文含义为“端口已经被占用”,出现错误的原因在于,操作系统中的端口8888存在冲突而报错。要解决这个问题,如果这个程序不太重要,只需要把占用端口的程序关闭就可以了,首先我们要查询是哪个程序在占用端口,打开cmd命令行窗口,输入命令“netstat -ano | findstr 8888”查询当前设备所有用到端口号8888的进程,如图16.5所示。
图16.5中的界面,是我们使用cmd命令行窗口找到的端口号和它对应的PID,其中8888端口已经被占用,占用这个端口号程序的PID为5260,然后同时按下Ctrl+ALT+Delete组合键,打开Windows任务管理器,点击选项卡中的“详细信息”,如图16.6所示。
图16.5 终端查询结果
找到任务管理器后,点击PID选项,让其顺序排列,然后找到我们要找的进程,右键点击结束任务即可,如图16.7所示。这时再运行例16-3结果就正常了,不会出现端口被占用的现象。
图16.6 Windows任务管理器 图16.7 关闭进程
16.2.5 UDP案例——聊天程序
通过前面的学习,我们知道通过UDP可以使用发送端为指定端口发送数据,下面来实现一个接收端和发送端互相通信的小程序——聊天窗口。
我们之前已经学习过多线程,那么这个案例就需要我们把多线程和UDP结合起来,将接收端和发送端一起运行,也就是使用多线程运行两个线程,实现步骤如下:
• 接收方:
• 创建一个接收方对象,实现Runnable接口。
• 接收方创建一个DatagramSocket,指定端口号不然无法接收信息。
• 接收方创建一个DatagramPacket(缓存区不需要指定IP)。
• 调用DatagramSocket.receive方法进行接收。
• 发送方:
• 创建发送方对象,实现Runnable接口。
• 发送方创建一个DatagramSocket。
• 发送方创建一个DatagramPacket指定IP 指定端口号。
• 发送方调用DatagramSocket的send方法发送信息。
具体代码如例16-5所示。
例16-5 Demo1605.java
1 package com.aaa.p1605;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStreamReader;
6 import java.net.DatagramPacket;
7 import java.net.DatagramSocket;
8 import java.net.InetAddress;
9 import java.net.SocketException;
10
11 public class Demo1605 {
12 public static void main(String[] args) {
13 // 在main方法调用两个线程,让他们一起运行
14 new Thread(new SenderThread()).start();
15 new Thread(new receiveThread()).start();
16 }
17 }
18
19 /**
20 * 发送端线程
21 */
22 class SenderThread implements Runnable {
23 public void run() {
24 try {
25 // 创建发送端的DatagramSocket,并为其指定端口
26 DatagramSocket socket = new DatagramSocket(8083);
27 // 从控制台获得用户输入信息
28 BufferedReader reader = null;
29 reader = new BufferedReader(new InputStreamReader(System.in));
30 // 定义空字符串,如果在do()方法里创建的话会因为作用域造成编译异常
31 String message = null;
32 do {
33 // 将控制台获取到的信息赋值给message
34 message = reader.readLine();
35 // 将数据封装在DatagramPacket对象里,并指定接收端的IP和端口号
36 int length = message.getBytes().length;
37 InetAddress inetAddress = InetAddress.getByName("localhost");
38 DatagramPacket packet = null;
39 packet = new DatagramPacket(message.getBytes(), 0, length, inetAddress, 12123);
40 // 发送给接收端
41 socket.send(packet);
42 // 当用户在控制台输入信息为‘bye’,循环结束
43 } while (!message.equals("bye"));
44 // 关闭控制台输入,关闭DatagramSocket对象,释放资源
45 reader.close();
46 socket.close();
47 } catch (SocketException e) {
48 e.printStackTrace();
49 } catch (IOException e) {
50 e.printStackTrace();
51 }
52 }
53 }
54
55 /**
56 * 接收端程序
57 */
58 class receiveThread implements Runnable {
59 public void run() {
60 try {
61 // 创建接收端的DatagramSocket,并为其指定端口
62 DatagramSocket socket = new DatagramSocket(12123);
63 // 创建一个长度为1024字节的数组,用来接收数据
64 byte[] arr = new byte[1024];
65 // 创建DatagramPacket对象,用来存放接收到的数据
66 DatagramPacket packet = new DatagramPacket(arr, arr.length);
67 String message = null;
68 do {
69 // 接收数据
70 socket.receive(packet);
71 // 将DatagramPacket内的源数据赋值给message字符串,并输出
72 message = new String(packet.getData(), 0, packet.getLength());
73 System.out.println("对方对您说:" + message);
74 // 当发送端发送的数据为‘bye’时,窗口关闭,运行结束
75 } while (!"bye".equals(message));
76 System.out.println("关闭聊天窗");
77 socket.close();
78 } catch (SocketException e) {
79 e.printStackTrace();
80 } catch (IOException e) {
81 e.printStackTrace();
82 }
83
84 }
85 }
程序运行结果如下:
我是谁?
对方对您说:我是谁?
我来自哪里?
对方对您说:我来自哪里?
bye
对方对您说:bye
关闭聊天窗
在例16-5中,运行结果打印出发送端和接收端的信息,当发送或接收到“bye”时,程序运行结束,发送端和接收端资源将释放。在例16-5中,创建了发送端SenderThread和接收端ReceiveThread两个类,并实现了Runnable接口,并在main方法中分别调用运行,当程序运行时,发送端将用户在控制台输入的数据,发送到本机中端口号为12123的程序也就是接收端,接收端成功接收并打印,程序继续执行,当发送端发送“bye”时,接收端接收并打印后,程序结束。这就是UDP实现聊天程序的基本原理。
16.3 TCP通信
TCP协议是英文Transmission Control Protocol的缩写,即传输控制文协议。TCP协议是一种可靠的网络协议,它在通信实例双方各自创建一个Socket,在通信双方之间形成网络虚拟链路。Java使用Socket对象来代表通信实例双方的通信端口,并通过Socket产生IO流来进行网络通信。
16.3.1 TCP概念
TCP被称作一种端对端协议,是一种可靠的网络协议,分别在通信的两端创建一个Socket,形成可以进行通信的虚拟链路。TCP通信是严格区分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户端,如果客户端没有发送连接请求,接收端将一直处于等待状态。
在JDK中有两个用于实现TCP程序的类,一个是表示服务器端的ServerSocket类,另一个是用于表示客户端的Socket类。在通信时,须先采用“三次握手”方式建立TCP连接,形成数据传输通道,第一次握手,客户端向服务器端发送连接请求,等待服务器进行确认;第二次握手,服务器端向客户端返回确认响应,通知客户端已经收到了连接请求;第三次握手,客户端再次向服务器端发送确认信息,确认连接完成。它保证了两台通信设备之间的无差别传输,在连接中可以进行大量数据的传输,传输完毕后要释放已建立的连接。TCP是一种可靠的网络通信协议,数据传输安全和完整,但是效率比较低。一些对完整性和安全性要求高的数据采用TCP协议传输,比如文件传输和下载,如果文件下载不完全,会导致文件损坏无法打开,TCP的“三次握手”如图16.8所示。
在图16.15中,客户端先向服务端发出连接请求,等待服务器进行确认,服务端收到后向客户端发送一个响应,告诉客户端已经收到了连接请求,最后客户端再次向服务端发送确认信息,确认连接成功。这就是TCP的连接方式。
16.3.2 ServerSocket
在java.net包中有一个ServerSocket类,它可以实现一个服务器端的程序,ServerSocket类的构造方法如表16.10所示。
表16.10 ServerSocket构造方法
构造方法声明 |
功能描述 |
public ServerSocket() |
构造非绑定服务器套接字 |
public ServerSocket(int port) |
构造绑定到特定端口的服务器套接字 |
public ServerSocket(int port, int backlog) |
利用指定的 backlog 构造服务器套接字并将其绑定到指定的本地端口号 |
public ServerSocket(int port, int backlog, InetAddress bindAddr) |
使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址构造套接字 |
表16.10中列出了ServerSocket类的构造方法,通过这些方法可以获得ServerSocket的实例,它还有一些常用方法,如表16.11所示。
表16.11 ServerSocket常用方法
方法声明 |
功能描述 |
Socket accept() |
监听并接受到此套接字的连接。 |
void close() |
关闭此套接字连接。 |
InetAddress getInetAddress() |
返回此服务器套接字的本地地址。 |
boolean isClosed() |
返回 此服务器套接字的关闭状态。 |
void bind(SocketAddress endpoint) |
将此服务器套接字绑定到特定地址(IP 地址和端口号)。 |
表16.9中列出了ServerSocket类的常用方法,其中accept()方法用来接收客户端的请求,当此方法执行后,服务端程序开始等待,直到客户端发送过来请求,程序才能继续执行,如图16.9所示。
图16.8 TCP三次握手 图16.9 TCP服务端和客户端
在图16.9中,ServerSocket代表服务端,Socket代表客户端,服务端在调用accept()方法后就开始绑定某个端口等待客户端的请求,在客户端发出连接请求后,accept()方法会将一个Socket对象返回给服务端,用于和客户端实现通信。
16.3.3 Socket
在java.net包中除了ServerSocket类还有一个Socket类,它是一个数据报套接字,包含了源IP地址和目的IP地址以及源端口号和目的端口号的组合,用于发送和接收UDP数据。Socket类的常用构造方法如表16.12示。
表16.12 Socket常用构造方法
构造方法声明 |
功能描述 |
public Socket() |
使用系统默认类型的SocketImpl,创建一个未连接的套接字 |
public Socket(InetAddress address, int port) |
构造一个流套接字并将其连接到指定 IP 地址的指定端口号 |
public Socket(Proxy proxy) |
构造一个未连接的套接字并指定代理类型(如果有),该代理不管其他设置如何都应被使用 |
public Socket(String host, int port) |
构造一个流套接字并将其连接到指定主机上的指定端口号 |
表16.10中列出了Socket类的常用构造方法,通过这些方法可以获得Socket的实例,它还有一些常用方法,如表16.13所示。
表16.13 Socket常用方法
方法声明 |
功能描述 |
void close() |
关闭此套接字连接 |
InetAddress getInetAddress() |
返回此套接字连接的InetAddress对象 |
InputStream getInputStream() |
返回此套接字的输入流 |
OutputStream getOutputStream() |
返回此套接字的输出流 |
int getPort() |
返回此套接字连接到的远程端口 |
boolean isClosed() |
返回套接字的关闭状态 |
void shutdownOutput() |
关闭此套接字的输出流 |
表16.13中列出了Socket类的常用方法,通过这些方法可以使用TCP协议进行网络通信。
16.3.4 简易TCP网络程序
我们在前面两节讲解了java.net包中ServerSocket类和Socket类的基本用法,接下来我们通过创建一个服务端程序和一个客户端程序来编写一个实战案例,通过这个案例为大家详细讲解TCP通信的运行方法,在运行程序时,必须先运行服务端程序,首先编写服务端程序,如例16-6所示。
例16-6 Demo1606.java
1 package com.aaa.p1606;
60 import java.io.InputStream;
61 import java.io.OutputStream;
62 import java.net.ServerSocket;
63 import java.net.Socket;
64
65 public class Demo1606 {
66 public static void main(String[] args) throws Exception {
67 // 创建服务端ServerSocket对象,并指定IP为8081
68 ServerSocket serverSocket = new ServerSocket(8081);
69 System.out.println("等待接收数据");
70 // 等待接收服务端的请求
71 Socket socket = serverSocket.accept();
72 // 获取输入流,并通过数组接收
73 InputStream inputStream = socket.getInputStream();
74 byte[] arr = new byte[1024];
75 int len;
76 while ((len=inputStream.read(arr))!=-1){
77 String str = new String(arr,0,len);
78 System.out.println(str);
79 }
80 // 获取输出流,并向客户端做出响应
81 OutputStream outputStream = socket.getOutputStream();
82 outputStream.write("师傅:为师知道了,退下吧".getBytes());
83 // 释放资源
84 outputStream.close();
85 socket.close();
86 serverSocket.close();
87 }
88 }
程序运行结果如下:
等待接收数据
在创建了服务端唐僧以后,他还在孤零零的等着客户端徒弟们给他请安,这时候我们就需要去创建客户端,让徒弟们过来请安了,如例16-7所示。
例16-7 Demo1607.java
1 package com.aaa.p1607;
89 import java.io.InputStream;
90 import java.io.OutputStream;
91 import java.net.InetAddress;
92 import java.net.Socket;
93
94 public class Demo1607 {
95 public static void main(String[] args) throws Exception{
96 // 准备去给服务端师傅请安
97 System.out.println("准备去叫师傅起床...");
98 // 创建一个Socket对象,并指定接收端的IP为本机IP,端口号为8081
99 Socket socket = new Socket(InetAddress.getByName("localhost"),8081);
100 // 获取输出流,并将数据通过输出流传送出去,输出字符串要转为字节
101 OutputStream outputStream = socket.getOutputStream();
102 outputStream.write("悟空说:师傅,老孙给您请安来了".getBytes());
103 // 关闭输出流
104 socket.shutdownOutput();
105 // 获取输入流,并定义一个大小为1024字节的数组用来接收数据
106 InputStream inputStream = socket.getInputStream();
107 byte[] arr = new byte[1024];
108
109 int len ;
110 // 当输入流读取的数据不为空的话,往下执行,输出服务端响应的消息
111 while ((len=inputStream.read(arr)) != -1){
112 String str = new String(arr,0,len);
113 System.out.println(str);
114 }
115 // 释放资源
116 inputStream.close();
117 outputStream.close();
118 socket.close();
119 }
120 }
客户端程序的运行结果如下:
准备叫师傅起床...
唐僧:为师知道了,悟空退下吧
在接收到客户端发送的数据时,服务端的运行结果如下:
等待接收数据
悟空说:师傅,老孙给您请安来了
注意:在运行的时候一定要先运行服务端,再运行客户端,不然在师傅没睡醒的时候来打扰,肯定要吃闭门羹啊。如果不小心先启动了客户端,那么吃了闭门羹的孙悟空就会报ConnectException异常,提示“Connection refused”,中文含义为“拒绝连接”,出现错误的原因在于服务器端程序未开启或者连接的端口错误。
16.3.5 多线程的TCP网络程序
我们在上一节中学习了最简单的服务器和客户端进行通信、交互,但是我们知道,在日常生活中,我们的服务器基本不会只为一台客户端服务,每台服务器都有很多客户端进行访问,这就用到了我们之前学习的多线程知识,接下来就带领大家来学习下怎么通过多线程来实现多个客户端与一个服务器进行交互,首先要先创建一个专门用于处理多线程操作的类,如例16-8所示。
例16-8 Demo1608.java
1 package com.aaa.p1608;
121 import java.io.BufferedReader;
122 import java.io.IOException;
123 import java.io.InputStreamReader;
124 import java.io.PrintStream;
125 import java.net.Socket;
126
127 public class Demo1608 implements Runnable{
128 // 创建Socket对象准备接收客户端
129 private Socket socket = null;
130 // 通过构造方法设置socket
131 public Demo1608(Socket socket) {
132 this.socket = socket;
133 }
134
135 public void run() {
136 BufferedReader bufferedReader = null;
137 PrintStream printStream = null;
138 try {
139 // 获取客户端信息
140 bufferedReader = new BufferedReader(new InputStreamReader(socket.
141 getInputStream()));
142 // 定义输出流
143 printStream = new PrintStream(socket.getOutputStream());
144 boolean flag = true;
145
146 while (flag){
147 String str = bufferedReader.readLine();
148 if (str == null ||str.equals("")){
149 flag = false;
150 System.out.println("悟空,我听不见你说什么");
151 }else{
152 System.out.println(str);
153 if ("aa".equals(str)){
154 flag = false;
155 System.out.println("本次通信结束");
156 }else {
157 flag = false;
158 printStream.println("悟空,不许打人");
159 }
160 }
161 };
162 } catch (IOException e) {
163 e.printStackTrace();
164 }finally {
165 if (printStream != null){
166 printStream.close();
167 }
168 if (socket != null){
169 try {
170 socket.close();
171 } catch (IOException e) {
172 e.printStackTrace();
173 }
174 }
175 }
176 }
177 }
在例16-8中,Demo1608类实现了Runnable接口,使用构造方法接收每一个客户端的Socket,并且通过重写run()方法,使用循环的方式接收客户端信息,并向客户端输出响应信息,最后释放资源,有了多线程类,接下来就让我们来创建一个服务端,如例16-9所示。
例16-9 Demo1609.java
1 package com.aaa.p1609;
2 import java.io.IOException;
3 import java.io.InputStream;
4 import java.io.OutputStream;
5 import java.net.ServerSocket;
6 import java.net.Socket;
7
8 public class Demo1609 {
9 public static void main(String[] args) throws IOException {
10 ServerSocket serverSocket = null;
11 Socket socket = null;
12 // 创建ServerSocket对象,并指定端口号为8081
13 serverSocket = new ServerSocket(8081);
14 boolean flag = true;
15 while (flag){
16 System.out.println("等待接收数据");
17 socket = serverSocket.accept();
18 // 通过构造方法运行线程
19 new Thread(new Demo1608(socket)).start();
20 };
21 serverSocket.close();
22 InputStream inputStream = socket.getInputStream();
23 byte[] arr = new byte[1024];
24 int len;
25 while ((len = inputStream.read(arr)) != -1){
26 String str = new String(arr,0,len);
27
28 System.out.println(str);
29 }
30 OutputStream outputStream = socket.getOutputStream();
31 outputStream.write("为师知道了".getBytes());
32 outputStream.close();
33 inputStream.close();
34 socket.close();
35 serverSocket.close();
36 }
37 }
在例16-9中,服务端类在接收到客户端信息后,调用多线程类来处理客户端传送过来的数据,并做出不同的相应,接下来我们对之前的发送端进行稍微的修改,为其加上用户输入功能,如例16-10所示。
例16-10 Demo1610.java
1 package com.aaa.p1610;
2 import java.io.BufferedReader;
3 import java.io.InputStream;
4 import java.io.InputStreamReader;
5 import java.io.OutputStream;
6 import java.net.InetAddress;
7 import java.net.Socket;
8
9 public class Demo1610 {
10 public static void main(String[] args) throws Exception{
11 // 客户端悟空要打白骨精了
12 System.out.println("我要开始打妖精了");
13 // 创建一个Socket对象,并指定接收端的IP为本机IP,端口号为8081
14 Socket socket = new Socket(InetAddress.getByName("localhost"),8081);
15 // 获取输出流,并将数据通过输出流传送出去,输出字符串要转为字节
16 OutputStream outputStream = socket.getOutputStream();
17 // 从控制台获取用户输入数据
18 BufferedReader bufferedReader =
19 new BufferedReader(new InputStreamReader(System.in));
20 String line = bufferedReader.readLine();
21 outputStream.write(line.getBytes());
22 // 关闭输出流
23 socket.shutdownOutput();
24 // 获取输入流,并定义一个大小为1024字节的数组用来接收数据
25 InputStream inputStream = socket.getInputStream();
26 byte[] arr = new byte[1024];
27 int len ;
28 // 当输入流读取的数据不为空的话,往下执行,输出服务端响应的消息
29 while ((len = inputStream.read(arr)) != -1){
30 String str = new String(arr,0,len);
31 System.out.println(str);
32 }
33 // 释放资源
34 inputStream.close();
35 outputStream.close();
36 socket.close();
37 }
38 }
先运行服务端程序例16-9,之后运行三次例16-10的客户端程序,程序16-9的运行结果如下:
等待接收数据
悟空打白骨精
等待接收数据
悟空打蝎子精
等待接收数据
悟空打兔子精
程序16-10的第一次运行结果如下:
悟空要开始打妖精了
悟空打白骨精
悟空,不许打人
程序16-10的第二次运行结果如下:
悟空要开始打妖精了
悟空打蝎子精
悟空,不许打人
程序16-10的第三次运行结果如下:
悟空要开始打妖精了
悟空打兔子精
悟空,不许打人
16.3.6 TCP案例——模拟百度网盘文件快传
通过前面的学习,相信大家已经基本掌握了客户端和服务端通过TCP协议进行通信的方式。接下来我们通过使用TCP模拟百度网盘的上传功能来进一步学习和练习,以便加深和巩固TCP的相关知识。
我们首先创建一个img文件夹,在里面放入我们要上传的图片logo.jpg,通过程序上传后保存到对应的路径"D:\\receive"下面,并将上传的文件重命名为"logo1.jpg",实现步骤如下:
• 服务器端:
• 使用ServerSocket 创建一个服务器端对象,并开始监听8081端口。
• 编写receiveFile方法,传入socket对象。
• 在receiveFile方法内部使用JavaIO将客户端上传的文件保存到指定位置。
• 客户端:
• 使用Socket创建一个连接服务器端的客户端对象,并指定端口号为8081。
• 编写sendFile文件上传方法,传入socket对象和待上传的文件路径。
• 在sendFile方法中读取文件内容并通过socket连接上传到服务器端。
接下来,我们通过例16-11和例16-12来分别编写服务器端代码和客户端代码。服务器端代码如例16-11所示。
例16-11 Demo1611.java
1 package com.aaa.p1611;
2 import java.io.*;
3 import java.net.ServerSocket;
4 import java.net.Socket;
5
6 public class Demo1611 {
7 public static void main(String[] args) throws Exception {
8 ServerSocket serverSocket = new ServerSocket(8081);
9 System.out.println("有请客户上传文件...");
10 Socket socket = serverSocket.accept();
11 System.out.println("正在接收" + socket.getInetAddress().getHostAddress()+
12 "为我们上传的文件");
13 // 获取连接成功,调用receiveFile方法,开始接收文件
14 receiveFile(socket);
15 serverSocket.close();
16 }
17
18 private static void receiveFile(Socket socket) throws Exception {
19 byte[] bytes = new byte[1024];
20 // 创建DataInputStream对象,并调用他的readUTF()方法读取客户端发送的文件名
21 DataInputStream dataInputStream = new DataInputStream(socket.
22 getInputStream());
23 String oldFileName = dataInputStream.readUTF();
24 // 定义另存文件的路径,并调用genereateFileName()方法重命名
25 String filePath = "D:/imgReceive/"+genereateFileName(oldFileName);
26 // 创建FileOutputStream对象对文件进行输出流操作
27 FileOutputStream fileOutputStream = new FileOutputStream(new File(filePath));
28 int length = 0;
29 while ((length = dataInputStream.read(bytes,0,bytes.length)) > 0){
30 fileOutputStream.write(bytes,0,length);
31 fileOutputStream.flush();
32 }
33 System.out.println("客户的文件已被另存为" + filePath);
34 dataInputStream.close();
35 fileOutputStream.close();
36 socket.close();
37 }
38
39 private static String genereateFileName(String oldFileName) {
40 String newName=null;
41 // 通过截取字符串更改文件名
42 newName = oldFileName.substring(0,oldFileName.lastIndexOf(".")) + "1" +
43 oldFileName.substring(oldFileName.lastIndexOf("."));
44 return newName;
45 }
46 }
程序的运行结果如下:
有请客户上传文件
我们在例16-9中,首先创建了ServerSocket对象并指定端口号,然后调用accept()方法进入等待状态,一直到客户端进行连接,连接成功后,调用receiveFile()方法完成文件的上传操作。
有了服务端的代码,客户端的程序当然也是必不可少的,接下来我们就来编写客户端程序,如例16-12所示。
例16-12 Demo1612.java
1 package com.aaa.p1612;
2 import java.io.*;
3 import java.net.InetAddress;
4 import java.net.Socket;
5
6 public class Demo1612 {
7 // 设置全局变量
8 public static final String fileDir = "E:/img/";
9 public static void main(String[] args) throws Exception {
10 String fileName = "logo.png";
11 // 定义要上传的文件
12 String filePath = fileDir + fileName;
13 System.out.println("正在上传文件:" + filePath);
14 // 指定服务端的端口号
15 Socket socket = new Socket(InetAddress.getLocalHost(),8081);
16 if (socket != null){
17 sendFile(socket,filePath);
18 System.out.println("文件上传成功");
19 }
20 }
21
22 private static void sendFile(Socket socket, String filePath) throws Exception {
23 byte[] bytes = new byte[1024];
24 BufferedInputStream bufferedInputStream = new BufferedInputStream(new
25 FileInputStream(new File(filePath)));
26 DataOutputStream dataOutputStream = new DataOutputStream(new
27 BufferedOutputStream(socket.getOutputStream()));
28 // 通过writeUTF方法先将文件上传
29 dataOutputStream.writeUTF(getFileName(filePath));
30 int length = 0;
31 // 循环将文件输出上传
32 while ((length = bufferedInputStream.read(bytes,0,bytes.length)) > 0){
33 dataOutputStream.write(bytes,0,length);
34 dataOutputStream.flush();
35 }
36 bufferedInputStream.close();
37 dataOutputStream.close();
38 socket.close();
39 }
40 /**
41 * 获取需要上传的文件名
42 */
43 private static String getFileName(String filePath) {
44 String[] file = filePath.split("/");
45 return file[file.length-1];
46 }
47 }
先启动例16-11的服务端程序准备,然后开启16-12的客户端程序,程序的运行结果如下:
有请客户上传文件...
正在接收192.168.43.9为我们上传的文件
客户的文件已经被另存为E:/imgReceive/logo1.png
例16-11程序运行结果如下:
正在上传文件:E:/img/logo.png
文件长传成功
在16-12的客户端程序的运行结果中,我们客户看见服务端接收到192.168.43.9的文件后,将其保存为“E:/imgReceive”目录下,并改名为logo1.png。在例16-11中的运行结果展示客户端的发送动作和文件上传成功的提示。分别查看上传前后对应的目录,如图16.10和图16.11所示。
图16.10 img下需要上传的文件 图16.11 imgReceive下上传以后的文件
通过这两张图,我们可以看到我们使用TCP模拟上传文件的功能已经完成了,在图16.10中展示的是我们需要上传到网盘中的文件,图16.11展示的是我们上传以后服务器另存的文件。
16.4 代理服务器网络编程
代理服务器(Proxy Server)的功能是代理网络用户去取得网络信息。形象地说,它是网络信息的中转站,是个人网络和Internet服务商之间的中间代理机构,负责转发合法的网络信息,对转发进行控制和登记。随着Internet与Intranet的飞速发展,作为连接Internet与Intranet的的桥梁,代理服务器在实际应用中发挥着极其重要的作用。Java从JDK1.5开始对代理服务器编程提供支持。
16.4.1 Proxy创建网络连接
Java在java.net包下提供了 Proxy和ProxySelector两个类,其中Proxy代表一个代理服务器,可以在打开URLConnection连接时指定Proxy,创建Socket连接时也可以指定Proxy;而 ProxySelector代表一个代理选择器,它提供了对代理服务器更加灵活的控制,它可以对HTTP、HTTPS、 FTP、SOCKS等进行分别设置,而且还可以设置不需要通过代理服务器的主机和地址。通过使用 ProxySelector,可以实现像在Internet Explorer、Firefox等软件中设置代理服务器类似的效果。浏览器中代理服务器的设置页面如图16.12所示。
Proxy有一个构造器:Proxy (Proxy. Type type, SocketAddress socketAddress ),用于创建表示代理服务器的Proxy 对象。其中socketAddress 参数指定代理服务器的地址,type表示该代理服务器的类型,该服务器类型有如下3种:
• Proxy.Type.DIRECT:表示直接连接,不使用代理。
• Proxy.Type.HTTP:表示支持高级协议代理,如HTTP或FTP。
• Proxy.Type.SOCKS:表示 SOCKS (V4 或 V5)代理。
一旦创建了 Proxy对象之后,程序就可以在使用URLConnection打开连接时,或者创建Socket连接时传入一个Proxy对象,作为本次连接所使用的代理服务器。其中,URL包含了一个URLConnection openConnection(Proxy proxy)方法,该方法使用指定的代理服务器来打开连接;而Socket则提供了一个Socket(Proxy proxy)构造器,该构造器使用指定的代理服务器创建一个没有连接的Socket对象。
下面以URLConnection为例来介绍如何在URLConnection中使用代理服务器。具体程序如例16-13所示。
例16-13 Demo1613.java
1 package com.aaa.p1613;
2 import java.io.IOException;
3 import java.net.*;
4 import java.util.Scanner;
5
6 public class Demo1613 {
7 public static void main(String[] args)
8 throws IOException {
9 // 代理服务器的ip地址和端口
10 String ip = "118.190.158.17";
11 int port = 8080;
12 // 定义需要访问的网站地址
13 String urlStr = "http://aaajy.net";
14 URL url = new URL(urlStr);
15 // 创建一个代理服务器对象
16 Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(ip, port));
17 // 使用指定的代理服务器打开连接
18 URLConnection conn = url.openConnection(proxy);
19 // 设置超时时长
20 conn.setConnectTimeout(3000);
21 // 通过代理服务器读取数据的Scanner
22 Scanner scan = new Scanner(conn.getInputStream());
23 System.out.println("============返回服务器响应的内容=============");
24 while (scan.hasNextLine()) {
25 String line = scan.nextLine();
26 // 在控制台输出网页资源内容
27 System.out.println(line);
28 }
29 }
30 }
程序运行结果如下:
============返回服务器响应的内容=============
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.5.65</title>
<link href="favicon.ico" rel="icon" type="image/x-icon" />
<link href="tomcat.css" rel="stylesheet" type="text/css" />
</head>
上面程序中第16行代码创建了一个Proxy对象,第19行代码就是用Proxy对象来打开 URLConnection连接。接下来程序使用URLConnection读取了一份网络资源,此时的URLConnection 并不是直接连接到http://aaajy.net,而是通过代理服务器去访问该网站。
注意:上面程序中的118.190.158.17服务器是AAA软件教育学院的在线测试服务器,8080端口指向服务器上的Tomcat应用服务,所以最终返回的响应为Tomcat应用的响应。如果想实现代理服务器端口映射,需要在代理服务器配置Nginx或者Iptables。
16.4.1 ProxySelector自动选择代理服务器
前面介绍的直接使用Proxy对象可以在打开URLConnection或Socket时指定代理服务器,但使用这种方式每次打开连接时都需要显式地设置代理服务器,比较麻烦。如果希望每次打开连接时总是具有默认的代理服务器,则可以借助于ProxySelector来实现。
ProxySelector代表一个代理选择器,它本身是一个抽象类,程序无法创建它的实例,开发者可以考虑继承ProxySelector来实现自己的代理选择器。实现ProxySelector的步骤非常简单,程序只要定义一 个继承ProxySelector的类,并让该类实现如下两个抽象方法:
• List<Proxy> select(URI uri):根据业务需要返回代理服务器列表,如果该方法返回的集合中只包含一个Proxy,该Proxy将会作为默认的代理服务器。
• connectFailed(URI uri, SocketAddress sa, IOException ioe):连接代理服务器失败时回调该方法。
实现了自己的 ProxySelector 类之后,调用 ProxySelector 的 setDefault(ProxySelector ps)静态方法来注册该代理选择器即可。
下面程序例16-14示范了如何让自定义的ProxySelector来自动选择代理服务器。
例16-14 Demo1614.java
1 package com.aaa.p1614;
2 import java.io.IOException;
3 import java.net.*;
4 import java.util.ArrayList;
5 import java.util.List;
6 import java.util.Scanner;
7
8 public class Demo1614 {
9 public static void main(String[] args) throws IOException {
10 // 设置默认代理选择器
11 ProxySelector.setDefault(new ProxySelector() {
12 @Override
13 public List<Proxy> select(URI uri) {
14 List<Proxy> proxyList = new ArrayList<Proxy>();
15 // 代理服务器的IP地址和端口
16 String ip1 = "118.190.158.17";
17 int port1 = 8080;
18 // 创建一个代理服务器对象
19 Proxy proxy1 = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(ip1, port1));
20 //代理服务器的ip地址和端口
21 String ip2 = "118.190.158.17";
22 int port2 = 9090;
23 // 创建一个代理服务器对象
24 Proxy proxy2 = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(ip2, port2));
25 proxyList.add(proxy2);
26 proxyList.add(proxy1);
27 return proxyList;
28 }
29
30 @Override
31 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
32 System.out.println("连接代理服务器失败!");
33 }
34 });
35
36 // 定义需要访问的网站地址
37 String urlStr = "http://aaajy.net";
38 URL url = new URL(urlStr);
39 // 不使用指定的代理服务器打开连接
40 URLConnection conn = url.openConnection();
41 // 设置超时时长
42 conn.setConnectTimeout(3000);
43 // 通过代理服务器读取数据的Scanner
44 Scanner scan = new Scanner(conn.getInputStream());
45 System.out.println("============返回服务器响应的内容=============");
46 while (scan.hasNextLine()) {
47 String line = scan.nextLine();
48 // 在控制台输出网页资源内容
49 System.out.println(line);
50 }
51 }
52 }
程序运行结果如下:
连接代理服务器失败!
============返回服务器响应的内容=============
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/8.5.65</title>
<link href="favicon.ico" rel="icon" type="image/x-icon" />
<link href="tomcat.css" rel="stylesheet" type="text/css" />
</head>
从上面程序的运行结果可以发现,虽然例16-14没有使用代理服务器打开链接,但是代理服务器仍然起作用了,也就是说,程序默认总会使用我们注册的代理服务器。
误区警告:代理服务器列表中保存的某个服务器无法连接的时候会出现“连接代理服务器失败!”错误。
16.5 HTTPClient
HttpClient是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。Java9开始引入的一个处理 HTTP 请求的的 HTTP Client API,该 API 支持同步和异步,而在 Java 11中已经成为正式可用状态,你可以在 java.net 包中找到这个 API。
16.5.1 HTTPClient概念
有人说,HttpClient不就是一个浏览器嘛可能不少人对HttpClient会产生这种误解,他们的观点是这样的:既然HttpClient是一个HTTP客户端编程工具,那不就相当于是一个浏览器了吗?无非它不能把HTML渲染出页面而已罢了。
其实HttpClient不是浏览器,它是一个HTTP通信库、一个工具包,因此它只提供一个通用浏览器应用程序所期望的功能子集。HttpClient与浏览器最根本的区别是:HttpClient中没有用户界面,浏览器需要一个渲染引擎来显示页面,并解释用户输入(例如鼠标点击显示页面上的某处之后如何响应、计算如何显示HTML页面、级联样式表和图像、javascript解释器运行嵌入HTML页面或从HTML页面引用的javascript代码、来自用户界面的事件被传递到javascript解释器进行处理等等等等)。HttpClient只能以编程的方式通过其API用于传输和接收HTTP消息,它对内容也是完全不可知的。
HTTP Client API的主要类包括:
• java.net.http.HttpClient:HTTPClient的核心对象,用于发送请求和接收响应。Java为创建该类提供了java.net.http.HttpClient.Builder接口。
• java.net.http.HttpRequest:代表请求对象。Java为了创建该类提供HttpRequest.Builder接口。
• java.net.http.HttpResponse:代表响应对象。
• java.net.http.WebSocket:代表web应用之间的Socket连接对象。
HTTP Client API特性:
• 对大多数场景提供简单易用的阻塞模型。
• 通过异步机制支持事件通知。完整支持HTTP协议的特性。
• 易于建立WebSocket握手。
• 支持HTTP/2,包括协议升级(Upgrade)和服务端推送(server push)。
• 支持 HTTPS/TLS。
• 和现有的其他实现类库相比,性能相当或有提升,内存占用少。
16.5.2 如何发送同步GET请求
使用HTTPClient发送请求的步骤如下:
• 创建HttpClient对象。
• 创建HttpRequest对象,如果有参数,使用HttpRequest.BodyPublishers设置请求参数。
• 调用HttpClient.send()方法或者HttpClient.sendAsync()方法发送请求,sendAsync方法用于发送异步请求。
下面的案例展示了如何发送GET请求,具体程序代码如例16-15所示:
例16-15 Demo1615.java
1 package com.aaa.p1615;
2 import java.io.IOException;
3 import java.net.URI;
4 import java.net.http.HttpClient;
5 import java.net.http.HttpRequest;
6 import java.net.http.HttpResponse;
7
8 public class Demo1615 {
9 public static void main(String[] args) throws IOException, InterruptedException {
10 // 创建HttpClient对象
11 HttpClient httpClient = HttpClient.newBuilder().build();
12 // 创建HttpRequest对象
13 HttpRequest request = HttpRequest.newBuilder()
14 // 设置请求的url地址和参数
15 .uri(URI.create("http://118.190.158.17:10520/student/findAllStudent?
16 page=1&limit=10"))
17 // 设置请求方式为GET请求
18 .GET().build();
19 // 将服务器响应转换成字符串
20 HttpResponse.BodyHandler<String> bh = HttpResponse.BodyHandlers.ofString();
21 // 发送请求,获取服务器响应
22 HttpResponse<String> response = httpClient.send(request, bh);
23 System.out.println("响应的状态码" + response.statusCode());
24 System.out.println("响应头" + response.headers());
25 System.out.println("响应体" + response.body());
26 }
27 }
程序运行结果如下:
响应的状态码200
响应头java.net.http.HttpHeaders@c84bfd11 { {content-type=[application/json], date = [Wed, 19 May 2021 02:19:48 GMT], transfer-encoding=[chunked]} }
响应体{"code":0,"msg":null,"count":1,"data":[{"id":11,"name":"陈建","age":37,"phone": "18538062907","school":"AAA软件学院","createTime":"2021-05-18 09:58:37"}]}
16.5.3 如何发送带请求体的请求
例16-15演示了如何发送GET请求,此外HTTP Client还可以发送POST,DELETE等方式的请求。下面我们来演示如何发送带请求体参数的POST请求。
知识点拨:HTTP请求类型共分8种:
OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以用于向Web服务器发送'*'的请求来测试服务器的功能性。
HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
GET:向特定的资源发出请求。
POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
PUT:向指定资源位置上传其最新内容。
DELETE:请求服务器删除Request-URI所标识的资源。
TRACE:回显服务器收到的请求,主要用于测试或诊断。
CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
下面的案例展示了如何发送带请求体参数的POST请求,具体程序代码如例16-16所示。
例16-16 Demo1616.java
1 package com.aaa.p1616;
2 import java.io.IOException;
3 import java.net.CookieHandler;
4 import java.net.CookieManager;
5 import java.net.URI;
6 import java.net.http.HttpClient;
7 import java.net.http.HttpRequest;
8 import java.net.http.HttpResponse;
9 import java.time.Duration;
10
11 public class Demo1616 {
12 public static void main(String[] args) throws IOException, InterruptedException {
13 // 创建HttpClient对象
14 HttpClient httpClient = HttpClient.newBuilder().build();
15 // 创建HttpRequest对象
16 HttpRequest request = HttpRequest.newBuilder()
17 // 设置请求的url地址和参数
18 .uri(URI.create("http://118.190.158.17:10520/student/findAllStudent"))
19 // 设置以提交表单的方式编码请求体
20 .headers("Content-Type","application/x-www-form-urlencoded")
21 // 设置请求方式为POST请求,并设置请求参数
22 .POST(HttpRequest.BodyPublishers.ofString("page=1&limit=10")).build();
23 // 将服务器响应转换成字符串
24 HttpResponse.BodyHandler<String> bh = HttpResponse.BodyHandlers.ofString();
25 // 发送请求,获取服务器响应
26 HttpResponse<String> response = httpClient.send(request, bh);
27 System.out.println("响应的状态码" + response.statusCode());
28 System.out.println("响应头" + response.headers());
29 System.out.println("响应体" + response.body());
30 System.out.println("程序运行结束");
31 }
32 }
程序运行结果如下:
响应的状态码200
响应头java.net.http.HttpHeaders@d4d4ce68 { {content-type=[application/json], date= [Wed, 19 May 2021 02:51:10 GMT], transfer-encoding=[chunked]} }
响应体{"code":0,"msg":null,"count":2,"data":[{"id":11,"name":"陈建","age":37,"phone": "18538062907","school":"AAA软件学院","createTime":"2021-05-18 09:58:37"},{"id":16,"name":"张三", "age":20,"phone":"13838383838","school":"AAA软件教育","createTime":"2021-05-19 10:35:57"}]}
程序运行结束
从上面的运行结果可以发现例16-16和例16-15的响应体结果相同,原因是两个例子发送的请求地址相同,请求参数相同,两个例子不同点在于请求的方式不同,例16-15的请求方式是GET请求,请求参数存放在请求头中,例16-16发送的是POST请求,请求参数存放在请求体中。具体使用哪种请求方式,根据服务器接口定义规则为准。
16.5.4 如何发送异步请求
在现实网络中,网络环境千差万别,只要发送网络请求,不可避免的就会存在网络延迟,并且服务器处理请求也需要等待时间,那么通过send()方法发送同步请求,在服务器返回响应之前,该方法会一直处于阻塞状态,效率比较低。为了提升程序效率,可以使用sendAsync()方法发送异步请求,异步请求不存在阻塞,异步请求发送之后,sendAsync方法会返回一个CompletableFuture对象,它代表一个将要完成的任务(但是具体何时完成,尚不确定),因此程序要为CompletableFuture设置消费监听器,当CompletableFuture代表的任务结束时监听器被触发执行。
下面的案例展示了如何发送异步POST请求,具体程序代码如例16-17所示:
例16-17 Demo1617.java
1 package com.aaa.p1617;
2 import java.io.IOException;
3 import java.net.URI;
4 import java.net.http.HttpClient;
5 import java.net.http.HttpRequest;
6 import java.net.http.HttpResponse;
7
8 public class Demo1617 {
9 public static void main(String[] args) throws IOException, InterruptedException {
10 // 创建HttpClient对象
11 HttpClient httpClient = HttpClient.newBuilder().build();
12 // 创建HttpRequest对象
13 HttpRequest request = HttpRequest.newBuilder()
14 // 设置请求的url地址和参数
15 .uri(URI.create("http://118.190.158.17:10520/student/findAllStudent"))
16 // 设置以提交表单的方式编码请求体
17 .headers("Content-Type", "application/x-www-form-urlencoded")
18 // 设置请求方式为POST请求,并设置请求参数
19 .POST(HttpRequest.BodyPublishers.ofString("page=1&limit=10")).build();
20 // 将服务器响应转换成字符串
21 HttpResponse.BodyHandler<String> bh = HttpResponse.BodyHandlers.ofString();
22 // 发送异步请求,获取服务器响应
23 httpClient.sendAsync(request, bh).thenApply(resp -> new Object[]{resp.statusCode(),
24 resp.body()}).thenAccept(rt -> {
25 System.out.println("响应的状态码" + rt[0]);
26 System.out.println("响应体" + rt[1]);
27 });
28 System.out.println("程序运行结束");
29 // 程序休眠3秒
30 Thread.sleep(3000);
31 }
32 }
程序运行结果如下:
程序运行结束
响应的状态码200
响应体{"code":0,"msg":null,"count":2,"data":[{"id":11,"name":"陈建","age":37,"phone": "18538062907","school":"AAA软件学院","createTime":"2021-05-18 09:58:37"},{"id":16,"name": "张三","age":20,"phone":"13838383838","school":"AAA软件教育","createTime":"2021-05-19 10:35:57"}]}
从上面的运行结果可以发现例16-17和例16-16的关键字“程序运行结束”的位置发生变化,例16-16中的同步请求是先打印响应信息然后打印程序结束,而例16-17发送的是异步请求,响应未返回,程序不会阻塞,继续执行并打印程序结束信息。
注意:在例16-17程序中加入了Thread.sleep(3000);目的是让程序打印关键字“程序结束”之后,程序不会真正的退出,休眠3秒,等待返回响应,触发监听器,打印响应信息,同理浏览器发送完异步请求之后,也不会立即关闭浏览器而是等待返回响应。
16.5.5 WebSocket接口
WebSocket类似普通的Socket,只不过它是Web应用之间的Socket连接,通常以异步方式进行通信的。通过Java来实现WebSocket客户端非常简单,只需要下面两个步骤即可:
• 定义一个WebSocketListener监听器对象,根据需要重写该监听器中的指定方法。
• 使用WebSocketBuilder构建WebSocket客户端。
下面的案例展示了如何构建WebSocket客户端,具体程序代码如例16-18所示:
例16-18 Demo1618.java
1 package com.aaa.p1618;
2 import java.net.URI;
3 import java.net.http.HttpClient;
4 import java.net.http.WebSocket;
5 import java.util.concurrent.CompletionStage;
6
7 public class Demo1618 {
8 public static void main(String[] args) throws InterruptedException {
9 // 定义一个WebSocketListener监听器对象,根据需要重写该监听器中的指定方法
10 WebSocket.Listener listener= new WebSocket.Listener() {
11 // 跟服务器端打开连接时触发该方法
12 @Override
13 public void onOpen(WebSocket webSocket) {
14 System.out.println("打开连接");
15 webSocket.sendText("我是AAA软件的陈老师",true);
16 webSocket.request(1);
17 }
18 // 接收到服务器端返回的消息时触发该方法
19 @Override
20 public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
21 System.out.println(data);
22 webSocket.request(1);
23 return null;
24 }
25 };
26 // 传入监听器作为参数,创建webSocket客户端
27 HttpClient httpClient = HttpClient.newHttpClient();
28 // 跟服务器建立异步通信
29 httpClient.newWebSocketBuilder()
30 .buildAsync(URI.create("ws://127.0.0.1:10520/mySocket"),listener);
31 Thread.sleep(5000);
32 }
33 }
上面的程序创建了一个WebSocket客户端,虽然有了客户端,但是还缺少服务器端,下面的代码可以创建一个WebSocket服务器端,具体技术使用到了SpringBoot框架,在此不做赘述,如果读者想深入了解学习服务器端的Java开发过程,请参考《深入浅出SpringBoot》。
下面的案例展示了如何构建WebSocket服务器端,具体程序代码如例16-19所示:
例16-19 Demo1619.java
1 package com.aaa.p1619;
2 import org.springframework.stereotype.Component;
3 import javax.websocket.*;
4 import javax.websocket.server.ServerEndpoint;
5 import java.io.IOException;
6 // 注解ServerEndpoint标识该类作为WebSocket服务器端
7 @ServerEndpoint(value = "/mySocket")
8 @Component
9
10 public class Demo1619 {
11 // 注解OnOpen修饰的方法将会在客户端连接时触发
12 @OnOpen
13 public void start(Session session){
14 System.out.println("客户端连接进来了。sessionId:" + session.getId());
15 }
16 // 注解OnMessage修饰的方法将会在客户端消息到达时触发
17 @OnMessage
18 public void message(Session session,String message){
19 System.out.println("接收到消息:" + message);
20 RemoteEndpoint.Basic remote = session.getBasicRemote();
21 try {
22 remote.sendText("收到信息,欢迎来到AAA软件学院");
23 } catch (IOException e) {
24 e.printStackTrace();
25 }
26 }
27 // 注解OnClose修饰的方法将会在客户端关闭时触发
28 @OnClose
29 public void end(Session session){
30 System.out.println("客户端关闭了。sessionId:" + session.getId());
31 }
32 // 注解OnError修饰的方法将会在客户端连接出错时触发
33 @OnError
34 public void error(Session session,Throwable throwable){
35 System.out.println("客户端出错了。sessionId:" + session.getId());
36 }
37 }
服务器端程序需要首先运行,然后再运行客户端程序。运行结果分客户端和服务器端两部分,客户端信息:
打开连接
收到信息,欢迎来到AAA软件学院
服务器端信息:
客户端连接进来了。sessionId:0
接收到消息:我是AAA软件的陈老师
客户端出错了。sessionId:0
客户端关闭了。sessionId:0
16.6 本章小结
• TCP/IP协议模型将网络分为4层,分别为物理+数据链路层、网络层、传输层和应用层,每层分别负责不同的通信功能。
• 端口号是一个16位的二进制整数,取值范围0~65535,其中0~1023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,避免端口号冲突。
• URL统一资源定位系统(uniform resource locator)是因特网 的万维网服务程序上用来指定信息位置的表示方法,在通常情况下,URL可以由协议名、主机、端口和资源组成。
• 在JDK中有两个用于实现TCP程序的类,一个是表示服务器端的ServerSocket类,另一个是用于表示客户端的Socket类。
• Java在java.net包下提供了 Proxy和ProxySelector两个类,其中Proxy代表一个代理服务器,可以在打开URLConnection连接时指定Proxy,创建Socket连接时也可以指定Proxy。
• 使用HTTPClient发送请求的步骤如下:
• 创建HttpClient对象。
• 创建HttpRequest对象,如果有参数,使用HttpRequest.BodyPublishers设置请求参数。
• 调用HttpClient.send()方法或者HttpClient.sendAsync()方法发送请求,sendAsync方法用于发送异步请求。
16.7 理论习题与实践练习
1.填空题
1.1 在JDK中有两个用于实现TCP程序的类,一个是表示服务器端的 类,另一个是用于表示客户端的 类。
1.2 Java提供了InetAddress类来代表IP地址,它有2个子类,分别为 类和 类。
1.3 HTTP协议的请求类型有 、 、 、 、 、 、 、 。
2.选择题
2.1 下面哪一项不属于OSI的7层模型( )
A.互联层 B.应用层
C.传输层 D.会话层
2.2 以下哪些协议位于传输层( )
A.TCP B.HTTP
C.SMTP D.IP
2.3 下面合法的端口是( )
A.9000000 B.-200
C.A50 D.3306
3.思考题
3.1 什么是Socket?
3.2 什么是URL?URL地址由哪几部分组成?
3.3 Java对网络编程提供了哪些支持?
4.编程题
4.1 使用HTTPClient工具查询自己手机号码归属地。接口地址参考阿里巴巴开放API(https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=手机号)。
- 点赞
- 收藏
- 关注作者
评论(0)