网络
依旧是《Java Core》ed.11 ed的学习笔记
连接到一个服务器
使用Telnet
telnet是网络编程中很好用的debug工具
服务器端的软件在远程机器上持续运行,等待客户端发起对某个端口号(服务)的请求。当它接收到这个请求之后,会唤醒监听此端口的服务,然后建立连接,直到连接双方有一方断开连接
两个例子:
telnet time-a.nist.gov 13
telnet horstmann.com 80
使用Java程序
public static void main(String[] args) throws IOException
{
try (var s = new Socket("time-a.nist.gov", 13);
var in = new Scanner(s.getInputStream(), StandardCharsets.UTF_8))
{
while (in.hasNextLine())
{
String line = in.nextLine();
System.out.println(line);
}
}
}
- socket就是网络程序的一种抽象,允许别的程序通过它来进行数据的传递。通过远程地址和端口号来创建socket
- 在创建的socket中可以获取输入(输出)流,进行正常的流操作
书中只写了TCP相关的网络连接,Java同样支持UDP传输(数据包无序且会丢失,适合音频或视频等可以容忍数据包丢失的传输内容)
socket超时
一旦主机超时,那么此socket可以不再等待数据,通过setSoTimeout()
方法来设置超时(单位毫秒)。一旦设置了超时时间,那么后续所有socket的操作在超时时间前完不成的话都会报超时异常。在写操作中没有超时
超时有一个问题,就是使用new Socket(ip, port)
构造器时,如果主机连接不上,那么代码可能在这里永久阻塞下去。解决方法是
Socket s = new Socket();
s.connect(ip, port, timeout);
网络地址
InetAddress
类可以帮助转换主机名和IP地址。java.net
包支持IPv6
// 获取处理IP地址的对象
InetAddress address = InetAddress.getByName("time-a.nist.gov");
// 获取IP地址
byte[] addressBytes = address.getAddress();
// 如果一个域名对应多个IP,这个方法可以一次性取出所有对应IP
InetAddress[] addresses = InetAddress.getAllByName(host);
// 获取自己本机的IP(不是localhost)
InetAddress address = InetAddress.getLocalHost();
实现Server
ServerSocket
// 建立一个服务器,监听8189端口号
var s = new ServerSocket(8189);
// 这里是让服务器开始(阻塞)等待一个客户端来访问
Socket incoming = s.accept();
当有客户端请求到达的时候,这个socket对象可以返回输入/输出流,可以进行普通的IO操作。服务器的输出流是客户端的输入流,反之亦然
服务多个客户端
每次一个客户端发来连接请求,都新建一个socket对象(新线程)来与它建立连接
这种方式的性能并不高,可以使用
java.nio
包来实现高性能(参考Java-NIO)
half-close
half-close允许socket连接的一端在还接收另一段数据的情况下关闭了输出流
解决问题:假如客户端给服务端传输数据,但是开始并不知道要传输的数据有多少,如果是一个文件,那么到数据传输完成把文件关闭即可,但是此时不能关闭socket。这时可以通过socket.shutdownOutput()
方法来关闭输出流,但是还可以保证拿到服务器的返回消息
这种情况适用于一次性的数据交互(HTTP),客户端发-服务器接-服务器返回
可以中断的socket
在交互式程序中,如果想给用户一个选项,将没有回应的socket连接关闭,此时,如果一个线程阻塞在无响应的socket上,那么没办法通过interrupt
来解除阻塞(可能是网络问题,无法提前处理)
使用java.nio
包中的SocketChannel
可以中断socket的操作
// 创建一个channel对象
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));
一个channel对象没有对应的流(输入/输出),而是read
write
方法(参考NIO内容)。这里操作的是NIO的Buffer
,如果不希望处理Buffer,输入可以通过Scanner
,输出可以通过Channels.getOutputStream(channel)
来获得
获取网络数据
URL和URI
URL
和URLConnection
类是用来处理从远程站点获取信息的复杂问题的
// 新建一个URL对象
var url = new URL(urlString);
// 通过URL对象来获取输入流,即可直接读取网页内容
InputStream inStream = url.openStream();
var in = new Scanner(inStream, StandardCharsets.UTF_8);
URL是URI的一种(统一资源定位符/标识符)。Java中URI并没有获取资源(流)的方法,它的唯一作用就是转换(parse)。URL可以处理的内容包括(http https ftp local file(file:)jar(jar:))
URI规范规定了构成标识符的结构 [scheme:]schemeSpecificPart[#fragment]
中括号的内容是可选的。冒号和#号都是作为字面量包含在里面的。如果scheme:
出现,那么这个URI是绝对路径,否则是相对路径。一个绝对路径的schemeSpecificPart
如果不包含/
,那么这个路径是模糊的(opaque),一个不模糊的绝对路径是分层的 “http://horstmann.com/index.html ../../java/net/Socket.html#Socket()
schemeSpecificPart
的结构[//authority][path][?query]
,在这个结构中,基于服务端的URI的authority
部分的结构是[user-info@]host[:port]
,这里端口号必须是整数(RFC2396标准化了URI),URI类的目的是将URI转换成组成它的各个部分(提供了很多方法,这里就不写了)
使用URLConnection来获取信息
这个类比URL
类提供的控制手段更多
URLConnection connection = url.openConnection();
// 设置连接属性
setDoInput
setDoOutput
setIfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
setConnectTimeout
setReadTimeout
// 连接(根据头信息查询服务器)
connection.connect();
// 这两个方法遍历header中所有的内容
getHeaderFieldKey
getHeaderField
getHeaderFields // Map对象
// 细节内容
getContentType
getContentLength
getContentEncoding
getDate
getExpiration
getLastModified
// 获取输入流,读取内容(和socket的同名方法性质不同,这里它处理的信息更多)
getInputStream
这个类中有一些方法在建立连接之前设置连接的属性,比较重要的setDoInput
setDoOutput
默认情况下,连接对象会返回一个输入流而没有输出流
// 设置一个输出流
connect.setDoOutput(true);
// 告诉连接只对从某个时间点修改之后的信息感兴趣
setIfModifiedSince
// 设置请求头
setRequestProperty
// 查看返回信息的头信息
String key = connection.getHeaderFieldKey(n); // n从1开始
// 返回值
String value = connection.getHeaderField(n);
// 返回Map
getHeaderFields
// 还有一些其它的方法,就不在这里写了,查API即可
发送表单数据
从URLConnection
对象中获取一个输出流,将name/value
对写入输出流中
// 建立连接
var url = new URL("http://host/path");
URLConnection connection = url.openConnection();
// 设置获取输出流
connection.setDoOutput(true);
// 如果是发送字符数据,使用PrintWriter
var out = new PrintWriter(connection.getOutputStream(), StandardCharsets.UTF_8);
// 发送数据
out.print(name1 + "=" + URLEncoder.encode(value1, StandardCharsets.UTF_8) + "&");
out.print(name2 + "=" + URLEncoder.encode(value2, StandardCharsets.UTF_8));
out.close();
// 如果服务端出现异常,也会返回一个错误页面
InputStream err = connection.getErrorStream();
HttpURLConnection
可以处理大部分的重定向行为
// 关闭自动重定向
connection.setInstanceFollowRedirects(false);
// 检查返回码
int responseCode = connection.getResponseCode();
// 类型
HttpURLConnection.HTTP_MOVED_PERM
HttpURLConnection.HTTP_MOVED_TEMP
HttpURLConnection.HTTP_SEE_OTHER
// 重定向其它位置(开一个新连接)
String location = connection.getHeaderField("Location");
if (location != null)
{
URL base = connection.getURL();
connection.disconnect();
connection = (HttpURLConnection) new URL(base, location).openConnection();
. . .
}
HTTP客户端
HttpClient
类提供了更方便的API和对Http/2的支持
HttpClient client = HttpClient.newHttpClient()
// 如果要配置客户端,则使用builderAPI
// 这也是构建不可变对象的一般模式
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
// 构建一个get请求对象
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://horstmann.com"))
.GET()
.build();
// 构建一个post请求对象
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonString))
.build();
// 处理返回数据,这里handler只是简单的将返回数据转为字符串
// 返回对象的泛型代表返回体的类型
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String bodyString = response.body();
// 还有不同的handler
// 保存返回的文件
BodyHandlers.ofFile(filePath)
// 保存文件到给定路径,使用头中的Content-Dispositio属性来命名
BodyHandlers.ofFileDownload(directoryPath)
// 丢弃返回体
BodyHandlers.discarding()
// 见名知意
int status = response.statusCode();
HttpHeaders responseHeaders = response.headers();
Map<String, List<String>> headerMap = responseHeaders.map();
Optional<String> lastModified = headerMap.firstValue("Last-Modified");
// 异步处理返回内容
ExecutorService executor = Executors.newCachedThreadPool();
HttpClient client = HttpClient.newBuilder().executor(executor).build();
// 异步请求客户端
HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
// CompletableFuture<HttpResponse<T>>对象,接收一个handler作为参数(回调函数)
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
thenAccept(response -> . . .);