网络

依旧是《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);
            }
      }
}
  1. socket就是网络程序的一种抽象,允许别的程序通过它来进行数据的传递。通过远程地址和端口号来创建socket
  2. 在创建的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

URLURLConnection类是用来处理从远程站点获取信息的复杂问题的

// 新建一个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 -> . . .);
posted on 2020-11-21 15:38  老鼠不上树  阅读(117)  评论(0)    收藏  举报