zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java SE 网络

2023-06-13 09:17:26 时间

连接到服务器

telnet

获取时间戳命令

telnet time-a.nist.gov 13

获取某个网页

telnet horstmann.com 80
GET /HTTP/1.1
Host: horstmann.com
​

注意:如果一台Web服务器用相同的IP地址位多个域提供宿主环境,那么在连接这台Web Server时,就必须提供Host键/值对。如果服务器只为单个域提供宿主环境,则可以忽略键/值对

用Java连接到服务器

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
​
/**
 * @author only
 */
public class SocketTest {
    public static void main(String[] args) throws IOException {
        try (Socket s = new Socket("time-a.nist.gov", 13);
             Scanner in = new Scanner(s.getInputStream(), "UTF-8")
        ) {
            while (in.hasNextLine()) {
                String line = in.nextLine();
                System.out.println(line);
            }
        }
    }
}

第一行代码用于打开一个套接字,它是网络软件中的一个抽象概念,负责启动该程序内部和外部之间的通信。

一旦套接字被打开,Socket类中的getInputstream方法返回一个InputStream对象。

TCP(传输控制协议)网络协议。 UDP(用户数据报协议)协议,可以用于发送数据包(数据报),所需的开销比TCP少得多。 UDP一个重要的缺点:数据包无需按照顺序传递到接受的应用程序,它们甚至可能在传输过程中全部丢失。UDP让数据包的接收者自己负责对它们进行排序,并请求发送者重新发送那些丢失的数据包。UDP比较适合用于那些可以忍受数据包丢失的应用,例如音频流和视频流的传输,或者用于连续测量的应用领域。

套接字超时

套接字读取消息时,在有数据可供访问之前,读操作将会阻塞。 如果此时主机不可达,那么应用将要等待很长的时间,并且因为受底层操作系统的限制而最终会导致超时。

对于不同 应用,应该确定合理的超时值。调用setSoTimeout方法设置超时值(单位:毫秒)

Socket socket = new Socket(...);
socket.setSoTimeout(1000);

如果设置了超时时间,之后的读操作和写操作在没有完成之前就超过了时间限制,那么这些操作就会抛出SocketTimeoutException异常。

在构造Socket的时候也有一个超时问题:Socket(String host, int port);

上面这个构造器会一直阻塞下去,直到建立了到达主机的初始连接为止。

可以通过构建一个无连接的套接字,再使用一个超时来进行连接的方式解决:

Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);

因特网地址

一个因特网地址由4个字节组成(在IPv6中是16个字节);

如果需要在主机名和因特网之间进行转换,可以使用InetAddress类。

只要主机操作系统支持IPv6格式的因特网地址,java.net包也将支持它。

InetAddress.getByName:返回代表某个注解的InetAddress对象。

getAddress:访问对象封装的一个4字节的序列。

一些访问量较大的主机名通常对应多个因特网地址,实现负载均衡。 可以通过getAllByName获得所有主机:

InteAddress[] address = InetAddress.getAllByName(host);

本机主机的地址:如果要求得到localhost的地址,总会得到本地环回地址127.0.0.1 。其他程序无法通过这个地址连接这台机器。可以通过getLocalHost方法得到本地主机的地址:

InetAddress address = InetAddress.getLocalHost();

实现服务器

服务器套接字

一旦启动服务器程序,它便会等到某个客户端连接到它的端口。

ServerSocket类用于建立套接字。

ServerSocket s = new ServerSocket(8888);

上面语句是:建立一个负责监控端口8888的服务器。

Socket incoming = s.accept();

上面语句是:告诉程序不停等待,直到有客户端连接到这个端口。一旦有人通过网络发送了正确的连接请求,并以此连接到了端口上,该方法会返回一个表示连接已经建立的Socket对象。

使用Socket获取输入流和输出流。

InputStream in = incoming.getInputStream();
OutputStream out = incoming.getOutputStream();

服务器发送服务器输出流的所有消息都会称为客户端程序的输入,同时来自客户端程序的所有输出都会被包含在服务器输入流中。

为多个客户端服务。

简单服务器存在一个问题。简单服务器会拒绝多客户端连接,使用某个用户可能会因长时间地连接服务而独占服务。 可以使用线程的方式来解决多客户端问题。

每当程序建立一个新的套接字连接,也就是当调用accpet()时,会启动一个新线程来处理服务器和客户端之间的连接,而主程序将来立即返回等待下一个连接。

while(true){
    Socket incoming = s.accept();
    Runnable r = new ThreadEchoHandler(incoming);
    
    new Thread(r).start();
}

上面的ThreadEchoHandler实现了Runnable接口的类,在它的run 方法中包含了与客户端循环通信的代码

public void run(){
    try(
        InputStream in = incoming.getInputStream();
        OutputStream out = incoming.getOutputStream();
    ){
        //...
    }catch(IOException e){
        ...
    }
}

半关闭

半关闭(half-close)提供了一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接受来自另一端的数据。

例如,向服务器传输数据,一开始不知道要传输多少数据。 向文件写数据时,一般是写入后关闭文件即可。但是,如果关闭一个套接字,那么与服务器的连接将立即断开,因而也就无法读取服务器的相应了。

使用半关闭可以解决上面的问题。通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。

try(Socket socket = new Socket(host, port)){
    Scanner in = new Scanner(socket.getInputStream, "utf-8");
    PrintWriter out = new PrintWriter(socket.getOutputStream);
    out.println(...);
    out.flush();
    socket.shutdownOutput();
    
    while(in.hasNextLine()){
        String line = in.nextLine();
        //...
    }
}

服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送相应。

当然,该协议只适用于一站式(one-shot)的服务,例如HTTP服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。

可中断套接字

当连接一个套接字时,当前线程将会被阻塞直到建立连接或产生超时为止。 同样地,当通过套接字读写数据时,当前线程也会被阻塞直到操作成功或产生超时为止。

线程因套接字无法响应而产生阻塞时,则无法通过调用interrupt来解除阻塞。

中断套接字操作,需要使用java.nio包提供的一个特性 ---SocketChannel类。

SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));

通道(channel)并没有与之相关联的流。实际上,它所拥有的read和write方法都是通过使用Buffer对象来实现的。ReadableByteChannel接口和WriteableByteChannel接口都声明了这两个方法。

如果不想处理缓冲区,可以使用Scanner类从SocketChannel中读取消息。

Scanner in = new Scanner(channel, "UTF-8");

通过调用静态方法Channels.newOutputStream,可以将通道流转换为输出流

OutputStream out = Channels.newOutputStream(channel);

上述就是所有的方法。当线程正在执行打开、读取或写入操作时,如果线程发生中断,那么这些操作将不会陷入阻塞,而是以抛出异常的方式结束。

获取web数据

URL和URI

URL和URLConnection类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息。

一个字符串构建一个URL对象:

URL url = new URL(urlString);

如果只是想获得资源的内容,可以使用URL类中的openStream方法。该方法将产生一个InputStream对象,然后就可以按照一般的用法来使用这个对象,比如构建一个Scanner对象:

InputStream in = url.openStream();
Scanner sc = new Scanner(in, "UTF-8");

java.net包对统一资源定位符(Uniform Resource Locator,URL)和统一资源标识符(Uniform Resource Identifier, URI)做了区分。

URI是个纯粹的语法结构,包含用来指定web资源的字符串的各种组成部分。URL是URI的一个特例,它包含了定位Web资源的足够信息。其他URI,例如:,则不属于定位符,因为根据标识符我们无法定位任何数据,像这样的URI我们称之为URN(uniform resource name,统一资源名称)

在Java类库中,URI类并不包含任何用于访问资源的方法,它的唯一作用就是解析。但是,URL类可以打开一个到达资源的流。因此,URL类只能作用于那些Java类库直到该如何处理的模式,例如http:、https:、ftp:、本地文件系统(file:)和JAR文件(jar:)。

URI规范给出了标记这些标识符的规则。一个URI具有以下句法:

[scheme:]schemeSpecificPart[#fragment]

上式中,[...]表示可选部分,并且:和#可以被包含在标识符内。

如果绝对的URI的schemeSpecificPart不是以/开头的,我们就称它是不透明的。例如:

所有绝对的透明的URI和所有相对的URI都是分层的(hierarchical)。

一个分层URI的schemeSpecificPart具有以下结构:[//authority][path][?query],这里的[...]仍是可选部分。

基于服务器的URI,authority部分具有以下格式:[user-info@]host:[:port],port必须是整数

URI类的作用之一是解析标识符并将它分解成各种不同的组成部分。可以用以下方法读取:

  • getScheme
  • getSchemeSpecificPart
  • getAuthority
  • getUserInfo
  • getHost
  • getPort
  • getPath
  • getQuery
  • getFragment URI类的另一个作用是处理绝对标识符和相对标识符。如绝对URI:http:/docs.mycompany.com/api/java/net/ServerSocket.html 和一个相对的URI:../../java/net/Socket.html#Socket() 那么可以用它们组合成一个绝对URI:http:/docs.mycompany.com/api/java/net/Socket.html#Socket() 这个过程称为解析相对URL。 与此相反的过程称为相对化(relativization)。 例如一个URI: 和另一个URI:相对化之后的URI就是:java/lang/String.html

URI类同时支持两种操作:

relative = base.relativize(combined);

combined = base.resolve(relative);

使用URLConnection获取信息

如果想要从Web资源中获取更多信息,应该使用URLConnection类。

执行步骤:

  1. 调用URL类中的openConnection方法获得URLConnection对象: URLConnection connection = url.openConnection();
  2. 使用以下方法设置任意的请求属性
    • setDoInput
    • setDoOutput
    • setIfModifiedSince
    • setUseCaches
    • setAllowUserInteraction
    • setRequestProperty
    • setConnectTimeout
    • setReadTimeout
  3. 调用connect方法连接远程资源 connection.connect()
  4. 与服务建立连接后,可以查询头信息。getHeaderFieldKey和getHeaderField这两个方法枚举了消费头的所有字段。getHeaderFields方法返回一个包含了消息头中所有字段的标准Map对象。为了方便使用,以下方法可以查询个标准字段
    • getContentType
    • getContentLength
    • getContentEncoding
    • getDate
    • getExpiration
    • getLastModified
  5. 最后,访问资源数据。使用getInputStream方法获取一个输入流用以读取信息,这个输入流与URL类中的openStream方法返回的流相同。另一个方法getContent在实际操作中并不是很有用。由标准内容类型(比如text/plain和image/gif)所返回的对象需要使用com.sun层次结构中的类来进行处理。

警告:URLConnection类中的getInputStream和getOutputStream方法与Socket类中的这些方法不是相似的。URLConnection类具有很多表象之下的神奇功能,尤其在处理请求和响应消息头时。

URLConnection类的方法:

与服务器建立连接属性:

setDoInput和setDoOutput。在默认情况下,建立的连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流。如果想要获取输出流,需要调用:

connection.getDoOutput(true);

设置请求头(request header):

setIfModifiedSince方法:告诉连接你只对自某个特定日期以来被修改过的数据感兴趣。

setUseCaches和setAllowUserInteraction:之作用于Applet; 前者用于命令浏览器首先检查它的缓存;后者则用于在访问密码保护的资源时弹出对话框,以便查询用户名和口令。

setRequestProperty总览全局的方法:设置对特定协议起作用的任何“名-值(name-value)对”

例如访问一个有密码保护的Web页:

  1. 将用户名、冒号和密码以字符串形式连接一起 String input = username + ":" + password;
  2. 计算上一步骤所得字符串的Base64编码 Base64.Encoder encoder = Base64.getEncoder(); String encoding = encoder.encodeToString(input.getBytes(StandardCharsets.UTF_8));
  3. 用“Autorization”这个名字和“Basic” + encoding的值调用setRequestProperty方法 connection.setRequestProperty("Autorization", "Basic" + encoding)

一旦调用了connect方法,就可以查询响应头信息了。调用以下方法:

String key = connection.getHeaderFieldKey(n);

获取响应头的第n个键,其中n从1开始。如果n为0或大于消息头的字段总数,该方法返回null值。没有方法可以返回字段的数量,必须反复调用getHeaderFieldKey方法直到返回null为止。同样地调用以下:

String value = connection.getHeaderField(n);

可以得到第n个值。

getHeaderFields方法返回一个封装了响应头字段的Map对象。

Map<String, List<String>> headerFields = connection.getHeaderFields();